diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 0f230fb3d165440e71bedef4e01f6f0e4bb2e058..df6399990c873ab1d81070d62eff349057e6bbb4 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -23,8 +23,9 @@ use std::path::Path; use std::sync::Arc; use theme::ActiveTheme; use ui::{ - AgentThreadStatus, ButtonStyle, HighlightedLabel, KeyBinding, ListItem, PopoverMenu, - PopoverMenuHandle, Tab, ThreadItem, TintColor, Tooltip, WithScrollbar, prelude::*, + AgentThreadStatus, ButtonStyle, CommonAnimationExt as _, HighlightedLabel, KeyBinding, + ListItem, PopoverMenu, PopoverMenuHandle, Tab, ThreadItem, TintColor, Tooltip, WithScrollbar, + prelude::*, }; use util::ResultExt as _; use util::path_list::PathList; @@ -117,6 +118,8 @@ enum ListEntry { label: SharedString, workspace: Entity, highlight_positions: Vec, + has_running_threads: bool, + waiting_thread_count: usize, }, Thread(ThreadEntry), ViewMore { @@ -717,6 +720,15 @@ impl Sidebar { let is_collapsed = self.collapsed_groups.contains(&path_list); let should_load_threads = !is_collapsed || !query.is_empty(); + let live_infos = Self::all_thread_infos_for_workspace(workspace, cx); + let has_running_threads = live_infos + .iter() + .any(|info| info.status == AgentThreadStatus::Running); + let waiting_thread_count = live_infos + .iter() + .filter(|info| info.status == AgentThreadStatus::WaitingForConfirmation) + .count(); + let mut threads: Vec = Vec::new(); if should_load_threads { @@ -846,8 +858,6 @@ impl Sidebar { } } - let live_infos = Self::all_thread_infos_for_workspace(workspace, cx); - if !live_infos.is_empty() { let thread_index_by_session: HashMap = threads .iter() @@ -941,6 +951,8 @@ impl Sidebar { label, workspace: workspace.clone(), highlight_positions: workspace_highlight_positions, + has_running_threads, + waiting_thread_count, }); // Track session IDs and compute active_entry_index as we add @@ -962,6 +974,8 @@ impl Sidebar { label, workspace: workspace.clone(), highlight_positions: Vec::new(), + has_running_threads, + waiting_thread_count, }); if is_collapsed { @@ -1113,6 +1127,8 @@ impl Sidebar { label, workspace, highlight_positions, + has_running_threads, + waiting_thread_count, } => self.render_project_header( ix, false, @@ -1120,6 +1136,8 @@ impl Sidebar { label, workspace, highlight_positions, + *has_running_threads, + *waiting_thread_count, is_selected, cx, ), @@ -1162,6 +1180,8 @@ impl Sidebar { label: &SharedString, workspace: &Entity, highlight_positions: &[usize], + has_running_threads: bool, + waiting_thread_count: usize, is_selected: bool, cx: &mut Context, ) -> AnyElement { @@ -1213,7 +1233,32 @@ impl Sidebar { .size(IconSize::Small) .color(Color::Custom(cx.theme().colors().icon_muted.opacity(0.6))), ) - .child(label), + .child(label) + .when(is_collapsed && has_running_threads, |this| { + this.child( + Icon::new(IconName::LoadCircle) + .size(IconSize::XSmall) + .color(Color::Muted) + .with_rotate_animation(2), + ) + }) + .when(is_collapsed && waiting_thread_count > 0, |this| { + let tooltip_text = if waiting_thread_count == 1 { + "1 thread is waiting for confirmation".to_string() + } else { + format!("{waiting_thread_count} threads are waiting for confirmation",) + }; + this.child( + div() + .id(format!("{id_prefix}waiting-indicator-{ix}")) + .child( + Icon::new(IconName::Warning) + .size(IconSize::XSmall) + .color(Color::Warning), + ) + .tooltip(Tooltip::text(tooltip_text)), + ) + }), ) .end_hover_gradient_overlay(true) .end_hover_slot( @@ -1297,6 +1342,8 @@ impl Sidebar { label, workspace, highlight_positions, + has_running_threads, + waiting_thread_count, } = self.contents.entries.get(header_idx)? else { return None; @@ -1312,6 +1359,8 @@ impl Sidebar { &label, &workspace, &highlight_positions, + *has_running_threads, + *waiting_thread_count, is_selected, cx, ); @@ -3095,6 +3144,8 @@ mod tests { label: "expanded-project".into(), workspace: workspace.clone(), highlight_positions: Vec::new(), + has_running_threads: false, + waiting_thread_count: 0, }, // Thread with default (Completed) status, not active ListEntry::Thread(ThreadEntry { @@ -3223,6 +3274,8 @@ mod tests { label: "collapsed-project".into(), workspace: workspace.clone(), highlight_positions: Vec::new(), + has_running_threads: false, + waiting_thread_count: 0, }, ]; // Select the Running thread (index 2) diff --git a/crates/ui/src/components/ai/thread_item.rs b/crates/ui/src/components/ai/thread_item.rs index ed9192f5ab9bb9054e201e3661a7df3d742e20c8..3f50cabf7a6ee96fc0233bd38550c7bb38aee2f1 100644 --- a/crates/ui/src/components/ai/thread_item.rs +++ b/crates/ui/src/components/ai/thread_item.rs @@ -1,6 +1,6 @@ use crate::{ CommonAnimationExt, DecoratedIcon, DiffStat, GradientFade, HighlightedLabel, IconDecoration, - IconDecorationKind, prelude::*, + IconDecorationKind, Tooltip, prelude::*, }; use gpui::{ @@ -206,12 +206,7 @@ impl RenderOnce for ThreadItem { }) }; - let decoration = if self.status == AgentThreadStatus::WaitingForConfirmation { - Some(decoration( - IconDecorationKind::Triangle, - cx.theme().status().warning, - )) - } else if self.status == AgentThreadStatus::Error { + let decoration = if self.status == AgentThreadStatus::Error { Some(decoration(IconDecorationKind::X, cx.theme().status().error)) } else if self.notified { Some(decoration(IconDecorationKind::Dot, color.text_accent)) @@ -219,22 +214,31 @@ impl RenderOnce for ThreadItem { None }; - let is_running = matches!( - self.status, - AgentThreadStatus::Running | AgentThreadStatus::WaitingForConfirmation - ); - - let icon = if is_running { - icon_container().child( - Icon::new(IconName::LoadCircle) - .size(IconSize::Small) - .color(Color::Muted) - .with_rotate_animation(2), - ) + let icon = if self.status == AgentThreadStatus::Running { + icon_container() + .child( + Icon::new(IconName::LoadCircle) + .size(IconSize::Small) + .color(Color::Muted) + .with_rotate_animation(2), + ) + .into_any_element() + } else if self.status == AgentThreadStatus::WaitingForConfirmation { + icon_container() + .id("waiting-for-confirmation") + .child( + Icon::new(IconName::Warning) + .size(IconSize::Small) + .color(Color::Warning), + ) + .tooltip(Tooltip::text("Waiting for confirmation")) + .into_any_element() } else if let Some(decoration) = decoration { - icon_container().child(DecoratedIcon::new(agent_icon, Some(decoration))) + icon_container() + .child(DecoratedIcon::new(agent_icon, Some(decoration))) + .into_any_element() } else { - icon_container().child(agent_icon) + icon_container().child(agent_icon).into_any_element() }; let title = self.title;