ricecoder_tui/
widgets.rs

1//! UI widgets for the TUI
2//!
3//! This module provides the core UI widgets used in the RiceCoder TUI, including:
4//! - `ChatWidget`: Displays conversation messages with markdown rendering and streaming support
5//! - `Message`: Represents a single message in the chat
6//! - `StreamingMessage`: Manages real-time token streaming for AI responses
7//! - `MessageAuthor`: Identifies the sender of a message (user or AI)
8//!
9//! # Examples
10//!
11//! Creating and displaying a chat message:
12//!
13//! ```ignore
14//! use ricecoder_tui::{Message, MessageAuthor};
15//!
16//! let message = Message {
17//!     content: "Hello, how can I help?".to_string(),
18//!     author: MessageAuthor::AI,
19//!     streaming: false,
20//! };
21//! ```
22//!
23//! Streaming a response token by token:
24//!
25//! ```ignore
26//! use ricecoder_tui::StreamingMessage;
27//!
28//! let mut streaming = StreamingMessage::new();
29//! streaming.append("Hello");
30//! streaming.append(" ");
31//! streaming.append("world");
32//! assert_eq!(streaming.content, "Hello world");
33//! ```
34
35use crate::clipboard::{CopyFeedback, CopyOperation};
36
37/// Message in the chat
38#[derive(Debug, Clone)]
39pub struct Message {
40    /// Message content
41    pub content: String,
42    /// Message author (user or AI)
43    pub author: MessageAuthor,
44    /// Whether message is being streamed
45    pub streaming: bool,
46}
47
48/// Streaming message state
49#[derive(Debug, Clone)]
50pub struct StreamingMessage {
51    /// Accumulated content
52    pub content: String,
53    /// Whether streaming is active
54    pub active: bool,
55    /// Cursor position for animation
56    pub cursor_pos: usize,
57    /// Animation frame counter for cursor blinking
58    pub animation_frame: u32,
59    /// Total tokens received
60    pub token_count: usize,
61}
62
63impl Default for StreamingMessage {
64    fn default() -> Self {
65        Self {
66            content: String::new(),
67            active: true,
68            cursor_pos: 0,
69            animation_frame: 0,
70            token_count: 0,
71        }
72    }
73}
74
75impl StreamingMessage {
76    /// Create a new streaming message
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Append a token to the message
82    pub fn append(&mut self, token: &str) {
83        self.content.push_str(token);
84        self.cursor_pos = self.content.len();
85        self.token_count += 1;
86    }
87
88    /// Finish streaming
89    pub fn finish(&mut self) {
90        self.active = false;
91    }
92
93    /// Update animation frame for cursor blinking
94    pub fn update_animation(&mut self) {
95        if self.active {
96            self.animation_frame = (self.animation_frame + 1) % 4;
97        }
98    }
99
100    /// Get the display text with animated cursor
101    pub fn display_text(&self) -> String {
102        if self.active {
103            // Cursor animation: show cursor every other frame
104            let cursor = if self.animation_frame < 2 { "_" } else { " " };
105            format!("{}{}", self.content, cursor)
106        } else {
107            self.content.clone()
108        }
109    }
110
111    /// Get the display text with a specific cursor style
112    pub fn display_text_with_cursor(&self, cursor_char: &str) -> String {
113        if self.active {
114            format!("{}{}", self.content, cursor_char)
115        } else {
116            self.content.clone()
117        }
118    }
119
120    /// Check if streaming is complete
121    pub fn is_complete(&self) -> bool {
122        !self.active
123    }
124
125    /// Get the length of accumulated content
126    pub fn len(&self) -> usize {
127        self.content.len()
128    }
129
130    /// Check if content is empty
131    pub fn is_empty(&self) -> bool {
132        self.content.is_empty()
133    }
134}
135
136/// Message author
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum MessageAuthor {
139    /// User message
140    User,
141    /// AI message
142    Assistant,
143}
144
145impl Message {
146    /// Create a new user message
147    pub fn user(content: impl Into<String>) -> Self {
148        Self {
149            content: content.into(),
150            author: MessageAuthor::User,
151            streaming: false,
152        }
153    }
154
155    /// Create a new assistant message
156    pub fn assistant(content: impl Into<String>) -> Self {
157        Self {
158            content: content.into(),
159            author: MessageAuthor::Assistant,
160            streaming: false,
161        }
162    }
163
164    /// Extract all code blocks from the message
165    pub fn extract_code_blocks(&self) -> Vec<String> {
166        let mut code_blocks = Vec::new();
167        let mut in_code_block = false;
168        let mut current_block = String::new();
169
170        for line in self.content.lines() {
171            if line.starts_with("```") {
172                if in_code_block {
173                    // End of code block
174                    if !current_block.is_empty() {
175                        code_blocks.push(current_block.clone());
176                        current_block.clear();
177                    }
178                    in_code_block = false;
179                } else {
180                    // Start of code block
181                    in_code_block = true;
182                }
183            } else if in_code_block {
184                if !current_block.is_empty() {
185                    current_block.push('\n');
186                }
187                current_block.push_str(line);
188            }
189        }
190
191        code_blocks
192    }
193
194    /// Get the first code block from the message
195    pub fn get_first_code_block(&self) -> Option<String> {
196        self.extract_code_blocks().into_iter().next()
197    }
198
199    /// Check if message contains code blocks
200    pub fn has_code_blocks(&self) -> bool {
201        self.content.contains("```")
202    }
203}
204
205/// Message action
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum MessageAction {
208    /// Copy message content
209    Copy,
210    /// Copy code block
211    CopyCode,
212    /// Edit message
213    Edit,
214    /// Regenerate response
215    Regenerate,
216    /// Delete message
217    Delete,
218}
219
220/// Chat widget for displaying conversations
221pub struct ChatWidget {
222    /// Messages in the chat
223    pub messages: Vec<Message>,
224    /// Current input
225    pub input: String,
226    /// Scroll offset
227    pub scroll: usize,
228    /// Selected message index
229    pub selected: Option<usize>,
230    /// Available actions for selected message
231    pub available_actions: Vec<MessageAction>,
232    /// Current streaming message (if any)
233    pub streaming_message: Option<StreamingMessage>,
234    /// Whether streaming is currently active
235    pub is_streaming: bool,
236    /// Current copy operation with feedback
237    pub copy_operation: Option<CopyOperation>,
238    /// Action menu visibility
239    pub show_action_menu: bool,
240    /// Selected action in menu
241    pub selected_action: Option<usize>,
242}
243
244impl ChatWidget {
245    /// Create a new chat widget
246    pub fn new() -> Self {
247        Self {
248            messages: Vec::new(),
249            input: String::new(),
250            scroll: 0,
251            selected: None,
252            available_actions: Vec::new(),
253            streaming_message: None,
254            is_streaming: false,
255            copy_operation: None,
256            show_action_menu: false,
257            selected_action: None,
258        }
259    }
260
261    /// Start streaming a new message
262    pub fn start_streaming(&mut self) {
263        self.streaming_message = Some(StreamingMessage::new());
264        self.is_streaming = true;
265    }
266
267    /// Append a token to the streaming message
268    pub fn append_token(&mut self, token: &str) {
269        if let Some(ref mut msg) = self.streaming_message {
270            msg.append(token);
271        }
272    }
273
274    /// Finish streaming and convert to a regular message
275    pub fn finish_streaming(&mut self) -> Option<Message> {
276        if let Some(mut msg) = self.streaming_message.take() {
277            msg.finish();
278            let content = msg.content.clone();
279            self.is_streaming = false;
280
281            // Create a regular message from the streaming message
282            let message = Message::assistant(content);
283            self.messages.push(message.clone());
284            return Some(message);
285        }
286        None
287    }
288
289    /// Update streaming animation
290    pub fn update_streaming_animation(&mut self) {
291        if let Some(ref mut msg) = self.streaming_message {
292            msg.update_animation();
293        }
294    }
295
296    /// Get the current streaming message display text
297    pub fn get_streaming_display(&self) -> Option<String> {
298        self.streaming_message
299            .as_ref()
300            .map(|msg| msg.display_text())
301    }
302
303    /// Cancel streaming
304    pub fn cancel_streaming(&mut self) {
305        self.streaming_message = None;
306        self.is_streaming = false;
307    }
308
309    /// Add a message
310    pub fn add_message(&mut self, message: Message) {
311        self.messages.push(message);
312    }
313
314    /// Clear all messages
315    pub fn clear(&mut self) {
316        self.messages.clear();
317        self.input.clear();
318        self.scroll = 0;
319        self.selected = None;
320        self.available_actions.clear();
321        self.streaming_message = None;
322        self.is_streaming = false;
323        self.copy_operation = None;
324        self.show_action_menu = false;
325        self.selected_action = None;
326    }
327
328    /// Update available actions for selected message
329    pub fn update_actions(&mut self) {
330        self.available_actions.clear();
331
332        if let Some(idx) = self.selected {
333            if let Some(msg) = self.messages.get(idx) {
334                self.available_actions.push(MessageAction::Copy);
335
336                if msg.content.contains("```") {
337                    self.available_actions.push(MessageAction::CopyCode);
338                }
339
340                if msg.author == MessageAuthor::User {
341                    self.available_actions.push(MessageAction::Edit);
342                } else {
343                    self.available_actions.push(MessageAction::Regenerate);
344                }
345
346                self.available_actions.push(MessageAction::Delete);
347            }
348        }
349    }
350
351    /// Execute an action on the selected message
352    pub fn execute_action(&mut self, action: MessageAction) -> Result<(), String> {
353        match action {
354            MessageAction::Copy => {
355                if let Some(msg) = self.selected_message() {
356                    let content = msg.content.clone();
357                    let mut op = CopyOperation::new(content);
358                    match op.execute() {
359                        Ok(()) => {
360                            tracing::info!("Copied message to clipboard");
361                            self.copy_operation = Some(op);
362                            Ok(())
363                        }
364                        Err(e) => {
365                            tracing::error!("Failed to copy message: {}", e);
366                            Err(format!("Failed to copy: {}", e))
367                        }
368                    }
369                } else {
370                    Err("No message selected".to_string())
371                }
372            }
373            MessageAction::CopyCode => {
374                if let Some(msg) = self.selected_message() {
375                    if let Some(code) = msg.get_first_code_block() {
376                        let mut op = CopyOperation::new(code);
377                        match op.execute() {
378                            Ok(()) => {
379                                tracing::info!("Copied code block to clipboard");
380                                self.copy_operation = Some(op);
381                                Ok(())
382                            }
383                            Err(e) => {
384                                tracing::error!("Failed to copy code: {}", e);
385                                Err(format!("Failed to copy code: {}", e))
386                            }
387                        }
388                    } else {
389                        Err("No code block found in message".to_string())
390                    }
391                } else {
392                    Err("No message selected".to_string())
393                }
394            }
395            MessageAction::Edit => {
396                if let Some(idx) = self.selected {
397                    if let Some(msg) = self.messages.get_mut(idx) {
398                        if msg.author == MessageAuthor::User {
399                            self.input = msg.content.clone();
400                            tracing::info!("Editing message");
401                            self.show_action_menu = false;
402                            return Ok(());
403                        }
404                    }
405                }
406                Err("Cannot edit non-user messages".to_string())
407            }
408            MessageAction::Regenerate => {
409                if let Some(msg) = self.selected_message() {
410                    if msg.author == MessageAuthor::Assistant {
411                        tracing::info!("Regenerating response");
412                        self.show_action_menu = false;
413                        return Ok(());
414                    }
415                }
416                Err("Can only regenerate assistant messages".to_string())
417            }
418            MessageAction::Delete => {
419                if let Some(idx) = self.selected {
420                    self.messages.remove(idx);
421                    self.selected = None;
422                    self.available_actions.clear();
423                    self.show_action_menu = false;
424                    tracing::info!("Deleted message");
425                    return Ok(());
426                }
427                Err("No message selected".to_string())
428            }
429        }
430    }
431
432    /// Get visible messages based on scroll
433    pub fn visible_messages(&self, height: usize) -> Vec<&Message> {
434        self.messages
435            .iter()
436            .skip(self.scroll)
437            .take(height)
438            .collect()
439    }
440
441    /// Scroll up
442    pub fn scroll_up(&mut self) {
443        if self.scroll > 0 {
444            self.scroll -= 1;
445        }
446    }
447
448    /// Scroll down
449    pub fn scroll_down(&mut self, height: usize) {
450        let max_scroll = self.messages.len().saturating_sub(height);
451        if self.scroll < max_scroll {
452            self.scroll += 1;
453        }
454    }
455
456    /// Select next message
457    pub fn select_next(&mut self) {
458        match self.selected {
459            None => self.selected = Some(0),
460            Some(idx) if idx < self.messages.len() - 1 => self.selected = Some(idx + 1),
461            _ => {}
462        }
463    }
464
465    /// Select previous message
466    pub fn select_prev(&mut self) {
467        match self.selected {
468            None => {}
469            Some(0) => self.selected = None,
470            Some(idx) => self.selected = Some(idx - 1),
471        }
472    }
473
474    /// Get selected message
475    pub fn selected_message(&self) -> Option<&Message> {
476        self.selected.and_then(|idx| self.messages.get(idx))
477    }
478
479    /// Toggle action menu visibility
480    pub fn toggle_action_menu(&mut self) {
481        if self.selected.is_some() && !self.available_actions.is_empty() {
482            self.show_action_menu = !self.show_action_menu;
483            if self.show_action_menu {
484                self.selected_action = Some(0);
485            } else {
486                self.selected_action = None;
487            }
488        }
489    }
490
491    /// Close action menu
492    pub fn close_action_menu(&mut self) {
493        self.show_action_menu = false;
494        self.selected_action = None;
495    }
496
497    /// Navigate action menu up
498    pub fn action_menu_up(&mut self) {
499        if let Some(idx) = self.selected_action {
500            if idx > 0 {
501                self.selected_action = Some(idx - 1);
502            }
503        }
504    }
505
506    /// Navigate action menu down
507    pub fn action_menu_down(&mut self) {
508        if let Some(idx) = self.selected_action {
509            if idx < self.available_actions.len() - 1 {
510                self.selected_action = Some(idx + 1);
511            }
512        }
513    }
514
515    /// Execute action by keyboard shortcut
516    pub fn execute_action_by_shortcut(&mut self, key: char) -> Result<(), String> {
517        let action = match key {
518            'c' | 'C' => MessageAction::Copy,
519            'o' | 'O' => MessageAction::CopyCode,
520            'e' | 'E' => MessageAction::Edit,
521            'r' | 'R' => MessageAction::Regenerate,
522            'd' | 'D' => MessageAction::Delete,
523            _ => return Err(format!("Unknown shortcut: {}", key)),
524        };
525
526        if self.available_actions.contains(&action) {
527            self.execute_action(action)
528        } else {
529            Err(format!("Action not available: {:?}", action))
530        }
531    }
532
533    /// Get current action menu item
534    pub fn get_selected_action(&self) -> Option<MessageAction> {
535        self.selected_action
536            .and_then(|idx| self.available_actions.get(idx))
537            .copied()
538    }
539
540    /// Execute selected action from menu
541    pub fn execute_selected_action(&mut self) -> Result<(), String> {
542        if let Some(action) = self.get_selected_action() {
543            self.execute_action(action)
544        } else {
545            Err("No action selected".to_string())
546        }
547    }
548
549    /// Update copy operation feedback
550    pub fn update_copy_feedback(&mut self) {
551        if let Some(ref mut op) = self.copy_operation {
552            op.update_feedback();
553            if !op.is_feedback_visible() {
554                self.copy_operation = None;
555            }
556        }
557    }
558
559    /// Get current copy feedback if visible
560    pub fn get_copy_feedback(&self) -> Option<CopyFeedback> {
561        self.copy_operation
562            .as_ref()
563            .and_then(|op| op.get_feedback())
564    }
565
566    /// Check if copy feedback is visible
567    pub fn is_copy_feedback_visible(&self) -> bool {
568        self.copy_operation
569            .as_ref()
570            .map(|op| op.is_feedback_visible())
571            .unwrap_or(false)
572    }
573}
574
575impl Default for ChatWidget {
576    fn default() -> Self {
577        Self::new()
578    }
579}
580
581// DiffWidget is defined in diff.rs module and re-exported from lib.rs
582
583/// Prompt widget for displaying the command prompt
584pub struct PromptWidget;
585
586impl PromptWidget {
587    /// Create a new prompt widget
588    pub fn new() -> Self {
589        Self
590    }
591}
592
593impl Default for PromptWidget {
594    fn default() -> Self {
595        Self::new()
596    }
597}
598
599/// Menu widget for navigation
600pub struct MenuWidget;
601
602impl MenuWidget {
603    /// Create a new menu widget
604    pub fn new() -> Self {
605        Self
606    }
607}
608
609impl Default for MenuWidget {
610    fn default() -> Self {
611        Self::new()
612    }
613}
614
615/// List widget for displaying items
616pub struct ListWidget;
617
618impl ListWidget {
619    /// Create a new list widget
620    pub fn new() -> Self {
621        Self
622    }
623}
624
625impl Default for ListWidget {
626    fn default() -> Self {
627        Self::new()
628    }
629}
630
631/// Dialog widget for user interactions
632pub struct DialogWidget;
633
634impl DialogWidget {
635    /// Create a new dialog widget
636    pub fn new() -> Self {
637        Self
638    }
639}
640
641impl Default for DialogWidget {
642    fn default() -> Self {
643        Self::new()
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650
651    #[test]
652    fn test_message_creation() {
653        let user_msg = Message::user("Hello");
654        assert_eq!(user_msg.content, "Hello");
655        assert_eq!(user_msg.author, MessageAuthor::User);
656        assert!(!user_msg.streaming);
657
658        let ai_msg = Message::assistant("Hi there");
659        assert_eq!(ai_msg.content, "Hi there");
660        assert_eq!(ai_msg.author, MessageAuthor::Assistant);
661    }
662
663    #[test]
664    fn test_chat_widget_creation() {
665        let widget = ChatWidget::new();
666        assert!(widget.messages.is_empty());
667        assert!(widget.input.is_empty());
668        assert_eq!(widget.scroll, 0);
669        assert!(widget.selected.is_none());
670    }
671
672    #[test]
673    fn test_add_message() {
674        let mut widget = ChatWidget::new();
675        widget.add_message(Message::user("Hello"));
676        widget.add_message(Message::assistant("Hi"));
677
678        assert_eq!(widget.messages.len(), 2);
679        assert_eq!(widget.messages[0].author, MessageAuthor::User);
680        assert_eq!(widget.messages[1].author, MessageAuthor::Assistant);
681    }
682
683    #[test]
684    fn test_clear() {
685        let mut widget = ChatWidget::new();
686        widget.add_message(Message::user("Hello"));
687        widget.input = "test".to_string();
688        widget.scroll = 5;
689
690        widget.clear();
691        assert!(widget.messages.is_empty());
692        assert!(widget.input.is_empty());
693        assert_eq!(widget.scroll, 0);
694    }
695
696    #[test]
697    fn test_scroll() {
698        let mut widget = ChatWidget::new();
699        for i in 0..10 {
700            widget.add_message(Message::user(format!("Message {}", i)));
701        }
702
703        widget.scroll_down(5);
704        assert_eq!(widget.scroll, 1);
705
706        widget.scroll_up();
707        assert_eq!(widget.scroll, 0);
708
709        widget.scroll_up();
710        assert_eq!(widget.scroll, 0);
711    }
712
713    #[test]
714    fn test_visible_messages() {
715        let mut widget = ChatWidget::new();
716        for i in 0..10 {
717            widget.add_message(Message::user(format!("Message {}", i)));
718        }
719
720        let visible = widget.visible_messages(5);
721        assert_eq!(visible.len(), 5);
722
723        widget.scroll = 5;
724        let visible = widget.visible_messages(5);
725        assert_eq!(visible.len(), 5);
726    }
727
728    #[test]
729    fn test_selection() {
730        let mut widget = ChatWidget::new();
731        widget.add_message(Message::user("Message 1"));
732        widget.add_message(Message::user("Message 2"));
733        widget.add_message(Message::user("Message 3"));
734
735        assert!(widget.selected_message().is_none());
736
737        widget.select_next();
738        assert_eq!(widget.selected, Some(0));
739
740        widget.select_next();
741        assert_eq!(widget.selected, Some(1));
742
743        widget.select_prev();
744        assert_eq!(widget.selected, Some(0));
745
746        widget.select_prev();
747        assert!(widget.selected.is_none());
748    }
749
750    #[test]
751    fn test_streaming_message_creation() {
752        let msg = StreamingMessage::new();
753        assert!(msg.active);
754        assert!(msg.content.is_empty());
755        assert_eq!(msg.cursor_pos, 0);
756    }
757
758    #[test]
759    fn test_streaming_message_append() {
760        let mut msg = StreamingMessage::new();
761        msg.append("Hello");
762        assert_eq!(msg.content, "Hello");
763        assert_eq!(msg.cursor_pos, 5);
764
765        msg.append(" world");
766        assert_eq!(msg.content, "Hello world");
767        assert_eq!(msg.cursor_pos, 11);
768    }
769
770    #[test]
771    fn test_streaming_message_display() {
772        let mut msg = StreamingMessage::new();
773        msg.append("Hello");
774        assert_eq!(msg.display_text(), "Hello_");
775
776        msg.finish();
777        assert_eq!(msg.display_text(), "Hello");
778        assert!(!msg.active);
779    }
780
781    #[test]
782    fn test_message_actions() {
783        let mut widget = ChatWidget::new();
784        widget.add_message(Message::user("Hello"));
785        widget.add_message(Message::assistant("Hi there"));
786
787        widget.select_next();
788        widget.update_actions();
789
790        assert!(!widget.available_actions.is_empty());
791        assert!(widget.available_actions.contains(&MessageAction::Copy));
792        assert!(widget.available_actions.contains(&MessageAction::Edit));
793    }
794
795    #[test]
796    fn test_execute_copy_action() {
797        let mut widget = ChatWidget::new();
798        widget.add_message(Message::user("Hello"));
799        widget.select_next();
800
801        let result = widget.execute_action(MessageAction::Copy);
802        // Result depends on clipboard availability
803        let _ = result;
804    }
805
806    #[test]
807    fn test_execute_delete_action() {
808        let mut widget = ChatWidget::new();
809        widget.add_message(Message::user("Message 1"));
810        widget.add_message(Message::user("Message 2"));
811
812        widget.select_next();
813        assert_eq!(widget.messages.len(), 2);
814
815        let result = widget.execute_action(MessageAction::Delete);
816        assert!(result.is_ok());
817        assert_eq!(widget.messages.len(), 1);
818    }
819
820    #[test]
821    fn test_execute_edit_action() {
822        let mut widget = ChatWidget::new();
823        widget.add_message(Message::user("Original message"));
824        widget.select_next();
825
826        let result = widget.execute_action(MessageAction::Edit);
827        assert!(result.is_ok());
828        assert_eq!(widget.input, "Original message");
829    }
830
831    #[test]
832    fn test_streaming_message_animation() {
833        let mut msg = StreamingMessage::new();
834        msg.append("Hello");
835
836        // Test animation frames
837        assert_eq!(msg.animation_frame, 0);
838        msg.update_animation();
839        assert_eq!(msg.animation_frame, 1);
840        msg.update_animation();
841        assert_eq!(msg.animation_frame, 2);
842        msg.update_animation();
843        assert_eq!(msg.animation_frame, 3);
844        msg.update_animation();
845        assert_eq!(msg.animation_frame, 0); // Wraps around
846    }
847
848    #[test]
849    fn test_streaming_message_cursor_animation() {
850        let mut msg = StreamingMessage::new();
851        msg.append("Test");
852
853        // Frame 0-1: show cursor
854        msg.animation_frame = 0;
855        assert_eq!(msg.display_text(), "Test_");
856
857        msg.animation_frame = 1;
858        assert_eq!(msg.display_text(), "Test_");
859
860        // Frame 2-3: hide cursor
861        msg.animation_frame = 2;
862        assert_eq!(msg.display_text(), "Test ");
863
864        msg.animation_frame = 3;
865        assert_eq!(msg.display_text(), "Test ");
866    }
867
868    #[test]
869    fn test_streaming_message_token_count() {
870        let mut msg = StreamingMessage::new();
871        assert_eq!(msg.token_count, 0);
872
873        msg.append("Hello");
874        assert_eq!(msg.token_count, 1);
875
876        msg.append(" ");
877        assert_eq!(msg.token_count, 2);
878
879        msg.append("world");
880        assert_eq!(msg.token_count, 3);
881    }
882
883    #[test]
884    fn test_streaming_message_custom_cursor() {
885        let msg = StreamingMessage::new();
886        let display = msg.display_text_with_cursor("▌");
887        assert_eq!(display, "▌");
888
889        let mut msg = StreamingMessage::new();
890        msg.append("Loading");
891        let display = msg.display_text_with_cursor("▌");
892        assert_eq!(display, "Loading▌");
893
894        msg.finish();
895        let display = msg.display_text_with_cursor("▌");
896        assert_eq!(display, "Loading");
897    }
898
899    #[test]
900    fn test_chat_widget_start_streaming() {
901        let mut widget = ChatWidget::new();
902        assert!(!widget.is_streaming);
903        assert!(widget.streaming_message.is_none());
904
905        widget.start_streaming();
906        assert!(widget.is_streaming);
907        assert!(widget.streaming_message.is_some());
908    }
909
910    #[test]
911    fn test_chat_widget_append_token() {
912        let mut widget = ChatWidget::new();
913        widget.start_streaming();
914
915        widget.append_token("Hello");
916        widget.append_token(" ");
917        widget.append_token("world");
918
919        let display = widget.get_streaming_display();
920        assert!(display.is_some());
921        assert!(display.unwrap().contains("Hello world"));
922    }
923
924    #[test]
925    fn test_chat_widget_finish_streaming() {
926        let mut widget = ChatWidget::new();
927        widget.start_streaming();
928        widget.append_token("Test message");
929
930        let message = widget.finish_streaming();
931        assert!(message.is_some());
932        assert!(!widget.is_streaming);
933        assert!(widget.streaming_message.is_none());
934        assert_eq!(widget.messages.len(), 1);
935        assert_eq!(widget.messages[0].content, "Test message");
936        assert_eq!(widget.messages[0].author, MessageAuthor::Assistant);
937    }
938
939    #[test]
940    fn test_chat_widget_cancel_streaming() {
941        let mut widget = ChatWidget::new();
942        widget.start_streaming();
943        widget.append_token("Partial message");
944
945        widget.cancel_streaming();
946        assert!(!widget.is_streaming);
947        assert!(widget.streaming_message.is_none());
948        assert!(widget.messages.is_empty());
949    }
950
951    #[test]
952    fn test_chat_widget_update_streaming_animation() {
953        let mut widget = ChatWidget::new();
954        widget.start_streaming();
955        widget.append_token("Animating");
956
957        let initial_frame = widget.streaming_message.as_ref().unwrap().animation_frame;
958        widget.update_streaming_animation();
959        let new_frame = widget.streaming_message.as_ref().unwrap().animation_frame;
960
961        assert_ne!(initial_frame, new_frame);
962    }
963
964    #[test]
965    fn test_chat_widget_clear_with_streaming() {
966        let mut widget = ChatWidget::new();
967        widget.add_message(Message::user("Hello"));
968        widget.start_streaming();
969        widget.append_token("Response");
970
971        widget.clear();
972        assert!(widget.messages.is_empty());
973        assert!(!widget.is_streaming);
974        assert!(widget.streaming_message.is_none());
975    }
976
977    #[test]
978    fn test_streaming_message_len_and_empty() {
979        let mut msg = StreamingMessage::new();
980        assert!(msg.is_empty());
981        assert_eq!(msg.len(), 0);
982
983        msg.append("Hello");
984        assert!(!msg.is_empty());
985        assert_eq!(msg.len(), 5);
986
987        msg.append(" world");
988        assert_eq!(msg.len(), 11);
989    }
990
991    #[test]
992    fn test_streaming_message_is_complete() {
993        let mut msg = StreamingMessage::new();
994        assert!(!msg.is_complete());
995
996        msg.finish();
997        assert!(msg.is_complete());
998    }
999
1000    #[test]
1001    fn test_message_extract_code_blocks() {
1002        let msg =
1003            Message::assistant("Here's some code:\n```rust\nfn main() {}\n```\nAnd more text");
1004        let blocks = msg.extract_code_blocks();
1005        assert_eq!(blocks.len(), 1);
1006        assert!(blocks[0].contains("fn main()"));
1007    }
1008
1009    #[test]
1010    fn test_message_extract_multiple_code_blocks() {
1011        let msg = Message::assistant(
1012            "First:\n```rust\nfn foo() {}\n```\nSecond:\n```python\ndef bar(): pass\n```",
1013        );
1014        let blocks = msg.extract_code_blocks();
1015        assert_eq!(blocks.len(), 2);
1016    }
1017
1018    #[test]
1019    fn test_message_get_first_code_block() {
1020        let msg = Message::assistant("Code:\n```rust\nfn main() {}\n```");
1021        let block = msg.get_first_code_block();
1022        assert!(block.is_some());
1023        assert!(block.unwrap().contains("fn main()"));
1024    }
1025
1026    #[test]
1027    fn test_message_has_code_blocks() {
1028        let msg_with_code = Message::assistant("```rust\ncode\n```");
1029        assert!(msg_with_code.has_code_blocks());
1030
1031        let msg_without_code = Message::assistant("Just text");
1032        assert!(!msg_without_code.has_code_blocks());
1033    }
1034
1035    #[test]
1036    fn test_chat_widget_action_menu_toggle() {
1037        let mut widget = ChatWidget::new();
1038        widget.add_message(Message::user("Hello"));
1039        widget.select_next();
1040        widget.update_actions();
1041
1042        assert!(!widget.show_action_menu);
1043        widget.toggle_action_menu();
1044        assert!(widget.show_action_menu);
1045        assert_eq!(widget.selected_action, Some(0));
1046
1047        widget.toggle_action_menu();
1048        assert!(!widget.show_action_menu);
1049    }
1050
1051    #[test]
1052    fn test_chat_widget_action_menu_navigation() {
1053        let mut widget = ChatWidget::new();
1054        widget.add_message(Message::user("Hello"));
1055        widget.select_next();
1056        widget.update_actions();
1057        widget.toggle_action_menu();
1058
1059        widget.action_menu_down();
1060        assert_eq!(widget.selected_action, Some(1));
1061
1062        widget.action_menu_down();
1063        assert_eq!(widget.selected_action, Some(2));
1064
1065        widget.action_menu_up();
1066        assert_eq!(widget.selected_action, Some(1));
1067
1068        widget.action_menu_up();
1069        assert_eq!(widget.selected_action, Some(0));
1070    }
1071
1072    #[test]
1073    fn test_chat_widget_close_action_menu() {
1074        let mut widget = ChatWidget::new();
1075        widget.add_message(Message::user("Hello"));
1076        widget.select_next();
1077        widget.update_actions();
1078        widget.toggle_action_menu();
1079
1080        assert!(widget.show_action_menu);
1081        widget.close_action_menu();
1082        assert!(!widget.show_action_menu);
1083        assert!(widget.selected_action.is_none());
1084    }
1085
1086    #[test]
1087    fn test_chat_widget_get_selected_action() {
1088        let mut widget = ChatWidget::new();
1089        widget.add_message(Message::user("Hello"));
1090        widget.select_next();
1091        widget.update_actions();
1092        widget.toggle_action_menu();
1093
1094        let action = widget.get_selected_action();
1095        assert_eq!(action, Some(MessageAction::Copy));
1096    }
1097
1098    #[test]
1099    fn test_chat_widget_execute_copy_action_result() {
1100        let mut widget = ChatWidget::new();
1101        widget.add_message(Message::user("Hello"));
1102        widget.select_next();
1103
1104        let result = widget.execute_action(MessageAction::Copy);
1105        // Result depends on clipboard availability, but should not panic
1106        let _ = result;
1107    }
1108
1109    #[test]
1110    fn test_chat_widget_execute_delete_action_result() {
1111        let mut widget = ChatWidget::new();
1112        widget.add_message(Message::user("Message 1"));
1113        widget.add_message(Message::user("Message 2"));
1114
1115        widget.select_next();
1116        assert_eq!(widget.messages.len(), 2);
1117
1118        let result = widget.execute_action(MessageAction::Delete);
1119        assert!(result.is_ok());
1120        assert_eq!(widget.messages.len(), 1);
1121    }
1122
1123    #[test]
1124    fn test_chat_widget_execute_edit_action_result() {
1125        let mut widget = ChatWidget::new();
1126        widget.add_message(Message::user("Original message"));
1127        widget.select_next();
1128
1129        let result = widget.execute_action(MessageAction::Edit);
1130        assert!(result.is_ok());
1131        assert_eq!(widget.input, "Original message");
1132    }
1133
1134    #[test]
1135    fn test_chat_widget_execute_action_no_selection() {
1136        let mut widget = ChatWidget::new();
1137        widget.add_message(Message::user("Hello"));
1138
1139        let result = widget.execute_action(MessageAction::Copy);
1140        assert!(result.is_err());
1141    }
1142
1143    #[test]
1144    fn test_chat_widget_execute_action_by_shortcut() {
1145        let mut widget = ChatWidget::new();
1146        widget.add_message(Message::user("Hello"));
1147        widget.select_next();
1148        widget.update_actions();
1149
1150        let result = widget.execute_action_by_shortcut('c');
1151        // Result depends on clipboard, but should not panic
1152        let _ = result;
1153    }
1154
1155    #[test]
1156    fn test_chat_widget_execute_action_by_invalid_shortcut() {
1157        let mut widget = ChatWidget::new();
1158        widget.add_message(Message::user("Hello"));
1159        widget.select_next();
1160        widget.update_actions();
1161
1162        let result = widget.execute_action_by_shortcut('x');
1163        assert!(result.is_err());
1164    }
1165
1166    #[test]
1167    fn test_chat_widget_copy_feedback_visibility() {
1168        let mut widget = ChatWidget::new();
1169        assert!(!widget.is_copy_feedback_visible());
1170
1171        widget.add_message(Message::user("Hello"));
1172        widget.select_next();
1173        let _ = widget.execute_action(MessageAction::Copy);
1174
1175        // Feedback should be visible after copy
1176        assert!(widget.is_copy_feedback_visible());
1177    }
1178
1179    #[test]
1180    fn test_chat_widget_update_copy_feedback() {
1181        let mut widget = ChatWidget::new();
1182        widget.add_message(Message::user("Hello"));
1183        widget.select_next();
1184        let _ = widget.execute_action(MessageAction::Copy);
1185
1186        assert!(widget.is_copy_feedback_visible());
1187
1188        // Update feedback multiple times
1189        for _ in 0..100 {
1190            widget.update_copy_feedback();
1191        }
1192
1193        // Feedback should eventually disappear
1194        assert!(!widget.is_copy_feedback_visible());
1195    }
1196
1197    #[test]
1198    fn test_chat_widget_execute_copy_code_action() {
1199        let mut widget = ChatWidget::new();
1200        widget.add_message(Message::assistant("Code:\n```rust\nfn main() {}\n```"));
1201        widget.select_next();
1202        widget.update_actions();
1203
1204        assert!(widget.available_actions.contains(&MessageAction::CopyCode));
1205        let result = widget.execute_action(MessageAction::CopyCode);
1206        // Result depends on clipboard, but should not panic
1207        let _ = result;
1208    }
1209
1210    #[test]
1211    fn test_chat_widget_execute_copy_code_no_code() {
1212        let mut widget = ChatWidget::new();
1213        widget.add_message(Message::assistant("Just text, no code"));
1214        widget.select_next();
1215
1216        let result = widget.execute_action(MessageAction::CopyCode);
1217        assert!(result.is_err());
1218    }
1219
1220    #[test]
1221    fn test_chat_widget_execute_regenerate_action() {
1222        let mut widget = ChatWidget::new();
1223        widget.add_message(Message::assistant("Response"));
1224        widget.select_next();
1225        widget.update_actions();
1226
1227        assert!(widget
1228            .available_actions
1229            .contains(&MessageAction::Regenerate));
1230        let result = widget.execute_action(MessageAction::Regenerate);
1231        assert!(result.is_ok());
1232    }
1233
1234    #[test]
1235    fn test_chat_widget_execute_regenerate_user_message() {
1236        let mut widget = ChatWidget::new();
1237        widget.add_message(Message::user("Question"));
1238        widget.select_next();
1239
1240        let result = widget.execute_action(MessageAction::Regenerate);
1241        assert!(result.is_err());
1242    }
1243
1244    #[test]
1245    fn test_chat_widget_clear_with_copy_operation() {
1246        let mut widget = ChatWidget::new();
1247        widget.add_message(Message::user("Hello"));
1248        widget.select_next();
1249        let _ = widget.execute_action(MessageAction::Copy);
1250
1251        assert!(widget.copy_operation.is_some());
1252        widget.clear();
1253        assert!(widget.copy_operation.is_none());
1254    }
1255
1256    #[test]
1257    fn test_chat_widget_execute_selected_action() {
1258        let mut widget = ChatWidget::new();
1259        widget.add_message(Message::user("Message 1"));
1260        widget.add_message(Message::user("Message 2"));
1261        widget.select_next();
1262        widget.update_actions();
1263        widget.toggle_action_menu();
1264
1265        // Navigate to delete action
1266        widget.action_menu_down();
1267        widget.action_menu_down();
1268        widget.action_menu_down();
1269
1270        let result = widget.execute_selected_action();
1271        assert!(result.is_ok());
1272        assert_eq!(widget.messages.len(), 1);
1273    }
1274
1275    #[test]
1276    fn test_chat_widget_action_menu_no_selection() {
1277        let mut widget = ChatWidget::new();
1278        widget.add_message(Message::user("Hello"));
1279
1280        // Try to toggle menu without selecting message
1281        widget.toggle_action_menu();
1282        assert!(!widget.show_action_menu);
1283    }
1284
1285    #[test]
1286    fn test_chat_widget_action_menu_no_actions() {
1287        let mut widget = ChatWidget::new();
1288        widget.add_message(Message::user("Hello"));
1289        widget.select_next();
1290        // Don't call update_actions
1291
1292        widget.toggle_action_menu();
1293        assert!(!widget.show_action_menu);
1294    }
1295}