agent_ui: Use the CircularProgress component also for split token display (#52599)

Danilo Leal created

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:

<img width="500" height="386" alt="Screenshot 2026-03-27 at 5  03@2x"
src="https://github.com/user-attachments/assets/1fa01f6e-a8d8-40fd-a663-b95de6c79174"
/>

Release Notes:

- Agent: Make token usage display consistent between the models that
support displaying split usage (input and output) and those that don't.

Change summary

crates/agent_ui/src/conversation_view/thread_view.rs | 110 ++++++-------
1 file changed, 49 insertions(+), 61 deletions(-)

Detailed changes

crates/agent_ui/src/conversation_view/thread_view.rs 🔗

@@ -3402,29 +3402,14 @@ impl ThreadView {
     fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
         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()))