mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-03 01:01:52 +08:00
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
This commit is contained in:
@@ -89,8 +89,8 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "config",
|
name: "config",
|
||||||
summary: "Inspect discovered Claude config files",
|
summary: "Inspect Claude config files or merged sections",
|
||||||
argument_hint: None,
|
argument_hint: Some("[env|hooks|model]"),
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
@@ -117,7 +117,7 @@ pub enum SlashCommand {
|
|||||||
Clear { confirm: bool },
|
Clear { confirm: bool },
|
||||||
Cost,
|
Cost,
|
||||||
Resume { session_path: Option<String> },
|
Resume { session_path: Option<String> },
|
||||||
Config,
|
Config { section: Option<String> },
|
||||||
Memory,
|
Memory,
|
||||||
Init,
|
Init,
|
||||||
Unknown(String),
|
Unknown(String),
|
||||||
@@ -150,7 +150,9 @@ impl SlashCommand {
|
|||||||
"resume" => Self::Resume {
|
"resume" => Self::Resume {
|
||||||
session_path: parts.next().map(ToOwned::to_owned),
|
session_path: parts.next().map(ToOwned::to_owned),
|
||||||
},
|
},
|
||||||
"config" => Self::Config,
|
"config" => Self::Config {
|
||||||
|
section: parts.next().map(ToOwned::to_owned),
|
||||||
|
},
|
||||||
"memory" => Self::Memory,
|
"memory" => Self::Memory,
|
||||||
"init" => Self::Init,
|
"init" => Self::Init,
|
||||||
other => Self::Unknown(other.to_string()),
|
other => Self::Unknown(other.to_string()),
|
||||||
@@ -230,7 +232,7 @@ pub fn handle_slash_command(
|
|||||||
| SlashCommand::Clear { .. }
|
| SlashCommand::Clear { .. }
|
||||||
| SlashCommand::Cost
|
| SlashCommand::Cost
|
||||||
| SlashCommand::Resume { .. }
|
| SlashCommand::Resume { .. }
|
||||||
| SlashCommand::Config
|
| SlashCommand::Config { .. }
|
||||||
| SlashCommand::Memory
|
| SlashCommand::Memory
|
||||||
| SlashCommand::Init
|
| SlashCommand::Init
|
||||||
| SlashCommand::Unknown(_) => None,
|
| SlashCommand::Unknown(_) => None,
|
||||||
@@ -280,7 +282,16 @@ mod tests {
|
|||||||
session_path: Some("session.json".to_string()),
|
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("/memory"), Some(SlashCommand::Memory));
|
||||||
assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
|
assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
|
||||||
}
|
}
|
||||||
@@ -297,7 +308,7 @@ mod tests {
|
|||||||
assert!(help.contains("/clear [--confirm]"));
|
assert!(help.contains("/clear [--confirm]"));
|
||||||
assert!(help.contains("/cost"));
|
assert!(help.contains("/cost"));
|
||||||
assert!(help.contains("/resume <session-path>"));
|
assert!(help.contains("/resume <session-path>"));
|
||||||
assert!(help.contains("/config"));
|
assert!(help.contains("/config [env|hooks|model]"));
|
||||||
assert!(help.contains("/memory"));
|
assert!(help.contains("/memory"));
|
||||||
assert!(help.contains("/init"));
|
assert!(help.contains("/init"));
|
||||||
assert_eq!(slash_command_specs().len(), 11);
|
assert_eq!(slash_command_specs().len(), 11);
|
||||||
@@ -370,5 +381,8 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.is_none());
|
.is_none());
|
||||||
assert!(handle_slash_command("/config", &session, CompactionConfig::default()).is_none());
|
assert!(handle_slash_command("/config", &session, CompactionConfig::default()).is_none());
|
||||||
|
assert!(
|
||||||
|
handle_slash_command("/config env", &session, CompactionConfig::default()).is_none()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -446,9 +446,9 @@ fn run_resume_command(
|
|||||||
message: Some(format_cost_report(usage)),
|
message: Some(format_cost_report(usage)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Config => Ok(ResumeCommandOutcome {
|
SlashCommand::Config { section } => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_config_report()?),
|
message: Some(render_config_report(section.as_deref())?),
|
||||||
}),
|
}),
|
||||||
SlashCommand::Memory => Ok(ResumeCommandOutcome {
|
SlashCommand::Memory => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
@@ -554,7 +554,7 @@ impl LiveCli {
|
|||||||
SlashCommand::Clear { confirm } => self.clear_session(confirm)?,
|
SlashCommand::Clear { confirm } => self.clear_session(confirm)?,
|
||||||
SlashCommand::Cost => self.print_cost(),
|
SlashCommand::Cost => self.print_cost(),
|
||||||
SlashCommand::Resume { session_path } => self.resume_session(session_path)?,
|
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::Memory => Self::print_memory()?,
|
||||||
SlashCommand::Init => Self::run_init()?,
|
SlashCommand::Init => Self::run_init()?,
|
||||||
SlashCommand::Unknown(name) => eprintln!("unknown slash command: /{name}"),
|
SlashCommand::Unknown(name) => eprintln!("unknown slash command: /{name}"),
|
||||||
@@ -708,8 +708,8 @@ impl LiveCli {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_config() -> Result<(), Box<dyn std::error::Error>> {
|
fn print_config(section: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("{}", render_config_report()?);
|
println!("{}", render_config_report(section)?);
|
||||||
Ok(())
|
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 cwd = env::current_dir()?;
|
||||||
let loader = ConfigLoader::default_for(&cwd);
|
let loader = ConfigLoader::default_for(&cwd);
|
||||||
let discovered = loader.discover();
|
let discovered = loader.discover();
|
||||||
@@ -868,6 +868,36 @@ fn render_config_report() -> Result<String, Box<dyn std::error::Error>> {
|
|||||||
entry.path.display()
|
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("Merged JSON".to_string());
|
||||||
lines.push(format!(" {}", runtime_config.as_json().render()));
|
lines.push(format!(" {}", runtime_config.as_json().render()));
|
||||||
Ok(lines.join(
|
Ok(lines.join(
|
||||||
@@ -1340,7 +1370,7 @@ mod tests {
|
|||||||
format_cost_report, format_model_report, format_model_switch_report,
|
format_cost_report, format_model_report, format_model_switch_report,
|
||||||
format_permissions_report, format_permissions_switch_report, format_resume_report,
|
format_permissions_report, format_permissions_switch_report, format_resume_report,
|
||||||
format_status_report, normalize_permission_mode, parse_args, parse_git_status_metadata,
|
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,
|
resume_supported_slash_commands, status_context, CliAction, SlashCommand, StatusUsage,
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
};
|
};
|
||||||
@@ -1447,7 +1477,7 @@ mod tests {
|
|||||||
assert!(help.contains("/clear [--confirm]"));
|
assert!(help.contains("/clear [--confirm]"));
|
||||||
assert!(help.contains("/cost"));
|
assert!(help.contains("/cost"));
|
||||||
assert!(help.contains("/resume <session-path>"));
|
assert!(help.contains("/resume <session-path>"));
|
||||||
assert!(help.contains("/config"));
|
assert!(help.contains("/config [env|hooks|model]"));
|
||||||
assert!(help.contains("/memory"));
|
assert!(help.contains("/memory"));
|
||||||
assert!(help.contains("/init"));
|
assert!(help.contains("/init"));
|
||||||
assert!(help.contains("/exit"));
|
assert!(help.contains("/exit"));
|
||||||
@@ -1571,6 +1601,12 @@ mod tests {
|
|||||||
assert!(status.contains("Memory files 4"));
|
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]
|
#[test]
|
||||||
fn memory_report_uses_sectioned_layout() {
|
fn memory_report_uses_sectioned_layout() {
|
||||||
let report = render_memory_report().expect("memory report should render");
|
let report = render_memory_report().expect("memory report should render");
|
||||||
@@ -1582,7 +1618,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_report_uses_sectioned_layout() {
|
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("Config"));
|
||||||
assert!(report.contains("Discovered files"));
|
assert!(report.contains("Discovered files"));
|
||||||
assert!(report.contains("Merged JSON"));
|
assert!(report.contains("Merged JSON"));
|
||||||
@@ -1644,7 +1680,16 @@ mod tests {
|
|||||||
SlashCommand::parse("/clear --confirm"),
|
SlashCommand::parse("/clear --confirm"),
|
||||||
Some(SlashCommand::Clear { confirm: true })
|
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("/memory"), Some(SlashCommand::Memory));
|
||||||
assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
|
assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user