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 { fn slash_command_category(name: &str) -> &'static str {
match name { match name {
"help" | "status" | "sandbox" | "model" | "permissions" | "cost" | "resume" | "session" "help" | "status" | "cost" | "resume" | "session" | "version" | "login" | "logout"
| "version" | "login" | "logout" | "usage" | "stats" | "rename" | "privacy-settings" => { | "usage" | "stats" | "rename" | "clear" | "compact" | "history" | "tokens" | "cache"
"Session & visibility" | "exit" | "summary" | "tag" | "thinkback" | "copy" | "share" | "feedback" | "rewind"
} | "pin" | "unpin" | "bookmarks" | "context" | "files" | "focus" | "unfocus" | "retry"
"compact" | "clear" | "config" | "memory" | "init" | "diff" | "commit" | "pr" | "issue" | "stop" | "undo" => "Session",
| "export" | "plugin" | "branch" | "add-dir" | "files" | "hooks" | "release-notes" => { "diff" | "commit" | "pr" | "issue" | "branch" | "blame" | "log" | "git" | "stash"
"Workspace & git" | "init" | "export" | "plan" | "review" | "security-review" | "bughunter" | "ultraplan"
} | "teleport" | "refactor" | "fix" | "autofix" | "explain" | "docs" | "perf" | "search"
"agents" | "skills" | "teleport" | "debug-tool-call" | "mcp" | "context" | "tasks" | "references" | "definition" | "hover" | "symbols" | "map" | "web" | "image"
| "doctor" | "ide" | "desktop" => "Discovery & debugging", | "screenshot" | "paste" | "listen" | "speak" | "test" | "lint" | "build" | "run"
"bughunter" | "ultraplan" | "review" | "security-review" | "advisor" | "insights" => { | "format" | "parallel" | "multi" | "macro" | "alias" | "templates" | "migrate"
"Analysis & automation" | "benchmark" | "cron" | "agent" | "subagent" | "agents" | "skills" | "team" | "plugin"
} | "mcp" | "hooks" | "tasks" | "advisor" | "insights" | "release-notes" | "chat"
"theme" | "vim" | "voice" | "color" | "effort" | "fast" | "brief" | "output-style" | "approve" | "deny" | "allowed-tools" | "add-dir" => "Tools",
| "keybindings" | "stickers" => "Appearance & input", "model" | "permissions" | "config" | "memory" | "theme" | "vim" | "voice" | "color"
"copy" | "share" | "feedback" | "summary" | "tag" | "thinkback" | "plan" | "exit" | "effort" | "fast" | "brief" | "output-style" | "keybindings" | "privacy-settings"
| "upgrade" | "rewind" => "Communication & control", | "stickers" | "language" | "profile" | "max-tokens" | "temperature" | "system-prompt"
_ => "Other", | "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(), String::new(),
]; ];
let categories = [ let categories = ["Session", "Tools", "Config", "Debug"];
"Session & visibility",
"Workspace & git",
"Discovery & debugging",
"Analysis & automation",
];
for category in categories { for category in categories {
lines.push(category.to_string()); lines.push(category.to_string());
@@ -1930,6 +1930,12 @@ pub fn render_slash_command_help() -> String {
lines.push(String::new()); 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 lines
.into_iter() .into_iter()
.rev() .rev()
@@ -2314,8 +2320,7 @@ pub fn resolve_skill_invocation(
.unwrap_or_default(); .unwrap_or_default();
if !skill_token.is_empty() { if !skill_token.is_empty() {
if let Err(error) = resolve_skill_path(cwd, skill_token) { if let Err(error) = resolve_skill_path(cwd, skill_token) {
let mut message = let mut message = format!("Unknown skill: {skill_token} ({error})");
format!("Unknown skill: {skill_token} ({error})");
let roots = discover_skill_roots(cwd); let roots = discover_skill_roots(cwd);
if let Ok(available) = load_skills_from_roots(&roots) { if let Ok(available) = load_skills_from_roots(&roots) {
let names: Vec<String> = available let names: Vec<String> = available
@@ -2324,15 +2329,10 @@ pub fn resolve_skill_invocation(
.map(|s| s.name.clone()) .map(|s| s.name.clone())
.collect(); .collect();
if !names.is_empty() { if !names.is_empty() {
message.push_str(&format!( message.push_str(&format!("\n Available skills: {}", names.join(", ")));
"\n Available skills: {}",
names.join(", ")
));
} }
} }
message.push_str( message.push_str("\n Usage: /skills [list|install <path>|help|<skill> [args]]");
"\n Usage: /skills [list|install <path>|help|<skill> [args]]",
);
return Err(message); return Err(message);
} }
} }
@@ -4297,7 +4297,7 @@ mod tests {
// then // then
assert!(error.contains("Usage: /teleport <symbol-or-path>")); assert!(error.contains("Usage: /teleport <symbol-or-path>"));
assert!(error.contains(" Category Discovery & debugging")); assert!(error.contains(" Category Tools"));
} }
#[test] #[test]
@@ -4371,10 +4371,10 @@ mod tests {
let help = render_slash_command_help(); let help = render_slash_command_help();
assert!(help.contains("Start here /status, /diff, /agents, /skills, /commit")); assert!(help.contains("Start here /status, /diff, /agents, /skills, /commit"));
assert!(help.contains("[resume] also works with --resume SESSION.jsonl")); assert!(help.contains("[resume] also works with --resume SESSION.jsonl"));
assert!(help.contains("Session & visibility")); assert!(help.contains("Session"));
assert!(help.contains("Workspace & git")); assert!(help.contains("Tools"));
assert!(help.contains("Discovery & debugging")); assert!(help.contains("Config"));
assert!(help.contains("Analysis & automation")); assert!(help.contains("Debug"));
assert!(help.contains("/help")); assert!(help.contains("/help"));
assert!(help.contains("/status")); assert!(help.contains("/status"));
assert!(help.contains("/sandbox")); assert!(help.contains("/sandbox"));
@@ -4411,6 +4411,53 @@ mod tests {
assert!(resume_supported_slash_commands().len() >= 39); 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] #[test]
fn renders_per_command_help_detail() { fn renders_per_command_help_detail() {
// given // given
@@ -4423,7 +4470,7 @@ mod tests {
assert!(help.contains("/plugin")); assert!(help.contains("/plugin"));
assert!(help.contains("Summary Manage Claw Code plugins")); assert!(help.contains("Summary Manage Claw Code plugins"));
assert!(help.contains("Aliases /plugins, /marketplace")); assert!(help.contains("Aliases /plugins, /marketplace"));
assert!(help.contains("Category Workspace & git")); assert!(help.contains("Category Tools"));
} }
#[test] #[test]
@@ -4431,7 +4478,7 @@ mod tests {
let help = render_slash_command_help_detail("mcp").expect("detail help should exist"); let help = render_slash_command_help_detail("mcp").expect("detail help should exist");
assert!(help.contains("/mcp")); assert!(help.contains("/mcp"));
assert!(help.contains("Summary Inspect configured MCP servers")); 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")); assert!(help.contains("Resume Supported with --resume SESSION.jsonl"));
} }

View File

@@ -4171,10 +4171,6 @@ fn render_repl_help() -> String {
"REPL".to_string(), "REPL".to_string(),
" /exit Quit the REPL".to_string(), " /exit Quit the REPL".to_string(),
" /quit 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(), " Auto-save .claw/sessions/<session-id>.jsonl".to_string(),
" Resume latest /resume latest".to_string(), " Resume latest /resume latest".to_string(),
" Browse sessions /session list".to_string(), " Browse sessions /session list".to_string(),