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}