fix(subagents): drive live result indicator off progress, not a timer - #1529
Conversation
Eliminate the foreground subagent widget flicker that appeared once a running subagent panel grew tall enough to reach or exceed the terminal viewport. The live compact result previously animated its spinner on an 80ms wall-clock timer. Because the panel renders into chat scrollback, a timer-driven spinner cell that scrolled above pi-tui's viewport fold forced a destructive full-screen + scrollback clear on every tick, strobing the indicator. The result glyph now shows an activity "pulse" that advances exactly once per real progress update (driven by snapshot version), so the differential renderer repaints only when content actually changes and never strobes. Replaces the timer-based running spinner/seed plumbing with a pulseFrame threaded through the slash result component and compact renderers, and drops the now-unused ensureResultAnimation export.
5fa1e21 to
e171d7f
Compare
Review — fix(subagents): drive live result indicator off progress, not a timerSolid, well-scoped fix. Net -138 lines, the root-cause writeup is excellent, and the key design distinction is correct: bottom-pinned widget rows (always in-viewport) keep the wall-clock runningGlyph, while scrollback content (foreground subagent results, which can scroll above pi-tui viewport fold) now uses the update-driven pulseGlyph. The pulseFrame?: number thread is consistent across every call site (extension/index.ts -> renderSubagentResult -> renderSingle/MultiCompact -> resultGlyph), and the tests were genuinely rewritten to assert the new invariant (no timer installed, pulse advances once per progress update, byte-stable across wall-clock advances with no update) rather than just deleted. File-length gate and CHANGELOG conventions are respected. I was unable to run bun/git in this environment (sandbox denied), so the below is static review; the PR reports lint / check:file-length / test:unit green. Worth confirming (UX tradeoff)
Cleanup suggestions (non-blocking)
Nits
Overall: clean, correct, and the test rewrite gives me good confidence. Recommend addressing the dead-timer-registry note (at least with a comment) and confirming the quiet-period liveness tradeoff is acceptable before merge. |
Assistant-model: GPT-5.5
Review —
|
Assistant-model: GPT-5.5
Code Review —
|
…#1529) * fix(subagents): drive live result indicator off progress, not a timer Eliminate the foreground subagent widget flicker that appeared once a running subagent panel grew tall enough to reach or exceed the terminal viewport. The live compact result previously animated its spinner on an 80ms wall-clock timer. Because the panel renders into chat scrollback, a timer-driven spinner cell that scrolled above pi-tui's viewport fold forced a destructive full-screen + scrollback clear on every tick, strobing the indicator. The result glyph now shows an activity "pulse" that advances exactly once per real progress update (driven by snapshot version), so the differential renderer repaints only when content actually changes and never strobes. Replaces the timer-based running spinner/seed plumbing with a pulseFrame threaded through the slash result component and compact renderers, and drops the now-unused ensureResultAnimation export. * chore(subagents): address result pulse cleanup Assistant-model: GPT-5.5 * test(workflows): stabilize resume picker ordering Assistant-model: GPT-5.5
Summary
Eliminates foreground subagent widget flicker that appeared once a running subagent panel grew tall enough to reach or exceed the terminal viewport, by replacing the 80ms wall-clock spinner with a progress-driven pulse glyph.
Problem
The live compact result animated its spinner on an 80ms wall-clock timer (
ensureResultAnimation). Because the panel renders into chat scrollback, a timer-driven spinner cell that scrolled above pi-tui's viewport fold forced a destructive full-screen + scrollback clear on every tick — strobing the indicator even when nothing about the run had changed. The flicker scaled with widget height: the taller the subagent panel, the more pronounced the strobing.Root Cause
ensureResultAnimation()scheduled a recurringsetInterval(…, 80)and stored the result insubagentResultSpinnerFrameNow/subagentResultAnimationTimer. Because the animated glyph lived in chat scrollback (not the pinned-to-bottom async widget), pi-tui had no way to do a partial repaint — it had to do a full-screen + scrollback clear on every 80ms tick to update a single glyph cell above the viewport fold.Fix
Replace the wall-clock
spinnerNowpath with apulseFramecounter that advances once per real progress update (driven bysnapshot.version). Because the only line diffs now coincide with content that genuinely changed, the differential renderer repaints exactly as it would for any progress update — no extra above-fold churn between updates.Key Changes
render-layout.ts: AddpulseGlyph(frame?)— a heartbeat glyph (·,•,●,•) whose frame is a monotonic counter, not a timestamp. ExportRUNNING_FRAMESandPULSE_FRAMESfor test assertions. Add inline doc explaining when to usepulseGlyphvs.runningGlyph(scrollback vs. pinned-to-bottom).render-result-animation.ts: RemoveensureResultAnimation(),activeResultAnimationTimers, and thesubagentResultSpinnerFrameNowtimer-driven field; replace withsubagentResultPulseFrame(monotonic counter, no timer) andadvanceResultPulseFrame().stopResultAnimations()is now a no-op stub kept for extension teardown compatibility.render-result.ts: AdvancesubagentResultPulseFrameonce per key change (i.e., per progress update) instead of scheduling interval re-renders. Always callclearResultAnimationTimerto evict any stale timer a prior version may have installed.render-result-compact.ts: ReplacespinnerNowparameter withpulseFrame; usepulseGlyph(pulseFrame)for both single and multi-agent running glyphs.render-status-progress.ts: UpdateresultGlyph()signature — dropseed/nowin favour ofpulseFrame, delegate topulseGlyph.extension/index.ts: ThreadpulseFrame: advanceResultPulseFrame(pulseFrame)intorebuildSlashResultContainer(advanced once persnapshot.versionchange) so the slash-result component also drives off progress, not a timer.setIntervalcalls are made for a running foreground subagent.Testing
bun run lint✅bun run check:file-length✅bun run test:unit✅ (all pass via pre-commit hook)Notes
CHANGELOG entry added under
@bastani/subagents[Unreleased] → Fixed.