agent_ui: Use circular progress component for displaying context window use (#49138)

Danilo Leal created

This PR uses the recently introduced `CircularProgress` component to
display context window use information. It creates more space for the
message editor controls as well as simplifies the UI a little bit.
Through a tooltip, we communicate the same things we communicated before
(and more, actually, because rules use will be displayed there, too).
Note that this doesn't touch the display of split token use (for models
like GPT and whatnot).

<img width="500" height="484" alt="Screenshot 2026-02-13 at 6  16@2x"
src="https://github.com/user-attachments/assets/831cdfbf-fddd-4a11-8ac6-e9a25609aae9"
/>

Release Notes:

- Agent: Simplified context window use display by using a circular
progress component.

Change summary

crates/agent_ui/src/acp/thread_view.rs                 |   8 
crates/agent_ui/src/acp/thread_view/active_thread.rs   | 100 ++++++++++-
crates/ui/src/components/progress/circular_progress.rs |  10 +
3 files changed, 103 insertions(+), 15 deletions(-)

Detailed changes

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

@@ -50,10 +50,10 @@ use terminal_view::terminal_panel::TerminalPanel;
 use text::{Anchor, ToPoint as _};
 use theme::AgentFontSize;
 use ui::{
-    Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DecoratedIcon,
-    DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind, KeyBinding,
-    PopoverMenu, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
-    right_click_menu,
+    Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton,
+    DecoratedIcon, DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind,
+    KeyBinding, PopoverMenu, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar,
+    prelude::*, right_click_menu,
 };
 use util::{ResultExt, size::format_file_size, time::duration_alt_display};
 use util::{debug_panic, defer};

crates/agent_ui/src/acp/thread_view/active_thread.rs 🔗

@@ -2672,7 +2672,7 @@ impl AcpThreadView {
             .is_some_and(|model| model.supports_split_token_display())
     }
 
-    fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<Div> {
+    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;
@@ -2758,24 +2758,104 @@ impl AcpThreadView {
                                     .size(LabelSize::Small)
                                     .color(Color::Muted),
                             ),
-                    ),
+                    )
+                    .into_any_element(),
             )
         } else {
             let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens);
             let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens);
+            let progress_ratio = if usage.max_tokens > 0 {
+                usage.used_tokens as f32 / usage.max_tokens as f32
+            } else {
+                0.0
+            };
+
+            let progress_color = if progress_ratio >= 0.85 {
+                cx.theme().status().warning
+            } else {
+                cx.theme().colors().text_muted
+            };
+            let separator_color = Color::Custom(cx.theme().colors().text_disabled.opacity(0.6));
+
+            let percentage = format!("{}%", (progress_ratio * 100.0).round() as u32);
+
+            let (user_rules_count, project_rules_count) = self
+                .as_native_thread(cx)
+                .map(|thread| {
+                    let project_context = thread.read(cx).project_context().read(cx);
+                    let user_rules = project_context.user_rules.len();
+                    let project_rules = project_context
+                        .worktrees
+                        .iter()
+                        .filter(|wt| wt.rules_file.is_some())
+                        .count();
+                    (user_rules, project_rules)
+                })
+                .unwrap_or((0, 0));
 
             Some(
                 h_flex()
-                    .flex_shrink_0()
-                    .gap_0p5()
-                    .mr_1p5()
-                    .child(token_label(used, "used-tokens-label"))
+                    .id("circular_progress_tokens")
+                    .mt_px()
+                    .mr_1()
                     .child(
-                        Label::new("/")
-                            .size(LabelSize::Small)
-                            .color(separator_color),
+                        CircularProgress::new(
+                            usage.used_tokens as f32,
+                            usage.max_tokens as f32,
+                            px(16.0),
+                            cx,
+                        )
+                        .stroke_width(px(2.))
+                        .progress_color(progress_color),
                     )
-                    .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)),
+                    .tooltip(Tooltip::element({
+                        move |_, cx| {
+                            v_flex()
+                                .min_w_40()
+                                .child(
+                                    Label::new("Context")
+                                        .color(Color::Muted)
+                                        .size(LabelSize::Small),
+                                )
+                                .child(
+                                    h_flex()
+                                        .gap_0p5()
+                                        .child(Label::new(percentage.clone()))
+                                        .child(Label::new("•").color(separator_color).mx_1())
+                                        .child(Label::new(used.clone()))
+                                        .child(Label::new("/").color(separator_color))
+                                        .child(Label::new(max.clone()).color(Color::Muted)),
+                                )
+                                .when(user_rules_count > 0 || project_rules_count > 0, |this| {
+                                    this.child(
+                                        v_flex()
+                                            .mt_1p5()
+                                            .pt_1p5()
+                                            .border_t_1()
+                                            .border_color(cx.theme().colors().border_variant)
+                                            .child(
+                                                Label::new("Rules")
+                                                    .color(Color::Muted)
+                                                    .size(LabelSize::Small),
+                                            )
+                                            .when(user_rules_count > 0, |this| {
+                                                this.child(Label::new(format!(
+                                                    "{} user rules",
+                                                    user_rules_count
+                                                )))
+                                            })
+                                            .when(project_rules_count > 0, |this| {
+                                                this.child(Label::new(format!(
+                                                    "{} project rules",
+                                                    project_rules_count
+                                                )))
+                                            }),
+                                    )
+                                })
+                                .into_any_element()
+                        }
+                    }))
+                    .into_any_element(),
             )
         }
     }

crates/ui/src/components/progress/circular_progress.rs 🔗

@@ -10,6 +10,7 @@ pub struct CircularProgress {
     value: f32,
     max_value: f32,
     size: Pixels,
+    stroke_width: Pixels,
     bg_color: Hsla,
     progress_color: Hsla,
 }
@@ -20,6 +21,7 @@ impl CircularProgress {
             value,
             max_value,
             size,
+            stroke_width: px(4.0),
             bg_color: cx.theme().colors().border_variant,
             progress_color: cx.theme().status().info,
         }
@@ -43,6 +45,12 @@ impl CircularProgress {
         self
     }
 
+    /// Sets the stroke width of the circular progress indicator.
+    pub fn stroke_width(mut self, stroke_width: Pixels) -> Self {
+        self.stroke_width = stroke_width;
+        self
+    }
+
     /// Sets the background circle color.
     pub fn bg_color(mut self, color: Hsla) -> Self {
         self.bg_color = color;
@@ -72,7 +80,7 @@ impl RenderOnce for CircularProgress {
                 let center_x = bounds.origin.x + bounds.size.width / 2.0;
                 let center_y = bounds.origin.y + bounds.size.height / 2.0;
 
-                let stroke_width = px(4.0);
+                let stroke_width = self.stroke_width;
                 let radius = (size / 2.0) - stroke_width;
 
                 // Draw background circle (full 360 degrees)