Skip to main content

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}