Serve PTY scrollback from on-disk pty.log instead of a 256KB ring#275
Merged
Conversation
Phase 1 of the web UI polish pass — small, high-leverage fixes for the four most-noticed rough edges (scroll jumps on click, the flash/lurch on session load, the confusing Insert/Enter buttons, and sluggishness): - Scroll anchoring: drop `scroll-behavior: smooth` on the transcript so programmatic "stick to bottom" lands instantly; preserve the session list's scrollTop across its innerHTML rebuild so clicks/background updates no longer teleport it to the top; rAF-coalesce scrollToBottom with a passively-tracked at-bottom flag (no forced reflow per token). - Composer: collapse Insert/Enter to a single "Send" for chat sessions (byte-identical there); show a secondary "Type" only for PTY-backed sessions; add titles/aria + a Shift+Enter hint; restore typed text if a send fails (guarded against session switches). - Performance: throttle the header matrix loop before its layout read and gate it on document.hidden (~60 -> ~8fps; idle when backgrounded). - Polish/a11y: composer button hover/active/focus-visible states, a global :focus-visible ring, a global prefers-reduced-motion block, on-accent contrast fixes (#fff -> #06160c), and operator-row alignment. All changes are confined to crates/daemon/assets/index.html and were adversarially reviewed for regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously each session kept a 256 KiB in-memory ring of raw PTY bytes and `pty_replay` returned a snapshot of that ring. After ~2 pages of page-up in a codex/claude/antigravity session the user had scrolled past what the ring held — older bytes had been evicted at write time and the shadow vt100 parser had nothing to render, so content was lost or the formatting fell apart. Zarvis didn't show the symptom because its scrollback is a structured `ItemHistory`, not raw PTY bytes. Switch `pty_replay` to read the tail of the on-disk `pty.log` (bounded by `PTY_REPLAY_CAP = 8 MiB`). The TUI feeds the bytes into its vt100 parser, whose `SCROLLBACK_MAX = 5000` rows is now the real ceiling on what survives in scrollback — exactly what the row-count cap was meant to be. Disk reads happen once per attach. The ring was the only consumer of `PtyState::push` / `snapshot` and the startup `read_pty_tail` rehydration, so they're all gone. Disk append is unchanged. `PtyState` keeps the size field for parser sizing on attach. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two gaps in the prior commit: - `pty_replay_returns_empty_when_pty_log_missing` — a brand-new session has no `pty.log` yet; replay must return an empty body rather than surfacing the underlying read error. - `pty_replay_preserves_pty_size_through_round_trip` — the refactor moved the bytes off `PtyState` but still reads `size` from the same lock. Lock that down so a future tidy-up doesn't silently start returning `None`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Scrolling up in a PTY-wrapped session (codex / claude / antigravity) loses content or breaks formatting after ~2 pages of page-up. Zarvis sessions don't have the symptom.
Root cause: each session kept a 256 KiB in-memory ring (
PTY_RING_CAPincrates/daemon/src/session.rs) of raw PTY bytes.pty_replayreturned a snapshot of that ring. The TUI's shadow vt100 parser was fed only from that snapshot, so once the user scrolled past the 256 KiB worth of bytes the ring held, there was nothing to render. Older bytes had been evicted at write time.The existing
ultracodesession on disk illustrates it: 2.8 MBpty.logbut only the last 256 KiB ever reached the TUI — an 11× gap, exactly the "~2 pages back" boundary.Zarvis sidesteps this entirely because its scrollback is the structured
ItemHistory, not raw PTY bytes.Fix
Switch
pty_replayto read the tail of the on-diskpty.logdirectly (storage.read_pty_tail), bounded by a new constantPTY_REPLAY_CAP = 8 MiB. The TUI feeds the bytes into its vt100 parser whoseSCROLLBACK_MAX = 5000rows is now the real ceiling on what survives in scrollback — exactly what the row-count cap was meant to be. Disk reads happen once per attach.The in-memory ring was the only consumer of
PtyState::push/snapshotand the startup rehydration, so they're all gone.PtyStatekeeps thesizefield for parser sizing on attach. Disk append is unchanged.What this does not fix
The secondary codex-only issue (in-place
\x1b[2J\x1b[Hredraws not pushing rows to vt100 scrollback) is a separate concern and not addressed here. With the cap raised from 256 KiB to 8 MiB, that issue becomes the next thing the user hits, but it's a much smaller fraction of the symptom space than the ring cap was.Test
Two new daemon tests:
pty_replay_returns_full_disk_tail_not_just_old_ring— writes 1 MiB (4× the old ring) topty.log; verifiespty_replayreturns the full 1 MiB.pty_replay_caps_at_replay_max_for_huge_logs— writesPTY_REPLAY_CAP + 1 MiB; verifies the response is exactlyPTY_REPLAY_CAPbytes AND that it's the tail of the file (most recent), not the head.🤖 Generated with Claude Code