70665e7375a9c8158c8bf78da9b524ca456f877d
An `ssh_exec` card was blank for as long as the command took. `apt upgrade` gave you a spinner for four minutes and then four minutes of text at once, which is the worst possible ordering for the one thing you were watching for — the line where it went wrong. Pi has had the answer since `AgentToolUpdateCallback`: a tool may report what it has so far, `executePreparedToolCall` turns each report into a `tool_execution_update`, and the TUI redraws the card from it. Ported here, with one change of address. Pi passes the callback as a fourth argument to `execute` because it has nowhere else to put it; `AgentToolExecutionContext` already threads per-call state to every tool, so `report` goes there and no tool signature changes. Where the partial goes is the whole design. `toolProgress` is not durable, on the same grounds as `messageDelta` — history is written from run snapshots, so a card that fills in as output arrives must not also stream database writes. And it never reaches the transcript: the model is owed one result per call, not a running commentary, and a turn that replayed three partial views of the same command would pay for all three. The card gets everything; the model gets what the tool finally returned. The card's state is also left alone, because a call that has something to show has not therefore finished. `SSHExecSession` grows an `onOutput` handler called at the four points the pump already captures a chunk. It runs on the session's serial queue between two `libssh2` reads, so the contract is that it returns promptly and never awaits — anything it blocks on stalls every command on that host. `AgentCommandOutputStream` is the intended shape on the other side: a lock, an accumulator, and a callback fired outside the lock. Two details it exists to get right. Reports carry everything captured so far rather than the newest chunk, because the card replaces its result wholesale and would otherwise flicker instead of growing. And they are throttled to one per 150ms — a build printing a thousand lines a second would otherwise be a thousand SwiftUI invalidations per second to render text nobody can read at that speed. Throttling drops the report, never the output: what was captured is still captured, and the finished result follows moments later regardless. A chunk boundary can also fall inside a multi-byte character; that decodes as a replacement character and corrects itself on the next report, and only the live view ever sees it, because the finished result is decoded once, whole, by the session. The test for the throttle is worth a note. The first version collected reports into an actor and awaited them, and raced its own assertions. The callback's contract is that it may not await, so a collector that has to be awaited is not testing the thing — it is locked now, like the real one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Description
AI-driven remote workspace for Mac, iPhone, and iPad.
https://termind.akinokaede.com
23 MiB
0 Stars
1 Watchers
0 Forks
Languages
Swift
98.9%
Python
0.8%
Ruby
0.3%