Commit graph inspector/clients/web/src/components/groups/ToolControls/ToolControls.test.tsx
Author SHA1 Message Date
cliffhall
4340283f70 test: add a duplicate-tool-names test server for the #1957 repro
Nothing in the repo could serve the shape this fix is about: every preset
registers a unique name, and the SDK's `registerTool` rejects a repeat. So the
only reproduction was a unit test — there was no way to smoke the real app
against a server that returns duplicates.

Adds `ServerConfig.duplicateToolNames` (a list of registered tool names) to the
composable test server, plumbed through the JSON config, and a showcase config
`duplicate-tool-names-http.json`. The named tools are emitted a second time in
`tools/list` with the same `name` and a "(duplicate)" title.

The repeats are appended after the whole list rather than placed beside their
twin. That is both the realistic shape (two tool sources concatenated) and what
makes the defect observable: React matches a leading run of same-key children
first, so a head-adjacent duplicate happens to line up and the bug hides. Only
a separated pair collides in the keyed map and orphans a row — verified both
ways against the prod bundle.

The `tools/list` override is shared with the existing pagination handler (which
already hand-builds the list); with no `maxPageSize` it returns a single
unbounded page. Duplication happens before pagination, so a duplicated pair can
straddle a page boundary the way a real server's would.

Also adds a ToolControls test for this appended ordering, distinct from the
reporter's interleaved fixture, and documents the config in the README showcase
table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SW1p8E2uiyyLx4RKwrwSrt
2026-08-10 20:22:47 -04:00
cliffhall
6f535356b4 fix: key tool rows by source position so duplicate names filter correctly
`ToolControls` keyed both the main list and the SEP-2243 excluded list by
`tool.name`. A server may legitimately return the same tool name more than
once, and colliding React keys let a filtered-out row survive reconciliation:
searching narrows the array, React matches the surviving rows against the
stale keys, and an unrelated row stays mounted (plus a "two children with the
same key" console warning).

Stamp each row's position in the unfiltered list before filtering and use
`<index>:<name>` as the key, on both lists. The position is captured before
the filter, so the key stays stable as the search narrows. No deduplication,
no change to what `tools/list` returns, and the name/title search behavior is
unchanged.

Adds regression tests for both lists using the reporter's fixture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SW1p8E2uiyyLx4RKwrwSrt
2026-08-10 19:56:38 -04:00
cliffhall
f201032137 fix: surface list-fetch failures instead of swallowing them
A list load that failed — a transport error, or a result the SDK's era
codec rejects as invalid — reported nothing. The connect-time refresh is
fired with no caller to await it (`void this.refresh()`), so its rejection
became an unhandled rejection: the header said "Connected", the panel
rendered an empty list indistinguishable from a server that has none, and
the failed exchange showed in the Protocol panel as a clean success.

Two surfaces now tell the truth:

- ManagedListState records the failure as observable state (`getError` +
  an `errorChange` event) and still re-throws, so App's auth-recovery
  wrapper keeps working. The four `useManaged*` hooks expose it via a
  shared `useManagedListError`, and the Tools/Prompts/Resources sidebars
  render a `ListLoadError` alert with the reason and a Retry.
- A response the client refused is attributed back to its Protocol entry
  (`markResponseRejected` → `responseRejected` → `MessageEntry.clientError`),
  which now renders an Error status and a "Rejected by the Inspector"
  alert rather than the green resultType badge alone. The SDK gives no
  request id with a decode failure, so the id is recovered by correlation;
  see the comment on `markResponseRejected` for why that is exact.

Also stops `listAllTools` from dropping a failing excluded-tools walk on
the floor — it stays non-fatal but is logged.

Closes #1953

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SW1p8E2uiyyLx4RKwrwSrt
2026-08-08 16:48:48 -04:00
Cliff Hall
e70a11360c
Tools: x-mcp-header annotation tooling + -32602 unknown-tool rendering (#1632) (#1735)
* Tools: x-mcp-header annotation tooling + -32602 unknown-tool rendering (#1632)

Card 9/10 of the SDK-v2 + New Spec stack. Adds SEP-2243 `x-mcp-header`
tooling to the Tools tab and adjusts result rendering for SDK v2's
unknown-tool behavior, and bumps the SDK to 2.0.0-beta.5.

- **`core/json/xMcpHeader.ts`**: a faithful port of the SDK's (non-public)
  `scanXMcpHeaderDeclarations` — validates every SEP-2243 constraint
  (primitive types incl. `number`, RFC 9110 token, case-insensitive
  uniqueness, statically-reachable `properties`-only chain) and derives the
  mirrored `Mcp-Param-{Name}` params for a kept tool.

- **Excluded tools**: the SDK silently drops tools with invalid annotations
  from `tools/list`. InspectorClient now re-lists the RAW (unfiltered) list
  and scans it to recompute the excluded set with reasons — gated to the
  modern non-stdio era (matching the SDK's gate), recomputed on every
  aggregate refresh, surfaced via a new `excludedToolsChange` event +
  `getExcludedTools()` and `useInspectorClient().excludedTools`. The Tools
  sidebar lists them struck-through under an "Excluded (SEP-2243)" divider
  with the reason on hover.

- **Mirrored args**: ToolDetailPanel shows a "Mirrored request headers"
  section listing each arg → `Mcp-Param-{Name}` (with the browser-omits note).

- **-32602 rendering**: SDK v2 rejects an unknown-tool call with `-32602`
  instead of an `isError` result, which previously rendered nothing. New
  ToolCallErrorPanel renders a thrown tool-call error, with a targeted
  "Unknown Tool" hint for `-32602`.

Adds unit + Storybook + integration coverage (a real modern server with an
invalid-header tool fixture) for every path. `npm run ci`-equivalent checks
green.

Closes #1632

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* test-servers: x-mcp-header modern showcase config + preset (#1632)

Register the `invalid_header_tool` fixture as a preset and add
`xmcpheader-modern-http.json` — a modern HTTP showcase serving echo, a
valid-header `get_weather`, and the invalid-header tool — so the Tools-tab
x-mcp-header tooling (mirrored headers, excluded-tools surface, -32602
rendering) can be exercised manually. Documented in the README alongside the
other per-SEP showcase configs. Smoke-tested end to end in the web client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* docs: add #1632 proof screenshots to pr-screenshots

End-to-end web-client verification of the x-mcp-header Tools tooling against
the modern showcase server: excluded-tools surface + reason, mirrored-headers
section, and the -32602 unknown-tool error panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Respond to review: disambiguate -32602, doc/search nits (#1632)

Addresses the review on PR #1735:

1. -32602 "Unknown Tool" mislabel: `-32602` is generic Invalid params, so a
   known tool called with bad arguments hit the same code and rendered under
   the definitive "Unknown Tool" heading. Replace `isUnknownToolError` with
   `classifyToolCallError(code, message)` → "unknown-tool" only when the
   message names it (not found / unknown tool / …), else "invalid-params"
   (new "Invalid Parameters" heading + schema hint), else "generic".

2. Double pagination walk: expand the `listAllTools` comment to document that
   the excluded-tools recompute is a deliberately un-cached second raw walk
   (doubles list round-trips on modern non-stdio, runs even on a cache-served
   aggregate) — accepted for a debugging tool, no-op on legacy/stdio.

3. Doc nit: `scanXMcpHeaderDeclarations` no longer claims to visit "$ref
   targets" — reworded to note `$ref` is never followed (refs resolve into the
   unreachable `$defs` body).

4. Search nit: the excluded-list filter now matches name AND title, matching
   the main tools filter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Tighten unknown-tool matcher to be tool-scoped (#1632 re-review nit)

The re-review noted the -32602 disambiguation was symmetric: broad markers
("does not exist", "unrecognized") could inversely mislabel a genuine
invalid-params message (e.g. "property 'region' does not exist") as "Unknown
Tool". Scope the not-found/does-not-exist family to require the word "tool" in
the same clause (matching the SDK's "Tool <name> not found"); keep "unknown
tool" / "no such tool" as standalone unambiguous markers. Adds a
tool-less-"does not exist" test asserting it classifies as invalid-params.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Add trigger_invalid_params showcase tool + Invalid Parameters screenshot (#1632)

Adds a `trigger_invalid_params` spec-error trigger to the modern showcase
server: the injector answers its `tools/call` with a real `-32602 Invalid
params` JSON-RPC error whose message does NOT name an unknown tool. This lets
the "Invalid Parameters" branch of the tool-call error panel (added in the
review response) be exercised live, distinct from the unknown-tool `-32602`.

- `SPEC_ERROR_TRIGGERS` gains the `trigger_invalid_params` entry (HTTP 200,
  in-band -32602) + preset registration.
- Enable `injectSpecErrors` on `xmcpheader-modern-http.json` and add the tool.
- README + pr-screenshots README document it; adds the live "Invalid
  Parameters" proof screenshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:42:54 -04:00
Cliff Hall
b3b04c6939
Thread cacheMode through managed list refresh via SDK cache-aware verbs (#1721) (#1723)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* Thread cacheMode through managed list refresh via SDK cache-aware verbs (#1721)

Manual/auto refresh of tools/resources/prompts now forces a cache-bypassing
round trip on modern servers, delivering the raw-request cacheMode control
deferred from #1628.

The InspectorClient list/read path uses the SDK's single-page `client.request`
verbs (for pagination debugging), which never consult the SDK response cache —
so `cacheMode` was inert there. This adds a separate aggregate path that routes
through the SDK's cache-aware high-level verbs, where `cacheMode` is honored:

- InspectorClient: new `listAllTools/Prompts/Resources/ResourceTemplates(
  { cacheMode, metadata })` that call `client.listTools()` etc. (all-page
  aggregate + SEP-2243 x-mcp-header exclusion + cache consult/write). A shared
  `getCacheableRequestOptions` / `aggregateListParams` back all four.
- InspectorClientProtocol: the four `listAll*` methods added.
- ManagedListState: delegates page walking to the SDK verb (config `fetchPage`
  loop → `fetchAll`); `refresh(metadata?, cacheMode?)` threads cacheMode. A
  `list_changed` auto-refresh bypasses the cache (`cacheMode: "refresh"`).
- Managed hooks: user-initiated `refresh()` passes `cacheMode: "refresh"`, so
  the existing per-tab Refresh buttons become genuinely cache-bypassing — no
  App wiring change. Connect-time load stays default (`'use'`).

The single-page `listTools(cursor)` primitive is unchanged; the paged hooks
still surface per-page `nextCursor`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Single-page list pagination: sidebar toggle + server setting (#1721)

Delivers the deferred single-page fetch UI: Tools/Resources/Prompts can be
loaded one page at a time instead of auto-aggregating every page — a defensive
option for servers with very large lists.

- New `singlePageLists` server setting (persisted, server-wide), plumbed through
  ServerConfig/InspectorServerSettings, serverList convert/default, the node +
  remote settings paths, and the Server Settings form ("Fetch Lists One Page at
  a Time").
- New `ListPaginationControls` element (a "Single page" switch + "Load next
  page" button + "N pages loaded" status) rendered in each list sidebar; the
  toggle edits the setting live (optimistic) and persists it.
- The paged state stores (PagedTools/Prompts/ResourcesState) now own loading:
  they auto-load page 1 on connect in single-page mode and track nextCursor +
  page count as observable state; the managed stores skip their all-page walk
  in single-page mode (so nothing is over-fetched) while still lighting the
  list-changed indicator. `usePaginatedList` selects the display source and
  pagination controls per mode — pure, no data-loading in React effects.
- Refresh is mode-aware: reload page 1 in single-page mode, refetch the whole
  aggregate otherwise. Sidebars widened 340→360 so the controls don't crowd
  list entries.
- Test server: `test-servers/configs/pagination-http.json` (12 tools/resources/
  prompts, page size 4 → 3 pages each) exercises the flow end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Add pr-screenshots proving single-page pagination (#1721)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Address @claude review: template load, double-click guard, auth recovery (#1721)

- Fix: resource templates never loaded on connect in single-page mode. The
  connect-time all-page skip now applies only to lists with a paged counterpart
  (new `deferInSinglePage` flag on ManagedListConfig; false for resource
  templates), so templates still aggregate on connect regardless of the setting.
- Guard `PagedTools/Prompts/ResourcesState.loadPage` against a concurrent load
  (fast double-click on "Load next page") so the same page can't be appended
  twice; the in-flight load owns the cursor and the second call is a no-op.
- Thread ambient auth recovery through the single-page refresh, Load-next-page,
  and mode-switch loads (they previously bypassed it) — usePaginatedList's
  onRefresh/onLoadMore now return their promises so App can wrap them.
- Drop the misleading "aggregated list" wording from the paged-store docs.

Tests added for each; npm run coverage green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Review round 2: revert optimistic toggle when persisting the setting fails (#1721)

Non-blocking review follow-up. If updateServerSettings rejects, the optimistic
`singlePageListsOverride` is now cleared and the live client setting rolled back
(the effect only clears the override when the persisted value changes, which
doesn't happen on failure), so the UI and client reflect what's on disk rather
than the failed edit. Added an App test for the revert path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Fix single-page Refresh not clearing list-changed indicator + UI polish (#1721)

Round-3 review fix (blocking): in single-page mode a `list_changed` lights the
list-changed indicator, but the single-page Refresh reloads page 1 of the paged
store and bypassed `useManaged*.refresh()`, so the indicator was never cleared.
Expose `clearListChanged` from useManagedTools/Prompts/Resources and call it in
the single-page branch of onRefreshTools/Prompts/Resources. Tests added.

UI polish (user feedback on the sidebar control):
- Rename the switch label "Single page" → "Paginated".
- Put "Load next page" on the same row as the switch, space-between (switch
  fully left, button fully right).
- Move the "N pages loaded" status to its own line below the row, centered.
- Hide the whole control once the list is known to be a single page
  (single-page mode, page 1 loaded, no nextCursor) — nothing to paginate.

pr-screenshots refreshed to the new layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Rename the singlePageLists setting to paginatedLists for clarity (#1721)

`singlePageLists` read backwards from the "Paginated" switch label (it sounds
like paginated=false). Rename the server setting to `paginatedLists` with the
same value meaning — true = paginated/one-page-at-a-time, false = aggregate-all
— and follow through on the related identifiers for consistency:

- Setting field `paginatedLists` across ServerConfig / InspectorServerSettings,
  serverList convert/default, node + remote settings paths, and the form/modal
  callbacks (onPaginatedListsChange).
- `usePaginatedList` / `ListPaginationControls` prop `singlePage` → `paginated`,
  `onSinglePageChange` → `onPaginatedChange`.
- `ManagedListConfig.deferInSinglePage` → `deferWhenPaginated`.
- App state (paginatedListsOverride, onTogglePaginatedLists, …) and all
  tests/fixtures + mode-referring comments.

"single-page" wording that denotes the SDK's one-page `client.request` verb
(inspectorClient) is intentionally left as-is. Purely a rename — no behavior
change; unit + integration + cli + tui + coverage all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Format App.test.tsx (prettier) after the paginatedLists rename (#1721)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 01:06:31 -04:00
cliffhall
19a6df7ad3 SDK v2 migration (behavior-neutral, versionNegotiation: 'legacy') (#1624)
Migrate all three clients + core + test-servers from
@modelcontextprotocol/sdk@1.29 to SDK v2 (2.0.0-beta.4), keeping behavior
identical against legacy (2025-11-25) servers. Foundation for the SDK V2 +
New Spec stack.

Packages
- Split @modelcontextprotocol/sdk -> @modelcontextprotocol/client (Client,
  transports, OAuth, error taxonomy, protocol types) + core (Zod schemas);
  stdio -> client/stdio, Ajv -> client/validators/ajv. Test-servers ->
  @modelcontextprotocol/server (+ server-legacy/sse). Deterministic import
  rewrite (schemas -> core, everything else -> client); the codemod misroutes
  client code to /server so it was not used for path routing.
- new Client(..., { versionNegotiation: { mode: 'legacy' } }) — nothing
  2026-era goes on the wire.

Runtime (core/mcp/inspectorClient.ts)
- Error taxonomy: McpError -> ProtocolError, ErrorCode -> ProtocolErrorCode;
  RequestTimeout -> SdkError/SdkErrorCode.
- Handlers take method strings; task methods (tasks/*, notifications/tasks/*)
  use the 3-arg custom form (excluded from v2's spec-method set).
- Legacy tasks kept working (per project requirement): requestor tasks re-
  implemented over raw client.request + explicit deprecated schemas;
  experimental.tasks.callToolStream replaced by a local pollTaskToolCall
  async generator (with _progressHandlers revival so task-execution progress
  still reaches the caller). Receiver (server-initiated) tasks: the v2 Client
  validates a sampling/elicit handler result and rejects the legacy { task }
  response with -32602 — installReceiverTaskResponseBypass swaps the wrapped
  entry in the private _requestHandlers map for the task-augmented branch so
  { task } reaches the wire (fixes server-initiated sampling-via-task).
- Pagination: list verbs use raw client.request({method}, List*ResultSchema)
  for single-page behavior (v2's client.listX() auto-aggregate all pages).

Web / auth / apps
- Rename local JsonSchemaType -> InspectorFormSchema + add toFormSchema()
  structural narrowing at the SDK-schema -> form boundary.
- ext-apps still peers on SDK v1: one compat boundary with documented casts
  (remove when ext-apps#702 lands a v2 peer).
- OAuth provider saveClientInformation reconciled with v2's
  OAuthClientInformationContext; InvalidClientMetadataError re-homed as a
  local class (server-legacy/auth is not browser-safe).
- vitest.shared.mts dedupes @modelcontextprotocol/client+core to one copy so
  vi.mock intercepts the SUT's import.

Accepted v2 behavior changes (per #1624 / spec §9.2): unknown-tool call
rejects with -32602; cancellation is stream-abort (no guaranteed
notifications/cancelled frame); output-schema strictness restored via the
Inspector's own Ajv check on the default path.

npm run ci green: validate (web 3087 / cli 212 / tui 274 / launcher 5),
coverage (>=90 per-file, all clients), smoke (launcher/cli/web), storybook
(410).

Closes #1624

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:44:24 -04:00
Cliff Hall
c96db0402e
test: raise per-file coverage gate to 90 across all clients (closes #1271) (#1532)
* test(web): audit branch-coverage outliers, raise per-file gate to 70 (#1271)

The web per-file coverage gate had drifted red: the recently-landed EMA
auth feature (core/auth/ema/*) and ClientSettings components shipped
undertested, and the prior 50% branch floor let business-logic branches
go untested. CI does not run the coverage gate, so this accumulated.

Add tests to bring every failing file above the gate and push branch
coverage well past 70 across the board, then raise the per-file branches
threshold 50 -> 70 in clients/web/vite.config.ts.

- EMA core (emaFlow, resourceContext, jwt, wire): 50-88% -> 86-100% branch
- EMA OIDC/transport (idpOidc, tokenEndpoint, transportProvider): -> 97-100%
- oauthManager / discovery: 73-83% -> 88-92%
- ClientSettingsModal/Form, ConnectionInfoContent: 0-88% -> 96-100%
- useClientSettingsDraft / useEmaIdpLoginState hooks: 20% -> 100%
- TaskStatusBadge / SortableServerCard outliers: 50% -> 100%

Coverable branches reached by mocking useComputedColorScheme (dark theme),
dnd-kit useSortable (isDragging), and fetch/OIDC paths. Remaining sub-100
branches are genuinely-unreachable defensive guards or happy-dom-inherent
(portal / useMediaQuery / typeof window) and documented in test comments.

No source files changed. Global web branch coverage 88.73% -> 90.63%.
AGENTS.md updated; cli/tui/launcher stay at 50 (out of scope).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: raise per-file coverage gate to 90/90/90/90 across all clients

Follows the #1271 web branch-coverage audit (50 -> 70) by raising the
whole per-file gate to >=90 on all four dimensions — lines, statements,
functions, branches — for web, cli, tui, and launcher (core/ is measured
under the web suite). A codebase-wide audit added real tests for every
outlier and brought global coverage up across the board.

Config: all four vitest/vite coverage configs set to 90/90/90/90.
Two pure-type modules (core/mcp/import/types.ts, clients/web/server/
types.ts) added to the web coverage exclude list, matching the existing
type-only exclusions.

Tests: ~50 test files extended/created across core (auth, ema, mcp,
remote, state, storage, react), web components/screens/server, and the
cli/tui clients. Highlights: full EMA OAuth flow + transport coverage,
inspectorClient/remote server backfill, every Mantine control's handler
branches, and the cli unsupported-method / argv-fallback paths.

Genuinely-unreachable branches are annotated at the source with justified
`/* v8 ignore -- <reason> */` comments rather than lowering the gate:
happy-dom-inherent paths (Mantine portal, useMediaQuery, typeof window
SSR guards), React StrictMode effect replay, and provably-dead defensive
guards (?? fallbacks the types make impossible, Select.onChange values
outside the allowed list). No runtime logic changed; the only structural
edits are two behavior-preserving refactors (ResourceTemplatePanel hoists
the field value to a local so the ignore applies in JSX; ToolsScreen
swaps a ternary for an if) and prettier line-wrapping.

Also fixed a flaky real-timer debounce assertion in PromptArgumentsForm
(asserted an exact sibling-typing interleaving that raced the 300ms
debounce; now asserts the load-bearing fact — a non-empty fire-time
sibling capture).

AGENTS.md updated to describe the uniform 90 gate and the v8-ignore
policy. All four client coverage gates, root validate, storybook (370),
and smoke pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(web): make connect-crash SSE reader stop on the terminal event

`readSseEvents` looped until the server closed the stream (`done`). The
server closes the events stream immediately after writing the terminal
`transport_error`, but under the heavy parallel scheduler pressure of a
full `coverage` run (v8 instrumentation across all workers) that close
can lag far enough behind the event to trip the 30s test timeout — a
hang, not a wrong result. This made the test intermittently fail under
`npm run coverage` (it passed in isolation and under CI's lighter
`test:integration`).

Stop reading as soon as the terminal `transport_error` event arrives
(cancelling the reader) instead of waiting only for `done`. That event is
always the last one the session queue emits, so nothing is lost, and the
test no longer depends on stream-close timing. The `done` break is kept
for any stream that closes first.

Verified green across two consecutive full `npm run test:coverage` runs
(3094 tests) where it previously hung, plus isolation (~0.5s).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:26:03 -04:00
Cliff Hall
8923a9bbf9
feat(web): tool progress toasts + persist all per-screen state across navigation (#1414, #1417) (#1420)
* feat(web): tool progress toasts + persist tool selection/result across navigation (#1414)

Surface incoming notifications/progress as toasts so the user can watch a
long-running tool's progress without leaving the tool view — the v2 replacement
for v1's always-visible "Server Notifications" shelf. Toasts are keyed by
progress stream and updated (not stacked) per tick so a chatty server updates
one toast rather than flooding the corner. Progress notifications still land in
the History tab via the existing transport-level tracking.

Companion change: lift the selected tool and its form values into App.tsx
alongside the already-lifted toolCallState, make ToolsScreen controlled, and
drop the clear-on-unmount effect. Leaving the Tools tab and returning now
preserves selection, inputs, and result. All three reset together on
disconnect via resetSessionScopedUiState, preserving the #1368/#1394 behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): persist per-screen selection/search/filter across navigation (#1417)

Lift every screen's selection, form, search, and filter state out of the
screen components into session-scoped state owned by App, passed down as
controlled props. The screens unmount on tab switch, so this is what keeps
the user's context in place when they navigate away and back within a live
session. State is cleared only on disconnect (resetSessionScopedUiState) or
an explicit user action — never on plain navigation.

Lifted per screen:
- Tools: sidebar search (selection/form/result were lifted in #1414)
- Prompts: selected prompt, argument values, submitted marker, search
- Resources: selected resource/template URI, originating-template marker,
  search, accordion open-sections
- Apps: selected app, form values, search (running/maximized stay local —
  they're tied to the live iframe/bridge that is torn down on unmount)
- Tasks: search + status filter
- Logs: filter text + visible-level set
- History: search + method filter
- Network: filter text + visible-category set

The four sidebar *Controls (Tool/Prompt/App/Resource) become controlled for
search; ResourceControls also controls accordion open-sections. Level/category
default maps move to logLevels.ts / fetchCategories.ts so the screen files
keep a single component export.

Pagination already persists (it lives in the App-level Paged*State managers),
so no work was needed there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): persist list scroll position across navigation + reset tests (#1417)

Add useScrollMemory: a module-keyed hook that remembers a Mantine ScrollArea's
offset across unmount/remount (the screens unmount on tab switch). Scroll lives
in a module-scope map rather than threaded App state — it's ephemeral DOM state,
and threading ~16 more props would balloon the surface for no benefit. App calls
clearScrollMemory() in resetSessionScopedUiState so offsets clear on disconnect
alongside the lifted selection/filter state.

Wired into the four main list panels (Logs, History, Network, Tasks) and the
Tools/Prompts/Apps sidebar lists. Pagination already persists via the App-level
Paged*State managers.

Tests: useScrollMemory unit test (save/restore, key isolation, clear), and the
App disconnect test now also exercises representative lifted UI state (tool
search, prompt selection, log filter) clearing on disconnect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(web): collapse per-screen lifted state into one {screen}Ui object

Replace the ~46 individual selection/search/filter props threaded App →
InspectorView → screen with one `{screen}Ui` object + one `on{Screen}UiChange`
setter per screen (8 pairs). Each screen reads its fields from `ui` and updates
via `onUiChange({ ...ui, field })`; the in-flight result panels
(toolCallState / getPromptState / readResourceState) stay separate since
they're written by App's async action handlers, not the screens.

The `*UiState` interfaces live with their screens; the empty-state defaults are
gathered in screenUiState.ts so the screen files keep a single component export
(react-refresh), mirroring logLevels.ts / fetchCategories.ts.

Trims InspectorView's prop surface substantially and keeps the persistence
behavior identical. Tests/stories collapse their stateful wrappers to a single
ui object; added Tools/Tasks/History interaction tests to keep per-file
coverage above the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): dismiss visible progress toasts on client teardown (#1414 review)

The progressNotification effect cleanup cleared its id-tracking set but left
on-screen "Tool progress" toasts to auto-close up to 5s later — they could
linger into the next session, and the lingering toast's onClose could delete an
id from the new session's set and trigger a duplicate-id re-show. Hide each
tracked toast in cleanup so both gaps close. Addresses @claude review note #2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(web): clear the tool result when a different tool is selected

The Tools result panel renders `toolCallState` regardless of which tool is
selected, so selecting a new tool used to leave the previous tool's result
showing beneath it. Route tools UI changes through an App handler that drops
`toolCallState` whenever `selectedToolName` changes; search/form edits keep the
selection (and the result) intact. Tests cover both the clear-on-switch and
keep-on-search paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:46:37 -04:00
Cliff Hall
75f74d64c1
test(coverage): bootstrap vitest unit infra and add tests at 90%+ per file (#1245) (#1269)
* test(coverage): bootstrap vitest unit infra and add tests at 90%+ per file (#1245)

Establishes the unit-test runtime AGENTS.md mandates: a vitest "unit"
project alongside the existing storybook project, RTL + jest-dom +
happy-dom, a Mantine-aware render helper, and a 90%-line per-file
coverage gate enforced by CI. Adds 66 test files covering all 62
components under elements/groups/screens/views, plus core/mcp and
clients/web/src/utils. Total: 577 tests, 98.7% line coverage globally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(coverage): address PR #1269 review feedback

- AGENTS.md: correct coverage gate description (lines ≥ 90, statements ≥ 85,
  functions ≥ 80, branches ≥ 50) with rationale for the relaxed branch
  threshold
- InspectorView.test.tsx: document what the Math.random() mock values
  (0.1 / 0.99) control vs. STUB_SUCCESS_RATE in InspectorView.tsx
- vite.config.ts / tsconfig.test.json: drop unused `globals: true` and the
  `vitest/globals` type now that every test file imports describe/it/expect/vi
  explicitly; manual `afterEach(cleanup)` in setup.ts remains required because
  testing-library auto-cleanup relies on globalThis.afterEach

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:15:32 -04:00