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

@ -13,17 +13,32 @@ event kind today. Source of truth lives in
Every row — flat `<div class="row …">` and expandable
`<details class="row …">` alike — shares one prefix column.
The mechanism is `padding-left + negative text-indent` on
`.live .row`: the row's first character (the prefix glyph)
gets pulled back into the column at ~0.5em, and wrapped
continuation lines hang under the body, not under the glyph.
`.live .row`: the row's first inline box gets pulled back
into the column at ~0.5em, and wrapped continuation lines
hang under the body, not under the glyph.
`<details>` summaries inherit those metrics. The disclosure
marker (`▸` / `▾`) is supplied by CSS `summary::before` so it
lands in the same column as flat-row glyphs. To make that
work the JS-side summary text **does not** include a
directional `→` / `←` — the row's colour (cyan = outbound,
muted = inbound) carries the direction, and the prefix
column never has to fit two glyphs side-by-side.
Rows that carry an icon (the per-tool emoji, `🧠`/`💭`
thinking, etc.) pass it as the `icon` argument to
`row()` / `details()` / `detailsDiff()`, which puts it in a
fixed-width `.row-glyph` cell (`display: inline-block;
width: 1.4em`) rather than as a bare first character. The
constant cell width means every icon's left edge lines up in
the column regardless of the glyph's rendered width (emoji
differ; some carry a variation selector) — a flat row's `🧠`
and a `details` summary's `🖥️` align. Rows with a plain
single-char glyph (`◆ · ! ←`) still pass it inline; it lands
at the same ~0.5em left edge.
`<details>` summaries inherit those metrics. The icon (when
present) sits in the `.row-glyph` cell; the summary text lives
in a `.summary-text` span and the disclosure caret (`▸` / `▾`)
is supplied by CSS `.summary-text::before` so it **leads the
text, not the icon** — a leading caret on the icon would push
it out of the shared column. Icon-less summaries have no
`.row-glyph`, so the caret falls into the prefix column like
the old directional glyph. The summary text carries no
`→` / `←`; the row colour (cyan = outbound, muted = inbound)
carries the direction.
Child blocks inside a row (the `.md` markdown wrapper, an
inner `<details>`) get `text-indent: 0` so their content