sidebar: Refine archive view (#53975)

Bennet Bo Fenner , Gaauwe Rombouts , and Danilo Leal created

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Gaauwe Rombouts <mail@grombouts.nl>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

assets/icons/knockouts/archive_bg.svg            |  10 +
assets/icons/knockouts/archive_fg.svg            |   4 
assets/keymaps/default-linux.json                |   2 
assets/keymaps/default-macos.json                |   2 
assets/keymaps/default-windows.json              |   2 
crates/agent_ui/src/threads_archive_view.rs      | 121 ++++++++++++++++-
crates/sidebar/src/sidebar.rs                    |  32 ++-
crates/sidebar/src/sidebar_tests.rs              |  32 ++--
crates/ui/src/components/ai/thread_item.rs       |  78 +++++++----
crates/ui/src/components/icon/icon_decoration.rs |   7 
10 files changed, 219 insertions(+), 71 deletions(-)

Detailed changes

assets/icons/knockouts/archive_bg.svg πŸ”—

@@ -0,0 +1,10 @@
+<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4330_2332)">
+<path d="M8.31127 0.918671C9.28885 0.918671 10.0813 1.71115 10.0813 2.68873C10.0813 3.22911 9.83832 3.71194 9.4566 4.03661V7.99891C9.4566 9.149 8.52427 10.0813 7.37418 10.0813H3.62582C2.47573 10.0813 1.5434 9.149 1.5434 7.99891V4.03661C1.16168 3.71194 0.918671 3.22911 0.918671 2.68873C0.918671 1.71115 1.71115 0.918671 2.68873 0.918671H8.31127Z" fill="#C6CAD0"/>
+</g>
+<defs>
+<clipPath id="clip0_4330_2332">
+<rect width="11" height="11" fill="white"/>
+</clipPath>
+</defs>
+</svg>

assets/icons/knockouts/archive_fg.svg πŸ”—

@@ -0,0 +1,4 @@
+<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.37637 1.75164C2.21069 1.75164 2.05179 1.81746 1.93463 1.93462C1.81747 2.05178 1.75165 2.21068 1.75165 2.37637V3.00109C1.75165 3.16678 1.81747 3.32568 1.93463 3.44284C2.05179 3.56 2.21069 3.62582 2.37637 3.62582H8.62364C8.78933 3.62582 8.94823 3.56 9.06539 3.44284C9.18255 3.32568 9.24837 3.16678 9.24837 3.00109V2.37637C9.24837 2.21068 9.18255 2.05178 9.06539 1.93462C8.94823 1.81746 8.78933 1.75164 8.62364 1.75164H2.37637Z" fill="#C6CAD0"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.37634 4.77041H8.62361V7.99891C8.62361 8.33028 8.49197 8.64808 8.25765 8.8824C8.02334 9.11672 7.70553 9.24836 7.37416 9.24836H3.6258C3.29442 9.24836 2.97662 9.11672 2.7423 8.8824C2.50798 8.64808 2.37634 8.33028 2.37634 7.99891L2.37634 4.77041ZM3.80699 6.39988C3.80699 6.23151 3.87388 6.07002 3.99294 5.95096C4.112 5.8319 4.27348 5.76501 4.44186 5.76501H6.55809C6.72647 5.76501 6.88795 5.8319 7.00701 5.95096C7.12607 6.07002 7.19296 6.23151 7.19296 6.39988C7.19296 6.56826 7.12607 6.72974 7.00701 6.8488C6.88795 6.96786 6.72647 7.03475 6.55809 7.03475H4.44186C4.27348 7.03475 4.112 6.96786 3.99294 6.8488C3.87388 6.72974 3.80699 6.56826 3.80699 6.39988Z" fill="#C6CAD0"/>
+</svg>

assets/keymaps/default-linux.json πŸ”—

@@ -720,7 +720,7 @@
       "right": "menu::SelectChild",
       "enter": "menu::Confirm",
       "ctrl-f": "agents_sidebar::FocusSidebarFilter",
-      "ctrl-g": "agents_sidebar::ToggleArchive",
+      "ctrl-g": "agents_sidebar::ViewAllThreads",
       "shift-backspace": "agent::RemoveSelectedThread",
       "ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
       "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],

assets/keymaps/default-macos.json πŸ”—

@@ -783,7 +783,7 @@
       "right": "menu::SelectChild",
       "enter": "menu::Confirm",
       "cmd-f": "agents_sidebar::FocusSidebarFilter",
-      "cmd-g": "agents_sidebar::ToggleArchive",
+      "cmd-g": "agents_sidebar::ViewAllThreads",
       "shift-backspace": "agent::RemoveSelectedThread",
       "ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
       "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],

assets/keymaps/default-windows.json πŸ”—

@@ -720,7 +720,7 @@
       "right": "menu::SelectChild",
       "enter": "menu::Confirm",
       "ctrl-f": "agents_sidebar::FocusSidebarFilter",
-      "ctrl-g": "agents_sidebar::ToggleArchive",
+      "ctrl-g": "agents_sidebar::ViewAllThreads",
       "shift-backspace": "agent::RemoveSelectedThread",
       "ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
       "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],

crates/agent_ui/src/threads_archive_view.rs πŸ”—

@@ -30,7 +30,7 @@ use picker::{
 use project::{AgentId, AgentServerStore};
 use settings::Settings as _;
 use theme::ActiveTheme;
-use ui::{AgentThreadStatus, ThreadItem};
+use ui::{AgentThreadStatus, IconDecoration, IconDecorationKind, Tab, ThreadItem};
 use ui::{
     Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tooltip, WithScrollbar,
     prelude::*, utils::platform_title_bar_height,
@@ -116,7 +116,7 @@ fn fuzzy_match_positions(query: &str, text: &str) -> Option<Vec<usize>> {
 
 pub enum ThreadsArchiveViewEvent {
     Close,
-    Unarchive { thread: ThreadMetadata },
+    Activate { thread: ThreadMetadata },
     CancelRestore { thread_id: ThreadId },
 }
 
@@ -140,6 +140,7 @@ pub struct ThreadsArchiveView {
     archived_thread_ids: HashSet<ThreadId>,
     archived_branch_names: HashMap<ThreadId, HashMap<PathBuf, String>>,
     _load_branch_names_task: Task<()>,
+    show_archived_only: bool,
 }
 
 impl ThreadsArchiveView {
@@ -154,7 +155,7 @@ impl ThreadsArchiveView {
 
         let filter_editor = cx.new(|cx| {
             let mut editor = Editor::single_line(window, cx);
-            editor.set_placeholder_text("Search archive…", window, cx);
+            editor.set_placeholder_text("Search threads…", window, cx);
             editor
         });
 
@@ -213,6 +214,7 @@ impl ThreadsArchiveView {
             archived_thread_ids: HashSet::default(),
             archived_branch_names: HashMap::default(),
             _load_branch_names_task: Task::ready(()),
+            show_archived_only: false,
         };
 
         this.update_items(cx);
@@ -251,9 +253,11 @@ impl ThreadsArchiveView {
     }
 
     fn update_items(&mut self, cx: &mut Context<Self>) {
+        let show_archived_only = self.show_archived_only;
         let sessions = ThreadMetadataStore::global(cx)
             .read(cx)
-            .archived_entries()
+            .entries()
+            .filter(|t| !show_archived_only || t.archived)
             .sorted_by_cached_key(|t| t.created_at.unwrap_or(t.updated_at))
             .rev()
             .cloned()
@@ -380,6 +384,11 @@ impl ThreadsArchiveView {
         });
     }
 
+    fn archive_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
+        self.preserve_selection_on_next_update = true;
+        ThreadMetadataStore::global(cx).update(cx, |store, cx| store.archive(thread_id, None, cx));
+    }
+
     fn unarchive_thread(
         &mut self,
         thread: ThreadMetadata,
@@ -398,7 +407,7 @@ impl ThreadsArchiveView {
         self.mark_restoring(&thread.thread_id, cx);
         self.selection = None;
         self.reset_filter_editor_text(window, cx);
-        cx.emit(ThreadsArchiveViewEvent::Unarchive { thread });
+        cx.emit(ThreadsArchiveViewEvent::Activate { thread });
     }
 
     fn show_project_picker_for_thread(
@@ -580,6 +589,8 @@ impl ThreadsArchiveView {
 
                 let is_restoring = self.restoring.contains(&thread.thread_id);
 
+                let is_archived = thread.archived;
+
                 let branch_names_for_thread: HashMap<PathBuf, SharedString> = self
                     .archived_branch_names
                     .get(&thread.thread_id)
@@ -589,18 +600,37 @@ impl ThreadsArchiveView {
                             .collect()
                     })
                     .unwrap_or_default();
+
                 let worktrees = worktree_info_from_thread_paths(
                     &thread.worktree_paths,
                     &branch_names_for_thread,
                 );
 
+                let color = cx.theme().colors();
+                let knockout_color = color
+                    .title_bar_background
+                    .blend(color.panel_background.opacity(0.25));
+                let archived_decoration =
+                    IconDecoration::new(IconDecorationKind::Archive, knockout_color, cx)
+                        .color(color.icon_disabled)
+                        .position(gpui::Point {
+                            x: px(-3.),
+                            y: px(-3.5),
+                        });
+
                 let base = ThreadItem::new(id, thread.display_title())
                     .icon(icon)
+                    .when(is_archived, |this| {
+                        this.icon_color(Color::Muted)
+                            .title_label_color(Color::Muted)
+                            .icon_decoration(archived_decoration)
+                    })
                     .when_some(icon_from_external_svg, |this, svg| {
                         this.custom_icon_from_external_svg(svg)
                     })
                     .timestamp(timestamp)
                     .highlight_positions(highlight_positions.clone())
+                    .project_paths(thread.folder_paths().paths_owned())
                     .worktrees(worktrees)
                     .focused(is_focused)
                     .hovered(is_hovered)
@@ -633,7 +663,7 @@ impl ThreadsArchiveView {
                         )
                         .tooltip(Tooltip::text("Restoring…"))
                         .into_any_element()
-                } else {
+                } else if is_archived {
                     base.action_slot(
                         IconButton::new("delete-thread", IconName::Trash)
                             .icon_size(IconSize::Small)
@@ -664,7 +694,31 @@ impl ThreadsArchiveView {
                                 })
                             }),
                     )
-                    .tooltip(move |_, cx| Tooltip::for_action("Restore Thread", &menu::Confirm, cx))
+                    .tooltip(move |_, cx| {
+                        Tooltip::for_action("Open Archived Thread", &menu::Confirm, cx)
+                    })
+                    .on_click({
+                        let thread = thread.clone();
+                        cx.listener(move |this, _, window, cx| {
+                            this.unarchive_thread(thread.clone(), window, cx);
+                        })
+                    })
+                    .into_any_element()
+                } else {
+                    base.action_slot(
+                        IconButton::new("archive-thread", IconName::Archive)
+                            .icon_size(IconSize::Small)
+                            .icon_color(Color::Muted)
+                            .tooltip(Tooltip::text("Archive Thread"))
+                            .on_click({
+                                let thread_id = thread.thread_id;
+                                cx.listener(move |this, _, _, cx| {
+                                    this.archive_thread(thread_id, cx);
+                                    cx.stop_propagation();
+                                })
+                            }),
+                    )
+                    .tooltip(move |_, cx| Tooltip::for_action("Open Thread", &menu::Confirm, cx))
                     .on_click({
                         let thread = thread.clone();
                         cx.listener(move |this, _, window, cx| {
@@ -807,6 +861,54 @@ impl ThreadsArchiveView {
                 )
             })
     }
+
+    fn render_toolbar(&self, cx: &mut Context<Self>) -> impl IntoElement {
+        let entry_count = self
+            .items
+            .iter()
+            .filter(|item| matches!(item, ArchiveListItem::Entry { .. }))
+            .count();
+
+        let count_label = if entry_count == 1 {
+            if self.show_archived_only {
+                "1 archived thread".to_string()
+            } else {
+                "1 thread".to_string()
+            }
+        } else if self.show_archived_only {
+            format!("{} archived threads", entry_count)
+        } else {
+            format!("{} threads", entry_count)
+        };
+
+        h_flex()
+            .mt_px()
+            .pl_2p5()
+            .pr_1p5()
+            .h(Tab::content_height(cx))
+            .justify_between()
+            .border_b_1()
+            .border_color(cx.theme().colors().border)
+            .child(
+                Label::new(count_label)
+                    .size(LabelSize::Small)
+                    .color(Color::Muted),
+            )
+            .child(
+                IconButton::new("toggle-archived-only", IconName::ListFilter)
+                    .icon_size(IconSize::Small)
+                    .toggle_state(self.show_archived_only)
+                    .tooltip(Tooltip::text(if self.show_archived_only {
+                        "Show All Threads"
+                    } else {
+                        "Show Archived Only"
+                    }))
+                    .on_click(cx.listener(|this, _, _, cx| {
+                        this.show_archived_only = !this.show_archived_only;
+                        this.update_items(cx);
+                    })),
+            )
+    }
 }
 
 pub fn format_history_entry_timestamp(entry_time: DateTime<Utc>) -> String {
@@ -847,7 +949,7 @@ impl Render for ThreadsArchiveView {
             let message = if has_query {
                 "No threads match your search."
             } else {
-                "No archived or hidden threads yet."
+                "No threads yet."
             };
 
             v_flex()
@@ -889,6 +991,7 @@ impl Render for ThreadsArchiveView {
             .on_action(cx.listener(Self::remove_selected_thread))
             .size_full()
             .child(self.render_header(window, cx))
+            .when(!has_query, |this| this.child(self.render_toolbar(cx)))
             .child(content)
     }
 }
@@ -1025,7 +1128,7 @@ impl ProjectPickerDelegate {
             .update(cx, |view, cx| {
                 view.selection = None;
                 view.reset_filter_editor_text(window, cx);
-                cx.emit(ThreadsArchiveViewEvent::Unarchive {
+                cx.emit(ThreadsArchiveViewEvent::Activate {
                     thread: self.thread.clone(),
                 });
             })

crates/sidebar/src/sidebar.rs πŸ”—

@@ -68,7 +68,7 @@ gpui::actions!(
         /// Creates a new thread in the currently selected or active project group.
         NewThreadInGroup,
         /// Toggles between the thread list and the archive view.
-        ToggleArchive,
+        ViewAllThreads,
     ]
 );
 
@@ -2490,7 +2490,7 @@ impl Sidebar {
         })
     }
 
-    fn activate_archived_thread(
+    fn open_thread_from_archive(
         &mut self,
         metadata: ThreadMetadata,
         window: &mut Window,
@@ -2533,9 +2533,13 @@ impl Sidebar {
         }
 
         let store = ThreadMetadataStore::global(cx);
-        let task = store
-            .read(cx)
-            .get_archived_worktrees_for_thread(thread_id, cx);
+        let task = if metadata.archived {
+            store
+                .read(cx)
+                .get_archived_worktrees_for_thread(thread_id, cx)
+        } else {
+            Task::ready(Ok(Vec::new()))
+        };
         let path_list = metadata.folder_paths().clone();
 
         let restore_task = cx.spawn_in(window, async move |this, cx| {
@@ -2545,8 +2549,10 @@ impl Sidebar {
                 if archived_worktrees.is_empty() {
                     this.update_in(cx, |this, window, cx| {
                         this.restoring_tasks.remove(&thread_id);
-                        ThreadMetadataStore::global(cx)
-                            .update(cx, |store, cx| store.unarchive(thread_id, cx));
+                        if metadata.archived {
+                            ThreadMetadataStore::global(cx)
+                                .update(cx, |store, cx| store.unarchive(thread_id, cx));
+                        }
 
                         if let Some(workspace) = this.find_current_workspace_for_path_list(
                             &path_list,
@@ -4389,7 +4395,7 @@ impl Sidebar {
                 this.child(
                     IconButton::new("thread-import", IconName::ThreadImport)
                         .icon_size(IconSize::Small)
-                        .tooltip(Tooltip::text("Import ACP Threads"))
+                        .tooltip(Tooltip::text("Import External Agent Threads"))
                         .on_click(cx.listener(|this, _, window, cx| {
                             this.show_archive(window, cx);
                             this.show_thread_import_modal(window, cx);
@@ -4401,10 +4407,10 @@ impl Sidebar {
                     .icon_size(IconSize::Small)
                     .toggle_state(is_archive)
                     .tooltip(move |_, cx| {
-                        Tooltip::for_action("Toggle Archived Threads", &ToggleArchive, cx)
+                        Tooltip::for_action("View All Threads", &ViewAllThreads, cx)
                     })
                     .on_click(cx.listener(|this, _, window, cx| {
-                        this.toggle_archive(&ToggleArchive, window, cx);
+                        this.toggle_archive(&ViewAllThreads, window, cx);
                     })),
             )
             .child(self.render_recent_projects_button(cx));
@@ -4522,7 +4528,7 @@ impl Sidebar {
             )
     }
 
-    fn toggle_archive(&mut self, _: &ToggleArchive, window: &mut Window, cx: &mut Context<Self>) {
+    fn toggle_archive(&mut self, _: &ViewAllThreads, window: &mut Window, cx: &mut Context<Self>) {
         match &self.view {
             SidebarView::ThreadList => {
                 let side = match self.side(cx) {
@@ -4574,8 +4580,8 @@ impl Sidebar {
                 ThreadsArchiveViewEvent::Close => {
                     this.show_thread_list(window, cx);
                 }
-                ThreadsArchiveViewEvent::Unarchive { thread } => {
-                    this.activate_archived_thread(thread.clone(), window, cx);
+                ThreadsArchiveViewEvent::Activate { thread } => {
+                    this.open_thread_from_archive(thread.clone(), window, cx);
                 }
                 ThreadsArchiveViewEvent::CancelRestore { thread_id } => {
                     this.restoring_tasks.remove(thread_id);

crates/sidebar/src/sidebar_tests.rs πŸ”—

@@ -4114,7 +4114,7 @@ async fn test_activate_archived_thread_with_saved_paths_activates_matching_works
     // Call activate_archived_thread – should resolve saved paths and
     // switch to the workspace for project-b.
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(
+        sidebar.open_thread_from_archive(
             ThreadMetadata {
                 thread_id: ThreadId::new(),
                 session_id: Some(session_id.clone()),
@@ -4182,7 +4182,7 @@ async fn test_activate_archived_thread_cwd_fallback_with_matching_workspace(
 
     // No thread saved to the store – cwd is the only path hint.
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(
+        sidebar.open_thread_from_archive(
             ThreadMetadata {
                 thread_id: ThreadId::new(),
                 session_id: Some(acp::SessionId::new(Arc::from("unknown-session"))),
@@ -4248,7 +4248,7 @@ async fn test_activate_archived_thread_no_paths_no_cwd_uses_active_workspace(
 
     // No saved thread, no cwd – should fall back to the active workspace.
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(
+        sidebar.open_thread_from_archive(
             ThreadMetadata {
                 thread_id: ThreadId::new(),
                 session_id: Some(acp::SessionId::new(Arc::from("no-context-session"))),
@@ -4304,7 +4304,7 @@ async fn test_activate_archived_thread_saved_paths_opens_new_workspace(cx: &mut
     );
 
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(
+        sidebar.open_thread_from_archive(
             ThreadMetadata {
                 thread_id: ThreadId::new(),
                 session_id: Some(session_id.clone()),
@@ -4359,7 +4359,7 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window(cx: &m
     let session_id = acp::SessionId::new(Arc::from("archived-cross-window"));
 
     sidebar.update_in(cx_a, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(
+        sidebar.open_thread_from_archive(
             ThreadMetadata {
                 thread_id: ThreadId::new(),
                 session_id: Some(session_id.clone()),
@@ -4439,7 +4439,7 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window_with_t
     let session_id = acp::SessionId::new(Arc::from("archived-cross-window-with-sidebar"));
 
     sidebar_a.update_in(cx_a, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(
+        sidebar.open_thread_from_archive(
             ThreadMetadata {
                 thread_id: ThreadId::new(),
                 session_id: Some(session_id.clone()),
@@ -4522,7 +4522,7 @@ async fn test_activate_archived_thread_prefers_current_window_for_matching_paths
     let session_id = acp::SessionId::new(Arc::from("archived-current-window"));
 
     sidebar_a.update_in(cx_a, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(
+        sidebar.open_thread_from_archive(
             ThreadMetadata {
                 thread_id: ThreadId::new(),
                 session_id: Some(session_id.clone()),
@@ -5330,7 +5330,7 @@ async fn test_restore_worktree_thread_uses_main_repo_project_group_key(cx: &mut
     // provisional ProjectGroupKey to find a matching workspace.
     let metadata = cx.update(|_window, cx| store.read(cx).entry(thread_id).unwrap().clone());
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(metadata, window, cx);
+        sidebar.open_thread_from_archive(metadata, window, cx);
     });
     cx.run_until_parked();
 
@@ -6151,7 +6151,7 @@ async fn test_unarchive_only_shows_restored_thread(cx: &mut TestAppContext) {
 
     // Unarchive it β€” the draft should be replaced by the restored thread.
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(metadata, window, cx);
+        sidebar.open_thread_from_archive(metadata, window, cx);
     });
     cx.run_until_parked();
 
@@ -6241,7 +6241,7 @@ async fn test_unarchive_first_thread_in_group_does_not_create_spurious_draft(
     });
 
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(metadata, window, cx);
+        sidebar.open_thread_from_archive(metadata, window, cx);
     });
     cx.run_until_parked();
 
@@ -6325,7 +6325,7 @@ async fn test_unarchive_into_new_workspace_does_not_create_duplicate_real_thread
     });
 
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(metadata, window, cx);
+        sidebar.open_thread_from_archive(metadata, window, cx);
     });
 
     cx.run_until_parked();
@@ -6467,7 +6467,7 @@ async fn test_unarchive_into_existing_workspace_replaces_draft(cx: &mut TestAppC
     });
 
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(metadata, window, cx);
+        sidebar.open_thread_from_archive(metadata, window, cx);
     });
     cx.run_until_parked();
 
@@ -6552,7 +6552,7 @@ async fn test_unarchive_into_inactive_existing_workspace_does_not_leave_active_d
     });
 
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(metadata, window, cx);
+        sidebar.open_thread_from_archive(metadata, window, cx);
     });
 
     let panel_b_before_settle = workspace_b.read_with(cx, |workspace, cx| {
@@ -6693,7 +6693,7 @@ async fn test_unarchive_after_removing_parent_project_group_restores_real_thread
     );
 
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(archived_metadata.clone(), window, cx);
+        sidebar.open_thread_from_archive(archived_metadata.clone(), window, cx);
     });
     cx.run_until_parked();
 
@@ -6805,7 +6805,7 @@ async fn test_unarchive_does_not_create_duplicate_real_thread_metadata(cx: &mut
     });
 
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(metadata, window, cx);
+        sidebar.open_thread_from_archive(metadata, window, cx);
     });
     cx.run_until_parked();
 
@@ -7396,7 +7396,7 @@ async fn test_unarchive_linked_worktree_thread_into_project_group_shows_only_res
     });
 
     sidebar.update_in(cx, |sidebar, window, cx| {
-        sidebar.activate_archived_thread(metadata, window, cx);
+        sidebar.open_thread_from_archive(metadata, window, cx);
     });
     cx.run_until_parked();
 

crates/ui/src/components/ai/thread_item.rs πŸ”—

@@ -1,4 +1,7 @@
-use crate::{CommonAnimationExt, DiffStat, GradientFade, HighlightedLabel, Tooltip, prelude::*};
+use crate::{
+    CommonAnimationExt, DecoratedIcon, DiffStat, GradientFade, HighlightedLabel, IconDecoration,
+    Tooltip, prelude::*,
+};
 
 use gpui::{
     Animation, AnimationExt, AnyView, ClickEvent, Hsla, MouseButton, SharedString,
@@ -39,6 +42,7 @@ pub struct ThreadItem {
     icon_color: Option<Color>,
     icon_visible: bool,
     custom_icon_from_external_svg: Option<SharedString>,
+    icon_decoration: Option<IconDecoration>,
     title: SharedString,
     title_label_color: Option<Color>,
     title_generating: bool,
@@ -71,6 +75,7 @@ impl ThreadItem {
             icon_color: None,
             icon_visible: true,
             custom_icon_from_external_svg: None,
+            icon_decoration: None,
             title: title.into(),
             title_label_color: None,
             title_generating: false,
@@ -117,6 +122,11 @@ impl ThreadItem {
         self
     }
 
+    pub fn icon_decoration(mut self, decoration: IconDecoration) -> Self {
+        self.icon_decoration = Some(decoration);
+        self
+    }
+
     pub fn custom_icon_from_external_svg(mut self, svg: impl Into<SharedString>) -> Self {
         self.custom_icon_from_external_svg = Some(svg.into());
         self
@@ -326,6 +336,10 @@ impl RenderOnce for ThreadItem {
                     icon.tooltip(Tooltip::text(tooltip))
                 })
                 .into_any_element()
+        } else if let Some(decoration) = self.icon_decoration {
+            icon_container()
+                .child(DecoratedIcon::new(agent_icon, Some(decoration)))
+                .into_any_element()
         } else {
             icon_container().child(agent_icon).into_any_element()
         };
@@ -553,35 +567,41 @@ impl RenderOnce for ThreadItem {
                             .min_w_0()
                             .gap_1p5()
                             .child(icon_container()) // Icon Spacing
-                            .child(
-                                h_flex()
-                                    .min_w_0()
-                                    .flex_shrink()
-                                    .overflow_hidden()
-                                    .gap_1p5()
-                                    .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(
+                                has_project_name || has_project_paths || has_worktree,
+                                |this| {
+                                    this.child(
+                                        h_flex()
+                                            .min_w_0()
+                                            .flex_shrink()
+                                            .overflow_hidden()
+                                            .gap_1p5()
+                                            .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)
+                                                        .size(LabelSize::Small)
+                                                        .color(Color::Muted)
+                                                        .into_any_element(),
+                                                )
+                                            })
+                                            .when(has_project_paths && has_worktree, |this| {
+                                                this.child(dot_separator())
+                                            })
+                                            .children(worktree_labels),
                                     )
-                                    .when_some(project_paths, |this, paths| {
-                                        this.child(
-                                            Label::new(paths)
-                                                .size(LabelSize::Small)
-                                                .color(Color::Muted)
-                                                .into_any_element(),
-                                        )
-                                    })
-                                    .when(has_project_paths && has_worktree, |this| {
-                                        this.child(dot_separator())
-                                    })
-                                    .children(worktree_labels),
+                                },
                             )
                             .when(
                                 (has_project_name || has_project_paths || has_worktree)

crates/ui/src/components/icon/icon_decoration.rs πŸ”—

@@ -18,6 +18,8 @@ pub enum KnockoutIconName {
     DotBg,
     TriangleFg,
     TriangleBg,
+    ArchiveFg,
+    ArchiveBg,
 }
 
 impl KnockoutIconName {
@@ -33,6 +35,7 @@ pub enum IconDecorationKind {
     X,
     Dot,
     Triangle,
+    Archive,
 }
 
 impl IconDecorationKind {
@@ -41,6 +44,7 @@ impl IconDecorationKind {
             Self::X => KnockoutIconName::XFg,
             Self::Dot => KnockoutIconName::DotFg,
             Self::Triangle => KnockoutIconName::TriangleFg,
+            Self::Archive => KnockoutIconName::ArchiveFg,
         }
     }
 
@@ -49,6 +53,7 @@ impl IconDecorationKind {
             Self::X => KnockoutIconName::XBg,
             Self::Dot => KnockoutIconName::DotBg,
             Self::Triangle => KnockoutIconName::TriangleBg,
+            Self::Archive => KnockoutIconName::ArchiveBg,
         }
     }
 }
@@ -163,7 +168,7 @@ impl RenderOnce for IconDecoration {
             .absolute()
             .bottom(self.position.y)
             .right(self.position.x)
-            .child(foreground)
             .child(background)
+            .child(foreground)
     }
 }