179 lines
8.6 KiB
Markdown
179 lines
8.6 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 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.
|