Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/tui/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
"CmdSidebarDescription": "Toggle or focus the right sidebar",
"CmdSkillDescription": "Activate a skill, or install/update/uninstall/trust a community skill",
"CmdSkillsDescription": "List local skills (filter by `/skills <prefix>`; --remote browses the curated registry)",
"CmdSlopDescription": "Inspect or export the debt ledger",
"CmdDebtDescription": "Inspect or export the debt ledger",
"CmdStashDescription": "Park or restore a composer draft (Ctrl+S sends queued follow-up first; otherwise stash, /stash list/pop)",
"CmdStatusDescription": "Show runtime session status",
"CmdStatuslineDescription": "Configure which items appear in the footer",
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/locales/es-419.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
"CmdSidebarDescription": "Alternar o enfocar la barra lateral derecha",
"CmdSkillDescription": "Activar una skill, o instalar/actualizar/desinstalar/confiar en una skill de la comunidad",
"CmdSkillsDescription": "Listar skills locales (filtra con `/skills <prefijo>`; --remote navega el registro curado)",
"CmdSlopDescription": "Inspeccionar o exportar el libro de deuda técnica",
"CmdDebtDescription": "Inspeccionar o exportar el libro de deuda técnica",
"CmdStashDescription": "Estacionar o restaurar borrador del compositor (Ctrl+S estaciona, /stash list|pop)",
"CmdStatusDescription": "Mostrar el estado de la sesión en ejecución",
"CmdStatuslineDescription": "Configurar qué elementos aparecen en el pie de página",
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
"CmdSidebarDescription": "右サイドバーの表示/フォーカスを切り替え",
"CmdSkillDescription": "スキルを有効化、またはコミュニティスキルをインストール/更新/アンインストール/信頼",
"CmdSkillsDescription": "ローカルスキルを一覧表示(`/skills <prefix>` で絞り込み、--remote で精選レジストリを参照)",
"CmdSlopDescription": "負債台帳を確認またはエクスポート",
"CmdDebtDescription": "負債台帳を確認またはエクスポート",
"CmdStashDescription": "コンポーザーの下書きを退避/復元(Ctrl+S で退避、/stash list|pop)",
"CmdStatusDescription": "実行中のセッション状態を表示",
"CmdStatuslineDescription": "フッターに表示する項目を設定",
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
"CmdSidebarDescription": "오른쪽 사이드바를 전환하거나 포커스를 옮깁니다",
"CmdSkillDescription": "스킬을 활성화하거나 커뮤니티 스킬을 설치/업데이트/제거/신뢰합니다",
"CmdSkillsDescription": "로컬 스킬 목록을 표시합니다 (`/skills <접두사>`로 필터링, --remote는 큐레이션된 레지스트리 조회)",
"CmdSlopDescription": "부채 원장을 확인하거나 내보냅니다",
"CmdDebtDescription": "부채 원장을 확인하거나 내보냅니다",
"CmdStashDescription": "입력창 초안을 보관하거나 복원합니다 (Ctrl+S는 대기 중인 후속 메시지를 먼저 전송, 그 외에는 보관, /stash list/pop)",
"CmdStatusDescription": "런타임 세션 상태를 표시합니다",
"CmdStatuslineDescription": "하단 표시줄에 표시할 항목을 설정합니다",
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
"CmdSidebarDescription": "Alternar ou focar a barra lateral direita",
"CmdSkillDescription": "Ativar uma skill, ou instalar/atualizar/desinstalar/confiar em uma skill da comunidade",
"CmdSkillsDescription": "Listar skills locais (filtre com `/skills <prefixo>`; --remote navega pelo registro curado)",
"CmdSlopDescription": "Inspecionar ou exportar o livro de dívida técnica",
"CmdDebtDescription": "Inspecionar ou exportar o livro de dívida técnica",
"CmdStashDescription": "Estacionar ou restaurar rascunho do compositor (Ctrl+S estaciona, /stash list|pop)",
"CmdStatusDescription": "Exibir o status da sessão em execução",
"CmdStatuslineDescription": "Configurar quais itens aparecem no rodapé",
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
"CmdSidebarDescription": "Bật/tắt hoặc lấy nét thanh bên phải",
"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",
"CmdSkillsDescription": "Liệt kê các kỹ năng cục bộ (lọc bằng `/skills <tiền_tố>`; --remote để duyệt kho lưu trữ được kiểm duyệt)",
"CmdSlopDescription": "Kiểm tra hoặc xuất sổ nợ kỹ thuật",
"CmdDebtDescription": "Kiểm tra hoặc xuất sổ nợ kỹ thuật",
"CmdStashDescription": "Tạm cất hoặc khôi phục bản nháp (Ctrl+S để cất, /stash list/pop để xem/lấy ra)",
"CmdStatusDescription": "Hiển thị trạng thái thời gian chạy của phiên",
"CmdStatuslineDescription": "Cấu hình các mục hiển thị ở thanh trạng thái dưới cùng",
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/locales/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
"CmdSidebarDescription": "切换或聚焦右侧边栏",
"CmdSkillDescription": "激活技能,或安装/更新/卸载/信任社区技能",
"CmdSkillsDescription": "列出本地技能(用 `/skills <prefix>` 按名称前缀过滤,--remote 浏览精选注册表)",
"CmdSlopDescription": "检查或导出债务账本",
"CmdDebtDescription": "检查或导出债务账本",
"CmdStashDescription": "暂存或恢复输入草稿(Ctrl+S 暂存,/stash list|pop)",
"CmdStatusDescription": "显示当前运行状态",
"CmdStatuslineDescription": "配置底栏要显示哪些条目",
Expand Down
4 changes: 2 additions & 2 deletions crates/tui/src/commands/groups/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ static LOGOUT_INFO: CommandInfo = CommandInfo {
};
static DEBT_INFO: CommandInfo = CommandInfo {
name: "debt",
aliases: &["cleanup"],
aliases: &["cleanup", "slop", "canzha"],
Comment thread
nightt5879 marked this conversation as resolved.
Comment thread
nightt5879 marked this conversation as resolved.
Comment thread
nightt5879 marked this conversation as resolved.
usage: "/debt [query|export]",
description_id: MessageId::CmdSlopDescription,
description_id: MessageId::CmdDebtDescription,
};

fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
Expand Down
23 changes: 20 additions & 3 deletions crates/tui/src/commands/groups/core/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ use super::CommandResult;
/// Show help information
pub fn help(app: &mut App, topic: Option<&str>) -> CommandResult {
if let Some(topic) = topic {
let user_commands = crate::commands::user_registry::with_registry_for_workspace(
Some(&app.workspace),
Clone::clone,
);
if user_commands.get(topic).is_some() {
return CommandResult::error(
tr(app.ui_locale, MessageId::HelpUnknownCommand).replace("{topic}", topic),
);
}

// Show help for specific command
if let Some(cmd) = crate::commands::get_command_info(topic) {
let mut help = format!(
Expand All @@ -26,12 +36,18 @@ pub fn help(app: &mut App, topic: Option<&str>) -> CommandResult {
tr(app.ui_locale, MessageId::HelpUsageLabel),
cmd.usage
);
if !cmd.aliases.is_empty() {
let visible_aliases = cmd
.aliases
.iter()
.filter(|alias| user_commands.get(alias).is_none())
.copied()
.collect::<Vec<_>>();
if !visible_aliases.is_empty() {
let _ = write!(
help,
"\n {} {}",
tr(app.ui_locale, MessageId::HelpAliasesLabel),
cmd.aliases.join(", ")
visible_aliases.join(", ")
);
}
return CommandResult::message(help);
Expand All @@ -43,7 +59,8 @@ pub fn help(app: &mut App, topic: Option<&str>) -> CommandResult {

// Show help overlay
if app.view_stack.top_kind() != Some(ModalKind::Help) {
app.view_stack.push(HelpView::new_for_locale(app.ui_locale));
let help = HelpView::new_for_workspace(app.ui_locale, &app.workspace);
app.view_stack.push(help);
}
CommandResult::ok()
}
Expand Down
176 changes: 142 additions & 34 deletions crates/tui/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,10 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
return result;
}

// Permanent backward-compatible aliases. They predate the group-owned
// registry and remain documented in docs/architecture/command-dispatch.md.
// Permanent backward-compatible mode aliases. They select a fixed mode
// rather than the canonical `/mode` behavior, so they still dispatch
// before registry lookup. Ordinary compatibility aliases belong in their
// command's `CommandInfo` metadata.
match command.as_str() {
"jihua" => {
return groups::config::dispatch(app, "jihua", arg).unwrap_or_else(|| {
Expand All @@ -162,11 +164,6 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
CommandResult::error("The /zidong alias could not be dispatched.")
});
}
"slop" | "canzha" => {
return groups::config::dispatch(app, "debt", arg).unwrap_or_else(|| {
CommandResult::error("The /debt command could not be dispatched.")
});
}
_ => {}
}

Expand All @@ -193,7 +190,10 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
if let Some(result) = groups::skills::run_skill_by_name(app, command.as_str(), arg) {
return result;
}
let suggestions = suggest_command_names(command.as_str(), 3);
let suggestions =
user_registry::with_registry_for_workspace(Some(&app.workspace), |user_commands| {
suggest_command_names(command.as_str(), 3, user_commands)
});
if suggestions.is_empty() {
CommandResult::error(format!(
"Unknown command: /{command}. Type /help for available commands."
Expand Down Expand Up @@ -251,40 +251,60 @@ fn edit_distance(a: &str, b: &str) -> usize {
previous[b_chars.len()]
}

fn suggest_command_names(input: &str, limit: usize) -> Vec<String> {
fn suggestion_score<'a>(
query: &str,
candidates: impl IntoIterator<Item = &'a str>,
) -> Option<(u8, usize)> {
let mut best: Option<(u8, usize)> = None;
for candidate in candidates {
let prefix_match = candidate.starts_with(query) || query.starts_with(candidate);
let contains_match = candidate.contains(query) || query.contains(candidate);
let distance = edit_distance(candidate, query);
let close_typo = distance <= 2;
if !(prefix_match || contains_match || close_typo) {
continue;
}

let rank = if prefix_match {
0
} else if contains_match {
1
} else {
2
};

match best {
Some((best_rank, best_distance))
if rank > best_rank || (rank == best_rank && distance >= best_distance) => {}
_ => best = Some((rank, distance)),
}
}
best
}

fn suggest_command_names(
input: &str,
limit: usize,
user_commands: &user_registry::UserCommandRegistry,
) -> Vec<String> {
let query = input.trim().to_ascii_lowercase();
if query.is_empty() || limit == 0 {
return Vec::new();
}

let mut scored: Vec<(u8, usize, String)> = Vec::new();
for command in registry().infos() {
let mut best: Option<(u8, usize)> = None;
for candidate in std::iter::once(command.name).chain(command.aliases.iter().copied()) {
let prefix_match = candidate.starts_with(&query) || query.starts_with(candidate);
let contains_match = candidate.contains(&query) || query.contains(candidate);
let distance = edit_distance(candidate, &query);
let close_typo = distance <= 2;
if !(prefix_match || contains_match || close_typo) {
continue;
}

let rank = if prefix_match {
0
} else if contains_match {
1
} else {
2
};

match best {
Some((best_rank, best_distance))
if rank > best_rank || (rank == best_rank && distance >= best_distance) => {}
_ => best = Some((rank, distance)),
}
if user_commands.get(command.name).is_some() {
continue;
}

if let Some((rank, distance)) = best {
let candidates = std::iter::once(command.name).chain(
command
.aliases
.iter()
.copied()
.filter(|alias| user_commands.get(alias).is_none()),
);
if let Some((rank, distance)) = suggestion_score(&query, candidates) {
scored.push((rank, distance, command.name.to_string()));
}
}
Expand Down Expand Up @@ -433,6 +453,94 @@ mod tests {
assert_eq!(links.aliases, &["dashboard", "api", "lianjie"]);
}

#[test]
fn debt_compat_aliases_use_registry_discovery_and_help() {
let debt = get_command_info("debt").expect("debt command should be registered");
assert_eq!(debt.aliases, &["cleanup", "slop", "canzha"]);
assert_eq!(debt.description_id, MessageId::CmdDebtDescription);

for alias in ["slop", "canzha"] {
let resolved = get_command_info(alias)
.unwrap_or_else(|| panic!("/{alias} should resolve through the registry"));
assert_eq!(resolved.name, "debt");

let mut app = create_test_app();
let result = execute(&format!("/help {alias}"), &mut app);
assert!(!result.is_error, "/help {alias} returned {result:?}");
let message = result
.message
.unwrap_or_else(|| panic!("/help {alias} should return text"));
assert!(
message.starts_with("debt\n"),
"unexpected help: {message:?}"
);
assert!(
message.contains("cleanup, slop, canzha"),
"help should list every debt alias: {message:?}"
);
}

let user_commands = user_registry::UserCommandRegistry::new();
assert!(
suggest_command_names("slpo", 3, &user_commands)
.iter()
.any(|name| name == "debt"),
"typo suggestions should consider the /slop alias"
);
}

#[test]
fn debt_alias_help_and_suggestions_respect_user_command_shadows() {
let temp = tempdir().unwrap();
let commands_dir = temp.path().join(".codewhale").join("commands");
std::fs::create_dir_all(&commands_dir).unwrap();
std::fs::write(
commands_dir.join("slop.md"),
"---\ndescription: Custom slop workflow\nargument-hint: <target>\n---\ncustom slop $ARGUMENTS",
)
.unwrap();
std::fs::write(
commands_dir.join("custom-debt.md"),
"---\ndescription: Custom debt alias\nalias: canzha\n---\ncustom debt $ARGUMENTS",
)
.unwrap();

let mut app = create_test_app();
app.workspace = temp.path().to_path_buf();

for alias in ["slop", "canzha"] {
let result = execute(&format!("/help {alias}"), &mut app);
assert!(result.is_error, "/help {alias} returned {result:?}");
let message = result.message.expect("shadowed alias help should error");
assert!(message.contains(alias), "{message:?}");
assert!(!message.contains("debt"), "{message:?}");
}

let debt_help = execute("/help debt", &mut app);
assert!(!debt_help.is_error);
let debt_message = debt_help
.message
.expect("canonical debt help should render");
assert!(debt_message.contains("cleanup"), "{debt_message:?}");
assert!(!debt_message.contains("slop"), "{debt_message:?}");
assert!(!debt_message.contains("canzha"), "{debt_message:?}");

let slop_typo = execute("/slpo", &mut app);
let slop_typo_message = slop_typo.message.expect("typo should return guidance");
assert!(!slop_typo_message.contains("/debt"), "{slop_typo_message}");

let canzha_typo = execute("/canzhaa", &mut app);
let canzha_typo_message = canzha_typo.message.expect("typo should return guidance");
assert!(
!canzha_typo_message.contains("/debt"),
"{canzha_typo_message}"
);

let debt_typo = execute("/detb", &mut app);
let debt_typo_message = debt_typo.message.expect("typo should return guidance");
assert!(debt_typo_message.contains("/debt"), "{debt_typo_message}");
}

#[test]
fn transcript_command_is_discoverable_and_opens_live_overlay() {
let transcript = command_infos()
Expand Down
4 changes: 2 additions & 2 deletions crates/tui/src/localization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ pub enum MessageId {
CmdSidebarDescription,
CmdSkillDescription,
CmdSkillsDescription,
CmdSlopDescription,
CmdDebtDescription,
CmdStashDescription,
CmdStatusDescription,
CmdStatuslineDescription,
Expand Down Expand Up @@ -1360,7 +1360,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CmdSidebarDescription,
MessageId::CmdSkillDescription,
MessageId::CmdSkillsDescription,
MessageId::CmdSlopDescription,
MessageId::CmdDebtDescription,
MessageId::CmdStashDescription,
MessageId::CmdStatusDescription,
MessageId::CmdStatuslineDescription,
Expand Down
3 changes: 2 additions & 1 deletion crates/tui/src/tui/mouse_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1321,7 +1321,8 @@ pub(crate) fn handle_context_menu_action(app: &mut App, action: ContextMenuActio
open_context_inspector(app);
}
ContextMenuAction::OpenHelp => {
app.view_stack.push(HelpView::new_for_locale(app.ui_locale));
let help = HelpView::new_for_workspace(app.ui_locale, &app.workspace);
app.view_stack.push(help);
}
ContextMenuAction::OpenFileAtLine { cell_index } => {
let width = app
Expand Down
4 changes: 2 additions & 2 deletions crates/tui/src/tui/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4845,8 +4845,8 @@ async fn run_event_loop(
if app.view_stack.top_kind() == Some(ModalKind::Help) {
app.view_stack.pop();
} else {
app.view_stack
.push(HelpView::new_for_shortcuts(app.ui_locale));
let help = HelpView::new_for_shortcuts(app.ui_locale, &app.workspace);
app.view_stack.push(help);
}
continue;
}
Expand Down
Loading
Loading