agent-ui: real fixed-width icon column for terminal rows

Replaces the first-character-glyph + negative-text-indent trick (which let a
wide emoji or a leading disclosure caret knock the icon out of column) with a
genuine icon cell.

terminal.js: row() / details() / detailsDiff() take an optional `icon` that
goes in a fixed-width `.row-glyph` element (inline-block, 1.4em). Details
summaries wrap their text in a `.summary-text` span; the disclosure caret
moves to `.summary-text::before` so it leads the text, not the icon — keeping
the icon in the shared column. terminal.css carries the cell + caret rules.

app.js passes the per-tool emoji as `icon` for the flat tool-use row and every
expandable tool summary (Write/Edit/send/ask/answer/bash) plus the 💭 thinking
row, instead of string-prefixing it. A details `🖥️` now lines up under a flat
row's `🧠` regardless of emoji width. Doc: terminal-rendering.md layout
contract updated. Closes #1844.
This commit is contained in:
iris 2026-06-22 00:38:00 +02:00 committed by mara
commit 1c47bd4333
4 changed files with 106 additions and 50 deletions

View file

@ -84,6 +84,19 @@
margin: 0.1em 0;
}
.live .row + .row { border-top: 0; }
/* Fixed-width icon column. Rows built with an `icon` (see terminal.js
`row()` / `details()`) put it in a `.row-glyph` element instead of as a
bare first character. `inline-block` with a fixed `width` means the icon
occupies one constant-width cell regardless of the glyph's rendered width
(emoji differ; some carry a variation selector), so every icon's left edge
lines up a flat-row `🧠` and a `details` summary's `🖥` share the column.
It's the first inline box, so the row's `text-indent: -1.4em` pulls it into
the reserved prefix slot exactly like a bare glyph; the following text then
starts at the `padding-left` (1.9em) where wrapped lines also hang. */
.live .row-glyph {
display: inline-block;
width: 1.4em;
}
/* Row-kind colours. Pages register renderers that emit these classes;
any class no page emits is just dead CSS, which is fine. Turn-framing
classes carry their signal entirely on the coloured border-left rule
@ -189,11 +202,15 @@
opacity: 0.6;
}
/* Expandable rows reuse the flat-row prefix metrics (padding-left +
negative text-indent) so the disclosure glyph (` / `) lands in
exactly the same column as flat-row prefix glyphs (` · `).
Summary text omits the per-row directional glyph (the row colour
already carries cyan = outbound tool, muted = inbound result) so
the prefix column doesn't have to fit two glyphs side-by-side. */
negative text-indent). The summary's icon (when present) sits in the
shared `.row-glyph` column same cell as a flat row's icon so a
`details` summary's `🖥️` lines up under a flat row's `🧠`. The
disclosure caret (` / `) leads the `.summary-text` (not the icon) via
`::before`, so it sits where the summary text starts rather than shoving
the icon out of the prefix column. Icon-less summaries have no `.row-glyph`,
so the caret falls back into the prefix column like the old directional
glyph. The summary text carries no ` / `; the row colour (cyan =
outbound tool, muted = inbound result) carries the direction. */
details.row {
white-space: normal;
}
@ -203,11 +220,11 @@ details.row > summary {
white-space: pre-wrap;
word-break: break-word;
}
details.row > summary::before {
details.row > summary > .summary-text::before {
content: '▸ ';
color: inherit;
}
details.row[open] > summary::before { content: '▾ '; }
details.row[open] > summary > .summary-text::before { content: '▾ '; }
details.row > pre.diff-body,
details.row > pre.tool-body {
margin: 0.3em 0 0.4em 0;