The file explorer on the phone

The other half of EXPLORER.md: a folder button on the session header opens
the machine's filesystem, starting where the session works.

It draws **over** the session in the same `Box`, so the session under it
stays composed -- its event stream keeps flowing, its draft and scroll
position stay where they were, and coming back from a file costs nothing.
Back steps one level inside it (editor, viewer, directory, parent) and only
closes from where it opened; the platform gesture, the button and the swipe
all go through the one function, so they cannot mean different things.

The viewer is a `LazyColumn` of lines rather than one `Text`, because text
layout is linear in the text and a twenty-thousand-line file in a single
`Text` measures all of it to draw a screenful. Lines do not wrap and share
one horizontal scroll, so a logical line is a visual line and the gutter
cannot come to number the wrong text; the gutter's width is measured from
the digit count of the line count in the style it is drawn in. The editor
is a `BasicTextField` with a `VisualTransformation` carrying the scanner's
spans, which is the one Compose API that colours a field's own text rather
than replacing the field.

`fileLanguage` reads the same table `fenceLanguage` does, so a language
added for fences is a language added for files.

A file that changed on the machine while it was open here refuses to be
overwritten and asks, with what each of the three answers costs. That is
the ordinary case, not the exotic one: an agent editing the file somebody
is reading is what this whole feature is for.

The speedometer moves off the header into the session settings dialog,
where the session's other about-the-session controls are, and the folder
takes a place between the usage chart and the cog -- widest scope to
narrowest, cog at the end, as Iris asked. Both benchmark scripts move onto
`ui-trace`'s new tap-by-label action in the same change, so the render
report is never unavailable and never pressed at a coordinate that has
stopped meaning anything; `app/bench-lib.sh` is what they share, and
`grep -n "tap [0-9]" app/*.sh` is the check.

Exercised on the emulator against the sandbox's new fixture tree, with a
screenshot or a ui-trace for each: the listing (dotfiles, directories
first, a symlink to a directory sorted with them, a name with a tab in it),
a highlighted file, binary, too big, a permission error, editing and
saving, the 409 and its Overwrite, back with unsaved edits, creating a name
that exists, creating one that does not and landing in the editor, an empty
directory, and `..` above the directory the session opened in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-03 23:58:59 -04:00
1 parent 4a9c547293
commit db55ed4a8f
22 files changed
+1647 -161

No files matched your search

+37
View File
@@ -129,6 +129,26 @@ 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, and a source file
per language. 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.
- `.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 +232,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