@slopus/ghostty-web
v0.1.3
Published
Client/server protocol for efficiently remoting a Ghostty-backed terminal to web and native clients.
Readme
@slopus/ghostty-web
Client/server protocol for efficiently remoting a Ghostty-backed terminal. Clients implement the protocol by importing this library:
import { RemoteTerminalProtocolClient, GhosttyRemoteTerminalReplica } from "@slopus/ghostty-web";The implementation is isolated: it does not depend on Rig sessions or agent execution, and the Ghostty adapters use structural typing so any compatible emulator (WASM or native) can plug in.
The protocol has two server-selected display modes:
- VT replay sends ordered raw PTY bytes to clients with a compatible terminal parser. A Ghostty-backed client applies the bytes locally and acknowledges only after its emulator has consumed them.
- Semantic grid sends a keyframe followed by patches containing changed rows. Slow clients may skip transient states. A VT attachment switches permanently to this mode when its unacknowledged window grows beyond its configured credit or when reconnect replay is no longer available.
Every binary packet is length-bounded, versioned, optionally deflate-compressed, and carries a monotonic byte offset or grid revision. Input and resize operations have independent monotonic IDs. Reconnects include a terminal epoch and server-issued lease so offsets from a previous process or another replica cannot be reused.
The important ordering rules are part of the wire contract:
- A semantic keyframe declares
coversOutputOffset; the server cannot use it to replace newer VT bytes. - Resize drains canonical parsing, resizes the PTY and canonical Ghostty, broadcasts an output barrier, and only then releases output produced by the resize.
- Resize events carry an independent revision and are acknowledged by replicas. A fresh attachment after any resize, or a reconnect that missed one, uses a semantic keyframe instead of replaying bytes into the wrong geometry.
- Exit is retained by the server and is sent only after the client acknowledges display state that covers the exit barrier. Attaching after exit produces the same final display and exit event.
- Output, grid, input, and resize acknowledgements are monotonic and bounded by state the server actually sent. Input deduplication uses bounded, single-attachment leases.
- Scrollback pages carry a history epoch, revision, absolute base row, and optional palette/style tables so a client can render cells and detect an evicted or shifting paging basis.
- Replay, per-client VT backlog, canonical parsing, resize-held output, inflated frames, and packet batches all have explicit byte or item caps. Output is split into fixed server-sized chunks, encoded once for fanout, and clients must advertise at least one chunk of credit, so hostile tiny credit cannot trigger per-client packet explosions.
createGhosttyRemoteTerminalServer and GhosttyRemoteTerminalReplica are the concrete adapters.
The server driver serializes canonical Ghostty parsing, snapshots, and resize. A Ghostty replica is
raw-VT-only because a semantic grid cannot reconstruct Ghostty's hidden parser state; web or native
grid renderers can advertise the semantic capability for recovery. The driver also owns ordered
exit and the optional canonical terminal-response sink; replies generated by a replica are never
forwarded to the PTY.
Measurements and tests
pnpm --filter @slopus/ghostty-web test runs the protocol independently over real TCP. The
suite covers byte-split and malformed frames, bounded control floods, real Ghostty on both ends,
reconnects, resize races and rejection, stale-grid prevention, durable exit, one slow client,
16-client encode-once fanout, 1 MiB output through a 96-byte credit window, hard pressure caps,
render-faithful semantic recovery, terminal-generated replies, lost input acknowledgements, Ctrl-C
during input flood, and stable scrollback paging.
The constrained-link scenario uses 1 Mbps, 150 ms RTT, deterministic jitter, and 137-byte network fragments. It reports p50/p95 handshake, first-byte, input-to-PTY, input-to-render, Ghostty render-ready, dense convergence, CPU, RSS, and wire bytes. Values are printed rather than treated as portable performance promises; broad ceilings catch regressions without turning host load into flaky microbenchmarks.
Design sources
- Mosh synchronizes terminal state instead of queueing every intermediate frame and demonstrates local echo for high-latency paths.
- RFB / VNC uses client-demanded incremental updates so a slow client can skip transient framebuffer states.
- RDP graphics acknowledgements advertise client capacity and bound frames in flight.
- tmux control mode pauses lagging pane output and recovers through an authoritative pane capture.
- xterm.js flow control recommends end-to-end acknowledgements tied to emulator consumption and high/low watermarks.
libghostty-vtRenderState tracks global and per-row dirty state, which maps directly to semantic row patches.
The transport is a Node Duplex; TCP, Unix sockets, TLS, or a binary WebSocket adapter can supply
that boundary. Authentication and encryption belong to the enclosing transport.
Hyperlinks
GhosttySnapshotCell.hyperlink is string | null. Semantic grids include it in the deduplicated
JSON style table, so keyframes, row patches, reconnect recovery, resize snapshots, and scrollback
retain the exact OSC 8 URI independently of the visible cell text. The protocol does not infer
links from URL-looking labels, open links, or normalize schemes.
The URI remains untrusted terminal metadata. UI consumers must allowlist acceptable schemes before
creating anchors; in particular, they must not pass arbitrary values directly to href.
