From abe366deadcc1ad7073615a6aa55288854b0079a Mon Sep 17 00:00:00 2001 From: aboimpinto <1231687+aboimpinto@users.noreply.github.com> Date: Fri, 5 Jun 2026 08:22:21 -0700 Subject: [PATCH] feat(tui): add sidebar slash command Harvested from PR #2788 by @aboimpinto. Refs #2766. --- crates/tui/src/commands/config.rs | 64 ++++++++++++++++++++++++ crates/tui/src/commands/mod.rs | 81 ++++++++++++++++++++++++++++++- crates/tui/src/localization.rs | 8 +++ 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/crates/tui/src/commands/config.rs b/crates/tui/src/commands/config.rs index 9f860ecd..5ef5e93b 100644 --- a/crates/tui/src/commands/config.rs +++ b/crates/tui/src/commands/config.rs @@ -309,6 +309,70 @@ pub fn verbose(app: &mut App, arg: Option<&str>) -> CommandResult { }) } +/// Toggle or focus the right sidebar. +/// +/// Bare `/sidebar` toggles between hidden and auto. Explicit values mirror +/// `sidebar_focus` so users have a discoverable copy-friendly path that does +/// not depend on terminal-specific key translations. +pub fn sidebar(app: &mut App, arg: Option<&str>) -> CommandResult { + let raw = arg.map(str::trim).unwrap_or(""); + let mut tokens = raw.split_whitespace().collect::>(); + let persist = matches!(tokens.last(), Some(&"--save" | &"-s")); + if persist { + tokens.pop(); + } + + let target = match tokens.as_slice() { + [] | ["toggle"] => { + if app.sidebar_focus == SidebarFocus::Hidden { + SidebarFocus::Auto + } else { + SidebarFocus::Hidden + } + } + [value] => match value.to_ascii_lowercase().as_str() { + "on" | "show" | "visible" => SidebarFocus::Auto, + "off" | "hide" | "hidden" | "closed" | "none" => SidebarFocus::Hidden, + "auto" => SidebarFocus::Auto, + "work" | "plan" | "todos" => SidebarFocus::Work, + "tasks" => SidebarFocus::Tasks, + "agents" | "subagents" | "sub-agents" => SidebarFocus::Agents, + "context" | "session" => SidebarFocus::Context, + _ => { + return CommandResult::error( + "Usage: /sidebar [on|off|auto|work|tasks|agents|context] [--save]", + ); + } + }, + _ => { + return CommandResult::error( + "Usage: /sidebar [on|off|auto|work|tasks|agents|context] [--save]", + ); + } + }; + + if persist { + let result = set_config_value(app, "sidebar_focus", target.as_setting(), true); + if result.is_error { + return result; + } + } else { + app.set_sidebar_focus(target); + } + + app.needs_redraw = true; + let message = sidebar_status_message(target).to_string(); + CommandResult::message(message) +} + +fn sidebar_status_message(focus: SidebarFocus) -> &'static str { + if focus == SidebarFocus::Hidden { + "Sidebar is hidden" + } else { + "Sidebar is visible" + } +} + /// Persist `tui.status_items` to `~/.codewhale/config.toml` without disturbing /// the rest of the file. We round-trip through `toml::Value` so any keys we /// don't know about (provider blocks, MCP, etc.) survive the write diff --git a/crates/tui/src/commands/mod.rs b/crates/tui/src/commands/mod.rs index bccc8e65..dfd337fe 100644 --- a/crates/tui/src/commands/mod.rs +++ b/crates/tui/src/commands/mod.rs @@ -352,6 +352,12 @@ pub const COMMANDS: &[CommandInfo] = &[ usage: "/config", description_id: MessageId::CmdConfigDescription, }, + CommandInfo { + name: "sidebar", + aliases: &[], + usage: "/sidebar [on|off|auto|work|tasks|agents|context] [--save]", + description_id: MessageId::CmdSidebarDescription, + }, CommandInfo { name: "mode", aliases: &["jihua", "zidong"], @@ -595,6 +601,7 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult { // Config commands "config" => config::config_command(app, arg), + "sidebar" => config::sidebar(app, arg), "settings" => config::show_settings(app), "status" => status::status(app), "statusline" => config::status_line(app), @@ -1117,7 +1124,7 @@ mod tests { use crate::config::{ApiProvider, Config}; use crate::tools::plan::{PlanItemArg, StepStatus, UpdatePlanArgs}; use crate::tools::todo::TodoStatus; - use crate::tui::app::{App, AppAction, TuiOptions}; + use crate::tui::app::{App, AppAction, SidebarFocus, TuiOptions}; use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::sync::MutexGuard; @@ -1151,6 +1158,16 @@ mod tests { #[test] fn command_registry_contains_config_and_links_but_not_set_or_deepseek() { assert!(COMMANDS.iter().any(|cmd| cmd.name == "config")); + let sidebar = COMMANDS + .iter() + .find(|cmd| cmd.name == "sidebar") + .expect("sidebar command should exist"); + assert_eq!(sidebar.description_id, MessageId::CmdSidebarDescription); + assert!( + sidebar + .description_for(Locale::En) + .contains("right sidebar") + ); assert!(COMMANDS.iter().any(|cmd| cmd.name == "links")); assert!(COMMANDS.iter().any(|cmd| cmd.name == "memory")); assert!(!COMMANDS.iter().any(|cmd| cmd.name == "set")); @@ -1355,6 +1372,68 @@ mod tests { assert!(result.message.unwrap().contains("off")); } + #[test] + fn execute_sidebar_toggles_visibility() { + let mut app = create_test_app(); + app.set_sidebar_focus(SidebarFocus::Auto); + + let result = execute("/sidebar", &mut app); + assert!(!result.is_error); + assert_eq!(app.sidebar_focus, SidebarFocus::Hidden); + assert!(app.status_message.is_none()); + assert_eq!(result.message.as_deref(), Some("Sidebar is hidden")); + + let result = execute("/sidebar", &mut app); + assert!(!result.is_error); + assert_eq!(app.sidebar_focus, SidebarFocus::Auto); + assert!(app.status_message.is_none()); + assert_eq!(result.message.as_deref(), Some("Sidebar is visible")); + } + + #[test] + fn execute_sidebar_accepts_explicit_focus_targets() { + let mut app = create_test_app(); + + let result = execute("/sidebar tasks", &mut app); + assert!(!result.is_error); + assert_eq!(app.sidebar_focus, SidebarFocus::Tasks); + assert!(app.status_message.is_none()); + + let result = execute("/sidebar off", &mut app); + assert!(!result.is_error); + assert_eq!(app.sidebar_focus, SidebarFocus::Hidden); + assert!(app.status_message.is_none()); + + let result = execute("/sidebar closed", &mut app); + assert!(!result.is_error); + assert_eq!(app.sidebar_focus, SidebarFocus::Hidden); + assert!(app.status_message.is_none()); + + let result = execute("/sidebar none", &mut app); + assert!(!result.is_error); + assert_eq!(app.sidebar_focus, SidebarFocus::Hidden); + assert!(app.status_message.is_none()); + + let result = execute("/sidebar on", &mut app); + assert!(!result.is_error); + assert_eq!(app.sidebar_focus, SidebarFocus::Auto); + assert!(app.status_message.is_none()); + } + + #[test] + fn execute_sidebar_rejects_invalid_args() { + let mut app = create_test_app(); + let result = execute("/sidebar maybe", &mut app); + assert!(result.is_error); + assert!( + result + .message + .as_deref() + .unwrap_or_default() + .contains("Usage: /sidebar") + ); + } + #[test] fn execute_links_and_aliases_return_links_message() { let mut app = create_test_app(); diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index e85990de..72508193 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -314,6 +314,7 @@ pub enum MessageId { CmdNewDescription, CmdSessionsDescription, CmdSettingsDescription, + CmdSidebarDescription, CmdSkillDescription, CmdSkillsDescription, CmdSlopDescription, @@ -637,6 +638,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::CmdNewDescription, MessageId::CmdSessionsDescription, MessageId::CmdSettingsDescription, + MessageId::CmdSidebarDescription, MessageId::CmdSkillDescription, MessageId::CmdSkillsDescription, MessageId::CmdSlopDescription, @@ -1181,6 +1183,7 @@ fn english(id: MessageId) -> &'static str { MessageId::CmdNewDescription => "Start a fresh saved session", MessageId::CmdSessionsDescription => "Open session history picker", MessageId::CmdSettingsDescription => "Show persistent settings", + MessageId::CmdSidebarDescription => "Toggle or focus the right sidebar", MessageId::CmdSkillDescription => { "Activate a skill, or install/update/uninstall/trust a community skill" } @@ -1661,6 +1664,7 @@ fn vietnamese(id: MessageId) -> Option<&'static str> { MessageId::CmdNewDescription => "Bắt đầu một phiên lưu mới", MessageId::CmdSessionsDescription => "Mở bảng chọn lịch sử phiên làm việc", MessageId::CmdSettingsDescription => "Hiển thị các cài đặt liên tục", + MessageId::CmdSidebarDescription => "Toggle or focus the right sidebar", MessageId::CmdSkillDescription => { "Kích hoạt một kỹ năng, hoặc cài đặt/cập nhật/gỡ bỏ/tin cậy một kỹ năng cộng đồng" } @@ -2204,6 +2208,7 @@ fn japanese(id: MessageId) -> Option<&'static str> { MessageId::CmdNewDescription => "新しい保存済みセッションを開始", MessageId::CmdSessionsDescription => "セッション履歴ピッカーを開く", MessageId::CmdSettingsDescription => "永続化された設定を表示", + MessageId::CmdSidebarDescription => "Toggle or focus the right sidebar", MessageId::CmdSkillDescription => { "スキルを有効化、またはコミュニティスキルをインストール/更新/アンインストール/信頼" } @@ -2645,6 +2650,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> { MessageId::CmdSessionsDescription => "打开会话历史选择器", MessageId::CmdSettingsDescription => "显示持久化设置", MessageId::CmdSkillDescription => "激活技能,或安装/更新/卸载/信任社区技能", + MessageId::CmdSidebarDescription => "Toggle or focus the right sidebar", MessageId::CmdSkillsDescription => { "列出本地技能(用 `/skills ` 按名称前缀过滤,--remote 浏览精选注册表)" } @@ -3072,6 +3078,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> { MessageId::CmdNewDescription => "Iniciar uma nova sessão salva", MessageId::CmdSessionsDescription => "Abrir seletor de histórico de sessões", MessageId::CmdSettingsDescription => "Exibir as configurações persistidas", + MessageId::CmdSidebarDescription => "Toggle or focus the right sidebar", MessageId::CmdSkillDescription => { "Ativar uma skill, ou instalar/atualizar/desinstalar/confiar em uma skill da comunidade" } @@ -3564,6 +3571,7 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> { MessageId::CmdNewDescription => "Iniciar una nueva sesión guardada", MessageId::CmdSessionsDescription => "Abrir el selector de sesiones", MessageId::CmdSettingsDescription => "Mostrar las configuraciones persistidas", + MessageId::CmdSidebarDescription => "Toggle or focus the right sidebar", MessageId::CmdSkillDescription => { "Activar una skill, o instalar/actualizar/desinstalar/confiar en una skill de la comunidad" }