0045 — Client-side copy-mode over the consumer's own engine
0045 — Client-side copy-mode over the consumer’s own engine
TL;DR. Copy-mode — selection, char/line/block modes, one-shot word/line/output grabs, and copy-to-clipboard — is a client-local projection over the focused pane’s own libghostty engine, not a wire feature. The abi epic’s server-side selection frames are withdrawn; selection state, the SelectionRect/SelectionMode/CopyRequest contract, engine resolution, and OSC 52 emission all live in phux-client, and nothing about a selection touches the wire.
Status: Accepted Date: 2026-07-11
Context
The abi epic scoped copy-mode as a wire feature: the client would send selection anchors, the server would track a selection against its authoritative grid, and the extracted text would come back as a new frame pair (a SELECT / COPY_RESULT round trip). That shape predates ADR-0030, which settled that any structured view of a terminal — and a selection over a grid is exactly that — is a consumer-side projection of the shared engine, never a wire tier. A selection frame would be a second terminal model on the wire: it can drift from the engine, it taxes every consumer that never selects, and it re-creates the re-parse liability ADR-0013 paid to remove. The two designs cannot both stand.
Both ends already run libghostty (ADR-0013); the client owns one Terminal per attached pane and feeds it TERMINAL_OUTPUT. Everything copy-mode needs — cell geometry, word/line/semantic boundaries, formatted extraction — is already answerable from that client-side engine.
Decision
Copy-mode is a client-local projection, implemented entirely in phux-client:
-
No wire surface. No selection frame, no copy round trip, no
PROTOCOL_VERSIONbump. The abi epic’sSELECT/COPY_RESULTframes are withdrawn. This is the direct application of ADR-0030 point 1. -
Selection is client state. The overlay tracks a two-corner selection in pane-local viewport cells plus a
SelectionMode(Charlinear,Line,Rectblock). Arrow/mouse input mutates it; nothing leaves the client. -
Extraction is engine-resolved. On copy, the dispatcher resolves the selection against the focused pane’s own
libghostty_vt::Terminal—format_selection_allocfor the two-corner rectangle (block when the mode isRect), andselect_word/select_line/select_all/select_outputfor the one-shot grabs — then writes the text to the host clipboard via OSC 52. The engine is never re-implemented and the grid is never re-encoded. -
One shared contract, one owner. The plain-data contract that both the selection UX and the renderer depend on —
SelectionRect { start, end, rectangle },SelectionMode,CopyRequest,SelectionGrab— lives in one leaf module (render/overlay/selection.rs) that imports neither the overlay state machine nor the renderer. The highlight geometry (SelectionRect::contains, block vs linear) is part of that contract, so the copy path and the on-screen highlight cannot disagree about what a block selection covers.
Why
Delegating to the client engine is the only choice consistent with ADR-0030: the engine is shared and never re-encoded, so a selection computed client-side is exactly what a server-side selection frame would have returned, minus the drift surface and the conformance tax on non-selecting consumers. libghostty already exposes the selection and formatting primitives, so the client calls them rather than re-implementing terminal semantics. OSC 52 puts the text on the host clipboard without a phux-specific clipboard verb.
Tradeoffs
- Every consumer that wants copy-mode runs the engine. This is the same cost the browser client already pays (ADR-0030 point 4); we accept it.
- Scrollback selection is bounded by the client mirror’s scrollback, not the server’s full history. Acceptable: the client already owns the scrollback it renders.
- The block-highlight and the mode UX share one
SelectionRectfield (rectangle) across two files. To keep a fan-out from racing on it, the field and its geometry are declared and landed in the shared contract module first; the UX then populates it and the renderer reads it against an already-landed type. See CONTRIBUTING.md “Multi-agent fan-out” — this is that discipline applied to a shared struct.
Alternatives
(A) Server-side selection frames (the abi epic) — rejected. Track the selection against the server grid and return extracted text as a frame. Rejected by ADR-0030: it puts a structured terminal view on the wire, drifts under capability mismatch, and taxes consumers that never select.
(B) A client-side selection that re-parses VT instead of calling libghostty’s selection API — rejected. Re-implements what the engine already does and re-introduces a second model client-side. We call format_selection_alloc / select_* directly.