diff --git a/rust/crates/api/src/providers/mod.rs b/rust/crates/api/src/providers/mod.rs index dc6a2d73..1a264547 100644 --- a/rust/crates/api/src/providers/mod.rs +++ b/rust/crates/api/src/providers/mod.rs @@ -408,14 +408,14 @@ pub fn provider_config_value(key: &str) -> Option { let provider = config.provider(); let kind = provider.kind()?; match (key, kind) { - ("ANTHROPIC_API_KEY" | "ANTHROPIC_AUTH_TOKEN", "anthropic") => provider.api_key().map(ToOwned::to_owned), - ("ANTHROPIC_BASE_URL", "anthropic") => provider.base_url().map(ToOwned::to_owned), - ("XAI_API_KEY", "xai") => provider.api_key().map(ToOwned::to_owned), - ("XAI_BASE_URL", "xai") => provider.base_url().map(ToOwned::to_owned), - ("OPENAI_API_KEY", "openai") => provider.api_key().map(ToOwned::to_owned), - ("OPENAI_BASE_URL", "openai") => provider.base_url().map(ToOwned::to_owned), - ("DASHSCOPE_API_KEY", "dashscope") => provider.api_key().map(ToOwned::to_owned), - ("DASHSCOPE_BASE_URL", "dashscope") => provider.base_url().map(ToOwned::to_owned), + ("ANTHROPIC_API_KEY" | "ANTHROPIC_AUTH_TOKEN", "anthropic") + | ("XAI_API_KEY", "xai") + | ("OPENAI_API_KEY", "openai") + | ("DASHSCOPE_API_KEY", "dashscope") => provider.api_key().map(ToOwned::to_owned), + ("ANTHROPIC_BASE_URL", "anthropic") + | ("XAI_BASE_URL", "xai") + | ("OPENAI_BASE_URL", "openai") + | ("DASHSCOPE_BASE_URL", "dashscope") => provider.base_url().map(ToOwned::to_owned), _ => None, } } @@ -437,7 +437,7 @@ pub fn read_env_or_config(key: &str) -> Result, ApiError> { Ok(None) } -/// Return the stored ProviderKind from config, if set. +/// Return the stored `ProviderKind` from config, if set. fn stored_provider_kind() -> Option { let cwd = std::env::current_dir().ok()?; let config = runtime::ConfigLoader::default_for(&cwd).load().ok()?; diff --git a/rust/crates/rusty-claude-cli/src/input.rs b/rust/crates/rusty-claude-cli/src/input.rs index 54cb9419..011065a7 100644 --- a/rust/crates/rusty-claude-cli/src/input.rs +++ b/rust/crates/rusty-claude-cli/src/input.rs @@ -18,12 +18,12 @@ pub enum ReadOutcome { Submit(String), Cancel, Exit, + ProviderSwap, } struct SlashCommandHelper { completions: Vec, current_line: RefCell, - ctrl_p_pending: RefCell, } impl SlashCommandHelper { @@ -31,7 +31,6 @@ impl SlashCommandHelper { Self { completions: normalize_completions(completions), current_line: RefCell::new(String::new()), - ctrl_p_pending: RefCell::new(false), } } @@ -88,19 +87,19 @@ impl Hinter for SlashCommandHelper { impl Highlighter for SlashCommandHelper { fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { self.set_current_line(line); - Cow::Borrowed(line) + // When sentinel is present, show visible prompt instead of invisible char + if line.contains('\x01') { + let display = line.replace('\x01', "\x1b[36m[Provider Swap]\x1b[0m "); + Cow::Owned(display) + } else { + Cow::Borrowed(line) + } } fn highlight_char(&self, line: &str, _pos: usize, _kind: CmdKind) -> bool { - // Detect Ctrl+P: when previous line was empty and the new line is - // just "P", that's Ctrl+P on an empty buffer (AcceptLine inserts - // the character then submits). Set flag so read_line can intercept. - let prev = self.current_line(); - if prev.is_empty() && line == "P" { - *self.ctrl_p_pending.borrow_mut() = true; - } self.set_current_line(line); - false + // Re-highlight when sentinel is present to show the prompt + line.contains('\x01') } } @@ -124,10 +123,12 @@ impl LineEditor { editor.set_helper(Some(SlashCommandHelper::new(completions))); editor.bind_sequence(KeyEvent(KeyCode::Char('J'), Modifiers::CTRL), Cmd::Newline); editor.bind_sequence(KeyEvent(KeyCode::Enter, Modifiers::SHIFT), Cmd::Newline); - // Ctrl+P: accept line to trigger provider wizard in the REPL loop + // Ctrl+P inserts a sentinel character that triggers provider swap. + // The sentinel is invisible but the highlighter shows "[Provider Swap]" prompt. + // User must press Enter to confirm (rustyline cannot chain commands). editor.bind_sequence( KeyEvent(KeyCode::Char('P'), Modifiers::CTRL), - Cmd::AcceptLine, + Cmd::SelfInsert(1, '\x01'), ); Self { @@ -162,16 +163,10 @@ impl LineEditor { match self.editor.readline(&self.prompt) { Ok(line) => { - // Check if Ctrl+P was detected by the highlighter. - // The highlighter sets a flag when it sees the previous - // empty line change to uppercase "P", which is what - // Ctrl+P + AcceptLine produces on an empty buffer. - let is_ctrl_p = self - .editor - .helper() - .is_some_and(|h| h.ctrl_p_pending.replace(false)); - if is_ctrl_p { - return Ok(ReadOutcome::Submit("/setup".to_string())); + // Ctrl+P inserts \x01 sentinel — triggers provider swap wizard. + // The sentinel is stripped and we return ProviderSwap to the REPL loop. + if line.contains('\x01') { + return Ok(ReadOutcome::ProviderSwap); } Ok(ReadOutcome::Submit(line)) } diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 665ce632..0931b4b0 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -7102,6 +7102,16 @@ fn run_repl( cli.record_prompt_history(&trimmed); cli.run_turn(&trimmed)?; } + input::ReadOutcome::ProviderSwap => { + // Ctrl+P triggered — launch setup wizard and hot-swap model + setup_wizard::run_setup_wizard()?; + let cwd = std::env::current_dir().unwrap_or_default(); + let config = runtime::ConfigLoader::default_for(&cwd).load().ok(); + if let Some(new_model) = config.as_ref().and_then(|c| c.provider().model().map(str::to_string)) { + cli.set_model(Some(new_model))?; + } + println!("{}", format_connected_line(&cli.model)); + } input::ReadOutcome::Cancel => {} input::ReadOutcome::Exit => { cli.persist_session()?;