Closing a modal sheet in the web UI hides it but never returns keyboard focus to whatever opened it, so focus falls back to <body> and the next keystroke goes nowhere.
Repro
- Open the web UI, select a session.
- Open the Rename sheet (row context menu → Rename).
- Press Escape, or click Cancel, or click the backdrop.
- Inspect
document.activeElement → <body>.
- Type — nothing reaches the terminal, the composer, or the session list.
Same shape was observed after creating a session and after switching to the chat view: focus lands on <body> rather than the surface the user is now looking at.
Cause
closeRenameDialog() (crates/daemon/assets/index.html, ~line 14551 @ 4c1459b) resets the sheet's visibility, error, and dataset but has no focus-restore step. The close-session and settings sheets follow the same pattern. openRenameDialog() correctly moves focus into the sheet, so the focus round trip is one-way.
Expected
Every dismissible overlay records the element that had focus when it opened and restores it on close. Where the invoker is gone (row deleted, list re-rendered), fall back to focusActiveSurface() so the caret lands on the terminal or composer for the current view rather than on <body>.
Notes
focusActiveSurface() already exists and encodes the right per-mode target; the fix is mostly wiring, not new policy.
- On touch devices, prefer restoring to the composer with a visible focus ring rather than silently focusing the terminal.
Acceptance criteria
Found during a power-user UX audit of the TUI and web UI.
Closing a modal sheet in the web UI hides it but never returns keyboard focus to whatever opened it, so focus falls back to
<body>and the next keystroke goes nowhere.Repro
document.activeElement→<body>.Same shape was observed after creating a session and after switching to the chat view: focus lands on
<body>rather than the surface the user is now looking at.Cause
closeRenameDialog()(crates/daemon/assets/index.html, ~line 14551 @ 4c1459b) resets the sheet's visibility, error, and dataset but has no focus-restore step. The close-session and settings sheets follow the same pattern.openRenameDialog()correctly moves focus into the sheet, so the focus round trip is one-way.Expected
Every dismissible overlay records the element that had focus when it opened and restores it on close. Where the invoker is gone (row deleted, list re-rendered), fall back to
focusActiveSurface()so the caret lands on the terminal or composer for the current view rather than on<body>.Notes
focusActiveSurface()already exists and encodes the right per-mode target; the fix is mostly wiring, not new policy.Acceptance criteria
document.activeElementis the invoking control (or the active surface if the invoker is gone) — never<body>.Found during a power-user UX audit of the TUI and web UI.