Commit graph inspector/clients/web/src/components/screens/ToolsScreen
Author SHA1 Message Date
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
ef921ba052
fix(web): hide resource subscription UI when server lacks subscribe capability (#1479)
* fix(web): hide resource subscription UI when server lacks subscribe capability (#1478)

Thread a `subscriptionsSupported` flag derived from
`capabilities?.resources?.subscribe === true` from App through
InspectorView → ResourcesScreen to the leaf components. When false:

- ResourcePreviewPanel hides the Subscribe/Unsubscribe button (Refresh
  stays).
- ResourceControls omits the Subscriptions accordion section entirely,
  dropping it from `allSections` so the expand/collapse-all toggle and
  open-section bookkeeping stay correct even if a stale "subscriptions"
  entry persists from a prior subscription-capable session.

The flag defaults to true at the leaf components so existing callers,
tests, and stories keep rendering the subscription controls unless a
server is explicitly marked unsupported.

Adds unit tests and Storybook stories covering the hidden and shown
states across all three components.

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

* fix(web): give the Servers screen scroll the same gutter/idle-hidden treatment (#1474)

The server grid's ScrollArea.Autosize used the default type="hover" with no
offsetScrollbars, so the scrollbar popped in on hover and overlaid the right
edge of the server cards (occluding their action icons / status badges) — the
same problem #1474 fixed for the History/Network/Logging list panels.

Apply the same `type="scroll"` + `offsetScrollbars` treatment, and reuse the
shared `expectScrollbarGutterIdleHidden` story assertion to regression-protect
the gutter + idle-hidden behavior.

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

* fix(web): stop the InspectorView shell from scrolling as a whole

AppShell.Main carried Mantine's default `padding="md"` on top of the
fixed-header offset, but every screen already sizes itself to
`calc(100vh - header)` and supplies its own `xl` padding (with no
allowance for Main's padding). That extra inset pushed screen content
past the viewport, so the whole InspectorView gained a page-level
scrollbar instead of confining overflow to each screen's inner
ScrollArea.

Set the shell `padding` to 0 (Main now contributes only the header
offset, matching what the screens were built for) and clamp the Main
slot to `height: 100dvh` with `overflow: hidden` in the theme, so the
view can never scroll regardless of content — only the inner ScrollArea
regions do.

Adds an InspectorView story play assertion guarding the invariant
(Main clips + is viewport-clamped) and a ManyServers fixture that
confirms it holds under enough content to overflow, with the inner
server-grid ScrollArea absorbing the overflow.

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

* fix(web): address review — align viewport units + drop stale subscriptions intent

Responding to the @claude review on #1479:

- Align all screen height calcs from `100vh` to `100dvh` so they match the
  Main slot's `100dvh` clamp. On a mobile viewport where `dvh < vh` a `100vh`
  child would have exceeded Main's `100dvh` content box and been clipped by
  `overflow: hidden`; the units are now in lockstep.
- Restrict `intendedButEmptySections` in ResourceControls to `allSections` so a
  stale "subscriptions" entry persisted from a prior subscription-capable
  session is dropped from persisted open-sections instead of being perpetually
  re-appended once the section is no longer rendered. Adds a regression test.

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-14 12:54:19 -04:00
Cliff Hall
8d4363d0ea
feat(web): expandable resource_link with read-on-demand (#1449) (#1456)
* feat(web): expandable resource_link with read-on-demand (#1449)

Replace the placeholder `resource_link` rendering in ContentViewer with a
dedicated, Mantine-native `ResourceLink` element that mirrors V1's behavior:

- Shows uri (monospace, link-styled), optional name/description, and a
  mimeType badge.
- Expand/collapse affordance (chevron + loading spinner) that is keyboard
  accessible (native button: role, aria-expanded, aria-label, Enter/Space).
- On first expand, reads the linked resource on demand via an injected
  callback and renders the full read result inline as formatted JSON under a
  green "Resource:" label, capped to a max height with scroll for large
  results. The fetched result is cached across collapse/expand.

The read handler is threaded as a "dumb component + callback as prop" through
ToolResultPanel → ToolsScreen → InspectorView, with App supplying a handler
that returns the `ReadResourceResult` from `inspectorClient.readResource`.

Also extract the shared `resourceContentsToBlock` helper into
`utils/resourceContents.ts` (previously private to ResourcePreviewPanel).

Tests: unit coverage for ResourceLink (collapsed/expanded/error/cache) and
the ContentViewer wiring, plus Storybook stories with play functions.

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

* fix(web): scroll large resource_link read results within the card

A plain ScrollArea with only `mah` does not scroll when its parent (the
content-sized card) has no definite height, so a large read result overflowed
the card instead of scrolling. Switch to ScrollArea.Autosize, which grows with
content up to the max height and then scrolls. Adds a LargeResult story
covering the bounded-scroll behavior.

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

* refactor(web): make ResourceLink a group; extract ResourceLinkInfo element

ResourceLink is stateful (expand/loading/cached result) and fetches data via
a callback — group-like behavior, and the only `elements/` component using
useState. Move it to `groups/` and split out the pure-display metadata as a
new `elements/ResourceLinkInfo` element (uri/name/description/mime + an
optional action slot).

This keeps the layer dependencies clean (no element→group import, which
moving ResourceLink alone would have introduced):
- ContentViewer (element) renders ResourceLinkInfo (element) statically for
  `resource_link` blocks and no longer takes an onReadResource prop.
- ToolResultPanel (group) renders the interactive ResourceLink (group) for
  `resource_link` blocks, ContentViewer for the rest.
- ResourceLink (group) composes ResourceLinkInfo + ContentViewer.

Also reverts the now-single-consumer resourceContents.ts extraction, restoring
ResourcePreviewPanel's local toContentBlock.

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

* fix(web): make ResourceLink read retryable and guard double-fetch

Address PR review: a failed read is no longer cached as terminal — only
a successful result is cached, so collapsing and re-expanding retries the
read after a transient failure. Add a loading guard so toggling while a
read is in flight does not fire a redundant second read.

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

* style(web): prettier formatting for ResourceLink test

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

* chore: remove accidentally-committed build artifacts; gitignore clients/*/build

A stray 'git add -A' swept 8 compiled build files into d25c9382. Remove
them from tracking and add clients/*/build to .gitignore so client build
output is never committed again.

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-13 17:15:25 -04:00
Cliff Hall
0890c7509a
feat(web): add Mantine clear buttons to text inputs app-wide (#1333) (#1448)
* feat(web): add Mantine clear buttons to text inputs app-wide (#1333)

Add a clear (×) affordance to every user-editable text input across the
web client so a populated field can be reset in one click.

- Autocomplete: enable `clearable` by default via the theme
  (`theme/Autocomplete.ts`) — covers the prompt-argument and
  resource-template variable autocomplete branches.
- TextInput / Textarea: Mantine v8 does not expose a public `clearable`
  prop on these primitives (only the combobox-based components do), so
  add the clear button manually via `rightSection` + `CloseButton`
  (aria-label="Clear") shown only when the field has a value, wired to
  reset the field's controlled value to "".

Applied to sidebar search inputs (prompts, tools, apps, tasks, history,
resources, network, logs), prompt-argument and resource-template
variable inputs, the sampling response/model fields, server config &
settings forms, roots table, schema form, experimental features panel,
and the server.json import panel. Read-only/display Textareas
(OutputValidationModal, UrlElicitationErrorModal) are intentionally left
untouched.

Tests cover the clear behavior on representative inputs from each
category (sidebar search, prompt argument, resource-template variable,
sampling response, plus the config/settings forms).

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

* fix(web): disable ServerConfigModal clear buttons while submitting

Address review #2 on #1448: the clear CloseButton sat in `rightSection`
with `rightSectionPointerEvents="auto"`, so it stayed clickable even
while the field was `disabled={submitting}`, letting a user wipe a field
mid-submit. Forward `disabled={submitting}` to each clear button so it
locks together with its input.

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

* refactor(web): remove redundant Auto-respond from SamplingRequestPanel

The sampling modal pre-loads its draft with the default stub result, and
"Send Response" sends that draft — so on an untouched form "Auto-respond"
did exactly the same thing as "Send Response", just with a fresh copy of
the same defaults. Drop the redundant button and its `onAutoRespond` prop;
"Send Response" now covers both the unedited (stub) and edited cases.

The InlineSamplingRequest variant keeps its Auto-respond button — there it
is the quick path versus "Edit & Send" (which opens this editable modal),
not a duplicate of an adjacent send action.

Updated PendingClientRequestModal wiring, stories, and tests accordingly.

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-12 09:51:05 -04:00
Cliff Hall
ed6eda7492
feat(web): wire up MCP task support — Tasks screen, Run-as-task, status toasts (#1422) (#1424)
Wires the ported Tasks screen + core task subsystem through App.tsx: sticky Clear Completed, cancel/refresh error toasts, core-side per-task progress correlation, Run-as-task on the Tools screen (gated on server support), per-server Task TTL, and live task-status toasts.
2026-06-04 08:49:47 -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
8f6a2e0f78
feat(web): render pending sampling/elicitation requests (#1407) (#1411)
* feat(web): render pending sampling/elicitation requests (#1407)

Server-initiated sampling and elicitation requests never reached the v2
web UI: the InspectorClient queued them and returned a Promise that only
resolved on a user response, but nothing subscribed to the pending-change
events or rendered the (already-built) request components. A tool that
triggered one spun the Execute button forever.

Wire the queues to the UI:
- Add usePendingClientRequests() (core/react) subscribing to
  pendingSamplesChange / pendingElicitationsChange and returning the live
  arrays. Expose getPendingSamples() / getPendingElicitations() on
  InspectorClientProtocol (and the test fake) so hooks can read them.
- Add PendingClientRequestModal, an App-level modal that surfaces one
  queued request at a time (sampling-first) via the existing
  SamplingRequestPanel / ElicitationFormPanel / ElicitationUrlPanel. It is
  non-dismissable so the blocked call only resolves via an explicit action.
- Wire it in App.tsx: responding/rejecting resolves the client's handler
  Promise, which unblocks the originating call and clears the spinner.

Also fold in two issues surfaced while verifying:
- SchemaForm rendered an `array` + `items.enum` (multi-select enum) as a
  JSON textarea because only `items.anyOf` had a branch; add a MultiSelect
  branch with enumNames label support.
- ToolsScreen's result lives in App and outlived the screen's local
  selection (reset on tab-switch unmount), leaving a result with no
  selected tool. Clear the result on unmount for a clean slate on return.
  Prompts/Resources gate their preview on selection, so they are unaffected.

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

* fix(elicitation): support explicit decline; honest queue label; review polish

Address @claude review on #1411:
- Add an explicit "Decline" action to ElicitationFormPanel so the spec's
  decline (refusal) is distinguishable from cancel (dismissal); the JSDoc
  claimed accept/decline/cancel but only accept/cancel were wired. Wire the
  modal body to emit { action: "decline" }.
- Replace the misleading "1 of N" queue label (the leading 1 never changed
  since the modal always shows the head) with an "N pending" hint, hidden when
  nothing else is queued.
- Make the dead useMemo honest: derive the head request inside the memo from
  the source arrays so it actually caches (the inline activeElicitation had a
  fresh identity every render).
- Document the URL elicitation accept-on-open semantic with a comment pointing
  at the follow-up (#1415).
- Add an App-level round-trip test driving pendingSamplesChange end-to-end
  (modal opens, respond() fires, empty event closes it).

Follow-ups filed: #1415 (URL two-step completion flow), #1416 (form required-
field submit gate + double-submit guard).

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

* fix(elicitation): include untouched schema defaults in submitted content

The schema form displays a field's default via resolveValue(), but onChange
only writes a field into `values` once the user edits it. So an elicitation
submitted with default-only fields left untouched dropped them entirely —
v2 sent just the manually-edited fields while v1 sent defaults too (reported
with the everything server: firstLine/integer/number/enum defaults missing).

Add collectSchemaDefaults() to jsonUtils (recurses nested objects, omits
fields with no default) and seed ElicitationFormModalBody's values state with
it via a lazy initializer. The modal body is keyed by request id, so it
re-seeds per request; user edits still override seeded defaults via the
existing onChange merge.

Tests: collectSchemaDefaults unit tests (field types, nested, empty) in
jsonUtils.test; a modal test asserting an untouched-default form still submits
its defaults in content.

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

* fix(forms): seed schema defaults for tool-call and app-launch forms too

Extend the elicitation defaults fix (collectSchemaDefaults) to the other two
SchemaForm consumers that own form state and submit it: ToolsScreen (tool
execute args) and AppsScreen (app launch args). Both reset formValues to {} on
selection, so default-only fields the user never edited were dropped from the
submitted args — same root cause as the elicitation modal.

Seed formValues from the selected tool/app inputSchema defaults on select.
User edits still override via the existing onFormChange merge.

(InlineElicitationRequest has no consumers wired up, so it's left as-is.)

Tests: ToolsScreen sends a tool's default on Execute without editing;
AppsScreen sends an app's default on Open App without editing.

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

* feat(elicitation): gate form submit on required fields; guard double-submit (#1416)

Fold the #1416 hardening into this PR:

- Gate the elicitation form's Submit button on required fields. Promote the
  hasMissingRequiredFields helper out of AppDetailPanel into jsonUtils (shared)
  and use it in ElicitationFormPanel so Submit is disabled until every required
  field has a value.
- Guard against double-submit. Add an optional `busy` prop to the sampling,
  elicitation-form, and elicitation-url panels that disables their responding
  actions. PendingClientRequestModal's bodies use a new ref-guarded
  useRespondOnce() hook: the first action dispatches and locks the panel; later
  clicks no-op. This stops a fast double-click from calling respond()/reject()
  twice (which throws and produced an unhandled rejection). Copy URL stays
  enabled — it doesn't resolve the request.

Tests: hasMissingRequiredFields unit tests; ElicitationFormPanel submit-gate
and busy-lock tests; a modal double-click-resolves-once test.

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

* fix(tools): cap detail/result panels at viewport and scroll internally

The Tools screen's detail and result columns sized to their content and bled
past the bottom of the viewport on long output (e.g. the env dump or a long
elicitation result). Cap each column at the screen's available height and let
its panel scroll internally instead.

- ToolsScreen: drop `align: flex-start` and wrap each column in a stretched
  `ContentPane` (Flex column, capped by `mah` = viewport − header − xl*2). The
  pane's definite height is what lets the card's inner ScrollArea know how far
  to shrink — a bare `mah` on the card does not (the card sizes to content, so
  the scroll region renders full-height and overflow:hidden just clips it,
  unreachable). Mirrors the proven Prompts/Resources preview panes.
- ToolResultPanel: pinned header + inner ScrollArea (flex 0 1 auto, mih 0).
- ToolDetailPanel: pinned title/annotations header, scrolling body
  (description + form + progress), pinned Execute footer.

Audited the other screens: Prompts/Resources already use this pattern,
ServerList and the four list panels (Logging/Network/History/Tasks) already cap
via ScrollArea.Autosize. Only AppsScreen's app-input form had the same gap —
fixed it the same way: AppDetailPanel now scrolls internally and the card uses
variant="preview" so the running app's iframe and a long form both stay within
the viewport.

Verified in-browser against the everything server: the get-env result now
stops at the viewport bottom with margin and scrolls within the card
(clientHeight 502 vs scrollHeight 1931), header pinned.

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

* test(remote): use sync pino destinations in logging tests to fix teardown race

CI flaked with an uncaught `ENOENT: ... /tmp/inspector-log-unknown-level-*/app.log`
that failed an unrelated later test. The remote-logging integration tests create
`pino.destination(logFile)` loggers (async sonic-boom streams) and rmSync the temp
dir in finally/afterEach without closing the stream — a buffered background flush
then fires after the dir is gone and throws an uncaught error, which Vitest counts
as a run failure even though all tests pass.

Make those destinations `sync: true` so the write lands during the request instead
of on a later flush, removing the race. Test-only; unrelated to this PR's feature
work but it was blocking the PR's CI.

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-02 17:52:20 -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
Cliff Hall
c04f96ffb8
feat(sampling): replace InlineSamplingRequest responseText with draftResult (#1239) (#1250)
Closes the Phase 5 audit partial: the inline card's prop interface now
matches SamplingRequestPanel's, accepting an optional CreateMessageResult
draft instead of a flat scalar. Lets future hook-layer wiring share a
single draft store between the inline card and the full panel.

Rendering:
- No draft → existing layout (request preview, model hints, action
  buttons), with the readonly placeholder textarea removed.
- Text draft → 2-line preview of draftResult.content.text (lineClamp 2).
- Image / audio draft → "[Image content]" / "[Audio content]" label,
  matching the ContentViewer fallback convention.

Updates the three consumer stories that threaded responseText="" through
(PendingClientRequests, ToolsScreen) — no callback signatures changed.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:29:58 -04:00
cliffhall
17d8532396 Address PR #1229 code review feedback
- ToolsScreen.fixtures.ts: replace empty `{}` schemas in query_database
  params and post_request body with explicit `type` for consistency
- ToolControls WithSearch story: add play function that types "git" so
  the story actually demonstrates filtering
- ResourcesScreen stories: comment expandUserProfileTemplate to document
  the userId/readUserProfileState.uri coupling

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:01:48 -04:00
cliffhall
bb8241c2aa Share long tool fixture across ToolControls and InspectorView stories
Extract a 20-tool fixture (filesystem, database, git, http, misc) into
ToolsScreen.fixtures.ts — large enough that the ToolControls list scrolls
and the Tools tab in InspectorView shows realistic surface area.

Wire it into the Default stories of both ToolControls and InspectorView,
mirroring the LoggingScreen.fixtures.ts pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:25:54 -04:00
cliffhall
4c1d2f2c34 Phase 4: Move screen selection state into the screens themselves
ToolsScreen, PromptsScreen, and ResourcesScreen each tracked the user's
current selection via `selectedXxxName` / `onSelectXxx` props, hoisting
screen-local UI state into whatever rendered them. InspectorView never
threaded those callbacks anywhere, so clicks in the controls panel did
not display the matching detail panel — the detail panel only rendered
if a parent fed selection back in.

Move selection into `useState` inside each screen and drop the
`selectedXxxName` / `onSelectXxx` props. MCP-derived data (`callState`,
`getPromptState`, `readState`) keeps flowing in from `InspectorView` as
props. ResourcesScreen wraps `onReadResource` so a template-expanded URI
becomes the selected resource for preview rendering.

Update each screen's stories to drop the deleted props. Stories that
need a pre-selected item use a `play` function to click it — no
component coupling to Storybook.

Add Phase 4.3 to the interfaces plan describing the rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:27:22 -04:00
cliffhall
513582ad50 Add sampling and elicitation modal stories to ToolsScreen
Add three new stories showing sampling/elicitation flows overlaid
on the ToolsScreen via Mantine Modal:

- WithSamplingModal: SamplingRequestPanel in a modal during tool execution
- WithElicitationModal: ElicitationFormPanel in a modal during tool execution
- WithPendingRequests: PendingClientRequests with inline sampling and
  elicitation cards in a modal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 20:13:17 -04:00
cliffhall
2d66f8cb86 Add WithError story to ToolsScreen for long error text wrapping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 17:35:04 -04:00
cliffhall
16f4d9b50f Address PR #1212 code review feedback
- Fix hasAnyAnnotation checking annotations.title which is never rendered
- Reset formValues when switching tools via onSelectTool handler
- Extract ToolProgress type from ToolDetailPanel, reuse in ToolsScreen
- Fix contradictory readOnlyHint + destructiveHint in story fixture
- Replace listMaxHeight function with LIST_MAX_HEIGHT constant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 17:12:02 -04:00
cliffhall
92ecfcbd6c Phase 2: Refactor Tools cluster to use MCP SDK types
Replace ad hoc local types with MCP SDK schema types across the Tools
group components and ToolsScreen, per the v2 UX interfaces plan.

- ToolListItem: accept `tool: Tool` instead of flat `name`/`title`
- ToolControls: accept `Tool[]` + `selectedName` instead of `ToolListItemProps[]`
- ToolDetailPanel: accept `tool: Tool` instead of flat props; use SDK
  `ToolAnnotations` (readOnlyHint/destructiveHint/idempotentHint/openWorldHint);
  use `ProgressNotification['params']` for progress
- ToolResultPanel: accept `CallToolResult` instead of `ResultContentItem[]`
- ToolsScreen: accept `Tool[]` + `selectedToolName` + `ToolCallState`;
  derive selected tool via `tools.find()`; own `formValues` state locally
- Delete local `ToolAnnotations`, `ResultContentItem` types
- Update all stories with real SDK `Tool` fixtures
- Update ConnectedView ToolsActive story

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 16:57:10 -04:00
cliffhall
064b807ea4 Move search state into ToolControls and PromptControls, search response in history
- ToolControls and PromptControls now manage searchText as local state
  and filter their lists internally (matches on name + title/description)
- Removed searchText/onSearchChange props from ToolsScreen, PromptsScreen,
  and their associated stories
- HistoryListPanel search now also matches against stringified response
  payloads

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 16:45:36 -04:00
cliffhall
5594e940c9 Inline ListChangedIndicator with titles, normalize screen gaps, and rename ResultPanel to ToolResultPanel
- Place ListChangedIndicator on same line as section titles using Group with space-between in ToolControls, ResourceControls, and PromptControls
- Add ListChanged stories for ResourceControls and show indicators in ConnectedView stories
- Normalize ScreenLayout gap from "xl" to "md" across all screens for consistent spacing
- Rename ResultPanel to ToolResultPanel (component, interface, files, and stories)
- Replace Grid layout in ToolsScreen with equal-width flex children for detail and result panels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:47:41 -04:00
cliffhall
a6724626b7 Normalize screen layouts: extract control groups, unify sidebar widths, and refactor PromptsScreen
- Extract ToolControls group from ToolsScreen (search, list, list-changed indicator)
- Extract PromptControls and PromptListItem groups for PromptsScreen sidebar
- Refactor PromptsScreen to sidebar + detail layout (matching ResourcesScreen pattern)
- Refactor PromptArgumentsForm to accept a single prompt instead of a dropdown
- Normalize all screen sidebar widths to 340px (Tools, Resources, Prompts, Logs, Tasks, History)
- Convert ToolsScreen from Grid-only to sidebar + Grid layout
- Replace LogControls checkboxes with full-width toggle buttons
- Move Clear/Export buttons from LogControls to LogStreamPanel toolbar
- Make PromptArgumentsForm "Get Prompt" button right-aligned and compact
- Update all affected stories and ConnectedView

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:54:20 -04:00
cliffhall
dd46acba87 Refactor renamed the component groups to something less obtuse:
Elements -> Groups -> Screens -> Views
2026-03-28 12:58:47 -04:00