Skip to main content

capo_agent/
events.rs

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/// User's answer to a `PermissionRequested` prompt, carried back from the
23/// front-end to the binary so the binary can update the session cache.
24#[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    /// `!cmd` / `!!cmd` — run a shell command typed directly by the user.
45    /// `send_to_llm` is true for `!cmd` (output becomes a user message),
46    /// false for `!!cmd` (output shown in the transcript only).
47    RunInlineBash {
48        command: String,
49        send_to_llm: bool,
50    },
51    /// Manual context compaction, triggered by the `/compact` slash command.
52    Compact,
53    /// `/new` — replace the live session with a fresh empty one.
54    NewSession,
55    /// `/model` — rebuild the live session under a different model.
56    SwitchModel(crate::model::ModelId),
57    /// `/resume` — replace the live session with a stored one, by id.
58    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    /// Output of a `!!cmd` inline-bash run, for transcript display.
87    InlineBashOutput {
88        command: String,
89        output: String,
90    },
91    /// The session was replaced (`/new` cleared it, or `/resume` loaded a
92    /// stored one). Carries the new transcript so the TUI can rebuild
93    /// `state.messages`. Empty Vec = a fresh session.
94    SessionReplaced(Vec<motosan_agent_loop::Message>),
95    /// The active model changed successfully.
96    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}