From 6180663f1419187f1c8b2f70d87fb202f625346a Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Thu, 3 Sep 2026 08:15:10 -0400 Subject: [PATCH] Attach any file, take shares from other apps, and survive a backwards highlight Attachments were images only. Now any file can be attached: from the file chooser behind the "+" menu, or from Android's share sheet, which the app is now in. An image still goes to the model as a picture; anything else is stored under its own name (`-`, cleaned by `safe_file_name`) and the Claude driver ends the message with `Attached file: /abs/path`, since the CLI reads files by path and a model cannot be shown a trace. The user-message field is renamed `images` -> `attachments` on both sides, with a serde alias reading the rows written before. A share arrives before anyone has said which session it is for, so it is held in AppRoot with a banner on the list until a session takes it; an open session takes it at once. Unreadable shares are reported beside the composer, not thrown. The tool card crashed the app when opened on a command holding a quoted glob such as `-path '*/.git/*'`: highlights 1.1.0's shell lexer answers `x '*/a/*'` with a span whose end is before its start, and AnnotatedString refuses the range. Such spans are dropped; the library is the place for the fix. The echo driver gains `/bash ` so a card with a given command can be produced on the emulator. ui-sandbox.sh's token salvage read the tokens block's close only at a line start, ran past the compact `),],` the server writes, and copied `setups` into the new config twice, which the server then refused. Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 13 ++ PLAN.md | 17 ++- app/androidApp/src/main/AndroidManifest.xml | 10 ++ .../src/main/kotlin/com/example/aiapp/Api.kt | 11 +- .../main/kotlin/com/example/aiapp/AppRoot.kt | 29 +++- .../kotlin/com/example/aiapp/Attachment.kt | 66 +++++++++ .../kotlin/com/example/aiapp/Attachments.kt | 67 ++++++++- .../main/kotlin/com/example/aiapp/Events.kt | 11 +- .../kotlin/com/example/aiapp/MainActivity.kt | 23 +++- .../kotlin/com/example/aiapp/MainScreen.kt | 21 +++ .../com/example/aiapp/PendingAttachments.kt | 40 +++++- .../kotlin/com/example/aiapp/SessionScreen.kt | 129 ++++++++++++------ .../main/kotlin/com/example/aiapp/Share.kt | 44 ++++++ .../kotlin/com/example/aiapp/ToolInput.kt | 9 ++ .../com/example/aiapp/TranscriptItems.kt | 4 +- .../com/example/aiapp/TranscriptUnits.kt | 4 +- app/ui-sandbox.sh | 13 +- server/src/routes.rs | 51 +++++-- server/src/session/claude.rs | 78 +++++++---- server/src/session/driver.rs | 25 +++- server/src/session/echo.rs | 60 +++++--- server/src/session/import.rs | 2 +- server/src/session/llama.rs | 32 ++--- server/src/session/mod.rs | 76 +++++++++-- server/src/session/transcript.rs | 2 +- 25 files changed, 672 insertions(+), 165 deletions(-) create mode 100644 app/androidApp/src/main/kotlin/com/example/aiapp/Attachment.kt create mode 100644 app/androidApp/src/main/kotlin/com/example/aiapp/Share.kt diff --git a/AGENTS.md b/AGENTS.md index 23824bb..8ff776d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,6 +65,19 @@ repo is in PLAN.md's "Backend layout" section. refuse to resume onto a partial from a different revision, and are checked against HuggingFace's published sha256 before the file gets its real name. +- **Attachments** are one list on a user message (`attachments`, the + ref the files route serves), in two shapes. An image is `.` + and goes to the model as an image block. Anything else is + `-` -- the name it was shared or picked under, cleaned by + `safe_file_name` -- and the Claude driver appends `Attached file: + /abs/path` to the message text, since the CLI reads files by path and + a model cannot be shown a trace. `media::media_type_for` on the server + and `isImageRef` on the phone tell the two apart; keep those lists + level. The phone attaches from the photo picker, the file chooser and + Android's share sheet (`Share.kt`; the manifest's SEND filter), all + through one `attach` path in `SessionScreen`. Files exist only on the + server's machine -- see PLAN.md's "Transport" for what that means for + remote sessions. - `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, diff --git a/PLAN.md b/PLAN.md index bbb68ca..2ded77a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -713,11 +713,18 @@ host) and **hosts**. The manager runs at most one llama-server per forwarded port (`ssh -L`) as well as a spawned process. A transport is therefore "run this" plus "reach this port", and the second operation is a no-op locally. -- Attachments need no file transfer, contrary to what this section said +- Images need no file transfer, contrary to what this section said before: `attachment_block` base64s an uploaded image into the stream-json message itself, and produced images come back the same way for the translator to write out locally. Nothing has to exist on the remote filesystem, so there is no `scp` step to get wrong. +- **Any other file is told to the session by path** (2026-09-03: a trace, + a log, a zip -- things a model cannot be shown and the CLI can read). + The upload stays under the session's `attachments/` and the message + ends with `Attached file: /abs/path`. That directory exists only on the + machine running this server, so a file attached to a remote (ssh) + session names a path that is not there. Shipping it is not built; the + one host in use runs its sessions locally. Images are unaffected. ### Usage limits (Claude) @@ -1094,9 +1101,11 @@ window just fills. with port forward, attachment shipping. *Host config and remote spawn done 2026-08-25* (any session of any provider can name a host; the command is the identical one wrapped in `ssh -T`, with every argument - shell-quoted). Attachment shipping turned out to be unnecessary for the - Claude driver — images ride the stdio JSONL as base64 in both - directions, so nothing needs `scp`. Still outstanding: remote + shell-quoted). Attachment shipping turned out to be unnecessary for + images — they ride the stdio JSONL as base64 in both directions, so + nothing needs `scp` — and became necessary again on 2026-09-03 for + files, which are attached by path (see "Transport" above). Still + outstanding: file shipping for remote sessions, and remote llama-server with its port forward, which comes with phase 4. Two things learned doing it: a remote session inherits ssh's non-login PATH, which is narrower than an interactive shell's (point `command` at diff --git a/app/androidApp/src/main/AndroidManifest.xml b/app/androidApp/src/main/AndroidManifest.xml index ec5afd0..2958fb5 100644 --- a/app/androidApp/src/main/AndroidManifest.xml +++ b/app/androidApp/src/main/AndroidManifest.xml @@ -61,6 +61,16 @@ + + + + + + +