Skip to content

chore(web): extract App.tsx's server commands into useServerCommands - #2173

Merged
cliffhall merged 5 commits into
v2/mainfrom
v2/chore/2155-use-server-commands
Aug 28, 2026
Merged

chore(web): extract App.tsx's server commands into useServerCommands#2173
cliffhall merged 5 commits into
v2/mainfrom
v2/chore/2155-use-server-commands

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #2155

Phase-2 step 4 of the App.tsx decomposition (#2129 for the phase, #2126 for the whole effort). Step 3 (#2154 / PR #2169) has landed, so this targets v2/main rather than that branch.

What moved

clients/web/src/hooks/useServerCommands.tsx — every command the screens issue against the live server:

  • onCallTool, onClearToolResult, onToolsUiChange
  • onGetPrompt, onReadResource, onReadResourceContents, onSubscribeResource, onUnsubscribeResource, onCompleteArgument
  • onCancelTask, onCancelToolCall, onClearCompletedTasks, onSetLogLevel, onSetModernLogLevel
  • the refresh and load-more callbacks, onRefreshTasks, and onTogglePaginatedLists

It is one hook because every one of these routes through the command-scoped auth recovery useOAuthRecovery publishes (#2153). A command that hits a lapsed authorization has to reach the same recovery, or two of them would prompt differently for one server — which is also why this cluster cannot precede the OAuth one.

useResultPanels

toolCallState / getPromptState / readResourceState move too, but behind a small useResultPanels exported from the same file. That split is call order, not taste — the same reason useHandshakeTelemetry sits beside useConnectionLifecycle: useOAuthRecovery drops those panels on a session reset (clearResultPanels) and routes a step-up failure into whichever one issued the command (setSourceScopedError), and it runs before the commands hook that consumes its wrappers. So the state has to exist earlier than the hook that otherwise owns it. App.tsx calls useResultPanels() up top and hands the whole surface down.

Inert by construction

No behavior change. In particular #2095's per-server pagination-override behavior is moved verbatim — the override record, the lastPersistedSettings baseline, the settle/rollback pair, and the sessionRef guard that keeps a rollback from pushing one server's value into another server's live client. Anything further belongs on top of this, per the constraint carried from #2129: a behavior change buried in a 135-line relocation is unreviewable.

Numbers

  • App.tsx: 2395 → 1811 lines (−681 / +82)
  • useServerCommands.tsx: 872 lines, useServerCommands.test.tsx: 72 tests
  • Coverage on the new file: 100% statements / lines / functions, 96.58% branches

Verification

npm run local:gate green end to end — validate (all three durable guards, test:scripts, validate:core, four client validations), the per-file ≥90 coverage gate, verify:build-gate, verify:bundle-externals, the smokes including all three web smokes, smoke:web:firefox, and the Storybook play functions.

No screenshots: this is a pure relocation with no rendered change.

🤖 Generated with Claude Code

https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev

Phase-2 step 4 of the App.tsx decomposition (#2155, under #2129/#2126).

Every command the screens issue against the live server moves into
`useServerCommands`: calling a tool, getting a prompt, reading and
subscribing to a resource, completing an argument, cancelling a task or
a call, setting either log level, refreshing or paging a list, and
flipping the pagination mode. All of them route through the
command-scoped auth recovery `useOAuthRecovery` publishes, which is why
this cluster cannot precede the OAuth one.

The three in-flight result panels move with them, behind a small
`useResultPanels` in the same file — split out only because of call
order, the same reason `useHandshakeTelemetry` sits beside
`useConnectionLifecycle`: `useOAuthRecovery` drops those panels on a
session reset and routes a step-up failure into whichever one issued
the command, and it runs before the commands hook.

The move is inert — no behavior change, #2095's per-server pagination
override included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev
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 00:30

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

Extracts live-server command handling from App.tsx into a dedicated, tested hook as part of the ongoing application decomposition.

Changes:

  • Adds useServerCommands and useResultPanels.
  • Moves command, pagination, logging, and result-panel logic out of App.tsx.
  • Adds comprehensive hook tests.

Reviewed changes

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

File Description
clients/web/src/App.tsx Wires the extracted hooks into the composition root.
clients/web/src/hooks/useServerCommands.tsx Implements server commands and result-panel state.
clients/web/src/hooks/useServerCommands.test.tsx Tests command and pagination behavior.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread clients/web/src/hooks/useServerCommands.test.tsx Outdated
Comment thread clients/web/src/hooks/useServerCommands.tsx
Comment thread clients/web/src/hooks/useServerCommands.tsx
Give the test's paginated-list fixture a precise return type instead of
an unjustified double cast. The cast was hiding that the fixture omitted
`error`, so an interface change would not have been type-checked there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review round 1 — responses

Mirrored at PR level because inline replies get hidden once the fix is pushed and the thread goes outdated.

1. Unjustified double cast in the paginated-list test fixture — fixed (5c7c518)

Right, and for a better reason than the style rule: the as unknown as PaginatedListModel<T> was hiding that the fixture omitted error, so a change to that interface would not have been type-checked at this call site. The helper now declares : PaginatedListModel<T> and supplies error: null (items: [] as T[] also stops being needed once the return type is annotated).

2 & 3. void inspectorClient.subscribeToResource(uri) / unsubscribeFromResource(uri)agreed, deferred to #2174

The finding is correct on the merits. Both methods throw — on "Client is not connected", on "Server does not support resource subscriptions", and after rolling subscription state back on a transport failure — so the callee does not own its failures, and AGENTS.md only permits a bare void when it does. These are also the only two commands in the hook that skip the shared recovery, so a mid-session 401 on a subscribe does not trigger re-auth the way a 401 on a refresh does.

It is not fixed in this PR because both lines are moved verbatim. #2155 is a decomposition step whose stated contract is that it changes no behavior, and routing these through runCommandInBackground adds auth recovery and an error toast — a real behavior change, and exactly the kind that is unreviewable buried inside a 680-line relocation. Fixing it separately keeps this diff readable as a move and gives the fix its own tests and its own review.

Filed as #2174 (v2 / bug / v2.5.0, on the board), with the repro, the rule it breaks, and the proposed fix.

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 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread clients/web/src/hooks/useServerCommands.test.tsx Outdated
Copilot round 2. The stand-in `runWithCommandAuthRecovery` was a bare
pass-through while its comment claimed it reproduced the real one, so
`onCompleteArgument` and `onCancelTask` — whose only auth handling *is*
that wrapper — would have passed every test here while bypassing the
shared recovery entirely. That routing is the integration this
extraction exists to preserve.

The stand-in now matches `useOAuthRecovery`'s: it catches an
`AuthRecoveryRequiredError`, hands it to `handleCommandScopedAuthRecovery`
with the call site's `source` and a `retryOperation`, retries once when
the recovery reports satisfied, and rethrows anything else.
`runCommandInBackground` is built on it, as the real one is. Three tests
pin the source and the retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Correct, and a good catch — the comment above that stand-in claimed something it did not do. Fixed in f7fe4bc.

The gap was exactly as described: onCompleteArgument and onCancelTask have no auth handling of their own — the wrapper is their recovery — so with a pass-through they would have passed every test here while bypassing it entirely.

The stand-in now mirrors useOAuthRecovery's implementation rather than approximating it: it returns operation() directly when there is no client or no active server, otherwise catches an AuthRecoveryRequiredError, calls handleCommandScopedAuthRecovery(err, { serverId, source, retryOperation: operation }), retries once when it reports satisfied, returns undefined when it does not, and rethrows anything else. runCommandInBackground is now built on that same wrapper — as the real one is — so a background command reaches the recovery on the same terms an awaited one does, and only the reporting is its own.

Three tests pin it, asserting the source rather than merely that some recovery ran (the source is what decides which panel a step-up failure lands in):

  • onCompleteArgument — recovers, retries, returns the post-reauth values; source: "tool", retryOperation present
  • onCompleteArgument — returns [] when the recovery is not satisfied
  • onCancelTask — recovers, retries, and stays silent because the recovery owns the prompt

The old "stays silent when the cancel raised a recovery" test was keeping its own name while testing the no-active-server path by accident; it is renamed to say what it actually covers.

75 tests, still 100% statements/lines/functions and 96.58% branches on the hook. npm run local:gate green.

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 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

clients/web/src/hooks/useServerCommands.tsx:895

  • Remove the duplicated word in this comment.
  // Wrap Load-next-page in ambient auth recovery too, so a paginated
  // paginated fetch that hits a 401 recovers like the all-pages path (#1721).

Copilot round 3, from its suppressed set: "a paginated paginated fetch".
The line came across verbatim with `onLoadMoreTools`, so the typo
predates this PR — fixed here since the comment is the thing being
moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review round 3 — response

No new comments. The one suppressed comment was real and is fixed in 71cd113:

clients/web/src/hooks/useServerCommands.tsx:895 — Remove the duplicated word in this comment.
// … so a paginated / // paginated fetch that hits a 401 …

The line came across verbatim with onLoadMoreTools, so the typo predates this PR — worth fixing here anyway, since the comment is one of the things being moved. Now reads "so a paginated fetch that hits a 401 recovers like the all-pages path (#1721)".

npm run local:gate green again on the change.


Round summary

Round Finding Disposition
1 Unjustified double cast in the paginated-list test fixture Fixed — 5c7c518
1 void subscribeToResource / void unsubscribeFromResource Agreed; deferred to #2174 (behavior change, and this step is an inert move)
2 Harness recovery wrapper was a pass-through claiming to model the real one Fixed — f7fe4bc, plus three tests pinning source and retry
3 Duplicated word in a moved comment Fixed — 71cd113

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 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

clients/web/src/App.tsx:781

  • “Every one” is false while the extracted subscribe/unsubscribe handlers bypass command-scoped recovery. Reword this ordering comment to describe the commands that participate in recovery rather than asserting coverage the hook does not yet provide.
  // --- Action handlers that route directly to the InspectorClient. Every one
  // of them routes through the command-scoped auth recovery above, which is
  // why they sit after it (#2155). ---

clients/web/src/hooks/useServerCommands.tsx:249

  • This rationale is currently inaccurate: onSubscribeResource and onUnsubscribeResource below still call the client directly and do not use the shared recovery surface (the follow-up is tracked separately). Please document those exceptions so future callers do not assume subscription 401s already recover through this hook.
 * It is one hook because every one of these routes through the same edge —
 * the command-scoped auth recovery `useOAuthRecovery` publishes (#2153). A
 * command that hits a lapsed authorization has to reach the *same* recovery,
 * or two of them would prompt differently for one server. That dependency is
 * also why this cluster cannot precede the OAuth one.

@cliffhall
cliffhall merged commit 2856f82 into v2/main Aug 28, 2026
4 checks passed
@cliffhall
cliffhall deleted the v2/chore/2155-use-server-commands branch August 28, 2026 02:23
@cliffhall cliffhall linked an issue Aug 28, 2026 that may be closed by this pull request
3 tasks
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 2 step 4: extract useServerCommands

2 participants