fix(tui): keep /permissions in TUI, populate Ctrl+K palette with slash commands

- run_tui_repl now treats handle_repl_command's return as a persist hint,
  not an exit signal, so /permissions re-enters the TUI instead of returning
  to the CLI screen.
- Added Action::RunSlashCommand(idx) and wired it in both component and
  legacy TUI dispatch paths.
- Command palette now includes every registered slash command so users can
  discover and run /permissions, /theme, /model, etc. from Ctrl+K.
- Updated palette filter test to account for the new slash entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
TheArchitectit 2026-06-16 15:42:15 -05:00
parent b08c32d760
commit 29d2df2f96
5 changed files with 359 additions and 90 deletions

View File

@ -1,6 +1,7 @@
//! Command palette — fuzzy-filterable modal for all available commands.
use crate::keybindings::Action;
use commands::slash_command_specs;
pub struct CommandPalette {
pub active: bool,
@ -21,7 +22,7 @@ pub struct PaletteEntry {
impl CommandPalette {
pub fn new() -> Self {
let entries = vec![
let mut entries = vec![
PaletteEntry {
label: "Submit".into(),
description: "Send message".into(),
@ -87,6 +88,26 @@ impl CommandPalette {
},
];
// Add every registered slash command so Ctrl+K can search /theme,
// /permissions, /model, etc. The index is stable because the spec
// slice is built once at first access.
for (i, spec) in slash_command_specs().iter().enumerate() {
let label = format!("/{}", spec.name);
let aliases = spec.aliases.join(", ");
let description = if aliases.is_empty() {
spec.summary.to_string()
} else {
format!("{} (aliases: {})", spec.summary, aliases)
};
entries.push(PaletteEntry {
label,
description,
action: Action::RunSlashCommand(i),
key_hint: "slash".into(),
category: "Slash".into(),
});
}
let filtered = (0..entries.len()).collect();
Self {
active: false,
@ -158,9 +179,7 @@ impl CommandPalette {
.map(|(i, _)| i)
.collect();
}
self.selected = self
.selected
.min(self.filtered.len().saturating_sub(1));
self.selected = self.selected.min(self.filtered.len().saturating_sub(1));
}
}
@ -183,12 +202,16 @@ mod tests {
fn test_filter_by_label() {
let mut cp = CommandPalette::new();
cp.open();
// 'Z' doesn't appear in any default entry label/description/category,
// 'ZZZZ' doesn't appear in any default entry label/description/category,
// so it should filter down to zero results.
cp.input('Z');
for _ in 0..4 {
cp.input('Z');
}
assert!(cp.filtered.is_empty());
// A more specific query that matches only some entries
cp.backspace();
for _ in 0..4 {
cp.backspace();
}
cp.input('H');
cp.input('e');
cp.input('l');

View File

@ -25,6 +25,8 @@ pub enum Action {
FocusInput,
FocusConversation,
CycleChatMode,
/// Run a slash command by index into `commands::slash_command_specs()`.
RunSlashCommand(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -71,15 +73,43 @@ impl KeyMap {
self.bind(KeyModifiers::SHIFT, KeyCode::Enter, Action::Newline);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('c'), Action::Cancel);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('d'), Action::Exit);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('p'), Action::ProviderSwap);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('t'), Action::TeamToggle);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('k'), Action::CommandPalette);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('b'), Action::ToggleSidebar);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('a'), Action::ToggleAgentView);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('l'), Action::ClearConversation);
self.bind(
KeyModifiers::CONTROL,
KeyCode::Char('p'),
Action::ProviderSwap,
);
self.bind(
KeyModifiers::CONTROL,
KeyCode::Char('t'),
Action::TeamToggle,
);
self.bind(
KeyModifiers::CONTROL,
KeyCode::Char('k'),
Action::CommandPalette,
);
self.bind(
KeyModifiers::CONTROL,
KeyCode::Char('b'),
Action::ToggleSidebar,
);
self.bind(
KeyModifiers::CONTROL,
KeyCode::Char('a'),
Action::ToggleAgentView,
);
self.bind(
KeyModifiers::CONTROL,
KeyCode::Char('l'),
Action::ClearConversation,
);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('h'), Action::Help);
self.bind(KeyModifiers::NONE, KeyCode::PageUp, Action::ScrollHalfUp);
self.bind(KeyModifiers::NONE, KeyCode::PageDown, Action::ScrollHalfDown);
self.bind(
KeyModifiers::NONE,
KeyCode::PageDown,
Action::ScrollHalfDown,
);
self.bind(KeyModifiers::CONTROL, KeyCode::Home, Action::ScrollTop);
self.bind(KeyModifiers::CONTROL, KeyCode::End, Action::ScrollBottom);
self.bind(KeyModifiers::NONE, KeyCode::F(1), Action::Help);
@ -90,13 +120,33 @@ impl KeyMap {
self.bind(KeyModifiers::NONE, KeyCode::Enter, Action::Newline);
self.bind(KeyModifiers::NONE, KeyCode::Esc, Action::Cancel);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('d'), Action::Exit);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('p'), Action::ProviderSwap);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('k'), Action::CommandPalette);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('a'), Action::ToggleAgentView);
self.bind(KeyModifiers::CONTROL, KeyCode::Char('l'), Action::ClearConversation);
self.bind(
KeyModifiers::CONTROL,
KeyCode::Char('p'),
Action::ProviderSwap,
);
self.bind(
KeyModifiers::CONTROL,
KeyCode::Char('k'),
Action::CommandPalette,
);
self.bind(
KeyModifiers::CONTROL,
KeyCode::Char('a'),
Action::ToggleAgentView,
);
self.bind(
KeyModifiers::CONTROL,
KeyCode::Char('l'),
Action::ClearConversation,
);
self.bind(KeyModifiers::NONE, KeyCode::F(1), Action::Help);
self.bind(KeyModifiers::NONE, KeyCode::PageUp, Action::ScrollHalfUp);
self.bind(KeyModifiers::NONE, KeyCode::PageDown, Action::ScrollHalfDown);
self.bind(
KeyModifiers::NONE,
KeyCode::PageDown,
Action::ScrollHalfDown,
);
}
fn bind(&mut self, modifiers: KeyModifiers, code: KeyCode, action: Action) {

View File

@ -7415,11 +7415,14 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box<dyn std::error::Error>> {
// (permissions prompts, setup wizard) leave alternate
// screen and let the user interact directly.
app.leave_for_turn()?;
let should_exit = cli.handle_repl_command(command)?;
let should_persist = cli.handle_repl_command(command)?;
app.reenter_after_turn()?;
if should_exit {
break;
// `true` means the runtime/session was mutated
// (e.g. /permissions, /model), not that we should
// leave the TUI. Persist and stay in dashboard mode.
if should_persist {
cli.persist_session()?;
}
update_dashboard(&dashboard_state, &cli);
let _ = app.redraw_after_turn();

View File

@ -13,6 +13,8 @@ use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Style;
use ratatui::Frame;
use commands::slash_command_specs;
use crate::keybindings::{Action, KeyMap, KeyPreset, VimMode};
use crate::theme::TuiTheme;
use crate::tui::component::{Component, Overlay};
@ -392,7 +394,7 @@ impl TuiApp {
Action::ToggleAgentView => Ok(TuiReadOutcome::ToggleAgentView),
Action::Help => {
let preset = self.key_preset_name().to_string();
let msg = format!("Keybindings ({preset}):\n\nEnter Submit Shift+Enter ↵\nCtrl+C Cancel Ctrl+D Exit\nCtrl+P Swap Ctrl+K Palette\nCtrl+A Agents Ctrl+T Team\n");
let msg = format!("Keybindings ({preset}):\n\nEnter Submit Shift+Enter ↵\nCtrl+C Cancel Ctrl+D Exit\nCtrl+P Swap Ctrl+K Palette (includes /slash commands)\nCtrl+A Agents Ctrl+T Team\n");
self.push_system_message(&msg);
Ok(TuiReadOutcome::Pending)
}
@ -424,6 +426,14 @@ impl TuiApp {
self.conversation.scroll_bottom();
Ok(TuiReadOutcome::Pending)
}
Action::RunSlashCommand(idx) => {
let specs = slash_command_specs();
if let Some(spec) = specs.get(idx) {
let cmd = format!("/{}", spec.name);
return Ok(TuiReadOutcome::Submit(cmd));
}
Ok(TuiReadOutcome::Pending)
}
_ => Ok(TuiReadOutcome::Pending),
}
}

View File

@ -467,11 +467,14 @@ impl TuiApp {
let drain_count = self.conversation.len() - MAX_CONVERSATION_LINES;
self.conversation.drain(..drain_count);
// Insert trim notice
self.conversation.insert(0, ConversationLine::plain(
"... (earlier messages trimmed)".to_string(),
self.theme.conversation_dim.to_color(),
false,
));
self.conversation.insert(
0,
ConversationLine::plain(
"... (earlier messages trimmed)".to_string(),
self.theme.conversation_dim.to_color(),
false,
),
);
}
self.conversation_scroll = 0;
self.needs_redraw = true;
@ -515,7 +518,12 @@ impl TuiApp {
let renderer = self.markdown_renderer.clone();
let theme = self.theme.clone();
let agent_view_active = self.agent_view.active;
let agent_sessions: Vec<_> = self.agent_view.filtered_sessions().into_iter().cloned().collect();
let agent_sessions: Vec<_> = self
.agent_view
.filtered_sessions()
.into_iter()
.cloned()
.collect();
let agent_selected = self.agent_view.selected;
let agent_filter = self.agent_view.filter;
let agent_sort = self.agent_view.sort_by;
@ -546,12 +554,28 @@ impl TuiApp {
// Command palette overlay
if palette_active {
draw_command_palette(f, area, &palette_query, &palette_entries, &palette_filtered, palette_selected, &theme);
draw_command_palette(
f,
area,
&palette_query,
&palette_entries,
&palette_filtered,
palette_selected,
&theme,
);
}
// Agent View overlay
if agent_view_active {
draw_agent_view(f, area, &agent_sessions, agent_selected, agent_filter, agent_sort, &theme);
draw_agent_view(
f,
area,
&agent_sessions,
agent_selected,
agent_filter,
agent_sort,
&theme,
);
}
})?;
self.terminal.backend_mut().flush()?;
@ -656,9 +680,7 @@ impl TuiApp {
self.needs_redraw = true;
Ok(TuiReadOutcome::Pending)
}
Some(Action::ToggleAgentView) => {
Ok(TuiReadOutcome::ToggleAgentView)
}
Some(Action::ToggleAgentView) => Ok(TuiReadOutcome::ToggleAgentView),
Some(Action::ToggleSidebar) => {
// Future: toggle sidebar
Ok(TuiReadOutcome::Pending)
@ -707,9 +729,7 @@ impl TuiApp {
}
Ok(TuiReadOutcome::Pending)
}
Some(Action::FocusConversation) => {
Ok(TuiReadOutcome::Pending)
}
Some(Action::FocusConversation) => Ok(TuiReadOutcome::Pending),
_ => {
// Unbound keys — check for history navigation, then pass to text area
if key.code == KeyCode::Up && !self.showing_completions {
@ -758,7 +778,10 @@ impl TuiApp {
}
/// Dispatch an Action — used by command palette and handle_key.
fn dispatch_action(&mut self, action: crate::keybindings::Action) -> io::Result<TuiReadOutcome> {
fn dispatch_action(
&mut self,
action: crate::keybindings::Action,
) -> io::Result<TuiReadOutcome> {
use crate::keybindings::Action;
match action {
Action::Submit => {
@ -766,28 +789,89 @@ impl TuiApp {
let text = lines.join("\n");
self.input.select_all();
self.input.cut();
if text.trim().is_empty() { return Ok(TuiReadOutcome::Pending); }
if text.trim().is_empty() {
return Ok(TuiReadOutcome::Pending);
}
Ok(TuiReadOutcome::Submit(text))
}
Action::Cancel => { self.showing_completions = false; self.input.select_all(); self.input.cut(); Ok(TuiReadOutcome::Cancel) }
Action::Newline => { self.input.insert_newline(); Ok(TuiReadOutcome::Pending) }
Action::Exit => { self.should_exit = true; Ok(TuiReadOutcome::Exit) }
Action::ProviderSwap => { self.input.select_all(); self.input.cut(); Ok(TuiReadOutcome::ProviderSwap) }
Action::TeamToggle => { self.input.select_all(); self.input.cut(); Ok(TuiReadOutcome::TeamToggle) }
Action::CommandPalette => { self.command_palette.open(); self.needs_redraw = true; Ok(TuiReadOutcome::Pending) }
Action::Cancel => {
self.showing_completions = false;
self.input.select_all();
self.input.cut();
Ok(TuiReadOutcome::Cancel)
}
Action::Newline => {
self.input.insert_newline();
Ok(TuiReadOutcome::Pending)
}
Action::Exit => {
self.should_exit = true;
Ok(TuiReadOutcome::Exit)
}
Action::ProviderSwap => {
self.input.select_all();
self.input.cut();
Ok(TuiReadOutcome::ProviderSwap)
}
Action::TeamToggle => {
self.input.select_all();
self.input.cut();
Ok(TuiReadOutcome::TeamToggle)
}
Action::CommandPalette => {
self.command_palette.open();
self.needs_redraw = true;
Ok(TuiReadOutcome::Pending)
}
Action::ToggleAgentView => Ok(TuiReadOutcome::ToggleAgentView),
Action::ToggleSidebar => Ok(TuiReadOutcome::Pending),
Action::ClearConversation => { self.conversation.clear(); self.conversation_scroll = 0; self.needs_redraw = true; Ok(TuiReadOutcome::Pending) }
Action::Help => { self.show_help(); Ok(TuiReadOutcome::Pending) }
Action::CycleChatMode => { self.handle_tab(); Ok(TuiReadOutcome::Pending) }
Action::ScrollUp => { self.conversation_scroll = self.conversation_scroll.saturating_add(1); Ok(TuiReadOutcome::Pending) }
Action::ScrollDown => { self.conversation_scroll = self.conversation_scroll.saturating_sub(1); Ok(TuiReadOutcome::Pending) }
Action::ScrollHalfUp => { self.conversation_scroll = self.conversation_scroll.saturating_add(5); Ok(TuiReadOutcome::Pending) }
Action::ScrollHalfDown => { self.conversation_scroll = self.conversation_scroll.saturating_sub(5); Ok(TuiReadOutcome::Pending) }
Action::ScrollTop => { self.conversation_scroll = u16::MAX; Ok(TuiReadOutcome::Pending) }
Action::ScrollBottom => { self.conversation_scroll = 0; Ok(TuiReadOutcome::Pending) }
Action::FocusInput => { Ok(TuiReadOutcome::Pending) }
Action::FocusConversation => { Ok(TuiReadOutcome::Pending) }
Action::ClearConversation => {
self.conversation.clear();
self.conversation_scroll = 0;
self.needs_redraw = true;
Ok(TuiReadOutcome::Pending)
}
Action::Help => {
self.show_help();
Ok(TuiReadOutcome::Pending)
}
Action::CycleChatMode => {
self.handle_tab();
Ok(TuiReadOutcome::Pending)
}
Action::ScrollUp => {
self.conversation_scroll = self.conversation_scroll.saturating_add(1);
Ok(TuiReadOutcome::Pending)
}
Action::ScrollDown => {
self.conversation_scroll = self.conversation_scroll.saturating_sub(1);
Ok(TuiReadOutcome::Pending)
}
Action::ScrollHalfUp => {
self.conversation_scroll = self.conversation_scroll.saturating_add(5);
Ok(TuiReadOutcome::Pending)
}
Action::ScrollHalfDown => {
self.conversation_scroll = self.conversation_scroll.saturating_sub(5);
Ok(TuiReadOutcome::Pending)
}
Action::ScrollTop => {
self.conversation_scroll = u16::MAX;
Ok(TuiReadOutcome::Pending)
}
Action::ScrollBottom => {
self.conversation_scroll = 0;
Ok(TuiReadOutcome::Pending)
}
Action::FocusInput => Ok(TuiReadOutcome::Pending),
Action::FocusConversation => Ok(TuiReadOutcome::Pending),
Action::RunSlashCommand(idx) => {
let specs = commands::slash_command_specs();
if let Some(spec) = specs.get(idx) {
return Ok(TuiReadOutcome::Submit(format!("/{}", spec.name)));
}
Ok(TuiReadOutcome::Pending)
}
}
}
@ -910,7 +994,10 @@ fn wrap_line<'a>(text: &str, width: usize, style: Style) -> Vec<Line<'a>> {
current_line = rest;
last_break_byte = 0;
} else {
result.push(Line::from(Span::styled(current_line.clone(), style.clone())));
result.push(Line::from(Span::styled(
current_line.clone(),
style.clone(),
)));
current_line.clear();
current_width = 0;
last_break_byte = 0;
@ -966,9 +1053,12 @@ fn build_wrapped_conversation<'a>(
// rendered is Vec<Line<'static>> — safe to extend
all_lines.extend(rendered.into_iter().map(|l: Line<'static>| {
// Convert Line<'static> to Line<'a> via into_owned pattern
Line::from(l.spans.into_iter().map(|s| {
Span::styled(s.content.into_owned(), s.style)
}).collect::<Vec<_>>())
Line::from(
l.spans
.into_iter()
.map(|s| Span::styled(s.content.into_owned(), s.style))
.collect::<Vec<_>>(),
)
}));
}
ConversationContent::CodeDiff { diff } => {
@ -976,9 +1066,12 @@ fn build_wrapped_conversation<'a>(
let count = rendered.len().max(1);
expand_counts.push(count);
all_lines.extend(rendered.into_iter().map(|l: Line<'static>| {
Line::from(l.spans.into_iter().map(|s| {
Span::styled(s.content.into_owned(), s.style)
}).collect::<Vec<_>>())
Line::from(
l.spans
.into_iter()
.map(|s| Span::styled(s.content.into_owned(), s.style))
.collect::<Vec<_>>(),
)
}));
}
}
@ -1008,7 +1101,8 @@ fn draw_left_pane(
// --- conversation with word-wrapping ---
// Subtract 1 for the top border, 2 for left/right block padding
let content_width = (left[0].width as usize).saturating_sub(2);
let (wrapped, expand_counts) = build_wrapped_conversation(conversation, content_width, markdown_renderer, theme);
let (wrapped, expand_counts) =
build_wrapped_conversation(conversation, content_width, markdown_renderer, theme);
let pane_rows = (left[0].height.saturating_sub(1) as usize).max(1);
let total_visual = wrapped.len();
@ -1086,7 +1180,9 @@ fn draw_left_pane(
.enumerate()
.map(|(i, m)| {
let style = if i == completion_index % matches.len() {
Style::default().bg(theme.completion_selected_bg.to_color()).fg(theme.completion_selected_fg.to_color())
Style::default()
.bg(theme.completion_selected_bg.to_color())
.fg(theme.completion_selected_fg.to_color())
} else {
Style::default().fg(theme.completion_fg.to_color())
};
@ -1122,19 +1218,54 @@ fn draw_right_pane(
let mut gauge_row: Option<usize> = None;
lines.push(section("Connection", theme));
lines.push(kv("Model", &state.model, theme.dashboard_value.to_color(), theme));
lines.push(kv("Provider", &state.provider, theme.dashboard_key.to_color(), theme));
lines.push(kv("URL", &state.provider_url, theme.conversation_dim.to_color(), theme));
lines.push(kv("Mode", &state.permission_mode, theme.conversation_system.to_color(), theme));
lines.push(kv(
"Model",
&state.model,
theme.dashboard_value.to_color(),
theme,
));
lines.push(kv(
"Provider",
&state.provider,
theme.dashboard_key.to_color(),
theme,
));
lines.push(kv(
"URL",
&state.provider_url,
theme.conversation_dim.to_color(),
theme,
));
lines.push(kv(
"Mode",
&state.permission_mode,
theme.conversation_system.to_color(),
theme,
));
if let Some(ref branch) = state.git_branch {
lines.push(kv("Branch", branch, theme.agent_done.to_color(), theme));
}
lines.push(Line::from(""));
lines.push(section("Tokens", theme));
lines.push(kv("Turns", &state.turn_count.to_string(), theme.dashboard_value.to_color(), theme));
lines.push(kv("Input", &state.input_tokens.to_string(), theme.dashboard_value.to_color(), theme));
lines.push(kv("Output", &state.output_tokens.to_string(), theme.dashboard_value.to_color(), theme));
lines.push(kv(
"Turns",
&state.turn_count.to_string(),
theme.dashboard_value.to_color(),
theme,
));
lines.push(kv(
"Input",
&state.input_tokens.to_string(),
theme.dashboard_value.to_color(),
theme,
));
lines.push(kv(
"Output",
&state.output_tokens.to_string(),
theme.dashboard_value.to_color(),
theme,
));
lines.push(kv(
"Cache R",
&state.cache_read_tokens.to_string(),
@ -1195,7 +1326,12 @@ fn draw_right_pane(
if let Some(ref team) = state.team {
lines.push(section("Team", theme));
lines.push(kv("Name", &team.team_name, theme.dashboard_value.to_color(), theme));
lines.push(kv(
"Name",
&team.team_name,
theme.dashboard_value.to_color(),
theme,
));
let progress = format!(
"{}/{} done, {} fail, {} run",
team.completed_agents, team.total_agents, team.failed_agents, team.running_agents
@ -1276,7 +1412,11 @@ fn draw_right_pane(
height: 1,
};
let gauge = Gauge::default()
.gauge_style(Style::default().fg(gauge_color).bg(theme.gauge_bg.to_color()))
.gauge_style(
Style::default()
.fg(gauge_color)
.bg(theme.gauge_bg.to_color()),
)
.ratio(if pct > 0.0 {
(pct / 100.0).min(1.0)
} else {
@ -1334,7 +1474,10 @@ fn draw_command_palette(
let mut lines: Vec<Line> = Vec::new();
lines.push(Line::from(vec![
Span::styled("🔍 ", Style::default().fg(theme.key_hint.to_color())),
Span::styled(query.to_string(), Style::default().fg(theme.input_fg.to_color())),
Span::styled(
query.to_string(),
Style::default().fg(theme.input_fg.to_color()),
),
Span::styled("", Style::default().fg(theme.input_cursor_bg.to_color())),
]));
lines.push(Line::from(""));
@ -1343,14 +1486,23 @@ fn draw_command_palette(
let entry = &entries[idx];
let is_sel = i == selected;
let (fg, bg) = if is_sel {
(theme.completion_selected_fg.to_color(), theme.completion_selected_bg.to_color())
(
theme.completion_selected_fg.to_color(),
theme.completion_selected_bg.to_color(),
)
} else {
(theme.completion_fg.to_color(), Color::Reset)
};
lines.push(Line::from(vec![
Span::styled(format!(" {} ", entry.label), Style::default().fg(fg).bg(bg)),
Span::styled(format!(" {} ", entry.description), Style::default().fg(theme.key_hint.to_color())),
Span::styled(&entry.key_hint, Style::default().fg(theme.key_hint.to_color())),
Span::styled(
format!(" {} ", entry.description),
Style::default().fg(theme.key_hint.to_color()),
),
Span::styled(
&entry.key_hint,
Style::default().fg(theme.key_hint.to_color()),
),
]));
}
@ -1378,7 +1530,7 @@ fn draw_agent_view(
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Header
Constraint::Length(3), // Header
Constraint::Min(10), // Session list
Constraint::Length(6), // Detail
])
@ -1390,7 +1542,9 @@ fn draw_agent_view(
let header = Paragraph::new(Line::from(vec![
Span::styled(
" Agent View ",
Style::default().fg(theme.dashboard_header.to_color()).add_modifier(Modifier::BOLD),
Style::default()
.fg(theme.dashboard_header.to_color())
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" [{filter_label}] [{sort_label}] Tab:filter S:sort Esc:close"),
@ -1424,10 +1578,22 @@ fn draw_agent_view(
format!("{}m{}s", elapsed / 60, elapsed % 60)
};
let line = Line::from(vec![
Span::styled(format!(" {} ", s.status.icon()), Style::default().fg(status_color)),
Span::styled(format!("{:<20}", s.name), Style::default().fg(theme.conversation_text.to_color())),
Span::styled(format!("{:<16}", s.model), Style::default().fg(theme.dashboard_key.to_color())),
Span::styled(format!("{} turns ", s.turn_count), Style::default().fg(theme.dashboard_value.to_color())),
Span::styled(
format!(" {} ", s.status.icon()),
Style::default().fg(status_color),
),
Span::styled(
format!("{:<20}", s.name),
Style::default().fg(theme.conversation_text.to_color()),
),
Span::styled(
format!("{:<16}", s.model),
Style::default().fg(theme.dashboard_key.to_color()),
),
Span::styled(
format!("{} turns ", s.turn_count),
Style::default().fg(theme.dashboard_value.to_color()),
),
Span::styled(elapsed_str, Style::default().fg(theme.key_hint.to_color())),
]);
let style = if is_sel {
@ -1451,19 +1617,34 @@ fn draw_agent_view(
if let Some(session) = sessions.get(selected) {
let detail = Paragraph::new(vec![
Line::from(vec![
Span::styled(" ID: ", Style::default().fg(theme.dashboard_key.to_color())),
Span::styled(&session.id, Style::default().fg(theme.dashboard_value.to_color())),
Span::styled(
" ID: ",
Style::default().fg(theme.dashboard_key.to_color()),
),
Span::styled(
&session.id,
Style::default().fg(theme.dashboard_value.to_color()),
),
]),
Line::from(vec![
Span::styled(" Task: ", Style::default().fg(theme.dashboard_key.to_color())),
Span::styled(
" Task: ",
Style::default().fg(theme.dashboard_key.to_color()),
),
Span::styled(
session.task_subject.as_deref().unwrap_or("(none)"),
Style::default().fg(theme.dashboard_value.to_color()),
),
]),
Line::from(vec![
Span::styled(" Dir: ", Style::default().fg(theme.dashboard_key.to_color())),
Span::styled(&session.working_dir, Style::default().fg(theme.dashboard_value.to_color())),
Span::styled(
" Dir: ",
Style::default().fg(theme.dashboard_key.to_color()),
),
Span::styled(
&session.working_dir,
Style::default().fg(theme.dashboard_value.to_color()),
),
]),
])
.block(
@ -1518,7 +1699,9 @@ mod tests {
use unicode_width::UnicodeWidthChar;
let lines = wrap_line("你好世界你好世界你好世界", 10, Style::default());
for line in &lines {
let width: usize = line.spans.iter()
let width: usize = line
.spans
.iter()
.flat_map(|s| s.content.chars())
.map(|c| c.width().unwrap_or(0))
.sum();