agent_core/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::tui::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.push(PendingAction::CompactConversation);
137    }
138
139    /// Request to open the theme picker.
140    ///
141    /// This is deferred until after the command returns.
142    pub fn open_theme_picker(&mut self) {
143        self.pending_actions.push(PendingAction::OpenThemePicker);
144    }
145
146    /// Request to open the session picker.
147    ///
148    /// This is deferred until after the command returns.
149    pub fn open_session_picker(&mut self) {
150        self.pending_actions.push(PendingAction::OpenSessionPicker);
151    }
152
153    /// Request to quit the application.
154    ///
155    /// This is deferred until after the command returns.
156    pub fn request_quit(&mut self) {
157        self.pending_actions.push(PendingAction::Quit);
158    }
159
160    /// Request to create a new session.
161    ///
162    /// This is deferred until after the command returns.
163    pub fn create_new_session(&mut self) {
164        self.pending_actions.push(PendingAction::CreateNewSession);
165    }
166
167    // --- Controller communication ---
168
169    /// Send a control command to the controller.
170    ///
171    /// This allows commands to interact with the LLM controller,
172    /// for example to create a new session.
173    pub fn send_to_controller(&self, cmd: ControlCmd) {
174        if let Some(tx) = self.controller_tx {
175            let payload = ControllerInputPayload::control(self.session_id, cmd);
176            let _ = tx.try_send(payload);
177        }
178    }
179
180    // --- Extension access ---
181
182    /// Get agent-specific extension data.
183    ///
184    /// Returns `None` if no extension was provided or if the type doesn't match.
185    ///
186    /// # Example
187    ///
188    /// ```ignore
189    /// struct MyContext { api_key: String }
190    ///
191    /// fn execute(&self, args: &str, ctx: &mut CommandContext) -> CommandResult {
192    ///     if let Some(my_ctx) = ctx.extension::<MyContext>() {
193    ///         // Use my_ctx.api_key
194    ///     }
195    /// }
196    /// ```
197    pub fn extension<T: 'static>(&self) -> Option<&T> {
198        self.extension?.downcast_ref::<T>()
199    }
200
201    // --- Internal: get pending actions ---
202
203    /// Take the pending actions (internal use by App).
204    pub(crate) fn take_pending_actions(&mut self) -> Vec<PendingAction> {
205        std::mem::take(&mut self.pending_actions)
206    }
207}