@@ -2482,13 +2482,17 @@ impl Sidebar {
}
fn mru_threads_for_switcher(&self, _cx: &App) -> Vec<ThreadSwitcherEntry> {
+ let mut current_header_label: Option<SharedString> = None;
let mut current_header_workspace: Option<Entity<Workspace>> = None;
let mut entries: Vec<ThreadSwitcherEntry> = self
.contents
.entries
.iter()
.filter_map(|entry| match entry {
- ListEntry::ProjectHeader { workspace, .. } => {
+ ListEntry::ProjectHeader {
+ label, workspace, ..
+ } => {
+ current_header_label = Some(label.clone());
current_header_workspace = Some(workspace.clone());
None
}
@@ -2518,8 +2522,16 @@ impl Sidebar {
status: thread.status,
metadata: thread.metadata.clone(),
workspace,
- worktree_name: thread.worktrees.first().map(|wt| wt.name.clone()),
-
+ project_name: current_header_label.clone(),
+ worktrees: thread
+ .worktrees
+ .iter()
+ .map(|wt| ThreadItemWorktreeInfo {
+ name: wt.name.clone(),
+ full_path: wt.full_path.clone(),
+ highlight_positions: Vec::new(),
+ })
+ .collect(),
diff_stats: thread.diff_stats,
is_title_generating: thread.is_title_generating,
notified,
@@ -2,20 +2,13 @@ use action_log::DiffStats;
use agent_client_protocol as acp;
use agent_ui::thread_metadata_store::ThreadMetadata;
use gpui::{
- Action as _, Animation, AnimationExt, AnyElement, DismissEvent, Entity, EventEmitter,
- FocusHandle, Focusable, Hsla, Modifiers, ModifiersChangedEvent, Render, SharedString,
- prelude::*, pulsating_between,
-};
-use std::time::Duration;
-use ui::{
- AgentThreadStatus, Color, CommonAnimationExt, DecoratedIcon, DiffStat, Icon, IconDecoration,
- IconDecorationKind, IconName, IconSize, Label, LabelSize, prelude::*,
+ Action as _, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Modifiers,
+ ModifiersChangedEvent, Render, SharedString, prelude::*,
};
+use ui::{AgentThreadStatus, ThreadItem, ThreadItemWorktreeInfo, prelude::*};
use workspace::{ModalView, Workspace};
use zed_actions::agents_sidebar::ToggleThreadSwitcher;
-const PANEL_WIDTH_REMS: f32 = 28.;
-
pub(crate) struct ThreadSwitcherEntry {
pub session_id: acp::SessionId,
pub title: SharedString,
@@ -24,7 +17,8 @@ pub(crate) struct ThreadSwitcherEntry {
pub status: AgentThreadStatus,
pub metadata: ThreadMetadata,
pub workspace: Entity<Workspace>,
- pub worktree_name: Option<SharedString>,
+ pub project_name: Option<SharedString>,
+ pub worktrees: Vec<ThreadItemWorktreeInfo>,
pub diff_stats: DiffStats,
pub is_title_generating: bool,
pub notified: bool,
@@ -193,179 +187,44 @@ impl Focusable for ThreadSwitcher {
impl Render for ThreadSwitcher {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
let selected_index = self.selected_index;
- let color = cx.theme().colors();
- let panel_bg = color
- .title_bar_background
- .blend(color.panel_background.opacity(0.2));
v_flex()
.key_context("ThreadSwitcher")
.track_focus(&self.focus_handle)
- .w(gpui::rems(PANEL_WIDTH_REMS))
+ .w(rems_from_px(440.))
+ .p_1p5()
+ .gap_0p5()
.elevation_3(cx)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::toggle))
.children(self.entries.iter().enumerate().map(|(ix, entry)| {
- let is_first = ix == 0;
- let is_last = ix == self.entries.len() - 1;
- let selected = ix == selected_index;
- let base_bg = if selected {
- color.element_active
- } else {
- panel_bg
- };
-
- let dot_separator = || {
- Label::new("\u{2022}")
- .size(LabelSize::Small)
- .color(Color::Muted)
- .alpha(0.5)
- };
-
- let icon_container = || h_flex().size_4().flex_none().justify_center();
-
- let agent_icon = || {
- if let Some(ref svg) = entry.icon_from_external_svg {
- Icon::from_external_svg(svg.clone())
- .color(Color::Muted)
- .size(IconSize::Small)
- } else {
- Icon::new(entry.icon)
- .color(Color::Muted)
- .size(IconSize::Small)
- }
- };
-
- let decoration = |kind: IconDecorationKind, deco_color: Hsla| {
- IconDecoration::new(kind, base_bg, cx)
- .color(deco_color)
- .position(gpui::Point {
- x: px(-2.),
- y: px(-2.),
- })
- };
-
- let icon_element: AnyElement = if entry.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 entry.status == AgentThreadStatus::Error {
- icon_container()
- .child(DecoratedIcon::new(
- agent_icon(),
- Some(decoration(IconDecorationKind::X, cx.theme().status().error)),
- ))
- .into_any_element()
- } else if entry.status == AgentThreadStatus::WaitingForConfirmation {
- icon_container()
- .child(DecoratedIcon::new(
- agent_icon(),
- Some(decoration(
- IconDecorationKind::Triangle,
- cx.theme().status().warning,
- )),
- ))
- .into_any_element()
- } else if entry.notified {
- icon_container()
- .child(DecoratedIcon::new(
- agent_icon(),
- Some(decoration(IconDecorationKind::Dot, color.text_accent)),
- ))
- .into_any_element()
- } else {
- icon_container().child(agent_icon()).into_any_element()
- };
-
- let title_label: AnyElement = if entry.is_title_generating {
- Label::new(entry.title.clone())
- .color(Color::Muted)
- .with_animation(
- "generating-title",
- Animation::new(Duration::from_secs(2))
- .repeat()
- .with_easing(pulsating_between(0.4, 0.8)),
- |label, delta| label.alpha(delta),
- )
- .into_any_element()
- } else {
- Label::new(entry.title.clone()).into_any_element()
- };
-
- let has_diff_stats =
- entry.diff_stats.lines_added > 0 || entry.diff_stats.lines_removed > 0;
- let has_worktree = entry.worktree_name.is_some();
- let has_timestamp = !entry.timestamp.is_empty();
-
- v_flex()
- .id(ix)
- .w_full()
- .py_1()
- .px_1p5()
- .border_1()
- .border_color(gpui::transparent_black())
- .when(selected, |s| s.bg(color.element_active))
- .when(is_first, |s| s.rounded_t_lg())
- .when(is_last, |s| s.rounded_b_lg())
- .child(
- h_flex()
- .min_w_0()
- .w_full()
- .gap_1p5()
- .child(icon_element)
- .child(title_label),
- )
- .when(has_worktree || has_diff_stats || has_timestamp, |this| {
- this.child(
- h_flex()
- .min_w_0()
- .gap_1p5()
- .child(icon_container())
- .when_some(entry.worktree_name.clone(), |this, worktree| {
- this.child(
- h_flex()
- .gap_1()
- .child(
- Icon::new(IconName::GitWorktree)
- .size(IconSize::XSmall)
- .color(Color::Muted),
- )
- .child(
- Label::new(worktree)
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- })
- .when(has_worktree && (has_diff_stats || has_timestamp), |this| {
- this.child(dot_separator())
- })
- .when(has_diff_stats, |this| {
- this.child(DiffStat::new(
- ix,
- entry.diff_stats.lines_added as usize,
- entry.diff_stats.lines_removed as usize,
- ))
- })
- .when(has_diff_stats && has_timestamp, |this| {
- this.child(dot_separator())
- })
- .when(has_timestamp, |this| {
- this.child(
- Label::new(entry.timestamp.clone())
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- }),
- )
+ let id = SharedString::from(format!("thread-switcher-{}", entry.session_id));
+
+ ThreadItem::new(id, entry.title.clone())
+ .rounded(true)
+ .icon(entry.icon)
+ .status(entry.status)
+ .when_some(entry.icon_from_external_svg.clone(), |this, svg| {
+ this.custom_icon_from_external_svg(svg)
+ })
+ .when_some(entry.project_name.clone(), |this, name| {
+ this.project_name(name)
+ })
+ .worktrees(entry.worktrees.clone())
+ .timestamp(entry.timestamp.clone())
+ .title_generating(entry.is_title_generating)
+ .notified(entry.notified)
+ .when(entry.diff_stats.lines_added > 0, |this| {
+ this.added(entry.diff_stats.lines_added as usize)
+ })
+ .when(entry.diff_stats.lines_removed > 0, |this| {
+ this.removed(entry.diff_stats.lines_removed as usize)
})
+ .selected(ix == selected_index)
+ .base_bg(cx.theme().colors().surface_background)
+ .into_any_element()
}))
}
}
@@ -43,14 +43,17 @@ pub struct ThreadItem {
selected: bool,
focused: bool,
hovered: bool,
+ rounded: bool,
added: Option<usize>,
removed: Option<usize>,
project_paths: Option<Arc<[PathBuf]>>,
+ project_name: Option<SharedString>,
worktrees: Vec<ThreadItemWorktreeInfo>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
action_slot: Option<AnyElement>,
tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
+ base_bg: Option<Hsla>,
}
impl ThreadItem {
@@ -71,15 +74,18 @@ impl ThreadItem {
selected: false,
focused: false,
hovered: false,
+ rounded: false,
added: None,
removed: None,
project_paths: None,
+ project_name: None,
worktrees: Vec::new(),
on_click: None,
on_hover: Box::new(|_, _, _| {}),
action_slot: None,
tooltip: None,
+ base_bg: None,
}
}
@@ -158,6 +164,11 @@ impl ThreadItem {
self
}
+ pub fn project_name(mut self, name: impl Into<SharedString>) -> Self {
+ self.project_name = Some(name.into());
+ self
+ }
+
pub fn worktrees(mut self, worktrees: Vec<ThreadItemWorktreeInfo>) -> Self {
self.worktrees = worktrees;
self
@@ -168,6 +179,11 @@ impl ThreadItem {
self
}
+ pub fn rounded(mut self, rounded: bool) -> Self {
+ self.rounded = rounded;
+ self
+ }
+
pub fn on_click(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
@@ -190,15 +206,22 @@ impl ThreadItem {
self.tooltip = Some(Box::new(tooltip));
self
}
+
+ pub fn base_bg(mut self, color: Hsla) -> Self {
+ self.base_bg = Some(color);
+ self
+ }
}
impl RenderOnce for ThreadItem {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let color = cx.theme().colors();
- let base_bg = color
+ let sidebar_base_bg = color
.title_bar_background
.blend(color.panel_background.opacity(0.2));
+ let base_bg = self.base_bg.unwrap_or(sidebar_base_bg);
+
let base_bg = if self.selected {
color.element_active
} else {
@@ -335,6 +358,7 @@ impl RenderOnce for ThreadItem {
}
});
+ let has_project_name = self.project_name.is_some();
let has_project_paths = project_paths.is_some();
let has_worktree = !self.worktrees.is_empty();
let has_timestamp = !self.timestamp.is_empty();
@@ -353,6 +377,7 @@ impl RenderOnce for ThreadItem {
.border_1()
.border_color(gpui::transparent_black())
.when(self.focused, |s| s.border_color(color.border_focused))
+ .when(self.rounded, |s| s.rounded_sm())
.hover(|s| s.bg(hover_color))
.on_hover(self.on_hover)
.child(
@@ -393,7 +418,11 @@ impl RenderOnce for ThreadItem {
}),
)
.when(
- has_project_paths || has_worktree || has_diff_stats || has_timestamp,
+ has_project_name
+ || has_project_paths
+ || has_worktree
+ || has_diff_stats
+ || has_timestamp,
|this| {
// Collect all full paths for the shared tooltip.
let worktree_tooltip: SharedString = self
@@ -413,13 +442,16 @@ impl RenderOnce for ThreadItem {
// "olivetti" produce a single chip. Highlight positions
// come from the first occurrence.
let mut seen_names: Vec<SharedString> = Vec::new();
- let mut worktree_chips: Vec<AnyElement> = Vec::new();
+ let mut worktree_labels: Vec<AnyElement> = Vec::new();
+
for wt in self.worktrees {
if seen_names.contains(&wt.name) {
continue;
}
+
let chip_index = seen_names.len();
seen_names.push(wt.name.clone());
+
let label = if wt.highlight_positions.is_empty() {
Label::new(wt.name)
.size(LabelSize::Small)
@@ -433,7 +465,8 @@ impl RenderOnce for ThreadItem {
};
let tooltip_title = worktree_tooltip_title;
let tooltip_meta = worktree_tooltip.clone();
- worktree_chips.push(
+
+ worktree_labels.push(
h_flex()
.id(format!("{}-worktree-{chip_index}", self.id.clone()))
.gap_0p5()
@@ -460,6 +493,15 @@ impl RenderOnce for ThreadItem {
.min_w_0()
.gap_1p5()
.child(icon_container()) // Icon Spacing
+ .when_some(self.project_name, |this, name| {
+ this.child(
+ Label::new(name).size(LabelSize::Small).color(Color::Muted),
+ )
+ })
+ .when(
+ has_project_name && (has_project_paths || has_worktree),
+ |this| this.child(dot_separator()),
+ )
.when_some(project_paths, |this, paths| {
this.child(
Label::new(paths)
@@ -471,16 +513,16 @@ impl RenderOnce for ThreadItem {
.when(has_project_paths && has_worktree, |this| {
this.child(dot_separator())
})
- .children(worktree_chips)
+ .children(worktree_labels)
.when(
- (has_project_paths || has_worktree)
+ (has_project_name || has_project_paths || has_worktree)
&& (has_diff_stats || has_timestamp),
|this| this.child(dot_separator()),
)
.when(has_diff_stats, |this| {
this.child(
DiffStat::new(diff_stat_id, added_count, removed_count)
- .tooltip("Unreviewed changes"),
+ .tooltip("Unreviewed Changes"),
)
})
.when(has_diff_stats && has_timestamp, |this| {