* 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>
* feat(web): two-step URL elicitation completion (#1415)
Replace accept-on-open with an explicit completion step for URL
elicitations. Opening the URL now only transitions the panel to its
waiting state; the elicitation resolves with `accept` only when the user
clicks the revealed "I've completed it" action, or with `cancel`.
- ElicitationUrlPanel: add `onComplete`, reveal the completion button and
relabel open→"Reopen in Browser" while `isWaiting`.
- PendingClientRequestModal: wire local `isWaiting`; open no longer sends
a response. Removed the accept-on-open comment that pointed here.
- Tests cover open→waiting→complete and open→cancel, and assert opening
alone does not resolve the elicitation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): handle URL-elicitation-required (-32042) error path
Implements the spec's "URL mode with elicitation required error" flow
(2025-11-25). When a tools/call returns a `-32042`
UrlElicitationRequired error, the client now surfaces the required URL
elicitation(s) and retries the call once completed, instead of showing a
bare error.
Core (InspectorClient.callTool):
- Detect `-32042` (typed UrlElicitationRequiredError or generic McpError)
via the new `getUrlElicitationsFromError` helper.
- Surface each carried elicitation in order through the existing pending-
elicitation queue / modal (reusing the two-step "Open → I've completed
it" UI), then re-issue the original call once all are accepted. A
decline/cancel aborts with a clear message; a bounded retry count guards
a server that keeps returning the error.
- `ElicitationCreateMessage.completeIfPending()` lets the optional
`notifications/elicitation/complete` auto-advance/auto-accept an open
URL elicitation; the completion-notification handler now resolves
(was: only removed) the pending entry.
- Extracted `attemptToolCall` / `dispatchFailedToolCall` from the old
callTool body to support the retry loop without duplicating bookkeeping.
Web (App):
- A non-spec `-32042` with no elicitations (nothing to open) shows a
dedicated yellow toast linking to a new UrlElicitationErrorModal with
the raw error body, mirroring the output-schema-validation toast.
Tests: helper unit tests, completeIfPending unit tests, callTool
error-path retry/cancel/no-list/ordinary-error tests (injected fake
client), the new modal (test + stories), and an App toast test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): abort URL-elicitation error path on a repeated URL
Guard the -32042 retry loop: if the server's retry error re-requests a
URL the user already completed earlier in the same call, completing it
again can't make progress, so callTool aborts with a new typed
UrlElicitationLoopError instead of re-prompting (which would loop until
the retry cap). The web layer detects it and shows a "URL elicitation
loop" toast naming the repeated URL rather than a generic failure.
Tests: core abort-on-repeat test (URL presented once, two attempts, then
loop error) and an App loop-toast test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): let Copy-URL reach the URL-elicitation completion step
Address review of #1436: the "I've completed it" action was gated behind
clicking "Open in Browser" (only that set isWaiting), so a user who used
Copy URL to finish the flow externally could only Cancel. Copying the URL
now also moves the panel into the waiting state, exposing the completion
step. Also trims the now-redundant second sentence of the waiting hint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(core): address review of error-path URL elicitation
Review follow-ups on #1436 (all non-blocking):
- Remove the leftover bare block scope in attemptToolCall (extraction
artifact); body de-indented one level.
- Log a warning when callTool gives up after the URL-elicitation retry
cap is hit, so a server that keeps demanding new elicitations is
diagnosable rather than looking like an ordinary failure.
- Assert the dispatch-once invariant on the loop-abort and cancel-abort
paths (exactly one failed toolCallResultChange event).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(core): fix attemptToolCall body indentation
Run prettier over the extracted attemptToolCall method body, which was left
over-indented by 2 spaces after the bare-block removal in 0754b43d. core/ is
outside clients/web's format:check scope, so npm run validate did not catch it.
Whitespace-only; the pre-existing line 658 wrap (also outside the format scope)
is left as-is, out of scope for this PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): settle pending elicitations on disconnect to avoid callTool hang
Address review of #1436: an error-path `awaitUrlElicitation` promise only
settled via respond, but `disconnect()` cleared `pendingElicitations`
wholesale — so a disconnect mid-elicitation (server crash, manual
disconnect, transport close) left `callTool` (and the UI's in-flight
toolCallState) hanging forever.
- Add `ElicitationCreateMessage.cancel()`: settles a pending elicitation
as `{ action: "cancel" }` without calling `onRemove` (the queue is
cleared by the caller, so it must not splice mid-iteration).
- `disconnect()` now cancels each pending elicitation before clearing the
array, and dispatches `pendingElicitationsChange` (it already did so for
samples) so the modal closes on a transport-close teardown too.
Tests: disconnect-mid-elicitation settles callTool (no hang); cancel()
unit tests (resolves-without-remove, no-op once responded).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>