use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore};use crate::threads_archive_view::{ThreadsArchiveView, ThreadsArchiveViewEvent};use crate::{Agent, AgentPanel, AgentPanelEvent, NewThread, RemoveSelectedThread};use acp_thread::ThreadStatus;use action_log::DiffStats;use agent::ThreadStore;use agent_client_protocol::{self as acp};use agent_settings::AgentSettings;use chrono::Utc;use db::kvp::KEY_VALUE_STORE;use editor::Editor;use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _};use gpui::{ Action as _, AnyElement, App, Context, Entity, FocusHandle, Focusable, ListState, Pixels, Render, SharedString, WeakEntity, Window, actions, list, prelude::*, px,};use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};use project::{AgentId, Event as ProjectEvent};use settings::Settings;use std::collections::{HashMap, HashSet};use std::mem;use std::path::Path;use std::sync::Arc;use theme::ActiveTheme;use ui::{ AgentThreadStatus, ButtonStyle, HighlightedLabel, IconButtonShape, ListItem, Tab, ThreadItem, Tooltip, WithScrollbar, prelude::*,};use util::ResultExt as _;use util::path_list::PathList;use workspace::{ MultiWorkspace, MultiWorkspaceEvent, ToggleWorkspaceSidebar, Workspace, multi_workspace_enabled,};use zed_actions::editor::{MoveDown, MoveUp}; actions!( agents_sidebar, [ /// Collapses the selected entry in the workspace sidebar. CollapseSelectedEntry, /// Expands the selected entry in the workspace sidebar. ExpandSelectedEntry, ]); const DEFAULT_WIDTH: Pixels = px(320.0);const MIN_WIDTH: Pixels = px(200.0);const MAX_WIDTH: Pixels = px(800.0);const DEFAULT_THREADS_SHOWN: usize = 5;const SIDEBAR_STATE_KEY: &str = "sidebar_state"; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]enum SidebarView { #[default] ThreadList, Archive,} fn read_sidebar_open_state(multi_workspace_id: u64) -> bool { KEY_VALUE_STORE .scoped(SIDEBAR_STATE_KEY) .read(&multi_workspace_id.to_string()) .log_err() .flatten() .and_then(|json| serde_json::from_str::<bool>(&json).ok()) .unwrap_or(false)} async fn save_sidebar_open_state(multi_workspace_id: u64, is_open: bool) { if let Ok(json) = serde_json::to_string(&is_open) { KEY_VALUE_STORE .scoped(SIDEBAR_STATE_KEY) .write(multi_workspace_id.to_string(), json) .await .log_err(); }} #[derive(Clone, Debug)]struct ActiveThreadInfo { session_id: acp::SessionId, title: SharedString, status: AgentThreadStatus, icon: IconName, icon_from_external_svg: Option<SharedString>, is_background: bool, is_title_generating: bool, diff_stats: DiffStats,} impl From<&ActiveThreadInfo> for acp_thread::AgentSessionInfo { fn from(info: &ActiveThreadInfo) -> Self { Self { session_id: info.session_id.clone(), work_dirs: None, title: Some(info.title.clone()), updated_at: Some(Utc::now()), created_at: Some(Utc::now()), meta: None, } }} #[derive(Clone)]enum ThreadEntryWorkspace { Open(Entity<Workspace>), Closed(PathList),} #[derive(Clone)]struct ThreadEntry { agent: Agent, session_info: acp_thread::AgentSessionInfo, icon: IconName, icon_from_external_svg: Option<SharedString>, status: AgentThreadStatus, workspace: ThreadEntryWorkspace, is_live: bool, is_background: bool, is_title_generating: bool, highlight_positions: Vec<usize>, worktree_name: Option<SharedString>, worktree_highlight_positions: Vec<usize>, diff_stats: DiffStats,} #[derive(Clone)]enum ListEntry { ProjectHeader { path_list: PathList, label: SharedString, workspace: Entity<Workspace>, highlight_positions: Vec<usize>, has_threads: bool, }, Thread(ThreadEntry), ViewMore { path_list: PathList, remaining_count: usize, is_fully_expanded: bool, }, NewThread { path_list: PathList, workspace: Entity<Workspace>, },} impl From<ThreadEntry> for ListEntry { fn from(thread: ThreadEntry) -> Self { ListEntry::Thread(thread) }} #[derive(Default)]struct SidebarContents { entries: Vec<ListEntry>, notified_threads: HashSet<acp::SessionId>, project_header_indices: Vec<usize>,} impl SidebarContents { fn is_thread_notified(&self, session_id: &acp::SessionId) -> bool { self.notified_threads.contains(session_id) }} fn fuzzy_match_positions(query: &str, candidate: &str) -> Option<Vec<usize>> { let mut positions = Vec::new(); let mut query_chars = query.chars().peekable(); for (byte_idx, candidate_char) in candidate.char_indices() { if let Some(&query_char) = query_chars.peek() { if candidate_char.eq_ignore_ascii_case(&query_char) { positions.push(byte_idx); query_chars.next(); } } else { break; } } if query_chars.peek().is_none() { Some(positions) } else { None }} // TODO: The mapping from workspace root paths to git repositories needs a// unified approach across the codebase: this function, `AgentPanel::classify_worktrees`,// thread persistence (which PathList is saved to the database), and thread// querying (which PathList is used to read threads back). All of these need// to agree on how repos are resolved for a given workspace, especially in// multi-root and nested-repo configurations.fn root_repository_snapshots( workspace: &Entity<Workspace>, cx: &App,) -> Vec<project::git_store::RepositorySnapshot> { let path_list = workspace_path_list(workspace, cx); let project = workspace.read(cx).project().read(cx); project .repositories(cx) .values() .filter_map(|repo| { let snapshot = repo.read(cx).snapshot(); let is_root = path_list .paths() .iter() .any(|p| p.as_path() == snapshot.work_directory_abs_path.as_ref()); is_root.then_some(snapshot) }) .collect()} fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList { PathList::new(&workspace.read(cx).root_paths(cx))} fn workspace_label_from_path_list(path_list: &PathList) -> SharedString { let mut names = Vec::with_capacity(path_list.paths().len()); for abs_path in path_list.paths() { if let Some(name) = abs_path.file_name() { names.push(name.to_string_lossy().to_string()); } } if names.is_empty() { // TODO: Can we do something better in this case? "Empty Workspace".into() } else { names.join(", ").into() }} pub struct Sidebar { multi_workspace: WeakEntity<MultiWorkspace>, persistence_key: Option<u64>, is_open: bool, width: Pixels, focus_handle: FocusHandle, filter_editor: Entity<Editor>, list_state: ListState, contents: SidebarContents, /// The index of the list item that currently has the keyboard focus /// /// Note: This is NOT the same as the active item. selection: Option<usize>, focused_thread: Option<acp::SessionId>, active_entry_index: Option<usize>, hovered_thread_index: Option<usize>, collapsed_groups: HashSet<PathList>, expanded_groups: HashMap<PathList, usize>, view: SidebarView, archive_view: Option<Entity<ThreadsArchiveView>>, _subscriptions: Vec<gpui::Subscription>, _update_entries_task: Option<gpui::Task<()>>,} impl Sidebar { pub fn new( multi_workspace: Entity<MultiWorkspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); cx.on_focus_in(&focus_handle, window, Self::focus_in) .detach(); let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Search…", window, cx); editor }); cx.subscribe_in( &multi_workspace, window, |this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event { MultiWorkspaceEvent::ActiveWorkspaceChanged => { this.update_entries(false, cx); } MultiWorkspaceEvent::WorkspaceAdded(workspace) => { this.subscribe_to_workspace(workspace, window, cx); this.update_entries(false, cx); } MultiWorkspaceEvent::WorkspaceRemoved(_) => { this.update_entries(false, cx); } }, ) .detach(); cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| { if let editor::EditorEvent::BufferEdited = event { let query = this.filter_editor.read(cx).text(cx); if !query.is_empty() { this.selection.take(); } this.update_entries(!query.is_empty(), cx); } }) .detach(); cx.observe(&ThreadMetadataStore::global(cx), |this, _store, cx| { this.update_entries(false, cx); }) .detach(); cx.observe_flag::<AgentV2FeatureFlag, _>(window, |_is_enabled, this, _window, cx| { this.update_entries(false, cx); }) .detach(); let workspaces = multi_workspace.read(cx).workspaces().to_vec(); cx.defer_in(window, move |this, window, cx| { for workspace in &workspaces { this.subscribe_to_workspace(workspace, window, cx); } this.update_entries(false, cx); }); let persistence_key = multi_workspace.read(cx).database_id().map(|id| id.0); let is_open = persistence_key .map(read_sidebar_open_state) .unwrap_or(false); Self { _update_entries_task: None, multi_workspace: multi_workspace.downgrade(), persistence_key, is_open, width: DEFAULT_WIDTH, focus_handle, filter_editor, list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)), contents: SidebarContents::default(), selection: None, focused_thread: None, active_entry_index: None, hovered_thread_index: None, collapsed_groups: HashSet::new(), expanded_groups: HashMap::new(), view: SidebarView::default(), archive_view: None, _subscriptions: Vec::new(), } } fn subscribe_to_workspace( &self, workspace: &Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) { let project = workspace.read(cx).project().clone(); cx.subscribe_in( &project, window, |this, _project, event, _window, cx| match event { ProjectEvent::WorktreeAdded(_) | ProjectEvent::WorktreeRemoved(_) | ProjectEvent::WorktreeOrderChanged => { this.update_entries(false, cx); } _ => {} }, ) .detach(); let git_store = workspace.read(cx).project().read(cx).git_store().clone(); cx.subscribe_in( &git_store, window, |this, _, event: &project::git_store::GitStoreEvent, window, cx| { if matches!( event, project::git_store::GitStoreEvent::RepositoryUpdated( _, project::git_store::RepositoryEvent::GitWorktreeListChanged, _, ) ) { this.prune_stale_worktree_workspaces(window, cx); this.update_entries(false, cx); } }, ) .detach(); cx.subscribe_in( workspace, window, |this, _workspace, event: &workspace::Event, window, cx| { if let workspace::Event::PanelAdded(view) = event { if let Ok(agent_panel) = view.clone().downcast::<AgentPanel>() { this.subscribe_to_agent_panel(&agent_panel, window, cx); } } }, ) .detach(); if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) { self.subscribe_to_agent_panel(&agent_panel, window, cx); } } fn subscribe_to_agent_panel( &self, agent_panel: &Entity<AgentPanel>, window: &mut Window, cx: &mut Context<Self>, ) { cx.subscribe_in( agent_panel, window, |this, _agent_panel, event: &AgentPanelEvent, _window, cx| match event { AgentPanelEvent::ActiveViewChanged | AgentPanelEvent::ThreadFocused | AgentPanelEvent::BackgroundThreadChanged => { this.update_entries(false, cx); } }, ) .detach(); } fn all_thread_infos_for_workspace( workspace: &Entity<Workspace>, cx: &App, ) -> Vec<ActiveThreadInfo> { let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) else { return Vec::new(); }; let agent_panel_ref = agent_panel.read(cx); agent_panel_ref .parent_threads(cx) .into_iter() .map(|thread_view| { let thread_view_ref = thread_view.read(cx); let thread = thread_view_ref.thread.read(cx); let icon = thread_view_ref.agent_icon; let icon_from_external_svg = thread_view_ref.agent_icon_from_external_svg.clone(); let title = thread.title(); let is_native = thread_view_ref.as_native_thread(cx).is_some(); let is_title_generating = is_native && thread.has_provisional_title(); let session_id = thread.session_id().clone(); let is_background = agent_panel_ref.is_background_thread(&session_id); let status = if thread.is_waiting_for_confirmation() { AgentThreadStatus::WaitingForConfirmation } else if thread.had_error() { AgentThreadStatus::Error } else { match thread.status() { ThreadStatus::Generating => AgentThreadStatus::Running, ThreadStatus::Idle => AgentThreadStatus::Completed, } }; let diff_stats = thread.action_log().read(cx).diff_stats(cx); ActiveThreadInfo { session_id, title, status, icon, icon_from_external_svg, is_background, is_title_generating, diff_stats, } }) .collect() } fn rebuild_contents(&mut self, thread_entries: Vec<ThreadMetadata>, cx: &App) { let Some(multi_workspace) = self.multi_workspace.upgrade() else { return; }; let mw = multi_workspace.read(cx); let workspaces = mw.workspaces().to_vec(); let active_workspace = mw.workspaces().get(mw.active_workspace_index()).cloned(); self.focused_thread = active_workspace .as_ref() .and_then(|ws| ws.read(cx).panel::<AgentPanel>(cx)) .and_then(|panel| panel.read(cx).active_conversation().cloned()) .and_then(|cv| cv.read(cx).parent_id(cx)); let mut threads_by_paths: HashMap<PathList, Vec<ThreadMetadata>> = HashMap::new(); for row in thread_entries { threads_by_paths .entry(row.folder_paths.clone()) .or_default() .push(row); } // Build a lookup for agent icons from the first workspace's AgentServerStore.Lines 1–500 of 4981. Load more
use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore}; use crate::threads_archive_view::{ThreadsArchiveView, ThreadsArchiveViewEvent}; use crate::{Agent, AgentPanel, AgentPanelEvent, NewThread, RemoveSelectedThread}; use acp_thread::ThreadStatus; use action_log::DiffStats; use agent::ThreadStore; use agent_client_protocol::{self as acp}; use agent_settings::AgentSettings; use chrono::Utc; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _}; use gpui::{ Action as _, AnyElement, App, Context, Entity, FocusHandle, Focusable, ListState, Pixels, Render, SharedString, WeakEntity, Window, actions, list, prelude::*, px, }; use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; use project::{AgentId, Event as ProjectEvent}; use settings::Settings; use std::collections::{HashMap, HashSet}; use std::mem; use std::path::Path; use std::sync::Arc; use theme::ActiveTheme; use ui::{ AgentThreadStatus, ButtonStyle, HighlightedLabel, IconButtonShape, ListItem, Tab, ThreadItem, Tooltip, WithScrollbar, prelude::*, }; use util::ResultExt as _; use util::path_list::PathList; use workspace::{ MultiWorkspace, MultiWorkspaceEvent, ToggleWorkspaceSidebar, Workspace, multi_workspace_enabled, }; use zed_actions::editor::{MoveDown, MoveUp}; actions!( agents_sidebar, [ /// Collapses the selected entry in the workspace sidebar. CollapseSelectedEntry, /// Expands the selected entry in the workspace sidebar. ExpandSelectedEntry, ] ); const DEFAULT_WIDTH: Pixels = px(320.0); const MIN_WIDTH: Pixels = px(200.0); const MAX_WIDTH: Pixels = px(800.0); const DEFAULT_THREADS_SHOWN: usize = 5; const SIDEBAR_STATE_KEY: &str = "sidebar_state"; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum SidebarView { #[default] ThreadList, Archive, } fn read_sidebar_open_state(multi_workspace_id: u64) -> bool { KEY_VALUE_STORE .scoped(SIDEBAR_STATE_KEY) .read(&multi_workspace_id.to_string()) .log_err() .flatten() .and_then(|json| serde_json::from_str::<bool>(&json).ok()) .unwrap_or(false) } async fn save_sidebar_open_state(multi_workspace_id: u64, is_open: bool) { if let Ok(json) = serde_json::to_string(&is_open) { KEY_VALUE_STORE .scoped(SIDEBAR_STATE_KEY) .write(multi_workspace_id.to_string(), json) .await .log_err(); } } #[derive(Clone, Debug)] struct ActiveThreadInfo { session_id: acp::SessionId, title: SharedString, status: AgentThreadStatus, icon: IconName, icon_from_external_svg: Option<SharedString>, is_background: bool, is_title_generating: bool, diff_stats: DiffStats, } impl From<&ActiveThreadInfo> for acp_thread::AgentSessionInfo { fn from(info: &ActiveThreadInfo) -> Self { Self { session_id: info.session_id.clone(), work_dirs: None, title: Some(info.title.clone()), updated_at: Some(Utc::now()), created_at: Some(Utc::now()), meta: None, } } } #[derive(Clone)] enum ThreadEntryWorkspace { Open(Entity<Workspace>), Closed(PathList), } #[derive(Clone)] struct ThreadEntry { agent: Agent, session_info: acp_thread::AgentSessionInfo, icon: IconName, icon_from_external_svg: Option<SharedString>, status: AgentThreadStatus, workspace: ThreadEntryWorkspace, is_live: bool, is_background: bool, is_title_generating: bool, highlight_positions: Vec<usize>, worktree_name: Option<SharedString>, worktree_highlight_positions: Vec<usize>, diff_stats: DiffStats, } #[derive(Clone)] enum ListEntry { ProjectHeader { path_list: PathList, label: SharedString, workspace: Entity<Workspace>, highlight_positions: Vec<usize>, has_threads: bool, }, Thread(ThreadEntry), ViewMore { path_list: PathList, remaining_count: usize, is_fully_expanded: bool, }, NewThread { path_list: PathList, workspace: Entity<Workspace>, }, } impl From<ThreadEntry> for ListEntry { fn from(thread: ThreadEntry) -> Self { ListEntry::Thread(thread) } } #[derive(Default)] struct SidebarContents { entries: Vec<ListEntry>, notified_threads: HashSet<acp::SessionId>, project_header_indices: Vec<usize>, } impl SidebarContents { fn is_thread_notified(&self, session_id: &acp::SessionId) -> bool { self.notified_threads.contains(session_id) } } fn fuzzy_match_positions(query: &str, candidate: &str) -> Option<Vec<usize>> { let mut positions = Vec::new(); let mut query_chars = query.chars().peekable(); for (byte_idx, candidate_char) in candidate.char_indices() { if let Some(&query_char) = query_chars.peek() { if candidate_char.eq_ignore_ascii_case(&query_char) { positions.push(byte_idx); query_chars.next(); } } else { break; } } if query_chars.peek().is_none() { Some(positions) } else { None } } // TODO: The mapping from workspace root paths to git repositories needs a // unified approach across the codebase: this function, `AgentPanel::classify_worktrees`, // thread persistence (which PathList is saved to the database), and thread // querying (which PathList is used to read threads back). All of these need // to agree on how repos are resolved for a given workspace, especially in // multi-root and nested-repo configurations. fn root_repository_snapshots( workspace: &Entity<Workspace>, cx: &App, ) -> Vec<project::git_store::RepositorySnapshot> { let path_list = workspace_path_list(workspace, cx); let project = workspace.read(cx).project().read(cx); project .repositories(cx) .values() .filter_map(|repo| { let snapshot = repo.read(cx).snapshot(); let is_root = path_list .paths() .iter() .any(|p| p.as_path() == snapshot.work_directory_abs_path.as_ref()); is_root.then_some(snapshot) }) .collect() } fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList { PathList::new(&workspace.read(cx).root_paths(cx)) } fn workspace_label_from_path_list(path_list: &PathList) -> SharedString { let mut names = Vec::with_capacity(path_list.paths().len()); for abs_path in path_list.paths() { if let Some(name) = abs_path.file_name() { names.push(name.to_string_lossy().to_string()); } } if names.is_empty() { // TODO: Can we do something better in this case? "Empty Workspace".into() } else { names.join(", ").into() } } pub struct Sidebar { multi_workspace: WeakEntity<MultiWorkspace>, persistence_key: Option<u64>, is_open: bool, width: Pixels, focus_handle: FocusHandle, filter_editor: Entity<Editor>, list_state: ListState, contents: SidebarContents, /// The index of the list item that currently has the keyboard focus /// /// Note: This is NOT the same as the active item. selection: Option<usize>, focused_thread: Option<acp::SessionId>, active_entry_index: Option<usize>, hovered_thread_index: Option<usize>, collapsed_groups: HashSet<PathList>, expanded_groups: HashMap<PathList, usize>, view: SidebarView, archive_view: Option<Entity<ThreadsArchiveView>>, _subscriptions: Vec<gpui::Subscription>, _update_entries_task: Option<gpui::Task<()>>, } impl Sidebar { pub fn new( multi_workspace: Entity<MultiWorkspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); cx.on_focus_in(&focus_handle, window, Self::focus_in) .detach(); let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Search…", window, cx); editor }); cx.subscribe_in( &multi_workspace, window, |this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event { MultiWorkspaceEvent::ActiveWorkspaceChanged => { this.update_entries(false, cx); } MultiWorkspaceEvent::WorkspaceAdded(workspace) => { this.subscribe_to_workspace(workspace, window, cx); this.update_entries(false, cx); } MultiWorkspaceEvent::WorkspaceRemoved(_) => { this.update_entries(false, cx); } }, ) .detach(); cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| { if let editor::EditorEvent::BufferEdited = event { let query = this.filter_editor.read(cx).text(cx); if !query.is_empty() { this.selection.take(); } this.update_entries(!query.is_empty(), cx); } }) .detach(); cx.observe(&ThreadMetadataStore::global(cx), |this, _store, cx| { this.update_entries(false, cx); }) .detach(); cx.observe_flag::<AgentV2FeatureFlag, _>(window, |_is_enabled, this, _window, cx| { this.update_entries(false, cx); }) .detach(); let workspaces = multi_workspace.read(cx).workspaces().to_vec(); cx.defer_in(window, move |this, window, cx| { for workspace in &workspaces { this.subscribe_to_workspace(workspace, window, cx); } this.update_entries(false, cx); }); let persistence_key = multi_workspace.read(cx).database_id().map(|id| id.0); let is_open = persistence_key .map(read_sidebar_open_state) .unwrap_or(false); Self { _update_entries_task: None, multi_workspace: multi_workspace.downgrade(), persistence_key, is_open, width: DEFAULT_WIDTH, focus_handle, filter_editor, list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)), contents: SidebarContents::default(), selection: None, focused_thread: None, active_entry_index: None, hovered_thread_index: None, collapsed_groups: HashSet::new(), expanded_groups: HashMap::new(), view: SidebarView::default(), archive_view: None, _subscriptions: Vec::new(), } } fn subscribe_to_workspace( &self, workspace: &Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) { let project = workspace.read(cx).project().clone(); cx.subscribe_in( &project, window, |this, _project, event, _window, cx| match event { ProjectEvent::WorktreeAdded(_) | ProjectEvent::WorktreeRemoved(_) | ProjectEvent::WorktreeOrderChanged => { this.update_entries(false, cx); } _ => {} }, ) .detach(); let git_store = workspace.read(cx).project().read(cx).git_store().clone(); cx.subscribe_in( &git_store, window, |this, _, event: &project::git_store::GitStoreEvent, window, cx| { if matches!( event, project::git_store::GitStoreEvent::RepositoryUpdated( _, project::git_store::RepositoryEvent::GitWorktreeListChanged, _, ) ) { this.prune_stale_worktree_workspaces(window, cx); this.update_entries(false, cx); } }, ) .detach(); cx.subscribe_in( workspace, window, |this, _workspace, event: &workspace::Event, window, cx| { if let workspace::Event::PanelAdded(view) = event { if let Ok(agent_panel) = view.clone().downcast::<AgentPanel>() { this.subscribe_to_agent_panel(&agent_panel, window, cx); } } }, ) .detach(); if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) { self.subscribe_to_agent_panel(&agent_panel, window, cx); } } fn subscribe_to_agent_panel( &self, agent_panel: &Entity<AgentPanel>, window: &mut Window, cx: &mut Context<Self>, ) { cx.subscribe_in( agent_panel, window, |this, _agent_panel, event: &AgentPanelEvent, _window, cx| match event { AgentPanelEvent::ActiveViewChanged | AgentPanelEvent::ThreadFocused | AgentPanelEvent::BackgroundThreadChanged => { this.update_entries(false, cx); } }, ) .detach(); } fn all_thread_infos_for_workspace( workspace: &Entity<Workspace>, cx: &App, ) -> Vec<ActiveThreadInfo> { let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) else { return Vec::new(); }; let agent_panel_ref = agent_panel.read(cx); agent_panel_ref .parent_threads(cx) .into_iter() .map(|thread_view| { let thread_view_ref = thread_view.read(cx); let thread = thread_view_ref.thread.read(cx); let icon = thread_view_ref.agent_icon; let icon_from_external_svg = thread_view_ref.agent_icon_from_external_svg.clone(); let title = thread.title(); let is_native = thread_view_ref.as_native_thread(cx).is_some(); let is_title_generating = is_native && thread.has_provisional_title(); let session_id = thread.session_id().clone(); let is_background = agent_panel_ref.is_background_thread(&session_id); let status = if thread.is_waiting_for_confirmation() { AgentThreadStatus::WaitingForConfirmation } else if thread.had_error() { AgentThreadStatus::Error } else { match thread.status() { ThreadStatus::Generating => AgentThreadStatus::Running, ThreadStatus::Idle => AgentThreadStatus::Completed, } }; let diff_stats = thread.action_log().read(cx).diff_stats(cx); ActiveThreadInfo { session_id, title, status, icon, icon_from_external_svg, is_background, is_title_generating, diff_stats, } }) .collect() } fn rebuild_contents(&mut self, thread_entries: Vec<ThreadMetadata>, cx: &App) { let Some(multi_workspace) = self.multi_workspace.upgrade() else { return; }; let mw = multi_workspace.read(cx); let workspaces = mw.workspaces().to_vec(); let active_workspace = mw.workspaces().get(mw.active_workspace_index()).cloned(); self.focused_thread = active_workspace .as_ref() .and_then(|ws| ws.read(cx).panel::<AgentPanel>(cx)) .and_then(|panel| panel.read(cx).active_conversation().cloned()) .and_then(|cv| cv.read(cx).parent_id(cx)); let mut threads_by_paths: HashMap<PathList, Vec<ThreadMetadata>> = HashMap::new(); for row in thread_entries { threads_by_paths .entry(row.folder_paths.clone()) .or_default() .push(row); } // Build a lookup for agent icons from the first workspace's AgentServerStore.