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:
1 parent
b332873894
commit
7e7cbb5402
13 files changed
+356
-111
No files matched your search
@@ -5,6 +5,45 @@ they can be judged and reversed later. Detail lives in RUST.md (and IRIS.md
|
||||
for iris API changes); this file is only the summary. Newest first. Items
|
||||
marked **DEFERRED** are ones the agent chose not to decide alone.
|
||||
|
||||
## 2026-09-06 (how a tool call looks, P1b)
|
||||
|
||||
- **A card that never got a result says "no result", in yellow, and it is
|
||||
a state Compose cannot say.** A call that finished having printed
|
||||
nothing and a call whose turn was interrupted before anything came back
|
||||
both leave an empty output. Compose draws both as an ordinary finished
|
||||
call, which reads as a fact somebody established. There are five states
|
||||
now, each with a word and a colour: nothing at all for a call that
|
||||
worked, "running" (grey), "your turn" (peach, Compose's own wording and
|
||||
colour), "failed" (red), "no result" (yellow).
|
||||
|
||||
- **A failed call is drawn as failed, which needed a field on the wire.**
|
||||
`is_error` is on the CLI's `tool_result` and was being dropped; the
|
||||
server now carries it to the phone. Reversible, but the alternative is a
|
||||
card that says a call succeeded because it cannot tell.
|
||||
|
||||
- **A group's cards do not each carry their own surface.** Compose gives
|
||||
each card a fill and squares the corners where it faces a neighbour, so
|
||||
a run reads as one object broken into parts. iris has no per-corner
|
||||
radius, and -- more to the point -- a group built the way Compose builds
|
||||
it hit a framework layout defect that drew every card's text a card
|
||||
below its own box. So a group is one surface with its cards on it,
|
||||
separated by a small gap, and the 4dp inset Compose holds them off the
|
||||
edge by is gone. Worth revisiting once the layout defect is fixed
|
||||
(docs/IRIS_TODO.md).
|
||||
|
||||
- **A long tool output is capped at 80 lines or 4 kB with a "Show all N
|
||||
lines".** Compose draws the whole thing, and gets away with it because
|
||||
its `Text` inside a `LazyColumn` lays out lazily; here the output is one
|
||||
text widget and shaping a hundred kilobytes of it costs what the file
|
||||
editor's 32 kB limit was measured against. If iris's text gets cheaper,
|
||||
this is the number to move.
|
||||
|
||||
- **A card's command is clipped, not pannable, and its summary line is
|
||||
clipped rather than ellipsised.** Both are framework gaps rather than
|
||||
choices (`scrollable_on` on a non-editable text draws nothing; there is
|
||||
no overflow ellipsis), and both are worse than Compose today. Named here
|
||||
because they are visible.
|
||||
|
||||
## 2026-09-06 (how a markdown block looks, P1a)
|
||||
|
||||
- **A table is drawn as padded monospace columns, not as a grid.** Your
|
||||
|
||||
@@ -8,6 +8,51 @@ capability that moved. Small and trivial changes do not go here.
|
||||
An entry gives the date, what changed, why, and a short before/after where
|
||||
it helps judge the change without the session that made it. Newest first.
|
||||
|
||||
## 2026-09-06: tool cards, `ToolState`, and a screen that knows whether its session is working
|
||||
|
||||
`transcript_ui::tool` is new: a card per tool call, a group per run
|
||||
(P1b). Three things in the public surface follow from it.
|
||||
|
||||
**`client_core::transcript_fold::ToolState`** is what a card colours
|
||||
itself by -- `Running`, `Deciding`, `Succeeded`, `Failed`, `NoResult` --
|
||||
built by `ToolState::of(&item, session_working)`. The pair it exists for
|
||||
is `Succeeded` against `NoResult`: a call that finished having printed
|
||||
nothing and a call whose result never arrived both leave an empty
|
||||
`output`, and drawing them the same way states a verdict nobody reached.
|
||||
Only the session's own status separates them, which is why `of` takes it.
|
||||
|
||||
**`event_model::Event::ToolEnd` gained `is_error`** (`#[serde(default)]`,
|
||||
so an older transcript still parses), and
|
||||
`client_core::transcript_fold::TranscriptItem::ToolRun` gained `failed`.
|
||||
Without them a result was everything a card knew and a broken call drew
|
||||
exactly as confidently as one that worked -- the missing state, not a
|
||||
wrong one. Every construction site of both had to gain a field; the value
|
||||
comes from the CLI's own `tool_result`, read in one place
|
||||
(`import::tool_result_is_error`) by both the live translator and the
|
||||
import replay.
|
||||
|
||||
**`TranscriptScreen::set_session_working(rsc, bool)`** is new, and is the
|
||||
only thing that writes it. Before: a card with no result was drawn the
|
||||
same whether its turn was still going or had been interrupted. After:
|
||||
only the *newest* row can say "running", because every row behind it
|
||||
belongs to a turn that has ended, and changing the flag redraws that one
|
||||
row rather than the screen. `TranscriptScreen::expand_tail_tools(rsc,
|
||||
bool)` joins it, answering whether there was a tool run to act on -- a
|
||||
group's expanded appearance is otherwise unreachable from anything that
|
||||
cannot press the screen.
|
||||
|
||||
**`transcript_ui::row::build_row` now returns a `TailRow`** rather than an
|
||||
`Option<RowBlocks>`: `Blocks` for a message (a delta costs the last
|
||||
markdown block) or `Tools` for a run (an arriving result costs one card).
|
||||
One mechanism for "what can this row change cheaply", asked of the row
|
||||
rather than decided again at each call site. It also takes the row's own
|
||||
`working` flag.
|
||||
|
||||
Two smaller ones. `client_core::tool_summary::parse_tool_input` is
|
||||
`ToolInput.kt`'s subject/description/timeout/rest split, and
|
||||
`client_core::durations::format_millis` is `Durations.kt`'s -- both pure,
|
||||
both with the Kotlin's own tests ported.
|
||||
|
||||
## 2026-09-06: a tap is its own gesture outcome, and opening a URL is a backend capability
|
||||
|
||||
Three related additions, all for following a markdown link.
|
||||
|
||||
@@ -639,6 +639,49 @@ agent ticks it here with the evidence.
|
||||
it is its own item, most likely the report pane not masking or not
|
||||
claiming its region.
|
||||
|
||||
## Found by P1b (2026-09-06), all with a headless repro
|
||||
|
||||
Each was found by looking at `iris/run-headless.sh transcript -- -p
|
||||
transcript-ui` rather than at a diff, and each is worked around in
|
||||
`transcript-ui/src/tool.rs` rather than fixed here. docs/RUST.md's P1b box
|
||||
has the fuller account.
|
||||
|
||||
- [ ] **A `Span` of `Pad`ded children inside another `Span` places those
|
||||
children a slot out of step.** Each child drew its content one sibling's
|
||||
height below its own box. Repro: `IRIS_TOOLS_EXPANDED=1
|
||||
iris/run-headless.sh transcript --shot /tmp/x.png -- -p transcript-ui`
|
||||
with `tool.rs`'s group built as `Span(DOWN)[header, Pad(Span(DOWN)
|
||||
[cards]), bar]` instead of the single `Span` it uses now. Bisected:
|
||||
removing the inner `Span` fixes it, and so does removing the children's
|
||||
own `Pad`; the background `Stack`, the `Sized` wrappers and the
|
||||
`WidgetPtr` per child make no difference. **Not** the `mov`-vs-
|
||||
`reposition` fault f5b8893 fixed -- it survives that commit. The
|
||||
workaround costs the group the 4dp inset its Compose counterpart holds
|
||||
its cards off the edge by, so this is worth fixing.
|
||||
- [ ] **`scrollable_on(Axis::X)` on a non-editable `Text` draws nothing.**
|
||||
The panel is drawn and the text inside it is not. A markdown fence does
|
||||
the same to a `TextEdit` and is fine, so it is the widget kind rather
|
||||
than the chain. `tool.rs`'s `raw_block` is `masked()` only until this is
|
||||
fixed, which means a long command is clipped rather than pannable.
|
||||
- [ ] **No overflow ellipsis.** `TextAttrs` can wrap or not wrap; there is
|
||||
no "one line, ellipsised" the way `maxLines = 1` + `TextOverflow.
|
||||
Ellipsis` gives Compose. A tool card's summary is clipped instead, so
|
||||
nothing on screen says it was cut. Whichever end is cut has to be a
|
||||
choice when this lands: a path is identified by its tail, a command by
|
||||
its head.
|
||||
- [ ] **A drawn chevron.** `Chevron.kt` draws its own strokes precisely
|
||||
because a chevron from a font is a glyph a system font may not have --
|
||||
and the bundled `NotoSans-Regular.ttf` indeed has no U+25B8/25BE/25B4,
|
||||
while `NotoSansMono-Regular.ttf` does. `tool.rs` sets the mark in the
|
||||
monospace face as a result. A real fix needs a line/path primitive;
|
||||
iris has rects, text and textures only.
|
||||
- [ ] **A tool card's text is not selectable.** `Selection` is keyed
|
||||
`(RowKey, block index)` and a card has no markdown blocks, so nothing in
|
||||
a card registers. Compose's `SelectionContainer` covers tool output,
|
||||
which is the text people most want to copy. Needs a key for "the nth
|
||||
text of this row" that a card can mint without colliding with a
|
||||
message's blocks.
|
||||
|
||||
## Build (for the port)
|
||||
|
||||
Widgets `RUST.md`'s "The port, in order (decided 2026-09-05)" needs and
|
||||
|
||||
+107
-6
@@ -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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
Reference in new issue
Block a user