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