PLAN.md: UserMessage/Answered events in the common model, the --bind dev override (fail-closed default untouched), enrollment via the aiapp:// intent filter, Keystore-sealed token storage; phase 1 marked done with what was verified. AGENTS.md: real layout, commands, and the lessons that bit (tracing callsite cache in tests, adjustResize, CMP accessor deprecation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
102 lines
5.7 KiB
Markdown
102 lines
5.7 KiB
Markdown
# ai-app
|
|
|
|
A phone interface to AI coding sessions (Claude Code and llama.cpp via pi),
|
|
replacing the Claude app for daily use. Rust/Axum backend on the desktop,
|
|
Kotlin/Compose Android app, WireGuard + pinned self-signed TLS + bearer token
|
|
between them.
|
|
|
|
**`PLAN.md` is the design source of truth.** Read it before building or
|
|
changing anything structural. It records every decision with its date, its
|
|
rationale, and the alternatives that were rejected and why — keep that habit
|
|
when a decision changes: update the plan in place, don't let this file and
|
|
the plan drift into two versions of the truth. This file is the working notes
|
|
layer: conventions, commands, and things that have bitten.
|
|
|
|
The central design point, worth not undoing by accident: **a session is a
|
|
child process speaking JSONL over stdio, translated into one common event
|
|
model.** Claude Code (stream-json) and pi (RPC mode) are two translators
|
|
behind one `Driver` trait; the transcript, the SSE stream, the phone UI, and
|
|
SSH spawning (the same command wrapped in `ssh host …`) all work purely in
|
|
the common model. A new session type is a new driver — never a
|
|
session-type branch in shared code (routes, transcript, app screens).
|
|
|
|
## Layout
|
|
|
|
Mirrors `../local-updater` deliberately — same stack (axum 0.8 +
|
|
axum-server/rustls, tokio, clap; Kotlin 2.4.x + Compose Multiplatform,
|
|
single `:androidApp` module), same cert scheme, same registry pattern (every
|
|
session mutation funnels through the manager so in-memory and on-disk state
|
|
can't come apart). Read local-updater's `README.md` and `AGENTS.md` for the
|
|
conventions before diverging from them; module-by-module intent for this
|
|
repo is in PLAN.md's "Backend layout" section.
|
|
|
|
- `server/` — Rust backend (`ai-server`). `main.rs` bootstraps (TLS, the
|
|
auth layer, token/QR enrollment, wg0 binding), `routes.rs` has the HTTP
|
|
table in its module doc comment, `auth.rs` the bearer-token middleware,
|
|
`config.rs` the persisted schema, `session/` the manager (registry
|
|
pattern), `Driver` trait + event model, `EchoDriver`, and transcripts.
|
|
- `app/` — Compose Android app, single `:androidApp` module, package
|
|
`com.example.aiapp`, label "AI Sessions". `AppRoot.kt` is the navigation
|
|
`when`; `Api.kt`/`EventStream.kt` the REST + SSE clients; `Events.kt` the
|
|
event model mirror; `ServerConfig.kt` settings + Keystore-sealed token;
|
|
screens in `SessionListScreen/SessionScreen/SpawnScreen/SettingsScreen`.
|
|
- `gen-dev-cert.sh` / `certs/` — copied from local-updater's scheme
|
|
(idempotent CA, reissued leaf; regenerating the CA strands the installed
|
|
app — same one-way door). Dev SANs cover 127.0.0.1, 10.0.2.2 (emulator →
|
|
host), and the LAN IP alongside the WireGuard address.
|
|
|
|
## Status
|
|
|
|
Phase 1 (Skeleton) done 2026-08-24 — the whole pipe works end-to-end
|
|
against `EchoDriver`: TLS + token auth, SSE with transcript cursors
|
|
surviving restarts, spawn/message/question/delete from the app. Next:
|
|
phase 2 (Claude driver); phases are in PLAN.md.
|
|
|
|
## Checking your work
|
|
|
|
- Server: `./run-tests.sh` (or `cargo test`) + `cargo clippy --all-targets`
|
|
from `server/` — the build stays warning-clean, keep it that way.
|
|
- App: from `app/`, `. ./android-env.sh && ./gradlew :androidApp:compileDebugKotlin`;
|
|
`./run-android.sh` builds, installs, and launches on the emulator.
|
|
- Run the server for development with `--bind 127.0.0.1` (wg0 doesn't exist
|
|
on this machine yet; the default fails closed). First run prints the
|
|
enrollment QR/URI with the token — capture it from the log.
|
|
- Prefer exercising the server directly over going through the UI:
|
|
`curl --cacert certs/ca.pem -H "Authorization: Bearer …" https://127.0.0.1:8443/sessions`.
|
|
The emulator app reaches it at `https://10.0.2.2:8443`; enroll it with
|
|
`adb shell "am start -a android.intent.action.VIEW -d 'aiapp://enroll?host=10.0.2.2&port=8443&token=…'"`
|
|
(quote so the device shell doesn't eat the `&`s).
|
|
|
|
## Things that have bitten
|
|
|
|
- **tracing caches callsite interest process-wide.** A test that hits a
|
|
`tracing::warn!` with no subscriber installed can poison the interest
|
|
cache for a concurrent test that captures logs (flaky "nothing was
|
|
logged" failures). Keep every exercise of a logging code path under the
|
|
one capturing subscriber — that's why the auth middleware has a single
|
|
combined gating+logging test.
|
|
- **The keyboard pans the window unless the activity opts into resize.**
|
|
Without `android:windowSoftInputMode="adjustResize"`, opening the IME
|
|
slides the whole window up (top bar off screen) instead of resizing —
|
|
`imePadding()` alone doesn't fix it and the transcript looks empty.
|
|
- **CMP 1.11 deprecates the `compose.*` dependency accessors** — declare
|
|
`org.jetbrains.compose.<x>:<x>` directly (material3 has its own release
|
|
train, separate from the CMP version).
|
|
|
|
## Environment notes (this machine, learned in local-updater)
|
|
|
|
- Android SDK is at `~/Android/Sdk`, not the root-owned `/opt/android-sdk`
|
|
the ambient `$ANDROID_HOME` may point at; copy local-updater's
|
|
`android-env.sh` override pattern.
|
|
- Each agent command runs in a fresh shell — exported environment does not
|
|
carry over. Chain: `cd app && . ./android-env.sh && ./gradlew …`. Never
|
|
pipe `source` into `head`/`grep` (subshell discards the exports).
|
|
- The shared emulator AVD is named `tdep` — one emulator across the Android
|
|
projects on this machine, not one per repo.
|
|
- Long-running servers launched from an agent must be fully detached
|
|
(`setsid nohup … & disown -h`, verify `PPID 1`), and every
|
|
`pgrep -f`/`pkill -f` pattern needs its first character bracketed
|
|
(`[a]i-server`) in **every** occurrence in the command, or the pattern
|
|
matches the shell running it. Full explanation in local-updater's
|
|
`AGENTS.md` — it bites exactly the same way here.
|