Skip to content

Overlay dismiss always returns focus to the editor — typing corrupts the file when the terminal was focused #578

Description

@eugenioenko

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
  1. ctrl+t → terminal open + focused
  2. ctrl+p → command palette (modal overlay)
  3. 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
  4. esc → palette dismissed
  5. 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.EscapeFallbackinternal/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:

  1. 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".
  2. 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().
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinglow priorityLow priority feature

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions