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 provider = config.provider();
|
||||||
let kind = provider.kind()?;
|
let kind = provider.kind()?;
|
||||||
match (key, kind) {
|
match (key, kind) {
|
||||||
("ANTHROPIC_API_KEY" | "ANTHROPIC_AUTH_TOKEN", "anthropic") => provider.api_key().map(ToOwned::to_owned),
|
("ANTHROPIC_API_KEY" | "ANTHROPIC_AUTH_TOKEN", "anthropic")
|
||||||
("ANTHROPIC_BASE_URL", "anthropic") => provider.base_url().map(ToOwned::to_owned),
|
| ("XAI_API_KEY", "xai")
|
||||||
("XAI_API_KEY", "xai") => provider.api_key().map(ToOwned::to_owned),
|
| ("OPENAI_API_KEY", "openai")
|
||||||
("XAI_BASE_URL", "xai") => provider.base_url().map(ToOwned::to_owned),
|
| ("DASHSCOPE_API_KEY", "dashscope") => provider.api_key().map(ToOwned::to_owned),
|
||||||
("OPENAI_API_KEY", "openai") => provider.api_key().map(ToOwned::to_owned),
|
("ANTHROPIC_BASE_URL", "anthropic")
|
||||||
("OPENAI_BASE_URL", "openai") => provider.base_url().map(ToOwned::to_owned),
|
| ("XAI_BASE_URL", "xai")
|
||||||
("DASHSCOPE_API_KEY", "dashscope") => provider.api_key().map(ToOwned::to_owned),
|
| ("OPENAI_BASE_URL", "openai")
|
||||||
("DASHSCOPE_BASE_URL", "dashscope") => provider.base_url().map(ToOwned::to_owned),
|
| ("DASHSCOPE_BASE_URL", "dashscope") => provider.base_url().map(ToOwned::to_owned),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -437,7 +437,7 @@ pub fn read_env_or_config(key: &str) -> Result<Option<String>, ApiError> {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the stored ProviderKind from config, if set.
|
/// Return the stored `ProviderKind` from config, if set.
|
||||||
fn stored_provider_kind() -> Option<ProviderKind> {
|
fn stored_provider_kind() -> Option<ProviderKind> {
|
||||||
let cwd = std::env::current_dir().ok()?;
|
let cwd = std::env::current_dir().ok()?;
|
||||||
let config = runtime::ConfigLoader::default_for(&cwd).load().ok()?;
|
let config = runtime::ConfigLoader::default_for(&cwd).load().ok()?;
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,12 @@ pub enum ReadOutcome {
|
||||||
Submit(String),
|
Submit(String),
|
||||||
Cancel,
|
Cancel,
|
||||||
Exit,
|
Exit,
|
||||||
|
ProviderSwap,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SlashCommandHelper {
|
struct SlashCommandHelper {
|
||||||
completions: Vec<String>,
|
completions: Vec<String>,
|
||||||
current_line: RefCell<String>,
|
current_line: RefCell<String>,
|
||||||
ctrl_p_pending: RefCell<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SlashCommandHelper {
|
impl SlashCommandHelper {
|
||||||
|
|
@ -31,7 +31,6 @@ impl SlashCommandHelper {
|
||||||
Self {
|
Self {
|
||||||
completions: normalize_completions(completions),
|
completions: normalize_completions(completions),
|
||||||
current_line: RefCell::new(String::new()),
|
current_line: RefCell::new(String::new()),
|
||||||
ctrl_p_pending: RefCell::new(false),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,19 +87,19 @@ impl Hinter for SlashCommandHelper {
|
||||||
impl Highlighter for SlashCommandHelper {
|
impl Highlighter for SlashCommandHelper {
|
||||||
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
|
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
|
||||||
self.set_current_line(line);
|
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 {
|
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);
|
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.set_helper(Some(SlashCommandHelper::new(completions)));
|
||||||
editor.bind_sequence(KeyEvent(KeyCode::Char('J'), Modifiers::CTRL), Cmd::Newline);
|
editor.bind_sequence(KeyEvent(KeyCode::Char('J'), Modifiers::CTRL), Cmd::Newline);
|
||||||
editor.bind_sequence(KeyEvent(KeyCode::Enter, Modifiers::SHIFT), 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(
|
editor.bind_sequence(
|
||||||
KeyEvent(KeyCode::Char('P'), Modifiers::CTRL),
|
KeyEvent(KeyCode::Char('P'), Modifiers::CTRL),
|
||||||
Cmd::AcceptLine,
|
Cmd::SelfInsert(1, '\x01'),
|
||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -162,16 +163,10 @@ impl LineEditor {
|
||||||
|
|
||||||
match self.editor.readline(&self.prompt) {
|
match self.editor.readline(&self.prompt) {
|
||||||
Ok(line) => {
|
Ok(line) => {
|
||||||
// Check if Ctrl+P was detected by the highlighter.
|
// Ctrl+P inserts \x01 sentinel — triggers provider swap wizard.
|
||||||
// The highlighter sets a flag when it sees the previous
|
// The sentinel is stripped and we return ProviderSwap to the REPL loop.
|
||||||
// empty line change to uppercase "P", which is what
|
if line.contains('\x01') {
|
||||||
// Ctrl+P + AcceptLine produces on an empty buffer.
|
return Ok(ReadOutcome::ProviderSwap);
|
||||||
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()));
|
|
||||||
}
|
}
|
||||||
Ok(ReadOutcome::Submit(line))
|
Ok(ReadOutcome::Submit(line))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7102,6 +7102,16 @@ fn run_repl(
|
||||||
cli.record_prompt_history(&trimmed);
|
cli.record_prompt_history(&trimmed);
|
||||||
cli.run_turn(&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::Cancel => {}
|
||||||
input::ReadOutcome::Exit => {
|
input::ReadOutcome::Exit => {
|
||||||
cli.persist_session()?;
|
cli.persist_session()?;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue