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:
@@ -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);
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user