1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use agent_client_protocol as acp;
7use anyhow::{Context as _, Result, anyhow};
8use gpui::{App, AsyncApp, Entity, Task};
9use project::{
10 LocalProjectFlags, Project, WorktreeId,
11 git_store::{Repository, resolve_git_worktree_to_main_repo},
12};
13use util::ResultExt;
14use workspace::{AppState, MultiWorkspace, Workspace};
15
16use crate::thread_metadata_store::{ArchivedGitWorktree, ThreadMetadataStore};
17
18/// The plan for archiving a single git worktree root.
19///
20/// A thread can have multiple folder paths open, so there may be multiple
21/// `RootPlan`s per archival operation. Each one captures everything needed to
22/// persist the worktree's git state and then remove it from disk.
23///
24/// All fields are gathered synchronously by [`build_root_plan`] while the
25/// worktree is still loaded in open projects. This is important because
26/// workspace removal tears down project and repository entities, making
27/// them unavailable for the later async persist/remove steps.
28#[derive(Clone)]
29pub struct RootPlan {
30 /// Absolute path of the git worktree on disk.
31 pub root_path: PathBuf,
32 /// Absolute path to the main git repository this worktree is linked to.
33 /// Used both for creating a git ref to prevent GC of WIP commits during
34 /// [`persist_worktree_state`], and for `git worktree remove` during
35 /// [`remove_root`].
36 pub main_repo_path: PathBuf,
37 /// Every open `Project` that has this worktree loaded, so they can all
38 /// call `remove_worktree` and release it during [`remove_root`].
39 /// Multiple projects can reference the same path when the user has the
40 /// worktree open in more than one workspace.
41 pub affected_projects: Vec<AffectedProject>,
42 /// The `Repository` entity for this linked worktree, used to run git
43 /// commands (create WIP commits, stage files, reset) during
44 /// [`persist_worktree_state`].
45 pub worktree_repo: Entity<Repository>,
46 /// The branch the worktree was on, so it can be restored later.
47 /// `None` if the worktree was in detached HEAD state.
48 pub branch_name: Option<String>,
49}
50
51/// A `Project` that references a worktree being archived, paired with the
52/// `WorktreeId` it uses for that worktree.
53///
54/// The same worktree path can appear in multiple open workspaces/projects
55/// (e.g. when the user has two windows open that both include the same
56/// linked worktree). Each one needs to call `remove_worktree` and wait for
57/// the release during [`remove_root`], otherwise the project would still
58/// hold a reference to the directory and `git worktree remove` would fail.
59#[derive(Clone)]
60pub struct AffectedProject {
61 pub project: Entity<Project>,
62 pub worktree_id: WorktreeId,
63}
64
65fn archived_worktree_ref_name(id: i64) -> String {
66 format!("refs/archived-worktrees/{}", id)
67}
68
69/// Builds a [`RootPlan`] for archiving the git worktree at `path`.
70///
71/// This is a synchronous planning step that must run *before* any workspace
72/// removal, because it needs live project and repository entities that are
73/// torn down when a workspace is removed. It does three things:
74///
75/// 1. Finds every `Project` across all open workspaces that has this
76/// worktree loaded (`affected_projects`).
77/// 2. Looks for a `Repository` entity whose snapshot identifies this path
78/// as a linked worktree (`worktree_repo`), which is needed for the git
79/// operations in [`persist_worktree_state`].
80/// 3. Determines the `main_repo_path` — the parent repo that owns this
81/// linked worktree — needed for both git ref creation and
82/// `git worktree remove`.
83///
84/// Returns `None` if the path is not a linked worktree (main worktrees
85/// cannot be archived to disk) or if no open project has it loaded.
86pub fn build_root_plan(
87 path: &Path,
88 workspaces: &[Entity<Workspace>],
89 cx: &App,
90) -> Option<RootPlan> {
91 let path = path.to_path_buf();
92
93 let affected_projects = workspaces
94 .iter()
95 .filter_map(|workspace| {
96 let project = workspace.read(cx).project().clone();
97 let worktree = project
98 .read(cx)
99 .visible_worktrees(cx)
100 .find(|worktree| worktree.read(cx).abs_path().as_ref() == path.as_path())?;
101 let worktree_id = worktree.read(cx).id();
102 Some(AffectedProject {
103 project,
104 worktree_id,
105 })
106 })
107 .collect::<Vec<_>>();
108
109 if affected_projects.is_empty() {
110 return None;
111 }
112
113 let linked_repo = workspaces
114 .iter()
115 .flat_map(|workspace| {
116 workspace
117 .read(cx)
118 .project()
119 .read(cx)
120 .repositories(cx)
121 .values()
122 .cloned()
123 .collect::<Vec<_>>()
124 })
125 .find_map(|repo| {
126 let snapshot = repo.read(cx).snapshot();
127 (snapshot.is_linked_worktree()
128 && snapshot.work_directory_abs_path.as_ref() == path.as_path())
129 .then_some((snapshot, repo))
130 });
131
132 // Only linked worktrees can be archived to disk via `git worktree remove`.
133 // Main worktrees must be left alone — git refuses to remove them.
134 let (linked_snapshot, repo) = linked_repo?;
135 let main_repo_path = linked_snapshot.original_repo_abs_path.to_path_buf();
136 let branch_name = linked_snapshot
137 .branch
138 .as_ref()
139 .map(|branch| branch.name().to_string());
140 Some(RootPlan {
141 root_path: path,
142 main_repo_path,
143 affected_projects,
144 worktree_repo: repo,
145 branch_name,
146 })
147}
148
149/// Returns `true` if any unarchived thread other than `current_session_id`
150/// references `path` in its folder paths. Used to determine whether a
151/// worktree can safely be removed from disk.
152pub fn path_is_referenced_by_other_unarchived_threads(
153 current_session_id: &acp::SessionId,
154 path: &Path,
155 cx: &App,
156) -> bool {
157 ThreadMetadataStore::global(cx)
158 .read(cx)
159 .entries()
160 .filter(|thread| thread.session_id != *current_session_id)
161 .filter(|thread| !thread.archived)
162 .any(|thread| {
163 thread
164 .folder_paths
165 .paths()
166 .iter()
167 .any(|other_path| other_path.as_path() == path)
168 })
169}
170
171/// Removes a worktree from all affected projects and deletes it from disk
172/// via `git worktree remove`.
173///
174/// This is the destructive counterpart to [`persist_worktree_state`]. It
175/// first detaches the worktree from every [`AffectedProject`], waits for
176/// each project to fully release it, then asks the main repository to
177/// delete the worktree directory. If the git removal fails, the worktree
178/// is re-added to each project via [`rollback_root`].
179pub async fn remove_root(root: RootPlan, cx: &mut AsyncApp) -> Result<()> {
180 let release_tasks: Vec<_> = root
181 .affected_projects
182 .iter()
183 .map(|affected| {
184 let project = affected.project.clone();
185 let worktree_id = affected.worktree_id;
186 project.update(cx, |project, cx| {
187 let wait = project.wait_for_worktree_release(worktree_id, cx);
188 project.remove_worktree(worktree_id, cx);
189 wait
190 })
191 })
192 .collect();
193
194 if let Err(error) = remove_root_after_worktree_removal(&root, release_tasks, cx).await {
195 rollback_root(&root, cx).await;
196 return Err(error);
197 }
198
199 Ok(())
200}
201
202async fn remove_root_after_worktree_removal(
203 root: &RootPlan,
204 release_tasks: Vec<Task<Result<()>>>,
205 cx: &mut AsyncApp,
206) -> Result<()> {
207 for task in release_tasks {
208 if let Err(error) = task.await {
209 log::error!("Failed waiting for worktree release: {error:#}");
210 }
211 }
212
213 let (repo, _temp_project) = find_or_create_repository(&root.main_repo_path, cx).await?;
214 // force=true is required because the working directory is still dirty
215 // — persist_worktree_state captures state into detached commits without
216 // modifying the real index or working tree, so git refuses to delete
217 // the worktree without --force.
218 let receiver = repo.update(cx, |repo: &mut Repository, _cx| {
219 repo.remove_worktree(root.root_path.clone(), true)
220 });
221 let result = receiver
222 .await
223 .map_err(|_| anyhow!("git worktree removal was canceled"))?;
224 // Keep _temp_project alive until after the await so the headless project isn't dropped mid-operation
225 drop(_temp_project);
226 result
227}
228
229/// Finds a live `Repository` entity for the given path, or creates a temporary
230/// `Project::local` to obtain one.
231///
232/// `Repository` entities can only be obtained through a `Project` because
233/// `GitStore` (which creates and manages `Repository` entities) is owned by
234/// `Project`. When no open workspace contains the repo we need, we spin up a
235/// headless `Project::local` just to get a `Repository` handle. The caller
236/// keeps the returned `Option<Entity<Project>>` alive for the duration of the
237/// git operations, then drops it.
238///
239/// Future improvement: decoupling `GitStore` from `Project` so that
240/// `Repository` entities can be created standalone would eliminate this
241/// temporary-project workaround.
242async fn find_or_create_repository(
243 repo_path: &Path,
244 cx: &mut AsyncApp,
245) -> Result<(Entity<Repository>, Option<Entity<Project>>)> {
246 let repo_path_owned = repo_path.to_path_buf();
247 let live_repo = cx.update(|cx| {
248 all_open_workspaces(cx)
249 .into_iter()
250 .flat_map(|workspace| {
251 workspace
252 .read(cx)
253 .project()
254 .read(cx)
255 .repositories(cx)
256 .values()
257 .cloned()
258 .collect::<Vec<_>>()
259 })
260 .find(|repo| {
261 repo.read(cx).snapshot().work_directory_abs_path.as_ref()
262 == repo_path_owned.as_path()
263 })
264 });
265
266 if let Some(repo) = live_repo {
267 return Ok((repo, None));
268 }
269
270 let app_state =
271 current_app_state(cx).context("no app state available for temporary project")?;
272 let temp_project = cx.update(|cx| {
273 Project::local(
274 app_state.client.clone(),
275 app_state.node_runtime.clone(),
276 app_state.user_store.clone(),
277 app_state.languages.clone(),
278 app_state.fs.clone(),
279 None,
280 LocalProjectFlags::default(),
281 cx,
282 )
283 });
284
285 let repo_path_for_worktree = repo_path.to_path_buf();
286 let create_worktree = temp_project.update(cx, |project, cx| {
287 project.create_worktree(repo_path_for_worktree, true, cx)
288 });
289 let _worktree = create_worktree.await?;
290 let initial_scan = temp_project.read_with(cx, |project, cx| project.wait_for_initial_scan(cx));
291 initial_scan.await;
292
293 let repo_path_for_find = repo_path.to_path_buf();
294 let repo = temp_project
295 .update(cx, |project, cx| {
296 project
297 .repositories(cx)
298 .values()
299 .find(|repo| {
300 repo.read(cx).snapshot().work_directory_abs_path.as_ref()
301 == repo_path_for_find.as_path()
302 })
303 .cloned()
304 })
305 .context("failed to resolve temporary repository handle")?;
306
307 let barrier = repo.update(cx, |repo: &mut Repository, _cx| repo.barrier());
308 barrier
309 .await
310 .map_err(|_| anyhow!("temporary repository barrier canceled"))?;
311 Ok((repo, Some(temp_project)))
312}
313
314/// Re-adds the worktree to every affected project after a failed
315/// [`remove_root`].
316async fn rollback_root(root: &RootPlan, cx: &mut AsyncApp) {
317 for affected in &root.affected_projects {
318 let task = affected.project.update(cx, |project, cx| {
319 project.create_worktree(root.root_path.clone(), true, cx)
320 });
321 task.await.log_err();
322 }
323}
324
325/// Saves the worktree's full git state so it can be restored later.
326///
327/// This creates two detached commits (via [`create_archive_checkpoint`] on
328/// the `GitRepository` trait) that capture the staged and unstaged state
329/// without moving any branch ref. The commits are:
330/// - "WIP staged": a tree matching the current index, parented on HEAD
331/// - "WIP unstaged": a tree with all files (including untracked),
332/// parented on the staged commit
333///
334/// After creating the commits, this function:
335/// 1. Records the commit SHAs, branch name, and paths in a DB record.
336/// 2. Links every thread referencing this worktree to that record.
337/// 3. Creates a git ref on the main repo to prevent GC of the commits.
338///
339/// On success, returns the archived worktree DB row ID for rollback.
340pub async fn persist_worktree_state(root: &RootPlan, cx: &mut AsyncApp) -> Result<i64> {
341 let worktree_repo = root.worktree_repo.clone();
342
343 let original_commit_hash = worktree_repo
344 .update(cx, |repo, _cx| repo.head_sha())
345 .await
346 .map_err(|_| anyhow!("head_sha canceled"))?
347 .context("failed to read original HEAD SHA")?
348 .context("HEAD SHA is None")?;
349
350 // Create two detached WIP commits without moving the branch.
351 let checkpoint_rx = worktree_repo.update(cx, |repo, _cx| repo.create_archive_checkpoint());
352 let (staged_commit_hash, unstaged_commit_hash) = checkpoint_rx
353 .await
354 .map_err(|_| anyhow!("create_archive_checkpoint canceled"))?
355 .context("failed to create archive checkpoint")?;
356
357 // Create DB record
358 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
359 let worktree_path_str = root.root_path.to_string_lossy().to_string();
360 let main_repo_path_str = root.main_repo_path.to_string_lossy().to_string();
361 let branch_name = root.branch_name.clone().or_else(|| {
362 worktree_repo.read_with(cx, |repo, _cx| {
363 repo.snapshot()
364 .branch
365 .as_ref()
366 .map(|branch| branch.name().to_string())
367 })
368 });
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 return Err(error);
388 }
389 };
390
391 // Link all threads on this worktree to the archived record
392 let session_ids: Vec<acp::SessionId> = store.read_with(cx, |store, _cx| {
393 store
394 .entries()
395 .filter(|thread| {
396 thread
397 .folder_paths
398 .paths()
399 .iter()
400 .any(|p| p.as_path() == root.root_path)
401 })
402 .map(|thread| thread.session_id.clone())
403 .collect()
404 });
405
406 for session_id in &session_ids {
407 let link_result = store
408 .read_with(cx, |store, cx| {
409 store.link_thread_to_archived_worktree(
410 session_id.0.to_string(),
411 archived_worktree_id,
412 cx,
413 )
414 })
415 .await;
416 if let Err(error) = link_result {
417 if let Err(delete_error) = store
418 .read_with(cx, |store, cx| {
419 store.delete_archived_worktree(archived_worktree_id, cx)
420 })
421 .await
422 {
423 log::error!(
424 "Failed to delete archived worktree DB record during link rollback: \
425 {delete_error:#}"
426 );
427 }
428 return Err(error.context("failed to link thread to archived worktree"));
429 }
430 }
431
432 // Create git ref on main repo to prevent GC of the detached commits.
433 // This is fatal: without the ref, git gc will eventually collect the
434 // WIP commits and a later restore will silently fail.
435 let ref_name = archived_worktree_ref_name(archived_worktree_id);
436 let (main_repo, _temp_project) = find_or_create_repository(&root.main_repo_path, cx)
437 .await
438 .context("could not open main repo to create archive ref")?;
439 let rx = main_repo.update(cx, |repo, _cx| {
440 repo.update_ref(ref_name.clone(), unstaged_commit_hash.clone())
441 });
442 rx.await
443 .map_err(|_| anyhow!("update_ref canceled"))
444 .and_then(|r| r)
445 .with_context(|| format!("failed to create ref {ref_name} on main repo"))?;
446 drop(_temp_project);
447
448 Ok(archived_worktree_id)
449}
450
451/// Undoes a successful [`persist_worktree_state`] by deleting the git ref
452/// on the main repo and removing the DB record. Since the WIP commits are
453/// detached (they don't move any branch), no git reset is needed — the
454/// commits will be garbage-collected once the ref is removed.
455pub async fn rollback_persist(archived_worktree_id: i64, root: &RootPlan, cx: &mut AsyncApp) {
456 // Delete the git ref on main repo
457 if let Ok((main_repo, _temp_project)) =
458 find_or_create_repository(&root.main_repo_path, cx).await
459 {
460 let ref_name = archived_worktree_ref_name(archived_worktree_id);
461 let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
462 rx.await.ok().and_then(|r| r.log_err());
463 drop(_temp_project);
464 }
465
466 // Delete the DB record
467 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
468 if let Err(error) = store
469 .read_with(cx, |store, cx| {
470 store.delete_archived_worktree(archived_worktree_id, cx)
471 })
472 .await
473 {
474 log::error!("Failed to delete archived worktree DB record during rollback: {error:#}");
475 }
476}
477
478/// Restores a previously archived worktree back to disk from its DB record.
479///
480/// Creates the git worktree at the original commit (the branch never moved
481/// during archival since WIP commits are detached), switches to the branch,
482/// then uses [`restore_archive_checkpoint`] to reconstruct the staged/
483/// unstaged state from the WIP commit trees.
484pub async fn restore_worktree_via_git(
485 row: &ArchivedGitWorktree,
486 cx: &mut AsyncApp,
487) -> Result<PathBuf> {
488 let (main_repo, _temp_project) = find_or_create_repository(&row.main_repo_path, cx).await?;
489
490 let worktree_path = &row.worktree_path;
491 let app_state = current_app_state(cx).context("no app state available")?;
492 let already_exists = app_state.fs.metadata(worktree_path).await?.is_some();
493
494 let created_new_worktree = if already_exists {
495 let is_git_worktree =
496 resolve_git_worktree_to_main_repo(app_state.fs.as_ref(), worktree_path)
497 .await
498 .is_some();
499
500 if !is_git_worktree {
501 let rx = main_repo.update(cx, |repo, _cx| repo.repair_worktrees());
502 rx.await
503 .map_err(|_| anyhow!("worktree repair was canceled"))?
504 .context("failed to repair worktrees")?;
505 }
506 false
507 } else {
508 // Create worktree at the original commit — the branch still points
509 // here because archival used detached commits.
510 let rx = main_repo.update(cx, |repo, _cx| {
511 repo.create_worktree_detached(worktree_path.clone(), row.original_commit_hash.clone())
512 });
513 rx.await
514 .map_err(|_| anyhow!("worktree creation was canceled"))?
515 .context("failed to create worktree")?;
516 true
517 };
518
519 let (wt_repo, _temp_wt_project) = match find_or_create_repository(worktree_path, cx).await {
520 Ok(result) => result,
521 Err(error) => {
522 remove_new_worktree_on_error(created_new_worktree, &main_repo, worktree_path, cx).await;
523 return Err(error);
524 }
525 };
526
527 // Switch to the branch. Since the branch was never moved during
528 // archival (WIP commits are detached), it still points at
529 // original_commit_hash, so this is essentially a no-op for HEAD.
530 if let Some(branch_name) = &row.branch_name {
531 let rx = wt_repo.update(cx, |repo, _cx| repo.change_branch(branch_name.clone()));
532 if let Err(checkout_error) = rx.await.map_err(|e| anyhow!("{e}")).and_then(|r| r) {
533 log::debug!(
534 "change_branch('{}') failed: {checkout_error:#}, trying create_branch",
535 branch_name
536 );
537 let rx = wt_repo.update(cx, |repo, _cx| {
538 repo.create_branch(branch_name.clone(), None)
539 });
540 if let Ok(Err(error)) | Err(error) = rx.await.map_err(|e| anyhow!("{e}")) {
541 log::warn!(
542 "Could not create branch '{}': {error} — \
543 restored worktree will be in detached HEAD state.",
544 branch_name
545 );
546 }
547 }
548 }
549
550 // Restore the staged/unstaged state from the WIP commit trees.
551 // read-tree --reset -u applies the unstaged tree (including deletions)
552 // to the working directory, then a bare read-tree sets the index to
553 // the staged tree without touching the working directory.
554 let restore_rx = wt_repo.update(cx, |repo, _cx| {
555 repo.restore_archive_checkpoint(
556 row.staged_commit_hash.clone(),
557 row.unstaged_commit_hash.clone(),
558 )
559 });
560 if let Err(error) = restore_rx
561 .await
562 .map_err(|_| anyhow!("restore_archive_checkpoint canceled"))
563 .and_then(|r| r)
564 {
565 remove_new_worktree_on_error(created_new_worktree, &main_repo, worktree_path, cx).await;
566 return Err(error.context("failed to restore archive checkpoint"));
567 }
568
569 Ok(worktree_path.clone())
570}
571
572async fn remove_new_worktree_on_error(
573 created_new_worktree: bool,
574 main_repo: &Entity<Repository>,
575 worktree_path: &PathBuf,
576 cx: &mut AsyncApp,
577) {
578 if created_new_worktree {
579 let rx = main_repo.update(cx, |repo, _cx| {
580 repo.remove_worktree(worktree_path.clone(), true)
581 });
582 rx.await.ok().and_then(|r| r.log_err());
583 }
584}
585
586/// Deletes the git ref and DB records for a single archived worktree.
587/// Used when an archived worktree is no longer referenced by any thread.
588pub async fn cleanup_archived_worktree_record(row: &ArchivedGitWorktree, cx: &mut AsyncApp) {
589 // Delete the git ref from the main repo
590 if let Ok((main_repo, _temp_project)) = find_or_create_repository(&row.main_repo_path, cx).await
591 {
592 let ref_name = archived_worktree_ref_name(row.id);
593 let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
594 match rx.await {
595 Ok(Ok(())) => {}
596 Ok(Err(error)) => log::warn!("Failed to delete archive ref: {error}"),
597 Err(_) => log::warn!("Archive ref deletion was canceled"),
598 }
599 // Keep _temp_project alive until after the await so the headless project isn't dropped mid-operation
600 drop(_temp_project);
601 }
602
603 // Delete the DB records
604 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
605 store
606 .read_with(cx, |store, cx| store.delete_archived_worktree(row.id, cx))
607 .await
608 .log_err();
609}
610
611/// Cleans up all archived worktree data associated with a thread being deleted.
612///
613/// This unlinks the thread from all its archived worktrees and, for any
614/// archived worktree that is no longer referenced by any other thread,
615/// deletes the git ref and DB records.
616pub async fn cleanup_thread_archived_worktrees(session_id: &acp::SessionId, cx: &mut AsyncApp) {
617 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
618
619 let archived_worktrees = store
620 .read_with(cx, |store, cx| {
621 store.get_archived_worktrees_for_thread(session_id.0.to_string(), cx)
622 })
623 .await;
624 let archived_worktrees = match archived_worktrees {
625 Ok(rows) => rows,
626 Err(error) => {
627 log::error!(
628 "Failed to fetch archived worktrees for thread {}: {error:#}",
629 session_id.0
630 );
631 return;
632 }
633 };
634
635 if archived_worktrees.is_empty() {
636 return;
637 }
638
639 if let Err(error) = store
640 .read_with(cx, |store, cx| {
641 store.unlink_thread_from_all_archived_worktrees(session_id.0.to_string(), cx)
642 })
643 .await
644 {
645 log::error!(
646 "Failed to unlink thread {} from archived worktrees: {error:#}",
647 session_id.0
648 );
649 return;
650 }
651
652 for row in &archived_worktrees {
653 let still_referenced = store
654 .read_with(cx, |store, cx| {
655 store.is_archived_worktree_referenced(row.id, cx)
656 })
657 .await;
658 match still_referenced {
659 Ok(true) => {}
660 Ok(false) => {
661 cleanup_archived_worktree_record(row, cx).await;
662 }
663 Err(error) => {
664 log::error!(
665 "Failed to check if archived worktree {} is still referenced: {error:#}",
666 row.id
667 );
668 }
669 }
670 }
671}
672
673/// Collects every `Workspace` entity across all open `MultiWorkspace` windows.
674pub fn all_open_workspaces(cx: &App) -> Vec<Entity<Workspace>> {
675 cx.windows()
676 .into_iter()
677 .filter_map(|window| window.downcast::<MultiWorkspace>())
678 .flat_map(|multi_workspace| {
679 multi_workspace
680 .read(cx)
681 .map(|multi_workspace| multi_workspace.workspaces().cloned().collect::<Vec<_>>())
682 .unwrap_or_default()
683 })
684 .collect()
685}
686
687fn current_app_state(cx: &mut AsyncApp) -> Option<Arc<AppState>> {
688 cx.update(|cx| {
689 all_open_workspaces(cx)
690 .into_iter()
691 .next()
692 .map(|workspace| workspace.read(cx).app_state().clone())
693 })
694}