use motosan_agent_tool::ToolResult;
use serde_json::Value;
#[derive(Debug, Clone)]
pub enum ProgressChunk {
Stdout(Vec<u8>),
Stderr(Vec<u8>),
Status(String),
}
impl From<crate::tools::ToolProgressChunk> for ProgressChunk {
fn from(c: crate::tools::ToolProgressChunk) -> Self {
use crate::tools::ToolProgressChunk as TPC;
match c {
TPC::Stdout(b) => Self::Stdout(b),
TPC::Stderr(b) => Self::Stderr(b),
TPC::Status(s) => Self::Status(s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PermissionResolution {
pub tool: String,
pub args: serde_json::Value,
pub choice: PermissionChoice,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PermissionChoice {
AllowOnce,
AllowSession,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
SendUserMessage(String),
CancelAgent,
Quit,
ResolvePermission(PermissionResolution),
RunInlineBash {
command: String,
send_to_llm: bool,
},
Compact,
NewSession,
CloneSession,
SwitchModel(crate::model::ModelId),
LoadSession(String),
ForkFrom {
from: String,
message: String,
},
}
#[derive(Debug)]
pub enum UiEvent {
AgentTurnStarted,
AgentThinking,
AgentTextDelta(String),
AgentMessageComplete(String),
ToolCallStarted {
id: String,
name: String,
args: Value,
},
ToolCallProgress {
id: String,
chunk: ProgressChunk,
},
ToolCallCompleted {
id: String,
result: UiToolResult,
},
AgentTurnComplete,
PermissionRequested {
tool: String,
args: serde_json::Value,
resolver: tokio::sync::oneshot::Sender<crate::permissions::Decision>,
},
InlineBashOutput {
command: String,
output: String,
},
SessionReplaced(Vec<motosan_agent_loop::Message>),
ModelSwitched(crate::model::ModelId),
ForkCandidates(Vec<(String, String)>),
BranchTree(motosan_agent_loop::BranchTree),
Error(String),
}
#[derive(Debug, Clone)]
pub struct UiToolResult {
pub is_error: bool,
pub text: String,
}
impl From<&ToolResult> for UiToolResult {
fn from(r: &ToolResult) -> Self {
Self {
is_error: r.is_error,
text: format!("{r:?}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compact_command_is_constructible() {
let c = Command::Compact;
assert_eq!(c, Command::Compact);
assert!(format!("{c:?}").contains("Compact"));
}
#[test]
fn d2_command_and_event_variants_construct() {
let _ = Command::NewSession;
let _ = Command::SwitchModel(crate::model::ModelId::from("m"));
let _ = Command::LoadSession("sess-id".into());
let e = UiEvent::SessionReplaced(Vec::new());
assert!(format!("{e:?}").contains("SessionReplaced"));
let e = UiEvent::ModelSwitched(crate::model::ModelId::from("m"));
assert!(format!("{e:?}").contains("ModelSwitched"));
}
#[test]
fn clone_session_command_constructs() {
let c = Command::CloneSession;
assert!(format!("{c:?}").contains("CloneSession"));
}
#[test]
fn fork_protocol_variants_construct() {
let c = Command::ForkFrom {
from: "e1".into(),
message: "hi".into(),
};
assert!(format!("{c:?}").contains("ForkFrom"));
let e = UiEvent::ForkCandidates(vec![("e1".into(), "first".into())]);
assert!(format!("{e:?}").contains("ForkCandidates"));
}
#[test]
fn branch_tree_event_constructs() {
let tree = motosan_agent_loop::BranchTree {
nodes: Vec::new(),
root: None,
active_leaf: None,
};
let e = UiEvent::BranchTree(tree);
assert!(format!("{e:?}").contains("BranchTree"));
}
#[test]
fn run_inline_bash_command_is_constructible() {
let c = Command::RunInlineBash {
command: "ls".into(),
send_to_llm: true,
};
assert!(format!("{c:?}").contains("RunInlineBash"));
}
#[test]
fn permission_protocol_variants_are_constructible() {
let command = Command::ResolvePermission(PermissionResolution {
tool: "bash".into(),
args: serde_json::json!({"command": "echo hi"}),
choice: PermissionChoice::AllowSession,
});
assert!(format!("{command:?}").contains("ResolvePermission"));
assert!(format!("{command:?}").contains("AllowSession"));
let (resolver, _rx) = tokio::sync::oneshot::channel::<crate::permissions::Decision>();
let event = UiEvent::PermissionRequested {
tool: "bash".into(),
args: serde_json::json!({"command": "echo hi"}),
resolver,
};
assert!(format!("{event:?}").contains("PermissionRequested"));
}
}