agent_air_tui/commands/context.rs
1//! Command execution context.
2
3use std::any::Any;
4
5use tokio::sync::mpsc;
6
7use crate::controller::{ControlCmd, ControllerInputPayload};
8use crate::widgets::ConversationView;
9
10use super::SlashCommand;
11
12/// Actions that require App-level handling after command returns.
13#[derive(Debug, Clone)]
14pub enum PendingAction {
15 /// Open the theme picker overlay.
16 OpenThemePicker,
17 /// Open the session picker overlay.
18 OpenSessionPicker,
19 /// Clear the conversation view.
20 ClearConversation,
21 /// Compact the conversation history.
22 CompactConversation,
23 /// Create a new session.
24 CreateNewSession,
25 /// Quit the application.
26 Quit,
27}
28
29/// Context provided to commands during execution.
30///
31/// Provides controlled access to app functionality through methods rather
32/// than exposing internal state directly.
33///
34/// # Extension Data
35///
36/// Agents can provide custom extension data that commands can access:
37///
38/// ```ignore
39/// // In agent setup:
40/// agent.set_command_extension(MyContext { api_key: "..." });
41///
42/// // In command execution:
43/// fn execute(&self, args: &str, ctx: &mut CommandContext) -> CommandResult {
44/// let my_ctx = ctx.extension::<MyContext>().expect("MyContext required");
45/// // use my_ctx.api_key
46/// }
47/// ```
48pub struct CommandContext<'a> {
49 // Base context
50 session_id: i64,
51 agent_name: &'a str,
52 version: &'a str,
53 commands: &'a [Box<dyn SlashCommand>],
54
55 // Conversation view for displaying messages
56 conversation: &'a mut dyn ConversationView,
57
58 // Channel to send commands to controller
59 controller_tx: Option<&'a mpsc::Sender<ControllerInputPayload>>,
60
61 // Pending actions to execute after command returns
62 pending_actions: Vec<PendingAction>,
63
64 // Agent-provided extension data
65 extension: Option<&'a dyn Any>,
66}
67
68impl<'a> CommandContext<'a> {
69 /// Create a new command context.
70 ///
71 /// This is called internally by App when executing commands.
72 #[allow(clippy::too_many_arguments)]
73 pub(crate) fn new(
74 session_id: i64,
75 agent_name: &'a str,
76 version: &'a str,
77 commands: &'a [Box<dyn SlashCommand>],
78 conversation: &'a mut dyn ConversationView,
79 controller_tx: Option<&'a mpsc::Sender<ControllerInputPayload>>,
80 extension: Option<&'a dyn Any>,
81 ) -> Self {
82 Self {
83 session_id,
84 agent_name,
85 version,
86 commands,
87 conversation,
88 controller_tx,
89 pending_actions: Vec::new(),
90 extension,
91 }
92 }
93
94 // --- Base context accessors ---
95
96 /// Get the current session ID.
97 pub fn session_id(&self) -> i64 {
98 self.session_id
99 }
100
101 /// Get the agent name.
102 pub fn agent_name(&self) -> &str {
103 self.agent_name
104 }
105
106 /// Get the agent version.
107 pub fn version(&self) -> &str {
108 self.version
109 }
110
111 /// Get all registered commands (useful for /help).
112 pub fn commands(&self) -> &[Box<dyn SlashCommand>] {
113 self.commands
114 }
115
116 // --- Conversation operations ---
117
118 /// Display a system message in the conversation.
119 pub fn show_message(&mut self, msg: impl Into<String>) {
120 self.conversation.add_system_message(msg.into());
121 }
122
123 // --- Deferred actions (handled by App after execute returns) ---
124
125 /// Request to clear the conversation.
126 ///
127 /// This is deferred until after the command returns.
128 pub fn clear_conversation(&mut self) {
129 self.pending_actions.push(PendingAction::ClearConversation);
130 }
131
132 /// Request to compact the conversation history.
133 ///
134 /// This is deferred until after the command returns.
135 pub fn compact_conversation(&mut self) {
136 self.pending_actions
137 .push(PendingAction::CompactConversation);
138 }
139
140 /// Request to open the theme picker.
141 ///
142 /// This is deferred until after the command returns.
143 pub fn open_theme_picker(&mut self) {
144 self.pending_actions.push(PendingAction::OpenThemePicker);
145 }
146
147 /// Request to open the session picker.
148 ///
149 /// This is deferred until after the command returns.
150 pub fn open_session_picker(&mut self) {
151 self.pending_actions.push(PendingAction::OpenSessionPicker);
152 }
153
154 /// Request to quit the application.
155 ///
156 /// This is deferred until after the command returns.
157 pub fn request_quit(&mut self) {
158 self.pending_actions.push(PendingAction::Quit);
159 }
160
161 /// Request to create a new session.
162 ///
163 /// This is deferred until after the command returns.
164 pub fn create_new_session(&mut self) {
165 self.pending_actions.push(PendingAction::CreateNewSession);
166 }
167
168 // --- Controller communication ---
169
170 /// Send a control command to the controller.
171 ///
172 /// This allows commands to interact with the LLM controller,
173 /// for example to create a new session.
174 pub fn send_to_controller(&self, cmd: ControlCmd) {
175 if let Some(tx) = self.controller_tx {
176 let payload = ControllerInputPayload::control(self.session_id, cmd);
177 let _ = tx.try_send(payload);
178 }
179 }
180
181 // --- Extension access ---
182
183 /// Get agent-specific extension data.
184 ///
185 /// Returns `None` if no extension was provided or if the type doesn't match.
186 ///
187 /// # Example
188 ///
189 /// ```ignore
190 /// struct MyContext { api_key: String }
191 ///
192 /// fn execute(&self, args: &str, ctx: &mut CommandContext) -> CommandResult {
193 /// if let Some(my_ctx) = ctx.extension::<MyContext>() {
194 /// // Use my_ctx.api_key
195 /// }
196 /// }
197 /// ```
198 pub fn extension<T: 'static>(&self) -> Option<&T> {
199 self.extension?.downcast_ref::<T>()
200 }
201
202 // --- Internal: get pending actions ---
203
204 /// Take the pending actions (internal use by App).
205 pub(crate) fn take_pending_actions(&mut self) -> Vec<PendingAction> {
206 std::mem::take(&mut self.pending_actions)
207 }
208}