feat(maestro): add the resume route path and preset command builder
The template's {agent_session} is filled from the session's own
conversation id; a template that asks for an id the session never
reported yields nothing rather than a `--resume` with no argument.
This commit is contained in:
@@ -536,6 +536,29 @@ pub struct SpawnPreset {
|
||||
pub resume_command: Option<String>,
|
||||
}
|
||||
|
||||
/// Placeholder a preset's `resume_command` may carry for the agent's own
|
||||
/// conversation id (#165), substituted from [`Session::agent_session_id`].
|
||||
pub const AGENT_SESSION_PLACEHOLDER: &str = "{agent_session}";
|
||||
|
||||
impl SpawnPreset {
|
||||
/// The command that resumes a session spawned from this preset, with
|
||||
/// [`AGENT_SESSION_PLACEHOLDER`] filled in from `agent_session`.
|
||||
///
|
||||
/// `None` when the preset has no `resume_command` — a plain shell has
|
||||
/// nothing to resume — and also when the template asks for an agent
|
||||
/// session id the session never reported: `claude --resume` with an empty
|
||||
/// id resumes nothing, so the caller is better off respawning the original
|
||||
/// command than running a broken one.
|
||||
pub fn resume_command_for(&self, agent_session: Option<&str>) -> Option<String> {
|
||||
let template = self.resume_command.as_deref()?;
|
||||
if !template.contains(AGENT_SESSION_PLACEHOLDER) {
|
||||
return Some(template.to_string());
|
||||
}
|
||||
let id = agent_session.filter(|s| !s.is_empty())?;
|
||||
Some(template.replace(AGENT_SESSION_PLACEHOLDER, id))
|
||||
}
|
||||
}
|
||||
|
||||
/// Build provenance of the running daemon: the commit it was compiled
|
||||
/// from and when that commit was authored.
|
||||
///
|
||||
@@ -785,6 +808,47 @@ pub enum ClientControl {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn preset(resume: Option<&str>) -> SpawnPreset {
|
||||
SpawnPreset {
|
||||
id: "claude".into(),
|
||||
label: "Claude".into(),
|
||||
command: "claude".into(),
|
||||
resume_command: resume.map(str::to_string),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_command_substitutes_the_agent_session() {
|
||||
let p = preset(Some("claude --resume {agent_session}"));
|
||||
assert_eq!(
|
||||
p.resume_command_for(Some("abc-123")).as_deref(),
|
||||
Some("claude --resume abc-123")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_command_without_a_placeholder_ignores_the_agent_session() {
|
||||
let p = preset(Some("opencode --continue"));
|
||||
assert_eq!(
|
||||
p.resume_command_for(None).as_deref(),
|
||||
Some("opencode --continue")
|
||||
);
|
||||
}
|
||||
|
||||
/// A template that wants an id the session never reported would run
|
||||
/// `claude --resume` against nothing; the caller respawns instead.
|
||||
#[test]
|
||||
fn resume_command_is_none_without_the_id_it_asks_for() {
|
||||
let p = preset(Some("claude --resume {agent_session}"));
|
||||
assert!(p.resume_command_for(None).is_none());
|
||||
assert!(p.resume_command_for(Some("")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_preset_without_a_resume_command_has_none() {
|
||||
assert!(preset(None).resume_command_for(Some("abc")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_compatibility_is_major_only() {
|
||||
assert!(is_compatible(PROTOCOL_VERSION));
|
||||
|
||||
@@ -66,6 +66,12 @@ pub fn session_focus(id: Uuid) -> String {
|
||||
format!("{SESSIONS}/{id}/focus")
|
||||
}
|
||||
|
||||
/// `POST /api/sessions/:id/resume` (#166): respawn a dormant session under its
|
||||
/// own id, keeping its sidecar, name, worktree and `created_at`.
|
||||
pub fn session_resume(id: Uuid) -> String {
|
||||
format!("{SESSIONS}/{id}/resume")
|
||||
}
|
||||
|
||||
/// `WS /api/attach/:id` — raw PTY bytes.
|
||||
pub fn attach(id: Uuid) -> String {
|
||||
format!("{API_PREFIX}/attach/{id}")
|
||||
@@ -79,6 +85,7 @@ pub mod patterns {
|
||||
pub const SESSION_INPUT: &str = "/api/sessions/:id/input";
|
||||
pub const SESSION_PROMPT: &str = "/api/sessions/:id/prompt";
|
||||
pub const SESSION_FOCUS: &str = "/api/sessions/:id/focus";
|
||||
pub const SESSION_RESUME: &str = "/api/sessions/:id/resume";
|
||||
pub const ATTACH: &str = "/api/attach/:id";
|
||||
}
|
||||
|
||||
@@ -202,6 +209,10 @@ mod tests {
|
||||
session_focus(id()),
|
||||
"/api/sessions/6f1a7c0e-0f5e-4a2b-9a3d-6b1c2d3e4f50/focus"
|
||||
);
|
||||
assert_eq!(
|
||||
session_resume(id()),
|
||||
"/api/sessions/6f1a7c0e-0f5e-4a2b-9a3d-6b1c2d3e4f50/resume"
|
||||
);
|
||||
assert_eq!(
|
||||
attach(id()),
|
||||
"/api/attach/6f1a7c0e-0f5e-4a2b-9a3d-6b1c2d3e4f50"
|
||||
@@ -236,6 +247,10 @@ mod tests {
|
||||
patterns::SESSION_FOCUS.trim_end_matches(":id/focus"),
|
||||
session(id()).trim_end_matches(&id().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
patterns::SESSION_RESUME.trim_end_matches(":id/resume"),
|
||||
session(id()).trim_end_matches(&id().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
patterns::ATTACH.trim_end_matches(":id"),
|
||||
attach(id()).trim_end_matches(&id().to_string())
|
||||
|
||||
Reference in New Issue
Block a user