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 CloneSession,
58 SwitchModel(crate::model::ModelId),
60 LoadSession(String),
62 ForkFrom {
64 from: String,
65 message: String,
66 },
67}
68
69#[derive(Debug)]
70pub enum UiEvent {
71 AgentTurnStarted,
72 AgentThinking,
73 AgentTextDelta(String),
74 AgentMessageComplete(String),
75 ToolCallStarted {
76 id: String,
77 name: String,
78 args: Value,
79 },
80 ToolCallProgress {
81 id: String,
82 chunk: ProgressChunk,
83 },
84 ToolCallCompleted {
85 id: String,
86 result: UiToolResult,
87 },
88 AgentTurnComplete,
89 PermissionRequested {
90 tool: String,
91 args: serde_json::Value,
92 resolver: tokio::sync::oneshot::Sender<crate::permissions::Decision>,
93 },
94 InlineBashOutput {
96 command: String,
97 output: String,
98 },
99 SessionReplaced(Vec<motosan_agent_loop::Message>),
103 ModelSwitched(crate::model::ModelId),
105 ForkCandidates(Vec<(String, String)>),
109 BranchTree(motosan_agent_loop::BranchTree),
112 Error(String),
113}
114
115#[derive(Debug, Clone)]
116pub struct UiToolResult {
117 pub is_error: bool,
118 pub text: String,
119}
120
121impl From<&ToolResult> for UiToolResult {
122 fn from(r: &ToolResult) -> Self {
123 Self {
124 is_error: r.is_error,
125 text: format!("{r:?}"),
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn compact_command_is_constructible() {
136 let c = Command::Compact;
137 assert_eq!(c, Command::Compact);
138 assert!(format!("{c:?}").contains("Compact"));
139 }
140
141 #[test]
142 fn d2_command_and_event_variants_construct() {
143 let _ = Command::NewSession;
144 let _ = Command::SwitchModel(crate::model::ModelId::from("m"));
145 let _ = Command::LoadSession("sess-id".into());
146 let e = UiEvent::SessionReplaced(Vec::new());
147 assert!(format!("{e:?}").contains("SessionReplaced"));
148 let e = UiEvent::ModelSwitched(crate::model::ModelId::from("m"));
149 assert!(format!("{e:?}").contains("ModelSwitched"));
150 }
151
152 #[test]
153 fn clone_session_command_constructs() {
154 let c = Command::CloneSession;
155 assert!(format!("{c:?}").contains("CloneSession"));
156 }
157
158 #[test]
159 fn fork_protocol_variants_construct() {
160 let c = Command::ForkFrom {
161 from: "e1".into(),
162 message: "hi".into(),
163 };
164 assert!(format!("{c:?}").contains("ForkFrom"));
165 let e = UiEvent::ForkCandidates(vec![("e1".into(), "first".into())]);
166 assert!(format!("{e:?}").contains("ForkCandidates"));
167 }
168
169 #[test]
170 fn branch_tree_event_constructs() {
171 let tree = motosan_agent_loop::BranchTree {
172 nodes: Vec::new(),
173 root: None,
174 active_leaf: None,
175 };
176 let e = UiEvent::BranchTree(tree);
177 assert!(format!("{e:?}").contains("BranchTree"));
178 }
179
180 #[test]
181 fn run_inline_bash_command_is_constructible() {
182 let c = Command::RunInlineBash {
183 command: "ls".into(),
184 send_to_llm: true,
185 };
186 assert!(format!("{c:?}").contains("RunInlineBash"));
187 }
188
189 #[test]
190 fn permission_protocol_variants_are_constructible() {
191 let command = Command::ResolvePermission(PermissionResolution {
192 tool: "bash".into(),
193 args: serde_json::json!({"command": "echo hi"}),
194 choice: PermissionChoice::AllowSession,
195 });
196 assert!(format!("{command:?}").contains("ResolvePermission"));
197 assert!(format!("{command:?}").contains("AllowSession"));
198
199 let (resolver, _rx) = tokio::sync::oneshot::channel::<crate::permissions::Decision>();
200 let event = UiEvent::PermissionRequested {
201 tool: "bash".into(),
202 args: serde_json::json!({"command": "echo hi"}),
203 resolver,
204 };
205 assert!(format!("{event:?}").contains("PermissionRequested"));
206 }
207}