Delete design-session citations (decision/audit/plan ordinals, stack
positions), change narration, review choreography, and reviewer-addressed
justification from comments, JSDoc, docs, READMEs, Agent Notes, tests, and
generator templates; restate every affected fact as current-state contract
prose. Fix generated docs at their sources and regenerate the catalogs and
cordis-surface regions; re-paste type-equiv blocks; update every bilingual
counterpart and re-record the pairs. Record the citation rule in the
committed-artifact-citations Agent Note.
Nine findings, one critical. Terminal cases verified in a real terminal first.
CRITICAL: cells held the accumulated SGR history, so every state boundary
re-emitted the whole chain — output switching color without a full reset
emitted O(n^2) characters. Measured: 3200 such cells produced 25 MB, and the
reviewer's ~90 KB alternating-color case is well under bash's own output cap.
State is now a normalized record (foreground, background, attribute set) with
one canonical sequence per boundary, so the emitted text is linear in cells;
the 90 KB case parses in 36 ms. That also makes the attribute closers every
chalk-based tool writes actually close: `\x1b[1mbold\x1b[22mplain` leaves the
following write PLAIN, which a real terminal confirms.
Width follows emoji presentation, not the U+2600-U+27BF block: `A✓B` redrawn
with `XY` shows `XYB`, so the check every progress line writes is ONE column.
Taking the block as wide misaligned exactly the output this card exists for.
Writing over either half of a wide pair blanks the other, since a terminal
cannot leave one cell of a two-cell glyph standing.
`line\n\x1b[0m` does not end in a newline as a string yet its last parsed line
holds nothing visible, so the terminator check now reads the parsed lines — it
had added a blank row and inflated the collapse count.
A line with no cursor movement no longer builds a column buffer at all; only
its SGR is folded, so an `ls -R` or a 5k-line log allocates nothing per
character.
The `.terminalDescription` rule had been inserted into an existing grouped
selector, silently giving `.codeBody` description typography and changing its
bottom margin from 4px to 0 — a pre-existing surface this PR does not own.
Split out, `.codeBody`'s margin restored.
Three comments contradicted their code: the fixture's exit-marker line (still
claiming recovery from a marker deliberately removed), `bash-sample`'s header
(still routing a click to the details panel, and calling the consumer's cap the
block's own), and a DetailsPanel comment stacked above the wrong rule. The
ui-primitives README documented only the CR/BS half of the replay, so a reader
would expect `OK0%` where `100%\r\x1b[KOK` renders `OK`.
Eight findings, each terminal case verified in a real terminal first:
`\x1b[32mdone\rok\x1b[0m` then `plain` shows `okne` green and `plain` in the
DEFAULT color. The replay returned the last written cell's state, so a reset
landing after the final write vanished from both the text and the returned
state — and every build tool writes exactly that shape, so the color leaked
onto all later output. The replay now converges to the state the scan ended
in, which is also what it hands to the next line.
`abcd\b\x1b[1K|` shows ` |`: CSI 1K erases THROUGH the cursor column, and
the loop stopped before it. The erase mode also reads only the first
parameter now, since a terminal treats `1;2K` exactly as `1K`.
`éx\rYZ` shows `YZ`: a combining mark takes no column, so it attaches
to the cell already written instead of advancing the cursor and leaving the
`x` standing.
`中x\rA` shows `A x`: overwriting a wide character's lead cell leaves its
spacer as a blank rather than closing the gap, which would shift everything
after it one column left.
The banner lost its span when the gutter became padding — a plain block child
only reaches the content box, so the reserved column was painted in the body
color and the card's top-left radius drawn in it. Invisible in the light
theme, where banner and body share a token; visible in the dark one. The
header now pulls back across the gutter and re-insets by the same amount.
The replay trigger matches the same CSI shape the parser accepts, so a form
like `\x1b[1;2K` can no longer skip its own erase, and `replayLine`'s JSDoc
documents its new parameter and returned pair.
Docs: four places still described the dot as sitting left of the card surface,
which stopped being true when the gutter became the card's own padding, and
two fixture comments still referenced the exit marker that was deliberately
removed.
Six findings. Each terminal-semantics case was checked in a real terminal
(tmux, reading back the painted screen) before changing anything:
`100%\r\x1b[KOK` shows `OK`. Modelling the `\r` without its erase left the
previous frame's tail standing — a regression against the old truncate, since
`\r\x1b[K` is the single idiom every spinner and progress bar writes. Erase is
now part of the same replay, in all three parameter forms.
`a\tb\rXY` shows `XY b`. Counting a tab as one column produced `XYb` and
destroyed the alignment this card exists to hold, so the cursor now advances
by terminal columns: tabs reach the next 8-column stop and a wide character
takes two cells.
`\x1b[31mabc\rX\nnext` paints BOTH lines red. A newline does not reset the
graphic state, so state threads from one replayed line to the next instead of
closing at each line end.
Only `m` accumulates into a cell's style now. Folding cursor and erase
sequences in grew the state string per redraw and emitted boundaries anser
had to discard.
The empty check reads the parsed lines the card renders rather than the raw
text: output that is only escapes or control bytes survives `trim()` yet
parses to nothing, and drew blank rows plus a copy control for invisible
bytes instead of the placeholder.
The gutter is the card's own left padding rather than a margin. Every render
site rewrites `margin` wholesale for its own indent, which silently cancelled
the reservation and let a container clip the dot.
The fixture sample no longer carries an `[exit code: 1]` line: the real bash
presenter consumes that marker precisely because the card shows the exit as
its own pill, so the built-bundle snapshot had pinned a frame showing it
twice — one the product path cannot produce.
Carriage return and backspace only MOVE the cursor; neither erases. Both of
my earlier approximations were wrong, and I checked each case against a real
terminal rather than reasoning about it:
`100%\rOK` shows `OK0%`, not `OK` — the redraw is shorter than the frame
beneath it, so the tail stands. `abc\b` still shows `abc`, not `ab` — a
trailing backspace has nothing to overwrite. `\x1b[31mgone\rkept` paints
`kept` RED, because a carriage return does not reset the graphic state, which
one of my own tests had asserted the opposite of.
Both now replay into a per-line column buffer with SGR state stamped per
column, as a terminal stores it per cell. That gives the partial-overwrite
case its real result too: red `bad`, three backspaces, then `ok` shows `okd`
with the `d` still red, since `ok` reached only two of the three cells.
The presenter description now also renders at every site. An expanded row
draws it itself — the collapsed summary is hidden while open, so otherwise
the description was visible only collapsed, the opposite of "above the card"
— and the details panel draws it above the card as well.
Three of my own tests encoded the wrong semantics and were corrected with
their behavior, and the emit loop's gap-filling arm was removed as
unreachable: `\r` and backspace only move left, so no column can be unwritten.
Four review findings. Two are defects the previous two rounds introduced,
which the existing tests did not catch:
A backspace erased raw bytes, so one landing after an SGR reset ate part of
the escape: `\x1b[31mabc\x1b[0m\b\bXY` left `\x1b[` and repainted the rest of
the line with whatever the remainder parsed as. Backspaces now resolve over
VISIBLE characters — a CSI sequence is one indivisible unit a backspace steps
over on its way to the last printed character, so the surviving text keeps
the color its run authored.
The cwd normalizer popped a UNC share root: `\\server\share` with a `..`
became `/server`, losing the separators too. A UNC path's server and share
are its root, and Windows cannot climb above a share, so they are split off
and the remainder collapses against that root.
The other two are gaps the earlier fixes left:
The render-site fallback row still passed the args-derived summary, so any
terminal-declaring tool without its own keyed row (`terminal_send`) lost the
contract's above-card description. It now prefers the description exactly as
BashRow does.
A settled call read `call?.cwd`, which cannot tell "the call omitted a cwd"
from "the paging window dropped the call head". The second case has no cwd
anywhere and the original call may have used an explicit workdir, so it now
draws a bare `$` instead of naming the session workspace.
A multi-line command repeated the cwd label on every prompt row, which
states something the view does not know: it carries ONE working directory —
where the call started — and a `cd` in the command moves later lines
elsewhere. `cd ~` then `ls` rendered both rows labelled with the session
workspace while `ls` actually listed the home directory.
The label now appears on the first row only, and later rows keep a bare `$`
so they still read as prompts. Same reasoning as the run-state dot: neither
a per-line directory nor a per-line exit status exists to report.
The built-bundle snapshot records the effect on fixture turn 60's two-line
command (`fixture echo done` becomes `$ echo done`).
Three review findings, each verified against the presentation contract:
The call view's `description` was dropped, so a presenter that authors one
(`terminal_send` declares `Terminal <id>`) lost the contract's above-card text
and the row fell back to an unrelated args-derived summary. It now rides the
same derivation and outranks that summary.
A relative workdir was concatenated but never normalized, while the bash
executor resolves it before running: with session cwd `/w/app` and workdir
`..` the command runs in `/w`, yet the card displayed the label `..`. The
resolved path now collapses `.`/`..` segments, drops a `..` that would climb
past a root the way a filesystem does, and keeps a Windows path's separators
since the value is only ever displayed.
`run_code` sub-dispatches carry no presenter views on the shipped wire —
`session.ts` folds `tool/code-dispatch(-start)` with null views and the host's
`viewFor` presents only top-level call/result events — so a nested bash call
cannot reach a terminal card. The existing test only passed by injecting views
that path cannot produce; it now says so, and a second arm pins the no-view
shape the wire actually delivers.
Restoring master's fixture also fixed the todo snapshot lane, which my earlier
merge had broken by dropping the projection support the todo dock reads. The
terminal sample turn moved ahead of the todo turn, because the standing plan
retires at the next `turn/start` and a turn appended after it emptied the dock.
The card props are now nested under `card` so a render site spreads exactly the
primitive's own surface, and the fixture reads each sample's authored exit
status instead of re-implementing the bash tool's `parseExitStatus`.
A `command` carrying two shell commands on two lines rendered as one row:
`.command` had `white-space: nowrap`, so the two collapsed into a single
ellipsized line that read as one command with stray arguments.
Render one prompt row per command line, and move the run-state dot out of
flow into a gutter reserved to the left of the card surface, so it neither
indents its command nor depends on the command's text metrics to line up.
The dot stays exactly one per card, on the first row. The exit status the
view carries is the whole call's and bash reports no per-command status, so
a dot per line would assert, of a line that succeeded inside a failing call,
that the line itself failed. The single visually hidden label keeps the same
scope, since one label per row would read to assistive technology as several
distinct outcomes.
Fixture turn 60's command becomes two lines, so the built-bundle snapshot
pins the layout and its dot distribution (`dotsPerPromptRow: [1, 0]`), and
the e2e adds that the dot starts left of the card surface — geometry jsdom
cannot compute. Both READMEs now also record that this package's
user-facing copy is inline Chinese, since zero-cordis atoms have no route to
`ctx.locale`; extracting it belongs to the repo-wide localization work.
Master replaced StateDot's ongoing ring with a pixel-art chase, so the
prompt line's run-state description named an indicator that no longer
exists. Same fix in the note, both READMEs, and the test name.
The terminal card showed no run state: a running command and a settled
command that produced no output rendered the same prompt line, so whether
a command was still running had to be inferred from the absence of output.
Lead the prompt line with a StateDot in three of its states — the spinning
ring while running, red for the same exit status that renders the status
pill, green for a clean settle. That is the same indicator a tool row's
leading icon carries, so a row and its own card cannot disagree about one
command; the row/card agreement is pinned in the ui-conversation spec.
StateDot is aria-hidden, so a visually hidden text label rides beside it,
which is what the refreshed aria goldens now record.
The e2e adds what jsdom cannot compute: the dot's color resolves to the
green success token through the real theme stylesheet, and the dot precedes
the prompt label in document order.
The bash tool already declares the `card: 'terminal'` render intent for
both its call and its result, and host/connection/runtime already deliver
it to the browser as callView/resultView. The Web client ignored it:
rows derived from raw args, and the details panel flattened every tool's
content into one soft-wrapping `<pre>`. Column-aligned output folded into
a paragraph and a long listing stretched the panel without bound.
`TerminalBlock` (ui-primitives) renders a command as a terminal surface:
a shortened-cwd prompt line, output at `white-space: pre` in a
horizontally scrolling box, a head/tail height cap with an expand
control, an exit-code/signal status pill, and a copy control for the raw
output. ANSI SGR runs are parsed with `anser` and resolved onto `--dsw-*`
theme tokens, with literal rgb kept for values the design system has no
token for. Geometry and fonts mirror CodeBlock; the clipboard write both
need moved into a package-internal `clipboard.ts`.
Both Web render sites for a bash call consume the intent through one
derivation (`terminal-card-model.ts`), so they cannot disagree about a
command, its cwd, or its exit status: the keyed BashRow carries the card
resident below its summary row, and the render-site fallback row keeps it
behind its existing expand control. Rows cap at 8 lines against the
panel's 16.
Inline output in the chat row reverses this package's stated
no-inline-output convention, on the owner's explicit decision; the Agent
Note records the reversal and its bound.
Tests: TerminalBlock/ansi/clipboard unit specs, ui-conversation wiring
specs at every render site, a built-client-graph snapshot covering both
chat-row shapes, and a real-browser e2e asserting the no-wrap layout and
the page's own Clipboard API.