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::trace;
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::ResponseStart { operation_id } => *operation_id,
199 AgentEvent::ContentChunk { operation_id, .. } => *operation_id,
200 AgentEvent::Done { operation_id } => *operation_id,
201 AgentEvent::Cancelled { operation_id } => *operation_id,
202 AgentEvent::Error { operation_id, .. } => *operation_id,
203 AgentEvent::TokenUsage { operation_id, .. } => *operation_id,
204 };
205
206 trace!(
207 "process_events: event_op_id={}, current_op_id={}, event={:?}",
208 event_op_id,
209 current_op_id,
210 std::mem::discriminant(&event)
211 );
212
213 if event_op_id != current_op_id {
215 trace!(
216 "process_events: Ignoring event from old operation {} (current: {})",
217 event_op_id,
218 current_op_id
219 );
220 continue;
221 }
222
223 match event {
224 AgentEvent::Thinking { operation_id: _ } => {
225 trace!("process_events: Thinking event received - setting state to Thinking",);
226 *self.state.lock().unwrap() = TuiState::Thinking;
227 trace!("process_events: state is now {:?}", self.state());
228 }
229 AgentEvent::ToolStart {
230 operation_id: _,
231 name,
232 args,
233 } => {
234 trace!("process_events: ToolStart event - {}", name);
235 let activity_msg = format_activity_message(&name, &args);
236 self.activity_feed.lock().unwrap().add(activity_msg, true);
238 }
239 AgentEvent::ToolComplete {
240 operation_id: _,
241 name: _,
242 result: _,
243 } => {
244 trace!("process_events: ToolComplete event");
245 self.activity_feed.lock().unwrap().complete_current();
247 }
248 AgentEvent::ResponseStart { operation_id: _ } => {
249 trace!("process_events: ResponseStart event - creating new assistant message");
250 self.chat_view.lock().unwrap().start_new_assistant_message();
251 }
252 AgentEvent::ContentChunk {
253 operation_id: _,
254 chunk,
255 } => {
256 trace!("process_events: ContentChunk event ({} chars)", chunk.len());
257 self.chat_view
258 .lock()
259 .unwrap()
260 .append_to_last_assistant(&chunk);
261 }
262 AgentEvent::Done { operation_id: _ } => {
263 trace!("process_events: Done event received");
264 *self.state.lock().unwrap() = TuiState::Idle;
265 self.activity_feed.lock().unwrap().complete_all();
267 }
268 AgentEvent::Cancelled { operation_id: _ } => {
269 trace!("process_events: Cancelled event received");
270 *self.state.lock().unwrap() = TuiState::Idle;
271 self.activity_feed.lock().unwrap().complete_all();
273 }
274 AgentEvent::Error {
275 operation_id: _,
276 message,
277 } => {
278 trace!("process_events: Error event - {}", message);
279 *self.state.lock().unwrap() = TuiState::Idle;
281 let chat_msg = Message::system(format!("Error: {}", message));
282 self.chat_view.lock().unwrap().add_message(chat_msg);
283 }
284 AgentEvent::TokenUsage { .. } => {}
285 }
286 }
287 if event_count > 0 {
288 trace!("process_events: processed {} events", event_count);
289 }
290 Ok(())
291 }
292
293 pub fn add_user_message(&self, content: String) {
295 let msg = Message::user(content);
296 self.chat_view.lock().unwrap().add_message(msg);
297 }
298
299 pub fn tick_spinner(&self) {
301 self.spinner.lock().unwrap().tick();
302 }
303
304 pub fn is_busy(&self) -> bool {
306 !matches!(self.state(), TuiState::Idle)
307 }
308
309 #[inline]
311 pub fn operation_id(&self) -> u64 {
312 *self.operation_id.lock().unwrap_or_else(|e| e.into_inner())
313 }
314
315 pub fn next_operation_id(&self) -> u64 {
317 let mut id = self.operation_id.lock().unwrap_or_else(|e| e.into_inner());
318 *id += 1;
319 *id
320 }
321
322 #[inline]
324 pub fn total_input_tokens(&self) -> u64 {
325 *self
326 .total_input_tokens
327 .lock()
328 .unwrap_or_else(|e| e.into_inner())
329 }
330
331 #[inline]
333 pub fn total_output_tokens(&self) -> u64 {
334 *self
335 .total_output_tokens
336 .lock()
337 .unwrap_or_else(|e| e.into_inner())
338 }
339
340 pub fn session_id(&self) -> String {
342 self.session_id
343 .lock()
344 .map(|guard| guard.clone())
345 .unwrap_or_else(|_| String::from("unknown"))
346 }
347
348 pub fn save_session(&self) -> Result<(), CliError> {
350 let session_id = self
351 .session_id
352 .lock()
353 .map(|guard| guard.clone())
354 .unwrap_or_else(|_| String::from("unknown"));
355
356 let messages = self
357 .messages
358 .lock()
359 .map(|guard| guard.clone())
360 .unwrap_or_default();
361
362 let input_tokens = self
363 .total_input_tokens
364 .lock()
365 .map(|guard| *guard)
366 .unwrap_or(0);
367
368 let output_tokens = self
369 .total_output_tokens
370 .lock()
371 .map(|guard| *guard)
372 .unwrap_or(0);
373
374 tracing::debug!(
375 "Saving session {} with {} messages, {} in tokens, {} out tokens",
376 session_id,
377 messages.len(),
378 input_tokens,
379 output_tokens
380 );
381
382 let session_manager = self.session_manager.lock().map_err(|e| {
383 CliError::ConfigError(format!("Failed to acquire session manager lock: {}", e))
384 })?;
385
386 session_manager.save_session(&session_id, &messages, input_tokens, output_tokens)?;
387
388 if !messages.is_empty() {
389 if let Err(e) = session_manager.migrate_to_tree(&session_id) {
390 tracing::warn!("Failed to migrate session to tree format: {}", e);
391 }
392 }
393
394 tracing::info!(
395 "✓ Session {} saved successfully ({} messages, {} in tokens, {} out tokens)",
396 session_id,
397 messages.len(),
398 input_tokens,
399 output_tokens
400 );
401 Ok(())
402 }
403
404 pub fn session_manager(&self) -> Arc<Mutex<SessionManager>> {
406 self.session_manager.clone()
407 }
408
409 pub fn messages(&self) -> Arc<Mutex<Vec<limit_llm::Message>>> {
411 self.messages.clone()
412 }
413
414 pub fn state_arc(&self) -> Arc<Mutex<TuiState>> {
416 self.state.clone()
417 }
418
419 pub fn total_input_tokens_arc(&self) -> Arc<Mutex<u64>> {
421 self.total_input_tokens.clone()
422 }
423
424 pub fn total_output_tokens_arc(&self) -> Arc<Mutex<u64>> {
426 self.total_output_tokens.clone()
427 }
428
429 pub fn session_id_arc(&self) -> Arc<Mutex<String>> {
431 self.session_id.clone()
432 }
433
434 pub fn set_state(&self, new_state: TuiState) {
436 *self.state.lock().unwrap() = new_state;
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 fn create_test_config() -> limit_llm::Config {
446 use limit_llm::{BrowserConfigSection, ProviderConfig};
447 let mut providers = std::collections::HashMap::new();
448 providers.insert(
449 "anthropic".to_string(),
450 ProviderConfig {
451 api_key: Some("test-key".to_string()),
452 model: "claude-3-5-sonnet-20241022".to_string(),
453 base_url: None,
454 max_tokens: 4096,
455 timeout: 60,
456 max_iterations: 100,
457 thinking_enabled: false,
458 clear_thinking: true,
459 },
460 );
461 limit_llm::Config {
462 provider: "anthropic".to_string(),
463 providers,
464 browser: BrowserConfigSection::default(),
465 compaction: limit_llm::CompactionSettings::default(),
466 cache: limit_llm::CacheSettings::default(),
467 }
468 }
469
470 #[test]
471 fn test_tui_bridge_new() {
472 let config = create_test_config();
473 let agent_bridge = AgentBridge::new(config).unwrap();
474 let (_tx, rx) = mpsc::unbounded_channel();
475
476 let tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
477 assert_eq!(tui_bridge.state(), TuiState::Idle);
478 }
479
480 #[test]
481 fn test_tui_bridge_state() {
482 let config = create_test_config();
483 let agent_bridge = AgentBridge::new(config).unwrap();
484 let (tx, rx) = mpsc::unbounded_channel();
485
486 let mut tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
487
488 let op_id = tui_bridge.operation_id();
489 tx.send(AgentEvent::Thinking {
490 operation_id: op_id,
491 })
492 .unwrap();
493 tui_bridge.process_events().unwrap();
494 assert!(matches!(tui_bridge.state(), TuiState::Thinking));
495
496 tx.send(AgentEvent::Done {
497 operation_id: op_id,
498 })
499 .unwrap();
500 tui_bridge.process_events().unwrap();
501 assert_eq!(tui_bridge.state(), TuiState::Idle);
502 }
503
504 #[test]
505 fn test_tui_bridge_chat_view() {
506 let config = create_test_config();
507 let agent_bridge = AgentBridge::new(config).unwrap();
508 let (_tx, rx) = mpsc::unbounded_channel();
509
510 let tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
511
512 tui_bridge.add_user_message("Hello".to_string());
513 assert_eq!(tui_bridge.chat_view().lock().unwrap().message_count(), 3); }
515}