…don't linger as spinners
The output-activity heartbeat throttle (500ms, leading-edge) drops
chunks rather than deferring them, so a fast command's output often
surfaces only on a heartbeat emitted after OSC 133;D already fired
while the pane read idle. The tracker then attributed that stale row
growth to a fresh activity run, showing a ~3s spinner for a command
that took milliseconds.
Completion edges (markCommandFinished, even from idle, and any
foreground transition in refreshForeground) now reset lastOutputRows,
so growth measured across a completion re-baselines instead of
restarting a finished command.
What
Stops the tab activity spinner from appearing late and hanging ~3 seconds after a fast command (
ls) has already finished. Follow-up to #185.Why
The status indicator still felt unresponsive after #185: running a trivial
lscould show a spinner for several seconds, sometimes only starting after the command was done.Root cause is a collision between two designs:
OUTPUT_ACTIVITYheartbeat throttle (patch 0002) is leading-edge with drops: the first pty chunk after ≥500ms of quiet emits immediately, and every chunk in the following 500ms is dropped entirely — no trailing flush. Typing echo opens the window, so a quickly-typedlscan have all of its output chunks dropped.lsfinishes in milliseconds, so OSC 133;D lands while the pane still reads.idle(a no-op), leaving the row baseline stale. The next emitted heartbeat — the prompt redraw, or the next keystroke's echo seconds later — carries the accumulated growth, which the tracker read as a fresh activity start. Nothing can end that run (the D marker already fired; the foreground poll never sees a 10ms process), so it sits out the full 3s quiet-settle.How
Completion edges now rebase the row baseline (
lastOutputRows = nil) inTerminalExecutionTracker:markCommandFinished— including when it arrives while idle, the exact fast-command race, andchangedbranch ofrefreshForeground— the poll's return-to-shell edge, covering the same staleness for shells without OSC 133 integration.Growth measured across a completion re-baselines instead of restarting the finished command; genuinely new output still starts a run on the next growing heartbeat.
quietIntervalstays 3s — the bug was the stale start, not the settle length. Also records the throttle-semantics invariant in the CLAUDE.md activity paragraph.Verified
mise run format,mise run lint, andmise run testall passBoth new tracker tests replay the two races and fail without the fix (
state → .runningwhere.idleis required), pass with it.Notes for reviewers
A fork-side follow-up could make sub-500ms commands crisper still: a trailing-edge flush in
shouldEmitOutputActivitywould let their output flash the spinner correctly instead of showing nothing. This PR only removes the false 3s spinner.