use super::super::render_scrollbar_with_theme;
use crate::tui::modal_container::{draw_modal_container, modal_area, modal_tab};
use crate::tui::{state::MissionControlState, theme::MissionControlTheme};
use ratatui::{
Frame,
layout::{Alignment, Margin, Rect},
widgets::{Block, Clear, Paragraph, Wrap},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SubagentViewerModalAreas {
pub(crate) tabs: Rect,
pub(crate) timeline: Rect,
pub(crate) prompt: Rect,
}
pub(crate) fn subagent_viewer_modal_areas(area: Rect) -> Option<SubagentViewerModalAreas> {
let text_area = modal_area(area, 60, 60).inner(Margin::new(1, 1));
if text_area.height == 0 || text_area.width == 0 {
return None;
}
let tabs = Rect::new(
text_area.x,
text_area.y,
text_area.width,
u16::from(text_area.height > 1),
);
let timeline_y = tabs.bottom();
let remaining_height = text_area.bottom().saturating_sub(timeline_y);
let prompt_height = remaining_height.saturating_sub(1).min(3);
let prompt_height = if prompt_height >= 3 { prompt_height } else { 0 };
let timeline = Rect::new(
text_area.x,
timeline_y,
text_area.width,
remaining_height.saturating_sub(prompt_height),
);
let prompt = Rect::new(
text_area.x,
timeline.bottom(),
text_area.width,
prompt_height,
);
(timeline.height > 0).then_some(SubagentViewerModalAreas {
tabs,
timeline,
prompt,
})
}
pub(crate) fn subagent_viewer_timeline_area(area: Rect) -> Option<Rect> {
subagent_viewer_modal_areas(area).map(|areas| areas.timeline)
}
pub(crate) fn subagent_viewer_prompt_area(area: Rect) -> Option<Rect> {
subagent_viewer_modal_areas(area)
.map(|areas| areas.prompt)
.filter(|area| area.height >= 3 && area.width > 2)
}
pub(crate) fn subagent_viewer_tab_at(
area: Rect,
state: &MissionControlState,
column: u16,
row: u16,
) -> Option<usize> {
let areas = subagent_viewer_modal_areas(area)?;
if !contains(areas.tabs, column, row) {
return None;
}
let tabs = state.subagent_viewer_tabs();
let selected = selected_tab_index(state, &tabs);
visible_tab_areas(areas.tabs, tabs.len(), selected)
.into_iter()
.find(|tab| contains(tab.area, column, row))
.map(|tab| tab.actual_index)
}
pub(in crate::tui::render) fn draw_subagent_viewer_modal(
frame: &mut Frame<'_>,
area: Rect,
state: &MissionControlState,
) {
if !state.subagent_viewer_visible() {
return;
}
let theme = state.theme;
draw_modal_container(
frame,
modal_area(area, 60, 60),
"Subagent Viewer",
"[Tab]/[Shift-Tab] switch · [PgUp]/[PgDn] scroll",
theme,
Some("[Esc] close"),
);
let Some(areas) = subagent_viewer_modal_areas(area) else {
return;
};
draw_timeline(frame, areas.timeline, state, theme);
draw_tabs(frame, areas.tabs, state, theme);
if areas.prompt.height >= 3 && areas.prompt.width > 2 {
super::super::prompt::draw_subagent_prompt(frame, areas.prompt, state);
if state.subagent_prompt_editable()
&& let Some(autocomplete) = state
.selected_subagent_prompt()
.and_then(|draft| draft.autocomplete.as_ref())
{
let completion_bounds = Rect::new(
areas.timeline.x,
areas.timeline.y,
areas.timeline.width,
areas.timeline.height + areas.prompt.height,
);
super::super::prompt::draw_prompt_autocomplete(
frame,
completion_bounds,
areas.prompt,
autocomplete,
theme,
);
}
}
}
fn draw_tabs(
frame: &mut Frame<'_>,
area: Rect,
state: &MissionControlState,
theme: MissionControlTheme,
) {
let tabs = state.subagent_viewer_tabs();
if tabs.is_empty() {
frame.render_widget(
Paragraph::new("No sibling subagents available")
.style(theme.muted(false).bg(theme.surface_background())),
area,
);
return;
}
let selected = selected_tab_index(state, &tabs);
for visible_tab in visible_tab_areas(area, tabs.len(), selected) {
let Some(task_id) = tabs.get(visible_tab.actual_index) else {
continue;
};
if visible_tab.area.width == 0 {
continue;
}
let label_width = usize::from(visible_tab.area.width.saturating_sub(2)).max(1);
let label = state.subagent_viewer_tab_label(task_id, label_width);
frame.render_widget(
Paragraph::new(modal_tab(
&label,
visible_tab.actual_index == selected,
theme,
))
.alignment(Alignment::Center),
visible_tab.area,
);
}
}
fn draw_timeline(
frame: &mut Frame<'_>,
area: Rect,
state: &MissionControlState,
theme: MissionControlTheme,
) {
if area.width == 0 || area.height == 0 {
return;
}
let Some(task_id) = state.subagent_viewer_selected_task_id() else {
frame.render_widget(
Paragraph::new("No live activity available")
.style(theme.pane())
.wrap(Wrap { trim: false }),
area,
);
return;
};
let content_width = super::super::scroll_view_content_width(area).max(1);
let offset = state.subagent_viewer_scroll_offset(area.height, content_width);
let timeline = state.cached_subagent_viewer_timeline(&task_id, content_width);
let content = Rect {
width: content_width.min(area.width.saturating_sub(1)),
..area
};
let (visible_lines, skipped_rows) = timeline.visible_line_range(offset, area.height);
let lines = timeline.lines[visible_lines]
.iter()
.map(|line| line.line.clone())
.collect::<Vec<_>>();
frame.render_widget(
Paragraph::new(lines)
.style(theme.pane())
.wrap(Wrap { trim: false })
.scroll((skipped_rows, 0)),
content,
);
let gutter = Rect::new(
area.x + content.width,
area.y,
area.width - content.width,
area.height,
);
frame.render_widget(Clear, gutter);
frame.render_widget(Block::default().style(theme.pane()), gutter);
render_scrollbar_with_theme(
frame,
area,
timeline.rows.saturating_sub(usize::from(area.height)) + 1,
offset,
usize::from(area.height),
theme,
);
}
const MIN_TAB_WIDTH: u16 = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct VisibleTabArea {
actual_index: usize,
area: Rect,
}
fn selected_tab_index(state: &MissionControlState, tabs: &[crate::output::ActivityId]) -> usize {
state
.subagent_viewer_selected_task_id()
.and_then(|selected| tabs.iter().position(|task_id| task_id == &selected))
.unwrap_or(0)
}
fn visible_tab_areas(area: Rect, count: usize, selected: usize) -> Vec<VisibleTabArea> {
if count == 0 || area.width == 0 {
return Vec::new();
}
let visible_count = count.min((usize::from(area.width) / usize::from(MIN_TAB_WIDTH)).max(1));
let selected = selected.min(count - 1);
let start = selected
.saturating_sub(visible_count / 2)
.min(count - visible_count);
let width = usize::from(area.width);
let base = width / visible_count;
let remainder = width % visible_count;
let mut x = area.x;
(start..start + visible_count)
.enumerate()
.map(|(visible_index, actual_index)| {
let tab_width = base + usize::from(visible_index < remainder);
let tab = Rect::new(x, area.y, tab_width as u16, area.height);
x = x.saturating_add(tab.width);
VisibleTabArea {
actual_index,
area: tab,
}
})
.collect()
}
fn contains(area: Rect, column: u16, row: u16) -> bool {
column >= area.x
&& column < area.x.saturating_add(area.width)
&& row >= area.y
&& row < area.y.saturating_add(area.height)
}