use super::{
PICKER_MODAL, draw_modal_panel, picker_list_area, picker_modal_list_area, render_picker_list,
};
use crate::tui::state::MissionControlState;
use ratatui::{
Frame,
layout::Rect,
text::Line,
widgets::{ListItem, Paragraph},
};
pub(crate) fn mcp_modal_list_area(area: Rect) -> Option<Rect> {
picker_modal_list_area(area, "MCP Servers", 1)
}
pub(in crate::tui::render) fn draw_mcp_modal(
frame: &mut Frame<'_>,
area: Rect,
state: &MissionControlState,
) {
let Some(m) = &state.modals.mcp_modal else {
return;
};
let theme = state.theme;
let Some(a) = draw_modal_panel(frame, area, "MCP Servers", PICKER_MODAL, theme) else {
return;
};
if a.height == 0 {
return;
}
frame.render_widget(
Paragraph::new(super::super::hotkey_line_with_theme(
theme,
"[Up]/[Down] • [Enter] toggle next launch • [Esc] close • default: off",
theme.muted(false),
))
.style(theme.muted(false)),
Rect::new(a.x, a.y, a.width, 1),
);
let Some(list_area) = picker_list_area(a, 1) else {
return;
};
if m.rows.is_empty() {
frame.render_widget(
Paragraph::new(vec![
Line::from("No MCP servers configured."),
Line::from("Add definitions to CONFIG_DIR/.mcp.json or cwd/.mcp.json"),
])
.style(theme.pane()),
list_area,
);
return;
}
let items = m
.rows
.iter()
.map(|r| {
ListItem::new(Line::from(format!(
"{} {} ({})",
if r.enabled { "[x]" } else { "[ ]" },
r.name,
r.kind
)))
})
.collect::<Vec<_>>();
render_picker_list(
frame,
list_area,
items,
m.selected,
&state.scroll_views.mcp_modal,
theme,
);
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::{Terminal, backend::TestBackend};
#[test]
fn mcp_modal_shows_opt_in_and_next_launch_guidance() {
let mut state = MissionControlState::default();
state.open_mcp_modal(
vec![crate::tui::state::McpServerToggleRow {
name: "unapproved".into(),
kind: "stdio",
enabled: false,
}],
10,
);
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
terminal
.draw(|frame| draw_mcp_modal(frame, frame.area(), &state))
.unwrap();
let text: String = terminal
.backend()
.buffer()
.content
.iter()
.map(|cell| cell.symbol())
.collect();
assert!(text.contains("default: off"));
assert!(text.contains("toggle next launch"));
assert!(text.contains("[ ] unapproved (stdio)"));
}
#[test]
fn empty_mcp_modal_shows_definition_locations() {
let mut state = MissionControlState::default();
state.open_mcp_modal(vec![], 10);
let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
terminal
.draw(|frame| draw_mcp_modal(frame, frame.area(), &state))
.unwrap();
let text: String = terminal
.backend()
.buffer()
.content
.iter()
.map(|cell| cell.symbol())
.collect();
assert!(text.contains("No MCP servers configured."));
assert!(text.contains("CONFIG_DIR/.mcp.json or cwd/.mcp.json"));
}
}