Make a command a thing the app knows, and hold it until it can run
Typing "/" now suggests what this app understands -- `/compact` and `/rename <name>` -- with a line each about what they do, and anything else beginning with a slash is passed to whatever runs the session, because a dialect's own vocabulary grows without this list. None of them are messages, and that is the substance of the change. A line written into a running turn is read by the *model*, so a command sent mid-turn either does nothing or arrives as text somebody has to puzzle over. They now wait for the turn to end. The waiting is done once for every provider, in the pump that already watches every event for the boundary, rather than in each driver where a new provider could get it wrong by leaving it out. Waiting is a state, so it is on screen: the command sits at the reader's end of the conversation in blue, with a spinner and "waiting for this turn to end", and becomes an ordinary blue row when it goes. Blue because these are about the session rather than about the task -- the same blue a compaction already used, which is now one colour with one name rather than two. Renaming from the settings screen sends exactly this, so it waits and draws the same way. The name itself is not held: it is this server's own datum, so the list and the header change at once and only telling the session waits. Echo grew the same split, which is where the bug in it showed: its commands are its messages, so running one announced a `MessageTaken` as well, and the same line drew twice -- once blue, once purple. A command owes no announcement; the manager has already recorded that it was sent. Watched rather than reasoned about: `/compact` during a 25 second turn held with its bubble up, went out when the turn ended, and the compaction that followed reported what it recovered.
This commit is contained in:
1 parent
bebaae7a94
commit
749b2db287
13 files changed
+678
-93
No files matched your search
@@ -223,11 +223,68 @@ pub enum Event {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
trigger: Option<String>,
|
||||
},
|
||||
/// A command the session was asked to run on itself, held because it
|
||||
/// cannot run yet.
|
||||
///
|
||||
/// These are not messages: `/compact` and `/rename` are instructions
|
||||
/// to the session about itself, and a session in the middle of a turn
|
||||
/// reads a line written to it as something the model should see. So
|
||||
/// they wait for the turn to end, and this is what a phone draws
|
||||
/// while they do -- otherwise pressing Compact during a long turn
|
||||
/// does nothing visible for minutes and looks like it was missed.
|
||||
CommandQueued {
|
||||
id: String,
|
||||
/// What to show for it: the command as a person would type it.
|
||||
text: String,
|
||||
},
|
||||
/// The same command, now handed to the session. Its [`CommandQueued`]
|
||||
/// stops being pending when this arrives, matched by `id`; a command
|
||||
/// that ran immediately has only this.
|
||||
CommandSent {
|
||||
id: String,
|
||||
text: String,
|
||||
},
|
||||
Error {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Something a session can be asked to do to itself.
|
||||
///
|
||||
/// A closed set rather than a string, because the two that are not
|
||||
/// dialect-specific have to reach every provider: compaction is a
|
||||
/// capability an llama session may one day have, and a name is this
|
||||
/// server's own. `Raw` is the escape for a dialect's own commands --
|
||||
/// `/context`, `/usage` -- which only the thing running the session can
|
||||
/// interpret.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SessionCommand {
|
||||
Compact,
|
||||
SetTitle(String),
|
||||
Raw(String),
|
||||
}
|
||||
|
||||
impl SessionCommand {
|
||||
/// What a person would have typed to ask for this, which is what a
|
||||
/// phone shows while it waits.
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Self::Compact => "/compact".to_string(),
|
||||
Self::SetTitle(title) => format!("/rename {title}"),
|
||||
Self::Raw(text) => text.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs it. Called only at a boundary -- see [`Event::CommandQueued`].
|
||||
pub fn apply(&self, driver: &dyn Driver) {
|
||||
match self {
|
||||
Self::Compact => driver.compact(),
|
||||
Self::SetTitle(title) => driver.set_title(title),
|
||||
Self::Raw(text) => driver.run_command(text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SessionStatus {
|
||||
@@ -300,6 +357,18 @@ pub trait Driver: Send + Sync {
|
||||
/// `/rename` afterwards, which is what puts the same name in its own
|
||||
/// session picker and in what other agents see.
|
||||
fn set_title(&self, title: &str);
|
||||
/// Runs a command this session's own dialect understands, verbatim.
|
||||
///
|
||||
/// For the ones this app has no opinion about -- `/context`, `/usage`,
|
||||
/// anything a CLI adds next month. A driver whose process has no such
|
||||
/// vocabulary says so with an [`Event::Error`] rather than sending it
|
||||
/// as a message, which would put a line meant for the session in front
|
||||
/// of the model instead.
|
||||
///
|
||||
/// Like [`Driver::compact`] and [`Driver::set_title`], this is called
|
||||
/// only when the session is between turns; the waiting is done above,
|
||||
/// once, for every driver.
|
||||
fn run_command(&self, text: &str);
|
||||
/// pi: native compaction; claude: `/compact`.
|
||||
fn compact(&self);
|
||||
/// Stop attending to the process but leave it running, because this
|
||||
|
||||
Reference in new issue
Block a user