Files
ai-app/AGENTS.md
T

178 lines
8.5 KiB
Markdown

# 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 "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/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.