Show agent icon instead of folder icon in sidebar thread list (#49421)

Eric Holk , cameron , and Anthony Eid created

Replace the generic folder icon next to thread entries in the workspace
sidebar with the actual agent icon (Claude, Zed Agent, Gemini, etc.) for
the active thread.

Release Notes:
- N/A

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Change summary

crates/agent_ui/src/agent_panel.rs | 66 ++++++++++++++++++-------------
crates/sidebar/src/sidebar.rs      | 42 ++++++++++++++------
2 files changed, 68 insertions(+), 40 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -14,7 +14,6 @@ use settings::{LanguageModelProviderSetting, LanguageModelSelection};
 
 use zed_actions::agent::{OpenClaudeAgentOnboardingModal, ReauthenticateAgent};
 
-use crate::ManageProfiles;
 use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
 use crate::{
     AddContextServer, AgentDiffPane, CopyThreadToClipboard, Follow, InlineAssistant,
@@ -36,6 +35,7 @@ use crate::{
     ExternalAgent, ExternalAgentInitialContent, NewExternalAgentThread,
     NewNativeAgentThreadFromSummary,
 };
+use crate::{ManageProfiles, acp::thread_view::AcpThreadView};
 use agent_settings::AgentSettings;
 use ai_onboarding::AgentPanelOnboarding;
 use anyhow::{Result, anyhow};
@@ -289,7 +289,7 @@ enum HistoryKind {
 enum ActiveView {
     Uninitialized,
     AgentThread {
-        thread_view: Entity<AcpServerView>,
+        server_view: Entity<AcpServerView>,
     },
     TextThread {
         text_thread_editor: Entity<TextThreadEditor>,
@@ -855,7 +855,7 @@ impl AgentPanel {
 
     pub(crate) fn active_thread_view(&self) -> Option<&Entity<AcpServerView>> {
         match &self.active_view {
-            ActiveView::AgentThread { thread_view, .. } => Some(thread_view),
+            ActiveView::AgentThread { server_view, .. } => Some(server_view),
             ActiveView::Uninitialized
             | ActiveView::TextThread { .. }
             | ActiveView::History { .. }
@@ -1529,9 +1529,21 @@ impl AgentPanel {
         }
     }
 
+    pub fn as_active_server_view(&self) -> Option<&Entity<AcpServerView>> {
+        match &self.active_view {
+            ActiveView::AgentThread { server_view } => Some(server_view),
+            _ => None,
+        }
+    }
+
+    pub fn as_active_thread_view(&self, cx: &App) -> Option<Entity<AcpThreadView>> {
+        let server_view = self.as_active_server_view()?;
+        server_view.read(cx).active_thread().cloned()
+    }
+
     pub fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
         match &self.active_view {
-            ActiveView::AgentThread { thread_view, .. } => thread_view
+            ActiveView::AgentThread { server_view, .. } => server_view
                 .read(cx)
                 .active_thread()
                 .map(|r| r.read(cx).thread.clone()),
@@ -1541,8 +1553,8 @@ impl AgentPanel {
 
     pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
         match &self.active_view {
-            ActiveView::AgentThread { thread_view, .. } => {
-                thread_view.read(cx).as_native_thread(cx)
+            ActiveView::AgentThread { server_view, .. } => {
+                server_view.read(cx).as_native_thread(cx)
             }
             _ => None,
         }
@@ -1592,8 +1604,8 @@ impl AgentPanel {
         }
 
         self._active_view_observation = match &self.active_view {
-            ActiveView::AgentThread { thread_view } => {
-                Some(cx.observe(thread_view, |this, _, cx| {
+            ActiveView::AgentThread { server_view } => {
+                Some(cx.observe(server_view, |this, _, cx| {
                     cx.emit(AgentPanelEvent::ActiveViewChanged);
                     this.serialize(cx);
                     cx.notify();
@@ -1844,7 +1856,7 @@ impl AgentPanel {
             .is_some()
             .then(|| self.thread_store.clone());
 
-        let thread_view = cx.new(|cx| {
+        let server_view = cx.new(|cx| {
             crate::acp::AcpServerView::new(
                 server,
                 resume_thread,
@@ -1859,7 +1871,7 @@ impl AgentPanel {
             )
         });
 
-        self.set_active_view(ActiveView::AgentThread { thread_view }, true, window, cx);
+        self.set_active_view(ActiveView::AgentThread { server_view }, true, window, cx);
     }
 }
 
@@ -1867,7 +1879,7 @@ impl Focusable for AgentPanel {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
         match &self.active_view {
             ActiveView::Uninitialized => self.focus_handle.clone(),
-            ActiveView::AgentThread { thread_view, .. } => thread_view.focus_handle(cx),
+            ActiveView::AgentThread { server_view, .. } => server_view.focus_handle(cx),
             ActiveView::History { kind } => match kind {
                 HistoryKind::AgentThreads => self.acp_history.focus_handle(cx),
                 HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
@@ -1988,13 +2000,13 @@ impl AgentPanel {
         const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
 
         let content = match &self.active_view {
-            ActiveView::AgentThread { thread_view } => {
-                let is_generating_title = thread_view
+            ActiveView::AgentThread { server_view } => {
+                let is_generating_title = server_view
                     .read(cx)
                     .as_native_thread(cx)
                     .map_or(false, |t| t.read(cx).is_generating_title());
 
-                if let Some(title_editor) = thread_view
+                if let Some(title_editor) = server_view
                     .read(cx)
                     .parent_thread(cx)
                     .map(|r| r.read(cx).title_editor.clone())
@@ -2002,7 +2014,7 @@ impl AgentPanel {
                     let container = div()
                         .w_full()
                         .on_action({
-                            let thread_view = thread_view.downgrade();
+                            let thread_view = server_view.downgrade();
                             move |_: &menu::Confirm, window, cx| {
                                 if let Some(thread_view) = thread_view.upgrade() {
                                     thread_view.focus_handle(cx).focus(window, cx);
@@ -2010,7 +2022,7 @@ impl AgentPanel {
                             }
                         })
                         .on_action({
-                            let thread_view = thread_view.downgrade();
+                            let thread_view = server_view.downgrade();
                             move |_: &editor::actions::Cancel, window, cx| {
                                 if let Some(thread_view) = thread_view.upgrade() {
                                     thread_view.focus_handle(cx).focus(window, cx);
@@ -2033,7 +2045,7 @@ impl AgentPanel {
                         container.into_any_element()
                     }
                 } else {
-                    Label::new(thread_view.read(cx).title(cx))
+                    Label::new(server_view.read(cx).title(cx))
                         .color(Color::Muted)
                         .truncate()
                         .into_any_element()
@@ -2171,12 +2183,12 @@ impl AgentPanel {
         };
 
         let thread_view = match &self.active_view {
-            ActiveView::AgentThread { thread_view } => Some(thread_view.clone()),
+            ActiveView::AgentThread { server_view } => Some(server_view.clone()),
             _ => None,
         };
         let thread_with_messages = match &self.active_view {
-            ActiveView::AgentThread { thread_view } => {
-                thread_view.read(cx).has_user_submitted_prompt(cx)
+            ActiveView::AgentThread { server_view } => {
+                server_view.read(cx).has_user_submitted_prompt(cx)
             }
             _ => false,
         };
@@ -2339,7 +2351,7 @@ impl AgentPanel {
             };
 
         let active_thread = match &self.active_view {
-            ActiveView::AgentThread { thread_view } => thread_view.read(cx).as_native_thread(cx),
+            ActiveView::AgentThread { server_view } => server_view.read(cx).as_native_thread(cx),
             ActiveView::Uninitialized
             | ActiveView::TextThread { .. }
             | ActiveView::History { .. }
@@ -2773,8 +2785,8 @@ impl AgentPanel {
             ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
                 false
             }
-            ActiveView::AgentThread { thread_view, .. }
-                if thread_view.read(cx).as_native_thread(cx).is_none() =>
+            ActiveView::AgentThread { server_view, .. }
+                if server_view.read(cx).as_native_thread(cx).is_none() =>
             {
                 false
             }
@@ -3046,8 +3058,8 @@ impl AgentPanel {
         cx: &mut Context<Self>,
     ) {
         match &self.active_view {
-            ActiveView::AgentThread { thread_view } => {
-                thread_view.update(cx, |thread_view, cx| {
+            ActiveView::AgentThread { server_view } => {
+                server_view.update(cx, |thread_view, cx| {
                     thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
                 });
             }
@@ -3165,8 +3177,8 @@ impl Render for AgentPanel {
 
                 match &self.active_view {
                     ActiveView::Uninitialized => parent,
-                    ActiveView::AgentThread { thread_view, .. } => parent
-                        .child(thread_view.clone())
+                    ActiveView::AgentThread { server_view, .. } => parent
+                        .child(server_view.clone())
                         .child(self.render_drag_target(cx)),
                     ActiveView::History { kind } => match kind {
                         HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),

crates/sidebar/src/sidebar.rs 🔗

@@ -37,6 +37,7 @@ pub enum AgentThreadStatus {
 struct AgentThreadInfo {
     title: SharedString,
     status: AgentThreadStatus,
+    icon: IconName,
 }
 
 const LAST_THREAD_TITLES_KEY: &str = "sidebar-last-thread-titles";
@@ -99,6 +100,7 @@ impl WorkspaceThreadEntry {
             Some(AgentThreadInfo {
                 title: SharedString::from(title.clone()),
                 status: AgentThreadStatus::Completed,
+                icon: IconName::ZedAgent,
             })
         });
 
@@ -112,14 +114,23 @@ impl WorkspaceThreadEntry {
 
     fn thread_info(workspace: &Entity<Workspace>, cx: &App) -> Option<AgentThreadInfo> {
         let agent_panel = workspace.read(cx).panel::<AgentPanel>(cx)?;
-        let thread = agent_panel.read(cx).active_agent_thread(cx)?;
-        let thread_ref = thread.read(cx);
-        let title = thread_ref.title();
-        let status = match thread_ref.status() {
+        let agent_panel_ref = agent_panel.read(cx);
+
+        let thread_view = agent_panel_ref.as_active_thread_view(cx)?.read(cx);
+        let thread = thread_view.thread.read(cx);
+
+        let icon = thread_view.agent_icon;
+        let title = thread.title();
+
+        let status = match thread.status() {
             ThreadStatus::Generating => AgentThreadStatus::Running,
             ThreadStatus::Idle => AgentThreadStatus::Completed,
         };
-        Some(AgentThreadInfo { title, status })
+        Some(AgentThreadInfo {
+            title,
+            status,
+            icon,
+        })
     }
 }
 
@@ -594,7 +605,6 @@ impl PickerDelegate for WorkspacePickerDelegate {
                 let workspace_index = thread_entry.index;
                 let multi_workspace = self.multi_workspace.clone();
                 let workspace_count = self.multi_workspace.read(cx).workspaces().len();
-                let is_active = self.active_workspace_index == workspace_index;
                 let is_hovered = self.hovered_thread_item == Some(workspace_index);
 
                 let remove_btn = IconButton::new(
@@ -628,11 +638,11 @@ impl PickerDelegate for WorkspacePickerDelegate {
                         ("workspace-item", thread_entry.index),
                         thread_subtitle.unwrap_or("New Thread".into()),
                     )
-                    .icon(if is_active {
-                        IconName::FolderOpen
-                    } else {
-                        IconName::Folder
-                    })
+                    .icon(
+                        thread_info
+                            .as_ref()
+                            .map_or(IconName::ZedAgent, |info| info.icon),
+                    )
                     .running(running)
                     .generation_done(has_notification)
                     .selected(selected)
@@ -859,8 +869,14 @@ impl Sidebar {
         title: SharedString,
         status: AgentThreadStatus,
     ) {
-        self.test_thread_infos
-            .insert(index, AgentThreadInfo { title, status });
+        self.test_thread_infos.insert(
+            index,
+            AgentThreadInfo {
+                title,
+                status,
+                icon: IconName::ZedAgent,
+            },
+        );
     }
 
     #[cfg(any(test, feature = "test-support"))]