fix(tui): provider picker r shortcut with modifier guard
- add r/R shortcut to re-enter API key for any provider in picker - guard against Ctrl/Alt/Meta modifiers (only plain r triggers) - dynamic footer: 'apply' when key exists, 'set key' otherwise - add 'R edit key' hint to picker footer - add route/model to scoped auth status output - add tests for r shortcut, ctrl-r guard, footer text, and route/model Ports #2717 with review fix. Fixes #2662.
This commit is contained in:
@@ -1000,8 +1000,20 @@ fn auth_status_lines_for_provider(
|
||||
let is_active = provider == store.config.provider;
|
||||
let active_marker = if is_active { " (active provider)" } else { "" };
|
||||
|
||||
let provider_cfg = store.config.providers.for_provider(provider);
|
||||
let base_url = provider_cfg
|
||||
.base_url
|
||||
.as_deref()
|
||||
.unwrap_or("(default)");
|
||||
let model = provider_cfg
|
||||
.model
|
||||
.as_deref()
|
||||
.unwrap_or("(default)");
|
||||
|
||||
vec![
|
||||
format!("provider: {}{}", provider.as_str(), active_marker),
|
||||
format!("route: {}", base_url),
|
||||
format!("model: {}", model),
|
||||
format!(
|
||||
"auth mode: {}",
|
||||
store.config.auth_mode.as_deref().unwrap_or("api_key")
|
||||
@@ -2723,6 +2735,8 @@ mod tests {
|
||||
|
||||
assert!(output.contains("provider: arcee"));
|
||||
assert!(output.contains("active source: config (last4: ...9999)"));
|
||||
assert!(output.contains("route:"));
|
||||
assert!(output.contains("model:"));
|
||||
assert!(!output.contains("sk-arcee-9999"));
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
|
||||
@@ -94,6 +94,11 @@ impl ProviderPickerView {
|
||||
self.providers[self.selected_idx].1
|
||||
}
|
||||
|
||||
fn enter_key_entry(&mut self) {
|
||||
self.stage = Stage::KeyEntry;
|
||||
self.api_key_input.clear();
|
||||
}
|
||||
|
||||
fn env_var_for(provider: ApiProvider) -> &'static str {
|
||||
match provider {
|
||||
ApiProvider::Deepseek | ApiProvider::DeepseekCN => "DEEPSEEK_API_KEY",
|
||||
@@ -158,6 +163,11 @@ impl ProviderPickerView {
|
||||
}
|
||||
|
||||
fn render_list(&self, area: Rect, buf: &mut Buffer) {
|
||||
let enter_action = if self.selected_has_key() {
|
||||
"apply"
|
||||
} else {
|
||||
"set key"
|
||||
};
|
||||
let outer = Block::default()
|
||||
.title(Line::from(Span::styled(
|
||||
" Provider ",
|
||||
@@ -169,7 +179,9 @@ impl ProviderPickerView {
|
||||
Span::styled(" ↑↓ ", Style::default().fg(palette::TEXT_MUTED)),
|
||||
Span::raw("move "),
|
||||
Span::styled(" Enter ", Style::default().fg(palette::TEXT_MUTED)),
|
||||
Span::raw("apply "),
|
||||
Span::raw(format!("{enter_action} ")),
|
||||
Span::styled(" R ", Style::default().fg(palette::TEXT_MUTED)),
|
||||
Span::raw("edit key "),
|
||||
Span::styled(" Esc ", Style::default().fg(palette::TEXT_MUTED)),
|
||||
Span::raw("cancel "),
|
||||
]))
|
||||
@@ -362,11 +374,17 @@ impl ModalView for ProviderPickerView {
|
||||
provider,
|
||||
})
|
||||
} else {
|
||||
self.stage = Stage::KeyEntry;
|
||||
self.api_key_input.clear();
|
||||
self.enter_key_entry();
|
||||
ViewAction::None
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c)
|
||||
if key.modifiers.is_empty()
|
||||
&& c.eq_ignore_ascii_case(&'r') =>
|
||||
{
|
||||
self.enter_key_entry();
|
||||
ViewAction::None
|
||||
}
|
||||
_ => ViewAction::None,
|
||||
},
|
||||
Stage::KeyEntry => match key.code {
|
||||
@@ -564,6 +582,57 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_provider_can_reenter_key_entry_with_r() {
|
||||
let config = Config {
|
||||
providers: Some(crate::config::ProvidersConfig {
|
||||
xiaomi_mimo: crate::config::ProviderConfig {
|
||||
api_key: Some("mimo-key".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
..Config::default()
|
||||
};
|
||||
let mut picker = ProviderPickerView::new(ApiProvider::Deepseek, &config);
|
||||
move_to_provider(&mut picker, ApiProvider::XiaomiMimo);
|
||||
|
||||
let action = picker.handle_key(key(KeyCode::Char('r')));
|
||||
|
||||
assert!(matches!(action, ViewAction::None));
|
||||
assert_eq!(picker.stage, Stage::KeyEntry);
|
||||
assert!(picker.api_key_input.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_r_does_not_trigger_key_entry() {
|
||||
let config = Config::default();
|
||||
let mut picker = ProviderPickerView::new(ApiProvider::Deepseek, &config);
|
||||
|
||||
let action = picker.handle_key(KeyEvent::new(
|
||||
KeyCode::Char('r'),
|
||||
KeyModifiers::CONTROL,
|
||||
));
|
||||
|
||||
assert!(matches!(action, ViewAction::None));
|
||||
assert_eq!(picker.stage, Stage::List);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_provider_footer_mentions_edit_key() {
|
||||
let config = Config {
|
||||
api_key: Some("existing-deepseek-key".to_string()),
|
||||
..Config::default()
|
||||
};
|
||||
let picker = ProviderPickerView::new(ApiProvider::Deepseek, &config);
|
||||
|
||||
let rendered = render_text(&picker, 80, 12);
|
||||
|
||||
assert!(rendered.contains("Enter"));
|
||||
assert!(rendered.contains("apply"));
|
||||
assert!(rendered.contains("edit key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_entry_enter_submits_after_typing() {
|
||||
let config = Config::default();
|
||||
|
||||
Reference in New Issue
Block a user