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>
This commit is contained in:
1 parent
4bc69e8f9c
commit
6180663f14
25 files changed
+672
-165
No files matched your search
+44
-16
@@ -7,6 +7,8 @@
|
||||
//! 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
|
||||
@@ -37,7 +39,7 @@
|
||||
//! 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, images
|
||||
//! 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.
|
||||
@@ -55,7 +57,9 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::driver::{Driver, Event, EventSink, ImageRef, QuestionOption, SessionStatus, Unqueued};
|
||||
use super::driver::{
|
||||
AttachmentRef, Driver, Event, EventSink, QuestionOption, SessionStatus, Unqueued,
|
||||
};
|
||||
|
||||
/// Delay between streamed deltas -- long enough that streaming is visibly
|
||||
/// streaming in the UI, short enough that tests waiting on a full turn
|
||||
@@ -94,7 +98,7 @@ pub struct EchoDriver {
|
||||
/// 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 images it references, which is the same
|
||||
/// 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
|
||||
@@ -249,7 +253,7 @@ impl EchoDriver {
|
||||
/// 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, images: Vec<ImageRef>, announce: bool) {
|
||||
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
|
||||
@@ -265,9 +269,13 @@ impl EchoDriver {
|
||||
self.queued
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((id.clone(), text.clone(), images.clone()));
|
||||
.push((id.clone(), text.clone(), attachments.clone()));
|
||||
if announce {
|
||||
self.emit(Event::MessageQueued { id, text, images });
|
||||
self.emit(Event::MessageQueued {
|
||||
id,
|
||||
text,
|
||||
attachments,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -288,7 +296,7 @@ impl EchoDriver {
|
||||
self.emit(Event::MessageTaken {
|
||||
id: None,
|
||||
text: text.clone(),
|
||||
images: images.clone(),
|
||||
attachments: attachments.clone(),
|
||||
});
|
||||
}
|
||||
self.emit(Event::Status {
|
||||
@@ -319,7 +327,7 @@ impl EchoDriver {
|
||||
self.emit(Event::MessageTaken {
|
||||
id: None,
|
||||
text: text.clone(),
|
||||
images: images.clone(),
|
||||
attachments: attachments.clone(),
|
||||
});
|
||||
}
|
||||
self.emit(Event::PeerMessage {
|
||||
@@ -345,7 +353,7 @@ impl EchoDriver {
|
||||
self.emit(Event::MessageTaken {
|
||||
id: None,
|
||||
text,
|
||||
images,
|
||||
attachments,
|
||||
});
|
||||
}
|
||||
self.compact();
|
||||
@@ -357,7 +365,7 @@ impl EchoDriver {
|
||||
self.emit(Event::MessageTaken {
|
||||
id: None,
|
||||
text,
|
||||
images,
|
||||
attachments,
|
||||
});
|
||||
}
|
||||
self.ask_user_question();
|
||||
@@ -427,6 +435,9 @@ impl EchoDriver {
|
||||
text.strip_prefix("/tool")
|
||||
.map(|rest| rest.trim().to_string())
|
||||
};
|
||||
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
|
||||
@@ -469,7 +480,7 @@ impl EchoDriver {
|
||||
send(Event::MessageTaken {
|
||||
id: None,
|
||||
text: text.clone(),
|
||||
images: images.clone(),
|
||||
attachments: attachments.clone(),
|
||||
});
|
||||
}
|
||||
send(Event::Status {
|
||||
@@ -585,6 +596,23 @@ impl EchoDriver {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(command) = run_bash {
|
||||
let id = format!("b-{}", super::random_hex());
|
||||
send(Event::ToolStart {
|
||||
id: id.clone(),
|
||||
tool: "Bash".to_string(),
|
||||
input: serde_json::json!({
|
||||
"command": command,
|
||||
"description": "Run what /bash was given",
|
||||
}),
|
||||
});
|
||||
tokio::time::sleep(DELTA_DELAY).await;
|
||||
send(Event::ToolEnd {
|
||||
id,
|
||||
output: format!("ran: {command}"),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(input) = run_tool {
|
||||
let id = format!("t-{}", super::random_hex());
|
||||
send(Event::ToolStart {
|
||||
@@ -762,7 +790,7 @@ async fn write_beat(sink: &EventSink, session_dir: &Path, beat: usize) {
|
||||
/// 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.
|
||||
type Held = (String, String, Vec<ImageRef>);
|
||||
type Held = (String, String, Vec<AttachmentRef>);
|
||||
|
||||
/// A markdown table [columns] wide, with cells too long for one line.
|
||||
///
|
||||
@@ -832,7 +860,7 @@ fn markdown_table(columns: usize) -> String {
|
||||
/// 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, images) in held {
|
||||
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
|
||||
@@ -840,7 +868,7 @@ fn finish_turn(sink: &EventSink, queued: &Mutex<Vec<Held>>, busy: &AtomicBool) {
|
||||
let _ = sink.send(Event::MessageTaken {
|
||||
id: Some(id),
|
||||
text: text.clone(),
|
||||
images,
|
||||
attachments,
|
||||
});
|
||||
let _ = sink.send(Event::AssistantText {
|
||||
delta: format!("\n(taken from the queue) You said: {text}"),
|
||||
@@ -874,14 +902,14 @@ impl Driver for EchoDriver {
|
||||
Unqueued::Dropped
|
||||
}
|
||||
|
||||
fn send_user_message(&self, text: String, images: Vec<ImageRef>) {
|
||||
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.
|
||||
self.handle(text, images, true);
|
||||
self.handle(text, attachments, true);
|
||||
}
|
||||
|
||||
/// Echo's commands *are* its messages -- `/tool`, `/slow`, `/ask` --
|
||||
|
||||
Reference in new issue
Block a user