Merge pull request #2796 from Hmbown/codex/harvest-2788-sidebar-command
feat(tui): add sidebar slash command
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user