Pi stock-UI protocol trial (design draft)¶
Status: working design note, draft 2026-09-25
Trial skeleton: implemented 2026-09-28 in
prototypes/integration/pi-stock-ui/
using the render-level zero-line variant; evidence and the browser boundary
finding are recorded in its README.
Question¶
Can Pi’s stock interactive UI keep its editor, footer, and status while the transcript region — user prompts, assistant text, and one tool-result type — is owned by protocol Blocks, with Pi’s existing rendering path unchanged when the terminal does not support the protocol?
Upstream evidence lives in prior art; the renderer and coupling trace lives in Pi rendering architecture. This note defines the smallest trial that can answer the question. Its purpose is not upstream acceptance; it is to learn whether terminal-owned mutable history can coexist with a real application’s stock UI, and to turn “a suitable integration seam” from an open question into a measured patch.
Terms¶
Transcript: the growing conversation record (prompts, assistant text, tool results) that scrolls into history.
Chrome: the application’s interactive controls around the transcript (editor, footer, status); never protocol-owned in this trial.
Writer: whichever side’s bytes can change a given screen row — the terminal’s Block adapter applying protocol frames, or Pi’s renderer emitting ANSI drawing bytes.
Ownership: the rule that every screen row has exactly one writer at a time. Two writers holding separate models of the same rows produce the documented failure classes (duplicated spans, blank space, cursor misbookkeeping).
Boundary: the moving line between terminal-owned history (Blocks) and the app-owned active screen (chrome), maintained by the terminal-side adapter.
Seam: the smallest upstream change that lets an outside implementation connect without forking internals; this trial measures it as a patch.
Scope¶
Pinned upstream: Pi 0.87.1 at
2b0a123. The session-event trials stay pinned at 0.86.1; this trial follows interactive-UI development instead.Transcript mapping follows the existing trial: user prompts become immediately sealed Blocks, assistant text becomes one mutable Block per message, and one tool type (the existing read-only sample tool) becomes one mutable Block per call.
Chrome stays stock: editor, footer, status, widgets, header, dialogs.
Explicit non-goals: styled Markdown or ANSI-styled Blocks (baseline
text/plainonly), fullscreen mode, extension-UI compatibility, session persistence/resume/fork, additional tool types, non-Windows platforms, performance measurement.
The central unknown: one writer per region¶
Two one-writer configurations already exist and neither conflicts: unmodified Pi, where its renderer draws everything; and our existing trials, where the application emits only protocol frames and the terminal’s adapter draws everything. This trial is the first configuration with two writers on one screen — the patch makes Pi emit protocol frames for the transcript while its renderer keeps drawing chrome.
InteractiveMode composes transcript components with chrome in one component
tree, and TuiMainScreen renders the whole tree. If transcript components
remain in the tree while the terminal mutates the same rows via Blocks, two
writers own one region (the conflicting-writers risk recorded in the
architecture trace). The trial exists to pick and validate an ownership
strategy:
S1 (first attempt): transcript components off the mounted tree, chrome-only rendering. Verified crash-free at the pin (2026-09-26): InteractiveMode updates transcript components through held references and guarded map lookups, never by tree lookup, so transcript components can live unmounted while
chatContainerstays mounted for chrome (error, status, and notice messages share it and must not be hidden). The stock renderer then owns only the active screen while the terminal owns the history above it. Protocol frames share the single output stream with chrome drawing bytes; the codec already accepts such mixed streams. A render-level zero-line gate on the transcript component classes is the smaller-delta variant — every enumeration keeps working — at the cost of wasted per-token rebuild work.S2 (fallback): a filtering TUI wrapper. Implement the
TUIinterface by delegation toTuiMainScreen, removing transcript components at the container boundary. Larger surface, but no InteractiveMode surgery.S3 (rejected): reconstruct Blocks from rendered ANSI at the terminal. No explicit content identity; mutation authority is unverifiable.
Output path and the seam candidates¶
createInteractiveTui accepts an injected Terminal. A trial-owned
Terminal implementation can:
pass chrome bytes through to stdout;
carry protocol frames from the trial’s SDK client on the same stream; and
intercept OSC 9002 replies on input before Pi’s input parser, passing all other input (editor keystrokes, keyboard-protocol replies) through verbatim.
What does not exist today: a public way to make the CLI use such a Terminal
and suppress transcript components. The trial measures that gap as a patch
against the pin. The patch is maintained in this repository, and its final
shape — files touched and lines changed — is the concrete seam proposal for
any later upstream discussion.
Input routing risks¶
ProcessTerminal owns stdin and consumes keyboard-protocol negotiation
replies; TuiBase consumes OSC/DSR replies. The wrapper must not reorder or
drop either. Kitty negotiation ordering and the fragment buffer are
known-sensitive areas. Input checks cover editor typing, submission, and
cursor keys during active streaming.
Lifecycle mapping¶
Reuse the multi-round mapping: one protocol Context per turn, explicit close
on completion or cancellation, abort label on cancel. Session replacement
(/new, resume, fork) is out of scope for the first trial. Thinking
visibility follows the existing trial policy (visible with a label);
message_end seals, since this trial’s simplified frontend does not reopen
thinking afterwards.
Acceptance scenarios¶
Fixed local provider (reusing the pi-session fixtures), Windows bundled ConPTY, and the experimental xterm host:
One turn completes; user prompt, assistant text, and the tool result each appear once, are searchable, and seal; the editor accepts input throughout streaming.
Cancel mid-stream: partial text kept with an abort label; the editor stays responsive; a further prompt completes.
Read earlier text during streaming, then resize 60→36→60 columns: reading position, selection, and copy are preserved; editor and footer reflow correctly.
Negotiation negative (or trial flag off): the stock Pi experience is unchanged and no protocol frames are emitted.
Patch report: files touched and diff size recorded against the pin; that record is the seam definition.
Verification and evidence¶
pnpm typecheck, focused Node tests for the adapter and the Terminal
wrapper, and browser checks through the existing host. Evidence is recorded
with dates in the eventual prototype README
(prototypes/integration/pi-stock-ui/), with claims bounded to the
scenarios actually run. The failure classes documented by the Kimi revert —
duplicated transcript spans, vanished rows, growing blank space, cursor
misbookkeeping — must be checked for absence explicitly, not assumed absent.
Resolved and remaining questions¶
Resolved by source verification at 2b0a123 (2026-09-26; line numbers refer
to that commit’s packages/coding-agent/src/):
Suppression does not crash bookkeeping. Assistant updates flow through the held
streamingComponentreference and tool updates through the guardedpendingToolsmap (interactive-mode.ts:3395,3503);removeChildon a non-child is a no-op.What silently degrades: settings and toggles that enumerate
chatContainerchildren (thinking visibility, tool expansion, output padding, image options) stop reaching transcript content; OSC 133 prompt markers disappear because the components emit them; the fullscreen-exit transcript print would emit a chrome-only residue and must be gated (interactive-mode.ts:839-846).Status indicator and footer read session state directly; editor history and completion are internal to the editor component. Neither depends on transcript components.
The protocol side needs no InteractiveMode event changes: a second
session.subscribelistener is explicitly supported (core/agent-session.ts:1144-1159).main.ts:938constructs InteractiveMode and is the natural flag injection point.Minimal patch points (gate
addChild; keep creating components):interactive-mode.ts:3389(assistant),3416/3492(live tool),3734addMessageToChat(covers user, history, resume, compaction),3712addCustomEntryToChat,3894(tool in history re-render), the bash-component additions, and the exit print above.
Resolved by the 2026-09-28 skeleton (render-level variant; evidence in the prototype README):
Input interception works: OSC 9002 replies are lifted before Pi’s input parser with keyboard bytes untouched and in order (Node checks plus browser keystrokes).
Theme invalidation still reaches components because the render-level variant keeps them in the tree (rebuild work is wasted, not lost).
Confirmed as the next work item:
Region ownership — met 2026-10-03. The region-ownership design is implemented in the experimental xterm host and the acceptance scenarios now hold: all four browser checks pass (turn with mid-stream editor input, cancel plus a further turn, 60/36/60 resize with re-materialization after Pi’s clear, stock fallback). The 2026-09-28 conflict record stays in the prototype README for context.