sidebar: Add some design tweaks (#53026)

Danilo Leal created

- Make notification icons show up even for threads of the currently
active workspace
- When with a notification/any other status, replace thread item's agent
icon a status icon for higher visbility
- Remove hover state from currently active project/workspace's header
- Make project/workspace label brighter if I'm inside of it
- Adjust colors all around a bit (sidebar background and border, and
icons within the project header)

Release Notes:

- N/A

Change summary

crates/sidebar/src/sidebar.rs              | 40 +++++++--------
crates/ui/src/components/ai/thread_item.rs | 62 ++++++++++-------------
2 files changed, 46 insertions(+), 56 deletions(-)

Detailed changes

crates/sidebar/src/sidebar.rs 🔗

@@ -974,21 +974,21 @@ impl Sidebar {
 
                     let session_id = &thread.metadata.session_id;
 
-                    let is_thread_workspace_active = match &thread.workspace {
-                        ThreadEntryWorkspace::Open(thread_workspace) => active_workspace
-                            .as_ref()
-                            .is_some_and(|active| active == thread_workspace),
-                        ThreadEntryWorkspace::Closed(_) => false,
-                    };
+                    let is_active_thread = self.active_entry.as_ref().is_some_and(|entry| {
+                        entry.is_active_thread(session_id)
+                            && active_workspace
+                                .as_ref()
+                                .is_some_and(|active| active == entry.workspace())
+                    });
 
                     if thread.status == AgentThreadStatus::Completed
-                        && !is_thread_workspace_active
+                        && !is_active_thread
                         && old_statuses.get(session_id) == Some(&AgentThreadStatus::Running)
                     {
                         notified_threads.insert(session_id.clone());
                     }
 
-                    if is_thread_workspace_active && !thread.is_background {
+                    if is_active_thread && !thread.is_background {
                         notified_threads.remove(session_id);
                     }
                 }
@@ -1280,7 +1280,7 @@ impl Sidebar {
             v_flex()
                 .w_full()
                 .border_t_1()
-                .border_color(cx.theme().colors().border.opacity(0.5))
+                .border_color(cx.theme().colors().border)
                 .child(rendered)
                 .into_any_element()
         } else {
@@ -1327,7 +1327,7 @@ impl Sidebar {
         has_running_threads: bool,
         waiting_thread_count: usize,
         is_active: bool,
-        is_selected: bool,
+        is_focused: bool,
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let id_prefix = if is_sticky { "sticky-" } else { "" };
@@ -1359,11 +1359,11 @@ impl Sidebar {
 
         let label = if highlight_positions.is_empty() {
             Label::new(label.clone())
-                .color(Color::Muted)
+                .when(!is_active, |this| this.color(Color::Muted))
                 .into_any_element()
         } else {
             HighlightedLabel::new(label.clone(), highlight_positions.to_vec())
-                .color(Color::Muted)
+                .when(!is_active, |this| this.color(Color::Muted))
                 .into_any_element()
         };
 
@@ -1381,14 +1381,13 @@ impl Sidebar {
             .pr_1p5()
             .border_1()
             .map(|this| {
-                if is_selected {
+                if is_focused {
                     this.border_color(color.border_focused)
                 } else {
                     this.border_color(gpui::transparent_black())
                 }
             })
             .justify_between()
-            .hover(|s| s.bg(hover_color))
             .child(
                 h_flex()
                     .when(!is_active, |this| this.cursor_pointer())
@@ -1469,7 +1468,6 @@ impl Sidebar {
                                 IconName::ListCollapse,
                             )
                             .icon_size(IconSize::Small)
-                            .icon_color(Color::Muted)
                             .tooltip(Tooltip::text("Collapse Displayed Threads"))
                             .on_click(cx.listener({
                                 let path_list_for_collapse = path_list_for_collapse.clone();
@@ -1491,7 +1489,6 @@ impl Sidebar {
                                 IconName::Plus,
                             )
                             .icon_size(IconSize::Small)
-                            .icon_color(Color::Muted)
                             .tooltip(Tooltip::text("New Thread"))
                             .on_click(cx.listener({
                                 let workspace_for_new_thread = workspace_for_new_thread.clone();
@@ -1508,7 +1505,9 @@ impl Sidebar {
                     })
             })
             .when(!is_active, |this| {
-                this.tooltip(Tooltip::text("Activate Workspace"))
+                this.cursor_pointer()
+                    .hover(|s| s.bg(hover_color))
+                    .tooltip(Tooltip::text("Activate Workspace"))
                     .on_click(cx.listener({
                         move |this, _, window, cx| {
                             this.active_entry =
@@ -1690,8 +1689,7 @@ impl Sidebar {
                     IconName::Ellipsis,
                 )
                 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                .icon_size(IconSize::Small)
-                .icon_color(Color::Muted),
+                .icon_size(IconSize::Small),
             )
             .anchor(gpui::Corner::TopRight)
             .offset(gpui::Point {
@@ -2825,7 +2823,7 @@ impl Sidebar {
         let color = cx.theme().colors();
         let sidebar_bg = color
             .title_bar_background
-            .blend(color.panel_background.opacity(0.32));
+            .blend(color.panel_background.opacity(0.25));
 
         let timestamp = format_history_entry_timestamp(
             self.thread_last_message_sent_or_queued
@@ -3682,7 +3680,7 @@ impl Render for Sidebar {
         let color = cx.theme().colors();
         let bg = color
             .title_bar_background
-            .blend(color.panel_background.opacity(0.32));
+            .blend(color.panel_background.opacity(0.25));
 
         let no_open_projects = !self.contents.has_open_projects;
         let no_search_results = self.contents.entries.is_empty();

crates/ui/src/components/ai/thread_item.rs 🔗

@@ -1,7 +1,4 @@
-use crate::{
-    CommonAnimationExt, DecoratedIcon, DiffStat, GradientFade, HighlightedLabel, IconDecoration,
-    IconDecorationKind, Tooltip, prelude::*,
-};
+use crate::{CommonAnimationExt, DiffStat, GradientFade, HighlightedLabel, Tooltip, prelude::*};
 
 use gpui::{
     Animation, AnimationExt, AnyView, ClickEvent, Hsla, MouseButton, SharedString,
@@ -218,7 +215,7 @@ impl RenderOnce for ThreadItem {
         let color = cx.theme().colors();
         let sidebar_base_bg = color
             .title_bar_background
-            .blend(color.panel_background.opacity(0.32));
+            .blend(color.panel_background.opacity(0.25));
 
         let raw_bg = self.base_bg.unwrap_or(sidebar_base_bg);
         let apparent_bg = color.background.blend(raw_bg);
@@ -266,31 +263,31 @@ impl RenderOnce for ThreadItem {
             Icon::new(self.icon).color(icon_color).size(IconSize::Small)
         };
 
-        let decoration = |icon: IconDecorationKind, color: Hsla| {
-            IconDecoration::new(icon, base_bg, cx)
-                .color(color)
-                .position(gpui::Point {
-                    x: px(-2.),
-                    y: px(-2.),
-                })
-        };
-
-        let (decoration, icon_tooltip) = if self.status == AgentThreadStatus::Error {
+        let (status_icon, icon_tooltip) = if self.status == AgentThreadStatus::Error {
             (
-                Some(decoration(IconDecorationKind::X, cx.theme().status().error)),
+                Some(
+                    Icon::new(IconName::Close)
+                        .size(IconSize::Small)
+                        .color(Color::Error),
+                ),
                 Some("Thread has an Error"),
             )
         } else if self.status == AgentThreadStatus::WaitingForConfirmation {
             (
-                Some(decoration(
-                    IconDecorationKind::Triangle,
-                    cx.theme().status().warning,
-                )),
+                Some(
+                    Icon::new(IconName::Warning)
+                        .size(IconSize::XSmall)
+                        .color(Color::Warning),
+                ),
                 Some("Thread is Waiting for Confirmation"),
             )
         } else if self.notified {
             (
-                Some(decoration(IconDecorationKind::Dot, color.text_accent)),
+                Some(
+                    Icon::new(IconName::Circle)
+                        .size(IconSize::Small)
+                        .color(Color::Accent),
+                ),
                 Some("Thread's Generation is Complete"),
             )
         } else {
@@ -306,9 +303,9 @@ impl RenderOnce for ThreadItem {
                         .with_rotate_animation(2),
                 )
                 .into_any_element()
-        } else if let Some(decoration) = decoration {
+        } else if let Some(status_icon) = status_icon {
             icon_container()
-                .child(DecoratedIcon::new(agent_icon, Some(decoration)))
+                .child(status_icon)
                 .when_some(icon_tooltip, |icon, tooltip| {
                     icon.tooltip(Tooltip::text(tooltip))
                 })
@@ -551,12 +548,17 @@ impl Component for ThreadItem {
     }
 
     fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+        let color = cx.theme().colors();
+        let bg = color
+            .title_bar_background
+            .blend(color.panel_background.opacity(0.25));
+
         let container = || {
             v_flex()
                 .w_72()
                 .border_1()
-                .border_color(cx.theme().colors().border_variant)
-                .bg(cx.theme().colors().panel_background)
+                .border_color(color.border_variant)
+                .bg(bg)
         };
 
         let thread_item_examples = vec![
@@ -570,16 +572,6 @@ impl Component for ThreadItem {
                     )
                     .into_any_element(),
             ),
-            single_example(
-                "Timestamp Only (hours)",
-                container()
-                    .child(
-                        ThreadItem::new("ti-1b", "Thread with just a timestamp")
-                            .icon(IconName::AiClaude)
-                            .timestamp("3h"),
-                    )
-                    .into_any_element(),
-            ),
             single_example(
                 "Notified (weeks)",
                 container()