chore(web): collapse InspectorView's prop wall into domain bundles - #2182
Conversation
…2130) `<InspectorView>` took ~130 flat props, which made ~170 of App.tsx's JSX lines a prop wall rather than a component tree. It now takes 12 domain props and the call site is 14 lines. Two conventions keep the change reviewable. No field is renamed — every field inside a bundle carries the exact name it had as a flat prop, so this is a regrouping rather than a rename. And the bundles stop at `InspectorView`: it destructures each one back into the same locals its body already used and passes the same individual props down, so nothing below the view knows they exist. Three props have no single screen and are placed deliberately: `onCompleteArgument` / `completionsSupported` are read by both the Prompts and Resources screens, so they sit in `connection` rather than being duplicated; `malformedListItems` is one array three screens filter for their own entries, so it sits in `shell`; `erroredServerId` / `connectedServerId` are connection outcomes the Servers screen renders, so they sit in `connection`. Every multi-line closure declared inline in the JSX is now a named `useCallback` above the return, including `onServerReorder`'s `.catch` and the seven `void`-discarding wrappers, which carry their `no-floating-promises` justification once rather than at each call site. The `InspectorView` double in App.test.tsx is now typed with the real `InspectorViewProps` instead of a hand-written structural mirror, so it cannot drift from the component it stands in for. Stories keep one constant per bundle, since Storybook merges args only at the top level. No behavior change. Signed-off-by: cliffhall <cliff@futurescale.com>
There was a problem hiding this comment.
Pull request overview
Refactors InspectorView to replace roughly 130 flat props with 12 domain-oriented bundles while preserving downstream screen interfaces.
Changes:
- Adds typed domain prop bundles and unpacks them at the view boundary.
- Replaces inline
App.tsxhandlers with named callbacks and concise bundle wiring. - Updates tests, mocks, fixtures, and Storybook stories for the new API.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
clients/web/src/App.tsx |
Assembles domain bundles and named callbacks. |
clients/web/src/App.test.tsx |
Updates the view mock for bundled props. |
clients/web/src/components/views/InspectorView/types.ts |
Defines the domain bundle interfaces. |
clients/web/src/components/views/InspectorView/InspectorView.tsx |
Accepts and destructures the new bundles. |
clients/web/src/components/views/InspectorView/InspectorView.test.tsx |
Updates fixtures and layered bundle overrides. |
clients/web/src/components/views/InspectorView/InspectorView.stories.tsx |
Migrates Storybook arguments to bundles. |
Suppressed comments (1)
clients/web/src/App.tsx:1565
onDisconnectusestry/finally, sofinalizeExplicitDisconnect()runs but a failed transport close still rejects (useConnectionLifecycle.ts:776-783). Barevoidmakes that a global unhandled rejection, contrary to the preceding claim that the callee catches failures. Add a terminating.catch(...)that reports the close failure and test the rejected-disconnect path.
const dispatchDisconnect = useCallback(() => {
void onDisconnect();
}, [onDisconnect]);
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Three findings, all valid on verification. `onToggleConnection` and `onDisconnect` do not own every rejection, so a bare `void` on them turns an ordinary Connect/Disconnect click into a global unhandled rejection with nothing shown to the user. The toggle has three escape paths outside its own try/catch — awaiting `initialConfigSettledRef`, constructing the client via `setupClientForServer`, and the already-connected disconnect branch, whose `try/finally` runs `finalizeExplicitDisconnect()` and then lets a failed transport close propagate — and `onDisconnect` is that same `try/finally` on its own. Both are now terminated with a `.catch` that toasts the failure. The blanket comment claiming all seven wrappers own their failures was wrong. The five command handlers (`onCallTool`, `onGetPrompt`, `onReadResource`, `onCancelTask`, `onOpenApp`) do each end in a `catch`, so `void` stays correct for them; the comment now splits the two groups and says which is which. Adds the two tests the review asked for, covering the rejected-close path through both the toggle and the explicit Disconnect. Each fails against the previous `void` code, so they pin the fix rather than merely passing. The `InspectorView` double grows a `disconnect` button, since the standalone `onDisconnect` was otherwise unreachable from it. Also restores the `PropOverrides` doc example, which the scripted regrouping had rewritten inside its own JSDoc into a triple-nested shape that would not type-check if copied. Signed-off-by: cliffhall <cliff@futurescale.com>
|
All three review findings addressed in 37add96 (mirrored here since inline replies get folded away once the fix is pushed). 1 + 2 — bare The comment above them was the actual defect: it asserted that all seven wrappers own their failures. That is true of the five command handlers ( Worth stating plainly: the unhandled rejection is pre-existing — the old JSX did Tests. Two new cases in the 3 — the
|
Two findings, both valid. `onCallTool`, `onGetPrompt`, `onReadResource` and `onOpenApp` await `handleCommandScopedAuthRecovery` from inside their catch blocks, and a rejection thrown from a catch is not caught by that same catch — so it escapes the handler. That helper awaits `checkAuthChallengeSatisfied` and `pushRemoteAuthState`, both of which reach the backend and can reject, so the bare `void` on those four could still leak. Rather than patch the four and keep a comment enumerating which handlers are safe, all seven wrappers now terminate the same way: a reporting `.catch` via `reportDispatchFailure`. Whether a handler "owns its failures" turned out not to be reliably knowable by reading — the previous two attempts at that judgement were both wrong — so the code no longer depends on getting it right. A handler that does surface its own failure resolves and never reaches the catch, so nothing double-toasts. The reorder callback lifted out of the JSX had no App-level coverage. The `InspectorView` double grows a reorder control, with tests for both forwarding the ordered ids and reporting a rejected reorder. Adds the auth-recovery escape test the review asked for. All four new rejection tests were confirmed to fail against the bare-`void` code, so they pin the fixes rather than merely passing beside them. Signed-off-by: cliffhall <cliff@futurescale.com>
|
Round 2 addressed in 0139fd7 (mirrored here, since inline replies fold away once the fix is pushed). Both findings valid. 1 — Rather than patch those four and keep a comment listing which handlers are safe, all seven wrappers now terminate identically through a shared 2 — reorder had no App-level coverage. Correct, and self-inflicted: I lifted that closure out of the JSX and gave it no route through the test double. The double now exposes a On verification. All four new rejection tests were confirmed to fail against the bare- Two things the gate caught that the test run could not:
|
Closes #2130
Phase 3 of #2126 — the finishing pass on
App.tsx.<InspectorView>took ~130 flat props, which made ~170 of App.tsx's JSX lines aprop wall rather than a component tree. It now takes 12 domain props, and the
call site is 14 lines:
Two conventions that keep this reviewable
had as a flat prop, so this is a regrouping and the diff can be read as one.
InspectorView. It destructures each one back into thesame locals its body already used and passes the same individual props down, so
nothing below the view knows the bundles exist. That was the issue's stated
recommendation, and it is why ~1,000 lines of view body are untouched.
Shapes live in a new
InspectorView/types.ts, carrying the JSDoc that was on theindividual props.
Three props that have no single screen
Named explicitly because a reviewer will look for them:
onCompleteArgument/completionsSupportedare read by both the Promptsand Resources screens, so they sit in
connectionrather than being duplicatedinto two bundles.
malformedListItemsis one array that three screens each filter for their ownentries, so it sits in
shell— it genuinely has no domain owner.erroredServerId/connectedServerIdare connection outcomes that theServers screen renders, so they sit in
connection, notservers.Closures lifted out of the JSX
Every multi-line closure that was declared inline is now a named
useCallbackabove the return: the server add/import/clone highlight-clearing handlers,
onServerRemove's lookup, andonServerReorder's ~15-line.catch+notifications.show. The sevenvoid-discarding wrappers(
onToggleConnection,onCallTool,onGetPrompt,onReadResource,onOpenApp,onCancelTask,onDisconnect) are also named, and carry theno-floating-promisesjustification once between them rather than at seven callsites.
They stay in
App.tsxrather than moving intouseServerCommands: the issuesuggests the owning hook, but these handlers drive App-local modal state
(
configModal,importConfigOpen,removeTarget,highlightedServerIds), andmoving that state into the hook is a separate change from regrouping props. The
"Done when" item this closes is the JSX one.
Note on App.tsx's line count
It goes 1,796 → 1,926. The JSX shrank by ~155 lines; the bundle literals and the
lifted closures add more than that back, because prop values that used to be
inline expressions are now named. The goal here was the legible tree, not the
line count — Phases 0–2 already met the parent issue's size target.
Tests and stories
InspectorView.test.tsx:makePropsnow takes any number of per-bundleoverride layers (
makeProps({ tools: { tools: [t] } })), which is what letsthe
connectedHttp/failedHttpscenario helpers supply a base and stillaccept a caller override. All 82 call sites converted; 82 tests pass
unchanged.
App.test.tsx: theInspectorViewdouble is now typed with the realInspectorViewPropsinstead of a hand-written structural mirror, so it can nolonger drift from the component it stands in for. 95 tests pass unchanged.
InspectorView.stories.tsx: one constant per bundle at module scope, becauseStorybook merges args only at the top level — a story overriding one field
spreads its bundle (
servers: { ...serversArgs, servers: [] }) rather thanreplacing it.
No behavior change.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Quk1hBUtyhkXowwMXzAnAY