Skip to content

feat(tui): migrate forms to Ink useFocus / useFocusManager - #251

Merged
kelsonpw merged 2 commits into
mainfrom
feat/tui-useFocus-form-migration
Apr 25, 2026
Merged

feat(tui): migrate forms to Ink useFocus / useFocusManager#251
kelsonpw merged 2 commits into
mainfrom
feat/tui-useFocus-form-migration

Conversation

@kelsonpw

@kelsonpw kelsonpw commented Apr 25, 2026

Copy link
Copy Markdown
Member

Summary

Audit task #14 follow-up. Replaces the hand-rolled focus state in ConfirmationInput with Ink's native useFocus / useFocusManager so 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 useState and only listened for arrow keys, so Tab did nothing. Ink's focus manager handles Tab / Shift-Tab cycling automatically once components opt in via useFocus, so the migration is a small, behaviour-preserving change with a real UX upgrade.

What changed

  • src/ui/tui/primitives/ConfirmationInput.tsx — replace useState<FocusTarget> with useFocus per option and useFocusManager for arrow-key cycling. Each option becomes a focusable child with a stable id derived from a new optional idPrefix prop, so multiple instances can coexist on a screen and tests can target specific fields. Each focused option owns its own Enter handler (gated by isActive: isFocused), so we never double-fire onConfirm.
  • src/ui/tui/screens/SettingsOverrideScreen.tsx — pass idPrefix="settings-override" to the embedded ConfirmationInput. The screen has no hand-rolled focus state of its own (the only useState here is for the feedback error 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, or src/commands/*. Other consumers of ConfirmationInput (LogoutScreen, OutageScreen, SlackScreen, McpScreen) inherit the new Tab-cycle behaviour for free without source edits — the prop API is fully backward compatible (idPrefix is optional and defaults to 'confirmation-input').

Test plan

  • pnpm tsc --noEmit — clean
  • pnpm test — 1237 passed / 17 skipped, no regressions
  • pnpm lint — prettier + eslint clean
  • pnpm build — compiles, smoke test passes
  • Reviewed every ConfirmationInput callsite (SettingsOverrideScreen, LogoutScreen, OutageScreen, SlackScreen, McpScreen) — props compatible, behaviour preserved
  • Manual smoke: in a pnpm try session, trigger an overlay that uses ConfirmationInput (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 ConfirmationInput plus one callsite update.

Overview
Updates ConfirmationInput to use Ink’s useFocus/useFocusManager instead of local focus state, enabling Tab / Shift-Tab focus cycling while preserving Up/Down arrow navigation.

Refactors confirm/cancel into focusable Option children with per-option Enter handling (active only when focused), adds an optional idPrefix for stable focus ids, and ensures Escape re-acquires focus before invoking onCancel.

Updates SettingsOverrideScreen to pass a unique idPrefix to its ConfirmationInput instance.

Reviewed by Cursor Bugbot for commit 512b8da. Bugbot is set up for automated code reviews on this repo. Configure here.

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>
@kelsonpw
kelsonpw requested a review from a team as a code owner April 25, 2026 19:36
@kelsonpw
kelsonpw requested a review from a team April 25, 2026 19:36
@github-actions

Copy link
Copy Markdown
Contributor

🧙 Wizard CI

Run 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:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci django
  • /wizard-ci fastapi
  • /wizard-ci flask
  • /wizard-ci javascript-node
  • /wizard-ci javascript-web
  • /wizard-ci next-js
  • /wizard-ci python
  • /wizard-ci react-router
  • /wizard-ci vue

Test an individual app:

  • /wizard-ci django/django3-saas
  • /wizard-ci fastapi/fastapi3-ai-saas
  • /wizard-ci flask/flask3-social-media
Show more apps
  • /wizard-ci javascript-node/express-todo
  • /wizard-ci javascript-node/fastify-blog
  • /wizard-ci javascript-node/hono-links
  • /wizard-ci javascript-node/koa-notes
  • /wizard-ci javascript-node/native-http-contacts
  • /wizard-ci javascript-web/saas-dashboard
  • /wizard-ci next-js/15-app-router-saas
  • /wizard-ci next-js/15-app-router-todo
  • /wizard-ci next-js/15-pages-router-saas
  • /wizard-ci next-js/15-pages-router-todo
  • /wizard-ci python/meeting-summarizer
  • /wizard-ci react-router/react-router-v7-project
  • /wizard-ci react-router/rrv7-starter
  • /wizard-ci react-router/saas-template
  • /wizard-ci react-router/shopper
  • /wizard-ci vue/movies

Results will be posted here when complete.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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 before onCancel() in the Escape handler to re-acquire focus after Ink's internal focus manager clears activeFocusId, ensuring Enter remains functional if the component stays mounted.

Create PR

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.

Comment thread src/ui/tui/primitives/ConfirmationInput.tsx
@kelsonpw

Copy link
Copy Markdown
Member Author

@cursor push 3a45ce2

@kelsonpw
kelsonpw merged commit 77e8f46 into main Apr 25, 2026
10 checks passed
@kelsonpw
kelsonpw deleted the feat/tui-useFocus-form-migration branch April 25, 2026 23:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants