RUST.md, DECISIONS.md, IRIS_TODO.md: the port plan, P1-P7, after iris-over-Masonry

Adds "The port, in order (decided 2026-09-05)" to RUST.md: seven ordered
steps building the app on iris now that the framework is decided, each
naming the Kotlin files it replaces, the client-core pieces it needs
(and which are not yet covered and must be ported first), the missing
iris widgets it needs (recorded in IRIS_TODO.md's new "Build (for the
port)" section), and a pass condition a later agent can run. Ordered by
risk to the daily-use path: session screen parity, then the shell merge
and a real phone install, then root tabs, the file explorer,
settings/enrolment, desktop parity, and the cutover itself.

Crate-shape decision recorded in DECISIONS.md: one UI crate, app-ui,
grown out of transcript-ui rather than started beside it, with
desktop-app/android-app as thin entry points over it and platform-only
code staying in the E3/E5 Java shell.

Updates RUST.md's "Where things stand" and "For the next agent" to point
at P1 rather than the now-closed framework decision.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-05 20:30:02 -04:00
1 parent 621f08d725
commit 5b7dc0e4e2
3 files changed
+298 -4

No files matched your search

+22
View File
@@ -7,6 +7,28 @@ marked **DEFERRED** are ones the agent chose not to decide alone.
## 2026-09-05
- **The rest of the port is one UI crate, `iris/app-ui`, grown out of
`iris/transcript-ui` rather than started beside it.** It holds a
`Screen` enum plus a back stack — the Rust equivalent of `AppRoot.kt`'s
`when` — and `iris/desktop-app`/`iris/android-app` become thin entry
points over it. Chosen over a fresh crate because `transcript-ui`
already has the right generic shape (`Rsc: HasEvents` +
`Rsc::State: FocusHost`) and the `client-core`/`event-model` path
dependencies every later screen needs, so growing it in place is the
smaller diff. Platform-only code (notification service, share target,
QR scanner, Keystore token, deep-link enrolment) stays in the E3/E5
Java shell (`android-shell/` + `app/shellApp`) rather than moving into
this crate, since none of it is a screen. The Android APK is built by
`cargo xtask apk` (E5), merging the app-ui cdylib into the E3 shell so
there is one app rather than a demo shell plus a service shell.
`app/androidApp` (the Compose app) stays untouched and is the baseline
every step is measured against, until parity is reached (P7 decides
the switch, and is itself a load-bearing decision left to Iris). Order
is by risk to the daily-use path: session screen first (P1, where
every hard behaviour already lives), then the shell merge and a real
phone install (P2), then root tabs (P3), the explorer (P4),
settings/enrolment (P5), desktop parity (P6), and the cutover itself
(P7). Full plan: RUST.md's "The port, in order (decided 2026-09-05)".
- **iris gets its own measured frame report, rather than waiting on a
`dumpsys`/`gfxinfo` answer that cannot see a `SurfaceView`'s GPU-drawn
frames.** `iris_core::FrameReport` (`iris/core/src/render/frame_report.rs`)
+36
View File
@@ -292,6 +292,42 @@ order and what "done" looks like. Tick and date them in place.
everything, the same way input is**. Whatever the mechanism, a widget
that does not animate must pay nothing and import nothing for it.
## Build (for the port)
Widgets `RUST.md`'s "The port, in order (decided 2026-09-05)" needs and
iris does not have yet, one entry per gap, named against the P-step that
first needs it. Move an entry up to "Fix" or tick it in place once built;
do not duplicate it there.
- [ ] **A history-paging cushion measured in on-screen viewports, not a
row count.** (**P1**.) `iris::widget::List` has no equivalent of the
Compose app's `HISTORY_SCREENS` — AGENTS.md's "Things that have
bitten" is explicit that a fixed row count under-fills a screen on a
tool-heavy transcript and over-fills one on a text-heavy one, so
whatever loads the next page has to ask the list how many viewports
are actually on screen, not assume a constant.
- [ ] **A scaled thumbnail/image widget for an in-transcript image.**
(**P1**.) `SessionImage.kt`'s bitmap decode-and-downscale has no iris
counterpart; iris's own image widget (used by `bench_images.rs`) draws
a loaded texture but does nothing about sourcing or scaling one from a
server-produced attachment.
- [ ] **A modal/dialog primitive.** (**P1**, reused by **P3** and
**P5**.) Needed for the session settings dialog, `UsageDialog`'s
equivalent, and the delete-with-`deleteForeign` confirmation with its
toggle switch. Build once, wherever it is first needed, rather than
once per screen that wants one.
- [ ] **A horizontal gauge/bar widget.** (**P1**.) For
`SessionUsageBar`'s equivalent — a bounded fill reflecting a fraction,
nothing fancier.
- [ ] **A `BusyItem` equivalent: a dimmed row carrying an operation
label that does not block its list's own scroll/drag.** (**P3**.) The
Compose version tried an overlay first and it swallowed the drag along
with the tap (AGENTS.md's "Shared appearance") — worth not repeating
that attempt in iris before building the row-level version directly.
- [ ] **A toggle switch.** (**P3**.) For the delete dialog's
`deleteForeign` control; iris has no switch/checkbox widget yet as far
as this pass found.
## Reconsider
- [ ] **`WidgetView`.** Iris is unsure of it: what she wants is an easy way
+240 -4
View File
@@ -40,6 +40,12 @@ session spending an afternoon on them again.
numbers in I5's box and E1/E2's findings. See the Recommendation's item
3 and `DECISIONS.md`. Next: the remaining screens and the app on iris —
a new ordered list is the next thing to write into this file.
- **The port plan exists, 2026-09-05: "## The port, in order (decided
2026-09-05)"**, seven steps (P1P7) below "Experiments, in order,"
ordered by risk to the daily-use path rather than by screen count. **P1
— session screen parity — is next.** One crate decision made there:
screens grow out of `iris/transcript-ui` into `iris/app-ui`, with
`desktop-app`/`android-app` as thin entry points over it.
- **I5 is now `[x]`: a clean, single-session, like-for-like 24-swipe
scroll comparison between Compose and iris exists, 2026-09-05.** Same
sandbox session content for both apps, same emulator, `EMU_GPU=software`
@@ -3195,6 +3201,236 @@ silently on real hardware.
`e2a1fad`'s own message. `docs/DECISIONS.md`'s DEFERRED item is
updated with this section's host-GPU table below.
## The port, in order (decided 2026-09-05)
Iris decided iris over Masonry (`DECISIONS.md`). This is the ordered plan
for the rest of the app, decided by the design agent per the standing
"decide technical questions yourself" instruction — a serious
user-facing tradeoff is not in play in the ordering itself, so it is not
deferred to her. **Crate shape, decided here**: the screens live in one
crate, **`iris/app-ui`**, grown from `iris/transcript-ui` rather than
started beside it — `transcript-ui` already has the right generic shape
(`Rsc: HasEvents` + `Rsc::State: FocusHost`, the same axis `tabs-ui`
varies along) and the same `client-core`/`event-model` path
dependencies every later screen needs, so growing it in place is a
rename plus new modules rather than a second crate re-declaring
dependencies the first already has. It holds a `Screen` enum and a back
stack — the direct equivalent of `AppRoot.kt`'s `when` and `MainScreen.kt`'s
tab `enum` — with each Compose screen becoming one `iris::widget`
subtree module. `iris/desktop-app` (E4) and `iris/android-app` (I2/I5)
become thin entry points that call into `app-ui`, the way `AppRoot`/
`MainActivity` today call into Compose screens they don't otherwise own.
Platform-only code (the notification foreground service, the share
target, the QR scanner, the Keystore-sealed token, deep-link enrolment)
stays exactly where E3/E5 already put it — `android-shell/` and
`app/shellApp` — since none of it is a screen `app-ui` could draw.
Order is by **risk to the daily-use path**, not by screen count: the
session screen is what the app is for and where every hard behaviour
(paging, cache, keyboard insets, selection) already lives, so it goes
first and on the phone as reachable code as soon as possible, before the
lower-risk screens.
Every step below assumes the `app/ui-sandbox.sh` fixtures (AGENTS.md's
"The rigs") and the `this-machine-android` skill's facts (per-checkout
AVD, `ui-trace` by accessibility name, GrapheneOS phone quirks, the
`adb shell` quoting traps) apply unchanged — read that skill before
running any pass condition below that touches an emulator or a real
device.
- [ ] **P1 — session screen parity.** History paging backward (with the
page-boundary healing `client-core` does not have yet, below),
`TranscriptSource`-backed cache/server stitching, jump-to-latest,
tool-call cards and grouping, the session settings dialog, composer
attachments, and the keyboard/insets behaviours AGENTS.md's "Things
that have bitten" names (the floating-composer bug, `adjustResize`,
the `imePadding`-vs-raw-inset rule). This is the highest-risk step:
it is the screen the app is used for, every hour of the day.
**Kotlin it replaces**: `SessionScreen.kt`, `TranscriptList.kt`,
`SessionSettingsDialog.kt`, `ToolInput.kt`, `ToolRows.kt`,
`AskQuestion.kt`, `Compaction.kt`, `SessionUsageBar.kt`,
`PendingAttachments.kt`, `Attachment.kt`, `Attachments.kt`,
`SessionImage.kt`, `MemoryNote.kt`, `PeerMessage.kt`, `RawBlock.kt`,
`CodeFence.kt`, `MarkdownLinks.kt`, `MarkdownPieces.kt`,
`Markdown.kt`, `Bubble.kt`, `ScrollAnchor.kt`, `Drafts.kt`,
`UsageDialog.kt`, `Chevron.kt`, `Dividers.kt`. (`transcript-ui`
already covers the row/markdown/selection/composer core these sit
on top of or beside.)
**`client-core` needed, and what is not yet covered and must be
ported first** (`CLIENT_CORE.md`): `TranscriptSource.kt` (deciding
cache vs. server per page and stitching them — "not started"),
`TranscriptItems.kt`'s `joinPages`/`healSplitMessage`/`adoptRun`
(page-boundary healing — "not ported," and paging backward is
exactly what exercises it), the markdown *block* model beyond
syntax spans (headings/lists/tables/fences as distinct nodes —
"not started," needed for `CodeFence`/`MarkdownPieces`' equivalents),
and the attachments route (`/sessions/{id}/attachments` — "not
covered" in `api.rs`, needed for `PendingAttachments`/`Attachment`).
**iris widgets missing, → `IRIS_TODO.md`'s new "Build (for the
port)" section**: row-level accessibility names and the tappable
link / background-chip primitive (both already listed under I5's
leftovers — this step is what needs them, not a new ask); a
history-paging cushion measured in on-screen viewports rather than
a row count (the `HISTORY_SCREENS` lesson in "Things that have
bitten," which iris's `List` has no equivalent of yet); a scaled
thumbnail/image widget for `SessionImage`'s in-transcript images; a
modal/dialog primitive for the session settings dialog and
`UsageDialog` (iris has none today — check before building a second
one for P3/P5); a horizontal gauge/bar widget for
`SessionUsageBar`.
**Pass condition**: `app/ui-sandbox.sh`'s fixtures driven by
`ui-trace record --do "tap '<label>'"` — a session with the big
transcript (`AI_SANDBOX_BIG_MB`), a paused/slow-spawning one
(`AI_SANDBOX_SPAWN_DELAY`), and `--delay` on the server — exercising
the four states UI_RULES.md says to design first: unknown (a page
that hasn't loaded), empty (a session with no messages yet), error
(a failed send/interrupt), and too-long (the big transcript,
paged). Re-take the I5 `FrameReport` (`iris frame report` in
logcat, same as I5's box) once this screen has real paging and
compare it against I5's own numbers, not against Compose's — the
three measurement sources still are not comparable per
`DECISIONS.md`'s DEFERRED item.
- [ ] **P2 — the shell merge and a real phone install.** Merge this
screen's cdylib into the E3/E5 shell (`android-shell` +
`app/shellApp`) behind the same feature-flag pattern I5 used to
extend `iris-android-app` (`DECISIONS.md`, 2026-09-05), so there is
one app — notification service, share target and the real screen —
rather than a demo shell and a service shell side by side. Package
with `cargo xtask apk` (E5) and get it onto the real GrapheneOS
phone, not just the emulator: `arm64-v8a` is the ABI that matters
there (the emulator here is x86_64), and the `this-machine-android`
skill's facts apply for the first time in this port — no System
Tracing on that phone (frame numbers have to come from `FrameReport`
itself), the local-network permission is required there even though
AOSP's docs say VPN traffic is excluded, and `ui-trace`/`adb`
target this checkout's own emulator by default so a real-device
command needs `-s <serial>` explicitly.
**Kotlin it replaces**: nothing further than E3 already did
(`Notifications.kt` → `notifications.rs`, `Share.kt` → `share.rs`,
`ServerConfig.kt`'s Keystore half → JNI calls into `wg-app-link`) —
this step is wiring P1's screen in as the shell's real content
instead of E3's placeholder, plus getting a signed APK onto a
physical device for the first time in this port.
**`client-core` needed**: none new; E3 already covers what the
shell itself needs. Attachments (P1's gap) matter here too if a
real photo share is exercised.
**iris widgets missing**: none — this step is integration, not new
widgets.
**Pass condition**: `cargo xtask apk`, install on the real phone
over adb, enroll via the deep link, background the app and get a
real notification, share a text snippet into a session, and
confirm `ui-trace` can still find controls by name on real
hardware (accessibility names are not guaranteed to survive a real
device's TalkBack/AccessKit wiring the way they do in the
emulator — this is the first time that gets checked for real).
- [ ] **P3 — root tabs.** `Screen`/`MainTab` in `app-ui`: the sessions
list, import, models and setups tabs, plus spawn and the app's one
level of back-stack navigation (`AppRoot.kt`'s `when`).
**Kotlin it replaces**: `AppRoot.kt`, `MainScreen.kt`,
`SessionListScreen.kt`, `ImportScreen.kt`, `ModelsScreen.kt`,
`SetupsScreen.kt`, `SpawnScreen.kt`, `BusyItem.kt`,
`UniqueItems.kt`, `SessionAlerts.kt`.
**`client-core` needed, not yet covered**: setups/machine/provider
discovery, the models routes (`/models*`, HuggingFace browsing and
downloads), and importing (`/setups/{id}/importable*`) — all three
listed "not covered" in `api.rs`'s table and none started; each is
real work, not a stub, per `CLIENT_CORE.md`'s own caveat.
**iris widgets missing**: a `BusyItem` equivalent — a row dimmed,
drained of colour, labelled with the operation in progress, that
does **not** block the list's own scroll/drag the way an overlay
did on the Compose side (AGENTS.md's "Shared appearance"); a
`uniqueItems` equivalent is logic, not a widget, and ports directly
into `app-ui` itself; a confirmation dialog with a toggle switch,
for the delete-with-`deleteForeign` flow, needs the same modal
primitive P1 flagged — build it once, here or in P1, whichever
lands first.
**Pass condition**: `ui-trace` tap-by-name on all four tabs against
`ui-sandbox.sh`'s fixtures; the two-copies-of-one-session-id
fixture (AGENTS.md's "Importing") does not crash the list — this is
the regression `uniqueItems` exists for and it must be exercised
here, not assumed; the delete dialog's paragraph reads correctly
both with and without `deleteForeign` toggled (its own text, not
appended, per AGENTS.md).
- [ ] **P4 — file explorer.** The viewer, the editor with its
`EDIT_LIMIT`, and the 409 conflict.
**Kotlin it replaces**: `FilesScreen.kt`, `FileViewer.kt`,
`FileEditor.kt`, `FileLines.kt`.
**`client-core` needed, not yet covered**: `/setups/{id}/dir|file`
— not in `api.rs`'s covered list, real work, port first.
**iris widgets missing**: nothing beyond what P1 needs (a
virtualised line-numbered text view is `iris::widget::List` reused,
per I3's box) — the open question is whether the editor's
`BasicTextField`-equivalent cost (`docs/EXPLORER.md`'s "what the
measurements said") reproduces in iris's `TextEdit` at the same
`EDIT_LIMIT`, which this step has to re-measure rather than assume.
**Pass condition**: `app/ui-sandbox.sh`'s `~/files` fixture tree
(empty dir, tab/apostrophe names, binary, over `FILE_LIMIT`,
`chmod 000`, symlinks good and broken, one source file per
language, `edit-32k.rs`/`edit-128k.rs`/`big-source.rs`) driven by
name; the 409 reproduced by editing the file on the machine between
opening it and saving, per AGENTS.md's own recipe.
- [ ] **P5 — settings, enrollment, notifications permission.**
**Kotlin it replaces**: `SettingsScreen.kt`, `ServerConfig.kt`'s
remaining non-Keystore parts, `DebugStats.kt`, `FrameStats.kt`,
`CrashLog.kt`. The QR scanner (`EnrollmentScanActivity`, in
`wg-app-link`) is platform-only and is **not** replaced — it stays
a Java/Kotlin activity per decision 1 above, called into from
`app-ui` the way it is called into from Compose today.
**`client-core` needed**: none new — `config.rs`'s
`EnrolledServer`/`parse_link` already cover the deep-link half; the
Keystore half stays the JNI call E3 already wired.
**iris widgets missing**: none identified yet — a plain form screen.
**Pass condition**: enroll via the same `aiappshell://enroll?...`
link `ui-sandbox.sh`'s banner prints; the local-network-not-allowed
banner (AGENTS.md's standing-condition text) reads by name when the
permission is off; `POST_NOTIFICATIONS` request flow checked on the
real phone from P2, not just the emulator.
- [ ] **P6 — desktop parity.** Root tabs, explorer and settings on
`iris/desktop-app`, matching P3P5 there. Not a Kotlin replacement
(the desktop app has no Compose original) — this is closing the
gap `E4` deliberately left (session list + transcript only).
**`client-core` needed**: the same P3/P4 gaps, once closed there.
**Pass condition**: `run-headless.sh` screenshots of each tab and
the explorer against `app/ui-sandbox.sh`, the same way E4's did.
- [ ] **P7 — the switch of `ai-app`'s main.** Point `ai-app`'s production
Android build at `app-ui`/`android-shell` instead of
`app/androidApp`; decide then whether `app/androidApp` stays as a
reference or is retired — a load-bearing decision (AGENTS.md's
"ask before changing load-bearing decisions") to bring to Iris
rather than make here.
**Pass condition**: the full set of pass conditions above, re-run
once more against a real `ai-server` (not the sandbox) on a real
phone, side by side with the Compose build until it holds.
## For the next agent
What to do when you pick this up, in order, so nothing here has to be
@@ -3206,10 +3442,10 @@ re-derived:
2. Work on the **`rustify`** branch of this clone (`ai-app-2`), not on
`main` and not in `ai-app`. Nothing on this branch is production until
Iris says so. Commit and push as you go.
3. Take the next unchecked box above, in order. E1 has proved android-view
on this emulator, so the E-steps and the I-steps can now proceed in
parallel in separate sessions; see "Where things stand" at the top for
which is next.
3. The E- and I-steps (the framework decision) are done — iris won,
`DECISIONS.md` 2026-09-05. Take the next unchecked P-box in "## The
port, in order (decided 2026-09-05)"; **P1 — session screen parity —
is next.**
4. Every step ends with its measurement written into this file beside the
box, and the box ticked or the reason it could not be written in its
place. A step that is blocked says by what, not "later". Write it as you