feat(tui): migrate forms to Ink useFocus / useFocusManager - #251
Merged
Conversation
Replace the hand-rolled focus state in ConfirmationInput with Ink's native useFocus / useFocusManager so Tab / Shift-Tab cycle between fields the way terminal users expect. Up / Down arrows still work, Enter still triggers the focused option, and Escape still cancels. Each option is a focusable child with a stable id derived from an optional idPrefix prop, so multiple ConfirmationInput instances can coexist on a screen and tests can target specific fields. The SettingsOverrideScreen passes idPrefix="settings-override". Audit task #14 follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
🧙 Wizard CIRun the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands: Test all apps:
Test all apps in a directory:
Test an individual app:
Show more apps
Results will be posted here when complete. |
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Ink Escape handler silently resets focus state
- Added a
focus(confirmId)call beforeonCancel()in the Escape handler to re-acquire focus after Ink's internal focus manager clearsactiveFocusId, ensuring Enter remains functional if the component stays mounted.
- Added a
Or push these changes by commenting:
@cursor push 3a45ce2b15
Preview (3a45ce2b15)
diff --git a/src/ui/tui/primitives/ConfirmationInput.tsx b/src/ui/tui/primitives/ConfirmationInput.tsx
--- a/src/ui/tui/primitives/ConfirmationInput.tsx
+++ b/src/ui/tui/primitives/ConfirmationInput.tsx
@@ -62,8 +62,11 @@
cancelLabel = 'Cancel [Esc]',
idPrefix = 'confirmation-input',
}: ConfirmationInputProps) => {
- const { focusNext, focusPrevious } = useFocusManager();
+ const { focusNext, focusPrevious, focus } = useFocusManager();
+ const confirmId = `${idPrefix}-confirm`;
+ const cancelId = `${idPrefix}-cancel`;
+
// Parent owns Escape + arrow-key cycling. Tab / Shift-Tab is handled
// automatically by Ink's focus manager.
useScreenInput((_input, key) => {
@@ -76,13 +79,15 @@
return;
}
if (key.escape) {
+ // Ink's focus manager clears activeFocusId on Escape before this
+ // handler runs, leaving both Options unfocused (Enter inert).
+ // Re-acquire focus so the component stays interactive if onCancel
+ // doesn't unmount it.
+ focus(confirmId);
onCancel();
}
});
- const confirmId = `${idPrefix}-confirm`;
- const cancelId = `${idPrefix}-cancel`;
-
return (
<Box flexDirection="column">
<PromptLabel message={message} />You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 3fa4355. Configure here.
Member
Author
… state Applied via @cursor push command
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.


Summary
Audit task #14 follow-up. Replaces the hand-rolled focus state in
ConfirmationInputwith Ink's nativeuseFocus/useFocusManagerso terminal users get the cycling behaviour they expect: Tab / Shift-Tab now move between options, on top of the existing Up / Down arrows. Enter still triggers the focused option, Escape still cancels, and every visible label, color, and icon stays exactly where it was.Why this matters
Terminal users press Tab to move between fields. The previous implementation tracked focus in a local
useStateand only listened for arrow keys, so Tab did nothing. Ink's focus manager handles Tab / Shift-Tab cycling automatically once components opt in viauseFocus, so the migration is a small, behaviour-preserving change with a real UX upgrade.What changed
src/ui/tui/primitives/ConfirmationInput.tsx— replaceuseState<FocusTarget>withuseFocusper option anduseFocusManagerfor arrow-key cycling. Each option becomes a focusable child with a stable id derived from a new optionalidPrefixprop, so multiple instances can coexist on a screen and tests can target specific fields. Each focused option owns its own Enter handler (gated byisActive: isFocused), so we never double-fireonConfirm.src/ui/tui/screens/SettingsOverrideScreen.tsx— passidPrefix="settings-override"to the embeddedConfirmationInput. The screen has no hand-rolled focus state of its own (the onlyuseStatehere is for thefeedbackerror string), so this is the full surface area for it.Files explicitly NOT touched
Multiple parallel PRs are open against neighbouring TUI files (#234, #235, #166, #156, #142, #138, #137, #136, #135, #169, #149, #246, #243, #244, #248). To keep this PR rebase-friendly, I deliberately did not modify:
store.ts,router.ts,AuthScreen,IntroScreen,OutroScreen,McpScreen,CreateProjectScreen,DataIngestionCheckScreen,DataSetupScreen,LogoutScreen,SignupEmailScreen,SignupFullNameScreen,SigningUpScreen,RegionSelectScreen,HeaderBar,ConsoleView,PickerMenu,LogViewer,ProgressList,bin.ts, orsrc/commands/*. Other consumers ofConfirmationInput(LogoutScreen,OutageScreen,SlackScreen,McpScreen) inherit the new Tab-cycle behaviour for free without source edits — the prop API is fully backward compatible (idPrefixis optional and defaults to'confirmation-input').Test plan
pnpm tsc --noEmit— cleanpnpm test— 1237 passed / 17 skipped, no regressionspnpm lint— prettier + eslint cleanpnpm build— compiles, smoke test passesConfirmationInputcallsite (SettingsOverrideScreen,LogoutScreen,OutageScreen,SlackScreen,McpScreen) — props compatible, behaviour preservedpnpm trysession, trigger an overlay that usesConfirmationInput(e.g./logout) and verify Tab / Shift-Tab cycle between options while Up/Down/Enter/Escape continue to work🤖 Generated with Claude Code
Note
Medium Risk
Touches interactive keyboard handling in the TUI, so regressions could break confirm/cancel selection or focus behavior in prompts. Scope is small and localized to
ConfirmationInputplus one callsite update.Overview
Updates
ConfirmationInputto use Ink’suseFocus/useFocusManagerinstead of local focus state, enabling Tab / Shift-Tab focus cycling while preserving Up/Down arrow navigation.Refactors confirm/cancel into focusable
Optionchildren with per-option Enter handling (active only when focused), adds an optionalidPrefixfor stable focus ids, and ensures Escape re-acquires focus before invokingonCancel.Updates
SettingsOverrideScreento pass a uniqueidPrefixto itsConfirmationInputinstance.Reviewed by Cursor Bugbot for commit 512b8da. Bugbot is set up for automated code reviews on this repo. Configure here.