8.6 KiB
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 in-tree iris framework. 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/— theai-appcrate.clientis platform/UI independent;uicontains Iris widget trees;androidanddesktopare thin hosts.android-project/packages the Rust cdylib. Thebenchfeature andbench-fixture/are retained performance rigs, not a second app.iris/— the framework, proc macro, tabs demo, and input rig.scripts/— repository-wide scripts and independent profiling rigs.wg-app-link/— a git submodule shared with dev-updater. Clone with--recurse-submodulesor rungit submodule update --init.docs/— design and working documents.
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_64for this machine's emulator, or./build-apk.sh releasefor 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.shruns 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.shinstalls a benchmark APK on this checkout's emulator, taps its accessibility-labelled control, and prints the report.cd app && cargo testdrives the real transcript screen without a window throughiris::harness; touch recordings live inapp/touch/.iris/scripts/run-headless.sh phone --phone --dir ../app --shot …opens the same screen at phone size.--replay ../app/touch/flick-120hz.touchreplays a recorded gesture.scripts/rigs/ui-profile/tests/frame_profile.rsmeasures CPU frame cost;arena_churn.rsmeasures 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
tracingcaches callsite interest process-wide. Logging tests must install their capturing subscriber before any tested callsite runs.serde_jsonneedsfloat_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.