thread_worktree_archive.rs

  1use std::{
  2    path::{Path, PathBuf},
  3    sync::Arc,
  4};
  5
  6use agent_client_protocol as acp;
  7use anyhow::{Context as _, Result, anyhow};
  8use git::repository::{AskPassDelegate, CommitOptions, ResetMode};
  9use gpui::{App, AsyncApp, Entity, Task};
 10use project::{
 11    LocalProjectFlags, Project, WorktreeId,
 12    git_store::{Repository, resolve_git_worktree_to_main_repo},
 13};
 14use util::ResultExt;
 15use workspace::{AppState, MultiWorkspace, Workspace};
 16
 17use crate::thread_metadata_store::{ArchivedGitWorktree, ThreadMetadataStore};
 18
 19#[derive(Clone)]
 20pub struct RootPlan {
 21    pub root_path: PathBuf,
 22    pub main_repo_path: PathBuf,
 23    pub affected_projects: Vec<AffectedProject>,
 24    pub worktree_repo: Option<Entity<Repository>>,
 25    pub branch_name: Option<String>,
 26}
 27
 28#[derive(Clone)]
 29pub struct AffectedProject {
 30    pub project: Entity<Project>,
 31    pub worktree_id: WorktreeId,
 32}
 33
 34fn archived_worktree_ref_name(id: i64) -> String {
 35    format!("refs/archived-worktrees/{}", id)
 36}
 37
 38pub struct PersistOutcome {
 39    pub archived_worktree_id: i64,
 40    pub staged_commit_hash: String,
 41}
 42
 43pub fn build_root_plan(
 44    path: &Path,
 45    workspaces: &[Entity<Workspace>],
 46    cx: &App,
 47) -> Option<RootPlan> {
 48    let path = path.to_path_buf();
 49
 50    let affected_projects = workspaces
 51        .iter()
 52        .filter_map(|workspace| {
 53            let project = workspace.read(cx).project().clone();
 54            let worktree = project
 55                .read(cx)
 56                .visible_worktrees(cx)
 57                .find(|worktree| worktree.read(cx).abs_path().as_ref() == path.as_path())?;
 58            let worktree_id = worktree.read(cx).id();
 59            Some(AffectedProject {
 60                project,
 61                worktree_id,
 62            })
 63        })
 64        .collect::<Vec<_>>();
 65
 66    let (linked_snapshot, worktree_repo) = workspaces
 67        .iter()
 68        .flat_map(|workspace| {
 69            workspace
 70                .read(cx)
 71                .project()
 72                .read(cx)
 73                .repositories(cx)
 74                .values()
 75                .cloned()
 76                .collect::<Vec<_>>()
 77        })
 78        .find_map(|repo| {
 79            let snapshot = repo.read(cx).snapshot();
 80            (snapshot.is_linked_worktree()
 81                && snapshot.work_directory_abs_path.as_ref() == path.as_path())
 82            .then_some((snapshot, repo))
 83        })?;
 84
 85    let branch_name = linked_snapshot
 86        .branch
 87        .as_ref()
 88        .map(|b| b.name().to_string());
 89
 90    Some(RootPlan {
 91        root_path: path,
 92        main_repo_path: linked_snapshot.original_repo_abs_path.to_path_buf(),
 93        affected_projects,
 94        worktree_repo: Some(worktree_repo),
 95        branch_name,
 96    })
 97}
 98
 99pub fn path_is_referenced_by_other_unarchived_threads(
100    current_session_id: &acp::SessionId,
101    path: &Path,
102    cx: &App,
103) -> bool {
104    ThreadMetadataStore::global(cx)
105        .read(cx)
106        .entries()
107        .filter(|thread| thread.session_id != *current_session_id)
108        .filter(|thread| !thread.archived)
109        .any(|thread| {
110            thread
111                .folder_paths
112                .paths()
113                .iter()
114                .any(|other_path| other_path.as_path() == path)
115        })
116}
117
118pub async fn remove_root(root: RootPlan, cx: &mut AsyncApp) -> Result<()> {
119    let release_tasks: Vec<_> = root
120        .affected_projects
121        .iter()
122        .map(|affected| {
123            let project = affected.project.clone();
124            let worktree_id = affected.worktree_id;
125            project.update(cx, |project, cx| {
126                let wait = project.wait_for_worktree_release(worktree_id, cx);
127                project.remove_worktree(worktree_id, cx);
128                wait
129            })
130        })
131        .collect();
132
133    if let Err(error) = remove_root_after_worktree_removal(&root, release_tasks, cx).await {
134        rollback_root(&root, cx).await;
135        return Err(error);
136    }
137
138    Ok(())
139}
140
141async fn remove_root_after_worktree_removal(
142    root: &RootPlan,
143    release_tasks: Vec<Task<Result<()>>>,
144    cx: &mut AsyncApp,
145) -> Result<()> {
146    for task in release_tasks {
147        if let Err(error) = task.await {
148            log::error!("Failed waiting for worktree release: {error:#}");
149        }
150    }
151
152    let (repo, _temp_project) = find_or_create_repository(&root.main_repo_path, cx).await?;
153    let receiver = repo.update(cx, |repo: &mut Repository, _cx| {
154        repo.remove_worktree(root.root_path.clone(), false)
155    });
156    let result = receiver
157        .await
158        .map_err(|_| anyhow!("git worktree removal was canceled"))?;
159    result
160}
161
162/// Finds a live `Repository` entity for the given path, or creates a temporary
163/// `Project::local` to obtain one.
164///
165/// `Repository` entities can only be obtained through a `Project` because
166/// `GitStore` (which creates and manages `Repository` entities) is owned by
167/// `Project`. When no open workspace contains the repo we need, we spin up a
168/// headless `Project::local` just to get a `Repository` handle. The caller
169/// keeps the returned `Option<Entity<Project>>` alive for the duration of the
170/// git operations, then drops it.
171///
172/// Future improvement: decoupling `GitStore` from `Project` so that
173/// `Repository` entities can be created standalone would eliminate this
174/// temporary-project workaround.
175async fn find_or_create_repository(
176    repo_path: &Path,
177    cx: &mut AsyncApp,
178) -> Result<(Entity<Repository>, Option<Entity<Project>>)> {
179    let repo_path_owned = repo_path.to_path_buf();
180    let live_repo = cx.update(|cx| {
181        all_open_workspaces(cx)
182            .into_iter()
183            .flat_map(|workspace| {
184                workspace
185                    .read(cx)
186                    .project()
187                    .read(cx)
188                    .repositories(cx)
189                    .values()
190                    .cloned()
191                    .collect::<Vec<_>>()
192            })
193            .find(|repo| {
194                repo.read(cx).snapshot().work_directory_abs_path.as_ref()
195                    == repo_path_owned.as_path()
196            })
197    });
198
199    if let Some(repo) = live_repo {
200        return Ok((repo, None));
201    }
202
203    let app_state =
204        current_app_state(cx).context("no app state available for temporary project")?;
205    let temp_project = cx.update(|cx| {
206        Project::local(
207            app_state.client.clone(),
208            app_state.node_runtime.clone(),
209            app_state.user_store.clone(),
210            app_state.languages.clone(),
211            app_state.fs.clone(),
212            None,
213            LocalProjectFlags::default(),
214            cx,
215        )
216    });
217
218    let repo_path_for_worktree = repo_path.to_path_buf();
219    let create_worktree = temp_project.update(cx, |project, cx| {
220        project.create_worktree(repo_path_for_worktree, true, cx)
221    });
222    let _worktree = create_worktree.await?;
223    let initial_scan = temp_project.read_with(cx, |project, cx| project.wait_for_initial_scan(cx));
224    initial_scan.await;
225
226    let repo_path_for_find = repo_path.to_path_buf();
227    let repo = temp_project
228        .update(cx, |project, cx| {
229            project
230                .repositories(cx)
231                .values()
232                .find(|repo| {
233                    repo.read(cx).snapshot().work_directory_abs_path.as_ref()
234                        == repo_path_for_find.as_path()
235                })
236                .cloned()
237        })
238        .context("failed to resolve temporary repository handle")?;
239
240    let barrier = repo.update(cx, |repo: &mut Repository, _cx| repo.barrier());
241    barrier
242        .await
243        .map_err(|_| anyhow!("temporary repository barrier canceled"))?;
244    Ok((repo, Some(temp_project)))
245}
246
247async fn rollback_root(root: &RootPlan, cx: &mut AsyncApp) {
248    for affected in &root.affected_projects {
249        let task = affected.project.update(cx, |project, cx| {
250            project.create_worktree(root.root_path.clone(), true, cx)
251        });
252        task.await.log_err();
253    }
254}
255
256pub async fn persist_worktree_state(root: &RootPlan, cx: &mut AsyncApp) -> Result<PersistOutcome> {
257    let worktree_repo = root
258        .worktree_repo
259        .clone()
260        .context("no worktree repo entity for persistence")?;
261
262    // Read original HEAD SHA before creating any WIP commits
263    let original_commit_hash = worktree_repo
264        .update(cx, |repo, _cx| repo.head_sha())
265        .await
266        .map_err(|_| anyhow!("head_sha canceled"))?
267        .context("failed to read original HEAD SHA")?
268        .context("HEAD SHA is None before WIP commits")?;
269
270    // Create WIP commit #1 (staged state)
271    let askpass = AskPassDelegate::new(cx, |_, _, _| {});
272    let commit_rx = worktree_repo.update(cx, |repo, cx| {
273        repo.commit(
274            "WIP staged".into(),
275            None,
276            CommitOptions {
277                allow_empty: true,
278                ..Default::default()
279            },
280            askpass,
281            cx,
282        )
283    });
284    commit_rx
285        .await
286        .map_err(|_| anyhow!("WIP staged commit canceled"))??;
287
288    // Read SHA after staged commit
289    let staged_sha_result = worktree_repo
290        .update(cx, |repo, _cx| repo.head_sha())
291        .await
292        .map_err(|_| anyhow!("head_sha canceled"))
293        .and_then(|r| r.context("failed to read HEAD SHA after staged commit"))
294        .and_then(|opt| opt.context("HEAD SHA is None after staged commit"));
295    let staged_commit_hash = match staged_sha_result {
296        Ok(sha) => sha,
297        Err(error) => {
298            let rx = worktree_repo.update(cx, |repo, cx| {
299                repo.reset("HEAD~1".to_string(), ResetMode::Mixed, cx)
300            });
301            rx.await.ok().and_then(|r| r.log_err());
302            return Err(error);
303        }
304    };
305
306    // Stage all files including untracked
307    let stage_rx = worktree_repo.update(cx, |repo, _cx| repo.stage_all_including_untracked());
308    if let Err(error) = stage_rx
309        .await
310        .map_err(|_| anyhow!("stage all canceled"))
311        .and_then(|inner| inner)
312    {
313        let rx = worktree_repo.update(cx, |repo, cx| {
314            repo.reset("HEAD~1".to_string(), ResetMode::Mixed, cx)
315        });
316        rx.await.ok().and_then(|r| r.log_err());
317        return Err(error.context("failed to stage all files including untracked"));
318    }
319
320    // Create WIP commit #2 (unstaged/untracked state)
321    let askpass = AskPassDelegate::new(cx, |_, _, _| {});
322    let commit_rx = worktree_repo.update(cx, |repo, cx| {
323        repo.commit(
324            "WIP unstaged".into(),
325            None,
326            CommitOptions {
327                allow_empty: true,
328                ..Default::default()
329            },
330            askpass,
331            cx,
332        )
333    });
334    if let Err(error) = commit_rx
335        .await
336        .map_err(|_| anyhow!("WIP unstaged commit canceled"))
337        .and_then(|inner| inner)
338    {
339        let rx = worktree_repo.update(cx, |repo, cx| {
340            repo.reset("HEAD~1".to_string(), ResetMode::Mixed, cx)
341        });
342        rx.await.ok().and_then(|r| r.log_err());
343        return Err(error);
344    }
345
346    // Read HEAD SHA after WIP commits
347    let head_sha_result = worktree_repo
348        .update(cx, |repo, _cx| repo.head_sha())
349        .await
350        .map_err(|_| anyhow!("head_sha canceled"))
351        .and_then(|r| r.context("failed to read HEAD SHA after WIP commits"))
352        .and_then(|opt| opt.context("HEAD SHA is None after WIP commits"));
353    let unstaged_commit_hash = match head_sha_result {
354        Ok(sha) => sha,
355        Err(error) => {
356            let rx = worktree_repo.update(cx, |repo, cx| {
357                repo.reset(format!("{}~1", staged_commit_hash), ResetMode::Mixed, cx)
358            });
359            rx.await.ok().and_then(|r| r.log_err());
360            return Err(error);
361        }
362    };
363
364    // Create DB record
365    let store = cx.update(|cx| ThreadMetadataStore::global(cx));
366    let worktree_path_str = root.root_path.to_string_lossy().to_string();
367    let main_repo_path_str = root.main_repo_path.to_string_lossy().to_string();
368    let branch_name = root.branch_name.clone();
369
370    let db_result = store
371        .read_with(cx, |store, cx| {
372            store.create_archived_worktree(
373                worktree_path_str.clone(),
374                main_repo_path_str.clone(),
375                branch_name.clone(),
376                staged_commit_hash.clone(),
377                unstaged_commit_hash.clone(),
378                original_commit_hash.clone(),
379                cx,
380            )
381        })
382        .await
383        .context("failed to create archived worktree DB record");
384    let archived_worktree_id = match db_result {
385        Ok(id) => id,
386        Err(error) => {
387            let rx = worktree_repo.update(cx, |repo, cx| {
388                repo.reset(format!("{}~1", staged_commit_hash), ResetMode::Mixed, cx)
389            });
390            rx.await.ok().and_then(|r| r.log_err());
391            return Err(error);
392        }
393    };
394
395    // Link all threads on this worktree to the archived record
396    let session_ids: Vec<acp::SessionId> = store.read_with(cx, |store, _cx| {
397        store
398            .entries()
399            .filter(|thread| {
400                thread
401                    .folder_paths
402                    .paths()
403                    .iter()
404                    .any(|p| p.as_path() == root.root_path)
405            })
406            .map(|thread| thread.session_id.clone())
407            .collect()
408    });
409
410    for session_id in &session_ids {
411        let link_result = store
412            .read_with(cx, |store, cx| {
413                store.link_thread_to_archived_worktree(
414                    session_id.0.to_string(),
415                    archived_worktree_id,
416                    cx,
417                )
418            })
419            .await;
420        if let Err(error) = link_result {
421            if let Err(delete_error) = store
422                .read_with(cx, |store, cx| {
423                    store.delete_archived_worktree(archived_worktree_id, cx)
424                })
425                .await
426            {
427                log::error!(
428                    "Failed to delete archived worktree DB record during link rollback: {delete_error:#}"
429                );
430            }
431            let rx = worktree_repo.update(cx, |repo, cx| {
432                repo.reset(format!("{}~1", staged_commit_hash), ResetMode::Mixed, cx)
433            });
434            rx.await.ok().and_then(|r| r.log_err());
435            return Err(error.context("failed to link thread to archived worktree"));
436        }
437    }
438
439    // Create git ref on main repo (non-fatal)
440    let ref_name = archived_worktree_ref_name(archived_worktree_id);
441    let main_repo_result = find_or_create_repository(&root.main_repo_path, cx).await;
442    match main_repo_result {
443        Ok((main_repo, _temp_project)) => {
444            let rx = main_repo.update(cx, |repo, _cx| {
445                repo.update_ref(ref_name.clone(), unstaged_commit_hash.clone())
446            });
447            if let Err(error) = rx
448                .await
449                .map_err(|_| anyhow!("update_ref canceled"))
450                .and_then(|r| r)
451            {
452                log::warn!(
453                    "Failed to create ref {} on main repo (non-fatal): {error}",
454                    ref_name
455                );
456            }
457        }
458        Err(error) => {
459            log::warn!(
460                "Could not find main repo to create ref {} (non-fatal): {error}",
461                ref_name
462            );
463        }
464    }
465
466    Ok(PersistOutcome {
467        archived_worktree_id,
468        staged_commit_hash,
469    })
470}
471
472pub async fn rollback_persist(outcome: &PersistOutcome, root: &RootPlan, cx: &mut AsyncApp) {
473    // Undo WIP commits on the worktree repo
474    if let Some(worktree_repo) = &root.worktree_repo {
475        let rx = worktree_repo.update(cx, |repo, cx| {
476            repo.reset(
477                format!("{}~1", outcome.staged_commit_hash),
478                ResetMode::Mixed,
479                cx,
480            )
481        });
482        rx.await.ok().and_then(|r| r.log_err());
483    }
484
485    // Delete the git ref on main repo
486    if let Ok((main_repo, _temp_project)) =
487        find_or_create_repository(&root.main_repo_path, cx).await
488    {
489        let ref_name = archived_worktree_ref_name(outcome.archived_worktree_id);
490        let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
491        rx.await.ok().and_then(|r| r.log_err());
492    }
493
494    // Delete the DB record
495    let store = cx.update(|cx| ThreadMetadataStore::global(cx));
496    if let Err(error) = store
497        .read_with(cx, |store, cx| {
498            store.delete_archived_worktree(outcome.archived_worktree_id, cx)
499        })
500        .await
501    {
502        log::error!("Failed to delete archived worktree DB record during rollback: {error:#}");
503    }
504}
505
506pub async fn restore_worktree_via_git(
507    row: &ArchivedGitWorktree,
508    cx: &mut AsyncApp,
509) -> Result<PathBuf> {
510    let (main_repo, _temp_project) = find_or_create_repository(&row.main_repo_path, cx).await?;
511
512    // Check if worktree path already exists on disk
513    let worktree_path = &row.worktree_path;
514    let app_state = current_app_state(cx).context("no app state available")?;
515    let already_exists = app_state.fs.metadata(worktree_path).await?.is_some();
516
517    if already_exists {
518        let is_git_worktree =
519            resolve_git_worktree_to_main_repo(app_state.fs.as_ref(), worktree_path)
520                .await
521                .is_some();
522
523        if is_git_worktree {
524            // Already a git worktree — another thread on the same worktree
525            // already restored it. Reuse as-is.
526            return Ok(worktree_path.clone());
527        }
528
529        // Path exists but isn't a git worktree. Ask git to adopt it.
530        let rx = main_repo.update(cx, |repo, _cx| repo.repair_worktrees());
531        rx.await
532            .map_err(|_| anyhow!("worktree repair was canceled"))?
533            .context("failed to repair worktrees")?;
534    } else {
535        // Create detached worktree at the unstaged commit
536        let rx = main_repo.update(cx, |repo, _cx| {
537            repo.create_worktree_detached(worktree_path.clone(), row.unstaged_commit_hash.clone())
538        });
539        rx.await
540            .map_err(|_| anyhow!("worktree creation was canceled"))?
541            .context("failed to create worktree")?;
542    }
543
544    // Get the worktree's repo entity
545    let (wt_repo, _temp_wt_project) = find_or_create_repository(worktree_path, cx).await?;
546
547    // Reset past the WIP commits to recover original state
548    let mixed_reset_ok = {
549        let rx = wt_repo.update(cx, |repo, cx| {
550            repo.reset(row.staged_commit_hash.clone(), ResetMode::Mixed, cx)
551        });
552        match rx.await {
553            Ok(Ok(())) => true,
554            Ok(Err(error)) => {
555                log::error!("Mixed reset to staged commit failed: {error:#}");
556                false
557            }
558            Err(_) => {
559                log::error!("Mixed reset to staged commit was canceled");
560                false
561            }
562        }
563    };
564
565    let soft_reset_ok = if mixed_reset_ok {
566        let rx = wt_repo.update(cx, |repo, cx| {
567            repo.reset(row.original_commit_hash.clone(), ResetMode::Soft, cx)
568        });
569        match rx.await {
570            Ok(Ok(())) => true,
571            Ok(Err(error)) => {
572                log::error!("Soft reset to original commit failed: {error:#}");
573                false
574            }
575            Err(_) => {
576                log::error!("Soft reset to original commit was canceled");
577                false
578            }
579        }
580    } else {
581        false
582    };
583
584    // If either WIP reset failed, fall back to a mixed reset directly to
585    // original_commit_hash so we at least land on the right commit.
586    if !mixed_reset_ok || !soft_reset_ok {
587        log::warn!(
588            "WIP reset(s) failed (mixed_ok={mixed_reset_ok}, soft_ok={soft_reset_ok}); \
589             falling back to mixed reset to original commit {}",
590            row.original_commit_hash
591        );
592        let rx = wt_repo.update(cx, |repo, cx| {
593            repo.reset(row.original_commit_hash.clone(), ResetMode::Mixed, cx)
594        });
595        match rx.await {
596            Ok(Ok(())) => {}
597            Ok(Err(error)) => {
598                return Err(error.context(format!(
599                    "fallback reset to original commit {} also failed",
600                    row.original_commit_hash
601                )));
602            }
603            Err(_) => {
604                return Err(anyhow!(
605                    "fallback reset to original commit {} was canceled",
606                    row.original_commit_hash
607                ));
608            }
609        }
610    }
611
612    // Verify HEAD is at original_commit_hash
613    let current_head = wt_repo
614        .update(cx, |repo, _cx| repo.head_sha())
615        .await
616        .map_err(|_| anyhow!("post-restore head_sha was canceled"))?
617        .context("failed to read HEAD after restore")?
618        .context("HEAD is None after restore")?;
619
620    if current_head != row.original_commit_hash {
621        anyhow::bail!(
622            "After restore, HEAD is at {current_head} but expected {}. \
623             The worktree may be in an inconsistent state.",
624            row.original_commit_hash
625        );
626    }
627
628    // Restore the branch
629    if let Some(branch_name) = &row.branch_name {
630        // Check if the branch exists and points at original_commit_hash.
631        // If it does, switch to it. If not, create a new branch there.
632        let rx = wt_repo.update(cx, |repo, _cx| repo.change_branch(branch_name.clone()));
633        if matches!(rx.await, Ok(Ok(()))) {
634            // Verify the branch actually points at original_commit_hash after switching
635            let head_after_switch = wt_repo
636                .update(cx, |repo, _cx| repo.head_sha())
637                .await
638                .ok()
639                .and_then(|r| r.ok())
640                .flatten();
641
642            if head_after_switch.as_deref() != Some(&row.original_commit_hash) {
643                // Branch exists but doesn't point at the right commit.
644                // Switch back to detached HEAD at original_commit_hash.
645                log::warn!(
646                    "Branch '{}' exists but points at {:?}, not {}. Creating fresh branch.",
647                    branch_name,
648                    head_after_switch,
649                    row.original_commit_hash
650                );
651                let rx = wt_repo.update(cx, |repo, cx| {
652                    repo.reset(row.original_commit_hash.clone(), ResetMode::Mixed, cx)
653                });
654                rx.await.ok().and_then(|r| r.log_err());
655                // Delete the old branch and create fresh
656                let rx = wt_repo.update(cx, |repo, _cx| {
657                    repo.create_branch(branch_name.clone(), None)
658                });
659                rx.await.ok().and_then(|r| r.log_err());
660            }
661        } else {
662            // Branch doesn't exist or can't be switched to — create it.
663            let rx = wt_repo.update(cx, |repo, _cx| {
664                repo.create_branch(branch_name.clone(), None)
665            });
666            if let Ok(Err(error)) | Err(error) = rx.await.map_err(|e| anyhow::anyhow!("{e}")) {
667                log::warn!(
668                    "Could not create branch '{}': {error} — \
669                     restored worktree is in detached HEAD state.",
670                    branch_name
671                );
672            }
673        }
674    }
675
676    Ok(worktree_path.clone())
677}
678
679pub async fn cleanup_archived_worktree_record(row: &ArchivedGitWorktree, cx: &mut AsyncApp) {
680    // Delete the git ref from the main repo
681    if let Ok((main_repo, _temp_project)) = find_or_create_repository(&row.main_repo_path, cx).await
682    {
683        let ref_name = archived_worktree_ref_name(row.id);
684        let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
685        match rx.await {
686            Ok(Ok(())) => {}
687            Ok(Err(error)) => log::warn!("Failed to delete archive ref: {error}"),
688            Err(_) => log::warn!("Archive ref deletion was canceled"),
689        }
690    }
691
692    // Delete the DB records
693    let store = cx.update(|cx| ThreadMetadataStore::global(cx));
694    store
695        .read_with(cx, |store, cx| store.delete_archived_worktree(row.id, cx))
696        .await
697        .log_err();
698}
699
700/// Cleans up all archived worktree data associated with a thread being deleted.
701///
702/// This unlinks the thread from all its archived worktrees and, for any
703/// archived worktree that is no longer referenced by any other thread,
704/// deletes the git ref and DB records.
705pub async fn cleanup_thread_archived_worktrees(session_id: &acp::SessionId, cx: &mut AsyncApp) {
706    let store = cx.update(|cx| ThreadMetadataStore::global(cx));
707
708    let archived_worktrees = store
709        .read_with(cx, |store, cx| {
710            store.get_archived_worktrees_for_thread(session_id.0.to_string(), cx)
711        })
712        .await;
713    let archived_worktrees = match archived_worktrees {
714        Ok(rows) => rows,
715        Err(error) => {
716            log::error!(
717                "Failed to fetch archived worktrees for thread {}: {error:#}",
718                session_id.0
719            );
720            return;
721        }
722    };
723
724    if archived_worktrees.is_empty() {
725        return;
726    }
727
728    if let Err(error) = store
729        .read_with(cx, |store, cx| {
730            store.unlink_thread_from_all_archived_worktrees(session_id.0.to_string(), cx)
731        })
732        .await
733    {
734        log::error!(
735            "Failed to unlink thread {} from archived worktrees: {error:#}",
736            session_id.0
737        );
738        return;
739    }
740
741    for row in &archived_worktrees {
742        let still_referenced = store
743            .read_with(cx, |store, cx| {
744                store.is_archived_worktree_referenced(row.id, cx)
745            })
746            .await;
747        match still_referenced {
748            Ok(true) => {}
749            Ok(false) => {
750                cleanup_archived_worktree_record(row, cx).await;
751            }
752            Err(error) => {
753                log::error!(
754                    "Failed to check if archived worktree {} is still referenced: {error:#}",
755                    row.id
756                );
757            }
758        }
759    }
760}
761
762pub fn all_open_workspaces(cx: &App) -> Vec<Entity<Workspace>> {
763    cx.windows()
764        .into_iter()
765        .filter_map(|window| window.downcast::<MultiWorkspace>())
766        .flat_map(|multi_workspace| {
767            multi_workspace
768                .read(cx)
769                .map(|multi_workspace| multi_workspace.workspaces().cloned().collect::<Vec<_>>())
770                .unwrap_or_default()
771        })
772        .collect()
773}
774
775fn current_app_state(cx: &mut AsyncApp) -> Option<Arc<AppState>> {
776    cx.update(|cx| {
777        all_open_workspaces(cx)
778            .into_iter()
779            .next()
780            .map(|workspace| workspace.read(cx).app_state().clone())
781    })
782}