Delete the decisions and design logs; scripts, rigs and xtask off the root
Iris: "remove both decisions and iris.md. I've decided to instead make decisions when planning with agents rather than after they do things, and they're both too long for me to wanna read, + don't cover all the decisions I'll wanna make about the code anyways. I'll just naturally run into things for now. Todo is important though." So docs/DECISIONS.md (850 lines) and docs/IRIS.md (1,986) are gone, and AGENTS.md now says not to start another: raise a choice while planning it with her, otherwise decide it and put the reasoning at the code it governs. The TODO lists stay. docs/SUBAGENTS_DECISIONS.md went with them -- same artefact, same reasoning, and she did not name it, so its six decisions were folded into docs/SUBAGENTS.md rather than deleted. Deleting the logs left ~30 citations dangling in code comments and docs. Each states its reason inline and cited the file only for provenance, so they now read "decided 2026-09-07" or name the module doc that carries the reasoning. The root had six things that were not a program or a document. Moved, per "I only meant top level sh files": run-tests.sh, test-wg-tunnel.sh, wg-setup-host.sh -> scripts/ rigs/ -> scripts/rigs/ xtask/ -> scripts/xtask/ A project's own scripts stayed with the project: app/*.sh, app-rust/*.sh, iris/*.sh and server/enroll-link.sh did not move. `target/` at the root is deleted and cannot come back: there was never a workspace there, and the 29 MB was only xtask's scratch space, now in scripts/xtask/target/. `cargo xtask apk` still runs from the repo root and now publishes to scripts/build/outputs/apk/<mode>/ -- one directory deep, because that is what Dev Updater's `*/build/outputs/apk/*/*.apk` discovery pattern needs, and scripts/xtask/build would have been two. Verified: ./scripts/run-tests.sh and `cd iris && cargo test` green, clippy and fmt clean everywhere, `cargo xtask apk debug --abi x86_64` builds and signs an APK carrying lib/x86_64/libai_app.so at the new publish path, and the repo root is now eleven entries with no build output among them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
09778346a0
commit
4ccfda6b8e
44 files changed
+193
-2989
No files matched your search
+7
-3
@@ -1,6 +1,10 @@
|
|||||||
# xtask convention (https://github.com/matklad/cargo-xtask), without folding
|
# xtask convention (https://github.com/matklad/cargo-xtask), without folding
|
||||||
# every crate in this repo into one workspace -- they are deliberately
|
# every crate in this repo into one workspace -- they are deliberately
|
||||||
# independent (see run-tests.sh, which cds into each). `cargo xtask apk`
|
# independent (see scripts/run-tests.sh, which cds into each).
|
||||||
# from the repo root runs xtask/src/main.rs directly.
|
#
|
||||||
|
# `cargo xtask apk` **from the repo root** runs scripts/xtask/src/main.rs.
|
||||||
|
# The manifest path is relative to the working directory cargo is run from,
|
||||||
|
# so the root is where it works; this file is found from any directory
|
||||||
|
# inside the checkout, but the path inside it is not.
|
||||||
[alias]
|
[alias]
|
||||||
xtask = "run --quiet --manifest-path xtask/Cargo.toml --"
|
xtask = "run --quiet --manifest-path scripts/xtask/Cargo.toml --"
|
||||||
+5
-3
@@ -64,9 +64,11 @@ components: [
|
|||||||
// defaults to this checkout's root, which both the `cargo xtask`
|
// defaults to this checkout's root, which both the `cargo xtask`
|
||||||
// alias (`.cargo/config.toml`, resolved relative to the working
|
// alias (`.cargo/config.toml`, resolved relative to the working
|
||||||
// directory cargo is run from) and `cargo xtask apk`'s own publishing
|
// directory cargo is run from) and `cargo xtask apk`'s own publishing
|
||||||
// step (`xtask/build/outputs/apk/<mode>/*.apk`, matching discover.rs's
|
// step (`scripts/build/outputs/apk/<mode>/*.apk`, matching
|
||||||
// `*/build/outputs/apk/*/*.apk` pattern -- see apk.rs's module doc)
|
// discover.rs's `*/build/outputs/apk/*/*.apk` pattern -- see apk.rs's
|
||||||
// both need.
|
// module doc) both need. The publish directory is `scripts/build`
|
||||||
|
// rather than `scripts/xtask/build` for exactly that reason: the
|
||||||
|
// pattern is one directory deep, and the tool moved two on 2026-09-09.
|
||||||
Apk(
|
Apk(
|
||||||
name: "shell",
|
name: "shell",
|
||||||
modes: ["release", "debug"],
|
modes: ["release", "debug"],
|
||||||
|
|||||||
+6
-9
@@ -9,8 +9,7 @@ local.properties
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
server/target/
|
server/target/
|
||||||
event-model/target/
|
event-model/target/
|
||||||
client-core/target/
|
app-rust/target/
|
||||||
android-shell/target/
|
|
||||||
|
|
||||||
# E3's native library, built by cargo-ndk straight into the Gradle module
|
# E3's native library, built by cargo-ndk straight into the Gradle module
|
||||||
# (RUST.md) -- an artifact, like server/target/ above, not source.
|
# (RUST.md) -- an artifact, like server/target/ above, not source.
|
||||||
@@ -32,11 +31,9 @@ sessions/
|
|||||||
|
|
||||||
# iris, the in-house UI library, is vendored at iris/ and built by cargo.
|
# iris, the in-house UI library, is vendored at iris/ and built by cargo.
|
||||||
iris/target/
|
iris/target/
|
||||||
iris/android-app/target/
|
|
||||||
|
|
||||||
# E5's packaging xtask (RUST.md). `build/` above already covers
|
# The packaging xtask and the GPU rigs, both under scripts/. `build/`
|
||||||
# xtask/build/outputs/apk (the published APK, see apk.rs's module doc).
|
# above already covers scripts/build/outputs/apk, where `cargo xtask apk`
|
||||||
# The repo root has no Cargo workspace, so this is xtask's own
|
# publishes for Dev Updater to find.
|
||||||
# intermediate working files (target/xtask/apk/...), not a shared one.
|
scripts/xtask/target/
|
||||||
xtask/target/
|
scripts/rigs/gpu-probe/target/
|
||||||
/target/
|
|
||||||
@@ -72,6 +72,24 @@ Module-by-module intent is in `docs/PLAN.md`'s "Backend layout".
|
|||||||
(sessions, import, models, setups); `Api.kt`/`EventStream.kt` the REST + SSE
|
(sessions, import, models, setups); `Api.kt`/`EventStream.kt` the REST + SSE
|
||||||
clients; `Events.kt` the event model mirror; `ServerConfig.kt` settings and
|
clients; `Events.kt` the event model mirror; `ServerConfig.kt` settings and
|
||||||
the Keystore-sealed token.
|
the Keystore-sealed token.
|
||||||
|
- `scripts/` — everything at the root that was neither a program nor a
|
||||||
|
document: the three repo-wide shell scripts (`run-tests.sh`,
|
||||||
|
`test-wg-tunnel.sh`, `wg-setup-host.sh`), `rigs/` (the `gpu-probe` and
|
||||||
|
`virtgpu-probe` device probes), and `xtask/`. **A project's own scripts
|
||||||
|
stay with the project** — `app/*.sh`, `app-rust/*.sh`, `iris/*.sh` and
|
||||||
|
`server/enroll-link.sh` did not move (Iris, 2026-09-09: "I only meant
|
||||||
|
top level sh files").
|
||||||
|
`scripts/xtask/` is the [cargo-xtask](https://github.com/matklad/cargo-xtask)
|
||||||
|
convention: an ordinary Rust binary that does build work a shell script
|
||||||
|
would otherwise do, run as `cargo xtask apk` **from the repo root**
|
||||||
|
(`.cargo/config.toml`'s alias, whose `--manifest-path` is relative to
|
||||||
|
the working directory). It packages `app/shellApp` without Gradle
|
||||||
|
driving it — `cargo ndk` → `javac`/`d8` → `aapt2` → `zipalign` →
|
||||||
|
`apksigner` — and publishes to `scripts/build/outputs/apk/<mode>/`,
|
||||||
|
which is where Dev Updater looks. There is deliberately **no `target/`
|
||||||
|
at the repo root** any more: there is no workspace there, and what used
|
||||||
|
to be one was only xtask's own scratch space, now in
|
||||||
|
`scripts/xtask/target/`.
|
||||||
- `wg-app-link/` — a **git submodule** shared with dev-updater: the pinned CA
|
- `wg-app-link/` — a **git submodule** shared with dev-updater: the pinned CA
|
||||||
and leaf (`certs`), QR enrollment and the bearer token (`enroll`), wg0
|
and leaf (`certs`), QR enrollment and the bearer token (`enroll`), wg0
|
||||||
binding and the certificate's SANs (`netif`), owner-only files (`private`),
|
binding and the certificate's SANs (`netif`), owner-only files (`private`),
|
||||||
@@ -89,19 +107,16 @@ Module-by-module intent is in `docs/PLAN.md`'s "Backend layout".
|
|||||||
Read it before touching `TranscriptCache.kt`, `TranscriptSource.kt`, or
|
Read it before touching `TranscriptCache.kt`, `TranscriptSource.kt`, or
|
||||||
the opening and stream effects in `SessionScreen.kt`.
|
the opening and stream effects in `SessionScreen.kt`.
|
||||||
- `docs/TODO.md` — the working list.
|
- `docs/TODO.md` — the working list.
|
||||||
- `docs/SUBAGENTS.md` and `docs/SUBAGENTS_DECISIONS.md` — a session's
|
- `docs/SUBAGENTS.md` — a session's subagents: the wire shape, the
|
||||||
subagents (the wire shape, the phone's view) and the choices behind
|
phone's view, and the choices behind the shape.
|
||||||
them still awaiting review.
|
|
||||||
- `docs/RUST.md` — the plan for moving the app to Rust (on the `rustify`
|
- `docs/RUST.md` — the plan for moving the app to Rust (on the `rustify`
|
||||||
branch of the `ai-app-2` clone): what has to be reproduced, the
|
branch of the `ai-app-2` clone): what has to be reproduced, the
|
||||||
framework decision, and the ordered experiments with their pass
|
framework decision, and the ordered experiments with their pass
|
||||||
conditions. Read it before touching anything under that branch.
|
conditions. Read it before touching anything under that branch.
|
||||||
- `docs/IRIS.md`, `docs/IRIS_TODO.md`, `docs/DECISIONS.md`,
|
- `docs/IRIS_TODO.md`, `docs/LAYOUT.md`, `docs/TEXTURES.md`,
|
||||||
`docs/LAYOUT.md`, `docs/TEXTURES.md`, `docs/CLIENT_CORE.md` — iris's
|
`docs/CLIENT_CORE.md` — iris's open working list, its layout/render
|
||||||
own build log (**any major addition or design decision, not only
|
design, its texture-atlas design, and the design of `app-rust`'s
|
||||||
public API** -- Iris, 2026-09-08), working list, decisions log,
|
`client` module, respectively.
|
||||||
layout/render design, texture-atlas design, and the design of
|
|
||||||
`app-rust`'s `client` module, respectively.
|
|
||||||
|
|
||||||
**These documents are pruned as the work lands, not appended to
|
**These documents are pruned as the work lands, not appended to
|
||||||
forever** (Iris, 2026-09-08: *"remove everything that's already done and
|
forever** (Iris, 2026-09-08: *"remove everything that's already done and
|
||||||
@@ -112,6 +127,18 @@ Module-by-module intent is in `docs/PLAN.md`'s "Backend layout".
|
|||||||
survives is what cannot be cheaply re-derived — measurements, dead ends
|
survives is what cannot be cheaply re-derived — measurements, dead ends
|
||||||
and failed hypotheses, invariants and their reasons, and the design of
|
and failed hypotheses, invariants and their reasons, and the design of
|
||||||
what exists now rather than the route to it.
|
what exists now rather than the route to it.
|
||||||
|
|
||||||
|
**There is no decisions log and no design log, and one should not be
|
||||||
|
started.** `docs/DECISIONS.md` and `docs/IRIS.md` were deleted on
|
||||||
|
2026-09-09 at Iris's instruction: *"I've decided to instead make
|
||||||
|
decisions when planning with agents rather than after they do things,
|
||||||
|
and they're both too long for me to wanna read, + don't cover all the
|
||||||
|
decisions I'll wanna make about the code anyways. I'll just naturally
|
||||||
|
run into things for now."* So raise a choice **while planning it with
|
||||||
|
her**, when the direction is still cheap to change; otherwise decide it,
|
||||||
|
put the reasoning at the code it governs, and carry on. TODO lists are
|
||||||
|
still wanted — a list of open work is useful, a list of finished work
|
||||||
|
is not.
|
||||||
- `docs/SCROLL.md` — how anything in iris scrolls: one
|
- `docs/SCROLL.md` — how anything in iris scrolls: one
|
||||||
`ScrollController` holds the position, the gesture, the fling and the
|
`ScrollController` holds the position, the gesture, the fling and the
|
||||||
pin, and the two widgets that scroll (`ScrollArea`, `LazySpan`) own
|
pin, and the two widgets that scroll (`ScrollArea`, `LazySpan`) own
|
||||||
@@ -151,13 +178,13 @@ test, `every_icon_is_in_the_bundled_font`), same Mono face, same Material
|
|||||||
Design family so an icon means the same thing in both apps. Its subset is
|
Design family so an icon means the same thing in both apps. Its subset is
|
||||||
separate rather than shared because subsetting only what one app draws is
|
separate rather than shared because subsetting only what one app draws is
|
||||||
the point. This is the **only** font iris bundles — body and monospace
|
the point. This is the **only** font iris bundles — body and monospace
|
||||||
text come from the platform (docs/DECISIONS.md, 2026-09-07), and an icon
|
text come from the platform (decided 2026-09-07), and an icon
|
||||||
is the opposite case: a small closed set of codepoints no system font is
|
is the opposite case: a small closed set of codepoints no system font is
|
||||||
guaranteed to have.
|
guaranteed to have.
|
||||||
|
|
||||||
## Checking your work
|
## Checking your work
|
||||||
|
|
||||||
- **Rust**: `./run-tests.sh` from the repo root runs `event-model`,
|
- **Rust**: `./scripts/run-tests.sh` from the repo root runs `event-model`,
|
||||||
`server/` and `app-rust/`; `cd iris && cargo test` runs the framework's
|
`server/` and `app-rust/`; `cd iris && cargo test` runs the framework's
|
||||||
own suite, which is slower and not about this product. Each workspace
|
own suite, which is slower and not about this product. Each workspace
|
||||||
also gets `cargo clippy --all-targets` and `cargo fmt`. The build stays
|
also gets `cargo clippy --all-targets` and `cargo fmt`. The build stays
|
||||||
@@ -226,7 +253,7 @@ guaranteed to have.
|
|||||||
whether it was continued or reset (`stream backlog:`). That is the only
|
whether it was continued or reset (`stream backlog:`). That is the only
|
||||||
place "how far had this phone fallen behind" is answerable — the app sees a
|
place "how far had this phone fallen behind" is answerable — the app sees a
|
||||||
window arrive and cannot tell.
|
window arrive and cannot tell.
|
||||||
- **`./test-wg-tunnel.sh up|test|down`** builds a real tunnel between two
|
- **`./scripts/test-wg-tunnel.sh up|test|down`** builds a real tunnel between two
|
||||||
network namespaces inside one machine and drives the server through it — a
|
network namespaces inside one machine and drives the server through it — a
|
||||||
genuine handshake against 10.66.0.1 with pinned TLS, no router or phone
|
genuine handshake against 10.66.0.1 with pinned TLS, no router or phone
|
||||||
involved. That is how to verify the wg0-only posture.
|
involved. That is how to verify the wg0-only posture.
|
||||||
@@ -236,7 +263,7 @@ guaranteed to have.
|
|||||||
Each exists because something was invisible without it.
|
Each exists because something was invisible without it.
|
||||||
|
|
||||||
- **The `bench` build type and `app/bench-fixture/`** exist for P0 (RUST.md
|
- **The `bench` build type and `app/bench-fixture/`** exist for P0 (RUST.md
|
||||||
and DECISIONS.md's 2026-09-05 entries), the phone benchmark gate Iris
|
and the 2026-09-05 decisions), the phone benchmark gate Iris
|
||||||
asked for before porting continues: a deterministic, checked-in synthetic
|
asked for before porting continues: a deterministic, checked-in synthetic
|
||||||
transcript (`app/bench-fixture/generate.py`, never a real one) that both
|
transcript (`app/bench-fixture/generate.py`, never a real one) that both
|
||||||
this app and iris open with no server, so a frame-time comparison
|
this app and iris open with no server, so a frame-time comparison
|
||||||
@@ -363,7 +390,7 @@ Each exists because something was invisible without it.
|
|||||||
lifecycle and one verification run before a build goes to the phone --
|
lifecycle and one verification run before a build goes to the phone --
|
||||||
not for iterating on layout.
|
not for iterating on layout.
|
||||||
- **The emulator is a GLES rig, deliberately** (Iris, 2026-09-08;
|
- **The emulator is a GLES rig, deliberately** (Iris, 2026-09-08;
|
||||||
docs/DECISIONS.md). Its guest has no hardware Vulkan -- only SwiftShader
|
docs/RUST.md). Its guest has no hardware Vulkan -- only SwiftShader
|
||||||
in software -- while its GLES *is* the host's real GPU through virgl at
|
in software -- while its GLES *is* the host's real GPU through virgl at
|
||||||
ES 3.1, so an ordinary build's runtime fallback lands there by itself
|
ES 3.1, so an ordinary build's runtime fallback lands there by itself
|
||||||
and nothing should pass `force-gles` to arrange it. The Vulkan path is
|
and nothing should pass `force-gles` to arrange it. The Vulkan path is
|
||||||
@@ -444,7 +471,7 @@ means here:
|
|||||||
|
|
||||||
- **`ai-server` belongs on the host in production.** That is where the LAN
|
- **`ai-server` belongs on the host in production.** That is where the LAN
|
||||||
address the phone can reach is, and where WireGuard terminates.
|
address the phone can reach is, and where WireGuard terminates.
|
||||||
`wg-setup-host.sh` sets that up (keys, `wg0.conf`, the phone's QR); run it
|
`scripts/wg-setup-host.sh` sets that up (keys, `wg0.conf`, the phone's QR); run it
|
||||||
there with `sudo WG_ENDPOINT=<ddns name>`.
|
there with `sudo WG_ENDPOINT=<ddns name>`.
|
||||||
- **The tunnel and the real phone can never terminate in the VM**, because
|
- **The tunnel and the real phone can never terminate in the VM**, because
|
||||||
nothing outside can open a connection into it. Phone bring-up is host work.
|
nothing outside can open a connection into it. Phone bring-up is host work.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
//! instead, one character per 100ms: laying the composer out from
|
//! instead, one character per 100ms: laying the composer out from
|
||||||
//! scratch and growing one already on screen are different cases, and
|
//! scratch and growing one already on screen are different cases, and
|
||||||
//! only the second reproduced the caret landing in the bar's padding
|
//! only the second reproduced the caret landing in the bar's padding
|
||||||
//! (IRIS.md, 2026-09-08).
|
//! (decided 2026-09-08).
|
||||||
//!
|
//!
|
||||||
//! No server: `transcript-fixture` embeds the transcript. Colour,
|
//! No server: `transcript-fixture` embeds the transcript. Colour,
|
||||||
//! spacing, type and anything a person has to *see* is answered here;
|
//! spacing, type and anything a person has to *see* is answered here;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
//! another's `logcat`. Nothing outside this process can recover what it
|
//! another's `logcat`. Nothing outside this process can recover what it
|
||||||
//! wrote, so the process keeps a copy -- and hands it to Dev Updater on
|
//! wrote, so the process keeps a copy -- and hands it to Dev Updater on
|
||||||
//! the same phone through `devlog`'s `ContentProvider`. See
|
//! the same phone through `devlog`'s `ContentProvider`. See
|
||||||
//! `docs/DECISIONS.md`, 2026-09-07.
|
//! decided 2026-09-07.
|
||||||
|
|
||||||
use crate::client::log_ring::{self, LogRing};
|
use crate::client::log_ring::{self, LogRing};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
//! desktop-app --ca /path/to/ca.pem # a link that carries no CA
|
//! desktop-app --ca /path/to/ca.pem # a link that carries no CA
|
||||||
//!
|
//!
|
||||||
//! `--link` is the same text `app/ui-sandbox.sh`'s banner prints and a
|
//! `--link` is the same text `app/ui-sandbox.sh`'s banner prints and a
|
||||||
//! phone would scan as a QR (DECISIONS.md, 2026-09-05) -- pasted rather
|
//! phone would scan as a QR (decided 2026-09-05) -- pasted rather
|
||||||
//! than scanned, since a desktop has no camera to assume. It is parsed and
|
//! than scanned, since a desktop has no camera to assume. It is parsed and
|
||||||
//! saved once; later runs read it back and `--link` is only needed again
|
//! saved once; later runs read it back and `--link` is only needed again
|
||||||
//! to enrol against a different server.
|
//! to enrol against a different server.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
//! the stream phase at p50 18.2ms against Compose's 13.4ms for exactly
|
//! the stream phase at p50 18.2ms against Compose's 13.4ms for exactly
|
||||||
//! that reason (docs/IRIS_TODO.md). A row is a column of one widget per
|
//! that reason (docs/IRIS_TODO.md). A row is a column of one widget per
|
||||||
//! block now, and a delta that lands in the last block leaves every
|
//! block now, and a delta that lands in the last block leaves every
|
||||||
//! earlier block's layout alone. `docs/DECISIONS.md`'s 2026-09-06 entry has
|
//! earlier block's layout alone. the 2026-09-06 decision has
|
||||||
//! what that rejected and why the split lives here rather than in the UI
|
//! what that rejected and why the split lives here rather than in the UI
|
||||||
//! crate: `docs/CLIENT_CORE.md` already wanted a block model for P1, and
|
//! crate: `docs/CLIENT_CORE.md` already wanted a block model for P1, and
|
||||||
//! keeping it here means iris stays a text renderer that knows nothing
|
//! keeping it here means iris stays a text renderer that knows nothing
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
//! a quote is *text behind a coloured bar*. Everything else markdown can
|
//! a quote is *text behind a coloured bar*. Everything else markdown can
|
||||||
//! say is expressed in the spans, which cost no widgets and no layout
|
//! say is expressed in the spans, which cost no widgets and no layout
|
||||||
//! nodes. `app/.../Markdown.kt`'s component table is the reference for the
|
//! nodes. `app/.../Markdown.kt`'s component table is the reference for the
|
||||||
//! sizes and colours; docs/DECISIONS.md's 2026-09-06 entry records where
|
//! sizes and colours; the 2026-09-06 decision records where
|
||||||
//! this deliberately differs.
|
//! this deliberately differs.
|
||||||
//!
|
//!
|
||||||
//! **What this deliberately does not attempt**, each for a reason recorded
|
//! **What this deliberately does not attempt**, each for a reason recorded
|
||||||
@@ -450,7 +450,7 @@ const TABLE_MAX_COL: usize = 28;
|
|||||||
/// and pixel widths the same thing, so padding each cell to its column's
|
/// and pixel widths the same thing, so padding each cell to its column's
|
||||||
/// width *is* alignment, the column widths are measured from the cells,
|
/// width *is* alignment, the column widths are measured from the cells,
|
||||||
/// and the block reuses `BlockFrame::Verbatim`'s sideways pan for a table
|
/// and the block reuses `BlockFrame::Verbatim`'s sideways pan for a table
|
||||||
/// too wide to fit. docs/DECISIONS.md, 2026-09-06, has what this trades.
|
/// too wide to fit. decided 2026-09-06, has what this trades.
|
||||||
pub fn table_text(src: &str) -> Rendered {
|
pub fn table_text(src: &str) -> Rendered {
|
||||||
let rows = table_cells(src);
|
let rows = table_cells(src);
|
||||||
if rows.is_empty() {
|
if rows.is_empty() {
|
||||||
|
|||||||
@@ -37,8 +37,8 @@
|
|||||||
//! every frame it was pressed. `row.rs` now routes every row's drag
|
//! every frame it was pressed. `row.rs` now routes every row's drag
|
||||||
//! through one shared `iris::sense::DragArbiter`
|
//! through one shared `iris::sense::DragArbiter`
|
||||||
//! (`Selection::drag`, `selection.rs`), which decides pan vs. select the
|
//! (`Selection::drag`, `selection.rs`), which decides pan vs. select the
|
||||||
//! way Android itself does -- see `DragArbiter`'s own doc and
|
//! way Android itself does -- `DragArbiter`'s own doc has the exact
|
||||||
//! `DECISIONS.md` for the exact rule. `LazySpan` scrolls correctly when
|
//! rule. `LazySpan` scrolls correctly when
|
||||||
//! driven programmatically (I3's benchmark), via the mouse wheel (wired
|
//! driven programmatically (I3's benchmark), via the mouse wheel (wired
|
||||||
//! below, `CursorSense::Scroll`), and now via a touch pan starting on a
|
//! below, `CursorSense::Scroll`), and now via a touch pan starting on a
|
||||||
//! row's own text too.
|
//! row's own text too.
|
||||||
@@ -275,8 +275,8 @@ impl TranscriptScreen {
|
|||||||
// delta path: the row is a column of one widget per
|
// delta path: the row is a column of one widget per
|
||||||
// markdown block, so a delta that lands in the last block
|
// markdown block, so a delta that lands in the last block
|
||||||
// is one `set_with_spans` and the earlier blocks keep
|
// is one `set_with_spans` and the earlier blocks keep
|
||||||
// their layouts (`row::RowBlocks::apply_delta`, and
|
// their layouts (`row::RowBlocks::apply_delta`, whose doc
|
||||||
// docs/DECISIONS.md for why the row is shaped that way).
|
// says why the row is shaped that way).
|
||||||
let old_key = row::row_key(&old_rows[common].key());
|
let old_key = row::row_key(&old_rows[common].key());
|
||||||
let new_key = row::row_key(&new_rows[common].key());
|
let new_key = row::row_key(&new_rows[common].key());
|
||||||
if new_key == old_key && self.apply_tail_delta(rsc, new_key, &new_rows[common]) {
|
if new_key == old_key && self.apply_tail_delta(rsc, new_key, &new_rows[common]) {
|
||||||
@@ -764,7 +764,7 @@ mod apply_tests {
|
|||||||
(draws, shapes)
|
(draws, shapes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pass condition for docs/DECISIONS.md's per-block row: a delta
|
/// The pass condition for the per-block row: a delta
|
||||||
/// costs the **last block**, not the message. A 3,000-character reply
|
/// costs the **last block**, not the message. A 3,000-character reply
|
||||||
/// has a hundred paragraphs already laid out; redrawing one delta into it
|
/// has a hundred paragraphs already laid out; redrawing one delta into it
|
||||||
/// must cost exactly what the same delta costs in a one-paragraph
|
/// must cost exactly what the same delta costs in a one-paragraph
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
//! meant a streamed delta re-shaped every paragraph of a long reply
|
//! meant a streamed delta re-shaped every paragraph of a long reply
|
||||||
//! through parley again -- the stream phase was the one place iris trailed
|
//! through parley again -- the stream phase was the one place iris trailed
|
||||||
//! Compose on Iris's phone. [`RowBlocks::apply_delta`] is the other half
|
//! Compose on Iris's phone. [`RowBlocks::apply_delta`] is the other half
|
||||||
//! of the fix; docs/DECISIONS.md's entry has what the alternative shapes
|
//! of the fix.
|
||||||
//! were and why this one.
|
|
||||||
//!
|
//!
|
||||||
//! A `TranscriptRow::Tools` (a run of adjacent tool calls, grouped by
|
//! A `TranscriptRow::Tools` (a run of adjacent tool calls, grouped by
|
||||||
//! `crate::client::transcript_fold::group_tool_runs`) is the row that proves
|
//! `crate::client::transcript_fold::group_tool_runs`) is the row that proves
|
||||||
@@ -127,7 +126,7 @@ fn tool_call_markdown(tool: &str, input: &str, output: &str) -> String {
|
|||||||
/// The per-block text widgets of one row, kept by `TranscriptScreen` for
|
/// The per-block text widgets of one row, kept by `TranscriptScreen` for
|
||||||
/// the row a reply is streaming into, so a delta can replace the block it
|
/// the row a reply is streaming into, so a delta can replace the block it
|
||||||
/// lands in instead of re-shaping the whole message
|
/// lands in instead of re-shaping the whole message
|
||||||
/// (docs/DECISIONS.md, 2026-09-06). Nothing else needs it: a row that is
|
/// (decided 2026-09-06). Nothing else needs it: a row that is
|
||||||
/// not the tail never changes.
|
/// not the tail never changes.
|
||||||
pub struct RowBlocks {
|
pub struct RowBlocks {
|
||||||
/// What each field was built from, in order -- compared against a
|
/// What each field was built from, in order -- compared against a
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ use std::{collections::BTreeMap, time::Instant};
|
|||||||
|
|
||||||
/// What this selects between: a row's `RowKey` and the index of one
|
/// What this selects between: a row's `RowKey` and the index of one
|
||||||
/// markdown **block** inside it. A row is a column of one text widget per
|
/// markdown **block** inside it. A row is a column of one text widget per
|
||||||
/// block since 2026-09-06 (`crate::client::markdown_blocks`, and
|
/// block since 2026-09-06 (`crate::client::markdown_blocks`), so the
|
||||||
/// docs/DECISIONS.md for why), so the block, not the row, is the unit --
|
/// block, not the row, is the unit --
|
||||||
/// `(row, block)` compares lexicographically, which is reading order for
|
/// `(row, block)` compares lexicographically, which is reading order for
|
||||||
/// both levels, so every range query below is unchanged.
|
/// both levels, so every range query below is unchanged.
|
||||||
pub type SelKey = (RowKey, u32);
|
pub type SelKey = (RowKey, u32);
|
||||||
@@ -49,7 +49,7 @@ pub struct Selection {
|
|||||||
/// pan wanting the same touch gesture). See `drag` below, and
|
/// pan wanting the same touch gesture). See `drag` below, and
|
||||||
/// `iris::sense::DragGesture`'s own doc for the arbitration, velocity
|
/// `iris::sense::DragGesture`'s own doc for the arbitration, velocity
|
||||||
/// tracking and pointer-capture mechanics this no longer owns itself
|
/// tracking and pointer-capture mechanics this no longer owns itself
|
||||||
/// -- Iris's 2026-09-06 ask (`IRIS.md`) that a drag's *mechanics* live
|
/// -- Iris's 2026-09-06 ask that a drag's *mechanics* live
|
||||||
/// in iris's default input layer, with only the pan-vs-select
|
/// in iris's default input layer, with only the pan-vs-select
|
||||||
/// *decision* staying here.
|
/// *decision* staying here.
|
||||||
gesture: DragGesture,
|
gesture: DragGesture,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* P0's benchmark gate (see docs/RUST.md and docs/DECISIONS.md's 2026-09-05 entry): an in-process
|
* P0's benchmark gate (see docs/RUST.md and the 2026-09-05 decision): an in-process
|
||||||
* fake of the backend, so the `bench` build type can drive a real session screen -- the real
|
* fake of the backend, so the `bench` build type can drive a real session screen -- the real
|
||||||
* [TranscriptSource], the real fold, the real paging -- with no server and no network permission.
|
* [TranscriptSource], the real fold, the real paging -- with no server and no network permission.
|
||||||
*
|
*
|
||||||
|
|||||||
+2
-2
@@ -133,7 +133,7 @@ the exact `aiapp://enroll?host=H&port=P&token=T` deep link
|
|||||||
`wg-app-link`'s `enroll` mints and `ServerConfig.kt`'s `handleEnrollment`
|
`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
|
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
|
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
|
E4, decided 2026-09-05). Deliberately does not decide where it is
|
||||||
persisted or under what file permissions -- a phone seals its token in the
|
persisted or under what file permissions -- a phone seals its token in the
|
||||||
Android Keystore, `src/desktop/config.rs` writes it to
|
Android Keystore, `src/desktop/config.rs` writes it to
|
||||||
`$XDG_CONFIG_HOME/ai-app-desktop/enrollment.json` at 0600 -- since that is
|
`$XDG_CONFIG_HOME/ai-app-desktop/enrollment.json` at 0600 -- since that is
|
||||||
@@ -214,7 +214,7 @@ does not repeat it again by hand.
|
|||||||
|
|
||||||
## Verifying
|
## Verifying
|
||||||
|
|
||||||
`./run-tests.sh` from the repo root runs `event-model`, `server` and
|
`./scripts/run-tests.sh` from the repo root runs `event-model`, `server` and
|
||||||
`app-rust` in that order (each `cargo test`, forwarding arguments the same
|
`app-rust` in that order (each `cargo test`, forwarding arguments the same
|
||||||
way it always has). From `app-rust/` directly: `cargo test`, `cargo clippy
|
way it always has). From `app-rust/` directly: `cargo test`, `cargo clippy
|
||||||
--all-targets`, `cargo fmt` -- all clean as of 2026-09-08, 229 tests across
|
--all-targets`, `cargo fmt` -- all clean as of 2026-09-08, 229 tests across
|
||||||
|
|||||||
@@ -1,850 +0,0 @@
|
|||||||
# Decisions taken for Iris to review
|
|
||||||
|
|
||||||
Short list of design choices made by the design agent without asking, so
|
|
||||||
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.
|
|
||||||
|
|
||||||
**Paths in older entries are pre-2026-09-08.** The port's crates became
|
|
||||||
one crate on that date -- `client-core` -> `app-rust`'s `src/client`,
|
|
||||||
`transcript-ui` -> `src/ui`, `transcript-fixture` -> `src/ui/fixture.rs`
|
|
||||||
and `tests/`, `desktop-app` -> `src/desktop`, `android-app` ->
|
|
||||||
`src/android`, `android-shell` -> `src/shell`. Entries are left as they
|
|
||||||
were written rather than rewritten, since each is a dated record;
|
|
||||||
`docs/RUST.md`'s "One app crate" is the mapping.
|
|
||||||
|
|
||||||
## 2026-09-08 (the port is one crate, and iris is framework-only)
|
|
||||||
|
|
||||||
Asked for by Iris directly, so the shape rather than the fact is what is
|
|
||||||
open to review. `app-rust/` is one crate, `ai-app`, with `client`, `ui`,
|
|
||||||
`desktop`, `android` and `shell` as modules; `iris/` holds only `core`,
|
|
||||||
`macro`, `iris`, `tabs-ui` and `rig-input`. docs/RUST.md's "One app crate"
|
|
||||||
has the table and the reason each old split did or did not survive.
|
|
||||||
|
|
||||||
Three calls made inside that, none of which she named:
|
|
||||||
|
|
||||||
1. **`event-model` stayed at the repo root** rather than moving into
|
|
||||||
`app-rust/` -- her choice when asked, since `server/` depends on it too.
|
|
||||||
2. **The Android application id and Java package were left alone**
|
|
||||||
(`dev.iris.android.demo`, label "iris android-view demo"), though both
|
|
||||||
now name the wrong thing. Renaming them makes the next install
|
|
||||||
side-by-side rather than an upgrade on her phone, and changes the
|
|
||||||
`DevLogProvider` authority Dev Updater reads. Say the word and it is a
|
|
||||||
small change.
|
|
||||||
3. **The `bench` fixture is behind a `fixture` feature that is on by
|
|
||||||
default**, with `build-apk.sh` passing `--no-default-features` so an APK
|
|
||||||
carries the 1.9 MB fixture only when it asked for `bench`. The
|
|
||||||
alternative -- default off -- would have made `cargo test` silently skip
|
|
||||||
the six harness suites, which is the worse failure.
|
|
||||||
|
|
||||||
## 2026-09-08 (last: scrolling moves out of the list)
|
|
||||||
|
|
||||||
Agreed with Iris in the exchange that followed, so most of this is her
|
|
||||||
call rather than mine. IRIS.md has the account. What I decided along the
|
|
||||||
way, and would flag for reversal:
|
|
||||||
|
|
||||||
- **A third `Widget` method, `scroll_offset`**, beyond the two we agreed.
|
|
||||||
`apply_scroll`'s remainder is exact only when the wall was already in
|
|
||||||
view, and a lazy span usually cannot see its wall until it has walked
|
|
||||||
there -- so `Scroll` reads the child's accumulated movement after the
|
|
||||||
placing draw instead of adding remainders up, which would drift.
|
|
||||||
- **One scroll-delta convention, the finger's.** The two widgets had
|
|
||||||
opposite ones under the same name; `LazySpan::scroll` is now private and
|
|
||||||
the single negation lives in its `apply_scroll`. Call sites that passed
|
|
||||||
`-dy`/`-v` pass them straight through, and one fixture's expected
|
|
||||||
velocity flipped sign with its magnitude unchanged.
|
|
||||||
- **The transcript builds its `Scroll` by hand rather than through
|
|
||||||
`.scrollable_to_end()`**, because that helper registers a finger drag
|
|
||||||
and `Selection` is already the arbiter for those frames -- two
|
|
||||||
`DragGesture`s seeing one gesture is what `DragGesture`'s own doc rules
|
|
||||||
out. The wheel is registered identically; only the drag differs.
|
|
||||||
- **DEFERRED: the pin stays in each widget.** Iris asked for `amt` and
|
|
||||||
the at-end control to live in `Scroll`; `amt` does, the pin does not,
|
|
||||||
because applying a pin happens when a row is appended -- between frames,
|
|
||||||
with no painter -- so moving it needs a fourth `Widget` method or a
|
|
||||||
parameter on `apply_scroll`. Nothing external edits a pin today.
|
|
||||||
docs/IRIS_TODO.md carries it.
|
|
||||||
|
|
||||||
## 2026-09-08 (later still: the list's overscroll clamp, in frame)
|
|
||||||
|
|
||||||
Finishes the item the previous entry deferred. IRIS.md has the account.
|
|
||||||
|
|
||||||
- **`List` lays out a second time within the frame** when its walk lands
|
|
||||||
off the end of the content, instead of writing the correction to the
|
|
||||||
anchor and asking for another frame. The extra walk is paid only on an
|
|
||||||
overscrolled frame, and it is mostly O(1) moves.
|
|
||||||
- **`Painter::draw_again` is removed**, `List` having been its only
|
|
||||||
caller -- so the framework no longer offers a way to ask for a
|
|
||||||
corrective frame at all.
|
|
||||||
- **`List::place`'s top-known and bottom-known cases are one path**
|
|
||||||
(`Placement::edges`), which is the "write the logic once" rule applied
|
|
||||||
to two symmetric directions rather than a behaviour change.
|
|
||||||
|
|
||||||
## 2026-09-08 (later: a scroll area measures and places in one frame)
|
|
||||||
|
|
||||||
From Iris's phone report about the composer's padding while typing
|
|
||||||
newlines, and the rule she stated when she read the first fix: layout is
|
|
||||||
a pure function of the state, nothing self-heals, and two draws to place
|
|
||||||
something happen in the same frame. IRIS.md's entry has the account.
|
|
||||||
|
|
||||||
- **`Scroll::draw` draws its child twice** -- once at last frame's length
|
|
||||||
to measure it, once at the measured length to place it -- instead of
|
|
||||||
placing against the stale length and leaving a wrong frame on screen.
|
|
||||||
The second draw is free unless the content's length changed.
|
|
||||||
- **An end-anchored `Scroll` is at its end on its first drawn frame**, a
|
|
||||||
consequence of the above. Two layout tests now build their area with
|
|
||||||
`at_end: false`, which is what they meant: they scroll down from the
|
|
||||||
top.
|
|
||||||
- **`List::clamp_to_content`'s next-frame correction is left in place**
|
|
||||||
and written down in docs/IRIS_TODO.md instead of fixed here, because
|
|
||||||
`List::place` is a larger piece of machinery and deserves its own
|
|
||||||
before/after on the phone.
|
|
||||||
|
|
||||||
## 2026-09-08 (every crate to its latest version, wgpu 28 -> 30)
|
|
||||||
|
|
||||||
At Iris's request. RUST.md's "Every crate to its latest version" box has
|
|
||||||
the full list and the migration.
|
|
||||||
|
|
||||||
- **wgpu 30 taken now rather than pinned at 28.** Two majors of API
|
|
||||||
change, all mechanical (instance descriptor, optional bind-group and
|
|
||||||
vertex-buffer slots, `Queue::present`, a `CurrentSurfaceTexture` enum),
|
|
||||||
and one that would have been a startup abort on a device rather than a
|
|
||||||
compile error: naga now demands `@interpolate(flat)` on the shader's
|
|
||||||
integer varyings. Verified on both backends before this was called
|
|
||||||
done, since a renderer that compiles proves nothing.
|
|
||||||
- **The desktop instance now carries winit's display handle.** wgpu 30
|
|
||||||
asks for it when a GLES surface will be presented on Wayland, which is
|
|
||||||
what this machine's Vulkan-to-GLES fallback produces. Android passes
|
|
||||||
none: its surface comes from a `NativeWindow`.
|
|
||||||
- **`syn` 2 -> 3, `pollster` 0.4 -> 1.0** with no source change in
|
|
||||||
`iris/macro` or anywhere else.
|
|
||||||
|
|
||||||
## 2026-09-08 evening (the fling is shared; a cancel is not a release)
|
|
||||||
|
|
||||||
From Iris's four-item phone report; RUST.md's "2026-09-08 (evening)" box
|
|
||||||
has the reasoning and the tests, IRIS.md the summary.
|
|
||||||
|
|
||||||
- **A `Flinger` that does not know which way the content moves.** Every
|
|
||||||
scroll area flings now, on either axis, as Iris asked -- and the
|
|
||||||
physics is one type shared by `List` and `Scroll` rather than a copy
|
|
||||||
each. The choice worth reviewing is the seam: `Flinger` owns the curve
|
|
||||||
and the clock, and the *caller* owns the sign convention and where the
|
|
||||||
content ends. Rejected: teaching `Flinger` a direction, which would
|
|
||||||
have to be told to it -- and being told is the same thing as not
|
|
||||||
knowing, with an extra field to get wrong.
|
|
||||||
- **A cancel is a first-class end to a gesture, not an early release.**
|
|
||||||
`CursorState::cancelled` is new state on the pointer sample, set by
|
|
||||||
Android's `ACTION_CANCEL` and the harness's `TouchAction::Cancel`.
|
|
||||||
Rejected: mapping a cancel to `PressEnd` and having each widget decide
|
|
||||||
what to suppress, which is what shipped and is why leaving the app
|
|
||||||
flung the transcript.
|
|
||||||
- **A `DragGesture` ignores a `Cancel` it caused.** One gesture is
|
|
||||||
driven by several widgets, so the widget that was pressed can be a
|
|
||||||
"loser" on the frame its own gesture won. The test is whether the
|
|
||||||
gesture's own capture id is the holder. This is what makes it safe for
|
|
||||||
every widget driving a gesture to register the whole `drag_senses()`
|
|
||||||
set, which is now the rule without exception.
|
|
||||||
- **`List::place` draws a resized row twice in one frame.** The old
|
|
||||||
comment accepted a one-frame lag by analogy with `Scroll`'s content
|
|
||||||
length. That analogy was wrong: a stale *length* only misplaces the
|
|
||||||
next thing, while a stale *box* is drawn, because a background fills
|
|
||||||
whatever box it is handed. The extra draw is bounded to frames where a
|
|
||||||
row's height actually changed.
|
|
||||||
|
|
||||||
## 2026-09-08 (iris ships an icon font, and the drawn mark is deleted)
|
|
||||||
|
|
||||||
- **Directed by Iris.** Her question on seeing `widget::mark`: "why does
|
|
||||||
mark exist? The font should be working if it's working for compose and
|
|
||||||
nerd fonts are bundled." It was not: the Compose app draws its icons
|
|
||||||
from **its own committed Nerd Fonts subset**, while iris was setting
|
|
||||||
the disclosure mark with bare Unicode geometric codepoints
|
|
||||||
(U+25B8/25BE/25B4) out of whatever face the platform resolved -- an
|
|
||||||
empty box on her phone, a dot on this VM. The 2026-09-07 entry below,
|
|
||||||
which said "iris had no equivalent icon font to keep", is what left
|
|
||||||
that gap: iris had no icon font because it had never had one, not
|
|
||||||
because it needed none.
|
|
||||||
- **So iris now bundles the same kind of subset**:
|
|
||||||
`iris/core/build-icon-font.sh` writes
|
|
||||||
`iris/core/assets/fonts/nerd_icons.ttf` (992 bytes, three Material
|
|
||||||
Design glyphs today), `iris::icon` names the codepoints, and
|
|
||||||
`Family::Icons` draws them. This does **not** reopen the platform-fonts
|
|
||||||
decision: body and monospace text still come from the platform, and an
|
|
||||||
icon is the opposite case -- a small, closed, known set of codepoints,
|
|
||||||
which is exactly the division the Compose app already makes.
|
|
||||||
- **`iris::widget::mark` is deleted** (added earlier the same day). It
|
|
||||||
drew a correct triangle, but only a triangle, and every further icon
|
|
||||||
would have been another bespoke rasteriser. An icon as text also takes
|
|
||||||
the size, colour and baseline of the line it sits in for free.
|
|
||||||
|
|
||||||
## 2026-09-08 (the emulator is a GLES machine, and Vulkan is verified elsewhere)
|
|
||||||
|
|
||||||
- **Directed by Iris, carried out here**: "make sure the setup uses GL for
|
|
||||||
the android emulator and remove any vulkan requirements. That'll be
|
|
||||||
tested through both the desktop version as well as my phone." So the
|
|
||||||
emulator is settled as a GLES rig and nothing chases hardware Vulkan in
|
|
||||||
it any more; the Vulkan path is covered by the desktop build and by her
|
|
||||||
phone.
|
|
||||||
- **Nothing had to be forced to make that true.** Measured in the guest
|
|
||||||
the same day: the emulator has no hardware Vulkan at all (its only
|
|
||||||
Vulkan is SwiftShader, in software) and its GLES is the host's real RX
|
|
||||||
7900 XT through virgl at ES 3.1. iris's existing runtime fallback --
|
|
||||||
`Backends::PRIMARY`, no adapter, rebuild on `Backends::GL` -- already
|
|
||||||
lands there, verified end to end with an ordinary (no `force-gles`)
|
|
||||||
debug APK.
|
|
||||||
- **The emulator and the phone therefore run the same binary**, differing
|
|
||||||
only in what that binary finds. That is deliberate and worth not
|
|
||||||
undoing: a build flag that changed the backend would mean the thing
|
|
||||||
measured on the emulator is not the thing shipped. `force-gles` stays,
|
|
||||||
but only for pinning the backend on a machine that *does* have Vulkan
|
|
||||||
(the desktop), and never for a phone build.
|
|
||||||
- **Every run now says which adapter drew it.** The Android renderer logs
|
|
||||||
the full adapter line at startup the way the desktop already did -- only
|
|
||||||
the backend enum was logged before, which cannot separate `Gl` on the
|
|
||||||
host's GPU from `Gl` on SwiftShader, or a phone's real Vulkan from a
|
|
||||||
software one. `run-bench.sh` prints that line before any number.
|
|
||||||
- **No Vulkan requirement was found in iris to remove.** `device_limits()`
|
|
||||||
asks for nothing beyond wgpu's defaults (and zeroes the compute fields),
|
|
||||||
neither backend requires a feature, and both probe rather than
|
|
||||||
`.expect()` an adapter. What was removed was the *documentation* telling
|
|
||||||
people to boot the emulator with SwiftShader Vulkan.
|
|
||||||
|
|
||||||
## 2026-09-07 (platform fonts, not bundled ones)
|
|
||||||
|
|
||||||
- **Iris's own decision, carried out as directed**: removed the 3.6 MB of
|
|
||||||
bundled Noto Sans/Noto Sans Mono TTFs from `iris-core` and load text
|
|
||||||
from the platform's own font collection instead (`fontique`'s system
|
|
||||||
discovery, already on by default). Matches what the Compose app does --
|
|
||||||
it takes body text from `FontFamily.Default` and code text from
|
|
||||||
`FontFamily.Monospace`, both platform-resolved, and ships no text font
|
|
||||||
of its own. Rejected alternative (the one this pass had left open
|
|
||||||
2026-09-06): subsetting the bundled Noto Sans to Latin/common
|
|
||||||
punctuation instead of removing it outright, which would have kept
|
|
||||||
identical rendering across devices for a smaller (not zero) size cost;
|
|
||||||
Iris chose to match Compose instead.
|
|
||||||
- `.so` **-3,748,136 bytes** (11,193,608 -> 7,445,472), matching the
|
|
||||||
original 3.6 MB estimate. Fallback still lands on the platform's own
|
|
||||||
tofu for a codepoint no resolved face has (checked with CJK + emoji on
|
|
||||||
desktop) rather than blank space, so the UI_RULES unknown-glyph rule
|
|
||||||
still holds.
|
|
||||||
- **Gap found, then closed same day**: this fontique version's Android
|
|
||||||
backend never resolved the `Monospace` generic family at all (confirmed
|
|
||||||
on this checkout's emulator, `mono=None` in the startup diagnostic) --
|
|
||||||
two pre-existing bugs in fontique's own `fonts.xml` parsing stacked (an
|
|
||||||
ordering bug, and a `<family name="monospace">` declaration whose
|
|
||||||
`<font>` children the backend's parser never reads), not something this
|
|
||||||
change introduced, but this change is what stopped masking it (the
|
|
||||||
bundled mono font used to be registered ahead of the broken platform
|
|
||||||
lookup, so it always won). Checked `linebender/parley`'s `main` branch
|
|
||||||
on GitHub: neither bug is fixed there, so there was no newer release to
|
|
||||||
bump to. Fixed instead in `iris-core` itself
|
|
||||||
(`TextData::patch_android_monospace`, Android-only): reads
|
|
||||||
`/system/etc/fonts.xml`'s own `"monospace"` declaration for the font
|
|
||||||
filename it names, then registers whichever of fontique's actually-
|
|
||||||
scanned families owns that file as the `Monospace` generic -- the same
|
|
||||||
authority Compose's `Typeface.MONOSPACE` resolves through, without
|
|
||||||
pinning an OEM-specific family name. Verified on this checkout's
|
|
||||||
emulator: `mono=Some("Droid Sans Mono")`, and a screenshot showing the
|
|
||||||
bench-fixture's code block and tool-card values in a visibly monospaced
|
|
||||||
face beside sans body text; the desktop `fontconfig` backend is
|
|
||||||
unaffected (still resolves monospace correctly, confirmed unchanged).
|
|
||||||
docs/RUST.md's "Platform fonts (2026-09-07)" has the full account.
|
|
||||||
|
|
||||||
## 2026-09-07 (a phone log reaches Iris through Dev Updater's own tab)
|
|
||||||
|
|
||||||
**Supersedes the "how a phone log reaches Iris" entry below, same day.**
|
|
||||||
Iris's call once the route was working: put it in Dev Updater properly
|
|
||||||
rather than smuggling the lines through `ai-server`'s log.
|
|
||||||
|
|
||||||
- **The app exposes its own log on the device, and Dev Updater reads it
|
|
||||||
there.** A `ContentProvider` at `<applicationId>.devlog`, one table of
|
|
||||||
lines queried with `?since=<seq>` so a poll is incremental, plus a
|
|
||||||
`status` row (`held`, `dropped`, `newest_seq`). Dev Updater's phone app
|
|
||||||
polls it while the component's **Runtime** tab is open and forwards what
|
|
||||||
is new to its own build machine, into that APK component's runtime log
|
|
||||||
-- so the same tab renders both kinds and the history outlives the
|
|
||||||
phone. No tunnel, no token, no second enrolment: the two apps are on the
|
|
||||||
same phone.
|
|
||||||
|
|
||||||
**It is a contract, not a feature for iris.** Written down in
|
|
||||||
dev-updater's `README.md` ("An app's own log"), so any app that server
|
|
||||||
delivers gets the tab by implementing it; the Compose app in `app/` can
|
|
||||||
do the same later. That is the reason it beat the route below on its
|
|
||||||
second look -- the earlier one only ever worked for the one project that
|
|
||||||
had a server, and put a phone's lines under a *different component* than
|
|
||||||
the one they came from.
|
|
||||||
|
|
||||||
- **Read access is `protectionLevel="normal"`, and that is a real trade.**
|
|
||||||
`signature` is what this wants and is not available: Dev Updater and the
|
|
||||||
apps it delivers are built on one machine but signed with different
|
|
||||||
locally generated keys, so a signature permission would be held by
|
|
||||||
nothing at all. What `normal` costs is that any app on that phone which
|
|
||||||
requests `dev.updater.permission.READ_DEVLOG` by name can read another
|
|
||||||
app's dev log. Accepted because these are development builds on a
|
|
||||||
development phone and the alternative was no log; stated in the manifest
|
|
||||||
beside the declaration and in dev-updater's README so it is not
|
|
||||||
rediscovered as a surprise.
|
|
||||||
|
|
||||||
- **The provider polls rather than notifying.** `notifyChange` was not
|
|
||||||
implemented: the ring is filled by a `log::Log` backend on whatever
|
|
||||||
thread logged, and giving that a route to a `ContentProvider` means
|
|
||||||
plumbing a callback through `client-core` for every platform. Dev
|
|
||||||
Updater's contract therefore says it polls (about a second, only while
|
|
||||||
the tab is open), which is what keeps implementing the contract cheap --
|
|
||||||
a provider that does notify loses nothing.
|
|
||||||
|
|
||||||
- **What was deleted, so there is one mechanism**: `client-core`'s
|
|
||||||
`log_upload` module, `POST /client-log` on `ai-server`, the
|
|
||||||
`AI_APP_LOG_HOST`/`_PORT`/`_TOKEN` baking in `iris/android-app/build.rs`
|
|
||||||
(which left that file with nothing to do, so it is gone too), and the
|
|
||||||
uploader fields on both Android clients. Kept: the ring, `RingLogger`,
|
|
||||||
`install_process_logger`, and the Diagnostics line counting what is
|
|
||||||
held. The upload-status line there is now **"devlog provider:
|
|
||||||
content://<authority>"** -- named from what the provider registered
|
|
||||||
rather than composed from the package here, so a screenshot of that pane
|
|
||||||
is evidence the contract is live and says which package's log it is.
|
|
||||||
|
|
||||||
## 2026-09-07 (how a phone log reaches Iris) -- superseded, see above
|
|
||||||
|
|
||||||
- **The app sends its own log to `ai-server`, and Dev Updater shows it as
|
|
||||||
`ai-server`'s runtime log.** Iris has no `adb`/`logcat` on her phone, and
|
|
||||||
Android forbids one app reading another's logcat, so the app has to carry
|
|
||||||
its own copy and post it somewhere. `POST /client-log` on `ai-server`
|
|
||||||
re-emits each line into that server's own `tracing` output; Dev Updater
|
|
||||||
already runs `ai-server` as a `Managed` component, whose stdout its own
|
|
||||||
service script redirects to a file and reports through
|
|
||||||
`GET /apps/{key}/components/{name}/logs?kind=runtime`, which the phone
|
|
||||||
app's log dialog already offers as a **Runtime** tab for a `server`
|
|
||||||
component. So **no change to Dev Updater at all** -- one route on
|
|
||||||
`ai-server`, and the client in `client-core`.
|
|
||||||
|
|
||||||
**Rejected: posting to Dev Updater's own server** (the first candidate,
|
|
||||||
and what the entry above went on to build -- the estimate below was
|
|
||||||
right about the work and wrong about it being too much).
|
|
||||||
It would need a new authenticated *write* route on a TLS surface whose
|
|
||||||
module doc says every route on it "is, or decides, the bytes that get
|
|
||||||
handed to `REQUEST_INSTALL_PACKAGES` next"; a per-app device-log store;
|
|
||||||
a change to `component_logs` so an APK component can have a runtime log;
|
|
||||||
a change to the phone app's `hasBothKinds = component.kind == "server"`
|
|
||||||
gate and to what `hasRuntimeLogs` means on the wire; and -- the real
|
|
||||||
cost -- a **second** enrollment for the iris app, since it has no CA or
|
|
||||||
token for Dev Updater and Dev Updater mints tokens per device by QR.
|
|
||||||
Five changes across two repos against one route, for the same line
|
|
||||||
landing in the same viewer.
|
|
||||||
|
|
||||||
**Rejected: a share intent from a debug button** (a log file in the app's
|
|
||||||
external files dir, shared by hand). It works today and needs no server,
|
|
||||||
but every line costs Iris a manual export and a message, which is the
|
|
||||||
round trip through a person this was meant to remove. It is still the
|
|
||||||
fallback when the tunnel is down, and GrapheneOS's own per-app log export
|
|
||||||
already covers the crash case (that is how the `ToolInput.highlighted`
|
|
||||||
crash was reported).
|
|
||||||
|
|
||||||
- **The ring is in `client-core`, not in the Android crate.** A bounded
|
|
||||||
in-memory ring (2000 lines or 256 KiB, whichever bites first) behind a
|
|
||||||
`log::Log` backend that *forwards* to whichever logger the platform
|
|
||||||
already installed, so `logcat` and a desktop terminal see exactly what
|
|
||||||
they saw before. The platform supplies only its own logger and its
|
|
||||||
destination. `Copy report` appends the ring to what goes on the
|
|
||||||
clipboard, and flushes the uploader first.
|
|
||||||
|
|
||||||
- **The destination is baked in at build time, from the build machine's
|
|
||||||
own files** (`AI_APP_LOG_HOST`/`_PORT`/`_TOKEN` plus the pinned CA) --
|
|
||||||
*gone; the provider above replaced it.* What is worth keeping from it is
|
|
||||||
the reason it went: an APK good only for the server that built it cannot
|
|
||||||
be built in this VM for Iris's phone, which is the case that mattered.
|
|
||||||
all three or none, never two. The same trust boundary the transcript
|
|
||||||
config and the Compose APK's CA already use: nothing secret is
|
|
||||||
committed, and an APK is good for the server that built it. A build told
|
|
||||||
nothing still keeps its ring and still copies it; the diagnostics pane
|
|
||||||
says which of "not tried yet", "failing -- <why>" and "no server
|
|
||||||
configured" it is, because otherwise all three look like silence.
|
|
||||||
|
|
||||||
## 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
|
|
||||||
call to reverse. Compose draws a real grid: cells on a tint, each
|
|
||||||
column with a 136dp floor, scrolling sideways when there are too many.
|
|
||||||
iris has no grid widget, and building one would be a widget per
|
|
||||||
markdown feature -- which is the thing the block model exists to avoid.
|
|
||||||
In a monospace face a character count *is* a pixel width, so padding
|
|
||||||
each cell to its column's width is alignment, the widths are still
|
|
||||||
measured from the cells, and a table that is too wide pans sideways
|
|
||||||
through the same mechanism a code fence already uses. The header is
|
|
||||||
bold with a rule under it, and a long cell wraps inside its column
|
|
||||||
(capped at 28 characters, which is what fits three columns across a
|
|
||||||
phone). **What it trades:** no cell borders, and a table looks like
|
|
||||||
code rather than like a table. If you want the grid, it is a new widget
|
|
||||||
and it is a day's work.
|
|
||||||
- **Three block frames, and only three.** A heading, paragraph and list
|
|
||||||
are plain text with spans; a fence and a table are a rounded panel that
|
|
||||||
does not wrap; a quote is a bar with the text padded past it.
|
|
||||||
Everything else markdown says is expressed in span styles, which cost
|
|
||||||
no widgets and no layout nodes. So a new markdown feature is a span,
|
|
||||||
not a widget.
|
|
||||||
- **A list's marker is part of the text, so a wrapped item's second line
|
|
||||||
returns to the left margin.** Compose keeps it indented by giving the
|
|
||||||
marker its own column. Doing the same here needs per-line indent in
|
|
||||||
iris's text attributes; it is written down rather than done, because
|
|
||||||
the list items in a real reply are usually one line.
|
|
||||||
- **A link opens on a tap and not on the end of a drag.** A press that
|
|
||||||
panned the transcript past a link, or that held long enough to start a
|
|
||||||
selection, does not follow it -- decided by the same gesture machine
|
|
||||||
that decides pan-versus-select, so there is one rule rather than two
|
|
||||||
that can disagree.
|
|
||||||
|
|
||||||
## 2026-09-06 (composer scroll and the streaming block model)
|
|
||||||
|
|
||||||
- **A streamed message becomes a column of per-block widgets.** Decided by
|
|
||||||
the design agent; recorded here because it is the shape of every message
|
|
||||||
on screen. A transcript row is one `TextEdit` today, so a streamed delta
|
|
||||||
re-shapes the entire message through parley on every event -- the stream
|
|
||||||
phase is the one place iris is behind Compose on your phone (p50 18.2ms
|
|
||||||
vs 13.4ms). A row becomes a column of one widget per markdown block
|
|
||||||
(paragraph, heading, fence, list, table) and a delta replaces only the
|
|
||||||
last block, keeping every earlier block's layout. **Rejected:** splitting
|
|
||||||
parley's layout at block boundaries inside one text widget (couples
|
|
||||||
iris's text widget to markdown structure, and parley has no incremental
|
|
||||||
API), and caching shaped runs per paragraph inside `TextEdit` (a second
|
|
||||||
cache with its own invalidation beside the glyph cache). Chosen because
|
|
||||||
P1's markdown block model is needed anyway, so the split happens once, in
|
|
||||||
`client-core`, and iris stays a text renderer. **Status: designed, not
|
|
||||||
built** -- this pass spent its budget on the composer's three layout
|
|
||||||
defects; docs/RUST.md has the design and the pass conditions.
|
|
||||||
- **The composer's overflowing text now scrolls on a finger**, capped at
|
|
||||||
six lines and clipped to the bar. Reverses the "still does not scroll"
|
|
||||||
item below.
|
|
||||||
- **A widget may not report a `dp` length** (see IRIS.md). A rule for
|
|
||||||
widget authors, enforced by a `debug_assert!`; nothing changes for app
|
|
||||||
code.
|
|
||||||
|
|
||||||
## 2026-09-06 (stale-primitives and touch-scroll pass)
|
|
||||||
|
|
||||||
- **A vertical drag inside a focused composer now scrolls rather than
|
|
||||||
selects.** Android's own `EditText` does this -- a vertical drag scrolls
|
|
||||||
the field, and only a long press starts a selection -- so the platform
|
|
||||||
decided it. What it costs: you can no longer drag straight down inside
|
|
||||||
the composer to select several lines of what you typed; use a long press
|
|
||||||
and then drag, or drag sideways. Say if that trade is wrong for you.
|
|
||||||
- **`Scroll` gets a finger pan but no fling.** `List` flings; a scroll area
|
|
||||||
does not, because it has no per-frame tick to animate one and the areas
|
|
||||||
it wraps are at most a screenful (Android does not fling a six-line text
|
|
||||||
box either). Easy to add later if a scroll area ever wraps something long.
|
|
||||||
- **The composer still does not scroll its overflowed text**, though the
|
|
||||||
mechanism it needs is now in place. Wrapping the field in `.scrollable()`
|
|
||||||
was tried and reverted the same day: `Scroll` measures its content and
|
|
||||||
container against the *window*, so inside the `MaxSize` that caps the
|
|
||||||
composer at six lines the two are in different spaces and the field pans
|
|
||||||
itself entirely out of the bar (measured on the emulator with 474
|
|
||||||
characters in it -- the bar collapsed to its padding). Fixing that means
|
|
||||||
`Scroll` measuring against its own offered box, which is a change to a
|
|
||||||
widget the transcript and the bench shell both use, so it is its own
|
|
||||||
piece of work rather than a rider on this one.
|
|
||||||
|
|
||||||
## 2026-09-06 (defect pass)
|
|
||||||
|
|
||||||
- **The keyboard-open diagnostics overlay is gone; the capture only
|
|
||||||
logs now.** It was added when `on_insets_changed` was not firing at all
|
|
||||||
and there was no way to get a report off the phone. It fires reliably
|
|
||||||
since the activity went edge-to-edge -- and what that looks like in
|
|
||||||
use is a full-screen report covering the app **every time the keyboard
|
|
||||||
opens**, with its own Copy/Close buttons sitting underneath the
|
|
||||||
keyboard, so it cannot be dismissed (reproduced on the emulator this
|
|
||||||
pass: two `tap 'CLOSE'` runs left it up). An interruption for something
|
|
||||||
nobody asked for, over the app you are trying to type into. The named
|
|
||||||
`Diagnostics` button still shows the same text on demand, and the new
|
|
||||||
`iris surface:`/`iris insets:` log lines carry the lifecycle a `logcat`
|
|
||||||
pull needs. Reversible: `capture_keyboard_diagnostics` is still the one
|
|
||||||
place this is decided, and `PlatformHandle::show_diagnostics_overlay`
|
|
||||||
is still there.
|
|
||||||
|
|
||||||
- **The bench shell's report pane is sized to its report, not to a share
|
|
||||||
of the window.** It held `.height(rest(1))` beside the transcript's
|
|
||||||
`rest(2)`, so an *empty* `TextEdit` reserved a third of every screen --
|
|
||||||
which is what Iris's "the app does not start with keyboard spacing
|
|
||||||
correct" screenshot was showing, with the composer two thirds down and
|
|
||||||
black below it. It is `.max_height(dp(260))` now and sits above the
|
|
||||||
transcript rather than under the composer, where it was eating the
|
|
||||||
navigation-bar clearance. Cost: a filled report is clipped at 260dp
|
|
||||||
rather than scrolling (a `Scroll` there drew itself off the top of the
|
|
||||||
screen, since `Scroll` pins to the end of its content and reports its
|
|
||||||
content's full length to the parent -- worth fixing in `Scroll`, not
|
|
||||||
worked around here). "Copy report" and `logcat` still have the whole
|
|
||||||
thing.
|
|
||||||
|
|
||||||
## 2026-09-05
|
|
||||||
|
|
||||||
- **iris no longer asks every device for compute-shader limits it never
|
|
||||||
uses.** `adapter.request_device` (both `iris/src/android/render.rs` and
|
|
||||||
`iris/src/default/render.rs`) used `Limits::default()` plus an override
|
|
||||||
for `max_buffer_size`, and `Limits::default()` unconditionally requests
|
|
||||||
desktop-tier compute limits (`max_compute_workgroups_per_dimension:
|
|
||||||
65535`, per `wgpu_types`) even though nothing in `iris`/`iris-core`
|
|
||||||
creates a `ComputePipeline` or writes a `@compute` shader stage —
|
|
||||||
confirmed by grepping the whole tree, not assumed. That crashed
|
|
||||||
`request_device` outright on the Android emulator's software GL path
|
|
||||||
(`EMU_GPU=software`, `--features force-gles`): SwiftShader's GL reports
|
|
||||||
itself as OpenGL ES 3.0, which has no compute shaders at all, so the
|
|
||||||
adapter's real limit is 0 against the unconditional request for 65535 —
|
|
||||||
`RUST.md`'s "Software mode ... crashes for a third, different reason,"
|
|
||||||
2026-09-05, earlier today. The same would happen on any real
|
|
||||||
GLES-3.0-only Android device, not just the emulator. Fixed by a new
|
|
||||||
`iris_core::device_limits()` (`iris/core/src/render/mod.rs`), shared by
|
|
||||||
both platform backends so the two requests cannot drift, that zeros the
|
|
||||||
six `max_compute_*` fields explicitly rather than switching to a
|
|
||||||
downlevel `Limits` preset — `Limits::downlevel_webgl2_defaults()` was
|
|
||||||
considered and rejected: it also zeros
|
|
||||||
`max_storage_buffers_per_shader_stage`, and `shader.wgsl`'s vertex stage
|
|
||||||
reads four `var<storage>` buffers (rects, glyphs, masks, move_offsets),
|
|
||||||
so that preset would trade the compute crash for a bind-group-layout
|
|
||||||
one on the same downlevel hardware this is meant to support. No
|
|
||||||
capability check or fallback path was needed since nothing is being
|
|
||||||
disabled — the request is simply narrowed to what the pipeline actually
|
|
||||||
uses. `rigs/gpu-probe`'s own mirrored limits (it is deliberately its own
|
|
||||||
crate, not a workspace member, so it cannot call `device_limits()`
|
|
||||||
directly) were updated to match, and confirm `IRIS DEVICE: ok` against
|
|
||||||
this VM's own Vulkan and GL adapters. **Not verified this pass**: the
|
|
||||||
specific SwiftShader-ES-3.0 crash this fixes, on-device — the
|
|
||||||
`EMU_GPU=software` cold boot this needs would have force-restarted this
|
|
||||||
checkout's emulator while another session was actively running its own
|
|
||||||
app on it (`com.example.aiapp` had window focus at the time), so it was
|
|
||||||
left for a pass when the emulator is free rather than disrupting that
|
|
||||||
session. Everything reachable without the emulator is clean: `cargo
|
|
||||||
fmt`/`clippy --workspace --all-targets`/`test --workspace`, `cargo ndk
|
|
||||||
build`/`clippy` for `iris-android-app` with `force-gles`, and
|
|
||||||
`gpu-probe` against this VM's own Vulkan and GL(ES 3.2, which still has
|
|
||||||
compute and so would not have reproduced the crash even before this
|
|
||||||
fix — not a substitute for the real ES-3.0 test).
|
|
||||||
|
|
||||||
- **P0's Compose half is built and smoke-tested on the emulator** — the
|
|
||||||
`bench` build type, the shared `app/bench-fixture/` transcript, and an
|
|
||||||
in-process fake backend (`BenchFixture.kt`/`BenchNetwork.kt`) that
|
|
||||||
answers `TranscriptSource`/`EventStream` from an in-memory event log
|
|
||||||
instead of a real server, so the fold and paging under test are the real
|
|
||||||
ones. Full account, the smoke run's report, and what is deliberately
|
|
||||||
left (the iris half, the real on-phone runs) are in RUST.md's P0 box.
|
|
||||||
Not a decision to review so much as the gate itself now being runnable —
|
|
||||||
flagged here because it is the first half of something Iris explicitly
|
|
||||||
asked to see before P1.
|
|
||||||
|
|
||||||
- **P0's iris half is also built and smoke-tested on the emulator,
|
|
||||||
2026-09-05.** A new `bench` Cargo feature on `iris-android-app`, on top
|
|
||||||
of `transcript-screen`: the same checked-in fixture (`include_str!`, no
|
|
||||||
asset pipeline needed), the same 24-swipe scroll loop animated through
|
|
||||||
`List::scroll` and the same 400-event/20s streaming phase through
|
|
||||||
`fold_event`, "Run benchmark"/"Copy report" as named accessible
|
|
||||||
controls, and the same three added report fields (process CPU time,
|
|
||||||
peak RSS, battery current) via direct JNI calls
|
|
||||||
(`bench_jni.rs::PlatformHandle`) since `android_view` has no
|
|
||||||
`BatteryManager`/`ClipboardManager` wrapper of its own. One small public
|
|
||||||
API addition to get there: `AndroidAppState::platform_ready` (`IRIS.md`),
|
|
||||||
a default-no-op lifecycle hook handing an implementor a `JavaVM` +
|
|
||||||
`GlobalRef` it can call Java through from any thread. Packaged with a
|
|
||||||
new `release` build type on `iris-android-app`'s own Gradle project
|
|
||||||
(there was previously only `debug`), signed with the same key
|
|
||||||
`app/build-apk.sh` generates. Smoke run and the full report are in
|
|
||||||
RUST.md's P0 box; not attempted this pass: the real on-phone runs and
|
|
||||||
Iris's pass/fail call, which is the actual gate.
|
|
||||||
|
|
||||||
- **The intermittent touch-scroll dropout is root-caused and fixed: a
|
|
||||||
missed `ACTION_DOWN` hit-test, not the previously-suspected coalesced
|
|
||||||
first `ACTION_MOVE`.** Diagnosed by temporary logcat tracing of every
|
|
||||||
touch event, `DragArbiter` state transition and `Selection::drag`
|
|
||||||
dispatch (removed once confirmed), reproduced on this checkout's own
|
|
||||||
emulator against a real sandbox session. The trace showed the actual
|
|
||||||
mechanism: a gesture's `ACTION_DOWN` lands wherever the finger actually
|
|
||||||
is, which is not guaranteed to fall inside the same row-local sensor
|
|
||||||
region a later `ACTION_MOVE` in the same gesture lands in (a row's own
|
|
||||||
padding/gap, or its non-selectable sender-name header, is
|
|
||||||
pointer-transparent to `iris::sense::CursorSense`). When that happens,
|
|
||||||
the widget that ends up handling the gesture never saw `PressStart`, so
|
|
||||||
`DragArbiter` sits in `Idle` — which answers every subsequent frame with
|
|
||||||
`Undecided` and has no way to tell "no press is happening" from "a press
|
|
||||||
is happening but I missed its start," so it never recovers on its own
|
|
||||||
for the rest of that gesture. One real trace showed exactly this: touch
|
|
||||||
`Down`/`Move`/`Up` all delivered correctly, but zero `PressStart`
|
|
||||||
reaching the arbiter, `state=Idle` unchanged from first frame to last.
|
|
||||||
Fixed at the call site that has the context to recover
|
|
||||||
(`iris::transcript_ui::selection::Selection::drag`,
|
|
||||||
`iris/transcript-ui/src/selection.rs`): a new `DragArbiter::is_idle()`
|
|
||||||
(`iris/src/sense.rs`) lets it notice a `Pressing` frame arriving with the
|
|
||||||
arbiter still `Idle` — which can only mean a missed `PressStart`, since a
|
|
||||||
`Pressing` sense requires the button to genuinely be down — and start the
|
|
||||||
press there instead of where it was missed. Three new unit tests in
|
|
||||||
`sense.rs`'s `drag_arbiter_tests` and one in `transcript-ui`'s
|
|
||||||
`selection::tests` (the latter fails on the code before this fix).
|
|
||||||
Commit follows. Not the same failure the earlier pass's `DECISIONS.md`
|
|
||||||
DEFERRED item speculated about (a coalesced first `ACTION_MOVE` skipping
|
|
||||||
slop detection) — that hypothesis is now ruled out; the arbiter's own
|
|
||||||
slop/long-press logic was never wrong. RUST.md's I5 box,
|
|
||||||
"Touch-scroll dropout root-caused, 2026-09-05" has the full trace.
|
|
||||||
- **P0, a phone benchmark gate before any porting, asked for by Iris
|
|
||||||
2026-09-05**: "before P1 I'd like to see benchmarks & also maybe stress
|
|
||||||
test on my own phone ... If it doesn't match compose reasonably well then
|
|
||||||
I don't think I'd wanna continue." Design (RUST.md's P0 box has the
|
|
||||||
detail): the same embedded synthetic fixture in both apps with no server
|
|
||||||
needed; the same scripted scroll loop then a streaming phase, run
|
|
||||||
programmatically since the phone has no usable system tracing and no
|
|
||||||
agent can drive it; the same report from both (frames, janky %, p50/p90/
|
|
||||||
p99, process CPU time, peak RSS, battery current where readable) with a
|
|
||||||
copy button; the iris app under its own id and the Compose one as a new
|
|
||||||
`bench` build type with an id suffix, so neither replaces her production
|
|
||||||
install; two arm64 APKs plus instructions delivered under `~/host/bench/`.
|
|
||||||
The gate is hers: iris within a reasonable margin of Compose release on
|
|
||||||
p50, p99 and CPU time, no crashes, no visible stutter. If it fails, the
|
|
||||||
port stops.
|
|
||||||
- **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`)
|
|
||||||
times each frame's wall clock from the same point `render()`'s redraw
|
|
||||||
starts to just after `queue.submit` + `present()` — the span Compose's
|
|
||||||
own render report and `gfxinfo` both count — into a fixed 4096-entry
|
|
||||||
ring (no allocation per frame; `report()` is the only place that
|
|
||||||
allocates, and only on a button tap). The report gives total frames,
|
|
||||||
janky % over the same 16.7ms budget `gfxinfo` uses, P50/P90/P99 and the
|
|
||||||
worst, plus a reset. Exposed the way the Compose app's copy-button
|
|
||||||
report already is: two named controls ("Frame report", "Reset frame
|
|
||||||
report") on the transcript screen, tappable by accessibility name via
|
|
||||||
`ui-trace`, logging under this crate's fixed `android_logger` tag
|
|
||||||
(`iris-android-app`) so a script can grep `"iris frame report"` the way
|
|
||||||
`transcript-bench.sh` greps `"ai-app render report"`. The report's own
|
|
||||||
`Display` line says plainly that it measures up to the `present()` call
|
|
||||||
returning, not GPU/compositor completion — wgpu's `present()` is not
|
|
||||||
fenced against either, so presenting that span as "time to reach the
|
|
||||||
screen" would be a measured-looking number that is actually inferred,
|
|
||||||
which the standing UI rule forbids.
|
|
||||||
- **`ui-trace` gains a hold-then-drag gesture, additive, in
|
|
||||||
`emulator-tools`.** Neither of its two existing actions can produce
|
|
||||||
"hold stationary for `LONG_PRESS`, then move without lifting" — `tap`
|
|
||||||
has no hold and `swipe X1 Y1 X2 Y2 MS` interpolates motion across its
|
|
||||||
whole duration from t=0. A new action presses, waits, then moves to a
|
|
||||||
second point and releases as one continuous touch (raw
|
|
||||||
`sendevent`/`MotionEvent` injection, extending whatever mechanism the
|
|
||||||
existing `swipe` already uses), so `DragArbiter`'s pan-vs-select rule
|
|
||||||
(`iris/src/sense.rs`, already covered by 8 unit tests against a
|
|
||||||
synthetic clock) can finally be driven on a real device instead of only
|
|
||||||
in a test harness.
|
|
||||||
- **Touch drag on a transcript row follows Android's own rule**: a vertical
|
|
||||||
drag pans the list immediately; a stationary press held 500 ms starts a
|
|
||||||
text selection which further dragging extends; a horizontal drag while
|
|
||||||
something is already selected extends that selection without the wait.
|
|
||||||
One `DragArbiter` per list decides it (`iris/src/sense.rs`). Chosen over a
|
|
||||||
"text layer always wins" or "list always wins" rule because either loses
|
|
||||||
one of the two gestures a reader expects.
|
|
||||||
- **E4's desktop shape is a new `iris/desktop-app` crate**: a winit window
|
|
||||||
holding `transcript-ui`'s screen beside a session list, talking to a real
|
|
||||||
`ai-server` through `client-core`. It enrols by pasting the same
|
|
||||||
`aiapp://enroll?…` link a phone scans (`client-core::config::EnrolledServer`)
|
|
||||||
and keeps it owner-only under `$XDG_CONFIG_HOME/ai-app-desktop/`. The
|
|
||||||
pinned CA is a path given on the command line, not baked in. Chosen so
|
|
||||||
the phone and desktop share one enrolment format and no second one is
|
|
||||||
invented.
|
|
||||||
- **I5's Android integration extends `iris-android-app` (I2's shell)
|
|
||||||
behind a Cargo feature (`transcript-screen`), rather than a third
|
|
||||||
shell crate.** That project already has the Gradle module, the
|
|
||||||
`IrisView`/`MainActivity` Java, and the JNI registration; the only
|
|
||||||
thing a second screen needs on top is a different `AndroidAppState`,
|
|
||||||
the same axis `tabs_ui::build`/`transcript_ui::build` already vary
|
|
||||||
along on the winit side. `tabs-screen`/`transcript-screen` are
|
|
||||||
mutually exclusive and each pulls in only its own deps, so the plain
|
|
||||||
tabs build (I2/I4) is untouched.
|
|
||||||
- **Order of remaining work, updated 2026-09-05**: the two in-flight
|
|
||||||
pieces and I5's Android integration are all done; next is giving iris
|
|
||||||
its own frame-timing report so item 3 below can be decided by a number.
|
|
||||||
- **DECIDED by Iris, 2026-09-05: iris is the app's framework; Masonry was
|
|
||||||
the calibration.** Her words: "I think iris definitely makes more sense
|
|
||||||
based on the limitations we've found." The limitations: Masonry has no
|
|
||||||
touch scroll on Android (E2), no per-span rich text and no cross-row
|
|
||||||
selection on the pinned commit (E2), and its keyboard bridge is a TODO
|
|
||||||
(E1); iris carries the same screen under the Compose baseline on the
|
|
||||||
host GPU (p50 15.0 ms against Compose's 20.0 ms, RUST.md's I5 box). What
|
|
||||||
follows: the E-steps are closed as calibration, and the port proceeds
|
|
||||||
on iris — screens, the shell (E3/E5), and `client-core` underneath.
|
|
||||||
The item below is kept as the record of what she decided from.
|
|
||||||
- **Was DEFERRED — whether to commit to iris over Masonry for `ai-app`.**
|
|
||||||
Updated 2026-09-05 with the clean comparison the recommendation wanted:
|
|
||||||
same sandbox session content, same emulator, `EMU_GPU=software`, one
|
|
||||||
session. Headline numbers (RUST.md's I5 box, "Clean scroll comparison,
|
|
||||||
2026-09-05," has the full table and every caveat):
|
|
||||||
|
|
||||||
| app | build | frames | janky % | p50 | p90 | p99 | worst |
|
|
||||||
|---|---|---|---|---|---|---|---|
|
|
||||||
| Compose (in-app report) | debug | 1102 | 99.0% late | 33.8ms | 50.6ms | 79.5ms | -- |
|
|
||||||
| Compose (`dumpsys gfxinfo`) | debug | 1499 | 21.15% (95.66% legacy) | 32ms | 48ms | 150ms (p99) | -- |
|
|
||||||
| iris (`FrameReport`) | **release** | 299 | 94.65% | 79.1ms | 98.6ms | 117.8ms | 212.6ms |
|
|
||||||
| iris (`FrameReport`, repeat) | **release** | 233 | 94.42% | 109.3ms | 130.8ms | 147.1ms | 150.5ms |
|
|
||||||
|
|
||||||
**Not a clean apples-to-apples reading, stated plainly rather than
|
|
||||||
smoothed over**: iris had to be built **release** (debug `SIGSEGV`s on
|
|
||||||
this emulator's Vulkan loader, I4's finding) against Compose's mandated
|
|
||||||
**debug** build, so this asymmetry likely *understates* iris's gap
|
|
||||||
rather than the reverse; the three frame-time sources measure different
|
|
||||||
things (Compose's own phase accounting vs. Android's HWUI deadline-miss
|
|
||||||
definition vs. iris's redraw-start-to-present window, the last of which
|
|
||||||
`dumpsys gfxinfo` cannot see at all for iris's `SurfaceView`); and both
|
|
||||||
figures are emulator numbers under software rasterisation, which
|
|
||||||
Compose's *own* in-app report shows already costs 20-34ms/frame in
|
|
||||||
`swap`+`gpu` alone under this GPU mode, so a same-mode iris number well
|
|
||||||
above 16.7ms was expected going in for either app. A second pair under
|
|
||||||
`-gpu host` was not taken this pass. The earlier session's suspected
|
|
||||||
intermittent touch-delivery dropout was **not reproduced** this pass —
|
|
||||||
the zero-frame results this time traced to this pass's own script bug
|
|
||||||
(a `cd` that changed which emulator `ui-trace` targeted), not the
|
|
||||||
emulator; a CPU-load rise during the gesture was observed by a sampler
|
|
||||||
running throughout, but did not correlate with any failure, so the
|
|
||||||
original candidate is neither confirmed nor ruled out.
|
|
||||||
The choice in front of Iris, updated: decide now on the
|
|
||||||
structural-plus-functional case already made (iris works end-to-end
|
|
||||||
where Masonry's scroll gesture doesn't exist at all on Android) plus
|
|
||||||
this table — reading the two build profiles and three jank definitions
|
|
||||||
with the caveats above rather than as a single number — or ask for a
|
|
||||||
same-profile, same-GPU-mode rerun first. RUST.md's I5 box has the full
|
|
||||||
account.
|
|
||||||
|
|
||||||
**Updated 2026-09-05, the `-gpu host` pair taken.** Real GPU rendering
|
|
||||||
(`force-gles` -- the default Vulkan backend has no adapter at all under
|
|
||||||
plain host-GPU boot, confirmed by the exact `wgpu` error) reverses the
|
|
||||||
software-mode shape:
|
|
||||||
|
|
||||||
| app | build | GPU mode | frames | janky % | p50 | p90 | p99 | worst | cpu p50 | gpu-wait p50 |
|
|
||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
||||||
| Compose (in-app report) | debug | host (virgl) | 1268 | 96.4% late | 20.0ms | 28.4ms | 37.7ms | -- | -- | -- |
|
|
||||||
| iris (`FrameReport`), **best of three, 2026-09-05** | release, `force-gles` | host (virgl) | 439 | 46.24% | 15.7ms | 23.3ms | 31.2ms | 57.4ms | 1.2ms | 13.2ms |
|
|
||||||
|
|
||||||
Under real GPU rendering iris's median frame is *faster* than
|
|
||||||
Compose's, not the 2-3x-slower shape the software-mode table shows. A
|
|
||||||
new split inside `FrameReport` (redraw-to-submit vs. submit-to-present,
|
|
||||||
commit `e2a1fad`) says why: iris's own CPU work per frame is a median
|
|
||||||
~1ms -- almost the entire frame is time spent handing the frame to the
|
|
||||||
driver, not in iris's layout/text/primitive code. This is consistent
|
|
||||||
with the earlier software-mode gap being mostly SwiftShader's CPU
|
|
||||||
rasterisation cost rather than an iris-specific slowness. **Still not
|
|
||||||
proof, and now closed as unanswerable rather than merely untaken**: a
|
|
||||||
same-mode software `force-gles` run to isolate the backend was retried
|
|
||||||
2026-09-05 after fixing the compute-limit crash the first attempt hit,
|
|
||||||
and hit a second, structural wall instead — SwiftShader's ES 3.0 GL
|
|
||||||
path has no storage-buffer capacity at all, and `shader.wgsl` reads
|
|
||||||
`var<storage>` buffers unconditionally, so reaching that path needs a
|
|
||||||
shader rewrite, not a limits fix (RUST.md's I5 box, "The three
|
|
||||||
remaining I5 verifications, closed 2026-09-05," item 2). The
|
|
||||||
intermittent touch-scroll dropout this pass also reproduced is
|
|
||||||
root-caused and fixed as of the same date (a missed `ACTION_DOWN` on a
|
|
||||||
row's padding/header left `DragArbiter` stuck in `Idle`); three clean
|
|
||||||
`iris-scroll.sh` runs post-fix each scrolled all 24/24 swipes, replacing
|
|
||||||
the single-attempt 62-frame reading this table used to carry. RUST.md's
|
|
||||||
I5 box, "Where iris's frame time goes, 2026-09-05, the `-gpu host`
|
|
||||||
pass," and "The three remaining I5 verifications, closed 2026-09-05,"
|
|
||||||
have the full account. The iris-vs-Masonry choice itself is still
|
|
||||||
Iris's to make.
|
|
||||||
|
|
||||||
## 2026-09-07: the enrolment link carries the CA, so an APK need not be built where its server runs
|
|
||||||
|
|
||||||
**Problem.** Every phone build pinned the CA of the machine that compiled
|
|
||||||
it -- the Compose app from `GeneratePinnedCert`, the iris app from
|
|
||||||
`build.rs` reading `$XDG_CONFIG_HOME/ai-app/certs/ca.pem`. That is fine
|
|
||||||
while the two are the same machine and impossible when they are not, which
|
|
||||||
is exactly the iris client's situation: cross-compiled in this VM,
|
|
||||||
delivered to a phone, run against `ai-server` on the host. Baking the
|
|
||||||
host/port/token as well made it worse -- a token in a built artifact.
|
|
||||||
|
|
||||||
**Decided: the CA rides in the enrolment link**, as `&ca=<base64url of the
|
|
||||||
DER>` (`wg_app_link::enroll::ca_param`), optional and per mint. The app
|
|
||||||
that opens the link pins what the link said, and an APK built anywhere
|
|
||||||
works against whatever server it is pointed at.
|
|
||||||
|
|
||||||
Two alternatives were worked out and rejected.
|
|
||||||
|
|
||||||
- **A CA *fingerprint* in the link, pinned at the TLS handshake.** The
|
|
||||||
smallest link (43 more characters) and the strongest shape, but `ureq`
|
|
||||||
3.4 exposes no hook for a custom `rustls` `ServerCertVerifier`: its
|
|
||||||
`TlsConfig` builds the `ClientConfig` itself, so this needs a hand-written
|
|
||||||
`Connector` on the `unversioned` API and `rustls` as a direct dependency
|
|
||||||
of `client-core`. A lot of machinery in the one crate that must stay
|
|
||||||
light.
|
|
||||||
- **A fingerprint in the link plus an unauthenticated `GET /ca.pem`.**
|
|
||||||
Small code, but it needs a first connection with verification disabled,
|
|
||||||
and it breaks a documented, tested posture -- `auth.rs`'s "gates every
|
|
||||||
route with zero unauthenticated endpoints", which is a load-bearing
|
|
||||||
decision rather than an implementation detail. Not something to change
|
|
||||||
silently for this.
|
|
||||||
|
|
||||||
**What it costs**, measured rather than guessed: on this project's P-256
|
|
||||||
CA the link goes from 89 bytes to 652, and `print_enrollment`'s terminal
|
|
||||||
QR from 45x23 to 93x47 characters. That is why the parameter is the
|
|
||||||
minter's choice per call: `ai-server` passes it (its iris client needs it),
|
|
||||||
`dev-updater` passes `None` (its app is built on the machine it talks to,
|
|
||||||
and its QR stays scannable in an 80-column terminal). The URI printed under
|
|
||||||
the QR is the fallback either way, and is the path Dev Updater's Enroll
|
|
||||||
button already uses -- it opens the link with `ACTION_VIEW`, so Android
|
|
||||||
offers whichever apps registered the scheme, which needed no change here.
|
|
||||||
|
|
||||||
The CA is a public certificate, so putting it in the QR leaks nothing the
|
|
||||||
token did not already: photographing the terminal still costs exactly the
|
|
||||||
token, which is rotatable.
|
|
||||||
|
|
||||||
**The log upload's destination is moot**, so it is not wired to this. On
|
|
||||||
the same day Iris decided Dev Updater will read an APK's runtime log from
|
|
||||||
an on-device ContentProvider instead, which removes `log_upload`,
|
|
||||||
`POST /client-log` and the `AI_APP_LOG_*` baking altogether -- so the
|
|
||||||
enrolment landed without touching any of them, for that change to delete
|
|
||||||
whole.
|
|
||||||
-1986
File diff suppressed because it is too large.
Load diff
+3
-4
@@ -6,10 +6,9 @@ than rediscovering them. Each item says where it sits in the order and
|
|||||||
what "done" looks like.
|
what "done" looks like.
|
||||||
|
|
||||||
**Only open items live here.** An item is deleted when it lands, not
|
**Only open items live here.** An item is deleted when it lands, not
|
||||||
ticked -- what it changed belongs in `docs/IRIS.md` (the design log), and
|
ticked: a list of finished work is context every future session pays for,
|
||||||
a list of finished work is context every future session pays for. Fifty
|
and what a change did belongs at the code it changed. Fifty closed items
|
||||||
closed items and six phone-report sections were removed on 2026-09-08 for
|
and six phone-report sections went on 2026-09-08 for that reason.
|
||||||
that reason.
|
|
||||||
|
|
||||||
## Fix
|
## Fix
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@ in `render_state.rs` -- each is a real bug the first draft would have
|
|||||||
reproduced), and the two later additions that build on it. The
|
reproduced), and the two later additions that build on it. The
|
||||||
pre-implementation framing -- what the old trait looked like, the checklist
|
pre-implementation framing -- what the old trait looked like, the checklist
|
||||||
the design had to answer, the migration list, the pass conditions and the
|
the design had to answer, the migration list, the pass conditions and the
|
||||||
"copy this into IRIS.md" note -- was deleted on 2026-09-08, having been
|
"copy this into the design log" note -- was deleted on 2026-09-08,
|
||||||
carried out; `docs/IRIS.md`'s 2026-09-04 entry is the public-API record.
|
having been carried out.
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
|
|||||||
+12
-13
@@ -10,8 +10,8 @@ piece from scratch where it does not fit is fine; effort and elapsed time
|
|||||||
do not matter, long-term robustness does.
|
do not matter, long-term robustness does.
|
||||||
|
|
||||||
**The framework question is closed.** Iris chose her own library,
|
**The framework question is closed.** Iris chose her own library,
|
||||||
[iris](https://github.com/cat16/iris), over Masonry on 2026-09-05
|
[iris](https://github.com/cat16/iris), over Masonry on 2026-09-05.
|
||||||
(`DECISIONS.md`); the bake-off that got there, and the twelve experiments
|
The bake-off that got there, and the twelve experiments
|
||||||
that proved it on a device, are summarised in "What the experiments
|
that proved it on a device, are summarised in "What the experiments
|
||||||
settled" below rather than kept at length. What is left in this file is
|
settled" below rather than kept at length. What is left in this file is
|
||||||
the plan for the rest of the app and the findings that outlive the tasks
|
the plan for the rest of the app and the findings that outlive the tasks
|
||||||
@@ -57,7 +57,7 @@ re-derived: measurements, dead ends, invariants and their reasons.
|
|||||||
at the end -- it is the layout everything else here assumes.
|
at the end -- it is the layout everything else here assumes.
|
||||||
- **Open across the rest of the docs**: `docs/IRIS_TODO.md` is iris's own
|
- **Open across the rest of the docs**: `docs/IRIS_TODO.md` is iris's own
|
||||||
list (streaming re-layout is the live one), `docs/TODO.md` is the Compose
|
list (streaming re-layout is the live one), `docs/TODO.md` is the Compose
|
||||||
app's, and `docs/DECISIONS.md` is the queue of choices for Iris to review.
|
app's.
|
||||||
|
|
||||||
## Desktop and phone share the code (Iris, 2026-09-07)
|
## Desktop and phone share the code (Iris, 2026-09-07)
|
||||||
|
|
||||||
@@ -358,7 +358,7 @@ needs its reasoning, the reasoning is at the thing itself.
|
|||||||
posts a real notification and receives a real share, and `cargo xtask
|
posts a real notification and receives a real share, and `cargo xtask
|
||||||
apk` packages an installable APK with `javac`/`d8`/`aapt2`/`zipalign`/
|
apk` packages an installable APK with `javac`/`d8`/`aapt2`/`zipalign`/
|
||||||
`apksigner` and one disclosed Gradle call, documented at
|
`apksigner` and one disclosed Gradle call, documented at
|
||||||
`xtask/src/apk.rs`'s module doc.
|
`scripts/xtask/src/apk.rs`'s module doc.
|
||||||
- **E4 -- the same screen on the desktop**, which is now
|
- **E4 -- the same screen on the desktop**, which is now
|
||||||
`app-rust`'s `src/desktop` and the `ai-app-desktop` binary.
|
`app-rust`'s `src/desktop` and the `ai-app-desktop` binary.
|
||||||
|
|
||||||
@@ -516,13 +516,13 @@ device.
|
|||||||
(`ui::markdown::BlockFrame`) — plain, verbatim, quote — with
|
(`ui::markdown::BlockFrame`) — plain, verbatim, quote — with
|
||||||
fences and tables verbatim, headings scaled, and inline
|
fences and tables verbatim, headings scaled, and inline
|
||||||
styling per span. `app-rust/src/ui/markdown.rs` is the code
|
styling per span. `app-rust/src/ui/markdown.rs` is the code
|
||||||
and `docs/IRIS.md`'s 2026-09-06 entry the design.
|
and its module doc the design.
|
||||||
- [x] **P1b — tool-call cards and grouping.** Done 2026-09-06.
|
- [x] **P1b — tool-call cards and grouping.** Done 2026-09-06.
|
||||||
`ToolRows.kt`/`ToolInput.kt` ported to
|
`ToolRows.kt`/`ToolInput.kt` ported to
|
||||||
`app-rust/src/ui/tool.rs`: a run of calls is one collapsible
|
`app-rust/src/ui/tool.rs`: a run of calls is one collapsible
|
||||||
group, each card carries its state and summary, and the five
|
group, each card carries its state and summary, and the five
|
||||||
`ToolState` values each have their own appearance.
|
`ToolState` values each have their own appearance.
|
||||||
`docs/DECISIONS.md`'s 2026-09-06 entries have what was chosen.
|
`tool.rs`'s module doc has what was chosen.
|
||||||
- [ ] **P1c — history paging and jump-to-latest.** Wire
|
- [ ] **P1c — history paging and jump-to-latest.** Wire
|
||||||
`client::transcript_source` into `src/ui`:
|
`client::transcript_source` into `src/ui`:
|
||||||
the opening page, paging back on scroll with the cushion
|
the opening page, paging back on scroll with the cushion
|
||||||
@@ -599,13 +599,12 @@ device.
|
|||||||
paged). Re-take the I5 `FrameReport` (`iris frame report` in
|
paged). Re-take the I5 `FrameReport` (`iris frame report` in
|
||||||
logcat, same as I5's box) once this screen has real paging and
|
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
|
compare it against I5's own numbers, not against Compose's — the
|
||||||
three measurement sources still are not comparable per
|
three measurement sources still are not comparable.
|
||||||
`DECISIONS.md`'s DEFERRED item.
|
|
||||||
|
|
||||||
- [ ] **P2 — the shell merge and a real phone install.** Merge this
|
- [ ] **P2 — the shell merge and a real phone install.** Merge this
|
||||||
screen's cdylib into the E3/E5 shell (`src/shell` +
|
screen's cdylib into the E3/E5 shell (`src/shell` +
|
||||||
`app/shellApp`) behind the same feature-flag pattern I5 used to
|
`app/shellApp`) behind the same feature-flag pattern I5 used to
|
||||||
extend `src/android` (`DECISIONS.md`, 2026-09-05), so there is
|
extend `src/android` (decided 2026-09-05), so there is
|
||||||
one app — notification service, share target and the real screen —
|
one app — notification service, share target and the real screen —
|
||||||
rather than a demo shell and a service shell side by side. Package
|
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
|
with `cargo xtask apk` (E5) and get it onto the real GrapheneOS
|
||||||
@@ -753,7 +752,7 @@ push, in the session that picked the task up.
|
|||||||
`main` and not in `ai-app`. Nothing on this branch is production until
|
`main` and not in `ai-app`. Nothing on this branch is production until
|
||||||
Iris says so. Commit and push as you go.
|
Iris says so. Commit and push as you go.
|
||||||
3. The E- and I-steps (the framework decision) are done — iris won,
|
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
|
decided 2026-09-05. Take the next unchecked P-box in "## The
|
||||||
port, in order (decided 2026-09-05)"; **P1 — session screen parity —
|
port, in order (decided 2026-09-05)"; **P1 — session screen parity —
|
||||||
is next.**
|
is next.**
|
||||||
4. Every step ends with its measurement written into this file beside the
|
4. Every step ends with its measurement written into this file beside the
|
||||||
@@ -764,7 +763,7 @@ push, in the session that picked the task up.
|
|||||||
for a server with fixtures, `transcript-bench.sh` for the scroll
|
for a server with fixtures, `transcript-bench.sh` for the scroll
|
||||||
baseline, `ui-trace` for anything positional, `emu up` for the
|
baseline, `ui-trace` for anything positional, `emu up` for the
|
||||||
emulator, `iris/run-headless.sh EXAMPLE --shot PNG` for an iris
|
emulator, `iris/run-headless.sh EXAMPLE --shot PNG` for an iris
|
||||||
example on this displayless machine, and `rigs/gpu-probe` to ask a
|
example on this displayless machine, and `scripts/rigs/gpu-probe` to ask a
|
||||||
device (this VM, the emulator, or a real phone over `adb push`) what
|
device (this VM, the emulator, or a real phone over `adb push`) what
|
||||||
`wgpu` features and limits it actually has before building anything on
|
`wgpu` features and limits it actually has before building anything on
|
||||||
the assumption it does. The Vulkan section below says how to get a
|
the assumption it does. The Vulkan section below says how to get a
|
||||||
@@ -810,7 +809,7 @@ Nine cargo workspaces, each with its own `Cargo.lock` and `target/`, and
|
|||||||
the port's project code in five places — `iris/transcript-ui`,
|
the port's project code in five places — `iris/transcript-ui`,
|
||||||
`iris/transcript-fixture`, `iris/desktop-app`, `iris/android-app` (all
|
`iris/transcript-fixture`, `iris/desktop-app`, `iris/android-app` (all
|
||||||
*inside* the framework), plus `client-core` and `android-shell` at the
|
*inside* the framework), plus `client-core` and `android-shell` at the
|
||||||
root. Two root markdown files (`DECISIONS.md`, `SUBAGENTS.md`) sat outside
|
root. Two root markdown files sat outside
|
||||||
`docs/`.
|
`docs/`.
|
||||||
|
|
||||||
### What it is
|
### What it is
|
||||||
@@ -896,7 +895,7 @@ So: three workspaces where there were nine — `event-model`, `server`,
|
|||||||
|
|
||||||
### Verified
|
### Verified
|
||||||
|
|
||||||
`./run-tests.sh` (event-model, server, app-rust) and `cd iris && cargo
|
`./scripts/run-tests.sh` (event-model, server, app-rust) and `cd iris && cargo
|
||||||
test` green; `cargo clippy --all-targets` and `cargo fmt` clean in every
|
test` green; `cargo clippy --all-targets` and `cargo fmt` clean in every
|
||||||
workspace. `cargo ndk -t x86_64` links `libai_app.so`; `./build-apk.sh
|
workspace. `cargo ndk -t x86_64` links `libai_app.so`; `./build-apk.sh
|
||||||
debug --abi x86_64` produces an installable APK; installed and launched on
|
debug --abi x86_64` produces an installable APK; installed and launched on
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# Scrolling in iris
|
# Scrolling in iris
|
||||||
|
|
||||||
How anything in iris scrolls, as of 2026-09-08. This is the current
|
How anything in iris scrolls, as of 2026-09-08. This is the current
|
||||||
design, not a history — `docs/IRIS.md`'s dated entries have the account of
|
design, not a history — the git log has the account of
|
||||||
how it got here, and `docs/IRIS_TODO.md` has what is still open.
|
how it got here, and `docs/IRIS_TODO.md` has what is still open.
|
||||||
|
|
||||||
Read this before touching `iris/src/widget/position/scrollable.rs`,
|
Read this before touching `iris/src/widget/position/scrollable.rs`,
|
||||||
|
|||||||
+41
-1
@@ -3,7 +3,7 @@
|
|||||||
A session's subagents -- the helpers a Claude Code session starts through its
|
A session's subagents -- the helpers a Claude Code session starts through its
|
||||||
Task tool -- each get a transcript of their own, listed under the session's
|
Task tool -- each get a transcript of their own, listed under the session's
|
||||||
card and readable in the same transcript view the session has. Designed
|
card and readable in the same transcript view the session has. Designed
|
||||||
2026-09-05; the decisions Bryan has not yet reviewed are in `SUBAGENTS_DECISIONS.md`.
|
2026-09-05; the choices behind it are at the bottom of this file.
|
||||||
|
|
||||||
## What a subagent is here
|
## What a subagent is here
|
||||||
|
|
||||||
@@ -139,3 +139,43 @@ The per-subagent status is only read when the list route is asked for.
|
|||||||
cache take a transcript address rather than a session id --
|
cache take a transcript address rather than a session id --
|
||||||
`sessions/{id}` or `sessions/{id}/subagents/{sub}` -- so the cache nests a
|
`sessions/{id}` or `sessions/{id}/subagents/{sub}` -- so the cache nests a
|
||||||
subagent's copy under its session's and the same code serves both.
|
subagent's copy under its session's and the same code serves both.
|
||||||
|
|
||||||
|
## The choices behind the shape, and what they rejected
|
||||||
|
|
||||||
|
|
||||||
|
Made without asking, each with limited blast radius:
|
||||||
|
|
||||||
|
1. **A subagent is a transcript, not a session.** It has no process,
|
||||||
|
controls or settings; it is addressed as `/sessions/{id}/subagents/{sub}`
|
||||||
|
and stored under the session's directory, so deleting the session takes
|
||||||
|
it. Alternative rejected: registering it as a session of its own, which
|
||||||
|
would give it a card in the main list and a driver that can do nothing.
|
||||||
|
2. **Read-only view is the session screen minus its controls**, rather than
|
||||||
|
a second, simpler transcript screen. Keeps paging, caching, selection
|
||||||
|
and rendering in one place. Cost: a `readOnly` mode threaded through
|
||||||
|
`SessionScreen`.
|
||||||
|
3. **The list only carries a count.** Each session row says how many
|
||||||
|
subagents it has; their titles and statuses are fetched when the card is
|
||||||
|
expanded. Keeps `GET /sessions` from reading every subagent transcript.
|
||||||
|
Consequence: an expanded card's statuses refresh with the list, not live.
|
||||||
|
4. **Expanded/collapsed is remembered per session on the phone**, not on
|
||||||
|
the server. Collapsed by default, per the transcript convention that new
|
||||||
|
things arrive collapsed.
|
||||||
|
5. **Subagents of imported sessions are not shown.** The import path still
|
||||||
|
skips `isSidechain` records; the CLI's own `subagents/agent-*.jsonl` files
|
||||||
|
are not read. Only subagents run while this backend was watching exist.
|
||||||
|
6. **Echo grows `/subagent [n]`** as the test rig, so nothing here needs a
|
||||||
|
paid turn to exercise.
|
||||||
|
|
||||||
|
Deferred, because they reach further than this feature:
|
||||||
|
|
||||||
|
- **Live status on the list.** Whether the session list should follow a
|
||||||
|
stream at all (it refreshes on demand today) decides whether subagent
|
||||||
|
status can ever be live there. Not changed.
|
||||||
|
- **Nested subagents.** A subagent's own Task calls are shown as tool calls
|
||||||
|
in its transcript and are not given transcripts of their own. Supporting
|
||||||
|
that is the same mechanism one level down, but the UI would need nested
|
||||||
|
expanders.
|
||||||
|
|
||||||
|
- **The subagent status row says "context unknown".** Nothing measures a
|
||||||
|
subagent's context; the row could leave it out rather than admit it.
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Decisions awaiting review
|
|
||||||
|
|
||||||
Choices made while working autonomously, for Bryan to keep or change. Each
|
|
||||||
says what was picked and why; the detail is in the design doc it names.
|
|
||||||
Delete an entry once it has been looked at.
|
|
||||||
|
|
||||||
## Subagent views (2026-09-05, `SUBAGENTS.md`)
|
|
||||||
|
|
||||||
Made on my own judgement, limited blast radius:
|
|
||||||
|
|
||||||
1. **A subagent is a transcript, not a session.** It has no process,
|
|
||||||
controls or settings; it is addressed as `/sessions/{id}/subagents/{sub}`
|
|
||||||
and stored under the session's directory, so deleting the session takes
|
|
||||||
it. Alternative rejected: registering it as a session of its own, which
|
|
||||||
would give it a card in the main list and a driver that can do nothing.
|
|
||||||
2. **Read-only view is the session screen minus its controls**, rather than
|
|
||||||
a second, simpler transcript screen. Keeps paging, caching, selection
|
|
||||||
and rendering in one place. Cost: a `readOnly` mode threaded through
|
|
||||||
`SessionScreen`.
|
|
||||||
3. **The list only carries a count.** Each session row says how many
|
|
||||||
subagents it has; their titles and statuses are fetched when the card is
|
|
||||||
expanded. Keeps `GET /sessions` from reading every subagent transcript.
|
|
||||||
Consequence: an expanded card's statuses refresh with the list, not live.
|
|
||||||
4. **Expanded/collapsed is remembered per session on the phone**, not on
|
|
||||||
the server. Collapsed by default, per the transcript convention that new
|
|
||||||
things arrive collapsed.
|
|
||||||
5. **Subagents of imported sessions are not shown.** The import path still
|
|
||||||
skips `isSidechain` records; the CLI's own `subagents/agent-*.jsonl` files
|
|
||||||
are not read. Only subagents run while this backend was watching exist.
|
|
||||||
6. **Echo grows `/subagent [n]`** as the test rig, so nothing here needs a
|
|
||||||
paid turn to exercise.
|
|
||||||
|
|
||||||
Deferred, because they reach further than this feature:
|
|
||||||
|
|
||||||
- **Live status on the list.** Whether the session list should follow a
|
|
||||||
stream at all (it refreshes on demand today) decides whether subagent
|
|
||||||
status can ever be live there. Not changed.
|
|
||||||
- **Nested subagents.** A subagent's own Task calls are shown as tool calls
|
|
||||||
in its transcript and are not given transcripts of their own. Supporting
|
|
||||||
that is the same mechanism one level down, but the UI would need nested
|
|
||||||
expanders.
|
|
||||||
|
|
||||||
- **The subagent status row says "context unknown".** Nothing measures a
|
|
||||||
subagent's context; the row could leave it out rather than admit it.
|
|
||||||
+4
-4
@@ -34,7 +34,7 @@ case that grows this array without bound: each attachment becomes its own
|
|||||||
|
|
||||||
## What was measured
|
## What was measured
|
||||||
|
|
||||||
**A new rig, `rigs/gpu-probe`**, asks a device for exactly iris's features
|
**A new rig, `scripts/rigs/gpu-probe`**, asks a device for exactly iris's features
|
||||||
and limits with no window and no APK — a plain executable pushed with
|
and limits with no window and no APK — a plain executable pushed with
|
||||||
`adb push` and run from `/data/local/tmp`. It has two parts:
|
`adb push` and run from `/data/local/tmp`. It has two parts:
|
||||||
`wgpu::Adapter::request_device` with iris's exact `Features`/`Limits`
|
`wgpu::Adapter::request_device` with iris's exact `Features`/`Limits`
|
||||||
@@ -99,7 +99,7 @@ literally fixed. Files: `core/src/primitive/texture.rs` (`Textures`,
|
|||||||
`core/src/render/atlas.rs`, `core/src/ui/painter.rs`,
|
`core/src/render/atlas.rs`, `core/src/ui/painter.rs`,
|
||||||
`core/src/render/mod.rs` (`UiRenderNode`, `UiLimits` removed),
|
`core/src/render/mod.rs` (`UiRenderNode`, `UiLimits` removed),
|
||||||
`core/src/render/shader.wgsl`, `src/default/render.rs`, and
|
`core/src/render/shader.wgsl`, `src/default/render.rs`, and
|
||||||
`rigs/gpu-probe/src/main.rs`.
|
`scripts/rigs/gpu-probe/src/main.rs`.
|
||||||
|
|
||||||
**1. Atlas pages as array layers.** `GpuTextures` owns one
|
**1. Atlas pages as array layers.** `GpuTextures` owns one
|
||||||
`texture_2d_array` (`array_texture`/`array_view`), grown by doubling
|
`texture_2d_array` (`array_texture`/`array_view`), grown by doubling
|
||||||
@@ -215,7 +215,7 @@ list's ordering would have to be reconciled.
|
|||||||
1024px page holding only a handful of glyphs at a time would be mostly
|
1024px page holding only a handful of glyphs at a time would be mostly
|
||||||
wasted space in normal use. Confirmed the revert left `tabs` and
|
wasted space in normal use. Confirmed the revert left `tabs` and
|
||||||
`minimal` byte-identical to the pre-check screenshots afterward.
|
`minimal` byte-identical to the pre-check screenshots afterward.
|
||||||
- **The decisive check**, `rigs/gpu-probe` rewritten to request iris's new
|
- **The decisive check**, `scripts/rigs/gpu-probe` rewritten to request iris's new
|
||||||
(empty) feature/limit set and run on this checkout's own emulator
|
(empty) feature/limit set and run on this checkout's own emulator
|
||||||
(`ai-app-2`, via `emu`), booted with `EMU_GPU=software` so the guest gets
|
(`ai-app-2`, via `emu`), booted with `EMU_GPU=software` so the guest gets
|
||||||
a real Vulkan device (SwiftShader) rather than the `-gpu host` default,
|
a real Vulkan device (SwiftShader) rather than the `-gpu host` default,
|
||||||
@@ -225,7 +225,7 @@ list's ordering would have to be reconciled.
|
|||||||
at all, which reads exactly like the old bindless failure if you don't
|
at all, which reads exactly like the old bindless failure if you don't
|
||||||
know to ask for `EMU_GPU=software`):
|
know to ask for `EMU_GPU=software`):
|
||||||
|
|
||||||
cd rigs/gpu-probe
|
cd scripts/rigs/gpu-probe
|
||||||
ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk/29.0.14206865 \
|
ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk/29.0.14206865 \
|
||||||
cargo ndk -t arm64-v8a -P 26 build --release
|
cargo ndk -t arm64-v8a -P 26 build --release
|
||||||
EMU_GPU=software emu up # from ~/repos/emulator-tools
|
EMU_GPU=software emu up # from ~/repos/emulator-tools
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! **Why a bundled font rather than ordinary Unicode**: the disclosure
|
//! **Why a bundled font rather than ordinary Unicode**: the disclosure
|
||||||
//! mark used to be U+25B8/25BE/25B4 out of whatever face the platform
|
//! mark used to be U+25B8/25BE/25B4 out of whatever face the platform
|
||||||
//! resolved, and once iris stopped bundling fonts (DECISIONS.md,
|
//! resolved, and once iris stopped bundling fonts (decided
|
||||||
//! 2026-09-07) Iris's phone drew an empty box for them and this VM drew a
|
//! 2026-09-07) Iris's phone drew an empty box for them and this VM drew a
|
||||||
//! dot. UI_RULES' answer is not to avoid glyphs but to ship them, which is
|
//! dot. UI_RULES' answer is not to avoid glyphs but to ship them, which is
|
||||||
//! also what the Compose app has always done for its icons
|
//! also what the Compose app has always done for its icons
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use swash::{
|
|||||||
///
|
///
|
||||||
/// This is the one font bundled here, and it is not a text font: body and
|
/// This is the one font bundled here, and it is not a text font: body and
|
||||||
/// monospace text still come from the platform's own collection
|
/// monospace text still come from the platform's own collection
|
||||||
/// (DECISIONS.md, 2026-09-07). An icon is the opposite case -- a small,
|
/// (decided 2026-09-07). An icon is the opposite case -- a small,
|
||||||
/// closed set of codepoints no system font is guaranteed to have -- which
|
/// closed set of codepoints no system font is guaranteed to have -- which
|
||||||
/// is the same division the Compose app makes.
|
/// is the same division the Compose app makes.
|
||||||
const NERD_ICONS: &[u8] = include_bytes!("../../assets/fonts/nerd_icons.ttf");
|
const NERD_ICONS: &[u8] = include_bytes!("../../assets/fonts/nerd_icons.ttf");
|
||||||
@@ -92,7 +92,7 @@ impl Default for TextData {
|
|||||||
/// `SansSerif`/`SystemUi` to `["Roboto Flex", "Roboto", "Noto Sans"]`
|
/// `SansSerif`/`SystemUi` to `["Roboto Flex", "Roboto", "Noto Sans"]`
|
||||||
/// and `Monospace` to the platform's `"monospace"` alias; the desktop
|
/// and `Monospace` to the platform's `"monospace"` alias; the desktop
|
||||||
/// build's backend is fontconfig. No font is bundled or registered
|
/// build's backend is fontconfig. No font is bundled or registered
|
||||||
/// here -- see DECISIONS.md's 2026-09-07 entry for why (matching what
|
/// here -- see the 2026-09-07 decision for why (matching what
|
||||||
/// the Compose app does: it takes body/monospace text from
|
/// the Compose app does: it takes body/monospace text from
|
||||||
/// `FontFamily.Default`/`FontFamily.Monospace`, i.e. Android's Roboto
|
/// `FontFamily.Default`/`FontFamily.Monospace`, i.e. Android's Roboto
|
||||||
/// and its platform monospace face, and ships no text font of its own,
|
/// and its platform monospace face, and ships no text font of its own,
|
||||||
@@ -137,7 +137,7 @@ fn register_icon_font(font_cx: &mut FontContext) -> Option<String> {
|
|||||||
/// `GenericFamily::Monospace` (confirmed against
|
/// `GenericFamily::Monospace` (confirmed against
|
||||||
/// `fontique-0.11.1/src/backend/android.rs`'s `SystemFonts::new`, and still
|
/// `fontique-0.11.1/src/backend/android.rs`'s `SystemFonts::new`, and still
|
||||||
/// present on `linebender/parley`'s `main` as of 2026-09-07, so there is no
|
/// present on `linebender/parley`'s `main` as of 2026-09-07, so there is no
|
||||||
/// released fix to bump to yet -- see DECISIONS.md's 2026-09-07 entry,
|
/// released fix to bump to yet -- see the 2026-09-07 decision,
|
||||||
/// "Platform fonts," for the full account). Two bugs stack, not one:
|
/// "Platform fonts," for the full account). Two bugs stack, not one:
|
||||||
/// `DEFAULT_GENERIC_FAMILIES` looks up the name `"monospace"` *before*
|
/// `DEFAULT_GENERIC_FAMILIES` looks up the name `"monospace"` *before*
|
||||||
/// `fonts.xml` is parsed into that same name map, and even after parsing,
|
/// `fonts.xml` is parsed into that same name map, and even after parsing,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//! wrapped text, one in twelve carrying a small image, scrollable with the
|
//! wrapped text, one in twelve carrying a small image, scrollable with the
|
||||||
//! mouse wheel. Run headless with `iris/run-headless.sh message_list --shot
|
//! mouse wheel. Run headless with `iris/run-headless.sh message_list --shot
|
||||||
//! /tmp/message_list.png` -- there is no display on this machine, so that
|
//! /tmp/message_list.png` -- there is no display on this machine, so that
|
||||||
//! is the only way to see it rendered; `run-tests.sh`/`cargo test` never
|
//! is the only way to see it rendered; `scripts/run-tests.sh`/`cargo test` never
|
||||||
//! touch this file.
|
//! touch this file.
|
||||||
//!
|
//!
|
||||||
//! Rows alternate two background tints so a screenshot can show the
|
//! Rows alternate two background tints so a screenshot can show the
|
||||||
|
|||||||
+1
-1
@@ -1191,7 +1191,7 @@ pub enum GestureOutcome {
|
|||||||
/// terminal release -- keeps reaching the same widget even after the
|
/// terminal release -- keeps reaching the same widget even after the
|
||||||
/// finger has moved off whatever hit region first noticed the press. Iris
|
/// finger has moved off whatever hit region first noticed the press. Iris
|
||||||
/// asked for this to live here rather than in `transcript-ui::Selection`
|
/// asked for this to live here rather than in `transcript-ui::Selection`
|
||||||
/// (2026-09-06, recorded in `IRIS.md`): "dragging should be part of the
|
/// (2026-09-06): "dragging should be part of the
|
||||||
/// default input system ... anything that provides good performance and
|
/// default input system ... anything that provides good performance and
|
||||||
/// can be generalized well is part of iris rather than the app." A caller
|
/// can be generalized well is part of iris rather than the app." A caller
|
||||||
/// still decides what a committed pan or a completed selection *means*
|
/// still decides what a committed pan or a completed selection *means*
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ fn run_shader(gpu: &Gpu, radius: f32) -> Vec<f32> {
|
|||||||
/// longer mapped. libtest runs every `#[test]` on a spawned thread, so a
|
/// longer mapped. libtest runs every `#[test]` on a spawned thread, so a
|
||||||
/// test that opens and closes an instance segfaults *after* printing its
|
/// test that opens and closes an instance segfaults *after* printing its
|
||||||
/// result, which reads exactly like the test failing. Measured
|
/// result, which reads exactly like the test failing. Measured
|
||||||
/// 2026-09-08 with `rigs/gpu-probe`'s `teardown` bin: it
|
/// 2026-09-08 with `scripts/rigs/gpu-probe`'s `teardown` bin: it
|
||||||
/// needs no wgpu (raw `ash` does it too), no GPU work, and no device --
|
/// needs no wgpu (raw `ash` does it too), no GPU work, and no device --
|
||||||
/// an instance created and destroyed on a spawned thread is enough, and
|
/// an instance created and destroyed on a spawned thread is enough, and
|
||||||
/// keeping any one instance alive is enough to prevent it.
|
/// keeping any one instance alive is enough to prevent it.
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Runs this repo's Rust tests. Extra arguments are forwarded to each
|
# Runs this repo's Rust tests. Extra arguments are forwarded to each
|
||||||
# `cargo test`, e.g. `./run-tests.sh transcript` to run just the transcript
|
# `cargo test`, e.g. `scripts/run-tests.sh transcript` to run just the transcript
|
||||||
# tests in every workspace.
|
# tests in every workspace.
|
||||||
#
|
#
|
||||||
# Three workspaces, in dependency order:
|
# Three workspaces, in dependency order:
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
# (`cd iris && cargo test`). They are not in this loop because iris is not
|
# (`cd iris && cargo test`). They are not in this loop because iris is not
|
||||||
# about this product and its suite is the slower of the two.
|
# about this product and its suite is the slower of the two.
|
||||||
set -eu
|
set -eu
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")/.."
|
||||||
for workspace in event-model server app-rust; do
|
for workspace in event-model server app-rust; do
|
||||||
(cd "$workspace" && cargo test "$@")
|
(cd "$workspace" && cargo test "$@")
|
||||||
done
|
done
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
# (certs.rs covers every local address), so a client inside the tunnel
|
# (certs.rs covers every local address), so a client inside the tunnel
|
||||||
# completes the same pinned-TLS handshake a phone will.
|
# completes the same pinned-TLS handshake a phone will.
|
||||||
#
|
#
|
||||||
# ./test-wg-tunnel.sh up create the tunnel (needs sudo)
|
# scripts/test-wg-tunnel.sh up create the tunnel (needs sudo)
|
||||||
# ./test-wg-tunnel.sh test run the server on wg0 and reach it from "phone"
|
# scripts/test-wg-tunnel.sh test run the server on wg0 and reach it from "phone"
|
||||||
# ./test-wg-tunnel.sh down remove everything it created
|
# scripts/test-wg-tunnel.sh down remove everything it created
|
||||||
#
|
#
|
||||||
# Everything here is torn down by `down`: the netns (taking wg1 and the veth
|
# Everything here is torn down by `down`: the netns (taking wg1 and the veth
|
||||||
# peer with it), wg0, and the temporary key files.
|
# peer with it), wg0, and the temporary key files.
|
||||||
@@ -35,7 +35,8 @@ SERVER_UDP_IP=10.99.0.1
|
|||||||
CLIENT_UDP_IP=10.99.0.2
|
CLIENT_UDP_IP=10.99.0.2
|
||||||
LISTEN_PORT=51820
|
LISTEN_PORT=51820
|
||||||
KEYDIR=/run/ai-app-wg-test
|
KEYDIR=/run/ai-app-wg-test
|
||||||
REPO=$(cd "$(dirname "$0")" && pwd)
|
# This script lives in scripts/; everything it names is under the root.
|
||||||
|
REPO=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
|
||||||
up() {
|
up() {
|
||||||
echo "==> Generating ephemeral keypairs in $KEYDIR"
|
echo "==> Generating ephemeral keypairs in $KEYDIR"
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
# dev VM (the VM is behind qemu user-mode networking and has no inbound path;
|
# dev VM (the VM is behind qemu user-mode networking and has no inbound path;
|
||||||
# see AGENTS.md).
|
# see AGENTS.md).
|
||||||
#
|
#
|
||||||
# sudo WG_ENDPOINT=your-name.duckdns.org ./wg-setup-host.sh
|
# sudo WG_ENDPOINT=your-name.duckdns.org scripts/wg-setup-host.sh
|
||||||
#
|
#
|
||||||
# What it creates:
|
# What it creates:
|
||||||
# /etc/wireguard/wg0.conf the backend's tunnel: 10.66.0.1, port 51820
|
# /etc/wireguard/wg0.conf the backend's tunnel: 10.66.0.1, port 51820
|
||||||
@@ -75,7 +75,7 @@ fi
|
|||||||
|
|
||||||
echo "==> Writing $WG_DIR/wg0.conf"
|
echo "==> Writing $WG_DIR/wg0.conf"
|
||||||
cat > "$WG_DIR/wg0.conf" <<EOF
|
cat > "$WG_DIR/wg0.conf" <<EOF
|
||||||
# Generated by ai-app/wg-setup-host.sh. The backend binds this interface's
|
# Generated by ai-app/scripts/wg-setup-host.sh. The backend binds this interface's
|
||||||
# address and refuses to start without it (see server/src/main.rs).
|
# address and refuses to start without it (see server/src/main.rs).
|
||||||
[Interface]
|
[Interface]
|
||||||
Address = $SERVER_IP/24
|
Address = $SERVER_IP/24
|
||||||
@@ -150,5 +150,5 @@ echo " If it doesn't, your router can't hairpin -- turn the tunnel off at"
|
|||||||
echo " home, or use a split-DNS entry pointing $ENDPOINT at the LAN IP."
|
echo " home, or use a split-DNS entry pointing $ENDPOINT at the LAN IP."
|
||||||
echo " 4. Start the backend here (it binds $SERVER_IP only, and refuses to"
|
echo " 4. Start the backend here (it binds $SERVER_IP only, and refuses to"
|
||||||
echo " start if wg0 is down):"
|
echo " start if wg0 is down):"
|
||||||
echo " cd $(dirname "$(readlink -f "$0")") && ./server/target/release/ai-server"
|
echo " cd $(dirname "$(dirname "$(readlink -f "$0")")") && ./server/target/release/ai-server"
|
||||||
echo " Add --rotate-token once to print a fresh enrollment QR for the app."
|
echo " Add --rotate-token once to print a fresh enrollment QR for the app."
|
||||||
File renamed without changes.
File renamed without changes.
@@ -43,12 +43,18 @@ pub fn build(variant: Variant, abis: &[String]) -> Result<PathBuf, Fail> {
|
|||||||
sdk::require_ndk_installed(&sdk.root)?;
|
sdk::require_ndk_installed(&sdk.root)?;
|
||||||
require_cargo_ndk()?;
|
require_cargo_ndk()?;
|
||||||
|
|
||||||
let out_dir = repo_root.join("target").join("xtask").join("apk");
|
// xtask's own intermediate files, in its own crate's target/ rather
|
||||||
|
// than a `target/` at the repo root -- there is no workspace there and
|
||||||
|
// a build directory in the root is not something anybody was looking
|
||||||
|
// for (Iris, 2026-09-09).
|
||||||
|
let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("target")
|
||||||
|
.join("apk");
|
||||||
std::fs::create_dir_all(&out_dir).map_err(|e| {
|
std::fs::create_dir_all(&out_dir).map_err(|e| {
|
||||||
Fail::new(
|
Fail::new(
|
||||||
"could not create the xtask output directory",
|
"could not create the xtask output directory",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
"check permissions under target/",
|
"check permissions under scripts/xtask/target/",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -98,13 +104,18 @@ pub fn build(variant: Variant, abis: &[String]) -> Result<PathBuf, Fail> {
|
|||||||
// `APK_PATTERNS`, which has no per-component path override) finds it
|
// `APK_PATTERNS`, which has no per-component path override) finds it
|
||||||
// without needing a change on that side -- `.dev-updater.ron`'s
|
// without needing a change on that side -- `.dev-updater.ron`'s
|
||||||
// `shell` component points its `cwd` here. The working files above
|
// `shell` component points its `cwd` here. The working files above
|
||||||
// stay under `target/xtask/apk/`, an ordinary build-cache location.
|
// stay under `scripts/xtask/target/apk/`, an ordinary build-cache location.
|
||||||
let published_dir = repo_root.join("xtask/build/outputs/apk").join(variant_name);
|
// `scripts/build/...`, not `scripts/xtask/build/...`: Dev Updater
|
||||||
|
// discovers APKs with `*/build/outputs/apk/*/*.apk` from the checkout
|
||||||
|
// root, which is exactly one directory deep. See `.dev-updater.ron`.
|
||||||
|
let published_dir = repo_root
|
||||||
|
.join("scripts/build/outputs/apk")
|
||||||
|
.join(variant_name);
|
||||||
std::fs::create_dir_all(&published_dir).map_err(|e| {
|
std::fs::create_dir_all(&published_dir).map_err(|e| {
|
||||||
Fail::new(
|
Fail::new(
|
||||||
"could not create the published APK directory",
|
"could not create the published APK directory",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
"check permissions under xtask/build",
|
"check permissions under scripts/build",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let published_apk = published_dir.join(format!("ai-app-shell-{variant_name}.apk"));
|
let published_apk = published_dir.join(format!("ai-app-shell-{variant_name}.apk"));
|
||||||
@@ -112,7 +123,7 @@ pub fn build(variant: Variant, abis: &[String]) -> Result<PathBuf, Fail> {
|
|||||||
Fail::new(
|
Fail::new(
|
||||||
"could not publish the signed APK",
|
"could not publish the signed APK",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
"check permissions under xtask/build",
|
"check permissions under scripts/build",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -120,12 +131,17 @@ pub fn build(variant: Variant, abis: &[String]) -> Result<PathBuf, Fail> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn repo_root() -> Result<PathBuf, Fail> {
|
fn repo_root() -> Result<PathBuf, Fail> {
|
||||||
// xtask's own Cargo.toml is at <repo_root>/xtask/Cargo.toml.
|
// xtask's own Cargo.toml is at <repo_root>/scripts/xtask/Cargo.toml,
|
||||||
|
// so the root is two levels up (2026-09-09: it used to be one).
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
manifest_dir.parent().map(Path::to_path_buf).ok_or_else(|| {
|
manifest_dir
|
||||||
|
.parent()
|
||||||
|
.and_then(Path::parent)
|
||||||
|
.map(Path::to_path_buf)
|
||||||
|
.ok_or_else(|| {
|
||||||
Fail::new(
|
Fail::new(
|
||||||
"could not find the repo root",
|
"could not find the repo root",
|
||||||
"CARGO_MANIFEST_DIR has no parent",
|
"CARGO_MANIFEST_DIR has no grandparent",
|
||||||
"run through cargo, not by hand",
|
"run through cargo, not by hand",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -251,7 +267,7 @@ fn compile_java(
|
|||||||
Fail::new(
|
Fail::new(
|
||||||
"could not create the generated-sources directory",
|
"could not create the generated-sources directory",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
"check permissions under target/",
|
"check permissions under scripts/xtask/target/",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
// Same shape as shellApp's Gradle `generatePinnedCa` task: the text
|
// Same shape as shellApp's Gradle `generatePinnedCa` task: the text
|
||||||
@@ -266,7 +282,7 @@ fn compile_java(
|
|||||||
Fail::new(
|
Fail::new(
|
||||||
"could not write PinnedCa.java",
|
"could not write PinnedCa.java",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
"check permissions under target/",
|
"check permissions under scripts/xtask/target/",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -275,7 +291,7 @@ fn compile_java(
|
|||||||
Fail::new(
|
Fail::new(
|
||||||
"could not create the classes directory",
|
"could not create the classes directory",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
"check permissions under target/",
|
"check permissions under scripts/xtask/target/",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -313,7 +329,7 @@ fn dex(
|
|||||||
Fail::new(
|
Fail::new(
|
||||||
"could not create the dex output directory",
|
"could not create the dex output directory",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
"check permissions under target/",
|
"check permissions under scripts/xtask/target/",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let mut cmd = Command::new(sdk.tool("d8"));
|
let mut cmd = Command::new(sdk.tool("d8"));
|
||||||
@@ -343,7 +359,7 @@ fn aapt2_link(sdk: &Sdk, shell_app_dir: &Path, base_apk: &Path) -> Result<(), Fa
|
|||||||
return Err(Fail::new(
|
return Err(Fail::new(
|
||||||
"app/shellApp's manifest already has a package attribute",
|
"app/shellApp's manifest already has a package attribute",
|
||||||
"aapt2_link() assumes it doesn't and injects one",
|
"aapt2_link() assumes it doesn't and injects one",
|
||||||
"update aapt2_link() in xtask/src/apk.rs to stop injecting a second one",
|
"update aapt2_link() in scripts/xtask/src/apk.rs to stop injecting a second one",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let merged_manifest = manifest_text.replacen(
|
let merged_manifest = manifest_text.replacen(
|
||||||
@@ -356,7 +372,7 @@ fn aapt2_link(sdk: &Sdk, shell_app_dir: &Path, base_apk: &Path) -> Result<(), Fa
|
|||||||
Fail::new(
|
Fail::new(
|
||||||
"could not write the merged manifest",
|
"could not write the merged manifest",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
"check permissions under target/",
|
"check permissions under scripts/xtask/target/",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -381,7 +397,7 @@ fn merge(
|
|||||||
Fail::new(
|
Fail::new(
|
||||||
"could not copy the base APK",
|
"could not copy the base APK",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
"check permissions under target/",
|
"check permissions under scripts/xtask/target/",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -422,14 +438,14 @@ fn merge(
|
|||||||
Fail::new(
|
Fail::new(
|
||||||
"could not stage the native library",
|
"could not stage the native library",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
"check permissions under target/",
|
"check permissions under scripts/xtask/target/",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
std::fs::copy(&src, dest_dir.join(so_name)).map_err(|e| {
|
std::fs::copy(&src, dest_dir.join(so_name)).map_err(|e| {
|
||||||
Fail::new(
|
Fail::new(
|
||||||
"could not stage the native library",
|
"could not stage the native library",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
"check permissions under target/",
|
"check permissions under scripts/xtask/target/",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
Reference in new issue
Block a user