Delete and import sessions in batches, and say which rows are busy

Clearing out imported sessions was one confirmation dialog per row, which
is why it was not worth doing. Holding a row on the import screen now
selects it and plain taps add more; Delete and Import act on the whole
selection from a bar along the bottom.

Submitting hands the work over and puts the screen back as it was: the
selection clears, the bar goes, and what says the work is happening is the
rows it is happening to -- the one in flight marked with its operation, the
rest marked "waiting". Both are inert, so a queued row cannot be tapped
into starting a second CLI behind the batch already coming for it. Rows
leave as each one lands rather than all at the end, because a finished row
still sitting there looks exactly like one that was never imported; the
rows below it therefore move, so a row that has just moved ignores taps for
half a second.

That busy appearance is one composable shared with the session list, which
had its own dimmed row and its own word for it. It is a word rather than a
bare spinner because deleting and importing differ in kind.

Deleting a session can now take the machine's own transcript with it, as a
switch in the confirmation and only where the driver keeps a record this
app's delete cannot otherwise reach. Off by default, since leaving that
copy is what makes an ordinary delete recoverable -- and the paragraph is
rewritten rather than appended to when it is on, because the sentence
promising the conversation is still there to import again is exactly the
one the switch makes false. The server removes the machine's copy first, so
a machine it cannot reach leaves the session where it was.

`app/ui-sandbox.sh` is how all of this was driven: a second server with its
own $HOME, invented transcripts and a two-line `claude`. Against the
ordinary server, testing delete deletes somebody's conversation and testing
import spends a turn on a real account.
This commit is contained in:
iris committed 2026-08-31 19:51:26 -04:00
1 parent 21d22f89c2
commit fd2e1d0798
8 files changed
+960 -296

No files matched your search

+38
View File
@@ -203,6 +203,33 @@ first if a remote spawn ever mangles an argument.
and lint, the app-side equivalent of the line above. Then `./build-apk.sh`
to produce the APK to install on a phone (through Dev Updater), or
`./run-android.sh` to build, install, and launch on the emulator.
- **A row something is happening to is dimmed, drained of colour, inert,
and says which operation in a word** -- `BusyItem`, used by both the
session list and the import list so the appearance is learned once. The
word rather than a bare spinner because "deleting" and "importing" differ
in kind, and the inertness is the overlay consuming pointer events rather
than each caller remembering to disable its own click handler.
- **The import screen selects in batches: hold to enter, tap to add.** The
options that act on a selection appear along the bottom, and are Delete
and Import only. Submitting clears the selection immediately and marks
every chosen row -- the one in flight as "importing" or "deleting", the
rest as "waiting" -- so the bar goes away and the affected set is what
says the work is happening. Rows are taken out as each one lands rather
than all at the end: a finished row still sitting there looks exactly
like one that has not been imported, and tapping it starts a second CLI
on the same transcript. What that costs is that the rows below slide up
under the reader's finger, so a row that has just moved ignores taps for
half a second (`SETTLE_MS`).
- **Deleting a session offers to take the machine's own transcript with
it.** `DELETE /sessions/{id}?deleteForeign=true`, behind a switch in the
confirmation, and only where the driver keeps a record of its own
(`keepsOwnTranscript`, which today means Claude Code). Off by default,
because leaving that copy is what makes an ordinary delete recoverable --
and the dialog's paragraph is rewritten when it is on rather than
appended to, since the sentence promising the conversation "should still
be there to import again" is exactly the one the switch makes false. The
server deletes the machine's copy *first*, so a machine it cannot reach
leaves the session where it was instead of half-deleted.
- **Android Lint is not optional and is not run by a build.** It found a
crash that had been shipping: `java.time` on a minSdk-24 app with
desugaring off — and later a permission check that silently dropped every
@@ -234,6 +261,17 @@ 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
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
import screen lists whatever is in `~/.claude/projects`, which in this VM
is real agent transcripts, so exercising *delete* against the ordinary
server deletes somebody's conversation and exercising *import* starts a
real `--resume` on Bryan'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.
It passes `--delay` by default for the reason the next entry gives.
- **`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