Tool-call cards and grouping, with the state a result never arrived in

P1b (docs/RUST.md). `transcript-ui/src/tool.rs` draws a card per tool
call and a group per run: collapsed, a card is its name and the one-line
summary `parse_tool_input` derives; open, it is the description, the
input (highlighted, on the verbatim surface) and the output, capped with
a "Show all N lines". A run is one surface with a heading and a chevron
bar at its foot, so it closes from either end.

Three things worth knowing.

**A collapsed card lays out its summary line and nothing else.** The
fixture's tool outputs are tens of kilobytes and a collapsed card never
builds a widget for one -- `collapsed_cards_shape_only_their_summary_
lines` opens a three-card group over 88 kB of output each and asserts the
text-shape count equals the same group's over three bytes (17 either
way; 17 against 20 when the discipline is deliberately broken, so the
test is real).

**A result arriving replaces one card.** `ToolRow::apply_calls` is the
group's half of `RowBlocks::apply_delta`'s rule, and `build_row` now
hands back one `TailRow` -- blocks for a message, cards for a run --
rather than two mechanisms chosen at each call site.

**Every tap is a tap**: `GestureOutcome::Tapped` out of the `DragArbiter`
`Selection` already owns, so a drag that started on a card scrolls the
transcript instead of opening it.

Three defects found by looking at the render, all recorded with their
repro in docs/IRIS_TODO.md: a `Span` of padded children inside another
`Span` places them a slot out of step (worked around by building the
group as one span, which costs the 4dp inset); `scrollable_on(Axis::X)`
on a non-editable text draws nothing, so a card's command is clipped
rather than pannable; and `NotoSans-Regular` has no U+25B8/25BE/25B4 at
all, so the expander mark is set in the monospace face.

Screenshots: docs/bench/p1b-2026-09-06/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-06 22:49:51 -04:00
1 parent b332873894
commit 7e7cbb5402
13 files changed
+356 -111

No files matched your search

+107 -6
View File
@@ -5687,12 +5687,113 @@ device.
correctly, and an emulator bench run with assertions live and no
abort (`2438 frames over 147.7s, p50 27.2ms`).
- [ ] **P1b — tool-call cards and grouping.** `ToolRows.kt`/
`ToolInput.kt`'s cards: a collapsed row per call with name
and a one-line summary, expand to input and output, runs of
calls grouped (`adopt_run` already groups in `client-core`),
the busy/failed states, and the kilobyte outputs the fixture
carries without laying them out while collapsed.
- [x] **P1b — tool-call cards and grouping.** Done 2026-09-06.
`ToolRows.kt`/`ToolInput.kt` ported to
`iris/transcript-ui/src/tool.rs` plus two new pure modules in
`client-core`. **Screenshots:
`docs/bench/p1b-2026-09-06/iris-tools-collapsed.png` and
`iris-tools-expanded.png`**, both from
`iris/run-headless.sh transcript -- -p transcript-ui` on the
desktop/winit backend (the emulator was not touched this pass
-- another agent held this checkout's AVD). The expanded one
is taken with `IRIS_TOOLS_EXPANDED=1`, which the example reads
to call `TranscriptScreen::expand_tail_tools` -- the expanded
appearance is otherwise unreachable on a machine with no
display and no finger.
**What the cards look like, against `ToolRows.kt`:**
- *A collapsed card* -- a mark, the tool's name (14pt), the
one-line summary `parse_tool_input` derives (12pt, Subtext
0, one line, clipped), and the state word at the far right.
Same as Compose, except that Compose ellipsises the summary
and iris clips it: there is no overflow-ellipsis in
`TextAttrs` yet (IRIS_TODO).
- *An open card* -- the timeout at the top right, the tool's
own description, the subject in a `Verbatim` panel with
`client_core::highlight`'s spans, the leftover input fields
under it, then the output. Same order as Compose.
- *A group* -- "Called N tools" (Compose's exact wording, and
so the name a `ui-trace` script taps), the cards on a Mantle
surface, and a chevron bar at the foot that closes it from
the end the reader is looking at.
- *States* -- `client_core::transcript_fold::ToolState`, five
of them, each with its own word and colour: nothing for
`Succeeded`, "running" (Subtext 0), "your turn" (Peach, the
Compose card's own wording and colour), "failed" (Red) and
**"no result" (Yellow)**. The last two are new -- Compose
can say neither.
**Two things the port had to add to be able to say "it
broke".** `event_model::Event::ToolEnd` gained `is_error`
(`#[serde(default)]`), read from the CLI's own `tool_result`
by one function used by both the live translator and the
import replay (`import::tool_result_is_error`); without it a
result was all a card had and a failed call drew exactly as
confidently as one that worked. And `ToolState` separates
`Succeeded`-with-empty-output from `NoResult`: both leave the
same empty string, and only the session's own status tells
them apart, which is why `TranscriptScreen::
set_session_working` exists and why only the *newest* row can
be "running" (every row behind it belongs to a turn that has
ended).
**Pass condition, met**:
`collapsed_cards_shape_only_their_summary_lines`
(`transcript-ui/src/lib.rs`) opens a group of three cards
whose calls carry 88 kB of output each and asserts the
text-shape count equals the same group's over three bytes.
**17 either way.** Confirmed to be a real test, not a
tautology, by pushing the output block into the collapsed
branch: **17 against 20**.
`a_result_arriving_redraws_one_card_whatever_the_run_holds` is
the second: one `ToolEnd` costs the same number of
`Widget::draw` calls in a twelve-call run as in a three-call
one.
**Three defects found on the way, all by looking at the
render rather than at the diff:**
1. **A `Span` of `Pad`ded children inside another `Span`
places those children a slot out of step.** Every card drew
its content one card's height below its own box, so the
group read as empty bars with somebody else's summary in
them. Bisected against
`IRIS_TOOLS_EXPANDED=1 iris/run-headless.sh transcript`:
removing the inner `Span` fixes it, and so does removing
the cards' own `Pad`; the card background, the `Sized`
wrappers and the per-card `WidgetPtr` all make no
difference. Worked around by building the group as **one**
`Span` (header, cards, collapse bar), which costs the 4dp
inset Compose holds its cards off the group's edge by. The
framework defect is still open -- IRIS_TODO has it, and it
is not the `mov`/`reposition` one f5b8893 fixed (it
survives that commit).
2. **`scrollable_on(Axis::X)` on a non-editable `Text` draws
nothing at all** -- an empty panel where the command should
be. A markdown fence does the same thing to a `TextEdit`
and is fine. So a card's verbatim block is `masked()` and
clips rather than panning; when this is fixed the pan
belongs there too, because the long command is the one
being read closely.
3. **`NotoSans-Regular.ttf` has no U+25B8/25BE/25B4** (read
out of the bundled `cmap`s) while `NotoSansMono-Regular`
does, so the expander mark is set in the monospace face at
the one place the character is written. The old
`build_tools` summary drew that codepoint in the sans face,
which was a missing glyph nobody had looked closely enough
to see.
**Checks**: `cargo fmt --all --check` clean in both
workspaces; `cargo clippy -p iris -p iris-core -p
transcript-ui -p desktop-app -p tabs-ui --all-targets` and
`cargo clippy --all-targets` in `client-core`/`server`/
`event-model` warning-free; tests 86 (iris) + 13 (iris-core) +
36 (transcript-ui, +5) + 137 (client-core, +11) + 160
(server, +1).
**Not done**: nothing on the emulator or the phone (the AVD
was another agent's this pass, so no frame times were taken);
a card's text is not selectable, unlike Compose's, since
`Selection` is keyed per markdown block and a card has none
(IRIS_TODO); no per-corner radius, so the "connected stack"
shape `connectedShape` draws is a 2dp gap instead;
`AskUserQuestionBody`/`PermissionAsk`'s answer buttons are not
ported -- an unanswered ask forces its card open and says
"your turn", but there is nothing to press yet, which is P1d's
modal/controls work.
- [ ] **P1c — history paging and jump-to-latest.** Wire
`client-core::transcript_source` into `transcript-ui`:
the opening page, paging back on scroll with the cushion