1use crate::agent_bridge::{AgentBridge, AgentEvent};
6use crate::error::CliError;
7use crate::session::SessionManager;
8use crate::tui::{activity::format_activity_message, TuiState};
9use limit_tui::components::{ActivityFeed, ChatView, Message, Spinner};
10use std::sync::{Arc, Mutex};
11use tokio::sync::mpsc;
12use tracing::debug;
13
14pub struct TuiBridge {
16 agent_bridge: Arc<Mutex<AgentBridge>>,
18 event_rx: mpsc::UnboundedReceiver<AgentEvent>,
20 state: Arc<Mutex<TuiState>>,
22 chat_view: Arc<Mutex<ChatView>>,
24 activity_feed: Arc<Mutex<ActivityFeed>>,
26 spinner: Arc<Mutex<Spinner>>,
29 messages: Arc<Mutex<Vec<limit_llm::Message>>>,
31 total_input_tokens: Arc<Mutex<u64>>,
33 total_output_tokens: Arc<Mutex<u64>>,
35 session_manager: Arc<Mutex<SessionManager>>,
37 session_id: Arc<Mutex<String>>,
39 operation_id: Arc<Mutex<u64>>,
41}
42
43impl TuiBridge {
44 pub fn new(
46 agent_bridge: AgentBridge,
47 event_rx: mpsc::UnboundedReceiver<AgentEvent>,
48 ) -> Result<Self, CliError> {
49 let session_manager = SessionManager::new().map_err(|e| {
50 CliError::ConfigError(format!("Failed to create session manager: {}", e))
51 })?;
52
53 Self::with_session_manager(agent_bridge, event_rx, session_manager)
54 }
55
56 #[cfg(test)]
58 pub fn new_for_test(
59 agent_bridge: AgentBridge,
60 event_rx: mpsc::UnboundedReceiver<AgentEvent>,
61 ) -> Result<Self, CliError> {
62 use tempfile::TempDir;
63
64 let temp_dir = TempDir::new().map_err(|e| {
66 CliError::ConfigError(format!("Failed to create temp directory: {}", e))
67 })?;
68
69 let db_path = temp_dir.path().join("session.db");
70 let sessions_dir = temp_dir.path().join("sessions");
71
72 let session_manager = SessionManager::with_paths(db_path, sessions_dir).map_err(|e| {
73 CliError::ConfigError(format!("Failed to create session manager: {}", e))
74 })?;
75
76 Self::with_session_manager(agent_bridge, event_rx, session_manager)
77 }
78
79 pub fn with_session_manager(
81 agent_bridge: AgentBridge,
82 event_rx: mpsc::UnboundedReceiver<AgentEvent>,
83 session_manager: SessionManager,
84 ) -> Result<Self, CliError> {
85 let session_id = session_manager
87 .create_new_session()
88 .map_err(|e| CliError::ConfigError(format!("Failed to create session: {}", e)))?;
89 tracing::info!("Created new TUI session: {}", session_id);
90
91 let messages: Vec<limit_llm::Message> = Vec::new();
93
94 let sessions = session_manager.list_sessions().unwrap_or_default();
96 let session_info = sessions.iter().find(|s| s.id == session_id);
97 let initial_input = session_info.map(|s| s.total_input_tokens).unwrap_or(0);
98 let initial_output = session_info.map(|s| s.total_output_tokens).unwrap_or(0);
99
100 let chat_view = Arc::new(Mutex::new(ChatView::new()));
101
102 for msg in &messages {
104 match msg.role {
105 limit_llm::Role::User => {
106 let chat_msg = Message::user(msg.content.clone().unwrap_or_default());
107 chat_view.lock().unwrap().add_message(chat_msg);
108 }
109 limit_llm::Role::Assistant => {
110 let content = msg.content.clone().unwrap_or_default();
111 let chat_msg = Message::assistant(content);
112 chat_view.lock().unwrap().add_message(chat_msg);
113 }
114 limit_llm::Role::System => {
115 }
117 limit_llm::Role::Tool => {
118 }
120 }
121 }
122
123 tracing::info!("Loaded {} messages into chat view", messages.len());
124
125 let session_short_id = format!("...{}", &session_id[session_id.len().saturating_sub(8)..]);
127 let welcome_msg =
128 Message::system(format!("🆕 New TUI session started: {}", session_short_id));
129 chat_view.lock().unwrap().add_message(welcome_msg);
130
131 let model_name = agent_bridge.model().to_string();
133 if !model_name.is_empty() {
134 let model_msg = Message::system(format!("Using model: {}", model_name));
135 chat_view.lock().unwrap().add_message(model_msg);
136 }
137
138 Ok(Self {
139 agent_bridge: Arc::new(Mutex::new(agent_bridge)),
140 event_rx,
141 state: Arc::new(Mutex::new(TuiState::Idle)),
142 chat_view,
143 activity_feed: Arc::new(Mutex::new(ActivityFeed::new())),
144 spinner: Arc::new(Mutex::new(Spinner::new("Thinking..."))),
145 messages: Arc::new(Mutex::new(messages)),
146 total_input_tokens: Arc::new(Mutex::new(initial_input)),
147 total_output_tokens: Arc::new(Mutex::new(initial_output)),
148 session_manager: Arc::new(Mutex::new(session_manager)),
149 session_id: Arc::new(Mutex::new(session_id)),
150 operation_id: Arc::new(Mutex::new(0)),
151 })
152 }
153
154 pub fn agent_bridge_arc(&self) -> Arc<Mutex<AgentBridge>> {
156 self.agent_bridge.clone()
157 }
158
159 #[allow(dead_code)]
161 pub fn agent_bridge(&self) -> std::sync::MutexGuard<'_, AgentBridge> {
162 self.agent_bridge.lock().unwrap()
163 }
164
165 pub fn state(&self) -> TuiState {
167 self.state.lock().unwrap().clone()
168 }
169
170 pub fn chat_view(&self) -> &Arc<Mutex<ChatView>> {
172 &self.chat_view
173 }
174
175 pub fn spinner(&self) -> &Arc<Mutex<Spinner>> {
177 &self.spinner
178 }
179
180 pub fn activity_feed(&self) -> &Arc<Mutex<ActivityFeed>> {
182 &self.activity_feed
183 }
184
185 pub fn process_events(&mut self) -> Result<(), CliError> {
187 let mut event_count = 0;
188 let current_op_id = self.operation_id();
189
190 while let Ok(event) = self.event_rx.try_recv() {
191 event_count += 1;
192
193 let event_op_id = match &event {
195 AgentEvent::Thinking { operation_id } => *operation_id,
196 AgentEvent::ToolStart { operation_id, .. } => *operation_id,
197 AgentEvent::ToolComplete { operation_id, .. } => *operation_id,
198 AgentEvent::ContentChunk { operation_id, .. } => *operation_id,
199 AgentEvent::Done { operation_id } => *operation_id,
200 AgentEvent::Cancelled { operation_id } => *operation_id,
201 AgentEvent::Error { operation_id, .. } => *operation_id,
202 AgentEvent::TokenUsage { operation_id, .. } => *operation_id,
203 };
204
205 debug!(
206 "process_events: event_op_id={}, current_op_id={}, event={:?}",
207 event_op_id,
208 current_op_id,
209 std::mem::discriminant(&event)
210 );
211
212 if event_op_id != current_op_id {
214 debug!(
215 "process_events: Ignoring event from old operation {} (current: {})",
216 event_op_id, current_op_id
217 );
218 continue;
219 }
220
221 match event {
222 AgentEvent::Thinking { operation_id: _ } => {
223 debug!("process_events: Thinking event received - setting state to Thinking",);
224 *self.state.lock().unwrap() = TuiState::Thinking;
225 debug!("process_events: state is now {:?}", self.state());
226 }
227 AgentEvent::ToolStart {
228 operation_id: _,
229 name,
230 args,
231 } => {
232 debug!("process_events: ToolStart event - {}", name);
233 let activity_msg = format_activity_message(&name, &args);
234 self.activity_feed.lock().unwrap().add(activity_msg, true);
236 }
237 AgentEvent::ToolComplete {
238 operation_id: _,
239 name: _,
240 result: _,
241 } => {
242 debug!("process_events: ToolComplete event");
243 self.activity_feed.lock().unwrap().complete_current();
245 }
246 AgentEvent::ContentChunk {
247 operation_id: _,
248 chunk,
249 } => {
250 debug!("process_events: ContentChunk event ({} chars)", chunk.len());
251 self.chat_view
252 .lock()
253 .unwrap()
254 .append_to_last_assistant(&chunk);
255 }
256 AgentEvent::Done { operation_id: _ } => {
257 debug!("process_events: Done event received");
258 *self.state.lock().unwrap() = TuiState::Idle;
259 self.activity_feed.lock().unwrap().complete_all();
261 }
262 AgentEvent::Cancelled { operation_id: _ } => {
263 debug!("process_events: Cancelled event received");
264 *self.state.lock().unwrap() = TuiState::Idle;
265 self.activity_feed.lock().unwrap().complete_all();
267 }
268 AgentEvent::Error {
269 operation_id: _,
270 message,
271 } => {
272 debug!("process_events: Error event - {}", message);
273 *self.state.lock().unwrap() = TuiState::Idle;
275 let chat_msg = Message::system(format!("Error: {}", message));
276 self.chat_view.lock().unwrap().add_message(chat_msg);
277 }
278 AgentEvent::TokenUsage {
279 operation_id: _,
280 input_tokens,
281 output_tokens,
282 } => {
283 debug!(
284 "process_events: TokenUsage event - in={}, out={}",
285 input_tokens, output_tokens
286 );
287 *self.total_input_tokens.lock().unwrap() += input_tokens;
289 *self.total_output_tokens.lock().unwrap() += output_tokens;
290 }
291 }
292 }
293 if event_count > 0 {
294 debug!("process_events: processed {} events", event_count);
295 }
296 Ok(())
297 }
298
299 pub fn add_user_message(&self, content: String) {
301 let msg = Message::user(content);
302 self.chat_view.lock().unwrap().add_message(msg);
303 }
304
305 pub fn tick_spinner(&self) {
307 self.spinner.lock().unwrap().tick();
308 }
309
310 pub fn is_busy(&self) -> bool {
312 !matches!(self.state(), TuiState::Idle)
313 }
314
315 #[inline]
317 pub fn operation_id(&self) -> u64 {
318 *self.operation_id.lock().unwrap_or_else(|e| e.into_inner())
319 }
320
321 pub fn next_operation_id(&self) -> u64 {
323 let mut id = self.operation_id.lock().unwrap_or_else(|e| e.into_inner());
324 *id += 1;
325 *id
326 }
327
328 #[inline]
330 pub fn total_input_tokens(&self) -> u64 {
331 *self
332 .total_input_tokens
333 .lock()
334 .unwrap_or_else(|e| e.into_inner())
335 }
336
337 #[inline]
339 pub fn total_output_tokens(&self) -> u64 {
340 *self
341 .total_output_tokens
342 .lock()
343 .unwrap_or_else(|e| e.into_inner())
344 }
345
346 pub fn session_id(&self) -> String {
348 self.session_id
349 .lock()
350 .map(|guard| guard.clone())
351 .unwrap_or_else(|_| String::from("unknown"))
352 }
353
354 pub fn save_session(&self) -> Result<(), CliError> {
356 let session_id = self
357 .session_id
358 .lock()
359 .map(|guard| guard.clone())
360 .unwrap_or_else(|_| String::from("unknown"));
361
362 let messages = self
363 .messages
364 .lock()
365 .map(|guard| guard.clone())
366 .unwrap_or_default();
367
368 let input_tokens = self
369 .total_input_tokens
370 .lock()
371 .map(|guard| *guard)
372 .unwrap_or(0);
373
374 let output_tokens = self
375 .total_output_tokens
376 .lock()
377 .map(|guard| *guard)
378 .unwrap_or(0);
379
380 tracing::debug!(
381 "Saving session {} with {} messages, {} in tokens, {} out tokens",
382 session_id,
383 messages.len(),
384 input_tokens,
385 output_tokens
386 );
387
388 let session_manager = self.session_manager.lock().map_err(|e| {
389 CliError::ConfigError(format!("Failed to acquire session manager lock: {}", e))
390 })?;
391
392 session_manager.save_session(&session_id, &messages, input_tokens, output_tokens)?;
393 tracing::info!(
394 "✓ Session {} saved successfully ({} messages, {} in tokens, {} out tokens)",
395 session_id,
396 messages.len(),
397 input_tokens,
398 output_tokens
399 );
400 Ok(())
401 }
402
403 pub fn session_manager(&self) -> Arc<Mutex<SessionManager>> {
405 self.session_manager.clone()
406 }
407
408 pub fn messages(&self) -> Arc<Mutex<Vec<limit_llm::Message>>> {
410 self.messages.clone()
411 }
412
413 pub fn state_arc(&self) -> Arc<Mutex<TuiState>> {
415 self.state.clone()
416 }
417
418 pub fn total_input_tokens_arc(&self) -> Arc<Mutex<u64>> {
420 self.total_input_tokens.clone()
421 }
422
423 pub fn total_output_tokens_arc(&self) -> Arc<Mutex<u64>> {
425 self.total_output_tokens.clone()
426 }
427
428 pub fn session_id_arc(&self) -> Arc<Mutex<String>> {
430 self.session_id.clone()
431 }
432
433 pub fn set_state(&self, new_state: TuiState) {
435 *self.state.lock().unwrap() = new_state;
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 fn create_test_config() -> limit_llm::Config {
445 use limit_llm::{BrowserConfigSection, ProviderConfig};
446 let mut providers = std::collections::HashMap::new();
447 providers.insert(
448 "anthropic".to_string(),
449 ProviderConfig {
450 api_key: Some("test-key".to_string()),
451 model: "claude-3-5-sonnet-20241022".to_string(),
452 base_url: None,
453 max_tokens: 4096,
454 timeout: 60,
455 max_iterations: 100,
456 thinking_enabled: false,
457 clear_thinking: true,
458 },
459 );
460 limit_llm::Config {
461 provider: "anthropic".to_string(),
462 providers,
463 browser: BrowserConfigSection::default(),
464 }
465 }
466
467 #[test]
468 fn test_tui_bridge_new() {
469 let config = create_test_config();
470 let agent_bridge = AgentBridge::new(config).unwrap();
471 let (_tx, rx) = mpsc::unbounded_channel();
472
473 let tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
474 assert_eq!(tui_bridge.state(), TuiState::Idle);
475 }
476
477 #[test]
478 fn test_tui_bridge_state() {
479 let config = create_test_config();
480 let agent_bridge = AgentBridge::new(config).unwrap();
481 let (tx, rx) = mpsc::unbounded_channel();
482
483 let mut tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
484
485 let op_id = tui_bridge.operation_id();
486 tx.send(AgentEvent::Thinking {
487 operation_id: op_id,
488 })
489 .unwrap();
490 tui_bridge.process_events().unwrap();
491 assert!(matches!(tui_bridge.state(), TuiState::Thinking));
492
493 tx.send(AgentEvent::Done {
494 operation_id: op_id,
495 })
496 .unwrap();
497 tui_bridge.process_events().unwrap();
498 assert_eq!(tui_bridge.state(), TuiState::Idle);
499 }
500
501 #[test]
502 fn test_tui_bridge_chat_view() {
503 let config = create_test_config();
504 let agent_bridge = AgentBridge::new(config).unwrap();
505 let (_tx, rx) = mpsc::unbounded_channel();
506
507 let tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
508
509 tui_bridge.add_user_message("Hello".to_string());
510 assert_eq!(tui_bridge.chat_view().lock().unwrap().message_count(), 3); }
512}