use ratatui::prelude::*;
use ratatui::widgets::{Block, Paragraph, Wrap};
use crate::tui::theme::Theme;
use super::{OptimizeSuggestionCtx, truncate_str};
pub(super) fn render_queue_confirm(
popup_area: Rect,
buf: &mut Buffer,
block: Block,
pending: &[String],
selected: usize,
_scroll: u16,
theme: &crate::tui::theme::Theme,
) {
let inner = block.inner(popup_area);
block.render(popup_area, buf);
let mut lines: Vec<Line> = vec![
Line::from(""),
Line::from(Span::styled(
format!(" {} message(s) pending in queue.", pending.len()),
Style::default().fg(theme.text),
)),
Line::from(""),
];
let show = pending.len().min(5);
for msg in &pending[..show] {
let preview = if msg.len() > 60 {
format!("{}…", crate::util::truncate_bytes(msg, 57))
} else {
msg.clone()
};
lines.push(Line::from(Span::styled(
format!(" • {preview}"),
Style::default().fg(theme.text_muted),
)));
}
if pending.len() > 5 {
lines.push(Line::from(Span::styled(
format!(" ... ({} more)", pending.len() - 5),
Style::default().fg(theme.text_dim),
)));
}
lines.push(Line::from(""));
let choices = ["Continue", "Cancel all queued"];
for (i, label) in choices.iter().enumerate() {
let (prefix, style) = if i == selected {
(
" ▶ ",
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)
} else {
(" ", Style::default().fg(theme.text))
};
lines.push(Line::from(Span::styled(format!("{prefix}{label}"), style)));
}
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.render(inner, buf);
}
pub(super) fn render_continuation_confirm(
popup_area: Rect,
buf: &mut Buffer,
block: Block,
content: &str,
selected: usize,
theme: &crate::tui::theme::Theme,
) {
let inner = block.inner(popup_area);
block.render(popup_area, buf);
let mut lines: Vec<Line> = vec![Line::from("")];
for line in content.lines() {
lines.push(Line::from(Span::styled(
format!(" {line}"),
Style::default().fg(theme.text),
)));
}
lines.push(Line::from(""));
let choices = ["Continue", "Stop"];
for (i, label) in choices.iter().enumerate() {
let (prefix, style) = if i == selected {
(
" ▶ ",
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)
} else {
(" ", Style::default().fg(theme.text))
};
lines.push(Line::from(Span::styled(format!("{prefix}{label}"), style)));
}
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.render(inner, buf);
}
pub(super) fn render_init_confirm(
popup_area: Rect,
buf: &mut Buffer,
block: Block,
selected: usize,
theme: &crate::tui::theme::Theme,
) {
let inner = block.inner(popup_area);
block.render(popup_area, buf);
let mut lines: Vec<Line> = vec![
Line::from(""),
Line::from(Span::styled(
" AGENTS.md already exists. How should /init proceed?",
Style::default().fg(theme.text),
)),
Line::from(""),
];
let choices = [
("Merge", "Preserve custom content, update changed sections"),
(
"Replace",
"Discard existing file and regenerate from scratch",
),
];
for (i, (label, hint)) in choices.iter().enumerate() {
let (prefix, style) = if i == selected {
(
" ▶ ",
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)
} else {
(" ", Style::default().fg(theme.text))
};
lines.push(Line::from(Span::styled(format!("{prefix}{label}"), style)));
lines.push(Line::from(Span::styled(
format!(" {hint}"),
Style::default().fg(theme.text_muted),
)));
}
lines.push(Line::from(""));
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.render(inner, buf);
}
pub(super) fn render_mode_approval(
popup_area: Rect,
buf: &mut Buffer,
block: Block,
_mode: &str,
description: &str,
selected: usize,
theme: &crate::tui::theme::Theme,
) {
let inner = block.inner(popup_area);
block.render(popup_area, buf);
let mut lines: Vec<Line> = vec![
Line::from(""),
Line::from(Span::styled(
format!(" {description}"),
Style::default().fg(theme.text),
)),
Line::from(""),
];
let choices = [
(
"Fork mode",
"Coordinator splits → parallel execution → merge results",
),
(
"Hive mode",
"Consensus communication between agents, coordinator supervises",
),
(
"Flock mode",
"Real-time messaging between agents (experimental)",
),
(
"Single agent",
"Sequential processing in the conventional way",
),
];
for (i, (label, _hint)) in choices.iter().enumerate() {
let (prefix, style) = if i == selected {
(
" ▶ ",
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)
} else {
(" ", Style::default().fg(theme.text))
};
lines.push(Line::from(Span::styled(format!("{prefix}{label}"), style)));
}
lines.push(Line::from(""));
let hint_text = choices
.get(selected)
.map(|(_, h)| *h)
.unwrap_or("Sequential processing by a single agent in the conventional way");
lines.push(Line::from(Span::styled(
format!(" {hint_text}"),
Style::default().fg(theme.text_muted),
)));
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.render(inner, buf);
}
pub(super) struct LspInstallCtx<'a> {
pub(super) language: &'a str,
pub(super) server: &'a str,
pub(super) install_cmd: &'a str,
pub(super) selected: usize,
pub(super) theme: &'a Theme,
}
pub(super) fn render_lsp_install(
popup_area: Rect,
buf: &mut Buffer,
block: Block,
ctx: &LspInstallCtx<'_>,
) {
let language = ctx.language;
let server = ctx.server;
let install_cmd = ctx.install_cmd;
let selected = ctx.selected;
let theme = ctx.theme;
let inner = block.inner(popup_area);
block.render(popup_area, buf);
let lines: Vec<Line> = vec![
Line::from(""),
Line::from(Span::styled(
format!(" LSP server not found for {language}"),
Style::default()
.fg(theme.warning)
.add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(vec![
Span::styled(" Server: ", Style::default().fg(theme.text_dim)),
Span::styled(
server,
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
),
]),
Line::from(vec![
Span::styled(" Install: ", Style::default().fg(theme.text_dim)),
Span::styled(install_cmd, Style::default().fg(theme.accent)),
]),
Line::from(""),
Line::from(if selected == 0 {
Span::styled(
" ▶ Install now",
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)
} else {
Span::styled(" Install now", Style::default().fg(theme.text))
}),
Line::from(if selected == 1 {
Span::styled(
" ▶ Skip",
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)
} else {
Span::styled(" Skip", Style::default().fg(theme.text))
}),
];
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.render(inner, buf);
}
pub(super) fn render_pii_warning(
popup_area: Rect,
buf: &mut Buffer,
block: Block,
findings: &[crate::security::pii_filter::PiiMatch],
selected: usize,
theme: &Theme,
) {
let inner = block.inner(popup_area);
block.render(popup_area, buf);
let mut lines: Vec<Line> = vec![
Line::from(""),
Line::from(Span::styled(
" ⚠ Sensitive data detected in your input:",
Style::default()
.fg(theme.warning)
.add_modifier(Modifier::BOLD),
)),
Line::from(""),
];
let show = findings.len().min(8);
for f in &findings[..show] {
lines.push(Line::from(vec![
Span::styled(" • ", Style::default().fg(theme.warning)),
Span::styled(
format!("{}: ", f.category),
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
),
Span::styled(&f.masked, Style::default().fg(theme.text_muted)),
]));
}
if findings.len() > 8 {
lines.push(Line::from(Span::styled(
format!(" ... ({} more)", findings.len() - 8),
Style::default().fg(theme.text_dim),
)));
}
lines.push(Line::from(""));
let choices = ["Proceed anyway", "Cancel"];
for (i, label) in choices.iter().enumerate() {
let (prefix, style) = if i == selected {
(
" ▶ ",
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)
} else {
(" ", Style::default().fg(theme.text))
};
lines.push(Line::from(Span::styled(format!("{prefix}{label}"), style)));
}
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.render(inner, buf);
}
pub(super) fn render_optimize_suggestion(
popup_area: Rect,
buf: &mut Buffer,
block: Block,
ctx: &OptimizeSuggestionCtx<'_>,
) {
let model = ctx.model;
let session_count = ctx.session_count;
let items = ctx.items;
let selected = ctx.selected;
let action = ctx.action;
let scroll = ctx.scroll;
let theme = ctx.theme;
let inner = block.inner(popup_area);
block.render(popup_area, buf);
for row in inner.y..inner.y + inner.height {
for col in inner.x..inner.x + inner.width {
buf[(col, row)]
.set_char(' ')
.set_bg(theme.bg_surface)
.set_fg(theme.text);
}
}
let mut y = inner.y;
let w = inner.width as usize;
if y < inner.y + inner.height {
let header = format!(" Based on {} sessions with {}", session_count, model,);
Paragraph::new(Span::styled(
truncate_str(&header, w),
Style::default().fg(theme.text_muted).italic(),
))
.render(Rect::new(inner.x, y, inner.width, 1), buf);
y += 2;
}
for (i, item) in items.iter().enumerate() {
if y.saturating_sub(scroll) >= inner.y + inner.height {
break;
}
let is_selected = i == selected;
let checkbox = if item.apply { "[x]" } else { "[ ]" };
let confidence_pct = (item.confidence * 100.0) as u8;
let line1 = format!(
" {} {}: {} → {} ({confidence_pct}%)",
checkbox, item.label, item.current, item.suggested,
);
let line1_style = if is_selected {
Style::default().fg(theme.accent).bold()
} else {
Style::default().fg(theme.text)
};
if y >= inner.y && y < inner.y + inner.height {
let display_y = y.saturating_sub(scroll);
if display_y >= inner.y && display_y < inner.y + inner.height {
Paragraph::new(Span::styled(truncate_str(&line1, w), line1_style))
.render(Rect::new(inner.x, display_y, inner.width, 1), buf);
}
}
y += 1;
let line2 = format!(" {}", item.reason);
if y >= inner.y && y < inner.y + inner.height {
let display_y = y.saturating_sub(scroll);
if display_y >= inner.y && display_y < inner.y + inner.height {
let max_w = w.saturating_sub(6);
let reason_display = if item.reason.len() > max_w {
format!(" {}…", &item.reason[..max_w.saturating_sub(1)])
} else {
line2
};
Paragraph::new(Span::styled(
reason_display,
Style::default().fg(theme.text_muted),
))
.render(Rect::new(inner.x, display_y, inner.width, 1), buf);
}
}
y += 2; }
let btn_y = inner.y + inner.height.saturating_sub(2);
if btn_y > inner.y {
let apply_style = if action == 0 {
Style::default().fg(theme.bg).bg(theme.accent).bold()
} else {
Style::default().fg(theme.text)
};
let dismiss_style = if action == 1 {
Style::default().fg(theme.bg).bg(theme.text_muted).bold()
} else {
Style::default().fg(theme.text_muted)
};
let apply_span = Span::styled(" Apply Selected ", apply_style);
let gap_span = Span::raw(" ");
let dismiss_span = Span::styled(" Dismiss ", dismiss_style);
Paragraph::new(Line::from(vec![
Span::raw(" "),
apply_span,
gap_span,
dismiss_span,
]))
.render(Rect::new(inner.x, btn_y, inner.width, 1), buf);
}
}
pub(super) fn render_tool_approval(
popup_area: Rect,
buf: &mut Buffer,
block: Block,
tool_name: &str,
tool_args: &str,
selected: usize,
theme: &crate::tui::theme::Theme,
) {
let inner = block.inner(popup_area);
block.render(popup_area, buf);
let max_args_len = inner.width.saturating_sub(4) as usize;
let args_preview = if tool_args.len() > max_args_len {
format!(
"{}…",
crate::util::truncate_bytes(tool_args, max_args_len.saturating_sub(1))
)
} else {
tool_args.to_string()
};
let mut lines: Vec<Line> = vec![
Line::from(""),
Line::from(Span::styled(
format!(" Tool: {tool_name}"),
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
format!(" Args: {args_preview}"),
Style::default().fg(theme.text_muted),
)),
Line::from(""),
Line::from(Span::styled(
" Allow this tool to execute?",
Style::default().fg(theme.text),
)),
Line::from(""),
];
let choices = ["Approve", "Approve for session", "Deny"];
for (i, label) in choices.iter().enumerate() {
let (prefix, style) = if i == selected {
(
" ▶ ",
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)
} else {
(" ", Style::default().fg(theme.text))
};
lines.push(Line::from(Span::styled(format!("{prefix}{label}"), style)));
}
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.render(inner, buf);
}