1use crate::agent_bridge::{AgentBridge, AgentEvent};
2use crate::clipboard::ClipboardManager;
3use crate::error::CliError;
4use crate::session::SessionManager;
5use crossterm::event::{
6 self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
7 Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind,
8};
9use crossterm::execute;
10use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
11use limit_tui::components::{ActivityFeed, ChatView, Message, Spinner};
12use ratatui::{
13 backend::CrosstermBackend,
14 layout::{Constraint, Direction, Layout},
15 style::{Color, Modifier, Style},
16 text::{Line, Span},
17 widgets::{Block, Borders, Paragraph, Wrap},
18 Frame, Terminal,
19};
20use std::io;
21use std::sync::{Arc, Mutex};
22use tokio::sync::mpsc;
23
24const MAX_PASTE_SIZE: usize = 100 * 1024;
26
27fn debug_log(msg: &str) {
29 use std::fs::OpenOptions;
30 use std::io::Write;
31 if let Ok(mut file) = OpenOptions::new()
32 .create(true)
33 .append(true)
34 .open(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()) + "/.limit/logs/tui.log")
35 {
36 let timestamp = chrono::Local::now().format("%H:%M:%S%.3f");
37 let _ = writeln!(file, "[{}] {}", timestamp, msg);
38 }
39}
40#[derive(Debug, Clone, PartialEq, Default)]
42pub enum TuiState {
43 #[default]
44 Idle,
45 Thinking,
46}
47
48pub struct TuiBridge {
50 agent_bridge: Arc<Mutex<AgentBridge>>,
52 event_rx: mpsc::UnboundedReceiver<AgentEvent>,
54 state: Arc<Mutex<TuiState>>,
56 chat_view: Arc<Mutex<ChatView>>,
58 activity_feed: Arc<Mutex<ActivityFeed>>,
60 spinner: Arc<Mutex<Spinner>>,
63 messages: Arc<Mutex<Vec<limit_llm::Message>>>,
65 total_input_tokens: Arc<Mutex<u64>>,
67 total_output_tokens: Arc<Mutex<u64>>,
69 session_manager: Arc<Mutex<SessionManager>>,
71 session_id: Arc<Mutex<String>>,
73}
74
75impl TuiBridge {
76 pub fn new(
78 agent_bridge: AgentBridge,
79 event_rx: mpsc::UnboundedReceiver<AgentEvent>,
80 ) -> Result<Self, CliError> {
81 let session_manager = SessionManager::new().map_err(|e| {
82 CliError::ConfigError(format!("Failed to create session manager: {}", e))
83 })?;
84
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 })
151 }
152
153 pub fn agent_bridge_arc(&self) -> Arc<Mutex<AgentBridge>> {
155 self.agent_bridge.clone()
156 }
157
158 #[allow(dead_code)]
160 pub fn agent_bridge(&self) -> std::sync::MutexGuard<'_, AgentBridge> {
161 self.agent_bridge.lock().unwrap()
162 }
163
164 pub fn state(&self) -> TuiState {
166 self.state.lock().unwrap().clone()
167 }
168
169 pub fn chat_view(&self) -> &Arc<Mutex<ChatView>> {
171 &self.chat_view
172 }
173
174 pub fn spinner(&self) -> &Arc<Mutex<Spinner>> {
176 &self.spinner
177 }
178
179 pub fn activity_feed(&self) -> &Arc<Mutex<ActivityFeed>> {
181 &self.activity_feed
182 }
183
184 pub fn process_events(&mut self) -> Result<(), CliError> {
186 while let Ok(event) = self.event_rx.try_recv() {
187 match event {
188 AgentEvent::Thinking => {
189 *self.state.lock().unwrap() = TuiState::Thinking;
190 }
191 AgentEvent::ToolStart { name, args } => {
192 let activity_msg = Self::format_activity_message(&name, &args);
193 self.activity_feed.lock().unwrap().add(activity_msg, true);
195 }
196 AgentEvent::ToolComplete { name: _, result: _ } => {
197 self.activity_feed.lock().unwrap().complete_current();
199 }
200 AgentEvent::ContentChunk(chunk) => {
201 self.chat_view
202 .lock()
203 .unwrap()
204 .append_to_last_assistant(&chunk);
205 }
206 AgentEvent::Done => {
207 *self.state.lock().unwrap() = TuiState::Idle;
208 self.activity_feed.lock().unwrap().complete_all();
210 }
211 AgentEvent::Error(err) => {
212 *self.state.lock().unwrap() = TuiState::Idle;
214 let chat_msg = Message::system(format!("Error: {}", err));
215 self.chat_view.lock().unwrap().add_message(chat_msg);
216 }
217 AgentEvent::TokenUsage {
218 input_tokens,
219 output_tokens,
220 } => {
221 *self.total_input_tokens.lock().unwrap() += input_tokens;
223 *self.total_output_tokens.lock().unwrap() += output_tokens;
224 }
225 }
226 }
227 Ok(())
228 }
229
230 fn format_activity_message(tool_name: &str, args: &serde_json::Value) -> String {
231 match tool_name {
232 "file_read" => args
233 .get("path")
234 .and_then(|p| p.as_str())
235 .map(|p| format!("Reading {}...", Self::truncate_path(p, 40)))
236 .unwrap_or_else(|| "Reading file...".to_string()),
237 "file_write" => args
238 .get("path")
239 .and_then(|p| p.as_str())
240 .map(|p| format!("Writing {}...", Self::truncate_path(p, 40)))
241 .unwrap_or_else(|| "Writing file...".to_string()),
242 "file_edit" => args
243 .get("path")
244 .and_then(|p| p.as_str())
245 .map(|p| format!("Editing {}...", Self::truncate_path(p, 40)))
246 .unwrap_or_else(|| "Editing file...".to_string()),
247 "bash" => args
248 .get("command")
249 .and_then(|c| c.as_str())
250 .map(|c| format!("Running {}...", Self::truncate_command(c, 30)))
251 .unwrap_or_else(|| "Executing command...".to_string()),
252 "git_status" => "Checking git status...".to_string(),
253 "git_diff" => "Checking git diff...".to_string(),
254 "git_log" => "Checking git log...".to_string(),
255 "git_add" => "Staging files...".to_string(),
256 "git_commit" => "Creating commit...".to_string(),
257 "git_push" => "Pushing to remote...".to_string(),
258 "git_pull" => "Pulling from remote...".to_string(),
259 "git_clone" => args
260 .get("url")
261 .and_then(|u| u.as_str())
262 .map(|u| format!("Cloning {}...", Self::truncate_path(u, 40)))
263 .unwrap_or_else(|| "Cloning repository...".to_string()),
264 "grep" => args
265 .get("pattern")
266 .and_then(|p| p.as_str())
267 .map(|p| format!("Searching for '{}'...", Self::truncate_command(p, 30)))
268 .unwrap_or_else(|| "Searching...".to_string()),
269 "ast_grep" => args
270 .get("pattern")
271 .and_then(|p| p.as_str())
272 .map(|p| format!("AST searching '{}'...", Self::truncate_command(p, 25)))
273 .unwrap_or_else(|| "AST searching...".to_string()),
274 "lsp" => args
275 .get("command")
276 .and_then(|c| c.as_str())
277 .map(|c| format!("Running LSP {}...", c))
278 .unwrap_or_else(|| "Running LSP...".to_string()),
279 _ => format!("Executing {}...", tool_name),
280 }
281 }
282
283 fn truncate_path(s: &str, max_len: usize) -> String {
284 if s.len() <= max_len {
285 s.to_string()
286 } else {
287 format!("...{}", &s[s.len().saturating_sub(max_len - 3)..])
288 }
289 }
290
291 fn truncate_command(s: &str, max_len: usize) -> String {
292 if s.len() <= max_len {
293 s.to_string()
294 } else {
295 format!("{}...", &s[..max_len.saturating_sub(3)])
296 }
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 pub fn total_input_tokens(&self) -> u64 {
317 self.total_input_tokens
318 .lock()
319 .map(|guard| *guard)
320 .unwrap_or(0)
321 }
322
323 pub fn total_output_tokens(&self) -> u64 {
325 self.total_output_tokens
326 .lock()
327 .map(|guard| *guard)
328 .unwrap_or(0)
329 }
330
331 pub fn session_id(&self) -> String {
333 self.session_id
334 .lock()
335 .map(|guard| guard.clone())
336 .unwrap_or_else(|_| String::from("unknown"))
337 }
338
339 pub fn save_session(&self) -> Result<(), CliError> {
341 let session_id = self
342 .session_id
343 .lock()
344 .map(|guard| guard.clone())
345 .unwrap_or_else(|_| String::from("unknown"));
346
347 let messages = self
348 .messages
349 .lock()
350 .map(|guard| guard.clone())
351 .unwrap_or_default();
352
353 let input_tokens = self
354 .total_input_tokens
355 .lock()
356 .map(|guard| *guard)
357 .unwrap_or(0);
358
359 let output_tokens = self
360 .total_output_tokens
361 .lock()
362 .map(|guard| *guard)
363 .unwrap_or(0);
364
365 tracing::debug!(
366 "Saving session {} with {} messages, {} in tokens, {} out tokens",
367 session_id,
368 messages.len(),
369 input_tokens,
370 output_tokens
371 );
372
373 let session_manager = self.session_manager.lock().map_err(|e| {
374 CliError::ConfigError(format!("Failed to acquire session manager lock: {}", e))
375 })?;
376
377 session_manager.save_session(&session_id, &messages, input_tokens, output_tokens)?;
378 tracing::info!(
379 "✓ Session {} saved successfully ({} messages, {} in tokens, {} out tokens)",
380 session_id,
381 messages.len(),
382 input_tokens,
383 output_tokens
384 );
385 Ok(())
386 }
387}
388
389pub struct TuiApp {
391 tui_bridge: TuiBridge,
392 terminal: Terminal<CrosstermBackend<io::Stdout>>,
393 running: bool,
394 input_text: String,
395 cursor_pos: usize,
396 status_message: String,
397 status_is_error: bool,
398 cursor_blink_state: bool,
399 cursor_blink_timer: std::time::Instant,
400 mouse_selection_start: Option<(u16, u16)>,
402 clipboard: Option<ClipboardManager>,
404}
405
406impl TuiApp {
407 pub fn new(tui_bridge: TuiBridge) -> Result<Self, CliError> {
409 let backend = CrosstermBackend::new(io::stdout());
410 let terminal =
411 Terminal::new(backend).map_err(|e| CliError::IoError(io::Error::other(e)))?;
412
413 let session_id = tui_bridge.session_id();
414 tracing::info!("TUI started with session: {}", session_id);
415
416 let clipboard = match ClipboardManager::new() {
417 Ok(cb) => {
418 debug_log("✓ Clipboard initialized successfully");
419 tracing::info!("Clipboard initialized successfully");
420 Some(cb)
421 }
422 Err(e) => {
423 debug_log(&format!("✗ Clipboard initialization failed: {}", e));
424 tracing::warn!("Clipboard unavailable: {}", e);
425 None
426 }
427 };
428
429 Ok(Self {
430 tui_bridge,
431 terminal,
432 running: true,
433 input_text: String::new(),
434 cursor_pos: 0,
435 status_message: "Ready - Type a message and press Enter".to_string(),
436 status_is_error: false,
437 cursor_blink_state: true,
438 cursor_blink_timer: std::time::Instant::now(),
439 mouse_selection_start: None,
440 clipboard,
441 })
442 }
443
444 pub fn run(&mut self) -> Result<(), CliError> {
446 execute!(std::io::stdout(), EnterAlternateScreen)
448 .map_err(|e| CliError::IoError(io::Error::other(e)))?;
449
450 execute!(std::io::stdout(), EnableMouseCapture)
452 .map_err(|e| CliError::IoError(io::Error::other(e)))?;
453
454 execute!(std::io::stdout(), EnableBracketedPaste)
456 .map_err(|e| CliError::IoError(io::Error::other(e)))?;
457
458 crossterm::terminal::enable_raw_mode()
459 .map_err(|e| CliError::IoError(io::Error::other(e)))?;
460
461 struct AlternateScreenGuard;
463 impl Drop for AlternateScreenGuard {
464 fn drop(&mut self) {
465 let _ = crossterm::terminal::disable_raw_mode();
466 let _ = execute!(std::io::stdout(), DisableBracketedPaste);
467 let _ = execute!(std::io::stdout(), DisableMouseCapture);
468 let _ = execute!(std::io::stdout(), LeaveAlternateScreen);
469 }
470 }
471 let _guard = AlternateScreenGuard;
472
473 self.run_inner()
474 }
475
476 fn run_inner(&mut self) -> Result<(), CliError> {
477 while self.running {
478 self.tui_bridge.process_events()?;
480
481 if matches!(self.tui_bridge.state(), TuiState::Thinking) {
483 self.tui_bridge.tick_spinner();
484 }
485
486 self.update_status();
488
489 if crossterm::event::poll(std::time::Duration::from_millis(100))
491 .map_err(|e| CliError::IoError(io::Error::other(e)))?
492 {
493 match event::read().map_err(|e| CliError::IoError(io::Error::other(e)))? {
494 Event::Key(key) => {
495 debug_log(&format!(
497 "Event::Key - code={:?} mod={:?} kind={:?}",
498 key.code, key.modifiers, key.kind
499 ));
500
501 if key.kind == KeyEventKind::Press {
502 self.handle_key_event(key)?;
503 }
504 }
505 Event::Mouse(mouse) => {
506 match mouse.kind {
507 MouseEventKind::Down(MouseButton::Left) => {
508 debug_log(&format!(
509 "MouseDown at ({}, {})",
510 mouse.column, mouse.row
511 ));
512 self.mouse_selection_start = Some((mouse.column, mouse.row));
513 let chat = self.tui_bridge.chat_view().lock().unwrap();
515 debug_log(&format!(
516 " render_positions count: {}",
517 chat.render_position_count()
518 ));
519 if let Some((msg_idx, char_offset)) =
520 chat.screen_to_text_pos(mouse.column, mouse.row)
521 {
522 debug_log(&format!(
523 " -> Starting selection at msg={}, offset={}",
524 msg_idx, char_offset
525 ));
526 drop(chat);
527 self.tui_bridge
528 .chat_view()
529 .lock()
530 .unwrap()
531 .start_selection(msg_idx, char_offset);
532 } else {
533 debug_log(" -> No match, clearing selection");
534 drop(chat);
535 self.tui_bridge
536 .chat_view()
537 .lock()
538 .unwrap()
539 .clear_selection();
540 }
541 }
542 MouseEventKind::Drag(MouseButton::Left) => {
543 if self.mouse_selection_start.is_some() {
544 let chat = self.tui_bridge.chat_view().lock().unwrap();
546 if let Some((msg_idx, char_offset)) =
547 chat.screen_to_text_pos(mouse.column, mouse.row)
548 {
549 drop(chat);
550 self.tui_bridge
551 .chat_view()
552 .lock()
553 .unwrap()
554 .extend_selection(msg_idx, char_offset);
555 }
556 }
557 }
558 MouseEventKind::Up(MouseButton::Left) => {
559 self.mouse_selection_start = None;
560 }
561 MouseEventKind::ScrollUp => {
562 let mut chat = self.tui_bridge.chat_view().lock().unwrap();
563 chat.scroll_up();
564 }
565 MouseEventKind::ScrollDown => {
566 let mut chat = self.tui_bridge.chat_view().lock().unwrap();
567 chat.scroll_down();
568 }
569 _ => {}
570 }
571 }
572 Event::Paste(pasted) => {
573 if !self.tui_bridge.is_busy() {
574 self.insert_paste(&pasted);
575 }
576 }
577 _ => {}
578 }
579 } else {
580 self.tick_cursor_blink();
582 }
583
584 self.draw()?;
586 }
587
588 if let Err(e) = self.tui_bridge.save_session() {
590 tracing::error!("Failed to save session: {}", e);
591 }
592
593 Ok(())
594 }
595
596 fn update_status(&mut self) {
597 let session_id = self.tui_bridge.session_id();
598 let has_activity = self
599 .tui_bridge
600 .activity_feed()
601 .lock()
602 .unwrap()
603 .has_in_progress();
604
605 match self.tui_bridge.state() {
606 TuiState::Idle => {
607 if has_activity {
608 let spinner = self.tui_bridge.spinner().lock().unwrap();
610 self.status_message = format!("{} Processing...", spinner.current_frame());
611 } else {
612 self.status_message = format!(
613 "Ready | Session: {}",
614 session_id.chars().take(8).collect::<String>()
615 );
616 }
617 self.status_is_error = false;
618 }
619 TuiState::Thinking => {
620 let spinner = self.tui_bridge.spinner().lock().unwrap();
621 self.status_message = format!("{} Thinking...", spinner.current_frame());
622 self.status_is_error = false;
623 }
624 }
625 }
626
627 fn insert_paste(&mut self, text: &str) {
629 let text = if text.len() > MAX_PASTE_SIZE {
631 self.status_message = "Paste truncated (too large)".to_string();
632 self.status_is_error = true;
633 &text[..text
635 .char_indices()
636 .nth(MAX_PASTE_SIZE)
637 .map(|(i, _)| i)
638 .unwrap_or(text.len())]
639 } else {
640 text
641 };
642
643 let normalized = text.replace("\r", "\n");
645 self.input_text.insert_str(self.cursor_pos, &normalized);
646 self.cursor_pos += normalized.len();
647 }
648
649 fn is_copy_paste_modifier(&self, key: &KeyEvent, char: char) -> bool {
653 #[cfg(target_os = "macos")]
654 {
655 let has_super = key.modifiers.contains(KeyModifiers::SUPER);
657 let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
658 let result = key.code == KeyCode::Char(char) && (has_super || has_ctrl);
659 debug_log(&format!("is_copy_paste_modifier('{}') macOS: code={:?}, mod={:?}, super={}, ctrl={}, result={}",
660 char, key.code, key.modifiers, has_super, has_ctrl, result));
661 result
662 }
663 #[cfg(not(target_os = "macos"))]
664 {
665 let result =
666 key.code == KeyCode::Char(char) && key.modifiers.contains(KeyModifiers::CONTROL);
667 debug_log(&format!(
668 "is_copy_paste_modifier('{}') non-macOS: code={:?}, mod={:?}, ctrl={:?}, result={}",
669 char,
670 key.code,
671 key.modifiers,
672 KeyModifiers::CONTROL,
673 result
674 ));
675 result
676 }
677 }
678
679 fn tick_cursor_blink(&mut self) {
680 if self.cursor_blink_timer.elapsed().as_millis() > 500 {
682 self.cursor_blink_state = !self.cursor_blink_state;
683 self.cursor_blink_timer = std::time::Instant::now();
684 }
685 }
686
687 fn handle_key_event(&mut self, key: KeyEvent) -> Result<(), CliError> {
688 debug_log(&format!(
690 "handle_key_event: code={:?} mod={:?} kind={:?}",
691 key.code, key.modifiers, key.kind
692 ));
693
694 if matches!(key.code, KeyCode::Char('c') | KeyCode::Char('v')) {
696 debug_log(&format!(
697 ">>> SPECIAL: '{}' key detected with modifiers: {:?} (SUPER={:?}, CONTROL={:?})",
698 if matches!(key.code, KeyCode::Char('c')) {
699 'c'
700 } else {
701 'v'
702 },
703 key.modifiers,
704 key.modifiers.contains(KeyModifiers::SUPER),
705 key.modifiers.contains(KeyModifiers::CONTROL)
706 ));
707 }
708
709 if self.is_copy_paste_modifier(&key, 'c') {
711 debug_log("✓ Copy shortcut CONFIRMED - processing...");
712 let mut chat = self.tui_bridge.chat_view().lock().unwrap();
713 let has_selection = chat.has_selection();
714 debug_log(&format!("has_selection={}", has_selection));
715
716 if has_selection {
717 if let Some(selected) = chat.get_selected_text() {
718 debug_log(&format!("Selected text length={}", selected.len()));
719 if !selected.is_empty() {
720 if let Some(ref clipboard) = self.clipboard {
721 debug_log("Attempting to copy to clipboard...");
722 match clipboard.set_text(&selected) {
723 Ok(()) => {
724 debug_log("✓ Clipboard copy successful");
725 self.status_message = "Copied to clipboard".to_string();
726 self.status_is_error = false;
727 }
728 Err(e) => {
729 debug_log(&format!("✗ Clipboard copy failed: {}", e));
730 self.status_message = format!("Clipboard error: {}", e);
731 self.status_is_error = true;
732 }
733 }
734 } else {
735 debug_log("✗ Clipboard not available (None)");
736 self.status_message = "Clipboard not available".to_string();
737 self.status_is_error = true;
738 }
739 } else {
740 debug_log("Selected text is empty");
741 }
742 chat.clear_selection();
743 } else {
744 debug_log("get_selected_text() returned None");
745 }
746 return Ok(());
747 }
748
749 debug_log("Ctrl/Cmd+C with no selection - ignoring");
751 return Ok(());
752 }
753
754 if self.is_copy_paste_modifier(&key, 'v') && !self.tui_bridge.is_busy() {
756 debug_log("✓ Paste shortcut CONFIRMED - processing...");
757 if let Some(ref clipboard) = self.clipboard {
758 debug_log("Attempting to read from clipboard...");
759 match clipboard.get_text() {
760 Ok(text) if !text.is_empty() => {
761 debug_log(&format!("Read {} chars from clipboard", text.len()));
762 self.insert_paste(&text);
763 }
764 Ok(_) => {
765 debug_log("Clipboard is empty");
766 } Err(e) => {
768 debug_log(&format!("✗ Failed to read clipboard: {}", e));
769 self.status_message = format!("Could not read clipboard: {}", e);
770 self.status_is_error = true;
771 }
772 }
773 } else {
774 debug_log("✗ Clipboard not available (None)");
775 self.status_message = "Clipboard not available".to_string();
776 self.status_is_error = true;
777 }
778 return Ok(());
779 }
780
781 let term_height = self.terminal.size().map(|s| s.height).unwrap_or(24);
784 let viewport_height = term_height
785 .saturating_sub(1) .saturating_sub(7); match key.code {
788 KeyCode::PageUp => {
789 let mut chat = self.tui_bridge.chat_view().lock().unwrap();
790 chat.scroll_page_up(viewport_height);
791 return Ok(());
792 }
793 KeyCode::PageDown => {
794 let mut chat = self.tui_bridge.chat_view().lock().unwrap();
795 chat.scroll_page_down(viewport_height);
796 return Ok(());
797 }
798 KeyCode::Up => {
799 let mut chat = self.tui_bridge.chat_view().lock().unwrap();
800 chat.scroll_up();
801 return Ok(());
802 }
803 KeyCode::Down => {
804 let mut chat = self.tui_bridge.chat_view().lock().unwrap();
805 chat.scroll_down();
806 return Ok(());
807 }
808 _ => {}
809 }
810
811 if self.tui_bridge.is_busy() {
813 debug_log("Agent busy, ignoring");
814 return Ok(());
815 }
816
817 if self.handle_backspace(&key) {
819 debug_log(&format!("Backspace handled, input: {:?}", self.input_text));
820 return Ok(());
821 }
822
823 match key.code {
824 KeyCode::Delete => {
825 if self.cursor_pos < self.input_text.len() {
826 let next_pos = self.next_char_pos();
827 self.input_text.drain(self.cursor_pos..next_pos);
828 debug_log(&format!("Delete: input now: {:?}", self.input_text));
829 }
830 }
831 KeyCode::Left => {
832 if self.cursor_pos > 0 {
833 self.cursor_pos = self.prev_char_pos();
834 }
835 }
836 KeyCode::Right => {
837 if self.cursor_pos < self.input_text.len() {
838 self.cursor_pos = self.next_char_pos();
839 }
840 }
841 KeyCode::Home => {
842 self.cursor_pos = 0;
843 }
844 KeyCode::End => {
845 self.cursor_pos = self.input_text.len();
846 }
847 KeyCode::Enter => {
848 self.handle_enter()?;
849 }
850 KeyCode::Esc => {
851 debug_log("Esc pressed, exiting");
852 self.running = false;
853 }
854 KeyCode::Char(c)
856 if key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT =>
857 {
858 self.input_text.insert(self.cursor_pos, c);
860 self.cursor_pos += c.len_utf8();
861 }
862 _ => {
863 }
865 }
866
867 Ok(())
868 }
869
870 fn handle_backspace(&mut self, key: &KeyEvent) -> bool {
872 if key.code == KeyCode::Backspace {
874 debug_log("Backspace detected via KeyCode::Backspace");
875 self.delete_char_before_cursor();
876 return true;
877 }
878
879 if key.code == KeyCode::Char('h') && key.modifiers == KeyModifiers::CONTROL {
881 debug_log("Backspace detected via Ctrl+H");
882 self.delete_char_before_cursor();
883 return true;
884 }
885
886 if let KeyCode::Char(c) = key.code {
888 if c == '\x7f' || c == '\x08' {
889 debug_log(&format!("Backspace detected via char code: {}", c as u8));
890 self.delete_char_before_cursor();
891 return true;
892 }
893 }
894
895 false
896 }
897
898 fn delete_char_before_cursor(&mut self) {
899 debug_log(&format!(
900 "delete_char: cursor={}, len={}, input={:?}",
901 self.cursor_pos,
902 self.input_text.len(),
903 self.input_text
904 ));
905 if self.cursor_pos > 0 {
906 let prev_pos = self.prev_char_pos();
907 debug_log(&format!("draining {}..{}", prev_pos, self.cursor_pos));
908 self.input_text.drain(prev_pos..self.cursor_pos);
909 self.cursor_pos = prev_pos;
910 debug_log(&format!(
911 "after delete: cursor={}, input={:?}",
912 self.cursor_pos, self.input_text
913 ));
914 } else {
915 debug_log("cursor at 0, nothing to delete");
916 }
917 }
918
919 fn handle_enter(&mut self) -> Result<(), CliError> {
920 let text = self.input_text.trim().to_string();
921
922 self.input_text.clear();
924 self.cursor_pos = 0;
925
926 if text.is_empty() {
927 return Ok(());
928 }
929
930 tracing::info!("Enter pressed with text: {:?}", text);
931
932 let text_lower = text.to_lowercase();
934 if text_lower == "/exit"
935 || text_lower == "/quit"
936 || text_lower == "exit"
937 || text_lower == "quit"
938 {
939 tracing::info!("Exit command detected, exiting");
940 self.running = false;
941 return Ok(());
942 }
943
944 if text_lower == "/clear" || text_lower == "clear" {
945 tracing::info!("Clear command detected");
946 self.tui_bridge.chat_view().lock().unwrap().clear();
947 return Ok(());
948 }
949
950 if text_lower == "/help" || text_lower == "help" {
951 tracing::info!("Help command detected");
952 let help_msg = Message::system(
953 "Available commands:\n\
954 /help - Show this help message\n\
955 /clear - Clear chat history\n\
956 /exit - Exit the application\n\
957 /quit - Exit the application\n\
958 /session list - List all sessions\n\
959 /session new - Create a new session\n\
960 /session load <id> - Load a session by ID\n\
961 \n\
962 Page Up/Down - Scroll chat history"
963 .to_string(),
964 );
965 self.tui_bridge
966 .chat_view()
967 .lock()
968 .unwrap()
969 .add_message(help_msg);
970 return Ok(());
971 }
972
973 if text_lower.starts_with("/session ") {
975 let session_cmd = text.strip_prefix("/session ").unwrap();
976 if session_cmd.trim() == "list" {
977 self.handle_session_list()?;
978 return Ok(());
979 } else if session_cmd.trim() == "new" {
980 self.handle_session_new()?;
981 return Ok(());
982 } else if session_cmd.starts_with("load ") {
983 let session_id = session_cmd.strip_prefix("load ").unwrap().trim();
984 self.handle_session_load(session_id)?;
985 return Ok(());
986 } else {
987 let error_msg = Message::system(
988 "Usage: /session list, /session new, /session load <id>".to_string(),
989 );
990 self.tui_bridge
991 .chat_view()
992 .lock()
993 .unwrap()
994 .add_message(error_msg);
995 return Ok(());
996 }
997 }
998
999 self.tui_bridge.add_user_message(text.clone());
1001
1002 let messages = self.tui_bridge.messages.clone();
1004 let agent_bridge = self.tui_bridge.agent_bridge_arc();
1005 let session_manager = self.tui_bridge.session_manager.clone();
1006 let session_id = self.tui_bridge.session_id();
1007 let total_input_tokens = self.tui_bridge.total_input_tokens.clone();
1008 let total_output_tokens = self.tui_bridge.total_output_tokens.clone();
1009
1010 tracing::debug!("Spawning LLM processing thread");
1011
1012 std::thread::spawn(move || {
1014 let rt = tokio::runtime::Runtime::new().unwrap();
1016
1017 #[allow(clippy::await_holding_lock)]
1019 rt.block_on(async {
1020 let mut messages_guard = messages.lock().unwrap();
1021 let mut bridge = agent_bridge.lock().unwrap();
1022
1023 match bridge.process_message(&text, &mut messages_guard).await {
1024 Ok(_response) => {
1025 let msgs = messages_guard.clone();
1029 let input_tokens = *total_input_tokens.lock().unwrap();
1030 let output_tokens = *total_output_tokens.lock().unwrap();
1031
1032 if let Err(e) = session_manager.lock().unwrap().save_session(
1033 &session_id,
1034 &msgs,
1035 input_tokens,
1036 output_tokens,
1037 ) {
1038 tracing::error!("✗ Failed to auto-save session {}: {}", session_id, e);
1039 } else {
1040 tracing::info!(
1041 "✓ Session {} auto-saved ({} messages, {} in, {} out tokens)",
1042 session_id,
1043 msgs.len(),
1044 input_tokens,
1045 output_tokens
1046 );
1047 }
1048 }
1049 Err(e) => {
1050 tracing::error!("LLM error: {}", e);
1051 }
1052 }
1053 });
1054 });
1055
1056 Ok(())
1057 }
1058
1059 fn handle_session_list(&self) -> Result<(), CliError> {
1061 tracing::info!("Session list command detected");
1062 let session_manager = self.tui_bridge.session_manager.lock().unwrap();
1063 let current_session_id = self.tui_bridge.session_id();
1064
1065 match session_manager.list_sessions() {
1066 Ok(sessions) => {
1067 if sessions.is_empty() {
1068 let msg = Message::system("No sessions found.".to_string());
1069 self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1070 } else {
1071 let mut output = vec!["Sessions (most recent first):".to_string()];
1072 for (i, session) in sessions.iter().enumerate() {
1073 let current = if session.id == current_session_id {
1074 " (current)"
1075 } else {
1076 ""
1077 };
1078 let short_id = if session.id.len() > 8 {
1079 &session.id[..8]
1080 } else {
1081 &session.id
1082 };
1083 output.push(format!(
1084 " {}. {}{} - {} messages, {} in tokens, {} out tokens",
1085 i + 1,
1086 short_id,
1087 current,
1088 session.message_count,
1089 session.total_input_tokens,
1090 session.total_output_tokens
1091 ));
1092 }
1093 let msg = Message::system(output.join("\n"));
1094 self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1095 }
1096 }
1097 Err(e) => {
1098 let msg = Message::system(format!("Error listing sessions: {}", e));
1099 self.tui_bridge.chat_view().lock().unwrap().add_message(msg);
1100 }
1101 }
1102 Ok(())
1103 }
1104
1105 fn handle_session_new(&mut self) -> Result<(), CliError> {
1107 tracing::info!("Session new command detected");
1108
1109 let save_result = self.tui_bridge.save_session();
1111 if let Err(e) = &save_result {
1112 tracing::error!("Failed to save current session: {}", e);
1113 let msg = Message::system(format!("⚠ Warning: Failed to save current session: {}", e));
1114 if let Ok(mut chat) = self.tui_bridge.chat_view.try_lock() {
1115 chat.add_message(msg);
1116 }
1117 }
1118
1119 let new_session_id = {
1121 let session_manager = self.tui_bridge.session_manager.lock().map_err(|e| {
1122 CliError::ConfigError(format!("Failed to acquire session manager lock: {}", e))
1123 })?;
1124
1125 session_manager
1126 .create_new_session()
1127 .map_err(|e| CliError::ConfigError(format!("Failed to create session: {}", e)))?
1128 };
1129
1130 let old_session_id = self.tui_bridge.session_id();
1131
1132 if let Ok(mut id_guard) = self.tui_bridge.session_id.try_lock() {
1134 *id_guard = new_session_id.clone();
1135 }
1136
1137 if let Ok(mut messages_guard) = self.tui_bridge.messages.try_lock() {
1139 messages_guard.clear();
1140 }
1141 if let Ok(mut input_guard) = self.tui_bridge.total_input_tokens.try_lock() {
1142 *input_guard = 0;
1143 }
1144 if let Ok(mut output_guard) = self.tui_bridge.total_output_tokens.try_lock() {
1145 *output_guard = 0;
1146 }
1147
1148 tracing::info!(
1149 "Created new session: {} (old: {})",
1150 new_session_id,
1151 old_session_id
1152 );
1153
1154 let session_short_id = if new_session_id.len() > 8 {
1156 &new_session_id[new_session_id.len().saturating_sub(8)..]
1157 } else {
1158 &new_session_id
1159 };
1160 let msg = Message::system(format!("🆕 New session created: {}", session_short_id));
1161
1162 if let Ok(mut chat) = self.tui_bridge.chat_view.try_lock() {
1163 chat.add_message(msg);
1164 }
1165
1166 Ok(())
1167 }
1168
1169 fn handle_session_load(&mut self, session_id: &str) -> Result<(), CliError> {
1171 tracing::info!("Session load command detected for session: {}", session_id);
1172
1173 let save_result = self.tui_bridge.save_session();
1175 if let Err(e) = &save_result {
1176 tracing::error!("Failed to save current session: {}", e);
1177 let msg = Message::system(format!("⚠ Warning: Failed to save current session: {}", e));
1178 if let Ok(mut chat) = self.tui_bridge.chat_view.try_lock() {
1179 chat.add_message(msg);
1180 }
1181 }
1182
1183 let (full_session_id, session_info, messages) = {
1185 let session_manager = self.tui_bridge.session_manager.lock().map_err(|e| {
1186 CliError::ConfigError(format!("Failed to acquire session manager lock: {}", e))
1187 })?;
1188
1189 let sessions = session_manager
1190 .list_sessions()
1191 .map_err(|e| CliError::ConfigError(format!("Failed to list sessions: {}", e)))?;
1192
1193 let matched_session = if session_id.len() >= 8 {
1194 sessions
1196 .iter()
1197 .find(|s| s.id == session_id)
1198 .or_else(|| sessions.iter().find(|s| s.id.starts_with(session_id)))
1200 } else {
1201 sessions.iter().find(|s| s.id.starts_with(session_id))
1203 };
1204
1205 match matched_session {
1206 Some(info) => {
1207 let full_id = info.id.clone();
1208 let msgs = session_manager.load_session(&full_id).map_err(|e| {
1210 CliError::ConfigError(format!("Failed to load session {}: {}", full_id, e))
1211 })?;
1212 (full_id, info.clone(), msgs)
1213 }
1214 None => {
1215 let msg = Message::system(format!("❌ Session not found: {}", session_id));
1216 if let Ok(mut chat) = self.tui_bridge.chat_view.try_lock() {
1217 chat.add_message(msg);
1218 }
1219 return Ok(());
1220 }
1221 }
1222 };
1223
1224 if let Ok(mut id_guard) = self.tui_bridge.session_id.try_lock() {
1226 *id_guard = full_session_id.clone();
1227 }
1228 if let Ok(mut input_guard) = self.tui_bridge.total_input_tokens.try_lock() {
1229 *input_guard = session_info.total_input_tokens;
1230 }
1231 if let Ok(mut output_guard) = self.tui_bridge.total_output_tokens.try_lock() {
1232 *output_guard = session_info.total_output_tokens;
1233 }
1234
1235 if let Ok(mut messages_guard) = self.tui_bridge.messages.try_lock() {
1237 *messages_guard = messages.clone();
1238 }
1239
1240 if let Ok(mut chat) = self.tui_bridge.chat_view.try_lock() {
1242 chat.clear();
1243
1244 for msg in &messages {
1246 match msg.role {
1247 limit_llm::Role::User => {
1248 let content = msg.content.as_deref().unwrap_or("");
1249 let chat_msg = Message::user(content.to_string());
1250 chat.add_message(chat_msg);
1251 }
1252 limit_llm::Role::Assistant => {
1253 let content = msg.content.as_deref().unwrap_or("");
1254 let chat_msg = Message::assistant(content.to_string());
1255 chat.add_message(chat_msg);
1256 }
1257 _ => {}
1258 }
1259 }
1260
1261 tracing::info!(
1262 "Loaded session: {} ({} messages)",
1263 full_session_id,
1264 messages.len()
1265 );
1266
1267 let session_short_id = if full_session_id.len() > 8 {
1269 &full_session_id[full_session_id.len().saturating_sub(8)..]
1270 } else {
1271 &full_session_id
1272 };
1273 let msg = Message::system(format!(
1274 "📂 Loaded session: {} ({} messages, {} in tokens, {} out tokens)",
1275 session_short_id,
1276 messages.len(),
1277 session_info.total_input_tokens,
1278 session_info.total_output_tokens
1279 ));
1280 chat.add_message(msg);
1281 }
1282
1283 Ok(())
1284 }
1285
1286 fn prev_char_pos(&self) -> usize {
1287 if self.cursor_pos == 0 {
1288 return 0;
1289 }
1290 let mut pos = self.cursor_pos - 1;
1292 while pos > 0 && !self.input_text.is_char_boundary(pos) {
1293 pos -= 1;
1294 }
1295 pos
1296 }
1297
1298 fn next_char_pos(&self) -> usize {
1299 if self.cursor_pos >= self.input_text.len() {
1300 return self.input_text.len();
1301 }
1302 let mut pos = self.cursor_pos + 1;
1304 while pos < self.input_text.len() && !self.input_text.is_char_boundary(pos) {
1305 pos += 1;
1306 }
1307 pos
1308 }
1309
1310 fn draw(&mut self) -> Result<(), CliError> {
1311 let chat_view = self.tui_bridge.chat_view().clone();
1312 let state = self.tui_bridge.state();
1313 let input_text = self.input_text.clone();
1314 let cursor_pos = self.cursor_pos;
1315 let status_message = self.status_message.clone();
1316 let status_is_error = self.status_is_error;
1317 let cursor_blink_state = self.cursor_blink_state;
1318 let tui_bridge = &self.tui_bridge;
1319
1320 self.terminal
1321 .draw(|f| {
1322 Self::draw_ui(
1323 f,
1324 &chat_view,
1325 state,
1326 &input_text,
1327 cursor_pos,
1328 &status_message,
1329 status_is_error,
1330 cursor_blink_state,
1331 tui_bridge,
1332 );
1333 })
1334 .map_err(|e| CliError::IoError(io::Error::other(e)))?;
1335
1336 Ok(())
1337 }
1338
1339 #[allow(clippy::too_many_arguments)]
1341 fn draw_ui(
1342 f: &mut Frame,
1343 chat_view: &Arc<Mutex<ChatView>>,
1344 _state: TuiState,
1345 input_text: &str,
1346 cursor_pos: usize,
1347 status_message: &str,
1348 status_is_error: bool,
1349 cursor_blink_state: bool,
1350 tui_bridge: &TuiBridge,
1351 ) {
1352 let size = f.area();
1353
1354 let activity_count = tui_bridge.activity_feed().lock().unwrap().len();
1356 let activity_height = if activity_count > 0 {
1357 (activity_count as u16).min(3) } else {
1359 0
1360 };
1361
1362 let constraints: Vec<Constraint> = vec![Constraint::Percentage(90)]; let mut constraints = constraints;
1365 if activity_height > 0 {
1366 constraints.push(Constraint::Length(activity_height)); }
1368 constraints.push(Constraint::Length(1)); constraints.push(Constraint::Length(6)); let chunks = Layout::default()
1373 .direction(Direction::Vertical)
1374 .constraints(constraints.as_slice())
1375 .split(size);
1376
1377 let mut chunk_idx = 0;
1378
1379 {
1381 let chat = chat_view.lock().unwrap();
1382 let total_input = tui_bridge.total_input_tokens();
1383 let total_output = tui_bridge.total_output_tokens();
1384 let title = format!(" Chat (↑{} ↓{}) ", total_input, total_output);
1385 let chat_block = Block::default()
1386 .borders(Borders::ALL)
1387 .title(title)
1388 .title_style(
1389 Style::default()
1390 .fg(Color::Cyan)
1391 .add_modifier(Modifier::BOLD),
1392 );
1393 f.render_widget(&*chat, chat_block.inner(chunks[chunk_idx]));
1394 f.render_widget(chat_block, chunks[chunk_idx]);
1395 chunk_idx += 1;
1396 }
1397
1398 if activity_height > 0 {
1400 let activity_feed = tui_bridge.activity_feed().lock().unwrap();
1401 let activity_block = Block::default()
1402 .borders(Borders::NONE)
1403 .style(Style::default().bg(Color::Reset));
1404 let activity_inner = activity_block.inner(chunks[chunk_idx]);
1405 f.render_widget(activity_block, chunks[chunk_idx]);
1406 activity_feed.render(activity_inner, f.buffer_mut());
1407 chunk_idx += 1;
1408 }
1409
1410 {
1412 let status_style = if status_is_error {
1413 Style::default().fg(Color::Red).bg(Color::Reset)
1414 } else {
1415 Style::default().fg(Color::Yellow)
1416 };
1417
1418 let status = Paragraph::new(Line::from(vec![
1419 Span::styled(" ● ", Style::default().fg(Color::Green)),
1420 Span::styled(status_message, status_style),
1421 ]));
1422 f.render_widget(status, chunks[chunk_idx]);
1423 chunk_idx += 1;
1424 }
1425
1426 {
1428 let input_block = Block::default()
1429 .borders(Borders::ALL)
1430 .title(" Input (Esc or /exit to quit) ")
1431 .title_style(Style::default().fg(Color::Cyan));
1432
1433 let input_inner = input_block.inner(chunks[chunk_idx]);
1434 f.render_widget(input_block, chunks[chunk_idx]);
1435
1436 let before_cursor = &input_text[..cursor_pos];
1438 let at_cursor = if cursor_pos < input_text.len() {
1439 &input_text[cursor_pos
1440 ..cursor_pos
1441 + input_text[cursor_pos..]
1442 .chars()
1443 .next()
1444 .map(|c| c.len_utf8())
1445 .unwrap_or(0)]
1446 } else {
1447 " "
1448 };
1449 let after_cursor = if cursor_pos < input_text.len() {
1450 &input_text[cursor_pos + at_cursor.len()..]
1451 } else {
1452 ""
1453 };
1454
1455 let cursor_style = if cursor_blink_state {
1456 Style::default().bg(Color::White).fg(Color::Black)
1457 } else {
1458 Style::default().bg(Color::Reset).fg(Color::Reset)
1459 };
1460
1461 let input_line = if input_text.is_empty() {
1462 Line::from(vec![Span::styled(
1463 "Type your message here...",
1464 Style::default().fg(Color::DarkGray),
1465 )])
1466 } else {
1467 Line::from(vec![
1468 Span::raw(before_cursor),
1469 Span::styled(at_cursor, cursor_style),
1470 Span::raw(after_cursor),
1471 ])
1472 };
1473
1474 let input_para = Paragraph::new(input_line).wrap(Wrap { trim: false });
1475 f.render_widget(input_para, input_inner);
1476 }
1477 }
1478}
1479
1480#[cfg(test)]
1481mod tests {
1482 use super::*;
1483
1484 fn create_test_config() -> limit_llm::Config {
1486 use limit_llm::ProviderConfig;
1487 let mut providers = std::collections::HashMap::new();
1488 providers.insert(
1489 "anthropic".to_string(),
1490 ProviderConfig {
1491 api_key: Some("test-key".to_string()),
1492 model: "claude-3-5-sonnet-20241022".to_string(),
1493 base_url: None,
1494 max_tokens: 4096,
1495 timeout: 60,
1496 max_iterations: 100,
1497 thinking_enabled: false,
1498 clear_thinking: true,
1499 },
1500 );
1501 limit_llm::Config {
1502 provider: "anthropic".to_string(),
1503 providers,
1504 }
1505 }
1506
1507 #[test]
1508 fn test_tui_bridge_new() {
1509 let config = create_test_config();
1510 let agent_bridge = AgentBridge::new(config).unwrap();
1511 let (_tx, rx) = mpsc::unbounded_channel();
1512
1513 let tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
1514 assert_eq!(tui_bridge.state(), TuiState::Idle);
1515 }
1516
1517 #[test]
1518 fn test_tui_bridge_state() {
1519 let config = create_test_config();
1520 let agent_bridge = AgentBridge::new(config).unwrap();
1521 let (tx, rx) = mpsc::unbounded_channel();
1522
1523 let mut tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
1524
1525 tx.send(AgentEvent::Thinking).unwrap();
1526 tui_bridge.process_events().unwrap();
1527 assert!(matches!(tui_bridge.state(), TuiState::Thinking));
1528
1529 tx.send(AgentEvent::Done).unwrap();
1530 tui_bridge.process_events().unwrap();
1531 assert_eq!(tui_bridge.state(), TuiState::Idle);
1532 }
1533
1534 #[test]
1535 fn test_tui_bridge_chat_view() {
1536 let config = create_test_config();
1537 let agent_bridge = AgentBridge::new(config).unwrap();
1538 let (_tx, rx) = mpsc::unbounded_channel();
1539
1540 let tui_bridge = TuiBridge::new(agent_bridge, rx).unwrap();
1541
1542 tui_bridge.add_user_message("Hello".to_string());
1543 assert_eq!(tui_bridge.chat_view().lock().unwrap().message_count(), 3); }
1545
1546 #[test]
1547 fn test_tui_state_default() {
1548 let state = TuiState::default();
1549 assert_eq!(state, TuiState::Idle);
1550 }
1551}