Skip to content

Transcript

Source: harness/docs/client/transcript.md · Status: current. Staged from the implemented Harness transcript documentation.

The live TUI transcript is a watch surface over PEW work: it projects identity, three-state liveness, and ask visibility. It does not mint Work, license BODY, or become the archive.

Not this file: Enter/Esc chrome on approved (shell.md, operate.md, mechanism ../runtime/approval.md); paint DOM replica / Quickdraw grain (../packages/quickdraw/docs/grain.md). Constructors live in packages/harness/src/tui/ (identity.ts, liveness.ts, work-cells.ts, tape-spans.ts, tea.ts). Quickdraw widgets have no agent knowledge. Paint never gates the agent loop.


The live tape names the current object, shows whether the turn is running, waiting for you, or idle, and pins the ask so a later cell cannot cover it.


Speech Meaning
Watch Live job. Identity of the current object. Viewport is follow-tail.
Audit After job. JSONL / session document + git. Not the live tape’s job.
Work cell One layout object from one ToolCallJob or ProgrammaticJob (plus inner invocations). Not a chat bubble.
Identity row Collapsed one-liner that names the object: path, command, pattern, expr. Never "Read 1 file". No duration. No +N −M — the Diff is the change.
Glance liveness turnLiveness: running | waiting-for-you | idle. Composer #busy is the admitting RPC, not this.
Cell liveness toolCellState / schemeCellState. Independent of glance, except sealed speech never holds the loop spinner.
Ask approvalQueueHead with approved === null (needsHuman). Durable on the Approvable leaf. Scheme-repl is a sequence, not Approvable.
Loop token ReACTChunk / TurnPromptChunk / TurnCompactChunk. Continuation fuel. Not a tape row.
Ask pin Layout sibling outside the viewport, above the composer. Projection of askPaintHead. Visibility only. No key binding.
Follow-tail Present PaintLocal.tapeAtBottom. Viewport is overflowAnchor: "none". At bottom, present passes a scroll the engine clamps to maxScroll. Any upward move freezes.
Receipt Expanded body of a result that exists. A card of intent without a result is not drawn.

Walk reactChunks(turn) plus ProgrammaticJob.invocations as sibling parts. Never walkWork — that list includes judges. Side-channel tools (update_metadata, context_need) are skipped. Compact density keeps every floor-allow call, so the question identity is on the tape while the job is still open. config.json hideFloorTools (default off) restores that omit. Comm-first (agent) stays omitted. Remainder (ask / deny / block, judge, human approved) never fuses.

Kind Paint?
User prompt yes
CompletionExpectation thought / speech yes when there is a body (ThinkingStream / Markdown). Live thought with text: thinking spinner on that row. Empty live thought is not a tape row. Sealed speech: void gutter — never a loop spinner after seal.
ToolCallJob yes. Folded identity disclosure. edit_file / write_file are open Diff blocks, not disclosures.
ProgrammaticJob yes. Folded identity of scheme_repl + sugarcoat expr(). Inners are sibling rows, not disclosure.children.
ReACTChunk / TurnPromptChunk / TurnCompactChunk no row. Feed turnLiveness / emptyWaitChrome.
Judge completions leftover intern PEW on the leaf. Thought/speech paint when they have a body. Empty live thought is occupancy (wait:pin unless a remainder tool already holds the spinner). Not walkWork.

createAskTapeCut skips askPaintHead and every later tool. The head is the pin, not a folded ? row. A scheme that contains the head still paints the bag; a scheme after the cut is omitted whole. After a human answers a mutate Diff, that job stays askPaintHead until outcomeJson or completed — the suggestion does not drop before the sealed Diff is a tape row. A later needsHuman leaf wins. Cover chrome still follows approvalQueueHead.

if turn == null || turn.closed: idle
if approvalQueueHead != null: waiting-for-you
if hasOpenWork || hasQueuedContinuation: running
else: idle

running does not require workerPresent. Minted Work waiting for an Actor is still running.

Heartbeat sits on tape-visible live work: thought with a body → thinking on that row; live tool → tool spinner on the question identity; live scheme → tool spinner only while no inner is live/ask (open inner owns the heartbeat; the bag paints a sealed ·). A floor row omitted by hideFloorTools is not tape-visible, so it does not swallow the pin. When there is no live work to name — empty completion, ReACT / compact gap, hidden floor call — occupancy is wait:pin immediately above the omnibox (waiting for response… / bootstrapping… / retry countdown), not a transcript row. Ask (waiting-for-you) owns the slot. Idle: no spinner.


toolCellState: "ask" | "live" | "ok" | "fail"
schemeCellState: "live" | "ok" | "fail"

needsHuman → ask. Open, no outcome → live. outcomeJson.ok === false → fail. Else ok. Human deny and BODY error both paint fail today — there is no shipped denied / cancelled cell state. Fail identity stays voice at rising prominence; the gutter is the error mark (error role), not a red row. Live identity stays voice at rising prominence; the spinner is the flux mark.

Sigils: user ❯; sealed tool ·; sealed shell $; ask ? (pin, not a spinner). Expand keeps that identity mark on the first row; extra body (receipt, script, thought markdown, live thought peek) is a dim ┃ rule. Sealed collapsed thought does not get the bar. Fail is that bar at error + ghost, not a red row. License chips read policyResult / judgeResult while approved is still null; floor deny / block are not tape chips. Shell omits the chip.


Parse argsJson. Never invent a file count. Path-shaped objects keep the filename (tailEllipsis). Commands / expr / patterns keep the start.

name Object Notes
read_file / list_dir path
glob / fff_search pattern / query, plus cwd when set cwd wins; path is the ingest alias
grep pattern, plus path when set
edit_file / write_file path Open Diff. Optional caption to the right of the path (wraps when tight). No +N −M.
shell command Gutter $. Status: cwd token (omit .). No exit N — fail is the gutter. No license chip.
scheme_repl sugarcoat expr() Bag is not Approvable.
agent__send / agent__ask spec name Dim status from kernel glance (turnDescription, else active / blocked). Expand: unquoted wrapped prompt, then model. Scheme inners start open.
MCP / pack dim server, bright action — or pretty-mcp telegram Args ride the composer stack at ask, not the row.
fallback invocation() or truncated argsJson still includes name

Disclosure XOR: folded shows summary only; open shows children only. children re-include identity, or expand hides the path. Fold is present-side toggle_fold, not a Work field.


Optional model-authored description on shell and scheme_repl (and on edit_file / write_file as a path-right token). Wire name is description; tape field is caption. Missing, empty, whitespace-only, or non-string ≡ omit. The call still runs. Never required.

Paint flattens whitespace, trims, and clamps to 80 Unicode code points. Collapsed shell / scheme identity is the caption XOR the command / expr. Expand always shows the script. Ask stack still shows the command — consent is not the collapsed row. Caption never feeds policy, hatch, judge-cache, peel-cwd, spawn, or outcomeJson. Do not scrape a leading # in command / expr.


Cards are receipts of results, never of intent. Live cells have no result card except a shell ShadowStream tail when tool-tails has a frame for that job. edit_file / write_file ask Diff sits on the pin (hideHeader; the rim already names the file). Sealed mutations stay open filename + Diff. Empty old_string/new_string or empty content is not a Diff. write_file create is empty-old; overwrite uses host priorContent. edit_file applies the span to that prior so both sides are the file. A span-only replica still paints file-absolute numbers via host priorLineStart (Diff lineStart). Paint keeps change-adjacent lines (3 of context) and folds the rest. Expand of other tools is Text / Markdown of the observation — no tree, no grep list, no public Code(). Sealed shell stdout/stderr is a square ┌┐└┘ box (not omnibox ╭╮). The command is shfmt + bash highlight on expand, wrapping at the column (last symbol, highlight kept).


Two projections of the same Work:

  1. Tape cutoff — askPaintHead and everything after it are not tape rows.
  2. Pin / Diff — identity pin (ask:pin) sits between the viewport and the composer, outside the viewport. Occupancy wait (wait:pin) sits immediately above the omnibox when the tape has no live progress; ask owns that slot (waiting-for-you). Edit/create Diff is the viewport’s last child (ask:diff) and squeezes when the clip cannot hold it. Rim-named work omits the identity line. No Diff and a named rim → nothing extra (askPinWorthPainting). MCP and shell never mount a pin: options / command ride the composer stack. Spawn does not cover. No Allow/Deny buttons. No keys. Cover owns Enter/Esc.

askPaintHead is approvalQueueHead when one exists. After a human answers a mutate Diff (edit_file / write_file with a real Diff), the same job stays the paint head until BODY writes a result. Cover Allow/Deny does not wait — needsHuman is still the cover predicate. A later needsHuman leaf wins the pin. Question overlays (#callback) are composer-owned; they are not UAC; glance stays running; the pin stays off.


Product viewport is overflowAnchor: "none". Present owns tapeAtBottom / tapeScroll. At bottom, the frame passes a scroll the engine clamps to maxScroll. Wheel over the tape and PageUp/PageDown write the present offset. Picker wheel stays picker. Not overflowAnchor: "auto" — that path ignores scroll every frame (force-pin). The ask pin is ask visibility while following; it is not a frozen running mark.


These will be re-proposed. The failure is in the name.

Alternative Failure
Pretty Diff / grep / code / tree widgets as the default live tape Identity lost behind chrome. Diff is for mutations and the ask pin.
Spinner on last assistant speech for the whole ReACT loop Sealed speech looks live while tools run.
Composer #busy as loop liveness #busy is the admitting RPC. It goes false while findRunningTurn() is live.
Waiting-for-you painted as a thinking gerund Permission wait looks like thinking.
overflowAnchor: "auto" as follow-tail Engine force-pins every frame. Freeze on upward move dies.
Count-only collapse (Read 1 file, Ran 3 commands) Operators wanted less body; vendors removed the name.
Disclosure children = body only Expand hides the path (XOR).
Distinct denied / cancelled cell states toolCellState maps every ok: false to fail. Do not claim a deny paint that is not in code.
Caption as policy / hatch / classifier evidence A pretty sentence must not lift a deny.
Caption replacing the command with no expand path Collapse hid the identifier. XOR keeps argv on expand and on ask.
Duration on identity rows Spinner is the live mark. elapsedLive is the loop pulse / thought cluster, not the object name.
Paint ack / “continue when painted” Collapsed panels must not throttle BODY.
Bind Enter on the pin to allow Cover owns the write. The pin is visibility.
walkWork for tape parts Leaks judges onto the ReACT watch surface.
TUI as SSoT / frozen Ctrl+O dump Archive is the session document. Fold is watch UX, not redaction.
Public Quickdraw ToolCall() / Code() Agent knowledge would leak into QD. Syntax stays internals.
Mutation-ledger pane Identity rows are the projection. No second PEW type.
Drop the Diff pin when approved is written The change vanishes, then the sealed Diff remounts; a late pin + tape Diff doubles the block.

Doc Role
operate.md Operator keys
shell.md Client duties, empty-draft law
../canon/grain.md Causal spine
../runtime/approval.md LICENSE WHEN
../runtime/session-job.md Session / Job graph
../packages/quickdraw/docs/grain.md Paint substrate