fix(#38): show provider chip in header when not on default DeepSeek

The reasoning-effort tier (`max` chip + whale icon) and the live/context
indicators were the only signals on the right of the header. Switching
to nvidia-nim left the right-hand side identical to a DeepSeek session,
so it wasn't obvious at a glance that requests were going to a different
backend.

Now: when `app.api_provider != Deepseek`, the header surfaces a bold
`NIM` chip on the right, leftmost in the chip cluster (so it survives
the narrow-width fallback variants in `right_spans`). Default-DeepSeek
sessions are unchanged — `provider_label = None` short-circuits the
chip.

Closes #38.
This commit is contained in:
Hunter Bown
2026-04-25 13:41:52 -05:00
parent ba40ae4aac
commit 07ae792068
2 changed files with 81 additions and 2 deletions
+6 -1
View File
@@ -2726,6 +2726,10 @@ fn render(f: &mut Frame, app: &mut App) {
.filter(|value| !value.is_empty()) .filter(|value| !value.is_empty())
.unwrap_or("workspace"); .unwrap_or("workspace");
let effort_label = app.reasoning_effort.short_label(); 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( let header_data = HeaderData::new(
app.mode, app.mode,
&app.model, &app.model,
@@ -2739,7 +2743,8 @@ fn render(f: &mut Frame, app: &mut App) {
app.session_cost, app.session_cost,
sanitized_prompt_tokens, 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 header_widget = HeaderWidget::new(header_data);
let buf = f.buffer_mut(); let buf = f.buffer_mut();
header_widget.render(chunks[0], buf); header_widget.render(chunks[0], buf);
+75 -1
View File
@@ -36,6 +36,11 @@ pub struct HeaderData<'a> {
/// Short label for the current reasoning-effort tier (e.g. "max", "high", /// Short label for the current reasoning-effort tier (e.g. "max", "high",
/// "off"). Rendered as a chip when space allows. /// "off"). Rendered as a chip when space allows.
pub reasoning_effort_label: Option<&'a str>, 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> { impl<'a> HeaderData<'a> {
@@ -59,6 +64,7 @@ impl<'a> HeaderData<'a> {
session_cost: 0.0, session_cost: 0.0,
last_prompt_tokens: None, last_prompt_tokens: None,
reasoning_effort_label: None, reasoning_effort_label: None,
provider_label: None,
} }
} }
@@ -69,6 +75,14 @@ impl<'a> HeaderData<'a> {
self 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. /// Set token/cost fields.
#[must_use] #[must_use]
pub fn with_usage( pub fn with_usage(
@@ -202,6 +216,22 @@ impl<'a> HeaderWidget<'a> {
)] )]
} }
fn provider_chip_spans(&self) -> Vec<Span<'static>> {
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<Span<'static>> { fn effort_chip_spans(&self, include_prefix: bool) -> Vec<Span<'static>> {
let Some(label) = self.data.reasoning_effort_label else { let Some(label) = self.data.reasoning_effort_label else {
return Vec::new(); return Vec::new();
@@ -234,14 +264,23 @@ impl<'a> HeaderWidget<'a> {
) -> Vec<Span<'static>> { ) -> Vec<Span<'static>> {
let mut spans = Vec::new(); 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 effort_spans = self.effort_chip_spans(true);
let has_effort = !effort_spans.is_empty(); let has_effort = !effort_spans.is_empty();
if has_effort { if has_effort {
if has_provider {
spans.push(Span::raw(" "));
}
spans.extend(effort_spans); spans.extend(effort_spans);
} }
if self.data.is_streaming { if self.data.is_streaming {
if has_effort { if has_effort || has_provider {
spans.push(Span::raw(" ")); spans.push(Span::raw(" "));
} }
spans.push(Span::styled( spans.push(Span::styled(
@@ -553,4 +592,39 @@ mod tests {
assert!(rendered.contains("100%")); assert!(rendered.contains("100%"));
assert!(!rendered.contains("250%")); 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"));
}
} }