Skip to content

feat(ui): row-precision virtual scroll on Ink 7 + React 19#380

Merged
esengine merged 3 commits into
mainfrom
chore/ink7-react19
May 7, 2026
Merged

feat(ui): row-precision virtual scroll on Ink 7 + React 19#380
esengine merged 3 commits into
mainfrom
chore/ink7-react19

Conversation

@esengine

@esengine esengine commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Upgrade Ink 5.2 → 7.0.2 + React 18.3 → 19.2; drop react-reconciler / yoga-layout
  • Retire src/renderer/ (cell-diff renderer + ink-compat shim) — Ink 7 ships native equivalents for every motivation
  • Default to alt-screen so the resize-ghost / scroll-yank class of bugs disappears
  • Row-precision virtual scroll for chat history with reading-mode UX

Why retire cell-diff

Ink 7 supersedes every reason the cell-diff renderer existed:

cell-diff motivation Ink 7
Resize-ghost workaround alternateScreen render option
Custom timer for spinners useAnimation hook (shared internal timer)
Auto-rerender on resize useWindowSize hook
Box measurement useBoxMetrics / measureElement
Render flicker mitigation synchronized output (DEC 2026) + incrementalRendering

Maintaining a parallel renderer was tracking upstream and re-implementing what already exists. Production was running real Ink anyway since 5d-19 was never wired up.

Behavior changes

  • reasonix code / reasonix chat default to alt-screen. Pass --no-alt-screen to keep chat output in shell scrollback (legacy).
  • ↑/↓ on an empty input buffer scrolls chat history (was: recall prompt history). Ctrl+P / Ctrl+N keeps the prompt-history binding (wheel-immune in WT raw mode).
  • Mouse wheel scrolls chat (translated through ↑/↓ in raw mode).
  • PgUp / PgDn always scroll chat. End jumps to bottom + re-pin.
  • When scrolled away from bottom, PromptInput hides and the bottom shows 📖 reading history — End / PgDn to return. End or PgDn returns to live.

Implementation notes

  • Virtual scroll: outer <Box overflow="hidden"> clips at viewport rows; inner <Box marginTop={-scrollRows}> slides content. useBoxMetrics reports inner / outer heights so useChatScroll can clamp scroll bounds and auto-pin to bottom on new content.
  • Drop <Static>: incompatible with alt-screen (no scrollback to shelter committed cards across resize-reflow). React.memo(CardRenderer) plus reference-stable cards in the reducer skips reconciler cost on unchanged history.
  • Routing: App owns PgUp/PgDn/End/wheel everywhere; PromptInput hands off ↑/↓ on empty buffer when pinned + idle; App takes ↑/↓ directly when busy or in reading mode (PromptInput can't process them).
  • Misc cleanup: CardStream.isSettled covers task / plan / subagent; useReserveRows quantizes to 4-row buckets; Ticker migrated to useAnimation; modal-open boolean reused across 14 OR'd checks; inline tests migrated off cell-diff to ink-testing-library.

Test plan

  • npm run verify passes (typecheck + lint + 2132 tests + build)
  • reasonix code boots cleanly into alt-screen
  • Resize terminal repeatedly: no ghost dividers / status duplicates
  • PgUp / mouse wheel smoothly scrolls chat history (3 rows / step)
  • End / PgDn returns to live; new content auto-follows when pinned
  • Scroll works during streaming AND while a subagent is running
  • Ctrl+P / Ctrl+N still recalls prompt history
  • reasonix code --no-alt-screen still works for users who need shell scrollback

esengine added 3 commits May 7, 2026 05:53
Bump ink 5.2→7.0.2 + react 18.3→19.2; drop react-reconciler/yoga-layout.
Retire src/renderer/ (cell-diff + ink-compat shim) — Ink 7 ships native
equivalents for every motivation: alternateScreen, useAnimation,
useWindowSize, useBoxMetrics, synchronized output, incrementalRendering.

Default to alt-screen so the resize-ghost / scroll-yank class of bugs
disappears. `--no-alt-screen` opts back into scrollback for users who
need chat output preserved in shell history on exit.

Row-precision virtual scroll with reading mode: scrolling away from
bottom hides PromptInput and replaces it with a "📖 reading history —
End / PgDn to return" indicator. PgUp/PgDn/End owned by App; ↑/↓ on
an empty buffer hand off to chat scroll via PromptInput; App takes ↑/↓
directly when busy or in reading mode (PromptInput can't process them).
Wheel events translate to ↑/↓ in raw mode, so the same paths cover wheel.
Ctrl+P/N keeps the prompt-history binding (wheel-immune).

Drop <Static> — incompatible with alt-screen (no scrollback to shelter
committed cards across resize-reflow). React.memo on CardRenderer plus
the reducer's reference-stable card array skips reconciler cost on
unchanged cards. CardStream renders all cards inside an overflow:hidden
Box; inner Box's marginTop=-scrollRows shifts content; useBoxMetrics
reports inner/outer heights so App can clamp scroll bounds.

CardStream.isSettled covers task / plan / subagent kinds. PromptInput's
useReserveRows quantizes to 4-row buckets so per-keystroke layout doesn't
churn. TickerProvider migrated to Ink 7's useAnimation. Modal-open
condition collapsed from 14 OR'd checks into one reused boolean.

Migrate inline tests (markdown, plan-confirm) off the cell-diff renderer
to ink-testing-library. Remove dead alt-screen.ts; strip stale comments
referencing removed scrollback-mode workarounds.
Ink 7 calls setRawMode on stdin during render(); CI's process.stdin
isn't a TTY so it throws "Raw mode is not supported". Pass a minimal
EventEmitter-based fake stdin (setRawMode no-op, isTTY=true) alongside
the existing fake stdout. Affects four tests that import render directly
from ink for in-process render-and-assert: ui-session-picker-currency,
ui-stats-panel-currency, ui-usage-card-balance, ui-status-row-balance.
@esengine esengine merged commit b07b86e into main May 7, 2026
3 checks passed
@esengine esengine deleted the chore/ink7-react19 branch May 7, 2026 13:14
@esengine esengine mentioned this pull request May 7, 2026
5 tasks
esengine added a commit that referenced this pull request May 7, 2026
Ink 7 + React 19 chat scroll rewrite (#380), configurable web_search
backend with SearXNG (#338), MCP filesystem sandbox preflight + wizard
mkdir confirm (#362, PR #379), README URL fix (#375), bug template
update (#378).
ChasLui pushed a commit to ChasLui/DeepSeek-Reasonix that referenced this pull request May 23, 2026
)

* feat(ui): row-precision virtual scroll on Ink 7 + React 19

Bump ink 5.2→7.0.2 + react 18.3→19.2; drop react-reconciler/yoga-layout.
Retire src/renderer/ (cell-diff + ink-compat shim) — Ink 7 ships native
equivalents for every motivation: alternateScreen, useAnimation,
useWindowSize, useBoxMetrics, synchronized output, incrementalRendering.

Default to alt-screen so the resize-ghost / scroll-yank class of bugs
disappears. `--no-alt-screen` opts back into scrollback for users who
need chat output preserved in shell history on exit.

Row-precision virtual scroll with reading mode: scrolling away from
bottom hides PromptInput and replaces it with a "📖 reading history —
End / PgDn to return" indicator. PgUp/PgDn/End owned by App; ↑/↓ on
an empty buffer hand off to chat scroll via PromptInput; App takes ↑/↓
directly when busy or in reading mode (PromptInput can't process them).
Wheel events translate to ↑/↓ in raw mode, so the same paths cover wheel.
Ctrl+P/N keeps the prompt-history binding (wheel-immune).

Drop <Static> — incompatible with alt-screen (no scrollback to shelter
committed cards across resize-reflow). React.memo on CardRenderer plus
the reducer's reference-stable card array skips reconciler cost on
unchanged cards. CardStream renders all cards inside an overflow:hidden
Box; inner Box's marginTop=-scrollRows shifts content; useBoxMetrics
reports inner/outer heights so App can clamp scroll bounds.

CardStream.isSettled covers task / plan / subagent kinds. PromptInput's
useReserveRows quantizes to 4-row buckets so per-keystroke layout doesn't
churn. TickerProvider migrated to Ink 7's useAnimation. Modal-open
condition collapsed from 14 OR'd checks into one reused boolean.

Migrate inline tests (markdown, plan-confirm) off the cell-diff renderer
to ink-testing-library. Remove dead alt-screen.ts; strip stale comments
referencing removed scrollback-mode workarounds.

* test(ui): provide fake stdin for Ink 7 raw-mode check in CI

Ink 7 calls setRawMode on stdin during render(); CI's process.stdin
isn't a TTY so it throws "Raw mode is not supported". Pass a minimal
EventEmitter-based fake stdin (setRawMode no-op, isTTY=true) alongside
the existing fake stdout. Affects four tests that import render directly
from ink for in-process render-and-assert: ui-session-picker-currency,
ui-stats-panel-currency, ui-usage-card-balance, ui-status-row-balance.

* test(ui): extract makeFakeStdin/Stdout to shared tests/helpers/ink-stdio
ChasLui pushed a commit to ChasLui/DeepSeek-Reasonix that referenced this pull request May 23, 2026
Ink 7 + React 19 chat scroll rewrite (esengine#380), configurable web_search
backend with SearXNG (esengine#338), MCP filesystem sandbox preflight + wizard
mkdir confirm (esengine#362, PR esengine#379), README URL fix (esengine#375), bug template
update (esengine#378).
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.

1 participant