fix(cli): wire /tokens and /cache as aliases for /stats; implement /stats

Dogfood found that /tokens and /cache had spec entries (resume_supported:
true) but no parse arms in the command parser, resulting in:
  'Unknown slash command: /tokens — Did you mean /tokens'
(the suggestion engine found the spec entry but parsing always failed)

Fix three things:
1. Add 'tokens' | 'cache' as aliases for 'stats' in the parse match so
   the commands actually resolve to SlashCommand::Stats
2. Implement SlashCommand::Stats in the REPL dispatch — previously fell
   through to 'Command registered but not yet implemented'. Now shows
   cumulative token usage for the session.
3. Implement SlashCommand::Stats in run_resume_command — previously
   returned 'unsupported resumed slash command'. Now emits:
   text:  Cost / Input tokens / Output tokens / Cache create / Cache read
   json:  {kind:stats, input_tokens, output_tokens, cache_*, total_tokens}

159 CLI tests pass, fmt clean.
This commit is contained in:
YeonGyu-Kim
2026-04-09 21:34:36 +09:00
parent 5f6f453b8d
commit 60ec2aed9b
2 changed files with 21 additions and 3 deletions

View File

@@ -1340,7 +1340,7 @@ pub fn validate_slash_command_input(
validate_no_args(command, &args)?; validate_no_args(command, &args)?;
SlashCommand::Upgrade SlashCommand::Upgrade
} }
"stats" => { "stats" | "tokens" | "cache" => {
validate_no_args(command, &args)?; validate_no_args(command, &args)?;
SlashCommand::Stats SlashCommand::Stats
} }

View File

@@ -2811,6 +2811,21 @@ fn run_resume_command(
message: Some(render_doctor_report()?.render()), message: Some(render_doctor_report()?.render()),
json: None, json: None,
}), }),
SlashCommand::Stats => {
let usage = UsageTracker::from_session(session).cumulative_usage();
Ok(ResumeCommandOutcome {
session: session.clone(),
message: Some(format_cost_report(usage)),
json: Some(serde_json::json!({
"kind": "stats",
"input_tokens": usage.input_tokens,
"output_tokens": usage.output_tokens,
"cache_creation_input_tokens": usage.cache_creation_input_tokens,
"cache_read_input_tokens": usage.cache_read_input_tokens,
"total_tokens": usage.total_tokens(),
})),
})
}
SlashCommand::History { count } => { SlashCommand::History { count } => {
let limit = parse_history_count(count.as_deref()) let limit = parse_history_count(count.as_deref())
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?; .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
@@ -2847,7 +2862,6 @@ fn run_resume_command(
| SlashCommand::Logout | SlashCommand::Logout
| SlashCommand::Vim | SlashCommand::Vim
| SlashCommand::Upgrade | SlashCommand::Upgrade
| SlashCommand::Stats
| SlashCommand::Share | SlashCommand::Share
| SlashCommand::Feedback | SlashCommand::Feedback
| SlashCommand::Files | SlashCommand::Files
@@ -3847,11 +3861,15 @@ impl LiveCli {
self.print_prompt_history(count.as_deref()); self.print_prompt_history(count.as_deref());
false false
} }
SlashCommand::Stats => {
let usage = UsageTracker::from_session(self.runtime.session()).cumulative_usage();
println!("{}", format_cost_report(usage));
false
}
SlashCommand::Login SlashCommand::Login
| SlashCommand::Logout | SlashCommand::Logout
| SlashCommand::Vim | SlashCommand::Vim
| SlashCommand::Upgrade | SlashCommand::Upgrade
| SlashCommand::Stats
| SlashCommand::Share | SlashCommand::Share
| SlashCommand::Feedback | SlashCommand::Feedback
| SlashCommand::Files | SlashCommand::Files