mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-02 15:29:49 +00:00
Preserve archived thread project paths after worktree removal (#53893)
When a thread created in a git worktree is archived, the archive flow removes the linked worktree from the project. If a subsequent thread event fires after this (e.g. an async title-generation task completing), `handle_thread_event` recalculates `worktree_paths` from the project's current state — which no longer includes the removed worktree — and overwrites the archived thread's stored paths with empty ones. This causes restoring the thread to prompt the user to re-associate a project, instead of restoring it directly. **Fix:** return early from `handle_thread_event` when the thread is already archived, so the stored paths are never overwritten. Release Notes: - Fixed archived threads in git worktrees losing their project association.
This commit is contained in:
parent
e8c6f29f44
commit
bf0fcfbbac
1 changed files with 129 additions and 4 deletions
|
|
@ -638,6 +638,10 @@ impl ThreadMetadataStore {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(thread) = self.threads.get(&thread_id) {
|
||||
debug_assert!(
|
||||
!thread.archived,
|
||||
"update_working_directories called on archived thread"
|
||||
);
|
||||
self.save_internal(ThreadMetadata {
|
||||
worktree_paths: WorktreePaths::from_path_lists(
|
||||
thread.main_worktree_paths().clone(),
|
||||
|
|
@ -664,6 +668,12 @@ impl ThreadMetadataStore {
|
|||
if thread.worktree_paths == worktree_paths {
|
||||
continue;
|
||||
}
|
||||
// Don't overwrite paths for archived threads — the
|
||||
// project may no longer include the worktree that was
|
||||
// removed during the archive flow.
|
||||
if thread.archived {
|
||||
continue;
|
||||
}
|
||||
self.save_internal(ThreadMetadata {
|
||||
worktree_paths: worktree_paths.clone(),
|
||||
..thread.clone()
|
||||
|
|
@ -1102,10 +1112,24 @@ impl ThreadMetadataStore {
|
|||
|
||||
let agent_id = thread_ref.connection().agent_id();
|
||||
|
||||
let project = thread_ref.project().read(cx);
|
||||
let worktree_paths = project.worktree_paths(cx);
|
||||
|
||||
let remote_connection = project.remote_connection_options(cx);
|
||||
// Preserve project-dependent fields for archived threads.
|
||||
// The worktree may already have been removed from the
|
||||
// project as part of the archive flow, so re-evaluating
|
||||
// these from the current project state would yield
|
||||
// empty/incorrect results.
|
||||
let (worktree_paths, remote_connection) =
|
||||
if let Some(existing) = existing_thread.filter(|t| t.archived) {
|
||||
(
|
||||
existing.worktree_paths.clone(),
|
||||
existing.remote_connection.clone(),
|
||||
)
|
||||
} else {
|
||||
let project = thread_ref.project().read(cx);
|
||||
(
|
||||
project.worktree_paths(cx),
|
||||
project.remote_connection_options(cx),
|
||||
)
|
||||
};
|
||||
|
||||
// Threads without a folder path (e.g. started in an empty
|
||||
// window) are archived by default so they don't get lost,
|
||||
|
|
@ -3543,4 +3567,105 @@ mod tests {
|
|||
let result = WorktreePaths::from_path_lists(main, folder);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Regression test: archiving a thread created in a git worktree must
|
||||
/// preserve the thread's folder paths so that restoring it later does
|
||||
/// not prompt the user to re-associate a project.
|
||||
#[gpui::test]
|
||||
async fn test_archived_thread_retains_paths_after_worktree_removal(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/worktrees/feature",
|
||||
serde_json::json!({ "src": { "main.rs": "" } }),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, [Path::new("/worktrees/feature")], cx).await;
|
||||
let connection = StubAgentConnection::new();
|
||||
|
||||
let (panel, mut vcx) = setup_panel_with_project(project.clone(), cx);
|
||||
crate::test_support::open_thread_with_connection(&panel, connection, &mut vcx);
|
||||
|
||||
let thread = panel.read_with(&vcx, |panel, cx| panel.active_agent_thread(cx).unwrap());
|
||||
let thread_id = crate::test_support::active_thread_id(&panel, &vcx);
|
||||
|
||||
// Push content so the event handler saves metadata with the
|
||||
// project's worktree paths.
|
||||
thread.update_in(&mut vcx, |thread, _window, cx| {
|
||||
thread.push_user_content_block(None, "Hello".into(), cx);
|
||||
});
|
||||
vcx.run_until_parked();
|
||||
|
||||
// Verify paths were saved correctly.
|
||||
let (folder_paths_before, main_paths_before) = cx.read(|cx| {
|
||||
let store = ThreadMetadataStore::global(cx).read(cx);
|
||||
let entry = store.entry(thread_id).unwrap();
|
||||
assert!(
|
||||
!entry.folder_paths().is_empty(),
|
||||
"thread should have folder paths before archiving"
|
||||
);
|
||||
(
|
||||
entry.folder_paths().clone(),
|
||||
entry.main_worktree_paths().clone(),
|
||||
)
|
||||
});
|
||||
|
||||
// Archive the thread.
|
||||
cx.update(|cx| {
|
||||
ThreadMetadataStore::global(cx).update(cx, |store, cx| {
|
||||
store.archive(thread_id, None, cx);
|
||||
});
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Remove the worktree from the project, simulating what the
|
||||
// archive flow does for linked git worktrees.
|
||||
let worktree_id = cx.update(|cx| {
|
||||
project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.id()
|
||||
});
|
||||
project.update(cx, |project, cx| {
|
||||
project.remove_worktree(worktree_id, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Trigger a thread event after archiving + worktree removal.
|
||||
// In production this happens when an async title-generation task
|
||||
// completes after the thread was archived.
|
||||
thread.update_in(&mut vcx, |thread, _window, cx| {
|
||||
thread.set_title("Generated title".into(), cx).detach();
|
||||
});
|
||||
vcx.run_until_parked();
|
||||
|
||||
// The archived thread must still have its original folder paths.
|
||||
cx.read(|cx| {
|
||||
let store = ThreadMetadataStore::global(cx).read(cx);
|
||||
let entry = store.entry(thread_id).unwrap();
|
||||
assert!(entry.archived, "thread should still be archived");
|
||||
assert_eq!(
|
||||
entry.display_title().as_ref(),
|
||||
"Generated title",
|
||||
"title should still be updated for archived threads"
|
||||
);
|
||||
assert_eq!(
|
||||
entry.folder_paths(),
|
||||
&folder_paths_before,
|
||||
"archived thread must retain its folder paths after worktree \
|
||||
removal + subsequent thread event, otherwise restoring it \
|
||||
will prompt the user to re-associate a project"
|
||||
);
|
||||
assert_eq!(
|
||||
entry.main_worktree_paths(),
|
||||
&main_paths_before,
|
||||
"archived thread must retain its main worktree paths after \
|
||||
worktree removal + subsequent thread event"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue