Split the newest reply once its turn ends, and make the UI harness reusable

The transcript's remaining lag was the newest assistant reply: transcriptUnits
kept the last row whole -- right while it streams (splitting a changing text
is a parse per delta), wrong forever after, so a session that ends on a long
reply drew it as one lazy-list item with every node alive. On a Pixel 9 Pro
XL that was 13.8ms of draw phase a frame, 79% of it the framework's own
per-node bookkeeping, against a 34,996px item.

An AssistantMsg now carries `settled`, folded from the status event that ends
its turn (status changes are transcript events with seqs, so replay settles
the same way), and cleared if a delta ever grows the message again. A settled
newest reply splits like every other. Folding it -- rather than reading the
screen's status -- routes the resplit through the held-events gate, so it can
only happen at the newest end while pinned, never under a reader. The
"session is working" predicate now lives once, in sessionWorking().

Measured on the emulator, same session and gestures, a 43KB reply as the
last row: draw phase 3.92ms -> 1.20ms per frame, framework share 3.07ms
(78%) -> 0.54ms (45%), worst single measure 82.5ms -> 9.1ms. The report's
"on screen" line went from one 60,674px AssistantMsg to five blocks of
95-846px. A live streamed turn settles and splits the moment it goes idle.

The harness half, asked for by Bryan: ui-sandbox.sh now derives its port and
root from the checkout name (two checkouts' sandboxes cannot reach each
other), keeps its token in ~/.config/ai-app/sandbox-token and salvages
enrolled device tokens across restarts (enrol the emulator once, ever), and
gained the driving verbs every UI session was re-inventing in /tmp: spawn,
send (text or @file), api. transcript-bench.sh is the standard
scroll-and-report measurement. AGENTS.md documents all of it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5 committed 2026-09-01 17:19:19 -04:00
1 parent 917eb9a7b3
commit 7a48f8ff1f
8 files changed
+250 -23

No files matched your search

+20 -2
View File
@@ -369,7 +369,7 @@ first if a remote spawn ever mangles an argument.
conversation somebody may still be in. **A transcript never goes in this
repository**: they hold whatever was said, read and written in that
session, and `~/repos` is shared with the host besides.
- **`app/ui-sandbox.sh` is the rig for anything that lists or deletes
- **`app/ui-sandbox.sh` is the rig for driving the UI against invented
sessions.** It starts a second `ai-server` with its own `$HOME`, config
and data directory, holding eight invented Claude Code transcripts and a
`claude` that is two lines of shell. That isolation is the point: the
@@ -378,13 +378,31 @@ first if a remote spawn ever mangles an argument.
server deletes somebody's conversation and exercising *import* starts a
real `--resume` on the owner's account. Neither is a price worth paying to
look at a list. It shares the real TLS certificates, because the
installed APK pins that CA, so run it while the ordinary server is down.
installed APK pins that CA.
Its port and root are derived from the checkout's name, so two checkouts'
sandboxes (and the emulators enrolled against them) cannot reach each
other, and its token is generated once into
`~/.config/ai-app/sandbox-token` and carried across restarts along with
any tokens the server's own enrolment flow appended -- so the emulator app
is enrolled **once** (the start banner prints the command) and stays
enrolled. It also carries the driving verbs every UI investigation needs,
so none of this is re-derived per session:
`./ui-sandbox.sh spawn [title]` (an echo session, prints its id),
`./ui-sandbox.sh send SID text|@file`, and
`./ui-sandbox.sh api /path [curl args]` for everything else.
It passes `--delay` by default for the reason the next entry gives, and
`AI_SANDBOX_BIG_MB` puts one large transcript among the small ones --
`AI_SANDBOX_SPAWN_DELAY` makes the fake CLI slow to start. Both exist
because operations that finish in milliseconds have states on the way that
nothing can observe, and an unobservable state is one where broken and
working look identical.
- **`app/transcript-bench.sh` is the standard scroll measurement.** It
opens the first session (or `-k` keeps the current screen), scrolls a
fixed gesture loop, and prints the app's render report -- the same one
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.
- **`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