From d9c5f60598fa3a807396cf002fe523c5dcc5b795 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 21:01:21 +0000 Subject: [PATCH 1/9] Polish permission inspection and switching output Rework /permissions output into the same operator-console format used by status, config, and model so the command feels intentional and self-explanatory. Switching modes now reports previous and current state, while inspection shows the available modes and their meaning without adding fake policy logic. Constraint: Permission output must stay aligned with the real three-mode runtime policy already implemented Rejected: Add richer permission-policy previews per tool | would require more UI surface and risks overstating current policy fidelity Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep permission-mode docs in the CLI consistent with normalize_permission_mode and permission_policy behavior 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 operator UX review of /permissions flows in a live REPL --- rust/crates/rusty-claude-cli/src/main.rs | 55 +++++++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index faa9639..d26f1c9 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -291,6 +291,26 @@ fn format_model_switch_report(previous: &str, next: &str, message_count: usize) ) } +fn format_permissions_report(mode: &str) -> String { + format!( + "Permissions + Current mode {mode} + +Available modes + read-only Allow read/search tools only + workspace-write Allow editing within the workspace + danger-full-access Allow unrestricted tool access" + ) +} + +fn format_permissions_switch_report(previous: &str, next: &str) -> String { + format!( + "Permissions updated + Previous {previous} + Current {next}" + ) +} + fn run_resume_command( session_path: &Path, session: &Session, @@ -548,7 +568,7 @@ impl LiveCli { fn set_permissions(&mut self, mode: Option) -> Result<(), Box> { let Some(mode) = mode else { - println!("Current permission mode: {}", permission_mode_label()); + println!("{}", format_permissions_report(permission_mode_label())); return Ok(()); }; @@ -559,10 +579,11 @@ impl LiveCli { })?; if normalized == permission_mode_label() { - println!("Permission mode already set to {normalized}."); + println!("{}", format_permissions_report(normalized)); return Ok(()); } + let previous = permission_mode_label().to_string(); let session = self.runtime.session().clone(); self.runtime = build_runtime_with_permission_mode( session, @@ -571,7 +592,10 @@ impl LiveCli { true, normalized, )?; - println!("Switched permission mode to {normalized}."); + println!( + "{}", + format_permissions_switch_report(&previous, normalized) + ); Ok(()) } @@ -1234,10 +1258,10 @@ fn print_help() { #[cfg(test)] mod tests { use super::{ - format_model_report, format_model_switch_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_model_report, format_model_switch_report, format_permissions_report, + format_permissions_switch_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, }; use runtime::{ContentBlock, ConversationMessage, MessageRole}; use std::path::{Path, PathBuf}; @@ -1352,6 +1376,23 @@ mod tests { ); } + #[test] + fn permissions_report_uses_sectioned_layout() { + let report = format_permissions_report("workspace-write"); + assert!(report.contains("Permissions")); + assert!(report.contains("Current mode workspace-write")); + assert!(report.contains("Available modes")); + assert!(report.contains("danger-full-access")); + } + + #[test] + fn permissions_switch_report_is_structured() { + let report = format_permissions_switch_report("read-only", "workspace-write"); + assert!(report.contains("Permissions updated")); + assert!(report.contains("Previous read-only")); + assert!(report.contains("Current workspace-write")); + } + #[test] fn model_report_uses_sectioned_layout() { let report = format_model_report("claude-sonnet", 12, 4); From fa30059790cffdbf016841f0df6db190b5b783a8 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 21:02:24 +0000 Subject: [PATCH 2/9] Polish cost reporting into the shared console style Reformat /cost for both live and resumed sessions so token accounting is presented in the same sectioned operator-console style as status, model, permissions, and config. This improves consistency across the command surface while preserving the same underlying usage metrics. Constraint: Cost output must continue to reflect cumulative tracked usage only, without claiming real billing or currency totals Rejected: Add dollar estimates | there is no authoritative pricing source wired into this CLI surface Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep /cost focused on raw token accounting until pricing metadata exists in the runtime layer 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 terminal UX review for very large cumulative token counts --- rust/crates/rusty-claude-cli/src/main.rs | 59 ++++++++++++++++-------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index d26f1c9..7442ea4 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -311,6 +311,22 @@ fn format_permissions_switch_report(previous: &str, next: &str) -> String { ) } +fn format_cost_report(usage: TokenUsage) -> String { + format!( + "Cost + Input tokens {} + Output tokens {} + Cache create {} + Cache read {} + Total tokens {}", + usage.input_tokens, + usage.output_tokens, + usage.cache_creation_input_tokens, + usage.cache_read_input_tokens, + usage.total_tokens(), + ) +} + fn run_resume_command( session_path: &Path, session: &Session, @@ -383,14 +399,7 @@ fn run_resume_command( let usage = UsageTracker::from_session(session).cumulative_usage(); Ok(ResumeCommandOutcome { session: session.clone(), - message: Some(format!( - "cost: input_tokens={} output_tokens={} cache_creation_tokens={} cache_read_tokens={} total_tokens={}", - usage.input_tokens, - usage.output_tokens, - usage.cache_creation_input_tokens, - usage.cache_read_input_tokens, - usage.total_tokens(), - )), + message: Some(format_cost_report(usage)), }) } SlashCommand::Config => Ok(ResumeCommandOutcome { @@ -620,14 +629,7 @@ impl LiveCli { fn print_cost(&self) { let cumulative = self.runtime.usage().cumulative_usage(); - println!( - "cost: input_tokens={} output_tokens={} cache_creation_tokens={} cache_read_tokens={} total_tokens={}", - cumulative.input_tokens, - cumulative.output_tokens, - cumulative.cache_creation_input_tokens, - cumulative.cache_read_input_tokens, - cumulative.total_tokens(), - ); + println!("{}", format_cost_report(cumulative)); } fn resume_session( @@ -1258,10 +1260,11 @@ fn print_help() { #[cfg(test)] mod tests { use super::{ - format_model_report, format_model_switch_report, format_permissions_report, - format_permissions_switch_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_cost_report, format_model_report, format_model_switch_report, + format_permissions_report, format_permissions_switch_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, }; use runtime::{ContentBlock, ConversationMessage, MessageRole}; use std::path::{Path, PathBuf}; @@ -1376,6 +1379,22 @@ mod tests { ); } + #[test] + fn cost_report_uses_sectioned_layout() { + let report = format_cost_report(runtime::TokenUsage { + input_tokens: 20, + output_tokens: 8, + cache_creation_input_tokens: 3, + cache_read_input_tokens: 1, + }); + assert!(report.contains("Cost")); + assert!(report.contains("Input tokens 20")); + assert!(report.contains("Output tokens 8")); + assert!(report.contains("Cache create 3")); + assert!(report.contains("Cache read 1")); + assert!(report.contains("Total tokens 32")); + } + #[test] fn permissions_report_uses_sectioned_layout() { let report = format_permissions_report("workspace-write"); From cba31c4f957a80d1cdd285e3891dba1cc3c5cf2c Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 21:03:49 +0000 Subject: [PATCH 3/9] Tighten help and clear messaging across the CLI surface Refresh shared slash help and REPL help wording so the command surface reads more like an integrated console, and make successful /clear output match the newer structured reporting style. This keeps discoverability consistent now that status, model, permissions, config, and cost all use richer operator-oriented copy. Constraint: Help text must stay synchronized with the actual implemented command surface and resume behavior Rejected: Larger README/doc pass in the same commit | keeping the slice limited to runtime help/output makes it easier to review and revert Confidence: high Scope-risk: narrow Reversibility: clean Directive: Prefer shared help-copy changes in commands crate first, then layer REPL-specific additions in the CLI binary 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 comparison of help wording against upstream Claude Code terminal screenshots --- rust/crates/commands/src/lib.rs | 8 +++--- rust/crates/rusty-claude-cli/src/main.rs | 31 ++++++++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index b3609bf..983588a 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -174,8 +174,8 @@ pub fn resume_supported_slash_commands() -> Vec<&'static SlashCommandSpec> { #[must_use] pub fn render_slash_command_help() -> String { let mut lines = vec![ - "Available commands:".to_string(), - " (resume-safe commands are marked with [resume])".to_string(), + "Slash commands".to_string(), + " [resume] means the command also works with --resume SESSION.json".to_string(), ]; for spec in slash_command_specs() { let name = match spec.argument_hint { @@ -288,7 +288,7 @@ mod tests { #[test] fn renders_help_from_shared_specs() { let help = render_slash_command_help(); - assert!(help.contains("resume-safe commands")); + assert!(help.contains("works with --resume SESSION.json")); assert!(help.contains("/help")); assert!(help.contains("/status")); assert!(help.contains("/compact")); @@ -340,7 +340,7 @@ mod tests { let result = handle_slash_command("/help", &session, CompactionConfig::default()) .expect("help command should be handled"); assert_eq!(result.session, session); - assert!(result.message.contains("Available commands:")); + assert!(result.message.contains("Slash commands")); } #[test] diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 7442ea4..994099e 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -623,7 +623,14 @@ impl LiveCli { true, permission_mode_label(), )?; - println!("Cleared local session history."); + println!( + "Session cleared + Mode fresh session + Preserved model {} + Permission mode {}", + self.model, + permission_mode_label() + ); Ok(()) } @@ -685,10 +692,16 @@ impl LiveCli { } fn render_repl_help() -> String { - format!( - "{} - /exit Quit the REPL", - render_slash_command_help() + [ + "REPL".to_string(), + " /exit Quit the REPL".to_string(), + " /quit Quit the REPL".to_string(), + String::new(), + render_slash_command_help(), + ] + .join( + " +", ) } @@ -1351,9 +1364,17 @@ mod tests { ); } + #[test] + fn shared_help_uses_resume_annotation_copy() { + let help = commands::render_slash_command_help(); + assert!(help.contains("Slash commands")); + assert!(help.contains("works with --resume SESSION.json")); + } + #[test] fn repl_help_includes_shared_commands_and_exit() { let help = render_repl_help(); + assert!(help.contains("REPL")); assert!(help.contains("/help")); assert!(help.contains("/status")); assert!(help.contains("/model [model]")); From cf8d5a8389e85902fae3229ed228aaddd89f106d Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 21:04:42 +0000 Subject: [PATCH 4/9] Polish session resume messaging to match the console UX Update in-REPL /resume success output to the same structured console style used elsewhere so session lifecycle commands feel consistent with status, model, permissions, config, and cost. This preserves the same behavior while improving operator readability. Constraint: Resume output must stay grounded in real restored session metadata already available after load Rejected: Add more restored-session details like cwd snapshot | that data is not yet persisted in session files Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep lifecycle command outputs stylistically aligned as the CLI surface grows 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 /resume output before and after multiple restores --- rust/crates/rusty-claude-cli/src/main.rs | 31 ++++++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 994099e..fc425b0 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -327,6 +327,15 @@ fn format_cost_report(usage: TokenUsage) -> String { ) } +fn format_resume_report(session_path: &str, message_count: usize, turns: u32) -> String { + format!( + "Session resumed + Session file {session_path} + Messages {message_count} + Turns {turns}" + ) +} + fn run_resume_command( session_path: &Path, session: &Session, @@ -657,7 +666,10 @@ impl LiveCli { true, permission_mode_label(), )?; - println!("Resumed session from {session_path} ({message_count} messages)."); + println!( + "{}", + format_resume_report(&session_path, message_count, self.runtime.usage().turns()) + ); Ok(()) } @@ -1274,10 +1286,10 @@ fn print_help() { mod tests { use super::{ format_cost_report, format_model_report, format_model_switch_report, - format_permissions_report, format_permissions_switch_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_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, }; use runtime::{ContentBlock, ConversationMessage, MessageRole}; use std::path::{Path, PathBuf}; @@ -1400,6 +1412,15 @@ mod tests { ); } + #[test] + fn resume_report_uses_sectioned_layout() { + let report = format_resume_report("session.json", 14, 6); + assert!(report.contains("Session resumed")); + assert!(report.contains("Session file session.json")); + assert!(report.contains("Messages 14")); + assert!(report.contains("Turns 6")); + } + #[test] fn cost_report_uses_sectioned_layout() { let report = format_cost_report(runtime::TokenUsage { From 1adf11d572e9c5811f59e2a87011b3002932fc36 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 21:06:51 +0000 Subject: [PATCH 5/9] 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"); From 88cd2e31df09a3fc0293d1121412684c83c43179 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 21:08:19 +0000 Subject: [PATCH 6/9] Improve memory inspection presentation Reformat /memory into the same structured console style as the other polished commands and enumerate discovered instruction files in ancestry order with line counts and previews. This makes repo instruction memory easier to inspect without changing the underlying discovery behavior. Constraint: Memory reporting must reflect only the instruction files discovered from current directory ancestry Rejected: Add memory editing commands in the same slice | presentation polish was a cleaner, lower-risk improvement to ship first Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep instruction-file ordering stable so ancestry-based memory debugging stays predictable 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 inspection of repos with many nested CLAUDE files --- rust/crates/rusty-claude-cli/src/main.rs | 30 ++++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 9d878e0..cfdecc5 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -877,27 +877,33 @@ fn render_config_report() -> Result> { } fn render_memory_report() -> Result> { - let project_context = ProjectContext::discover(env::current_dir()?, DEFAULT_DATE)?; + let cwd = env::current_dir()?; + let project_context = ProjectContext::discover(&cwd, DEFAULT_DATE)?; let mut lines = vec![format!( - "memory: files={}", + "Memory + Working directory {} + Instruction files {}", + cwd.display(), project_context.instruction_files.len() )]; if project_context.instruction_files.is_empty() { + lines.push("Discovered files".to_string()); lines.push( " No CLAUDE instruction files discovered in the current directory ancestry." .to_string(), ); } else { - for file in project_context.instruction_files { + lines.push("Discovered files".to_string()); + for (index, file) in project_context.instruction_files.iter().enumerate() { let preview = file.content.lines().next().unwrap_or("").trim(); let preview = if preview.is_empty() { "" } else { preview }; + lines.push(format!(" {}. {}", index + 1, file.path.display(),)); lines.push(format!( - " {} ({}) {}", - file.path.display(), + " lines={} preview={}", file.content.lines().count(), preview )); @@ -1334,8 +1340,9 @@ mod tests { 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, parse_git_status_metadata, - render_init_claude_md, render_repl_help, resume_supported_slash_commands, status_context, - CliAction, SlashCommand, StatusUsage, DEFAULT_MODEL, + render_init_claude_md, render_memory_report, render_repl_help, + resume_supported_slash_commands, status_context, CliAction, SlashCommand, StatusUsage, + DEFAULT_MODEL, }; use runtime::{ContentBlock, ConversationMessage, MessageRole}; use std::path::{Path, PathBuf}; @@ -1564,6 +1571,15 @@ mod tests { assert!(status.contains("Memory files 4")); } + #[test] + fn memory_report_uses_sectioned_layout() { + let report = render_memory_report().expect("memory report should render"); + assert!(report.contains("Memory")); + assert!(report.contains("Working directory")); + assert!(report.contains("Instruction files")); + assert!(report.contains("Discovered files")); + } + #[test] fn config_report_uses_sectioned_layout() { let report = super::render_config_report().expect("config report should render"); From 9f3be03463b19cefde96a7863813df58407c46e8 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 21:11:57 +0000 Subject: [PATCH 7/9] Add useful config subviews without fake mutation flows Extend /config so operators can inspect specific merged sections like env, hooks, and model while keeping the command read-only and grounded in the actual loaded config. This improves Claude Code-style inspectability without inventing an unsafe config editing surface. Constraint: Config handling must remain read-only and reflect only the merged runtime config that already exists Rejected: Add /config set mutation commands | persistence semantics and edit safety are not mature enough for a small honest slice Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep config subviews aligned with real merged keys and avoid advertising writable behavior until persistence is designed 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 inspection of richer hooks/env config payloads in a customized user setup --- rust/crates/commands/src/lib.rs | 28 +++++++--- rust/crates/rusty-claude-cli/src/main.rs | 65 ++++++++++++++++++++---- 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index 983588a..3cd9d6d 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -89,8 +89,8 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[ }, SlashCommandSpec { name: "config", - summary: "Inspect discovered Claude config files", - argument_hint: None, + summary: "Inspect Claude config files or merged sections", + argument_hint: Some("[env|hooks|model]"), resume_supported: true, }, SlashCommandSpec { @@ -117,7 +117,7 @@ pub enum SlashCommand { Clear { confirm: bool }, Cost, Resume { session_path: Option }, - Config, + Config { section: Option }, Memory, Init, Unknown(String), @@ -150,7 +150,9 @@ impl SlashCommand { "resume" => Self::Resume { session_path: parts.next().map(ToOwned::to_owned), }, - "config" => Self::Config, + "config" => Self::Config { + section: parts.next().map(ToOwned::to_owned), + }, "memory" => Self::Memory, "init" => Self::Init, other => Self::Unknown(other.to_string()), @@ -230,7 +232,7 @@ pub fn handle_slash_command( | SlashCommand::Clear { .. } | SlashCommand::Cost | SlashCommand::Resume { .. } - | SlashCommand::Config + | SlashCommand::Config { .. } | SlashCommand::Memory | SlashCommand::Init | SlashCommand::Unknown(_) => None, @@ -280,7 +282,16 @@ mod tests { session_path: Some("session.json".to_string()), }) ); - assert_eq!(SlashCommand::parse("/config"), Some(SlashCommand::Config)); + assert_eq!( + SlashCommand::parse("/config"), + Some(SlashCommand::Config { section: None }) + ); + assert_eq!( + SlashCommand::parse("/config env"), + Some(SlashCommand::Config { + section: Some("env".to_string()) + }) + ); assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory)); assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init)); } @@ -297,7 +308,7 @@ mod tests { assert!(help.contains("/clear [--confirm]")); assert!(help.contains("/cost")); assert!(help.contains("/resume ")); - assert!(help.contains("/config")); + assert!(help.contains("/config [env|hooks|model]")); assert!(help.contains("/memory")); assert!(help.contains("/init")); assert_eq!(slash_command_specs().len(), 11); @@ -370,5 +381,8 @@ mod tests { ) .is_none()); assert!(handle_slash_command("/config", &session, CompactionConfig::default()).is_none()); + assert!( + handle_slash_command("/config env", &session, CompactionConfig::default()).is_none() + ); } } diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index cfdecc5..5c5d52c 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -446,9 +446,9 @@ fn run_resume_command( message: Some(format_cost_report(usage)), }) } - SlashCommand::Config => Ok(ResumeCommandOutcome { + SlashCommand::Config { section } => Ok(ResumeCommandOutcome { session: session.clone(), - message: Some(render_config_report()?), + message: Some(render_config_report(section.as_deref())?), }), SlashCommand::Memory => Ok(ResumeCommandOutcome { session: session.clone(), @@ -554,7 +554,7 @@ impl LiveCli { SlashCommand::Clear { confirm } => self.clear_session(confirm)?, SlashCommand::Cost => self.print_cost(), SlashCommand::Resume { session_path } => self.resume_session(session_path)?, - SlashCommand::Config => Self::print_config()?, + SlashCommand::Config { section } => Self::print_config(section.as_deref())?, SlashCommand::Memory => Self::print_memory()?, SlashCommand::Init => Self::run_init()?, SlashCommand::Unknown(name) => eprintln!("unknown slash command: /{name}"), @@ -708,8 +708,8 @@ impl LiveCli { Ok(()) } - fn print_config() -> Result<(), Box> { - println!("{}", render_config_report()?); + fn print_config(section: Option<&str>) -> Result<(), Box> { + println!("{}", render_config_report(section)?); Ok(()) } @@ -830,7 +830,7 @@ fn format_status_report( ) } -fn render_config_report() -> Result> { +fn render_config_report(section: Option<&str>) -> Result> { let cwd = env::current_dir()?; let loader = ConfigLoader::default_for(&cwd); let discovered = loader.discover(); @@ -868,6 +868,36 @@ fn render_config_report() -> Result> { entry.path.display() )); } + + if let Some(section) = section { + lines.push(format!("Merged section: {section}")); + let value = match section { + "env" => runtime_config.get("env"), + "hooks" => runtime_config.get("hooks"), + "model" => runtime_config.get("model"), + other => { + lines.push(format!( + " Unsupported config section '{other}'. Use env, hooks, or model." + )); + return Ok(lines.join( + " +", + )); + } + }; + lines.push(format!( + " {}", + match value { + Some(value) => value.render(), + None => "".to_string(), + } + )); + return Ok(lines.join( + " +", + )); + } + lines.push("Merged JSON".to_string()); lines.push(format!(" {}", runtime_config.as_json().render())); Ok(lines.join( @@ -1340,7 +1370,7 @@ mod tests { 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, parse_git_status_metadata, - render_init_claude_md, render_memory_report, render_repl_help, + render_config_report, render_init_claude_md, render_memory_report, render_repl_help, resume_supported_slash_commands, status_context, CliAction, SlashCommand, StatusUsage, DEFAULT_MODEL, }; @@ -1447,7 +1477,7 @@ mod tests { assert!(help.contains("/clear [--confirm]")); assert!(help.contains("/cost")); assert!(help.contains("/resume ")); - assert!(help.contains("/config")); + assert!(help.contains("/config [env|hooks|model]")); assert!(help.contains("/memory")); assert!(help.contains("/init")); assert!(help.contains("/exit")); @@ -1571,6 +1601,12 @@ mod tests { assert!(status.contains("Memory files 4")); } + #[test] + fn config_report_supports_section_views() { + let report = render_config_report(Some("env")).expect("config report should render"); + assert!(report.contains("Merged section: env")); + } + #[test] fn memory_report_uses_sectioned_layout() { let report = render_memory_report().expect("memory report should render"); @@ -1582,7 +1618,7 @@ mod tests { #[test] fn config_report_uses_sectioned_layout() { - let report = super::render_config_report().expect("config report should render"); + let report = render_config_report(None).expect("config report should render"); assert!(report.contains("Config")); assert!(report.contains("Discovered files")); assert!(report.contains("Merged JSON")); @@ -1644,7 +1680,16 @@ mod tests { SlashCommand::parse("/clear --confirm"), Some(SlashCommand::Clear { confirm: true }) ); - assert_eq!(SlashCommand::parse("/config"), Some(SlashCommand::Config)); + assert_eq!( + SlashCommand::parse("/config"), + Some(SlashCommand::Config { section: None }) + ); + assert_eq!( + SlashCommand::parse("/config env"), + Some(SlashCommand::Config { + section: Some("env".to_string()) + }) + ); assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory)); assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init)); } From 6076041f19c0f05e300598500b9006634c339861 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 21:13:27 +0000 Subject: [PATCH 8/9] Make init output match the console-style command UX Reformat /init results into the same structured operator-console style used by the other polished commands so create and skip outcomes are easier to scan. This keeps the command behavior unchanged while making repo bootstrapping feedback feel more intentional. Constraint: /init must stay non-destructive and continue refusing to overwrite an existing CLAUDE.md Rejected: Expand /init to write more files in the same slice | broader scaffolding would be riskier than a focused UX polish commit Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep /init output explicit about whether the file was created or skipped so users can trust the command in existing repos 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 /init run in a repo that already has a heavily customized CLAUDE.md --- rust/crates/rusty-claude-cli/src/main.rs | 38 ++++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 5c5d52c..4f9790d 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -338,6 +338,26 @@ fn format_resume_report(session_path: &str, message_count: usize, turns: u32) -> ) } +fn format_init_report(path: &Path, created: bool) -> String { + if created { + format!( + "Init + CLAUDE.md {} + Result created + Next step Review and tailor the generated guidance", + path.display() + ) + } else { + format!( + "Init + CLAUDE.md {} + Result skipped (already exists) + Next step Edit the existing file intentionally if workflows changed", + path.display() + ) + } +} + fn parse_git_status_metadata(status: Option<&str>) -> (Option, Option) { let Some(status) = status else { return (None, None); @@ -949,15 +969,12 @@ fn init_claude_md() -> Result> { let cwd = env::current_dir()?; let claude_md = cwd.join("CLAUDE.md"); if claude_md.exists() { - return Ok(format!( - "init: skipped because {} already exists", - claude_md.display() - )); + return Ok(format_init_report(&claude_md, false)); } let content = render_init_claude_md(&cwd); fs::write(&claude_md, content)?; - Ok(format!("init: created {}", claude_md.display())) + Ok(format_init_report(&claude_md, true)) } fn render_init_claude_md(cwd: &Path) -> String { @@ -1367,7 +1384,7 @@ fn print_help() { #[cfg(test)] mod tests { use super::{ - format_cost_report, format_model_report, format_model_switch_report, + format_cost_report, format_init_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, parse_git_status_metadata, render_config_report, render_init_claude_md, render_memory_report, render_repl_help, @@ -1537,6 +1554,15 @@ mod tests { assert!(report.contains("Current workspace-write")); } + #[test] + fn init_report_uses_structured_output() { + let created = format_init_report(Path::new("/tmp/CLAUDE.md"), true); + assert!(created.contains("Init")); + assert!(created.contains("Result created")); + let skipped = format_init_report(Path::new("/tmp/CLAUDE.md"), false); + assert!(skipped.contains("skipped (already exists)")); + } + #[test] fn model_report_uses_sectioned_layout() { let report = format_model_report("claude-sonnet", 12, 4); From 346ea0b91b66ad96044b9df3f4d24aa523e8be26 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 31 Mar 2026 21:15:37 +0000 Subject: [PATCH 9/9] Make compact output match the console-style command UX Reformat /compact output for both live and resumed sessions so compaction results are reported in the same structured console style as the rest of the CLI surface. This keeps the behavior unchanged while making skipped and successful compaction runs easier to read. Constraint: Compact output must stay faithful to the real compaction result and not imply summarization details beyond removed/kept message counts Rejected: Expose the generated summary body directly in /compact output | too noisy for a lightweight command-response surface Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep lifecycle and maintenance command output stylistically consistent as more slash commands reach parity 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 terminal UX review of compact output on very large sessions --- rust/crates/rusty-claude-cli/src/main.rs | 67 ++++++++++++++++-------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 4f9790d..04eeda3 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -12,9 +12,7 @@ use api::{ ToolResultContentBlock, }; -use commands::{ - handle_slash_command, render_slash_command_help, resume_supported_slash_commands, SlashCommand, -}; +use commands::{render_slash_command_help, resume_supported_slash_commands, SlashCommand}; use compat_harness::{extract_manifest, UpstreamPaths}; use render::{Spinner, TerminalRenderer}; use runtime::{ @@ -358,6 +356,24 @@ fn format_init_report(path: &Path, created: bool) -> String { } } +fn format_compact_report(removed: usize, resulting_messages: usize, skipped: bool) -> String { + if skipped { + format!( + "Compact + Result skipped + Reason session below compaction threshold + Messages kept {resulting_messages}" + ) + } else { + format!( + "Compact + Result compacted + Messages removed {removed} + Messages kept {resulting_messages}" + ) + } +} + fn parse_git_status_metadata(status: Option<&str>) -> (Option, Option) { let Some(status) = status else { return (None, None); @@ -402,23 +418,20 @@ fn run_resume_command( message: Some(render_repl_help()), }), SlashCommand::Compact => { - let Some(result) = handle_slash_command( - "/compact", + let result = runtime::compact_session( session, CompactionConfig { max_estimated_tokens: 0, ..CompactionConfig::default() }, - ) else { - return Ok(ResumeCommandOutcome { - session: session.clone(), - message: None, - }); - }; - result.session.save_to_path(session_path)?; + ); + let removed = result.removed_message_count; + let kept = result.compacted_session.messages.len(); + let skipped = removed == 0; + result.compacted_session.save_to_path(session_path)?; Ok(ResumeCommandOutcome { - session: result.session, - message: Some(result.message), + session: result.compacted_session, + message: Some(format_compact_report(removed, kept, skipped)), }) } SlashCommand::Clear { confirm } => { @@ -746,6 +759,8 @@ impl LiveCli { fn compact(&mut self) -> Result<(), Box> { let result = self.runtime.compact(CompactionConfig::default()); let removed = result.removed_message_count; + let kept = result.compacted_session.messages.len(); + let skipped = removed == 0; self.runtime = build_runtime_with_permission_mode( result.compacted_session, self.model.clone(), @@ -753,7 +768,7 @@ impl LiveCli { true, permission_mode_label(), )?; - println!("Compacted {removed} messages."); + println!("{}", format_compact_report(removed, kept, skipped)); Ok(()) } } @@ -1384,12 +1399,12 @@ fn print_help() { #[cfg(test)] mod tests { use super::{ - format_cost_report, format_init_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, parse_git_status_metadata, - render_config_report, render_init_claude_md, render_memory_report, render_repl_help, - resume_supported_slash_commands, status_context, CliAction, SlashCommand, StatusUsage, - DEFAULT_MODEL, + format_compact_report, format_cost_report, format_init_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, + parse_git_status_metadata, render_config_report, render_init_claude_md, + render_memory_report, render_repl_help, resume_supported_slash_commands, status_context, + CliAction, SlashCommand, StatusUsage, DEFAULT_MODEL, }; use runtime::{ContentBlock, ConversationMessage, MessageRole}; use std::path::{Path, PathBuf}; @@ -1521,6 +1536,16 @@ mod tests { assert!(report.contains("Turns 6")); } + #[test] + fn compact_report_uses_structured_output() { + let compacted = format_compact_report(8, 5, false); + assert!(compacted.contains("Compact")); + assert!(compacted.contains("Result compacted")); + assert!(compacted.contains("Messages removed 8")); + let skipped = format_compact_report(0, 3, true); + assert!(skipped.contains("Result skipped")); + } + #[test] fn cost_report_uses_sectioned_layout() { let report = format_cost_report(runtime::TokenUsage {