From 20368bae44cd2a91bb6fd72045a424593dd9c550 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 1 Apr 2026 18:28:50 -0700 Subject: [PATCH] Collect main worktree paths on threads, and query them in the sidebar (#52945) This PR has us collecting the main worktree path metadata for the sidebar, allows us to query by that path, and in the interim adds in logic for querying the main worktree paths. This means that, as this change percolates, there shouldn't be much difference between opening a workspace to a linked worktree, and opening a workspace to the main worktree. 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 --- crates/agent_ui/src/thread_import.rs | 3 +- crates/agent_ui/src/thread_metadata_store.rs | 126 ++++++++++++++-- crates/sidebar/src/sidebar.rs | 45 ++++++ crates/sidebar/src/sidebar_tests.rs | 145 ++++++++++++++++++- 4 files changed, 302 insertions(+), 17 deletions(-) diff --git a/crates/agent_ui/src/thread_import.rs b/crates/agent_ui/src/thread_import.rs index f5fc89d3df4991ff5186e2af6d73ad6a840c09a1..5402b1c74353b73a522a068aa32dfd0a9dc85c60 100644 --- a/crates/agent_ui/src/thread_import.rs +++ b/crates/agent_ui/src/thread_import.rs @@ -17,7 +17,7 @@ use ui::{ prelude::*, }; use util::ResultExt; -use workspace::{ModalView, MultiWorkspace, Workspace}; +use workspace::{ModalView, MultiWorkspace, PathList, Workspace}; use crate::{ Agent, AgentPanel, @@ -500,6 +500,7 @@ fn collect_importable_threads( updated_at: session.updated_at.unwrap_or_else(|| Utc::now()), created_at: session.created_at, folder_paths, + main_worktree_paths: PathList::default(), archived: true, }); } diff --git a/crates/agent_ui/src/thread_metadata_store.rs b/crates/agent_ui/src/thread_metadata_store.rs index 4c66d57bcfafe98432319a173e7736a581f1d986..410403ba8d8ce618583d81c72205ee268d7b62f6 100644 --- a/crates/agent_ui/src/thread_metadata_store.rs +++ b/crates/agent_ui/src/thread_metadata_store.rs @@ -66,6 +66,7 @@ fn migrate_thread_metadata(cx: &mut App) { updated_at: entry.updated_at, created_at: entry.created_at, folder_paths: entry.folder_paths, + main_worktree_paths: PathList::default(), archived: true, }) }) @@ -126,6 +127,7 @@ pub struct ThreadMetadata { pub updated_at: DateTime, pub created_at: Option>, pub folder_paths: PathList, + pub main_worktree_paths: PathList, pub archived: bool, } @@ -149,6 +151,7 @@ pub struct ThreadMetadataStore { db: ThreadMetadataDb, threads: HashMap, threads_by_paths: HashMap>, + threads_by_main_paths: HashMap>, reload_task: Option>>, session_subscriptions: HashMap, pending_thread_ops_tx: smol::channel::Sender, @@ -238,6 +241,21 @@ impl ThreadMetadataStore { .filter(|s| !s.archived) } + /// Returns threads whose `main_worktree_paths` matches the given path list, + /// excluding archived threads. This finds threads that were opened in a + /// linked worktree but are associated with the given main worktree. + pub fn entries_for_main_worktree_path( + &self, + path_list: &PathList, + ) -> impl Iterator + '_ { + self.threads_by_main_paths + .get(path_list) + .into_iter() + .flatten() + .filter_map(|s| self.threads.get(s)) + .filter(|s| !s.archived) + } + fn reload(&mut self, cx: &mut Context) -> Shared> { let db = self.db.clone(); self.reload_task.take(); @@ -254,12 +272,19 @@ impl ThreadMetadataStore { this.update(cx, |this, cx| { this.threads.clear(); this.threads_by_paths.clear(); + this.threads_by_main_paths.clear(); for row in rows { this.threads_by_paths .entry(row.folder_paths.clone()) .or_default() .insert(row.session_id.clone()); + if !row.main_worktree_paths.is_empty() { + this.threads_by_main_paths + .entry(row.main_worktree_paths.clone()) + .or_default() + .insert(row.session_id.clone()); + } this.threads.insert(row.session_id.clone(), row); } @@ -298,12 +323,22 @@ impl ThreadMetadataStore { } fn save_internal(&mut self, metadata: ThreadMetadata) { - // If the folder paths have changed, we need to clear the old entry - if let Some(thread) = self.threads.get(&metadata.session_id) - && thread.folder_paths != metadata.folder_paths - && let Some(session_ids) = self.threads_by_paths.get_mut(&thread.folder_paths) - { - session_ids.remove(&metadata.session_id); + if let Some(thread) = self.threads.get(&metadata.session_id) { + if thread.folder_paths != metadata.folder_paths { + if let Some(session_ids) = self.threads_by_paths.get_mut(&thread.folder_paths) { + session_ids.remove(&metadata.session_id); + } + } + if thread.main_worktree_paths != metadata.main_worktree_paths + && !thread.main_worktree_paths.is_empty() + { + if let Some(session_ids) = self + .threads_by_main_paths + .get_mut(&thread.main_worktree_paths) + { + session_ids.remove(&metadata.session_id); + } + } } self.threads @@ -314,6 +349,13 @@ impl ThreadMetadataStore { .or_default() .insert(metadata.session_id.clone()); + if !metadata.main_worktree_paths.is_empty() { + self.threads_by_main_paths + .entry(metadata.main_worktree_paths.clone()) + .or_default() + .insert(metadata.session_id.clone()); + } + self.pending_thread_ops_tx .try_send(DbOperation::Upsert(metadata)) .log_err(); @@ -370,10 +412,18 @@ impl ThreadMetadataStore { return; } - if let Some(thread) = self.threads.get(&session_id) - && let Some(session_ids) = self.threads_by_paths.get_mut(&thread.folder_paths) - { - session_ids.remove(&session_id); + if let Some(thread) = self.threads.get(&session_id) { + if let Some(session_ids) = self.threads_by_paths.get_mut(&thread.folder_paths) { + session_ids.remove(&session_id); + } + if !thread.main_worktree_paths.is_empty() { + if let Some(session_ids) = self + .threads_by_main_paths + .get_mut(&thread.main_worktree_paths) + { + session_ids.remove(&session_id); + } + } } self.threads.remove(&session_id); self.pending_thread_ops_tx @@ -449,6 +499,7 @@ impl ThreadMetadataStore { db, threads: HashMap::default(), threads_by_paths: HashMap::default(), + threads_by_main_paths: HashMap::default(), reload_task: None, session_subscriptions: HashMap::default(), pending_thread_ops_tx: tx, @@ -517,6 +568,20 @@ impl ThreadMetadataStore { PathList::new(&paths) }; + let main_worktree_paths = { + let project = thread_ref.project().read(cx); + let mut main_paths: Vec> = Vec::new(); + for repo in project.repositories(cx).values() { + let snapshot = repo.read(cx).snapshot(); + if snapshot.is_linked_worktree() { + main_paths.push(snapshot.original_repo_abs_path.clone()); + } + } + main_paths.sort(); + main_paths.dedup(); + PathList::new(&main_paths) + }; + // Threads without a folder path (e.g. started in an empty // window) are archived by default so they don't get lost, // because they won't show up in the sidebar. Users can reload @@ -532,6 +597,7 @@ impl ThreadMetadataStore { created_at: Some(created_at), updated_at, folder_paths, + main_worktree_paths, archived, }; @@ -567,6 +633,8 @@ impl Domain for ThreadMetadataDb { ) STRICT; ), sql!(ALTER TABLE sidebar_threads ADD COLUMN archived INTEGER DEFAULT 0), + sql!(ALTER TABLE sidebar_threads ADD COLUMN main_worktree_paths TEXT), + sql!(ALTER TABLE sidebar_threads ADD COLUMN main_worktree_paths_order TEXT), ]; } @@ -583,7 +651,7 @@ impl ThreadMetadataDb { /// List all sidebar thread metadata, ordered by updated_at descending. pub fn list(&self) -> anyhow::Result> { self.select::( - "SELECT session_id, agent_id, title, updated_at, created_at, folder_paths, folder_paths_order, archived \ + "SELECT session_id, agent_id, title, updated_at, created_at, folder_paths, folder_paths_order, archived, main_worktree_paths, main_worktree_paths_order \ FROM sidebar_threads \ ORDER BY updated_at DESC" )?() @@ -606,11 +674,18 @@ impl ThreadMetadataDb { } else { (Some(serialized.paths), Some(serialized.order)) }; + let main_serialized = row.main_worktree_paths.serialize(); + let (main_worktree_paths, main_worktree_paths_order) = if row.main_worktree_paths.is_empty() + { + (None, None) + } else { + (Some(main_serialized.paths), Some(main_serialized.order)) + }; let archived = row.archived; self.write(move |conn| { - let sql = "INSERT INTO sidebar_threads(session_id, agent_id, title, updated_at, created_at, folder_paths, folder_paths_order, archived) \ - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) \ + let sql = "INSERT INTO sidebar_threads(session_id, agent_id, title, updated_at, created_at, folder_paths, folder_paths_order, archived, main_worktree_paths, main_worktree_paths_order) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10) \ ON CONFLICT(session_id) DO UPDATE SET \ agent_id = excluded.agent_id, \ title = excluded.title, \ @@ -618,7 +693,9 @@ impl ThreadMetadataDb { created_at = excluded.created_at, \ folder_paths = excluded.folder_paths, \ folder_paths_order = excluded.folder_paths_order, \ - archived = excluded.archived"; + archived = excluded.archived, \ + main_worktree_paths = excluded.main_worktree_paths, \ + main_worktree_paths_order = excluded.main_worktree_paths_order"; let mut stmt = Statement::prepare(conn, sql)?; let mut i = stmt.bind(&id, 1)?; i = stmt.bind(&agent_id, i)?; @@ -627,7 +704,9 @@ impl ThreadMetadataDb { i = stmt.bind(&created_at, i)?; i = stmt.bind(&folder_paths, i)?; i = stmt.bind(&folder_paths_order, i)?; - stmt.bind(&archived, i)?; + i = stmt.bind(&archived, i)?; + i = stmt.bind(&main_worktree_paths, i)?; + stmt.bind(&main_worktree_paths_order, i)?; stmt.exec() }) .await @@ -657,6 +736,10 @@ impl Column for ThreadMetadata { let (folder_paths_order_str, next): (Option, i32) = Column::column(statement, next)?; let (archived, next): (bool, i32) = Column::column(statement, next)?; + let (main_worktree_paths_str, next): (Option, i32) = + Column::column(statement, next)?; + let (main_worktree_paths_order_str, next): (Option, i32) = + Column::column(statement, next)?; let agent_id = agent_id .map(|id| AgentId::new(id)) @@ -678,6 +761,15 @@ impl Column for ThreadMetadata { }) .unwrap_or_default(); + let main_worktree_paths = main_worktree_paths_str + .map(|paths| { + PathList::deserialize(&util::path_list::SerializedPathList { + paths, + order: main_worktree_paths_order_str.unwrap_or_default(), + }) + }) + .unwrap_or_default(); + Ok(( ThreadMetadata { session_id: acp::SessionId::new(id), @@ -686,6 +778,7 @@ impl Column for ThreadMetadata { updated_at, created_at, folder_paths, + main_worktree_paths, archived, }, next, @@ -742,6 +835,7 @@ mod tests { updated_at, created_at: Some(updated_at), folder_paths, + main_worktree_paths: PathList::default(), } } @@ -957,6 +1051,7 @@ mod tests { updated_at: now - chrono::Duration::seconds(10), created_at: Some(now - chrono::Duration::seconds(10)), folder_paths: project_a_paths.clone(), + main_worktree_paths: PathList::default(), archived: false, }; @@ -1066,6 +1161,7 @@ mod tests { updated_at: existing_updated_at, created_at: Some(existing_updated_at), folder_paths: project_paths.clone(), + main_worktree_paths: PathList::default(), archived: false, }; diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index cd9bfb4d7ab5e66dafd088999e484513c5074411..a9664a048123253d617a08507cfe4288914d0e9e 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -906,6 +906,51 @@ impl Sidebar { } } + // Load threads from main worktrees when a workspace in this + // group is itself a linked worktree checkout. + let main_repo_queries: Vec = group + .workspaces + .iter() + .flat_map(|ws| root_repository_snapshots(ws, cx)) + .filter(|snapshot| snapshot.is_linked_worktree()) + .map(|snapshot| { + PathList::new(std::slice::from_ref(&snapshot.original_repo_abs_path)) + }) + .collect(); + + for main_repo_path_list in main_repo_queries { + let folder_path_matches = thread_store + .read(cx) + .entries_for_path(&main_repo_path_list) + .cloned(); + let main_worktree_path_matches = thread_store + .read(cx) + .entries_for_main_worktree_path(&main_repo_path_list) + .cloned(); + + for row in folder_path_matches.chain(main_worktree_path_matches) { + if !seen_session_ids.insert(row.session_id.clone()) { + continue; + } + 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 { + metadata: row, + icon, + icon_from_external_svg, + status: AgentThreadStatus::default(), + workspace: ThreadEntryWorkspace::Closed(main_repo_path_list.clone()), + is_live: false, + is_background: false, + is_title_generating: false, + highlight_positions: Vec::new(), + worktrees, + diff_stats: DiffStats::default(), + }); + } + } + // Build a lookup from live_infos and compute running/waiting // counts in a single pass. let mut live_info_by_session: HashMap<&acp::SessionId, &ActiveThreadInfo> = diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index 72f0bbdc18aaeead26de62164fa64ebf3bb64e8d..1499fc48a9fd094b07d181701866ab941c5968f3 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -149,6 +149,7 @@ fn save_thread_metadata( updated_at, created_at, folder_paths: path_list, + main_worktree_paths: PathList::default(), archived: false, }; cx.update(|cx| { @@ -697,6 +698,7 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) { session_id: acp::SessionId::new(Arc::from("t-1")), agent_id: AgentId::new("zed-agent"), folder_paths: PathList::default(), + main_worktree_paths: PathList::default(), title: "Completed thread".into(), updated_at: Utc::now(), created_at: Some(Utc::now()), @@ -719,6 +721,7 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) { session_id: acp::SessionId::new(Arc::from("t-2")), agent_id: AgentId::new("zed-agent"), folder_paths: PathList::default(), + main_worktree_paths: PathList::default(), title: "Running thread".into(), updated_at: Utc::now(), created_at: Some(Utc::now()), @@ -741,6 +744,7 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) { session_id: acp::SessionId::new(Arc::from("t-3")), agent_id: AgentId::new("zed-agent"), folder_paths: PathList::default(), + main_worktree_paths: PathList::default(), title: "Error thread".into(), updated_at: Utc::now(), created_at: Some(Utc::now()), @@ -763,6 +767,7 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) { session_id: acp::SessionId::new(Arc::from("t-4")), agent_id: AgentId::new("zed-agent"), folder_paths: PathList::default(), + main_worktree_paths: PathList::default(), title: "Waiting thread".into(), updated_at: Utc::now(), created_at: Some(Utc::now()), @@ -785,6 +790,7 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) { session_id: acp::SessionId::new(Arc::from("t-5")), agent_id: AgentId::new("zed-agent"), folder_paths: PathList::default(), + main_worktree_paths: PathList::default(), title: "Notified thread".into(), updated_at: Utc::now(), created_at: Some(Utc::now()), @@ -2052,6 +2058,7 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) { updated_at: Utc::now(), created_at: None, folder_paths: PathList::default(), + main_worktree_paths: PathList::default(), archived: false, }, &workspace_a, @@ -2107,6 +2114,7 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) { updated_at: Utc::now(), created_at: None, folder_paths: PathList::default(), + main_worktree_paths: PathList::default(), archived: false, }, &workspace_b, @@ -3571,6 +3579,7 @@ async fn test_activate_archived_thread_with_saved_paths_activates_matching_works updated_at: Utc::now(), created_at: None, folder_paths: PathList::new(&[PathBuf::from("/project-b")]), + main_worktree_paths: PathList::default(), archived: false, }, window, @@ -3633,6 +3642,7 @@ async fn test_activate_archived_thread_cwd_fallback_with_matching_workspace( updated_at: Utc::now(), created_at: None, folder_paths: PathList::new(&[std::path::PathBuf::from("/project-b")]), + main_worktree_paths: PathList::default(), archived: false, }, window, @@ -3695,6 +3705,7 @@ async fn test_activate_archived_thread_no_paths_no_cwd_uses_active_workspace( updated_at: Utc::now(), created_at: None, folder_paths: PathList::default(), + main_worktree_paths: PathList::default(), archived: false, }, window, @@ -3749,6 +3760,7 @@ async fn test_activate_archived_thread_saved_paths_opens_new_workspace(cx: &mut updated_at: Utc::now(), created_at: None, folder_paths: path_list_b, + main_worktree_paths: PathList::default(), archived: false, }, window, @@ -3798,6 +3810,7 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window(cx: &m updated_at: Utc::now(), created_at: None, folder_paths: PathList::new(&[PathBuf::from("/project-b")]), + main_worktree_paths: PathList::default(), archived: false, }, window, @@ -3874,6 +3887,7 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window_with_t updated_at: Utc::now(), created_at: None, folder_paths: PathList::new(&[PathBuf::from("/project-b")]), + main_worktree_paths: PathList::default(), archived: false, }, window, @@ -3949,6 +3963,7 @@ async fn test_activate_archived_thread_prefers_current_window_for_matching_paths updated_at: Utc::now(), created_at: None, folder_paths: PathList::new(&[PathBuf::from("/project-a")]), + main_worktree_paths: PathList::default(), archived: false, }, window, @@ -4688,12 +4703,96 @@ async fn test_archived_threads_excluded_from_sidebar_entries(cx: &mut TestAppCon }); } +#[gpui::test] +async fn test_linked_worktree_workspace_shows_main_worktree_threads(cx: &mut TestAppContext) { + // When only a linked worktree workspace is open (not the main repo), + // threads saved against the main repo should still appear in the sidebar. + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + // Create the main repo with a linked worktree. + fs.insert_tree( + "/project", + serde_json::json!({ + ".git": { + "worktrees": { + "feature-a": { + "commondir": "../../", + "HEAD": "ref: refs/heads/feature-a", + }, + }, + }, + "src": {}, + }), + ) + .await; + + fs.insert_tree( + "/wt-feature-a", + serde_json::json!({ + ".git": "gitdir: /project/.git/worktrees/feature-a", + "src": {}, + }), + ) + .await; + + fs.add_linked_worktree_for_repo( + std::path::Path::new("/project/.git"), + false, + git::repository::Worktree { + path: std::path::PathBuf::from("/wt-feature-a"), + ref_name: Some("refs/heads/feature-a".into()), + sha: "abc".into(), + is_main: false, + }, + ) + .await; + + cx.update(|cx| ::set_global(fs.clone(), cx)); + + // Only open the linked worktree as a workspace — NOT the main repo. + let worktree_project = project::Project::test(fs.clone(), ["/wt-feature-a".as_ref()], cx).await; + worktree_project + .update(cx, |p, cx| p.git_scans_complete(cx)) + .await; + + let (multi_workspace, cx) = cx.add_window_view(|window, cx| { + MultiWorkspace::test_new(worktree_project.clone(), window, cx) + }); + let sidebar = setup_sidebar(&multi_workspace, cx); + + // Save a thread against the MAIN repo path. + let main_paths = PathList::new(&[std::path::PathBuf::from("/project")]); + save_named_thread_metadata("main-thread", "Main Repo Thread", &main_paths, cx).await; + + // Save a thread against the linked worktree path. + let wt_paths = PathList::new(&[std::path::PathBuf::from("/wt-feature-a")]); + save_named_thread_metadata("wt-thread", "Worktree Thread", &wt_paths, cx).await; + + multi_workspace.update_in(cx, |_, _window, cx| cx.notify()); + cx.run_until_parked(); + + // Both threads should be visible: the worktree thread by direct lookup, + // and the main repo thread because the workspace is a linked worktree + // and we also query the main repo path. + let entries = visible_entries_as_strings(&sidebar, cx); + assert!( + entries.iter().any(|e| e.contains("Main Repo Thread")), + "expected main repo thread to be visible in linked worktree workspace, got: {entries:?}" + ); + assert!( + entries.iter().any(|e| e.contains("Worktree Thread")), + "expected worktree thread to be visible, got: {entries:?}" + ); +} + mod property_test { use super::*; use gpui::EntityId; struct UnopenedWorktree { path: String, + main_workspace_path: String, } struct TestState { @@ -4834,6 +4933,34 @@ mod property_test { save_thread_metadata(session_id, title, updated_at, None, path_list, cx); } + fn save_thread_to_path_with_main( + state: &mut TestState, + path_list: PathList, + main_worktree_paths: PathList, + cx: &mut gpui::VisualTestContext, + ) { + let session_id = state.next_thread_id(); + let title: SharedString = format!("Thread {}", session_id).into(); + let updated_at = chrono::TimeZone::with_ymd_and_hms(&chrono::Utc, 2024, 1, 1, 0, 0, 0) + .unwrap() + + chrono::Duration::seconds(state.thread_counter as i64); + let metadata = ThreadMetadata { + session_id, + agent_id: agent::ZED_AGENT_ID.clone(), + title, + updated_at, + created_at: None, + folder_paths: path_list, + main_worktree_paths, + archived: false, + }; + cx.update(|_, cx| { + ThreadMetadataStore::global(cx) + .update(cx, |store, cx| store.save_manually(metadata, cx)) + }); + cx.run_until_parked(); + } + async fn perform_operation( operation: Operation, state: &mut TestState, @@ -4852,7 +4979,9 @@ mod property_test { Operation::SaveWorktreeThread { worktree_index } => { let worktree = &state.unopened_worktrees[worktree_index]; let path_list = PathList::new(&[std::path::PathBuf::from(&worktree.path)]); - save_thread_to_path(state, path_list, cx); + let main_worktree_paths = + PathList::new(&[std::path::PathBuf::from(&worktree.main_workspace_path)]); + save_thread_to_path_with_main(state, path_list, main_worktree_paths, cx); } Operation::DeleteThread { index } => { let session_id = state.remove_thread(index); @@ -5004,6 +5133,7 @@ mod property_test { state.unopened_worktrees.push(UnopenedWorktree { path: worktree_path, + main_workspace_path: main_path.clone(), }); } } @@ -5108,6 +5238,19 @@ mod property_test { metadata_thread_ids.insert(metadata.session_id.clone()); } } + if snapshot.is_linked_worktree() { + let main_path_list = + PathList::new(std::slice::from_ref(&snapshot.original_repo_abs_path)); + for metadata in thread_store.read(cx).entries_for_path(&main_path_list) { + metadata_thread_ids.insert(metadata.session_id.clone()); + } + for metadata in thread_store + .read(cx) + .entries_for_main_worktree_path(&main_path_list) + { + metadata_thread_ids.insert(metadata.session_id.clone()); + } + } } }