Repository navigation
TUI: keep the test and roots forms inert under a pending request overlay - #2975
Merged
Merged
Conversation
…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>
There was a problem hiding this comment.
🟡 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-formtests 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>
Member
Author
|
Copilot round 1: 1 finding, fixed.
|
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Closes #2967
Problem
While a sampling or elicitation request is open in
PendingRequestModal, the tool, resource and prompt test modals gated their ownuseInput, but theink-form<Form>underneath was not gated.ink-formexposes noisActive: every field, the section tabs and the arrow-key focus moves are its ownuseInput/useFocushooks, 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
clients/tui/src/components/GatedForm.tsx. It wrapsink-form's<Form>with anactiveprop. Ink cannot mask stdin for a subtree, so "inert" means "not mounted": whileactiveis false the<Form>is not rendered, so none of its handlers or focusables exist. The draft survives because the form runs inink-form's controlled mode, with its value held inGatedForm, which stays mounted. The value is seeded from each field'sinitialValue, which is what the uncontrolled form did on mount, so a submit carries the same defaults as before.ToolTestModal,ResourceTestModal,PromptTestModalrenderGatedForm active={inputActive}. Theirif (!inputActive) return;submit guards are removed: a form that is not mounted cannot submit.RootsModalgets 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.tsxruns the realink-form, not the double the modal suites use, because the bug lives in ink-form's own handlers: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 ↓ Enter14AEnter (fill Seat) and ↓ Enter (Submit).After: draft filled before the request arrives

After: request open above the form

After: Seat filled in the overlay

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

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.🤖 Generated with Claude Code