Skip to content

fix(scroll): coalesce pinned-mode shrinks to stop double-Esc flicker#666

Merged
esengine merged 1 commit into
esengine:mainfrom
dimasd-angga:fix/issue-653-flicker-scroll-position-bounce-when-doub
May 11, 2026
Merged

fix(scroll): coalesce pinned-mode shrinks to stop double-Esc flicker#666
esengine merged 1 commit into
esengine:mainfrom
dimasd-angga:fix/issue-653-flicker-scroll-position-bounce-when-doub

Conversation

@dimasd-angga

Copy link
Copy Markdown

Summary

Double-Esc aborting a multi-subagent run past one screen would flicker and bounce the scroll position: each card teardown re-measured and called setMaxScroll synchronously, snapping scrollRows N times in pinned mode. This change coalesces those shrinks behind the same COALESCE_MS trailing-edge timer the delta path already uses, so a burst of re-measurements collapses into one settled transition.

Changes

  • src/cli/ui/state/chat-scroll-store.ts: add pendingMaxShrink / shrinkTimer and a flushShrink() helper; in setMaxScroll, defer pinned-mode shrinks to a trailing setTimeout(COALESCE_MS) flush while keeping grows immediate; flush any pending shrink before applying a non-shrink update so its trailing state can't clobber a fresh value; clear the pending shrink in jumpToBottom so an explicit pin isn't undone by a stale target.
  • tests/modal-scroll-reset.test.ts: add a setMaxScroll coalescing (issue #653) suite covering the burst-of-shrinks case (one trailing transition at the final target), grows applying immediately, the unpinned path still clamping scrollRows synchronously, a grow flushing a pending shrink, and jumpToBottom dropping a pending shrink.

Testing

npm run test -- tests/modal-scroll-reset.test.ts — the new coalescing suite passes alongside the existing jumpToBottom (issue #642) tests. The burst test uses vi.useFakeTimers() and advances 32ms to assert exactly one subscriber notification at the final target.

Notes

The fix piggybacks on the existing COALESCE_MS window rather than introducing a separate knob, to keep the timing model in the store consistent. Grows are intentionally left synchronous so streaming output stays pinned without added latency; only pinned-mode shrinks are deferred.

Closes #653

When an Esc abort tears down multiple subagent cards at once, each card's re-measurement called setMaxScroll synchronously, snapping scrollRows N times and bouncing the viewport past one screen. Holding a trailing-edge target on a microtask flush collapses the burst into one settled transition while keeping grows immediate so streaming output stays pinned without latency.
@esengine

Copy link
Copy Markdown
Owner

Thanks — clean fix, and the test split is exactly what I'd want for this layer.

The cause-analysis matches hypothesis #1 in #653 (N subagent cards collapsing in the same render burst), and piggybacking on the existing COALESCE_MS instead of inventing a separate knob is the right call — one timing model in the store is much easier to reason about than two.

Worth calling out for anyone reading the diff later: keeping grows synchronous matters more than it might look. Pinned streaming during a long completion can't pay 16ms of latency per maxScroll bump, or the bottom-pin visibly lags the cursor. Glad to see that's preserved.

Five-case test coverage (burst, grow-immediate, unpinned-still-synchronous, grow-flushes-shrink, jumpToBottom-cancels) hits every invariant I'd have asked for. Merging.

@esengine esengine merged commit 7ac18f5 into esengine:main May 11, 2026
3 checks passed
esengine added a commit that referenced this pull request May 12, 2026
…il, CardStream fix (#705)

npm-only release. The Tauri desktop source is in the repo and the CLI
subcommand works, but installer bundles for macOS / Windows / Linux
don't ship this round (separate release once signing's settled).

Highlights:
- Tauri desktop client with multi-tab concurrent runtimes (#689)
  plus a near-full polish pass: wallet balance, version chip, active-
  plan rail, abortable pause-gates, edit-gate pill, en + zh-CN i18n,
  shared pause-policy module dedup'd with the CLI TUI (#701)
- checkpoint API + git-changes panel in the embedded dashboard (#682)
- outside-sandbox file access approval modal (#696)
- MCP loading pill + readiness gate on tool dispatch (#687)
- escalate-after flag for flash → pro threshold (#699)

Fixes:
- CardStream Maximum-update-depth crash, quantize window so boundary
  cards stop oscillating (#700, #702)
- `reasonix code` bridges config key to env + lazy subagent client so
  fresh installs can reach the setup wizard (#703)
- pinned-mode scroll shrinks coalesced (#666), generic CSI key decode
  (#692), shell-confirm preview clamp (#691), frontmatter BOM/folded
  lines (#690), MCP error classification (#688), and more
ChasLui pushed a commit to ChasLui/DeepSeek-Reasonix that referenced this pull request May 23, 2026
…sengine#666)

When an Esc abort tears down multiple subagent cards at once, each card's re-measurement called setMaxScroll synchronously, snapping scrollRows N times and bouncing the viewport past one screen. Holding a trailing-edge target on a microtask flush collapses the burst into one settled transition while keeping grows immediate so streaming output stays pinned without latency.
ChasLui pushed a commit to ChasLui/DeepSeek-Reasonix that referenced this pull request May 23, 2026
…il, CardStream fix (esengine#705)

npm-only release. The Tauri desktop source is in the repo and the CLI
subcommand works, but installer bundles for macOS / Windows / Linux
don't ship this round (separate release once signing's settled).

Highlights:
- Tauri desktop client with multi-tab concurrent runtimes (esengine#689)
  plus a near-full polish pass: wallet balance, version chip, active-
  plan rail, abortable pause-gates, edit-gate pill, en + zh-CN i18n,
  shared pause-policy module dedup'd with the CLI TUI (esengine#701)
- checkpoint API + git-changes panel in the embedded dashboard (esengine#682)
- outside-sandbox file access approval modal (esengine#696)
- MCP loading pill + readiness gate on tool dispatch (esengine#687)
- escalate-after flag for flash → pro threshold (esengine#699)

Fixes:
- CardStream Maximum-update-depth crash, quantize window so boundary
  cards stop oscillating (esengine#700, esengine#702)
- `reasonix code` bridges config key to env + lazy subagent client so
  fresh installs can reach the setup wizard (esengine#703)
- pinned-mode scroll shrinks coalesced (esengine#666), generic CSI key decode
  (esengine#692), shell-confirm preview clamp (esengine#691), frontmatter BOM/folded
  lines (esengine#690), MCP error classification (esengine#688), and more
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.

Flicker + scroll-position bounce when double-Esc aborts a multi-subagent run past one screen

2 participants