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:
1 parent
ba6817fee5
commit
b133d85943
3 files changed
+137
-9
No files matched your search
@@ -26,6 +26,7 @@ next (a Masonry or iris transcript screen, most likely).
|
||||
| `api.rs` | `Api.kt` | Partial -- see below |
|
||||
| `event_stream.rs` | `EventStream.kt` | Done |
|
||||
| `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 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
|
||||
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
|
||||
|
||||
- **`TranscriptSource.kt`** -- the layer that decides whether a page comes
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
A `TextBuffer` used to have exactly one style (`TextAttrs`: colour, size,
|
||||
|
||||
@@ -37,13 +37,18 @@ session spending an afternoon on them again.
|
||||
## Where things stand (2026-09-05)
|
||||
|
||||
- **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
|
||||
`iris/src/sense.rs` wired into `transcript-ui`'s selection; (b) E4 as a
|
||||
new `iris/desktop-app` crate on winit, embedding `transcript-ui` via
|
||||
`build_tree` beside a session list, with `client-core::config` holding the
|
||||
enrolment. Design choices for both are summarised in `DECISIONS.md` at
|
||||
the repo root, which is the file Iris reads for choices made without her.
|
||||
Next after those: I5's Android integration and the bench numbers.
|
||||
the I5 touch-drag pan-vs-select gap, as a `DragArbiter` in
|
||||
`iris/src/sense.rs` wired into `transcript-ui`'s selection. Design
|
||||
choices are summarised in `DECISIONS.md` at the repo root, which is the
|
||||
file Iris reads for choices made without her. Next after it: I5's
|
||||
Android integration and the bench numbers.
|
||||
- **E4 done, 2026-09-05.** `iris/desktop-app`: a winit window with a
|
||||
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
|
||||
keyboard gap — now explained, see below), E2 (a transcript in Masonry,
|
||||
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
|
||||
reached the most-recently-active session as a real message, not
|
||||
a mock.
|
||||
- [ ] **E4 — the same screen on the desktop** in a winit window, from the
|
||||
same crate, with only the layout differing.
|
||||
- [x] **E4 — the same screen on the desktop (2026-09-05).** A new
|
||||
`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
|
||||
proofs held on this checkout's own emulator: `adb install -r` of the
|
||||
xtask-built APK over the Gradle-built one succeeded, and the
|
||||
|
||||
Reference in new issue
Block a user