Browse Source

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
Yeachan-Heo 2 tháng trước cách đây
mục cha
commit
9f3be03463
2 tập tin đã thay đổi với 76 bổ sung17 xóa
  1. 21 7
      rust/crates/commands/src/lib.rs
  2. 55 10
      rust/crates/rusty-claude-cli/src/main.rs

+ 21 - 7
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<String> },
-    Config,
+    Config { section: Option<String> },
     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 <session-path>"));
-        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()
+        );
     }
 }

+ 55 - 10
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<dyn std::error::Error>> {
-        println!("{}", render_config_report()?);
+    fn print_config(section: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
+        println!("{}", render_config_report(section)?);
         Ok(())
     }
 
@@ -830,7 +830,7 @@ fn format_status_report(
     )
 }
 
-fn render_config_report() -> Result<String, Box<dyn std::error::Error>> {
+fn render_config_report(section: Option<&str>) -> Result<String, Box<dyn std::error::Error>> {
     let cwd = env::current_dir()?;
     let loader = ConfigLoader::default_for(&cwd);
     let discovered = loader.discover();
@@ -868,6 +868,36 @@ fn render_config_report() -> Result<String, Box<dyn std::error::Error>> {
             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 => "<unset>".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 <session-path>"));
-        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));
     }