diff --git a/crates/agent_ui/src/thread_metadata_store.rs b/crates/agent_ui/src/thread_metadata_store.rs index 1dadd3d9f875177d51a57106f0a2d054d0252868..c4f65881f88a0b6043de3a0d0bbeb49bdd7f102d 100644 --- a/crates/agent_ui/src/thread_metadata_store.rs +++ b/crates/agent_ui/src/thread_metadata_store.rs @@ -105,6 +105,19 @@ pub struct ThreadMetadata { pub archived: bool, } +impl From<&ThreadMetadata> for acp_thread::AgentSessionInfo { + fn from(meta: &ThreadMetadata) -> Self { + Self { + session_id: meta.session_id.clone(), + work_dirs: Some(meta.folder_paths.clone()), + title: Some(meta.title.clone()), + updated_at: Some(meta.updated_at), + created_at: meta.created_at, + meta: None, + } + } +} + impl ThreadMetadata { pub fn from_thread( is_archived: bool, diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 54e7dd32a49f3ebf0f314502cb542a0df07dca16..96a3b4e9c41618a54637bfdd78642e559b975228 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -4,7 +4,7 @@ use acp_thread::ThreadStatus; use action_log::DiffStats; use agent_client_protocol::{self as acp}; use agent_settings::AgentSettings; -use agent_ui::thread_metadata_store::ThreadMetadataStore; +use agent_ui::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore}; use agent_ui::threads_archive_view::{ ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp, }; @@ -92,19 +92,6 @@ struct ActiveThreadInfo { 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), @@ -120,8 +107,7 @@ struct WorktreeInfo { #[derive(Clone)] struct ThreadEntry { - agent: Agent, - session_info: acp_thread::AgentSessionInfo, + metadata: ThreadMetadata, icon: IconName, icon_from_external_svg: Option, status: AgentThreadStatus, @@ -141,7 +127,7 @@ impl ThreadEntry { /// but if we have a correspond thread already loaded we want to apply the /// live information. fn apply_active_info(&mut self, info: &ActiveThreadInfo) { - self.session_info.title = Some(info.title.clone()); + self.metadata.title = info.title.clone(); self.status = info.status; self.icon = info.icon; self.icon_from_external_svg = info.icon_from_external_svg.clone(); @@ -191,7 +177,7 @@ impl ListEntry { fn session_id(&self) -> Option<&acp::SessionId> { match self { - ListEntry::Thread(thread_entry) => Some(&thread_entry.session_info.session_id), + ListEntry::Thread(thread_entry) => Some(&thread_entry.metadata.session_id), _ => None, } } @@ -690,7 +676,7 @@ impl Sidebar { .iter() .filter_map(|entry| match entry { ListEntry::Thread(thread) if thread.is_live => { - Some((thread.session_info.session_id.clone(), thread.status)) + Some((thread.metadata.session_id.clone(), thread.status)) } _ => None, }) @@ -710,7 +696,7 @@ impl Sidebar { .iter() .any(|ws| !workspace_path_list(ws, cx).paths().is_empty()); - let resolve_agent = |agent_id: &AgentId| -> (Agent, IconName, Option) { + let resolve_agent_icon = |agent_id: &AgentId| -> (IconName, Option) { let agent = Agent::from(agent_id.clone()); let icon = match agent { Agent::NativeAgent => IconName::ZedAgent, @@ -719,7 +705,7 @@ impl Sidebar { let icon_from_external_svg = agent_server_store .as_ref() .and_then(|store| store.read(cx).agent_icon(&agent_id)); - (agent, icon, icon_from_external_svg) + (icon, icon_from_external_svg) }; for (group_name, group) in project_groups.groups() { @@ -768,19 +754,11 @@ impl Sidebar { if !seen_session_ids.insert(row.session_id.clone()) { continue; } - let (agent, icon, icon_from_external_svg) = resolve_agent(&row.agent_id); + let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id); let worktrees = worktree_info_from_thread_paths(&row.folder_paths, &project_groups); threads.push(ThreadEntry { - agent, - session_info: acp_thread::AgentSessionInfo { - session_id: row.session_id.clone(), - work_dirs: None, - title: Some(row.title.clone()), - updated_at: Some(row.updated_at), - created_at: row.created_at, - meta: None, - }, + metadata: row, icon, icon_from_external_svg, status: AgentThreadStatus::default(), @@ -818,19 +796,11 @@ impl Sidebar { if !seen_session_ids.insert(row.session_id.clone()) { continue; } - let (agent, icon, icon_from_external_svg) = resolve_agent(&row.agent_id); + let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id); let worktrees = worktree_info_from_thread_paths(&row.folder_paths, &project_groups); threads.push(ThreadEntry { - agent, - session_info: acp_thread::AgentSessionInfo { - session_id: row.session_id.clone(), - work_dirs: None, - title: Some(row.title.clone()), - updated_at: Some(row.updated_at), - created_at: row.created_at, - meta: None, - }, + metadata: row, icon, icon_from_external_svg, status: AgentThreadStatus::default(), @@ -862,11 +832,11 @@ impl Sidebar { // Merge live info into threads and update notification state // in a single pass. for thread in &mut threads { - if let Some(info) = live_info_by_session.get(&thread.session_info.session_id) { + if let Some(info) = live_info_by_session.get(&thread.metadata.session_id) { thread.apply_active_info(info); } - let session_id = &thread.session_info.session_id; + let session_id = &thread.metadata.session_id; let is_thread_workspace_active = match &thread.workspace { ThreadEntryWorkspace::Open(thread_workspace) => active_workspace @@ -890,16 +860,16 @@ impl Sidebar { threads.sort_by(|a, b| { let a_time = self .thread_last_message_sent_or_queued - .get(&a.session_info.session_id) + .get(&a.metadata.session_id) .copied() - .or(a.session_info.created_at) - .or(a.session_info.updated_at); + .or(a.metadata.created_at) + .or(Some(a.metadata.updated_at)); let b_time = self .thread_last_message_sent_or_queued - .get(&b.session_info.session_id) + .get(&b.metadata.session_id) .copied() - .or(b.session_info.created_at) - .or(b.session_info.updated_at); + .or(b.metadata.created_at) + .or(Some(b.metadata.updated_at)); b_time.cmp(&a_time) }); } else { @@ -920,12 +890,7 @@ impl Sidebar { let mut matched_threads: Vec = Vec::new(); for mut thread in threads { - let title = thread - .session_info - .title - .as_ref() - .map(|s| s.as_ref()) - .unwrap_or(""); + let title: &str = &thread.metadata.title; if let Some(positions) = fuzzy_match_positions(&query, title) { thread.highlight_positions = positions; } @@ -960,7 +925,7 @@ impl Sidebar { }); for thread in matched_threads { - current_session_ids.insert(thread.session_info.session_id.clone()); + current_session_ids.insert(thread.metadata.session_id.clone()); entries.push(thread.into()); } } else { @@ -1011,7 +976,7 @@ impl Sidebar { for (index, thread) in threads.into_iter().enumerate() { let is_hidden = index >= count; - let session_id = &thread.session_info.session_id; + let session_id = &thread.metadata.session_id; if is_hidden { let is_promoted = thread.status == AgentThreadStatus::Running || thread.status == AgentThreadStatus::WaitingForConfirmation @@ -1861,22 +1826,15 @@ impl Sidebar { self.toggle_collapse(&path_list, window, cx); } ListEntry::Thread(thread) => { - let session_info = thread.session_info.clone(); + let metadata = thread.metadata.clone(); match &thread.workspace { ThreadEntryWorkspace::Open(workspace) => { let workspace = workspace.clone(); - self.activate_thread( - thread.agent.clone(), - session_info, - &workspace, - window, - cx, - ); + self.activate_thread(metadata, &workspace, window, cx); } ThreadEntryWorkspace::Closed(path_list) => { self.open_workspace_and_activate_thread( - thread.agent.clone(), - session_info, + metadata, path_list.clone(), window, cx, @@ -1942,8 +1900,7 @@ impl Sidebar { fn load_agent_thread_in_workspace( workspace: &Entity, - agent: Agent, - session_info: acp_thread::AgentSessionInfo, + metadata: &ThreadMetadata, focus: bool, window: &mut Window, cx: &mut App, @@ -1955,10 +1912,10 @@ impl Sidebar { if let Some(agent_panel) = workspace.read(cx).panel::(cx) { agent_panel.update(cx, |panel, cx| { panel.load_agent_thread( - agent, - session_info.session_id, - session_info.work_dirs, - session_info.title, + Agent::from(metadata.agent_id.clone()), + metadata.session_id.clone(), + Some(metadata.folder_paths.clone()), + Some(metadata.title.clone()), focus, window, cx, @@ -1969,8 +1926,7 @@ impl Sidebar { fn activate_thread_locally( &mut self, - agent: Agent, - session_info: acp_thread::AgentSessionInfo, + metadata: &ThreadMetadata, workspace: &Entity, window: &mut Window, cx: &mut Context, @@ -1982,40 +1938,32 @@ impl Sidebar { // Set focused_thread eagerly so the sidebar highlight updates // immediately, rather than waiting for a deferred AgentPanel // event which can race with ActiveWorkspaceChanged clearing it. - self.focused_thread = Some(session_info.session_id.clone()); - self.record_thread_access(&session_info.session_id); + self.focused_thread = Some(metadata.session_id.clone()); + self.record_thread_access(&metadata.session_id); multi_workspace.update(cx, |multi_workspace, cx| { multi_workspace.activate(workspace.clone(), cx); }); - Self::load_agent_thread_in_workspace(workspace, agent, session_info, true, window, cx); + Self::load_agent_thread_in_workspace(workspace, metadata, true, window, cx); self.update_entries(cx); } fn activate_thread_in_other_window( &self, - agent: Agent, - session_info: acp_thread::AgentSessionInfo, + metadata: ThreadMetadata, workspace: Entity, target_window: WindowHandle, cx: &mut Context, ) { - let target_session_id = session_info.session_id.clone(); + let target_session_id = metadata.session_id.clone(); let activated = target_window .update(cx, |multi_workspace, window, cx| { window.activate_window(); multi_workspace.activate(workspace.clone(), cx); - Self::load_agent_thread_in_workspace( - &workspace, - agent, - session_info, - true, - window, - cx, - ); + Self::load_agent_thread_in_workspace(&workspace, &metadata, true, window, cx); }) .log_err() .is_some(); @@ -2040,8 +1988,7 @@ impl Sidebar { fn activate_thread( &mut self, - agent: Agent, - session_info: acp_thread::AgentSessionInfo, + metadata: ThreadMetadata, workspace: &Entity, window: &mut Window, cx: &mut Context, @@ -2050,7 +1997,7 @@ impl Sidebar { .find_workspace_in_current_window(cx, |candidate, _| candidate == workspace) .is_some() { - self.activate_thread_locally(agent, session_info, &workspace, window, cx); + self.activate_thread_locally(&metadata, &workspace, window, cx); return; } @@ -2060,13 +2007,12 @@ impl Sidebar { return; }; - self.activate_thread_in_other_window(agent, session_info, workspace, target_window, cx); + self.activate_thread_in_other_window(metadata, workspace, target_window, cx); } fn open_workspace_and_activate_thread( &mut self, - agent: Agent, - session_info: acp_thread::AgentSessionInfo, + metadata: ThreadMetadata, path_list: PathList, window: &mut Window, cx: &mut Context, @@ -2084,7 +2030,7 @@ impl Sidebar { let workspace = open_task.await?; this.update_in(cx, |this, window, cx| { - this.activate_thread(agent, session_info, &workspace, window, cx); + this.activate_thread(metadata, &workspace, window, cx); })?; anyhow::Ok(()) }) @@ -2113,31 +2059,23 @@ impl Sidebar { fn activate_archived_thread( &mut self, - agent: Agent, - session_info: acp_thread::AgentSessionInfo, + metadata: ThreadMetadata, window: &mut Window, cx: &mut Context, ) { - ThreadMetadataStore::global(cx).update(cx, |store, cx| { - store.unarchive(&session_info.session_id, cx) - }); + ThreadMetadataStore::global(cx) + .update(cx, |store, cx| store.unarchive(&metadata.session_id, cx)); - if let Some(path_list) = &session_info.work_dirs { - if let Some(workspace) = self.find_current_workspace_for_path_list(path_list, cx) { - self.activate_thread_locally(agent, session_info, &workspace, window, cx); + if !metadata.folder_paths.paths().is_empty() { + let path_list = metadata.folder_paths.clone(); + if let Some(workspace) = self.find_current_workspace_for_path_list(&path_list, cx) { + self.activate_thread_locally(&metadata, &workspace, window, cx); } else if let Some((target_window, workspace)) = - self.find_open_workspace_for_path_list(path_list, cx) + self.find_open_workspace_for_path_list(&path_list, cx) { - self.activate_thread_in_other_window( - agent, - session_info, - workspace, - target_window, - cx, - ); + self.activate_thread_in_other_window(metadata, workspace, target_window, cx); } else { - let path_list = path_list.clone(); - self.open_workspace_and_activate_thread(agent, session_info, path_list, window, cx); + self.open_workspace_and_activate_thread(metadata, path_list, window, cx); } return; } @@ -2150,7 +2088,7 @@ impl Sidebar { }); if let Some(workspace) = active_workspace { - self.activate_thread_locally(agent, session_info, &workspace, window, cx); + self.activate_thread_locally(&metadata, &workspace, window, cx); } } @@ -2306,7 +2244,7 @@ impl Sidebar { // a blank new thread in the panel instead. if self.focused_thread.as_ref() == Some(session_id) { let current_pos = self.contents.entries.iter().position(|entry| { - matches!(entry, ListEntry::Thread(t) if &t.session_info.session_id == session_id) + matches!(entry, ListEntry::Thread(t) if &t.metadata.session_id == session_id) }); // Find the workspace that owns this thread's project group by @@ -2358,10 +2296,7 @@ impl Sidebar { }); if let Some(next) = next_thread { - let next_session_id = next.session_info.session_id.clone(); - let next_agent = next.agent.clone(); - let next_work_dirs = next.session_info.work_dirs.clone(); - let next_title = next.session_info.title.clone(); + let next_metadata = next.metadata.clone(); // Use the thread's own workspace when it has one open (e.g. an absorbed // linked worktree thread that appears under the main workspace's header // but belongs to its own workspace). Loading into the wrong panel binds @@ -2371,17 +2306,17 @@ impl Sidebar { ThreadEntryWorkspace::Open(ws) => Some(ws.clone()), ThreadEntryWorkspace::Closed(_) => group_workspace, }; - self.focused_thread = Some(next_session_id.clone()); - self.record_thread_access(&next_session_id); + self.focused_thread = Some(next_metadata.session_id.clone()); + self.record_thread_access(&next_metadata.session_id); if let Some(workspace) = target_workspace { if let Some(agent_panel) = workspace.read(cx).panel::(cx) { agent_panel.update(cx, |panel, cx| { panel.load_agent_thread( - next_agent, - next_session_id, - next_work_dirs, - next_title, + Agent::from(next_metadata.agent_id.clone()), + next_metadata.session_id.clone(), + Some(next_metadata.folder_paths.clone()), + Some(next_metadata.title.clone()), true, window, cx, @@ -2419,7 +2354,7 @@ impl Sidebar { AgentThreadStatus::Completed | AgentThreadStatus::Error => {} } - let session_id = thread.session_info.session_id.clone(); + let session_id = thread.metadata.session_id.clone(); self.archive_thread(&session_id, window, cx) } @@ -2453,28 +2388,22 @@ impl Sidebar { }; let notified = self .contents - .is_thread_notified(&thread.session_info.session_id); - let timestamp: SharedString = self - .thread_last_message_sent_or_queued - .get(&thread.session_info.session_id) - .copied() - .or(thread.session_info.created_at) - .or(thread.session_info.updated_at) - .map(format_history_entry_timestamp) - .unwrap_or_default() - .into(); + .is_thread_notified(&thread.metadata.session_id); + let timestamp: SharedString = format_history_entry_timestamp( + self.thread_last_message_sent_or_queued + .get(&thread.metadata.session_id) + .copied() + .or(thread.metadata.created_at) + .unwrap_or(thread.metadata.updated_at), + ) + .into(); Some(ThreadSwitcherEntry { - session_id: thread.session_info.session_id.clone(), - title: thread - .session_info - .title - .clone() - .unwrap_or_else(|| "Untitled".into()), + session_id: thread.metadata.session_id.clone(), + title: thread.metadata.title.clone(), icon: thread.icon, icon_from_external_svg: thread.icon_from_external_svg.clone(), status: thread.status, - agent: thread.agent.clone(), - session_info: thread.session_info.clone(), + metadata: thread.metadata.clone(), workspace, worktree_name: thread.worktrees.first().map(|wt| wt.name.clone()), @@ -2505,8 +2434,8 @@ impl Sidebar { (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => { - let a_time = a.session_info.created_at.or(a.session_info.updated_at); - let b_time = b.session_info.created_at.or(b.session_info.updated_at); + let a_time = a.metadata.created_at.or(Some(a.metadata.updated_at)); + let b_time = b.metadata.created_at.or(Some(b.metadata.updated_at)); b_time.cmp(&a_time) } } @@ -2560,16 +2489,11 @@ impl Sidebar { let weak_multi_workspace = self.multi_workspace.clone(); - let original_agent = self - .focused_thread - .as_ref() - .and_then(|focused_id| entries.iter().find(|e| &e.session_id == focused_id)) - .map(|e| e.agent.clone()); - let original_session_info = self + let original_metadata = self .focused_thread .as_ref() .and_then(|focused_id| entries.iter().find(|e| &e.session_id == focused_id)) - .map(|e| e.session_info.clone()); + .map(|e| e.metadata.clone()); let original_workspace = self .multi_workspace .upgrade() @@ -2583,8 +2507,7 @@ impl Sidebar { let thread_switcher = thread_switcher.clone(); move |this, _emitter, event: &ThreadSwitcherEvent, window, cx| match event { ThreadSwitcherEvent::Preview { - agent, - session_info, + metadata, workspace, } => { if let Some(mw) = weak_multi_workspace.upgrade() { @@ -2592,22 +2515,14 @@ impl Sidebar { mw.activate(workspace.clone(), cx); }); } - this.focused_thread = Some(session_info.session_id.clone()); + this.focused_thread = Some(metadata.session_id.clone()); this.update_entries(cx); - Self::load_agent_thread_in_workspace( - workspace, - agent.clone(), - session_info.clone(), - false, - window, - cx, - ); + Self::load_agent_thread_in_workspace(workspace, metadata, false, window, cx); let focus = thread_switcher.focus_handle(cx); window.focus(&focus, cx); } ThreadSwitcherEvent::Confirmed { - agent, - session_info, + metadata, workspace, } => { if let Some(mw) = weak_multi_workspace.upgrade() { @@ -2615,17 +2530,10 @@ impl Sidebar { mw.activate(workspace.clone(), cx); }); } - this.record_thread_access(&session_info.session_id); - this.focused_thread = Some(session_info.session_id.clone()); + this.record_thread_access(&metadata.session_id); + this.focused_thread = Some(metadata.session_id.clone()); this.update_entries(cx); - Self::load_agent_thread_in_workspace( - workspace, - agent.clone(), - session_info.clone(), - false, - window, - cx, - ); + Self::load_agent_thread_in_workspace(workspace, metadata, false, window, cx); this.dismiss_thread_switcher(cx); workspace.update(cx, |workspace, cx| { workspace.focus_panel::(window, cx); @@ -2639,15 +2547,13 @@ impl Sidebar { }); } } - if let Some(session_info) = &original_session_info { - this.focused_thread = Some(session_info.session_id.clone()); + if let Some(metadata) = &original_metadata { + this.focused_thread = Some(metadata.session_id.clone()); this.update_entries(cx); - let agent = original_agent.clone().unwrap_or(Agent::NativeAgent); if let Some(original_ws) = &original_workspace { Self::load_agent_thread_in_workspace( original_ws, - agent, - session_info.clone(), + metadata, false, window, cx, @@ -2672,13 +2578,10 @@ impl Sidebar { // Replay the initial preview that was emitted during construction // before subscriptions were wired up. - let initial_preview = thread_switcher.read(cx).selected_entry().map(|entry| { - ( - entry.agent.clone(), - entry.session_info.clone(), - entry.workspace.clone(), - ) - }); + let initial_preview = thread_switcher + .read(cx) + .selected_entry() + .map(|entry| (entry.metadata.clone(), entry.workspace.clone())); self.thread_switcher = Some(thread_switcher); self._thread_switcher_subscriptions = subscriptions; @@ -2688,22 +2591,15 @@ impl Sidebar { }); } - if let Some((agent, session_info, workspace)) = initial_preview { + if let Some((metadata, workspace)) = initial_preview { if let Some(mw) = self.multi_workspace.upgrade() { mw.update(cx, |mw, cx| { mw.activate(workspace.clone(), cx); }); } - self.focused_thread = Some(session_info.session_id.clone()); + self.focused_thread = Some(metadata.session_id.clone()); self.update_entries(cx); - Self::load_agent_thread_in_workspace( - &workspace, - agent, - session_info, - false, - window, - cx, - ); + Self::load_agent_thread_in_workspace(&workspace, &metadata, false, window, cx); } window.focus(&focus, cx); @@ -2718,36 +2614,32 @@ impl Sidebar { ) -> AnyElement { let has_notification = self .contents - .is_thread_notified(&thread.session_info.session_id); - - let title: SharedString = thread - .session_info - .title - .clone() - .unwrap_or_else(|| "Untitled".into()); - let session_info = thread.session_info.clone(); + .is_thread_notified(&thread.metadata.session_id); + + let title: SharedString = thread.metadata.title.clone(); + let metadata = thread.metadata.clone(); let thread_workspace = thread.workspace.clone(); let is_hovered = self.hovered_thread_index == Some(ix); - let is_selected = self.agent_panel_visible - && self.focused_thread.as_ref() == Some(&session_info.session_id); + let is_selected = + self.agent_panel_visible && self.focused_thread.as_ref() == Some(&metadata.session_id); let is_running = matches!( thread.status, AgentThreadStatus::Running | AgentThreadStatus::WaitingForConfirmation ); - let session_id_for_delete = thread.session_info.session_id.clone(); + let session_id_for_delete = thread.metadata.session_id.clone(); let focus_handle = self.focus_handle.clone(); let id = SharedString::from(format!("thread-entry-{}", ix)); - let timestamp = self - .thread_last_message_sent_or_queued - .get(&thread.session_info.session_id) - .copied() - .or(thread.session_info.created_at) - .or(thread.session_info.updated_at) - .map(format_history_entry_timestamp); + let timestamp = format_history_entry_timestamp( + self.thread_last_message_sent_or_queued + .get(&thread.metadata.session_id) + .copied() + .or(thread.metadata.created_at) + .unwrap_or(thread.metadata.updated_at), + ); ThreadItem::new(id, title) .icon(thread.icon) @@ -2766,7 +2658,7 @@ impl Sidebar { }) .collect(), ) - .when_some(timestamp, |this, ts| this.timestamp(ts)) + .timestamp(timestamp) .highlight_positions(thread.highlight_positions.to_vec()) .title_generating(thread.is_title_generating) .notified(has_notification) @@ -2827,23 +2719,15 @@ impl Sidebar { ) }) .on_click({ - let agent = thread.agent.clone(); cx.listener(move |this, _, window, cx| { this.selection = None; match &thread_workspace { ThreadEntryWorkspace::Open(workspace) => { - this.activate_thread( - agent.clone(), - session_info.clone(), - workspace, - window, - cx, - ); + this.activate_thread(metadata.clone(), workspace, window, cx); } ThreadEntryWorkspace::Closed(path_list) => { this.open_workspace_and_activate_thread( - agent.clone(), - session_info.clone(), + metadata.clone(), path_list.clone(), window, cx, @@ -3333,18 +3217,7 @@ impl Sidebar { } ThreadsArchiveViewEvent::Unarchive { thread } => { this.show_thread_list(window, cx); - - let agent = Agent::from(thread.agent_id.clone()); - let session_info = acp_thread::AgentSessionInfo { - session_id: thread.session_id.clone(), - work_dirs: Some(thread.folder_paths.clone()), - title: Some(thread.title.clone()), - updated_at: Some(thread.updated_at), - created_at: thread.created_at, - meta: None, - }; - - this.activate_archived_thread(agent, session_info, window, cx); + this.activate_archived_thread(thread.clone(), window, cx); } }, ); diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index 58f98da9fc1a1685e6d3795023ea4f5bbadd0ea0..63c3124ae2557558f7644fe0257520f175dd35d1 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -11,6 +11,7 @@ use feature_flags::FeatureFlagAppExt as _; use fs::FakeFs; use gpui::TestAppContext; use pretty_assertions::assert_eq; +use project::AgentId; use settings::SettingsStore; use std::{path::PathBuf, sync::Arc}; use util::path_list::PathList; @@ -30,9 +31,11 @@ fn init_test(cx: &mut TestAppContext) { } fn has_thread_entry(sidebar: &Sidebar, session_id: &acp::SessionId) -> bool { - sidebar.contents.entries.iter().any( - |entry| matches!(entry, ListEntry::Thread(t) if &t.session_info.session_id == session_id), - ) + sidebar + .contents + .entries + .iter() + .any(|entry| matches!(entry, ListEntry::Thread(t) if &t.metadata.session_id == session_id)) } async fn init_test_project( @@ -176,12 +179,7 @@ fn visible_entries_as_strings( format!("{} [{}]{}", icon, label, selected) } ListEntry::Thread(thread) => { - let title = thread - .session_info - .title - .as_ref() - .map(|s| s.as_ref()) - .unwrap_or("Untitled"); + let title = thread.metadata.title.as_ref(); let active = if thread.is_live { " *" } else { "" }; let status_str = match thread.status { AgentThreadStatus::Running => " (running)", @@ -191,7 +189,7 @@ fn visible_entries_as_strings( }; let notified = if sidebar .contents - .is_thread_notified(&thread.session_info.session_id) + .is_thread_notified(&thread.metadata.session_id) { " (!)" } else { @@ -565,14 +563,14 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) { is_active: true, }, ListEntry::Thread(ThreadEntry { - agent: Agent::NativeAgent, - session_info: acp_thread::AgentSessionInfo { + metadata: ThreadMetadata { session_id: acp::SessionId::new(Arc::from("t-1")), - work_dirs: None, - title: Some("Completed thread".into()), - updated_at: Some(Utc::now()), + agent_id: AgentId::new("zed-agent"), + folder_paths: PathList::default(), + title: "Completed thread".into(), + updated_at: Utc::now(), created_at: Some(Utc::now()), - meta: None, + archived: false, }, icon: IconName::ZedAgent, icon_from_external_svg: None, @@ -587,14 +585,14 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) { }), // Active thread with Running status ListEntry::Thread(ThreadEntry { - agent: Agent::NativeAgent, - session_info: acp_thread::AgentSessionInfo { + metadata: ThreadMetadata { session_id: acp::SessionId::new(Arc::from("t-2")), - work_dirs: None, - title: Some("Running thread".into()), - updated_at: Some(Utc::now()), + agent_id: AgentId::new("zed-agent"), + folder_paths: PathList::default(), + title: "Running thread".into(), + updated_at: Utc::now(), created_at: Some(Utc::now()), - meta: None, + archived: false, }, icon: IconName::ZedAgent, icon_from_external_svg: None, @@ -609,14 +607,14 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) { }), // Active thread with Error status ListEntry::Thread(ThreadEntry { - agent: Agent::NativeAgent, - session_info: acp_thread::AgentSessionInfo { + metadata: ThreadMetadata { session_id: acp::SessionId::new(Arc::from("t-3")), - work_dirs: None, - title: Some("Error thread".into()), - updated_at: Some(Utc::now()), + agent_id: AgentId::new("zed-agent"), + folder_paths: PathList::default(), + title: "Error thread".into(), + updated_at: Utc::now(), created_at: Some(Utc::now()), - meta: None, + archived: false, }, icon: IconName::ZedAgent, icon_from_external_svg: None, @@ -631,14 +629,14 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) { }), // Thread with WaitingForConfirmation status, not active ListEntry::Thread(ThreadEntry { - agent: Agent::NativeAgent, - session_info: acp_thread::AgentSessionInfo { + metadata: ThreadMetadata { session_id: acp::SessionId::new(Arc::from("t-4")), - work_dirs: None, - title: Some("Waiting thread".into()), - updated_at: Some(Utc::now()), + agent_id: AgentId::new("zed-agent"), + folder_paths: PathList::default(), + title: "Waiting thread".into(), + updated_at: Utc::now(), created_at: Some(Utc::now()), - meta: None, + archived: false, }, icon: IconName::ZedAgent, icon_from_external_svg: None, @@ -653,14 +651,14 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) { }), // Background thread that completed (should show notification) ListEntry::Thread(ThreadEntry { - agent: Agent::NativeAgent, - session_info: acp_thread::AgentSessionInfo { + metadata: ThreadMetadata { session_id: acp::SessionId::new(Arc::from("t-5")), - work_dirs: None, - title: Some("Notified thread".into()), - updated_at: Some(Utc::now()), + agent_id: AgentId::new("zed-agent"), + folder_paths: PathList::default(), + title: "Notified thread".into(), + updated_at: Utc::now(), created_at: Some(Utc::now()), - meta: None, + archived: false, }, icon: IconName::ZedAgent, icon_from_external_svg: None, @@ -1919,14 +1917,14 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) { sidebar.update_in(cx, |sidebar, window, cx| { sidebar.activate_thread( - Agent::NativeAgent, - acp_thread::AgentSessionInfo { + ThreadMetadata { session_id: session_id_a.clone(), - work_dirs: None, - title: Some("Test".into()), - updated_at: None, + agent_id: agent::ZED_AGENT_ID.clone(), + title: "Test".into(), + updated_at: Utc::now(), created_at: None, - meta: None, + folder_paths: PathList::default(), + archived: false, }, &workspace_a, window, @@ -1974,14 +1972,14 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) { // which also triggers a workspace switch. sidebar.update_in(cx, |sidebar, window, cx| { sidebar.activate_thread( - Agent::NativeAgent, - acp_thread::AgentSessionInfo { + ThreadMetadata { session_id: session_id_b.clone(), - work_dirs: None, - title: Some("Thread B".into()), - updated_at: None, + agent_id: agent::ZED_AGENT_ID.clone(), + title: "Thread B".into(), + updated_at: Utc::now(), created_at: None, - meta: None, + folder_paths: PathList::default(), + archived: false, }, &workspace_b, window, @@ -3231,24 +3229,14 @@ async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_proje ); } ListEntry::Thread(thread) - if thread - .session_info - .title - .as_ref() - .map(|title| title.as_ref()) - == Some("WT Thread") + if thread.metadata.title.as_ref() == "WT Thread" && thread.worktrees.first().map(|wt| wt.name.as_ref()) == Some("wt-feature-a") => { saw_expected_thread = true; } ListEntry::Thread(thread) => { - let title = thread - .session_info - .title - .as_ref() - .map(|title| title.as_ref()) - .unwrap_or("Untitled"); + let title = thread.metadata.title.as_ref(); let worktree_name = thread .worktrees .first() @@ -3446,14 +3434,14 @@ async fn test_activate_archived_thread_with_saved_paths_activates_matching_works // switch to the workspace for project-b. sidebar.update_in(cx, |sidebar, window, cx| { sidebar.activate_archived_thread( - Agent::NativeAgent, - acp_thread::AgentSessionInfo { + ThreadMetadata { session_id: session_id.clone(), - work_dirs: Some(PathList::new(&[PathBuf::from("/project-b")])), - title: Some("Archived Thread".into()), - updated_at: None, + agent_id: agent::ZED_AGENT_ID.clone(), + title: "Archived Thread".into(), + updated_at: Utc::now(), created_at: None, - meta: None, + folder_paths: PathList::new(&[PathBuf::from("/project-b")]), + archived: false, }, window, cx, @@ -3507,14 +3495,14 @@ 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( - Agent::NativeAgent, - acp_thread::AgentSessionInfo { + ThreadMetadata { session_id: acp::SessionId::new(Arc::from("unknown-session")), - work_dirs: Some(PathList::new(&[std::path::PathBuf::from("/project-b")])), - title: Some("CWD Thread".into()), - updated_at: None, + agent_id: agent::ZED_AGENT_ID.clone(), + title: "CWD Thread".into(), + updated_at: Utc::now(), created_at: None, - meta: None, + folder_paths: PathList::new(&[std::path::PathBuf::from("/project-b")]), + archived: false, }, window, cx, @@ -3568,14 +3556,14 @@ 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( - Agent::NativeAgent, - acp_thread::AgentSessionInfo { + ThreadMetadata { session_id: acp::SessionId::new(Arc::from("no-context-session")), - work_dirs: None, - title: Some("Contextless Thread".into()), - updated_at: None, + agent_id: agent::ZED_AGENT_ID.clone(), + title: "Contextless Thread".into(), + updated_at: Utc::now(), created_at: None, - meta: None, + folder_paths: PathList::default(), + archived: false, }, window, cx, @@ -3622,14 +3610,14 @@ async fn test_activate_archived_thread_saved_paths_opens_new_workspace(cx: &mut sidebar.update_in(cx, |sidebar, window, cx| { sidebar.activate_archived_thread( - Agent::NativeAgent, - acp_thread::AgentSessionInfo { + ThreadMetadata { session_id: session_id.clone(), - work_dirs: Some(path_list_b), - title: Some("New WS Thread".into()), - updated_at: None, + agent_id: agent::ZED_AGENT_ID.clone(), + title: "New WS Thread".into(), + updated_at: Utc::now(), created_at: None, - meta: None, + folder_paths: path_list_b, + archived: false, }, window, cx, @@ -3671,14 +3659,14 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window(cx: &m sidebar.update_in(cx_a, |sidebar, window, cx| { sidebar.activate_archived_thread( - Agent::NativeAgent, - acp_thread::AgentSessionInfo { + ThreadMetadata { session_id: session_id.clone(), - work_dirs: Some(PathList::new(&[PathBuf::from("/project-b")])), - title: Some("Cross Window Thread".into()), - updated_at: None, + agent_id: agent::ZED_AGENT_ID.clone(), + title: "Cross Window Thread".into(), + updated_at: Utc::now(), created_at: None, - meta: None, + folder_paths: PathList::new(&[PathBuf::from("/project-b")]), + archived: false, }, window, cx, @@ -3747,14 +3735,14 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window_with_t sidebar_a.update_in(cx_a, |sidebar, window, cx| { sidebar.activate_archived_thread( - Agent::NativeAgent, - acp_thread::AgentSessionInfo { + ThreadMetadata { session_id: session_id.clone(), - work_dirs: Some(PathList::new(&[PathBuf::from("/project-b")])), - title: Some("Cross Window Thread".into()), - updated_at: None, + agent_id: agent::ZED_AGENT_ID.clone(), + title: "Cross Window Thread".into(), + updated_at: Utc::now(), created_at: None, - meta: None, + folder_paths: PathList::new(&[PathBuf::from("/project-b")]), + archived: false, }, window, cx, @@ -3822,14 +3810,14 @@ async fn test_activate_archived_thread_prefers_current_window_for_matching_paths sidebar_a.update_in(cx_a, |sidebar, window, cx| { sidebar.activate_archived_thread( - Agent::NativeAgent, - acp_thread::AgentSessionInfo { + ThreadMetadata { session_id: session_id.clone(), - work_dirs: Some(PathList::new(&[PathBuf::from("/project-a")])), - title: Some("Current Window Thread".into()), - updated_at: None, + agent_id: agent::ZED_AGENT_ID.clone(), + title: "Current Window Thread".into(), + updated_at: Utc::now(), created_at: None, - meta: None, + folder_paths: PathList::new(&[PathBuf::from("/project-a")]), + archived: false, }, window, cx, diff --git a/crates/sidebar/src/thread_switcher.rs b/crates/sidebar/src/thread_switcher.rs index 56767a5adfc6c73ac21b8968b7ed35587faa36f4..a7f6584896c197374d02c7180f5d7bf19844daa7 100644 --- a/crates/sidebar/src/thread_switcher.rs +++ b/crates/sidebar/src/thread_switcher.rs @@ -1,7 +1,6 @@ -use acp_thread; use action_log::DiffStats; use agent_client_protocol as acp; -use agent_ui::Agent; +use agent_ui::thread_metadata_store::ThreadMetadata; use gpui::{ Action as _, Animation, AnimationExt, AnyElement, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Modifiers, ModifiersChangedEvent, Render, SharedString, @@ -23,8 +22,7 @@ pub(crate) struct ThreadSwitcherEntry { pub icon: IconName, pub icon_from_external_svg: Option, pub status: AgentThreadStatus, - pub agent: Agent, - pub session_info: acp_thread::AgentSessionInfo, + pub metadata: ThreadMetadata, pub workspace: Entity, pub worktree_name: Option, pub diff_stats: DiffStats, @@ -35,13 +33,11 @@ pub(crate) struct ThreadSwitcherEntry { pub(crate) enum ThreadSwitcherEvent { Preview { - agent: Agent, - session_info: acp_thread::AgentSessionInfo, + metadata: ThreadMetadata, workspace: Entity, }, Confirmed { - agent: Agent, - session_info: acp_thread::AgentSessionInfo, + metadata: ThreadMetadata, workspace: Entity, }, Dismissed, @@ -72,8 +68,7 @@ impl ThreadSwitcher { if let Some(entry) = entries.get(selected_index) { cx.emit(ThreadSwitcherEvent::Preview { - agent: entry.agent.clone(), - session_info: entry.session_info.clone(), + metadata: entry.metadata.clone(), workspace: entry.workspace.clone(), }); } @@ -130,8 +125,7 @@ impl ThreadSwitcher { fn emit_preview(&mut self, cx: &mut Context) { if let Some(entry) = self.entries.get(self.selected_index) { cx.emit(ThreadSwitcherEvent::Preview { - agent: entry.agent.clone(), - session_info: entry.session_info.clone(), + metadata: entry.metadata.clone(), workspace: entry.workspace.clone(), }); } @@ -140,8 +134,7 @@ impl ThreadSwitcher { fn confirm(&mut self, _: &menu::Confirm, _window: &mut gpui::Window, cx: &mut Context) { if let Some(entry) = self.entries.get(self.selected_index) { cx.emit(ThreadSwitcherEvent::Confirmed { - agent: entry.agent.clone(), - session_info: entry.session_info.clone(), + metadata: entry.metadata.clone(), workspace: entry.workspace.clone(), }); }