ghostscope_ui/components/command_panel/
script_editor.rs

1use crate::action::{Action, ResponseType};
2use crate::model::panel_state::{CommandPanelState, InteractionMode, ScriptCache, ScriptStatus};
3use crate::ui::emoji::{EmojiConfig, ScriptStatus as EmojiScriptStatus, TraceElement};
4use crate::ui::strings::UIStrings;
5
6/// Detailed information for successful trace operations
7#[derive(Debug, Clone)]
8pub struct TraceDetails {
9    pub trace_id: Option<u32>,
10    pub binary_path: Option<String>,
11    pub address: Option<u64>,
12    pub source_file: Option<String>,
13    pub line_number: Option<u32>,
14    pub function_name: Option<String>,
15}
16
17/// Detailed error information for failed trace operations
18#[derive(Debug, Clone)]
19pub struct TraceErrorDetails {
20    pub compilation_errors: Option<Vec<(u32, String)>>, // (line_number, error_message)
21    pub uprobe_error: Option<String>,
22    pub suggestion: Option<String>,
23}
24
25/// Handles script editing functionality for the command panel
26pub struct ScriptEditor;
27
28impl ScriptEditor {
29    /// Enter script editing mode for a trace command
30    pub fn enter_script_mode(state: &mut CommandPanelState, command: &str) -> Vec<Action> {
31        let rest = command.trim_start_matches("trace").trim();
32        // Support optional index: trace <target> [index]
33        let mut parts = rest.split_whitespace();
34        let base_target = parts.next().unwrap_or("");
35        let index_opt = parts.next().and_then(|s| s.parse::<usize>().ok());
36
37        if base_target.is_empty() {
38            let plain =
39                "Usage: trace <function_name|file:line|0xADDR|module_suffix:0xADDR>".to_string();
40            let styled = vec![
41                crate::components::command_panel::style_builder::StyledLineBuilder::new()
42                    .styled(
43                        plain.clone(),
44                        crate::components::command_panel::style_builder::StylePresets::ERROR,
45                    )
46                    .build(),
47            ];
48            return vec![Action::AddResponseWithStyle {
49                content: plain,
50                styled_lines: Some(styled),
51                response_type: ResponseType::Error,
52            }];
53        }
54
55        // Check if we have a cached script for this target
56        let (lines, cursor_line, cursor_col, restored_from_cache) =
57            if let Some(ref cache) = state.script_cache {
58                if let Some(saved_script) = cache.saved_scripts.get(base_target) {
59                    let mut lines: Vec<String> =
60                        saved_script.content.lines().map(String::from).collect();
61                    // Ensure at least one line exists (empty string.lines() returns empty iterator)
62                    if lines.is_empty() {
63                        lines.push(String::new());
64                    }
65                    (
66                        lines,
67                        saved_script.cursor_line,
68                        saved_script.cursor_col,
69                        true,
70                    )
71                } else {
72                    (vec![String::new()], 0, 0, false)
73                }
74            } else {
75                (vec![String::new()], 0, 0, false)
76            };
77
78        // Create or update script cache
79        state.script_cache = Some(ScriptCache {
80            target: base_target.to_string(),
81            original_command: command.to_string(),
82            selected_index: index_opt,
83            lines,
84            cursor_line,
85            cursor_col,
86            status: ScriptStatus::Draft,
87            saved_scripts: state
88                .script_cache
89                .as_ref()
90                .map(|c| c.saved_scripts.clone())
91                .unwrap_or_default(),
92        });
93
94        // Switch to script editor mode
95        state.mode = InteractionMode::ScriptEditor;
96
97        let message = if restored_from_cache {
98            if let Some(idx) = index_opt {
99                format!("📝 Script editor opened for '{base_target}' (index {idx}, restored)\nPress Ctrl+S to submit, ESC to cancel")
100            } else {
101                format!("📝 Script editor opened for '{base_target}' (restored from cache)\nPress Ctrl+S to submit, ESC to cancel")
102            }
103        } else if let Some(idx) = index_opt {
104            format!("📝 Script editor opened for '{base_target}' (index {idx})\nPress Ctrl+S to submit, ESC to cancel")
105        } else {
106            format!("📝 Script editor opened for '{base_target}'\nPress Ctrl+S to submit, ESC to cancel")
107        };
108
109        let styled = vec![
110            crate::components::command_panel::style_builder::StyledLineBuilder::new()
111                .styled(
112                    message.clone(),
113                    crate::components::command_panel::style_builder::StylePresets::TIP,
114                )
115                .build(),
116        ];
117
118        vec![Action::AddResponseWithStyle {
119            content: message,
120            styled_lines: Some(styled),
121            response_type: ResponseType::Info,
122        }]
123    }
124
125    /// Exit script editing mode
126    pub fn exit_script_mode(state: &mut CommandPanelState) -> Vec<Action> {
127        // Save current script state before exiting (always save, even if empty)
128        if let Some(ref mut cache) = state.script_cache {
129            let script_content = cache.lines.join("\n");
130            // Always save script state to cache, even if empty
131            // This ensures second entry will properly restore cursor and content
132            cache.saved_scripts.insert(
133                cache.target.clone(),
134                crate::model::panel_state::SavedScript {
135                    content: script_content,
136                    cursor_line: cache.cursor_line,
137                    cursor_col: cache.cursor_col,
138                },
139            );
140        }
141
142        state.mode = InteractionMode::Input;
143
144        let plain = "Script editing cancelled".to_string();
145        let styled = vec![
146            crate::components::command_panel::style_builder::StyledLineBuilder::new()
147                .styled(
148                    plain.clone(),
149                    crate::components::command_panel::style_builder::StylePresets::WARNING,
150                )
151                .build(),
152        ];
153
154        vec![Action::AddResponseWithStyle {
155            content: plain,
156            styled_lines: Some(styled),
157            response_type: ResponseType::Warning,
158        }]
159    }
160
161    /// Submit the current script
162    pub fn submit_script(state: &mut CommandPanelState) -> Vec<Action> {
163        if let Some(ref mut cache) = state.script_cache {
164            // Remove empty lines at the end
165            while cache
166                .lines
167                .last()
168                .is_some_and(|line| line.trim().is_empty())
169            {
170                cache.lines.pop();
171            }
172
173            // Ensure at least one line
174            if cache.lines.is_empty() {
175                cache.lines.push(String::new());
176            }
177
178            let script_content = cache.lines.join("\n");
179            // Wrap script content in braces for proper syntax
180            let wrapped_script = if script_content.trim().is_empty() {
181                "{}".to_string()
182            } else {
183                format!("{{{script_content}}}")
184            };
185            let full_script = format!("trace {target} {wrapped_script}", target = cache.target);
186
187            // Save script to cache with cursor position
188            cache.saved_scripts.insert(
189                cache.target.clone(),
190                crate::model::panel_state::SavedScript {
191                    content: script_content,
192                    cursor_line: cache.cursor_line,
193                    cursor_col: cache.cursor_col,
194                },
195            );
196            cache.status = ScriptStatus::Submitted;
197
198            // Set waiting state for trace command
199            state.input_state = crate::model::panel_state::InputState::WaitingResponse {
200                command: if let Some(idx) = cache.selected_index {
201                    format!("trace {} {}", cache.target, idx)
202                } else {
203                    format!("trace {}", cache.target)
204                },
205                sent_time: std::time::Instant::now(),
206                command_type: crate::model::panel_state::CommandType::Script,
207            };
208
209            // Exit script editor mode
210            state.mode = InteractionMode::Input;
211
212            return vec![Action::SendRuntimeCommand(
213                crate::action::RuntimeCommand::ExecuteScript {
214                    command: full_script,
215                    selected_index: cache.selected_index,
216                },
217            )];
218        }
219
220        let plain = "No script to submit".to_string();
221        let styled = vec![
222            crate::components::command_panel::style_builder::StyledLineBuilder::new()
223                .styled(
224                    plain.clone(),
225                    crate::components::command_panel::style_builder::StylePresets::ERROR,
226                )
227                .build(),
228        ];
229
230        vec![Action::AddResponseWithStyle {
231            content: plain,
232            styled_lines: Some(styled),
233            response_type: ResponseType::Error,
234        }]
235    }
236
237    /// Clear the current script
238    pub fn clear_script(state: &mut CommandPanelState) -> Vec<Action> {
239        if let Some(ref mut cache) = state.script_cache {
240            cache.lines = vec![String::new()];
241            cache.cursor_line = 0;
242            cache.cursor_col = 0;
243            cache.status = ScriptStatus::Draft;
244        }
245
246        Vec::new()
247    }
248
249    /// Insert a character at the cursor position in script
250    pub fn insert_char(state: &mut CommandPanelState, c: char) -> Vec<Action> {
251        if let Some(ref mut cache) = state.script_cache {
252            if cache.cursor_line < cache.lines.len() {
253                let line = &mut cache.lines[cache.cursor_line];
254                let byte_pos = Self::char_pos_to_byte_pos(line, cache.cursor_col);
255                line.insert(byte_pos, c);
256                cache.cursor_col += 1;
257            }
258        }
259        Vec::new()
260    }
261
262    /// Insert a string at the cursor position (supports newlines)
263    pub fn insert_text(state: &mut CommandPanelState, text: &str) -> Vec<Action> {
264        if let Some(ref mut cache) = state.script_cache {
265            // Normalize line endings to LF
266            let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
267            let mut iter = normalized.split('\n');
268
269            if cache.cursor_line >= cache.lines.len() {
270                cache.lines.push(String::new());
271                cache.cursor_line = cache.lines.len() - 1;
272                cache.cursor_col = 0;
273            }
274
275            // Insert first segment into current line
276            if let Some(first) = iter.next() {
277                let line = &mut cache.lines[cache.cursor_line];
278                let byte_pos = Self::char_pos_to_byte_pos(line, cache.cursor_col);
279                line.insert_str(byte_pos, first);
280                cache.cursor_col += first.chars().count();
281            }
282
283            // Remaining segments: create new lines
284            let mut is_first_newline = true;
285            for seg in iter {
286                // Split current line at cursor once to move the right part down
287                if is_first_newline {
288                    let byte_pos = Self::char_pos_to_byte_pos(
289                        &cache.lines[cache.cursor_line],
290                        cache.cursor_col,
291                    );
292                    let right = cache.lines[cache.cursor_line].split_off(byte_pos);
293                    let new_line = format!("{seg}{right}");
294                    cache.cursor_line += 1;
295                    cache.lines.insert(cache.cursor_line, new_line);
296                    cache.cursor_col = seg.chars().count();
297                    is_first_newline = false;
298                } else {
299                    cache.cursor_line += 1;
300                    cache.lines.insert(cache.cursor_line, seg.to_string());
301                    cache.cursor_col = seg.chars().count();
302                }
303            }
304        }
305        Vec::new()
306    }
307
308    /// Insert a newline at the cursor position
309    pub fn insert_newline(state: &mut CommandPanelState) -> Vec<Action> {
310        if let Some(ref mut cache) = state.script_cache {
311            if cache.cursor_line < cache.lines.len() {
312                let byte_pos =
313                    Self::char_pos_to_byte_pos(&cache.lines[cache.cursor_line], cache.cursor_col);
314
315                // Split the current line at cursor position
316                let current_line = cache.lines[cache.cursor_line].clone();
317                let (left, right) = current_line.split_at(byte_pos);
318                cache.lines[cache.cursor_line] = left.to_string();
319                cache.lines.insert(cache.cursor_line + 1, right.to_string());
320
321                // Move cursor to beginning of new line
322                cache.cursor_line += 1;
323                cache.cursor_col = 0;
324            }
325        }
326        Vec::new()
327    }
328
329    /// Insert a tab (4 spaces) at the cursor position
330    pub fn insert_tab(state: &mut CommandPanelState) -> Vec<Action> {
331        if let Some(ref mut cache) = state.script_cache {
332            if cache.cursor_line < cache.lines.len() {
333                let line = &mut cache.lines[cache.cursor_line];
334                let byte_pos = Self::char_pos_to_byte_pos(line, cache.cursor_col);
335                line.insert_str(byte_pos, "    ");
336                cache.cursor_col += 4;
337            }
338        }
339        Vec::new()
340    }
341
342    /// Delete character before cursor in script
343    pub fn delete_char(state: &mut CommandPanelState) -> Vec<Action> {
344        if let Some(ref mut cache) = state.script_cache {
345            if cache.cursor_col > 0 {
346                // Delete character in current line
347                if cache.cursor_line < cache.lines.len() {
348                    let line = &mut cache.lines[cache.cursor_line];
349                    cache.cursor_col -= 1;
350                    let byte_pos = Self::char_pos_to_byte_pos(line, cache.cursor_col);
351                    if byte_pos < line.len() {
352                        let mut end_pos = byte_pos + 1;
353                        while end_pos < line.len() && !line.is_char_boundary(end_pos) {
354                            end_pos += 1;
355                        }
356                        line.drain(byte_pos..end_pos);
357                    }
358                }
359            } else if cache.cursor_line > 0 {
360                // Merge with previous line
361                let current_line = cache.lines.remove(cache.cursor_line);
362                cache.cursor_line -= 1;
363                cache.cursor_col = cache.lines[cache.cursor_line].chars().count();
364                cache.lines[cache.cursor_line].push_str(&current_line);
365            }
366        }
367        Vec::new()
368    }
369
370    /// Move cursor up in script
371    pub fn move_cursor_up(state: &mut CommandPanelState) -> Vec<Action> {
372        if let Some(ref mut cache) = state.script_cache {
373            if cache.cursor_line > 0 {
374                cache.cursor_line -= 1;
375                let line_len = cache.lines[cache.cursor_line].chars().count();
376                cache.cursor_col = cache.cursor_col.min(line_len);
377            }
378        }
379        Vec::new()
380    }
381
382    /// Move cursor down in script
383    pub fn move_cursor_down(state: &mut CommandPanelState) -> Vec<Action> {
384        if let Some(ref mut cache) = state.script_cache {
385            if cache.cursor_line + 1 < cache.lines.len() {
386                cache.cursor_line += 1;
387                let line_len = cache.lines[cache.cursor_line].chars().count();
388                cache.cursor_col = cache.cursor_col.min(line_len);
389            }
390        }
391        Vec::new()
392    }
393
394    /// Move cursor left in script
395    pub fn move_cursor_left(state: &mut CommandPanelState) -> Vec<Action> {
396        if let Some(ref mut cache) = state.script_cache {
397            if cache.cursor_col > 0 {
398                cache.cursor_col -= 1;
399            } else if cache.cursor_line > 0 {
400                // Move to end of previous line
401                cache.cursor_line -= 1;
402                cache.cursor_col = cache.lines[cache.cursor_line].chars().count();
403            }
404        }
405        Vec::new()
406    }
407
408    /// Move cursor right in script
409    pub fn move_cursor_right(state: &mut CommandPanelState) -> Vec<Action> {
410        if let Some(ref mut cache) = state.script_cache {
411            if cache.cursor_line < cache.lines.len() {
412                let line_len = cache.lines[cache.cursor_line].chars().count();
413                if cache.cursor_col < line_len {
414                    cache.cursor_col += 1;
415                } else if cache.cursor_line + 1 < cache.lines.len() {
416                    // Move to beginning of next line
417                    cache.cursor_line += 1;
418                    cache.cursor_col = 0;
419                }
420            }
421        }
422        Vec::new()
423    }
424
425    /// Move cursor to beginning of line
426    pub fn move_to_beginning(state: &mut CommandPanelState) -> Vec<Action> {
427        if let Some(ref mut cache) = state.script_cache {
428            cache.cursor_col = 0;
429        }
430        Vec::new()
431    }
432
433    /// Move cursor to end of line
434    pub fn move_to_end(state: &mut CommandPanelState) -> Vec<Action> {
435        if let Some(ref mut cache) = state.script_cache {
436            if cache.cursor_line < cache.lines.len() {
437                cache.cursor_col = cache.lines[cache.cursor_line].chars().count();
438            }
439        }
440        Vec::new()
441    }
442
443    /// Move cursor to next word (Ctrl+f)
444    pub fn move_to_next_word(state: &mut CommandPanelState) -> Vec<Action> {
445        if let Some(ref mut cache) = state.script_cache {
446            if cache.cursor_line < cache.lines.len() {
447                let line = &cache.lines[cache.cursor_line];
448                let chars: Vec<char> = line.chars().collect();
449                let mut pos = cache.cursor_col;
450
451                // Skip current word if we're in the middle of one
452                while pos < chars.len() && !chars[pos].is_whitespace() {
453                    pos += 1;
454                }
455                // Skip whitespace
456                while pos < chars.len() && chars[pos].is_whitespace() {
457                    pos += 1;
458                }
459
460                cache.cursor_col = pos;
461            }
462        }
463        Vec::new()
464    }
465
466    /// Move cursor to previous word (Ctrl+b)
467    pub fn move_to_previous_word(state: &mut CommandPanelState) -> Vec<Action> {
468        if let Some(ref mut cache) = state.script_cache {
469            if cache.cursor_line < cache.lines.len() {
470                let line = &cache.lines[cache.cursor_line];
471                let chars: Vec<char> = line.chars().collect();
472                let mut pos = cache.cursor_col;
473
474                // Skip whitespace backwards
475                while pos > 0 && chars[pos - 1].is_whitespace() {
476                    pos -= 1;
477                }
478                // Skip current word backwards
479                while pos > 0 && !chars[pos - 1].is_whitespace() {
480                    pos -= 1;
481                }
482
483                cache.cursor_col = pos;
484            }
485        }
486        Vec::new()
487    }
488
489    /// Delete previous word (Ctrl+w)
490    pub fn delete_previous_word(state: &mut CommandPanelState) -> Vec<Action> {
491        if let Some(ref mut cache) = state.script_cache {
492            if cache.cursor_line < cache.lines.len() {
493                let line = &mut cache.lines[cache.cursor_line];
494                let start_pos = cache.cursor_col;
495                let mut end_pos = start_pos;
496
497                // Find start of current word
498                let chars: Vec<char> = line.chars().collect();
499                while end_pos > 0 && chars[end_pos - 1].is_whitespace() {
500                    end_pos -= 1;
501                }
502                while end_pos > 0 && !chars[end_pos - 1].is_whitespace() {
503                    end_pos -= 1;
504                }
505
506                // Convert to byte positions
507                let start_byte = Self::char_pos_to_byte_pos(line, end_pos);
508                let end_byte = Self::char_pos_to_byte_pos(line, start_pos);
509
510                // Remove the word
511                line.drain(start_byte..end_byte);
512                cache.cursor_col = end_pos;
513            }
514        }
515        Vec::new()
516    }
517
518    /// Delete from cursor to end of line
519    pub fn delete_to_end(state: &mut CommandPanelState) -> Vec<Action> {
520        if let Some(ref mut cache) = state.script_cache {
521            if cache.cursor_line < cache.lines.len() {
522                let line = &mut cache.lines[cache.cursor_line];
523                let byte_pos = Self::char_pos_to_byte_pos(line, cache.cursor_col);
524                line.truncate(byte_pos);
525            }
526        }
527        Vec::new()
528    }
529
530    /// Delete from cursor to beginning of line (Ctrl+u)
531    pub fn delete_to_line_start(state: &mut CommandPanelState) -> Vec<Action> {
532        if let Some(ref mut cache) = state.script_cache {
533            if cache.cursor_line < cache.lines.len() {
534                let line = &mut cache.lines[cache.cursor_line];
535                let byte_pos = Self::char_pos_to_byte_pos(line, cache.cursor_col);
536                let remaining = line[byte_pos..].to_string();
537                cache.lines[cache.cursor_line] = remaining;
538                cache.cursor_col = 0;
539            }
540        }
541        Vec::new()
542    }
543
544    /// Delete from cursor to beginning of line (legacy name for compatibility)
545    pub fn delete_to_beginning(state: &mut CommandPanelState) -> Vec<Action> {
546        Self::delete_to_line_start(state)
547    }
548
549    /// Delete character at cursor position (Ctrl+h - backspace)
550    pub fn delete_char_at_cursor(state: &mut CommandPanelState) -> Vec<Action> {
551        if let Some(ref mut cache) = state.script_cache {
552            if cache.cursor_line < cache.lines.len() && cache.cursor_col > 0 {
553                let line = &mut cache.lines[cache.cursor_line];
554                let char_pos = cache.cursor_col - 1;
555                let byte_pos = Self::char_pos_to_byte_pos(line, char_pos);
556                let next_byte_pos = Self::char_pos_to_byte_pos(line, cache.cursor_col);
557
558                // Remove the character at cursor-1 position
559                line.drain(byte_pos..next_byte_pos);
560                cache.cursor_col = char_pos;
561            }
562        }
563        Vec::new()
564    }
565
566    /// Check if script editor can be re-entered
567    pub fn can_edit_script(state: &CommandPanelState) -> bool {
568        state
569            .script_cache
570            .as_ref()
571            .is_some_and(|cache| cache.status == ScriptStatus::Submitted)
572    }
573
574    /// Re-enter script editing mode for last submitted script
575    pub fn edit_script_again(state: &mut CommandPanelState) -> Vec<Action> {
576        if let Some(ref mut cache) = state.script_cache {
577            if cache.status == ScriptStatus::Submitted {
578                cache.status = ScriptStatus::Draft;
579                state.mode = InteractionMode::ScriptEditor;
580
581                let plain = format!("📝 Re-editing script for '{}'", cache.target);
582                let styled = vec![
583                    crate::components::command_panel::style_builder::StyledLineBuilder::new()
584                        .styled(
585                            plain.clone(),
586                            crate::components::command_panel::style_builder::StylePresets::TIP,
587                        )
588                        .build(),
589                ];
590
591                return vec![Action::AddResponseWithStyle {
592                    content: plain,
593                    styled_lines: Some(styled),
594                    response_type: ResponseType::Info,
595                }];
596            }
597        }
598        Vec::new()
599    }
600
601    /// Format script for display with emoji configuration
602    pub fn format_script_display_with_config(
603        target: &str,
604        lines: &[String],
605        emoji_config: &EmojiConfig,
606    ) -> String {
607        let mut result = Vec::new();
608
609        // Header with target info
610        let target_emoji = emoji_config.get_trace_element(TraceElement::Target);
611
612        result.push(format!(
613            "{} {} {}",
614            target_emoji,
615            UIStrings::SCRIPT_TARGET_PREFIX,
616            target
617        ));
618        result.push(UIStrings::SCRIPT_SEPARATOR.repeat(50));
619
620        // Script content with line numbers
621        if lines.is_empty() || (lines.len() == 1 && lines[0].trim().is_empty()) {
622            result.push(format!(
623                "  {} No script content",
624                emoji_config.get_script_status(EmojiScriptStatus::Error)
625            ));
626        } else {
627            for (line_idx, line) in lines.iter().enumerate() {
628                if line.trim().is_empty() {
629                    result.push(format!("{:3} │", line_idx + 1));
630                } else {
631                    result.push(format!("{:3} │ {line}", line_idx + 1));
632                }
633            }
634        }
635
636        result.push(UIStrings::SCRIPT_SEPARATOR.repeat(50));
637
638        // Footer with compilation status
639        let compile_emoji = emoji_config.get_script_status(EmojiScriptStatus::Compiling);
640        result.push(format!("{compile_emoji} Compiling and loading script..."));
641
642        result.join("\n")
643    }
644
645    /// Format trace success response with detailed information
646    pub fn format_trace_success_response(
647        target: &str,
648        details: Option<&TraceDetails>,
649        emoji_config: &EmojiConfig,
650    ) -> String {
651        Self::format_trace_success_response_with_script(target, details, None, emoji_config)
652    }
653
654    pub fn format_trace_success_response_with_script(
655        target: &str,
656        details: Option<&TraceDetails>,
657        script_content: Option<&str>,
658        emoji_config: &EmojiConfig,
659    ) -> String {
660        let mut result = Vec::new();
661
662        // 📝 Script section
663        if let Some(script) = script_content {
664            let script_lines = Self::format_script_display_section(script, emoji_config);
665            result.extend(script_lines);
666        }
667
668        // 🎯 Target line
669        let target_emoji = emoji_config.get_trace_element(crate::ui::emoji::TraceElement::Target);
670        result.push(format!("{target_emoji} Target: {target}"));
671
672        // Empty line for separation
673        result.push("".to_string());
674
675        // ✅ Results line with trace details
676        let success_emoji = emoji_config.get_script_status(crate::ui::emoji::ScriptStatus::Success);
677
678        if let Some(details) = details {
679            let address_display = if let Some(address) = details.address {
680                format!("(0x{address:x})")
681            } else {
682                "".to_string()
683            };
684
685            result.push(format!(
686                "{success_emoji} Trace Results: \x1b[32m1 successful\x1b[0m, 0 failed"
687            ));
688
689            if let Some(trace_id) = details.trace_id {
690                result.push(format!(
691                    "  • {target} {address_display} → trace_id: {trace_id}"
692                ));
693            } else {
694                result.push(format!("  • {target} {address_display} → trace attached"));
695            }
696        } else {
697            result.push(format!(
698                "{success_emoji} Trace Results: \x1b[32m1 successful\x1b[0m, 0 failed"
699            ));
700            result.push(format!("  • {target} → trace attached"));
701        }
702
703        result.join("\n")
704    }
705
706    /// Format compilation results with all successful and failed traces
707    pub fn format_compilation_results(
708        compilation_details: &crate::events::ScriptCompilationDetails,
709        script_content: Option<&str>,
710        emoji_config: &EmojiConfig,
711    ) -> String {
712        let mut result = Vec::new();
713
714        // 📝 Script section
715        if let Some(script) = script_content {
716            let script_lines = Self::format_script_display_section(script, emoji_config);
717            result.extend(script_lines);
718        }
719
720        // 🎯 Target line (use first result's target or generic message)
721        let target_emoji = emoji_config.get_trace_element(crate::ui::emoji::TraceElement::Target);
722        let target = compilation_details
723            .results
724            .first()
725            .map(|r| r.target_name.clone())
726            .unwrap_or_else(|| "unknown".to_string());
727        result.push(format!("{target_emoji} Target: {target}"));
728
729        // Empty line for separation
730        result.push("".to_string());
731
732        // ✅ Results summary
733        let success_emoji = emoji_config.get_script_status(crate::ui::emoji::ScriptStatus::Success);
734        let error_emoji = emoji_config.get_script_status(crate::ui::emoji::ScriptStatus::Error);
735
736        let summary_emoji = if compilation_details.failed_count > 0 {
737            error_emoji
738        } else {
739            success_emoji
740        };
741
742        // Format with colors: green for successful (if > 0), red for failed (if > 0)
743        let success_part = if compilation_details.success_count > 0 {
744            format!(
745                "\x1b[32m{} successful\x1b[0m",
746                compilation_details.success_count
747            )
748        } else {
749            format!("{} successful", compilation_details.success_count)
750        };
751
752        let failed_part = if compilation_details.failed_count > 0 {
753            format!("\x1b[31m{} failed\x1b[0m", compilation_details.failed_count)
754        } else {
755            format!("{} failed", compilation_details.failed_count)
756        };
757
758        result.push(format!(
759            "{summary_emoji} Trace Results: {success_part}, {failed_part}"
760        ));
761
762        // List all successful traces
763        let mut trace_idx = 0;
764        for exec_result in &compilation_details.results {
765            match &exec_result.status {
766                crate::events::ExecutionStatus::Success => {
767                    let trace_id = compilation_details.trace_ids.get(trace_idx).copied();
768                    // Build classification + source string if available
769                    let class_part = match exec_result.is_inline {
770                        Some(true) => "inline",
771                        Some(false) => "call",
772                        None => "",
773                    };
774                    let src_part = match (&exec_result.source_file, exec_result.source_line) {
775                        (Some(f), Some(l)) => format!(" @ {f}:{l}"),
776                        _ => String::new(),
777                    };
778
779                    if let Some(tid) = trace_id {
780                        if class_part.is_empty() && src_part.is_empty() {
781                            result.push(format!(
782                                "  • {} (0x{:x}) → trace_id: {}",
783                                exec_result.target_name, exec_result.pc_address, tid
784                            ));
785                        } else {
786                            result.push(format!(
787                                "  • {} (0x{:x}) — {}{} → trace_id: {}",
788                                exec_result.target_name,
789                                exec_result.pc_address,
790                                class_part,
791                                src_part,
792                                tid
793                            ));
794                        }
795                        trace_idx += 1;
796                    } else if class_part.is_empty() && src_part.is_empty() {
797                        result.push(format!(
798                            "  • {} (0x{:x}) → trace attached",
799                            exec_result.target_name, exec_result.pc_address
800                        ));
801                    } else {
802                        result.push(format!(
803                            "  • {} (0x{:x}) — {}{} → trace attached",
804                            exec_result.target_name, exec_result.pc_address, class_part, src_part
805                        ));
806                    }
807                }
808                crate::events::ExecutionStatus::Failed(error) => {
809                    result.push(format!(
810                        "  ✗ {} (0x{:x}): {}",
811                        exec_result.target_name, exec_result.pc_address, error
812                    ));
813                }
814                crate::events::ExecutionStatus::Skipped(reason) => {
815                    result.push(format!(
816                        "  ⊘ {} (0x{:x}): {}",
817                        exec_result.target_name, exec_result.pc_address, reason
818                    ));
819                }
820            }
821        }
822
823        result.join("\n")
824    }
825
826    /// Format trace error response with detailed information
827    pub fn format_trace_error_response(
828        target: &str,
829        error: &str,
830        details: Option<&TraceErrorDetails>,
831        emoji_config: &EmojiConfig,
832    ) -> String {
833        Self::format_trace_error_response_with_script(target, error, details, None, emoji_config)
834    }
835
836    pub fn format_trace_error_response_with_script(
837        target: &str,
838        error: &str,
839        _details: Option<&TraceErrorDetails>,
840        script_content: Option<&str>,
841        emoji_config: &EmojiConfig,
842    ) -> String {
843        let mut result = Vec::new();
844
845        // 📝 Script section (if available)
846        if let Some(script) = script_content {
847            let script_lines = Self::format_script_display_section(script, emoji_config);
848            result.extend(script_lines);
849        }
850
851        // 🎯 Target line
852        let target_emoji = emoji_config.get_trace_element(crate::ui::emoji::TraceElement::Target);
853        result.push(format!("{target_emoji} Target: {target}"));
854
855        // Empty line for separation
856        result.push("".to_string());
857
858        // ❌ Error summary
859        let error_emoji = emoji_config.get_script_status(crate::ui::emoji::ScriptStatus::Error);
860        result.push(format!(
861            "{error_emoji} Trace Results: 0 successful, \x1b[31m1 failed\x1b[0m"
862        ));
863
864        // Simplified error message (remove redundant prefixes)
865        let clean_error = if error.contains("eBPF Loading Error:") {
866            error.replace("eBPF Loading Error:", "").trim().to_string()
867        } else if error.contains("Uprobe Attachment Error:") {
868            error
869                .replace("Uprobe Attachment Error:", "")
870                .trim()
871                .to_string()
872        } else if error.contains("Code Generation Error:") {
873            error
874                .replace("Code Generation Error:", "")
875                .trim()
876                .to_string()
877        } else if error.contains("Script compilation failed") {
878            error
879                .replace("Script compilation failed", "")
880                .trim()
881                .to_string()
882        } else {
883            error.to_string()
884        };
885
886        result.push(format!("  • {target} → {clean_error}"));
887
888        result.join("\n")
889    }
890
891    /// Format script display section with line numbers (like script mode)
892    fn format_script_display_section(script: &str, emoji_config: &EmojiConfig) -> Vec<String> {
893        let mut result = Vec::new();
894        let script_emoji = emoji_config.get_trace_element(crate::ui::emoji::TraceElement::Line);
895
896        if script.trim().is_empty() {
897            result.push(format!("{script_emoji} Script: {{}}"));
898        } else {
899            let script_lines: Vec<&str> = script.lines().collect();
900            if script_lines.len() == 1 {
901                // Single line - compact format
902                result.push(format!("{script_emoji} Script: {}", script.trim()));
903            } else {
904                // Multi-line - use block format with line numbers (similar to script mode)
905                result.push(format!("{script_emoji} Script:"));
906                for (idx, line) in script_lines.iter().enumerate() {
907                    result.push(format!("  {:2} │ {line}", idx + 1));
908                }
909            }
910        }
911
912        result
913    }
914
915    /// Utility function to convert character position to byte position
916    fn char_pos_to_byte_pos(text: &str, char_pos: usize) -> usize {
917        text.char_indices()
918            .nth(char_pos)
919            .map_or(text.len(), |(pos, _)| pos)
920    }
921}