Expose real workspace context in status output

Expand /status so it reports the current working directory, whether the CLI is operating on a live REPL or resumed session file, how many Claude config files were loaded, and how many instruction memory files were discovered. This makes status feel more like an operator dashboard instead of a bare token counter while still only surfacing metadata we can inspect locally.

Constraint: Status must only report context available from the current filesystem and session state
Rejected: Include guessed project metadata or upstream-only fields | would make the status output look richer than the implementation actually is
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep status additive and local-truthful; avoid inventing context that is not directly discoverable
Tested: cargo fmt --manifest-path ./rust/Cargo.toml --all; cargo clippy --manifest-path ./rust/Cargo.toml --workspace --all-targets -- -D warnings; cargo test --manifest-path ./rust/Cargo.toml --workspace
Not-tested: Manual interactive comparison of REPL /status versus resumed-session /status
This commit is contained in:
Yeachan-Heo
2026-03-31 20:22:59 +00:00
parent a8f5da6427
commit 2ad2ec087f

View File

@@ -251,6 +251,24 @@ struct ResumeCommandOutcome {
message: Option<String>, message: Option<String>,
} }
#[derive(Debug, Clone)]
struct StatusContext {
cwd: PathBuf,
session_path: Option<PathBuf>,
loaded_config_files: usize,
discovered_config_files: usize,
memory_file_count: usize,
}
#[derive(Debug, Clone, Copy)]
struct StatusUsage {
message_count: usize,
turns: u32,
latest: TokenUsage,
cumulative: TokenUsage,
estimated_tokens: usize,
}
fn run_resume_command( fn run_resume_command(
session_path: &Path, session_path: &Path,
session: &Session, session: &Session,
@@ -297,14 +315,17 @@ fn run_resume_command(
let usage = tracker.cumulative_usage(); let usage = tracker.cumulative_usage();
Ok(ResumeCommandOutcome { Ok(ResumeCommandOutcome {
session: session.clone(), session: session.clone(),
message: Some(format_status_line( message: Some(format_status_report(
"restored-session", "restored-session",
session.messages.len(), StatusUsage {
tracker.turns(), message_count: session.messages.len(),
tracker.current_turn_usage(), turns: tracker.turns(),
usage, latest: tracker.current_turn_usage(),
0, cumulative: usage,
estimated_tokens: 0,
},
permission_mode_label(), permission_mode_label(),
&status_context(Some(session_path))?,
)), )),
}) })
} }
@@ -443,14 +464,17 @@ impl LiveCli {
let latest = self.runtime.usage().current_turn_usage(); let latest = self.runtime.usage().current_turn_usage();
println!( println!(
"{}", "{}",
format_status_line( format_status_report(
&self.model, &self.model,
self.runtime.session().messages.len(), StatusUsage {
self.runtime.usage().turns(), message_count: self.runtime.session().messages.len(),
latest, turns: self.runtime.usage().turns(),
cumulative, latest,
self.runtime.estimated_tokens(), cumulative,
estimated_tokens: self.runtime.estimated_tokens(),
},
permission_mode_label(), permission_mode_label(),
&status_context(None).expect("status context should load"),
) )
); );
} }
@@ -586,21 +610,58 @@ fn render_repl_help() -> String {
) )
} }
fn format_status_line( fn status_context(
session_path: Option<&Path>,
) -> Result<StatusContext, Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
let loader = ConfigLoader::default_for(&cwd);
let discovered_config_files = loader.discover().len();
let runtime_config = loader.load()?;
let project_context = ProjectContext::discover(&cwd, DEFAULT_DATE)?;
Ok(StatusContext {
cwd,
session_path: session_path.map(Path::to_path_buf),
loaded_config_files: runtime_config.loaded_entries().len(),
discovered_config_files,
memory_file_count: project_context.instruction_files.len(),
})
}
fn format_status_report(
model: &str, model: &str,
message_count: usize, usage: StatusUsage,
turns: u32,
latest: TokenUsage,
cumulative: TokenUsage,
estimated_tokens: usize,
permission_mode: &str, permission_mode: &str,
context: &StatusContext,
) -> String { ) -> String {
format!( let mut lines = vec![format!(
"status: model={model} permission_mode={permission_mode} messages={message_count} turns={turns} estimated_tokens={estimated_tokens} latest_tokens={} cumulative_input_tokens={} cumulative_output_tokens={} cumulative_total_tokens={}", "status: model={model} permission_mode={permission_mode} messages={} turns={} estimated_tokens={} latest_tokens={} cumulative_input_tokens={} cumulative_output_tokens={} cumulative_total_tokens={}",
latest.total_tokens(), usage.message_count,
cumulative.input_tokens, usage.turns,
cumulative.output_tokens, usage.estimated_tokens,
cumulative.total_tokens(), usage.latest.total_tokens(),
usage.cumulative.input_tokens,
usage.cumulative.output_tokens,
usage.cumulative.total_tokens(),
)];
lines.push(format!(" cwd {}", context.cwd.display()));
lines.push(format!(
" session {}",
context.session_path.as_ref().map_or_else(
|| "live-repl".to_string(),
|path| path.display().to_string()
)
));
lines.push(format!(
" config loaded {}/{} files",
context.loaded_config_files, context.discovered_config_files
));
lines.push(format!(
" memory {} instruction files",
context.memory_file_count
));
lines.join(
"
",
) )
} }
@@ -1097,8 +1158,9 @@ fn print_help() {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
format_status_line, normalize_permission_mode, parse_args, render_init_claude_md, format_status_report, normalize_permission_mode, parse_args, render_init_claude_md,
render_repl_help, resume_supported_slash_commands, CliAction, SlashCommand, DEFAULT_MODEL, render_repl_help, resume_supported_slash_commands, status_context, CliAction, SlashCommand,
StatusUsage, DEFAULT_MODEL,
}; };
use runtime::{ContentBlock, ConversationMessage, MessageRole}; use runtime::{ContentBlock, ConversationMessage, MessageRole};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -1215,30 +1277,51 @@ mod tests {
#[test] #[test]
fn status_line_reports_model_and_token_totals() { fn status_line_reports_model_and_token_totals() {
let status = format_status_line( let status = format_status_report(
"claude-sonnet", "claude-sonnet",
7, StatusUsage {
3, message_count: 7,
runtime::TokenUsage { turns: 3,
input_tokens: 5, latest: runtime::TokenUsage {
output_tokens: 4, input_tokens: 5,
cache_creation_input_tokens: 1, output_tokens: 4,
cache_read_input_tokens: 0, cache_creation_input_tokens: 1,
cache_read_input_tokens: 0,
},
cumulative: runtime::TokenUsage {
input_tokens: 20,
output_tokens: 8,
cache_creation_input_tokens: 2,
cache_read_input_tokens: 1,
},
estimated_tokens: 128,
}, },
runtime::TokenUsage {
input_tokens: 20,
output_tokens: 8,
cache_creation_input_tokens: 2,
cache_read_input_tokens: 1,
},
128,
"workspace-write", "workspace-write",
&super::StatusContext {
cwd: PathBuf::from("/tmp/project"),
session_path: Some(PathBuf::from("session.json")),
loaded_config_files: 2,
discovered_config_files: 3,
memory_file_count: 4,
},
); );
assert!(status.contains("model=claude-sonnet")); assert!(status.contains("model=claude-sonnet"));
assert!(status.contains("permission_mode=workspace-write")); assert!(status.contains("permission_mode=workspace-write"));
assert!(status.contains("messages=7")); assert!(status.contains("messages=7"));
assert!(status.contains("latest_tokens=10")); assert!(status.contains("latest_tokens=10"));
assert!(status.contains("cumulative_total_tokens=31")); assert!(status.contains("cumulative_total_tokens=31"));
assert!(status.contains("cwd /tmp/project"));
assert!(status.contains("session session.json"));
assert!(status.contains("config loaded 2/3 files"));
assert!(status.contains("memory 4 instruction files"));
}
#[test]
fn status_context_reads_real_workspace_metadata() {
let context = status_context(None).expect("status context should load");
assert!(context.cwd.is_absolute());
assert_eq!(context.discovered_config_files, 3);
assert!(context.loaded_config_files <= context.discovered_config_files);
} }
#[test] #[test]