diff --git a/crates/agent_ui/src/thread_metadata_store.rs b/crates/agent_ui/src/thread_metadata_store.rs index 71e63c78639049d84a82a19d007a83ef81b71646..339d3f48772cc21ad501ce3016919e96eb58ec6a 100644 --- a/crates/agent_ui/src/thread_metadata_store.rs +++ b/crates/agent_ui/src/thread_metadata_store.rs @@ -36,9 +36,12 @@ pub fn init(cx: &mut App) { } /// Migrate existing thread metadata from native agent thread store to the new metadata storage. +/// We migrate the last 10 threads per project and skip threads that do not have a project. /// /// TODO: Remove this after N weeks of shipping the sidebar fn migrate_thread_metadata(cx: &mut App) { + const MAX_MIGRATED_THREADS_PER_PROJECT: usize = 10; + let store = SidebarThreadMetadataStore::global(cx); let db = store.read(cx).db.clone(); @@ -48,16 +51,32 @@ fn migrate_thread_metadata(cx: &mut App) { } let metadata = store.read_with(cx, |_store, app| { + let mut migrated_threads_per_project = HashMap::default(); + 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, + .filter_map(|entry| { + if entry.folder_paths.is_empty() { + return None; + } + + let migrated_thread_count = migrated_threads_per_project + .entry(entry.folder_paths.clone()) + .or_insert(0); + if *migrated_thread_count >= MAX_MIGRATED_THREADS_PER_PROJECT { + return None; + } + *migrated_thread_count += 1; + + Some(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::>() }); @@ -730,35 +749,68 @@ mod tests { }); assert_eq!(list.len(), 0); + let project_a_paths = PathList::new(&[Path::new("/project-a")]); + let project_b_paths = PathList::new(&[Path::new("/project-b")]); let now = Utc::now(); - // Populate the native ThreadStore via save_thread - let save1 = cx.update(|cx| { - let thread_store = ThreadStore::global(cx); - thread_store.update(cx, |store, cx| { - store.save_thread( - acp::SessionId::new("session-1"), - make_db_thread("Thread 1", now), - PathList::default(), - cx, - ) - }) - }); - save1.await.unwrap(); - cx.run_until_parked(); + for index in 0..12 { + let updated_at = now + chrono::Duration::seconds(index as i64); + let session_id = format!("project-a-session-{index}"); + let title = format!("Project A Thread {index}"); + + let save_task = cx.update(|cx| { + let thread_store = ThreadStore::global(cx); + let session_id = session_id.clone(); + let title = title.clone(); + let project_a_paths = project_a_paths.clone(); + thread_store.update(cx, |store, cx| { + store.save_thread( + acp::SessionId::new(session_id), + make_db_thread(&title, updated_at), + project_a_paths, + cx, + ) + }) + }); + save_task.await.unwrap(); + cx.run_until_parked(); + } + + for index in 0..3 { + let updated_at = now + chrono::Duration::seconds(100 + index as i64); + let session_id = format!("project-b-session-{index}"); + let title = format!("Project B Thread {index}"); + + let save_task = cx.update(|cx| { + let thread_store = ThreadStore::global(cx); + let session_id = session_id.clone(); + let title = title.clone(); + let project_b_paths = project_b_paths.clone(); + thread_store.update(cx, |store, cx| { + store.save_thread( + acp::SessionId::new(session_id), + make_db_thread(&title, updated_at), + project_b_paths, + cx, + ) + }) + }); + save_task.await.unwrap(); + cx.run_until_parked(); + } - let save2 = cx.update(|cx| { + let save_projectless = cx.update(|cx| { let thread_store = ThreadStore::global(cx); thread_store.update(cx, |store, cx| { store.save_thread( - acp::SessionId::new("session-2"), - make_db_thread("Thread 2", now), + acp::SessionId::new("projectless-session"), + make_db_thread("Projectless Thread", now + chrono::Duration::seconds(200)), PathList::default(), cx, ) }) }); - save2.await.unwrap(); + save_projectless.await.unwrap(); cx.run_until_parked(); // Run migration @@ -768,26 +820,73 @@ mod tests { cx.run_until_parked(); - // Verify the metadata was migrated + // Verify the metadata was migrated, limited to 10 per project, and + // projectless threads were skipped. let list = cx.update(|cx| { let store = SidebarThreadMetadataStore::global(cx); store.read(cx).entries().collect::>() }); - assert_eq!(list.len(), 2); + assert_eq!(list.len(), 13); - let metadata1 = list + assert!( + list.iter() + .all(|metadata| !metadata.folder_paths.is_empty()) + ); + assert!( + list.iter() + .all(|metadata| metadata.session_id.0.as_ref() != "projectless-session") + ); + + let project_a_entries = list .iter() - .find(|m| m.session_id.0.as_ref() == "session-1") - .expect("session-1 should be in migrated metadata"); - assert_eq!(metadata1.title.as_ref(), "Thread 1"); - assert!(metadata1.agent_id.is_none()); + .filter(|metadata| metadata.folder_paths == project_a_paths) + .collect::>(); + assert_eq!(project_a_entries.len(), 10); + assert_eq!( + project_a_entries + .iter() + .map(|metadata| metadata.session_id.0.as_ref()) + .collect::>(), + vec![ + "project-a-session-11", + "project-a-session-10", + "project-a-session-9", + "project-a-session-8", + "project-a-session-7", + "project-a-session-6", + "project-a-session-5", + "project-a-session-4", + "project-a-session-3", + "project-a-session-2", + ] + ); + assert!( + project_a_entries + .iter() + .all(|metadata| metadata.agent_id.is_none()) + ); - let metadata2 = list + let project_b_entries = list .iter() - .find(|m| m.session_id.0.as_ref() == "session-2") - .expect("session-2 should be in migrated metadata"); - assert_eq!(metadata2.title.as_ref(), "Thread 2"); - assert!(metadata2.agent_id.is_none()); + .filter(|metadata| metadata.folder_paths == project_b_paths) + .collect::>(); + assert_eq!(project_b_entries.len(), 3); + assert_eq!( + project_b_entries + .iter() + .map(|metadata| metadata.session_id.0.as_ref()) + .collect::>(), + vec![ + "project-b-session-2", + "project-b-session-1", + "project-b-session-0", + ] + ); + assert!( + project_b_entries + .iter() + .all(|metadata| metadata.agent_id.is_none()) + ); } #[gpui::test]