ba6817fee5b44fd803e72dda56441d62b8eead46
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
79682f03a7 |
Condense the documentation and thin the server's comments
The markdown had accumulated a lot that was stale rather than wrong. PLAN.md still described pi as the llama.cpp harness, a refcounted LlamaServerManager, and a providers-by-hosts cross-product, all of which were superseded or never built; it also carried a second copy of the HTTP table that routes.rs owns. EXPLORER.md and TRANSCRIPT_CACHE.md held implementation checklists for work that has since landed. AGENTS.md restated most of PLAN.md's design instead of being the working-notes layer it says it is. 3225 lines of markdown to 2180, with the stale sections gone rather than reworded. On the server, comments explaining what the code already says are out and the ones recording a constraint, a measurement or an incident are kept but cut to a few lines each: 5504 comment lines to 4586. Four doc comments in session/mod.rs, and one each in process.rs and usage.rs, had drifted onto the item above the one they describe -- functions were reordered without them, so `stop_session`'s doc sat on `set_session_cwd`, `stat_of`'s on `struct Stat`, and `UsageMonitor`'s on `type Cached`. Each is back on its own item. routes.rs's module table also claimed later phases would add `/hosts`, which setups replaced. cargo test (127 passed), clippy --all-targets and fmt are clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a802522039 |
Keep visited transcripts on the phone
Reopening a session downloaded the conversation again, every time, over the tunnel. It now draws from a copy of what the server has already sent and asks for one event to check that copy is still current. Per session, under cacheDir, the server's own event lines in chunks named for the range they cover -- so a coalesced page, whose lines do not say what they cover, still records it. Only the contiguous run ending at the newest chunk is served; a gap is closed by paging through it, bounded by `after` on /transcript so the page stops where the phone's copy starts and can therefore be kept. Nothing is derived and stored: rows are a rendering, and a cache of them would need throwing away on every change to the fold. Nothing here is load-bearing. Missing, evicted, damaged or unwritable all degrade to the cold open this screen did before, and the check before the stream resumes -- one request, one event -- is what stops a replaced or truncated file being spliced onto a copy of a different conversation. What that check cannot see, a line changed mid-file with the tail intact, is what Reload in session settings is for. Measured on the emulator against ui-sandbox, on a 505-event session: reopening it costs one request for one event, including scrolling the whole conversation back; a cold open is two requests and 100 events. A reset after falling 300 behind fetched the gap as four coalesced rows rather than re-fetching 104 events and discarding them. Every chunk was checked line by line against what the server says for the range its name claims, across the reset and the gap-fill. transcript-bench.sh, same viewport content and gestures, before and after: p50 16.9ms both, p90 25.6 -> 23.2ms, p99 33.5 -> 36.7ms, and the transcript's own draw accounting 0.33ms -> 0.32ms with place 0.31ms either way. Within the emulator's noise, which is what a cache must be: it changes what is fetched, not what is drawn. Building it also found that the server handed out the same transcript line two different ways. serde_json's default float parser is not correctly rounded, so a ts written as ...0757 came back from /transcript as ...0755 while the SSE stream sent the original -- invisible on screen, since a ts is drawn as a relative time, and visible here only because the cache compares a line it holds against the server's answer. Fixed with float_roundtrip, with a test that fails the moment it is dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cc7e4f63ef |
Read and change a machine's files from the backend
The first half of EXPLORER.md: server/src/files.rs, which lists a directory, reads a file, writes one, and creates a file or a directory on whichever machine a setup names. Each operation is one small POSIX script run through `Transport`, the way the import listing and the usage fetch already ask a machine a question, so the local and the ssh case are one implementation rather than two that drift. The path crosses as a positional argument and never as script text; `PATH_PRELUDE` is the one line that gives a leading `~` its meaning, because a shell expands a tilde in text and not in an argument, and it is the far machine's home that has to answer. A read has four answers -- text, binary, tooBig, or the machine's own error -- because a binary file drawn as text and a big one cut off silently are both wrong in ways the reader cannot see. A write carries the sha256 the read reported and is refused with a 409 when the file has moved on, which is what happens whenever an agent is editing the file somebody is reading. `Transport::capture_with_input` is the one description of "run this there, with this on stdin", and `ship_attachment` moves onto it rather than assembling a second ssh invocation of its own. It is also the only capture that hands back the exit status, which is how the write says "this is not the file you read" without that answer looking like a failure. Exercised on both transports against the sandbox -- ssh to this VM with a throwaway key, since the quoting and the stdin path are what that proves -- including a filename with an apostrophe, one with a tab, an unreadable file, a binary one, one over the limit, and the 409. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
801618ba0e |
Stream attachments end to end, ship files to a remote session's machine, and write up the transcript work
Uploads no longer sit whole in memory anywhere: the phone writes the multipart body chunked as it reads the picked file, and the server writes each chunk to a `.part` file under the session and renames it when whole. The per-request cap is 4 GB and bounds disk, not memory. A file attached to a session on another machine is copied there in the same request: one ssh invocation takes the bytes on stdin into the setup's `attachmentsDir` (new, optional, on the machine form and in the config), else the session's cwd, else the login home, and answers with `pwd -P`, which is recorded beside the file as `<name>.remote` and is the path the driver tells the CLI. A failed copy fails the upload and says why, so no message ever names a file that is not there. The host keeps its copy so transcripts can reference and fetch it. Measured against the Gentoo test guest: a 40 MB file shared from the phone arrived there byte for byte. The tilde in that setting is the remote home, so it is not expanded on the server the way other setup paths are. TRANSCRIPT_RENDERING.md records the week of transcript work -- the measurements behind each decision, the harness, what was rejected, and what to do next -- so a new session can start from it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
6180663f14 |
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 (`<hex>-<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 <command>` 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 <noreply@anthropic.com> |
||
|
|
465645cefb |
Draw a peer message above the turn it started, not below it
The live Claude Code path only learns a turn was another agent's when the turn ends -- the whole of the message arrives as an `origin` object on the `result` -- so the note was appended after everything it caused, and the transcript showed the answer above the question. It cannot be recorded in place: by the time anyone knows, the reply is already written, and the transcript is append-only. So the event carries where it belongs instead. `PeerMessage` gains `turnStart`, the seq of the status that opened its turn, stamped by the pump -- the only thing that knows a seq and the only thing that sees every driver's turns. The phone gives the note that seq, so it sorts into place rather than being drawn out of order at the end. A status draws no row, so there is nothing for it to collide with and the list stays sorted, which the scroll anchor and paging both depend on. Absent where there is nothing to correct: a message replayed out of a session file by `import` is already in the right place, and one that opened no turn has no turn to sit above. Both stay where they arrive. The echo driver gets `/peer-turn` for the live shape, beside `/peer` for the in-place one. Verified on the emulator both ways, live and on replay, plus an ordinary `/tools` turn to confirm the run grouping the insertion cuts through is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
deb908034c |
Move a session to another working directory
`POST /sessions/{id}/cwd`, behind a field in the session settings dialog. A
working directory is settled when the process is spawned -- the CLI is
launched with it as its cwd and there is no control request that changes one
-- so this records the new one and ends the process that is in the old one.
It does not start a replacement, and the field says so in a line beside it:
a session with no process starts on the next message or on Start, which is
this app's rule for that everywhere else, and "usually restarts" is a worse
control than "always stops".
The path is checked against the session's own machine and refused if it is
not there. The spawn path corrects instead of refusing, because it is
resuming a directory the *machine* recorded and that can be gone through
nobody's fault; a path somebody has just typed is different, and a mistyped
one accepted here would surface much later as a session that would not
start, with nothing pointing at the typo. The refusal names the machine and
the path, and is drawn under the field it is about.
Nothing of Claude Code's own is moved, and that is measured rather than
assumed: on CLI 2.1.237, `claude --resume <id>` finds a session from any
working directory -- an id that does not exist answers "No conversation
found with session ID", and a real one resumed from an unrelated directory
did not. So the conversation continues in the new place with nothing
relocated. Doing otherwise would mean reproducing a rule this app cannot
see the whole of; PLAN.md records what that rule is, for whoever tries.
Found while checking it: `SessionInfo.cwd` came from the snapshot a session
launched with, so a moved session went on reporting its *old* directory for
as long as its process lived -- a dialog showing a directory the next launch
would not use, with nothing saying so. It is read from the config where the
row is built now, the same way `setup_name` already was, and for the reason
already written above `setup_name`: only the manager holds the config, and
both of these change under a running session.
Checked end to end on the emulator against a session whose process really
does take a cwd: /proc said /tmp/cwd-a before and /tmp/cwd-b after, the
dialog showed the new path immediately rather than after a restart, and a
directory that is not there and a relative path were both refused with the
session left exactly as it was.
|
||
|
|
6236f0d5bd |
Show a message another agent sent, on a session this server is running
Peer messages were only ever produced by the *import* path, reading them out of the CLI's own session file. A message another agent sent to a session this server was driving appeared nowhere, so the session started working on something nobody on the phone had asked for and there was nothing on screen to explain it. Measured rather than guessed, because the obvious place to look for it is empty: a real cross-session message sent to a real `--input-format stream-json` session on CLI 2.1.237 produces **no `user` record**, and nothing in the partial-message stream mentions it either. The whole of it arrives as an `origin` object on the turn's `result`, in exactly the shape the session file records -- so `import::peer_message` now reads both, one function for one wire format. Two copies would drift the first time a field is renamed, and the half that drifted would go on producing nothing, which is indistinguishable from nobody having sent anything. The cost is the position: the note lands after the reply it caused rather than above it, because at no earlier point in the turn does the CLI say why the turn started. Taken deliberately over the alternative -- a second reader tailing the CLI's own session file for the one record stdout does not carry, which is two sources of truth for one conversation and a poll per live session. Recorded in PLAN.md so that if the CLI ever announces the injection where it happens, the next reader knows to move it there. Both halves tested: the real record shape, and an ordinary result carrying no `origin` -- which is the half that decides whether the check is a check. Four ordinary results on a real session's stdout had none between them. |
||
|
|
a1eedd7a78 |
Don't call a turn finished with a message still waiting behind it
A message written into the tail of a turn is read the moment that turn's `result` lands: the session reports idle and is running again in the same breath. The phone that sent it got "finished" in between -- seconds before anything it asked for had been done, which is the notification arriving to say the opposite of what is happening. `notification_for` now takes how many messages the session has been given and not started reading, and a turn ending with any of them waiting is not an ending. The count is kept in `pump`, from the recorded events, because that is the one place that sees all of them in transcript order: a `messageQueued` up, and the `userMessage` that resolves it or a `messageDropped` down. Asking the driver instead would answer about the moment the question was asked rather than the moment the status was written, which is the same class of mistake as reading a session's status to decide what a queue contains. It deliberately does not suppress *awaiting input*. A question is worth interrupting somebody for whatever is queued behind it -- the queue is precisely what will not move until it is answered. Tested both halves: the decision on the number, and the number itself, where an echo turn that reads its queued message before going idle still announces its finish. That last is the case a suppression written slightly wrong silences, and it is the common one. |
||
|
|
82401cd887 |
Select any of the transcript, and take a queued message back
Two things a reader could not do to what is on screen.
**Selection.** Nothing in the transcript was selectable at all, so a
command, a path or an error message could be read and not copied. One
`SelectionContainer` around the whole list rather than one per row: a
transcript is one body of text to a reader, and a selection has to be able
to run from a reply into the tool output under it. Per row it also could
not, and whatever was drawn without a container would have been silently
unselectable -- a state nothing on screen reports. Rows keep their tap
handlers; checked on the emulator that expanding a tool call, scrolling and
flinging are all unaffected, since a selection is a long press.
**Taking a message back.** A message sent into a running turn sits as a
bubble waiting to be read, and there was no way to change your mind: it is
tappable now, and the server answers `POST /sessions/{id}/unqueue`.
The answer has three states, and the middle one is the point. Claude's
driver writes a steer into the CLI's stdin the instant it arrives -- that
is what makes it reach the model at the next tool boundary rather than at
the end of the turn, and it was measured -- so the line is already gone and
`AlreadySent` is the only honest answer it can give. Holding the write
until a boundary would make the drop real and cost a steer one model call,
which is the latency the immediate write exists to remove; rejected on that
trade, with the reasoning in PLAN.md. The refusal is drawn on the bubble
that was pressed rather than in the error row under the header, a screen
away from it.
Where a driver really does hold its queue -- echo today -- the message goes
for good, and it goes as an `Event::MessageDropped` rather than as a return
value: every device watching the session loses the bubble, and a phone that
reconnects and replays the `messageQueued` does not put back one that was
cancelled with nothing left to resolve it.
|
||
|
|
b172c464ea |
ai-app: a phone interface to Claude Code and llama.cpp sessions
A Rust backend that owns the sessions and an Android app that reads them. The server spawns and adopts CLI processes, normalises everything they emit into one event model, keeps the transcript, and serves it over pinned TLS on a WireGuard interface; the phone streams that, replies, sends images, and imports conversations the machine already has. `AGENTS.md` is the working guide -- what runs where, what has been measured, and the faults that were expensive to find. `PLAN.md` is the design record. History before this point was squashed away. It was a personal project's running commentary and carried a name and a couple of machine paths that have no business in a public repository; the tree is what mattered and the tree is here. |