feat: b5-cost-tracker — batch 5 upstream parity

This commit is contained in:
YeonGyu-Kim
2026-04-07 14:51:12 +09:00
parent 700534de41
commit 65f4c3ad82
2 changed files with 87 additions and 44 deletions

View File

@@ -1786,24 +1786,29 @@ pub fn resume_supported_slash_commands() -> Vec<&'static SlashCommandSpec> {
fn slash_command_category(name: &str) -> &'static str {
match name {
"help" | "status" | "sandbox" | "model" | "permissions" | "cost" | "resume" | "session"
| "version" | "login" | "logout" | "usage" | "stats" | "rename" | "privacy-settings" => {
"Session & visibility"
}
"compact" | "clear" | "config" | "memory" | "init" | "diff" | "commit" | "pr" | "issue"
| "export" | "plugin" | "branch" | "add-dir" | "files" | "hooks" | "release-notes" => {
"Workspace & git"
}
"agents" | "skills" | "teleport" | "debug-tool-call" | "mcp" | "context" | "tasks"
| "doctor" | "ide" | "desktop" => "Discovery & debugging",
"bughunter" | "ultraplan" | "review" | "security-review" | "advisor" | "insights" => {
"Analysis & automation"
}
"theme" | "vim" | "voice" | "color" | "effort" | "fast" | "brief" | "output-style"
| "keybindings" | "stickers" => "Appearance & input",
"copy" | "share" | "feedback" | "summary" | "tag" | "thinkback" | "plan" | "exit"
| "upgrade" | "rewind" => "Communication & control",
_ => "Other",
"help" | "status" | "cost" | "resume" | "session" | "version" | "login" | "logout"
| "usage" | "stats" | "rename" | "clear" | "compact" | "history" | "tokens" | "cache"
| "exit" | "summary" | "tag" | "thinkback" | "copy" | "share" | "feedback" | "rewind"
| "pin" | "unpin" | "bookmarks" | "context" | "files" | "focus" | "unfocus" | "retry"
| "stop" | "undo" => "Session",
"diff" | "commit" | "pr" | "issue" | "branch" | "blame" | "log" | "git" | "stash"
| "init" | "export" | "plan" | "review" | "security-review" | "bughunter" | "ultraplan"
| "teleport" | "refactor" | "fix" | "autofix" | "explain" | "docs" | "perf" | "search"
| "references" | "definition" | "hover" | "symbols" | "map" | "web" | "image"
| "screenshot" | "paste" | "listen" | "speak" | "test" | "lint" | "build" | "run"
| "format" | "parallel" | "multi" | "macro" | "alias" | "templates" | "migrate"
| "benchmark" | "cron" | "agent" | "subagent" | "agents" | "skills" | "team" | "plugin"
| "mcp" | "hooks" | "tasks" | "advisor" | "insights" | "release-notes" | "chat"
| "approve" | "deny" | "allowed-tools" | "add-dir" => "Tools",
"model" | "permissions" | "config" | "memory" | "theme" | "vim" | "voice" | "color"
| "effort" | "fast" | "brief" | "output-style" | "keybindings" | "privacy-settings"
| "stickers" | "language" | "profile" | "max-tokens" | "temperature" | "system-prompt"
| "api-key" | "terminal-setup" | "notifications" | "telemetry" | "providers" | "env"
| "project" | "reasoning" | "budget" | "rate-limit" | "workspace" | "reset" | "ide"
| "desktop" | "upgrade" => "Config",
"debug-tool-call" | "doctor" | "sandbox" | "diagnostics" | "tool-details" | "changelog"
| "metrics" => "Debug",
_ => "Tools",
}
}
@@ -1912,12 +1917,7 @@ pub fn render_slash_command_help() -> String {
String::new(),
];
let categories = [
"Session & visibility",
"Workspace & git",
"Discovery & debugging",
"Analysis & automation",
];
let categories = ["Session", "Tools", "Config", "Debug"];
for category in categories {
lines.push(category.to_string());
@@ -1930,6 +1930,12 @@ pub fn render_slash_command_help() -> String {
lines.push(String::new());
}
lines.push("Keyboard shortcuts".to_string());
lines.push(" Up/Down Navigate prompt history".to_string());
lines.push(" Tab Complete commands, modes, and recent sessions".to_string());
lines.push(" Ctrl-C Clear input (or exit on empty prompt)".to_string());
lines.push(" Shift+Enter/Ctrl+J Insert a newline".to_string());
lines
.into_iter()
.rev()
@@ -2314,8 +2320,7 @@ pub fn resolve_skill_invocation(
.unwrap_or_default();
if !skill_token.is_empty() {
if let Err(error) = resolve_skill_path(cwd, skill_token) {
let mut message =
format!("Unknown skill: {skill_token} ({error})");
let mut message = format!("Unknown skill: {skill_token} ({error})");
let roots = discover_skill_roots(cwd);
if let Ok(available) = load_skills_from_roots(&roots) {
let names: Vec<String> = available
@@ -2324,15 +2329,10 @@ pub fn resolve_skill_invocation(
.map(|s| s.name.clone())
.collect();
if !names.is_empty() {
message.push_str(&format!(
"\n Available skills: {}",
names.join(", ")
));
message.push_str(&format!("\n Available skills: {}", names.join(", ")));
}
}
message.push_str(
"\n Usage: /skills [list|install <path>|help|<skill> [args]]",
);
message.push_str("\n Usage: /skills [list|install <path>|help|<skill> [args]]");
return Err(message);
}
}
@@ -4297,7 +4297,7 @@ mod tests {
// then
assert!(error.contains("Usage: /teleport <symbol-or-path>"));
assert!(error.contains(" Category Discovery & debugging"));
assert!(error.contains(" Category Tools"));
}
#[test]
@@ -4371,10 +4371,10 @@ mod tests {
let help = render_slash_command_help();
assert!(help.contains("Start here /status, /diff, /agents, /skills, /commit"));
assert!(help.contains("[resume] also works with --resume SESSION.jsonl"));
assert!(help.contains("Session & visibility"));
assert!(help.contains("Workspace & git"));
assert!(help.contains("Discovery & debugging"));
assert!(help.contains("Analysis & automation"));
assert!(help.contains("Session"));
assert!(help.contains("Tools"));
assert!(help.contains("Config"));
assert!(help.contains("Debug"));
assert!(help.contains("/help"));
assert!(help.contains("/status"));
assert!(help.contains("/sandbox"));
@@ -4411,6 +4411,53 @@ mod tests {
assert!(resume_supported_slash_commands().len() >= 39);
}
#[test]
fn renders_help_with_grouped_categories_and_keyboard_shortcuts() {
// given
let categories = ["Session", "Tools", "Config", "Debug"];
// when
let help = render_slash_command_help();
// then
for category in categories {
assert!(
help.contains(category),
"expected help to contain category {category}"
);
}
let session_index = help.find("Session").expect("Session header should exist");
let tools_index = help.find("Tools").expect("Tools header should exist");
let config_index = help.find("Config").expect("Config header should exist");
let debug_index = help.find("Debug").expect("Debug header should exist");
assert!(session_index < tools_index);
assert!(tools_index < config_index);
assert!(config_index < debug_index);
assert!(help.contains("Keyboard shortcuts"));
assert!(help.contains("Up/Down Navigate prompt history"));
assert!(help.contains("Tab Complete commands, modes, and recent sessions"));
assert!(help.contains("Ctrl-C Clear input (or exit on empty prompt)"));
assert!(help.contains("Shift+Enter/Ctrl+J Insert a newline"));
// every command should still render with a summary line
for spec in slash_command_specs() {
let usage = match spec.argument_hint {
Some(hint) => format!("/{} {hint}", spec.name),
None => format!("/{}", spec.name),
};
assert!(
help.contains(&usage),
"expected help to contain command {usage}"
);
assert!(
help.contains(spec.summary),
"expected help to contain summary for /{}",
spec.name
);
}
}
#[test]
fn renders_per_command_help_detail() {
// given
@@ -4423,7 +4470,7 @@ mod tests {
assert!(help.contains("/plugin"));
assert!(help.contains("Summary Manage Claw Code plugins"));
assert!(help.contains("Aliases /plugins, /marketplace"));
assert!(help.contains("Category Workspace & git"));
assert!(help.contains("Category Tools"));
}
#[test]
@@ -4431,7 +4478,7 @@ mod tests {
let help = render_slash_command_help_detail("mcp").expect("detail help should exist");
assert!(help.contains("/mcp"));
assert!(help.contains("Summary Inspect configured MCP servers"));
assert!(help.contains("Category Discovery & debugging"));
assert!(help.contains("Category Tools"));
assert!(help.contains("Resume Supported with --resume SESSION.jsonl"));
}

View File

@@ -4171,10 +4171,6 @@ fn render_repl_help() -> String {
"REPL".to_string(),
" /exit Quit the REPL".to_string(),
" /quit Quit the REPL".to_string(),
" Up/Down Navigate prompt history".to_string(),
" Tab Complete commands, modes, and recent sessions".to_string(),
" Ctrl-C Clear input (or exit on empty prompt)".to_string(),
" Shift+Enter/Ctrl+J Insert a newline".to_string(),
" Auto-save .claw/sessions/<session-id>.jsonl".to_string(),
" Resume latest /resume latest".to_string(),
" Browse sessions /session list".to_string(),