diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 40f7ad1b..60968ae7 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -2726,6 +2726,10 @@ fn render(f: &mut Frame, app: &mut App) { .filter(|value| !value.is_empty()) .unwrap_or("workspace"); let effort_label = app.reasoning_effort.short_label(); + let provider_label = match app.api_provider { + crate::config::ApiProvider::Deepseek => None, + crate::config::ApiProvider::NvidiaNim => Some("NIM"), + }; let header_data = HeaderData::new( app.mode, &app.model, @@ -2739,7 +2743,8 @@ fn render(f: &mut Frame, app: &mut App) { app.session_cost, sanitized_prompt_tokens, ) - .with_reasoning_effort(Some(effort_label)); + .with_reasoning_effort(Some(effort_label)) + .with_provider(provider_label); let header_widget = HeaderWidget::new(header_data); let buf = f.buffer_mut(); header_widget.render(chunks[0], buf); diff --git a/crates/tui/src/tui/widgets/header.rs b/crates/tui/src/tui/widgets/header.rs index 036b366d..28b9b74e 100644 --- a/crates/tui/src/tui/widgets/header.rs +++ b/crates/tui/src/tui/widgets/header.rs @@ -36,6 +36,11 @@ pub struct HeaderData<'a> { /// Short label for the current reasoning-effort tier (e.g. "max", "high", /// "off"). Rendered as a chip when space allows. pub reasoning_effort_label: Option<&'a str>, + /// Short label for the active provider (e.g. "NIM"). When `None` (the + /// default-DeepSeek case), no provider chip is rendered. Surfaces the + /// fact that requests are going somewhere other than DeepSeek's API so + /// it's visible at a glance after a `/provider nvidia-nim`. + pub provider_label: Option<&'a str>, } impl<'a> HeaderData<'a> { @@ -59,6 +64,7 @@ impl<'a> HeaderData<'a> { session_cost: 0.0, last_prompt_tokens: None, reasoning_effort_label: None, + provider_label: None, } } @@ -69,6 +75,14 @@ impl<'a> HeaderData<'a> { self } + /// Attach a short provider label for the header chip. Pass `None` when on + /// the default DeepSeek provider so the chip is hidden. + #[must_use] + pub fn with_provider(mut self, label: Option<&'a str>) -> Self { + self.provider_label = label; + self + } + /// Set token/cost fields. #[must_use] pub fn with_usage( @@ -202,6 +216,22 @@ impl<'a> HeaderWidget<'a> { )] } + fn provider_chip_spans(&self) -> Vec> { + let Some(label) = self.data.provider_label else { + return Vec::new(); + }; + let trimmed = label.trim(); + if trimmed.is_empty() { + return Vec::new(); + } + vec![Span::styled( + trimmed.to_string(), + Style::default() + .fg(palette::DEEPSEEK_SKY) + .add_modifier(Modifier::BOLD), + )] + } + fn effort_chip_spans(&self, include_prefix: bool) -> Vec> { let Some(label) = self.data.reasoning_effort_label else { return Vec::new(); @@ -234,14 +264,23 @@ impl<'a> HeaderWidget<'a> { ) -> Vec> { let mut spans = Vec::new(); + let provider_spans = self.provider_chip_spans(); + let has_provider = !provider_spans.is_empty(); + if has_provider { + spans.extend(provider_spans); + } + let effort_spans = self.effort_chip_spans(true); let has_effort = !effort_spans.is_empty(); if has_effort { + if has_provider { + spans.push(Span::raw(" ")); + } spans.extend(effort_spans); } if self.data.is_streaming { - if has_effort { + if has_effort || has_provider { spans.push(Span::raw(" ")); } spans.push(Span::styled( @@ -553,4 +592,39 @@ mod tests { assert!(rendered.contains("100%")); assert!(!rendered.contains("250%")); } + + #[test] + fn header_shows_provider_chip_when_set() { + let rendered = render_header( + HeaderData::new( + AppMode::Agent, + "deepseek-ai/deepseek-v4-flash", + "deepseek-tui", + false, + palette::DEEPSEEK_INK, + ) + .with_provider(Some("NIM")), + 72, + ); + assert!( + rendered.contains("NIM"), + "expected NIM chip in header, got: {rendered}" + ); + } + + #[test] + fn header_hides_provider_chip_when_default_deepseek() { + let rendered = render_header( + HeaderData::new( + AppMode::Agent, + "deepseek-v4-pro", + "deepseek-tui", + false, + palette::DEEPSEEK_INK, + ), + 72, + ); + // Sanity: no `NIM` text leaks in when provider is None. + assert!(!rendered.contains("NIM")); + } }