Merge pull request #2796 from Hmbown/codex/harvest-2788-sidebar-command

feat(tui): add sidebar slash command
This commit is contained in:
Hunter Bown
2026-06-05 08:24:15 -07:00
committed by GitHub
5 changed files with 160 additions and 1 deletions
+4
View File
@@ -57,6 +57,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
helper for future fallback routing. This preserves the requested contract
without enabling silent runtime provider switches yet (#2574, #2777). Thanks
@hsdbeebou for the request and @idling11 for the data-model draft.
- Added `/sidebar` so users can toggle, show, hide, and optionally persist the
TUI sidebar from the command line instead of relying on copy-hostile sidebar
state during long transcript work (#2766, #2788). Thanks @mo-vic for the
detailed report and @aboimpinto for the fix.
### Changed
+4
View File
@@ -57,6 +57,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
helper for future fallback routing. This preserves the requested contract
without enabling silent runtime provider switches yet (#2574, #2777). Thanks
@hsdbeebou for the request and @idling11 for the data-model draft.
- Added `/sidebar` so users can toggle, show, hide, and optionally persist the
TUI sidebar from the command line instead of relying on copy-hostile sidebar
state during long transcript work (#2766, #2788). Thanks @mo-vic for the
detailed report and @aboimpinto for the fix.
### Changed
+64
View File
@@ -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::<Vec<_>>();
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
+80 -1
View File
@@ -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();
+8
View File
@@ -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 <prefix>` 按名称前缀过滤,--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"
}