Merge remote-tracking branch 'origin/main'

# Conflicts:
#	app/androidApp/src/main/kotlin/com/example/aiapp/Sizes.kt
This commit is contained in:
iris committed 2026-09-04 15:03:37 -04:00
commit e3e02d55f7
29 files changed
+2737 -479

No files matched your search

+89 -7
View File
@@ -5,11 +5,6 @@ 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.
**`TRANSCRIPT_RENDERING.md` is the record of the transcript work** --
measurements, techniques, the harness, and the ordered list of what is
next. Read it before touching anything under `Markdown*.kt`,
`Transcript*.kt` or `SessionScreen.kt`'s list.
**`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
@@ -129,6 +124,35 @@ repo is in PLAN.md's "Backend layout" section.
what makes two icon buttons the same width without either being given
one, and it is why `GLYPH_SIZE` is smaller than it looks like it should
be.
- **The file explorer** — `FilesScreen.kt` (the navigation stack, the
per-directory cache, the create dialog), `FileViewer.kt` (a `LazyColumn`
of lines, each with its own colours from `FileLines.kt`, sharing one
horizontal scroll so nothing wraps), `FileEditor.kt` (a
`BasicTextField` with a `VisualTransformation` carrying the scanner's
spans, which is the one Compose API that colours a field's own text).
It draws **over** the session in `AppRoot`'s `Screen.Session`, so the
session under it stays composed and coming back from a file costs
nothing; back steps editor → viewer → directory → parent and only closes
from where it opened. `EXPLORER.md` is the design and `server/src/files.rs`
is the other half.
To exercise it, `./ui-sandbox.sh` builds a fixture tree at the sandbox
home's `~/files` holding the states that are otherwise only reachable by
finding a real machine in one: an empty directory, a name with a tab in
it and one with an apostrophe, a binary file, one over `FILE_LIMIT`, one
`chmod 000`, a symlink to a directory and a broken one, a source file per
language, and the three sizes the limits were measured against
(`edit-32k.rs`, `edit-128k.rs`, `big-source.rs`), so those figures can be
taken again rather than re-derived. Point a session at it with
`./ui-sandbox.sh api /sessions/<id>/cwd -X POST -H 'content-type: application/json' -d '{"cwd":"~/files"}'`.
The 409 is produced by editing the file on the machine (`printf … > file`)
between pressing the pencil and pressing save.
**Reading is cheap and editing is not**, and the sizes are measured
rather than guessed -- see EXPLORER.md's "What the measurements said".
The viewer handles a 1 MiB, 28,000-line file because it draws one row per
line; the editor is one `BasicTextField`, which costs two seconds a frame
at 128 kB and stops the app at 1 MiB, so `EDIT_LIMIT` caps it at 32 kB
with the reason said on screen. If you make the editor faster, that
number is what to move.
- `.dev-updater.ron` — what Dev Updater is asked to do with this checkout:
the server (built in `server/`, run as `service: Managed(...)`) and the
APK (built in `app/`), built in parallel. The project it serves is the
@@ -212,6 +236,23 @@ IQ2_XXS of that model produces fluent nonsense, which reads exactly like a
broken driver — `llama-cli` produces the same from the file directly, which
is how to tell the two apart in a hurry.
**No script that drives this app's UI presses a coordinate.** Every control
is found by the name it already carries for assistive technology --
`ui-trace record --do "tap 'Session settings'"`, which resolves the label
against the screen at the moment of the gesture and fails the whole run
when it is not there. `app/bench-lib.sh` is what `transcript-bench.sh` and
`stream-bench.sh` share for it. A coordinate is a position measured once by
hand, and anything that moves the control makes the tap land on whatever
now sits there -- the bench then reports a number that was never measured,
which reads exactly like a result. Both scripts pressed the render report
at `tap 723 205` until that button moved into the session settings dialog
on 2026-09-03. The check that none has crept back:
grep -n "tap [0-9]" app/*.sh
Swipes are still coordinates, deliberately: a gesture across a scrolling
area is a distance rather than a control.
**How to test SSH here, since there is no second machine:** ssh this VM to
itself. Generate a throwaway key, append the public half to
`~/.ssh/authorized_keys`, and configure a host of `bob@127.0.0.1` with
@@ -324,7 +365,7 @@ first if a remote spawn ever mangles an argument.
**An inline code chip is drawn behind the text** rather than as the
renderer's span background, because a span background is part of the
text's own drawing and hid the selection under it -- see
`appendCodeChip` in `MarkdownLinks.kt` and TRANSCRIPT_RENDERING.md.
`appendCodeChip` in `MarkdownLinks.kt`.
- **A session can be moved to another directory** from the settings dialog
(`POST /sessions/{id}/cwd`). It stops the process, because a working
directory is settled at spawn; the next message starts it in the new one.
@@ -477,7 +518,48 @@ first if a remote spawn ever mangles an argument.
the in-app copy button produces, whose `on screen:` line names what the
viewport was actually holding. Compare two runs of it with the same
gestures; the emulator's absolute frame times transfer nothing, the
report's accounting does.
report's accounting does. Run it either side of any change under
`Markdown*.kt`, `Transcript*.kt` or `SessionScreen.kt`'s list, and put the
report in the commit; the numbers that move first are the worst
`record: one block`, the reparse mean while streaming, and the draw
phase's accounting line.
- **`app/stream-bench.sh [-k] FILE` is that measurement for a reply still
arriving.** It opens the first session, taps "Jump to latest" so the list
is pinned to the newest end, resets the report, sends FILE, waits for the
transcript to stop growing, and prints. Both of those are corrections to a
first version that measured nothing: a transcript parked further back never
redraws while a reply streams into it, and a session is idle at *both* ends
of a turn, so polling for idle answers before the turn has started.
- **`app/trace-draw.sh` names what a scrolling frame spends inside the
framework**, from `atrace` text output with no trace processor needed. It
is how the cost of a layout node per link was attributed to the framework
rather than guessed at.
- **Two traps in the emulator bench loop**, each of which cost a run.
`adb shell pm clear` removes the enrolment and the notification permission
along with the saved anchors, so the next run measures a permission dialog
-- re-enrol with the command `ui-sandbox.sh` prints, and
`pm grant ... POST_NOTIFICATIONS`. And a saved scroll anchor is per session
id, so the only way two builds start a scroll from the same place is a
*fresh session for each*.
- **A phone that falls behind the stream is answered with `reset`, and
`RUST_LOG=ai_server=debug` says when.** Every SSE subscriber logs the
cursor it arrived with and whether it was continued or reset
(`stream backlog:` in `send_backlog`), which is the only place that
question is answerable: the app sees a window arrive and cannot tell how
far it had fallen, and a reset is the one thing that makes its screen jump
to the newest end. Measured 2026-09-04 against a session streaming at 20
events a second: reopening one with an anchor 1,800 events back connects
**87-119 events behind**, well under `CATCH_UP_LIMIT`'s 200, because the
restore is two requests -- the opening page, then one span covering the
whole distance to the anchor. So the reset path is not reachable by
reopening a session, and **to exercise it at all you have to lower
`CATCH_UP_LIMIT`** in a throwaway server build; at 5 the app takes the
reset on a live connection, clears, refills and carries on without
reconnecting. Worth knowing alongside it: **the session screen's stream
survives backgrounding here** -- 20 seconds at the launcher while 415
events were produced brought no reconnect at all -- which is not what the
comment above that loop expects, and is most likely this emulator being
headless rather than the phone's behaviour.
- **`ai-server --delay MS` holds every response back.** Over the tunnel a
phone's requests take tens to hundreds of milliseconds, and several
faults live entirely in what the app does *while* one is outstanding. On