Skip to content

TUI: handle sampling and elicitation requests from the server - #2903

Merged
cliffhall merged 4 commits into
v2/mainfrom
v2/feat/2829-tui-sampling-elicitation
Nov 5, 2026
Merged

cliffhall merged 4 commits into
v2/mainfrom
v2/feat/2829-tui-sampling-elicitation

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #2829

What changed

The TUI now answers server-initiated sampling/createMessage and elicitation/create requests. Before this change, a tool that asked the user something hung forever. Core already advertised sampling and form elicitation by default, but nothing in the TUI rendered the queue.

  • PendingRequestModal (new, clients/tui/src/components/) is a full-screen modal that App renders above every other overlay. It reads core's pending-request queue through usePendingClientRequests, the same plumbing the web client's PendingClientRequestModal uses, and shows one request at a time, sampling first.
    • Sampling shows the messages, system prompt, token budget and model hints above an ink-form response form (text, model pre-filled with stub-model, stop reason). Submitting the form approves the request. Esc then r rejects it.
    • Form elicitation builds its form from requestedSchema with the tool modal's schemaToForm. Titled enums (oneOf/anyOf of {const, title}) are normalized to selects, and a multi-select is sent as an array. Missing required fields are reported instead of sent. Submitting accepts. Esc then d declines, and Esc then c cancels. On a task-input-required request, cancel cancels the task, as on web (Tasks: io.modelcontextprotocol/tasks extension on modern servers (era fork) #1631).
    • URL elicitation: o opens the URL in the browser (http/https only, the same allowlist web uses), Enter accepts, d declines and Esc cancels. URL-mode requests can arrive through the -32042 error path whatever the advertised capabilities, so they are handled here.
    • ink-form owns Esc while a field is being edited. So in the two form modes Esc opens a one-line dismiss prompt instead of settling the request, and any other key returns to the form. The form stays mounted under the prompt, so nothing typed is lost.
  • App now states sample: true, elicit: { form: true, url: true } instead of relying on core's defaults, which advertise form elicitation only. While a request is open, the global accelerators and every pane stand down (focus becomes "overlay", renamed from "help"). The six other overlays (tool, resource and prompt test modals, details, help, roots) take a new inputActive prop, so Esc meant for the request does not also close the tool modal underneath.
  • The pure helpers are in clients/tui/src/utils/pendingRequests.ts.
  • README: a Features entry for the new modal.

Tests

  • __tests__/pendingRequests.test.ts covers result building, request description, enum normalization, content decoding and missing-field reporting, and the URL allowlist.
  • __tests__/PendingRequestModal.test.tsx covers every settle path for all three kinds, the once-guard, the back-out-on-any-key prompt, task cancel, and the queue order.
  • __tests__/App.test.tsx covers the advertised capabilities, the request modal taking Esc over an open tool modal without closing it, and the global accelerators going inert.

npm run local:gate passes.

Screenshots

These were captured from the built TUI driven in a pseudoterminal against a composable stdio test server (collect_sample, submit_ticket). The missing first letter of the tool input in the sampling shots ("ummarize") comes from the capture harness typing faster than ink-text-input re-renders. It is not part of this change.

Sampling request, with the response filled in:
tui-sampling-request

The tool result after approving:
tui-sampling-result

Form elicitation (submit_ticket):
tui-elicitation-request

The Esc dismiss prompt:
tui-elicitation-dismiss-prompt

The tool result after accepting:
tui-elicitation-result

🤖 Generated with Claude Code

@cliffhall cliffhall added the v2 Issues and PRs for v2 label Nov 5, 2026
@cliffhall cliffhall linked an issue Nov 5, 2026 that may be closed by this pull request
2 tasks done
@cliffhall
cliffhall requested a balanced review from Copilot November 5, 2026 06:52

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.

🟡 Changes recommended

Unresolved input-handling bugs can trigger unintended tool calls or approvals and block recovery after cancellation failures, while elicitation handling can omit answers or submit invalid arrays.

5 open issues
What changed in this PR

Adds TUI handling for server-initiated sampling and elicitation requests, using core’s pending-request queue so terminal users can respond without switching to the web client.

Changes:

  • Adds response forms, dismissal controls, URL elicitation handling, and task cancellation.
  • Advertises sampling and both elicitation modes, and gates surrounding keyboard handlers while requests are open.
  • Adds helper, modal, and App tests plus feature documentation.
File Description
clients/​tui/​src/​utils/​pendingRequests.ts Builds forms, response content, request summaries, and URL checks.
clients/​tui/​src/​components/​ToolTestModal.tsx Adds keyboard-handler activation control.
clients/​tui/​src/​components/​RootsModal.tsx Gates root-editor keyboard handling.
clients/​tui/​src/​components/​ResourceTestModal.tsx Adds keyboard-handler activation control.
clients/​tui/​src/​components/​PromptTestModal.tsx Adds keyboard-handler activation control.
clients/​tui/​src/​components/​PendingRequestModal.tsx Presents and answers queued sampling and elicitation requests.
clients/​tui/​src/​components/​HelpOverlay.tsx Gates help navigation and dismissal.
clients/​tui/​src/​components/​DetailsModal.tsx Gates details-overlay keyboard handling.
clients/​tui/​src/​App.tsx Advertises capabilities and integrates the request overlay.
clients/​tui/​README.md Documents request handling and keyboard controls.
clients/​tui/​__tests__/​pendingRequests.test.ts Tests schema conversion, response building, and URL checks.
clients/​tui/​__tests__/​PendingRequestModal.test.tsx Tests response actions, queue priority, and task cancellation.
clients/​tui/​__tests__/​App.test.tsx Tests capabilities and overlay keyboard coordination.

Review effort: Balanced

@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review, round 1: 5 findings. 4 fixed in 1a3d251, 1 declined. npm run local:gate passes.

  • Failed cancellation left the request blocked: fixed. A failed settle now releases the guard.
  • Esc then Enter submitted the form: fixed. Submission is blocked while the dismiss prompt is open and on the key that closes it.
  • Inactive modal forms still consumed input: fixed for submission. The tool, prompt, resource and roots forms no longer submit while a request holds input. Keystrokes into the hidden draft are kept on purpose.
  • A __proto__ field was lost: fixed with Object.fromEntries.
  • Multi-select array fields could not be answered fully: declined as out of scope and filed as TUI: support multi-select (array) fields in ink-form forms #2904. This is the same tool-form limitation that tool arguments have.

Nothing was in the Suppressed comments block. Requesting round 2.

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.

🔵 Needs a closer look

Long elicitation messages can overflow the modal and hide the response fields, Submit button, and dismissal prompt, so the message area needs bounded scrolling that preserves space for the controls.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Bound and scroll long elicitation messages

clients/​tui/​src/​components/​PendingRequestModal.tsx:363

A long elicitation message can hide the entire response form. For example, a 24-line message on a 24-row terminal consumes more than the modal's available body height: this box cannot shrink, and ModalFrame clips overflow, so the fields, Submit button, and Esc dismissal prompt end up outside the visible area. Please bound the message area and make it scrollable while reserving space for the response controls.

5 resolved since last review

Review effort: Balanced

@cliffhall
cliffhall force-pushed the v2/feat/2829-tui-sampling-elicitation branch from 1a3d251 to 0ad3c29 Compare November 5, 2026 08:21
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review, round 2: no inline comments. One "previously missed" finding, which has no thread, so it is answered here.

  • A long elicitation message could hide the form, Submit and the dismiss prompt: fixed in 0ad3c29. The message now sits in a box capped at a third of the terminal (messageBoxHeight, with unit tests) and scrolls with PgUp/PgDn, so the response controls always keep their space.

The branch is also rebased onto the current v2/main (which includes #2845), and npm run local:gate passes. Requesting round 3.

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.

🟡 Changes recommended

Long URL-elicitation messages can push the destination outside the modal’s hidden viewport, with no keyboard scrolling to inspect or copy it when browser launch is unavailable.

1 open issue

Review effort: Balanced

cliffhall and others added 4 commits November 5, 2026 03:28
Advertise sampling and form + URL elicitation explicitly, and render a
pending server request in a full-screen PendingRequestModal driven by
core's pending-request queue (usePendingClientRequests), the same
plumbing the web client uses. Sampling shows the request and a response
form; form elicitation reuses schemaToForm; URL elicitation opens the
link and settles on Enter/d/Esc. Other overlays and panes go inert while
a request is open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
- Release the settle guard when a settle fails (a failed task cancel
  leaves the request queued), so the user can retry or answer.
- Block form submission while the Esc dismiss prompt is open and on the
  keypress that closes it.
- Hold Enter from submitting a tool/prompt/resource/roots form while a
  pending request has their input.
- Build elicitation content with Object.fromEntries so a __proto__ field
  survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
)

A long message could push the response form, Submit and the dismiss
prompt out of the modal. Cap the message box at a third of the terminal
(messageBoxHeight) and scroll it with PgUp/PgDn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
…2829)

Show the URL first and bound the message under it (messageBoxHeight),
scrolled with PgUp/PgDn, so a long message cannot push the destination
out of the modal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall
cliffhall force-pushed the v2/feat/2829-tui-sampling-elicitation branch from 0ad3c29 to 7c5d702 Compare November 5, 2026 08:44
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review, round 3: 1 finding, fixed in 7c5d702. A long URL-elicitation message could push the URL out of view. The URL now renders first, and the message is bounded and scrolls with PgUp/PgDn. The branch is rebased onto current v2/main, and npm run local:gate passes. Requesting round 4.

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.

🟢 Approval recommended

No active review issues remain for the terminal sampling and elicitation changes.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review closed: round 4 was clean (no inline comments, no headline finding, no suppressed comments), so the loop has ended on its first clean round. Over 4 rounds there were 8 findings: 7 fixed, 1 declined and filed as #2904.

@cliffhall
cliffhall merged commit dd5c366 into v2/main Nov 5, 2026
11 checks passed
@cliffhall
cliffhall deleted the v2/feat/2829-tui-sampling-elicitation branch November 5, 2026 08:59
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.

TUI: handle sampling and elicitation requests from the server

2 participants