mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-11 18:44:52 +08:00
feat: b5-tool-timeout — batch 5 upstream parity
This commit is contained in:
@@ -58,6 +58,7 @@ pub struct RuntimeFeatureConfig {
|
|||||||
mcp: McpConfigCollection,
|
mcp: McpConfigCollection,
|
||||||
oauth: Option<OAuthConfig>,
|
oauth: Option<OAuthConfig>,
|
||||||
model: Option<String>,
|
model: Option<String>,
|
||||||
|
aliases: BTreeMap<String, String>,
|
||||||
permission_mode: Option<ResolvedPermissionMode>,
|
permission_mode: Option<ResolvedPermissionMode>,
|
||||||
permission_rules: RuntimePermissionRuleConfig,
|
permission_rules: RuntimePermissionRuleConfig,
|
||||||
sandbox: SandboxConfig,
|
sandbox: SandboxConfig,
|
||||||
@@ -290,6 +291,7 @@ impl ConfigLoader {
|
|||||||
},
|
},
|
||||||
oauth: parse_optional_oauth_config(&merged_value, "merged settings.oauth")?,
|
oauth: parse_optional_oauth_config(&merged_value, "merged settings.oauth")?,
|
||||||
model: parse_optional_model(&merged_value),
|
model: parse_optional_model(&merged_value),
|
||||||
|
aliases: parse_optional_aliases(&merged_value)?,
|
||||||
permission_mode: parse_optional_permission_mode(&merged_value)?,
|
permission_mode: parse_optional_permission_mode(&merged_value)?,
|
||||||
permission_rules: parse_optional_permission_rules(&merged_value)?,
|
permission_rules: parse_optional_permission_rules(&merged_value)?,
|
||||||
sandbox: parse_optional_sandbox_config(&merged_value)?,
|
sandbox: parse_optional_sandbox_config(&merged_value)?,
|
||||||
@@ -364,6 +366,11 @@ impl RuntimeConfig {
|
|||||||
self.feature_config.model.as_deref()
|
self.feature_config.model.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn aliases(&self) -> &BTreeMap<String, String> {
|
||||||
|
&self.feature_config.aliases
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn permission_mode(&self) -> Option<ResolvedPermissionMode> {
|
pub fn permission_mode(&self) -> Option<ResolvedPermissionMode> {
|
||||||
self.feature_config.permission_mode
|
self.feature_config.permission_mode
|
||||||
@@ -423,6 +430,11 @@ impl RuntimeFeatureConfig {
|
|||||||
self.model.as_deref()
|
self.model.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn aliases(&self) -> &BTreeMap<String, String> {
|
||||||
|
&self.aliases
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn permission_mode(&self) -> Option<ResolvedPermissionMode> {
|
pub fn permission_mode(&self) -> Option<ResolvedPermissionMode> {
|
||||||
self.permission_mode
|
self.permission_mode
|
||||||
@@ -680,6 +692,13 @@ fn parse_optional_model(root: &JsonValue) -> Option<String> {
|
|||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_optional_aliases(root: &JsonValue) -> Result<BTreeMap<String, String>, ConfigError> {
|
||||||
|
let Some(object) = root.as_object() else {
|
||||||
|
return Ok(BTreeMap::new());
|
||||||
|
};
|
||||||
|
Ok(optional_string_map(object, "aliases", "merged settings")?.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_optional_hooks_config(root: &JsonValue) -> Result<RuntimeHookConfig, ConfigError> {
|
fn parse_optional_hooks_config(root: &JsonValue) -> Result<RuntimeHookConfig, ConfigError> {
|
||||||
let Some(object) = root.as_object() else {
|
let Some(object) = root.as_object() else {
|
||||||
return Ok(RuntimeHookConfig::default());
|
return Ok(RuntimeHookConfig::default());
|
||||||
@@ -1613,6 +1632,49 @@ mod tests {
|
|||||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_user_defined_model_aliases_from_settings() {
|
||||||
|
// given
|
||||||
|
let root = temp_dir();
|
||||||
|
let cwd = root.join("project");
|
||||||
|
let home = root.join("home").join(".claw");
|
||||||
|
fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
|
||||||
|
fs::create_dir_all(&home).expect("home config dir");
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
home.join("settings.json"),
|
||||||
|
r#"{"aliases":{"fast":"claude-haiku-4-5-20251213","smart":"claude-opus-4-6"}}"#,
|
||||||
|
)
|
||||||
|
.expect("write user settings");
|
||||||
|
fs::write(
|
||||||
|
cwd.join(".claw").join("settings.local.json"),
|
||||||
|
r#"{"aliases":{"smart":"claude-sonnet-4-6","cheap":"grok-3-mini"}}"#,
|
||||||
|
)
|
||||||
|
.expect("write local settings");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let loaded = ConfigLoader::new(&cwd, &home)
|
||||||
|
.load()
|
||||||
|
.expect("config should load");
|
||||||
|
|
||||||
|
// then
|
||||||
|
let aliases = loaded.aliases();
|
||||||
|
assert_eq!(
|
||||||
|
aliases.get("fast").map(String::as_str),
|
||||||
|
Some("claude-haiku-4-5-20251213")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
aliases.get("smart").map(String::as_str),
|
||||||
|
Some("claude-sonnet-4-6")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
aliases.get("cheap").map(String::as_str),
|
||||||
|
Some("grok-3-mini")
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_settings_file_loads_defaults() {
|
fn empty_settings_file_loads_defaults() {
|
||||||
// given
|
// given
|
||||||
|
|||||||
@@ -356,11 +356,11 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
let value = args
|
let value = args
|
||||||
.get(index + 1)
|
.get(index + 1)
|
||||||
.ok_or_else(|| "missing value for --model".to_string())?;
|
.ok_or_else(|| "missing value for --model".to_string())?;
|
||||||
model = resolve_model_alias(value).to_string();
|
model = resolve_model_alias_with_config(value);
|
||||||
index += 2;
|
index += 2;
|
||||||
}
|
}
|
||||||
flag if flag.starts_with("--model=") => {
|
flag if flag.starts_with("--model=") => {
|
||||||
model = resolve_model_alias(&flag[8..]).to_string();
|
model = resolve_model_alias_with_config(&flag[8..]);
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
"--output-format" => {
|
"--output-format" => {
|
||||||
@@ -401,7 +401,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
}
|
}
|
||||||
return Ok(CliAction::Prompt {
|
return Ok(CliAction::Prompt {
|
||||||
prompt,
|
prompt,
|
||||||
model: resolve_model_alias(&model).to_string(),
|
model: resolve_model_alias_with_config(&model),
|
||||||
output_format,
|
output_format,
|
||||||
allowed_tools: normalize_allowed_tools(&allowed_tool_values)?,
|
allowed_tools: normalize_allowed_tools(&allowed_tool_values)?,
|
||||||
permission_mode: permission_mode_override
|
permission_mode: permission_mode_override
|
||||||
@@ -813,6 +813,27 @@ fn resolve_model_alias(model: &str) -> &str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a model name through user-defined config aliases first, then fall
|
||||||
|
/// back to the built-in alias table. This is the entry point used wherever a
|
||||||
|
/// user-supplied model string is about to be dispatched to a provider.
|
||||||
|
fn resolve_model_alias_with_config(model: &str) -> String {
|
||||||
|
let trimmed = model.trim();
|
||||||
|
if let Some(resolved) = config_alias_for_current_dir(trimmed) {
|
||||||
|
return resolve_model_alias(&resolved).to_string();
|
||||||
|
}
|
||||||
|
resolve_model_alias(trimmed).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_alias_for_current_dir(alias: &str) -> Option<String> {
|
||||||
|
if alias.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let cwd = env::current_dir().ok()?;
|
||||||
|
let loader = ConfigLoader::default_for(&cwd);
|
||||||
|
let config = loader.load().ok()?;
|
||||||
|
config.aliases().get(alias).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_allowed_tools(values: &[String]) -> Result<Option<AllowedToolSet>, String> {
|
fn normalize_allowed_tools(values: &[String]) -> Result<Option<AllowedToolSet>, String> {
|
||||||
if values.is_empty() {
|
if values.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -903,10 +924,10 @@ fn resolve_repl_model(cli_model: String) -> String {
|
|||||||
.map(|value| value.trim().to_string())
|
.map(|value| value.trim().to_string())
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
{
|
{
|
||||||
return resolve_model_alias(&env_model).to_string();
|
return resolve_model_alias_with_config(&env_model);
|
||||||
}
|
}
|
||||||
if let Some(config_model) = config_model_for_current_dir() {
|
if let Some(config_model) = config_model_for_current_dir() {
|
||||||
return resolve_model_alias(&config_model).to_string();
|
return resolve_model_alias_with_config(&config_model);
|
||||||
}
|
}
|
||||||
cli_model
|
cli_model
|
||||||
}
|
}
|
||||||
@@ -3667,7 +3688,7 @@ impl LiveCli {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
let model = resolve_model_alias(&model).to_string();
|
let model = resolve_model_alias_with_config(&model);
|
||||||
|
|
||||||
if model == self.model {
|
if model == self.model {
|
||||||
println!(
|
println!(
|
||||||
@@ -7463,12 +7484,11 @@ mod tests {
|
|||||||
parse_git_status_metadata_for, parse_git_workspace_summary, permission_policy,
|
parse_git_status_metadata_for, parse_git_workspace_summary, permission_policy,
|
||||||
print_help_to, push_output_block, render_config_report, render_diff_report,
|
print_help_to, push_output_block, render_config_report, render_diff_report,
|
||||||
render_diff_report_for, render_memory_report, render_repl_help, render_resume_usage,
|
render_diff_report_for, render_memory_report, render_repl_help, render_resume_usage,
|
||||||
render_session_markdown, resolve_model_alias, resolve_repl_model,
|
resolve_model_alias, resolve_model_alias_with_config, resolve_repl_model,
|
||||||
resolve_session_reference, response_to_events, resume_supported_slash_commands,
|
resolve_session_reference, response_to_events,
|
||||||
run_resume_command, short_tool_id,
|
resume_supported_slash_commands, run_resume_command,
|
||||||
slash_command_completion_candidates_with_sessions, status_context,
|
slash_command_completion_candidates_with_sessions, status_context, validate_no_args,
|
||||||
summarize_tool_payload_for_markdown, validate_no_args, write_mcp_server_fixture,
|
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
||||||
CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
|
||||||
InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic,
|
InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic,
|
||||||
SlashCommand, StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE,
|
SlashCommand, StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE,
|
||||||
};
|
};
|
||||||
@@ -8123,6 +8143,45 @@ mod tests {
|
|||||||
assert_eq!(resolve_model_alias("claude-opus"), "claude-opus");
|
assert_eq!(resolve_model_alias("claude-opus"), "claude-opus");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_defined_aliases_resolve_before_provider_dispatch() {
|
||||||
|
// given
|
||||||
|
let _guard = env_lock();
|
||||||
|
let root = temp_dir();
|
||||||
|
let cwd = root.join("project");
|
||||||
|
let config_home = root.join("config-home");
|
||||||
|
std::fs::create_dir_all(cwd.join(".claw")).expect("project config dir should exist");
|
||||||
|
std::fs::create_dir_all(&config_home).expect("config home should exist");
|
||||||
|
std::fs::write(
|
||||||
|
cwd.join(".claw").join("settings.json"),
|
||||||
|
r#"{"aliases":{"fast":"claude-haiku-4-5-20251213","smart":"opus","cheap":"grok-3-mini"}}"#,
|
||||||
|
)
|
||||||
|
.expect("project config should write");
|
||||||
|
|
||||||
|
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||||
|
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
||||||
|
|
||||||
|
// when
|
||||||
|
let direct = with_current_dir(&cwd, || resolve_model_alias_with_config("fast"));
|
||||||
|
let chained = with_current_dir(&cwd, || resolve_model_alias_with_config("smart"));
|
||||||
|
let cross_provider = with_current_dir(&cwd, || resolve_model_alias_with_config("cheap"));
|
||||||
|
let unknown = with_current_dir(&cwd, || resolve_model_alias_with_config("unknown-model"));
|
||||||
|
let builtin = with_current_dir(&cwd, || resolve_model_alias_with_config("haiku"));
|
||||||
|
|
||||||
|
match original_config_home {
|
||||||
|
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||||
|
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||||
|
}
|
||||||
|
std::fs::remove_dir_all(root).expect("temp config root should clean up");
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(direct, "claude-haiku-4-5-20251213");
|
||||||
|
assert_eq!(chained, "claude-opus-4-6");
|
||||||
|
assert_eq!(cross_provider, "grok-3-mini");
|
||||||
|
assert_eq!(unknown, "unknown-model");
|
||||||
|
assert_eq!(builtin, "claude-haiku-4-5-20251213");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_version_flags_without_initializing_prompt_mode() {
|
fn parses_version_flags_without_initializing_prompt_mode() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
Reference in New Issue
Block a user