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>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-04 15:45:43 -04:00
1 parent e3e02d55f7
commit 79682f03a7
24 files changed
+4572 -6821

No files matched your search

+176 -239
View File
@@ -1,56 +1,44 @@
//! The phase-1 fake driver: no child process, just events. It exists to
//! prove the whole pipe -- spawn, transcript, SSE cursors, questions,
//! interrupts, compaction -- before any AI is involved, and stays useful afterwards as
//! a connectivity check that costs no tokens.
//! The fake driver: no child process, just events. It proves the whole pipe --
//! spawn, transcript, SSE cursors, questions, interrupts, compaction -- and
//! stays useful afterwards as a connectivity check that costs no tokens. It
//! produces exactly the event vocabulary the real drivers do, so a UI that
//! renders echo sessions correctly renders the real thing.
//!
//! Behavior: every message is echoed back as a few streamed text deltas.
//! A leading word asks for something more specific:
//! Every message is echoed back as a few streamed text deltas. A leading word
//! asks for something more specific:
//!
//! - `/tool [input]` -- a full tool run, start through end.
//! - `/bash [command]` -- a Bash call carrying that command, for what the
//! phone's shell highlighting does to a particular line.
//! - `/tools [n] [gap]` -- n calls back to back, for what a run of them
//! looks like when a screen groups them. `gap` is seconds between one
//! call and the next, default none: it is what makes a run *grow* while
//! somebody is looking at it, which is the only way to reach the state
//! where a call opened on its own gains a neighbour. The first call
//! carries a screenshot, so that state can also be reached with an image
//! open full screen -- which is where it used to close itself.
//! - `/tools [n] [gap]` -- n calls back to back. `gap` is seconds between one
//! call and the next, which is what makes a run *grow* while somebody is
//! looking at it -- the only way to reach the state where a call opened on
//! its own gains a neighbour. The first call carries a screenshot, so that
//! state is also reachable with an image open full screen.
//! - `/question [text]` -- a question, exercising the answer path.
//! - `/ask` -- an AskUserQuestion call: two questions on one tool call,
//! with descriptions, a preview and a multi-select, which is the shape
//! that is awkward to get a real model to produce on demand. Wrapped in
//! a run of ordinary calls on each side, because being asked something
//! happens in the middle of work and the screen has to keep it out of
//! the collapsed group around it.
//! - `/slow [seconds]` -- a turn that stays running (default 30), so states that only
//! exist *while* something is happening can be looked at.
//! - `/ask` -- an AskUserQuestion call: two questions on one tool call, with
//! descriptions, a preview and a multi-select, which is the shape that is
//! awkward to get a real model to produce on demand. Wrapped in a run of
//! ordinary calls on each side, because being asked something happens in the
//! middle of work.
//! - `/slow [seconds]` -- a turn that stays running (default 30), so states
//! that only exist *while* something is happening can be looked at.
//! - `/error [text]` -- a failure, which is otherwise awkward to cause.
//! - `/peer [text]` -- a message from another agent, which otherwise takes
//! two live sessions and one of them deciding to write.
//! - `/compact` -- a compaction, start to finish. Typed rather than
//! pressed, because the real dialects take it as a typed command too and
//! the phone no longer has a button for it.
//! - `/peer [text]`, `/peer-turn` -- a message from another agent, in the
//! in-place and the live shapes.
//! - `/compact` -- a compaction, start to finish.
//! - `/stream N` -- one long answer in N small pieces, 50ms apart: the shape a
//! real model's reply arrives in, and the one where the row a reader is
//! anchored to is the row that keeps changing height.
//! - `/mixed N` -- N beats of an interleaved transcript: rows of every shape
//! and height the app draws, in one session, which is what a scrolling
//! problem needs in order to be reproduced twice the same way.
//! - `/table [columns]` -- a markdown table with cells too long for one line.
//!
//! This is exactly the event vocabulary the real drivers produce, so a UI
//! that renders echo sessions correctly renders the real thing.
//!
//! - `/stream N` -- one long answer in N small pieces, 50ms apart: the
//! shape a real model's reply arrives in, and the one where the row a
//! reader is anchored to is the row that keeps changing height.
//! - `/mixed N` -- N beats of an interleaved transcript: paragraphs of
//! different lengths, single tool calls, runs of adjacent ones, attachments
//! and a peer message. Rows of every shape and height the app draws, in
//! one session, which is what a scrolling problem needs in order to be
//! reproduced twice the same way.
//!
//! `/slow` earns its place: a queued message, a Stop button, a spinner
//! where the answer will go are all states that only exist mid-turn, and
//! the obvious way to get one -- ask a real model to sleep -- does not
//! work. It declines, reasonably, and answers instantly instead, so the
//! state never arrives and the attempt still costs a turn on somebody's
//! account. A driver that can be *told* to take its time costs nothing and
//! is the same every run.
//! `/slow` earns its place: a queued message, a Stop button and a spinner are
//! states that only exist mid-turn, and the obvious way to get one -- ask a
//! real model to sleep -- does not work. It declines and answers instantly, so
//! the state never arrives and the attempt still costs a turn.
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
@@ -62,24 +50,18 @@ use super::driver::{
};
/// Delay between streamed deltas -- long enough that streaming is visibly
/// streaming in the UI, short enough that tests waiting on a full turn
/// stay fast.
/// streaming, short enough that tests waiting on a full turn stay fast.
const DELTA_DELAY: Duration = Duration::from_millis(50);
/// How long a fake compaction takes.
///
/// A measured one, near enough: driving a real session through `/compact`
/// on 2026-08-29 took 13 seconds for a small conversation, and a large one
/// takes minutes. Three seconds -- what this was -- is too short to look
/// at the row that only exists while a compaction is running, and too
/// short to watch its elapsed count reach two digits.
/// How long a fake compaction takes. A measured one, near enough: driving a
/// real session through `/compact` on 2026-08-29 took 13 seconds for a small
/// conversation. Three seconds -- what this was -- is too short to look at the
/// row that only exists while a compaction is running.
const COMPACT_TIME: Duration = Duration::from_secs(13);
/// A question echo is waiting on, and the tool call it belongs to.
///
/// `call` is `None` for `/question`, which asks on its own the way a
/// permission does; `Some` for `/ask`, where several questions share one
/// call and the call ends when the last of them is answered.
/// A question echo is waiting on, and the tool call it belongs to. `call` is
/// `None` for `/question`, which asks on its own the way a permission does;
/// `Some` for `/ask`, where several questions share one call.
struct PendingQuestion {
id: String,
call: Option<String>,
@@ -89,37 +71,31 @@ pub struct EchoDriver {
sink: EventSink,
/// Whether a turn is in flight, and what arrived during it.
///
/// A real CLI holds a message sent mid-turn and injects it at the next
/// tool boundary; echo used to answer it on the spot, which made it
/// the wrong shape for testing anything about queueing -- the status
/// dropped to idle immediately, so a phone had nothing to show as
/// pending. Holding it here is what makes echo able to stand in.
/// A real CLI holds a message sent mid-turn and injects it at the next tool
/// boundary; echo used to answer it on the spot, which made it the wrong
/// shape for testing anything about queueing.
busy: Arc<AtomicBool>,
/// Held messages with the id of the `MessageQueued` each one announced,
/// so the announcement can say which waiting bubble it resolves.
/// Held messages with the id of the `MessageQueued` each one announced, so
/// the announcement can say which waiting bubble it resolves.
queued: Arc<Mutex<Vec<Held>>>,
/// Where `/mixed` writes the attachments it references, which is the same
/// directory the files route serves them from.
session_dir: PathBuf,
/// Ids of the questions awaiting an answer, in the order they were
/// asked. A list because `/ask` puts up to four on one tool call, the
/// way AskUserQuestion does, and the turn resumes when the last of
/// them is answered rather than the first.
/// Ids of the questions awaiting an answer, in the order asked. A list
/// because `/ask` puts up to four on one tool call, and the turn resumes
/// when the last is answered rather than the first.
pending_questions: Mutex<Vec<PendingQuestion>>,
/// A pretend context, so the status row has something that behaves the
/// way a real one does: it grows with each turn, drops to what the
/// compaction says it recovered, and a clear leaves it unmeasured. The
/// numbers are invented like everything else here; what is real is
/// which way they move.
/// A pretend context, so the status row has something that behaves the way
/// a real one does: it grows with each turn, drops to what the compaction
/// says it recovered, and a clear leaves it unmeasured. What is real is
/// which way the numbers move.
context: Arc<AtomicU64>,
}
impl EchoDriver {
/// A short run of ordinary calls, to sit either side of something.
///
/// Three, because two is the fewest that groups and three makes it
/// obvious the group is a group -- and because the point of the
/// fixture is what a question looks like with work around it.
/// A short run of ordinary calls, to sit either side of something. Three,
/// because two is the fewest that groups and three makes it obvious the
/// group is a group.
fn some_calls(&self, label: &str) {
for index in 0..3 {
let id = format!("echo-{label}-{index}-{}", super::random_hex());
@@ -137,16 +113,15 @@ impl EchoDriver {
/// An AskUserQuestion call, in the shape the CLI sends one.
///
/// Two questions on one call, because that is where the display is
/// hardest and where it was wrong: one question with four options
/// reads fine even when the options are laid out badly. Written out
/// in full rather than generated so it carries the parts that are
/// easy to leave out of a fixture -- a header, an option with a
/// description, an option with a preview block, and a multi-select.
/// Two questions on one call, because that is where the display is hardest
/// and where it was wrong. Written out in full rather than generated so it
/// carries the parts that are easy to leave out of a fixture -- a header, an
/// option with a description, an option with a preview block, and a
/// multi-select.
fn ask_user_question(&self) {
// Written once, in the shape the events carry, and turned into
// the tool call's own input below -- the CLI sends both, and two
// hand-written copies of one question would drift.
// Written once, in the shape the events carry, and turned into the tool
// call's own input below -- the CLI sends both, and two hand-written
// copies of one question would drift.
let asked = [
(
"Theme",
@@ -236,8 +211,7 @@ impl EchoDriver {
header: Some(header.to_string()),
options,
multi_select: multi,
// The call that asked, so all of it draws as one thing --
// which is the whole point of the fixture.
// The call that asked, so all of it draws as one thing.
about: Some(call.clone()),
});
}
@@ -248,23 +222,21 @@ impl EchoDriver {
/// One typed line, whether it arrived as a message or as a command.
///
/// `announce` is the difference and it is the whole of it: a message
/// is announced with `MessageTaken`, which is what puts it in the
/// transcript, and a command is not -- the manager has already
/// recorded that one was sent, and saying so twice drew the same
/// line in both colours.
/// `announce` is the whole difference: a message is announced with
/// `MessageTaken`, which is what puts it in the transcript, and a command is
/// not -- the manager has already recorded that one was sent, and saying so
/// twice drew the same line in both colours.
fn handle(&self, text: String, attachments: Vec<AttachmentRef>, announce: bool) {
let sink = self.sink.clone();
// Mid-turn messages are held rather than answered, the way a real
// CLI holds them until the next tool boundary. Without this the
// session went idle the instant one arrived, and every state that
// only exists while something is queued was untestable.
// Mid-turn messages are held rather than answered, the way a real CLI
// holds them until the next tool boundary. Without this the session went
// idle the instant one arrived, and every state that only exists while
// something is queued was untestable.
if self.busy.load(Ordering::SeqCst) {
// The waiting is recorded, exactly as the real driver records
// it: the phone draws its pending bubbles from the server, so
// an echo session has to produce the same events or the states
// it exists to exercise are not the app's real ones.
// The waiting is recorded, exactly as the real driver records it:
// the phone draws its pending bubbles from the server, so an echo
// session has to produce the same events.
let id = super::random_hex();
self.queued
.lock()
@@ -280,17 +252,11 @@ impl EchoDriver {
return;
}
// Answered on the spot rather than in the turn below, because a
// peer message is not a turn: it is something that arrives, and
// what is being exercised is the row it becomes. The message that
// asked for it is still announced -- every driver owes exactly one
// `MessageTaken` per message, and a command that quietly vanishes
// from the transcript is the one thing echo must not model.
// The live Claude Code shape, which is the one the ordering has to
// survive: the CLI says nothing about a peer message until the
// turn's `result`, so the event arrives below the whole reply it
// caused and the phone has to put it back. Checked before `/peer`,
// which would otherwise take the rest of this word as the body.
// survive: the CLI says nothing about a peer message until the turn's
// `result`, so the event arrives below the whole reply it caused and the
// phone has to put it back. Checked before `/peer`, which would
// otherwise take the rest of this word as the body.
if let Some(rest) = text.strip_prefix("/peer-turn") {
if announce {
self.emit(Event::MessageTaken {
@@ -345,9 +311,9 @@ impl EchoDriver {
return;
}
// The same word the real CLI takes, so a phone drives both the same
// way. `Driver::compact` is what the manager's own route calls;
// this is the typed path onto it.
// The same word the real CLI takes, so a phone drives both the same way.
// `Driver::compact` is what the manager's route calls; this is the typed
// path onto it.
if text.trim() == "/compact" {
if announce {
self.emit(Event::MessageTaken {
@@ -403,23 +369,20 @@ impl EchoDriver {
return;
}
// Checked before `/tool`, which is a prefix of it: matching the
// shorter one first would read "/tools 4" as a single tool whose
// input is "s 4".
// Checked before `/tool`, which is a prefix of it: matching the shorter
// one first would read "/tools 4" as a single tool whose input is "s 4".
let many_tools = text.strip_prefix("/tools").map(|rest| {
let mut words = rest.split_whitespace();
// At least two, because one call is not a run of them and this
// exists to produce a run.
// At least two, because one call is not a run of them.
let count = words
.next()
.and_then(|w| w.parse().ok())
.unwrap_or(3usize)
.clamp(2, 12);
// How long to wait between calls, default none. A run that
// arrives all at once cannot exercise anything about a run
// *growing*: the case worth watching is a call somebody has
// opened and is reading when the next one turns it into a
// group, and 50ms apart is faster than anybody can open one.
// How long to wait between calls, default none. A run that arrives
// all at once cannot exercise a run *growing*: the case worth
// watching is a call somebody has opened and is reading when the
// next one turns it into a group.
let gap = Duration::from_secs(
words
.next()
@@ -438,21 +401,19 @@ impl EchoDriver {
let run_bash = text
.strip_prefix("/bash")
.map(|rest| rest.trim().to_string());
// Seconds to stay running before answering, default 30. Clamped
// rather than trusted: this is a test affordance, and a session
// pinned running for an hour by a typo is a worse outcome than a
// short wait.
// Seconds to stay running before answering, default 30. Clamped rather
// than trusted: a session pinned running for an hour by a typo is a
// worse outcome than a short wait.
let stream = text
.strip_prefix("/stream")
.map(|rest| rest.trim().parse::<usize>().unwrap_or(400).clamp(1, 4000));
let mixed = text
.strip_prefix("/mixed")
.map(|rest| rest.trim().parse::<usize>().unwrap_or(12).clamp(1, 400));
// How many columns wide a fixture table should be, default six.
// The count is the parameter because it is the thing the phone
// has to react to: a narrow table lays itself out across the
// screen and a wide one has to start scrolling sideways, and the
// boundary between the two is where the layout is wrong.
// How many columns wide a fixture table should be, default six. The
// count is the parameter because it is what the phone has to react to: a
// narrow table lays itself out across the screen and a wide one has to
// scroll sideways, and the boundary is where the layout is wrong.
let table = text
.strip_prefix("/table")
.map(|rest| rest.trim().parse::<usize>().unwrap_or(6).clamp(1, 12));
@@ -472,10 +433,9 @@ impl EchoDriver {
let _ = sink.send(event);
};
let finish = || finish_turn(&sink, &queued, &busy);
// Echo takes a message the instant it gets one, but it says so
// anyway: a driver that skips this leaves the phone holding a
// message it thinks is still queued, and the point of an echo
// provider is that it behaves like the real ones.
// Echo takes a message the instant it gets one, but says so anyway:
// a driver that skips this leaves the phone holding a message it
// thinks is still queued.
if announce {
send(Event::MessageTaken {
id: None,
@@ -488,8 +448,7 @@ impl EchoDriver {
});
if let Some(linger) = linger {
// A delta a second: visibly alive rather than merely slow,
// which is what the states being looked at accompany.
// A delta a second: visibly alive rather than merely slow.
let seconds = linger.as_secs();
for remaining in (1..=seconds).rev() {
send(Event::AssistantText {
@@ -532,16 +491,14 @@ impl EchoDriver {
"timeout": 5000,
}),
});
// The first call carries a screenshot, and only the
// first. That is what makes this rig cover the case a
// growing run is actually about: an image opened full
// screen from a call that is alone, and then a second
// call arriving and turning that row into a group. The
// dialog used to be inside the row, so the reader was
// thrown back to the transcript by the session making
// another tool call. Any of the calls would do; the
// first is the one that is on its own for a whole
// `gap`, which is the window somebody can open it in.
// The first call carries a screenshot, and only the first.
// That is what makes this rig cover the case a growing run
// is about: an image opened full screen from a call that is
// alone, and then a second call turning that row into a
// group. The dialog used to be inside the row, so the reader
// was thrown back to the transcript by the session making
// another tool call. The first call is the one that is on
// its own for a whole `gap`.
if i == 1 {
let part = serde_json::json!({
"source": {"media_type": "image/png", "data": SAMPLE_PNG}
@@ -565,9 +522,9 @@ impl EchoDriver {
// One long answer arriving in small pieces, which is what a real
// model does and what `/slow` does not: `/slow` emits a line a
// second, so its message grows in steps a reader can watch one
// at a time. A jump caused by the *anchor row itself* changing
// height needs growth that is continuous.
// second, so its message grows in steps a reader can watch one at a
// time. A jump caused by the *anchor row itself* changing height
// needs growth that is continuous.
if let Some(pieces) = stream {
for i in 0..pieces {
let len = 3 + (i * 7) % 14;
@@ -632,7 +589,6 @@ impl EchoDriver {
});
}
// Word-at-a-time so streaming is visibly streaming.
for word in format!("You said: {text}").split_inclusive(' ') {
send(Event::AssistantText {
delta: word.to_string(),
@@ -640,8 +596,7 @@ impl EchoDriver {
tokio::time::sleep(DELTA_DELAY).await;
}
// A conversation gets bigger, so the pretend context does too:
// roughly a hundred tokens a turn plus the words themselves,
// which is enough to watch it climb between compactions.
// roughly a hundred tokens a turn plus the words themselves.
let spent = text.split_whitespace().count() as u64;
send(Event::UsageDelta {
tokens: spent,
@@ -667,36 +622,32 @@ impl EchoDriver {
}
/// Sends are infallible from the driver's point of view: a closed sink
/// means the session is being torn down, and there is nobody left to
/// report to.
/// means the session is being torn down.
fn emit(&self, event: Event) {
let _ = self.sink.send(event);
}
}
/// A 16x10 checkerboard, the smallest thing that is recognisably an image
/// rather than a blank rectangle.
///
/// Embedded rather than generated because the alternative is a PNG encoder
/// in a test rig, and drawn at the transcript's fixed thumbnail height
/// anyway -- what a scroll test needs from an image is that it occupies an
/// image's worth of space, not that it is pretty.
/// A 16x10 checkerboard, the smallest thing recognisably an image rather than a
/// blank rectangle. Embedded rather than generated because the alternative is a
/// PNG encoder in a test rig, and what a scroll test needs from an image is
/// that it occupies an image's worth of space.
const SAMPLE_PNG: &str = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAKCAIAAAAy3EnLAAAAIklEQVR42mPo3PILiOTk9ICIGDYDyRqIVwphk65h1A9EsAGCYdJRj+JH4wAAAABJRU5ErkJggg==";
/// One beat of `/mixed`: a row shape chosen by position, so the same N
/// always produces the same transcript.
/// One beat of `/mixed`: a row shape chosen by position, so the same N always
/// produces the same transcript.
///
/// Repeatable on purpose. A scrolling fault is judged by watching the same
/// content behave differently, and a rig that produced a different
/// transcript each run would make every comparison an argument about
/// whether the content changed.
/// content behave differently, and a rig that produced a different transcript
/// each run would make every comparison an argument about whether the content
/// changed.
async fn write_beat(sink: &EventSink, session_dir: &Path, beat: usize) {
let send = |event: Event| {
let _ = sink.send(event);
};
match beat % 5 {
// A paragraph, of three lengths, because a list of uniform rows
// hides exactly the faults that uneven ones expose.
// A paragraph, of three lengths, because a list of uniform rows hides
// exactly the faults that uneven ones expose.
1 => {
let words = match beat % 3 {
0 => 12,
@@ -705,9 +656,8 @@ async fn write_beat(sink: &EventSink, session_dir: &Path, beat: usize) {
};
// Deliberately ragged: each word's length is a function of its
// position, so no two lines wrap the same way. A paragraph of
// uniform tokens is a wall that looks identical at every
// offset, which makes it impossible to tell a scroll of one
// line from a scroll of ten -- by eye or by comparing frames.
// uniform tokens looks identical at every offset, which makes it
// impossible to tell a scroll of one line from a scroll of ten.
let body: String = (0..words)
.map(|w| {
let len = 3 + (w * 7 + beat * 3) % 14;
@@ -731,8 +681,8 @@ async fn write_beat(sink: &EventSink, session_dir: &Path, beat: usize) {
output: format!("beat {beat}: forty-two lines of nothing in particular"),
});
}
// A run of three, which the app folds into one collapsed group --
// the row whose identity depends on what is next to it.
// A run of three, which the app folds into one collapsed group -- the
// row whose identity depends on what is next to it.
3 => {
for i in 1..=3 {
let id = format!("t-{}", super::random_hex());
@@ -779,30 +729,27 @@ async fn write_beat(sink: &EventSink, session_dir: &Path, beat: usize) {
});
}
}
// Slow enough that the phone renders each beat as it arrives rather
// than composing the whole run in one frame -- which is the condition
// a scrolling fault actually happens under.
// Slow enough that the phone renders each beat as it arrives rather than
// composing the whole run in one frame -- which is the condition a scrolling
// fault actually happens under.
tokio::time::sleep(Duration::from_millis(120)).await;
}
/// A message written during a turn and waiting for it to end: the id of the
/// `MessageQueued` that announced it, what it said, and what was attached to
/// it. All three, because all three are what the `MessageTaken` at the other
/// end owes -- named rather than written out at each of the four places that
/// mention it.
/// `MessageQueued` that announced it, what it said, and what was attached. All
/// three, because all three are what the `MessageTaken` at the other end owes.
type Held = (String, String, Vec<AttachmentRef>);
/// A markdown table [columns] wide, with cells too long for one line.
///
/// Both halves of that matter. Long cells are what the renderer used to cut
/// off with an ellipsis, and a cut cell looks exactly like a short one, so
/// a fixture of tidy one-word values would have rendered perfectly while
/// the defect was still there. The column count is what decides whether
/// the table fits the screen or has to scroll sideways.
/// Both halves matter. Long cells are what the renderer used to cut off with an
/// ellipsis, and a cut cell looks exactly like a short one, so a fixture of
/// tidy one-word values would have rendered perfectly while the defect was
/// still there. The column count decides whether the table fits the screen.
///
/// Written out as markdown rather than assembled from a grid type because
/// what is being tested is the renderer's parse of the syntax a model
/// actually writes, pipes and alignment row included.
/// Written out as markdown rather than assembled from a grid type because what
/// is being tested is the renderer's parse of the syntax a model actually
/// writes, pipes and alignment row included.
fn markdown_table(columns: usize) -> String {
let headings = [
"What it is",
@@ -854,17 +801,16 @@ fn markdown_table(columns: usize) -> String {
out
}
/// Ending a turn is also when anything held during it is taken up -- the
/// moment a real CLI would have injected it. One place, because a turn has
/// several ways to end (a reply, an interrupt, a compaction) and every one
/// of them owes the same answer.
/// Ending a turn is also when anything held during it is taken up -- the moment
/// a real CLI would have injected it. One place, because a turn has several
/// ways to end (a reply, an interrupt, a compaction) and every one of them owes
/// the same answer.
fn finish_turn(sink: &EventSink, queued: &Mutex<Vec<Held>>, busy: &AtomicBool) {
let held = std::mem::take(&mut *queued.lock().unwrap());
for (id, text, attachments) in held {
// Announced before it is answered, in that order: a phone showing
// the message as pending needs the signal that it has been read,
// and the answer is meaningless above a message still drawn as
// waiting.
// Announced before it is answered, in that order: a phone showing the
// message as pending needs the signal that it has been read, and the
// answer is meaningless above a message still drawn as waiting.
let _ = sink.send(Event::MessageTaken {
id: Some(id),
text: text.clone(),
@@ -885,12 +831,10 @@ impl Driver for EchoDriver {
!self.busy.load(Ordering::SeqCst)
}
/// Really droppable, which is what makes this the rig for the phone's
/// side of it: the held message is this driver's own and nothing has
/// been written anywhere, so a tap here exercises the whole path
/// through to the bubble disappearing on every device. The Claude
/// driver can only ever refuse -- see its own `unqueue` -- so it
/// cannot exercise the case where the drop succeeds.
/// Really droppable, which is what makes this the rig for the phone's side
/// of it: the held message is this driver's own and nothing has been written
/// anywhere, so a tap here exercises the whole path through to the bubble
/// disappearing on every device. The Claude driver can only ever refuse.
fn unqueue(&self, id: &str) -> Unqueued {
let mut queued = self.queued.lock().unwrap();
let Some(at) = queued.iter().position(|(waiting, ..)| waiting == id) else {
@@ -903,18 +847,15 @@ impl Driver for EchoDriver {
}
fn send_user_message(&self, text: String, attachments: Vec<AttachmentRef>) {
// Announced, because this is a message: every driver owes exactly
// one `MessageTaken` per message, and one that quietly vanishes
// from the transcript is the thing echo must not model. A command
// owes none -- the manager has already recorded that it was sent,
// and announcing it again drew the same line twice, once in each
// colour.
// Announced, because this is a message: every driver owes exactly one
// `MessageTaken` per message, and one that quietly vanishes from the
// transcript is the thing echo must not model. A command owes none.
self.handle(text, attachments, true);
}
/// Echo's commands *are* its messages -- `/tool`, `/slow`, `/ask` --
/// so this is the same path with the same parsing, and the fixture
/// behaves like a real session driven the same way.
/// Echo's commands *are* its messages -- `/tool`, `/slow`, `/ask` -- so
/// this is the same path with the same parsing, and the fixture behaves like
/// a real session driven the same way.
fn run_command(&self, text: &str) {
self.handle(text.to_string(), Vec::new(), false);
}
@@ -930,8 +871,8 @@ impl Driver for EchoDriver {
return;
};
let answered = pending.remove(at);
// Whether anything on the same call is still unanswered: a
// tool that asked four questions ends once, not four times.
// Whether anything on the same call is still unanswered: a tool that
// asked four questions ends once, not four times.
let waiting = answered
.call
.as_ref()
@@ -946,9 +887,9 @@ impl Driver for EchoDriver {
id: call,
output: format!("answered: {answer}"),
});
// The work carries on where it left off, which is what makes
// the asked-here row a boundary with a group on each side
// rather than the last thing in the turn.
// The work carries on where it left off, which is what makes the
// asked-here row a boundary with a group on each side rather than
// the last thing in the turn.
self.some_calls("after");
} else {
self.emit(Event::AssistantText {
@@ -961,17 +902,16 @@ impl Driver for EchoDriver {
}
fn interrupt(&self) {
// Nothing real to stop; a pending question is abandoned so the
// session isn't stuck awaiting input forever.
// Nothing real to stop; a pending question is abandoned so the session
// isn't stuck awaiting input forever.
self.pending_questions.lock().unwrap().clear();
self.emit(Event::Status {
state: SessionStatus::Idle,
});
}
// Nothing to forward: this process has no notion of what the
// conversation is called, and the rename it belongs to has already
// happened where the name lives. See `Driver::set_title`.
// Nothing to forward: this process has no notion of what the conversation
// is called, and the rename has already happened where the name lives.
fn set_title(&self, _title: &str) {}
fn set_permission_mode(&self, mode: &str) {
@@ -986,14 +926,11 @@ impl Driver for EchoDriver {
});
}
/// A compaction with nothing to compact.
///
/// The counts are invented, like everything else this driver says --
/// what is real is the shape and the order: busy, a pause long enough
/// to see, then the result. `Compacting` and `Compacted` are states a
/// screen has to draw, and the only other way to reach them is to fill
/// a real session's context and spend two minutes of somebody's
/// account getting it back.
/// A compaction with nothing to compact. The counts are invented, like
/// everything else this driver says -- what is real is the shape and the
/// order: busy, a pause long enough to see, then the result. The only other
/// way to reach those states is to fill a real session's context and spend
/// two minutes of somebody's account getting it back.
fn compact(&self) {
let sink = self.sink.clone();
let queued = Arc::clone(&self.queued);
@@ -1005,10 +942,10 @@ impl Driver for EchoDriver {
state: SessionStatus::Compacting,
});
tokio::time::sleep(COMPACT_TIME).await;
// What it says it recovered is what the pretend context becomes,
// so the figure on the status row and the one on the divider
// agree -- two numbers about the same moment disagreeing is the
// thing this rig exists to catch.
// What it says it recovered is what the pretend context becomes, so
// the figure on the status row and the one on the divider agree --
// two numbers about the same moment disagreeing is the thing this
// rig exists to catch.
context.store(9_617, Ordering::SeqCst);
let _ = sink.send(Event::Compacted {
pre_tokens: Some(128_402),