Skip to content

fix(tui): quantize CardStream window so boundary cards stop oscillating#702

Merged
esengine merged 1 commit into
mainfrom
fix/cardstream-pinned-loop
May 12, 2026
Merged

fix(tui): quantize CardStream window so boundary cards stop oscillating#702
esengine merged 1 commit into
mainfrom
fix/cardstream-pinned-loop

Conversation

@esengine

Copy link
Copy Markdown
Owner

Summary

Second instance of the same setState-loop class that #549 fixed. CardStream's items useMemo depends on both scrollRows and outer.height. In pinned mode scrollRows = maxScroll = inner.height − outer.height, so when a sibling row toggles (ToastRail appearing, ThinkingRow flipping based on busy/streaming), outer.height shifts by ±1 → window slides → a card sitting on the live/spacer boundary flips → inner.height changes by Δ → setMaxScrollscrollRows updates → useMemo recomputes → card flips back. With flash streaming compounding inside the same React batch, the chain reaches MAX_NESTED_UPDATES = 50 and Ink throws inside useBoxMetrics (use-box-metrics.js:51).

#549 patched one specific cause (the "↑ earlier" hint changing outer.height based on scrollRows). #700 is the residual case where any sibling toggle does the same thing.

Fix

Quantize the window position to VISIBLE_BUFFER_ROWS buckets. Sub-bucket scrollRows / outer.height wiggles now map to the same window, so the items array is referentially-stable across them. The boundary card doesn't move on its own anymore — only on real scrolls that cross a bucket.

Trade: a single bucket (30 rows) of "stickiness" at scroll boundaries. Imperceptible in practice — the existing pre-#700 buffer was already 30 rows.

Test plan

  • New tests/card-stream-items.test.ts covers the invariant: the live-card set is identical for every sub-bucket scrollRows delta (0..VISIBLE_BUFFER_ROWS−1) and for outer.height values in [20..40]
  • Full suite: 2689 / 2689 passing (was 2681, +5 new + 3 incidental from other branches)
  • Existing CardStream behaviors (live-by-default for unmeasured cards, spacer-when-scrolled-far) still pass

Closes #700

Same shape as #549. In pinned mode scrollRows tracks maxScroll, and
maxScroll = inner.height - outer.height. The items useMemo depends on
both scrollRows and outer.height, so a sibling row toggling (ToastRail
appearing, ThinkingRow flipping) shifts outer.height by ±1 → window
moves → a card straddling the live↔spacer boundary flips → inner.height
changes by Δ → setMaxScroll → scrollRows changes → useMemo recomputes →
card flips back. With flash streaming compounding inside the same React
batch, the chain reaches MAX_NESTED_UPDATES and aborts inside ink's
useBoxMetrics (#700).

Quantize the window position to VISIBLE_BUFFER_ROWS buckets. Sub-bucket
scrollRows / outer.height wiggles now map to the same window, so items
is referentially-stable across them and the feedback loop dies on the
first re-render.

- extract computeCardStreamItems as a pure exported function for tests
- new tests/card-stream-items.test.ts asserts live-set stability across
  every sub-bucket scrollRows delta and a range of outer.height values

Closes #700
@esengine esengine merged commit adf62e8 into main May 12, 2026
2 checks passed
@esengine esengine deleted the fix/cardstream-pinned-loop branch May 12, 2026 10:51
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
…ng (esengine#702)

Same shape as esengine#549. In pinned mode scrollRows tracks maxScroll, and
maxScroll = inner.height - outer.height. The items useMemo depends on
both scrollRows and outer.height, so a sibling row toggling (ToastRail
appearing, ThinkingRow flipping) shifts outer.height by ±1 → window
moves → a card straddling the live↔spacer boundary flips → inner.height
changes by Δ → setMaxScroll → scrollRows changes → useMemo recomputes →
card flips back. With flash streaming compounding inside the same React
batch, the chain reaches MAX_NESTED_UPDATES and aborts inside ink's
useBoxMetrics (esengine#700).

Quantize the window position to VISIBLE_BUFFER_ROWS buckets. Sub-bucket
scrollRows / outer.height wiggles now map to the same window, so items
is referentially-stable across them and the feedback loop dies on the
first re-render.

- extract computeCardStreamItems as a pure exported function for tests
- new tests/card-stream-items.test.ts asserts live-set stability across
  every sub-bucket scrollRows delta and a range of outer.height values

Closes esengine#700
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.

Error: Maximum update depth exceeded.

1 participant