Summary
Dismissing an overlay (dialog, command palette, find bar) always returns focus to the editor, regardless of what was focused before the overlay opened. When the integrated terminal was focused, the next keystrokes meant for the shell land in the editor buffer instead — silently modifying (and, on save, corrupting) the open file.
From the 2026-07-12 UX bug audit: BUG-057 (+ closely coupled BUG-058). Re-verified on main @ b93468d.
Repro A — dialog dismiss steals focus from the terminal
bin/ttt --exec 'wait 300; key ctrl+t; wait 500; key ctrl+p; wait 300; key escape; wait 200; type XY; debug /tmp/d.json; quit' f.txt
f.txt = hello world → buffer becomes XYhello world, and the tab isn't even flagged modified in the dump.
Repro B — force key toggles a panel through a modal overlay, then dismiss corrupts
bin/ttt --exec 'wait 300; key ctrl+t; wait 500; type "echo hi"; key ctrl+p; wait 200; key ctrl+t; wait 200; key escape; wait 200; type ZZ; quit' f.txt
ctrl+t → terminal open + focused
ctrl+p → command palette (modal overlay)
ctrl+t → the terminal.toggle force key fires while the palette is still open (BUG-058), hides the bottom panel, and calls FocusEditor() behind the overlay
esc → palette dismissed
- typing →
ZZhello world
Root cause
Two coupled issues:
1. Hardcoded "return to editor" on every dismiss path. There is no notion of "previous focus". ~10 call sites hardcode a.FocusEditor():
App.DismissDialog() / App.ClosePluginDrawer() — internal/app/app.go:661,666
Root.EscapeFallback — internal/app/commands.go:112
- the find-bar
OnDismiss routes through DismissDialog() — internal/app/commands_search.go:38
- plus
commandline.go:59, commands_view.go:67,92,98, menus.go:219, settings_view.go:256,294, commands_plugin.go:545
2. Non-quit force keys punch through modal overlays. Root.HandleEvent (internal/ui/root.go:191) runs every registered force key while an overlay is open. The in-code comment only justifies this for ctrl+q force-quit; terminal.toggle (and any other force key) mutating background layout under a modal palette is unintended.
Why this is not trivial to fix
A naive "save focus on overlay open, restore on dismiss" is not enough — Repro B shows the saved widget (the terminal panel) can be hidden by the time the overlay is dismissed. Restoring focus to a hidden RawKeyConsumer would be worse than the current bug (keystrokes route to an invisible terminal). The fix is three coupled parts:
- Gate force keys on modal overlays (BUG-058). Only force-key commands explicitly flagged "works through modals" (quit) should fire while a modal overlay is open. This removes the class of "background state shifts under the saved focus".
- Validated focus-restore. Save
Root.Focused when the first modal overlay opens; on dismiss, SetFocus(saved) only if the widget is still mounted / visible / focusable (terminal panel ShowBottom, sidebar Visible, plugin panel still present) — otherwise fall back to FocusEditor().
- Shared helper.
DismissDialog, EscapeFallback, and the find-bar / menu OnDismiss paths all call a single a.RestoreFocus() instead of hardcoding FocusEditor(). A parallel focus stack (mirroring Root.Overlays) is needed because dismiss handlers chain (dismiss one dialog, open another).
Also note: some overlays are pushed via raw Root.PushOverlay(ui.Overlay{Modal:true}) (plugin dropdowns in commands_plugin.go, commandline.go, commands_view.go) rather than through ShowDialog, so they need to participate in the focus stack too.
Touch points: internal/ui/root.go, internal/app/app.go, internal/app/commands.go, internal/app/commands_search.go. No architectural change, but ~40–80 lines across several files plus tests, and the validity guard needs per-widget visibility checks.
Why low priority
- Trigger requires the integrated terminal to be focused, then opening and dismissing an overlay, then continuing to type without looking at the screen (the cursor is visibly back in the editor).
- No silent-on-disk corruption: the change is visible in the buffer immediately and is undoable; a save is still an explicit action.
- The integrated terminal is a less-central surface than the editor.
Test
The --exec repros above are deterministic. Good candidates for tests/integration/ (real PTY) plus an e2e focus-state assertion that dismissing a dialog opened from a non-editor focus does not focus the editor.
References
audit/2026-07-12-ux-bug-audit.md — BUG-057, BUG-058
audit/critical.md — re-verification against main @ b93468d
Summary
Dismissing an overlay (dialog, command palette, find bar) always returns focus to the editor, regardless of what was focused before the overlay opened. When the integrated terminal was focused, the next keystrokes meant for the shell land in the editor buffer instead — silently modifying (and, on save, corrupting) the open file.
From the 2026-07-12 UX bug audit: BUG-057 (+ closely coupled BUG-058). Re-verified on
main@b93468d.Repro A — dialog dismiss steals focus from the terminal
f.txt=hello world→ buffer becomesXYhello world, and the tab isn't even flagged modified in the dump.Repro B — force key toggles a panel through a modal overlay, then dismiss corrupts
ctrl+t→ terminal open + focusedctrl+p→ command palette (modal overlay)ctrl+t→ theterminal.toggleforce key fires while the palette is still open (BUG-058), hides the bottom panel, and callsFocusEditor()behind the overlayesc→ palette dismissedZZhello worldRoot cause
Two coupled issues:
1. Hardcoded "return to editor" on every dismiss path. There is no notion of "previous focus". ~10 call sites hardcode
a.FocusEditor():App.DismissDialog()/App.ClosePluginDrawer()—internal/app/app.go:661,666Root.EscapeFallback—internal/app/commands.go:112OnDismissroutes throughDismissDialog()—internal/app/commands_search.go:38commandline.go:59,commands_view.go:67,92,98,menus.go:219,settings_view.go:256,294,commands_plugin.go:5452. Non-quit force keys punch through modal overlays.
Root.HandleEvent(internal/ui/root.go:191) runs every registered force key while an overlay is open. The in-code comment only justifies this forctrl+qforce-quit;terminal.toggle(and any other force key) mutating background layout under a modal palette is unintended.Why this is not trivial to fix
A naive "save focus on overlay open, restore on dismiss" is not enough — Repro B shows the saved widget (the terminal panel) can be hidden by the time the overlay is dismissed. Restoring focus to a hidden
RawKeyConsumerwould be worse than the current bug (keystrokes route to an invisible terminal). The fix is three coupled parts:Root.Focusedwhen the first modal overlay opens; on dismiss,SetFocus(saved)only if the widget is still mounted / visible / focusable (terminal panelShowBottom, sidebarVisible, plugin panel still present) — otherwise fall back toFocusEditor().DismissDialog,EscapeFallback, and the find-bar / menuOnDismisspaths all call a singlea.RestoreFocus()instead of hardcodingFocusEditor(). A parallel focus stack (mirroringRoot.Overlays) is needed because dismiss handlers chain (dismiss one dialog, open another).Also note: some overlays are pushed via raw
Root.PushOverlay(ui.Overlay{Modal:true})(plugin dropdowns incommands_plugin.go,commandline.go,commands_view.go) rather than throughShowDialog, so they need to participate in the focus stack too.Touch points:
internal/ui/root.go,internal/app/app.go,internal/app/commands.go,internal/app/commands_search.go. No architectural change, but ~40–80 lines across several files plus tests, and the validity guard needs per-widget visibility checks.Why low priority
Test
The
--execrepros above are deterministic. Good candidates fortests/integration/(real PTY) plus an e2e focus-state assertion that dismissing a dialog opened from a non-editor focus does not focus the editor.References
audit/2026-07-12-ux-bug-audit.md— BUG-057, BUG-058audit/critical.md— re-verification againstmain@b93468d