Move the conversation stream to append-only; drop the self-managed virtual-view layer
Background
App.tsx currently re-renders the entire conversation history as a dynamic Ink tree. To support that, we maintain a large pile of "we manage the screen ourselves" machinery:
- A byte-level virtual frame layer (
src/frame/* — viewport / slice / overlay / bottom)
- Viewport budgeting (
layout/viewport-budget.tsx, prompt-viewport.ts)
- Chat scroll state + scrollback hook (
state/chat-scroll-store.ts, hooks/useScrollback.ts)
- An application-level copy-mode (
copy-mode/* — press q to rebuild a selection region)
The price is that terminal behavior is inconsistent across emulators. Scrolling, copy, resize, line wrapping — each one fights the terminal for the same pixels, and each terminal behaves differently. Directly related issues: #1527 (mouse capture breaks native selection), #1523 (cursor disappears on Ghostty).
Direction
Adopt the Claude Code model:
- History region → Ink
<Static>. Rendered once, written through process.stdout.write, then Ink stops touching it. The terminal owns native scrollback / copy / mouse wheel.
- Live region → A small zone at the bottom (composer, spinner, status line). Dynamic redraws only patch the last few lines.
- Full-screen scenes (Setup / CheckpointPicker / McpHub) → Enter alt-screen for the duration, exit back to the append-only main stream. They never compete with history for screen space.
- Streaming messages → Render progressively in the live region; commit to
<Static> only on finalize.
Stages
In dependency order, each stage is its own PR:
Net deletion target: ~2.5–3k lines.
Tradeoff we're accepting
The application-level copy-mode goes away. Copy relies on the terminal: native mouse selection, Ctrl+Shift+C, tmux copy-mode. Every modern emulator handles this. The small population on pure ssh + mouseless tmux loses application-level selection — accepted. As a bonus, #1527 resolves automatically: we no longer need to capture the mouse outside alt-screen scenes.
Out of scope
- Implementation details of putting Setup / CheckpointPicker / McpHub into alt-screen (separate issue)
- Incremental syntax highlighting for streaming markdown in the live region (separate issue)
Move the conversation stream to append-only; drop the self-managed virtual-view layer
Background
App.tsxcurrently re-renders the entire conversation history as a dynamic Ink tree. To support that, we maintain a large pile of "we manage the screen ourselves" machinery:src/frame/*—viewport / slice / overlay / bottom)layout/viewport-budget.tsx,prompt-viewport.ts)state/chat-scroll-store.ts,hooks/useScrollback.ts)copy-mode/*— pressqto rebuild a selection region)The price is that terminal behavior is inconsistent across emulators. Scrolling, copy, resize, line wrapping — each one fights the terminal for the same pixels, and each terminal behaves differently. Directly related issues: #1527 (mouse capture breaks native selection), #1523 (cursor disappears on Ghostty).
Direction
Adopt the Claude Code model:
<Static>. Rendered once, written throughprocess.stdout.write, then Ink stops touching it. The terminal owns native scrollback / copy / mouse wheel.<Static>only on finalize.Stages
In dependency order, each stage is its own PR:
<Static>parallel validation: Move the history region inApp.tsxto<Static>. Keep the old path behind a flag and run them in parallel until every card type finalizes correctly.src/frame/*: The lowest layer, removed last. If anything inframe/ansi.tsorframe/width.tsis still useful, lift it intocli/ui/text-utils.ts.Net deletion target: ~2.5–3k lines.
Tradeoff we're accepting
The application-level copy-mode goes away. Copy relies on the terminal: native mouse selection,
Ctrl+Shift+C, tmux copy-mode. Every modern emulator handles this. The small population on pure ssh + mouseless tmux loses application-level selection — accepted. As a bonus, #1527 resolves automatically: we no longer need to capture the mouse outside alt-screen scenes.Out of scope