Skip to main content

winx_code_agent/state/
terminal.rs

1use regex::Regex;
2use std::collections::{HashMap, VecDeque};
3use std::sync::{Arc, Mutex, RwLock};
4use std::time::Instant;
5use tracing::{debug, warn};
6use vte::{Parser, Perform};
7
8// Import our enhanced ANSI code module
9#[allow(unused_imports)]
10use crate::state::ansi_codes;
11
12/// Maximum number of lines to keep in the screen buffer
13pub const MAX_SCREEN_LINES: usize = 10000;
14/// Default maximum number of lines to keep in the screen buffer
15pub const DEFAULT_MAX_SCREEN_LINES: usize = 500;
16/// Maximum number of columns for the screen. Must match the PTY width
17/// (`pty::DEFAULT_COLS`) so emulator-rendered scrollback wraps exactly where the
18/// real terminal did, instead of silently re-wrapping long lines at a narrower
19/// width.
20const DEFAULT_COLUMNS: usize = 200;
21/// Maximum output size in bytes to prevent excessive memory usage
22pub const MAX_OUTPUT_SIZE: usize = 500_000;
23/// Maximum cache entry lifetime in seconds
24const CACHE_TTL: u64 = 300; // 5 minutes
25
26/// Compact bitset for terminal text styles.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub struct CellStyle(u32);
29
30impl CellStyle {
31    pub const BOLD: Self = Self(1 << 0);
32    pub const UNDERLINE: Self = Self(1 << 1);
33    pub const BLINK: Self = Self(1 << 2);
34    pub const REVERSE: Self = Self(1 << 3);
35    pub const ITALIC: Self = Self(1 << 4);
36    pub const STRIKETHROUGH: Self = Self(1 << 5);
37    pub const DIM: Self = Self(1 << 6);
38    pub const DOUBLE_UNDERLINE: Self = Self(1 << 7);
39    pub const FRAMED: Self = Self(1 << 8);
40    pub const ENCIRCLED: Self = Self(1 << 9);
41    pub const OVERLINED: Self = Self(1 << 10);
42    pub const FRAKTUR: Self = Self(1 << 11);
43    pub const CONCEAL: Self = Self(1 << 12);
44    pub const SUPERSCRIPT: Self = Self(1 << 13);
45    pub const SUBSCRIPT: Self = Self(1 << 14);
46    pub const HYPERLINK: Self = Self(1 << 15);
47
48    #[must_use]
49    pub const fn union(self, other: Self) -> Self {
50        Self(self.0 | other.0)
51    }
52
53    #[must_use]
54    pub const fn contains(self, flag: Self) -> bool {
55        self.0 & flag.0 != 0
56    }
57
58    pub fn set(&mut self, flag: Self, enabled: bool) {
59        if enabled {
60            self.0 |= flag.0;
61        } else {
62            self.0 &= !flag.0;
63        }
64    }
65}
66
67/// Container for all possible character attributes
68#[derive(Debug, Clone, Default, PartialEq, Eq)]
69pub struct ScreenCellAttributes {
70    /// Text style flags such as bold, underline, reverse, and blink.
71    pub style: CellStyle,
72    /// Foreground color
73    pub fg_color: Option<TerminalColor>,
74    /// Background color
75    pub bg_color: Option<TerminalColor>,
76    /// URL for hyperlink, if applicable
77    pub hyperlink_url: Option<String>,
78    /// Font selection (0-9, where 0 is the primary font)
79    pub font: u8,
80}
81
82/// Represents a character with attributes in the terminal
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ScreenCell {
85    /// The character to display
86    pub character: char,
87    /// Text style flags such as bold, underline, reverse, and blink.
88    pub style: CellStyle,
89    /// Foreground color (0-255 for 8-bit colors, RGB for 24-bit colors)
90    pub fg_color: Option<TerminalColor>,
91    /// Background color (0-255 for 8-bit colors, RGB for 24-bit colors)
92    pub bg_color: Option<TerminalColor>,
93    /// URL for hyperlink, if applicable
94    pub hyperlink_url: Option<String>,
95    /// Font selection (0-9, where 0 is the primary font)
96    pub font: u8,
97}
98
99/// Represents a terminal color
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum TerminalColor {
102    /// Basic 8 colors (0-7)
103    Basic(u8),
104    /// Extended 8-bit color (0-255)
105    Color256(u8),
106    /// 24-bit RGB color
107    TrueColor { r: u8, g: u8, b: u8 },
108    /// Named color like "red", "blue", etc.
109    Named(String),
110}
111
112impl ScreenCell {
113    fn new(character: char, attributes: ScreenCellAttributes) -> Self {
114        Self {
115            character,
116            style: attributes.style,
117            fg_color: attributes.fg_color,
118            bg_color: attributes.bg_color,
119            hyperlink_url: attributes.hyperlink_url,
120            font: attributes.font,
121        }
122    }
123}
124
125impl Default for ScreenCell {
126    fn default() -> Self {
127        Self {
128            character: ' ',
129            style: CellStyle::default(),
130            fg_color: None,
131            bg_color: None,
132            hyperlink_url: None,
133            font: 0, // Primary font
134        }
135    }
136}
137
138/// Represents the current state of a terminal screen
139#[derive(Debug, Clone)]
140pub struct Screen {
141    /// Lines of characters with attributes
142    pub lines: VecDeque<Vec<ScreenCell>>,
143    /// Current cursor position (row, column)
144    pub cursor_position: (usize, usize),
145    /// Number of columns in the screen
146    pub columns: usize,
147    /// Whether the cursor should be visible
148    pub cursor_visible: bool,
149    /// Maximum number of lines to keep
150    pub max_lines: usize,
151    /// Last time the screen was modified
152    last_modified: Instant,
153}
154
155impl Default for Screen {
156    fn default() -> Self {
157        let mut lines = VecDeque::with_capacity(DEFAULT_MAX_SCREEN_LINES);
158        lines.push_back(vec![ScreenCell::default(); DEFAULT_COLUMNS]);
159
160        Self {
161            lines,
162            cursor_position: (0, 0),
163            columns: DEFAULT_COLUMNS,
164            cursor_visible: true,
165            max_lines: DEFAULT_MAX_SCREEN_LINES,
166            last_modified: Instant::now(),
167        }
168    }
169}
170
171impl Screen {
172    /// Creates a new screen with specified dimensions
173    pub fn new(columns: usize) -> Self {
174        let mut lines = VecDeque::with_capacity(DEFAULT_MAX_SCREEN_LINES);
175        lines.push_back(vec![ScreenCell::default(); columns]);
176
177        Self {
178            lines,
179            cursor_position: (0, 0),
180            columns,
181            cursor_visible: true,
182            max_lines: DEFAULT_MAX_SCREEN_LINES,
183            last_modified: Instant::now(),
184        }
185    }
186
187    /// Creates a new screen with specified dimensions and maximum lines
188    pub fn new_with_max_lines(columns: usize, max_lines: usize) -> Self {
189        let mut lines = VecDeque::with_capacity(max_lines.min(MAX_SCREEN_LINES));
190        lines.push_back(vec![ScreenCell::default(); columns]);
191
192        Self {
193            lines,
194            cursor_position: (0, 0),
195            columns,
196            cursor_visible: true,
197            max_lines: max_lines.min(MAX_SCREEN_LINES),
198            last_modified: Instant::now(),
199        }
200    }
201
202    /// Get the current cursor row
203    pub fn cursor_row(&self) -> usize {
204        self.cursor_position.0
205    }
206
207    /// Get the current cursor column
208    pub fn cursor_col(&self) -> usize {
209        self.cursor_position.1
210    }
211
212    /// Ensure that a line exists at the specified index
213    fn ensure_line(&mut self, line_idx: usize) {
214        // Add new lines if needed
215        while self.lines.len() <= line_idx {
216            self.lines.push_back(vec![ScreenCell::default(); self.columns]);
217        }
218
219        // Limit the number of lines to prevent memory growth
220        while self.lines.len() > self.max_lines {
221            self.lines.pop_front();
222
223            // Adjust cursor position to account for the removed line
224            if self.cursor_position.0 > 0 {
225                self.cursor_position.0 -= 1;
226            }
227        }
228
229        self.last_modified = Instant::now();
230    }
231
232    /// Ensure that the cursor position is valid
233    fn ensure_cursor_position(&mut self) {
234        self.ensure_line(self.cursor_position.0);
235
236        // Ensure the cursor column is within bounds
237        if self.cursor_position.1 >= self.columns {
238            self.cursor_position.1 = self.columns - 1;
239        }
240
241        self.last_modified = Instant::now();
242    }
243
244    /// Put a character at the current cursor position and advance the cursor
245    pub fn put_char(&mut self, c: char, attributes: ScreenCellAttributes) {
246        self.ensure_cursor_position();
247
248        // Get the current cursor position
249        let row = self.cursor_position.0;
250        let col = self.cursor_position.1;
251
252        // Put the character at the cursor position
253        if col < self.lines[row].len() {
254            self.lines[row][col] = ScreenCell::new(c, attributes);
255        } else {
256            // Add cells if needed
257            while self.lines[row].len() <= col {
258                self.lines[row].push(ScreenCell::default());
259            }
260            self.lines[row][col] = ScreenCell::new(c, attributes);
261        }
262
263        // Advance the cursor
264        self.cursor_position.1 += 1;
265        if self.cursor_position.1 >= self.columns {
266            self.cursor_position.1 = 0;
267            self.cursor_position.0 += 1;
268            self.ensure_cursor_position();
269        }
270
271        self.last_modified = Instant::now();
272    }
273
274    /// Put a character at the current cursor position with basic attributes
275    #[allow(clippy::too_many_arguments)]
276    pub fn put_char_basic(
277        &mut self,
278        c: char,
279        style: CellStyle,
280        fg_color: Option<TerminalColor>,
281        bg_color: Option<TerminalColor>,
282    ) {
283        let attributes = ScreenCellAttributes { style, fg_color, bg_color, ..Default::default() };
284
285        self.put_char(c, attributes);
286    }
287
288    /// Move the cursor to a specific position
289    pub fn move_cursor(&mut self, row: usize, col: usize) {
290        self.cursor_position = (row, col);
291        self.ensure_cursor_position();
292        self.last_modified = Instant::now();
293    }
294
295    /// Add a new line at the cursor position
296    pub fn linefeed(&mut self) {
297        self.cursor_position.0 += 1;
298        self.ensure_cursor_position();
299        self.last_modified = Instant::now();
300    }
301
302    /// Return the cursor to the first column
303    pub fn carriage_return(&mut self) {
304        self.cursor_position.1 = 0;
305        self.last_modified = Instant::now();
306    }
307
308    /// Clear the screen
309    pub fn clear(&mut self) {
310        self.lines.clear();
311        self.lines.push_back(vec![ScreenCell::default(); self.columns]);
312        self.cursor_position = (0, 0);
313        self.last_modified = Instant::now();
314    }
315
316    /// Clear from the cursor to the end of the line
317    pub fn clear_line_forward(&mut self) {
318        let row = self.cursor_position.0;
319        let col = self.cursor_position.1;
320
321        if row < self.lines.len() {
322            for i in col..self.lines[row].len() {
323                self.lines[row][i] = ScreenCell::default();
324            }
325        }
326        self.last_modified = Instant::now();
327    }
328
329    /// Clear the current line
330    pub fn clear_line(&mut self) {
331        let row = self.cursor_position.0;
332        if row < self.lines.len() {
333            self.lines[row] = vec![ScreenCell::default(); self.columns];
334        }
335        self.last_modified = Instant::now();
336    }
337
338    /// Scroll the screen up by one line
339    pub fn scroll_up(&mut self) {
340        if !self.lines.is_empty() {
341            self.lines.pop_front();
342            self.ensure_line(self.cursor_position.0);
343        }
344        self.last_modified = Instant::now();
345    }
346
347    /// Smart truncate the screen buffer to keep it within reasonable limits
348    pub fn smart_truncate(&mut self, max_size: usize) {
349        let current_size = self.lines.len();
350
351        if current_size <= max_size {
352            return;
353        }
354
355        // Calculate how many lines to remove
356        let to_remove = current_size - max_size;
357
358        // Keep a reasonable amount at the beginning
359        let beginning_lines = max_size / 10; // 10% of max size
360
361        if to_remove <= beginning_lines {
362            // Simple case: just remove from the beginning
363            for _ in 0..to_remove {
364                self.lines.pop_front();
365            }
366        } else {
367            // Complex case: keep beginning and end, with a marker in the middle
368            let end_lines = max_size - beginning_lines - 1; // -1 for truncation marker
369
370            // Save important parts
371            let beginning: VecDeque<Vec<ScreenCell>> =
372                self.lines.drain(0..beginning_lines.min(self.lines.len())).collect();
373
374            let end_start_index = self.lines.len().saturating_sub(end_lines);
375            let end: VecDeque<Vec<ScreenCell>> = self.lines.drain(end_start_index..).collect();
376
377            // Clear and rebuild with beginning + marker + end
378            self.lines.clear();
379
380            // Add beginning
381            for line in beginning {
382                self.lines.push_back(line);
383            }
384
385            // Add truncation marker
386            let mut marker_line = vec![ScreenCell::default(); self.columns];
387            let marker_text = " [... TRUNCATED OUTPUT ...] ";
388
389            for (i, c) in marker_text.chars().enumerate() {
390                if i < self.columns {
391                    marker_line[i] = ScreenCell {
392                        character: c,
393                        style: CellStyle::BOLD.union(CellStyle::REVERSE),
394                        ..ScreenCell::default()
395                    };
396                }
397            }
398
399            self.lines.push_back(marker_line);
400
401            // Add end
402            for line in end {
403                self.lines.push_back(line);
404            }
405        }
406
407        // Adjust cursor position if necessary
408        if self.cursor_position.0 >= self.lines.len() {
409            self.cursor_position.0 = self.lines.len().saturating_sub(1);
410        }
411
412        self.last_modified = Instant::now();
413    }
414
415    /// Get the screen as plain text
416    pub fn to_plain_text(&self) -> String {
417        let mut result = String::with_capacity(self.lines.len() * self.columns);
418
419        for line in &self.lines {
420            let line_text: String = line.iter().map(|cell| cell.character).collect();
421            result.push_str(&line_text);
422            result.push('\n');
423        }
424
425        result
426    }
427
428    /// Get the screen as a vector of strings, with each string representing a line
429    pub fn display(&self) -> Vec<String> {
430        let mut result = Vec::with_capacity(self.lines.len());
431
432        for line in &self.lines {
433            let line_text: String = line.iter().map(|cell| cell.character).collect();
434
435            // Trim trailing spaces
436            let trimmed = line_text.trim_end();
437            result.push(trimmed.to_string());
438        }
439
440        // Remove empty lines from the end
441        while let Some(last) = result.last() {
442            if last.is_empty() {
443                result.pop();
444            } else {
445                break;
446            }
447        }
448
449        result
450    }
451
452    /// Returns the last time the screen was modified
453    pub fn last_modified(&self) -> Instant {
454        self.last_modified
455    }
456
457    /// Time since last modification in seconds
458    pub fn time_since_last_modified(&self) -> f64 {
459        self.last_modified.elapsed().as_secs_f64()
460    }
461}
462
463/// Terminal state performer that handles VTE events
464#[derive(Clone)]
465pub struct TerminalPerformer {
466    /// The screen state
467    screen: Arc<Mutex<Screen>>,
468    /// Current text attributes
469    attributes: ScreenCellAttributes,
470    /// SGR parameters cache for optimization
471    sgr_state: HashMap<u16, bool>,
472    /// Active hyperlink ID, if any
473    current_hyperlink_id: Option<String>,
474    /// Active hyperlink URL, if any
475    current_hyperlink_url: Option<String>,
476    /// Current OSC parameters being parsed
477    osc_params: Vec<String>,
478}
479
480// Custom debug implementation to avoid using the one from VTE
481impl std::fmt::Debug for TerminalPerformer {
482    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483        f.debug_struct("TerminalPerformer")
484            .field("attributes", &self.attributes)
485            .field("hyperlink_id", &self.current_hyperlink_id)
486            .field("hyperlink_url", &self.current_hyperlink_url)
487            .finish_non_exhaustive()
488    }
489}
490
491impl TerminalPerformer {
492    /// Creates a new terminal performer
493    pub fn new(screen: Arc<Mutex<Screen>>) -> Self {
494        Self {
495            screen,
496            attributes: ScreenCellAttributes::default(),
497            sgr_state: HashMap::new(),
498            current_hyperlink_id: None,
499            current_hyperlink_url: None,
500            osc_params: Vec::new(),
501        }
502    }
503
504    /// Get a reference to the screen
505    pub fn screen(&self) -> &Arc<Mutex<Screen>> {
506        &self.screen
507    }
508
509    /// Reset all text attributes
510    fn reset_attributes(&mut self) {
511        self.attributes = ScreenCellAttributes::default();
512        self.sgr_state.clear();
513    }
514
515    /// Reset hyperlink state
516    fn reset_hyperlink(&mut self) {
517        self.current_hyperlink_id = None;
518        self.current_hyperlink_url = None;
519        self.attributes.style.set(CellStyle::HYPERLINK, false);
520        self.attributes.hyperlink_url = None;
521    }
522
523    fn track_sgr(&mut self, param: u16) {
524        self.sgr_state.insert(param, true);
525    }
526
527    fn untrack_sgr(&mut self, params: &[u16]) {
528        for param in params {
529            self.sgr_state.remove(param);
530        }
531    }
532
533    /// Parse and handle SGR (Select Graphic Rendition) parameters
534    fn handle_sgr_params(&mut self, params: &vte::Params) {
535        if params.is_empty() {
536            self.reset_attributes();
537            return;
538        }
539
540        for param_values in params.iter().flatten() {
541            self.handle_sgr_param(*param_values);
542        }
543    }
544
545    fn handle_sgr_param(&mut self, param: u16) {
546        if self.handle_basic_sgr_style(param)
547            || self.handle_font_sgr(param)
548            || self.handle_color_sgr(param)
549            || self.handle_frame_sgr(param)
550            || self.handle_script_sgr(param)
551        {
552            return;
553        }
554
555        debug!("Unsupported SGR parameter: {}", param);
556    }
557
558    fn handle_basic_sgr_style(&mut self, param: u16) -> bool {
559        match param {
560            0 => self.reset_attributes(),
561            1 => self.attributes.style.set(CellStyle::BOLD, true),
562            2 => self.attributes.style.set(CellStyle::DIM, true),
563            3 => self.attributes.style.set(CellStyle::ITALIC, true),
564            4 => {
565                self.attributes.style.set(CellStyle::UNDERLINE, true);
566                self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, false);
567            }
568            5 | 6 => self.attributes.style.set(CellStyle::BLINK, true),
569            7 => self.attributes.style.set(CellStyle::REVERSE, true),
570            8 => self.attributes.style.set(CellStyle::CONCEAL, true),
571            9 => self.attributes.style.set(CellStyle::STRIKETHROUGH, true),
572            20 => self.attributes.style.set(CellStyle::FRAKTUR, true),
573            21 => {
574                self.attributes.style.set(CellStyle::UNDERLINE, true);
575                self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, true);
576            }
577            22 => {
578                self.attributes.style.set(CellStyle::BOLD, false);
579                self.attributes.style.set(CellStyle::DIM, false);
580                self.untrack_sgr(&[1, 2]);
581                return true;
582            }
583            23 => {
584                self.attributes.style.set(CellStyle::ITALIC, false);
585                self.attributes.style.set(CellStyle::FRAKTUR, false);
586                self.untrack_sgr(&[3, 20]);
587                return true;
588            }
589            24 => {
590                self.attributes.style.set(CellStyle::UNDERLINE, false);
591                self.attributes.style.set(CellStyle::DOUBLE_UNDERLINE, false);
592                self.untrack_sgr(&[4, 21]);
593                return true;
594            }
595            25 => {
596                self.attributes.style.set(CellStyle::BLINK, false);
597                self.untrack_sgr(&[5, 6]);
598                return true;
599            }
600            27 => self.attributes.style.set(CellStyle::REVERSE, false),
601            28 => self.attributes.style.set(CellStyle::CONCEAL, false),
602            29 => self.attributes.style.set(CellStyle::STRIKETHROUGH, false),
603            _ => return false,
604        }
605
606        self.track_sgr(param);
607        true
608    }
609
610    fn handle_font_sgr(&mut self, param: u16) -> bool {
611        match param {
612            10 => self.attributes.font = 0,
613            11..=19 => self.attributes.font = (param - 10) as u8,
614            _ => return false,
615        }
616
617        self.track_sgr(param);
618        true
619    }
620
621    fn handle_color_sgr(&mut self, param: u16) -> bool {
622        match param {
623            26 | 38 | 48 => {}
624            30..=37 => self.attributes.fg_color = Some(TerminalColor::Basic(param as u8 - 30)),
625            39 => self.attributes.fg_color = None,
626            40..=47 => self.attributes.bg_color = Some(TerminalColor::Basic(param as u8 - 40)),
627            49 => self.attributes.bg_color = None,
628            90..=97 => self.attributes.fg_color = Some(TerminalColor::Basic(param as u8 - 90 + 8)),
629            100..=107 => {
630                self.attributes.bg_color = Some(TerminalColor::Basic(param as u8 - 100 + 8));
631            }
632            _ => return false,
633        }
634
635        true
636    }
637
638    fn handle_frame_sgr(&mut self, param: u16) -> bool {
639        match param {
640            51 => {
641                self.attributes.style.set(CellStyle::FRAMED, true);
642                self.attributes.style.set(CellStyle::ENCIRCLED, false);
643            }
644            52 => {
645                self.attributes.style.set(CellStyle::FRAMED, false);
646                self.attributes.style.set(CellStyle::ENCIRCLED, true);
647            }
648            53 => self.attributes.style.set(CellStyle::OVERLINED, true),
649            54 => {
650                self.attributes.style.set(CellStyle::FRAMED, false);
651                self.attributes.style.set(CellStyle::ENCIRCLED, false);
652                self.untrack_sgr(&[51, 52]);
653                return true;
654            }
655            55 => {
656                self.attributes.style.set(CellStyle::OVERLINED, false);
657                self.untrack_sgr(&[53]);
658                return true;
659            }
660            60..=65 => {}
661            _ => return false,
662        }
663
664        self.track_sgr(param);
665        true
666    }
667
668    fn handle_script_sgr(&mut self, param: u16) -> bool {
669        match param {
670            73 => {
671                self.attributes.style.set(CellStyle::SUPERSCRIPT, true);
672                self.attributes.style.set(CellStyle::SUBSCRIPT, false);
673            }
674            74 => {
675                self.attributes.style.set(CellStyle::SUBSCRIPT, true);
676                self.attributes.style.set(CellStyle::SUPERSCRIPT, false);
677            }
678            75 => {
679                self.attributes.style.set(CellStyle::SUPERSCRIPT, false);
680                self.attributes.style.set(CellStyle::SUBSCRIPT, false);
681                self.untrack_sgr(&[73, 74]);
682                return true;
683            }
684            _ => return false,
685        }
686
687        self.track_sgr(param);
688        true
689    }
690}
691
692// Additional methods for TerminalPerformer outside of the Perform trait
693impl TerminalPerformer {
694    /// Handle SGR (Select Graphic Rendition) parameters and extended color sequences
695    fn handle_sgr_dispatch(&mut self, params: &vte::Params) {
696        // Process the basic SGR parameters
697        self.handle_sgr_params(params);
698
699        // Handle extended color params (38, 48) manually since they require sequences
700        let param_arrays: Vec<Vec<u16>> = params.iter().map(<[u16]>::to_vec).collect();
701
702        if param_arrays.len() >= 3 {
703            let mut i = 0;
704            while i < param_arrays.len() {
705                if param_arrays[i].len() == 1 {
706                    if param_arrays[i][0] == 38 && i + 2 < param_arrays.len() {
707                        // Extended foreground color
708                        if param_arrays[i + 1].len() == 1
709                            && param_arrays[i + 1][0] == 5
710                            && param_arrays[i + 2].len() == 1
711                        {
712                            // 8-bit color (256 colors)
713                            let color = param_arrays[i + 2][0] as u8;
714                            self.attributes.fg_color = Some(TerminalColor::Color256(color));
715                            i += 3;
716                            continue;
717                        } else if param_arrays[i + 1].len() == 1
718                            && param_arrays[i + 1][0] == 2
719                            && i + 4 < param_arrays.len()
720                            && param_arrays[i + 2].len() == 1
721                            && param_arrays[i + 3].len() == 1
722                            && param_arrays[i + 4].len() == 1
723                        {
724                            // 24-bit RGB color
725                            let r = param_arrays[i + 2][0] as u8;
726                            let g = param_arrays[i + 3][0] as u8;
727                            let b = param_arrays[i + 4][0] as u8;
728                            self.attributes.fg_color = Some(TerminalColor::TrueColor { r, g, b });
729                            i += 5;
730                            continue;
731                        }
732                    } else if param_arrays[i][0] == 48 && i + 2 < param_arrays.len() {
733                        // Extended background color
734                        if param_arrays[i + 1].len() == 1
735                            && param_arrays[i + 1][0] == 5
736                            && param_arrays[i + 2].len() == 1
737                        {
738                            // 8-bit color (256 colors)
739                            let color = param_arrays[i + 2][0] as u8;
740                            self.attributes.bg_color = Some(TerminalColor::Color256(color));
741                            i += 3;
742                            continue;
743                        } else if param_arrays[i + 1].len() == 1
744                            && param_arrays[i + 1][0] == 2
745                            && i + 4 < param_arrays.len()
746                            && param_arrays[i + 2].len() == 1
747                            && param_arrays[i + 3].len() == 1
748                            && param_arrays[i + 4].len() == 1
749                        {
750                            // 24-bit RGB color
751                            let r = param_arrays[i + 2][0] as u8;
752                            let g = param_arrays[i + 3][0] as u8;
753                            let b = param_arrays[i + 4][0] as u8;
754                            self.attributes.bg_color = Some(TerminalColor::TrueColor { r, g, b });
755                            i += 5;
756                            continue;
757                        }
758                    }
759                }
760                i += 1;
761            }
762        }
763    }
764
765    /// Handle OSC (Operating System Command) sequences
766    fn handle_osc_params(&mut self, params: &[&[u8]], _bell_terminated: bool) {
767        if params.is_empty() {
768            return;
769        }
770
771        // Convert the params to strings for easier handling
772        let param_strings: Vec<String> =
773            params.iter().map(|p| String::from_utf8_lossy(p).to_string()).collect();
774
775        if param_strings.is_empty() {
776            return;
777        }
778
779        // Handle known OSC sequences
780        if param_strings[0] == "8" && param_strings.len() >= 3 {
781            // OSC 8: Hyperlink
782            // Format: OSC 8 ; params ; URI ST
783
784            // Get hyperlink parameters and URL
785            let params =
786                if param_strings.len() > 1 { param_strings[1].clone() } else { String::new() };
787
788            let url =
789                if param_strings.len() > 2 { param_strings[2].clone() } else { String::new() };
790
791            // Parse parameters (id=value format)
792            let mut hyperlink_id = None;
793            for param in params.split(':') {
794                let parts: Vec<&str> = param.split('=').collect();
795                if parts.len() == 2 && parts[0] == "id" {
796                    hyperlink_id = Some(parts[1].to_string());
797                }
798            }
799
800            // Handle hyperlinks
801            if url.is_empty() {
802                // Empty URL means end of hyperlink
803                self.reset_hyperlink();
804            } else {
805                // Start of hyperlink
806                self.attributes.style.set(CellStyle::HYPERLINK, true);
807                self.attributes.hyperlink_url = Some(url.clone());
808                self.current_hyperlink_url = Some(url);
809
810                if let Some(id) = hyperlink_id {
811                    self.current_hyperlink_id = Some(id);
812                }
813            }
814        }
815        // Add support for other OSC sequences here (window title, color definitions, etc.)
816    }
817
818    fn csi_param(params: &vte::Params, index: usize, default: u16) -> u16 {
819        params.iter().nth(index).and_then(|p| p.first().copied()).unwrap_or(default)
820    }
821
822    fn handle_cursor_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
823        let n = usize::from(Self::csi_param(params, 0, 1));
824        let current_row = screen.cursor_row();
825        let current_col = screen.cursor_col();
826
827        match c {
828            'A' => screen.move_cursor(current_row.saturating_sub(n), current_col),
829            'B' => screen.move_cursor(current_row + n, current_col),
830            'C' => screen.move_cursor(current_row, current_col + n),
831            'D' => screen.move_cursor(current_row, current_col.saturating_sub(n)),
832            'H' | 'f' => {
833                let row = usize::from(Self::csi_param(params, 0, 1)).saturating_sub(1);
834                let col = usize::from(Self::csi_param(params, 1, 1)).saturating_sub(1);
835                screen.move_cursor(row, col);
836            }
837            _ => return false,
838        }
839
840        true
841    }
842
843    fn clear_line_to_cursor(screen: &mut Screen) {
844        let row = screen.cursor_row();
845        let col = screen.cursor_col();
846
847        if row < screen.lines.len() {
848            for i in 0..=col.min(screen.lines[row].len().saturating_sub(1)) {
849                screen.lines[row][i] = ScreenCell::default();
850            }
851        }
852    }
853
854    fn handle_erase_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
855        match c {
856            'J' => Self::handle_erase_display(screen, Self::csi_param(params, 0, 0)),
857            'K' => Self::handle_erase_line(screen, Self::csi_param(params, 0, 0)),
858            _ => return false,
859        }
860
861        true
862    }
863
864    fn handle_erase_display(screen: &mut Screen, mode: u16) {
865        match mode {
866            0 => {
867                screen.clear_line_forward();
868                let row = screen.cursor_row();
869                if row + 1 < screen.lines.len() {
870                    for i in row + 1..screen.lines.len() {
871                        screen.lines[i] = vec![ScreenCell::default(); screen.columns];
872                    }
873                }
874            }
875            1 => {
876                Self::clear_line_to_cursor(screen);
877                for i in 0..screen.cursor_row() {
878                    if i < screen.lines.len() {
879                        screen.lines[i] = vec![ScreenCell::default(); screen.columns];
880                    }
881                }
882            }
883            2 | 3 => screen.clear(),
884            _ => debug!("Unhandled erase in display: {}", mode),
885        }
886    }
887
888    fn handle_erase_line(screen: &mut Screen, mode: u16) {
889        match mode {
890            0 => screen.clear_line_forward(),
891            1 => Self::clear_line_to_cursor(screen),
892            2 => screen.clear_line(),
893            _ => debug!("Unhandled erase in line: {}", mode),
894        }
895    }
896
897    fn handle_scroll_csi(screen: &mut Screen, params: &vte::Params, c: char) -> bool {
898        let n = usize::from(Self::csi_param(params, 0, 1));
899
900        match c {
901            'S' => {
902                for _ in 0..n {
903                    screen.scroll_up();
904                }
905            }
906            'T' => {
907                let columns = screen.columns;
908                for _ in 0..n {
909                    screen.lines.push_front(vec![ScreenCell::default(); columns]);
910                    if screen.lines.len() > screen.max_lines {
911                        screen.lines.pop_back();
912                    }
913                }
914                screen.move_cursor(screen.cursor_row() + n, screen.cursor_col());
915            }
916            _ => return false,
917        }
918
919        true
920    }
921}
922
923// Implement the VTE Perform trait
924impl Perform for TerminalPerformer {
925    fn print(&mut self, c: char) {
926        if let Ok(mut screen) = self.screen.lock() {
927            screen.put_char(c, self.attributes.clone());
928        } else {
929            warn!("Failed to lock screen for print");
930        }
931    }
932
933    fn execute(&mut self, byte: u8) {
934        if let Ok(mut screen) = self.screen.lock() {
935            match byte {
936                b'\r' => screen.carriage_return(),
937                b'\n' => {
938                    screen.carriage_return();
939                    screen.linefeed();
940                }
941                b'\t' => {
942                    // Handle tab - advance to next 8-char boundary
943                    let current_col = screen.cursor_col();
944                    let new_col = (current_col + 8) & !7;
945                    // Get the current row first to avoid multiple borrows
946                    let current_row = screen.cursor_row();
947                    screen.move_cursor(current_row, new_col);
948                }
949                b'\x08' => {
950                    // Backspace
951                    if screen.cursor_col() > 0 {
952                        let current_row = screen.cursor_row();
953                        let new_col = screen.cursor_col() - 1;
954                        screen.move_cursor(current_row, new_col);
955                    }
956                }
957                b'\x0C' => {
958                    // Form feed - clear screen
959                    screen.clear();
960                }
961                b'\x07' => { // Bell - ignore
962                }
963                _ => {
964                    debug!("Unhandled execute: {:?}", byte);
965                }
966            }
967        } else {
968            warn!("Failed to lock screen for execute");
969        }
970    }
971
972    fn hook(&mut self, _params: &vte::Params, _intermediates: &[u8], _ignore: bool, _c: char) {
973        // Not implemented
974    }
975
976    fn put(&mut self, _byte: u8) {
977        // Not implemented
978    }
979
980    fn unhook(&mut self) {
981        // Not implemented
982    }
983
984    fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
985        // Implement OSC parameter handling
986        self.handle_osc_params(params, bell_terminated);
987    }
988
989    fn csi_dispatch(
990        &mut self,
991        params: &vte::Params,
992        _intermediates: &[u8],
993        _ignore: bool,
994        c: char,
995    ) {
996        // Special case for SGR ('m') to avoid borrowing conflict
997        if c == 'm' {
998            self.handle_sgr_dispatch(params);
999            return;
1000        }
1001
1002        if let Ok(mut screen) = self.screen.lock() {
1003            if Self::handle_cursor_csi(&mut screen, params, c)
1004                || Self::handle_erase_csi(&mut screen, params, c)
1005                || Self::handle_scroll_csi(&mut screen, params, c)
1006            {
1007                return;
1008            }
1009
1010            debug!("Unhandled CSI: {:?} {:?}", params, c);
1011        } else {
1012            warn!("Failed to lock screen for csi_dispatch");
1013        }
1014    }
1015
1016    fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
1017        if intermediates.is_empty() {
1018            match byte {
1019                b'c' => {
1020                    // RIS - Reset to Initial State
1021                    if let Ok(mut screen) = self.screen.lock() {
1022                        screen.clear();
1023                    }
1024                    self.reset_attributes();
1025                }
1026                b'7' | b'8' => {
1027                    // DECSC/DECRC - Save/restore cursor
1028                    // Not implemented yet
1029                }
1030                _ => debug!("Unhandled ESC dispatch: {:?}", byte),
1031            }
1032        }
1033    }
1034}
1035
1036/// Terminal emulator that processes input and maintains screen state
1037#[derive(Clone)]
1038pub struct TerminalEmulator {
1039    /// The performer that handles terminal events
1040    performer: TerminalPerformer,
1041    /// The shared screen state
1042    screen: Arc<Mutex<Screen>>,
1043}
1044
1045// Custom debug implementation to avoid issues with Parser
1046impl std::fmt::Debug for TerminalEmulator {
1047    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1048        f.debug_struct("TerminalEmulator")
1049            .field("performer", &self.performer)
1050            .finish_non_exhaustive()
1051    }
1052}
1053
1054impl TerminalEmulator {
1055    /// Creates a new terminal emulator
1056    pub fn new(columns: usize) -> Self {
1057        let screen = Arc::new(Mutex::new(Screen::new(columns)));
1058        let performer = TerminalPerformer::new(screen.clone());
1059
1060        Self { performer, screen }
1061    }
1062
1063    /// Creates a new terminal emulator with specified maximum lines
1064    pub fn new_with_max_lines(columns: usize, max_lines: usize) -> Self {
1065        let screen = Arc::new(Mutex::new(Screen::new_with_max_lines(columns, max_lines)));
1066        let performer = TerminalPerformer::new(screen.clone());
1067
1068        Self { performer, screen }
1069    }
1070
1071    /// Process input and update screen state
1072    pub fn process(&mut self, data: &str) {
1073        let mut parser = Parser::new();
1074
1075        // Process data in chunks to avoid excessive locking
1076        let chunk_size = 4096;
1077        let data_bytes = data.as_bytes();
1078
1079        for chunk in data_bytes.chunks(chunk_size) {
1080            parser.advance(&mut self.performer, chunk);
1081        }
1082    }
1083
1084    /// Process input with limited buffer (for large outputs)
1085    pub fn process_with_limited_buffer(&mut self, data: &str, max_lines: usize) {
1086        if let Ok(mut screen) = self.screen.lock() {
1087            // Update max_lines setting
1088            screen.max_lines = max_lines.min(MAX_SCREEN_LINES);
1089        }
1090
1091        self.process(data);
1092
1093        // After processing, check if we need to smart truncate
1094        if let Ok(mut screen) = self.screen.lock() {
1095            if screen.lines.len() > max_lines {
1096                screen.smart_truncate(max_lines);
1097            }
1098        }
1099    }
1100
1101    /// Get the current screen state
1102    pub fn get_screen(&self) -> Arc<Mutex<Screen>> {
1103        self.screen.clone()
1104    }
1105
1106    /// Get the screen contents as a vector of strings
1107    pub fn display(&self) -> Vec<String> {
1108        if let Ok(screen) = self.screen.lock() {
1109            screen.display()
1110        } else {
1111            warn!("Failed to lock screen for display");
1112            vec![]
1113        }
1114    }
1115
1116    /// Get the screen contents as plain text
1117    pub fn to_plain_text(&self) -> String {
1118        if let Ok(screen) = self.screen.lock() {
1119            screen.to_plain_text()
1120        } else {
1121            warn!("Failed to lock screen for to_plain_text");
1122            String::new()
1123        }
1124    }
1125
1126    /// Clear the screen
1127    pub fn clear(&mut self) {
1128        if let Ok(mut screen) = self.screen.lock() {
1129            screen.clear();
1130        } else {
1131            warn!("Failed to lock screen for clear");
1132        }
1133    }
1134}
1135
1136/// Map from a 64-bit text hash to (rendered output, insertion time).
1137///
1138/// Keyed by a hash of the source text rather than the text itself: a terminal
1139/// dump can be hundreds of KB, and storing it as the map key (plus cloning it
1140/// on every eviction) was the dominant cost. A hash collision only causes a
1141/// stale render to be recomputed, never corruption.
1142type CacheEntryMap = HashMap<u64, (Vec<String>, Instant)>;
1143
1144/// Hash a chunk of terminal text into the cache key space.
1145fn hash_terminal_text(text: &str) -> u64 {
1146    use std::hash::{Hash, Hasher};
1147    let mut hasher = std::collections::hash_map::DefaultHasher::new();
1148    text.hash(&mut hasher);
1149    hasher.finish()
1150}
1151
1152/// Inner cache state behind a single lock so the map and the eviction queue can
1153/// never drift out of sync.
1154#[derive(Debug, Default)]
1155struct CacheInner {
1156    map: CacheEntryMap,
1157    /// Keys in insertion order, for O(1) FIFO eviction (no O(n log n) sort).
1158    order: VecDeque<u64>,
1159}
1160
1161/// Caching system for terminal output rendering
1162#[derive(Debug, Clone)]
1163struct TerminalCache {
1164    inner: Arc<RwLock<CacheInner>>,
1165    /// Maximum number of entries in the cache
1166    max_entries: usize,
1167    /// Time-to-live for cache entries in seconds
1168    ttl: u64,
1169}
1170
1171impl TerminalCache {
1172    /// Create a new terminal cache
1173    fn new(max_entries: usize, ttl: u64) -> Self {
1174        Self { inner: Arc::new(RwLock::new(CacheInner::default())), max_entries, ttl }
1175    }
1176
1177    /// Get a cached value if available and not expired
1178    fn get(&self, text: &str) -> Option<Vec<String>> {
1179        let key = hash_terminal_text(text);
1180        let inner = self.inner.read().ok()?;
1181        let (value, timestamp) = inner.map.get(&key)?;
1182        if timestamp.elapsed().as_secs() < self.ttl {
1183            Some(value.clone())
1184        } else {
1185            None
1186        }
1187    }
1188
1189    /// Insert a value into the cache, evicting the oldest entries if over cap.
1190    fn insert(&self, text: &str, value: Vec<String>) {
1191        let key = hash_terminal_text(text);
1192        let Ok(mut inner) = self.inner.write() else {
1193            return;
1194        };
1195        if inner.map.insert(key, (value, Instant::now())).is_none() {
1196            inner.order.push_back(key);
1197        }
1198        while inner.order.len() > self.max_entries {
1199            let Some(old) = inner.order.pop_front() else {
1200                break;
1201            };
1202            inner.map.remove(&old);
1203        }
1204    }
1205
1206    /// Clear expired entries from the cache
1207    fn cleanup(&self) {
1208        let Ok(mut inner) = self.inner.write() else {
1209            return;
1210        };
1211        let ttl = self.ttl;
1212        let CacheInner { map, order } = &mut *inner;
1213        map.retain(|_, (_, timestamp)| timestamp.elapsed().as_secs() < ttl);
1214        order.retain(|key| map.contains_key(key));
1215    }
1216}
1217
1218// Initialize the global terminal cache
1219lazy_static::lazy_static! {
1220    static ref TERMINAL_CACHE: TerminalCache = TerminalCache::new(100, CACHE_TTL);
1221}
1222
1223/// Terminal output difference detector
1224#[derive(Debug, Clone)]
1225pub struct TerminalOutputDiff {
1226    /// Previous output lines
1227    previous_output: Vec<String>,
1228    /// Hash of previous output
1229    output_hash: String,
1230    /// Maximum number of lines to compare
1231    max_lines: usize,
1232}
1233
1234impl Default for TerminalOutputDiff {
1235    fn default() -> Self {
1236        Self::new()
1237    }
1238}
1239
1240impl TerminalOutputDiff {
1241    /// Create a new terminal output diff detector
1242    pub fn new() -> Self {
1243        Self { previous_output: Vec::new(), output_hash: String::new(), max_lines: 1000 }
1244    }
1245
1246    /// Create a new terminal output diff detector with specified maximum lines
1247    pub fn new_with_max_lines(max_lines: usize) -> Self {
1248        Self { previous_output: Vec::new(), output_hash: String::new(), max_lines }
1249    }
1250
1251    /// Detect changes between previous and new output
1252    pub fn detect_changes(&mut self, new_output: &[String]) -> Vec<String> {
1253        if self.previous_output.is_empty() {
1254            // First run, just return all lines
1255            self.previous_output = new_output.to_vec();
1256            self.output_hash = self.calculate_hash(new_output);
1257            return new_output.to_vec();
1258        }
1259
1260        // Check if output is identical (fast path)
1261        let new_hash = self.calculate_hash(new_output);
1262        if new_hash == self.output_hash {
1263            return Vec::new(); // No changes
1264        }
1265
1266        // Find differences
1267        let mut changes = Vec::new();
1268
1269        // Find where new content starts
1270        let nold = self.previous_output.len().min(self.max_lines);
1271        let nnew = new_output.len().min(self.max_lines);
1272
1273        // Try to find where old output ends and new output begins using a more efficient algorithm
1274        let mut matched_position = None;
1275
1276        // Check if new output contains all of old output as a prefix
1277        let is_prefix = nold <= nnew && (0..nold).all(|i| self.previous_output[i] == new_output[i]);
1278
1279        if is_prefix {
1280            // Simple case: new output is old output plus additions
1281            matched_position = Some(nold);
1282        } else {
1283            // More complex case: try to find the last matching block
1284            let mut best_match = 0;
1285            let mut best_position = 0;
1286
1287            // Use sliding window approach to find largest match
1288            let window_size = 3.min(nold); // Use 3 lines as context for matching
1289
1290            if window_size > 0 {
1291                for i in (0..=nnew.saturating_sub(window_size)).rev() {
1292                    // Try matching last window_size lines of old output with window at position i in new output
1293                    let mut match_count = 0;
1294                    for j in 0..window_size {
1295                        if i + j < nnew
1296                            && nold.saturating_sub(window_size) + j < nold
1297                            && new_output[i + j]
1298                                == self.previous_output[nold.saturating_sub(window_size) + j]
1299                        {
1300                            match_count += 1;
1301                        }
1302                    }
1303
1304                    if match_count > best_match {
1305                        best_match = match_count;
1306                        best_position = i + window_size;
1307
1308                        if best_match == window_size {
1309                            // Perfect match, no need to continue
1310                            break;
1311                        }
1312                    }
1313                }
1314            }
1315
1316            if best_match >= window_size / 2 {
1317                // Found a reasonable match
1318                matched_position = Some(best_position);
1319            }
1320        }
1321
1322        // Extract changes based on matched position
1323        if let Some(pos) = matched_position {
1324            if pos < nnew {
1325                changes = new_output[pos..].to_vec();
1326
1327                // Check if first line of changes matches last line of previous output
1328                if !changes.is_empty()
1329                    && !self.previous_output.is_empty()
1330                    && changes[0] == self.previous_output[self.previous_output.len() - 1]
1331                {
1332                    changes.remove(0);
1333                }
1334            }
1335        } else {
1336            // Fallback: couldn't find a good match, show all new lines
1337            changes = new_output.to_vec();
1338        }
1339
1340        // Update state for next comparison
1341        self.previous_output = new_output.to_vec();
1342        self.output_hash = new_hash;
1343
1344        changes
1345    }
1346
1347    /// Calculate a hash of the output lines for quick comparison
1348    fn calculate_hash(&self, lines: &[String]) -> String {
1349        // Simple hash function based on content
1350        // In a production setting, use a proper hash function
1351        let mut hasher = std::collections::hash_map::DefaultHasher::new();
1352        for line in lines.iter().take(self.max_lines) {
1353            std::hash::Hash::hash(line, &mut hasher);
1354        }
1355        format!("{:x}", std::hash::Hasher::finish(&hasher))
1356    }
1357
1358    /// Reset the diff detector
1359    pub fn reset(&mut self) {
1360        self.previous_output.clear();
1361        self.output_hash.clear();
1362    }
1363}
1364
1365/// Render terminal output with line wrapping
1366pub fn render_terminal_output(text: &str) -> Vec<String> {
1367    // Check cache first
1368    if let Some(cached) = TERMINAL_CACHE.get(text) {
1369        return cached;
1370    }
1371
1372    let mut terminal = TerminalEmulator::new(DEFAULT_COLUMNS);
1373
1374    // Check if we need to limit processing for large outputs
1375    if text.len() > MAX_OUTPUT_SIZE {
1376        // For large outputs, use limited buffer mode
1377        terminal.process_with_limited_buffer(text, DEFAULT_MAX_SCREEN_LINES);
1378    } else {
1379        terminal.process(text);
1380    }
1381
1382    let result = terminal.display();
1383
1384    // Cache the result for future use (only if reasonably sized)
1385    if text.len() < MAX_OUTPUT_SIZE {
1386        TERMINAL_CACHE.insert(text, result.clone());
1387    }
1388
1389    // Periodically clean up expired cache entries
1390    if rand::random::<u32>() % 100 == 0 {
1391        TERMINAL_CACHE.cleanup();
1392    }
1393
1394    // IMPORTANT: Strip any remaining ANSI codes from the result lines to prevent JSON-RPC errors.
1395    // This is the core fix for "invalid character '\x1b'" errors.
1396    result.into_iter().map(|line| strip_ansi_codes(&line)).collect()
1397}
1398
1399/// Get incremental text output by comparing old and new terminal states
1400pub fn incremental_text(text: &str, last_pending_output: &str) -> String {
1401    // Optimization: Quick check for empty input
1402    if text.is_empty() {
1403        return String::new();
1404    }
1405
1406    // Optimization: If last output is empty, just process everything
1407    if last_pending_output.is_empty() {
1408        // First call, return all processed lines with leading/trailing whitespace trimmed
1409        let lines = render_terminal_output(text);
1410        return lines.join("\n").trim().to_string();
1411    }
1412
1413    // Optimization: Handle case where new text is just appended to old text
1414    let is_append = text.starts_with(last_pending_output);
1415
1416    if is_append && text.len() > last_pending_output.len() {
1417        // Incremental case - only process the new part
1418        let new_part = &text[last_pending_output.len()..];
1419
1420        // Ensure we have enough context by including a bit more than just the new part
1421        let context_len = 200.min(last_pending_output.len());
1422        let full_context = if context_len > 0 {
1423            let start_pos = last_pending_output.len() - context_len;
1424            format!("{}{}", &last_pending_output[start_pos..], new_part)
1425        } else {
1426            new_part.to_string()
1427        };
1428
1429        // Process the combined output for context
1430        let previous_lines = render_terminal_output(last_pending_output);
1431        let combined_lines = render_terminal_output(&full_context);
1432
1433        // Create a diff detector for efficient comparison
1434        let mut diff_detector = TerminalOutputDiff::new();
1435        diff_detector.previous_output = previous_lines;
1436
1437        // Get just the changes
1438        let changes = diff_detector.detect_changes(&combined_lines);
1439
1440        if changes.is_empty() {
1441            return String::new();
1442        }
1443
1444        return changes.join("\n");
1445    }
1446
1447    // Fallback for non-append cases:
1448
1449    // Limit text size to prevent excessive memory usage
1450    let text_limit = if text.len() > MAX_OUTPUT_SIZE {
1451        let start_offset = text.len() - MAX_OUTPUT_SIZE;
1452
1453        // Find the start of a line to avoid cutting in the middle
1454        let adjusted_offset =
1455            text[start_offset..].find('\n').map_or(start_offset, |pos| start_offset + pos + 1);
1456
1457        &text[adjusted_offset..]
1458    } else {
1459        text
1460    };
1461
1462    // Process both old and new output
1463    let previous_lines = render_terminal_output(last_pending_output);
1464    let new_lines = render_terminal_output(text_limit);
1465
1466    // Create a diff detector for efficient comparison
1467    let mut diff_detector = TerminalOutputDiff::new();
1468    diff_detector.previous_output = previous_lines;
1469
1470    // Get the incremental changes
1471    let changes = diff_detector.detect_changes(&new_lines);
1472
1473    if changes.is_empty() {
1474        return String::new();
1475    }
1476
1477    changes.join("\n")
1478}
1479
1480/// Strip ANSI escape codes from a string using a robust regex
1481pub fn strip_ansi_codes(input: &str) -> String {
1482    static RE: std::sync::OnceLock<Option<Regex>> = std::sync::OnceLock::new();
1483
1484    // Fast path: no ESC byte means nothing to strip.
1485    if !input.contains('\u{1b}') {
1486        return input.to_string();
1487    }
1488
1489    // Cover the full escape grammar, not just SGR colors. Interactive programs
1490    // (python/node REPLs, psql, readline) emit far more than colors:
1491    //   - CSI: ESC [ <params> <intermediates> <final> (cursor moves, `?2004h`
1492    //          bracketed-paste, `2K` erase, `0m` reset, ...)
1493    //   - OSC: ESC ] ... (BEL | ST)                    (window-title sets)
1494    //   - 2-byte ESC sequences (ESC =, ESC >, ESC M, ...)
1495    // Without OSC/CSI-cursor coverage the scrollback leaked raw bracketed-paste
1496    // and `]0;user@host` noise into the model's view. Cached so we don't
1497    // recompile the pattern on every rendered line.
1498    let re = RE.get_or_init(|| {
1499        Regex::new(
1500            r"\x1b\[[0-9;:?<>=!]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[=>MNcD78]",
1501        )
1502        .ok()
1503    });
1504    let cleaned = match re {
1505        Some(re) => re.replace_all(input, "").into_owned(),
1506        None => input.to_string(),
1507    };
1508    // Defensive: drop any stray ESC the pattern didn't consume (partial seqs).
1509    cleaned.replace('\u{1b}', "")
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514    use super::*;
1515
1516    #[test]
1517    fn render_applies_cursor_left_overwrite() {
1518        // 10 Z's, move cursor 5 left, write 5 Q's -> the emulator must OVERWRITE
1519        // the last 5 Z's (this is what collapses readline echo in scrollback).
1520        let rendered = render_terminal_output("ZZZZZZZZZZ\u{1b}[5DQQQQQ").join("\n");
1521        assert!(rendered.contains("ZZZZZQQQQQ"), "cursor-left not applied; got {rendered:?}");
1522        // and crucially NOT the naive-strip result (escape removed, bytes kept)
1523        assert!(
1524            !rendered.contains("ZZZZZZZZZZQQQQQ"),
1525            "looks like a plain strip; got {rendered:?}"
1526        );
1527    }
1528
1529    #[test]
1530    fn strips_csi_osc_and_cursor_sequences() {
1531        // SGR colors
1532        assert_eq!(strip_ansi_codes("\u{1b}[1;35mhi\u{1b}[0m"), "hi");
1533        // bracketed-paste + cursor move (the REPL noise we saw leak through)
1534        assert_eq!(strip_ansi_codes("\u{1b}[?2004h>>> \u{1b}[?2004l\u{1b}[4D42"), ">>> 42");
1535        // OSC window-title, BEL-terminated
1536        assert_eq!(strip_ansi_codes("\u{1b}]0;user@host\u{7}prompt$ "), "prompt$ ");
1537        // plain text is returned untouched (fast path)
1538        assert_eq!(strip_ansi_codes("no escapes here"), "no escapes here");
1539    }
1540
1541    #[test]
1542    fn test_screen_basic_operations() {
1543        let mut screen = Screen::new(80);
1544
1545        // Create default attributes
1546        let _attributes = ScreenCellAttributes::default();
1547
1548        // Test putting characters
1549        screen.put_char_basic('H', CellStyle::default(), None, None);
1550        screen.put_char_basic('e', CellStyle::default(), None, None);
1551        screen.put_char_basic('l', CellStyle::default(), None, None);
1552        screen.put_char_basic('l', CellStyle::default(), None, None);
1553        screen.put_char_basic('o', CellStyle::default(), None, None);
1554
1555        let display = screen.display();
1556        assert_eq!(display, vec!["Hello"]);
1557
1558        // Test cursor movement
1559        screen.carriage_return();
1560        screen.linefeed();
1561
1562        screen.put_char_basic('W', CellStyle::default(), None, None);
1563        screen.put_char_basic('o', CellStyle::default(), None, None);
1564        screen.put_char_basic('r', CellStyle::default(), None, None);
1565        screen.put_char_basic('l', CellStyle::default(), None, None);
1566        screen.put_char_basic('d', CellStyle::default(), None, None);
1567
1568        let display = screen.display();
1569        assert_eq!(display, vec!["Hello", "World"]);
1570
1571        // Test clearing line
1572        screen.clear_line();
1573        let display = screen.display();
1574        assert_eq!(display, vec!["Hello"]);
1575    }
1576
1577    #[test]
1578    fn test_terminal_emulator_basic() {
1579        let mut terminal = TerminalEmulator::new(80);
1580
1581        // Test simple text processing
1582        terminal.process("Hello\r\nWorld");
1583        let display = terminal.display();
1584        assert_eq!(display, vec!["Hello", "World"]);
1585
1586        // Test escape sequences
1587        terminal.clear();
1588        terminal.process("Normal \x1b[1mBold\x1b[0m Normal");
1589        let display = terminal.display();
1590        assert_eq!(display, vec!["Normal Bold Normal"]);
1591
1592        // Test cursor movement
1593        terminal.clear();
1594        terminal.process("Hello\x1b[5D_\x1b[1C_\x1b[1C_");
1595        let display = terminal.display();
1596        assert_eq!(display, vec!["_e_l_"]);
1597    }
1598
1599    #[test]
1600    fn test_incremental_output() {
1601        let old = vec!["Line 1".to_string(), "Line 2".to_string()];
1602        let new = vec!["Line 1".to_string(), "Line 2".to_string(), "Line 3".to_string()];
1603
1604        let mut diff_detector = TerminalOutputDiff::new();
1605        diff_detector.previous_output = old;
1606
1607        let incremental = diff_detector.detect_changes(&new);
1608        assert_eq!(incremental, vec!["Line 3"]);
1609
1610        // Test with completely different content
1611        let old = vec!["Line A".to_string(), "Line B".to_string()];
1612        let new = vec!["Line X".to_string(), "Line Y".to_string()];
1613
1614        let mut diff_detector = TerminalOutputDiff::new();
1615        diff_detector.previous_output = old;
1616
1617        let incremental = diff_detector.detect_changes(&new);
1618        assert_eq!(incremental, vec!["Line X", "Line Y"]);
1619    }
1620
1621    #[test]
1622    fn test_render_terminal_output() {
1623        let text = "Hello\r\nWorld\r\n\x1b[31mRed\x1b[0m Text";
1624        let lines = render_terminal_output(text);
1625        assert_eq!(lines, vec!["Hello", "World", "Red Text"]);
1626    }
1627
1628    #[test]
1629    fn test_smart_truncate() {
1630        let mut screen = Screen::new_with_max_lines(80, 20);
1631
1632        // Add 30 lines of content
1633        for i in 0..30 {
1634            let line = format!("Line {i}");
1635            for c in line.chars() {
1636                screen.put_char(c, ScreenCellAttributes::default());
1637            }
1638            screen.carriage_return();
1639            screen.linefeed();
1640        }
1641
1642        // Should have truncated to 20 lines
1643        assert_eq!(screen.lines.len(), 20);
1644
1645        // Now test smart truncate
1646        screen.smart_truncate(10);
1647
1648        // Should now have 10 lines
1649        assert_eq!(screen.lines.len(), 10);
1650
1651        // One of the lines should be the truncation marker
1652        let has_truncation_marker = screen.lines.iter().any(|line| {
1653            let line_text: String = line.iter().map(|cell| cell.character).collect();
1654            line_text.contains("TRUNCATED")
1655        });
1656
1657        assert!(has_truncation_marker);
1658    }
1659
1660    #[test]
1661    fn test_terminal_cache() {
1662        let cache = TerminalCache::new(10, 60);
1663
1664        // Insert a value
1665        cache.insert("test", vec!["line1".to_string(), "line2".to_string()]);
1666
1667        // Should be able to retrieve it
1668        let retrieved = cache.get("test");
1669        assert_eq!(retrieved, Some(vec!["line1".to_string(), "line2".to_string()]));
1670
1671        // Unknown key should return None
1672        let not_found = cache.get("unknown");
1673        assert_eq!(not_found, None);
1674    }
1675
1676    #[test]
1677    fn test_incremental_text_append() {
1678        let old_text = "Line 1\nLine 2\n";
1679        let new_text = "Line 1\nLine 2\nLine 3\n";
1680
1681        let incremental = incremental_text(new_text, old_text);
1682        assert_eq!(incremental, "Line 3");
1683    }
1684
1685    #[test]
1686    fn test_terminal_color_handling() {
1687        let mut terminal = TerminalEmulator::new(80);
1688
1689        // Test basic colors
1690        terminal.process("\x1b[31mRed\x1b[32mGreen\x1b[0mNormal");
1691        let display = terminal.display();
1692        assert_eq!(display, vec!["RedGreenNormal"]);
1693
1694        // Test 256 colors
1695        terminal.clear();
1696        terminal.process("\x1b[38;5;208mOrange\x1b[0mNormal");
1697        let display = terminal.display();
1698        assert_eq!(display, vec!["OrangeNormal"]);
1699    }
1700}