Skip to main content

hematite/ui/
modal_review.rs

1use ratatui::{
2    layout::{Constraint, Direction, Layout, Rect},
3    style::{Color, Modifier, Style, Stylize},
4    widgets::{Block, Borders, Clear, Paragraph, Wrap},
5    Frame,
6};
7
8pub struct ActiveReview {
9    pub worker_id: String,
10    pub file_path: String,
11    pub before: String,
12    pub after: String,
13    pub tx: tokio::sync::oneshot::Sender<crate::agent::swarm::ReviewResponse>,
14}
15
16pub fn draw_diff_review(f: &mut Frame, review: &ActiveReview) {
17    let area = centered_rect(80, 80, f.area());
18    f.render_widget(Clear, area);
19
20    // ── Outer Frame ──────────────────────────────────────────────
21    let rust_color = Color::Rgb(100, 100, 100);
22    let outer_block = Block::default()
23        .borders(Borders::ALL)
24        .border_style(Style::default().fg(rust_color))
25        .style(Style::default().bg(Color::Rgb(25, 25, 25))); // Obsidian background
26
27    let inner_area = outer_block.inner(area);
28    f.render_widget(outer_block, area);
29
30    // ── Inner Layout (Hardened for connectivity) ───────────────────────────
31    let chunks = Layout::default()
32        .direction(Direction::Vertical)
33        .margin(1) // Vital padding to prevent sub-box bleed
34        .constraints(
35            [
36                Constraint::Length(3),      // Header
37                Constraint::Percentage(40), // Original
38                Constraint::Percentage(40), // Synthesized
39                Constraint::Length(3),      // Footer
40            ]
41            .as_ref(),
42        )
43        .split(inner_area);
44
45    let title = if review.worker_id.is_empty() {
46        "⚠  SYNTHESIS MERGE CONFLICT  ⚠".to_string()
47    } else {
48        format!("⚙  SWARM SYNTHESIS: Worker {}  ⚙", review.worker_id)
49    };
50
51    let header = Paragraph::new(title)
52        .block(
53            Block::default()
54                .borders(Borders::ALL)
55                .style(Style::default().fg(Color::Yellow)),
56        )
57        .alignment(ratatui::layout::Alignment::Center);
58    f.render_widget(header, chunks[0]);
59
60    // Draw Before (Red Trace)
61    let before_para = Paragraph::new(review.before.clone())
62        .block(
63            Block::default()
64                .title(" Original Context ")
65                .borders(Borders::ALL)
66                .fg(Color::Red),
67        )
68        .wrap(Wrap { trim: true });
69    f.render_widget(before_para, chunks[1]);
70
71    // Draw After (Cyan diff trace)
72    // Automatically strip AI-reasoning/thought noise for a clean professional view
73    let cleaned_after = review
74        .after
75        .replace("<thought>", "")
76        .replace("</thought>", "")
77        .replace("<think>", "")
78        .replace("</think>", "");
79    let final_after = if let Some(start) = cleaned_after.find("```") {
80        let rest = &cleaned_after[start + 3..];
81        if let Some(end) = rest.find("```") {
82            // Find the first newline to skip language identifier if present
83            if let Some(first_line) = rest.find('\n') {
84                if first_line < end {
85                    rest[first_line + 1..end].trim().to_string()
86                } else {
87                    rest[..end].trim().to_string()
88                }
89            } else {
90                rest[..end].trim().to_string()
91            }
92        } else {
93            cleaned_after
94        }
95    } else {
96        cleaned_after
97    };
98
99    let after_para = Paragraph::new(final_after)
100        .block(
101            Block::default()
102                .title(" AI Synthesized Patch ")
103                .borders(Borders::ALL)
104                .fg(Color::Cyan),
105        )
106        .wrap(Wrap { trim: true });
107    f.render_widget(after_para, chunks[2]);
108
109    // Footer Triggers
110    let footer = Paragraph::new(" [Y] Accept  |  [N] Abort  |  [R] Retry Synthesis ")
111        .block(
112            Block::default()
113                .borders(Borders::ALL)
114                .style(Style::default().add_modifier(Modifier::BOLD)),
115        )
116        .alignment(ratatui::layout::Alignment::Center);
117    f.render_widget(footer, chunks[3]);
118}
119
120fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
121    let popup_layout = Layout::default()
122        .direction(Direction::Vertical)
123        .constraints(
124            [
125                Constraint::Percentage((100 - percent_y) / 2),
126                Constraint::Percentage(percent_y),
127                Constraint::Percentage((100 - percent_y) / 2),
128            ]
129            .as_ref(),
130        )
131        .split(r);
132
133    Layout::default()
134        .direction(Direction::Horizontal)
135        .constraints(
136            [
137                Constraint::Percentage((100 - percent_x) / 2),
138                Constraint::Percentage(percent_x),
139                Constraint::Percentage((100 - percent_x) / 2),
140            ]
141            .as_ref(),
142        )
143        .split(popup_layout[1])[1]
144}