iris is the framework alone; the app is one crate in app-rust/

Iris: "the organization of the rust rewrite is a mess right now... there
shouldn't be anything related to the app inside of iris. Iris is supposed
to be the UI framework alone." And, on the crate count: "I'm confused why
the app only code needs more than one crate though."

Nine cargo workspaces become three, and the port's project code -- which
sat in five places, four of them inside the framework -- becomes one crate,
`ai-app`, in `app-rust/`:

  client-core                -> app-rust/src/client
  iris/transcript-ui         -> app-rust/src/ui
  iris/transcript-fixture    -> app-rust/src/ui/fixture.rs + tests/ + touch/
  iris/desktop-app           -> app-rust/src/desktop + src/bin_desktop.rs
  iris/android-app           -> app-rust/src/android + android-project/
  android-shell              -> app-rust/src/shell

iris/ keeps core, macro, the iris crate, tabs-ui and rig-input, and now
mentions no session, transcript, setup or server anywhere.

Only two of the old splits had a reason that survived reading. event-model
stays a crate at the repo root because server/ depends on it too, so a
crate is what makes the backend and the app agree by construction. The two
Android .so names looked like a hard constraint -- a package produces one
library artifact -- until P2 turned out to already plan merging those two
Android apps into one; both faces now come out of libai_app.so, picked
apart by features so `--no-default-features --features shell` keeps wgpu,
parley and iris out of the Compose app's APK. docs/RUST.md's "One app
crate" has the rest, including what each remaining feature is for.

DECISIONS.md and SUBAGENTS.md move into docs/ with everything else.

Verified: ./run-tests.sh and `cd iris && cargo test` green, clippy and fmt
clean in all five workspaces, `cargo ndk -t x86_64` links libai_app.so,
build-apk.sh produces an APK that installs and launches on this checkout's
emulator (Gl ... virgl, as expected), and the phone-sized headless
screenshot renders the transcript unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-08 23:36:38 -04:00
1 parent e9a6562dc6
commit 6d5a231f5c
100 files changed
+923 -3294

No files matched your search

+64 -28
View File
@@ -20,19 +20,26 @@ is a new driver — never a session-type branch in shared code (routes,
transcript, app screens).
The second one, for the Rust port on the `rustify` branch: **the phone app
and a planned desktop app share almost all of their code.** Screens, widgets,
folding, paging, config and the network client live in the shared crates
(`iris`, `client-core`, `transcript-ui`, `tabs-ui`); `android-app` and
`desktop-app` are thin entry points that own only what the platform forces
(JNI and the IME on one side, winit and argv on the other). The two
and a planned desktop app share almost all of their code.** Screens,
widgets, folding, paging, config and the network client live in
`app-rust/`'s `client` and `ui` modules, drawn with `iris`; `src/android`
and `src/desktop` are thin entry points that own only what the platform
forces (JNI and the IME on one side, winit and argv on the other). The two
*layouts* will differ, to suit a phone's screen and a finger against a
desktop's screen and a mouse -- but the widgets a layout is made of (a
button, a text field, a list, a card) and the styling (colours, spacing,
type) are one implementation with no per-platform copy. Anything that could
work on both goes in a shared crate the first time it is written, and a
platform crate growing a widget or a colour is a defect to move, not a
convenience to keep. Iris said this on 2026-09-07; docs/RUST.md carries the
details.
work on both goes in `ui` the first time it is written, and a platform
module growing a widget or a colour is a defect to move, not a convenience
to keep. Iris said this on 2026-09-07; docs/RUST.md carries the details.
The third, from Iris on 2026-09-08: **`iris/` is the UI framework and
nothing else.** Nothing in it may know about a session, a transcript, a
setup or a server; anything that does belongs in `app-rust/`, and the
dependency runs one way only. The port's project code is **one crate**
(`ai-app`) rather than the six it was scattered across — see docs/RUST.md's
"One app crate" for what forced each of the splits that were removed and
the two that remain.
## Layout
@@ -44,6 +51,22 @@ Module-by-module intent is in `docs/PLAN.md`'s "Backend layout".
- `server/` — the Rust backend (`ai-server`). `routes.rs`'s module doc
comment is the HTTP table and the surface's source of truth.
- `event-model/` — the wire shape `server/` and `app-rust/` both depend on,
which is the whole reason it is a crate of its own rather than part of
either: it is the contract between them, so the two agree by construction.
- `app-rust/` — the Rust app, one crate (`ai-app`) with three faces. `src/
client` is everything with no UI in it (the REST and SSE clients, the
transcript cache and fold, the highlighter, the ANSI parser, config and
the enrolment link); `src/ui` is the screens as iris widget trees;
`src/desktop` + `src/bin_desktop.rs` is the winit binary; `src/android`
is the `android-view` entry point and `android-project/` its Gradle app;
`src/shell` is the separate JNI bridge the Kotlin `app/shellApp` calls.
Features pick which face a build is: `screens` (default) for anything
that draws, `shell` for the Compose app's bridge, `bench` for P0's
fixture build. See its `Cargo.toml` header.
- `iris/` — the UI framework, and **only** the UI framework: `core`,
`macro`, the `iris` crate itself, `tabs-ui` (its own demo widget tree)
and `rig-input`. It must not mention anything this product is about.
- `app/` — the Compose app, package `com.example.aiapp`, label "AI Sessions".
`AppRoot.kt` is the navigation `when`; `MainScreen.kt` the root's four tabs
(sessions, import, models, setups); `Api.kt`/`EventStream.kt` the REST + SSE
@@ -66,6 +89,9 @@ Module-by-module intent is in `docs/PLAN.md`'s "Backend layout".
Read it before touching `TranscriptCache.kt`, `TranscriptSource.kt`, or
the opening and stream effects in `SessionScreen.kt`.
- `docs/TODO.md` — the working list.
- `docs/SUBAGENTS.md` and `docs/SUBAGENTS_DECISIONS.md` — a session's
subagents (the wire shape, the phone's view) and the choices behind
them still awaiting review.
- `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
framework decision, and the ordered experiments with their pass
@@ -74,8 +100,8 @@ Module-by-module intent is in `docs/PLAN.md`'s "Backend layout".
`docs/LAYOUT.md`, `docs/TEXTURES.md`, `docs/CLIENT_CORE.md` — iris's
own build log (**any major addition or design decision, not only
public API** -- Iris, 2026-09-08), working list, decisions log,
layout/render design, and texture-atlas design, and the client-core
crate's design, respectively.
layout/render design, texture-atlas design, and the design of
`app-rust`'s `client` module, respectively.
- `docs/SCROLL.md` — how anything in iris scrolls: one
`ScrollController` holds the position, the gesture, the fling and the
pin, and the two widgets that scroll (`ScrollArea`, `LazySpan`) own
@@ -121,10 +147,15 @@ guaranteed to have.
## Checking your work
- **Server**: `./run-tests.sh` from the repo root (or `cargo test` from
`server/`), plus `cargo clippy --all-targets` and `cargo fmt`. The build
stays warning-clean and rustfmt-clean at the defaults — there is no
`rustfmt.toml` and there should not be one.
- **Rust**: `./run-tests.sh` from the repo root runs `event-model`,
`server/` and `app-rust/`; `cd iris && cargo test` runs the framework's
own suite, which is slower and not about this product. Each workspace
also gets `cargo clippy --all-targets` and `cargo fmt`. The build stays
warning-clean and rustfmt-clean at the defaults — there is no
`rustfmt.toml` and there should not be one. `app-rust/` and `iris/` are
pinned to the same dated nightly (`rust-toolchain.toml`, one copy each,
because a pin applies per directory); `server/` and `event-model/` are
stable.
- **App**: from `app/`,
`. ./android-env.sh && ./gradlew :androidApp:ktfmtFormat
:androidApp:compileDebugKotlin :androidApp:lintDebug
@@ -295,25 +326,30 @@ Each exists because something was invisible without it.
framework, from `atrace` text output with no trace processor needed. It is
how the cost of a layout node per link was attributed to the framework
rather than guessed at.
- **`iris/android-app/build-apk.sh [debug|release] [--abi ...] [--features
...]`** builds iris-android-app's cdylib (`cargo ndk`) and its APK
(Gradle) in one step and verifies the result (`aapt2`/`apksigner`), and
**`iris/android-app/run-bench.sh [--apk PATH]`** installs it on this
checkout's own emulator, taps "Run benchmark" by label, and prints the
report -- written so the P0 build/install/tap/read-report cycle stops
being retyped by hand each time (docs/RUST.md's P0 box).
- **`app-rust/build-apk.sh [debug|release] [--abi ...] [--features
...]`** builds the Rust app's cdylib (`cargo ndk` from `app-rust/`,
straight into `android-project/app/src/main/jniLibs/`) and its APK
(Gradle, from `android-project/`) in one step and verifies the result
(`aapt2`/`apksigner`), and **`app-rust/run-bench.sh [--apk PATH]`**
installs it on this checkout's own emulator, taps "Run benchmark" by
label, and prints the report -- written so the P0
build/install/tap/read-report cycle stops being retyped by hand each
time (docs/RUST.md's P0 box). It passes `--no-default-features`, so
`--features` alone decides what is in the `.so`; that is what keeps the
1.9 MB bench fixture out of a build that did not ask for `bench`.
- **iris's three test layers** (docs/RUST.md's "Three test layers" has
the commands and what each cannot answer): test at the cheapest one
that can answer the question. `cargo test -p transcript-fixture` runs
the real transcript screen over the bench fixture with **no window, no
that can answer the question. `cd app-rust && cargo test` runs the real
transcript screen over the bench fixture with **no window, no
compositor and no GPU** (`iris::harness`), on a clock the test owns and
a gesture replayed from a `t_ms action x y` file under
`iris/transcript-fixture/touch/` -- which is how the batched 120Hz
`app-rust/touch/` -- which is how the batched 120Hz
flick a finger actually makes is testable at all, since a `ui-trace`
swipe is many evenly-spaced events. `iris/run-headless.sh phone --phone
--shot …` opens the same screen in a window at the phone's own size and
density for looking at, and `--replay FILE` drives the same recording
into it. The emulator is for JNI, the IME, insets, the surface
--dir ../app-rust --shot …` opens the same screen in a window at the
phone's own size and density for looking at, and `--replay FILE` drives
the same recording into it (`--dir` names the workspace to build in,
since the rig lives in iris and the app's examples do not). The emulator is for JNI, the IME, insets, the surface
lifecycle and one verification run before a build goes to the phone --
not for iterating on layout.
- **The emulator is a GLES rig, deliberately** (Iris, 2026-09-08;
-1130
View File
File diff suppressed because it is too large. Load diff
-36
View File
@@ -1,36 +0,0 @@
[package]
name = "android-shell"
version = "0.1.0"
edition = "2024"
# The JNI bridge behind E3's two Java stub classes (`MainActivity`,
# `NotificationService` -- see RUST.md's "How much Java is unavoidable" for
# why those two classes cannot be anything but Java/Kotlin, registered from
# the manifest by name). Everything they would otherwise have done in
# Kotlin -- the SSE follow loop, deciding where a notification is shown,
# picking a session for a share -- is here instead, built on `client-core`
# so the networking and parsing are not duplicated a third time next to the
# server and the Kotlin app.
#
# `cdylib` for `System.loadLibrary`; `lib` too so `cargo test`/`clippy` run
# on a normal host target without an Android NDK toolchain, the same
# posture `client-core` and `server` already have.
[lib]
name = "android_shell"
crate-type = ["cdylib", "lib"]
[dependencies]
client-core = { path = "../client-core" }
jni = "0.22"
log = "0.4"
# `LogErrorAndDefault` (the `native_method!` error policy this crate uses
# throughout, see lib.rs) logs through the `log` facade, which is a no-op
# without a backend installed -- so without this, every recoverable error
# at a native entry point would be silently dropped rather than reaching
# logcat. Android-only: nothing else here needs it, and it does not build
# off-device (see `notify::ensure_logger`'s call site, the only place this
# is used).
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.15"
+10
View File
@@ -0,0 +1,10 @@
android-project/.gradle/
android-project/build/
android-project/app/build/
# Rebuilt by `cargo ndk -o app/src/main/jniLibs/ build` before every
# Gradle build -- see RUST.md's I2 for the exact command.
android-project/app/src/main/jniLibs/
target/
Cargo.lock.orig
+49 -80
View File
@@ -166,6 +166,28 @@ dependencies = [
"memchr",
]
[[package]]
name = "ai-app"
version = "0.1.0"
dependencies = [
"android-view",
"android_logger",
"base64",
"event-model",
"iris",
"jni 0.22.4",
"libc",
"log",
"pulldown-cmark",
"serde",
"serde_json",
"tabs-ui",
"tempfile",
"tokio",
"ureq",
"winit",
]
[[package]]
name = "aligned"
version = "0.4.3"
@@ -743,19 +765,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "client-core"
version = "0.1.0"
dependencies = [
"base64",
"event-model",
"log",
"pulldown-cmark",
"serde",
"serde_json",
"ureq",
]
[[package]]
name = "clipboard-win"
version = "5.4.1"
@@ -881,9 +890,9 @@ dependencies = [
[[package]]
name = "crossbeam-deque"
version = "0.8.7"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
@@ -891,18 +900,18 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.20"
version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.22"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]]
name = "crunchy"
@@ -1055,7 +1064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1167,9 +1176,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "font-types"
version = "0.12.4"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64eb721ca85a34323425f4041adc5d82704d3782d5f8f03793bc012419dce23"
checksum = "b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6"
dependencies = [
"bytemuck",
]
@@ -1718,24 +1727,6 @@ dependencies = [
"winit",
]
[[package]]
name = "iris-android-app"
version = "0.1.0"
dependencies = [
"android-view",
"android_logger",
"client-core",
"event-model",
"iris",
"libc",
"log",
"serde_json",
"tabs-ui",
"tokio",
"transcript-fixture",
"transcript-ui",
]
[[package]]
name = "iris-core"
version = "0.1.0"
@@ -1943,7 +1934,7 @@ dependencies = [
"bitflags 2.13.1",
"libc",
"plain",
"redox_syscall 0.9.3",
"redox_syscall 0.9.4",
]
[[package]]
@@ -2599,7 +2590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3173,9 +3164,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5"
checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e"
dependencies = [
"bitflags 2.13.1",
]
@@ -3282,14 +3273,14 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.43"
version = "0.23.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba"
dependencies = [
"log",
"once_cell",
@@ -3638,7 +3629,7 @@ dependencies = [
"getrandom 0.4.3",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3840,28 +3831,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "transcript-fixture"
version = "0.1.0"
dependencies = [
"client-core",
"event-model",
"iris",
"serde_json",
"transcript-ui",
]
[[package]]
name = "transcript-ui"
version = "0.1.0"
dependencies = [
"client-core",
"event-model",
"iris",
"log",
"pulldown-cmark",
]
[[package]]
name = "tree_magic_mini"
version = "3.2.2"
@@ -3887,7 +3856,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -3932,9 +3901,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "3.4.0"
version = "3.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
checksum = "af5546be8f5378d5414f83733f5c9a2526f4645829edbc1c41790aeef1b38e8b"
dependencies = [
"base64",
"cookie_store",
@@ -3952,9 +3921,9 @@ dependencies = [
[[package]]
name = "ureq-proto"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
checksum = "fabc3e92916c89c95b20eef7b06b00b066bc217ef9ea3a4ac9bf1a7e35261e10"
dependencies = [
"base64",
"http",
@@ -4417,7 +4386,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5075,18 +5044,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
[[package]]
name = "zerocopy"
version = "0.8.56"
version = "0.8.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.56"
version = "0.8.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
dependencies = [
"proc-macro2",
"quote",
+154
View File
@@ -0,0 +1,154 @@
# The app: everything that is about *this product* rather than about the UI
# framework it draws with. One package, because splitting it was buying
# nothing -- see `docs/RUST.md`'s "One app crate" for the account. In short:
# `client` (the REST/SSE clients, the transcript cache and fold, the
# highlighter) and `ui` (the screens, in iris widgets) only ever ship
# together, and the three entry points below are three faces of one binary
# rather than three programs.
#
# `iris/` is the framework and knows nothing about any of this; the
# dependency runs one way, and a widget or a colour appearing here that is
# not about a session, a transcript or a setup belongs there instead
# (AGENTS.md).
[package]
name = "ai-app"
version = "0.1.0"
edition = "2024"
# `cdylib` is the Android face -- both the iris app (`android-project/`,
# `System.loadLibrary("ai_app")`) and the JNI bridge the Kotlin shell in
# `app/shellApp` calls (the `shell` feature). `rlib` is what the desktop
# binary, the examples and `tests/` link against. One package produces one
# library artifact, so the two Android apps share a `.so` name and pick
# what goes in it with features rather than with a second crate.
[lib]
name = "ai_app"
crate-type = ["cdylib", "rlib"]
[[bin]]
name = "ai-app-desktop"
path = "src/bin_desktop.rs"
required-features = ["screens"]
[[example]]
name = "transcript"
required-features = ["screens"]
[[example]]
name = "phone"
required-features = ["fixture"]
[dependencies]
# The event model, shared with `server/` so the two agree by construction.
# It stays a crate of its own at the repo root for exactly that reason:
# it is the contract between this app and the backend, not app code.
event-model = { path = "../event-model" }
serde = { version = "1", features = ["derive"] }
# `float_roundtrip` for the same reason `server/` sets it -- AGENTS.md's
# "Things that have bitten". `raw_value` for the transcript cache.
serde_json = { version = "1", features = ["float_roundtrip", "raw_value"] }
ureq = { version = "3", features = ["json"] }
pulldown-cmark = "0.13.4"
base64 = "0.23"
log = { version = "0.4.34", features = ["std"] }
# The UI framework. Optional so `--no-default-features --features shell`
# builds the Kotlin shell's JNI bridge without linking wgpu, parley and
# the rest of a renderer into an APK that draws with Compose.
iris = { path = "../iris", optional = true }
# iris's own tabs demo, kept runnable on Android through this project's
# Gradle app (the `tabs-screen` feature). The dependency direction is the
# right way round: the app may reach into the framework's example widget
# tree, never the reverse.
tabs-ui = { path = "../iris/tabs-ui", optional = true }
jni = { version = "0.22", optional = true }
# `bench` only: `libc` for the process CPU-time and RSS samples, `tokio`
# for the run's own timer.
libc = { version = "0.2.189", optional = true }
tokio = { version = "1.53.1", features = ["rt", "time"], optional = true }
[target.'cfg(not(target_os = "android"))'.dependencies]
winit = "0.30.13"
# Pinned to the exact commit RUST.md's E1 measured on this emulator; see
# `iris/Cargo.toml`'s copy of this pin for what advancing it costs.
[target.'cfg(target_os = "android")'.dependencies]
android-view = { git = "https://github.com/rust-mobile/android-view.git", rev = "bec6c62a96cef8239b0fd7fedeef9b184d02e3a1" }
android_logger = "0.15.1"
[features]
default = ["screens", "fixture"]
# The iris half: `src/ui` and everything that draws. Off for the Kotlin
# shell's bridge, which is JNI and `src/client` only.
screens = ["dep:iris"]
# `src/ui/fixture.rs` and the harness tests that drive it. Default-on so
# `cargo test` covers them; `build-apk.sh` passes `--no-default-features`
# so an APK carries the 1.9 MB fixture only when it asked for `bench`.
fixture = ["screens"]
# The two Android widget trees, on the same axis: a build picks one.
transcript-screen = ["screens"]
tabs-screen = ["screens", "dep:tabs-ui"]
# P0's iris half (docs/RUST.md): the fixture screen with a "Run benchmark"
# control, driving the same scroll loop and streaming phase the Compose
# bench build type does.
bench = ["transcript-screen", "fixture", "dep:libc", "dep:tokio"]
# The JNI bridge `app/shellApp` calls -- notifications, the share target
# and the Keystore-sealed settings.
shell = ["dep:jni"]
# See `iris/Cargo.toml`'s feature of the same name. Never for a phone.
force-gles = ["screens", "iris/force-gles"]
[dev-dependencies]
tempfile = "3"
tokio = { version = "1.53.1", features = ["rt", "time"] }
# The Android builds, kept off `release`/`dev` so a desktop build is not
# also optimised for size and unwinding is not also turned off for the
# tests. `build-apk.sh` passes `--profile android-release`.
[profile.android-release]
inherits = "release"
panic = "abort"
strip = true
lto = "fat"
codegen-units = 1
opt-level = "s"
[profile.android-dev]
inherits = "dev"
panic = "abort"
# Same reasoning as `iris/Cargo.toml`'s copy: full DWARF in every test
# binary is what made `cargo test` here write tens of gigabytes.
[profile.dev]
debug = "line-tables-only"
[profile.test]
debug = "line-tables-only"
# The headless harness tests (`iris::harness`, no window and no GPU) all
# open the bench fixture, so they say so rather than failing to compile
# when it is off.
[[test]]
name = "catch_a_fling"
required-features = ["fixture"]
[[test]]
name = "fence_fling"
required-features = ["fixture"]
[[test]]
name = "gesture_cancel"
required-features = ["fixture"]
[[test]]
name = "input_log_roundtrip"
required-features = ["fixture"]
[[test]]
name = "phone_screen"
required-features = ["fixture"]
[[test]]
name = "top_edge"
required-features = ["fixture"]
File renamed without changes.
@@ -39,7 +39,7 @@
<data android:scheme="aiapp" android:host="enroll" />
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="main" />
<meta-data android:name="android.app.lib_name" android:value="ai_app" />
</activity>
<!-- This app's own recent log, for Dev Updater to read on the
@@ -49,7 +49,7 @@ public final class DevLogProvider extends ContentProvider {
static {
// The provider is created before any activity, so it cannot rely
// on MainActivity's own load. Loading twice is a no-op.
System.loadLibrary("main");
System.loadLibrary("ai_app");
}
/** Matches {@link #nativeLinesSince}'s flat answer. Both sides say it once. */
@@ -19,7 +19,7 @@ import java.util.List;
*/
public final class MainActivity extends Activity {
static {
System.loadLibrary("main");
System.loadLibrary("ai_app");
}
/**
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,7 @@
#!/bin/sh
# Builds iris-android-app end to end: the cdylib (cargo ndk, straight into
# app/src/main/jniLibs/) then the APK (Gradle). Written to stop re-typing
# Builds the Android app end to end: the cdylib (cargo ndk from this
# directory, straight into android-project/app/src/main/jniLibs/) then the
# APK (Gradle, from android-project/). Written to stop re-typing
# the same incantation by hand every time (ANDROID_HOME/NDK exports, the
# cargo ndk invocation, the keystore env for a release build, apksigner/
# aapt2 verification) -- see docs/RUST.md's P0 box. Same shape as `app/
@@ -66,7 +67,7 @@ export ANDROID_NDK_HOME="$NDK_DIR"
# whatever earlier builds left here, and Gradle packages every directory it
# finds -- a debug x86_64 emulator build left behind made an arm64 "release"
# 339 MB on 2026-09-06.
rm -rf app/src/main/jniLibs
rm -rf android-project/app/src/main/jniLibs
# ...and Gradle's own copy of them, which `rm -rf jniLibs` does not reach.
# `mergeReleaseNativeLibs` is *up to date* against its cached inputs, so a
# build that switches ABI packages the previous ABI: an `--abi x86_64`
@@ -76,22 +77,24 @@ rm -rf app/src/main/jniLibs
# exactly like the phone's own Vulkan problem and is nothing of the kind.
# Scoped to the merge task's directory rather than all of `app/build`, so
# an ABI change costs the native merge and not the whole Gradle build.
rm -rf app/build/intermediates/merged_native_libs \
app/build/intermediates/stripped_native_libs \
app/build/intermediates/merged_jni_libs
rm -rf android-project/app/build/intermediates/merged_native_libs \
android-project/app/build/intermediates/stripped_native_libs \
android-project/app/build/intermediates/merged_jni_libs
echo "build-apk.sh: cargo ndk -t $ABI build ${BUILD_TYPE:+(${BUILD_TYPE})} --features \"$FEATURES\""
if [ "$BUILD_TYPE" = "release" ]; then
cargo ndk -t "$ABI" -P 29 -o app/src/main/jniLibs/ build --release --features "$FEATURES"
cargo ndk -t "$ABI" -P 29 -o android-project/app/src/main/jniLibs/ build --lib \
--profile android-release --no-default-features --features "$FEATURES"
else
cargo ndk -t "$ABI" -P 29 -o app/src/main/jniLibs/ build --features "$FEATURES"
cargo ndk -t "$ABI" -P 29 -o android-project/app/src/main/jniLibs/ build --lib \
--profile android-dev --no-default-features --features "$FEATURES"
fi
GRADLE_TASK="assembleDebug"
APK_DIR="app/build/outputs/apk/debug"
APK_DIR="android-project/app/build/outputs/apk/debug"
APK_NAME="app-debug.apk"
if [ "$BUILD_TYPE" = "release" ]; then
GRADLE_TASK="assembleRelease"
APK_DIR="app/build/outputs/apk/release"
APK_DIR="android-project/app/build/outputs/apk/release"
APK_NAME="app-release.apk"
# Same key `app/build-apk.sh` (the Compose app) generates once under
# ~/.config/ai-app/release.jks -- see AGENTS.md's "Checking your work".
@@ -104,7 +107,7 @@ if [ "$BUILD_TYPE" = "release" ]; then
AI_APP_KEYSTORE_PASSWORD=$(cat "$AI_APP_KEYSTORE.password")
fi
gradle ":app:$GRADLE_TASK" --console=plain
(cd android-project && gradle ":app:$GRADLE_TASK" --console=plain)
APK_PATH="$(pwd)/$APK_DIR/$APK_NAME"
BUILD_TOOLS=$(ls -d "$SDK_ROOT"/build-tools/*/ | sort -V | tail -1)
@@ -5,7 +5,7 @@
//!
//! `--phone` sets the headless sway output to the phone's own 1080x2424
//! and exports `IRIS_SCALE=2.55`, so this draws at the density Iris's
//! phone reports (`transcript_fixture::PHONE_SCALE`) rather than the
//! phone reports (`ai_app::ui::fixture::PHONE_SCALE`) rather than the
//! desktop's 1.0 -- same screen, same fixture and the same folding as
//! the Android bench and the headless tests, so what differs between a
//! screenshot here and one from the phone is the renderer, never the
@@ -80,7 +80,7 @@ fn main() {
pub struct Client {
ui_state: DefaultUiState,
#[allow(dead_code)]
screen: Option<transcript_ui::TranscriptScreen>,
screen: Option<ai_app::ui::TranscriptScreen>,
}
impl DefaultAppState for Client {
@@ -88,8 +88,8 @@ impl DefaultAppState for Client {
WindowAttributes::default()
.with_title("iris transcript (bench fixture)")
.with_inner_size(PhysicalSize::new(
transcript_fixture::PHONE_WIDTH,
transcript_fixture::PHONE_HEIGHT,
ai_app::ui::fixture::PHONE_WIDTH,
ai_app::ui::fixture::PHONE_HEIGHT,
))
}
@@ -98,7 +98,7 @@ impl DefaultAppState for Client {
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let screen = match transcript_fixture::open(rsc, &mut ui_state) {
let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) {
Ok(opened) => {
if let Some(message) = message_argv() {
opened.screen.composer.field.edit(rsc).set(&message);
@@ -1,5 +1,5 @@
//! I5's desktop proof: the transcript screen built from synthetic
//! `client_core::transcript_fold` rows (no network, no server -- see
//! `ai_app::client::transcript_fold` rows (no network, no server -- see
//! `lib.rs`'s doc for why `transcript-ui` itself never fetches anything),
//! run via `iris/run-headless.sh transcript -- -p transcript-ui` for a
//! screenshot on the winit backend, or `cargo run --example transcript -p
@@ -13,8 +13,8 @@
//! with `ui-trace record --do "tap 'Tools'"` on Android, to prove
//! hold-the-edge expand).
use client_core::QuestionOption;
use client_core::transcript_fold::{QuestionCard, TranscriptItem, TranscriptRow as FoldedRow};
use ai_app::client::QuestionOption;
use ai_app::client::transcript_fold::{QuestionCard, TranscriptItem, TranscriptRow as FoldedRow};
use iris::prelude::*;
fn main() {
@@ -25,7 +25,7 @@ fn main() {
pub struct Client {
ui_state: DefaultUiState,
#[allow(dead_code)]
screen: transcript_ui::TranscriptScreen,
screen: ai_app::ui::TranscriptScreen,
}
fn msg(seq: u64, from_user: bool, text: &str) -> FoldedRow {
@@ -108,7 +108,7 @@ fn asking(id: &str, tool: &str, input: &str) -> TranscriptItem {
/// screen in the expanded shot.
fn long_output() -> String {
(0..200)
.map(|i| format!("test transcript_ui::case_{i} ... ok"))
.map(|i| format!("test ai_app::ui::case_{i} ... ok"))
.collect::<Vec<_>>()
.join("\n")
}
@@ -159,7 +159,7 @@ fn synthetic_rows() -> Vec<FoldedRow> {
Some((&long_output(), false)),
)),
msg(6, true, "Looks good, thanks!"),
// Every block kind `client_core::markdown_blocks` names, in one
// Every block kind `ai_app::client::markdown_blocks` names, in one
// row, so P1a's appearance can be looked at against the Compose
// app's without a server (docs/RUST.md's P1a box). The heading,
// paragraph, fence and table are the *same source* the bench
@@ -207,7 +207,7 @@ impl DefaultAppState for Client {
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let screen = transcript_ui::build(rsc, &mut ui_state, synthetic_rows());
let screen = ai_app::ui::build(rsc, &mut ui_state, synthetic_rows());
// Exercises `push_row`/`ItemKey` beyond construction time, matching
// how a live SSE loop appends -- a row arriving after the screen
// already exists must land at the bottom without disturbing what's
@@ -7,7 +7,8 @@
#
# Usage: ./run-bench.sh [--apk PATH]
# Defaults to this checkout's own release APK
# (app/build/outputs/apk/release/app-release.apk) if it exists, else the
# (android-project/app/build/outputs/apk/release/app-release.apk) if it
# exists, else the
# debug one -- build one first with ./build-apk.sh.
set -eu
cd "$(dirname "$0")"
@@ -20,10 +21,10 @@ while [ $# -gt 0 ]; do
esac
done
if [ -z "$APK" ]; then
if [ -f app/build/outputs/apk/release/app-release.apk ]; then
APK=app/build/outputs/apk/release/app-release.apk
if [ -f android-project/app/build/outputs/apk/release/app-release.apk ]; then
APK=android-project/app/build/outputs/apk/release/app-release.apk
else
APK=app/build/outputs/apk/debug/app-debug.apk
APK=android-project/app/build/outputs/apk/debug/app-debug.apk
fi
fi
if [ ! -f "$APK" ]; then
+11
View File
@@ -0,0 +1,11 @@
# iris needs nightly (see the #![feature] list in core/src/lib.rs and src/lib.rs).
# The pin is dated rather than "nightly" because the const-traits feature set
# changes shape between nightlies: on 2026-09-04 the vendored January tree would
# not parse at all, because `impl const Trait for T` had become
# `const impl Trait for T`. A rolling channel turns that into a build that
# breaks unattended on whatever machine Dev Updater happens to build on.
# Advance this deliberately, with the feature list in RUST.md's I0b.
[toolchain]
channel = "nightly-2026-09-03"
components = ["clippy", "rustfmt"]
targets = ["aarch64-linux-android", "x86_64-linux-android"]
@@ -1,5 +1,5 @@
//! The platform half of this app's logging: what
//! `client_core::log_ring` needs that only Android can supply, which is
//! `crate::client::log_ring` needs that only Android can supply, which is
//! `android_logger` as the logger to forward to and nothing else.
//!
//! Everything general -- the ring, its bounds, the `log::Log` backend --
@@ -13,7 +13,7 @@
//! the same phone through `devlog`'s `ContentProvider`. See
//! `docs/DECISIONS.md`, 2026-09-07.
use client_core::log_ring::{self, LogRing};
use crate::client::log_ring::{self, LogRing};
/// Installs the ring in front of `android_logger`, so `logcat` still sees
/// exactly what it saw before and the ring sees it too.
@@ -61,7 +61,7 @@ pub fn ring() -> &'static LogRing {
/// contract is live and which package's log it is -- the bench build and
/// the ordinary one have different ones.
pub fn diagnostics_line() -> String {
let where_to_read = match crate::devlog::authority() {
let where_to_read = match crate::android::devlog::authority() {
Some(authority) => format!("devlog provider: content://{authority}"),
// Not "off": Android creates a provider lazily, so this is what
// "nobody has asked for it yet" looks like, and it is a different
@@ -17,9 +17,9 @@
//! this file exists to measure -- see docs/RUST.md's P0 box for the
//! before/after report.
use crate::bench_jni::PlatformHandle;
use crate::android::bench_jni::PlatformHandle;
use crate::client::transcript_fold::{TranscriptItem, fold_event};
use android_view::jni::{JavaVM, objects::GlobalRef};
use client_core::transcript_fold::{TranscriptItem, fold_event};
use event_model::SeqEvent;
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
use iris::prelude::*;
@@ -98,7 +98,7 @@ pub struct BenchClient {
/// P0 box, "the status-bar inset is not applied," found the row sitting
/// directly under it because nothing here read `insets().top` at all.
top_bar: WeakWidget<WidgetPtr>,
screen: Option<transcript_ui::TranscriptScreen>,
screen: Option<crate::ui::TranscriptScreen>,
items: Vec<TranscriptItem>,
/// The events not yet streamed -- consumed by `start_benchmark`'s own
/// clone, kept here only as the source a second run would need (the
@@ -288,7 +288,7 @@ impl AndroidAppState for BenchClient {
last_top_pad: 0.0,
};
match transcript_fixture::build_screen(rsc) {
match crate::ui::fixture::build_screen(rsc) {
Ok((opened, tree)) => {
client.items = opened.items;
client.stream_tail = opened.stream_tail;
@@ -364,7 +364,7 @@ impl AndroidAppState for BenchClient {
// The composer bar sits directly on whichever of the IME or the
// navigation bar is currently the bottom of usable space -- see
// `transcript_ui::composer::Composer::set_bottom_inset`'s doc.
// `crate::ui::composer::Composer::set_bottom_inset`'s doc.
// `ime_bottom` already exceeds the plain nav-bar inset whenever the
// keyboard covers it, so the larger of the two is always the right
// answer without needing to know which is currently showing.
@@ -602,7 +602,7 @@ impl BenchClient {
}
fn rebuild_transcript(&mut self, rsc: &mut Rsc) {
let (screen, tree) = transcript_ui::build_tree(rsc, transcript_fixture::rows(&self.items));
let (screen, tree) = crate::ui::build_tree(rsc, crate::ui::fixture::rows(&self.items));
(self.content)(rsc).set(tree);
self.screen = Some(screen);
}
@@ -667,8 +667,8 @@ impl BenchClient {
// answer is "none" -- the bench itself opens a checked-in
// fixture and needs no server, so this pane is the only place
// an enrolment can be seen to have taken.
crate::enrollment::status_line(),
crate::app_log::diagnostics_line()
crate::android::enrollment::status_line(),
crate::android::app_log::diagnostics_line()
)
}
@@ -974,7 +974,7 @@ async fn read_anchor_position(
}
/// Register the scrolling widget with the frame loop, exactly as a
/// finger's own release does (`transcript_ui::Selection::drag`'s
/// finger's own release does (`crate::ui::Selection::drag`'s
/// `Released` arm) -- `Scrollable::fling` sets a velocity and drives
/// nothing by itself.
fn animate_scroll(scroll: iris::prelude::WeakWidget<iris::prelude::LazySpan>, rsc: &mut Rsc) {
File renamed without changes.
@@ -4,7 +4,7 @@
//! **Why**: Iris runs these builds on a phone with no `adb`, and Android
//! forbids one app reading another's `logcat`, so nothing outside this
//! process can recover what it wrote. The app already keeps a bounded copy
//! (`client_core::log_ring`); this is how the copy leaves the process. Dev
//! (`crate::client::log_ring`); this is how the copy leaves the process. Dev
//! Updater is on the same phone, so handing it over needs no tunnel, no
//! token and no second enrolment -- and it is Dev Updater's own contract
//! rather than something invented here, so any app it delivers can do the
@@ -80,7 +80,7 @@ pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeReady(
// line announcing this one rather than buried under it.
#[cfg(feature = "transcript-screen")]
if let Some(dir) = string_arg(&mut env, &files_dir) {
crate::app_log::set_crash_dir(std::path::Path::new(&dir));
crate::android::app_log::set_crash_dir(std::path::Path::new(&dir));
}
#[cfg(not(feature = "transcript-screen"))]
let _ = &files_dir;
@@ -124,7 +124,7 @@ pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeStatus(
/// `DevLogProvider.nativeLinesSince` -- every held line with a sequence at
/// or after `since`, oldest first, [`FIELDS_PER_LINE`] strings each.
///
/// Inclusive of `since` because [`client_core::log_ring::LogRing::since`]
/// Inclusive of `since` because [`crate::client::log_ring::LogRing::since`]
/// is, and one definition of the cursor is what keeps the app's own
/// uploaded report and this provider describing the same lines.
///
@@ -144,7 +144,7 @@ pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeLinesSinc
/// The three status numbers, as the provider's row.
#[cfg(feature = "transcript-screen")]
fn status_fields() -> Vec<String> {
let ring = client_core::log_ring::process_ring();
let ring = crate::client::log_ring::process_ring();
vec![
ring.len().to_string(),
ring.dropped().to_string(),
@@ -164,7 +164,7 @@ fn status_fields() -> Vec<String> {
#[cfg(feature = "transcript-screen")]
fn line_fields(since: u64) -> Vec<String> {
let (lines, _next) = client_core::log_ring::process_ring().since(since);
let (lines, _next) = crate::client::log_ring::process_ring().since(since);
let mut fields = Vec::with_capacity(lines.len() * FIELDS_PER_LINE);
for line in lines {
fields.push(line.seq.to_string());
@@ -1,7 +1,7 @@
//! Which `ai-server` this app talks to, and how it was told.
//!
//! The parsing, the file and its owner-only mode are
//! `client_core::config` (`EnrolledServer`/`EnrollmentStore`), shared with
//! `crate::client::config` (`EnrolledServer`/`EnrollmentStore`), shared with
//! the desktop app. What is genuinely this platform's, and all that is
//! here, is the intent plumbing: Android hands an `aiapp://enroll?...`
//! link to `MainActivity`, which passes it and the app's private files
@@ -19,11 +19,11 @@
//! avoids: it arrives from the activity, and `AndroidAppState::new` -- the
//! first thing that wants the enrollment -- has no parameter it could come
//! in through. Same shape, and the same reason, as
//! `client_core::log_ring`'s process ring.
//! `crate::client::log_ring`'s process ring.
#[cfg(not(feature = "bench"))]
use client_core::api::UreqTransport;
use client_core::config::{EnrolledServer, EnrollmentStore};
use crate::client::api::UreqTransport;
use crate::client::config::{EnrolledServer, EnrollmentStore};
use std::path::PathBuf;
use std::sync::OnceLock;
@@ -18,7 +18,7 @@
//! `IrisView`/`MainActivity` Java, and the JNI registration I2 built and
//! measured against, and the only thing a transcript screen needs on top
//! is a different `AndroidAppState` -- the same axis `tabs_ui::build` vs.
//! `transcript_ui::build` already varies along on the winit side (compare
//! `crate::ui::build` already varies along on the winit side (compare
//! `iris/examples/tabs.rs` and `iris/transcript-ui/examples/transcript.rs`).
//! A build picks one screen or the other, never both, so `Client` and
//! `TranscriptClient` are cfg-gated apart rather than switched at runtime --
@@ -26,7 +26,7 @@
//!
//! **`bench` feature (P0's iris half, docs/RUST.md):** a third
//! `AndroidAppState`, `bench_client::BenchClient`, on the same axis --
//! `transcript_ui::build_tree` again, this time against the checked-in
//! `crate::ui::build_tree` again, this time against the checked-in
//! fixture (`app/bench-fixture/assets/transcript.jsonl`) instead of a real
//! server, with a "Run benchmark" control that drives the same scroll loop
//! and streaming phase the Compose `bench` build type's `BenchRun.kt`
@@ -13,13 +13,13 @@
//! straight on the screen under test.
//!
//! Which server it opens it against is no longer baked in: it is the
//! enrollment an `aiapp://enroll` link left behind (`crate::enrollment`,
//! enrollment an `aiapp://enroll` link left behind (`crate::android::enrollment`,
//! and `desktop-app`'s identical `--link`), because an APK
//! cross-compiled here cannot pin the CA of a server on the host.
//!
//! **Reuses `iris/desktop-app`'s `app.rs` shape almost exactly** --
//! `fold_event`/`group_tool_runs`/`fold_page`/`raw_seq` from
//! `client_core::transcript_fold`, a `generation` counter guarding against
//! `crate::client::transcript_fold`, a `generation` counter guarding against
//! a stale background response. What differs is only the redraw
//! mechanism: android-view has no `winit::EventLoopProxy`, so this uses
//! `iris::task::Tasks::redraw_handle` (new, added alongside this box) to
@@ -30,16 +30,16 @@
//! **Streaming no longer costs a full rebuild** (fixed after the P0 gate
//! showed why it mattered -- 20 events/second means 20 rebuilds/second of
//! a ~3,200-row transcript otherwise): `apply_event` calls
//! `transcript_ui::TranscriptScreen::apply` with the item list before and
//! `crate::ui::TranscriptScreen::apply` with the item list before and
//! after `fold_event`, which updates only the row(s) that actually
//! changed (almost always just the one open assistant message) instead of
//! refolding and rebuilding every row. `rebuild_transcript` still runs
//! the whole widget tree once, for the opening page and for `apply`'s own
//! rare regroup fallback.
use client_core::api::{ApiClient, UreqTransport};
use client_core::event_stream::{StreamItem, follow_session_events};
use client_core::transcript_fold::{TranscriptItem, fold_event, fold_page, group_tool_runs};
use crate::client::api::{ApiClient, UreqTransport};
use crate::client::event_stream::{StreamItem, follow_session_events};
use crate::client::transcript_fold::{TranscriptItem, fold_event, fold_page, group_tool_runs};
use event_model::SeqEvent;
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
use iris::prelude::*;
@@ -54,7 +54,7 @@ pub struct TranscriptClient {
/// `set` calls the way `desktop-app`'s `transcript_ptr` isn't touched
/// by rebuilding the session list beside it.
content: WeakWidget<WidgetPtr>,
screen: Option<transcript_ui::TranscriptScreen>,
screen: Option<crate::ui::TranscriptScreen>,
/// The folded transcript as of the last rebuild -- kept here (not
/// re-derived) for the same reason `desktop-app`'s `Client::items`
/// exists: a live `StreamEvent` only carries one new wire event, and
@@ -90,7 +90,7 @@ impl HasAndroidUiState for TranscriptClient {
/// while the app is running is how somebody points it at another server,
/// and a cached transport would keep talking to the old one.
fn build_transport() -> Result<UreqTransport, String> {
crate::enrollment::transport()
crate::android::enrollment::transport()
}
fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
@@ -262,14 +262,14 @@ impl TranscriptClient {
.fetch_transcript_page(&session_id, None, 200, true)
.map_err(|e| e.to_string());
// The wire `seq` of the last line, not a folded item's `seq()`
// -- see `client_core::transcript_fold::raw_seq`'s doc for why
// -- see `crate::client::transcript_fold::raw_seq`'s doc for why
// resuming from the latter re-delivers deltas already folded
// into an in-progress reply.
let after = page
.as_ref()
.ok()
.and_then(|values| values.last())
.and_then(client_core::transcript_fold::raw_seq)
.and_then(crate::client::transcript_fold::raw_seq)
.unwrap_or(0);
let result = page.and_then(|values| fold_page(&values));
@@ -341,7 +341,7 @@ impl TranscriptClient {
.filter(|t| !t.is_empty());
let rows = group_tool_runs(&self.items);
let (screen, tree) = transcript_ui::build_tree(rsc, rows);
let (screen, tree) = crate::ui::build_tree(rsc, rows);
if let Some(text) = in_progress {
screen.composer.field.edit(rsc).set(&text);
+35
View File
@@ -0,0 +1,35 @@
//! RUST.md's E4: the transcript screen (`transcript-ui`, I5) in a real
//! winit window on the desktop, with a session list beside it, talking to
//! a real `ai-server` over `client-core`'s REST + SSE clients. See
//! `app.rs`'s module doc for the widget tree and the event flow.
//!
//! Usage:
//!
//! desktop-app --link 'aiapp://enroll?host=H&port=P&token=T&ca=B'
//! desktop-app # after the first run above
//! 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
//! phone would scan as a QR (DECISIONS.md, 2026-09-05) -- pasted rather
//! 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
//! to enrol against a different server.
//!
//! The CA comes with the link (`wg_app_link::enroll::ca_param`, which
//! `ai-server` now always includes) and is saved with it. `--ca` is the
//! override for a link that carries none, and names the same
//! `certs/ca.pem` a `curl --cacert` call uses.
use ai_app::desktop::{app, startup};
fn main() {
// Validated once here so a bad `--ca`/`--link` is reported on stderr
// before any window opens; `Client::new` calls this same function
// again once the window exists, so this first call is a fast-fail
// rather than the only place the values come from.
if let Err(e) = startup::load_startup_config() {
eprintln!("desktop-app: {e}");
std::process::exit(2);
}
app::run();
}
File renamed without changes.
@@ -1,5 +1,5 @@
//! The REST half of the backend's surface (see `server/src/routes.rs`'s
//! module doc for the table); the SSE half is [`crate::event_stream`].
//! module doc for the table); the SSE half is [`crate::client::event_stream`].
//! Ported from `app/.../Api.kt`, but **not at full parity yet** -- see
//! `CLIENT_CORE.md` for exactly which routes have a typed method here and
//! which do not.
@@ -64,7 +64,7 @@ pub trait Transport: Send + Sync {
/// Opens `path` and answers a reader over the response body, for a
/// caller that reads it as a stream rather than all at once (the SSE
/// connections in [`crate::event_stream`]). Fails the same way
/// connections in [`crate::client::event_stream`]). Fails the same way
/// [`Transport::request`] does for a non-2xx response.
fn stream(&self, path: &str) -> Result<Box<dyn Read + Send>, ApiError>;
}
@@ -265,7 +265,7 @@ impl<T: Transport> ApiClient<T> {
/// A page of transcript history. `before` is the newest-first cursor
/// (server default is "the newest page" when absent, which a caller
/// gets by passing `None`); the events themselves are handed back as
/// [`event_model::SeqEvent`] via `crate::event_stream`'s parsing, kept
/// [`event_model::SeqEvent`] via `crate::client::event_stream`'s parsing, kept
/// out of this method's signature so a caller that only wants the raw
/// lines (for the transcript cache) is not forced to parse them.
pub fn fetch_transcript_page(
@@ -284,7 +284,7 @@ impl<T: Transport> ApiClient<T> {
/// A page of transcript history, each line handed back paired with the
/// exact text it came from, and bounded below by `after` -- the shape
/// `crate::transcript_source::TranscriptSource` needs to store what it
/// `crate::client::transcript_source::TranscriptSource` needs to store what it
/// fetched in the transcript cache without a second round trip to fetch
/// the raw text separately. Ported from `Api.kt`'s `fetchTranscript`.
///
@@ -99,7 +99,7 @@ impl EnrolledServer {
})
}
/// Where a `client_core::api::UreqTransport` reaches this server.
/// Where a `crate::client::api::UreqTransport` reaches this server.
pub fn base_url(&self) -> String {
format!("https://{}:{}", self.host, self.port)
}
File renamed without changes.
@@ -1,14 +1,14 @@
//! The SSE half of the API: one long-lived GET per open session screen,
//! replaying the transcript after a cursor and then following it live.
//! Ported from `app/.../EventStream.kt`; the framing itself is
//! [`crate::sse`].
//! [`crate::client::sse`].
use std::io::{BufRead, BufReader};
use event_model::SeqEvent;
use crate::api::{ApiError, Transport};
use crate::sse::SseReader;
use crate::client::api::{ApiError, Transport};
use crate::client::sse::SseReader;
/// The frame name the server uses to say a cursor was too far behind to
/// continue from. Must match `send_backlog` in `server/src/routes.rs`.
@@ -83,7 +83,7 @@ pub fn follow_session_events(
#[cfg(test)]
mod tests {
use super::*;
use crate::api::{Body, RawResponse};
use crate::client::api::{Body, RawResponse};
use std::io::Cursor;
struct FixtureTransport {
@@ -107,8 +107,8 @@ impl MarkdownScanner {
fn table_delimiter(&self, start: usize, end: usize) -> bool {
let mut dashes = false;
let mut pipes = false;
for at in self.indented(start, end)..end {
match self.code[at] {
for c in &self.code[self.indented(start, end)..end] {
match c {
'-' => dashes = true,
'|' => pipes = true,
':' | ' ' | '\t' => {}
@@ -263,8 +263,7 @@ impl MarkdownScanner {
return false;
}
let mut seen = 0usize;
for at in start..end {
let c = self.code[at];
for &c in &self.code[start..end] {
if c == marker {
seen += 1;
} else if !c.is_whitespace() {
File renamed without changes.
@@ -305,16 +305,16 @@ impl LogRing {
}
}
/// Whether a target belongs to this app's own crates (`iris` or
/// `client_core`) rather than a dependency's -- `starts_with` guarded by an
/// Whether a target belongs to this app or to `iris` rather than to a
/// dependency -- `starts_with` guarded by an
/// exact match or a `::` so an unrelated crate that merely begins with the
/// same letters (there is no such crate today, but the check should not
/// rely on that) is never mistaken for one of ours.
fn is_own_target(target: &str) -> bool {
target == "iris"
|| target.starts_with("iris::")
|| target == "client_core"
|| target.starts_with("client_core::")
|| target == "ai_app"
|| target.starts_with("ai_app::")
}
/// Whether a line at `level` from `target` belongs in the ring, given
@@ -733,7 +733,7 @@ mod tests {
"own Debug, tracing on: included"
);
assert!(
ring_accepts(Level::Trace, "client_core::api", true),
ring_accepts(Level::Trace, "ai_app::api", true),
"own Trace, tracing on: included"
);
}
@@ -742,8 +742,8 @@ mod tests {
fn is_own_target_matches_the_crate_or_its_modules_only() {
assert!(is_own_target("iris"));
assert!(is_own_target("iris::sense"));
assert!(is_own_target("client_core"));
assert!(is_own_target("client_core::log_ring"));
assert!(is_own_target("ai_app"));
assert!(is_own_target("ai_app::log_ring"));
assert!(!is_own_target("iris_something_else"));
assert!(!is_own_target("naga::front"));
assert!(!is_own_target("jni"));
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,7 @@
//! `GET /notifications`, the attention stream PLAN.md's "Notifications: two
//! places, never both" describes. Ported from the parsing half of
//! `app/.../Notifications.kt`'s `NotificationService` -- the framing
//! ([`crate::sse`]) and the wire shape ([`SessionNotification`],
//! ([`crate::client::sse`]) and the wire shape ([`SessionNotification`],
//! [`NotificationKind`], mirroring `server/src/session/mod.rs`'s
//! `Notification`/`NotificationKind`).
//!
@@ -17,8 +17,8 @@ use std::io::{BufRead, BufReader};
use serde::Deserialize;
use crate::api::{ApiError, Transport};
use crate::sse::SseReader;
use crate::client::api::{ApiError, Transport};
use crate::client::sse::SseReader;
/// One frame of `GET /notifications`, matching `server/src/session/mod.rs`'s
/// `Notification` field for field.
@@ -93,7 +93,7 @@ pub fn follow_notifications(
#[cfg(test)]
mod tests {
use super::*;
use crate::api::{Body, RawResponse};
use crate::client::api::{Body, RawResponse};
use std::io::Cursor;
struct FixtureTransport {
File renamed without changes.
File renamed without changes.
@@ -13,8 +13,8 @@
//! of this crate exists: the derivation is the same on a phone and on a
//! desktop, and it is testable without a renderer.
use crate::durations::format_millis_text;
use crate::highlight::Language;
use crate::client::durations::format_millis_text;
use crate::client::highlight::Language;
use serde_json::{Map, Value};
/// A tool call's input, split into the parts a card draws separately.
File renamed without changes.
File renamed without changes.
@@ -22,9 +22,9 @@
use event_model::SeqEvent;
use crate::api::{ApiClient, ApiError, Transport};
use crate::event_stream::{self, StreamItem};
use crate::transcript_cache::SessionCache;
use crate::client::api::{ApiClient, ApiError, Transport};
use crate::client::event_stream::{self, StreamItem};
use crate::client::transcript_cache::SessionCache;
/// How many events a session screen opens with, cached or fetched.
///
@@ -284,7 +284,7 @@ impl<T: Transport> TranscriptSource<T> {
#[cfg(test)]
mod tests {
use super::*;
use crate::api::{Body, RawResponse};
use crate::client::api::{Body, RawResponse};
use std::collections::VecDeque;
use std::io::Read;
use std::sync::Mutex;
@@ -352,7 +352,7 @@ mod tests {
cache_root: &std::path::Path,
) -> TranscriptSource<ScriptedTransport> {
let api = ApiClient::new(transport);
let cache = crate::transcript_cache::TranscriptCache::new(cache_root).session("s1");
let cache = crate::client::transcript_cache::TranscriptCache::new(cache_root).session("s1");
TranscriptSource::new(api, "s1", cache)
}
@@ -385,7 +385,7 @@ mod tests {
let transport2 = ScriptedTransport::default();
transport2.respond(200, format!("[{}]", status_line(1)));
let cache = crate::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
let cache = crate::client::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
let source2 = TranscriptSource::new(ApiClient::new(transport2), "s1", cache);
assert!(source2.probe().unwrap());
assert!(source2.cache.tail().is_some());
@@ -404,7 +404,7 @@ mod tests {
let transport2 = ScriptedTransport::default();
let different = r#"{"seq":1,"ts":1.0,"type":"status","state":"running"}"#.to_string();
transport2.respond(200, format!("[{different}]"));
let cache = crate::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
let cache = crate::client::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
let source2 = TranscriptSource::new(ApiClient::new(transport2), "s1", cache);
assert!(!source2.probe().unwrap());
assert!(source2.cache.tail().is_none());
@@ -420,7 +420,7 @@ mod tests {
let transport2 = ScriptedTransport::default();
transport2.respond(500, "server on fire");
let cache = crate::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
let cache = crate::client::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
let source2 = TranscriptSource::new(ApiClient::new(transport2), "s1", cache);
assert!(source2.probe().is_err());
assert!(
@@ -475,7 +475,7 @@ mod tests {
let transport2 = ScriptedTransport::default();
transport2.respond(200, format!("[{}]", status_line(3)));
let cache = crate::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
let cache = crate::client::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
let source2 = TranscriptSource::new(ApiClient::new(transport2), "s1", cache);
source2.page(5, 10, true).unwrap();
assert_eq!(
@@ -491,7 +491,7 @@ mod tests {
#[test]
fn a_server_page_is_floored_at_the_end_of_the_cached_run() {
let dir = tempfile::tempdir().unwrap();
let cache = crate::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
let cache = crate::client::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
// A stored page covering [3, 6) and two live events above it, so the run this
// phone holds is [3, 8) -- the newest chunk has to be an appended one, or the
// cache reads the directory as damaged and discards it.
@@ -529,7 +529,7 @@ mod tests {
#[test]
fn an_unreadable_cached_page_is_a_parse_error_rather_than_an_empty_one() {
let dir = tempfile::tempdir().unwrap();
let cache = crate::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
let cache = crate::client::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
cache.store_page(
&[r#"{"seq":3,"but":"not an event"}"#.to_string()],
3,
@@ -551,7 +551,7 @@ mod tests {
#[test]
fn a_bad_cached_opening_line_purges_rather_than_panicking() {
let dir = tempfile::tempdir().unwrap();
let cache = crate::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
let cache = crate::client::transcript_cache::TranscriptCache::new(dir.path()).session("s1");
cache.append("not json at all", 1);
cache.flush();
let transport = ScriptedTransport::default();
@@ -7,7 +7,7 @@
//!
//! ```text
//! +-----------+--------------------------------------+
//! | session | transcript_ui::TranscriptScreen |
//! | session | crate::ui::TranscriptScreen |
//! | list | (List of folded rows + composer) |
//! | (WidgetPtr| |
//! | swapped | (WidgetPtr swapped whole on session |
@@ -16,7 +16,7 @@
//! ```
//!
//! **Incoming SSE events go through `TranscriptScreen::apply`**, not a
//! full rebuild: `client_core::transcript_fold::fold_event` folds the new
//! full rebuild: `crate::client::transcript_fold::fold_event` folds the new
//! item list as before, then `apply` updates only the row(s) that actually
//! changed (almost always the one still-open assistant message a delta
//! landed in) instead of rebuilding the whole right-hand widget tree from
@@ -29,7 +29,7 @@
//! would otherwise lose data on -- `apply`'s own path never touches the
//! composer at all, so this only matters on the fallback.
//!
//! Background network I/O (`client_core::api`/`event_stream`, both
//! Background network I/O (`crate::client::api`/`event_stream`, both
//! blocking by design -- see `client-core`'s `Cargo.toml`) runs on plain
//! `std::thread`s that report back through `winit`'s `EventLoopProxy`
//! (`Proxy<AppEvent>`), rather than through iris's own `Tasks`/`task_on`:
@@ -39,9 +39,9 @@
//! `Proxy::send_event` wakes the window's event loop immediately, once per
//! event, which is what a stream wants.
use client_core::api::{ApiClient, SessionSummary, UreqTransport};
use client_core::event_stream::{StreamItem, follow_session_events};
use client_core::transcript_fold::{
use crate::client::api::{ApiClient, SessionSummary, UreqTransport};
use crate::client::event_stream::{StreamItem, follow_session_events};
use crate::client::transcript_fold::{
TranscriptItem, fold_event, fold_page, group_tool_runs, raw_seq,
};
use event_model::SeqEvent;
@@ -103,7 +103,7 @@ struct Client {
items: Vec<TranscriptItem>,
list_ptr: WeakWidget<WidgetPtr>,
transcript_ptr: WeakWidget<WidgetPtr>,
screen: Option<transcript_ui::TranscriptScreen>,
screen: Option<crate::ui::TranscriptScreen>,
/// Bumped every time the selected session changes; see `AppEvent`'s
/// doc for what it guards against.
generation: Arc<AtomicU64>,
@@ -123,7 +123,7 @@ impl DefaultAppState for Client {
// already called this once to fail fast before a window opens;
// this call only fails if the filesystem changed underneath the
// process in between, which is not a case worth a nicer message.
let (server, ca_pem) = crate::load_startup_config().unwrap_or_else(|e| {
let (server, ca_pem) = super::startup::load_startup_config().unwrap_or_else(|e| {
eprintln!("desktop-app: {e}");
std::process::exit(2);
});
@@ -364,7 +364,7 @@ impl Client {
.filter(|t| !t.is_empty());
let rows = group_tool_runs(&self.items);
let (screen, tree) = transcript_ui::build_tree(rsc, rows);
let (screen, tree) = crate::ui::build_tree(rsc, rows);
if let Some(text) = in_progress {
screen.composer.field.edit(rsc).set(&text);
@@ -1,4 +1,4 @@
//! Where the desktop app keeps its enrollment: `client_core::config`'s
//! Where the desktop app keeps its enrollment: `crate::client::config`'s
//! [`EnrollmentStore`] pointed at `$XDG_CONFIG_HOME/ai-app-desktop`.
//!
//! Only the directory is this app's -- the file's name, its JSON, and its
@@ -6,7 +6,7 @@
//! are the store's, shared with the Android client so the two cannot come
//! to disagree about them.
use client_core::config::EnrollmentStore;
use crate::client::config::EnrollmentStore;
use std::path::PathBuf;
/// `$XDG_CONFIG_HOME/ai-app-desktop`, falling back to `~/.config` the way
+7
View File
@@ -0,0 +1,7 @@
//! The desktop entry point's state: the widget tree, the event flow and
//! the saved enrolment. `src/bin_desktop.rs` is the binary that drives it
//! -- see that file's doc comment for the command line.
pub mod app;
pub mod config;
pub mod startup;
@@ -1,29 +1,11 @@
//! RUST.md's E4: the transcript screen (`transcript-ui`, I5) in a real
//! winit window on the desktop, with a session list beside it, talking to
//! a real `ai-server` over `client-core`'s REST + SSE clients. See
//! `app.rs`'s module doc for the widget tree and the event flow.
//!
//! Usage:
//!
//! desktop-app --link 'aiapp://enroll?host=H&port=P&token=T&ca=B'
//! desktop-app # after the first run above
//! 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
//! phone would scan as a QR (DECISIONS.md, 2026-09-05) -- pasted rather
//! 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
//! to enrol against a different server.
//!
//! The CA comes with the link (`wg_app_link::enroll::ca_param`, which
//! `ai-server` now always includes) and is saved with it. `--ca` is the
//! override for a link that carries none, and names the same
//! `certs/ca.pem` a `curl --cacert` call uses.
//! The desktop binary's command line and the enrolment it resolves --
//! `--link`/`--ca`, parsed once at startup and again from `app.rs`'s
//! `Client::new`. Here rather than in `src/bin_desktop.rs` because both
//! callers are in the library; the binary is only `fn main`.
mod app;
mod config;
use crate::client::config::EnrolledServer;
use client_core::config::EnrolledServer;
use super::config;
struct Args {
ca_path: Option<std::path::PathBuf>,
@@ -54,7 +36,7 @@ fn parse_args() -> Result<Args, String> {
/// argv and config file, so it is safe to call again from `Client::new` --
/// see that call site's comment for why it is not threaded through some
/// other way (`DefaultApp::run()` takes no payload).
fn load_startup_config() -> Result<(EnrolledServer, Vec<u8>), String> {
pub fn load_startup_config() -> Result<(EnrolledServer, Vec<u8>), String> {
let args = parse_args()?;
let store = config::store();
let server = match args.link {
@@ -92,15 +74,3 @@ fn load_startup_config() -> Result<(EnrolledServer, Vec<u8>), String> {
};
Ok((server, ca_pem))
}
fn main() {
// Validated once here so a bad `--ca`/`--link` is reported on stderr
// before any window opens; `Client::new` calls this same function
// again once the window exists, so this first call is a fast-fail
// rather than the only place the values come from.
if let Err(e) = load_startup_config() {
eprintln!("desktop-app: {e}");
std::process::exit(2);
}
app::run();
}
+38
View File
@@ -0,0 +1,38 @@
//! The app -- a phone and desktop interface to AI coding sessions, drawn
//! with `iris` (the UI framework next door, which knows nothing about any
//! of this) and talking to `ai-server` over the model in `event-model`.
//!
//! Four modules, and the three entry points that combine them:
//!
//! - [`client`] -- no UI at all: the REST and SSE clients, the transcript
//! cache and fold, the highlighter, the ANSI parser, config and the
//! enrolment link. `docs/CLIENT_CORE.md` is its design.
//! - [`ui`] -- the screens, as iris widget trees. `docs/RUST.md`'s I5.
//! - [`desktop`] -- the winit entry point's app state (`src/bin_desktop.rs`
//! is the binary itself).
//! - [`android`] -- the `android-view` entry point: JNI registration, the
//! view peer, the enrolment deep link and the on-device log.
//! - [`shell`] -- a second, separate JNI surface: the bridge the *Kotlin*
//! app in `app/shellApp` calls for notifications, sharing and settings.
//! It draws nothing, which is why it is behind its own feature.
pub mod client;
#[cfg(feature = "screens")]
pub mod ui;
#[cfg(all(feature = "screens", not(target_os = "android")))]
pub mod desktop;
#[cfg(all(feature = "screens", target_os = "android"))]
pub mod android;
// `jni` 0.22's `native_method!` expands to `AtomicBool::fetch_update`,
// which this toolchain deprecates in favour of `try_update`. The call is
// inside the macro, so there is nothing here to migrate -- the fix is a
// `jni` release, and this allow comes out when one lands. Scoped to the
// module the macro is used in rather than the crate, so a deprecation in
// our own code is still a warning.
#[cfg(feature = "shell")]
#[allow(deprecated)]
pub mod shell;
File renamed without changes.
File renamed without changes.
@@ -12,14 +12,14 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use client_core::api::UreqTransport;
use client_core::notifications::{SessionNotification, follow_notifications};
use crate::client::api::UreqTransport;
use crate::client::notifications::{SessionNotification, follow_notifications};
use jni::Env;
use jni::errors::Result;
use jni::objects::{JObject, JValue};
use jni::sys::{JNI_TRUE, jint};
use crate::settings::{self, ServerSettings};
use crate::shell::settings::{self, ServerSettings};
const ALERT_CHANNEL: &str = "sessions";
const ONGOING_CHANNEL: &str = "connection";
@@ -53,17 +53,17 @@ static RUNNING: AtomicBool = AtomicBool::new(false);
/// handle. So a stop lands at the next reconnect, not mid-read. `/notifications`
/// is idle between events (a keep-alive, per `server/src/routes.rs`), so in
/// practice this is a bounded wait rather than a hang; closing that gap
/// for real means adding a cancellation point to `client_core::Transport`,
/// for real means adding a cancellation point to `crate::client::Transport`,
/// which is a decision affecting every caller of that trait, not just this
/// one -- left for whoever next depends on prompt shutdown.
static STOPPING: AtomicBool = AtomicBool::new(false);
fn static_int(env: &mut Env, class: &str, field: &str) -> Result<i32> {
crate::jcall::get_static_field(env, class, field, "I")?.i()
crate::shell::jcall::get_static_field(env, class, field, "I")?.i()
}
fn notification_manager<'l>(env: &mut Env<'l>, context: &JObject) -> Result<JObject<'l>> {
crate::jcall::call_static_method(
crate::shell::jcall::call_static_method(
env,
"androidx/core/app/NotificationManagerCompat",
"from",
@@ -80,22 +80,22 @@ fn create_channel(
name: &str,
importance: i32,
) -> Result<()> {
let id_j = crate::jcall::jstr_obj(env, id)?;
let builder = crate::jcall::new_object(
let id_j = crate::shell::jcall::jstr_obj(env, id)?;
let builder = crate::shell::jcall::new_object(
env,
"androidx/core/app/NotificationChannelCompat$Builder",
"(Ljava/lang/String;I)V",
&[JValue::Object(&id_j), JValue::Int(importance)],
)?;
let name_j = crate::jcall::jstr_obj(env, name)?;
crate::jcall::call_method(
let name_j = crate::shell::jcall::jstr_obj(env, name)?;
crate::shell::jcall::call_method(
env,
&builder,
"setName",
"(Ljava/lang/CharSequence;)Landroidx/core/app/NotificationChannelCompat$Builder;",
&[JValue::Object(&name_j)],
)?;
let channel = crate::jcall::call_method(
let channel = crate::shell::jcall::call_method(
env,
&builder,
"build",
@@ -103,7 +103,7 @@ fn create_channel(
&[],
)?
.l()?;
crate::jcall::call_method(
crate::shell::jcall::call_method(
env,
manager,
"createNotificationChannel",
@@ -146,8 +146,8 @@ fn new_intent_for<'l>(
context: &JObject,
class_name: &str,
) -> Result<JObject<'l>> {
let target_class = crate::jcall::find_class(env, class_name)?;
crate::jcall::new_object(
let target_class = crate::shell::jcall::find_class(env, class_name)?;
crate::shell::jcall::new_object(
env,
"android/content/Intent",
"(Landroid/content/Context;Ljava/lang/Class;)V",
@@ -165,41 +165,42 @@ fn session_intent<'l>(
session_id: &str,
) -> Result<JObject<'l>> {
let intent = new_intent_for(env, context, "com/example/aiapp/shell/MainActivity")?;
let action_view = crate::jcall::jstr_obj(env, "android.intent.action.VIEW")?;
crate::jcall::call_method(
let action_view = crate::shell::jcall::jstr_obj(env, "android.intent.action.VIEW")?;
crate::shell::jcall::call_method(
env,
&intent,
"setAction",
"(Ljava/lang/String;)Landroid/content/Intent;",
&[JValue::Object(&action_view)],
)?;
let builder = crate::jcall::new_object(env, "android/net/Uri$Builder", "()V", &[])?;
let scheme = crate::jcall::jstr_obj(env, settings::SCHEME)?;
crate::jcall::call_method(
let builder = crate::shell::jcall::new_object(env, "android/net/Uri$Builder", "()V", &[])?;
let scheme = crate::shell::jcall::jstr_obj(env, settings::SCHEME)?;
crate::shell::jcall::call_method(
env,
&builder,
"scheme",
"(Ljava/lang/String;)Landroid/net/Uri$Builder;",
&[JValue::Object(&scheme)],
)?;
let authority = crate::jcall::jstr_obj(env, "session")?;
crate::jcall::call_method(
let authority = crate::shell::jcall::jstr_obj(env, "session")?;
crate::shell::jcall::call_method(
env,
&builder,
"authority",
"(Ljava/lang/String;)Landroid/net/Uri$Builder;",
&[JValue::Object(&authority)],
)?;
let path = crate::jcall::jstr_obj(env, session_id)?;
crate::jcall::call_method(
let path = crate::shell::jcall::jstr_obj(env, session_id)?;
crate::shell::jcall::call_method(
env,
&builder,
"appendPath",
"(Ljava/lang/String;)Landroid/net/Uri$Builder;",
&[JValue::Object(&path)],
)?;
let uri = crate::jcall::call_method(env, &builder, "build", "()Landroid/net/Uri;", &[])?.l()?;
crate::jcall::call_method(
let uri = crate::shell::jcall::call_method(env, &builder, "build", "()Landroid/net/Uri;", &[])?
.l()?;
crate::shell::jcall::call_method(
env,
&intent,
"setData",
@@ -216,7 +217,7 @@ fn pending_activity<'l>(
) -> Result<JObject<'l>> {
let update_current = static_int(env, "android/app/PendingIntent", "FLAG_UPDATE_CURRENT")?;
let immutable = static_int(env, "android/app/PendingIntent", "FLAG_IMMUTABLE")?;
crate::jcall::call_static_method(
crate::shell::jcall::call_static_method(
env,
"android/app/PendingIntent",
"getActivity",
@@ -238,7 +239,7 @@ fn builder_call<'l>(
sig: &str,
args: &[JValue],
) -> Result<()> {
crate::jcall::call_method(env, builder, method, sig, args)?;
crate::shell::jcall::call_method(env, builder, method, sig, args)?;
Ok(())
}
@@ -259,14 +260,14 @@ fn foreground_type(env: &mut Env) -> Result<i32> {
}
fn ongoing_notification<'l>(env: &mut Env<'l>, context: &JObject) -> Result<JObject<'l>> {
let channel = crate::jcall::jstr_obj(env, ONGOING_CHANNEL)?;
let builder = crate::jcall::new_object(
let channel = crate::shell::jcall::jstr_obj(env, ONGOING_CHANNEL)?;
let builder = crate::shell::jcall::new_object(
env,
"androidx/core/app/NotificationCompat$Builder",
"(Landroid/content/Context;Ljava/lang/String;)V",
&[JValue::Object(context), JValue::Object(&channel)],
)?;
let title = crate::jcall::jstr_obj(env, "Watching for sessions that need you")?;
let title = crate::shell::jcall::jstr_obj(env, "Watching for sessions that need you")?;
builder_call(
env,
&builder,
@@ -297,7 +298,8 @@ fn ongoing_notification<'l>(env: &mut Env<'l>, context: &JObject) -> Result<JObj
"(I)Landroidx/core/app/NotificationCompat$Builder;",
&[JValue::Int(priority_min)],
)?;
crate::jcall::call_method(env, &builder, "build", "()Landroid/app/Notification;", &[])?.l()
crate::shell::jcall::call_method(env, &builder, "build", "()Landroid/app/Notification;", &[])?
.l()
}
/// Starts the service if there is a server to connect to, and stops it
@@ -306,7 +308,7 @@ pub fn sync(env: &mut Env, context: &JObject) -> Result<()> {
let service_intent =
new_intent_for(env, context, "com/example/aiapp/shell/NotificationService")?;
if settings::load(env, context)?.is_none() {
crate::jcall::call_method(
crate::shell::jcall::call_method(
env,
context,
"stopService",
@@ -316,7 +318,7 @@ pub fn sync(env: &mut Env, context: &JObject) -> Result<()> {
return Ok(());
}
create_channels(env, context)?;
crate::jcall::call_static_method(
crate::shell::jcall::call_static_method(
env,
"androidx/core/content/ContextCompat",
"startForegroundService",
@@ -335,7 +337,7 @@ pub fn on_start_command(env: &mut Env, service: JObject) -> jint {
match try_start(env, &service) {
Ok(true) => static_int(env, "android/app/Service", "START_STICKY").unwrap_or(1),
Ok(false) => {
let _ = crate::jcall::call_method(env, &service, "stopSelf", "()V", &[]);
let _ = crate::shell::jcall::call_method(env, &service, "stopSelf", "()V", &[]);
static_int(env, "android/app/Service", "START_NOT_STICKY").unwrap_or(2)
}
Err(e) => {
@@ -352,7 +354,7 @@ fn try_start(env: &mut Env, service: &JObject) -> Result<bool> {
let ca = settings::load_pinned_ca(env)?;
let notification = ongoing_notification(env, service)?;
let fg_type = foreground_type(env)?;
crate::jcall::call_static_method(
crate::shell::jcall::call_static_method(
env,
"androidx/core/app/ServiceCompat",
"startForeground",
@@ -424,13 +426,14 @@ fn show(env: &mut Env, context: &JObject, notification: &SessionNotification) ->
let allowed = if sdk < tiramisu {
true
} else {
let permission = crate::jcall::jstr_obj(env, "android.permission.POST_NOTIFICATIONS")?;
let permission =
crate::shell::jcall::jstr_obj(env, "android.permission.POST_NOTIFICATIONS")?;
let granted = static_int(
env,
"android/content/pm/PackageManager",
"PERMISSION_GRANTED",
)?;
let result = crate::jcall::call_static_method(
let result = crate::shell::jcall::call_static_method(
env,
"androidx/core/content/ContextCompat",
"checkSelfPermission",
@@ -441,20 +444,21 @@ fn show(env: &mut Env, context: &JObject, notification: &SessionNotification) ->
result == granted
};
let enabled =
crate::jcall::call_method(env, &manager, "areNotificationsEnabled", "()Z", &[])?.z()?;
crate::shell::jcall::call_method(env, &manager, "areNotificationsEnabled", "()Z", &[])?
.z()?;
if !allowed || !enabled {
return Ok(());
}
let intent = session_intent(env, context, &notification.session_id)?;
let pending = pending_activity(env, context, &intent)?;
let channel = crate::jcall::jstr_obj(env, ALERT_CHANNEL)?;
let builder = crate::jcall::new_object(
let channel = crate::shell::jcall::jstr_obj(env, ALERT_CHANNEL)?;
let builder = crate::shell::jcall::new_object(
env,
"androidx/core/app/NotificationCompat$Builder",
"(Landroid/content/Context;Ljava/lang/String;)V",
&[JValue::Object(context), JValue::Object(&channel)],
)?;
let title = crate::jcall::jstr_obj(env, &notification.title)?;
let title = crate::shell::jcall::jstr_obj(env, &notification.title)?;
builder_call(
env,
&builder,
@@ -462,7 +466,7 @@ fn show(env: &mut Env, context: &JObject, notification: &SessionNotification) ->
"(Ljava/lang/CharSequence;)Landroidx/core/app/NotificationCompat$Builder;",
&[JValue::Object(&title)],
)?;
let text = crate::jcall::jstr_obj(env, notification.kind.attention_line())?;
let text = crate::shell::jcall::jstr_obj(env, notification.kind.attention_line())?;
builder_call(
env,
&builder,
@@ -507,11 +511,16 @@ fn show(env: &mut Env, context: &JObject, notification: &SessionNotification) ->
"(Z)Landroidx/core/app/NotificationCompat$Builder;",
&[JValue::Bool(JNI_TRUE)],
)?;
let built =
crate::jcall::call_method(env, &builder, "build", "()Landroid/app/Notification;", &[])?
let built = crate::shell::jcall::call_method(
env,
&builder,
"build",
"()Landroid/app/Notification;",
&[],
)?
.l()?;
let tag = crate::jcall::jstr_obj(env, &notification.session_id)?;
crate::jcall::call_method(
let tag = crate::shell::jcall::jstr_obj(env, &notification.session_id)?;
crate::shell::jcall::call_method(
env,
&manager,
"notify",
@@ -542,9 +551,9 @@ pub fn on_destroy() {
pub fn log_error(env: &mut Env, where_: &str, error: &jni::errors::Error) {
let message = format!("android-shell: {where_}: {error}");
let _ = (|| -> Result<()> {
let tag = crate::jcall::jstr_obj(env, "android-shell")?;
let msg = crate::jcall::jstr_obj(env, &message)?;
crate::jcall::call_static_method(
let tag = crate::shell::jcall::jstr_obj(env, "android-shell")?;
let msg = crate::shell::jcall::jstr_obj(env, &message)?;
crate::shell::jcall::call_static_method(
env,
"android/util/Log",
"e",
@@ -39,9 +39,9 @@ const STORE_CLASS: &str = "com/example/wgapplink/ServerStore";
const SETTINGS_CLASS: &str = "com/example/wgapplink/ServerSettings";
fn new_store<'l>(env: &mut Env<'l>) -> Result<JObject<'l>> {
let scheme = crate::jcall::jstr_obj(env, SCHEME)?;
let alias = crate::jcall::jstr_obj(env, KEY_ALIAS)?;
crate::jcall::new_object(
let scheme = crate::shell::jcall::jstr_obj(env, SCHEME)?;
let alias = crate::shell::jcall::jstr_obj(env, KEY_ALIAS)?;
crate::shell::jcall::new_object(
env,
STORE_CLASS,
"(Ljava/lang/String;Ljava/lang/String;)V",
@@ -51,13 +51,14 @@ fn new_store<'l>(env: &mut Env<'l>) -> Result<JObject<'l>> {
fn read_settings(env: &mut Env, settings_obj: &JObject) -> Result<ServerSettings> {
let host = get_string(env, settings_obj, "getHost")?;
let port = crate::jcall::call_method(env, settings_obj, "getPort", "()I", &[])?.i()?;
let port = crate::shell::jcall::call_method(env, settings_obj, "getPort", "()I", &[])?.i()?;
let token = get_string(env, settings_obj, "getToken")?;
Ok(ServerSettings { host, port, token })
}
fn get_string(env: &mut Env, obj: &JObject, getter: &str) -> Result<String> {
let value = crate::jcall::call_method(env, obj, getter, "()Ljava/lang/String;", &[])?.l()?;
let value =
crate::shell::jcall::call_method(env, obj, getter, "()Ljava/lang/String;", &[])?.l()?;
let jstr: JString = env.cast_local::<JString>(value)?;
jstr.try_to_string(env)
}
@@ -66,7 +67,7 @@ fn get_string(env: &mut Env, obj: &JObject, getter: &str) -> Result<String> {
/// `ServerConfig.kt`'s `loadServerSettings`.
pub fn load(env: &mut Env, context: &JObject) -> Result<Option<ServerSettings>> {
let store = new_store(env)?;
let settings_obj = crate::jcall::call_method(
let settings_obj = crate::shell::jcall::call_method(
env,
&store,
"load",
@@ -83,9 +84,9 @@ pub fn load(env: &mut Env, context: &JObject) -> Result<Option<ServerSettings>>
/// Seals and stores `settings` -- mirrors `ServerConfig.kt`'s `saveServerSettings`.
pub fn save(env: &mut Env, context: &JObject, settings: &ServerSettings) -> Result<()> {
let store = new_store(env)?;
let host = crate::jcall::jstr_obj(env, &settings.host)?;
let token = crate::jcall::jstr_obj(env, &settings.token)?;
let settings_obj = crate::jcall::new_object(
let host = crate::shell::jcall::jstr_obj(env, &settings.host)?;
let token = crate::shell::jcall::jstr_obj(env, &settings.token)?;
let settings_obj = crate::shell::jcall::new_object(
env,
SETTINGS_CLASS,
"(Ljava/lang/String;ILjava/lang/String;)V",
@@ -95,7 +96,7 @@ pub fn save(env: &mut Env, context: &JObject, settings: &ServerSettings) -> Resu
JValue::Object(&token),
],
)?;
crate::jcall::call_method(
crate::shell::jcall::call_method(
env,
&store,
"save",
@@ -110,7 +111,7 @@ pub fn save(env: &mut Env, context: &JObject, settings: &ServerSettings) -> Resu
/// query-parameter rules rather than re-deriving them here.
pub fn parse_enrollment_uri(env: &mut Env, uri: &JObject) -> Result<Option<ServerSettings>> {
let store = new_store(env)?;
let settings_obj = crate::jcall::call_method(
let settings_obj = crate::shell::jcall::call_method(
env,
&store,
"parseEnrollmentUri",
@@ -129,7 +130,7 @@ pub fn parse_enrollment_uri(env: &mut Env, uri: &JObject) -> Result<Option<Serve
/// but into a plain Java constant, since this module has no Kotlin of its
/// own to generate into.
pub fn load_pinned_ca(env: &mut Env) -> Result<Vec<u8>> {
let value = crate::jcall::get_static_field(
let value = crate::shell::jcall::get_static_field(
env,
"com/example/aiapp/shell/PinnedCa",
"PINNED_CA_PEM",
@@ -18,13 +18,13 @@
//! likely to be what somebody meant. Worth revisiting once a real screen
//! exists to ask instead of guessing.
use client_core::api::{ApiClient, UreqTransport};
use crate::client::api::{ApiClient, UreqTransport};
use jni::Env;
use jni::errors::Result;
use jni::objects::{JObject, JString, JValue};
use crate::notify;
use crate::settings;
use crate::shell::notify;
use crate::shell::settings;
const ACTION_SEND: &str = "android.intent.action.SEND";
const ACTION_SEND_MULTIPLE: &str = "android.intent.action.SEND_MULTIPLE";
@@ -32,7 +32,8 @@ const ACTION_VIEW: &str = "android.intent.action.VIEW";
const EXTRA_TEXT: &str = "android.intent.extra.TEXT";
fn get_string_method(env: &mut Env, obj: &JObject, method: &str) -> Result<Option<String>> {
let value = crate::jcall::call_method(env, obj, method, "()Ljava/lang/String;", &[])?.l()?;
let value =
crate::shell::jcall::call_method(env, obj, method, "()Ljava/lang/String;", &[])?.l()?;
if value.is_null() {
return Ok(None);
}
@@ -41,8 +42,8 @@ fn get_string_method(env: &mut Env, obj: &JObject, method: &str) -> Result<Optio
}
fn toast(env: &mut Env, context: &JObject, message: &str) -> Result<()> {
let message = crate::jcall::jstr_obj(env, message)?;
crate::jcall::call_static_method(
let message = crate::shell::jcall::jstr_obj(env, message)?;
crate::shell::jcall::call_static_method(
env,
"com/example/aiapp/shell/MainActivity",
"toast",
@@ -65,7 +66,8 @@ pub fn handle_intent(env: &mut Env, activity: &JObject, intent: &JObject) -> Res
if action.as_deref() != Some(ACTION_VIEW) {
return Ok(());
}
let uri = crate::jcall::call_method(env, intent, "getData", "()Landroid/net/Uri;", &[])?.l()?;
let uri = crate::shell::jcall::call_method(env, intent, "getData", "()Landroid/net/Uri;", &[])?
.l()?;
if uri.is_null() {
return Ok(());
}
@@ -109,8 +111,8 @@ fn handle_enrollment(env: &mut Env, activity: &JObject, uri: &JObject) -> Result
/// as a share, and `AttachmentButton`'s upload-then-message pattern for
/// what happens to it, minus attachments per this module's doc comment.
fn handle_share(env: &mut Env, activity: &JObject, intent: &JObject) -> Result<()> {
let extra_text = crate::jcall::jstr_obj(env, EXTRA_TEXT)?;
let text = crate::jcall::call_method(
let extra_text = crate::shell::jcall::jstr_obj(env, EXTRA_TEXT)?;
let text = crate::shell::jcall::call_method(
env,
intent,
"getStringExtra",
File renamed without changes.
@@ -5,7 +5,7 @@
//! bytes, generated by `app/bench-fixture/generate.py`, never a real
//! transcript -- that file's own README), embedded with `include_str!`.
//! The first [`BACKLOG_COUNT`] non-blank lines are the opening window,
//! folded once through `client_core::transcript_fold::fold_page` exactly
//! folded once through `crate::client::transcript_fold::fold_page` exactly
//! as a real `/transcript` page would be; the rest are the streaming
//! tail, replayed one at a time through `fold_event` the way a live SSE
//! frame arrives.
@@ -16,7 +16,7 @@
//! UI-shaped in a platform crate). What stayed there is the JNI half --
//! the clipboard, the battery sampler, the IME calls and the report.
use client_core::transcript_fold::{TranscriptItem, TranscriptRow, fold_page, group_tool_runs};
use crate::client::transcript_fold::{TranscriptItem, TranscriptRow, fold_page, group_tool_runs};
use event_model::SeqEvent;
use iris::prelude::*;
@@ -105,7 +105,7 @@ pub fn rows(items: &[TranscriptItem]) -> Vec<TranscriptRow> {
/// streamed. The tree itself comes back separately from
/// [`build_screen`], since whoever takes it owns it.
pub struct Opened {
pub screen: transcript_ui::TranscriptScreen,
pub screen: crate::ui::TranscriptScreen,
pub items: Vec<TranscriptItem>,
/// The tail, for a caller that goes on replaying it one event at a
/// time through `fold_event`/`TranscriptScreen::apply` -- the
@@ -114,7 +114,7 @@ pub struct Opened {
}
/// Build the transcript screen over the fixture's opening page, without
/// claiming the window's root -- `transcript_ui::build_tree`'s own split,
/// claiming the window's root -- `crate::ui::build_tree`'s own split,
/// for a caller (the Android bench) that puts the screen inside a shell
/// of its own.
pub fn build_screen<Rsc: HasEvents>(rsc: &mut Rsc) -> Result<(Opened, StrongWidget), String>
@@ -123,7 +123,7 @@ where
{
let fixture = Fixture::parse();
let items = fixture.backlog_items()?;
let (screen, tree) = transcript_ui::build_tree(rsc, rows(&items));
let (screen, tree) = crate::ui::build_tree(rsc, rows(&items));
Ok((
Opened {
screen,
@@ -1,4 +1,4 @@
//! One markdown **block** (`client_core::markdown_blocks::Block`) rendered
//! One markdown **block** (`crate::client::markdown_blocks::Block`) rendered
//! for display: the plain text to draw, the [`SpanStyle`]s that style it,
//! the links inside it, and the [`BlockFrame`] the row builder puts around
//! it.
@@ -44,8 +44,8 @@
//! paragraph's -- visible, not incorrect, and not fixed here since it
//! needs `SpanStyle` to carry line-height too.
use client_core::highlight::{self, Kind, Language};
use client_core::markdown_blocks::{Block, BlockKind};
use crate::client::highlight::{self, Kind, Language};
use crate::client::markdown_blocks::{Block, BlockKind};
use iris::prelude::*;
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
use std::ops::Range;
@@ -385,14 +385,14 @@ pub fn render_markdown(src: &str, base_size: f32) -> Rendered {
}
}
/// The same option set `client_core::markdown_blocks` splits with, so a
/// The same option set `crate::client::markdown_blocks` splits with, so a
/// block boundary there and the styling here cannot disagree about what
/// the source means.
fn options() -> Options {
Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TABLES | Options::ENABLE_TASKLISTS
}
/// `client_core::highlight`'s spans for the code at `range` inside `text`,
/// `crate::client::highlight`'s spans for the code at `range` inside `text`,
/// appended to `spans`.
///
/// The highlighter indexes **chars** and `SpanStyle` indexes **bytes**
@@ -567,7 +567,7 @@ fn wrap_cell(text: &str, width: usize) -> Vec<String> {
#[cfg(test)]
mod tests {
use super::*;
use client_core::markdown_blocks::split_blocks;
use crate::client::markdown_blocks::split_blocks;
fn block(src: &str) -> Rendered {
let blocks = split_blocks(src);
@@ -7,22 +7,22 @@
//!
//! ```text
//! +------------------------------------------+
//! | iris::widget::LazySpan (transcript_ui::row) | <- .height(rest(1))
//! | iris::widget::LazySpan (crate::ui::row) | <- .height(rest(1))
//! | row 1: sender label + one TextEdit |
//! | row 2: sender label + one TextEdit |
//! | row 3 (Tools): collapsed/expanded |
//! | ... |
//! +------------------------------------------+
//! | composer bar (transcript_ui::composer) | <- natural height
//! | composer bar (crate::ui::composer) | <- natural height
//! +------------------------------------------+
//! ```
//!
//! **What this crate does not do itself**: fetch anything over the network
//! or read the transcript cache. [`build`] takes an already-folded
//! `Vec<client_core::transcript_fold::TranscriptRow>` and
//! `Vec<crate::client::transcript_fold::TranscriptRow>` and
//! [`TranscriptScreen::push_row`] takes one more as it arrives -- the
//! caller (an app's own `main`, or a future `iris-android-app`-shaped
//! cdylib) owns `client_core::ApiClient`/
//! cdylib) owns `crate::client::ApiClient`/
//! `event_stream::follow_session_events` and the transcript cache, per the
//! code rules' "ask for the least you need": a widget-tree builder that
//! also knew how to make an HTTPS request would be untestable without a
@@ -44,13 +44,19 @@
//! row's own text too.
pub mod composer;
// The checked-in bench fixture opened as a real screen -- 1.9 MB of
// `include_str!`, so it is a feature rather than always present: a build
// meant for a phone must not carry it. `bench` turns it on; so does the
// default, which is what makes `cargo test` here run the harness tests.
#[cfg(feature = "fixture")]
pub mod fixture;
pub mod markdown;
pub mod row;
pub mod selection;
pub(crate) mod tap;
pub mod tool;
use client_core::transcript_fold::TranscriptRow as FoldedRow;
use crate::client::transcript_fold::TranscriptRow as FoldedRow;
use iris::prelude::*;
use selection::Selection;
use std::{cell::RefCell, rc::Rc};
@@ -111,7 +117,7 @@ impl TranscriptScreen {
}
/// Whether the session this transcript belongs to is still doing
/// something (`client_core::transcript_fold::session_working`).
/// something (`crate::client::transcript_fold::session_working`).
///
/// The one thing a tool card cannot read off its own call: a call with
/// no result is *running* while the session works and *never came
@@ -189,7 +195,7 @@ impl TranscriptScreen {
// row -- rebuild it.
if matches!(
item,
client_core::transcript_fold::TranscriptItem::ToolRun { .. }
crate::client::transcript_fold::TranscriptItem::ToolRun { .. }
) {
return false;
}
@@ -214,7 +220,7 @@ impl TranscriptScreen {
/// Apply the effect of one more folded event without rebuilding the
/// whole screen -- RUST.md's "streaming still costs a full rebuild"
/// fix. `old`/`new` are `client_core::transcript_fold::fold_event`'s
/// fix. `old`/`new` are `crate::client::transcript_fold::fold_event`'s
/// own before/after item lists (never grouped into rows -- that
/// happens here, over both, so the common tail cases can be told
/// apart; `group_tool_runs` is pure bookkeeping over already-folded
@@ -245,12 +251,12 @@ impl TranscriptScreen {
pub fn apply<Rsc: HasEvents>(
&self,
rsc: &mut Rsc,
old: &[client_core::transcript_fold::TranscriptItem],
new: &[client_core::transcript_fold::TranscriptItem],
old: &[crate::client::transcript_fold::TranscriptItem],
new: &[crate::client::transcript_fold::TranscriptItem],
) where
Rsc::State: FocusHost + OpenUrl,
{
use client_core::transcript_fold::group_tool_runs;
use crate::client::transcript_fold::group_tool_runs;
let old_rows = group_tool_runs(old);
let new_rows = group_tool_runs(new);
@@ -521,7 +527,7 @@ fn diff_rows(old: &[FoldedRow], new: &[FoldedRow]) -> RowDiff {
#[cfg(test)]
mod diff_tests {
use super::*;
use client_core::transcript_fold::TranscriptItem;
use crate::client::transcript_fold::TranscriptItem;
fn user(seq: u64, text: &str) -> FoldedRow {
FoldedRow::Single(TranscriptItem::UserMsg {
@@ -627,7 +633,7 @@ mod diff_tests {
#[cfg(test)]
mod apply_tests {
use super::*;
use client_core::transcript_fold::TranscriptItem;
use crate::client::transcript_fold::TranscriptItem;
struct TestFocus {
focus: Option<WeakWidget<TextEdit>>,
@@ -744,7 +750,7 @@ mod apply_tests {
)];
let (screen, tree) = build_tree(
&mut rsc,
client_core::transcript_fold::group_tool_runs(&old_items),
crate::client::transcript_fold::group_tool_runs(&old_items),
);
let mut render = UiRenderState::new();
render.resize((1080.0, 20000.0));
@@ -799,7 +805,7 @@ mod apply_tests {
#[test]
fn a_row_dropped_by_a_regroup_does_not_outlive_itself_in_selection() {
use client_core::transcript_fold::group_tool_runs;
use crate::client::transcript_fold::group_tool_runs;
let mut rsc = TestRsc {
ui: UiData::default(),
@@ -824,7 +830,7 @@ mod apply_tests {
// land on; `begin` deselects every *other* registered row first,
// which is exactly what used to resolve a stale `WeakWidget` left
// by the regrouped-away rows and panic.
let surviving_key = row::row_key(&client_core::transcript_fold::ItemKey::Seq(4));
let surviving_key = row::row_key(&crate::client::transcript_fold::ItemKey::Seq(4));
screen.selection.borrow_mut().begin(
&mut rsc,
(surviving_key, 0),
@@ -844,7 +850,7 @@ mod apply_tests {
/// `if new_key != old_key` guard this replaced could not see it.
#[test]
fn a_tail_rebuilt_with_fewer_blocks_leaves_none_of_them_in_selection() {
use client_core::transcript_fold::group_tool_runs;
use crate::client::transcript_fold::group_tool_runs;
let mut rsc = TestRsc {
ui: UiData::default(),
@@ -862,7 +868,7 @@ mod apply_tests {
);
let (screen, _tree) = build_tree(&mut rsc, group_tool_runs(&old_items));
let tail_key = row::row_key(&client_core::transcript_fold::ItemKey::Seq(2));
let tail_key = row::row_key(&crate::client::transcript_fold::ItemKey::Seq(2));
assert_eq!(
screen
.selection
@@ -886,7 +892,7 @@ mod apply_tests {
// What a reader does next: press the row that survived. `begin`
// resolves every registered handle, so a stale one panics here.
let surviving_key = row::row_key(&client_core::transcript_fold::ItemKey::Seq(1));
let surviving_key = row::row_key(&crate::client::transcript_fold::ItemKey::Seq(1));
screen.selection.borrow_mut().begin(
&mut rsc,
(surviving_key, 0),
@@ -924,7 +930,8 @@ mod apply_tests {
rsc: &mut TestRsc,
items: &[TranscriptItem],
) -> (TranscriptScreen, StrongWidget, UiRenderState) {
let (screen, tree) = build_tree(rsc, client_core::transcript_fold::group_tool_runs(items));
let (screen, tree) =
build_tree(rsc, crate::client::transcript_fold::group_tool_runs(items));
let mut render = UiRenderState::new();
render.resize((1080.0, 20000.0));
render.update(&tree, rsc);
@@ -948,7 +955,7 @@ mod apply_tests {
let items = run_of(3, output, true);
let (screen, tree) = build_tree(
&mut rsc,
client_core::transcript_fold::group_tool_runs(&items),
crate::client::transcript_fold::group_tool_runs(&items),
);
let mut render = UiRenderState::new();
render.resize((1080.0, 20000.0));
@@ -1017,7 +1024,7 @@ mod apply_tests {
];
let (_screen, tree) = build_tree(
&mut rsc,
client_core::transcript_fold::group_tool_runs(&items),
crate::client::transcript_fold::group_tool_runs(&items),
);
let mut render = UiRenderState::new();
render.resize((1080.0, 20000.0));
@@ -1038,8 +1045,8 @@ mod apply_tests {
#[test]
fn a_long_message_is_drawn_only_as_far_as_the_cap() {
let paragraphs = |n: usize| "a paragraph of a reply\n\n".repeat(n);
let capped = shapes_for_message(&paragraphs(client_core::text_cap::MESSAGE_LINES * 4));
let bigger = shapes_for_message(&paragraphs(client_core::text_cap::MESSAGE_LINES * 40));
let capped = shapes_for_message(&paragraphs(crate::client::text_cap::MESSAGE_LINES * 4));
let bigger = shapes_for_message(&paragraphs(crate::client::text_cap::MESSAGE_LINES * 40));
assert!(
capped > 0,
"the screen shaped nothing, so this compares zeroes"
@@ -1112,7 +1119,7 @@ mod apply_tests {
let (screen, tree) = build_tree(
&mut rsc,
client_core::transcript_fold::group_tool_runs(&before),
crate::client::transcript_fold::group_tool_runs(&before),
);
let mut render = UiRenderState::new();
render.resize((1080.0, 20000.0));
@@ -1180,7 +1187,7 @@ mod apply_tests {
let after = vec![user(1, "stable"), user(2, "not a tool call at all")];
let (screen, _tree) = build_tree(
&mut rsc,
client_core::transcript_fold::group_tool_runs(&before),
crate::client::transcript_fold::group_tool_runs(&before),
);
screen.apply(&mut rsc, &before, &after);
assert_eq!(
@@ -1,7 +1,7 @@
//! One `iris::widget::list::LazyItem` per folded transcript row
//! (`client_core::transcript_fold::TranscriptRow`). A row is a **column of
//! (`crate::client::transcript_fold::TranscriptRow`). A row is a **column of
//! one `TextEdit` per top-level markdown block** (paragraph, heading,
//! fence, list, table -- `client_core::markdown_blocks`), each rendered
//! fence, list, table -- `crate::client::markdown_blocks`), each rendered
//! with `markdown`'s inline spans, so that RUST.md's "hard to get back"
//! behaviour 2 (rich inline text) still holds within a block and
//! behaviour 1 (selection) runs across blocks and rows alike through
@@ -15,7 +15,7 @@
//! were and why this one.
//!
//! A `TranscriptRow::Tools` (a run of adjacent tool calls, grouped by
//! `client_core::transcript_fold::group_tool_runs`) is the row that proves
//! `crate::client::transcript_fold::group_tool_runs`) is the row that proves
//! behaviour 3's "hold the edge nearest the tap" on expand: tapping its
//! header calls `LazySpan::note_tap` at the row's own on-screen position
//! (read back from `LazySpan::extent`, since the tap event only knows its
@@ -23,13 +23,13 @@
//! collapsed summary and the full detail -- the same two-step contract
//! `lazy_span.rs`'s module doc describes for `AGENTS.md`'s `holdTopEdge`.
use crate::markdown::{BlockFrame, Link, frame_of, render_block};
use crate::selection::{SelKey, Selection};
use crate::tap::{hold_edge, on_tap};
use crate::tool::ToolRow;
use client_core::markdown_blocks::{Block, BlockKind, common_prefix, split_blocks};
use client_core::text_cap::{MESSAGE_BYTES, MESSAGE_LINES, cut, show_all_label};
use client_core::transcript_fold::{QuestionCard, TranscriptItem, TranscriptRow as FoldedRow};
use crate::client::markdown_blocks::{Block, BlockKind, common_prefix, split_blocks};
use crate::client::text_cap::{MESSAGE_BYTES, MESSAGE_LINES, cut, show_all_label};
use crate::client::transcript_fold::{QuestionCard, TranscriptItem, TranscriptRow as FoldedRow};
use crate::ui::markdown::{BlockFrame, Link, frame_of, render_block};
use crate::ui::selection::{SelKey, Selection};
use crate::ui::tap::{hold_edge, on_tap};
use crate::ui::tool::ToolRow;
use iris::prelude::*;
use std::{cell::RefCell, rc::Rc};
@@ -53,8 +53,8 @@ pub const BASE_SIZE: f32 = 16.0;
/// tool-call rows sharing a list slot, not data loss), and the high bit is
/// forced on so a hashed key can never collide with a real sequence number
/// (this build never produces 2^63 events).
pub fn row_key(key: &client_core::transcript_fold::ItemKey) -> RowKey {
use client_core::transcript_fold::ItemKey;
pub fn row_key(key: &crate::client::transcript_fold::ItemKey) -> RowKey {
use crate::client::transcript_fold::ItemKey;
use std::hash::{Hash, Hasher};
match key {
ItemKey::Seq(seq) => *seq,
@@ -132,7 +132,7 @@ fn tool_call_markdown(tool: &str, input: &str, output: &str) -> String {
pub struct RowBlocks {
/// What each field was built from, in order -- compared against a
/// fresh split to decide what may be kept. See
/// `client_core::markdown_blocks`' module doc for why this is a
/// `crate::client::markdown_blocks`' module doc for why this is a
/// comparison and not an assumption.
blocks: Vec<Block>,
fields: Vec<WeakWidget<TextEdit>>,
@@ -291,8 +291,8 @@ where
})
.size(BASE_SIZE)
.color(match frame {
BlockFrame::Quote => crate::markdown::QUOTE_TEXT_COLOR,
_ => crate::markdown::TEXT_COLOR,
BlockFrame::Quote => crate::ui::markdown::QUOTE_TEXT_COLOR,
_ => crate::ui::markdown::TEXT_COLOR,
})
.add(rsc);
selection.borrow_mut().register(key, field);
@@ -378,7 +378,7 @@ where
left: dp(QUOTE_BAR_DP + FRAME_PAD_DP),
..Padding::ZERO
})
.background(rect(crate::markdown::QUOTE_BAR_COLOR).width(dp(QUOTE_BAR_DP)))
.background(rect(crate::ui::markdown::QUOTE_BAR_COLOR).width(dp(QUOTE_BAR_DP)))
.width(rest(1))
.add_strong(rsc)
.any(),
@@ -392,7 +392,7 @@ where
/// One widget per block rather than one per message is what makes a
/// streamed delta cost the last block instead of the whole reply -- see
/// [`RowBlocks::apply_delta`] for the other half, and
/// `client_core::markdown_blocks` for the split. Selection still runs
/// `crate::client::markdown_blocks` for the split. Selection still runs
/// across the whole transcript; the unit it steps in is a block now rather
/// than a row (`selection::SelKey`).
///
@@ -700,7 +700,7 @@ where
if let Some(calls) = calls {
let key = row_key(&calls[0].key());
let (widget, tools) =
crate::tool::build_tool_row(rsc, list, selection, key, calls.to_vec(), working);
crate::ui::tool::build_tool_row(rsc, list, selection, key, calls.to_vec(), working);
return (key, widget, Some(TailRow::Tools(tools)));
}
let FoldedRow::Single(item) = row else {
@@ -35,7 +35,7 @@ use std::{collections::BTreeMap, time::Instant};
/// 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
/// block since 2026-09-06 (`client_core::markdown_blocks`, and
/// block since 2026-09-06 (`crate::client::markdown_blocks`, and
/// docs/DECISIONS.md for why), so the block, not the row, is the unit --
/// `(row, block)` compares lexicographically, which is reading order for
/// both levels, so every range query below is unchanged.
@@ -2,13 +2,13 @@
//! while what they tapped changes height.
//!
//! Both halves are shared by everything in the transcript that opens: a
//! tool card and its group ([`crate::tool`]), and a message's "Show all"
//! ([`crate::row`]). They are here rather than in either of those because
//! tool card and its group ([`crate::ui::tool`]), and a message's "Show all"
//! ([`crate::ui::row`]). They are here rather than in either of those because
//! there is one right answer to "was that a tap?" on this screen, and two
//! copies of it would eventually disagree -- which on a scrolling screen
//! means one of them firing at the end of a pan.
use crate::selection::Selection;
use crate::ui::selection::Selection;
use iris::prelude::*;
use std::{cell::RefCell, rc::Rc};
@@ -2,7 +2,7 @@
//! `ToolRows.kt`/`ToolInput.kt` (RUST.md's P1b).
//!
//! One card per call. Closed, it is a single line: the tool's name and
//! what the call is for ([`client_core::tool_summary::parse_tool_input`]'s
//! what the call is for ([`crate::client::tool_summary::parse_tool_input`]'s
//! `title`). The command itself is not on it, because a wrapped command
//! turns one row into four and a run of them into a wall. Open, it shows
//! the description, the input and the output.
@@ -16,7 +16,7 @@
//! `a_delta_into_a_long_reply_...` counts it for a streamed delta.
//!
//! **Two or more adjacent calls are one group** -- decided in
//! `client_core::transcript_fold::group_tool_runs`/`adopt_run` and never
//! `crate::client::transcript_fold::group_tool_runs`/`adopt_run` and never
//! re-derived here. A group is a header, a column of cards on its own
//! surface, and a bar at its foot: it closes from either end, because a
//! long group's header scrolls off while its last call is still on screen,
@@ -32,12 +32,12 @@
//! rule is written once, in the gesture machine, and this file only reads
//! its answer.
use crate::markdown::{TEXT_COLOR, VERBATIM_BACKGROUND, highlight_into};
use crate::selection::Selection;
use crate::tap::{hold_edge, on_tap};
use client_core::text_cap::{VERBATIM_BYTES, VERBATIM_LINES, cut, show_all_label};
use client_core::tool_summary::{ToolInput, parse_tool_input};
use client_core::transcript_fold::{ToolState, TranscriptItem};
use crate::client::text_cap::{VERBATIM_BYTES, VERBATIM_LINES, cut, show_all_label};
use crate::client::tool_summary::{ToolInput, parse_tool_input};
use crate::client::transcript_fold::{ToolState, TranscriptItem};
use crate::ui::markdown::{TEXT_COLOR, VERBATIM_BACKGROUND, highlight_into};
use crate::ui::selection::Selection;
use crate::ui::tap::{hold_edge, on_tap};
use iris::prelude::*;
use std::{cell::Cell, cell::RefCell, collections::HashMap, rc::Rc};
@@ -163,7 +163,7 @@ struct Shared {
/// One transcript row's worth of tool calls, kept by the caller for the
/// row a result can still land in -- the tool-call counterpart of
/// [`crate::row::RowBlocks`], and the reason a `ToolEnd` costs one card
/// [`crate::ui::row::RowBlocks`], and the reason a `ToolEnd` costs one card
/// rather than a row.
pub struct ToolRow {
shared: Rc<Shared>,
@@ -11,17 +11,17 @@
//! down. Compose does not do that: a down while `isScrollInProgress`
//! starts the drag immediately (`scrollable`'s `startDragImmediately`).
use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
use iris::harness::{Harness, TouchAction, TouchScript};
use iris::prelude::*;
use iris::sense::DRAG_SLOP;
use transcript_fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
/// The screen open on the fixture, framed twice -- once to draw, once for
/// `LazySpan::repair_anchor` to resolve the opening `snap_end` into a real
/// anchor, which is what every assertion about scroll position reads.
fn opened() -> (Harness, transcript_ui::TranscriptScreen) {
fn opened() -> (Harness, ai_app::ui::TranscriptScreen) {
let mut h = Harness::new(phone_size(), PHONE_SCALE);
let opened = transcript_fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
h.frame(0);
h.frame(PHONE_FRAME_MS);
(h, opened.screen)
@@ -31,7 +31,7 @@ fn opened() -> (Harness, transcript_ui::TranscriptScreen) {
/// under the middle of the viewport. `LazySpan` has no travel accessor and
/// this needs none -- a row's own extent moves exactly as far as the
/// content does, and the row is picked once so the two readings compare.
fn tracked_row(h: &mut Harness, screen: &transcript_ui::TranscriptScreen) -> (RowKey, f32) {
fn tracked_row(h: &mut Harness, screen: &ai_app::ui::TranscriptScreen) -> (RowKey, f32) {
let middle = phone_size().y / 2.0;
let list = (screen.list)(&mut h.rsc);
let key = list.key_at(middle).expect("a row under the viewport");
@@ -39,7 +39,7 @@ fn tracked_row(h: &mut Harness, screen: &transcript_ui::TranscriptScreen) -> (Ro
(key, top)
}
fn row_top(h: &mut Harness, screen: &transcript_ui::TranscriptScreen, key: RowKey) -> f32 {
fn row_top(h: &mut Harness, screen: &ai_app::ui::TranscriptScreen, key: RowKey) -> f32 {
(screen.list)(&mut h.rsc)
.extent(key)
.expect("the tracked row is still loaded")
@@ -59,7 +59,7 @@ const CATCH_X: f32 = 540.0;
/// delta. Returns the release time.
fn drag_from(
h: &mut Harness,
screen: &transcript_ui::TranscriptScreen,
screen: &ai_app::ui::TranscriptScreen,
key: RowKey,
y0: f32,
t0: u64,
@@ -8,9 +8,9 @@
//! handle to it from the outside, and a coordinate would only prove that
//! *something* moved.
use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
use iris::harness::{Harness, TouchAction};
use iris::prelude::*;
use transcript_fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
/// The horizontal scroll area drawn inside `top..bottom`, with the box
/// it was drawn at -- a fence is the only thing in a transcript that pans
@@ -58,10 +58,10 @@ fn amt(h: &Harness, id: WidgetId) -> f32 {
#[test]
fn a_flick_across_a_code_fence_keeps_moving_after_the_finger_leaves() {
use client_core::transcript_fold::{TranscriptItem, TranscriptRow};
use ai_app::client::transcript_fold::{TranscriptItem, TranscriptRow};
let mut h = Harness::new(phone_size(), PHONE_SCALE);
let opened = transcript_fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
let screen = opened.screen;
h.frame(0);
h.frame(PHONE_FRAME_MS);
@@ -78,7 +78,7 @@ fn a_flick_across_a_code_fence_keeps_moving_after_the_finger_leaves() {
h.frame(100);
h.frame(108);
let key = transcript_ui::row::row_key(&fence.key());
let key = ai_app::ui::row::row_key(&fence.key());
let (top, bottom) = (screen.list)(&mut h.rsc)
.extent(key)
.expect("the fence row is on screen");
@@ -147,10 +147,10 @@ fn a_flick_across_a_code_fence_keeps_moving_after_the_finger_leaves() {
/// list, while the fence carries on coasting untouched.
#[test]
fn a_drag_away_from_a_coasting_fence_scrolls_the_list_and_leaves_it_coasting() {
use client_core::transcript_fold::{TranscriptItem, TranscriptRow};
use ai_app::client::transcript_fold::{TranscriptItem, TranscriptRow};
let mut h = Harness::new(phone_size(), PHONE_SCALE);
let opened = transcript_fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
let screen = opened.screen;
h.frame(0);
h.frame(PHONE_FRAME_MS);
@@ -171,7 +171,7 @@ fn a_drag_away_from_a_coasting_fence_scrolls_the_list_and_leaves_it_coasting() {
h.frame(100);
h.frame(108);
let key = transcript_ui::row::row_key(&fence.key());
let key = ai_app::ui::row::row_key(&fence.key());
let (top, bottom) = (screen.list)(&mut h.rsc)
.extent(key)
.expect("the fence row is on screen");
@@ -8,13 +8,13 @@
//! or the next press anywhere on screen is measured from the origin the
//! abandoned one left behind.
use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
use iris::harness::{Harness, TouchAction, TouchScript};
use iris::prelude::*;
use transcript_fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
fn opened() -> (Harness, transcript_ui::TranscriptScreen) {
fn opened() -> (Harness, ai_app::ui::TranscriptScreen) {
let mut h = Harness::new(phone_size(), PHONE_SCALE);
let opened = transcript_fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
h.frame(0);
h.frame(PHONE_FRAME_MS);
(h, opened.screen)
@@ -30,7 +30,7 @@ fn script(name: &str, text: &str) -> TouchScript {
/// different row without the content moving at all -- so a test asserting
/// "nothing moved" reads a row's own extent, the way `catch_a_fling.rs`
/// does.
fn tracked_row(h: &mut Harness, screen: &transcript_ui::TranscriptScreen) -> (RowKey, f32) {
fn tracked_row(h: &mut Harness, screen: &ai_app::ui::TranscriptScreen) -> (RowKey, f32) {
let middle = phone_size().y / 2.0;
let list = (screen.list)(&mut h.rsc);
let key = list.key_at(middle).expect("a row under the viewport");
@@ -38,7 +38,7 @@ fn tracked_row(h: &mut Harness, screen: &transcript_ui::TranscriptScreen) -> (Ro
(key, top)
}
fn row_top(h: &mut Harness, screen: &transcript_ui::TranscriptScreen, key: RowKey) -> f32 {
fn row_top(h: &mut Harness, screen: &ai_app::ui::TranscriptScreen, key: RowKey) -> f32 {
(screen.list)(&mut h.rsc)
.extent(key)
.expect("the tracked row is still loaded")
@@ -123,7 +123,7 @@ fn a_press_ended_by_a_cancel_leaves_no_origin_for_the_next_one() {
/// pointer capture the moment it commits. Everything else that was handed
/// a frame of that press is told so with `CursorSense::Cancel` -- and the
/// widget the press actually landed on is the fence's own text block,
/// which drives `transcript_ui::Selection`'s shared `DragGesture`. A
/// which drives `ai_app::ui::Selection`'s shared `DragGesture`. A
/// block that does not register `Cancel` never hears it, so the gesture
/// stays open with the fence's touch-down as its origin and the next
/// press anywhere is measured from there.
@@ -132,7 +132,7 @@ fn a_press_ended_by_a_cancel_leaves_no_origin_for_the_next_one() {
/// test knows exactly which row it is pressing and where.
#[test]
fn panning_a_code_fence_then_tapping_elsewhere_moves_nothing() {
use client_core::transcript_fold::{TranscriptItem, TranscriptRow};
use ai_app::client::transcript_fold::{TranscriptItem, TranscriptRow};
let (mut h, screen) = opened();
let fence = TranscriptRow::Single(TranscriptItem::AssistantMsg {
@@ -161,7 +161,7 @@ fn panning_a_code_fence_then_tapping_elsewhere_moves_nothing() {
// Press in the middle of the fence's own row, so the gesture starts on
// the text block inside the scroll area rather than in a gap.
let key = transcript_ui::row::row_key(&fence.key());
let key = ai_app::ui::row::row_key(&fence.key());
let (top, bottom) = (screen.list)(&mut h.rsc)
.extent(key)
.expect("the fence row is on screen");
@@ -182,7 +182,7 @@ fn panning_a_code_fence_then_tapping_elsewhere_moves_nothing() {
// A tap on the paragraph, a long way down the screen from where that
// pan started.
let para_key = transcript_ui::row::row_key(&para.key());
let para_key = ai_app::ui::row::row_key(&para.key());
let (ptop, pbottom) = (screen.list)(&mut h.rsc)
.extent(para_key)
.expect("the paragraph row is on screen");
@@ -27,8 +27,8 @@
use std::process::{Command, Stdio};
use std::sync::{Mutex, OnceLock};
use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
use iris::harness::{Harness, TouchScript};
use transcript_fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
/// Records every line's level and formatted message -- enough to answer
/// both "is the ring quiet" (no line at `Debug` or below) and "what did
@@ -72,9 +72,9 @@ fn drain(logger: &CaptureLogger) -> Vec<(log::Level, String)> {
std::mem::take(&mut *logger.lines.lock().unwrap())
}
fn opened() -> (Harness, transcript_ui::TranscriptScreen) {
fn opened() -> (Harness, ai_app::ui::TranscriptScreen) {
let mut h = Harness::new(phone_size(), PHONE_SCALE);
let opened = transcript_fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
h.frame(0);
h.frame(PHONE_FRAME_MS);
(h, opened.screen)
@@ -149,7 +149,10 @@ fn tracing_is_silent_off_and_round_trips_the_flick_on() {
// is prefix-agnostic (it `search`es for the marker), so handing it
// the bare message is the same as handing it a real ring line.
let report = input_lines.join("\n");
let script_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../benches/report_to_touch.py");
let script_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../iris/benches/report_to_touch.py"
);
let mut child = Command::new("python3")
.arg(script_path)
.stdin(Stdio::piped())
@@ -8,16 +8,16 @@
//! many evenly-spaced events; a finger at 120Hz is five samples in
//! 20ms).
use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
use iris::harness::{Harness, TouchScript};
use iris::prelude::*;
use transcript_fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
/// The screen open on the fixture, framed twice: once to draw, once for
/// `LazySpan::repair_anchor` to resolve the opening `snap_end` into a real
/// anchor, which is what every assertion about scroll position reads.
fn opened() -> (Harness, transcript_ui::TranscriptScreen) {
fn opened() -> (Harness, ai_app::ui::TranscriptScreen) {
let mut h = Harness::new(phone_size(), PHONE_SCALE);
let opened = transcript_fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
h.frame(0);
h.frame(PHONE_FRAME_MS);
(h, opened.screen)
@@ -27,7 +27,7 @@ fn script(name: &str, text: &str) -> TouchScript {
TouchScript::parse(text).unwrap_or_else(|e| panic!("{name}: {e}"))
}
fn offset(h: &mut Harness, screen: &transcript_ui::TranscriptScreen) -> String {
fn offset(h: &mut Harness, screen: &ai_app::ui::TranscriptScreen) -> String {
(screen.list)(&mut h.rsc).anchor_position_display()
}
@@ -9,9 +9,9 @@
//! it is inside the list's own box, and nothing outside that box reaches
//! the screen.
use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
use iris::harness::Harness;
use iris::prelude::*;
use transcript_fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
/// A header band above the transcript, as `bench_client.rs` puts one --
/// the surface the rows were drawing over on the phone. Its exact height
@@ -21,9 +21,9 @@ use transcript_fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size};
const HEADER_H: f32 = 300.0;
const HEADER: UiColor = UiColor::new(28, 28, 34, 255);
fn opened() -> (Harness, transcript_ui::TranscriptScreen) {
fn opened() -> (Harness, ai_app::ui::TranscriptScreen) {
let mut h = Harness::new(phone_size(), PHONE_SCALE);
let (opened, tree) = transcript_fixture::build_screen(&mut h.rsc).expect("the fixture folds");
let (opened, tree) = ai_app::ui::fixture::build_screen(&mut h.rsc).expect("the fixture folds");
let content = WidgetPtr::new().add(&mut h.rsc);
content(&mut h.rsc).set(tree);
let root = (rect(HEADER).height(abs(HEADER_H)), content.height(rest(1)))
@@ -37,7 +37,7 @@ fn opened() -> (Harness, transcript_ui::TranscriptScreen) {
}
/// The list's own on-screen box, in window pixels.
fn list_box(h: &Harness, screen: &transcript_ui::TranscriptScreen) -> PixelRegion {
fn list_box(h: &Harness, screen: &ai_app::ui::TranscriptScreen) -> PixelRegion {
h.render
.window_region(&screen.list.id(), &h.rsc)
.expect("the list is on screen")
@@ -47,7 +47,7 @@ fn list_box(h: &Harness, screen: &transcript_ui::TranscriptScreen) -> PixelRegio
/// topmost first. A `LazySpan`'s direct children are exactly its rows, and
/// `draw_inner`'s old-children diffing means a row it did not place this
/// frame is not among them.
fn drawn_rows(h: &Harness, screen: &transcript_ui::TranscriptScreen) -> Vec<(f32, f32)> {
fn drawn_rows(h: &Harness, screen: &ai_app::ui::TranscriptScreen) -> Vec<(f32, f32)> {
let mut rows: Vec<(f32, f32)> = h
.render
.active
@@ -69,7 +69,7 @@ fn drawn_rows(h: &Harness, screen: &transcript_ui::TranscriptScreen) -> Vec<(f32
/// position lives in the `LazySpan`'s own `ScrollController`.
/// It used to be the opposite here, because a `LazySpan`'s anchor offset
/// ran the other way.
fn scrolled(h: &mut Harness, screen: &transcript_ui::TranscriptScreen, amount: f32, t: u64) -> u64 {
fn scrolled(h: &mut Harness, screen: &ai_app::ui::TranscriptScreen, amount: f32, t: u64) -> u64 {
(screen.list)(&mut h.rsc).scroll(amount);
h.frame(t);
t + PHONE_FRAME_MS
File renamed without changes.
@@ -280,7 +280,7 @@ fun fetchSession(settings: ServerSettings, sessionId: String): SessionSummary =
* One row of `GET /sessions/{id}/subagents`, oldest first.
*
* A subagent is a second transcript owned by a session -- no process, no controls of its own -- so
* this carries only what a card needs to draw and to open it; see SUBAGENTS.md. [status] is
* this carries only what a card needs to draw and to open it; see docs/SUBAGENTS.md. [status] is
* "running", "exited" or "unknown": a subagent whose session is not itself running cannot be
* running, and the list says so rather than reporting a state that cannot hold.
*/
@@ -49,7 +49,7 @@ private sealed class Screen {
/**
* One subagent's own transcript, read-only. See [SessionScreen]'s `subagent` parameter and
* SUBAGENTS.md's "Phone". Closing it returns to [Main] under it, not to [Session]: a subagent
* docs/SUBAGENTS.md's "Phone". Closing it returns to [Main] under it, not to [Session]: a subagent
* is opened from the session list's card rather than from inside the session it belongs to.
*/
data class SubagentTarget(val summary: SessionSummary, val subagent: SubagentSummary)
@@ -478,7 +478,7 @@ private fun SubagentCard(subagent: SubagentSummary, onClick: () -> Unit) {
}
/**
* The subcard's word for a subagent's status -- see SUBAGENTS.md's "Wire shape". Its own function
* The subcard's word for a subagent's status -- see docs/SUBAGENTS.md's "Wire shape". Its own function
* rather than a branch inside [StatusText], because a subagent's three states are not that
* composable's five: "exited" reads as "finished" here, since its process was always its parent's
* and never something of its own to have merely stopped.
@@ -5,7 +5,7 @@ package com.example.aiapp
*
* The single mechanism [fetchTranscript], [EventStream], [TranscriptSource] and
* [TranscriptCache.session] all take, rather than each growing its own branch between a session and
* a subagent -- see SUBAGENTS.md's "Phone" and "Wire shape". A caller that has only a session id
* a subagent -- see docs/SUBAGENTS.md's "Phone" and "Wire shape". A caller that has only a session id
* builds one with the one-argument constructor; a subagent's screen supplies both ids.
*/
data class TranscriptAddress(val sessionId: String, val subagentId: String? = null) {
+5 -5
View File
@@ -1,11 +1,11 @@
plugins { alias(libs.plugins.androidApplication) }
// E3 (RUST.md): the Kotlin/Java shell being replaced by a thin JNI bridge
// into Rust (`../../android-shell`). Deliberately its own module rather
// into Rust (`../../app-rust`, the `shell` feature). Deliberately its own module rather
// than a rewrite of `:androidApp` in place -- that module is ~13,000 lines
// of working Compose UI this experiment does not touch, and the two can be
// installed side by side on the same development device (see
// `settings.SCHEME`'s doc in `android-shell` for why the deep-link scheme
// `settings.SCHEME`'s doc in `app-rust/src/shell` for why the deep-link scheme
// and Keystore alias are not the production app's). No Compose plugin, no
// Kotlin source of its own: `MainActivity`/`NotificationService` are plain
// Java, and the CA constant below is generated as Java too.
@@ -13,7 +13,7 @@ plugins { alias(libs.plugins.androidApplication) }
// The CA this build pins is baked in the same way `androidApp`'s does --
// see that module's `build.gradle.kts` comment for the reasoning (the
// trust boundary follows the machine that builds, never a pasted copy).
// `PinnedCa.java`'s package must match `android-shell`'s
// `PinnedCa.java`'s package must match `app-rust/src/shell`'s
// `settings::load_pinned_ca` lookup (`com/example/aiapp/shell/PinnedCa`).
val pinnedCaPath: String =
System.getenv("AI_APP_CA")
@@ -150,12 +150,12 @@ androidComponents {
dependencies {
// The Keystore-sealed enrollment (ServerStore/ServerSettings) --
// android-shell's settings.rs calls into this Kotlin class directly
// the shell bridge's settings.rs calls into this Kotlin class directly
// over JNI rather than re-sealing the token in Rust; see that file's
// module doc.
implementation(project(":link"))
// NotificationCompat/NotificationManagerCompat/NotificationChannelCompat/
// ServiceCompat -- android-shell's notify.rs calls these classes over
// ServiceCompat -- the shell bridge's notify.rs calls these classes over
// JNI so the pre-26 fallback behaviour (no channels) lives once, in
// the library that already has it, rather than being re-derived as a
// set of Build.VERSION.SDK_INT branches in Rust.
@@ -17,7 +17,7 @@ import android.widget.Toast;
*/
public class MainActivity extends Activity {
static {
System.loadLibrary("android_shell");
System.loadLibrary("ai_app");
}
@Override
@@ -14,7 +14,7 @@ import android.os.IBinder;
*/
public class NotificationService extends Service {
static {
System.loadLibrary("android_shell");
System.loadLibrary("ai_app");
}
@Override
-983
View File
@@ -1,983 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "base64"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cc"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "client-core"
version = "0.1.0"
dependencies = [
"base64",
"event-model",
"log",
"pulldown-cmark",
"serde",
"serde_json",
"tempfile",
"ureq",
]
[[package]]
name = "cookie"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "cookie_store"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
dependencies = [
"cookie",
"document-features",
"idna",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"time",
"url",
]
[[package]]
name = "crc32fast"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"
dependencies = [
"cfg-if",
]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
[[package]]
name = "displaydoc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.5",
]
[[package]]
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
"litrs",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "event-model"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "find-msvc-tools"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
[[package]]
name = "flate2"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
dependencies = [
"crc32fast",
"miniz_oxide",
"zlib-rs",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "getopts"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "http"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "icu_collections"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
dependencies = [
"displaydoc",
"potential_utf",
"utf8_iter",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
dependencies = [
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
[[package]]
name = "icu_properties"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
[[package]]
name = "icu_provider"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
dependencies = [
"displaydoc",
"icu_locale_core",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "log"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "miniz_oxide"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "potential_utf"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pulldown-cmark"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
dependencies = [
"bitflags",
"getopts",
"memchr",
"pulldown-cmark-escape",
"unicase",
]
[[package]]
name = "pulldown-cmark-escape"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.5",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "simd-adler32"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]]
name = "smallvec"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "time"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "3.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af5546be8f5378d5414f83733f5c9a2526f4645829edbc1c41790aeef1b38e8b"
dependencies = [
"base64",
"cookie_store",
"flate2",
"log",
"percent-encoding",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"ureq-proto",
"utf8-zero",
"webpki-roots",
]
[[package]]
name = "ureq-proto"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabc3e92916c89c95b20eef7b06b00b066bc217ef9ea3a4ac9bf1a7e35261e10"
dependencies = [
"base64",
"http",
"httparse",
"log",
]
[[package]]
name = "url"
version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "utf8-zero"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "webpki-roots"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "writeable"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "yoke"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
dependencies = [
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"synstructure",
]
[[package]]
name = "zerofrom"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zerotrie"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.5",
]
[[package]]
name = "zlib-rs"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
-52
View File
@@ -1,52 +0,0 @@
[package]
name = "client-core"
version = "0.1.0"
edition = "2024"
# The app's pure logic, held once instead of twice: the event model (shared
# with `server/` via `event-model`), the REST + SSE clients for its HTTP
# surface (see `server/src/routes.rs`'s module doc for the table), the
# transcript fold and cache, the markdown block model, the syntax
# highlighter and the ANSI parser. See `docs/CLIENT_CORE.md` for
# what this holds today, what it does not yet, and how it corresponds to
# the Kotlin it replaces.
#
# No UI framework dependency of any kind -- this crate is meant to outlive
# whichever one the app ends up drawing with (see RUST.md).
[dependencies]
event-model = { path = "../event-model" }
serde = { version = "1", features = ["derive"] }
# "raw_value" is `fetch_transcript_lines`'s reason -- it needs the exact
# bytes the server sent, not this crate's own re-serialization of a parsed
# `Value`, so a cached line and a live SSE frame for the same event agree
# byte-for-byte (see that method's doc). "float_roundtrip" is why they
# agree on a `ts` at all -- see server/Cargo.toml's identical comment.
serde_json = { version = "1", features = ["float_roundtrip", "raw_value"] }
# The blocking HTTP client for the REST calls and the long-lived SSE GETs.
# `server/` already depends on ureq for its own outbound HTTPS (the usage
# poll in usage.rs) and it is rustls-backed like the rest of this project's
# TLS, so this reuses that choice rather than pulling in reqwest's async
# stack -- a client that runs one blocking request at a time, the way
# Api.kt's `HttpURLConnection` calls and Sse.kt's blocking read loop do, has
# no need of an async runtime, and RUST.md's brief for this port is
# "lightweight" throughout.
ureq = { version = "3", features = ["json"] }
# The markdown block split (`markdown_blocks`), which has to agree with the
# renderer in `iris/transcript-ui` about where a block begins -- so it is
# the same parser at the same version, rather than a hand-written splitter
# that would drift from it.
pulldown-cmark = "0.13.4"
# The enrollment link's `ca` parameter is base64url of the CA's DER
# (`config::parse_link`). Same version `wg-app-link` already pins for the
# minting half, so a workspace that has both resolves one copy.
base64 = "0.23"
# The logging facade only -- `log_ring` implements a `log::Log` backend and
# wraps whichever real one the platform installed (`android_logger` on the
# phone, `env_logger` on the desktop), which is why neither of those is a
# dependency here. See `log_ring`'s module doc.
log = { version = "0.4.34", features = ["std"] }
[dev-dependencies]
tempfile = "3"
+29 -24
View File
@@ -1,21 +1,26 @@
# client-core
# `app-rust`'s `client` module
`client-core/` is the app's pure logic held once instead of twice, per
RUST.md's recommendation item 1. It is a plain Rust library crate with no UI
framework dependency of any kind, so it can outlive whichever one the app
ends up drawing with (Masonry, iris, or something else -- see RUST.md).
`event-model/` is its sibling: the wire shape both this crate and `server/`
share, extracted from `server/src/session/driver.rs` and
`session/transcript.rs` on 2026-09-04.
**Renamed 2026-09-08.** This was the `client-core` crate; it is now
`app-rust/src/client/`, a module of the one app crate rather than a crate
of its own (docs/RUST.md's "One app crate"). Nothing about what it *is*
changed: it is still the app's pure logic held once instead of twice, per
RUST.md's recommendation item 1, and still has **no UI framework
dependency of any kind** -- the `iris` dependency sits behind the `screens`
feature and nothing under `src/client` may reach it. That independence is
what lets it outlive whichever framework the app draws with, and it is now
an invariant of a module rather than of a manifest, so it is worth stating
plainly: a `use iris::` under `src/client/` is a defect.
Neither crate is wired into anything yet. `server/` re-exports `event-model`
so its own behaviour is unchanged (`./run-tests.sh` covers it); `client-core`
has no caller -- it exists for whichever experiment in RUST.md picks it up
next (a Masonry or iris transcript screen, most likely).
`event-model/` stayed a crate, and is the one split in the port that was
never optional: it is the wire shape both this app and `server/` depend on,
extracted from `server/src/session/driver.rs` and `session/transcript.rs`
on 2026-09-04, so a crate is what makes the two agree by construction.
Paths below are written as `src/client/…`, relative to `app-rust/`.
## What's here, and what Kotlin file it replaces
| `client-core/src/…` | Kotlin original | Status |
| `src/client/…` | Kotlin original | Status |
|------------------------------------------|-------------------------------------------|--------|
| `event-model/src/lib.rs` (shared crate) | `Events.kt` (the enum mirror) | Done |
| `ansi.rs` | `Ansi.kt` | Done, ported test-for-test |
@@ -118,7 +123,7 @@ has no `Unknown`/catch-all variant, unlike `Events.kt`'s hand-kept mirror.
A server newer than this build that adds an event type will fail to parse
that line rather than degrading to a placeholder row. Closing this means
deciding how `event_model` itself represents "a shape I don't recognise"
-- a shared-model decision affecting `server/` too, not a `client-core`-only
-- a shared-model decision affecting `server/` too, not a `client`-only
fix, so it is recorded here rather than silently worked around.
## `config.rs`: `EnrolledServer`
@@ -130,11 +135,11 @@ parses on the phone -- so any Rust client enrols from the same text a
phone would scan as a QR, with no second format invented for it (RUST.md's
E4, DECISIONS.md 2026-09-05). Deliberately does not decide where it is
persisted or under what file permissions -- a phone seals its token in the
Android Keystore, `iris/desktop-app/src/config.rs` writes it to
Android Keystore, `src/desktop/config.rs` writes it to
`$XDG_CONFIG_HOME/ai-app-desktop/enrollment.json` at 0600 -- since that is
caller-specific (the code rules' "ask for the least you need"). Its only
caller today is `desktop-app`; a future Android build of this crate would
be a second one, not a reason to move the type.
caller today is `src/desktop`; the Android entry point is a second one, not
a reason to move the type.
## What `transcript_source.rs` covers, and what it does not
@@ -184,7 +189,7 @@ thread. Both are wall-clock/thread-lifetime policy that belongs to
whichever runtime embeds this crate (iris's own timers, a Tokio task, a
Kotlin coroutine scope), not to this pure logic -- `follow` is the same
"write to the cache, then hand the frame to the caller" decorator
`iris/desktop-app/src/app.rs` and `iris/android-app/src/transcript_client.rs`
`src/desktop/app.rs` and `src/android/transcript_client.rs`
already hand-wrote around `event_stream::follow_session_events` before this
existed; the cache write moved into one shared place so a third caller
does not repeat it again by hand.
@@ -198,7 +203,7 @@ does not repeat it again by hand.
block instead of the message (docs/RUST.md's Task B). What it
deliberately does **not** build is the tree below that: nested list
items, table cells, inline spans. Inline styling is still the renderer's
own job per block (`iris/transcript-ui/src/markdown.rs`), and nothing
own job per block (`src/ui/markdown.rs`), and nothing
has needed the rest yet. `CodeFence.kt`'s use of `org.intellij.markdown`
for a full CommonMark AST is Compose rendering plumbing, not something
to port as-is.
@@ -209,8 +214,8 @@ does not repeat it again by hand.
## Verifying
`./run-tests.sh` from the repo root now runs `event-model`, `client-core`
and `server` in that order (each `cargo test`, forwarding arguments the
same way it always has). From `client-core/` directly: `cargo test`
(119 tests), `cargo clippy --all-targets`, `cargo fmt` -- all clean as of
this writing (2026-09-06).
`./run-tests.sh` from the repo root runs `event-model`, `server` and
`app-rust` in that order (each `cargo test`, forwarding arguments the same
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
the crate and its headless harness suites.
+24
View File
@@ -5,6 +5,30 @@ 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.
## 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
+108 -1
View File
@@ -2089,7 +2089,7 @@ column above.
`Tasks::redraw_handle`'s design had anticipated) -- both in I5's own box,
both in `IRIS.md`.
- **Design choices for the two pieces before this are summarised in
`DECISIONS.md`** at the repo root, which is the file Iris reads for
`DECISIONS.md`** in `docs/`, which is the file Iris reads for
choices made without her.
- **E4 done, 2026-09-05.** `iris/desktop-app`: a winit window with a
session list beside `transcript-ui`'s screen (`build_tree`), against a
@@ -8394,3 +8394,110 @@ again `--features iris/force-gles` on virgl, identical output. Layer 3:
`build-apk.sh release` (arm64) builds, and the x86_64 debug bench ran a
full fling/stream/type/keyboard cycle on the emulator's GLES adapter with
no crash.
## One app crate, 2026-09-08 (the repository reorganised)
Iris, reading the tree: *"the organization of the rust rewrite is a mess
right now… there shouldn't be anything related to the app inside of iris.
Iris is supposed to be the UI framework alone."* Then, on the crate count:
*"I'm confused why the app only code needs more than one crate though."*
### What it was
Nine cargo workspaces, each with its own `Cargo.lock` and `target/`, and
the port's project code in five places — `iris/transcript-ui`,
`iris/transcript-fixture`, `iris/desktop-app`, `iris/android-app` (all
*inside* the framework), plus `client-core` and `android-shell` at the
root. Two root markdown files (`DECISIONS.md`, `SUBAGENTS.md`) sat outside
`docs/`.
### What it is
**One crate, `ai-app`, in `app-rust/`.** Modules, not crates:
| was | is |
|----------------------------------------|-----------------------------------|
| `client-core` | `src/client` |
| `iris/transcript-ui` | `src/ui` |
| `iris/transcript-fixture` | `src/ui/fixture.rs` + `tests/`, `touch/` |
| `iris/desktop-app` | `src/desktop` + `src/bin_desktop.rs` |
| `iris/android-app` | `src/android` + `android-project/` |
| `android-shell` | `src/shell` |
`iris/` now holds `core`, `macro`, the `iris` crate, `tabs-ui` and
`rig-input` — framework only, with no mention of a session, a transcript,
a setup or a server anywhere in it.
### Why one crate really is enough
Each split had a stated reason at the time; on inspection only two
survived, and one of those is not in `app-rust` at all.
- **`client-core` separate from the UI** was "pure logic with no framework
dependency". That property is worth keeping and does not need a crate:
`iris` is behind the `screens` feature and `src/client/` may not reach
it. An invariant on a module instead of on a manifest, stated in
docs/CLIENT_CORE.md.
- **`transcript-fixture` separate from `transcript-ui`** was so the
headless harness and a desktop window opened the same bytes. Both are
now the same crate, so it is `src/ui/fixture.rs` behind a `fixture`
feature (1.9 MB of `include_str!` must not reach a phone build) with the
six harness suites in `tests/`.
- **Two Android `.so` names**, `libmain.so` for the iris app and
`libandroid_shell.so` for the Kotlin shell's JNI bridge, looked like the
one hard constraint: a package produces exactly one library artifact.
It dissolves because **P2 already plans to merge those two Android apps
into one**. So both faces come out of one package as `libai_app.so`,
picked apart by features (`--no-default-features --features shell` keeps
wgpu, parley and iris out of the Compose app's APK), which is the
direction of travel rather than a workaround. `xtask apk` and
`app/shellApp`'s `System.loadLibrary` were updated to match.
- **A desktop binary and an Android cdylib in one package** is not a
problem: `iris` itself already target-gates winit against android-view
in one manifest, and the same table does it here. `build-apk.sh` passes
`--lib` so `cargo ndk` never tries to build the desktop binary.
- **`event-model` stays a crate**, and is the one split that was never
optional: `server/` depends on it too, so a crate is what makes the
backend and the app agree by construction. Iris chose to leave it at the
repo root rather than inside `app-rust/`, since it is the contract
between the two rather than app code.
So: three workspaces where there were nine — `event-model`, `server`,
`app-rust` — plus `iris` and `xtask`.
### Things that moved with it, worth knowing
- **The toolchain pin is per directory.** `app-rust/rust-toolchain.toml` is
a copy of `iris/`'s, because `client-core` used to build on stable and
now shares iris's dated nightly. Two consequences appeared immediately:
two `needless_range_loop` warnings in the markdown highlighter (fixed),
and four `AtomicBool::fetch_update` deprecations from inside `jni`
0.22's `native_method!` macro. The last are not ours to migrate — the
fix is a `jni` release — so `src/lib.rs` carries an `#[allow(deprecated)]`
scoped to `mod shell` with that reason written at it.
- **The Android release profile is `android-release`, not `release`.** The
aggressive settings `iris/android-app` had (`panic = "abort"`,
`opt-level = "s"`, fat LTO) would otherwise apply to the desktop build
too, which is a testing surface. `build-apk.sh` passes
`--profile android-release` / `--profile android-dev`.
- **`iris/run-headless.sh` grew `--dir DIR`**, defaulting to `iris/`. The
rig belongs to the framework; the examples it usually runs no longer do.
`replay-touch` is still built from `iris/`.
- **The log target changed** from `client_core` to `ai_app`
(`src/client/log_ring.rs`'s `is_own_target`).
- **Not renamed, deliberately:** the Android application id and Java
package are still `dev.iris.android.demo` and the label is still "iris
android-view demo", both now misleading. Changing them changes the app's
identity on Iris's phone (a side-by-side install rather than an upgrade)
and the `DevLogProvider` authority Dev Updater reads, so it is hers to
decide rather than a tidy-up to make quietly.
### Verified
`./run-tests.sh` (event-model, server, app-rust) and `cd iris && cargo
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
debug --abi x86_64` produces an installable APK; installed and launched on
this checkout's emulator, drawing through `Gl … virgl` as expected. The
phone-sized headless screenshot (`run-headless.sh phone --phone --dir
../app-rust --shot …`) renders the transcript unchanged.
+1 -1
View File
@@ -3,7 +3,7 @@
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
card and readable in the same transcript view the session has. Designed
2026-09-05; the decisions Bryan has not yet reviewed are in `DECISIONS.md`.
2026-09-05; the decisions Bryan has not yet reviewed are in `SUBAGENTS_DECISIONS.md`.
## What a subagent is here
File renamed without changes.
-390
View File
@@ -532,12 +532,6 @@ dependencies = [
"arrayvec",
]
[[package]]
name = "base64"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "bit-set"
version = "0.10.0"
@@ -717,19 +711,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "client-core"
version = "0.1.0"
dependencies = [
"base64",
"event-model",
"log",
"pulldown-cmark",
"serde",
"serde_json",
"ureq",
]
[[package]]
name = "clipboard-win"
version = "5.4.1"
@@ -775,35 +756,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "cookie"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "cookie_store"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
dependencies = [
"cookie",
"document-features",
"idna",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"time",
"url",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -890,25 +842,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
[[package]]
name = "desktop-app"
version = "0.1.0"
dependencies = [
"client-core",
"event-model",
"iris",
"serde_json",
"tempfile",
"transcript-ui",
"winit",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -1061,14 +994,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "event-model"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "exr"
version = "1.74.2"
@@ -1200,15 +1125,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-core"
version = "0.3.34"
@@ -1283,26 +1199,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "getopts"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
@@ -1447,22 +1343,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "icu_collections"
version = "2.3.0"
@@ -1591,27 +1471,6 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad"
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.10"
@@ -1729,12 +1588,6 @@ dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jni"
version = "0.21.1"
@@ -2143,12 +1996,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-derive"
version = "0.4.2"
@@ -2814,12 +2661,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -2872,25 +2713,6 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "pulldown-cmark"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
dependencies = [
"bitflags 2.13.1",
"getopts",
"memchr",
"pulldown-cmark-escape",
"unicase",
]
[[package]]
name = "pulldown-cmark-escape"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "pulp"
version = "0.22.3"
@@ -3162,20 +2984,6 @@ dependencies = [
"wayland-protocols-wlr",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "roxmltree"
version = "0.21.1"
@@ -3226,41 +3034,6 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "rustls"
version = "0.23.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.23"
@@ -3343,19 +3116,6 @@ dependencies = [
"syn 3.0.5",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_repr"
version = "0.1.21"
@@ -3512,12 +3272,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swash"
version = "0.2.10"
@@ -3645,36 +3399,6 @@ dependencies = [
"zune-jpeg",
]
[[package]]
name = "time"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tiny-skia"
version = "0.11.4"
@@ -3781,31 +3505,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "transcript-fixture"
version = "0.1.0"
dependencies = [
"client-core",
"event-model",
"iris",
"log",
"serde_json",
"tokio",
"transcript-ui",
"winit",
]
[[package]]
name = "transcript-ui"
version = "0.1.0"
dependencies = [
"client-core",
"event-model",
"iris",
"log",
"pulldown-cmark",
]
[[package]]
name = "tree_magic_mini"
version = "3.2.2"
@@ -3844,12 +3543,6 @@ dependencies = [
"keyboard-types",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -3868,62 +3561,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
dependencies = [
"base64",
"cookie_store",
"flate2",
"log",
"percent-encoding",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"ureq-proto",
"utf8-zero",
"webpki-roots",
]
[[package]]
name = "ureq-proto"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
dependencies = [
"base64",
"http",
"httparse",
"log",
]
[[package]]
name = "url"
version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "utf8-zero"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -3968,12 +3605,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.4+wasi-0.2.12"
@@ -4167,15 +3798,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "weezl"
version = "0.1.12"
@@ -5058,12 +4680,6 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zerotrie"
version = "0.2.5"
@@ -5105,12 +4721,6 @@ version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]]
name = "zune-core"
version = "0.5.3"
-15
View File
@@ -101,18 +101,8 @@ members = [
"core",
"macro",
"tabs-ui",
"transcript-ui",
"transcript-fixture",
"rig-input",
"desktop-app",
]
# android-app pulls in android-view, which needs the NDK sysroot to link
# -- excluded so `cargo build --workspace --all-targets` on the host stays
# buildable. Cross-compile it from its own directory (its own single-crate
# workspace, since it has no `[workspace]` table of its own and this
# exclusion stops it inheriting this one): `cd android-app && cargo ndk
# -t x86_64 -P 29 build`.
exclude = ["android-app"]
[workspace.package]
version = "0.1.0"
@@ -161,8 +151,3 @@ accesskit = "0.25.0"
iris-core = { path = "core" }
iris-macro = { path = "macro" }
tokio = "1.53.1"
# Current stable as of 2026-09-05 (`cargo search`) -- I5's markdown block
# model, the same crate E2's uncommitted `e2-transcript` experiment used for
# the identical job (RUST.md), rather than reimplementing a CommonMark
# parser.
pulldown-cmark = "0.13.4"
-6
View File
@@ -1,6 +0,0 @@
.gradle/
build/
app/build/
# Rebuilt by `cargo ndk -o app/src/main/jniLibs/ build` before every
# Gradle build -- see RUST.md's I2 for the exact command.
app/src/main/jniLibs/
-90
View File
@@ -1,90 +0,0 @@
[package]
name = "iris-android-app"
version = "0.1.0"
edition = "2024"
# Deliberately outside the `iris` workspace (see that Cargo.toml's
# `[workspace] exclude`): this crate exists only to be cross-compiled with
# `cargo ndk` for the emulator/a phone, and pulls in android-view, which
# needs the NDK sysroot to link. Folding it into the main workspace would
# make `cargo build --workspace --all-targets` -- the host command RUST.md
# and AGENTS.md both require to stay clean -- try to link a cdylib against
# libraries that do not exist on this machine. See RUST.md's I2.
[lib]
name = "main"
crate-type = ["cdylib"]
[dependencies]
iris = { path = "../" }
android-view = { git = "https://github.com/rust-mobile/android-view.git", rev = "bec6c62a96cef8239b0fd7fedeef9b184d02e3a1" }
android_logger = "0.15.1"
log = "0.4.34"
# `tabs-screen` (default, I2/I4's demo) and `transcript-screen` (I5's
# Android integration) are mutually exclusive -- one `ActiveClient` type is
# compiled in, never both (`lib.rs`'s doc comment) -- so both sets of deps
# are optional and each screen's feature pulls in only its own. Without
# this, building `--features transcript-screen` alone (default features
# still on) left `tabs-ui` linked but never referenced under that cfg,
# which Cargo's `unused_dependencies` lint (on by default) correctly flags.
tabs-ui = { path = "../tabs-ui", optional = true }
transcript-ui = { path = "../transcript-ui", optional = true }
# P0's bench build only: the fixture and the folded screen both bench
# clients open, shared with the headless harness and the desktop window
# (docs/RUST.md's "Three test layers").
transcript-fixture = { path = "../transcript-fixture", optional = true }
client-core = { path = "../../client-core", optional = true }
event-model = { path = "../../event-model", optional = true }
serde_json = { version = "1", features = ["float_roundtrip"], optional = true }
# P0's bench build only (docs/RUST.md): `getrusage(RUSAGE_SELF)` for
# process CPU time, matching `libc::getrusage`'s mention in that box over
# parsing `/proc/self/stat` by hand and assuming `USER_HZ`. Already in the
# workspace's own dependency tree transitively (`iris/Cargo.lock`, pinned
# at 0.2.179) -- this makes it a direct dependency at the same version
# rather than a second, possibly-drifting resolution.
libc = { version = "0.2.189", optional = true }
# P0's bench build only: the scroll animation and the streaming phase are
# both a sequence of `sleep`s inside the async task `rsc.spawn_task` already
# runs on iris's own tokio runtime (`iris/src/task.rs`'s `Tasks::init`), and
# the battery sampler is a second, concurrent task on that same runtime
# (`tokio::spawn`) -- so this crate needs `tokio` directly rather than only
# through `iris`. `rt`+`time` only: no I/O, no macros, nothing this crate
# doesn't call. Version matches the one `iris`'s own dependency tree already
# resolves to (`iris/Cargo.lock`), so there is one copy of the runtime, not
# two.
tokio = { version = "1.53.1", features = ["rt", "time"], optional = true }
[features]
default = ["tabs-screen"]
tabs-screen = ["dep:tabs-ui"]
transcript-screen = ["dep:transcript-ui", "dep:client-core", "dep:event-model", "dep:serde_json"]
# RUST.md's I5 "Where iris's frame time goes": forces the GLES backend
# instead of SwiftShader's software Vulkan. See `iris/Cargo.toml`'s own doc
# on the feature this forwards to.
force-gles = ["iris/force-gles"]
# P0's iris half (docs/RUST.md, docs/AGENTS.md's "The rigs"): the same
# checked-in fixture, scroll loop and streaming phase the Compose `bench`
# build type drives, run here against `transcript-ui`'s real screen with no
# server. Depends on `transcript-screen` for `transcript-ui`/`client-core`/
# `event-model` -- `lib.rs`'s `ActiveClient` selection gives this feature
# priority over `transcript-screen`'s own `TranscriptClient` when both are
# listed, which is how this crate's build command names both explicitly.
bench = ["transcript-screen", "dep:transcript-fixture", "dep:libc", "dep:tokio"]
[profile.release]
panic = "abort"
# Measured 2026-09-07 (docs/RUST.md's "APK size" subsection): together these
# take libmain.so from 18,546,488 to 11,193,608 bytes (-39.7%) and the APK
# from 20,678,956 to 13,326,076 bytes (-35.5%), arm64-v8a release. `strip`
# also works around AGP's own stripReleaseDebugSymbols failing silently on
# this .so ("packaging them as they are"). `opt-level = "s"` over `"z"`:
# `z` measured another ~800 KB smaller but was not checked against iris's
# own frame-time bench, so it is not worth the unmeasured risk -- see the
# doc for the number and the follow-up this leaves.
strip = true
lto = "fat"
codegen-units = 1
opt-level = "s"
[profile.dev]
panic = "abort"
-27
View File
@@ -1,27 +0,0 @@
[package]
name = "desktop-app"
version.workspace = true
edition.workspace = true
# RUST.md's E4: the same transcript-ui screen (I5) in a winit window on the
# desktop, beside a session list, talking to a real `ai-server` through
# `client-core`'s REST + SSE clients. Enrolment reuses the phone's own
# `aiapp://enroll?...` link (`client-core::config`) rather than inventing a
# second format -- see DECISIONS.md's 2026-09-05 entry. An ordinary
# workspace member (unlike `android-app`): nothing here needs the NDK, so
# `cargo build --workspace --all-targets` at the host stays clean with it
# included.
[dependencies]
iris = { path = ".." }
transcript-ui = { path = "../transcript-ui" }
client-core = { path = "../../client-core" }
event-model = { path = "../../event-model" }
# Already pulled in transitively through client-core; used directly here
# only to persist `EnrolledServer` as the app's own tiny config file (see
# `config.rs`) -- no new dependency.
serde_json = { version = "1", features = ["float_roundtrip"] }
winit = { workspace = true }
[dev-dependencies]
tempfile = "3"
+21 -12
View File
@@ -3,14 +3,20 @@
#
# ./run-headless.sh tabs [-- cargo args]
# ./run-headless.sh tabs --shot /tmp/tabs.png --seconds 4
# ./run-headless.sh phone --phone --shot /tmp/p.png -- -p transcript-fixture
# ./run-headless.sh phone --phone --replay transcript-fixture/touch/flick-120hz.touch \
# --shot /tmp/p.png -- -p transcript-fixture
# ./run-headless.sh phone --phone --dir ../app-rust --shot /tmp/p.png
# ./run-headless.sh phone --phone --dir ../app-rust \
# --replay ../app-rust/touch/flick-120hz.touch --shot /tmp/p.png
#
# `--dir DIR` names the workspace to build in, defaulting to `iris/` (this
# script's own directory). The app's examples -- the phone-sized transcript
# screen and everything else that is about *this product* -- live in
# `app-rust/`, which is a workspace of its own; `replay-touch` is still
# built from iris, since it is part of the rig rather than of either app.
#
# `--phone` is layer 2 of docs/RUST.md's "Three test layers": the output
# and the window take Iris's phone's own size and density (1080x2424 at
# `content_scale` 2.55, from docs/bench/iris-phone-v2-2026-09-06.md,
# carried in `transcript_fixture::PHONE_*`), and `IRIS_SCALE` hands that
# carried in `ai_app::ui::fixture::PHONE_*`), and `IRIS_SCALE` hands that
# density to iris the way `DisplayMetrics.density` does on Android
# (`iris::default::content_scale`). So a screenshot from here and one
# from the phone are the same layout at the same density, and what
@@ -18,13 +24,13 @@
# shaped, which is what every other example wants.
#
# `--replay FILE` drives one of the `.touch` recordings the headless
# tests use (`iris/transcript-fixture/touch/`) into the window through
# tests use (`app-rust/touch/`) into the window through
# `rig-input`'s `replay-touch` -- one recording, both layers. With
# `--shot` it also writes `<shot>-before.png` from just before the
# gesture, since "the list moved" is a claim about two pictures.
#
# `--bin` runs a real crate binary instead of an example (E4's
# `desktop-app`, which is a window a person runs, not a demo) --
# `ai-app-desktop`, which is a window a person runs, not a demo) --
# `cargo build --bin NAME` instead of `--example NAME`, and
# `target/debug/NAME` instead of `target/debug/examples/NAME`. Its own
# argv (the CLI flags a real binary takes, as opposed to `cargo build`'s
@@ -45,6 +51,8 @@
set -eu
here=$(cd "$(dirname "$0")" && pwd)
# The workspace `--dir` selects; see the header. `$here` is iris itself.
workdir="$here"
run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
seconds=3
shot=""
@@ -54,7 +62,7 @@ kind=example
phone=no
# The phone Iris runs the bench on. Not typed from memory: these are
# `transcript_fixture::PHONE_WIDTH`/`PHONE_HEIGHT`/`PHONE_SCALE`, which
# `ai_app::ui::fixture::PHONE_WIDTH`/`PHONE_HEIGHT`/`PHONE_SCALE`, which
# in turn come from her own reports -- keep the three in step.
PHONE_MODE=1080x2424@120Hz
PHONE_SCALE=2.55
@@ -67,11 +75,12 @@ while [ $# -gt 0 ]; do
--bin) kind=bin; shift ;;
--phone) phone=yes; shift ;;
--replay) replay=$2; shift 2 ;;
--dir) workdir=$(cd "$2" && pwd); shift 2 ;;
--) shift; break ;;
*) example=$1; shift ;;
esac
done
[ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--phone] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
[ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--phone] [--dir DIR] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
[ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; }
mkdir -p "$run"
@@ -128,15 +137,15 @@ out_h=${mode#*x}; out_h=${out_h%@*}
# Built before the app starts, so a compile error is not reported as a
# window that failed to move.
[ -z "$replay" ] || cargo build --bin replay-touch -p rig-input >&2
[ -z "$replay" ] || (cd "$here" && cargo build --bin replay-touch -p rig-input) >&2
cd "$here"
cd "$workdir"
if [ "$kind" = bin ]; then
cargo build --bin "$example" "$@" >&2
bin="$here/target/debug/$example"
bin="$workdir/target/debug/$example"
else
cargo build --example "$example" "$@" >&2
bin="$here/target/debug/examples/$example"
bin="$workdir/target/debug/examples/$example"
fi
# shellcheck disable=SC2086 -- deliberately word-split: this is the
-33
View File
@@ -1,33 +0,0 @@
[package]
name = "transcript-fixture"
version.workspace = true
edition.workspace = true
# The bench fixture, opened as a real transcript screen with no server --
# docs/RUST.md's "Three test layers". It was `iris-android-app`'s
# `bench_client.rs` alone until 2026-09-07; the fixture-loading and
# fold-driving half moved here so the headless harness (layer 1), the
# phone-shaped desktop window (layer 2) and the Android bench (layer 3)
# all open the *same* screen from the same bytes, per AGENTS.md's rule
# that nothing UI-shaped lives in a platform crate.
[dependencies]
iris = { path = ".." }
transcript-ui = { path = "../transcript-ui" }
client-core = { path = "../../client-core" }
event-model = { path = "../../event-model" }
# `float_roundtrip` for the same reason `server/Cargo.toml` has it: a `ts`
# read back must be the one that was written (AGENTS.md).
serde_json = { version = "1", features = ["float_roundtrip"] }
[dev-dependencies]
winit = { workspace = true }
# `examples/phone.rs`'s `--typed` only: it spaces its insertions out over
# real frames, and a task spawned through `iris`'s own runtime
# (`iris/src/task.rs`) is where the sleep has to happen. Same version the
# workspace already pins for `iris-android-app`.
tokio = { workspace = true, features = ["time"] }
# For the `iris::input`/`iris::frame` round-trip test: a capturing `log::Log`
# to read back what `iris::diagnostics::log_frame`/`sense::log_input_event`
# wrote, pinned to the same version `iris/Cargo.toml` already carries.
log = "0.4.34"
-26
View File
@@ -1,26 +0,0 @@
[package]
name = "transcript-ui"
version.workspace = true
edition.workspace = true
# I5 (RUST.md): the transcript screen's widget tree, built the same way
# `tabs-ui` is -- its own crate, generic over `Rsc: HasEvents` +
# `Rsc::State: FocusHost`, so the winit example and an eventual
# `iris-android-app`-style cdylib call the same `build`. See its own module
# doc for the design and RUST.md's I5 box for what is and is not proved yet.
#
# `client-core`/`event-model` by path, real code and not a reimplementation
# -- the same dependency shape E2's uncommitted Masonry experiment used for
# the identical job.
[dependencies]
iris = { path = ".." }
client-core = { path = "../../client-core" }
event-model = { path = "../../event-model" }
pulldown-cmark = { workspace = true }
# Selection has no accessibility label of its own yet (IRIS_TODO.md's
# "Row-level accessibility names"), so a logcat line at
# begin/extend is the smallest way to confirm a real long-press-then-drag
# reached `Selection` on-device (RUST.md's I5 box, "Measurements taken"
# (c)) -- version pinned to match `iris-android-app`'s own dependency.
log = "0.4.34"
+20 -13
View File
@@ -1,18 +1,25 @@
#!/bin/sh
# Runs this repo's tests. Extra arguments are forwarded to each `cargo test`,
# e.g. `./run-tests.sh transcript` to run just the transcript tests in all
# three crates.
# 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
# tests in every workspace.
#
# server/ holds the backend's own logic (event normalization, transcript
# cursors, config persistence, token auth); event-model is the wire shape
# server/ and client-core share; client-core is the app's pure logic held
# once instead of twice (see CLIENT_CORE.md) -- the syntax highlighter, the
# ANSI parser, the transcript cache and fold, the REST and SSE clients. It
# is not wired into the Android app yet (RUST.md), which still carries its
# own Kotlin copies and has no tests of its own to run here; verifying it
# means running it -- see AGENTS.md.
# Three workspaces, in dependency order:
#
# event-model the wire shape server/ and app-rust/ share, so the two
# agree by construction rather than by review
# server/ the backend's own logic (event normalization, transcript
# cursors, config persistence, token auth)
# app-rust/ the app itself: `client` (highlighter, ANSI parser,
# transcript cache and fold, REST and SSE clients -- see
# docs/CLIENT_CORE.md) and `ui` (the screens, drawn with
# iris), plus the headless harness tests over the bench
# fixture that need no window and no GPU.
#
# `iris/` is the UI framework and has its own tests; run them from there
# (`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.
set -eu
cd "$(dirname "$0")"
for crate in event-model client-core server; do
(cd "$crate" && cargo test "$@")
for workspace in event-model server app-rust; do
(cd "$workspace" && cargo test "$@")
done
+5 -5
View File
@@ -30,7 +30,7 @@
//! ?limit=N, ?coalesce=true to count rows not deltas,
//! ?after=N to floor it at what the caller already holds
//! GET /sessions/{id}/subagents [{id, title, status, created, lastActivity}], oldest
//! first -- see SUBAGENTS.md
//! first -- see docs/SUBAGENTS.md
//! GET /sessions/{id}/subagents/{sub}/transcript exactly the transcript route above,
//! against that subagent's own transcript
//! GET /sessions/{id}/subagents/{sub}/events?after=N exactly the events route above,
@@ -1779,7 +1779,7 @@ async fn transcript(
}
/// Exactly [`transcript`]'s route and answer, against one subagent's own
/// transcript instead of its session's -- see `SUBAGENTS.md`'s wire shape.
/// transcript instead of its session's -- see `docs/SUBAGENTS.md`'s wire shape.
async fn subagent_transcript(
State(manager): State<Arc<SessionManager>>,
UrlPath((id, sub)): UrlPath<(String, String)>,
@@ -1846,7 +1846,7 @@ async fn events(
}
/// Exactly [`events`]'s route and answer, against one subagent's own stream
/// instead of its session's -- see `SUBAGENTS.md`'s wire shape.
/// instead of its session's -- see `docs/SUBAGENTS.md`'s wire shape.
async fn subagent_events(
State(manager): State<Arc<SessionManager>>,
UrlPath((id, sub)): UrlPath<(String, String)>,
@@ -1884,7 +1884,7 @@ fn sse_stream(
/// `GET /sessions/{id}/subagents`: every subagent this session has started,
/// oldest first, with a status read from its own transcript -- see
/// `SUBAGENTS.md`'s wire shape. A subagent whose last status is `Running` is
/// `docs/SUBAGENTS.md`'s wire shape. A subagent whose last status is `Running` is
/// reported `Unknown` instead when the session itself is not running: its
/// process was the session's, and a session with none has nothing left to
/// ask.
@@ -1895,7 +1895,7 @@ async fn list_subagents(
let session = lookup(&manager, &id)?;
// Anything but `Exited` or `Unknown` has a process behind it, which is
// what decides whether a subagent still reading `Running` from its own
// transcript can be believed -- see `SUBAGENTS.md`'s wire shape.
// transcript can be believed -- see `docs/SUBAGENTS.md`'s wire shape.
let running = !matches!(
session.status(),
crate::session::driver::SessionStatus::Exited
+16 -6
View File
@@ -8,7 +8,7 @@
//! the Keystore-sealed enrollment, RUST.md's E3 entry explains why that
//! code is reused rather than re-derived in Rust) and on
//! `androidx.core:core-ktx` (used at runtime through JNI by
//! `android-shell`'s `notify.rs`, for `NotificationCompat` and friends).
//! the shell bridge's `notify.rs`, for `NotificationCompat` and friends).
//! Both are ordinary Maven/AAR dependency graphs, and reimplementing a
//! dependency resolver to avoid one Gradle invocation was not a good trade
//! against "smallest honest route" (RUST.md's E5 box) -- especially since
@@ -34,7 +34,10 @@ pub fn build(variant: Variant, abis: &[String]) -> Result<PathBuf, Fail> {
let repo_root = repo_root()?;
let app_dir = repo_root.join("app");
let shell_app_dir = app_dir.join("shellApp");
let android_shell_dir = repo_root.join("android-shell");
// The JNI bridge is the `shell` feature of the one app crate now, not
// a crate of its own -- `--no-default-features` is what keeps iris,
// wgpu and parley out of a `.so` for an app that draws with Compose.
let app_crate_dir = repo_root.join("app-rust");
let sdk = sdk::find()?;
sdk::require_ndk_installed(&sdk.root)?;
@@ -49,8 +52,8 @@ pub fn build(variant: Variant, abis: &[String]) -> Result<PathBuf, Fail> {
)
})?;
println!("==> Building android-shell for {}", abis.join(", "));
build_native_libs(&android_shell_dir, &shell_app_dir, &sdk, abis)?;
println!("==> Building the shell JNI bridge for {}", abis.join(", "));
build_native_libs(&app_crate_dir, &shell_app_dir, &sdk, abis)?;
println!("==> Resolving the runtime classpath (one Gradle call -- see apk.rs's module doc)");
let classpath_jars = runtime_classpath_jars(&app_dir, &sdk)?;
@@ -159,7 +162,14 @@ fn build_native_libs(
// segfaults this emulator's driver (RUST.md's E1 entry), and there is
// no reason for this crate's debug build to be bigger or slower for a
// signing choice that has nothing to do with it.
cmd.args(["build", "--release", "-p", "android-shell"]);
cmd.args([
"build",
"--release",
"--lib",
"--no-default-features",
"--features",
"shell",
]);
cmd.env("ANDROID_HOME", &sdk.root);
cmd.env("ANDROID_SDK_ROOT", &sdk.root);
run_checked(
@@ -395,7 +405,7 @@ fn merge(
std::fs::remove_dir_all(&stage).ok();
}
for abi in abis {
let so_name = "libandroid_shell.so";
let so_name = "libai_app.so";
let src = shell_app_dir
.join("src/main/jniLibs")
.join(abi)
+1 -1
View File
@@ -1,6 +1,6 @@
//! `cargo xtask apk` -- E5 (RUST.md): packages `app/shellApp` into a
//! signed, installable APK with no Gradle in the packaging step itself.
//! `cargo ndk` cross-compiles `android-shell`; `javac`/`d8` turn its two
//! `cargo ndk` cross-compiles the app crate's `shell` feature; `javac`/`d8` turn its two
//! Java stub classes (plus the generated pinned-CA constant) into dex;
//! `aapt2` compiles the manifest into `resources.arsc`; the dex and native
//! libraries are merged into that base APK with `jar`; `zipalign` and