sidebar: Remove AgentSessionInfo usage (#52732)

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)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-03-30 18:33:55 +02:00
1 parent 8ae53240a4
commit 8d39d9e4e0
4 files changed
+226 -359

No files matched your search

@@ -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,
+112 -239
View File
@@ -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<Workspace>),
@@ -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<SharedString>,
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<SharedString>) {
let resolve_agent_icon = |agent_id: &AgentId| -> (IconName, Option<SharedString>) {
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<ThreadEntry> = 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<Workspace>,
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::<AgentPanel>(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<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -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<Workspace>,
target_window: WindowHandle<MultiWorkspace>,
cx: &mut Context<Self>,
) {
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<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -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<Self>,
@@ -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<Self>,
) {
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::<AgentPanel>(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
let original_metadata = 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
.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::<AgentPanel>(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);
.is_thread_notified(&thread.metadata.session_id);
let title: SharedString = thread
.session_info
.title
.clone()
.unwrap_or_else(|| "Untitled".into());
let session_info = thread.session_info.clone();
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);
}
},
);
+94 -106
View File
@@ -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,
+7 -14
View File
@@ -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<SharedString>,
pub status: AgentThreadStatus,
pub agent: Agent,
pub session_info: acp_thread::AgentSessionInfo,
pub metadata: ThreadMetadata,
pub workspace: Entity<Workspace>,
pub worktree_name: Option<SharedString>,
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<Workspace>,
},
Confirmed {
agent: Agent,
session_info: acp_thread::AgentSessionInfo,
metadata: ThreadMetadata,
workspace: Entity<Workspace>,
},
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<Self>) {
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<Self>) {
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(),
});
}