@@ -3,7 +3,7 @@ use std::{path::Path, sync::Arc};
use acp_thread::AgentSessionInfo;
use agent::{ThreadStore, ZED_AGENT_ID};
use agent_client_protocol as acp;
-use anyhow::Result;
+use anyhow::{Context as _, Result};
use chrono::{DateTime, Utc};
use collections::HashMap;
use db::{
@@ -14,9 +14,11 @@ use db::{
sqlez_macros::sql,
};
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
+use futures::{FutureExt as _, future::Shared};
use gpui::{AppContext as _, Entity, Global, Subscription, Task};
use project::AgentId;
use ui::{App, Context, SharedString};
+use util::ResultExt as _;
use workspace::PathList;
pub fn init(cx: &mut App) {
@@ -37,35 +39,39 @@ pub fn init(cx: &mut App) {
///
/// TODO: Remove this after N weeks of shipping the sidebar
fn migrate_thread_metadata(cx: &mut App) {
- SidebarThreadMetadataStore::global(cx).update(cx, |store, cx| {
- let list = store.list(cx);
- cx.spawn(async move |this, cx| {
- let Ok(list) = list.await else {
- return;
- };
- if list.is_empty() {
- this.update(cx, |this, cx| {
- let metadata = ThreadStore::global(cx)
- .read(cx)
- .entries()
- .map(|entry| ThreadMetadata {
- session_id: entry.id,
- agent_id: None,
- title: entry.title,
- updated_at: entry.updated_at,
- created_at: entry.created_at,
- folder_paths: entry.folder_paths,
- })
- .collect::<Vec<_>>();
- for entry in metadata {
- this.save(entry, cx).detach_and_log_err(cx);
- }
+ let store = SidebarThreadMetadataStore::global(cx);
+ let db = store.read(cx).db.clone();
+
+ cx.spawn(async move |cx| {
+ if !db.is_empty()? {
+ return Ok::<(), anyhow::Error>(());
+ }
+
+ let metadata = store.read_with(cx, |_store, app| {
+ ThreadStore::global(app)
+ .read(app)
+ .entries()
+ .map(|entry| ThreadMetadata {
+ session_id: entry.id,
+ agent_id: None,
+ title: entry.title,
+ updated_at: entry.updated_at,
+ created_at: entry.created_at,
+ folder_paths: entry.folder_paths,
})
- .ok();
- }
- })
- .detach();
- });
+ .collect::<Vec<_>>()
+ });
+
+ // Manually save each entry to the database and call reload, otherwise
+ // we'll end up triggering lots of reloads after each save
+ for entry in metadata {
+ db.save(entry).await?;
+ }
+
+ let _ = store.update(cx, |store, cx| store.reload(cx));
+ Ok(())
+ })
+ .detach_and_log_err(cx);
}
struct GlobalThreadMetadataStore(Entity<SidebarThreadMetadataStore>);
@@ -146,6 +152,9 @@ impl ThreadMetadata {
/// Automatically listens to AcpThread events and updates metadata if it has changed.
pub struct SidebarThreadMetadataStore {
db: ThreadMetadataDb,
+ threads: Vec<ThreadMetadata>,
+ threads_by_paths: HashMap<PathList, Vec<ThreadMetadata>>,
+ reload_task: Option<Shared<Task<()>>>,
session_subscriptions: HashMap<acp::SessionId, Subscription>,
}
@@ -180,20 +189,61 @@ impl SidebarThreadMetadataStore {
cx.global::<GlobalThreadMetadataStore>().0.clone()
}
- pub fn list_ids(&self, cx: &App) -> Task<Result<Vec<acp::SessionId>>> {
- let db = self.db.clone();
- cx.background_spawn(async move {
- let s = db.list_ids()?;
- Ok(s)
- })
+ pub fn is_empty(&self) -> bool {
+ self.threads.is_empty()
+ }
+
+ pub fn entries(&self) -> impl Iterator<Item = ThreadMetadata> + '_ {
+ self.threads.iter().cloned()
+ }
+
+ pub fn entry_ids(&self) -> impl Iterator<Item = acp::SessionId> + '_ {
+ self.threads.iter().map(|thread| thread.session_id.clone())
+ }
+
+ pub fn entries_for_path(
+ &self,
+ path_list: &PathList,
+ ) -> impl Iterator<Item = ThreadMetadata> + '_ {
+ self.threads_by_paths
+ .get(path_list)
+ .into_iter()
+ .flatten()
+ .cloned()
}
- pub fn list(&self, cx: &App) -> Task<Result<Vec<ThreadMetadata>>> {
+ fn reload(&mut self, cx: &mut Context<Self>) -> Shared<Task<()>> {
let db = self.db.clone();
- cx.background_spawn(async move {
- let s = db.list()?;
- Ok(s)
- })
+ self.reload_task.take();
+
+ let list_task = cx
+ .background_spawn(async move { db.list().context("Failed to fetch sidebar metadata") });
+
+ let reload_task = cx
+ .spawn(async move |this, cx| {
+ let Some(rows) = list_task.await.log_err() else {
+ return;
+ };
+
+ this.update(cx, |this, cx| {
+ this.threads.clear();
+ this.threads_by_paths.clear();
+
+ for row in rows {
+ this.threads_by_paths
+ .entry(row.folder_paths.clone())
+ .or_default()
+ .push(row.clone());
+ this.threads.push(row);
+ }
+
+ cx.notify();
+ })
+ .ok();
+ })
+ .shared();
+ self.reload_task = Some(reload_task.clone());
+ reload_task
}
pub fn save(&mut self, metadata: ThreadMetadata, cx: &mut Context<Self>) -> Task<Result<()>> {
@@ -204,7 +254,9 @@ impl SidebarThreadMetadataStore {
let db = self.db.clone();
cx.spawn(async move |this, cx| {
db.save(metadata).await?;
- this.update(cx, |_this, cx| cx.notify())
+ let reload_task = this.update(cx, |this, cx| this.reload(cx))?;
+ reload_task.await;
+ Ok(())
})
}
@@ -220,7 +272,9 @@ impl SidebarThreadMetadataStore {
let db = self.db.clone();
cx.spawn(async move |this, cx| {
db.delete(session_id).await?;
- this.update(cx, |_this, cx| cx.notify())
+ let reload_task = this.update(cx, |this, cx| this.reload(cx))?;
+ reload_task.await;
+ Ok(())
})
}
@@ -257,10 +311,15 @@ impl SidebarThreadMetadataStore {
})
.detach();
- Self {
+ let mut this = Self {
db,
+ threads: Vec::new(),
+ threads_by_paths: HashMap::default(),
+ reload_task: None,
session_subscriptions: HashMap::default(),
- }
+ };
+ let _ = this.reload(cx);
+ this
}
fn handle_thread_update(
@@ -309,10 +368,9 @@ impl Domain for ThreadMetadataDb {
db::static_connection!(ThreadMetadataDb, []);
impl ThreadMetadataDb {
- /// List all sidebar thread session IDs.
- pub fn list_ids(&self) -> anyhow::Result<Vec<acp::SessionId>> {
- self.select::<Arc<str>>("SELECT session_id FROM sidebar_threads")?()
- .map(|ids| ids.into_iter().map(|id| acp::SessionId::new(id)).collect())
+ pub fn is_empty(&self) -> anyhow::Result<bool> {
+ self.select::<i64>("SELECT COUNT(*) FROM sidebar_threads")?()
+ .map(|counts| counts.into_iter().next().unwrap_or_default() == 0)
}
/// List all sidebar thread metadata, ordered by updated_at descending.
@@ -427,7 +485,6 @@ mod tests {
use project::Project;
use std::path::Path;
use std::rc::Rc;
- use util::path_list::PathList;
fn make_db_thread(title: &str, updated_at: DateTime<Utc>) -> DbThread {
DbThread {
@@ -450,6 +507,207 @@ mod tests {
}
}
+ fn make_metadata(
+ session_id: &str,
+ title: &str,
+ updated_at: DateTime<Utc>,
+ folder_paths: PathList,
+ ) -> ThreadMetadata {
+ ThreadMetadata {
+ session_id: acp::SessionId::new(session_id),
+ agent_id: None,
+ title: title.to_string().into(),
+ updated_at,
+ created_at: Some(updated_at),
+ folder_paths,
+ }
+ }
+
+ #[gpui::test]
+ async fn test_store_initializes_cache_from_database(cx: &mut TestAppContext) {
+ let first_paths = PathList::new(&[Path::new("/project-a")]);
+ let second_paths = PathList::new(&[Path::new("/project-b")]);
+ let now = Utc::now();
+ let older = now - chrono::Duration::seconds(1);
+
+ let thread = std::thread::current();
+ let test_name = thread.name().unwrap_or("unknown_test");
+ let db_name = format!("THREAD_METADATA_DB_{}", test_name);
+ let db = ThreadMetadataDb(smol::block_on(db::open_test_db::<ThreadMetadataDb>(
+ &db_name,
+ )));
+
+ db.save(make_metadata(
+ "session-1",
+ "First Thread",
+ now,
+ first_paths.clone(),
+ ))
+ .await
+ .unwrap();
+ db.save(make_metadata(
+ "session-2",
+ "Second Thread",
+ older,
+ second_paths.clone(),
+ ))
+ .await
+ .unwrap();
+
+ cx.update(|cx| {
+ let settings_store = settings::SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ cx.update_flags(true, vec!["agent-v2".to_string()]);
+ SidebarThreadMetadataStore::init_global(cx);
+ });
+
+ cx.run_until_parked();
+
+ cx.update(|cx| {
+ let store = SidebarThreadMetadataStore::global(cx);
+ let store = store.read(cx);
+
+ let entry_ids = store
+ .entry_ids()
+ .map(|session_id| session_id.0.to_string())
+ .collect::<Vec<_>>();
+ assert_eq!(entry_ids, vec!["session-1", "session-2"]);
+
+ let first_path_entries = store
+ .entries_for_path(&first_paths)
+ .map(|entry| entry.session_id.0.to_string())
+ .collect::<Vec<_>>();
+ assert_eq!(first_path_entries, vec!["session-1"]);
+
+ let second_path_entries = store
+ .entries_for_path(&second_paths)
+ .map(|entry| entry.session_id.0.to_string())
+ .collect::<Vec<_>>();
+ assert_eq!(second_path_entries, vec!["session-2"]);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_store_cache_updates_after_save_and_delete(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let settings_store = settings::SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ cx.update_flags(true, vec!["agent-v2".to_string()]);
+ SidebarThreadMetadataStore::init_global(cx);
+ });
+
+ let first_paths = PathList::new(&[Path::new("/project-a")]);
+ let second_paths = PathList::new(&[Path::new("/project-b")]);
+ let initial_time = Utc::now();
+ let updated_time = initial_time + chrono::Duration::seconds(1);
+
+ let initial_metadata = make_metadata(
+ "session-1",
+ "First Thread",
+ initial_time,
+ first_paths.clone(),
+ );
+
+ let second_metadata = make_metadata(
+ "session-2",
+ "Second Thread",
+ initial_time,
+ second_paths.clone(),
+ );
+
+ cx.update(|cx| {
+ let store = SidebarThreadMetadataStore::global(cx);
+ store.update(cx, |store, cx| {
+ store.save(initial_metadata, cx).detach();
+ store.save(second_metadata, cx).detach();
+ });
+ });
+
+ cx.run_until_parked();
+
+ cx.update(|cx| {
+ let store = SidebarThreadMetadataStore::global(cx);
+ let store = store.read(cx);
+
+ let first_path_entries = store
+ .entries_for_path(&first_paths)
+ .map(|entry| entry.session_id.0.to_string())
+ .collect::<Vec<_>>();
+ assert_eq!(first_path_entries, vec!["session-1"]);
+
+ let second_path_entries = store
+ .entries_for_path(&second_paths)
+ .map(|entry| entry.session_id.0.to_string())
+ .collect::<Vec<_>>();
+ assert_eq!(second_path_entries, vec!["session-2"]);
+ });
+
+ let moved_metadata = make_metadata(
+ "session-1",
+ "First Thread",
+ updated_time,
+ second_paths.clone(),
+ );
+
+ cx.update(|cx| {
+ let store = SidebarThreadMetadataStore::global(cx);
+ store.update(cx, |store, cx| {
+ store.save(moved_metadata, cx).detach();
+ });
+ });
+
+ cx.run_until_parked();
+
+ cx.update(|cx| {
+ let store = SidebarThreadMetadataStore::global(cx);
+ let store = store.read(cx);
+
+ let entry_ids = store
+ .entry_ids()
+ .map(|session_id| session_id.0.to_string())
+ .collect::<Vec<_>>();
+ assert_eq!(entry_ids, vec!["session-1", "session-2"]);
+
+ let first_path_entries = store
+ .entries_for_path(&first_paths)
+ .map(|entry| entry.session_id.0.to_string())
+ .collect::<Vec<_>>();
+ assert!(first_path_entries.is_empty());
+
+ let second_path_entries = store
+ .entries_for_path(&second_paths)
+ .map(|entry| entry.session_id.0.to_string())
+ .collect::<Vec<_>>();
+ assert_eq!(second_path_entries, vec!["session-1", "session-2"]);
+ });
+
+ cx.update(|cx| {
+ let store = SidebarThreadMetadataStore::global(cx);
+ store.update(cx, |store, cx| {
+ store.delete(acp::SessionId::new("session-2"), cx).detach();
+ });
+ });
+
+ cx.run_until_parked();
+
+ cx.update(|cx| {
+ let store = SidebarThreadMetadataStore::global(cx);
+ let store = store.read(cx);
+
+ let entry_ids = store
+ .entry_ids()
+ .map(|session_id| session_id.0.to_string())
+ .collect::<Vec<_>>();
+ assert_eq!(entry_ids, vec!["session-1"]);
+
+ let second_path_entries = store
+ .entries_for_path(&second_paths)
+ .map(|entry| entry.session_id.0.to_string())
+ .collect::<Vec<_>>();
+ assert_eq!(second_path_entries, vec!["session-1"]);
+ });
+ }
+
#[gpui::test]
async fn test_migrate_thread_metadata(cx: &mut TestAppContext) {
cx.update(|cx| {
@@ -457,13 +715,11 @@ mod tests {
SidebarThreadMetadataStore::init_global(cx);
});
- // Verify the list is empty before migration
- let metadata_list = cx.update(|cx| {
+ // Verify the cache is empty before migration
+ let list = cx.update(|cx| {
let store = SidebarThreadMetadataStore::global(cx);
- store.read(cx).list(cx)
+ store.read(cx).entries().collect::<Vec<_>>()
});
-
- let list = metadata_list.await.unwrap();
assert_eq!(list.len(), 0);
let now = Utc::now();
@@ -505,12 +761,10 @@ mod tests {
cx.run_until_parked();
// Verify the metadata was migrated
- let metadata_list = cx.update(|cx| {
+ let list = cx.update(|cx| {
let store = SidebarThreadMetadataStore::global(cx);
- store.read(cx).list(cx)
+ store.read(cx).entries().collect::<Vec<_>>()
});
-
- let list = metadata_list.await.unwrap();
assert_eq!(list.len(), 2);
let metadata1 = list
@@ -577,12 +831,10 @@ mod tests {
cx.run_until_parked();
// Verify only the existing metadata is present (migration was skipped)
- let metadata_list = cx.update(|cx| {
+ let list = cx.update(|cx| {
let store = SidebarThreadMetadataStore::global(cx);
- store.read(cx).list(cx)
+ store.read(cx).entries().collect::<Vec<_>>()
});
-
- let list = metadata_list.await.unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].session_id.0.as_ref(), "existing-session");
}
@@ -650,14 +902,12 @@ mod tests {
});
cx.run_until_parked();
- // List all metadata from the store.
- let metadata_list = cx.update(|cx| {
+ // List all metadata from the store cache.
+ let list = cx.update(|cx| {
let store = SidebarThreadMetadataStore::global(cx);
- store.read(cx).list(cx)
+ store.read(cx).entries().collect::<Vec<_>>()
});
- let list = metadata_list.await.unwrap();
-
// The subagent thread should NOT appear in the sidebar metadata.
// Only the regular thread should be listed.
assert_eq!(
@@ -242,11 +242,9 @@ pub struct Sidebar {
hovered_thread_index: Option<usize>,
collapsed_groups: HashSet<PathList>,
expanded_groups: HashMap<PathList, usize>,
- threads_by_paths: HashMap<PathList, Vec<ThreadMetadata>>,
view: SidebarView,
recent_projects_popover_handle: PopoverMenuHandle<RecentProjects>,
_subscriptions: Vec<gpui::Subscription>,
- _list_threads_task: Option<gpui::Task<()>>,
_draft_observation: Option<gpui::Subscription>,
}
@@ -303,7 +301,7 @@ impl Sidebar {
cx.observe(
&SidebarThreadMetadataStore::global(cx),
|this, _store, cx| {
- this.list_threads(cx);
+ this.update_entries(cx);
},
)
.detach();
@@ -321,8 +319,7 @@ impl Sidebar {
this.update_entries(cx);
});
- let mut this = Self {
- _list_threads_task: None,
+ Self {
multi_workspace: multi_workspace.downgrade(),
width: DEFAULT_WIDTH,
focus_handle,
@@ -335,14 +332,11 @@ impl Sidebar {
hovered_thread_index: None,
collapsed_groups: HashSet::new(),
expanded_groups: HashMap::new(),
- threads_by_paths: HashMap::new(),
view: SidebarView::default(),
recent_projects_popover_handle: PopoverMenuHandle::default(),
_subscriptions: Vec::new(),
_draft_observation: None,
- };
- this.list_threads(cx);
- this
+ }
}
fn subscribe_to_workspace(
@@ -687,46 +681,47 @@ impl Sidebar {
if should_load_threads {
let mut seen_session_ids: HashSet<acp::SessionId> = HashSet::new();
- // Read threads from SidebarDb for this workspace's path list.
- if let Some(rows) = self.threads_by_paths.get(&path_list) {
- for row in rows {
- seen_session_ids.insert(row.session_id.clone());
- let (agent, icon, icon_from_external_svg) = match &row.agent_id {
- None => (Agent::NativeAgent, IconName::ZedAgent, None),
- Some(id) => {
- let custom_icon = agent_server_store
- .as_ref()
- .and_then(|store| store.read(cx).agent_icon(&id));
- (
- Agent::Custom { id: id.clone() },
- IconName::Terminal,
- custom_icon,
- )
- }
- };
- 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,
- },
- icon,
- icon_from_external_svg,
- status: AgentThreadStatus::default(),
- workspace: ThreadEntryWorkspace::Open(workspace.clone()),
- is_live: false,
- is_background: false,
- is_title_generating: false,
- highlight_positions: Vec::new(),
- worktree_name: None,
- worktree_highlight_positions: Vec::new(),
- diff_stats: DiffStats::default(),
- });
- }
+ // Read threads from the store cache for this workspace's path list.
+ let thread_store = SidebarThreadMetadataStore::global(cx);
+ let workspace_rows: Vec<_> =
+ thread_store.read(cx).entries_for_path(&path_list).collect();
+ for row in workspace_rows {
+ seen_session_ids.insert(row.session_id.clone());
+ let (agent, icon, icon_from_external_svg) = match &row.agent_id {
+ None => (Agent::NativeAgent, IconName::ZedAgent, None),
+ Some(id) => {
+ let custom_icon = agent_server_store
+ .as_ref()
+ .and_then(|store| store.read(cx).agent_icon(&id));
+ (
+ Agent::Custom { id: id.clone() },
+ IconName::Terminal,
+ custom_icon,
+ )
+ }
+ };
+ 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,
+ },
+ icon,
+ icon_from_external_svg,
+ status: AgentThreadStatus::default(),
+ workspace: ThreadEntryWorkspace::Open(workspace.clone()),
+ is_live: false,
+ is_background: false,
+ is_title_generating: false,
+ highlight_positions: Vec::new(),
+ worktree_name: None,
+ worktree_highlight_positions: Vec::new(),
+ diff_stats: DiffStats::default(),
+ });
}
// Load threads from linked git worktrees of this workspace's repos.
@@ -767,52 +762,52 @@ impl Sidebar {
None => ThreadEntryWorkspace::Closed(worktree_path_list.clone()),
};
- if let Some(rows) = self.threads_by_paths.get(worktree_path_list) {
- for row in rows {
- if !seen_session_ids.insert(row.session_id.clone()) {
- continue;
- }
- let (agent, icon, icon_from_external_svg) = match &row.agent_id {
- None => (Agent::NativeAgent, IconName::ZedAgent, None),
- Some(name) => {
- let custom_icon =
- agent_server_store.as_ref().and_then(|store| {
- store
- .read(cx)
- .agent_icon(&AgentId(name.clone().into()))
- });
- (
- Agent::Custom {
- id: AgentId::new(name.clone()),
- },
- IconName::Terminal,
- custom_icon,
- )
- }
- };
- 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,
- },
- icon,
- icon_from_external_svg,
- status: AgentThreadStatus::default(),
- workspace: target_workspace.clone(),
- is_live: false,
- is_background: false,
- is_title_generating: false,
- highlight_positions: Vec::new(),
- worktree_name: Some(worktree_name.clone()),
- worktree_highlight_positions: Vec::new(),
- diff_stats: DiffStats::default(),
- });
+ let worktree_rows: Vec<_> = thread_store
+ .read(cx)
+ .entries_for_path(worktree_path_list)
+ .collect();
+ for row in worktree_rows {
+ if !seen_session_ids.insert(row.session_id.clone()) {
+ continue;
}
+ let (agent, icon, icon_from_external_svg) = match &row.agent_id {
+ None => (Agent::NativeAgent, IconName::ZedAgent, None),
+ Some(name) => {
+ let custom_icon =
+ agent_server_store.as_ref().and_then(|store| {
+ store.read(cx).agent_icon(&AgentId(name.clone().into()))
+ });
+ (
+ Agent::Custom {
+ id: AgentId::new(name.clone()),
+ },
+ IconName::Terminal,
+ custom_icon,
+ )
+ }
+ };
+ 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,
+ },
+ icon,
+ icon_from_external_svg,
+ status: AgentThreadStatus::default(),
+ workspace: target_workspace.clone(),
+ is_live: false,
+ is_background: false,
+ is_title_generating: false,
+ highlight_positions: Vec::new(),
+ worktree_name: Some(worktree_name.clone()),
+ worktree_highlight_positions: Vec::new(),
+ diff_stats: DiffStats::default(),
+ });
}
}
}
@@ -1010,30 +1005,7 @@ impl Sidebar {
};
}
- fn list_threads(&mut self, cx: &mut Context<Self>) {
- let list_task = SidebarThreadMetadataStore::global(cx).read(cx).list(cx);
- self._list_threads_task = Some(cx.spawn(async move |this, cx| {
- let Some(thread_entries) = list_task.await.log_err() else {
- return;
- };
- this.update(cx, |this, 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);
- }
- this.threads_by_paths = threads_by_paths;
- this.update_entries(cx);
- })
- .ok();
- }));
- }
-
/// Rebuilds the sidebar's visible entries from already-cached state.
- /// Data fetching happens elsewhere (e.g. `list_threads`); this just
- /// re-derives the entry list, list state, and notifications.
fn update_entries(&mut self, cx: &mut Context<Self>) {
let Some(multi_workspace) = self.multi_workspace.upgrade() else {
return;