Show the model and mode the session has, not the ones it was asked for
Picking either from the phone wrote the choice straight into the session's state and then sent the request. Asking and having are different things, and the difference is not rare: `auto` is a permission mode the CLI accepts on the command line, silently resolves to `default`, and refuses outright over the control channel -- "auto mode unavailable for this model" -- so a session spawned in auto was in default and one switched to auto stayed where it was, with the phone reporting auto in both cases. So the drivers report what they are set to and the manager follows that. Measured, because the confirmations are not uniform: a model change answers success with no value, so what was asked is remembered until the answer arrives; a mode change echoes the mode it became, and that answer wins over the request; and `init` names both -- resolving `haiku` to claude-haiku-4-5-20251001 -- which also covers a session adopted from a terminal that set them outside this app. A driver that cannot change either already says so with an error, and now that error is the whole story rather than a note beside a display that changed anyway. The config keeps the requested value, deliberately: that answers a different question, which is what to launch this session with next time. Two things fall out. Control request ids are random rather than the clock, because two in the same second shared an id and something now looks them up. And the phone shortens a resolved name for the button -- `haiku-4-5` -- since the full one is what the CLI reports and roughly twice the room that row has once Stop is in it.
This commit is contained in:
1 parent
404066fa7d
commit
3eccf7e443
8 files changed
+359
-29
No files matched your search
+52
-10
@@ -678,7 +678,11 @@ impl SessionManager {
|
||||
candidate.save(&self.config_path)?;
|
||||
inner.config = candidate;
|
||||
if let Some(session) = inner.live.get(id) {
|
||||
*session.shared.permission_mode.lock().unwrap() = Some(mode.to_string());
|
||||
// Asked for, not recorded: what the session is actually set to
|
||||
// comes back as an `Event::Settings` if the driver makes the
|
||||
// change, and as an error if it cannot. The config above is a
|
||||
// different question -- what to launch this session with next
|
||||
// time -- and it is answered by the request.
|
||||
session.driver.set_permission_mode(mode);
|
||||
}
|
||||
Ok(())
|
||||
@@ -696,7 +700,8 @@ impl SessionManager {
|
||||
candidate.save(&self.config_path)?;
|
||||
inner.config = candidate;
|
||||
if let Some(session) = inner.live.get(id) {
|
||||
*session.shared.model.lock().unwrap() = Some(model.to_string());
|
||||
// See `set_session_permission_mode`: the driver reports what
|
||||
// it is set to, this only asks.
|
||||
session.driver.set_model(model);
|
||||
}
|
||||
Ok(())
|
||||
@@ -1000,6 +1005,29 @@ fn launch(
|
||||
/// The appends are synchronous file writes from an async task,
|
||||
/// deliberately: each is one small line on a local disk, and funneling
|
||||
/// them through one task is what makes the sequence numbering safe.
|
||||
/// Whether this event tells anyone anything they do not already know.
|
||||
///
|
||||
/// Only the two events that report state rather than something that
|
||||
/// happened can fail this: everything else is an occurrence, and an
|
||||
/// occurrence is news by existing. A `Settings` naming one field is
|
||||
/// judged on that field alone, since the other is not a claim that it is
|
||||
/// unset.
|
||||
fn is_news(event: &Event, shared: &Shared) -> bool {
|
||||
match event {
|
||||
Event::Status { state } => *shared.status.lock().unwrap() != *state,
|
||||
Event::Settings {
|
||||
model,
|
||||
permission_mode,
|
||||
} => {
|
||||
let model_changed = model.is_some() && *shared.model.lock().unwrap() != *model;
|
||||
let mode_changed = permission_mode.is_some()
|
||||
&& *shared.permission_mode.lock().unwrap() != *permission_mode;
|
||||
model_changed || mode_changed
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn pump(
|
||||
mut transcript: Transcript,
|
||||
mut source: mpsc::UnboundedReceiver<Event>,
|
||||
@@ -1016,18 +1044,32 @@ async fn pump(
|
||||
Event::MessageTaken { text } => Event::UserMessage { text },
|
||||
other => other,
|
||||
};
|
||||
// A status the session is already in is not news. Imported
|
||||
// sessions make this the common case rather than a rarity: each
|
||||
// sync reads the turn state off the file's newest record, and
|
||||
// most of them find the same answer as the sync before -- which
|
||||
// would otherwise be a transcript entry, a broadcast, and a
|
||||
// Nothing changed, so there is nothing to record. Both of these
|
||||
// repeat: an imported session reads the turn state off its file's
|
||||
// newest record on every sync and mostly finds the answer it found
|
||||
// last time, and the CLI restates its model and mode at every
|
||||
// `init`, which includes the one after every compaction. Recording
|
||||
// those would be a transcript entry, a broadcast and a
|
||||
// recomposition on every phone, several times a minute, to say
|
||||
// nothing at all.
|
||||
if let Event::Status { state } = &event
|
||||
&& *shared.status.lock().unwrap() == *state
|
||||
{
|
||||
if !is_news(&event, &shared) {
|
||||
continue;
|
||||
}
|
||||
if let Event::Settings {
|
||||
model,
|
||||
permission_mode,
|
||||
} = &event
|
||||
{
|
||||
// The session's own account of what it is set to, which is
|
||||
// what the list and the session screen show. Not written
|
||||
// where the change is *asked for* -- see `Event::Settings`.
|
||||
if let Some(model) = model {
|
||||
*shared.model.lock().unwrap() = Some(model.clone());
|
||||
}
|
||||
if let Some(mode) = permission_mode {
|
||||
*shared.permission_mode.lock().unwrap() = Some(mode.clone());
|
||||
}
|
||||
}
|
||||
match transcript.append(event, ts) {
|
||||
Ok(entry) => {
|
||||
if let Event::Status { state } = &entry.event {
|
||||
|
||||
Reference in new issue
Block a user