From 29d2df2f96a9969ef803012b7cfaa7afd56cf5ed Mon Sep 17 00:00:00 2001 From: TheArchitectit Date: Tue, 16 Jun 2026 15:42:15 -0500 Subject: [PATCH] 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 --- .../rusty-claude-cli/src/command_palette.rs | 37 +- .../rusty-claude-cli/src/keybindings.rs | 74 +++- rust/crates/rusty-claude-cli/src/main.rs | 9 +- rust/crates/rusty-claude-cli/src/tui/app.rs | 12 +- .../crates/rusty-claude-cli/src/tui/legacy.rs | 317 ++++++++++++++---- 5 files changed, 359 insertions(+), 90 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/command_palette.rs b/rust/crates/rusty-claude-cli/src/command_palette.rs index 692eef59..a4bb7579 100644 --- a/rust/crates/rusty-claude-cli/src/command_palette.rs +++ b/rust/crates/rusty-claude-cli/src/command_palette.rs @@ -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'); diff --git a/rust/crates/rusty-claude-cli/src/keybindings.rs b/rust/crates/rusty-claude-cli/src/keybindings.rs index 6d08a8ce..7d1d4758 100644 --- a/rust/crates/rusty-claude-cli/src/keybindings.rs +++ b/rust/crates/rusty-claude-cli/src/keybindings.rs @@ -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) { diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index fd5ea6fa..a7bb967c 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -7415,11 +7415,14 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box> { // (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(); diff --git a/rust/crates/rusty-claude-cli/src/tui/app.rs b/rust/crates/rusty-claude-cli/src/tui/app.rs index e854fb8b..73a3e3ed 100644 --- a/rust/crates/rusty-claude-cli/src/tui/app.rs +++ b/rust/crates/rusty-claude-cli/src/tui/app.rs @@ -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), } } diff --git a/rust/crates/rusty-claude-cli/src/tui/legacy.rs b/rust/crates/rusty-claude-cli/src/tui/legacy.rs index bcf3ab5a..126e8c03 100644 --- a/rust/crates/rusty-claude-cli/src/tui/legacy.rs +++ b/rust/crates/rusty-claude-cli/src/tui/legacy.rs @@ -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 { + fn dispatch_action( + &mut self, + action: crate::keybindings::Action, + ) -> io::Result { 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> { 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> — 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::>()) + Line::from( + l.spans + .into_iter() + .map(|s| Span::styled(s.content.into_owned(), s.style)) + .collect::>(), + ) })); } 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::>()) + Line::from( + l.spans + .into_iter() + .map(|s| Span::styled(s.content.into_owned(), s.style)) + .collect::>(), + ) })); } } @@ -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 = 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 = 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();