1use std::io;
8
9use agent_core_runtime::agent::AgentCore;
10
11use crate::app::{App, AppConfig};
12use crate::commands::SlashCommand;
13use crate::keys::{DefaultKeyHandler, ExitHandler, KeyBindings, KeyHandler};
14use crate::layout::LayoutTemplate;
15use crate::widgets::{widget_ids, ConversationView, ConversationViewFactory, SessionInfo, Widget};
16
17pub struct TuiRunner {
23 agent: AgentCore,
25
26 conversation_factory: Option<ConversationViewFactory>,
28
29 widgets_to_register: Vec<Box<dyn Widget>>,
31
32 layout_template: Option<LayoutTemplate>,
34
35 key_handler: Option<Box<dyn KeyHandler>>,
37
38 exit_handler: Option<Box<dyn ExitHandler>>,
40
41 commands: Option<Vec<Box<dyn SlashCommand>>>,
43
44 command_extension: Option<Box<dyn std::any::Any + Send>>,
46
47 custom_status_bar: Option<Box<dyn Widget>>,
49
50 hide_status_bar: bool,
52}
53
54impl TuiRunner {
55 pub fn new(agent: AgentCore) -> Self {
57 Self {
58 agent,
59 conversation_factory: None,
60 widgets_to_register: Vec::new(),
61 layout_template: None,
62 key_handler: None,
63 exit_handler: None,
64 commands: None,
65 command_extension: None,
66 custom_status_bar: None,
67 hide_status_bar: false,
68 }
69 }
70
71 pub fn agent_mut(&mut self) -> &mut AgentCore {
73 &mut self.agent
74 }
75
76 pub fn agent(&self) -> &AgentCore {
78 &self.agent
79 }
80
81 pub fn set_conversation_factory<F>(&mut self, factory: F) -> &mut Self
87 where
88 F: Fn() -> Box<dyn ConversationView> + Send + Sync + 'static,
89 {
90 self.conversation_factory = Some(Box::new(factory));
91 self
92 }
93
94 pub fn set_layout(&mut self, template: LayoutTemplate) -> &mut Self {
99 self.layout_template = Some(template);
100 self
101 }
102
103 pub fn set_key_handler<H: KeyHandler>(&mut self, handler: H) -> &mut Self {
107 self.key_handler = Some(Box::new(handler));
108 self
109 }
110
111 pub fn set_key_bindings(&mut self, bindings: KeyBindings) -> &mut Self {
116 self.key_handler = Some(Box::new(DefaultKeyHandler::new(bindings)));
117 self
118 }
119
120 pub fn set_exit_handler<H: ExitHandler>(&mut self, handler: H) -> &mut Self {
125 self.exit_handler = Some(Box::new(handler));
126 self
127 }
128
129 pub fn set_commands(&mut self, commands: Vec<Box<dyn SlashCommand>>) -> &mut Self {
133 self.commands = Some(commands);
134 self
135 }
136
137 pub fn set_command_extension<T: std::any::Any + Send + 'static>(&mut self, ext: T) -> &mut Self {
141 self.command_extension = Some(Box::new(ext));
142 self
143 }
144
145 pub fn register_widget<W: Widget>(&mut self, widget: W) -> &mut Self {
150 self.widgets_to_register.push(Box::new(widget));
151 self
152 }
153
154 pub fn set_status_bar<W: Widget>(&mut self, status_bar: W) -> &mut Self {
156 self.custom_status_bar = Some(Box::new(status_bar));
157 self
158 }
159
160 pub fn hide_status_bar(&mut self) -> &mut Self {
162 self.hide_status_bar = true;
163 self
164 }
165
166 pub fn run(mut self) -> io::Result<()> {
176 let name = self.agent.name().to_string();
177 tracing::info!("{} starting", name);
178
179 self.agent.start_background_tasks();
181
182 let app_config = AppConfig {
184 agent_name: name.clone(),
185 version: self.agent.version().to_string(),
186 commands: self.commands.take(),
187 command_extension: self.command_extension.take(),
188 error_no_session: self.agent.error_no_session().map(|s| s.to_string()),
189 ..Default::default()
190 };
191 let mut app = App::with_config(app_config);
192
193 if let Some(factory) = self.conversation_factory.take() {
195 app.set_conversation_factory(move || factory());
196 }
197
198 if self.hide_status_bar {
200 app.widgets.remove(widget_ids::STATUS_BAR);
202 } else if let Some(custom_status_bar) = self.custom_status_bar.take() {
203 app.widgets.insert(widget_ids::STATUS_BAR, custom_status_bar);
205 }
206
207 for widget in self.widgets_to_register.drain(..) {
209 let id = widget.id();
210 app.widgets.insert(id, widget);
211 }
212 app.rebuild_priority_order();
213
214 app.set_to_controller(self.agent.to_controller_tx());
216 if let Some(rx) = self.agent.take_from_controller_rx() {
217 app.set_from_controller(rx);
218 }
219 app.set_controller(self.agent.controller().clone());
220 app.set_runtime_handle(self.agent.runtime_handle());
221 app.set_user_interaction_registry(self.agent.user_interaction_registry().clone());
222 app.set_permission_registry(self.agent.permission_registry().clone());
223
224 if let Some(layout) = self.layout_template.take() {
226 app.set_layout(layout);
227 }
228
229 if let Some(handler) = self.key_handler.take() {
231 app.set_key_handler_boxed(handler);
232 }
233
234 if let Some(handler) = self.exit_handler.take() {
236 app.set_exit_handler_boxed(handler);
237 }
238
239 match self.agent.create_initial_session() {
241 Ok((session_id, model, context_limit)) => {
242 let session_info = SessionInfo::new(session_id, model.clone(), context_limit);
243 app.add_session(session_info);
244 app.set_session_id(session_id);
245 app.set_model_name(&model);
246 app.set_context_limit(context_limit);
247 tracing::info!(
248 session_id = session_id,
249 model = %model,
250 "Auto-created session on startup"
251 );
252 }
253 Err(e) => {
254 tracing::warn!(error = %e, "No initial session created");
255 }
256 }
257
258 if let Some(registry) = self.agent.take_llm_registry() {
260 app.set_llm_registry(registry);
261 }
262
263 let result = app.run();
265
266 self.agent.shutdown();
268
269 tracing::info!("{} stopped", name);
270 result
271 }
272}
273
274pub trait AgentCoreExt {
279 fn into_tui(self) -> TuiRunner;
281}
282
283impl AgentCoreExt for AgentCore {
284 fn into_tui(self) -> TuiRunner {
285 TuiRunner::new(self)
286 }
287}