agent: Collapsed project thread UI indicators (#51824)

This commit is contained in:
Cameron Mcloughlin authored and GitHub committed 2026-03-18 10:13:17 +00:00
1 parent a6109443ed
commit eeb212f6a7
2 files changed
+83 -26

No files matched your search

+58 -5
View File
@@ -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<Workspace>,
highlight_positions: Vec<usize>,
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<ThreadEntry> = 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<acp::SessionId, usize> = 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<Workspace>,
highlight_positions: &[usize],
has_running_threads: bool,
waiting_thread_count: usize,
is_selected: bool,
cx: &mut Context<Self>,
) -> 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)
+25 -21
View File
@@ -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;