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:
Hunter Bown
2026-06-03 15:14:39 -07:00
parent 3f8e02d6cf
commit be7a3e7e69
2 changed files with 86 additions and 3 deletions
+14
View File
@@ -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);
+72 -3
View File
@@ -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();