Files
ai-app/AGENTS.md
T

8.5 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/ — 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 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-submodules or run git submodule update --init.
  • docs/ — design and working documents.

Nerd Font icons are a committed subset. iris/core/build-icon-font.sh produces iris/core/assets/fonts/nerd_icons.ttf; its codepoints must match iris/core/src/icon.rs. Body and monospace fonts come from the platform.

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 "transcript-screen 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/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.