Skip to content

tui: render PTY scroll-up from the structured message log#328

Closed
edwin-zvs wants to merge 2 commits into
mainfrom
fix-tui-pty-scrollback-from-messages
Closed

tui: render PTY scroll-up from the structured message log#328
edwin-zvs wants to merge 2 commits into
mainfrom
fix-tui-pty-scrollback-from-messages

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

Problem

Scrolling up an interactive harness session in the TUI showed a broken history:

  • codex pins its input box with a DECSTBM scroll region. vt100 0.16 only saves a scrolled-off line to scrollback when no region is active (grid.rs::scroll_up!scroll_region_active()), so codex's conversation lines are dropped — scrollback is empty, and the viewport-snapshot fallback fragments it (duplicated banner, spliced status, only a few lines recoverable).
  • claude sets no region but rewrites lines with cursor-up + a bordered input box. The snapshot fallback then duplicates answer lines and splices chrome (spinner, borders, input box) into them.

The web client doesn't have this — xterm.js keeps a real native scrollback for the same byte stream.

Fix

For PTY sessions that emit structured Message/Reasoning events and have no tool blocks, render scroll-up from the structured message log — rebuilt and reflowed to the current width. The live view stays the faithful PTY; a raw shell (no messages) keeps the snapshot path; tool-block sessions keep replay_full. Messages are fed into the shadow on both the hydration/replay and live event paths.

Rather than vendor/patch vt100 to match xterm.js's scrollback semantics, this uses the clean, complete, reflowable source the harness already provides.

Design rationale: specs/0017-pty-scrollback-from-messages.md.

Evidence

Reproduced claude's break by feeding a real interactive-claude capture through the shadow path:

distinct=69/80  duplicated=[49×2 … 76×3, 77×3, 78×3, 79×3]
❯ Output the integers 1 through 80 …
✶ Scampering…                          ← spinner spliced into history
⏺ 1
──2─────────────────────────────  3    ← "2"/"3" mangled into a border line

Tests (224 green):

  • app::tests::pty_session_scrollback_uses_message_log — full hydration path → scroll-up renders the clean message log ( prompt, no PTY chrome), and the live view stays the PTY.
  • pty_render::tests::codex_scrollback_renders_clean_message_log — codex scroll-region case.
  • pty_render::tests::claude_style_redraw_scrollback_uses_messages — claude cursor-up redraw case: no chrome, no run-together, all answers scrollable.
  • pty_render::tests::shell_session_without_messages_keeps_natural_scrollback — message-less sessions keep the snapshot path.

Binary

Code is entirely in crates/cli → the relevant binary is agent (the TUI). agentd / agentd-mcp / agentd-adapter-* build but are unchanged from main.

No live screenshot: interactive sessions in a fresh isolated daemon stall on first-run folder-trust / sandbox prompts that one-shot input can't fully drive. Engagement was confirmed via temporary debug instrumentation (render_shadow … msg_mode=true) on an interactive codex session, then removed.

🤖 Generated with Claude Code

Scrolling up an interactive harness session showed a broken history.
Codex pins its input box with a DECSTBM scroll region, and vt100 0.16
only saves scrolled-off lines to scrollback when no region is active, so
codex's conversation was dropped entirely. Claude sets no region but
rewrites lines with cursor-up + a bordered input box, so the
viewport-snapshot fallback duplicated answer lines and spliced chrome
(spinner, borders) into them.

For PTY sessions that emit structured Message/Reasoning events and have
no tool blocks, render scroll-up from the message log instead, rebuilt
and reflowed to width. The live view stays the faithful PTY; a raw shell
(no messages) keeps the snapshot path; tool-block sessions keep
replay_full. Fed into both the hydration/replay and live event paths.

See specs/0017-pty-scrollback-from-messages.md.
Message text carries bare \n line feeds (codex/claude emit multi-line
prose). vt100 treats \n as line-feed only -- down a row, no carriage
return -- so each line started where the previous one ended and the whole
message staircased to the right. Normalize \n to \r\n in
rebuild_shadow_from_messages, matching synth_message on the headless
render path. Adds a multi-line regression test.
@edwin-zvs

Copy link
Copy Markdown
Contributor Author

Closing — this approach (rendering scroll-up from the structured message log) is a net regression in practice: it shows only the prose messages and drops the tool calls / command output / diffs that make up most of a coding session. The current main (snapshot scrollback) keeps the full terminal history, which is more useful even with its imperfections.

Pivoting to fixing the scrollback itself: patching the vt100 backend so codex's scroll-region-scrolled lines actually land in scrollback — the root cause (grid.rs::scroll_up only pushes to scrollback when no scroll region is active, whereas xterm/xterm.js save when the region's top margin is row 0). New PR to follow.

@edwin-zvs edwin-zvs closed this Jun 2, 2026
@edwin-zvs
edwin-zvs deleted the fix-tui-pty-scrollback-from-messages branch June 6, 2026 17:24
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