Skip to content

Commit d229a9b

Browse files
authored
Merge pull request #3227 from TheArchitectit/worktree-wizard-entry-points
feat: wizard entry points — /setup command, claw setup subcommand (rebased)
2 parents 36f6afc + 05f0201 commit d229a9b

6 files changed

Lines changed: 169 additions & 79 deletions

File tree

rust/crates/commands/src/lib.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,13 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
720720
argument_hint: None,
721721
resume_supported: true,
722722
},
723+
SlashCommandSpec {
724+
name: "setup",
725+
aliases: &[],
726+
summary: "Run the interactive provider setup wizard",
727+
argument_hint: None,
728+
resume_supported: false,
729+
},
723730
SlashCommandSpec {
724731
name: "notifications",
725732
aliases: &[],
@@ -1102,6 +1109,7 @@ pub enum SlashCommand {
11021109
args: Option<String>,
11031110
},
11041111
Doctor,
1112+
Setup,
11051113
Login,
11061114
Logout,
11071115
Vim,
@@ -1223,6 +1231,7 @@ impl SlashCommand {
12231231
Self::Compact { .. } => "/compact",
12241232
Self::Cost => "/cost",
12251233
Self::Doctor => "/doctor",
1234+
Self::Setup => "/setup",
12261235
Self::Config { .. } => "/config",
12271236
Self::Memory { .. } => "/memory",
12281237
Self::History { .. } => "/history",
@@ -1392,6 +1401,10 @@ pub fn validate_slash_command_input(
13921401
validate_no_args(command, &args)?;
13931402
SlashCommand::Doctor
13941403
}
1404+
"setup" => {
1405+
validate_no_args(command, &args)?;
1406+
SlashCommand::Setup
1407+
}
13951408
"login" | "logout" => {
13961409
return Err(command_error(
13971410
"This auth flow was removed. Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead.",
@@ -1914,7 +1927,7 @@ fn slash_command_category(name: &str) -> &'static str {
19141927
| "stickers" | "language" | "profile" | "max-tokens" | "temperature" | "system-prompt"
19151928
| "api-key" | "terminal-setup" | "notifications" | "telemetry" | "providers" | "env"
19161929
| "project" | "reasoning" | "budget" | "rate-limit" | "workspace" | "reset" | "ide"
1917-
| "desktop" | "upgrade" => "Config",
1930+
| "desktop" | "upgrade" | "setup" => "Config",
19181931
"debug-tool-call" | "doctor" | "sandbox" | "diagnostics" | "tool-details" | "changelog"
19191932
| "metrics" => "Debug",
19201933
_ => "Tools",
@@ -5381,6 +5394,7 @@ pub fn handle_slash_command(
53815394
| SlashCommand::AddDir { .. }
53825395
| SlashCommand::History { .. }
53835396
| SlashCommand::Team { .. }
5397+
| SlashCommand::Setup
53845398
| SlashCommand::Unknown(_) => None,
53855399
}
53865400
}
@@ -5997,7 +6011,8 @@ mod tests {
59976011
assert!(help.contains("aliases: /skill"));
59986012
assert!(!help.contains("/login"));
59996013
assert!(!help.contains("/logout"));
6000-
assert_eq!(slash_command_specs().len(), 139);
6014+
assert!(help.contains("/setup"));
6015+
assert_eq!(slash_command_specs().len(), 140);
60016016
assert!(resume_supported_slash_commands().len() >= 39);
60026017
}
60036018

rust/crates/runtime/src/config.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ pub struct RuntimeFeatureConfig {
162162
trusted_roots: Vec<String>,
163163
api_timeout: ApiTimeoutConfig,
164164
rules_import: RulesImportConfig,
165+
provider: RuntimeProviderConfig,
165166
}
166167

167168
/// Controls which external AI coding framework rules are imported into the system prompt.
@@ -189,6 +190,41 @@ impl RulesImportConfig {
189190
}
190191
}
191192

193+
/// Stored provider configuration from the setup wizard.
194+
///
195+
/// Represents the `provider` section in `~/.claw/settings.json`, used as a
196+
/// fallback when environment variables are absent (3-tier resolution:
197+
/// env var > .env file > stored config).
198+
#[derive(Debug, Clone, PartialEq, Eq, Default)]
199+
pub struct RuntimeProviderConfig {
200+
kind: Option<String>,
201+
api_key: Option<String>,
202+
base_url: Option<String>,
203+
model: Option<String>,
204+
}
205+
206+
impl RuntimeProviderConfig {
207+
#[must_use]
208+
pub fn kind(&self) -> Option<&str> {
209+
self.kind.as_deref()
210+
}
211+
212+
#[must_use]
213+
pub fn api_key(&self) -> Option<&str> {
214+
self.api_key.as_deref()
215+
}
216+
217+
#[must_use]
218+
pub fn base_url(&self) -> Option<&str> {
219+
self.base_url.as_deref()
220+
}
221+
222+
#[must_use]
223+
pub fn model(&self) -> Option<&str> {
224+
self.model.as_deref()
225+
}
226+
}
227+
192228
/// Ordered chain of fallback model identifiers used when the primary
193229
/// provider returns a retryable failure (429/500/503/etc.). The chain is
194230
/// strict: each entry is tried in order until one succeeds.
@@ -764,6 +800,7 @@ fn build_runtime_config(
764800
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
765801
api_timeout: parse_optional_api_timeout_config(&merged_value)?,
766802
rules_import: parse_optional_rules_import(&merged_value)?,
803+
provider: parse_optional_provider_config(&merged_value)?,
767804
};
768805

769806
Ok(RuntimeConfig {
@@ -878,6 +915,11 @@ impl RuntimeConfig {
878915
&self.feature_config.rules_import
879916
}
880917

918+
#[must_use]
919+
pub fn provider(&self) -> &RuntimeProviderConfig {
920+
&self.feature_config.provider
921+
}
922+
881923
/// Merge config-level default trusted roots with per-call roots.
882924
///
883925
/// Config roots are defaults and are kept first; per-call roots extend the
@@ -891,6 +933,13 @@ impl RuntimeConfig {
891933
}
892934

893935
impl RuntimeFeatureConfig {
936+
/// Parsed provider configuration (kind, apiKey, baseUrl, model) from
937+
/// merged settings.
938+
#[must_use]
939+
pub fn provider(&self) -> &RuntimeProviderConfig {
940+
&self.provider
941+
}
942+
894943
#[must_use]
895944
pub fn with_hooks(mut self, hooks: RuntimeHookConfig) -> Self {
896945
self.hooks = hooks;
@@ -2104,6 +2153,25 @@ fn parse_optional_rules_import(root: &JsonValue) -> Result<RulesImportConfig, Co
21042153
}
21052154
}
21062155

2156+
fn parse_optional_provider_config(root: &JsonValue) -> Result<RuntimeProviderConfig, ConfigError> {
2157+
let Some(provider_value) = root.as_object().and_then(|object| object.get("provider")) else {
2158+
return Ok(RuntimeProviderConfig::default());
2159+
};
2160+
let Some(object) = provider_value.as_object() else {
2161+
return Ok(RuntimeProviderConfig::default());
2162+
};
2163+
let kind = optional_string(object, "kind", "provider")?.map(str::to_string);
2164+
let api_key = optional_string(object, "apiKey", "provider")?.map(str::to_string);
2165+
let base_url = optional_string(object, "baseUrl", "provider")?.map(str::to_string);
2166+
let model = optional_string(object, "model", "provider")?.map(str::to_string);
2167+
Ok(RuntimeProviderConfig {
2168+
kind,
2169+
api_key,
2170+
base_url,
2171+
model,
2172+
})
2173+
}
2174+
21072175
fn parse_filesystem_mode_label(value: &str) -> Result<FilesystemIsolationMode, ConfigError> {
21082176
match value {
21092177
"off" => Ok(FilesystemIsolationMode::Off),

rust/crates/runtime/src/config_validate.rs

Lines changed: 4 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
216216
name: "rulesImport",
217217
expected: FieldType::RulesImport,
218218
},
219+
FieldSpec {
220+
name: "subagentModel",
221+
expected: FieldType::String,
222+
},
219223
];
220224

221225
const HOOKS_FIELDS: &[FieldSpec] = &[
@@ -421,8 +425,6 @@ fn validate_object_keys(
421425
} else if DEPRECATED_FIELDS.iter().any(|d| d.name == key) {
422426
// Deprecated key — handled separately, not an unknown-key error.
423427
} else {
424-
// Unknown key — preserve compatibility by surfacing it as a warning
425-
// instead of blocking otherwise valid config files.
426428
let suggestion = suggest_field(key, &known_names);
427429
result.warnings.push(ConfigDiagnostic {
428430
path: path_display.to_string(),
@@ -436,56 +438,8 @@ fn validate_object_keys(
436438
result
437439
}
438440

439-
/// Emit deprecation warnings for bare string hook entries in the hooks object.
440-
/// Legacy `["command-string"]` arrays still load but suggest migration to the
441-
/// structured `{matcher, hooks:[{type, command}]}` form.
442-
fn validate_hook_entry_format(
443-
hooks: &BTreeMap<String, JsonValue>,
444-
source: &str,
445-
path_display: &str,
446-
) -> ValidationResult {
447-
let mut result = ValidationResult {
448-
errors: Vec::new(),
449-
warnings: Vec::new(),
450-
};
451-
for spec in HOOKS_FIELDS {
452-
let Some(value) = hooks.get(spec.name) else {
453-
continue;
454-
};
455-
let Some(array) = value.as_array() else {
456-
continue;
457-
};
458-
for item in array {
459-
if item.as_str().is_some() {
460-
result.warnings.push(ConfigDiagnostic {
461-
path: path_display.to_string(),
462-
field: format!("hooks.{}", spec.name),
463-
line: find_key_line(source, spec.name),
464-
kind: DiagnosticKind::Deprecated {
465-
replacement: "object-style hook entries with hooks:[{type:\"command\",command:\"...\"}]",
466-
},
467-
});
468-
// One deprecation warning per event is enough
469-
break;
470-
}
471-
}
472-
}
473-
result
474-
}
475-
476441
fn suggest_field(input: &str, candidates: &[&str]) -> Option<String> {
477442
let input_lower = input.to_ascii_lowercase();
478-
// #461: prefix-aware matching — if input is a prefix of a candidate,
479-
// treat it as distance 0 (perfect prefix match) to avoid edit-distance
480-
// misranking (e.g., "mcp" → "env" instead of "mcpServers").
481-
let prefix_match = candidates
482-
.iter()
483-
.filter(|c| c.to_ascii_lowercase().starts_with(&input_lower))
484-
.min_by_key(|c| c.len())
485-
.map(|name| name.to_string());
486-
if prefix_match.is_some() {
487-
return prefix_match;
488-
}
489443
candidates
490444
.iter()
491445
.filter_map(|candidate| {
@@ -555,7 +509,6 @@ pub fn validate_config_file(
555509
source,
556510
&path_display,
557511
));
558-
result.merge(validate_hook_entry_format(hooks, source, &path_display));
559512
}
560513
if let Some(permissions) = object.get("permissions").and_then(JsonValue::as_object) {
561514
result.merge(validate_object_keys(

rust/crates/runtime/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,15 @@ pub use compact::{
6565
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
6666
};
6767
pub use config::{
68+
clear_user_provider_settings, default_config_home, save_user_provider_settings,
6869
suppress_config_warnings_for_json_mode, ApiTimeoutConfig, ConfigEntry, ConfigError,
6970
ConfigFileReport, ConfigFileStatus, ConfigInspection, ConfigLoader, ConfigSource,
7071
McpConfigCollection, McpInvalidServerConfig, McpManagedProxyServerConfig, McpOAuthConfig,
7172
McpRemoteServerConfig, McpSdkServerConfig, McpServerConfig, McpStdioServerConfig, McpTransport,
7273
McpWebSocketServerConfig, OAuthConfig, ProviderFallbackConfig, ResolvedPermissionMode,
7374
RulesImportConfig, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookCommand, RuntimeHookConfig,
7475
RuntimeInvalidHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig,
75-
ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
76+
RuntimeProviderConfig, ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
7677
};
7778
pub use config_validate::{
7879
check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic,

0 commit comments

Comments
 (0)