use super::*;
use crate::core_tui::session::inline_list::{InlineListRow, selection_padding};
use crate::core_tui::session::list_panel::{
ListPanelLayout, SharedListPanelSections, SharedListPanelStyles, SharedSearchField,
StaticRowsListPanelModel, fixed_section_rows, fixed_section_rows_with_divider,
render_shared_list_panel, rows_to_u16,
};
use ratatui::widgets::{Clear, Paragraph, Wrap};
#[derive(Clone)]
struct AgentPaletteRenderRow {
text: String,
subtitle: Option<String>,
style: Style,
selectable: bool,
selected: bool,
}
#[derive(Clone)]
struct FilePaletteRenderRow {
text: String,
style: Style,
selectable: bool,
selected: bool,
}
pub(crate) fn agent_palette_panel_layout(session: &Session) -> Option<ListPanelLayout> {
if !session.agent_palette_visible() || !session.inline_lists_visible() {
return None;
}
let palette = session.agent_palette.as_ref()?;
let info_rows = if palette.has_agents() {
agent_palette_instructions(session, palette).len()
} else {
1
};
let fixed_rows = fixed_section_rows(1, info_rows, palette.has_agents());
let list_rows = if palette.has_agents() {
let mut rows = palette.current_page_items().len().max(1);
if palette.has_more_items() {
rows += 1;
}
rows.min(ui::INLINE_LIST_MAX_ROWS)
} else {
1
};
Some(ListPanelLayout::new(fixed_rows, rows_to_u16(list_rows)))
}
pub fn split_inline_agent_palette_area(session: &mut Session, area: Rect) -> (Rect, Option<Rect>) {
if area.height == 0 || area.width == 0 {
return (area, None);
}
let Some(layout) = agent_palette_panel_layout(session) else {
return (area, None);
};
layout.split(area)
}
pub fn render_agent_palette(session: &mut Session, frame: &mut Frame<'_>, area: Rect) {
if !session.inline_lists_visible()
|| area.height == 0
|| area.width == 0
|| !session.agent_palette_visible()
{
return;
}
let Some(palette) = session.agent_palette.as_ref() else {
return;
};
frame.render_widget(Clear, area);
if !palette.has_agents() {
let loading = Paragraph::new(Line::from(Span::styled(
"Loading subagents...".to_owned(),
default_style(session).add_modifier(Modifier::DIM),
)))
.wrap(Wrap { trim: true });
frame.render_widget(loading, area);
return;
}
let instructions = agent_palette_instructions(session, palette);
let rows = build_agent_palette_rows(session, palette);
if rows.is_empty() {
return;
}
let default_style = default_style(session);
let highlight_style = modal_list_highlight_style(session);
let unselected_prefix = selection_padding();
let selected = rows.iter().position(|row| row.selectable && row.selected);
let rendered_rows = rows
.into_iter()
.map(|row| {
let mut spans = vec![
Span::styled(unselected_prefix.clone(), default_style),
Span::styled(row.text, row.style),
];
if let Some(subtitle) = row.subtitle {
spans.push(Span::styled(
format!(" {}", subtitle),
default_style.add_modifier(Modifier::DIM),
));
}
(
InlineListRow::single(
Line::from(spans),
if row.selectable {
default_style
} else {
default_style.add_modifier(Modifier::DIM)
},
),
1_u16,
)
})
.collect::<Vec<_>>();
let sections = SharedListPanelSections {
header: vec![Line::from(Span::styled("Agents".to_owned(), default_style))],
info: instructions,
search: Some(SharedSearchField {
label: "Search agents".to_owned(),
placeholder: Some("name or description".to_owned()),
query: palette.filter_query().to_owned(),
}),
};
let mut model = StaticRowsListPanelModel {
rows: rendered_rows,
selected,
offset: 0,
visible_rows: 0,
};
render_shared_list_panel(
frame,
area,
sections,
SharedListPanelStyles {
base_style: default_style,
selected_style: Some(highlight_style),
text_style: default_style,
divider_style: None,
},
&mut model,
);
}
pub(crate) fn file_palette_panel_layout(session: &Session) -> Option<ListPanelLayout> {
if !session.file_palette_visible() || !session.inline_lists_visible() {
return None;
}
let palette = session.file_palette.as_ref()?;
let info_rows = if palette.has_files() {
file_palette_instructions(session, palette).len()
} else {
1
};
let fixed_rows = fixed_section_rows_with_divider(1, info_rows, palette.has_files(), true);
let list_rows = if palette.has_files() {
let mut rows = palette.current_page_items().len().max(1);
if palette.has_more_items() {
rows += 1;
}
rows.min(ui::INLINE_LIST_MAX_ROWS)
} else {
1
};
Some(ListPanelLayout::new(fixed_rows, rows_to_u16(list_rows)))
}
pub fn split_inline_file_palette_area(session: &mut Session, area: Rect) -> (Rect, Option<Rect>) {
if area.height == 0 || area.width == 0 {
return (area, None);
}
let Some(layout) = file_palette_panel_layout(session) else {
return (area, None);
};
layout.split(area)
}
pub fn render_file_palette(session: &mut Session, frame: &mut Frame<'_>, area: Rect) {
if !session.inline_lists_visible()
|| area.height == 0
|| area.width == 0
|| !session.file_palette_visible()
{
return;
}
let Some(palette) = session.file_palette.as_ref() else {
return;
};
frame.render_widget(Clear, area);
if !palette.has_files() {
let loading = Paragraph::new(Line::from(Span::styled(
"Loading workspace files...".to_owned(),
default_style(session).add_modifier(Modifier::DIM),
)))
.wrap(Wrap { trim: true });
frame.render_widget(loading, area);
return;
}
let instructions = file_palette_instructions(session, palette);
let rows = build_file_palette_rows(session, palette);
if rows.is_empty() {
return;
}
let default_style = default_style(session);
let highlight_style = modal_list_highlight_style(session);
let unselected_prefix = selection_padding();
let selected = rows.iter().position(|row| row.selectable && row.selected);
let rendered_rows = rows
.into_iter()
.map(|row| {
(
InlineListRow::single(
Line::from(vec![
Span::styled(unselected_prefix.clone(), default_style),
Span::styled(row.text, row.style),
]),
if row.selectable {
default_style
} else {
default_style.add_modifier(Modifier::DIM)
},
),
1_u16,
)
})
.collect::<Vec<_>>();
let sections = SharedListPanelSections {
header: vec![Line::from(Span::styled(
"Files".to_owned(),
session.core.section_title_style(),
))],
info: instructions,
search: Some(SharedSearchField {
label: "Search files".to_owned(),
placeholder: Some("filename or path".to_owned()),
query: palette.filter_query().to_owned(),
}),
};
let mut model = StaticRowsListPanelModel {
rows: rendered_rows,
selected,
offset: 0,
visible_rows: 0,
};
render_shared_list_panel(
frame,
area,
sections,
SharedListPanelStyles {
base_style: default_style,
selected_style: Some(highlight_style),
text_style: default_style,
divider_style: Some(session.core.styles.border_style()),
},
&mut model,
);
}
fn build_agent_palette_rows(
session: &Session,
palette: &AgentPalette,
) -> Vec<AgentPaletteRenderRow> {
let mut rows = Vec::new();
let default = default_style(session);
for (_global_idx, entry, selected) in palette.current_page_items() {
rows.push(AgentPaletteRenderRow {
text: entry.display_name.clone(),
subtitle: entry.description.clone(),
style: default.add_modifier(Modifier::BOLD),
selectable: true,
selected,
});
}
if rows.is_empty() {
rows.push(AgentPaletteRenderRow {
text: "No matching agents".to_owned(),
subtitle: None,
style: default.add_modifier(Modifier::DIM),
selectable: false,
selected: false,
});
}
if palette.has_more_items() {
let remaining = palette
.total_items()
.saturating_sub(palette.current_page_number() * 20);
rows.push(AgentPaletteRenderRow {
text: format!("... ({} more items)", remaining),
subtitle: None,
style: default.add_modifier(Modifier::DIM | Modifier::ITALIC),
selectable: false,
selected: false,
});
}
rows
}
fn build_file_palette_rows(session: &Session, palette: &FilePalette) -> Vec<FilePaletteRenderRow> {
let mut rows = Vec::new();
let default = default_style(session);
for (_global_idx, entry, selected) in palette.current_page_items() {
let mut style = default;
let prefix = if entry.is_dir {
style = style.add_modifier(Modifier::BOLD);
"↳ "
} else {
" · "
};
rows.push(FilePaletteRenderRow {
text: format!("{}{}", prefix, entry.display_name),
style,
selectable: true,
selected,
});
}
if rows.is_empty() {
rows.push(FilePaletteRenderRow {
text: "No matching files".to_owned(),
style: default.add_modifier(Modifier::DIM),
selectable: false,
selected: false,
});
}
if palette.has_more_items() {
let remaining = palette
.total_items()
.saturating_sub(palette.current_page_number() * 20);
rows.push(FilePaletteRenderRow {
text: format!(" ... ({} more items)", remaining),
style: default.add_modifier(Modifier::DIM | Modifier::ITALIC),
selectable: false,
selected: false,
});
}
rows
}
fn agent_palette_instructions(session: &Session, palette: &AgentPalette) -> Vec<Line<'static>> {
let mut lines = vec![];
if palette.is_empty() {
lines.push(Line::from(Span::styled(
"No agents found matching filter".to_owned(),
default_style(session).add_modifier(Modifier::DIM),
)));
} else {
let total = palette.total_items();
let count_text = if total == 1 {
"1 agent".to_owned()
} else {
format!("{} agents", total)
};
lines.push(Line::from(Span::styled(
"↑↓ Navigate · PgUp/PgDn Page · Tab/Enter Select · Esc Close".to_owned(),
default_style(session),
)));
lines.push(Line::from(vec![
Span::styled(
format!("Showing {}", count_text),
default_style(session).add_modifier(Modifier::DIM),
),
Span::styled(
if !palette.filter_query().is_empty() {
format!(" matching '{}'", palette.filter_query())
} else {
String::new()
},
accent_style(session),
),
]));
}
lines
}
fn file_palette_instructions(session: &Session, palette: &FilePalette) -> Vec<Line<'static>> {
let mut lines = vec![];
if palette.is_empty() {
lines.push(Line::from(Span::styled(
"No files found matching filter".to_owned(),
default_style(session).add_modifier(Modifier::DIM),
)));
} else {
let total = palette.total_items();
let count_text = if total == 1 {
"1 file".to_owned()
} else {
format!("{} files", total)
};
lines.push(Line::from(vec![Span::styled(
"↑↓ Navigate · PgUp/PgDn Page · Tab/Enter Select · Esc Close".to_owned(),
default_style(session),
)]));
lines.push(Line::from(vec![
Span::styled(
format!("Showing {}", count_text),
default_style(session).add_modifier(Modifier::DIM),
),
Span::styled(
if !palette.filter_query().is_empty() {
format!(" matching '{}'", palette.filter_query())
} else {
String::new()
},
accent_style(session),
),
]));
}
lines
}