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:
@@ -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),
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user