diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d2ae5a26fd9567777e301c4d070c7ed7d976756e..6e7ff8486bda34cb017b98374acabc81b1f614fc 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/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}; diff --git a/crates/agent_ui/src/acp/thread_view/active_thread.rs b/crates/agent_ui/src/acp/thread_view/active_thread.rs index 73b2408c02f2a9950a8c38b57bebc1fc1b3a51bc..1e8c7d35a3cb9214f557399a34244b7f2bf665c5 100644 --- a/crates/agent_ui/src/acp/thread_view/active_thread.rs +++ b/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) -> Option
{ + 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; @@ -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(), ) } } diff --git a/crates/ui/src/components/progress/circular_progress.rs b/crates/ui/src/components/progress/circular_progress.rs index ddd620282dc81836cefa79fc65f85bfb0bec7078..f571332edaec7387e5d083596db42879c56d01ac 100644 --- a/crates/ui/src/components/progress/circular_progress.rs +++ b/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)