Protocol¶
An agent mutates a tab's panel by running laura open <file> — a separate process from the TUI host. This is the seam it crosses.
Request / response¶
One connection carries one request and one response: the producer connects, writes a single request frame, and reads a single response frame before the socket closes. The TUI run loop answers, because it holds the live layout state a reply reports on. A client that reads EOF with no frame treats it as ok.
Request shapes¶
Typed messages, one JSON object per line (NDJSON), internally tagged by type. Fields have defaults, so older/shorter frames still parse:
{"type":"open","path":"spec.md","split":null,"dir":"horizontal","ratio":50,"side":"second","focus":true,"dry_run":false,"highlight":null,"diff":false}
{"type":"close","pane":null,"all":false}
{"type":"focus","pane":1}
{"type":"highlight","pane":null,"start":40,"end":52}
{"type":"diffview","pane":null,"on":null}
{"type":"layout"}
{"type":"ready"}
{"type":"update","path":"spec.md"}
opensplits a pane (split, default: the focused pane) into a new panel renderingpath.dirishorizontal/vertical,ratio(1..99) is the new panel's percent,side(first/second) is where the new panel lands,focusmoves focus into it (defaulttrue),dry_runreports the would-be layout without mutating.highlightis an optional[start, end]pair (1-based inclusive,null= none) that points the new panel at a line range on open — the same treatment as thehighlightmessage, applied as the panel first paints (see below).diff(defaultfalse) opens straight into the inline diff view.closeremoves panepane(default: the focused panel);allreturns the tab to shell-only. The shell (pane0) can't be closed.focusfocuses a pane by id.highlightreverse-videos linesstart..=endin a panel (pane, default: the focused panel) and scrolls the range into view. Line numbers are 1-based inclusive source-file lines (whatwc -l/an editor/git blameshow), matching the gutter and reviewL<n>— for markdown a hand-wrapped paragraph collapses onto one rendered row, so any of its source lines points at that whole block;enddefaults tostart(single line). The highlight is independent of focus (direct attention to an unfocused panel) and of the cursor, and persists until re-set or the file reloads shorter. Out-of-range values clamp to the file.diffviewtoggles a panel's inline diff view vs gitHEAD(pane, default: the focused panel).onisnullto toggle,true/falseto set. It's refused (anerrorresponse, a no-op) when there's nothing to diff — nogitbinary, or a clean/untracked file — since a diff view with no diff is a lie. Thedifffield onopenopens straight into the view (same refusal, surfaced as anopenedwarning rather than an error).layoutasks for the current layout report (no mutation).readymarks the tab as hosting an agent, which gates review injection (see below).updateis a reserved re-render nudge, not yet emitted.
Response shapes¶
One response per request, tagged by type:
{"type":"ok"}
{"type":"opened","pane":1,"warnings":["panel shown, but run `laura ready` to enable review submission"]}
{"type":"report","area":{...},"panes":[{"id":0,"kind":"pty","rect":{...},"overflow_rows":0,"clipped":false}, ...]}
{"type":"error","message":"no pane #7"}
opened carries the new pane id (which laura open prints) and any non-fatal warnings. report answers layout and open --dry-run: one PaneReport per pane with rect, content_rows, visible_rows, overflow_rows, and clipped, so a producer can measure fit. error is a typed failure (laura prints the message and exits non-zero).
Pane identity¶
A tab is a recursive binary split tree; each leaf is a pane with a per-tab monotonic u64 id. The shell is always pane 0. Ids are stable and never reused within a tab, so closing a middle pane leaves a gap (ids 0, 4 after closing 1..3). Requests address panes by id; the ^p panes popup maps a 1-based positional label to the current id.
Addressing¶
LAURA_TAB holds the tab's namespaced socket name (Windows named pipe / Unix namespaced). One socket per tab. A producer reaches a tab by connecting to that name and writing one frame. The name carries per-process entropy (laura-<pid>-<nonce>-<n>) so a reused PID can't re-mint a dead tab's name: a stale inherited LAURA_TAB fails to connect rather than routing into a live tab.
Scoping is a consequence of addressing, not a security boundary — anything that can read
LAURA_TABcan write the tab.
CLI¶
Client verbs read $LAURA_TAB, send one message, and exit:
laura— run the TUI, hosting your default shell in tab 1.laura -- <cmd>— run the TUI, hosting<cmd>in tab 1 (new tabs still get the shell).laura open <file> [--split <id>] [--dir h|v] [--ratio n] [--side first|second] [--no-focus] [--dry-run] [--highlight <start> [end]]— split a pane and open a panel; prints the new pane id.--highlightpoints it at a line range on open.laura close [<id>] [--all]— close a panel (default: focused;--allfor shell-only).laura focus <id>— focus a pane.laura highlight <start> [end] [--pane <id>]— reverse-video a 1-based line range in a panel and scroll it into view.laura diff [--pane <id>] [--off]— toggle a panel's inline diff view vsHEAD(--offturns it off).laura layout— print the layout report (JSON).laura ready— mark the tab as hosting an agent (enables review submission).
See the CLI reference for flag defaults and the report shape.
--help, --version, and per-subcommand --help are provided by clap.
Review payload¶
Submitting a review (S on a commented panel) doesn't send a protocol message — it injects text straight into the tab's agent PTY, so the agent reads its own review from its input stream. Injection is gated on ready and fails closed: until the tab has received a ready message, both c (comment) and S (submit) are inert — no point building comments there's no consumer to read. This is the injection boundary — anything laura shows is unconditional; anything it writes into a PTY needs a declared consumer. The assembled block:
[laura review · <path>]
<overall body> ← line omitted when overall is empty
L<n> <line n text>
> <comment>
> <second comment on the same line>
L<n> is a 1-based source-file line (matching the gutter and comment UI); comments on one line group under a single header. A comment on a collapsed markdown block emits its source range as L<a>-<b> instead of a single L<n>. If the file shrank past a commented line, the header is emitted without body text. Surrounding-context lines aren't included — bare L<n>/L<a>-<b> refs only.
The block is wrapped in bracketed paste (ESC[200~ … ESC[201~) with a single trailing \r outside the close marker: embedded newlines stay inside the markers so a line-reading REPL doesn't submit each line early, and the block submits once. Paste-honoring is REPL-specific — confirm it against the target agent's REPL; a bare shell ignores the markers. Per-tab messaging will reuse this injection path.