Shrink a photo to what the provider takes, and put it in its own bubble
Sending an image was broken in the way that is hardest to see from the phone: a camera photo is twelve megapixels and several megabytes, the Claude API resizes anything past 1568px on its long edge before looking at it and refuses far larger outright, so the picture was uploaded whole over the tunnel to be thrown away or rejected at the other end. Shrunk on the phone, to a limit the server states. Which number it is comes from the provider's *kind* -- `DriverKind::max_image_edge`, reported on the session row -- because that is where a provider's requirements are known, and a phone carrying its own copy of them would be a second place to update when one changes. `None` where nothing cares, rather than a large number: "no limit" and "a limit that happens to be big" are different answers and only one of them stays true. Doing it before the upload rather than after is the point -- the expensive part on a phone is the tunnel, not the decode -- and an image already inside the limit is uploaded byte for byte rather than being round-tripped through JPEG for nothing. EXIF orientation is applied while scaling. The camera writes which way up the picture is into a tag rather than into the pixels, and re-encoding drops it, so a portrait photo would have arrived at the model on its side with nothing anywhere saying so. **What is attached is now visible before it is sent**, in a row directly above the box it will be sent from: the count on the "+" button said how many and never which, so the only way to find out what you had picked was to send it. It scrolls sideways rather than shrinking, and tapping one takes it back off -- an image picked by mistake could otherwise only be dealt with by sending it. The tile is outlined as well as filled, because most of what gets attached here is a screenshot of a dark app and a cropped one is near-black: without an edge the only thing on screen saying an image was attached was the cross drawn on top of nothing. **And the picture is inside the bubble that sent it.** Attachments used to be their own `Image` events emitted just before the message, which drew somebody's screenshot as a row floating above the bubble and left the phone deciding from adjacency alone which message an image belonged to -- a thing the sender knew and could simply say. `UserMessage`, `MessageQueued` and `MessageTaken` carry the refs now, so a waiting message keeps its picture for as long as the turn runs, and a replay puts it back in the same place. Verified on a real claude-cli session rather than an echo one, since the limit only exists for that kind: a 3000x4000 image arrived as 1176x1568 JPEG -- long edge exactly the limit, aspect ratio intact -- and haiku answered "AI Sessions displays idle Photo", which is what the picture was. No error, and the transcript records the message with `images` on it.
This commit is contained in:
1 parent
5d47a1ec89
commit
b0629f77ca
16 files changed
+529
-70
No files matched your search
@@ -139,7 +139,7 @@ struct Queue {
|
||||
/// Written, not yet announced, oldest first, each with the id of the
|
||||
/// `MessageQueued` that told the phone it was waiting -- so the
|
||||
/// announcement can name which bubble it resolves.
|
||||
awaiting: VecDeque<(String, String)>,
|
||||
awaiting: VecDeque<(String, String, Vec<ImageRef>)>,
|
||||
/// The process is gone, so nothing can be taken up any more.
|
||||
///
|
||||
/// Needed because every other way out of a turn is an `Idle` this
|
||||
@@ -159,7 +159,7 @@ impl Queue {
|
||||
fn close(&mut self, sink: &EventSink, why: &str) {
|
||||
self.closed = true;
|
||||
self.running = false;
|
||||
let lost: Vec<String> = self.awaiting.drain(..).map(|(_, text)| text).collect();
|
||||
let lost: Vec<String> = self.awaiting.drain(..).map(|(_, text, _)| text).collect();
|
||||
if lost.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -523,9 +523,11 @@ impl Driver for ClaudeDriver {
|
||||
// session or restarting the app drew nothing pending while a
|
||||
// message was still in the queue.
|
||||
let id = super::random_hex();
|
||||
queue.awaiting.push_back((id.clone(), text.clone()));
|
||||
queue
|
||||
.awaiting
|
||||
.push_back((id.clone(), text.clone(), images.clone()));
|
||||
drop(queue);
|
||||
let _ = self.sink.send(Event::MessageQueued { id, text });
|
||||
let _ = self.sink.send(Event::MessageQueued { id, text, images });
|
||||
self.send_line(line);
|
||||
return;
|
||||
}
|
||||
@@ -534,7 +536,11 @@ impl Driver for ClaudeDriver {
|
||||
// Nothing is in flight, so there is nothing to wait for: this
|
||||
// message *is* the turn about to start, and it never had a
|
||||
// `MessageQueued` to resolve.
|
||||
let _ = self.sink.send(Event::MessageTaken { id: None, text });
|
||||
let _ = self.sink.send(Event::MessageTaken {
|
||||
id: None,
|
||||
text,
|
||||
images,
|
||||
});
|
||||
let _ = self.sink.send(Event::Status {
|
||||
state: SessionStatus::Running,
|
||||
});
|
||||
@@ -945,13 +951,17 @@ fn proves_a_turn(event: &Event) -> bool {
|
||||
/// [`translate_line`], and the pair is the whole of the rule -- a steer
|
||||
/// announced anywhere else lands above output that predates it.
|
||||
fn announce_steers(queue: &Arc<Mutex<Queue>>, sink: &EventSink) -> bool {
|
||||
let taken: Vec<(String, String)> = {
|
||||
let taken: Vec<(String, String, Vec<ImageRef>)> = {
|
||||
let mut queue = queue.lock().unwrap();
|
||||
queue.awaiting.drain(..).collect()
|
||||
};
|
||||
for (id, text) in taken {
|
||||
for (id, text, images) in taken {
|
||||
if sink
|
||||
.send(Event::MessageTaken { id: Some(id), text })
|
||||
.send(Event::MessageTaken {
|
||||
id: Some(id),
|
||||
text,
|
||||
images,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return false;
|
||||
@@ -1151,11 +1161,11 @@ mod tests {
|
||||
);
|
||||
// Typed after the first delta, with the answer still arriving.
|
||||
let events = events_with_interjection(&lines, 1, |queue| {
|
||||
queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.awaiting
|
||||
.push_back(("q1".into(), "do the other one instead".into()))
|
||||
queue.lock().unwrap().awaiting.push_back((
|
||||
"q1".into(),
|
||||
"do the other one instead".into(),
|
||||
Vec::new(),
|
||||
))
|
||||
});
|
||||
|
||||
let at = |find: fn(&Event) -> bool| {
|
||||
@@ -1212,7 +1222,7 @@ mod tests {
|
||||
.lock()
|
||||
.unwrap()
|
||||
.awaiting
|
||||
.push_back(("q2".into(), "never mind".into()))
|
||||
.push_back(("q2".into(), "never mind".into(), Vec::new()))
|
||||
});
|
||||
|
||||
let taken = events
|
||||
@@ -1375,8 +1385,12 @@ mod tests {
|
||||
running: true,
|
||||
..Queue::default()
|
||||
};
|
||||
queue.awaiting.push_back(("q1".into(), "first".into()));
|
||||
queue.awaiting.push_back(("q2".into(), "second".into()));
|
||||
queue
|
||||
.awaiting
|
||||
.push_back(("q1".into(), "first".into(), Vec::new()));
|
||||
queue
|
||||
.awaiting
|
||||
.push_back(("q2".into(), "second".into(), Vec::new()));
|
||||
queue.close(&sink, "the session ended");
|
||||
|
||||
// Named rather than counted, because these never reached the
|
||||
|
||||
@@ -81,6 +81,16 @@ pub enum Event {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
id: Option<String>,
|
||||
text: String,
|
||||
/// What was attached to it, by the ref the files route serves.
|
||||
///
|
||||
/// On the message rather than beside it. These used to be their own
|
||||
/// `Image` events emitted just before, which drew a person's
|
||||
/// screenshot as a row of its own floating above the bubble that
|
||||
/// sent it -- and left the phone to decide, from nothing but
|
||||
/// adjacency, which message an image belonged to. Belonging is not
|
||||
/// something to infer when the sender knew.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
images: Vec<ImageRef>,
|
||||
},
|
||||
/// A message accepted from the phone that the session cannot read yet.
|
||||
///
|
||||
@@ -98,6 +108,11 @@ pub enum Event {
|
||||
MessageQueued {
|
||||
id: String,
|
||||
text: String,
|
||||
/// Carried for the same reason [`Event::UserMessage`] carries it,
|
||||
/// and it matters more here: a waiting message is on screen for as
|
||||
/// long as the turn runs, so its attachment has nowhere else to be.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
images: Vec<ImageRef>,
|
||||
},
|
||||
/// A driver has taken one of the user's messages and started reading
|
||||
/// it. The manager turns this into the `UserMessage` above, so it
|
||||
@@ -114,6 +129,9 @@ pub enum Event {
|
||||
/// waited. Carried through onto the `UserMessage`.
|
||||
id: Option<String>,
|
||||
text: String,
|
||||
/// Carried through onto the `UserMessage` with everything else.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
images: Vec<ImageRef>,
|
||||
},
|
||||
/// Streaming assistant text; the phone renders the concatenation as
|
||||
/// markdown.
|
||||
|
||||
@@ -82,7 +82,7 @@ pub struct EchoDriver {
|
||||
busy: Arc<AtomicBool>,
|
||||
/// 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<(String, String)>>>,
|
||||
queued: Arc<Mutex<Vec<Held>>>,
|
||||
/// Where `/mixed` writes the images it references, which is the same
|
||||
/// directory the files route serves them from.
|
||||
session_dir: PathBuf,
|
||||
@@ -211,7 +211,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, images: Vec<ImageRef>, announce: bool) {
|
||||
let sink = self.sink.clone();
|
||||
|
||||
// Mid-turn messages are held rather than answered, the way a real
|
||||
@@ -224,9 +224,12 @@ impl EchoDriver {
|
||||
// an echo session has to produce the same events or the states
|
||||
// it exists to exercise are not the app's real ones.
|
||||
let id = super::random_hex();
|
||||
self.queued.lock().unwrap().push((id.clone(), text.clone()));
|
||||
self.queued
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((id.clone(), text.clone(), images.clone()));
|
||||
if announce {
|
||||
self.emit(Event::MessageQueued { id, text });
|
||||
self.emit(Event::MessageQueued { id, text, images });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -242,6 +245,7 @@ impl EchoDriver {
|
||||
self.emit(Event::MessageTaken {
|
||||
id: None,
|
||||
text: text.clone(),
|
||||
images: images.clone(),
|
||||
});
|
||||
}
|
||||
self.emit(Event::PeerMessage {
|
||||
@@ -263,7 +267,11 @@ impl EchoDriver {
|
||||
// this is the typed path onto it.
|
||||
if text.trim() == "/compact" {
|
||||
if announce {
|
||||
self.emit(Event::MessageTaken { id: None, text });
|
||||
self.emit(Event::MessageTaken {
|
||||
id: None,
|
||||
text,
|
||||
images,
|
||||
});
|
||||
}
|
||||
self.compact();
|
||||
return;
|
||||
@@ -271,7 +279,11 @@ impl EchoDriver {
|
||||
|
||||
if text.trim() == "/ask" {
|
||||
if announce {
|
||||
self.emit(Event::MessageTaken { id: None, text });
|
||||
self.emit(Event::MessageTaken {
|
||||
id: None,
|
||||
text,
|
||||
images,
|
||||
});
|
||||
}
|
||||
self.ask_user_question();
|
||||
return;
|
||||
@@ -352,6 +364,7 @@ impl EchoDriver {
|
||||
send(Event::MessageTaken {
|
||||
id: None,
|
||||
text: text.clone(),
|
||||
images: images.clone(),
|
||||
});
|
||||
}
|
||||
send(Event::Status {
|
||||
@@ -584,13 +597,20 @@ async fn write_beat(sink: &EventSink, session_dir: &Path, beat: usize) {
|
||||
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.
|
||||
type Held = (String, String, Vec<ImageRef>);
|
||||
|
||||
/// 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<(String, String)>>, busy: &AtomicBool) {
|
||||
fn finish_turn(sink: &EventSink, queued: &Mutex<Vec<Held>>, busy: &AtomicBool) {
|
||||
let held = std::mem::take(&mut *queued.lock().unwrap());
|
||||
for (id, text) in held {
|
||||
for (id, text, images) 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
|
||||
@@ -598,6 +618,7 @@ fn finish_turn(sink: &EventSink, queued: &Mutex<Vec<(String, String)>>, busy: &A
|
||||
let _ = sink.send(Event::MessageTaken {
|
||||
id: Some(id),
|
||||
text: text.clone(),
|
||||
images,
|
||||
});
|
||||
let _ = sink.send(Event::AssistantText {
|
||||
delta: format!("\n(taken from the queue) You said: {text}"),
|
||||
|
||||
@@ -540,7 +540,15 @@ fn push_user(events: &mut Vec<Event>, content: &Value, session_dir: &std::path::
|
||||
if !text.trim().is_empty() {
|
||||
// Replayed from the CLI's own file: it was read long ago, so
|
||||
// there is no waiting bubble for it to resolve.
|
||||
events.push(Event::UserMessage { id: None, text });
|
||||
// The images in this record are saved and referenced separately just
|
||||
// above, because a replayed message's pictures came out of somebody
|
||||
// else's file rather than out of this app's composer -- there is no
|
||||
// upload here whose refs could ride on the message.
|
||||
events.push(Event::UserMessage {
|
||||
id: None,
|
||||
text,
|
||||
images: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -347,6 +347,9 @@ impl Driver for LlamaDriver {
|
||||
let _ = sink.send(Event::MessageTaken {
|
||||
id: None,
|
||||
text: text.clone(),
|
||||
// Never any: this driver refuses images above, and saying
|
||||
// so is what the refusal above is for.
|
||||
images: Vec::new(),
|
||||
});
|
||||
let _ = sink.send(Event::Status {
|
||||
state: SessionStatus::Running,
|
||||
@@ -636,6 +639,7 @@ mod tests {
|
||||
Event::UserMessage {
|
||||
id: None,
|
||||
text: "hello".into(),
|
||||
images: Vec::new(),
|
||||
},
|
||||
Event::AssistantText {
|
||||
delta: "hi ".into(),
|
||||
@@ -649,6 +653,7 @@ mod tests {
|
||||
Event::UserMessage {
|
||||
id: None,
|
||||
text: "again".into(),
|
||||
images: Vec::new(),
|
||||
},
|
||||
Event::AssistantText {
|
||||
delta: "yes".into(),
|
||||
@@ -680,6 +685,7 @@ mod tests {
|
||||
Event::UserMessage {
|
||||
id: None,
|
||||
text: "count".into(),
|
||||
images: Vec::new(),
|
||||
},
|
||||
Event::AssistantText {
|
||||
delta: "one two".into(),
|
||||
@@ -705,6 +711,7 @@ mod tests {
|
||||
Event::UserMessage {
|
||||
id: None,
|
||||
text: "hello".into(),
|
||||
images: Vec::new(),
|
||||
},
|
||||
Event::Error {
|
||||
message: "something went wrong".into(),
|
||||
@@ -732,6 +739,7 @@ mod tests {
|
||||
Event::UserMessage {
|
||||
id: None,
|
||||
text: "the long expensive conversation".into(),
|
||||
images: Vec::new(),
|
||||
},
|
||||
Event::AssistantText {
|
||||
delta: "at length".into(),
|
||||
@@ -740,6 +748,7 @@ mod tests {
|
||||
Event::UserMessage {
|
||||
id: None,
|
||||
text: "a fresh start".into(),
|
||||
images: Vec::new(),
|
||||
},
|
||||
Event::AssistantText {
|
||||
delta: "cheaply".into(),
|
||||
@@ -759,16 +768,19 @@ mod tests {
|
||||
Event::UserMessage {
|
||||
id: None,
|
||||
text: "one".into(),
|
||||
images: Vec::new(),
|
||||
},
|
||||
Event::Cleared,
|
||||
Event::UserMessage {
|
||||
id: None,
|
||||
text: "two".into(),
|
||||
images: Vec::new(),
|
||||
},
|
||||
Event::Cleared,
|
||||
Event::UserMessage {
|
||||
id: None,
|
||||
text: "three".into(),
|
||||
images: Vec::new(),
|
||||
},
|
||||
]);
|
||||
let messages = conversation(&path);
|
||||
|
||||
+35
-18
@@ -146,6 +146,13 @@ pub struct SessionInfo {
|
||||
/// Every token this session has spent, so a phone showing a total does
|
||||
/// not have to add up a transcript it only holds part of.
|
||||
pub total_tokens: u64,
|
||||
/// The longest edge an image should have by the time it gets here, or
|
||||
/// absent where this provider has no limit -- see
|
||||
/// [`DriverKind::max_image_edge`]. Absent rather than a large number,
|
||||
/// because "no limit" and "a limit that happens to be big" are different
|
||||
/// answers and only one of them stays true.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_image_edge: Option<u32>,
|
||||
/// Whether this session announces itself -- reported for the same
|
||||
/// reason `permission_mode` is: a switch that guesses its own position
|
||||
/// is how you turn something off while believing you are reading it.
|
||||
@@ -290,17 +297,11 @@ impl LiveSession {
|
||||
/// turn it waits, and writing it down on the way past would put it
|
||||
/// above output that happened before the session ever saw it.
|
||||
pub fn send_message(&self, text: String, images: Vec<ImageRef>) {
|
||||
// Attachments are the exception, recorded on the way past: they
|
||||
// are uploaded whether or not the message waits, and the phone
|
||||
// fetches them by the same ref the files route serves. So a queued
|
||||
// message's picture appears a little before its text.
|
||||
for image in &images {
|
||||
let _ = self.sink.send(Event::Image {
|
||||
image: image.clone(),
|
||||
// A person's own attachment belongs to no tool call.
|
||||
about: None,
|
||||
});
|
||||
}
|
||||
// The attachments ride *on* the message rather than as `Image`
|
||||
// events emitted just before it. They used to be the latter, which
|
||||
// drew a person's screenshot as a row floating above the bubble
|
||||
// that sent it, and left the phone inferring from adjacency which
|
||||
// message an image went with -- a thing the sender already knew.
|
||||
self.driver.send_user_message(text, images);
|
||||
}
|
||||
|
||||
@@ -370,7 +371,13 @@ impl LiveSession {
|
||||
|
||||
/// `setup_name` is passed in rather than stored: only the manager
|
||||
/// holds the config, and the label can change under a running session.
|
||||
fn info(&self, setup_name: &str, imported: bool, keeps_own_transcript: bool) -> SessionInfo {
|
||||
/// `kind` rather than the facts derived from it: two of this row's
|
||||
/// fields are answers about the provider's *kind*, and passing them
|
||||
/// separately meant every caller deriving each one and a third arriving
|
||||
/// as a third parameter. `None` where the provider has been edited away,
|
||||
/// which is a session that cannot run -- so both answers are the
|
||||
/// cautious one rather than a guess.
|
||||
fn info(&self, setup_name: &str, imported: bool, kind: Option<DriverKind>) -> SessionInfo {
|
||||
SessionInfo {
|
||||
id: self.meta.id.clone(),
|
||||
provider: self.meta.provider.clone(),
|
||||
@@ -381,8 +388,9 @@ impl LiveSession {
|
||||
permission_mode: self.shared.permission_mode.lock().unwrap().clone(),
|
||||
total_tokens: *self.shared.total_tokens.lock().unwrap(),
|
||||
notify: *self.shared.notify.lock().unwrap(),
|
||||
max_image_edge: kind.and_then(DriverKind::max_image_edge),
|
||||
imported,
|
||||
keeps_own_transcript,
|
||||
keeps_own_transcript: kind.is_some_and(DriverKind::keeps_own_transcript),
|
||||
cwd: self.meta.cwd.clone(),
|
||||
status: *self.shared.status.lock().unwrap(),
|
||||
last_activity: *self.shared.last_activity.lock().unwrap(),
|
||||
@@ -706,7 +714,7 @@ impl SessionManager {
|
||||
Some(session) => session.info(
|
||||
label_of(&inner.config, &meta.setup),
|
||||
import::read_cursor(&self.data_dir.join(&meta.id)).is_some(),
|
||||
keeps_own_transcript(&inner.config, &meta.setup, &meta.provider),
|
||||
kind_of(&inner.config, &meta.setup, &meta.provider),
|
||||
),
|
||||
None => SessionInfo {
|
||||
id: meta.id.clone(),
|
||||
@@ -717,6 +725,8 @@ impl SessionManager {
|
||||
model: meta.model.clone(),
|
||||
permission_mode: meta.permission_mode.clone(),
|
||||
total_tokens: 0,
|
||||
max_image_edge: kind_of(&inner.config, &meta.setup, &meta.provider)
|
||||
.and_then(DriverKind::max_image_edge),
|
||||
notify: meta.notify,
|
||||
imported: import::read_cursor(&self.data_dir.join(&meta.id)).is_some(),
|
||||
keeps_own_transcript: keeps_own_transcript(
|
||||
@@ -853,7 +863,7 @@ impl SessionManager {
|
||||
let info = session.info(
|
||||
&setup.name,
|
||||
import::read_cursor(&self.data_dir.join(&id)).is_some(),
|
||||
provider.kind.keeps_own_transcript(),
|
||||
Some(provider.kind),
|
||||
);
|
||||
inner.live.insert(id, session);
|
||||
Ok(info)
|
||||
@@ -1057,10 +1067,17 @@ fn label_of<'a>(config: &'a Config, id: &'a str) -> &'a str {
|
||||
/// "this can be brought back" on no evidence is the answer that loses
|
||||
/// somebody's conversation.
|
||||
fn keeps_own_transcript(config: &Config, setup: &str, provider: &str) -> bool {
|
||||
kind_of(config, setup, provider).is_some_and(DriverKind::keeps_own_transcript)
|
||||
}
|
||||
|
||||
/// What a session's provider is, for the questions answered by its *kind*
|
||||
/// rather than by its name. `None` for a provider that has been edited away,
|
||||
/// which is a session that cannot run at all.
|
||||
fn kind_of(config: &Config, setup: &str, provider: &str) -> Option<DriverKind> {
|
||||
config
|
||||
.setup(setup)
|
||||
.and_then(|setup| setup.providers.iter().find(|it| it.name == provider))
|
||||
.is_some_and(|provider| provider.kind.keeps_own_transcript())
|
||||
.map(|provider| provider.kind)
|
||||
}
|
||||
|
||||
/// Names for a failure message: what there is, so the reader can see what
|
||||
@@ -1363,7 +1380,7 @@ async fn pump(
|
||||
// message here rather than being carried alongside it. One rule
|
||||
// for where a user's message sits: where the session read it.
|
||||
let event = match event {
|
||||
Event::MessageTaken { id, text } => Event::UserMessage { id, text },
|
||||
Event::MessageTaken { id, text, images } => Event::UserMessage { id, text, images },
|
||||
// The running total is the pump's to keep, for the reason the
|
||||
// field gives: a driver knows what its own turn cost and
|
||||
// nothing else does. Added here rather than at each driver so
|
||||
@@ -1606,7 +1623,7 @@ mod tests {
|
||||
assert_eq!(first.session_id, info.id);
|
||||
// The title travels with it, because the phone may have no screen
|
||||
// open to look one up on.
|
||||
assert_eq!(first.title, session.info("m", false, false).title);
|
||||
assert_eq!(first.title, session.info("m", false, None).title);
|
||||
|
||||
manager.set_session_notify(&info.id, false).expect("off");
|
||||
// Subscribed before the message, or the turn can finish in the gap
|
||||
|
||||
@@ -358,6 +358,7 @@ mod tests {
|
||||
Event::UserMessage {
|
||||
id: None,
|
||||
text: "hi".into(),
|
||||
images: Vec::new(),
|
||||
},
|
||||
text("hello"),
|
||||
Event::ToolStart {
|
||||
|
||||
Reference in new issue
Block a user