From 1adf11d572e9c5811f59e2a87011b3002932fc36 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 21:06:51 +0000 Subject: [PATCH] Enrich status with git and project context Extend /status with project root and git branch details derived from the local repository so the report feels closer to a real Claude Code session dashboard. This adds high-value workspace context without inventing any persisted metadata the runtime does not actually have. Constraint: Status metadata must be computed from the current working tree at runtime and tolerate non-git directories Rejected: Persist branch/root into session files first | a local runtime derivation is smaller and immediately useful without changing session format Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep status context opportunistic and degrade cleanly to unknown when git metadata is unavailable 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 non-git-directory /status run --- rust/crates/rusty-claude-cli/src/main.rs | 68 ++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index fc425b0..9d878e0 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -258,6 +258,8 @@ struct StatusContext { loaded_config_files: usize, discovered_config_files: usize, memory_file_count: usize, + project_root: Option, + git_branch: Option, } #[derive(Debug, Clone, Copy)] @@ -336,6 +338,39 @@ fn format_resume_report(session_path: &str, message_count: usize, turns: u32) -> ) } +fn parse_git_status_metadata(status: Option<&str>) -> (Option, Option) { + let Some(status) = status else { + return (None, None); + }; + let branch = status.lines().next().and_then(|line| { + line.strip_prefix("## ") + .map(|line| { + line.split(['.', ' ']) + .next() + .unwrap_or_default() + .to_string() + }) + .filter(|value| !value.is_empty()) + }); + let project_root = find_git_root().ok(); + (project_root, branch) +} + +fn find_git_root() -> Result> { + let output = std::process::Command::new("git") + .args(["rev-parse", "--show-toplevel"]) + .current_dir(env::current_dir()?) + .output()?; + if !output.status.success() { + return Err("not a git repository".into()); + } + let path = String::from_utf8(output.stdout)?.trim().to_string(); + if path.is_empty() { + return Err("empty git root".into()); + } + Ok(PathBuf::from(path)) +} + fn run_resume_command( session_path: &Path, session: &Session, @@ -724,13 +759,17 @@ fn status_context( 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)?; + let project_context = ProjectContext::discover_with_git(&cwd, DEFAULT_DATE)?; + let (project_root, git_branch) = + parse_git_status_metadata(project_context.git_status.as_deref()); 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(), + project_root, + git_branch, }) } @@ -764,10 +803,17 @@ fn format_status_report( format!( "Workspace Cwd {} + Project root {} + Git branch {} Session {} Config files loaded {}/{} Memory files {}", context.cwd.display(), + context + .project_root + .as_ref() + .map_or_else(|| "unknown".to_string(), |path| path.display().to_string()), + context.git_branch.as_deref().unwrap_or("unknown"), context.session_path.as_ref().map_or_else( || "live-repl".to_string(), |path| path.display().to_string() @@ -1287,9 +1333,9 @@ mod tests { use super::{ format_cost_report, format_model_report, format_model_switch_report, format_permissions_report, format_permissions_switch_report, format_resume_report, - format_status_report, normalize_permission_mode, parse_args, render_init_claude_md, - render_repl_help, resume_supported_slash_commands, status_context, CliAction, SlashCommand, - StatusUsage, DEFAULT_MODEL, + format_status_report, normalize_permission_mode, parse_args, parse_git_status_metadata, + render_init_claude_md, render_repl_help, resume_supported_slash_commands, status_context, + CliAction, SlashCommand, StatusUsage, DEFAULT_MODEL, }; use runtime::{ContentBlock, ConversationMessage, MessageRole}; use std::path::{Path, PathBuf}; @@ -1500,6 +1546,8 @@ mod tests { loaded_config_files: 2, discovered_config_files: 3, memory_file_count: 4, + project_root: Some(PathBuf::from("/tmp")), + git_branch: Some("main".to_string()), }, ); assert!(status.contains("Status")); @@ -1509,6 +1557,8 @@ mod tests { assert!(status.contains("Latest total 10")); assert!(status.contains("Cumulative total 31")); assert!(status.contains("Cwd /tmp/project")); + assert!(status.contains("Project root /tmp")); + assert!(status.contains("Git branch main")); assert!(status.contains("Session session.json")); assert!(status.contains("Config files loaded 2/3")); assert!(status.contains("Memory files 4")); @@ -1522,6 +1572,16 @@ mod tests { assert!(report.contains("Merged JSON")); } + #[test] + fn parses_git_status_metadata() { + let (root, branch) = parse_git_status_metadata(Some( + "## rcc/cli...origin/rcc/cli + M src/main.rs", + )); + assert_eq!(branch.as_deref(), Some("rcc/cli")); + let _ = root; + } + #[test] fn status_context_reads_real_workspace_metadata() { let context = status_context(None).expect("status context should load");