Skip to content

Attach webui terminals via server-rendered screen snapshots - #1201

Merged
edwin-zvs merged 1 commit into
mainfrom
screen-snapshot
Aug 4, 2026
Merged

Attach webui terminals via server-rendered screen snapshots#1201
edwin-zvs merged 1 commit into
mainfrom
screen-snapshot

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

Why

Opening a session in the web UI replays its raw PTY history through xterm.js. xterm parses and paints incrementally, so the replay is visible (we've papered over it with loading overlays and offscreen hydration), and the client has to download up to megabytes of history to reconstruct one screen. The native TUI hides the same replay only because it parses bytes into an in-memory vt100 grid and paints a single frame.

This PR moves that trick server-side (spec 0188): the daemon parses the history invisibly and the browser paints once.

What

New session.screen_snapshot RPC. The daemon feeds a bounded pty.log tail (1 MiB) through a native vt100 parser at the session's real PTY geometry, then serializes: scrollback rows (flow-style — hard breaks as CRLF, soft wraps left to the receiving terminal's autowrap, colors preserved, no absolute positioning), line feeds that push those rows into the client's scrollback buffer, a visible-screen repaint, and cursor / attributes / scroll region (DECSTBM) / origin mode / input modes. Result carries the rendered pty.log span, a scrollback-truncation flag, and the geometry. strip_alt_screen: true renders with alt-screen switches removed first, mirroring the filter the web UI applies to every byte it writes into xterm. Sessions with no known PTY size fail the RPC deliberately — a guessed width would miswrap every line.

Web UI hydrates from the snapshot. Terminal open, duplicate-pane mirrors, and passive split mirrors all write one snapshot instead of replaying raw bytes; raw session.pty_replay remains the fallback (older daemon, unknown geometry). Since xterm cannot prepend, the first "load older" leaves snapshot mode: it re-fetches the rendered span plus one 64 KiB page and rebuilds classically; later pages extend as before. A truncated-scrollback snapshot still offers "load older" even when its span started at offset 0.

Vendored vt100 fork additions (additive only): Grid/Screen scrollback serializer and scroll_region() / origin_mode() accessors — the upstream contents_formatted dump covers neither scrollback nor those two modes.

The existing post-attach force-redraw bump is untouched and remains the authority on the child's real screen — a snapshot has exactly the same trust level as a client-side replay of the same bytes.

Tests

  • 6 unit tests on the pure renderer: round-trip of screen + scrollback + colors through a second parser, soft-wrap preservation across the scrollback boundary, alt-screen stripping, scroll-region/cursor restoration, truncation reporting, strip-filter exactness.
  • 2 manager-level tests: full RPC round-trip from on-disk pty.log (offsets, scrollback depth, cursor), and the no-geometry error path.
  • New web_smoke Chromium scenario: snapshot hydration paints screen + scrollback with zero pty_replay calls, requests strip_alt_screen, sizes xterm to the snapshot geometry, shows the history button for a non-zero span start, and the first "load older" re-fetches [span]+page from the span's end and exits snapshot mode.
  • Full workspace suite green; also verified end-to-end against a live daemon (60 colored lines → 38 serialized scrollback rows + screen, 2.2 KB snapshot, colors intact).

🤖 Generated with Claude Code

New session.screen_snapshot RPC: the daemon feeds a bounded pty.log tail
through its own native vt100 parser at the session's PTY size and returns
just scrollback rows + a visible-screen repaint as escape sequences, plus
cursor, attributes, scroll region, and input modes. The web UI hydrates
terminals (main view and both mirror paths) from that stream in one
xterm.write instead of replaying raw history, so attach cost is
O(screen + scrollback) and nothing visibly replays. Raw pty_replay stays
for older daemons, sessions with unknown geometry, and history paging:
the first "load older" re-fetches the rendered span plus one page and
rebuilds classically.

The vendored vt100 fork grows a scrollback serializer (flow-style, no
absolute positioning, soft wraps preserved) and scroll-region/origin-mode
accessors the upstream state dump lacks. Spec 0188 records the decision.
@edwin-zvs
edwin-zvs merged commit 33c868d into main Aug 4, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the screen-snapshot branch August 4, 2026 04:26
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