Skip to content

TUI: keep the test and roots forms inert under a pending request overlay - #2975

Merged
cliffhall merged 2 commits into
v2/mainfrom
v2/fix/2967-gate-forms-under-overlay
Nov 12, 2026
Merged

cliffhall merged 2 commits into
v2/mainfrom
v2/fix/2967-gate-forms-under-overlay

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #2967

Problem

While a sampling or elicitation request is open in PendingRequestModal, the tool, resource and prompt test modals gated their own useInput, but the ink-form <Form> underneath was not gated. ink-form exposes no isActive: every field, the section tabs and the arrow-key focus moves are its own useInput/useFocus hooks, and Ink's focus manager is shared with the overlay's form. Keys typed into the overlay therefore also moved focus and could edit values in the hidden form.

Change

  • New clients/tui/src/components/GatedForm.tsx. It wraps ink-form's <Form> with an active prop. Ink cannot mask stdin for a subtree, so "inert" means "not mounted": while active is false the <Form> is not rendered, so none of its handlers or focusables exist. The draft survives because the form runs in ink-form's controlled mode, with its value held in GatedForm, which stays mounted. The value is seeded from each field's initialValue, which is what the uncontrolled form did on mount, so a submit carries the same defaults as before.
  • ToolTestModal, ResourceTestModal, PromptTestModal render GatedForm active={inputActive}. Their if (!inputActive) return; submit guards are removed: a form that is not mounted cannot submit.
  • RootsModal gets the same treatment. Its add-root form had the identical gap and the identical "ink-form still sees its keys" guard, so leaving it out would leave the same bug in a fourth place.

One limit, stated in the component header: ink-form's own view state is not kept across the toggle. That covers which tab and field had focus, and an edit still open in a field (not yet committed with Enter), which is discarded the way Esc discards it. Committed values, which are everything a submit sends, are kept exactly.

Tests

clients/tui/__tests__/GatedForm.test.tsx runs the real ink-form, not the double the modal suites use, because the bug lives in ink-form's own handlers:

  • defaults are submitted as before;
  • with a committed edit in place, the gated form shows a paused line and ignores focus moves, edit/commit keys and a submit; when it is re-activated the draft is exactly as left, and the next submit sends it.

I checked that the second test fails against a variant that keeps <Form> mounted while gated: the gated keys submit the hidden form. The existing modal suites' inputActive={false} cases (Enter does not submit) still pass.

Screenshots

Captured from the built TUI in a pseudoterminal against a scratch stdio server. The server has a book_trip(city, note) tool and sends an elicitation unprompted a few seconds after connecting, which is the trigger the issue describes. The same key script was used for every shot: fill the form, wait for the request, then in the overlay press ↓ Enter 14A Enter (fill Seat) and ↓ Enter (Submit).

After: draft filled before the request arrives
after-1-draft

After: request open above the form
after-2-overlay-open

After: Seat filled in the overlay
after-3-overlay-typed

After: ↓ Enter submitted the request and the form returns with its draft intact
after-4-form-after

Before (origin/v2/main), same keys: ↓ Enter never reaches the overlay's Submit. Focus left the overlay for the hidden form's fields, so no field and no button in the overlay is highlighted, and the request stays open.
before-4-form-after

🤖 Generated with Claude Code

…pen (#2967)

ink-form gives no way to gate its own useInput/useFocus hooks, so a key
typed into a sampling or elicitation overlay also moved focus and edited
values in the tool, resource, prompt or roots form underneath it.
GatedForm runs the form in ink-form's controlled mode with its value held
in a component that stays mounted, and unmounts the Form itself while the
overlay owns the keyboard, so nothing underneath sees a key and the draft
returns intact.

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

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

Form defaults for __proto__ arguments are not preserved because assignment to a plain object invokes its prototype setter instead of creating an own property, causing those values to be lost during serialization and decoding.

1 open issue
What changed in this PR

This PR addresses #2967 by preventing underlying TUI forms from receiving keyboard input while a sampling or elicitation request overlay is open.

Changes:

  • Adds GatedForm, which unmounts inactive forms while preserving committed values; focus and uncommitted edits are not retained.
  • Applies the wrapper to tool, resource, prompt, and roots forms.
  • Adds real ink-form tests for defaults, inactive input handling, and draft restoration.
File Description
clients/​tui/​src/​components/​ToolTestModal.tsx Gates the tool argument form through GatedForm.
clients/​tui/​src/​components/​RootsModal.tsx Gates the add-root form during pending requests.
clients/​tui/​src/​components/​ResourceTestModal.tsx Gates the resource template form.
clients/​tui/​src/​components/​PromptTestModal.tsx Gates the prompt argument form.
clients/​tui/​src/​components/​GatedForm.tsx Adds controlled draft storage and conditional form mounting.
clients/​tui/​__tests__/​GatedForm.test.tsx Tests defaults, input isolation, and committed draft preservation.

Review effort: Balanced

…stays an own value (#2967)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall
cliffhall requested a balanced review from Copilot November 12, 2026 06:13
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 1: 1 finding, fixed.

  • __proto__ defaults were not kept as own values in initialFormValues. Fixed in c5dbafd by switching to Object.fromEntries, with a test.

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 are reported for the change that prevents hidden TUI forms from receiving keystrokes while request overlays are open.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review loop closed. Round 2 was clean, with no inline comments, no headline findings and no suppressed comments. That is the clean-round exit, after 2 rounds.

@cliffhall
cliffhall merged commit d10b08b into v2/main Nov 12, 2026
12 checks passed
@cliffhall
cliffhall deleted the v2/fix/2967-gate-forms-under-overlay branch November 12, 2026 06:29
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: tool, resource and prompt forms still receive keys while a sampling or elicitation overlay is open

2 participants