RUST.md, IRIS.md, CLIENT_CORE.md: record E4 done

RUST.md: E4 ticked with the screenshot path, the exact commands against
app/ui-sandbox.sh, and the streaming-duplication bug the screenshot found;
"Where things stand" moved E4 out of "in flight" into its own done bullet.
IRIS.md: transcript_ui::build_tree, the public API change transcript-ui
gained for this. CLIENT_CORE.md: client_core::config's table row and its
correspondence note.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-05 12:55:59 -04:00
1 parent ba6817fee5
commit b133d85943
3 files changed
+137 -9

No files matched your search

+16
View File
@@ -26,6 +26,7 @@ next (a Masonry or iris transcript screen, most likely).
| `api.rs` | `Api.kt` | Partial -- see below | | `api.rs` | `Api.kt` | Partial -- see below |
| `event_stream.rs` | `EventStream.kt` | Done | | `event_stream.rs` | `EventStream.kt` | Done |
| `transcript_fold.rs` | `TranscriptItems.kt`, `ToolRows.kt` | Partial -- see below | | `transcript_fold.rs` | `TranscriptItems.kt`, `ToolRows.kt` | Partial -- see below |
| `config.rs` | `ServerConfig.kt`'s `handleEnrollment` | New, desktop-only so far -- see below |
| *(not started)* | `TranscriptSource.kt` | Not started | | *(not started)* | `TranscriptSource.kt` | Not started |
| *(not ported, and may never be)* | `TranscriptUnits.kt` | Out of scope -- see below | | *(not ported, and may never be)* | `TranscriptUnits.kt` | Out of scope -- see below |
@@ -103,6 +104,21 @@ deciding how `event_model` itself represents "a shape I don't recognise"
-- a shared-model decision affecting `server/` too, not a `client-core`-only -- a shared-model decision affecting `server/` too, not a `client-core`-only
fix, so it is recorded here rather than silently worked around. fix, so it is recorded here rather than silently worked around.
## `config.rs`: `EnrolledServer`
`EnrolledServer` (host, port, bearer token) plus `parse_link`, which reads
the exact `aiapp://enroll?host=H&port=P&token=T` deep link
`wg-app-link`'s `enroll` mints and `ServerConfig.kt`'s `handleEnrollment`
parses on the phone -- so any Rust client enrols from the same text a
phone would scan as a QR, with no second format invented for it (RUST.md's
E4, DECISIONS.md 2026-09-05). Deliberately does not decide where it is
persisted or under what file permissions -- a phone seals its token in the
Android Keystore, `iris/desktop-app/src/config.rs` writes it to
`$XDG_CONFIG_HOME/ai-app-desktop/enrollment.json` at 0600 -- since that is
caller-specific (the code rules' "ask for the least you need"). Its only
caller today is `desktop-app`; a future Android build of this crate would
be a second one, not a reason to move the type.
## What is not started at all ## What is not started at all
- **`TranscriptSource.kt`** -- the layer that decides whether a page comes - **`TranscriptSource.kt`** -- the layer that decides whether a page comes
+22
View File
@@ -8,6 +8,28 @@ 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 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. it helps judge the change without the session that made it. Newest first.
## 2026-09-05: `transcript_ui::build_tree` (RUST.md's E4)
`transcript_ui::build` claimed the whole window (`ui_state.set_root(tree)`)
as its last step, which is right for a window that *is* the transcript
screen (the winit example, an eventual Android cdylib) and wrong for the
desktop app, which puts a session list beside it. `build_tree` is `build`
minus that last step: it returns `(TranscriptScreen, StrongWidget)` instead
of just `TranscriptScreen`, and the caller decides where the tree goes —
into `ui_state.set_root`, or into a `WidgetPtr` alongside something else
(`iris/desktop-app`'s `rebuild_transcript`). `build` is now one line calling
`build_tree` and doing the `set_root` itself, so existing callers are
unaffected.
```rust
// before, and still available, for a caller that wants to *be* the window:
let screen = transcript_ui::build(rsc, &mut ui_state, rows);
// new, for a caller embedding the screen beside something else:
let (screen, tree) = transcript_ui::build_tree(rsc, rows);
some_widget_ptr(rsc).set(tree);
```
## 2026-09-05: `SpanStyle`, per-range text styling (RUST.md's I5) ## 2026-09-05: `SpanStyle`, per-range text styling (RUST.md's I5)
A `TextBuffer` used to have exactly one style (`TextAttrs`: colour, size, A `TextBuffer` used to have exactly one style (`TextAttrs`: colour, size,
+99 -9
View File
@@ -37,13 +37,18 @@ session spending an afternoon on them again.
## Where things stand (2026-09-05) ## Where things stand (2026-09-05)
- **In flight, 2026-09-05 (session cleared mid-work, picked up again):** - **In flight, 2026-09-05 (session cleared mid-work, picked up again):**
(a) the I5 touch-drag pan-vs-select gap, as a `DragArbiter` in the I5 touch-drag pan-vs-select gap, as a `DragArbiter` in
`iris/src/sense.rs` wired into `transcript-ui`'s selection; (b) E4 as a `iris/src/sense.rs` wired into `transcript-ui`'s selection. Design
new `iris/desktop-app` crate on winit, embedding `transcript-ui` via choices are summarised in `DECISIONS.md` at the repo root, which is the
`build_tree` beside a session list, with `client-core::config` holding the file Iris reads for choices made without her. Next after it: I5's
enrolment. Design choices for both are summarised in `DECISIONS.md` at Android integration and the bench numbers.
the repo root, which is the file Iris reads for choices made without her. - **E4 done, 2026-09-05.** `iris/desktop-app`: a winit window with a
Next after those: I5's Android integration and the bench numbers. session list beside `transcript-ui`'s screen (`build_tree`), against a
real `ai-server` through `client-core`, enrolled from the same
`aiapp://enroll?...` link a phone scans. Both pass conditions held on
`app/ui-sandbox.sh` -- see E4's own box for the commands, the
screenshot, and a real streaming-duplication bug the screenshot found
and a regression test now covers.
- **Done**: E0 (toolchain), E1 (Masonry on android-view, which found the - **Done**: E0 (toolchain), E1 (Masonry on android-view, which found the
keyboard gap — now explained, see below), E2 (a transcript in Masonry, keyboard gap — now explained, see below), E2 (a transcript in Masonry,
which found that Masonry has no touch-scroll on Android at all — see which found that Masonry has no touch-scroll on Android at all — see
@@ -1299,8 +1304,93 @@ accepted.
errors."}` followed by the echo driver's reply -- the share errors."}` followed by the echo driver's reply -- the share
reached the most-recently-active session as a real message, not reached the most-recently-active session as a real message, not
a mock. a mock.
- [ ] **E4 — the same screen on the desktop** in a winit window, from the - [x] **E4 — the same screen on the desktop (2026-09-05).** A new
same crate, with only the layout differing. `iris/desktop-app` crate (added to the `iris` workspace's members, not
excluded the way `android-app` is -- nothing here needs the NDK):
a real winit window showing a session list (`iris::widget::Span`,
rebuilt on selection) beside `transcript-ui`'s screen
(`transcript_ui::build_tree`, new this box -- see IRIS.md's
2026-09-05 entry), talking to a real `ai-server` through
`client-core`'s `ApiClient`/`UreqTransport`/`follow_session_events`.
Enrolment is `client_core::config::EnrolledServer::parse_link`
against the same `aiapp://enroll?host=H&port=P&token=T` link a phone
scans, pasted via `--link` and persisted at
`$XDG_CONFIG_HOME/ai-app-desktop/enrollment.json` (0600 --
`iris/desktop-app/src/config.rs`); the pinned CA is a `--ca PATH`
argument, never baked in (DECISIONS.md, 2026-09-05).
*Both pass-condition proofs held, against `app/ui-sandbox.sh`'s real
server.* (1) The list showed the sandbox's spawned session
("Demo session", its live status); selecting it loaded the real
transcript and the composer's `Submit` posted a message whose reply
streamed in live over SSE, both proved by two `run-headless.sh`
screenshots taken seconds apart around a real `./ui-sandbox.sh send`
-- the second showed the new turn appended under the first with
nothing duplicated or lost. (2) Screenshotted headless:
`/tmp/iris_e4_desktop.png` (1920x1200, 15.9 KB, the real first-run
state -- list populated, "Select a session." on the right, nothing
selected yet). `run-headless.sh` gained a `--bin` flag for this
(`cargo build --bin NAME` + `target/debug/NAME` instead of the
`--example` path, since `desktop-app` is a real binary a person
runs, not a demo) and `$RUN_HEADLESS_ARGS`, word-split into the
launched binary's own argv (a real CLI's flags, which no example
needed a way to pass before). Exact commands, from `iris/`:
TOKEN=$(cat "${XDG_CONFIG_HOME:-$HOME/.config}/ai-app/sandbox-token")
LINK="aiapp://enroll?host=127.0.0.1&port=<PORT>&token=$(python3 -c \
'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$TOKEN")"
CA="${XDG_CONFIG_HOME:-$HOME/.config}/ai-app/certs/ca.pem"
RUN_HEADLESS_ARGS="--ca $CA --link $LINK" \
./run-headless.sh desktop-app --bin --shot /tmp/iris_e4_desktop.png -- -p desktop-app
**A real bug this screenshot found, not a synthetic one**: the first
attempt resumed the live SSE stream from
`items.iter().map(TranscriptItem::seq).max()` -- the *folded* item's
seq, which for a still-open `AssistantMsg` is the seq of its
*first* delta by design (`fold_event`'s own doc comment: "a row
whose identity changed with every delta would be a new row every
frame"). Resuming from there re-delivered every delta already
folded into that message, and the screenshot showed the assistant's
reply with its own tail duplicated ("You said: ... testsaid: ...
test"). Fixed by computing the resume cursor from the raw wire
`seq` of the last fetched line (`app.rs`'s `raw_seq`) instead of
from any folded item -- regression test
`the_resume_cursor_is_the_last_wire_seq_not_the_last_items_seq` in
`iris/desktop-app/src/app.rs`. Exactly the class of bug CODE_RULES
warns about under "a fix tried only on what it was meant to fix":
the bare REST fetch (no live stream yet) looked perfect on its own,
and only *resuming* a stream after it exposed the seam.
**Deliberately left simple, and why** (`app.rs`'s module doc has the
full account): every incoming SSE event refolds the session's whole
item list and rebuilds the entire right-hand widget tree from
scratch, rather than reaching for `TranscriptScreen::push_row`'s
incremental append -- `push_row` can only add a new row, and a
streaming reply is exactly a row whose text keeps changing after it
first appears. Fine at the size a desktop session's conversation
is; wrong for a long, fast-streaming one, and the real fix needs
`transcript-ui` to expose updating a row already on screen, which it
does not yet. The composer's in-progress text is saved and restored
across a rebuild so a reply streaming in while the reader is typing
a followup doesn't erase it. No history paging (I3's job, reused
as-is if this becomes permanent) and no scroll-position preservation
across a rebuild -- both named rather than silently missing.
Background network I/O runs on plain `std::thread`s reporting back
through winit's `EventLoopProxy<AppEvent>` rather than iris's own
`Tasks`/`task_on`, because `Tasks` only requests a redraw once after
its whole async closure finishes, which fits "one request, one
update" and not a live stream that needs a redraw after *each*
event it relays.
Verification: `cargo fmt --all`, `cargo clippy --workspace
--all-targets` (zero warnings), `cargo test --workspace` from
`iris/` (7 new tests in `desktop-app` -- 4 for
`config.rs`'s save/load/permissions/corruption, 3 for `app.rs`'s
transcript folding and the resume-cursor regression above -- plus
the existing 37 unchanged), and `./run-tests.sh` at the repo root
(127 passing, `client-core` alone 93 -- the `EnrolledServer` parsing
tests already existed before this box). Android is untouched by
this step, as asked.
- [x] **E5 — the packaging xtask (2026-09-05).** Both pass-condition - [x] **E5 — the packaging xtask (2026-09-05).** Both pass-condition
proofs held on this checkout's own emulator: `adb install -r` of the proofs held on this checkout's own emulator: `adb install -r` of the
xtask-built APK over the Gradle-built one succeeded, and the xtask-built APK over the Gradle-built one succeeded, and the