Add runtime status command

Add a dedicated /status command that reports the current runtime session state.

  The new report shows provider, model, workspace, mode, permissions, session,
  context usage, token telemetry, cache telemetry, cost, transcript counts, and
  rate-limit availability. /statusline remains available for footer configuration.
This commit is contained in:
reidliu41
2026-05-08 23:37:17 +08:00
committed by Hunter Bown
parent 4f8eff0c69
commit 24ec0839e1
3 changed files with 266 additions and 2 deletions
+10 -2
View File
@@ -26,6 +26,7 @@ mod session;
pub mod share;
mod skills;
mod stash;
mod status;
mod task;
mod user_commands;
@@ -420,9 +421,15 @@ pub const COMMANDS: &[CommandInfo] = &[
usage: "/settings",
description_id: MessageId::CmdSettingsDescription,
},
CommandInfo {
name: "status",
aliases: &[],
usage: "/status",
description_id: MessageId::CmdStatusDescription,
},
CommandInfo {
name: "statusline",
aliases: &["status"],
aliases: &[],
usage: "/statusline",
description_id: MessageId::CmdStatuslineDescription,
},
@@ -531,7 +538,8 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
// Config commands
"config" => config::config_command(app, arg),
"settings" => config::show_settings(app),
"statusline" | "status" => config::status_line(app),
"status" => status::status(app),
"statusline" => config::status_line(app),
"mode" => config::mode(app, arg),
"theme" => config::theme(app),
"verbose" => config::verbose(app, arg),
+250
View File
@@ -0,0 +1,250 @@
//! Runtime status command.
use std::fmt::Write as _;
use std::path::Path;
use super::CommandResult;
use crate::compaction::estimate_input_tokens_conservative;
use crate::models::{LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS, context_window_for_model};
use crate::tui::app::App;
use crate::utils::{display_path, estimate_message_chars};
/// Show a compact runtime status report for the current TUI session.
pub fn status(app: &mut App) -> CommandResult {
CommandResult::message(format_status(app))
}
fn format_status(app: &App) -> String {
let mut out = String::new();
let (context_used, context_max, context_percent) = context_usage(app);
let _ = writeln!(out, "DeepSeek TUI Status");
let _ = writeln!(out, "===================");
let _ = writeln!(out);
push_row(&mut out, "Version:", env!("CARGO_PKG_VERSION"));
push_row(&mut out, "Provider:", app.api_provider.as_str());
push_row(
&mut out,
"Model:",
&format!(
"{} (reasoning {})",
app.model_display_label(),
app.reasoning_effort_display_label()
),
);
push_row(&mut out, "Directory:", &display_path(&app.workspace));
push_row(&mut out, "Mode:", app.mode.label());
push_row(&mut out, "Permissions:", &permission_summary(app));
push_row(&mut out, "Project docs:", &project_docs(&app.workspace));
push_row(
&mut out,
"Session:",
app.current_session_id.as_deref().unwrap_or("not saved yet"),
);
push_row(
&mut out,
"MCP:",
&format!("{} configured", app.mcp_configured_count),
);
push_row(&mut out, "Footer items:", &footer_items(app));
let _ = writeln!(out);
push_row(
&mut out,
"Context window:",
&format!("{context_percent:.1}% used ({context_used} / {context_max} tokens)"),
);
push_row(
&mut out,
"Last API input:",
&token_count(app.session.last_prompt_tokens),
);
push_row(
&mut out,
"Last API output:",
&token_count(app.session.last_completion_tokens),
);
push_row(&mut out, "Cache hit/miss:", &cache_summary(app));
push_row(
&mut out,
"Total tokens:",
&app.session.total_tokens.to_string(),
);
push_row(
&mut out,
"Session cost:",
&app.format_cost_amount_precise(app.session_cost_for_currency(app.cost_currency)),
);
push_row(
&mut out,
"Transcript:",
&format!(
"{} cells, {} API messages",
app.history.len(),
app.api_messages.len()
),
);
push_row(
&mut out,
"Rate limits:",
"not available from provider telemetry",
);
let _ = writeln!(out);
let _ = writeln!(out, "Use /statusline to configure footer items.");
out
}
fn push_row(out: &mut String, label: &str, value: &str) {
let _ = writeln!(out, " {label:<16} {value}");
}
fn permission_summary(app: &App) -> String {
let trust = if app.trust_mode {
"trusted workspace"
} else {
"workspace"
};
let shell = if app.allow_shell {
"shell on"
} else {
"shell off"
};
format!(
"{trust}, approvals {}, {shell}",
app.approval_mode.label().to_ascii_lowercase()
)
}
fn project_docs(workspace: &Path) -> String {
let docs: Vec<&str> = ["AGENTS.md", "CLAUDE.md"]
.into_iter()
.filter(|name| workspace.join(name).is_file())
.collect();
if docs.is_empty() {
"not found".to_string()
} else {
docs.join(", ")
}
}
fn footer_items(app: &App) -> String {
if app.status_items.is_empty() {
return "none".to_string();
}
app.status_items
.iter()
.map(|item| item.key())
.collect::<Vec<_>>()
.join(", ")
}
fn context_usage(app: &App) -> (usize, u32, f64) {
let max = context_window_for_model(&app.model).unwrap_or(LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS);
let estimated =
estimate_input_tokens_conservative(&app.api_messages, app.system_prompt.as_ref());
let total_chars = estimate_message_chars(&app.api_messages);
let used = estimated.max(total_chars / 4);
let percent = ((used as f64 / f64::from(max)) * 100.0).clamp(0.0, 100.0);
(used, max, percent)
}
fn token_count(value: Option<u32>) -> String {
value.map_or_else(|| "not reported".to_string(), |tokens| tokens.to_string())
}
fn cache_summary(app: &App) -> String {
match (
app.session.last_prompt_cache_hit_tokens,
app.session.last_prompt_cache_miss_tokens,
) {
(Some(hit), Some(miss)) => format!("{hit} hit / {miss} miss"),
(Some(hit), None) => format!("{hit} hit / miss not reported"),
(None, Some(miss)) => format!("hit not reported / {miss} miss"),
(None, None) => "not reported".to_string(),
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use tempfile::TempDir;
use super::*;
use crate::config::{ApiProvider, Config};
use crate::models::{ContentBlock, Message};
use crate::tui::app::TuiOptions;
use crate::tui::history::HistoryCell;
fn create_test_app(workspace: PathBuf) -> App {
let options = TuiOptions {
model: "deepseek-v4-pro".to_string(),
workspace,
config_path: None,
config_profile: None,
allow_shell: false,
use_alt_screen: true,
use_mouse_capture: false,
use_bracketed_paste: true,
max_subagents: 1,
skills_dir: PathBuf::from("/tmp/test-skills"),
memory_path: PathBuf::from("memory.md"),
notes_path: PathBuf::from("notes.txt"),
mcp_config_path: PathBuf::from("mcp.json"),
use_memory: false,
start_in_agent_mode: false,
skip_onboarding: true,
yolo: false,
resume_session_id: None,
initial_input: None,
};
let mut app = App::new(options, &Config::default());
app.api_provider = ApiProvider::Deepseek;
app
}
#[test]
fn status_report_includes_runtime_fields() {
let tmpdir = TempDir::new().expect("temp dir");
std::fs::write(tmpdir.path().join("AGENTS.md"), "# Instructions").expect("write docs");
let mut app = create_test_app(tmpdir.path().to_path_buf());
app.current_session_id = Some("session-123".to_string());
app.session.total_tokens = 1234;
app.session.last_prompt_tokens = Some(100);
app.session.last_completion_tokens = Some(25);
app.session.last_prompt_cache_hit_tokens = Some(70);
app.session.last_prompt_cache_miss_tokens = Some(30);
app.api_messages.push(Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: "hello".to_string(),
cache_control: None,
}],
});
app.history.push(HistoryCell::User {
content: "hello".to_string(),
});
let result = status(&mut app);
let msg = result.message.expect("status message");
assert!(msg.contains("DeepSeek TUI Status"));
assert!(msg.contains("Provider:"));
assert!(msg.contains("Model:"));
assert!(msg.contains("Directory:"));
assert!(msg.contains("Permissions:"));
assert!(msg.contains("Project docs:"));
assert!(msg.contains("AGENTS.md"));
assert!(msg.contains("Session:"));
assert!(msg.contains("session-123"));
assert!(msg.contains("Context window:"));
assert!(msg.contains("Cache hit/miss:"));
assert!(msg.contains("70 hit / 30 miss"));
assert!(msg.contains("Use /statusline to configure footer items."));
}
#[test]
fn project_docs_reports_missing_docs() {
let tmpdir = TempDir::new().expect("temp dir");
assert_eq!(project_docs(tmpdir.path()), "not found");
}
}
+6
View File
@@ -259,6 +259,7 @@ pub enum MessageId {
CmdSkillDescription,
CmdSkillsDescription,
CmdStashDescription,
CmdStatusDescription,
CmdStatuslineDescription,
CmdSubagentsDescription,
CmdSwarmDescription,
@@ -448,6 +449,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CmdSkillDescription,
MessageId::CmdSkillsDescription,
MessageId::CmdStashDescription,
MessageId::CmdStatusDescription,
MessageId::CmdStatuslineDescription,
MessageId::CmdSubagentsDescription,
MessageId::CmdSwarmDescription,
@@ -785,6 +787,7 @@ fn english(id: MessageId) -> &'static str {
MessageId::CmdStashDescription => {
"Park or restore a composer draft (Ctrl+S to push, /stash list/pop)"
}
MessageId::CmdStatusDescription => "Show runtime session status",
MessageId::CmdStatuslineDescription => "Configure which items appear in the footer",
MessageId::CmdSubagentsDescription => "List sub-agent status",
MessageId::CmdSwarmDescription => {
@@ -1072,6 +1075,7 @@ fn japanese(id: MessageId) -> Option<&'static str> {
MessageId::CmdStashDescription => {
"コンポーザーの下書きを退避/復元(Ctrl+S で退避、/stash list|pop"
}
MessageId::CmdStatusDescription => "実行中のセッション状態を表示",
MessageId::CmdStatuslineDescription => "フッターに表示する項目を設定",
MessageId::CmdSubagentsDescription => "サブエージェントの状態を一覧表示",
MessageId::CmdSwarmDescription => {
@@ -1331,6 +1335,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
MessageId::CmdSkillDescription => "激活技能,或安装/更新/卸载/信任社区技能",
MessageId::CmdSkillsDescription => "列出本地技能(或使用 --remote 浏览精选注册表)",
MessageId::CmdStashDescription => "暂存或恢复输入草稿(Ctrl+S 暂存,/stash list|pop",
MessageId::CmdStatusDescription => "显示当前运行状态",
MessageId::CmdStatuslineDescription => "配置底栏要显示哪些条目",
MessageId::CmdSubagentsDescription => "列出子代理状态",
MessageId::CmdSwarmDescription => {
@@ -1604,6 +1609,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
MessageId::CmdStashDescription => {
"Estacionar ou restaurar rascunho do compositor (Ctrl+S estaciona, /stash list|pop)"
}
MessageId::CmdStatusDescription => "Exibir o status da sessão em execução",
MessageId::CmdStatuslineDescription => "Configurar quais itens aparecem no rodapé",
MessageId::CmdSubagentsDescription => "Listar o status dos sub-agentes",
MessageId::CmdSwarmDescription => {