Public snapshot state stays in the store engine's plain-data vocabulary
(immer drafts reject Sets without the MapSet plugin, which stays off):
manager/service/contract carry readonly SessionId[] in Host order, and
the tree derivations build their own transient Set — the
expandedProjects pattern. Membership-unchanged installs still keep the
array reference for Object.is short-circuits.
WorkspaceListState gains archivedSessionIds (ReadonlySet, replaced only
on membership change), installed as full snapshots from the list
baseline, the unary echo, and the changed frame. Archiving the current
session clears the selection into the New Session view state. Test
doubles (test-runtime, fake APIs, fixture client) follow the widened
IWorkspaces/IApiClient faces.
Every expandable tool row shares one interaction (whole-row toggle,
icon-to-chevron hover preview) and one expanded body: an IN/OUT
gutter-labeled card with per-section 150px scroll caps and sticky labels.
toolRowModel derives result output and the error first line, terminalFailed
surfaces a failing exit as the collapsed row's red dot, a hover Inspect
pill jumps to the call's trajectory record through a one-shot store
handoff, and the chat view keeps its scroll offset across view switches.
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.
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.
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.