From 54b0ec9325f77b6e6c7608e87dfe0eea954d8b75 Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Fri, 27 Mar 2026 18:21:43 -0300
Subject: [PATCH] agent_ui: Use the CircularProgress component also for split
token display (#52599)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR swaps numbers for two circular progress components for the
models that support displaying token usage broken down by input and
output tokens. Here's how the UI looks like:
Release Notes:
- Agent: Make token usage display consistent between the models that
support displaying split usage (input and output) and those that don't.
---
.../src/conversation_view/thread_view.rs | 110 ++++++++----------
1 file changed, 49 insertions(+), 61 deletions(-)
diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs
index b6708647868214d3ca02a2952ce718defe6ab557..7caeb687bebb32083c2647a2bc0d359b36e03b58 100644
--- a/crates/agent_ui/src/conversation_view/thread_view.rs
+++ b/crates/agent_ui/src/conversation_view/thread_view.rs
@@ -3402,29 +3402,14 @@ impl ThreadView {
fn render_token_usage(&self, cx: &mut Context) -> Option {
let thread = self.thread.read(cx);
let usage = thread.token_usage()?;
- let is_generating = thread.status() != ThreadStatus::Idle;
let show_split = self.supports_split_token_display(cx);
- let separator_color = Color::Custom(cx.theme().colors().text_muted.opacity(0.5));
- let token_label = |text: String, animation_id: &'static str| {
- Label::new(text)
- .size(LabelSize::Small)
- .color(Color::Muted)
- .map(|label| {
- if is_generating {
- label
- .with_animation(
- animation_id,
- Animation::new(Duration::from_secs(2))
- .repeat()
- .with_easing(pulsating_between(0.3, 0.8)),
- |label, delta| label.alpha(delta),
- )
- .into_any()
- } else {
- label.into_any_element()
- }
- })
+ let progress_color = |ratio: f32| -> Hsla {
+ if ratio >= 0.85 {
+ cx.theme().status().warning
+ } else {
+ cx.theme().colors().text_muted
+ }
};
let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens);
@@ -3439,6 +3424,10 @@ impl ThreadView {
} else {
0.0
};
+
+ let ring_size = px(16.0);
+ let stroke_width = px(2.);
+
let percentage = format!("{}%", (progress_ratio * 100.0).round() as u32);
let tooltip_separator_color = Color::Custom(cx.theme().colors().text_disabled.opacity(0.6));
@@ -3478,8 +3467,6 @@ impl ThreadView {
let output_max_label = crate::text_thread_editor::humanize_token_count(max_output_tokens);
let build_tooltip = {
- let input_max_label = input_max_label.clone();
- let output_max_label = output_max_label.clone();
move |_window: &mut Window, cx: &mut App| {
let percentage = percentage.clone();
let used = used.clone();
@@ -3511,17 +3498,26 @@ impl ThreadView {
};
if show_split {
- let input = crate::text_thread_editor::humanize_token_count(usage.input_tokens);
- let input_max = input_max_label;
- let output = crate::text_thread_editor::humanize_token_count(usage.output_tokens);
- let output_max = output_max_label;
+ let input_max_raw = usage.max_tokens.saturating_sub(max_output_tokens);
+ let output_max_raw = max_output_tokens;
+
+ let input_ratio = if input_max_raw > 0 {
+ usage.input_tokens as f32 / input_max_raw as f32
+ } else {
+ 0.0
+ };
+ let output_ratio = if output_max_raw > 0 {
+ usage.output_tokens as f32 / output_max_raw as f32
+ } else {
+ 0.0
+ };
Some(
h_flex()
.id("split_token_usage")
.flex_shrink_0()
- .gap_1()
- .mr_1p5()
+ .gap_1p5()
+ .mr_1()
.child(
h_flex()
.gap_0p5()
@@ -3530,16 +3526,15 @@ impl ThreadView {
.size(IconSize::XSmall)
.color(Color::Muted),
)
- .child(token_label(input, "input-tokens-label"))
- .child(
- Label::new("/")
- .size(LabelSize::Small)
- .color(separator_color),
- )
.child(
- Label::new(input_max)
- .size(LabelSize::Small)
- .color(Color::Muted),
+ CircularProgress::new(
+ usage.input_tokens as f32,
+ input_max_raw as f32,
+ ring_size,
+ cx,
+ )
+ .stroke_width(stroke_width)
+ .progress_color(progress_color(input_ratio)),
),
)
.child(
@@ -3550,28 +3545,21 @@ impl ThreadView {
.size(IconSize::XSmall)
.color(Color::Muted),
)
- .child(token_label(output, "output-tokens-label"))
.child(
- Label::new("/")
- .size(LabelSize::Small)
- .color(separator_color),
- )
- .child(
- Label::new(output_max)
- .size(LabelSize::Small)
- .color(Color::Muted),
+ CircularProgress::new(
+ usage.output_tokens as f32,
+ output_max_raw as f32,
+ ring_size,
+ cx,
+ )
+ .stroke_width(stroke_width)
+ .progress_color(progress_color(output_ratio)),
),
)
.hoverable_tooltip(build_tooltip)
.into_any_element(),
)
} else {
- let progress_color = if progress_ratio >= 0.85 {
- cx.theme().status().warning
- } else {
- cx.theme().colors().text_muted
- };
-
Some(
h_flex()
.id("circular_progress_tokens")
@@ -3581,11 +3569,11 @@ impl ThreadView {
CircularProgress::new(
usage.used_tokens as f32,
usage.max_tokens as f32,
- px(16.0),
+ ring_size,
cx,
)
- .stroke_width(px(2.))
- .progress_color(progress_color),
+ .stroke_width(stroke_width)
+ .progress_color(progress_color(progress_ratio)),
)
.hoverable_tooltip(build_tooltip)
.into_any_element(),
@@ -4173,13 +4161,13 @@ impl Render for TokenUsageTooltip {
ui::tooltip_container(cx, move |container, cx| {
container
.min_w_40()
+ .child(
+ Label::new("Context")
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ )
.when(!show_split, |this| {
this.child(
- Label::new("Context")
- .color(Color::Muted)
- .size(LabelSize::Small),
- )
- .child(
h_flex()
.gap_0p5()
.child(Label::new(percentage.clone()))