fix: Ctrl+P provider swap with visual feedback + clippy cleanup
Ctrl+P now inserts a sentinel char (\x01) that the highlighter renders as a cyan "[Provider Swap]" prompt. User presses Enter to confirm and launch the setup wizard. Returns ReadOutcome::ProviderSwap so the REPL loop can hot-swap the model and reprint the connection line. Also fixes clippy warnings: merged duplicate match arms in provider_config_value, doc_markdown on ProviderKind, map_unwrap_or idioms in setup_wizard.rs, and pre-existing clippy issues in main.rs and commands/lib.rs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
da19b7c997
commit
050d505b9b
|
|
@ -408,14 +408,14 @@ pub fn provider_config_value(key: &str) -> Option<String> {
|
|||
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<Option<String>, ApiError> {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
/// Return the stored ProviderKind from config, if set.
|
||||
/// Return the stored `ProviderKind` from config, if set.
|
||||
fn stored_provider_kind() -> Option<ProviderKind> {
|
||||
let cwd = std::env::current_dir().ok()?;
|
||||
let config = runtime::ConfigLoader::default_for(&cwd).load().ok()?;
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ pub enum ReadOutcome {
|
|||
Submit(String),
|
||||
Cancel,
|
||||
Exit,
|
||||
ProviderSwap,
|
||||
}
|
||||
|
||||
struct SlashCommandHelper {
|
||||
completions: Vec<String>,
|
||||
current_line: RefCell<String>,
|
||||
ctrl_p_pending: RefCell<bool>,
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||
Loading…
Reference in New Issue