1use motosan_agent_tool::ToolResult;
2use serde_json::Value;
3
4#[derive(Debug, Clone)]
5pub enum ProgressChunk {
6 Stdout(Vec<u8>),
7 Stderr(Vec<u8>),
8 Status(String),
9}
10
11impl From<crate::tools::ToolProgressChunk> for ProgressChunk {
12 fn from(c: crate::tools::ToolProgressChunk) -> Self {
13 use crate::tools::ToolProgressChunk as TPC;
14 match c {
15 TPC::Stdout(b) => Self::Stdout(b),
16 TPC::Stderr(b) => Self::Stderr(b),
17 TPC::Status(s) => Self::Status(s),
18 }
19 }
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct PermissionResolution {
26 pub tool: String,
27 pub args: serde_json::Value,
28 pub choice: PermissionChoice,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum PermissionChoice {
33 AllowOnce,
34 AllowSession,
35 Deny,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum Command {
40 SendUserMessage(String),
41 CancelAgent,
42 Quit,
43 ResolvePermission(PermissionResolution),
44 RunInlineBash {
48 command: String,
49 send_to_llm: bool,
50 },
51 Compact,
53 NewSession,
55 SwitchModel(crate::model::ModelId),
57 LoadSession(String),
59}
60
61#[derive(Debug)]
62pub enum UiEvent {
63 AgentTurnStarted,
64 AgentThinking,
65 AgentTextDelta(String),
66 AgentMessageComplete(String),
67 ToolCallStarted {
68 id: String,
69 name: String,
70 args: Value,
71 },
72 ToolCallProgress {
73 id: String,
74 chunk: ProgressChunk,
75 },
76 ToolCallCompleted {
77 id: String,
78 result: UiToolResult,
79 },
80 AgentTurnComplete,
81 PermissionRequested {
82 tool: String,
83 args: serde_json::Value,
84 resolver: tokio::sync::oneshot::Sender<crate::permissions::Decision>,
85 },
86 InlineBashOutput {
88 command: String,
89 output: String,
90 },
91 SessionReplaced(Vec<motosan_agent_loop::Message>),
95 ModelSwitched(crate::model::ModelId),
97 Error(String),
98}
99
100#[derive(Debug, Clone)]
101pub struct UiToolResult {
102 pub is_error: bool,
103 pub text: String,
104}
105
106impl From<&ToolResult> for UiToolResult {
107 fn from(r: &ToolResult) -> Self {
108 Self {
109 is_error: r.is_error,
110 text: format!("{r:?}"),
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn compact_command_is_constructible() {
121 let c = Command::Compact;
122 assert_eq!(c, Command::Compact);
123 assert!(format!("{c:?}").contains("Compact"));
124 }
125
126 #[test]
127 fn d2_command_and_event_variants_construct() {
128 let _ = Command::NewSession;
129 let _ = Command::SwitchModel(crate::model::ModelId::from("m"));
130 let _ = Command::LoadSession("sess-id".into());
131 let e = UiEvent::SessionReplaced(Vec::new());
132 assert!(format!("{e:?}").contains("SessionReplaced"));
133 let e = UiEvent::ModelSwitched(crate::model::ModelId::from("m"));
134 assert!(format!("{e:?}").contains("ModelSwitched"));
135 }
136
137 #[test]
138 fn run_inline_bash_command_is_constructible() {
139 let c = Command::RunInlineBash {
140 command: "ls".into(),
141 send_to_llm: true,
142 };
143 assert!(format!("{c:?}").contains("RunInlineBash"));
144 }
145
146 #[test]
147 fn permission_protocol_variants_are_constructible() {
148 let command = Command::ResolvePermission(PermissionResolution {
149 tool: "bash".into(),
150 args: serde_json::json!({"command": "echo hi"}),
151 choice: PermissionChoice::AllowSession,
152 });
153 assert!(format!("{command:?}").contains("ResolvePermission"));
154 assert!(format!("{command:?}").contains("AllowSession"));
155
156 let (resolver, _rx) = tokio::sync::oneshot::channel::<crate::permissions::Decision>();
157 let event = UiEvent::PermissionRequested {
158 tool: "bash".into(),
159 args: serde_json::json!({"command": "echo hi"}),
160 resolver,
161 };
162 assert!(format!("{event:?}").contains("PermissionRequested"));
163 }
164}