jsonrpc_debugger/
ui.rs

1use ratatui::{
2    layout::{Constraint, Direction, Layout, Margin, Rect},
3    style::{Color, Modifier, Style},
4    text::{Line, Span},
5    widgets::{
6        Block, Borders, Cell, Clear, List, ListItem, Paragraph, Row, Table, TableState, Wrap,
7    },
8    Frame,
9};
10
11use crate::app::{App, AppMode, InputMode, TransportType};
12
13// Helper function to format JSON with syntax highlighting and 2-space indentation
14fn format_json_with_highlighting(json_value: &serde_json::Value) -> Vec<Line<'static>> {
15    // Use the standard pretty formatter
16    let json_str = match serde_json::to_string_pretty(json_value) {
17        Ok(s) => s,
18        Err(_) => return vec![Line::from("Failed to format JSON")],
19    };
20
21    let mut lines = Vec::new();
22
23    for (line_num, line) in json_str.lines().enumerate() {
24        // Limit total lines to prevent UI issues
25        if line_num > 1000 {
26            lines.push(Line::from(Span::styled(
27                "... (content truncated)",
28                Style::default().fg(Color::Gray),
29            )));
30            break;
31        }
32
33        // Don't trim the line - work with it as-is to preserve indentation
34        let mut spans = Vec::new();
35        let mut chars = line.chars().peekable();
36        let mut current_token = String::new();
37
38        while let Some(ch) = chars.next() {
39            match ch {
40                '"' => {
41                    // Flush any accumulated token (including spaces)
42                    if !current_token.is_empty() {
43                        spans.push(Span::raw(current_token.clone()));
44                        current_token.clear();
45                    }
46
47                    // Collect the entire string
48                    let mut string_content = String::from("\"");
49                    for string_ch in chars.by_ref() {
50                        string_content.push(string_ch);
51                        if string_ch == '"' && !string_content.ends_with("\\\"") {
52                            break;
53                        }
54                    }
55
56                    // Check if this is a key (followed by colon)
57                    let peek_chars = chars.clone();
58                    let mut found_colon = false;
59                    for peek_ch in peek_chars {
60                        if peek_ch == ':' {
61                            found_colon = true;
62                            break;
63                        } else if !peek_ch.is_whitespace() {
64                            break;
65                        }
66                    }
67
68                    if found_colon {
69                        // This is a key
70                        spans.push(Span::styled(
71                            string_content,
72                            Style::default()
73                                .fg(Color::Cyan)
74                                .add_modifier(Modifier::BOLD),
75                        ));
76                    } else {
77                        // This is a string value
78                        spans.push(Span::styled(
79                            string_content,
80                            Style::default().fg(Color::Green),
81                        ));
82                    }
83                }
84                ':' => {
85                    if !current_token.is_empty() {
86                        spans.push(Span::raw(current_token.clone()));
87                        current_token.clear();
88                    }
89                    spans.push(Span::styled(":", Style::default().fg(Color::White)));
90                }
91                ',' => {
92                    if !current_token.is_empty() {
93                        spans.push(Span::raw(current_token.clone()));
94                        current_token.clear();
95                    }
96                    spans.push(Span::styled(",", Style::default().fg(Color::White)));
97                }
98                '{' | '}' | '[' | ']' => {
99                    if !current_token.is_empty() {
100                        spans.push(Span::raw(current_token.clone()));
101                        current_token.clear();
102                    }
103                    spans.push(Span::styled(
104                        ch.to_string(),
105                        Style::default()
106                            .fg(Color::Yellow)
107                            .add_modifier(Modifier::BOLD),
108                    ));
109                }
110                _ => {
111                    // Accumulate all other characters including spaces
112                    current_token.push(ch);
113                }
114            }
115        }
116
117        // Handle any remaining token (including trailing spaces)
118        if !current_token.is_empty() {
119            let trimmed_token = current_token.trim();
120            if trimmed_token == "true" || trimmed_token == "false" {
121                spans.push(Span::styled(
122                    current_token,
123                    Style::default().fg(Color::Magenta),
124                ));
125            } else if trimmed_token == "null" {
126                spans.push(Span::styled(current_token, Style::default().fg(Color::Red)));
127            } else if trimmed_token.parse::<f64>().is_ok() {
128                spans.push(Span::styled(
129                    current_token,
130                    Style::default().fg(Color::Blue),
131                ));
132            } else {
133                // This includes spaces and other whitespace - preserve as-is
134                spans.push(Span::raw(current_token));
135            }
136        }
137
138        lines.push(Line::from(spans));
139    }
140
141    lines
142}
143
144pub fn draw(f: &mut Frame, app: &App) {
145    // Calculate footer height dynamically
146    let keybinds = get_keybinds_for_mode(app);
147    let available_width = f.size().width as usize;
148    let line_spans = arrange_keybinds_responsive(keybinds, available_width);
149    let footer_height = (line_spans.len() + 2).max(3); // +2 for borders, minimum 3
150
151    let chunks = Layout::default()
152        .direction(Direction::Vertical)
153        .constraints([
154            Constraint::Length(3),                    // Header
155            Constraint::Min(10),                      // Main content
156            Constraint::Length(footer_height as u16), // Dynamic footer height
157            Constraint::Length(1),                    // Input dialog
158        ])
159        .split(f.size());
160
161    draw_header(f, chunks[0], app);
162
163    // Choose layout based on app mode
164    match app.app_mode {
165        AppMode::Normal => {
166            draw_main_content(f, chunks[1], app);
167        }
168        AppMode::Paused | AppMode::Intercepting => {
169            draw_intercept_content(f, chunks[1], app);
170        }
171    }
172
173    draw_footer(f, chunks[2], app);
174
175    // Draw input dialogs
176    if app.input_mode == InputMode::EditingTarget {
177        draw_input_dialog(f, app, "Edit Target URL", "Target URL");
178    } else if app.input_mode == InputMode::FilteringRequests {
179        draw_input_dialog(f, app, "Filter Requests", "Filter");
180    }
181}
182
183fn draw_header(f: &mut Frame, area: Rect, app: &App) {
184    let status = if app.is_running { "RUNNING" } else { "STOPPED" };
185    let status_color = if app.is_running {
186        Color::Green
187    } else {
188        Color::Red
189    };
190
191    let mode_text = match app.app_mode {
192        AppMode::Normal => String::new(),
193        AppMode::Paused => " | Mode: PAUSED".to_string(),
194        AppMode::Intercepting => format!(
195            " | Mode: INTERCEPTING ({} pending)",
196            app.pending_requests.len()
197        ),
198    };
199    let mode_color = match app.app_mode {
200        AppMode::Normal => Color::White,
201        AppMode::Paused => Color::Yellow,
202        AppMode::Intercepting => Color::Red,
203    };
204
205    let header_text = vec![Line::from(vec![
206        Span::raw("JSON-RPC Debugger | Status: "),
207        Span::styled(
208            status,
209            Style::default()
210                .fg(status_color)
211                .add_modifier(Modifier::BOLD),
212        ),
213        Span::raw(format!(
214            " | Port: {} | Target: {} | Filter: {}",
215            app.proxy_config.listen_port, app.proxy_config.target_url, app.filter_text
216        )),
217        Span::styled(
218            mode_text,
219            Style::default().fg(mode_color).add_modifier(Modifier::BOLD),
220        ),
221    ])];
222
223    let header =
224        Paragraph::new(header_text).block(Block::default().borders(Borders::ALL).title("Status"));
225
226    f.render_widget(header, area);
227}
228
229fn draw_main_content(f: &mut Frame, area: Rect, app: &App) {
230    let chunks = Layout::default()
231        .direction(Direction::Horizontal)
232        .constraints([
233            Constraint::Percentage(50), // Message list
234            Constraint::Percentage(50), // Message details
235        ])
236        .split(area);
237
238    draw_message_list(f, chunks[0], app);
239    draw_message_details(f, chunks[1], app);
240}
241
242fn draw_message_list(f: &mut Frame, area: Rect, app: &App) {
243    if app.exchanges.is_empty() {
244        let empty_message = if app.is_running {
245            format!(
246                "Proxy is running on port {}. Waiting for JSON-RPC requests...",
247                app.proxy_config.listen_port
248            )
249        } else {
250            "Press 's' to start the proxy and begin capturing messages".to_string()
251        };
252
253        let paragraph = Paragraph::new(empty_message.as_str())
254            .block(Block::default().borders(Borders::ALL).title("JSON-RPC"))
255            .style(Style::default().fg(Color::Gray))
256            .wrap(Wrap { trim: true });
257
258        f.render_widget(paragraph, area);
259        return;
260    }
261
262    // Create table headers
263    let header = Row::new(vec![
264        Cell::from("Status"),
265        Cell::from("Transport"),
266        Cell::from("Method"),
267        Cell::from("ID"),
268        Cell::from("Duration"),
269    ])
270    .style(Style::default().add_modifier(Modifier::BOLD))
271    .height(1);
272
273    // Create table rows
274    let rows: Vec<Row> = app
275        .exchanges
276        .iter()
277        .enumerate()
278        .filter(|(_, exchange)| {
279            if app.filter_text.is_empty() {
280                true
281            } else {
282                // TODO: Filter by id, params, result, error, etc.
283                exchange
284                    .method
285                    .as_deref()
286                    .unwrap_or("")
287                    .contains(&app.filter_text)
288            }
289        })
290        .map(|(i, exchange)| {
291            let transport_symbol = match exchange.transport {
292                TransportType::Http => "HTTP",
293                TransportType::WebSocket => "WS",
294            };
295
296            let method = exchange.method.as_deref().unwrap_or("unknown");
297            let id = exchange
298                .id
299                .as_ref()
300                .map(|v| match v {
301                    serde_json::Value::String(s) => s.clone(),
302                    serde_json::Value::Number(n) => n.to_string(),
303                    _ => v.to_string(),
304                })
305                .unwrap_or_else(|| "null".to_string());
306
307            // Determine status
308            let (status_symbol, status_color) = if exchange.response.is_none() {
309                ("⏳ Pending", Color::Yellow)
310            } else if let Some(response) = &exchange.response {
311                if response.error.is_some() {
312                    ("✗ Error", Color::Red)
313                } else {
314                    ("✓ Success", Color::Green)
315                }
316            } else {
317                ("? Unknown", Color::Gray)
318            };
319
320            // Calculate duration if we have both request and response
321            let duration_text =
322                if let (Some(request), Some(response)) = (&exchange.request, &exchange.response) {
323                    match response.timestamp.duration_since(request.timestamp) {
324                        Ok(duration) => {
325                            let millis = duration.as_millis();
326                            if millis < 1000 {
327                                format!("{}ms", millis)
328                            } else {
329                                format!("{:.2}s", duration.as_secs_f64())
330                            }
331                        }
332                        Err(_) => "-".to_string(),
333                    }
334                } else {
335                    "-".to_string()
336                };
337
338            let style = if i == app.selected_exchange {
339                Style::default()
340                    .bg(Color::Cyan)
341                    .fg(Color::Black)
342                    .add_modifier(Modifier::BOLD)
343            } else {
344                Style::default()
345            };
346
347            Row::new(vec![
348                Cell::from(status_symbol).style(Style::default().fg(status_color)),
349                Cell::from(transport_symbol).style(Style::default().fg(Color::Blue)),
350                Cell::from(method).style(Style::default().fg(Color::Red)),
351                Cell::from(id).style(Style::default().fg(Color::Gray)),
352                Cell::from(duration_text).style(Style::default().fg(Color::Magenta)),
353            ])
354            .style(style)
355            .height(1)
356        })
357        .collect();
358
359    let table = Table::new(
360        rows,
361        [
362            Constraint::Length(12), // Status
363            Constraint::Length(9),  // Transport
364            Constraint::Min(15),    // Method (flexible)
365            Constraint::Length(12), // ID
366            Constraint::Length(10), // Duration
367        ],
368    )
369    .header(header)
370    .block(Block::default().borders(Borders::ALL).title("JSON-RPC"))
371    .highlight_style(
372        Style::default()
373            .bg(Color::Cyan)
374            .fg(Color::Black)
375            .add_modifier(Modifier::BOLD),
376    )
377    .highlight_symbol("→ ");
378
379    let mut table_state = TableState::default();
380    table_state.select(Some(app.selected_exchange));
381    f.render_stateful_widget(table, area, &mut table_state);
382}
383
384fn draw_message_details(f: &mut Frame, area: Rect, app: &App) {
385    let content = if let Some(exchange) = app.get_selected_exchange() {
386        let mut lines = Vec::new();
387
388        // Basic exchange info
389        lines.push(Line::from(vec![
390            Span::styled("Transport: ", Style::default().add_modifier(Modifier::BOLD)),
391            Span::raw(format!("{:?}", exchange.transport)),
392        ]));
393
394        if let Some(method) = &exchange.method {
395            lines.push(Line::from(vec![
396                Span::styled("Method: ", Style::default().add_modifier(Modifier::BOLD)),
397                Span::raw(method.clone()),
398            ]));
399        }
400
401        if let Some(id) = &exchange.id {
402            lines.push(Line::from(vec![
403                Span::styled("ID: ", Style::default().add_modifier(Modifier::BOLD)),
404                Span::raw(id.to_string()),
405            ]));
406        }
407
408        // Request details
409        if let Some(request) = &exchange.request {
410            lines.push(Line::from(""));
411            lines.push(Line::from(Span::styled(
412                "REQUEST:",
413                Style::default()
414                    .add_modifier(Modifier::BOLD)
415                    .fg(Color::Green),
416            )));
417
418            // Show HTTP headers if available
419            if let Some(headers) = &request.headers {
420                lines.push(Line::from(""));
421                lines.push(Line::from("HTTP Headers:"));
422                for (key, value) in headers {
423                    lines.push(Line::from(format!("  {}: {}", key, value)));
424                }
425            }
426
427            // Build and show the complete JSON-RPC request object
428            lines.push(Line::from(""));
429            lines.push(Line::from(Span::styled(
430                "JSON-RPC Request:",
431                Style::default()
432                    .fg(Color::Green)
433                    .add_modifier(Modifier::BOLD),
434            )));
435            lines.push(Line::from(""));
436            let mut request_json = serde_json::Map::new();
437            request_json.insert(
438                "jsonrpc".to_string(),
439                serde_json::Value::String("2.0".to_string()),
440            );
441
442            if let Some(id) = &request.id {
443                request_json.insert("id".to_string(), id.clone());
444            }
445            if let Some(method) = &request.method {
446                request_json.insert(
447                    "method".to_string(),
448                    serde_json::Value::String(method.clone()),
449                );
450            }
451            if let Some(params) = &request.params {
452                request_json.insert("params".to_string(), params.clone());
453            }
454
455            let request_json_value = serde_json::Value::Object(request_json);
456            let request_json_lines = format_json_with_highlighting(&request_json_value);
457            for line in request_json_lines {
458                lines.push(line);
459            }
460        }
461
462        // Response details
463        if let Some(response) = &exchange.response {
464            lines.push(Line::from(""));
465            lines.push(Line::from(Span::styled(
466                "RESPONSE:",
467                Style::default()
468                    .add_modifier(Modifier::BOLD)
469                    .fg(Color::Blue),
470            )));
471
472            // Show HTTP headers if available
473            if let Some(headers) = &response.headers {
474                lines.push(Line::from(""));
475                lines.push(Line::from("HTTP Headers:"));
476                for (key, value) in headers {
477                    lines.push(Line::from(format!("  {}: {}", key, value)));
478                }
479            }
480
481            // Build and show the complete JSON-RPC response object
482            lines.push(Line::from(""));
483            lines.push(Line::from(Span::styled(
484                "JSON-RPC Response:",
485                Style::default()
486                    .fg(Color::Blue)
487                    .add_modifier(Modifier::BOLD),
488            )));
489            lines.push(Line::from(""));
490            let mut response_json = serde_json::Map::new();
491            response_json.insert(
492                "jsonrpc".to_string(),
493                serde_json::Value::String("2.0".to_string()),
494            );
495
496            if let Some(id) = &response.id {
497                response_json.insert("id".to_string(), id.clone());
498            }
499            if let Some(result) = &response.result {
500                response_json.insert("result".to_string(), result.clone());
501            }
502            if let Some(error) = &response.error {
503                response_json.insert("error".to_string(), error.clone());
504            }
505
506            let response_json_value = serde_json::Value::Object(response_json);
507            let response_json_lines = format_json_with_highlighting(&response_json_value);
508            for line in response_json_lines {
509                lines.push(line);
510            }
511        } else {
512            lines.push(Line::from(""));
513            lines.push(Line::from(Span::styled(
514                "RESPONSE: Pending...",
515                Style::default()
516                    .add_modifier(Modifier::BOLD)
517                    .fg(Color::Yellow),
518            )));
519        }
520
521        lines
522    } else {
523        vec![Line::from("No request selected")]
524    };
525
526    // Calculate visible area for scrolling
527    let inner_area = area.inner(&Margin {
528        vertical: 1,
529        horizontal: 1,
530    });
531    let visible_lines = inner_area.height as usize;
532    let total_lines = content.len();
533
534    // Apply scrolling offset
535    let start_line = app.details_scroll;
536    let end_line = std::cmp::min(start_line + visible_lines, total_lines);
537    let visible_content = if start_line < total_lines {
538        content[start_line..end_line].to_vec()
539    } else {
540        vec![]
541    };
542
543    // Create title with scroll indicator
544    let scroll_info = if total_lines > visible_lines {
545        let progress =
546            ((app.details_scroll as f32 / (total_lines - visible_lines) as f32) * 100.0) as u8;
547        format!("Details ({}% - vim: j/k/d/u/G/g)", progress)
548    } else {
549        "Details".to_string()
550    };
551
552    let details = Paragraph::new(visible_content)
553        .block(Block::default().borders(Borders::ALL).title(scroll_info))
554        .wrap(Wrap { trim: false });
555
556    f.render_widget(details, area);
557}
558
559// Helper struct to represent a keybind with its display information
560#[derive(Clone)]
561struct KeybindInfo {
562    key: String,
563    description: String,
564    priority: u8, // Lower number = higher priority
565}
566
567impl KeybindInfo {
568    fn new(key: &str, description: &str, priority: u8) -> Self {
569        Self {
570            key: key.to_string(),
571            description: description.to_string(),
572            priority,
573        }
574    }
575
576    // Calculate the display width of this keybind (key + description + separators)
577    fn display_width(&self) -> usize {
578        self.key.len() + 1 + self.description.len() + 3 // " | " separator
579    }
580
581    // Convert to spans for rendering
582    fn to_spans(&self) -> Vec<Span<'static>> {
583        vec![
584            Span::styled(
585                self.key.clone(),
586                Style::default()
587                    .fg(Color::Yellow)
588                    .add_modifier(Modifier::BOLD),
589            ),
590            Span::raw(format!(" {} | ", self.description)),
591        ]
592    }
593}
594
595fn get_keybinds_for_mode(app: &App) -> Vec<KeybindInfo> {
596    let mut keybinds = vec![
597        // Essential keybinds (priority 1)
598        KeybindInfo::new("q", "quit", 1),
599        KeybindInfo::new("↑↓", "navigate", 1),
600        KeybindInfo::new("s", "start/stop proxy", 1),
601        // Navigation keybinds (priority 2)
602        KeybindInfo::new("^n/^p", "navigate", 2),
603        KeybindInfo::new("t", "edit target", 2),
604        KeybindInfo::new("/", "filter", 2),
605        KeybindInfo::new("p", "pause", 2),
606        // Advanced keybinds (priority 3)
607        KeybindInfo::new("j/k/d/u/G/g", "scroll details", 3),
608    ];
609
610    // Add context-specific keybinds (priority 4)
611    match app.app_mode {
612        AppMode::Paused | AppMode::Intercepting => {
613            // Only show intercept controls if there are pending requests
614            if !app.pending_requests.is_empty() {
615                keybinds.extend(vec![
616                    KeybindInfo::new("a", "allow", 4),
617                    KeybindInfo::new("e", "edit", 4),
618                    KeybindInfo::new("h", "headers", 4),
619                    KeybindInfo::new("c", "complete", 4),
620                    KeybindInfo::new("b", "block", 4),
621                    KeybindInfo::new("r", "resume", 4),
622                ]);
623            }
624        }
625        AppMode::Normal => {
626            keybinds.push(KeybindInfo::new("c", "create request", 4));
627        }
628    }
629
630    keybinds
631}
632
633fn arrange_keybinds_responsive(
634    keybinds: Vec<KeybindInfo>,
635    available_width: usize,
636) -> Vec<Vec<Span<'static>>> {
637    let mut lines = Vec::new();
638    let mut current_line_spans = Vec::new();
639    let mut current_line_width = 0;
640
641    // Account for border padding (2 chars for left/right borders)
642    let usable_width = available_width.saturating_sub(4);
643
644    // Sort keybinds by priority
645    let mut sorted_keybinds = keybinds;
646    sorted_keybinds.sort_by_key(|k| k.priority);
647
648    for (i, keybind) in sorted_keybinds.iter().enumerate() {
649        let keybind_width = keybind.display_width();
650        let is_last = i == sorted_keybinds.len() - 1;
651
652        // Check if this keybind fits on the current line
653        let width_needed = if is_last {
654            keybind_width - 3 // Remove " | " from last item
655        } else {
656            keybind_width
657        };
658
659        if current_line_width + width_needed <= usable_width || current_line_spans.is_empty() {
660            // Add to current line
661            let mut spans = keybind.to_spans();
662            if is_last {
663                // Remove the trailing " | " from the last keybind
664                if let Some(last_span) = spans.last_mut() {
665                    if let Some(content) = last_span.content.strip_suffix(" | ") {
666                        *last_span = Span::raw(content.to_string());
667                    }
668                }
669            }
670            current_line_spans.extend(spans);
671            current_line_width += width_needed;
672        } else {
673            // Start a new line
674            // Remove trailing " | " from the last span of the current line
675            if let Some(last_span) = current_line_spans.last_mut() {
676                if let Some(content) = last_span.content.strip_suffix(" | ") {
677                    *last_span = Span::raw(content.to_string());
678                }
679            }
680
681            lines.push(current_line_spans);
682            current_line_spans = keybind.to_spans();
683            current_line_width = keybind_width;
684
685            // If this is the last keybind, remove trailing separator
686            if is_last {
687                if let Some(last_span) = current_line_spans.last_mut() {
688                    if let Some(content) = last_span.content.strip_suffix(" | ") {
689                        *last_span = Span::raw(content.to_string());
690                    }
691                }
692            }
693        }
694    }
695
696    // Add the last line if it has content
697    if !current_line_spans.is_empty() {
698        lines.push(current_line_spans);
699    }
700
701    lines
702}
703
704fn draw_footer(f: &mut Frame, area: Rect, app: &App) {
705    let keybinds = get_keybinds_for_mode(app);
706    let available_width = area.width as usize;
707
708    let line_spans = arrange_keybinds_responsive(keybinds, available_width);
709
710    // Convert spans to Lines
711    let footer_text: Vec<Line> = line_spans.into_iter().map(Line::from).collect();
712
713    let footer =
714        Paragraph::new(footer_text).block(Block::default().borders(Borders::ALL).title("Controls"));
715
716    f.render_widget(footer, area);
717}
718
719fn draw_input_dialog(f: &mut Frame, app: &App, title: &str, label: &str) {
720    let area = f.size();
721
722    // Create a centered popup
723    let popup_area = Rect {
724        x: area.width / 4,
725        y: area.height / 2 - 3,
726        width: area.width / 2,
727        height: 7,
728    };
729
730    // Clear the entire screen first
731    f.render_widget(Clear, area);
732
733    // Render a black background
734    let background = Block::default().style(Style::default().bg(Color::Black));
735    f.render_widget(background, area);
736
737    // Clear the popup area specifically
738    f.render_widget(Clear, popup_area);
739
740    let input_text = vec![
741        Line::from(""),
742        Line::from(vec![
743            Span::raw(format!("{}: ", label)),
744            Span::styled(&app.input_buffer, Style::default().fg(Color::Green)),
745        ]),
746        Line::from(""),
747        Line::from(Span::styled(
748            "Press Enter to confirm, Esc to cancel",
749            Style::default().fg(Color::Gray),
750        )),
751        Line::from(""),
752    ];
753
754    let input_dialog = Paragraph::new(input_text)
755        .block(
756            Block::default()
757                .borders(Borders::ALL)
758                .title(title)
759                .style(Style::default().fg(Color::White).bg(Color::DarkGray)),
760        )
761        .wrap(Wrap { trim: true });
762
763    f.render_widget(input_dialog, popup_area);
764}
765
766fn draw_intercept_content(f: &mut Frame, area: Rect, app: &App) {
767    let chunks = Layout::default()
768        .direction(Direction::Horizontal)
769        .constraints([
770            Constraint::Percentage(50), // Pending requests list
771            Constraint::Percentage(50), // Request details/editor
772        ])
773        .split(area);
774
775    draw_pending_requests(f, chunks[0], app);
776    draw_request_details(f, chunks[1], app);
777}
778
779fn draw_pending_requests(f: &mut Frame, area: Rect, app: &App) {
780    if app.pending_requests.is_empty() {
781        let mode_text = match app.app_mode {
782            AppMode::Paused => "Pause mode active. New requests will be intercepted.",
783            _ => "No pending requests.",
784        };
785
786        let paragraph = Paragraph::new(mode_text)
787            .block(
788                Block::default()
789                    .borders(Borders::ALL)
790                    .title("Pending Requests"),
791            )
792            .style(Style::default().fg(Color::Yellow))
793            .wrap(Wrap { trim: true });
794
795        f.render_widget(paragraph, area);
796        return;
797    }
798
799    let requests: Vec<ListItem> = app
800        .pending_requests
801        .iter()
802        .enumerate()
803        .filter(|(_, pending)| {
804            if app.filter_text.is_empty() {
805                true
806            } else {
807                // Filter pending requests by method name (same as main list)
808                pending
809                    .original_request
810                    .method
811                    .as_deref()
812                    .unwrap_or("")
813                    .contains(&app.filter_text)
814            }
815        })
816        .map(|(i, pending)| {
817            let method = pending
818                .original_request
819                .method
820                .as_deref()
821                .unwrap_or("unknown");
822            let id = pending
823                .original_request
824                .id
825                .as_ref()
826                .map(|v| v.to_string())
827                .unwrap_or_else(|| "null".to_string());
828
829            let style = if i == app.selected_pending {
830                Style::default()
831                    .bg(Color::Cyan)
832                    .fg(Color::Black)
833                    .add_modifier(Modifier::BOLD)
834            } else {
835                Style::default()
836            };
837
838            // Show different icon if request has been modified
839            let (icon, icon_color) =
840                if pending.modified_request.is_some() || pending.modified_headers.is_some() {
841                    ("✏ ", Color::Blue) // Modified
842                } else {
843                    ("⏸ ", Color::Red) // Paused/Intercepted
844                };
845
846            let mut modification_labels = Vec::new();
847            if pending.modified_request.is_some() {
848                modification_labels.push("BODY");
849            }
850            if pending.modified_headers.is_some() {
851                modification_labels.push("HEADERS");
852            }
853            let modification_text = if !modification_labels.is_empty() {
854                format!(" [{}]", modification_labels.join("+"))
855            } else {
856                String::new()
857            };
858
859            ListItem::new(Line::from(vec![
860                Span::styled(icon, Style::default().fg(icon_color)),
861                Span::styled(format!("{} ", method), Style::default().fg(Color::Red)),
862                Span::styled(format!("(id: {})", id), Style::default().fg(Color::Gray)),
863                if !modification_text.is_empty() {
864                    Span::styled(
865                        modification_text,
866                        Style::default()
867                            .fg(Color::Blue)
868                            .add_modifier(Modifier::BOLD),
869                    )
870                } else {
871                    Span::raw("")
872                },
873            ]))
874            .style(style)
875        })
876        .collect();
877
878    let requests_list = List::new(requests)
879        .block(
880            Block::default()
881                .borders(Borders::ALL)
882                .title(format!("Pending Requests ({})", app.pending_requests.len())),
883        )
884        .highlight_style(
885            Style::default()
886                .bg(Color::Cyan)
887                .fg(Color::Black)
888                .add_modifier(Modifier::BOLD),
889        );
890
891    f.render_widget(requests_list, area);
892}
893
894fn draw_request_details(f: &mut Frame, area: Rect, app: &App) {
895    let content = if let Some(pending) = app.get_selected_pending() {
896        let mut lines = Vec::new();
897
898        if pending.modified_request.is_some() || pending.modified_headers.is_some() {
899            lines.push(Line::from(Span::styled(
900                "MODIFIED REQUEST:",
901                Style::default()
902                    .add_modifier(Modifier::BOLD)
903                    .fg(Color::Blue),
904            )));
905        } else {
906            lines.push(Line::from(Span::styled(
907                "INTERCEPTED REQUEST:",
908                Style::default().add_modifier(Modifier::BOLD).fg(Color::Red),
909            )));
910        }
911        lines.push(Line::from(""));
912
913        // Show headers section
914        lines.push(Line::from(Span::styled(
915            "HTTP Headers:",
916            Style::default()
917                .add_modifier(Modifier::BOLD)
918                .fg(Color::Green),
919        )));
920        let headers_to_show = pending
921            .modified_headers
922            .as_ref()
923            .or(pending.original_request.headers.as_ref());
924
925        if let Some(headers) = headers_to_show {
926            for (key, value) in headers {
927                lines.push(Line::from(format!("  {}: {}", key, value)));
928            }
929            if pending.modified_headers.is_some() {
930                lines.push(Line::from(Span::styled(
931                    "  [Headers have been modified]",
932                    Style::default()
933                        .fg(Color::Blue)
934                        .add_modifier(Modifier::ITALIC),
935                )));
936            }
937        } else {
938            lines.push(Line::from("  No headers"));
939        }
940        lines.push(Line::from(""));
941
942        // Show JSON-RPC body section
943        lines.push(Line::from(Span::styled(
944            "JSON-RPC Request:",
945            Style::default()
946                .add_modifier(Modifier::BOLD)
947                .fg(Color::Green),
948        )));
949
950        // Show the modified request if available, otherwise show original
951        let json_to_show = if let Some(ref modified_json) = pending.modified_request {
952            if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(modified_json) {
953                parsed
954            } else {
955                // Fallback to original if modified JSON is invalid
956                let mut request_json = serde_json::Map::new();
957                request_json.insert(
958                    "jsonrpc".to_string(),
959                    serde_json::Value::String("2.0".to_string()),
960                );
961
962                if let Some(id) = &pending.original_request.id {
963                    request_json.insert("id".to_string(), id.clone());
964                }
965                if let Some(method) = &pending.original_request.method {
966                    request_json.insert(
967                        "method".to_string(),
968                        serde_json::Value::String(method.clone()),
969                    );
970                }
971                if let Some(params) = &pending.original_request.params {
972                    request_json.insert("params".to_string(), params.clone());
973                }
974
975                serde_json::Value::Object(request_json)
976            }
977        } else {
978            // Show original request
979            let mut request_json = serde_json::Map::new();
980            request_json.insert(
981                "jsonrpc".to_string(),
982                serde_json::Value::String("2.0".to_string()),
983            );
984
985            if let Some(id) = &pending.original_request.id {
986                request_json.insert("id".to_string(), id.clone());
987            }
988            if let Some(method) = &pending.original_request.method {
989                request_json.insert(
990                    "method".to_string(),
991                    serde_json::Value::String(method.clone()),
992                );
993            }
994            if let Some(params) = &pending.original_request.params {
995                request_json.insert("params".to_string(), params.clone());
996            }
997
998            serde_json::Value::Object(request_json)
999        };
1000
1001        let request_json_lines = format_json_with_highlighting(&json_to_show);
1002        for line in request_json_lines {
1003            lines.push(line);
1004        }
1005
1006        lines.push(Line::from(""));
1007        lines.push(Line::from(Span::styled(
1008            "Actions:",
1009            Style::default().add_modifier(Modifier::BOLD),
1010        )));
1011        lines.push(Line::from("• Press 'a' to Allow request"));
1012        lines.push(Line::from("• Press 'e' to Edit request body"));
1013        lines.push(Line::from("• Press 'h' to Edit headers"));
1014        lines.push(Line::from("• Press 'c' to Complete with custom response"));
1015        lines.push(Line::from("• Press 'b' to Block request"));
1016        lines.push(Line::from("• Press 'r' to Resume all requests"));
1017
1018        lines
1019    } else {
1020        vec![Line::from("No request selected")]
1021    };
1022
1023    // Calculate visible area for scrolling
1024    let inner_area = area.inner(&Margin {
1025        vertical: 1,
1026        horizontal: 1,
1027    });
1028    let visible_lines = inner_area.height as usize;
1029    let total_lines = content.len();
1030
1031    // Apply scrolling offset
1032    let start_line = app.intercept_details_scroll;
1033    let end_line = std::cmp::min(start_line + visible_lines, total_lines);
1034    let visible_content = if start_line < total_lines {
1035        content[start_line..end_line].to_vec()
1036    } else {
1037        vec![]
1038    };
1039
1040    // Create title with scroll indicator
1041    let scroll_info = if total_lines > visible_lines {
1042        let progress = ((app.intercept_details_scroll as f32
1043            / (total_lines - visible_lines) as f32)
1044            * 100.0) as u8;
1045        format!("Request Details ({}% - vim: j/k/d/u/G/g)", progress)
1046    } else {
1047        "Request Details".to_string()
1048    };
1049
1050    let details = Paragraph::new(visible_content)
1051        .block(Block::default().borders(Borders::ALL).title(scroll_info))
1052        .wrap(Wrap { trim: false });
1053
1054    f.render_widget(details, area);
1055}