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 let session_id = session_manager
55 .create_new_session()
56 .map_err(|e| CliError::ConfigError(format!("Failed to create session: {}", e)))?;
57 tracing::info!("Created new TUI session: {}", session_id);
58
59 let messages: Vec<limit_llm::Message> = Vec::new();
61
62 let sessions = session_manager.list_sessions().unwrap_or_default();
64 let session_info = sessions.iter().find(|s| s.id == session_id);
65 let initial_input = session_info.map(|s| s.total_input_tokens).unwrap_or(0);
66 let initial_output = session_info.map(|s| s.total_output_tokens).unwrap_or(0);
67
68 let chat_view = Arc::new(Mutex::new(ChatView::new()));
69
70 for msg in &messages {
72 match msg.role {
73 limit_llm::Role::User => {
74 let chat_msg = Message::user(msg.content.clone().unwrap_or_default());
75 chat_view.lock().unwrap().add_message(chat_msg);
76 }
77 limit_llm::Role::Assistant => {
78 let content = msg.content.clone().unwrap_or_default();
79 let chat_msg = Message::assistant(content);
80 chat_view.lock().unwrap().add_message(chat_msg);
81 }
82 limit_llm::Role::System => {
83 }
85 limit_llm::Role::Tool => {
86 }
88 }
89 }
90
91 tracing::info!("Loaded {} messages into chat view", messages.len());
92
93 let session_short_id = format!("...{}", &session_id[session_id.len().saturating_sub(8)..]);
95 let welcome_msg =
96 Message::system(format!("🆕 New TUI session started: {}", session_short_id));
97 chat_view.lock().unwrap().add_message(welcome_msg);
98
99 let model_name = agent_bridge.model().to_string();
101 if !model_name.is_empty() {
102 let model_msg = Message::system(format!("Using model: {}", model_name));
103 chat_view.lock().unwrap().add_message(model_msg);
104 }
105
106 Ok(Self {
107 agent_bridge: Arc::new(Mutex::new(agent_bridge)),
108 event_rx,
109 state: Arc::new(Mutex::new(TuiState::Idle)),
110 chat_view,
111 activity_feed: Arc::new(Mutex::new(ActivityFeed::new())),
112 spinner: Arc::new(Mutex::new(Spinner::new("Thinking..."))),
113 messages: Arc::new(Mutex::new(messages)),
114 total_input_tokens: Arc::new(Mutex::new(initial_input)),
115 total_output_tokens: Arc::new(Mutex::new(initial_output)),
116 session_manager: Arc::new(Mutex::new(session_manager)),
117 session_id: Arc::new(Mutex::new(session_id)),
118 operation_id: Arc::new(Mutex::new(0)),
119 })
120 }
121
122 pub fn agent_bridge_arc(&self) -> Arc<Mutex<AgentBridge>> {
124 self.agent_bridge.clone()
125 }
126
127 #[allow(dead_code)]
129 pub fn agent_bridge(&self) -> std::sync::MutexGuard<'_, AgentBridge> {
130 self.agent_bridge.lock().unwrap()
131 }
132
133 pub fn state(&self) -> TuiState {
135 self.state.lock().unwrap().clone()
136 }
137
138 pub fn chat_view(&self) -> &Arc<Mutex<ChatView>> {
140 &self.chat_view
141 }
142
143 pub fn spinner(&self) -> &Arc<Mutex<Spinner>> {
145 &self.spinner
146 }
147
148 pub fn activity_feed(&self) -> &Arc<Mutex<ActivityFeed>> {
150 &self.activity_feed
151 }
152
153 pub fn process_events(&mut self) -> Result<(), CliError> {
155 let mut event_count = 0;
156 let current_op_id = self.operation_id();
157
158 while let Ok(event) = self.event_rx.try_recv() {
159 event_count += 1;
160
161 let event_op_id = match &event {
163 AgentEvent::Thinking { operation_id } => *operation_id,
164 AgentEvent::ToolStart { operation_id, .. } => *operation_id,
165 AgentEvent::ToolComplete { operation_id, .. } => *operation_id,
166 AgentEvent::ContentChunk { operation_id, .. } => *operation_id,
167 AgentEvent::Done { operation_id } => *operation_id,
168 AgentEvent::Cancelled { operation_id } => *operation_id,
169 AgentEvent::Error { operation_id, .. } => *operation_id,
170 AgentEvent::TokenUsage { operation_id, .. } => *operation_id,
171 };
172
173 debug!(
174 "process_events: event_op_id={}, current_op_id={}, event={:?}",
175 event_op_id,
176 current_op_id,
177 std::mem::discriminant(&event)
178 );
179
180 if event_op_id != current_op_id {
182 debug!(
183 "process_events: Ignoring event from old operation {} (current: {})",
184 event_op_id, current_op_id
185 );
186 continue;
187 }
188
189 match event {
190 AgentEvent::Thinking { operation_id: _ } => {
191 debug!("process_events: Thinking event received - setting state to Thinking",);
192 *self.state.lock().unwrap() = TuiState::Thinking;
193 debug!("process_events: state is now {:?}", self.state());
194 }
195 AgentEvent::ToolStart {
196 operation_id: _,
197 name,
198 args,
199 } => {
200 debug!("process_events: ToolStart event - {}", name);
201 let activity_msg = format_activity_message(&name, &args);
202 self.activity_feed.lock().unwrap().add(activity_msg, true);
204 }
205 AgentEvent::ToolComplete {
206 operation_id: _,
207 name: _,
208 result: _,
209 } => {
210 debug!("process_events: ToolComplete event");
211 self.activity_feed.lock().unwrap().complete_current();
213 }
214 AgentEvent::ContentChunk {
215 operation_id: _,
216 chunk,
217 } => {
218 debug!("process_events: ContentChunk event ({} chars)", chunk.len());
219 self.chat_view
220 .lock()
221 .unwrap()
222 .append_to_last_assistant(&chunk);
223 }
224 AgentEvent::Done { operation_id: _ } => {
225 debug!("process_events: Done event received");
226 *self.state.lock().unwrap() = TuiState::Idle;
227 self.activity_feed.lock().unwrap().complete_all();
229 }
230 AgentEvent::Cancelled { operation_id: _ } => {
231 debug!("process_events: Cancelled event received");
232 *self.state.lock().unwrap() = TuiState::Idle;
233 self.activity_feed.lock().unwrap().complete_all();
235 }
236 AgentEvent::Error {
237 operation_id: _,
238 message,
239 } => {
240 debug!("process_events: Error event - {}", message);
241 *self.state.lock().unwrap() = TuiState::Idle;
243 let chat_msg = Message::system(format!("Error: {}", message));
244 self.chat_view.lock().unwrap().add_message(chat_msg);
245 }
246 AgentEvent::TokenUsage {
247 operation_id: _,
248 input_tokens,
249 output_tokens,
250 } => {
251 debug!(
252 "process_events: TokenUsage event - in={}, out={}",
253 input_tokens, output_tokens
254 );
255 *self.total_input_tokens.lock().unwrap() += input_tokens;
257 *self.total_output_tokens.lock().unwrap() += output_tokens;
258 }
259 }
260 }
261 if event_count > 0 {
262 debug!("process_events: processed {} events", event_count);
263 }
264 Ok(())
265 }
266
267 pub fn add_user_message(&self, content: String) {
269 let msg = Message::user(content);
270 self.chat_view.lock().unwrap().add_message(msg);
271 }
272
273 pub fn tick_spinner(&self) {
275 self.spinner.lock().unwrap().tick();
276 }
277
278 pub fn is_busy(&self) -> bool {
280 !matches!(self.state(), TuiState::Idle)
281 }
282
283 #[inline]
285 pub fn operation_id(&self) -> u64 {
286 *self.operation_id.lock().unwrap_or_else(|e| e.into_inner())
287 }
288
289 pub fn next_operation_id(&self) -> u64 {
291 let mut id = self.operation_id.lock().unwrap_or_else(|e| e.into_inner());
292 *id += 1;
293 *id
294 }
295
296 #[inline]
298 pub fn total_input_tokens(&self) -> u64 {
299 *self.total_input_tokens.lock().unwrap_or_else(|e| e.into_inner())
300 }
301
302 #[inline]
304 pub fn total_output_tokens(&self) -> u64 {
305 *self.total_output_tokens.lock().unwrap_or_else(|e| e.into_inner())
306 }
307
308 pub fn session_id(&self) -> String {
310 self.session_id
311 .lock()
312 .map(|guard| guard.clone())
313 .unwrap_or_else(|_| String::from("unknown"))
314 }
315
316 pub fn save_session(&self) -> Result<(), CliError> {
318 let session_id = self
319 .session_id
320 .lock()
321 .map(|guard| guard.clone())
322 .unwrap_or_else(|_| String::from("unknown"));
323
324 let messages = self
325 .messages
326 .lock()
327 .map(|guard| guard.clone())
328 .unwrap_or_default();
329
330 let input_tokens = self
331 .total_input_tokens
332 .lock()
333 .map(|guard| *guard)
334 .unwrap_or(0);
335
336 let output_tokens = self
337 .total_output_tokens
338 .lock()
339 .map(|guard| *guard)
340 .unwrap_or(0);
341
342 tracing::debug!(
343 "Saving session {} with {} messages, {} in tokens, {} out tokens",
344 session_id,
345 messages.len(),
346 input_tokens,
347 output_tokens
348 );
349
350 let session_manager = self.session_manager.lock().map_err(|e| {
351 CliError::ConfigError(format!("Failed to acquire session manager lock: {}", e))
352 })?;
353
354 session_manager.save_session(&session_id, &messages, input_tokens, output_tokens)?;
355 tracing::info!(
356 "✓ Session {} saved successfully ({} messages, {} in tokens, {} out tokens)",
357 session_id,
358 messages.len(),
359 input_tokens,
360 output_tokens
361 );
362 Ok(())
363 }
364
365 pub fn session_manager(&self) -> Arc<Mutex<SessionManager>> {
367 self.session_manager.clone()
368 }
369
370 pub fn messages(&self) -> Arc<Mutex<Vec<limit_llm::Message>>> {
372 self.messages.clone()
373 }
374
375 pub fn state_arc(&self) -> Arc<Mutex<TuiState>> {
377 self.state.clone()
378 }
379
380 pub fn total_input_tokens_arc(&self) -> Arc<Mutex<u64>> {
382 self.total_input_tokens.clone()
383 }
384
385 pub fn total_output_tokens_arc(&self) -> Arc<Mutex<u64>> {
387 self.total_output_tokens.clone()
388 }
389
390 pub fn session_id_arc(&self) -> Arc<Mutex<String>> {
392 self.session_id.clone()
393 }
394
395 pub fn set_state(&self, new_state: TuiState) {
397 *self.state.lock().unwrap() = new_state;
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 fn create_test_config() -> limit_llm::Config {
407 use limit_llm::ProviderConfig;
408 let mut providers = std::collections::HashMap::new();
409 providers.insert(
410 "anthropic".to_string(),
411 ProviderConfig {
412 api_key: Some("test-key".to_string()),
413 model: "claude-3-5-sonnet-20241022".to_string(),
414 base_url: None,
415 max_tokens: 4096,
416 timeout: 60,
417 max_iterations: 100,
418 thinking_enabled: false,
419 clear_thinking: true,
420 },
421 );
422 limit_llm::Config {
423 provider: "anthropic".to_string(),
424 providers,
425 }
426 }
427
428 #[test]
429 fn test_tui_bridge_new() {
430 let config = create_test_config();
431 let agent_bridge = AgentBridge::new(config).unwrap();
432 let (_tx, rx) = mpsc::unbounded_channel();
433
434 let tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
435 assert_eq!(tui_bridge.state(), TuiState::Idle);
436 }
437
438 #[test]
439 fn test_tui_bridge_state() {
440 let config = create_test_config();
441 let agent_bridge = AgentBridge::new(config).unwrap();
442 let (tx, rx) = mpsc::unbounded_channel();
443
444 let mut tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
445
446 let op_id = tui_bridge.operation_id();
447 tx.send(AgentEvent::Thinking {
448 operation_id: op_id,
449 })
450 .unwrap();
451 tui_bridge.process_events().unwrap();
452 assert!(matches!(tui_bridge.state(), TuiState::Thinking));
453
454 tx.send(AgentEvent::Done {
455 operation_id: op_id,
456 })
457 .unwrap();
458 tui_bridge.process_events().unwrap();
459 assert_eq!(tui_bridge.state(), TuiState::Idle);
460 }
461
462 #[test]
463 fn test_tui_bridge_chat_view() {
464 let config = create_test_config();
465 let agent_bridge = AgentBridge::new(config).unwrap();
466 let (_tx, rx) = mpsc::unbounded_channel();
467
468 let tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
469
470 tui_bridge.add_user_message("Hello".to_string());
471 assert_eq!(tui_bridge.chat_view().lock().unwrap().message_count(), 3); }
473}