# ai-app A phone and desktop interface to AI coding sessions. The backend is Rust/Axum; the shared client and UI are Rust, drawn by the `iris` framework pinned as a submodule. The Android app uses a thin Java activity and `android-view`; desktop uses winit. `docs/PLAN.md` is the design source of truth. Read it before structural work and update it when a decision changes. Working documents are pruned as work lands: preserve current invariants, measurements, and failed hypotheses, not a chronicle of completed tasks. Do not create a decisions log. ## Architecture A session is a child process translated by a driver into one common event model. A new session type is a new driver, never a session-type branch in shared routes, transcripts, or screens. Android and desktop share `app/src/client` and `app/src/ui`. Platform modules own only what the platform forces: JNI, lifecycle, insets and IME on one side; winit and argv on the other. Layouts may differ, but widgets, styling, folding, paging, config, and network logic are shared. `iris/` is a UI framework and nothing else. It must not know about sessions, transcripts, setups, or servers. Product code belongs in `app/`, and the dependency runs one way. ## Layout - `server/` — `ai-server`. `routes.rs`'s module comment is the HTTP table. - `event-model/` — the wire contract shared by server and app. - `app/` — the `ai-app` crate. `client` is platform/UI independent; `ui` contains Iris widget trees; `android` and `desktop` are thin hosts. `android-project/` packages the Rust cdylib. The `bench` feature and `bench-fixture/` are retained performance rigs, not a second app. - `iris/` — the pinned framework submodule: proc macro, demos, and input rig. - `scripts/` — repository-wide scripts and independent profiling rigs. - `wg-app-link/` — a git submodule shared with dev-updater. - `docs/` — design and working documents. Clone with `--recurse-submodules` or run `git submodule update --init` to populate both `iris/` and `wg-app-link/`. Nerd Font icons are an app-owned committed subset. `app/build-icon-font.sh` produces `app/assets/fonts/nerd_icons.ttf`; its codepoints must match `app/src/ui/icon.rs`. The app registers it with Iris at startup. Body and monospace fonts come from the platform; Iris ships no font assets. ## Checking work Commit each coherent, warning-clean slice and push it. - Whole product: `./scripts/run-tests.sh`. - Framework: `cd iris && cargo fmt --all --check && cargo clippy --all-targets -- -D warnings && cargo test`. - App: `cd app && cargo fmt --all --check && cargo clippy --all-targets -- -D warnings && cargo test`. - Android: `cd app && ./build-apk.sh debug --abi x86_64` for this machine's emulator, or `./build-apk.sh release` for a phone. The script builds with cargo-ndk, packages with Gradle, and verifies the APK. Never infer phone frame times from a debug emulator build. `app/`, `iris/`, and `scripts/rigs/ui-profile/` use rolling nightly through per-directory toolchain files. `server/` and `event-model/` use stable. The release signing key lives at `~/.config/ai-app/release.jks`, never in the checkout. `build-apk.sh` creates it once. Normal builds use application id `com.example.aiapp`; benchmark builds add `.bench` and are built explicitly: ./build-apk.sh release --features "screens bench" ## Running the server Use `--bind 127.0.0.1` for emulator development. Without it the server binds wg0, which the emulator cannot reach. Use scratch state: ai-server --bind 127.0.0.1 --config /tmp/ai-config.ron \ --data-dir /tmp/ai-sessions --port 8444 The emulator reaches the host at `10.0.2.2`. `ai-server --enroll-link` mints another device link while the server runs. `--delay MS` is important for UI states that disappear too quickly on loopback. `RUST_LOG=ai_server=debug` logs transcript page bounds and SSE catch-up/reset decisions. Exercise the server directly when possible: curl --cacert ~/.config/ai-app/certs/ca.pem \ -H "Authorization: Bearer …" https://127.0.0.1:8443/sessions `./scripts/test-wg-tunnel.sh up|test|down` builds a real WireGuard tunnel between network namespaces and verifies pinned TLS against 10.66.0.1. ## Rigs - `app/ui-sandbox.sh` runs an isolated delayed server with invented transcripts, a fake CLI, stable enrollment, and a file-explorer fixture. Its HOME and data are disposable; never point import/delete tests at real `~/.claude/projects`. - A two-line fake CLI (`#!/bin/sh`, `cat > /dev/null`) exercises adoption, stop, restart, and process lifetime without using an account or token. - `app/run-bench.sh` installs a benchmark APK on this checkout's emulator, taps its accessibility-labelled control, and prints the report. - `cd app && cargo test` drives the real transcript screen without a window through `iris::harness`; touch recordings live in `app/touch/`. - `iris/scripts/run-headless.sh phone --phone --dir ../app --shot …` opens the same screen at phone size. `--replay ../app/touch/flick-120hz.touch` replays a recorded gesture. - `scripts/rigs/ui-profile/tests/frame_profile.rs` measures CPU frame cost; `arena_churn.rs` measures GPU-array upload. Run ignored profiling tests in release mode or the numbers are meaningless. The checked-in benchmark transcript is synthetic. Never put a real transcript in this repository; it contains conversation text, tool input, and file data. The emulator is a GLES rig. Its Vulkan implementation is SwiftShader, while GLES is host-accelerated through virgl. Let Iris's runtime fallback select GLES; do not pass `force-gles`. Verify the `iris renderer:` log line before interpreting a measurement. Vulkan is verified on desktop and a real phone. ## Driving Android UI Read the installed `this-machine-android` skill before using Gradle, adb, an AVD, screenshots, or UI traces. This checkout gets its own AVD; resolve it with `emu serial` rather than typing a device name. Scripts tap controls by accessibility label, never by coordinate. Coordinates are allowed for swipes because a swipe describes a distance across a scrolling surface. A coordinate tap can silently hit a different control and turn a failed run into a plausible-looking result. ## Host and VM boundary Production `ai-server` runs on the host, where the phone can reach WireGuard. The Claude CLI is in this VM, so the host reaches it as a remote provider. The VM's wg0 is useful for development but has no reachable phone peer. A dev server in the VM creates a throwaway CA; never install an APK enrolled against that CA on the real phone. For llama.cpp tests, the CPU build is at `~/.local/opt/llama.cpp`; add that directory to `LD_LIBRARY_PATH`. Avoid 2-bit quants for driver diagnosis because their fluent nonsense resembles a broken integration. For SSH transport tests, SSH this VM to itself with a throwaway key and a harmless command. Remove the key afterwards. The remote login shell is fish, so POSIX-quoting assumptions require explicit verification. ## Session invariants Sessions deliberately outlive `ai-server`. Shutdown leaves marked processes running; restart adopts their process records without starting stopped sessions. Sending a message to a stopped session starts it. Use `--throwaway-sessions` for test-created sessions. Each session directory contains `process.json`, `stdin.fifo`, `stdout.log`, and `stderr.log`. Do not edit or remove them while live: the stdout byte offset in `process.json` prevents replay and loss. Never import a Claude Code session open in a terminal. One Claude session id may occur in multiple project directories; import listing deduplicates by id and prefers the copy with more lines, while deletion removes every copy. Deleting an app session only deletes the provider's transcript when `deleteForeign=true`. The server deletes the foreign transcript first so an unreachable machine cannot leave a half-deleted session. ## Known traps - `tracing` caches callsite interest process-wide. Logging tests must install their capturing subscriber before any tested callsite runs. - `serde_json` needs `float_roundtrip`: transcript pages and SSE must preserve identical timestamp bytes. - Import lookup must use `import::find`, not list every transcript. Validate ids before putting them in a glob. - Transcript sequence numbers increase, so page edges are found by bisection. Do not replace indexed window reads with whole-transcript parsing. - A page's event count has no fixed relationship to visible rows because deltas and tool calls fold together. History cushions are measured in viewports, not row counts. - Android generic motion is separate from touch. Keep hover, wheel, and mouse button handling in `iris::android`; product UI consumes the same pointer state on desktop and Android.