From 25370731d09023d619c7b5a07359e03a5c6ef6c4 Mon Sep 17 00:00:00 2001
From: iris <2+iris@noreply.localhost>
Date: Thu, 10 Sep 2026 00:44:13 -0400
Subject: [PATCH] Prune commentary and stale Rust port notes
---
AGENTS.md | 7 +-
app-rust/Cargo.toml | 76 +-
.../dev/iris/android/demo/DevLogProvider.java | 76 +-
.../java/dev/iris/android/demo/IrisView.java | 15 +-
.../dev/iris/android/demo/MainActivity.java | 99 +-
.../linebender/android/rustview/RustView.java | 8 +-
app-rust/examples/phone.rs | 44 -
app-rust/examples/transcript.rs | 62 -
app-rust/rust-toolchain.toml | 9 +-
app-rust/src/android/app_log.rs | 53 +-
app-rust/src/android/bench_client.rs | 332 +-----
app-rust/src/android/bench_jni.rs | 46 -
app-rust/src/android/devlog.rs | 51 -
app-rust/src/android/enrollment.rs | 46 -
app-rust/src/android/mod.rs | 67 --
app-rust/src/android/transcript_client.rs | 95 --
app-rust/src/bin_desktop.rs | 26 -
app-rust/src/client/ansi.rs | 78 --
app-rust/src/client/api.rs | 63 -
app-rust/src/client/config.rs | 49 -
app-rust/src/client/durations.rs | 26 -
app-rust/src/client/event_stream.rs | 17 -
app-rust/src/client/highlight/languages.rs | 45 -
app-rust/src/client/highlight/markdown.rs | 62 -
app-rust/src/client/highlight/mod.rs | 48 -
app-rust/src/client/log_ring.rs | 114 --
app-rust/src/client/markdown_blocks.rs | 84 --
app-rust/src/client/mod.rs | 4 -
app-rust/src/client/notifications.rs | 13 -
app-rust/src/client/sse.rs | 19 -
app-rust/src/client/text_cap.rs | 41 -
app-rust/src/client/tool_summary.rs | 58 -
app-rust/src/client/transcript_cache.rs | 226 ----
app-rust/src/client/transcript_fold.rs | 183 ---
app-rust/src/client/transcript_source.rs | 109 --
app-rust/src/desktop/app.rs | 94 --
app-rust/src/desktop/config.rs | 13 -
app-rust/src/desktop/mod.rs | 4 -
app-rust/src/desktop/startup.rs | 6 -
app-rust/src/lib.rs | 18 -
app-rust/src/shell/jcall.rs | 53 -
app-rust/src/shell/mod.rs | 36 -
app-rust/src/shell/notify.rs | 31 -
app-rust/src/shell/settings.rs | 9 -
app-rust/src/shell/share.rs | 28 -
app-rust/src/ui/composer.rs | 59 -
app-rust/src/ui/fixture.rs | 40 -
app-rust/src/ui/markdown.rs | 160 ---
app-rust/src/ui/mod.rs | 301 -----
app-rust/src/ui/row.rs | 172 ---
app-rust/src/ui/selection.rs | 152 ---
app-rust/src/ui/tap.rs | 45 -
app-rust/src/ui/tool.rs | 250 ----
app-rust/tests/catch_a_fling.rs | 45 -
app-rust/tests/fence_fling.rs | 39 -
app-rust/tests/gesture_cancel.rs | 56 -
app-rust/tests/input_log_roundtrip.rs | 48 -
app-rust/tests/phone_screen.rs | 94 --
app-rust/tests/top_edge.rs | 91 --
docs/CLIENT_CORE.md | 257 +---
docs/IRIS_TODO.md | 38 +-
docs/LAYOUT.md | 497 +-------
docs/RUST.md | 381 +-----
docs/SCROLL.md | 100 +-
docs/TEXTURES.md | 266 +----
docs/TODO.md | 45 +-
event-model/Cargo.toml | 13 +-
event-model/src/lib.rs | 253 +---
iris/Cargo.toml | 90 +-
iris/benches/fling_spline_reference.py | 5 -
iris/benches/message_list.rs | 129 --
iris/benches/report_to_touch.py | 2 -
iris/benches/velocity_reference.py | 10 -
iris/core/Cargo.toml | 6 +-
iris/core/build-icon-font.sh | 2 -
iris/core/src/event/manager.rs | 1 -
iris/core/src/event/mod.rs | 13 -
iris/core/src/icon.rs | 34 -
iris/core/src/orientation/align.rs | 10 +-
iris/core/src/orientation/len.rs | 18 -
iris/core/src/primitive/color.rs | 1 -
iris/core/src/primitive/layer.rs | 9 -
iris/core/src/primitive/text.rs | 653 ++++-------
iris/core/src/primitive/texture.rs | 46 -
iris/core/src/render/atlas.rs | 78 --
iris/core/src/render/data.rs | 54 -
iris/core/src/render/frame_report.rs | 247 ----
iris/core/src/render/mod.rs | 137 ---
iris/core/src/render/primitive.rs | 137 ---
iris/core/src/render/sdf.rs | 27 -
iris/core/src/render/shader.wgsl | 86 +-
iris/core/src/render/texture.rs | 62 -
iris/core/src/render/util/mod.rs | 26 -
iris/core/src/ui/access.rs | 27 -
iris/core/src/ui/active.rs | 11 -
iris/core/src/ui/mod.rs | 21 -
iris/core/src/ui/painter.rs | 84 --
iris/core/src/ui/render_state.rs | 253 +---
iris/core/src/util/arena.rs | 8 -
iris/core/src/util/dirty.rs | 28 -
iris/core/src/util/id.rs | 2 -
iris/core/src/util/math.rs | 3 -
iris/core/src/util/typemap.rs | 1 -
iris/core/src/util/vec2.rs | 1 -
iris/core/src/widget/data.rs | 1 -
iris/core/src/widget/handle.rs | 7 -
iris/core/src/widget/like.rs | 1 -
iris/core/src/widget/mod.rs | 7 -
iris/core/src/widget/widgets.rs | 12 -
iris/examples/bench_images.rs | 24 -
iris/examples/message_list.rs | 36 -
iris/examples/tabs/main.rs | 4 -
iris/macro/src/lib.rs | 7 -
iris/rig-input/Cargo.toml | 22 +-
iris/rig-input/src/main.rs | 23 -
iris/run-headless.sh | 4 -
iris/rust-toolchain.toml | 9 +-
iris/src/access_tests.rs | 21 -
iris/src/android/access.rs | 18 -
iris/src/android/ime.rs | 51 -
iris/src/android/insets.rs | 49 +-
iris/src/android/mod.rs | 14 -
iris/src/android/platform.rs | 6 -
iris/src/android/render.rs | 110 --
iris/src/android/view.rs | 251 ----
iris/src/attr.rs | 53 -
iris/src/default/access.rs | 10 -
iris/src/default/app.rs | 3 -
iris/src/default/attr.rs | 2 -
iris/src/default/logging.rs | 27 -
iris/src/default/mod.rs | 42 -
iris/src/default/platform.rs | 11 -
iris/src/default/render.rs | 25 -
iris/src/diagnostics.rs | 34 -
iris/src/event.rs | 13 -
iris/src/harness.rs | 77 --
iris/src/layout_tests.rs | 268 -----
iris/src/lib.rs | 7 -
iris/src/platform.rs | 19 -
iris/src/sense.rs | 1043 +----------------
iris/src/sense_tests.rs | 149 ---
iris/src/widget/position/lazy_span.rs | 631 +---------
iris/src/widget/position/pad.rs | 5 -
iris/src/widget/position/scroll_area.rs | 72 --
iris/src/widget/position/scrollable.rs | 189 ---
iris/src/widget/text/build.rs | 4 -
iris/src/widget/text/edit.rs | 102 --
iris/src/widget/text/mod.rs | 41 -
iris/src/widget/trait_fns.rs | 25 -
iris/tabs-ui/Cargo.toml | 6 +-
iris/tabs-ui/src/lib.rs | 16 -
iris/tests/mask_sdf.rs | 109 --
resources.ron | 28 +-
scripts/rigs/gpu-probe/Cargo.toml | 11 +-
scripts/rigs/gpu-probe/src/bin/teardown.rs | 35 -
scripts/rigs/gpu-probe/src/main.rs | 51 -
scripts/rigs/gpu-probe/src/vk.rs | 11 -
scripts/rigs/ui-profile/Cargo.toml | 16 +-
scripts/rigs/ui-profile/rust-toolchain.toml | 9 +-
scripts/rigs/ui-profile/src/stats.rs | 9 -
scripts/rigs/ui-profile/tests/arena_churn.rs | 54 +-
.../rigs/ui-profile/tests/frame_profile.rs | 95 +-
scripts/run-tests.sh | 20 +-
scripts/test-wg-tunnel.sh | 5 +-
scripts/wg-setup-host.sh | 4 +-
scripts/xtask/Cargo.toml | 9 +-
scripts/xtask/src/apk.rs | 52 -
scripts/xtask/src/keystore.rs | 13 -
scripts/xtask/src/main.rs | 22 -
server/Cargo.toml | 40 +-
server/src/auth.rs | 40 -
server/src/config.rs | 207 ----
server/src/files.rs | 129 +-
server/src/main.rs | 90 --
server/src/media.rs | 13 -
server/src/models.rs | 114 --
server/src/resume.rs | 77 --
server/src/routes.rs | 405 -------
server/src/session/claude.rs | 361 ------
server/src/session/claude/translate.rs | 322 -----
server/src/session/driver.rs | 96 --
server/src/session/echo.rs | 213 ----
server/src/session/import.rs | 294 -----
server/src/session/llama.rs | 138 +--
server/src/session/mod.rs | 996 ----------------
server/src/session/pending.rs | 46 -
server/src/session/process.rs | 97 --
server/src/session/subagent.rs | 60 -
server/src/session/transcript.rs | 157 ---
server/src/session/transport.rs | 76 --
server/src/setups.rs | 61 -
server/src/ssh.rs | 109 --
server/src/usage.rs | 300 +----
193 files changed, 693 insertions(+), 16219 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index a59124d..93b3477 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -196,10 +196,9 @@ guaranteed to have.
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.
+ `rustfmt.toml` and there should not be one. `app-rust/`, `iris/`, and the
+ UI profiling rig use the rolling nightly channel through per-directory
+ `rust-toolchain.toml` files; `server/` and `event-model/` are stable.
- **App**: from `app/`,
`. ./android-env.sh && ./gradlew :androidApp:ktfmtFormat
:androidApp:compileDebugKotlin :androidApp:lintDebug
diff --git a/app-rust/Cargo.toml b/app-rust/Cargo.toml
index f0ed4ae..439d6bd 100644
--- a/app-rust/Cargo.toml
+++ b/app-rust/Cargo.toml
@@ -1,26 +1,10 @@
-# 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).
+# Product code lives here; reusable UI belongs in `iris/`.
[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.
+# Android loads the cdylib; desktop, examples, and tests link the rlib.
[lib]
name = "ai_app"
crate-type = ["cdylib", "rlib"]
@@ -39,106 +23,67 @@ 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.
+# Transcript lines must retain exact float values and raw JSON bytes.
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.
+# Optional so the Compose shell does not link the renderer.
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.
+# Keep this pin synchronized with `iris/Cargo.toml`.
[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`.
+# Default-on for tests; APK builds opt in so ordinary APKs omit the 1.9 MB fixture.
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`.
+# APK builds select these profiles explicitly.
[profile.android-release]
inherits = "release"
panic = "abort"
strip = true
lto = "fat"
codegen-units = 1
-# **Speed, not size** (2026-09-09). This was `"s"`, chosen when the
-# question was why the APK was double the Compose one -- but that was
-# measured in bytes only, and `"s"` costs the loop vectorisation and
-# inlining a renderer runs on. Measured with
-# `scripts/rigs/ui-profile`'s `frame_profile.rs`, the same warm fling eight
-# times over:
-# iris's own per-frame work is p90 0.15ms / p99 0.42ms at `"s"` and
-# p90 0.09ms / p99 0.26ms at `3`, so about a third of the CPU half of a
-# scrolling frame was being paid for 1.9 MB of download. The same
-# argument the table in docs/RUST.md gives for refusing `"z"`, applied one
-# level further up.
+# A warm-fling profile measured p90/p99 0.09/0.26 ms at 3 versus
+# 0.15/0.42 ms at "s"; the 1.9 MB saving is not worth that frame cost.
opt-level = 3
[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.
+# Full DWARF in each renderer-linked test binary writes 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"]
@@ -162,4 +107,3 @@ required-features = ["fixture"]
[[test]]
name = "top_edge"
required-features = ["fixture"]
-
diff --git a/app-rust/android-project/app/src/main/java/dev/iris/android/demo/DevLogProvider.java b/app-rust/android-project/app/src/main/java/dev/iris/android/demo/DevLogProvider.java
index 9585c28..8477403 100644
--- a/app-rust/android-project/app/src/main/java/dev/iris/android/demo/DevLogProvider.java
+++ b/app-rust/android-project/app/src/main/java/dev/iris/android/demo/DevLogProvider.java
@@ -7,52 +7,13 @@ import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
-/**
- * This app's own recent log, exposed on the device.
- *
- * Iris runs these builds on a phone with no {@code adb}, and Android
- * forbids one app reading another's {@code logcat} -- so nothing outside
- * this process can recover what it wrote. The process already keeps a
- * bounded copy of its log (Rust: {@code client_core::log_ring}); this
- * hands it to Dev Updater, which is on the same phone, so it needs no
- * tunnel, no token and no second enrolment.
- *
- *
The shape is Dev Updater's contract, not something invented
- * here -- see that project's {@code README.md}, "An app's own log". Any
- * app it delivers can implement the same and get the same Runtime tab.
- * Two paths:
- *
- *
- * - {@code lines?since=} -- every held line with a sequence at or
- * after {@code since}, oldest first.
- *
- {@code status} -- one row: how many lines are held, how many the
- * ring's own bound has dropped, and the newest sequence ({@code -1}
- * for a log nothing has been written to, which is also how a reader
- * notices this process restarted).
- *
- *
- * Read-only: there is nothing here for anyone else to change, so the
- * three writing methods throw rather than silently doing nothing.
- *
- *
The authority is {@code .devlog}, filled in from
- * Gradle so the bench build and the ordinary one each get their own and
- * neither can read the other's. Read access is guarded by
- * {@code dev.updater.permission.READ_DEVLOG}, declared in the manifest.
- *
- * No {@code notifyChange}: the ring is filled by a {@code log::Log}
- * backend on whatever thread logged, and giving that a way to reach a
- * provider would mean plumbing a callback through {@code client-core} for
- * every platform. Dev Updater polls while its tab is open, which its
- * contract says it does precisely so implementing this stays cheap.
- */
+/** Read-only Dev Updater log provider; its URI and column schema are an external contract. */
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.
+ // A provider can start the process without creating MainActivity.
System.loadLibrary("ai_app");
}
- /** Matches {@link #nativeLinesSince}'s flat answer. Both sides say it once. */
private static final int FIELDS_PER_LINE = 5;
private static final String[] LINE_COLUMNS = {"seq", "t_ms", "level", "target", "message"};
@@ -63,33 +24,16 @@ public final class DevLogProvider extends ContentProvider {
private UriMatcher matcher;
- /** Every held line, {@link #FIELDS_PER_LINE} strings each, oldest first. */
private static native String[] nativeLinesSince(long since);
- /** Three strings: held, dropped, newest sequence. */
private static native String[] nativeStatus();
- /**
- * Tells the Rust side which authority this build registered under, so
- * the diagnostics pane can name somewhere a reader can actually query
- * -- and so "declared but never created" is a state it can say. Only
- * the provider knows it was instantiated; Android creates one lazily.
- *
- *
The files directory goes with it because this is usually the
- * only thing running: after the app has died, Dev Updater's query
- * starts the process for the provider alone, with no activity, so
- * {@code MainActivity.nativeSetFilesDir} is never called and the line
- * the panic hook left on disk is never replayed into the ring. That is
- * exactly the run whose log somebody wants.
- */
+ // The provider may be the process's only component, so it must supply
+ // the files directory normally initialized by MainActivity.
private static native void nativeReady(String authority, String filesDir);
@Override
public boolean onCreate() {
- // The authority is not a constant here: it is derived from this
- // build's applicationId, so the bench package and the ordinary one
- // do not share one. Read back from the manifest rather than
- // recomposed, so there is one answer to what it is.
String authority = getContext().getPackageName() + ".devlog";
matcher = new UriMatcher(UriMatcher.NO_MATCH);
matcher.addURI(authority, "lines", LINES);
@@ -111,19 +55,10 @@ public final class DevLogProvider extends ContentProvider {
case STATUS:
return status();
default:
- // Null rather than an exception: an unknown path is a
- // reader asking for something this app does not have, and
- // the contract's own answer for that is no cursor.
return null;
}
}
- /**
- * {@code ?since=} as a number, or 0 for a reader starting from the
- * beginning. A value that is not a number is treated as 0 rather than
- * refused -- what a caller wants from a malformed cursor is the log,
- * not a stack trace about the query string.
- */
private static long sinceOf(Uri uri) {
String since = uri.getQueryParameter("since");
if (since == null) {
@@ -170,9 +105,6 @@ public final class DevLogProvider extends ContentProvider {
@Override
public String getType(Uri uri) {
- // A MIME type is for something meant to be handed to another app
- // as data; these rows are read by one reader that knows the
- // columns. Saying nothing is the honest answer, not a gap.
return null;
}
diff --git a/app-rust/android-project/app/src/main/java/dev/iris/android/demo/IrisView.java b/app-rust/android-project/app/src/main/java/dev/iris/android/demo/IrisView.java
index ded3d11..35fbec1 100644
--- a/app-rust/android-project/app/src/main/java/dev/iris/android/demo/IrisView.java
+++ b/app-rust/android-project/app/src/main/java/dev/iris/android/demo/IrisView.java
@@ -39,20 +39,7 @@ public final class IrisView extends RustView {
super.onDetachedFromWindow();
}
- /**
- * Called from the Rust side (iris/src/android/view.rs's
- * `show_renderer_error`) when `AndroidRenderer::new` fails instead of
- * drawing -- an ordinary instance method rather than a `native` one,
- * since this call is Rust reaching into Java rather than the other
- * direction. Replaces the whole activity content with plain,
- * selectable, scrollable text rather than leaving the last frame (or a
- * blank surface) on screen with no way to report what happened:
- * UI_RULES.md's "a failure is reported where it happened, and says
- * what to do next." No dialog and no styling beyond what is needed to
- * read and copy the text -- this path exists for exactly the crash it
- * replaces, so it must not depend on anything that could itself fail
- * to render.
- */
+ // This path must not depend on the renderer that failed to initialize.
void showRendererError(String report) {
Context context = getContext();
if (!(context instanceof Activity)) {
diff --git a/app-rust/android-project/app/src/main/java/dev/iris/android/demo/MainActivity.java b/app-rust/android-project/app/src/main/java/dev/iris/android/demo/MainActivity.java
index fa8ac58..9ea799a 100644
--- a/app-rust/android-project/app/src/main/java/dev/iris/android/demo/MainActivity.java
+++ b/app-rust/android-project/app/src/main/java/dev/iris/android/demo/MainActivity.java
@@ -10,38 +10,18 @@ import android.view.WindowInsetsAnimation;
import android.widget.FrameLayout;
import java.util.List;
-/**
- * The android-view backend's demo activity (RUST.md's I2): one IrisView
- * filling the window, running iris's tabs example through
- * iris-android-app's Rust side. Mirrors android-view's own
- * DemoActivity, plus the window-insets wiring that has no android-view
- * counterpart.
- */
public final class MainActivity extends Activity {
static {
System.loadLibrary("ai_app");
}
- /**
- * The app's private directory, where the Rust side keeps its enrollment
- * (`src/enrollment.rs`). Handed over before the view is built, because
- * the client the view creates reads the enrollment as it starts.
- */
private static native void nativeSetFilesDir(String path);
- /**
- * One `aiapp://enroll?host=&port=&token=&ca=` link, as Dev Updater's
- * Enroll button opens it. Parsed and stored on the Rust side, which is
- * where the enrollment lives for the desktop app too -- nothing about
- * the link's format is known here.
- */
private static native void nativeEnroll(String uri);
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
- // Before the view: creating it starts the Rust client, which asks
- // straight away which server it is enrolled with.
nativeSetFilesDir(getFilesDir().getAbsolutePath());
handleEnrollmentIntent(getIntent());
IrisView view = new IrisView(this);
@@ -54,46 +34,14 @@ public final class MainActivity extends Activity {
setContentView(layout);
view.requestFocus();
- // RUST.md's P0 box, defect 4 ("keyboard: could not be shown"):
- // `logcat` showed the platform's own IME open/resize happening
- // while `setOnApplyWindowInsetsListener` fired only once, at
- // attach, and never again for a pure keyboard toggle -- a plain
- // (non-edge-to-edge) window is only guaranteed that one initial
- // dispatch; `adjustResize` handling the IME entirely by resizing
- // the window is not itself a trigger for a fresh one. Opting into
- // edge-to-edge (a platform call, API 30+, no new dependency) is
- // what makes the system redeliver insets on every change,
- // including the ones this activity actually cares about --
- // `getSystemWindowInset*` below is unaffected by this (it has
- // always reported the raw system-bar/IME overlap regardless of
- // who consumes it), so the on-screen bars and the padding Rust
- // already derives from those four numbers are unchanged; only the
- // callback's firing became reliable.
+ // Edge-to-edge makes IME-only changes produce fresh inset dispatches.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getWindow().setDecorFitsSystemWindows(false);
}
- // **The keyboard's height arrives twice, over two different
- // paths, and the phone needs the second one** (Iris, 2026-09-07:
- // the emulator pushed the composer up and her Pixel did not).
- // `setOnApplyWindowInsetsListener` is the platform's *settled*
- // answer; `WindowInsetsAnimation.Callback` is the running one, and
- // an IME that animates in delivers every intermediate height
- // through the callback with the static dispatch arriving only at
- // the ends -- on some devices only at `onEnd`. Registering both
- // means neither device depends on the other's timing, and it is
- // also what makes the push-up *animate* with the keyboard rather
- // than jump when it lands.
- //
- // The two do not disagree, because they are the same call with the
- // same numbers read out of whichever `WindowInsets` is current.
- // `DISPATCH_MODE_CONTINUE_ON_SUBTREE` so this view consuming
- // nothing keeps the ordinary dispatch running underneath.
- // `onEnd` re-reads the root's insets rather than trusting the last
- // `onProgress`: an animation interrupted mid-flight never delivers
- // its final frame, which is exactly the fault the Compose app hit
- // (AGENTS.md, "the composer can get stuck floating above the
- // bottom of the screen").
+ // Static dispatch supplies settled insets; the animation callback
+ // supplies intermediate IME heights. An interrupted animation may
+ // omit its final progress frame, so onEnd re-reads the root insets.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
view.setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(
WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
@@ -120,25 +68,14 @@ public final class MainActivity extends Activity {
});
}
- /**
- * A link that arrives while the activity is already up. `singleTop` is
- * not set, so this is the resumed case only -- the fresh-launch case
- * goes through `onCreate`'s `getIntent`. `setIntent` so a later
- * `getIntent` reports the one actually being acted on rather than the
- * one this activity started with.
- */
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
+ // Keep getIntent() consistent with the enrollment being handled.
setIntent(intent);
handleEnrollmentIntent(intent);
}
- /**
- * Hands a VIEW intent's URI to the Rust side, which decides whether it
- * is an enrollment link -- the scheme is checked here only so a launch
- * intent (which carries no data) costs nothing.
- */
private static void handleEnrollmentIntent(Intent intent) {
if (intent == null) {
return;
@@ -149,35 +86,13 @@ public final class MainActivity extends Activity {
}
}
- /** Read one `WindowInsets` and hand it to the Rust side. The only
- * place that reads these fields, so the static dispatch and the
- * animation callback above cannot come to report different things. */
private static void sendInsets(IrisView view, WindowInsets insets) {
int left = insets.getSystemWindowInsetLeft();
int top = insets.getSystemWindowInsetTop();
int right = insets.getSystemWindowInsetRight();
int bottom = insets.getSystemWindowInsetBottom();
- // **Two separate answers, because they are separate questions**
- // (Iris's phone, 2026-09-06: "message box does not push up the
- // scroll area"). `isVisible(ime())` says whether the keyboard is
- // up; `getInsets(ime()).bottom` says how tall it is. An earlier
- // pass sent the boolean *as* the height (0 or 1) because under
- // plain `adjustResize` the window shrinks to make room and the ime
- // inset therefore measures a zero overlap by construction -- true
- // then, and no longer true now that this is an edge-to-edge window
- // (`targetSdk` 35+, plus the `setDecorFitsSystemWindows` call
- // above for the devices below that), which is exactly the case
- // where the system stops resizing and hands the app the real
- // overlap instead. Sending 1 for it left the Rust side padding the
- // composer by one physical pixel, so the keyboard covered the bar
- // and the transcript alike.
- //
- // The visibility is still sent in its own right rather than
- // inferred from `height > 0`: the two disagree during the
- // keyboard's slide-in and -out (visible, height still climbing),
- // and "is the IME up" drives the bench's own state machine
- // (`bench_client.rs`'s `ime_state`) where a half-open frame
- // reading as "closed" is a miscount.
+ // Visibility and height disagree during IME animation, so neither
+ // can be inferred from the other.
int imeBottom = 0;
int imeVisible = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
diff --git a/app-rust/android-project/app/src/main/java/org/linebender/android/rustview/RustView.java b/app-rust/android-project/app/src/main/java/org/linebender/android/rustview/RustView.java
index ddde7d6..b7fa4ad 100644
--- a/app-rust/android-project/app/src/main/java/org/linebender/android/rustview/RustView.java
+++ b/app-rust/android-project/app/src/main/java/org/linebender/android/rustview/RustView.java
@@ -16,12 +16,8 @@ import android.view.inputmethod.InputMethodManager;
public abstract class RustView extends SurfaceView
implements SurfaceHolder.Callback, Choreographer.FrameCallback {
- // Vendored from android-view (bec6c62, https://github.com/rust-mobile/android-view)
- // with one deliberate change: `protected` rather than package-private, so a
- // subclass in a different package (dev.iris.android.demo.IrisView) can pass
- // it to the window-insets native call android-view itself has no hook for --
- // see iris/src/android/insets.rs's doc comment for why that call exists at
- // all. No other line differs from upstream.
+ // Vendored from android-view bec6c62. The only local change is `protected`,
+ // allowing IrisView to forward insets through this native peer.
protected final long mViewPeer;
final InputMethodManager mInputMethodManager;
diff --git a/app-rust/examples/phone.rs b/app-rust/examples/phone.rs
index 8781926..c22037a 100644
--- a/app-rust/examples/phone.rs
+++ b/app-rust/examples/phone.rs
@@ -1,39 +1,6 @@
-//! Layer 2 of docs/RUST.md's "Three test layers": the fixture-backed
-//! transcript screen in a phone-shaped window, for looking at.
-//!
-//! iris/run-headless.sh phone --phone --shot /tmp/phone.png -- -p transcript-fixture
-//!
-//! `--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 (`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
-//! data.
-//!
-//! `--message TEXT` (through `RUN_HEADLESS_ARGS`) starts with that text
-//! already in the composer, `\n` for a newline -- the composer's grown
-//! and overflowing states are otherwise unreachable here, since this
-//! window has no keyboard to type into (UI_RULES.md's "check the states
-//! you can't see by default"). `--typed TEXT` *enters* the same text
-//! instead, one character per 100ms: laying the composer out from
-//! scratch and growing one already on screen are different cases, and
-//! only the second reproduced the caret landing in the bar's padding
-//! (decided 2026-09-08).
-//!
-//! No server: `transcript-fixture` embeds the transcript. Colour,
-//! spacing, type and anything a person has to *see* is answered here;
-//! anything with an assertion behind it belongs in `tests/
-//! phone_screen.rs` one layer down.
-
use iris::prelude::*;
use winit::{dpi::PhysicalSize, window::WindowAttributes};
-/// The `--ime PX` argument: the bottom inset a keyboard would report,
-/// applied after the first frame the way Android's `on_insets_changed`
-/// does. The composer's keyboard-open layout is otherwise unreachable
-/// here, and it is where its mask went wrong before (see
-/// `ActiveData::own_mask`).
fn ime_argv() -> Option {
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
@@ -44,8 +11,6 @@ fn ime_argv() -> Option {
None
}
-/// The `--message TEXT` argument, with `\n` taken as a newline so a
-/// multi-line message survives one shell word.
fn message_argv() -> Option {
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
@@ -56,12 +21,6 @@ fn message_argv() -> Option {
None
}
-/// The `--typed TEXT` argument: the same text as `--message`, but
-/// *entered* rather than preloaded -- one insertion per 100ms, into a
-/// focused field, the way a person types. The two are different cases
-/// for layout: `--message` is laid out from scratch on the first frame,
-/// while this grows an already-drawn composer, which is the path
-/// Iris's 2026-09-08 phone report is about.
fn typed_argv() -> Option {
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
@@ -127,9 +86,6 @@ impl DefaultAppState for Client {
}
Some(opened.screen)
}
- // On screen rather than a panic: this window exists to be
- // looked at, and "the fixture stopped folding" is something
- // to read, not a process that vanished (UI_RULES.md).
Err(message) => {
let text = wtext(format!("Couldn't fold the bench fixture: {message}"))
.color(Color::WHITE)
diff --git a/app-rust/examples/transcript.rs b/app-rust/examples/transcript.rs
index a90cd23..a33bf8a 100644
--- a/app-rust/examples/transcript.rs
+++ b/app-rust/examples/transcript.rs
@@ -1,18 +1,3 @@
-//! I5's desktop proof: the transcript screen built from synthetic
-//! `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
-//! transcript-ui` with a real compositor.
-//!
-//! The rows exercise every one of the seven "hard to get back" behaviours
-//! this box's markdown/selection work is meant to show: a heading, bold,
-//! italic, an inline code span, a link, a fenced code block (rich inline
-//! text), a multi-message conversation (bottom-anchored virtualised list),
-//! and a three-call tool run (collapsed by default -- tap it, or drive it
-//! with `ui-trace record --do "tap 'Tools'"` on Android, to prove
-//! hold-the-edge expand).
-
use ai_app::client::QuestionOption;
use ai_app::client::transcript_fold::{QuestionCard, TranscriptItem, TranscriptRow as FoldedRow};
use iris::prelude::*;
@@ -44,17 +29,10 @@ fn msg(seq: u64, from_user: bool, text: &str) -> FoldedRow {
})
}
-/// One tool call. `result` is `None` for a call with no result yet and
-/// `Some((output, failed))` for one that answered.
fn tool_call(id: &str, tool: &str, input: &str, result: Option<(&str, bool)>) -> TranscriptItem {
tool_call_in("run1", id, tool, input, result)
}
-/// The same, in a named run. Two runs in one transcript must not share a
-/// `run_id`: it is the row's identity in the list (`row::row_key`), and
-/// two rows under one key is the duplicate-key fault AGENTS.md's
-/// "Importing" section describes. Here it made two rows swap cached
-/// heights and draw at each other's boxes.
fn tool_call_in(
run: &str,
id: &str,
@@ -76,7 +54,6 @@ fn tool_call_in(
}
}
-/// A call stopped on the reader: one unanswered permission question.
fn asking(id: &str, tool: &str, input: &str) -> TranscriptItem {
let mut call = tool_call_in("run2", id, tool, input, None);
if let TranscriptItem::ToolRun { asks, .. } = &mut call {
@@ -104,8 +81,6 @@ fn asking(id: &str, tool: &str, input: &str) -> TranscriptItem {
call
}
-/// Longer than the card's own cap, so the "Show all N lines" control is on
-/// screen in the expanded shot.
fn long_output() -> String {
(0..200)
.map(|i| format!("test ai_app::ui::case_{i} ... ok"))
@@ -125,12 +100,6 @@ fn synthetic_rows() -> Vec {
false,
"# Sure\n\nHere's a [link to the repo](https://example.com/ai-app-2) and a fenced block:\n\n```rust\nfn main() {\n println!(\"hi\");\n}\n```",
),
- // Every state a tool card has to draw, in one run (P1b): a call
- // that worked, one the tool reported as failed, one whose result
- // never arrived, and one still running. The last two look the same
- // in the events -- an empty output and `done: false` -- and are
- // told apart only by whether the session is still working, which
- // is what `TranscriptScreen::set_session_working` says.
FoldedRow::Tools(vec![
tool_call(
"t1",
@@ -149,9 +118,6 @@ fn synthetic_rows() -> Vec {
),
tool_call("t3", "Grep", r#"{"pattern": "fn fold_event"}"#, None),
]),
- // A lone call is a card too rather than a group of one -- and this
- // one carries the kilobyte output a collapsed card must not lay
- // out.
FoldedRow::Single(tool_call(
"t5",
"Bash",
@@ -159,19 +125,10 @@ fn synthetic_rows() -> Vec {
Some((&long_output(), false)),
)),
msg(6, true, "Looks good, thanks!"),
- // 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
- // fixture carries (`app/bench-fixture/generate.py`), so the two
- // screenshots differ only in the renderer; the list and the quote
- // are extra, because the fixture has neither.
msg(7, false, BLOCK_SAMPLER),
]
}
-/// One of each markdown block, for the P1a screenshot pair. See
-/// [`synthetic_rows`].
const BLOCK_SAMPLER: &str = "\
## What changed
@@ -181,7 +138,6 @@ iris measure iris scroll call transcript layout *cursor* context, and a \
```rust
fn fold_event(items: Vec- , seq: u64) -> Vec
- {
- // a comment worth keeping: this is the fold the app's own screen runs
let mut out = items;
out.push(Item::new(seq));
out
@@ -208,10 +164,6 @@ impl DefaultAppState for Client {
_: Proxy,
) -> Self {
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
- // above it (I3's `push_back`/`snap_end`).
screen.push_row(
rsc,
&FoldedRow::Single(TranscriptItem::CommandRow {
@@ -219,11 +171,6 @@ impl DefaultAppState for Client {
text: "clear".into(),
}),
);
- // A second run at the live end, so the *running* state is on
- // screen too. It cannot share a row with "no result": the two are
- // the same events and are told apart only by whether the session
- // is working, which is a property of the row rather than of the
- // call (`TranscriptScreen::set_session_working`).
screen.push_row(
rsc,
&FoldedRow::Tools(vec![
@@ -242,12 +189,6 @@ impl DefaultAppState for Client {
Some(("error: unused variable `x`", true)),
),
tool_call_in("run2", "t8", "Glob", r#"{"pattern": "**/*.rs"}"#, None),
- // Waiting on a permission, so this card is drawn *open*
- // whatever the reader last chose -- the command is the
- // thing being decided, and a row saying only "Bash"
- // cannot be decided on. It is also how the expanded card
- // (input block, output block, timeout) gets into the
- // screenshot without a finger.
asking(
"t9",
"Bash",
@@ -256,9 +197,6 @@ impl DefaultAppState for Client {
]),
);
screen.set_session_working(rsc, true);
- // The expanded picture has no other way to be looked at on a
- // machine with no display and no finger -- see `run-headless.sh`
- // and docs/RUST.md's P1b box.
if std::env::var_os("IRIS_TOOLS_EXPANDED").is_some() {
assert!(
screen.expand_tail_tools(rsc, true),
diff --git a/app-rust/rust-toolchain.toml b/app-rust/rust-toolchain.toml
index 7fd6ab2..303e39c 100644
--- a/app-rust/rust-toolchain.toml
+++ b/app-rust/rust-toolchain.toml
@@ -1,11 +1,4 @@
-# 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"
+channel = "nightly"
components = ["clippy", "rustfmt"]
targets = ["aarch64-linux-android", "x86_64-linux-android"]
diff --git a/app-rust/src/android/app_log.rs b/app-rust/src/android/app_log.rs
index 213aec0..10a82ad 100644
--- a/app-rust/src/android/app_log.rs
+++ b/app-rust/src/android/app_log.rs
@@ -1,17 +1,6 @@
//! The platform half of this app's logging: what
//! `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 --
-//! is in `client-core`, shared with the desktop app (AGENTS.md's sharing
-//! rule).
-//!
-//! **Why an app carries its own log at all**: Iris tests these builds on a
-//! GrapheneOS phone with no `adb`, and Android forbids one app reading
-//! another's `logcat`. Nothing outside this process can recover what it
-//! wrote, so the process keeps a copy -- and hands it to Dev Updater on
-//! the same phone through `devlog`'s `ContentProvider`. See
-//! decided 2026-09-07.
use crate::client::log_ring::{self, LogRing};
@@ -43,23 +32,11 @@ pub fn install(max_level: log::LevelFilter) {
install_panic_hook();
}
-/// The process's ring -- what `Copy report` appends, what the diagnostics
-/// pane counts, and what `devlog`'s provider hands to Dev Updater.
pub fn ring() -> &'static LogRing {
log_ring::process_ring()
}
-/// Only the bench build has a diagnostics pane to put this in; the
-/// transcript build's screen is the app's own and has no room for a
-/// readout. Gated rather than left dead so the build stays warning-clean.
#[cfg(feature = "bench")]
-/// Two lines for the diagnostics pane: how much of this app's log is held,
-/// and where it can be read from.
-///
-/// The second names the provider's authority rather than saying "logging
-/// is on", so a screenshot of this pane is enough to tell whether the
-/// 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::android::devlog::authority() {
Some(authority) => format!("devlog provider: content://{authority}"),
@@ -88,28 +65,11 @@ const CRASH_FILE: &str = "last-panic.txt";
/// unbounded paste would evict the run that is actually being watched.
const CRASH_CONTEXT_LINES: usize = 80;
-/// The target the replayed context lines carry, so a reader can tell a
-/// line from the run that died from one this run wrote. They keep their
-/// original timestamp and level inside the text, which is why the level
-/// they are re-pushed at is not meaningful and the target has to be.
const PREVIOUS_RUN_TARGET: &str = "previous_run";
static CRASH_PATH: std::sync::OnceLock = std::sync::OnceLock::new();
-/// Installs a `log`-level panic hook, so a panic's message and location
-/// reach the ring and `logcat` rather than only the tombstone.
-///
-/// **Why this is needed at all**: these builds are `panic = "abort"`
-/// (`Cargo.toml`), and the default hook writes to `stderr` plus
-/// `android_set_abort_message` -- the crash report. Iris runs these on a
-/// phone with no `adb`, so the crash report is exactly the surface she
-/// cannot read, and an `assert!` that fired said nothing anywhere she
-/// could see it. Routing it through `log::error!` puts it in front of
-/// `android_logger` *and* in the ring `devlog`'s provider hands to Dev
-/// Updater.
-///
-/// The ring is memory only, so after an abort the process that holds it
-/// is gone -- hence the file half. [`set_crash_dir`] replays it.
+/// Copies aborting panics into the device-readable log ring.
fn install_panic_hook() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
@@ -117,8 +77,6 @@ fn install_panic_hook() {
Some(at) => format!("{}:{}:{}", at.file(), at.line(), at.column()),
None => "an unknown location".to_string(),
};
- // `info`'s own `Display` repeats the location and a newline;
- // the payload alone keeps this to the one line the ring wants.
let message = info.payload_as_str().unwrap_or("Box");
let line = format!("iris panic at {where_at}: {message}");
log::error!("{line}");
@@ -128,8 +86,6 @@ fn install_panic_hook() {
// `set_crash_dir`.
let context = ring()
.try_tail_text(CRASH_CONTEXT_LINES)
- // Said rather than left empty, so "the ring was locked as
- // we died" cannot be read as "nothing had been logged".
.unwrap_or_else(|| {
"(the log ring was locked as this run died; no context)".to_string()
});
@@ -167,13 +123,6 @@ pub fn set_crash_dir(dir: &std::path::Path) {
/// Puts a previous run's report back in the ring: its context lines in
/// the order they happened, then the panic itself.
-///
-/// Chronological, so the Runtime tab reads as one story -- the lines that
-/// led to the crash, then the crash, then this run. The context goes in
-/// through `LogRing::push` rather than through `log::info!` so it is not
-/// stamped with this run's clock: each line already carries the time and
-/// level it was written at, and [`PREVIOUS_RUN_TARGET`] is what says
-/// whose run it was.
fn replay_crash(report: &str) {
let (panic_line, context) = report.split_once('\n').unwrap_or((report, ""));
for line in context.lines().filter(|line| !line.is_empty()) {
diff --git a/app-rust/src/android/bench_client.rs b/app-rust/src/android/bench_client.rs
index 3127bd6..698a302 100644
--- a/app-rust/src/android/bench_client.rs
+++ b/app-rust/src/android/bench_client.rs
@@ -1,22 +1,3 @@
-//! P0's iris half (docs/RUST.md's P0 box, docs/AGENTS.md's "The rigs"):
-//! the same fixture, scroll loop and streaming phase the Compose `bench`
-//! build type's `BenchRun.kt`/`BenchFixture.kt` drive, run here against
-//! `transcript-ui`'s real screen with no server -- a frame-time comparison
-//! that measures the renderer rather than the data or the network.
-//!
-//! **Reuses `transcript_client.rs`'s shape** (folded items, the same
-//! `TranscriptScreen::apply` incremental update on every event) with the
-//! network half replaced by the checked-in fixture. Reading that fixture
-//! and folding it into a screen is **`transcript-fixture`'s** job, not
-//! this file's -- the same crate the headless harness and the
-//! phone-shaped desktop window open, so all three measure one screen
-//! (AGENTS.md's sharing rule; moved out of here 2026-09-07). The tail is
-//! replayed one at a time through `fold_event` -- the same fold path a
-//! live SSE reply arrives on -- by the "Run benchmark" control below.
-//! Streaming through `apply` rather than a full rebuild per event is what
-//! this file exists to measure -- see docs/RUST.md's P0 box for the
-//! before/after report.
-
use crate::android::bench_jni::PlatformHandle;
use crate::client::transcript_fold::{TranscriptItem, fold_event};
use android_view::jni::{JavaVM, objects::GlobalRef};
@@ -27,35 +8,16 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
-/// RUST.md's "Benchmark v2" spec, written once so both apps' bench clients
-/// implement the identical four phases -- see that box before changing any
-/// constant here, since a mismatch would make the two reports stop
-/// measuring the same thing while still looking like they do.
const STREAM_EVENTS_PER_SEC: u64 = 20;
const STREAM_SECONDS: u64 = 20;
-/// Kept only so this phase's own label text still reads "scroll: 6 cycles
-/// (24 swipes, legacy tween)" the way `BenchRun.kt`'s v2 report does --
-/// `docs/bench/compose-phone-v2-2026-09-06.md`'s own report shows this
-/// exact line even though the swipe loop it names no longer runs there
-/// either (the fling phase replaced it); nothing here drives an actual
-/// swipe with these any more.
const LEGACY_CYCLES: usize = 6;
-/// Fling phase (v2): a real fling through `Scroll::fling`, not a tween --
-/// Iris's ask was that it "travel way faster" than the v1 swipe, and a
-/// tween can never exceed the distance/time it is given while a real
-/// fling decays from an initial velocity the way a finger flick does.
-/// 12,000 px/s matches `BenchRun.kt`'s own constant exactly.
const FLING_VELOCITY_PX_S: f32 = 12_000.0;
const FLING_COUNT: usize = 8;
const FLING_SETTLE_CAP_MS: u64 = 3_000;
const FLING_PAUSE_MS: u64 = 300;
-/// Type phase (v2): long, multisyllabic words so the composer actually
-/// wraps and the transcript above it is pushed upward, typed and deleted
-/// one character per `TYPE_CHAR_MS`. Exactly `BenchRun.TYPE_TEXT` --
-/// verified 600 characters by `type_text_is_exactly_600_characters` below.
const TYPE_TEXT: &str = "Benchmarking this transcript screen requires unusually long, \
multisyllabic words so wrapping and reflow are properly exercised: internationalization, \
counterproductiveness, disproportionately, incomprehensibility, deinstitutionalization, \
@@ -66,72 +28,29 @@ keyboard-adjacent box, which is exactly what a real reader typing a long message
happening now!!!";
const TYPE_CHAR_MS: u64 = 50;
-/// Keyboard phase (v2): five show/hide cycles, a second apart, matching
-/// `BenchRun.kt`'s `KEYBOARD_CYCLES`/`KEYBOARD_SHOW_WAIT_MS`/
-/// `KEYBOARD_HIDE_WAIT_MS`.
const KEYBOARD_CYCLES: usize = 5;
const KEYBOARD_WAIT_MS: u64 = 1_000;
-/// How often this file *asks a question of* the running app -- polls for
-/// a `ctx.update` closure's answer, or for a fling to have settled.
-///
-/// It is not an animation cadence and nothing on screen moves at this
-/// rate: the frame loop advances animations once per frame at the
-/// display's own refresh (`UiData::tick_animations`). It used to be both,
-/// and that is the defect Iris reported on 2026-09-08 -- see
-/// `wait_for_fling_settle`.
const POLL_MS: u64 = 16;
-/// How much of the screen a *filled* benchmark report may take before it
-/// scrolls instead of growing -- roughly a third of a phone screen, the
-/// share the pane used to reserve unconditionally. An empty report takes
-/// nothing at all; see `new`'s comment at the tree it is used in.
const REPORT_MAX_HEIGHT_DP: f32 = 260.0;
pub struct BenchClient {
ui_state: AndroidUiState,
content: WeakWidget,
report_display: WeakWidget,
- /// The top button row, in a `WidgetPtr` slot rather than added
- /// directly (like `content`) so `on_insets_changed` can swap in a
- /// version padded for the status bar once insets are known -- RUST.md's
- /// 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,
screen: Option,
items: Vec,
- /// The events not yet streamed -- consumed by `start_benchmark`'s own
- /// clone, kept here only as the source a second run would need (the
- /// button can be pressed more than once; `running` just stops overlap,
- /// not repeat).
stream_tail: Vec,
platform: Option>,
last_report: Option,
running: bool,
- /// The keyboard phase's own confirmation channel -- updated from
- /// `on_insets_changed` (the platform's own answer for whether the IME
- /// is actually visible, per `WindowInsets::ime_bottom`), read from the
- /// benchmark's spawned task via the shared `Arc>` rather than
- /// `ctx.update`, since neither side needs the widget tree for this.
ime_state: Arc>,
- /// Edge-triggers the keyboard diagnostics capture below -- set on the
- /// first `on_insets_changed` where `ime_bottom > 0.0`, cleared on the
- /// first where it is not, so opening the keyboard fires this once
- /// rather than on every insets update while it stays open (a rotation
- /// or a status-bar change with the keyboard already up would otherwise
- /// re-fire it).
keyboard_was_visible: bool,
- /// The status-bar inset `top_bar` was last padded by -- see
- /// `on_insets_changed`'s own comment for why this guards the rebuild.
last_top_pad: f32,
}
-/// See `BenchClient::ime_state`'s doc. `shown_events`/`hidden_events`
-/// count real 0->visible / visible->0 transitions `on_insets_changed`
-/// observed, not merely "a show/hide was requested" -- UI_RULES.md: never
-/// present an inferred value as a measured one. `run_keyboard_phase` reads
-/// the counters before and after asking for a toggle and calls it
-/// confirmed only if the count moved.
#[derive(Default)]
struct ImeState {
visible: bool,
@@ -157,10 +76,6 @@ fn placeholder(rsc: &mut Rsc, message: &str) -> StrongWidget {
.any()
}
-/// `getrusage(RUSAGE_SELF)`'s user+system time, in ms -- `None` only if
-/// the syscall itself fails, which UI_RULES.md's "never present an
-/// inferred value as a measured one" says to keep apart from a real (and
-/// here, impossible) zero.
fn process_cpu_ms() -> Option {
// SAFETY: `rusage` is a plain-old-data struct `getrusage` fully
// initialises on success; on failure it is never read.
@@ -175,9 +90,6 @@ fn process_cpu_ms() -> Option {
}
}
-/// `VmHWM` from `/proc/self/status` -- the process's peak RSS since it
-/// started, in kB. Same source `BenchRun.kt`'s `peakRssLine` reads, so the
-/// two reports' numbers mean the same thing.
fn peak_rss_kb() -> Option {
std::fs::read_to_string("/proc/self/status")
.ok()?
@@ -192,12 +104,6 @@ fn battery_line(samples: &[i32]) -> String {
return " battery current: unavailable on this device".to_string();
}
let mean = samples.iter().map(|&v| v as i64).sum::() / samples.len() as i64;
- // `min`/`max` are guarded by the `is_empty` check above, three lines
- // up -- pairing the `Option` unwraps with the emptiness check right
- // here (rather than two statements apart, with `mean` in between
- // reading the same slice) is what keeps a future reorder from
- // separating the guard from what it protects (review, 2026-09-06
- // finding 7).
let (Some(min), Some(max)) = (samples.iter().min(), samples.iter().max()) else {
unreachable!("samples is non-empty, checked above");
};
@@ -226,22 +132,6 @@ impl AndroidAppState for BenchClient {
let top_bar = WidgetPtr::new().add(rsc);
let controls = bench_controls(rsc, 0.0);
top_bar(rsc).set(controls);
- // The report pane is sized to whatever report it is holding, not
- // to a share of the window: `rest(1)` here reserved a third of
- // the screen for an *empty* `TextEdit` at every launch, which is
- // what Iris's 2026-09-06 11:39 phone report described as "the app
- // does not start with keyboard spacing correct" -- the composer
- // two thirds down with black below it, nothing to do with the IME
- // inset (measured: `iris insets:` reports bottom=63 ime_bottom=0
- // at launch, while the `Message` field's own box sat 789px above
- // the bottom of a 2282px surface -- exactly this pane's third).
- // Capped and scrollable so a long report cannot take the screen
- // back over, the same idiom `composer.rs` uses for the field.
- // Above the transcript, not below it: the report is what the
- // header's own "Run benchmark" button produces (UI_RULES.md --
- // results appear where the action was started), and a pane under
- // the composer would eat the navigation-bar clearance
- // `set_bottom_inset` gives it.
let tree = (
top_bar,
report_display
@@ -254,10 +144,6 @@ impl AndroidAppState for BenchClient {
.any();
ui_state.set_root(tree);
- // Startup log line (RUST.md's P0 box, "log once at startup ... the
- // number of font families found, the default family resolved"):
- // what font discovery actually found on this device, before
- // anything is drawn.
let font = rsc.ui.text.font_diagnostics();
log::info!(
"iris fonts: {} families found, default={:?} mono={:?}, resolved regular={:?} \
@@ -310,47 +196,6 @@ impl AndroidAppState for BenchClient {
false
}
- /// Pads the top button row by the status-bar inset -- see `top_bar`'s
- /// field comment. Rebuilds the row rather than mutating a stored
- /// `Padding` in place, since nothing here holds a handle to one --
- /// but **only when `insets.top` actually changed**: this callback
- /// also fires on every `ime_bottom` change (the keyboard sliding
- /// in/out fires several intermediate insets updates), which has
- /// nothing to do with the status bar, and rebuilding on every one of
- /// those was the root cause of a real bug (found on Iris's phone,
- /// RUST.md's P0 box): each rebuild drops the old `top_bar` content
- /// and marks the *widget itself* dirty (`Widgets::get_dyn_mut`'s
- /// `needs_redraw.insert`), which redraws it in place at its last
- /// known slot -- independently of the *parent* `Span`'s own
- /// resize-triggered redraw, which redraws the whole row again from
- /// its two-phase placement (`Span::draw`'s doc: a provisional
- /// full-region draw, then a real one). A `.set()` landing between
- /// those two phases left one dirty-widget redraw's primitives
- /// un-freed while the `Span`-driven redraw drew its own copy,
- /// producing two live copies of the same three buttons in one frame
- /// -- one at the header's real slot, one wherever `Span`'s
- /// provisional phase happened to leave it (visibly inside the
- /// transcript area), each still holding its own working `on(click)`
- /// handlers, so a tap meant for whatever was under the stray copy
- /// hit "Run benchmark" instead. Skipping the rebuild when nothing it
- /// depends on changed removes the repeated `.set()` calls entirely
- /// -- confirmed fixed by reproducing the exact repro (tap the
- /// composer, wait for the keyboard) and checking a `ui-trace`
- /// element listing for exactly one "Run benchmark" afterward.
- ///
- /// Also two things downstream of the same `ime_bottom` transition:
- /// **the keyboard phase's own confirmation signal** (`ime_state`'s
- /// doc -- the platform's own answer for whether the IME actually
- /// opened or closed, rather than assumed from having called
- /// `show_ime`/`hide_ime`), and **the trigger for the keyboard
- /// diagnostics capture** (RUST.md's P0 box): the IME resizing the
- /// surface is exactly the case a previous commit found wiped text,
- /// and Iris needs a way to get a report off the phone even if that
- /// (or some other keyboard-triggered regression) is still happening
- /// on the build she is holding -- `capture_keyboard_diagnostics`
- /// below fires ~500ms after the keyboard becomes visible, once per
- /// keyboard opening, and shows its report in a plain overlay view
- /// that draws independently of whatever iris itself is doing.
fn on_insets_changed(
&mut self,
rsc: &mut AndroidRsc,
@@ -362,12 +207,6 @@ impl AndroidAppState for BenchClient {
(self.top_bar)(rsc).set(controls);
}
- // The composer bar sits directly on whichever of the IME or the
- // navigation bar is currently the bottom of usable space -- see
- // `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.
if let Some(screen) = &self.screen {
screen
.composer
@@ -407,13 +246,6 @@ impl AndroidAppState for BenchClient {
}
}
-/// How long to wait after the keyboard becomes visible before capturing
-/// diagnostics -- long enough that the resize, the reported wipe (if it is
-/// still happening) and a couple of frames have all had time to land, per
-/// AGENTS.md's "so that operations that finish in milliseconds have states
-/// on the way that nothing can observe" reasoning applied the other way:
-/// this wants to observe the state *after* the transition settles, not
-/// mid-flight.
const KEYBOARD_DIAGNOSTICS_DELAY_MS: u64 = 500;
type Rsc = AndroidRsc;
@@ -452,39 +284,8 @@ const HEADER_SURFACE: UiColor = UiColor::new(28, 28, 34, 255);
/// exposing the unadded builder for a caller to `.pad()` itself, because
/// naming that builder's type at each call site is more machinery than a
/// top-of-screen padding number is worth.
-///
-/// **Backed by an opaque rect the full size of the row, not just the three
-/// buttons.** Iris's phone report (docs/RUST.md's P0 box, screenshots on
-/// build a9232ac): "the header buttons have nothing behind them and
-/// overlap the transcript text" -- before this, only each button's own
-/// `rect(...)` painted anything, so the gaps between and around them (and
-/// the status-bar strip above them) showed whatever was one layer back
-/// (`CLEAR_COLOR`, black), and the row's true height was three
-/// physical-pixel-sized (`abs`, not `dp`) button boxes rather than the
-/// density-correct size the transcript below was already using post-P0 --
-/// exactly what reads as "overlap" once the two disagree. Fixed two ways
-/// together: a `HEADER_SURFACE` rect stacked behind the whole row (this
-/// function), and every size below moved from a bare number (physical
-/// pixels) to `dp(...)` (IRIS_TODO.md's density-independent length unit),
-/// so the row's reserved height in the outer `Span::DOWN`
-/// (`AndroidAppState::new`) matches what is actually painted.
-/// The size every label in the header row is drawn at.
-///
-/// One constant for all four rather than a number per button, because the
-/// whole row has to be sized together. Adding the trace switch made four
-/// controls too wide for one row at the size three had used (18), and an
-/// earlier pass shrank this constant to 13 to make them fit -- exactly
-/// what UI_RULES forbids ("never shrink text to make it fit": a label a
-/// different size from its neighbours elsewhere in the app for a reason
-/// the reader cannot see). The fix is [`bench_controls`]'s two rows
-/// instead, which leaves room to put this back. Whoever adds a fifth
-/// control reconsiders the row split, not this number.
const HEADER_TEXT: f32 = 18.0;
-/// The height of one row of header controls, in dp. `bench_controls` now
-/// stacks two of these, so this is the one number to change if a control's
-/// own padding ever changes instead of `dp(56)` and `dp(112)` needing to
-/// be kept in sync by hand.
const HEADER_ROW_HEIGHT_DP: f32 = 56.0;
fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
@@ -548,11 +349,6 @@ fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
// session fills the 2000-line ring in seconds, so "is it on right
// now" is the question somebody has while looking at a log that is
// either full of trace or has none.
- //
- // The visible text carries the state and the accessibility label does
- // not, deliberately: the label is also what `run-bench.sh` taps by
- // name, and a control that renames itself when pressed is one no
- // script can find twice.
let tracing = iris::diagnostics::trace_enabled();
let trace_rect = rect(if tracing {
Color::rgb(90, 70, 30)
@@ -576,12 +372,6 @@ fn bench_controls(rsc: &mut Rsc, top_pad: f32) -> StrongWidget {
.pad(dp(8))
.add(rsc);
- // Two rows rather than one: four controls at the restored `HEADER_TEXT`
- // no longer fit a 1080px-wide row (that was the shrink this replaces --
- // see the constant's own doc). Grouped by what they act on: the first
- // row starts a benchmark and copies its result; the second is the
- // diagnostics pane and the switch that decides what it will contain
- // next time.
let row1 = (run, copy).span(Dir::RIGHT).add(rsc);
let row2 = (diagnostics, trace).span(Dir::RIGHT).add(rsc);
let buttons = (row1, row2).span(Dir::DOWN).add(rsc);
@@ -607,12 +397,6 @@ impl BenchClient {
self.screen = Some(screen);
}
- /// RUST.md's P0 box: "a named `Diagnostics` control ... with 'copy this
- /// and send it to Iris'." Fills `report_display` (the same TextEdit the
- /// benchmark report uses) rather than a separate widget, so the
- /// existing "Copy report" button and clipboard path work on whichever
- /// text is currently shown -- `last_report` is what `copy_report` reads,
- /// so it's set here too rather than adding a second copy path.
fn show_diagnostics(&mut self, rsc: &mut Rsc) {
let report = self.diagnostics_text(rsc);
self.report_display.edit(rsc).set(&report);
@@ -621,11 +405,6 @@ impl BenchClient {
/// Turns the `iris::input`/`iris::frame` trace on or off, redraws the
/// switch that says so, and shows the pane that now reports it.
- ///
- /// Showing the pane is the point rather than a convenience: this is a
- /// control whose whole effect is on what a *later* report says, so
- /// putting the state on screen at the moment of the press is the only
- /// thing that distinguishes it from a button that did nothing.
fn toggle_trace(&mut self, rsc: &mut Rsc) {
let on = !iris::diagnostics::trace_enabled();
iris::diagnostics::set_trace(on);
@@ -652,10 +431,8 @@ impl BenchClient {
Some(renderer) => renderer.diagnostics_report(&font, &frame_report),
None => "iris diagnostics: no renderer yet (no surface)".to_string(),
};
- // The insets line goes in the pane, not just the log: Iris has no
- // logcat on her phone, and "the keyboard does not push the
- // composer up" cannot be told from "the listener never fired"
- // without it (`AndroidUiState::insets_report`).
+ // Insets must be visible without adb so a missing callback can be
+ // distinguished from a callback reporting zero IME height.
format!(
"{renderer}\n{}\n{}\n{}\n{}",
trace_line(
@@ -672,40 +449,11 @@ impl BenchClient {
)
}
- /// The keyboard's own diagnostics capture -- see `on_insets_changed`'s
- /// doc comment. **Logged only.** It used to also copy the report to
- /// the clipboard unprompted and put it in the shell's overlay view,
- /// from when the keyboard-inset callback was not firing at all and a
- /// report could not be got off the phone any other way. Both are gone
- /// as of 2026-09-06: the callback fires reliably now (edge-to-edge,
- /// `MainActivity.java`), and the overlay covered the whole screen on
- /// *every* keyboard open with its own Copy/Close buttons underneath
- /// the keyboard, so it could not be dismissed -- an interruption for
- /// something nobody asked for, over an app you are trying to type
- /// into (UI_RULES.md). The named `Diagnostics` button still shows the
- /// same text on demand, and `iris surface:`/`iris insets:` (view.rs)
- /// carry the lifecycle a `logcat` pull actually needs.
fn capture_keyboard_diagnostics(&mut self, rsc: &mut Rsc) {
let report = self.diagnostics_text(rsc);
log::info!("iris keyboard diagnostics:\n{report}");
}
- /// Always copies something, and never depends on `Diagnostics` or
- /// `Run benchmark` having been pressed first (docs/IRIS_TODO.md,
- /// 2026-09-07 night: "the copy report button seemed impossible to hit
- /// until I hit the diagnostics one" -- it was silently declining
- /// instead of reporting where it had failed, the UI_RULES failure "a
- /// failure is reported where it happened"). With no benchmark run yet,
- /// it copies the diagnostics pane's own text instead, with a first
- /// line saying so -- `diagnostics_text` needs no prior button press
- /// either, so this is never actually empty-handed.
- /// The report carries **no copy of the app log** (removed 2026-09-08,
- /// Iris: "please remove the app log from the diagnostics. Those can be
- /// obtained through dev updater now"). Dev Updater's Runtime tab reads
- /// the same ring through `devlog`'s provider, and the diagnostics
- /// pane's own `devlog provider:` line names the authority to read it
- /// from -- so what is left here is the measurement, not a second copy
- /// of something already reachable.
fn copy_report(&mut self, rsc: &mut Rsc) {
let Some(platform) = &self.platform else {
log::info!("iris bench report: no platform handle, can't reach the clipboard");
@@ -725,10 +473,6 @@ impl BenchClient {
}
}
- /// RUST.md's "Benchmark v2": fling, then stream (unchanged from v1),
- /// then type, then keyboard, then the report -- run in-process for the
- /// same reason `BenchRun.kt`'s own doc gives (no usable system tracing
- /// on a real phone, no agent that can drive one).
fn start_benchmark(&mut self, rsc: &mut Rsc) {
if self.running {
log::info!("iris bench report: already running");
@@ -742,11 +486,6 @@ impl BenchClient {
let platform = self.platform.clone();
let stream_tail = self.stream_tail.clone();
let ime_state = self.ime_state.clone();
- // What the platform *says*, kept apart from what the run measured
- // -- see where the two are resolved below. A phone that varies its
- // refresh rate answers with whichever mode it is in when asked, so
- // this alone judged a 120Hz run against a 60Hz budget (Iris's
- // phone, 2026-09-09).
let platform_hz = platform.as_ref().and_then(|p| p.refresh_rate_hz());
let cpu_start = process_cpu_ms();
// Read at the start as well as the end, because the switch is on
@@ -804,13 +543,6 @@ impl BenchClient {
ctx.update(move |state: &mut BenchClient, rsc| {
state.running = false;
let now = Instant::now();
- // **The larger of the two, because each can only be wrong
- // one way.** The platform under-reports a display that
- // varies its rate (60 for a run that sustained 120 on
- // Iris's phone), and the sustained rate is a floor -- an
- // app that cannot keep up says nothing about the panel.
- // Printed together whenever they disagree, so the
- // resolution is visible rather than silent.
let drawn_hz = state.android_state().frame_report.sustained_frame_hz();
let refresh_hz = match (drawn_hz, platform_hz) {
(Some(d), Some(p)) => d.max(p),
@@ -934,16 +666,6 @@ where
}
}
-/// Phase 1: starting pinned at the newest end, `FLING_COUNT` flings away
-/// from it (toward older messages) through `Scroll::fling`, then
-/// `FLING_COUNT` back. Outward is *positive* in `Scroll::scroll`'s
-/// convention, which is the finger's: a finger dragged down the screen
-/// brings earlier content into view. It was negative here until
-/// 2026-09-08, when the transcript's scroll position moved out of the
-/// `LazySpan` -- whose anchor offset ran the other way -- and into the
-/// `ScrollArea` around it. The two apps' *travel* is directly comparable
-/// whichever way the signs run, because both report it as a row index plus
-/// a pixel offset rather than a signed distance.
async fn run_fling_phase(
ctx: &mut iris::task::TaskCtx,
redraw: &Arc,
@@ -988,9 +710,6 @@ async fn run_fling_phase(
}
let end = read_anchor_position(ctx, redraw).await;
- // Says how the fling was advanced, because that is what changed on
- // 2026-09-08 and a report from before then is not comparable: the
- // phase used to tick the fling itself at ~60Hz.
format!("start={start} outward={outward} end={end} ticked=frame-loop")
}
@@ -1005,32 +724,11 @@ async fn read_anchor_position(
.await
}
-/// Register the scrolling widget with the frame loop, exactly as a
-/// 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, rsc: &mut Rsc) {
let id = scroll.id();
rsc.ui_mut().animate(id);
}
-/// Waits for the fling started above to settle, or for
-/// `FLING_SETTLE_CAP_MS` -- belt-and-suspenders the same way
-/// `BenchRun.kt`'s own `waitForSettle` is, since a fling's own
-/// spline-decided `duration()` already caps how long it can run.
-///
-/// **It observes; it does not drive.** Until 2026-09-08 this loop called
-/// `Scrollable::tick_fling` itself every `POLL_MS`, which advanced the
-/// fling in 16ms steps -- so on Iris's 120Hz phone every second frame
-/// redrew the list at a position it had already drawn, and the benchmark
-/// looked distinctly less smooth than the same list under her finger.
-/// That is what she reported that day, and it was the rig rather than the
-/// renderer: a real fling is ticked once per frame by
-/// `UiData::tick_animations`, from the frame callback. So the bench now
-/// starts the fling the way a gesture does (`fling` + `UiData::animate`)
-/// and polls `is_scrolling` to know when it is over, which makes the
-/// phase measure the same path a finger takes. The poll interval is only
-/// how often the *question* is asked and has no bearing on the animation.
async fn wait_for_fling_settle(
ctx: &mut iris::task::TaskCtx,
redraw: &Arc,
@@ -1050,11 +748,6 @@ async fn wait_for_fling_settle(
}
}
-/// Phase 2, unchanged from v1: pinned to the newest end before streaming
-/// starts (matching `stream-bench.sh`'s "Jump to latest" tap), then
-/// `STREAM_EVENTS_PER_SEC * STREAM_SECONDS` fixture events replayed
-/// through the real `fold_event`/`TranscriptScreen::apply` path. Returns
-/// `(sent, total)`.
async fn run_stream_phase(
ctx: &mut iris::task::TaskCtx,
redraw: &Arc,
@@ -1085,17 +778,10 @@ async fn run_stream_phase(
sent += 1;
tokio::time::sleep(Duration::from_millis(1000 / STREAM_EVENTS_PER_SEC)).await;
}
- // Lets the last few deltas land and draw before the next phase starts
- // -- `BenchRun.kt`'s own closing delay.
tokio::time::sleep(Duration::from_millis(300)).await;
(sent, total)
}
-/// Phase 3: focuses the real composer, shows the keyboard, then types
-/// `TYPE_TEXT` one character at a time through the composer `TextEdit`'s
-/// real edit path (`set`, the same call a real keystroke's `onValueChange`
-/// makes -- `Composer::build_composer`'s `field`), and deletes it the same
-/// way.
async fn run_type_phase(
ctx: &mut iris::task::TaskCtx,
redraw: &Arc,
@@ -1114,9 +800,6 @@ async fn run_type_phase(
if let Some(p) = platform {
p.show_ime();
}
- // Lets focus and the keyboard's opening animation land before typing
- // starts, so the frames this phase records are the wrap/reflow it is
- // measuring, not the keyboard opening -- `BenchRun.kt`'s own delay.
tokio::time::sleep(Duration::from_millis(300)).await;
let mut typed = String::new();
@@ -1145,13 +828,6 @@ async fn run_type_phase(
}
}
-/// Phase 4: `KEYBOARD_CYCLES` show/hide cycles through the shell's own
-/// `InputMethodManager` (`bench_jni.rs`'s `show_ime`/`hide_ime`), each
-/// confirmed by `on_insets_changed`'s real `ime_bottom` transition rather
-/// than assumed from the JNI call having returned -- `ImeState`'s doc.
-/// "keyboard: could not be shown" if the platform never confirms it even
-/// once, per UI_RULES.md ("design the unknown/failed state before the
-/// answer's").
async fn run_keyboard_phase(
ctx: &mut iris::task::TaskCtx,
platform: &Option>,
@@ -1198,10 +874,6 @@ async fn run_keyboard_phase(
mod tests {
use super::TYPE_TEXT;
- /// `BenchRun.kt`'s own `TYPE_TEXT` is verified `.length == 600`; this
- /// is the same string, so it has to match exactly or the two apps'
- /// type phases stop typing the same content -- RUST.md's "Benchmark
- /// v2" spec is one shared string for both.
#[test]
fn type_text_is_exactly_600_characters() {
assert_eq!(TYPE_TEXT.chars().count(), 600);
diff --git a/app-rust/src/android/bench_jni.rs b/app-rust/src/android/bench_jni.rs
index 6fac495..a9fa1f5 100644
--- a/app-rust/src/android/bench_jni.rs
+++ b/app-rust/src/android/bench_jni.rs
@@ -1,24 +1,3 @@
-//! JNI calls the `bench` feature needs that go through the shell's own
-//! Java side rather than anything `iris`/`android-view` already wraps:
-//! `BatteryManager.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW)` for the
-//! per-second battery sample, `ClipboardManager.setPrimaryClip` for the
-//! "Copy report" control (P0's iris half, docs/RUST.md), and -- added for
-//! RUST.md's "Benchmark v2" -- `Display.getRefreshRate()` for the phase
-//! report's real late-frame budget and `InputMethodManager.
-//! showSoftInput`/`hideSoftInputFromWindow` for the keyboard phase. None
-//! of these are part of `android_view::context`'s own `Context`/
-//! `Resources` wrappers (that file's own `// TODO: more methods?`), so
-//! this calls them directly rather than growing that crate's wrapper for
-//! calls this crate alone needs.
-//!
-//! Holds its own `JavaVM` + `GlobalRef` to the view (handed in through
-//! [`iris::android::AndroidAppState::platform_ready`]) so it can attach
-//! whichever thread calls it -- the battery sampler runs on a background
-//! tokio task, not the UI thread the rest of `IrisViewPeer`'s JNI calls
-//! run on. `JavaVM::attach_current_thread` is safe to call from a thread
-//! already attached (the `jni` crate detects it and does not double
-//! attach), so no caller here needs to know or care which thread it is.
-
use android_view::jni::{
JNIEnv, JavaVM,
objects::{GlobalRef, JObject, JValue},
@@ -69,13 +48,6 @@ impl PlatformHandle {
.ok()
}
- /// One sample of `BATTERY_PROPERTY_CURRENT_NOW`, in microamps. `None`
- /// on any JNI failure, on a device with no `BatteryManager` service,
- /// or when the platform itself answers "not supported" -- `0` or
- /// `Integer.MIN_VALUE` are both documented SDK answers for that, and
- /// both would read as a real (and wrong) measurement if folded into an
- /// average rather than named apart. UI_RULES.md: never present an
- /// inferred value as a measured one.
pub fn battery_current_ua(&self) -> Option {
let mut guard = self.vm.attach_current_thread().ok()?;
let env: &mut JNIEnv = &mut guard;
@@ -135,14 +107,6 @@ impl PlatformHandle {
Some(())
}
- /// The display's own refresh rate in Hz (`View::getDisplay()` ->
- /// `Display::getRefreshRate()`), for RUST.md's "Benchmark v2": late
- /// frames are judged against *this* device's real budget, not an
- /// assumed 60Hz -- a 90Hz or 120Hz phone would otherwise call frames
- /// "late" that met their own faster deadline. `None` if the view is
- /// not yet attached to a window (`getDisplay` returns `null`) or the
- /// platform reports a non-positive rate, which is not a real answer
- /// either.
pub fn refresh_rate_hz(&self) -> Option {
let mut guard = self.vm.attach_current_thread().ok()?;
let env: &mut JNIEnv = &mut guard;
@@ -167,20 +131,10 @@ impl PlatformHandle {
if rate > 0.0 { Some(rate) } else { None }
}
- /// `InputMethodManager.showSoftInput(view, 0)` -- the keyboard phase's
- /// own show, called directly rather than through the focus-driven
- /// `pending_show_keyboard` path `android/view.rs` uses for a real tap,
- /// since RUST.md's "Benchmark v2" spec asks for this "through the
- /// shell's InputMethodManager" independent of focus state. `true` only
- /// if the platform itself reports the request succeeded -- whether the
- /// IME actually became visible is confirmed separately, from
- /// `on_insets_changed`, per UI_RULES.md ("never present an inferred
- /// value as a measured one").
pub fn show_ime(&self) -> bool {
self.try_toggle_ime(true).unwrap_or(false)
}
- /// `InputMethodManager.hideSoftInputFromWindow(windowToken, 0)`.
pub fn hide_ime(&self) -> bool {
self.try_toggle_ime(false).unwrap_or(false)
}
diff --git a/app-rust/src/android/devlog.rs b/app-rust/src/android/devlog.rs
index ac80199..cc54620 100644
--- a/app-rust/src/android/devlog.rs
+++ b/app-rust/src/android/devlog.rs
@@ -1,36 +1,11 @@
//! The JNI half of `DevLogProvider`: reading this process's own log ring
//! for a `ContentProvider` that Dev Updater queries.
-//!
-//! **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
-//! (`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
-//! same (its `README.md`, "An app's own log").
-//!
-//! **Everything general stays in `client-core`** (AGENTS.md's sharing
-//! rule). What is here is only what Android forces: the JNI boundary and
-//! the Java class on the other side of it.
-//!
-//! Both entry points answer a **flat `String[]`** rather than a row of
-//! typed columns. That is the whole of the JNI, and it is one array type
-//! instead of three interleaved ones for a payload the provider is about
-//! to hand back over binder as a `MatrixCursor` anyway; `DevLogProvider`
-//! parses the two numeric fields. Kept flat rather than nested for the
-//! same reason -- an array of arrays is four more JNI calls per line.
use android_view::jni::JNIEnv;
use android_view::jni::objects::{JClass, JObject, JString};
use android_view::jni::sys::{jlong, jobjectArray};
use std::sync::OnceLock;
-/// How many `String`s each log line occupies in the flat answer:
-/// `seq`, `t_ms`, `level`, `target`, `message`, in that order. The Java
-/// side has the same constant, and the two are the one place the shape is
-/// written down on each side.
-///
/// Gated with its one reader: the tabs demo links no `client-core` and so
/// has no ring to lay out, and an ungated constant is a warning in that
/// build (`iris-android-app` without `transcript-screen`).
@@ -43,21 +18,11 @@ const FIELDS_PER_LINE: usize = 5;
/// the same from inside this process otherwise.
static AUTHORITY: OnceLock = OnceLock::new();
-/// Where this app's log can be read from, for the diagnostics pane.
-///
-/// The provider's own answer rather than one composed from the package
-/// name here: what makes the line worth showing is that it names an
-/// authority somebody can actually query, and only the provider knows it
-/// registered.
#[cfg(feature = "bench")]
pub fn authority() -> Option<&'static str> {
AUTHORITY.get().map(String::as_str)
}
-/// `DevLogProvider.nativeReady` -- the provider announcing the authority
-/// it registered under and the app's private directory, from its own
-/// `onCreate`.
-///
/// The directory is taken here as well as in
/// `MainActivity.nativeSetFilesDir` because **the provider is often the
/// only thing running**: once the app has died, Dev Updater's query
@@ -76,8 +41,6 @@ pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeReady(
authority: JString,
files_dir: JString,
) {
- // Before the authority line, so the previous run's death is above the
- // line announcing this one rather than buried under it.
#[cfg(feature = "transcript-screen")]
if let Some(dir) = string_arg(&mut env, &files_dir) {
crate::android::app_log::set_crash_dir(std::path::Path::new(&dir));
@@ -91,7 +54,6 @@ pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeReady(
let _ = AUTHORITY.set(authority);
}
-/// One `String` argument, or `None` for a null or unreadable one.
fn string_arg(env: &mut JNIEnv, value: &JString) -> Option {
if value.is_null() {
return None;
@@ -99,18 +61,11 @@ fn string_arg(env: &mut JNIEnv, value: &JString) -> Option {
env.get_string(value).ok().map(Into::into)
}
-/// `DevLogProvider.nativeStatus` -- `held`, `dropped`, `newest_seq`, as
-/// three strings.
-///
/// `newest_seq` is `-1` for a ring nothing has been written to, which is
/// what tells a reader holding a cursor that this process **restarted**:
/// the ring is in memory, so a new process starts again at zero and a
/// stale cursor would otherwise skip everything silently.
///
-/// Exported by name rather than registered, matching this crate's other
-/// activity-side natives: the mangled name is the whole of what a class
-/// this app owns needs.
-///
/// # Safety
/// Called by the JVM with the arguments its `native` declaration names.
#[unsafe(no_mangle)]
@@ -121,9 +76,6 @@ pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeStatus(
string_array(&mut env, &status_fields())
}
-/// `DevLogProvider.nativeLinesSince` -- every held line with a sequence at
-/// or after `since`, oldest first, [`FIELDS_PER_LINE`] strings each.
-///
/// 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.
@@ -141,7 +93,6 @@ pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeLinesSinc
string_array(&mut env, &line_fields(since.max(0) as u64))
}
-/// The three status numbers, as the provider's row.
#[cfg(feature = "transcript-screen")]
fn status_fields() -> Vec {
let ring = crate::client::log_ring::process_ring();
@@ -181,8 +132,6 @@ fn line_fields(_since: u64) -> Vec {
Vec::new()
}
-/// A Java `String[]` of those, or a null array if the JVM refused one.
-///
/// Null rather than a panic across the JNI boundary: `DevLogProvider`
/// reads it as "the provider could not answer" and returns no cursor,
/// which Dev Updater already draws as a distinct state. Taking the app
diff --git a/app-rust/src/android/enrollment.rs b/app-rust/src/android/enrollment.rs
index d9a99b5..78a33a9 100644
--- a/app-rust/src/android/enrollment.rs
+++ b/app-rust/src/android/enrollment.rs
@@ -1,36 +1,9 @@
-//! Which `ai-server` this app talks to, and how it was told.
-//!
-//! The parsing, the file and its owner-only mode are
-//! `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
-//! directory across JNI (see `lib.rs`'s two exported functions).
-//!
-//! **Why the app is told at runtime rather than at build time.** The APK
-//! is cross-compiled in a VM and run against the server on the host, whose
-//! CA and token are not this machine's -- so nothing about the destination
-//! can be baked in, and no token or CA may sit in a repo or a delivered
-//! artifact either way. The CA arrives with the link (`ca` parameter,
-//! `wg_app_link::enroll::ca_param`), which is what makes an APK built
-//! anywhere able to pin the server it is pointed at.
-//!
-//! The files directory is process-wide state, which this project otherwise
-//! 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
-//! `crate::client::log_ring`'s process ring.
-
#[cfg(not(feature = "bench"))]
use crate::client::api::UreqTransport;
use crate::client::config::{EnrolledServer, EnrollmentStore};
use std::path::PathBuf;
use std::sync::OnceLock;
-/// `Context.getFilesDir()`, handed over by `MainActivity` before it builds
-/// the view. Set once per process; a second call with a different path is
-/// a programmer error rather than something to recover from, and a second
-/// call with the same one is what a re-created activity does.
static FILES_DIR: OnceLock = OnceLock::new();
pub fn set_files_dir(dir: PathBuf) {
@@ -42,17 +15,12 @@ pub fn set_files_dir(dir: PathBuf) {
}
}
-/// `None` before `MainActivity` has handed the directory over -- which is
-/// **not** the same as "not enrolled", and is why [`status`] has a state
-/// for it (UI_RULES: design the unknown state first).
fn store() -> Option {
FILES_DIR.get().map(EnrollmentStore::new)
}
-/// What this app has been told, or why it has not been.
pub enum Status {
Enrolled(EnrolledServer),
- /// Nothing has been enrolled yet: the ordinary first-run state.
NotEnrolled,
/// The question could not be answered -- the activity never handed a
/// files directory over, or the file is there and unreadable. Kept
@@ -75,11 +43,6 @@ pub fn status() -> Status {
/// One line for the diagnostics pane. The three states read differently on
/// purpose: "not enrolled" says what to do about it, and "couldn't tell"
/// must not be mistaken for it.
-///
-/// Only the bench build has a pane to put this in -- same gate, and the
-/// same reason, as `app_log::diagnostics_line`. The transcript build says
-/// the same things where they matter to it, in the message
-/// [`transport`]'s error becomes on screen.
#[cfg(feature = "bench")]
pub fn status_line() -> String {
match status() {
@@ -89,13 +52,6 @@ pub fn status_line() -> String {
}
}
-/// Parses an `aiapp://enroll?...` link and saves it, replacing whatever
-/// was enrolled before -- opening a link is how somebody says "this server
-/// now", including after the old one's token was rotated.
-///
-/// The returned `Err` is the message for a person: this is called from a
-/// tap on a link, and a link that did nothing with nothing said is the
-/// failure the UI rules are most insistent about.
pub fn apply_link(uri: &str) -> Result {
let server = EnrolledServer::parse_link(uri)?;
let store = store().ok_or("the app has no files directory to save an enrollment in")?;
@@ -105,8 +61,6 @@ pub fn apply_link(uri: &str) -> Result {
Ok(server)
}
-/// A transport for the enrolled server, pinning the CA the link carried.
-///
/// Gated to the same builds as `transcript_client`, its only caller: the
/// bench build opens a checked-in fixture and reaches no server, so
/// compiling this into it would be a warning about dead code that is
diff --git a/app-rust/src/android/mod.rs b/app-rust/src/android/mod.rs
index 24f1085..ce81b7f 100644
--- a/app-rust/src/android/mod.rs
+++ b/app-rust/src/android/mod.rs
@@ -1,41 +1,3 @@
-//! The android-view demo app: by default, iris's `tabs` widget tree
-//! (`tabs_ui::build`, shared with the winit example) running through
-//! `iris::android`'s `ViewPeer`. This is RUST.md's I2 pass condition made
-//! concrete -- there is no UI here beyond what `tabs-ui` already draws.
-//!
-//! `JNI_OnLoad` and `new_view_peer` mirror android-view's own demo
-//! (`~/src/android-view/demo/src/lib.rs`): the only android-view-specific
-//! plumbing a real app needs is registering its `View` subclass and
-//! wrapping `iris::android::new_peer`'s generic function in a concrete
-//! `extern "system" fn`, since `register_view_class` wants a plain
-//! function pointer.
-//!
-//! **`transcript-screen` feature (RUST.md's I5 Android integration):** with
-//! `--features transcript-screen`, `new_view_peer` instantiates
-//! `transcript_client::TranscriptClient` instead of the tabs `Client`
-//! below, against a real `ai-server` (see that module's doc). Chosen over a
-//! third shell crate: this one already has the Gradle project, the
-//! `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.
-//! `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 --
-//! there is no in-app navigation to switch *to* on either side yet.
-//!
-//! **`bench` feature (P0's iris half, docs/RUST.md):** a third
-//! `AndroidAppState`, `bench_client::BenchClient`, on the same axis --
-//! `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`
-//! does. `bench` depends on `transcript-screen` (Cargo.toml) for
-//! `transcript-ui`/`client-core`/`event-model`, so both features end up
-//! enabled together -- `ActiveClient` below gives `bench` priority in that
-//! case, the same way `transcript-screen` already takes priority over the
-//! default `tabs-screen`.
-
use android_view::{
Context, View,
jni::{
@@ -61,14 +23,7 @@ mod app_log;
mod bench_client;
#[cfg(feature = "bench")]
mod bench_jni;
-/// This app's log ring, handed to Dev Updater on the phone through a
-/// `ContentProvider`. Declared in every build for the reason the module
-/// gives: the Java class is in the manifest either way, and a `native`
-/// method the library does not export fails the class load.
mod devlog;
-/// Which server this app talks to, told to it at runtime by an
-/// `aiapp://enroll` link. Only where `client-core` is linked -- the plain
-/// tabs demo makes no network call and has nothing to enrol against.
#[cfg(feature = "transcript-screen")]
mod enrollment;
#[cfg(all(feature = "transcript-screen", not(feature = "bench")))]
@@ -105,12 +60,6 @@ impl AndroidAppState for Client {
}
fn back_pressed(&mut self, _rsc: &mut AndroidRsc, _render: &mut UiRenderState) -> bool {
- // Nothing in the tabs example has a back stack of its own to pop --
- // declining lets the activity finish, which is the same "no
- // handler" behaviour the default impl gives. Present as an
- // explicit override (rather than relying on the default) so a
- // reader checking "does the back gesture reach this app" finds an
- // answer here rather than nothing.
false
}
}
@@ -163,11 +112,6 @@ pub unsafe extern "system" fn JNI_OnLoad(vm: *mut RawJavaVM, _: *mut c_void) ->
/// and hands out one function pointer, whereas these two are this app's
/// own activity and the mangled name is the whole of what is needed.
///
-/// Declared in every build, including the tabs demo that has no
-/// `client-core` to store anything -- a `native` method Java declares and
-/// the library does not export is an `UnsatisfiedLinkError` when the class
-/// loads, which would take down a build that merely shares the activity.
-///
/// # Safety
/// Called by the JVM with the arguments its `native` declaration names.
#[unsafe(no_mangle)]
@@ -187,14 +131,6 @@ pub extern "system" fn Java_dev_iris_android_demo_MainActivity_nativeSetFilesDir
log::debug!("iris app: files directory is {dir}");
}
-/// `MainActivity.nativeEnroll` -- one `aiapp://enroll?...` link, from the
-/// VIEW intent that started or resumed the activity.
-///
-/// Logged either way rather than answered: the activity has nothing to do
-/// with the result, and where the enrollment shows up is the diagnostics
-/// pane (`enrollment::status_line`), which reads the stored answer rather
-/// than being told it.
-///
/// # Safety
/// Called by the JVM with the arguments its `native` declaration names.
#[unsafe(no_mangle)]
@@ -217,9 +153,6 @@ pub extern "system" fn Java_dev_iris_android_demo_MainActivity_nativeEnroll(
log::warn!("iris app: {uri} arrived, but this build has no server to enrol with");
}
-/// A `JString` as a Rust `String`, or `None` for a null or non-UTF-8 one --
-/// neither is worth taking the app down for, and both are logged where
-/// they happen.
fn jstring(env: &mut JNIEnv, value: JString) -> Option {
if value.is_null() {
log::warn!("iris app: the activity passed a null string across JNI");
diff --git a/app-rust/src/android/transcript_client.rs b/app-rust/src/android/transcript_client.rs
index a550923..85973de 100644
--- a/app-rust/src/android/transcript_client.rs
+++ b/app-rust/src/android/transcript_client.rs
@@ -1,42 +1,3 @@
-//! RUST.md's I5 Android integration: `transcript-ui`'s screen filling the
-//! whole window on android-view, against a real `ai-server` through
-//! `client-core` -- the missing half `iris-android-app` (I2) only had for
-//! `tabs-ui` until now. Behind the `transcript-screen` Cargo feature so the
-//! plain build (`cargo ndk build`, no `--features`) stays exactly the tabs
-//! demo I2/I4 already measured against.
-//!
-//! **Deliberate simplification, recorded rather than left to be
-//! rediscovered (RUST.md's I5 box has the full account)**: there is no
-//! session list here -- the first session `ApiClient::fetch_sessions`
-//! returns is opened automatically, since there is nothing to tap to get
-//! there, which is what `transcript-bench.sh` and `ui-trace` need to land
-//! 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::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
-//! `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
-//! request a frame after each `TaskCtx::update` instead of relying on
-//! `Tasks::spawn`'s single end-of-future redraw -- see that method's own
-//! doc for why.
-//!
-//! **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
-//! `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 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};
@@ -55,20 +16,8 @@ pub struct TranscriptClient {
/// by rebuilding the session list beside it.
content: WeakWidget,
screen: Option,
- /// 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
- /// `fold_event` needs everything folded so far to fold it in.
items: Vec,
- /// The session currently open -- `None` only before the first fetch
- /// resolves. Read back by `apply_event`'s rebuild, which has no session
- /// id of its own (a live `SeqEvent` doesn't carry one).
session_id: Option,
- /// Bumped every time a new session load starts; a background response
- /// checks it before touching state, so a slow reply for a session this
- /// screen has moved on from can't overwrite what replaced it. There is
- /// only ever one session here (no list to switch away to), but the
- /// guard still matters for the *first* fetch racing a `stop`/`start`.
generation: Arc,
}
@@ -81,14 +30,6 @@ impl HasAndroidUiState for TranscriptClient {
}
}
-/// Builds one `UreqTransport` from the stored enrollment. Called twice per
-/// session load, same as `desktop-app`'s `build_transport` closure --
-/// `ApiClient` and the live-stream follow each need their own, since
-/// `UreqTransport` holds its own `ureq::Agent`.
-///
-/// Read afresh each time rather than held: opening a new enrolment link
-/// 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 {
crate::android::enrollment::transport()
}
@@ -102,15 +43,6 @@ fn placeholder(rsc: &mut Rsc, message: &str) -> StrongWidget {
.any()
}
-/// The two named controls RUST.md's I5 box ("Measurements taken" (b))
-/// drives by name over `ui-trace`, e.g. `ui-trace record --do "tap 'Frame
-/// report'"`. `dumpsys gfxinfo` cannot see this screen's own GPU-drawn
-/// frames at all -- this is the screen's own equivalent of the Compose
-/// app's "Copy render timings" control, logged rather than clipboarded
-/// (no clipboard wiring exists here) under this crate's own fixed
-/// `android_logger` tag (`iris-android-app`, `lib.rs`'s `JNI_OnLoad`),
-/// grep-able on the fixed string `"iris frame report"` the way
-/// `transcript-bench.sh` greps `"ai-app render report"`.
fn frame_report_controls(rsc: &mut AndroidRsc) -> WeakWidget {
type Rsc = AndroidRsc;
let report_rect = rect(Color::rgb(50, 50, 60))
@@ -224,10 +156,6 @@ impl TranscriptClient {
});
}
- /// Loads the opening page, then follows the live SSE stream for the
- /// rest of this session's life -- `desktop-app`'s `select_session`
- /// almost verbatim, with `Proxy::send_event` replaced by `ctx.update` +
- /// `redraw.request_redraw()` (see this module's doc).
fn select_session(&mut self, rsc: &mut AndroidRsc, session_id: String) {
let my_generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
self.items.clear();
@@ -254,17 +182,9 @@ impl TranscriptClient {
};
let api = ApiClient::new(rest);
- // The most recent 200 events, coalesced -- the same page size
- // `desktop-app` uses; RUST.md's I3/history-paging work is what
- // a real scrollback would reuse (out of scope here, same as
- // E4).
let page: Result, String> = api
.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 `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()
@@ -295,12 +215,6 @@ impl TranscriptClient {
if live_generation.load(Ordering::SeqCst) != my_generation {
return;
}
- // The outer closure here is an `FnMut` -- `follow_session_events`
- // calls it once per line -- so it captures `live_generation` by
- // move and re-clones it for each inner `ctx.update` closure
- // rather than moving a shared `stop`-style helper into itself:
- // a value moved out of an `FnMut`'s captures on one call leaves
- // nothing there for the next.
let _ =
follow_session_events(
&stream_transport,
@@ -329,10 +243,6 @@ impl TranscriptClient {
});
}
- /// Rebuilds the whole widget tree from `self.items` -- same tradeoff as
- /// `desktop-app`'s `rebuild_transcript` (this module's doc comment).
- /// Reads `self.session_id` rather than taking one, since every caller
- /// (the opening page, and every live event) already has it set there.
fn rebuild_transcript(&mut self, rsc: &mut AndroidRsc) {
let in_progress = self
.screen
@@ -365,12 +275,7 @@ impl TranscriptClient {
let old_items = self.items.clone();
self.items = fold_event(&self.items, event);
match &self.screen {
- // The common path: update only the row(s) that actually
- // changed instead of refolding and rebuilding all ~3,200 of
- // them per event (RUST.md's P0 streaming-phase fix).
Some(screen) => screen.apply(rsc, &old_items, &self.items),
- // No screen yet (the opening page hasn't landed) -- build one
- // the ordinary way once it has.
None => self.rebuild_transcript(rsc),
}
}
diff --git a/app-rust/src/bin_desktop.rs b/app-rust/src/bin_desktop.rs
index 9bb869f..16b47f7 100644
--- a/app-rust/src/bin_desktop.rs
+++ b/app-rust/src/bin_desktop.rs
@@ -1,32 +1,6 @@
-//! 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 (decided 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);
diff --git a/app-rust/src/client/ansi.rs b/app-rust/src/client/ansi.rs
index b810849..c23e806 100644
--- a/app-rust/src/client/ansi.rs
+++ b/app-rust/src/client/ansi.rs
@@ -1,25 +1,3 @@
-//! What a tool printed, with its terminal styling applied and everything
-//! else taken out. Ported from `app/.../Ansi.kt`, module for module: the
-//! Kotlin version builds a Compose `AnnotatedString`, which does not exist
-//! here, so a [`StyledText`] of plain text plus non-overlapping
-//! `(Range, Style)` spans stands in for it -- a future UI layer maps
-//! [`Style`] onto whatever it draws with.
-//!
-//! Bash output arrives exactly as the program wrote it, escape sequences
-//! included, and drawn verbatim those are line noise in the middle of the
-//! thing being read. Stripping them all would be the other half-answer --
-//! colour is often the whole of what a diff or a test run is saying.
-//!
-//! So the sequences that decide how text *looks* become spans, and every
-//! other one is dropped rather than shown: the rest move a cursor around a
-//! grid this is not, and "go to column 40" has no meaning in a scrolling
-//! document.
-//!
-//! A carriage return is honoured the way a terminal honours it: what was
-//! written since the last line break is thrown away and the line starts
-//! again. That is what makes a progress bar show its final state rather
-//! than every state it passed through.
-
use std::ops::Range;
/// An RGB colour, the same shape wherever this crate names one -- no alpha,
@@ -38,18 +16,10 @@ impl Rgb {
}
}
-/// The sixteen colours a terminal program names, and the two it assumes.
-///
-/// Its own palette rather than the syntax one: a program that prints in red
-/// has chosen red, where a highlighter's colours are this app's reading of
-/// somebody else's code.
#[derive(Debug, Clone)]
pub struct AnsiPalette {
- /// Indexes 0-7, then 8-15 bright, in the terminal's own order.
pub colours: [Rgb; 16],
- /// What uncoloured text is, needed only where a style has to state a colour.
pub foreground: Rgb,
- /// What the text sits on, needed for reverse video.
pub background: Rgb,
}
@@ -67,8 +37,6 @@ pub struct Style {
pub strikethrough: bool,
}
-/// Plain text plus the non-overlapping, ordered spans that style parts of it
-/// -- this crate's stand-in for Compose's `AnnotatedString`.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct StyledText {
pub text: String,
@@ -87,11 +55,7 @@ impl StyledText {
const ESC: char = '\u{1B}';
const BELL: char = '\u{7}';
-/// [text] with its terminal styling applied and everything else taken out;
-/// see the module doc.
pub fn ansi_styled(text: &str, palette: &AnsiPalette) -> StyledText {
- // The common case by a long way -- nothing to do, and nothing allocated
- // to find that out.
if !text.contains(ESC) && !text.contains('\r') {
return StyledText::plain(text.to_string());
}
@@ -118,19 +82,12 @@ pub fn ansi_styled(text: &str, palette: &AnsiPalette) -> StyledText {
}
});
} else if c == '\r' && chars.get(at + 1) != Some(&'\n') {
- // A bare carriage return rewrites the line. One before a newline
- // is the other half of a Windows line ending: it rewrites
- // nothing, and it is dropped rather than kept, since that pair
- // is one line break.
flush(&mut plain, sgr, &mut runs);
drop_line(&mut runs);
at += 1;
} else if c == '\r' {
at += 1;
} else if c >= ' ' || c == '\n' || c == '\t' {
- // Everything printable, plus the two control characters that are
- // layout rather than terminal commands. A stray bell or
- // backspace goes for the same reason a cursor move does.
plain.push(c);
at += 1;
} else {
@@ -151,8 +108,6 @@ pub fn ansi_styled(text: &str, palette: &AnsiPalette) -> StyledText {
StyledText { text: out, spans }
}
-/// Throws away everything written since the last line break, as a carriage
-/// return does.
fn drop_line(runs: &mut Vec<(String, Option