Skip to content

fix(cli): eliminate terminal scroll flicker and rerender storm#310

Merged
bradygaster merged 1 commit intobradygaster:mainfrom
jongio:fix/cli-scroll-rerender-storm
Mar 10, 2026
Merged

fix(cli): eliminate terminal scroll flicker and rerender storm#310
bradygaster merged 1 commit intobradygaster:mainfrom
jongio:fix/cli-scroll-rerender-storm

Conversation

@jongio
Copy link
Copy Markdown
Contributor

@jongio jongio commented Mar 9, 2026

Summary

Fixes persistent scroll-to-top flicker and excessive re-rendering in the Ink-based CLI shell that made the interactive terminal unusable in PowerShell and standard terminals. The root cause was a cascade of issues: Ink's fullscreen clearTerminal path firing on every render, high-frequency animation timers triggering unnecessary React re-renders, unstable component keys causing remounts, and the live viewport layout pushing the input prompt below the fold.

Closes #309

Root Cause

Four independent issues combined into the scroll storm:

  1. Ink fullscreen clearTerminal — Ink's render path called clearTerminal() (writes \x1b[2J\x1b[3J\x1b[H) on every render cycle, forcing the viewport to scroll-to-top
  2. High-frequency timers — Three unsynchronized animation timers (spinner at 80ms, pulsing dot at 500ms, elapsed counter at 1s) produced ~16 re-renders/sec, most of which changed nothing visible
  3. Unstable Static keys — Message keys used shifting array indices, causing Ink to remount elements and trigger layout recalculation
  4. Layout shift on processing toggle — The live region height was only constrained during processing=true, causing the layout to jump when toggling states

Changes

Core Rendering Architecture (App.tsx)

  • Live viewport pinning: Set fixed height={rootHeight} on the root <Box> so logUpdate tracks exactly that many lines — no cursor drift or scroll-to-top
  • Messages stay in live region: Current messages render in the live (rewritable) region instead of being pushed to <Static> scrollback, preventing the "conversation vanishes on re-render" problem
  • Responsive maxVisible: Derives visible message count from terminal height (Math.floor((terminalHeight - 8) / 3)) so taller terminals show more context
  • Stable Static keys: Uses timestamp + index-at-creation instead of shifting array indices to prevent Ink remounts
  • Content width capping: Clamps contentWidth to Ink's useStdout().columns to prevent text overflow/clipping
  • Version footer: Displays v{version} at the bottom of the viewport

Re-render Throttling

File Before After Effect
AgentPanel.tsx (PulsingDot) 500ms interval 800ms interval Fewer animation re-renders
AgentPanel.tsx (elapsed) Blind tick every 1s Ref-gated: only triggers React update when display string changes Skips no-op re-renders entirely
InputPrompt.tsx (spinner) 80ms interval (~12.5 fps) 150ms interval (~6.7 fps) 47% fewer re-renders
MessageStream.tsx (elapsed) setElapsedMs every 1s lastElapsedSecRef skips setState when rounded seconds unchanged Eliminates sub-second no-op updates

Combined effect: ~16 re-renders/sec down to ~7/sec, with most being no-ops that React skips via referential equality.

Terminal Utilities (terminal.ts)

  • Deduplicated hooks: Extracted shared useTerminalDimension(getter)useTerminalWidth and useTerminalHeight were copy-paste duplicates
  • Debounced resize: 150ms debounce on process.stdout resize events to batch rapid resize sequences
  • Safe row default: getTerminalHeight() falls back to 50 rows (instead of 24) so piped/test environments have enough room for /help output

Ink Runtime Patches (index.ts)

  • Documented the three patches applied directly to ink.js:
    1. Disabled incrementalRendering (standard erase-and-rewrite instead)
    2. Removed trailing \n appended to output
    3. Disabled clearTerminal scroll-to-top path
  • Added terminal clear (\x1b[2J\x1b[3J\x1b[H) before Ink render to start from a clean viewport

Dead Code & Smell Cleanup

  • Removed redundant @mention fallback from MessageStreamApp.tsx already derives mentionHint and passes it via activityHint
  • Separated system message rendering into its own layout branch with proper padding
  • Removed stale comments referencing the "Static scrollback refactor"

Build & Test Fixes

File Issue Fix
scripts/bump-build.mjs 4-part versions (0.8.25.4) rejected by npm as invalid semver Use prerelease tag format (0.8.25-build.N)
test/bump-build.test.ts Assertion expected old invalid format Updated to expect new X.Y.Z-build.N format
test/cli-packaging-smoke.test.ts Build bump mutates versions during test Set SKIP_BUILD_BUMP=1 in test env
test/marketplace.test.ts /nonexistent resolves to D:\nonexistent on Windows (exists) UUID-based path under tmpDir
test/repl-ux.test.ts @mention tests relied on removed MessageStream fallback Pass activityHint prop to match new architecture

Files Changed (12 files, +158/-80)

File Change
CHANGELOG.md Added [Unreleased] section with fix summary
packages/squad-cli/src/cli/shell/components/AgentPanel.tsx Re-render gate, wider pulse interval
packages/squad-cli/src/cli/shell/components/App.tsx Fixed root height, live viewport, stable keys, responsive maxVisible
packages/squad-cli/src/cli/shell/components/InputPrompt.tsx Wider spinner interval
packages/squad-cli/src/cli/shell/components/MessageStream.tsx Elapsed-time dedup, system msg layout, removed dead fallback
packages/squad-cli/src/cli/shell/index.ts Documented Ink patches, viewport clear
packages/squad-cli/src/cli/shell/terminal.ts Shared dimension hook, debounced resize, safe row default
scripts/bump-build.mjs Valid semver for non-prerelease bumps
test/bump-build.test.ts Updated expected version format
test/cli-packaging-smoke.test.ts SKIP_BUILD_BUMP env for pack stability
test/marketplace.test.ts UUID-based nonexistent path for Windows
test/repl-ux.test.ts Pass activityHint for @mention tests

Verification

  • npm run build passes
  • TypeScript type checks pass
  • All tests pass (3959/3959)
  • MQ quality gate passed (code review, security audit, idiomatic audit, preflight, hack)
  • Pre-existing Windows test failures fixed

@jongio jongio marked this pull request as draft March 9, 2026 15:14
@jongio jongio changed the title fix: reduce CLI shell re-render storm causing scroll flicker fix(cli): eliminate terminal scroll flicker and rerender storm Mar 9, 2026
@jongio jongio force-pushed the fix/cli-scroll-rerender-storm branch 2 times, most recently from 193549e to 366bd6c Compare March 9, 2026 22:04
@jongio jongio marked this pull request as ready for review March 9, 2026 22:06
@bradygaster bradygaster requested a review from Copilot March 9, 2026 22:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes persistent terminal scroll flicker and excessive re-rendering in the Ink-based CLI shell that made interactive sessions unusable on Windows/PowerShell. It addresses four root causes: Ink's clearTerminal firing on every render, high-frequency animation timers, unstable component keys, and layout jumping. Several pre-existing test failures on Windows are also fixed.

Changes:

  • Core rendering architecture (App.tsx, MessageStream.tsx): Fixed viewport height pinning, moved current messages to live region instead of Static scrollback, added stable timestamp-based keys, added responsive maxVisible, and added version footer.
  • Re-render throttling (AgentPanel.tsx, InputPrompt.tsx, MessageStream.tsx): Reduced animation timer frequencies and added ref-gated state updates to skip no-op re-renders.
  • Build & test fixes (bump-build.mjs, terminal.ts, test files): Fixed invalid 4-part semver output, deduplicated terminal hooks with debouncing, and fixed Windows-specific test failures.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/squad-cli/src/cli/shell/components/App.tsx Fixed viewport height, stable keys, live region layout, maxVisible, version footer
packages/squad-cli/src/cli/shell/components/MessageStream.tsx Elapsed-time dedup, system message layout, removed @mention fallback
packages/squad-cli/src/cli/shell/components/AgentPanel.tsx Ref-gated elapsed tick to skip no-op re-renders, wider pulse interval
packages/squad-cli/src/cli/shell/components/InputPrompt.tsx Spinner interval widened from 80ms to 150ms
packages/squad-cli/src/cli/shell/terminal.ts Shared useTerminalDimension hook, 150ms debounced resize, 50-row fallback
packages/squad-cli/src/cli/shell/index.ts Documents Ink runtime patches, viewport clear before render
scripts/bump-build.mjs Non-prerelease builds now use valid semver X.Y.Z-build.N
CHANGELOG.md Added [Unreleased] section documenting the rendering fixes
test/bump-build.test.ts Updated assertion for new semver format
test/cli-packaging-smoke.test.ts Added SKIP_BUILD_BUMP=1 to prevent version mutation during test
test/marketplace.test.ts UUID-based non-existent path for Windows compatibility
test/repl-ux.test.ts Pass activityHint prop to match new MessageStream architecture

Comment thread packages/squad-cli/src/cli/shell/components/App.tsx Outdated
Comment thread packages/squad-cli/src/cli/shell/components/MessageStream.tsx Outdated
Comment thread test/bump-build.test.ts
Comment thread scripts/bump-build.mjs
@jongio jongio force-pushed the fix/cli-scroll-rerender-storm branch from 366bd6c to bceeae1 Compare March 9, 2026 22:34
@jongio
Copy link
Copy Markdown
Contributor Author

jongio commented Mar 9, 2026

@copilot please re-review the latest changes addressing all 4 review threads.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comment thread packages/squad-cli/src/cli/shell/terminal.ts Outdated
Comment thread packages/squad-cli/src/cli/shell/components/MessageStream.tsx Outdated
Comment thread packages/squad-cli/src/cli/shell/components/AgentPanel.tsx
@jongio jongio force-pushed the fix/cli-scroll-rerender-storm branch from bceeae1 to 2e78a94 Compare March 9, 2026 23:11
@jongio
Copy link
Copy Markdown
Contributor Author

jongio commented Mar 9, 2026

@copilot please re-review the latest changes.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Comment thread packages/squad-cli/src/cli/shell/index.ts
Comment thread packages/squad-cli/src/cli/shell/components/MessageStream.tsx Outdated
Comment thread test/repl-ux.test.ts Outdated
Comment thread test/repl-ux.test.ts Outdated
@jongio jongio marked this pull request as draft March 10, 2026 00:07
@jongio jongio force-pushed the fix/cli-scroll-rerender-storm branch from 2e78a94 to 0a445b4 Compare March 10, 2026 00:20
@jongio
Copy link
Copy Markdown
Contributor Author

jongio commented Mar 10, 2026

@copilot please re-review the latest changes.

@jongio jongio force-pushed the fix/cli-scroll-rerender-storm branch from 0a445b4 to 916617c Compare March 10, 2026 00:43
@jongio
Copy link
Copy Markdown
Contributor Author

jongio commented Mar 10, 2026

@copilot please re-review the latest changes. MQ quality gate applied: regex safety fix in patch script, memoized roleMap, inlined resolvedHint alias, idiomatic Array.find, corrected CHANGELOG description.

Ab3y pushed a commit to Ab3y/squad that referenced this pull request Mar 10, 2026
…migration

feat: Wave 3 — Documentation Epic complete (bradygaster#182)
@jongio jongio marked this pull request as ready for review March 10, 2026 12:23
Root cause: four independent issues combined into a scroll storm:
1. Ink fullscreen clearTerminal path firing every render cycle
2. ~16 unsynchronized animation re-renders/sec from 3 timers
3. Unstable Static component keys causing Ink remounts
4. Layout shift from height toggling between processing states

Changes:
- Patch Ink fullscreen path (disable clearTerminal, incrementalRendering, trailing newline)
- Widen spinner/animation intervals (80->150ms spinner, 500->800ms pulse)
- Share terminal dimension hook with 150ms debounce
- Pin root height to prevent logUpdate cursor drift
- Keep conversation in live viewport (not Static scrollback)
- Stable UUID-based Static keys, responsive maxVisible
- Fix bump-build.mjs to produce valid semver prerelease format
- Fix marketplace test for Windows path compat

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jongio jongio force-pushed the fix/cli-scroll-rerender-storm branch from 916617c to a8020c7 Compare March 10, 2026 13:10
@bradygaster
Copy link
Copy Markdown
Owner

Squad Team Review — PR #310

Thanks for this, Jon — this is exceptional systems work. You found four independent root causes that our team wouldn't have caught through normal code review. All four required library-internals archaeology across React → Ink → log-update → terminal emulation, and the fixes are surgical.

What our team found

Flight (Architecture): The design philosophy shift from React is cheap, render everything to only render when pixels change is the right model for TUI apps. The bounded reactive system with explicit render budgets is now defensible. LGTM.

INCO (CLI UX): A+ grade. Stable viewport, smoother animations (slower timers feel faster because they don't cause flicker), and the pinned-input model matches modern chat UIs. The animation slowdowns (spinner 80→150ms, pulse 500→800ms) are the right trade-off — consistency beats speed.

FIDO (Quality): Build passes clean. 3,931 tests pass. The two test failures (shell-metrics, remote-control) are pre-existing on main — confirmed by running the same tests against main. Zero regressions introduced by this PR.

The four root causes you caught

Root Cause Why we missed it
Ink's clearTerminal() on every render Required reading Ink's compiled source + ANSI sequence tracing
3 independent timers creating ~16 re-renders/sec Multiplicative effect only visible under profiling
log-update trailing newline causing line-count drift State machine bug across two third-party libraries
Unstable Static keys from array index shifts Ink's reconciliation behavior isn't documented

Follow-ups we'll track separately

  1. Pre-existing shell-metrics telemetry opt-in bug (on main, not from this PR)
  2. Long-term: upstream Ink patches or spike a custom renderer
  3. Add pty-based scroll regression tests

Ship it. 🚀

@bradygaster bradygaster merged commit 153eabc into bradygaster:main Mar 10, 2026
chilipadi added a commit to chilipadi/squadro-5 that referenced this pull request Mar 11, 2026
Merge upstream/main to incorporate recent improvements:
- Fix: Resolve CLI scroll flicker and re-render storms (bradygaster#310)
- Docs: Remote Q&A scenario, .squad/ directory explainer, decision framework
- Docs: Node.js version alignment, contributor credits
- UI: Open Graph/Twitter Cards, Astro Image component, navbar improvements
- Release: v0.8.25 with CLI packaging smoke test

Preserved Squadro-5 branding in README while incorporating upstream improvements.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bradygaster added a commit that referenced this pull request Mar 15, 2026
fix(cli): cherry-pick terminal flicker fix from main (#254, #310)
bradygaster added a commit that referenced this pull request Mar 20, 2026
…nt updates

Session: 2026-03-10-pr310-merge-and-abstract

Changes:
- Orchestration logs for PAO (presentation abstract), Flight (arch review), INCO (UX review), FIDO (build+test)
- Session log for PR #310 merge and abstract delivery
- Cross-agent updates: INCO and FIDO history.md include Ink root cause findings and postinstall patch pattern
- Decision inbox: empty (no new decisions)
- Team updates: PR #310 merged, 3,931 tests pass (zero regressions), presentation abstract delivered

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

CLI shell scroll flicker on Windows Terminal / PowerShell

3 participants