# Rust app and Iris The production app is one Rust crate under `app/`, with shared Android and desktop UI drawn by the in-tree `iris` framework. This document keeps the open work and measured constraints that are expensive to rediscover. ## Boundaries - `app/src/client` contains transport, config, caching, folding, highlighting, and other platform/UI-independent logic. It must not depend on Iris. - `app/src/ui` contains product screens and reusable product widgets. - `app/src/android` and `app/src/desktop` contain only platform integration. Platform services are exposed to shared UI through small traits. - `iris/` is only a UI framework. Session, transcript, server, setup, and product styling concepts never belong there. - `iris/src/rsc` groups resources carried by or installed through `StdRsc`: attributes, events, overlays, pointer sensing/input, tasks, and per-widget state. It is the standard bundle used by Iris's hosts, not a requirement; shared widgets depend on its narrow resource traits so a custom bundle can implement only what it needs. `widget/` is reserved for concrete retained widgets. Public paths such as `iris::sense` remain aliases for readability. - Phone and desktop layouts may differ. Their component implementations, colors, spacing, text styles, gestures, folding, and network behavior do not. The application crate has an rlib for tests and desktop and a cdylib for Android. Features select optional surfaces: `screens` links Iris, `fixture` embeds the synthetic transcript, and `bench` adds the benchmark runner. An ordinary APK uses `screens`; only a benchmark build adds `bench`. ## Current state The transcript screen runs in the headless harness, a desktop window, the Android emulator, and a real phone. The phone performance gate passed; the preserved reports are under `docs/bench/`. Open session-screen work: - History paging, viewport-sized prefetch, distinct unknown/empty/error states, and jump-to-latest. - Images, attachments, session settings, usage display, and dialogs. - Re-verification of IME/inset behavior on the real phone. Later product work: - Root navigation and the sessions/import/models/setups surfaces. - File explorer and editor, including conflict handling. - Settings, notification permission, sharing, and notification service. - Desktop layouts for those surfaces. Client route coverage and gaps live in `docs/CLIENT_CORE.md`; framework work lives in `docs/IRIS_TODO.md`. ## Test layers Use the cheapest layer that can answer the question. ### 1. In-process harness `iris::harness` builds the real widget tree with no window, compositor, or GPU. Its clock is controlled by the test. `TouchScript` replays the same historical samples Android supplies. Recordings live under `app/touch/`. cd app && cargo test The suite covers fling velocity and duration, catching a moving list, cancellation, taps, long-press selection, list bounds, and composer insets. It cannot answer pixel, atlas, GPU upload, surface lifecycle, JNI, clipboard, or real-inset questions. `iris/tests/mask_sdf.rs` is the narrow exception: it uses a headless GPU compute pass to prove the shader and CPU hit-test arithmetic agree. ### 2. Phone-shaped desktop window cd iris ./run-headless.sh phone --phone --dir ../app --shot /tmp/phone.png ./run-headless.sh phone --phone --dir ../app \ --replay ../app/touch/flick-120hz.touch --shot /tmp/replay.png This layer is for visual output and desktop input integration. Debug frame times on the VM's GPU are not phone measurements. ### 3. Android cd app ./build-apk.sh debug --abi x86_64 Android is required for JNI, lifecycle, IME, insets, clipboard, accessibility, and renderer fallback. Read the `this-machine-android` skill first. Use a release arm64 build for real-phone measurements. The emulator is deliberately a GLES rig: Vulkan is SwiftShader, while GLES is host-accelerated through virgl. Do not compile with `force-gles`; Iris's runtime fallback must make the same decision the shipped binary makes. Read the `iris renderer:` log before interpreting any result. ## Performance rigs and established results `scripts/rigs/ui-profile/tests/frame_profile.rs` measures the CPU half of a frame. Run it only in release mode: cd scripts/rigs/ui-profile cargo test --release --test frame_profile -- --ignored --nocapture A fling is not CPU-bound after the first pass over content: only roughly one frame in six performs layout, and later passes measured p99 0.26 ms. Initial multi-millisecond spikes are text shaping for newly visited transcript rows. A streamed event originally measured 3.86 ms in the host rig and 9.5 ms on the phone. Folding was 0.35 ms and applying the widget diff 0.41 ms; shaping dominated because the old fixture grew one 14,888-character markdown block. Real replies measured a largest block of 1,580 characters over 7,706 blocks. The fixture now inserts blank lines and retains the run-on block only as a one-shot stress case. `arena_churn.rs` measures GPU-array upload by reporting the byte-diff floor, the bytes Iris uploads, and the former whole-array cost. The rig established: - Whole-array writes cost 758 MB over one fling and 1.2 GB over 401 streamed deltas. - `ArrBuf` must retain allocation across length changes. - Redraw must reuse primitive slots within the frame rather than freeing and appending them. - Dirty ranges must track changed entries and cancel provisional writes that are restored before upload. - `Painter::set_child_offset` and stable `LazySpan` row boxes reduce pinned growth to a 1.1% instance upload, matching its 1.1% floor. Median instance upload fell from 176,496 bytes to 1,488 bytes. Do not optimize folding again without new evidence. The measurements ruled it out as the streamed-frame bottleneck. ## Text and fonts Body and monospace text come from platform fonts. Icons use the bundled Nerd Font subset because their codepoints are a small closed set no platform must provide. Android font enumeration previously rendered some bold spans as blank gaps, so every real-phone build must visually verify bold text and the triangle disclosure glyphs; desktop and emulator font sets are not evidence for the phone. Markdown is split into blocks once and retained. A delta reshapes only the block it changes. Tables wrap cells and horizontally scroll only after their minimum useful column width. Links share a text widget and hit regions; they must not become one layout node per link. ## Scrolling and retained rendering `docs/SCROLL.md` defines scrolling. One `ScrollController` owns position, gesture, fling, and pin state. `ScrollArea` and `LazySpan` each own one through `Scrollable`. The transcript is bottom-anchored. Appending while pinned keeps the newest edge fixed; loading history above the anchor must not move visible content. Expanding a row holds the edge nearest the tap. Retained child offsets move a subtree without rewriting every primitive. A page is a number of events, not a number of visible rows: many deltas fold into one reply and many tool calls can group into one row. Prefetch therefore uses measured viewports, never a fixed row count. ## Android host The Java activity owns the Android lifecycle, the `View`, enrollment intent, and insets. `iris::android` owns surface lifecycle, touch history, generic motion, hover, mouse buttons, wheel input, IME plumbing, and accessibility. The product client consumes the same Iris pointer state as desktop. Static inset dispatch supplies settled values; `WindowInsetsAnimation` supplies intermediate IME heights and re-reads settled insets at animation end. Visibility and height are separate signals during animation. targetSdk 35+ is required for reliable IME overlap under enforced edge-to-edge. The application id is `com.example.aiapp`, preserving the retired app's identity. The Java package remains `dev.iris.android.demo`, an internal JNI name. Explicit benchmark builds add `.bench` to the application id. ## Build constraints - `app`, `iris`, and `scripts/rigs/ui-profile` use synchronized rolling nightly toolchains. - Android uses the `android-release`/`android-dev` profiles so phone settings do not affect desktop builds. - `app/build-apk.sh` removes prior ABI native outputs before packaging; Gradle otherwise may reuse a stale merged library from another ABI. - Release builds are signed by the machine-local `~/.config/ai-app/release.jks`. No key or enrollment secret belongs in the repository. - `iris/run-headless.sh --dir DIR` selects the workspace containing the example and defaults to `iris/`. ## Session-screen pass condition Use `app/ui-sandbox.sh` with artificial delay, a large transcript, and slow spawn. Exercise unknown, empty, error, and too-long states. Drive controls by accessibility label. Verify paging seams, jump-to-latest, selection, tool-card state, message steering, and IME closure. Then re-run the frame report on the real phone and compare only with the same Iris reporter—not historical Compose timings, whose clocks and accounting differ.