Skip to content

chore(web): collapse InspectorView's prop wall into domain bundles - #2182

Merged
cliffhall merged 5 commits into
v2/mainfrom
v2/chore/2130-inspectorview-prop-bundles
Aug 28, 2026
Merged

chore(web): collapse InspectorView's prop wall into domain bundles#2182
cliffhall merged 5 commits into
v2/mainfrom
v2/chore/2130-inspectorview-prop-bundles

Conversation

@cliffhall

Copy link
Copy Markdown
Member

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 a
prop wall rather than a component tree. It now takes 12 domain props, and the
call site is 14 lines:

<InspectorView
  shell={shellProps}
  connection={connectionProps}
  servers={serverListProps}
  tools={toolsPanelProps}
  prompts={promptsPanelProps}
  resources={resourcesPanelProps}
  apps={appsPanelProps}
  tasks={tasksPanelProps}
  logs={logsPanelProps}
  protocol={protocolPanelProps}
  network={networkPanelProps}
  console={consolePanelProps}
/>

Two conventions that keep this reviewable

  • No field was renamed. Every field inside a bundle carries the exact name it
    had as a flat prop, so this is a regrouping and the diff can be read as one.
  • 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 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 the
individual props.

Three props that have no single screen

Named explicitly because a reviewer will look for them:

  • onCompleteArgument / completionsSupported are read by both the Prompts
    and Resources screens, so they sit in connection rather than being duplicated
    into two bundles.
  • malformedListItems is one array that three screens each filter for their own
    entries, so it sits in shell — it genuinely has no domain owner.
  • erroredServerId / connectedServerId are connection outcomes that the
    Servers screen renders, so they sit in connection, not servers.

Closures lifted out of the JSX

Every multi-line closure that was declared inline is now a named useCallback
above the return: the server add/import/clone highlight-clearing handlers,
onServerRemove's lookup, and onServerReorder's ~15-line .catch +
notifications.show. The seven void-discarding wrappers
(onToggleConnection, onCallTool, onGetPrompt, onReadResource,
onOpenApp, onCancelTask, onDisconnect) are also named, and carry the
no-floating-promises justification once between them rather than at seven call
sites.

They stay in App.tsx rather than moving into useServerCommands: the issue
suggests the owning hook, but these handlers drive App-local modal state
(configModal, importConfigOpen, removeTarget, highlightedServerIds), and
moving 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: makeProps now takes any number of per-bundle
    override layers (makeProps({ tools: { tools: [t] } })), which is what lets
    the connectedHttp / failedHttp scenario helpers supply a base and still
    accept a caller override. All 82 call sites converted; 82 tests pass
    unchanged
    .
  • App.test.tsx: the InspectorView double is now typed with the real
    InspectorViewProps instead of a hand-written structural mirror, so it can no
    longer drift from the component it stands in for. 95 tests pass unchanged.
  • InspectorView.stories.tsx: one constant per bundle at module scope, because
    Storybook merges args only at the top level — a story overriding one field
    spreads its bundle (servers: { ...serversArgs, servers: [] }) rather than
    replacing it.

No behavior change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Quk1hBUtyhkXowwMXzAnAY

…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>
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Aug 28, 2026
@cliffhall
cliffhall requested a balanced review from Copilot August 28, 2026 03:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.tsx handlers 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

  • onDisconnect uses try/finally, so finalizeExplicitDisconnect() runs but a failed transport close still rejects (useConnectionLifecycle.ts:776-783). Bare void makes 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.

Comment thread clients/web/src/App.tsx
Comment thread clients/web/src/components/views/InspectorView/InspectorView.test.tsx Outdated
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>
@cliffhall

Copy link
Copy Markdown
Member Author

All three review findings addressed in 37add96 (mirrored here since inline replies get folded away once the fix is pushed).

1 + 2 — bare void on onToggleConnection / onDisconnect (inline, and the suppressed comment on App.tsx:1565). Both valid; verified in useConnectionLifecycle.ts rather than taken on faith. The toggle has three escape paths outside its own try/catch — awaiting initialConfigSettledRef.current?.promise, the setupClientForServer(target) construction, and the already-connected disconnect branch, whose try/finally runs finalizeExplicitDisconnect() and then lets a failed transport close propagate. onDisconnect is that same try/finally on its own. Both now terminate with a .catch that toasts.

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 (onCallTool, onGetPrompt, onReadResource, onCancelTask, onOpenApp — each ends in a catch), so void stays right for those; it was false for the two lifecycle ones. The comment now splits the groups and explains the distinction.

Worth stating plainly: the unhandled rejection is pre-existing — the old JSX did void onToggleConnection(id) inline, so this PR relocated it rather than introducing it. What the PR did introduce was a comment claiming it was safe, which is exactly the kind of false reassurance that outlives the code it describes. Fixed the behavior as well as the comment.

Tests. Two new cases in the App background command rejections (#2049) block, one per path, asserting both the toast and an empty unhandled-rejection list. I reverted the fix and re-ran to confirm both fail against the old void code — they pin the fix rather than merely passing alongside it. The InspectorView double grew a disconnect button, since the standalone onDisconnect had no route through it.

3 — the PropOverrides doc example. Valid, and self-inflicted: the example was correct as written, but the script I used to convert the 82 makeProps call sites also rewrote the example inside its own JSDoc into a triple-nested shape. Restored, and I swept the file for other comment text the script had touched — this was the only one.

npm run local:gate green end to end after the changes: ≥90 coverage gate, both build gates, all smokes under Chromium and Firefox, 122 Storybook play functions.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread clients/web/src/App.tsx Outdated
Comment thread clients/web/src/App.tsx
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>
@cliffhall

Copy link
Copy Markdown
Member Author

Round 2 addressed in 0139fd7 (mirrored here, since inline replies fold away once the fix is pushed). Both findings valid.

1 — void still leaks on four command handlers. Traced and confirmed: onCallTool / onGetPrompt / onReadResource / onOpenApp await handleCommandScopedAuthRecovery from inside their catch blocks, and that helper awaits tryApplyStoredAuthRecoveryclient.checkAuthChallengeSatisfied() and client.pushRemoteAuthState(), both uncaught and both reaching the backend. A rejection thrown from a catch is not caught by that same catch, so it escapes the handler entirely.

Rather than patch those four and keep a comment listing which handlers are safe, all seven wrappers now terminate identically through a shared reportDispatchFailure(title). The reason is worth stating plainly: whether a handler "owns its failures" has not proven reliably knowable by reading it. My first pass asserted all seven did; my second, corrected pass asserted five did; both were wrong. So the code no longer depends on getting that judgement right. Handlers that do surface their own failures resolve and never reach the .catch, so the ordinary error paths do not double-toast.

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 reorder-servers control, with tests for both forwarding the ordered ids and reporting a rejected reorder.

On verification. All four new rejection tests were confirmed to fail against the bare-void code before being kept — I reverted the fixes and re-ran rather than assuming. That mattered: an earlier revert attempt aborted before writing the file, so the "confirmed failing" result I nearly reported would have been vacuous.

Two things the gate caught that the test run could not:

  • My first cut of the reorder tests built the useServers mock by spreading vi.mocked(useServers)(), which does not type-check (the hook takes a required argument). vitest does not typecheck, so npm run test was green while tsc -b rejected it. Now uses a properly typed serversWithReorder helper.
  • An intermediate gate run failed on scripts/lib/render-smoke.test.mjs — unrelated to this PR, and the known flake described by render-smoke's under-the-deadline test is timing-marginal, and aborts the whole gate when it flakes #2180 ("timing-marginal, and aborts the whole gate when it flakes"). Its fix had landed on v2/main after the last merge, so v2/main is merged in again here.

npm run local:gate green end to end on the final state: ≥90 coverage gate, both build gates, all smokes under Chromium and Firefox, 122 Storybook play functions.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@cliffhall
cliffhall merged commit 18fe354 into v2/main Aug 28, 2026
5 checks passed
@cliffhall
cliffhall deleted the v2/chore/2130-inspectorview-prop-bundles branch August 28, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decompose App.tsx phase 3: collapse InspectorView's prop wall into domain bundles

2 participants