Skip to content

Move the conversation stream to append-only; drop the self-managed virtual-view layer #1529

@esengine

Description

@esengine

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:

  • Stage 1 — <Static> parallel validation: Move the history region in App.tsx to <Static>. Keep the old path behind a flag and run them in parallel until every card type finalizes correctly.
  • Stage 2 — Drop copy-mode / chat-scroll-store / useScrollback: Once the terminal owns scrolling and selection, these have no callers. Grep for dead refs, then delete.
  • Stage 3 — Drop viewport-budget / prompt-viewport: After confirming the composer no longer depends on them.
  • Stage 4 — Drop src/frame/*: The lowest layer, removed last. If anything in frame/ansi.ts or frame/width.ts is still useful, lift it into cli/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

  • Implementation details of putting Setup / CheckpointPicker / McpHub into alt-screen (separate issue)
  • Incremental syntax highlighting for streaming markdown in the live region (separate issue)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions