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