Skip to content

Alternate buffer rendering with history flush #149

@bananabot9000

Description

@bananabot9000

Summary

The CLI renders its zone (editor, status line, output) in the terminal's main buffer. This means all zone content is subject to terminal reflow on resize, causing visual corruption and scrollback bleed. The terminal reflows content without notifying the application, and no escape sequence can prevent it.

Feature

Switch the CLI to render in the alternate screen buffer during operation. The alternate buffer provides full screen ownership with no scrollback, eliminating reflow and scrollback corruption entirely.

History (completed responses, tool output, etc.) must not be lost. When a response completes, the CLI briefly exits the alternate buffer, writes the formatted response to the main buffer (creating real scrollback), and re-enters the alternate buffer. On exit, the user can scroll up in the main buffer to see all session output.

Functionality

Alternate buffer lifecycle

  • Enter alternate buffer on startup
  • Exit alternate buffer on clean exit (/quit, Ctrl+C)
  • Exit alternate buffer on crash/signal (cleanup handler for SIGTERM, SIGINT, uncaught exceptions). If the process dies without exiting the alternate buffer, the user is stuck on a blank screen (recoverable with reset but bad UX)

Zone rendering

  • The CLI owns the entire screen. Zone renders from a known position without relative cursor tracking
  • Resize handling becomes: clear the alternate buffer and redraw. No position recovery, no heuristics, no reflow prediction
  • The narrow/widen heuristic, pendingResize flag, and notifyResize() complexity are no longer necessary

History flush to main buffer

  • On response complete: exit alternate buffer, write formatted response content to main buffer, re-enter alternate buffer
  • Minimise time in main buffer -- pre-build the output string before the swap
  • One flush per completed response, not per line
  • Content written to main buffer must be human-readable when the user exits and scrolls up (formatted text, not raw ANSI zone frames)

Cleanup handler

  • Register signal/exit handlers that ensure \x1b[?1049l (exit alternate buffer) is sent before the process terminates
  • Must cover: clean exit, SIGTERM, SIGINT, uncaught exception, unhandled rejection

What this replaces

  • Zone position tracking via cursorUp(lastVisibleCursorRow)
  • The pendingResize flag and narrow/widen resize heuristic
  • notifyResize() propagation chain
  • The "content shorter than screen" startup padding hack
  • The entire class of scrollback corruption / history bleed bugs
  • Issue Renderer has no awareness of absolute terminal position #148 (renderer has no awareness of absolute terminal position) -- no longer relevant

What this does NOT include

  • In-app scrollback / history browsing during a session (separate issue, builds on this)
  • Scroll regions (DECSTBM) for normal-operation rendering
  • DSR-based position queries

Guidance

POC verified

A bash POC (alt-buffer-poc.sh in bananabot9000/claude-sandbox) confirms:

  • Sync-wrapped buffer swap is imperceptible at real-world speeds (no sleep)
  • History lines appear in main buffer scrollback after exit
  • Resize in alternate buffer is clean -- just redraw

Sync output (\x1b[?2026h)

Already used in the codebase's render() method. Does NOT prevent flicker during buffer swaps (buffer switch is a screen state change, not buffered output). However, the swap is fast enough that flicker is invisible without sync. Sync does NOT work through tmux (tmux intercepts/swallows the sequence on older versions).

History flush sequence

\x1b[?1049l        (exit alternate -- main buffer restored)
<write formatted history>
\x1b[?1049h        (re-enter alternate -- alternate buffer cleared)

Re-entering the alternate buffer clears it. A full zone redraw is required after every flush.

Existing infrastructure that applies

  • Viewport class already implements a stateful scroll window over an unbounded buffer
  • Renderer.render() already uses cursorAt() for absolute positioning (used in the narrow-resize path)
  • syncStart/syncEnd primitives already exist
  • The trim logic in render() (trailing empty row removal) prevents overwriting the full screen when the zone is smaller than the terminal

Cleanup handler pattern

const exitAlternateBuffer = () => process.stdout.write('\x1b[?1049l');
process.on('exit', exitAlternateBuffer);
process.on('SIGTERM', () => { exitAlternateBuffer(); process.exit(143); });
process.on('SIGINT', () => { exitAlternateBuffer(); process.exit(130); });
process.on('uncaughtException', (err) => { exitAlternateBuffer(); /* log */ process.exit(1); });

What "formatted history" means

The flush writes what the user would want to see if they scrolled up after exiting: the assistant's response text, tool call summaries, code blocks -- the same content currently visible in the zone during streaming, but as final static text. Not the raw ANSI rendering frames.

Phase 0 investigation

Full investigation at projects/claude-cli/investigations/resize-position-tracking.md in the fleet repo. Covers 10 approaches with trade-off tables, compatibility matrices, and terminal behaviour constraints.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions