Skip to content

feat(tui): replay the focus sweep when typing resumes after idle - #1299

Merged
edwin-zvs merged 5 commits into
mainfrom
tui/focus-sweep-idle-resume
Aug 21, 2026
Merged

feat(tui): replay the focus sweep when typing resumes after idle#1299
edwin-zvs merged 5 commits into
mainfrom
tui/focus-sweep-idle-resume

Conversation

@edwin-zvs

@edwin-zvs edwin-zvs commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

What

The focus sweep (#1295, spec 0203) fires when a pane gains focus. That answers "where did my focus go" at the instant it moves — and says nothing to someone coming back to a terminal they left ten minutes ago, staring at a fleet of similarly shaped panes.

This replays the same sweep on the already-focused pane when a keystroke reaches it after a long silence. No second visual, no new drawing code: it re-arms the existing FocusBorderSweep so anything that changes the animation's look or geometry keeps applying to both cases.

idle resume sweep

State machine

FocusBorderSweep gains one field, last_key_at, and one method, note_focused_pane_key(now, idle_after), called once per dispatched key before routing — so the pane credited is the one about to consume the key. In order:

  1. Every qualifying key refreshes the idle clock. Continuous or rapid typing therefore closes the gap on every keystroke and can never replay the sweep. At most one replay per idle→resume transition.
  2. A key arriving while a sweep is already on screen is absorbed — it neither restarts nor extends it. That is what keeps focus-gain and idle-resume from stacking: the key that moved focus can't also re-arm, and the key after it lands inside the focus-gain sweep. No double animation.
  3. Otherwise, a key idle_after or later than the previous one replays the sweep on the focused pane. The first key of a session counts as ending an idle gap (the TUI has been sitting untouched since it opened).

Focus-gain behavior is untouched: observe() still owns target transitions, and the startup baseline still does not animate.

Which keys count

Only keys a focus-bearing surface consumes. keyboard_overlay_owns_input() mirrors the gates at the top of on_key and suppresses the cue while a floating surface owns the keyboard — help, session picker, /configure, remote-control, route menu, suggestion deck, tasks popup, inline title rename, pinned fleet panel, the prompt/minibuffer strip, and the disconnected state. None of those are FocusBorderTargets, so a key they swallow must not flash an unrelated pane's border.

It is deliberately conservative: a popup that consumes some keys and falls through on others suppresses the cue for the key that dismisses it, and the pane gets it back on the next key. A missed cue beats flashing a border the keystroke never reached. Everything hosted inside a focused pane — PTY, Playbook, operator editor, inline UI, pinned clip — is the pane, and animates. Bare modifier presses are ignored.

Threshold: 120 s

FOCUS_BORDER_IDLE_RESUME_MS = 120_000, sitting next to FOCUS_BORDER_SWEEP_MS / FOCUS_BORDER_SWEEP_FRAME_MS. Two minutes is above every ordinary working pause — reading a diff, waiting out a harness turn, composing the next instruction — where the user has not lost track of focus and the motion would be pure distraction. It is short enough that stepping away for a coffee comes back to the cue. Anything in the tens of seconds fires during normal work; anything past a few minutes stops covering the common "switched windows and came back" case.

CONSTRUCT_FOCUS_SWEEP_IDLE_MS overrides it. That exists precisely so the recording below didn't require shortening the shipped constant — the committed default is 120 s, and the tape ran the daemon with the override at 4 s. Unset or unparseable falls back to the constant.

Frames

The sweep already gates its own 40 ms cadence timer on is_animating(), so nothing new runs while idle. One gap needed closing: the keystroke that re-arms the sweep is usually the same one the focused PTY consumes, and that path deliberately skips its paint (the child's echo is the visible change). skip_draw now yields while a sweep is on screen, so the band starts at the top-left corner instead of wherever the cadence timer picks it up. Gated on a live animation — an idle TUI keeps every existing skip, and there is no permanent repaint loop.

Tests

  • focus_border_sweep_tests — clock injected, no sleeping: a 160 s run of 400 keystrokes (deliberately outlasting the threshold) never re-triggers; the transition on either side of it does; a key during a focus-gain sweep leaves started_at untouched; the env parser falls back on empty/garbage.
  • app::tests (TestBackend) — idle → key paints a sweep whose every differing cell lies on the focused pane's border perimeter and nowhere else; continuous typing across five key kinds never re-arms; keys swallowed by help / the prompt / a disconnected TUI animate nothing, and the same key animates once the overlay is gone; C-x o then a key keeps one sweep on the new pane.

cargo test --workspace and cargo test -p construct-e2e both green.

Spec

specs/0207-tui-idle-typing-focus-sweep.md, with a pointer added to 0203. (Started life as 0206; renumbered after #1298 landed 0206-playbook-mark-and-region.md while this branch was open.)

Recording

vhs, isolated CONSTRUCT_* dirs, worktree binary, CONSTRUCT_FOCUS_SWEEP_IDLE_MS=4000. The tape types, keeps typing (no animation), goes quiet past the threshold, then presses one key. Verified frame-by-frame: the only multi-cell border repaint in the whole 32 s capture is the 200 ms band right after the resuming keystroke.

verified midpoint frame

Local artifacts: /tmp/sweep-artifacts/ (mp4, gif, verified midpoint frame, steady-while-typing frame).

Focus acquisition answers "where did my focus go" at the instant it
moves. It says nothing to a user coming back to a terminal they left
minutes ago, where a fleet of similar panes gives the eye nothing to
lock onto. Replay the same sweep on the already-focused pane when a
keystroke reaches it after a long silence.

The transition animates, not the typing: every key the focused pane
consumes refreshes the idle clock, and a key landing while a sweep is
already on screen is absorbed, so focus gain and idle resume can never
stack. Keys swallowed by a modal, popup, or the prompt strip are not
pane input and animate nothing.

Threshold: 120s, in a named constant beside the other sweep constants,
with a CONSTRUCT_FOCUS_SWEEP_IDLE_MS override for recordings.
0206 was taken by the Playbook mark-and-region spec (#1298) while this
branch was open.
@edwin-zvs
edwin-zvs merged commit e409175 into main Aug 21, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the tui/focus-sweep-idle-resume branch August 21, 2026 16:46
@edwin-zvs edwin-zvs mentioned this pull request Aug 22, 2026
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.

1 participant