Skip to content

Skip quiet-settle for occluded panes so parked renderers can't fake completion#123

Merged
thdxg merged 2 commits into
mainfrom
settle-occlusion-guard
Jul 4, 2026
Merged

Skip quiet-settle for occluded panes so parked renderers can't fake completion#123
thdxg merged 2 commits into
mainfrom
settle-occlusion-guard

Conversation

@thdxg

@thdxg thdxg commented Jul 4, 2026

Copy link
Copy Markdown
Owner

What

Quiet-settle (the status indicator's "running → done after 3s of silence" transition) now only runs for panes whose window is actually visible, with a fresh quiet window granted on the occluded→visible edge.

Why

#115 parks the renderer of occluded surfaces, which suppresses the render/scrollbar heartbeats that keep an activity-sourced .running state alive. The settle logic read that suppressed silence as completion: run btop in a tab, switch away for 3 seconds, and the sidebar shows a done checkmark for a program that's still running. Worse, the checkmark sticks — markTerminalActivity deliberately never revives a .done pane (pinned by TerminalExecutionTrackerTests), so the false state survives until user interaction.

Follow-up identified during the #110/#111/#115 review cycle.

How

  • AppState.refreshAllForegroundProcesses consults an injectable paneIsOccluded closure (default: pane.nsView?.window?.occlusionState; no window counts as occluded, which covers incubator-hosted panes) and skips settle while occluded.
  • On the occluded→visible transition, TerminalExecutionTracker.refreshActivityWindow restarts the quiet window — without this, the first visible tick would settle instantly off the stale timestamp and the false .done would stick.
  • OSC-driven signals (133 command-finished, 9;4 progress) are termio-side and unaffected; they still finish hidden panes correctly.

Verified

  • mise run format, mise run lint, and mise run test all pass
  • Added tests: occluded pane doesn't settle; visible pane still settles; de-occluded pane gets a fresh quiet window instead of instantly settling

🤖 Generated with Claude Code

…ompletion

Since #115 parks an occluded surface's renderer, hidden panes emit no
render/scrollbar heartbeats. The status indicator's quiet-settle read that
silence as "command finished" and flipped a still-running TUI in a hidden
tab to a done checkmark — which sticks, because activity events can never
revive a done pane.

Settle now runs only while the pane's window is visible. On the
occluded-to-visible edge the quiet window restarts, so a still-running
program gets its heartbeats back before the 3s settle can fire. The
occlusion probe is an injected closure (no window = occluded, covering
incubator-hosted panes) so the behavior is unit-tested.
@github-actions github-actions Bot added area:state AppState, models, persistence area:tests Test changes labels Jul 4, 2026
The tick-based tests raced the parallel runner: they toggled the shared
Preferences.showTabStatusIndicator singleton and drove the whole
refreshAllForegroundProcesses path (which also re-reads a live surface's
foreground pid — nil under test — clearing the run source). CI's clean
UserDefaults + parallel scheduling surfaced the flake the local run hid.

settleIfVisible is now internal and tested directly with an injected
paneIsOccluded closure: no global mutation, no surface dependency, and a
truer unit of the guard this PR adds.
@thdxg thdxg merged commit cf91d97 into main Jul 4, 2026
5 checks passed
@thdxg thdxg deleted the settle-occlusion-guard branch July 4, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:state AppState, models, persistence area:tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant