RUST.md, IRIS.md, IRIS_TODO.md: record I5 -- transcript-ui built and

partial, the recommendation's numbers still missing

I5's own box: the seven "hard to get back" behaviours each shown or
given a sourced reason, the exact verification commands and results,
and what remains (Android integration, touch-drag-vs-selection
arbitration, row accessibility names, a tappable link, code-span chip,
selection's anchor-row shortcut, code-fence syntax highlighting) --
each also a dated IRIS_TODO.md item so it is not silently dropped.
Ticked [~] rather than [x]: the widget-tree half is built and tested,
the emulator half is not.

"Where things stand" and the Recommendation's item 3 updated in place
to say plainly that neither Masonry (E2) nor iris (I5) has produced a
render number yet, and why -- not a bad measurement, no measurement
obtainable yet on either side -- with the structural findings that do
exist (iris now does cross-row selection and per-span inline rich text,
neither of which exists in masonry/masonry_core/xilem today) recorded
as what currently favours iris absent a number.

IRIS.md gets SpanStyle's own entry: what changed, why, and the one
thing a future TextBuilderOutput impl must remember (both TextOutput
and TextEditOutput apply .spans() -- this box shipped the bug of
missing one of the pair once already).

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
This commit is contained in:
irisandClaude Sonnet committed 2026-09-05 07:55:04 -04:00
1 parent 3f25e7ebca
commit b063fbd7f9
3 files changed
+347 -4

No files matched your search

+258 -4
View File
@@ -42,7 +42,31 @@ session spending an afternoon on them again.
below), **E3 (the Kotlin/Java shell over a JNI bridge into Rust, both
pass conditions proved on the emulator — see its own box)**, I0a, I0b
(iris builds on a pinned nightly and runs), I1 (parley + glyph atlas),
I2 (iris on android-view), I3 (`iris::widget::List`).
I2 (iris on android-view), I3 (`iris::widget::List`), I4 (host half).
- **I5 — the transcript screen in iris: partial, 2026-09-05 (ticked `[~]`
in its own box, not `[x]`).** `iris/transcript-ui/` builds a real
transcript screen — markdown-folded rows in `iris::widget::List`,
cross-row selection, a growing composer, tool-row expand-hold — on top
of a new, genuinely useful iris capability this box added:
**`SpanStyle`**, per-range text styling (`core/src/primitive/text.rs`),
which is what lets one wrapped, selectable `TextEdit` carry a heading,
bold, italic, inline code and a link all inside the same paragraph —
exactly the inline-rich-text ceiling E2 found Masonry structurally
unable to cross. Screenshotted via `run-headless.sh` (real inline
styling visible, not just block-level). 9 new tests, all passing;
`cargo build/clippy/fmt/test --workspace` and `cargo ndk` (both `iris`
and `transcript-ui`) all clean. **What did not happen this pass**: any
Android integration for this specific screen (no cdylib/Gradle shell
exists for it yet, unlike `tabs-ui`'s `iris-android-app`), and therefore
the emulator-side pass condition (`transcript-bench.sh` against the
Compose baseline, `ui-trace` tap-by-name on a row) — `emu list` showed
the one emulator here held by another session, but the real blocker is
that the integration work itself is unbuilt, not the emulator being
busy. Touch-drag panning over a row's own rendered text is also not yet
reachable, for a specific, diagnosed reason (it competes with this box's
own row-level drag-select for the same gesture) rather than an absent
primitive. Full accounting, every citation, and the dated
IRIS_TODO.md items are in I5's own box below.
- **E3 done, 2026-09-05, and unlike E1/E2 it is committed to this repo**
(`android-shell/` — a JNI-bridge crate on `client-core` — plus a new
Gradle module `app/shellApp/`, left deliberately separate from
@@ -591,6 +615,25 @@ light" has a knob inside the same stack.
carries the screen within the Compose baseline, it is the app's
framework and Masonry was the calibration. If it does not, the
measurement says which parts of Masonry to adopt underneath it.
**Not decidable yet, 2026-09-05 — what's missing, named rather than
guessed at.** Neither side of this comparison has a render number:
E2 found Masonry's own scroll gesture path absent on Android
entirely (its box, "measurable frames"), and I5 built the iris side of
the screen (`iris/transcript-ui/`) but not the Android integration
around it — no cdylib/Gradle shell exists for this screen yet (unlike
`tabs-ui`'s `iris-android-app`, I2), so there is nothing installed on a
device for `transcript-bench.sh` to measure against the Compose
baseline. What would close this: build that integration (real
`client-core` networking against `app/ui-sandbox.sh --delay`, a cdylib
+ Gradle module the way I2 did for `tabs-ui`), then run
`transcript-bench.sh`'s gesture on both. Until then, the decision rests
on the structural findings both sides *did* produce: Masonry cannot do
cross-row selection or per-span inline rich text at all today (E2's
`grep -rln`, zero hits, cited in its own box), and iris now does both
(I5's `SpanStyle` and `selection.rs`) as well as programmatic
touch-scroll (I3) — three structural points in iris's favour with no
opposing measurement yet on either side.
4. Then the shell (E3), the desktop window (E4) and the packaging (E5),
which do not depend on the choice.
@@ -1924,9 +1967,220 @@ silently on real hardware.
immediately after the first (attach, detach, attach again) and
confirm the process is still alive afterward -- the detach-abort
this box's mitigation exists for.
- [ ] **I5 — the transcript screen in iris.** E2's pass conditions, all
seven behaviours, against the sandbox with `--delay`. This is the
point the decision in the recommendation is made at.
- [~] **I5 — the transcript screen in iris (2026-09-05). The widget-tree
half is built, tested and screenshotted; the emulator half (real
device numbers against the Compose baseline) is not -- ticked
partial rather than done, see "What remains" at the end of this box.**
**Where it lives.** `iris/transcript-ui/` (new workspace member,
`[lib]`), the same shape as `iris/tabs-ui`: generic over `Rsc:
HasEvents` + `Rsc::State: FocusHost` so the same `build()` can run
under winit (`transcript-ui/examples/transcript.rs`) or an
android-view cdylib later. Depends on `client-core`/`event-model` by
path (real code, matching E2's precedent) and `pulldown-cmark`
(0.13.4, current stable). Four modules: `markdown.rs` (CommonMark ->
plain text + `Vec<SpanStyle>`), `row.rs` (one `iris::widget::List`
row per folded `TranscriptRow`), `selection.rs` (cross-row
selection), `composer.rs` (the growing input field). `lib.rs`'s own
module doc has the screen's shape and the one gap it documents up
front (below).
**New iris API, added in this box and recorded in `IRIS.md`:
`SpanStyle`, per-range text styling.** This is the actual answer to
RUST.md's E2 finding against Masonry ("rich inline text -- block-level
yes, inline no, and both for the same reason":
`masonry/src/widgets/text_area.rs:43-44`'s `TextArea::edit_styles()`
returns one `StyleSet` for the whole editor, with `// TODO:
RichTextInput` beside it). `core/src/primitive/text.rs`'s
`TextBuffer` gained `spans: Vec<SpanStyle>` and `set_spans`;
`SpanStyle{range, color, family, font_size, bold, italic,
underline}` pushes into parley's `RangedBuilder` via `.push(property,
range)` instead of only `.push_default(...)`, so one `TextEdit` can
carry a heading's bigger bold font, an inline-code span's monospace
colour, a link's colour+underline and an ordinary paragraph's base
style all in the *same* wrapped, selectable buffer.
`core/src/render/atlas.rs`'s `PlacedGlyph` gained a `color: UiColor`
field (read from parley's own per-run `Style::brush`,
`core/src/primitive/text.rs`'s `TextData::place`) and
`core/src/ui/painter.rs`'s `glyphs()` now colours each glyph from
that field instead of one colour for the whole `RenderedText` --
the change that actually makes a span's colour reach the screen.
**Real bug found and fixed while wiring this in**: `TextBuilder`'s
`.spans(...)` was only threaded through `TextOutput::run` (the
read-only `Text` widget), not the sibling `TextEditOutput::run` (the
`TextEdit` every transcript row actually uses) -- a "rule that
governs a set belongs to the set, not one member" miss, per
CODE_RULES.md; found because `run-headless.sh`'s screenshot showed
*no* styling at all despite `markdown.rs`'s own unit tests passing
(they only check the string/range logic, not the render path -- see
`iris/src/widget/text/build.rs`'s `TextEditOutput::run`, now fixed).
**The seven behaviours, each shown or given a sourced reason, same
structure as E2's own accounting:**
1. **Selection spanning rows -- shown, with a scoped shortcut
recorded rather than hidden.** `selection.rs`'s `Selection`
coordinates each visible row's own `TextEditCtx::select`/
`select_all`/`deselect` (already built for one field, I2) from a
single drag that crosses row boundaries: rows between the anchor
and the pointer get `select_all()`, the row under the pointer gets
a true partial selection from whichever edge faces the anchor,
and `selected_text()` concatenates the result in row order. The
one shortcut: the *anchor* row is selected in full once the drag
leaves it, rather than "from the click point to its far edge",
because that needs the row's own laid-out size and
`TextEditCtx`'s `layout()` helper is private
(`iris/src/widget/text/edit.rs`) -- see `selection.rs`'s module
doc. Pure range-membership logic (`in_range`, mirroring
`begin`/`extend`'s row-selection arithmetic) is unit-tested
without any render harness; the widget-level wiring is not
independently screenshotted this pass (would need a synthetic
drag injected into the winit example -- not attempted, time).
2. **Rich inline text -- shown, genuinely inline this time.**
`markdown::render_markdown` folds one row's whole markdown (not
one block at a time) into one string plus spans, so a heading, a
**bold** word, *italic* text, `inline code`, and a
[link](url) inside the same paragraph render in one `TextEdit`
that still wraps and selects as a single buffer --
screenshotted, see below. Deliberately not attempted, each
recorded at the point it would have gone in `markdown.rs`'s own
doc: a background chip behind inline code (needs glyph-run
geometry `TextEdit`-internal and not exposed, the same primitive
`TextEdit::draw`'s selection highlight uses,
`iris/src/widget/text/edit.rs:99`), a tappable link (same missing
primitive), a real table layout, and per-token syntax colour
inside a fence.
3. **Bottom-anchored virtualised list, hold-the-edge on expand --
shown**, reusing I3's `List` unmodified. A `TranscriptRow::Tools`
row collapses to "N tool calls" and expands to every call's own
tool/input/output on tap; `row.rs`'s click handler calls
`List::extent(key)` to convert the tap's row-local position into
the viewport-relative position `List::note_tap` wants, exactly
the two-step contract `list.rs`'s module doc describes for
`holdTopEdge`. Not independently screenshotted mid-expand this
pass (no input-injection into the desktop example was built) --
the mechanism is the same one I3 already benchmarked
(`expand-hold`, flat at 0.10-0.11ms across N), applied to real
content instead of a synthetic row.
4. **The soft keyboard -- inherited from I2, not re-investigated.**
The composer (`composer.rs`) is an ordinary `TextEdit` with the
same `InputConnection` bridge I2 built and measured (Gboard
suggestions over real buffer content); nothing new to add here,
and no Android shell exists yet for this screen specifically to
re-verify it against (see "What remains").
5. **Platform integration -- out of scope by design**, same as E2:
E3's list, not this box's.
6. **Accessibility names -- shown for the composer, not yet for
rows.** The composer field carries `.label("Message")` (I4). Rows
do not yet carry per-row labels (a row's own text *is* its
accessible content via `TextEdit`'s `access_role`, I4, but
nothing calls `.label()` on it, so `Widgets::named()` does not
include it) -- a small, real gap, recorded as an IRIS_TODO.md
item rather than silently left, since AGENTS.md's bench scripts
depend on exactly this for driving a screen by name.
7. **Measurable frames / the render-number pass condition -- not
attempted, and unlike E2 the reason is not an absent gesture
path.** `List` demonstrably scrolls (I3's flat draws/moves,
programmatic `scroll()`) and mouse-wheel scrolling is wired here
(`lib.rs`'s `CursorSense::Scroll` on `list`). What is *not*
reachable yet is a **touch-drag pan starting on a row's own
text**: `row.rs` registers `CursorSense::click_or_drag()` on each
row's `TextEdit` for selection, and `TextEdit::draw` calls
`painter.child_layer()` (`iris/src/widget/text/edit.rs:87`), so
`core/src/sense.rs`'s `run_sensors` (which stops at the first
layer, checked innermost-first, that consumed the gesture) gives
that row first refusal on *every* frame it is pressed, not just
the frame the press started -- a row's drag-select wins the same
gesture a list-level pan would want. `lib.rs`'s own module doc
states this precisely, with the fix named (a press distance/time
arbiter deciding pan vs. select before either commits, or gating
text-drag-selection behind a long-press). This is a genuine,
diagnosed architecture gap this box's *own* two features created
by both wanting the same gesture -- not a missing primitive the
way Masonry's absent `on_pointer_event` drag handling was.
**Verification, exact commands and results (2026-09-05, this VM):**
- `cargo fmt --all -- --check`: clean.
- `cargo build --workspace --all-targets`: clean, all six workspace
members (`iris`, `iris-core`, `iris-macro`, `tabs-ui`,
`transcript-ui`, plus the excluded `android-app`).
- `cargo clippy --all-targets` and `cargo clippy -p transcript-ui
--all-targets`: zero warnings.
- `cargo test --workspace`: 28 tests in `iris`/`iris-core` (all
pre-existing, unaffected) + **9 new in `transcript-ui`** -- 5 pure
markdown tests (`bold_and_italic_produce_spans_over_the_right_range`,
`heading_gets_a_bigger_font_size_span`,
`link_is_styled_and_keeps_its_visible_text`,
`fenced_code_block_is_monospaced`, a plain-text baseline) and 4
selection tests (forward/backward/single-row range arithmetic,
plus `unregister_forgets_the_row_and_clears_a_matching_anchor`
against a real minimal `TextEdit` in the arena, no window needed --
same harness style as `list.rs`'s own tests).
- `cargo ndk -t x86_64 -P 26 build -p transcript-ui` and `... clippy
-p transcript-ui --lib`: clean (`--lib` only -- the example uses
`iris::default`, winit-only by design, same as `iris/examples/
tabs`'s own example never having an Android build of itself; the
Android-facing entry point is a separate cdylib, not built this
pass, see below). `cargo ndk ... build -p iris` / `clippy -p iris`
also re-checked clean, since this box touched `iris-core`'s text
pipeline.
- `run-headless.sh transcript --shot ... -- -p transcript-ui`:
renders. Cropped for legibility (this VM has no image viewer --
see I3's own note on the same limitation and the throwaway crop
tool used here, not committed): a full conversation with a
**bold** word, *italic* text, `inline code` in its own colour, a
`# Sure` heading rendered visibly larger and bold, a coloured link,
a monospaced fenced code block, a collapsed "▸ 3 tool calls" row,
and the composer bar at the bottom -- every one of E2's markdown
screenshot's features, now inline within single paragraphs rather
than block-per-widget. Screenshots at `/tmp/iris_i5_transcript2.png`
(full) and crops there, not committed per the standing rule against
screenshots of real content leaving this repo -- these are
synthetic rows, but the rule is kept uniform regardless.
**What remains, named rather than silently dropped (also in
IRIS_TODO.md, dated 2026-09-05):**
- **The emulator half of the pass condition was not attempted.**
`emu list` shows `emulator-5554` (AVD `ai-app`, a different
checkout) held by another session during this pass, but even with
a free emulator this needs real Android integration that does not
exist yet for this screen: a cdylib + Gradle shell the way
`iris-android-app` wraps `tabs-ui` (I2), real
`client-core::ApiClient`/`event_stream::follow_session_events`
wiring against `app/ui-sandbox.sh` with `--delay` (this crate
deliberately does not fetch anything itself, see `lib.rs`'s doc),
and then `transcript-bench.sh`'s gesture compared against the
Compose baseline. That is real, multi-part follow-on work in its
own right -- closer in size to E2/E3 than to "run one more
script" -- not something this pass's remaining time could
responsibly rush and still report honestly.
- **Touch-drag panning over a row's own text** -- behaviour 7 above.
- **Row-level accessibility names** -- behaviour 6 above.
- **A tappable link and a code-span background chip** -- behaviour 2.
- **`Selection`'s anchor-row shortcut** -- behaviour 1.
- **No syntax highlighting inside a fenced code block** -- `markdown.rs`
notes `client_core::highlight` exists and could feed this.
- **`row.rs`'s tool-row expand and `selection.rs`'s cross-row drag
are not independently screenshotted/driven** -- covered by reading
and by the primitives they reuse (I3's `List` tests, this box's
own unit tests), not by a dedicated repro this pass.
**Net for the recommendation.** Item 3 ("decide when the transcript
screen exists in both, from the measurements") still cannot be
decided by a number -- E2 could not produce one for Masonry, and I5
has not yet produced one for iris either, for an unrelated reason
(no Android harness built yet, not an absent capability). What *can*
be said structurally, updating E2's own conclusion: iris now also
demonstrates the two things E2 found Masonry structurally unable to
do at all -- cross-row selection and true per-span inline rich text
inside one wrapped, selectable buffer -- neither of which exists
anywhere in `masonry`/`masonry_core`/`xilem` today (E2's own
`grep -rln` finding). That is a second structural point in iris's
favour, alongside I2's working touch-scroll-vs-Masonry's-absent one,
still short of the render-number comparison the recommendation
ultimately wants.
## For the next agent