diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 854ec4e7..652cfff4 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -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); diff --git a/crates/tui/src/tui/provider_picker.rs b/crates/tui/src/tui/provider_picker.rs index 495e8659..42b89241 100644 --- a/crates/tui/src/tui/provider_picker.rs +++ b/crates/tui/src/tui/provider_picker.rs @@ -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();