diff --git a/Cargo.lock b/Cargo.lock index 2ad9a3e29ba927e23946e2a0867ace5722e03ee5..ddfa6e21f28c6b3da52ba84ae2f1f8761524e929 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21659,7 +21659,6 @@ dependencies = [ "collections", "component", "db", - "feature_flags", "fs", "futures 0.3.32", "git", @@ -21675,6 +21674,7 @@ dependencies = [ "postage", "pretty_assertions", "project", + "release_channel", "remote", "schemars", "serde", diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 86d2132123bcca1fdeaf54d04b337c2deeea1554..73380d4ca4ce7d9025353e933c22b3634e992287 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -71,6 +71,7 @@ use project::git_store::{GitStoreEvent, RepositoryEvent}; use project::project_settings::ProjectSettings; use project::{Project, ProjectPath, Worktree, WorktreePaths, linked_worktree_short_name}; use prompt_store::{PromptStore, UserPromptId}; +use release_channel::ReleaseChannel; use remote::RemoteConnectionOptions; use rules_library::{RulesLibrary, open_rules_library}; use settings::TerminalDockPosition; @@ -93,6 +94,10 @@ const AGENT_PANEL_KEY: &str = "agent_panel"; const MIN_PANEL_WIDTH: Pixels = px(300.); const RECENTLY_UPDATED_MENU_LIMIT: usize = 6; const LAST_USED_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; + +fn agent_v2_enabled(cx: &App) -> bool { + !matches!(ReleaseChannel::try_global(cx), Some(ReleaseChannel::Stable)) +} /// Maximum number of idle threads kept in the agent panel's retained list. /// Set as a GPUI global to override; otherwise defaults to 5. pub struct MaxIdleRetainedThreads(pub usize); @@ -1006,10 +1011,10 @@ impl AgentPanel { StartThreadIn::LocalProject => true, StartThreadIn::NewWorktree { .. } => { let project = panel.project.read(cx); - !project.is_via_collab() + agent_v2_enabled(cx) && !project.is_via_collab() } StartThreadIn::LinkedWorktree { path, .. } => { - path.exists() + agent_v2_enabled(cx) && path.exists() } }; if is_valid { @@ -2547,6 +2552,9 @@ impl AgentPanel { let new_target = match action { StartThreadIn::LocalProject => StartThreadIn::LocalProject, StartThreadIn::NewWorktree { .. } => { + if !agent_v2_enabled(cx) { + return; + } if !self.project_has_git_repository(cx) { log::error!( "set_start_thread_in: cannot use worktree mode without a git repository" @@ -2562,6 +2570,9 @@ impl AgentPanel { action.clone() } StartThreadIn::LinkedWorktree { .. } => { + if !agent_v2_enabled(cx) { + return; + } if !self.project_has_git_repository(cx) { log::error!( "set_start_thread_in: cannot use LinkedWorktree without a git repository" @@ -2586,6 +2597,10 @@ impl AgentPanel { } fn cycle_start_thread_in(&mut self, window: &mut Window, cx: &mut Context) { + if !agent_v2_enabled(cx) { + return; + } + let next = match &self.start_thread_in { StartThreadIn::LocalProject => StartThreadIn::NewWorktree { worktree_name: None, @@ -2600,6 +2615,15 @@ impl AgentPanel { fn reset_start_thread_in_to_default(&mut self, cx: &mut Context) { use settings::{NewThreadLocation, Settings}; + if !agent_v2_enabled(cx) { + if self.start_thread_in != StartThreadIn::LocalProject { + self.start_thread_in = StartThreadIn::LocalProject; + self.serialize(cx); + cx.notify(); + } + return; + } + let default = AgentSettings::get_global(cx).new_thread_location; let start_thread_in = match default { NewThreadLocation::LocalProject => StartThreadIn::LocalProject, @@ -2622,6 +2646,15 @@ impl AgentPanel { } fn sync_start_thread_in_with_git_state(&mut self, cx: &mut Context) { + if !agent_v2_enabled(cx) { + if self.start_thread_in != StartThreadIn::LocalProject { + self.start_thread_in = StartThreadIn::LocalProject; + self.serialize(cx); + cx.notify(); + } + return; + } + if matches!(self.start_thread_in, StartThreadIn::LocalProject) { return; } @@ -4108,6 +4141,47 @@ impl AgentPanel { }) } + fn render_recent_entries_menu( + &self, + icon: IconName, + corner: Corner, + cx: &mut Context, + ) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + PopoverMenu::new("agent-nav-menu") + .trigger_with_tooltip( + IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), + { + move |_window, cx| { + Tooltip::for_action_in( + "Toggle Recently Updated Threads", + &ToggleNavigationMenu, + &focus_handle, + cx, + ) + } + }, + ) + .anchor(corner) + .with_handle(self.agent_navigation_menu_handle.clone()) + .menu({ + let menu = self.agent_navigation_menu.clone(); + move |window, cx| { + telemetry::event!("View Thread History Clicked"); + + if let Some(menu) = menu.as_ref() { + menu.update(cx, |_, cx| { + cx.defer_in(window, |menu, window, cx| { + menu.rebuild(window, cx); + }); + }) + } + menu.clone() + } + }) + } + fn render_toolbar_back_button(&self, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle(cx); @@ -4493,6 +4567,8 @@ impl AgentPanel { selected_agent.into_any_element() }; + let show_history_menu = self.has_history_for_selected_agent(cx); + let agent_v2_enabled = agent_v2_enabled(cx); let is_empty_state = !self.active_thread_has_messages(cx); let is_in_history_or_config = self.is_history_or_configuration_visible(); @@ -4514,7 +4590,7 @@ impl AgentPanel { })) }; - let use_v2_empty_toolbar = is_empty_state && !is_in_history_or_config; + let use_v2_empty_toolbar = agent_v2_enabled && is_empty_state && !is_in_history_or_config; let max_content_width = AgentSettings::get_global(cx).max_content_width; @@ -4584,11 +4660,17 @@ impl AgentPanel { .pl(DynamicSpacing::Base04.rems(cx)) .child(agent_selector_menu) .when( - has_visible_worktrees && self.project_has_git_repository(cx), + agent_v2_enabled + && has_visible_worktrees + && self.project_has_git_repository(cx), |this| this.child(self.render_start_thread_in_selector(cx)), ) .when( - matches!(self.start_thread_in, StartThreadIn::NewWorktree { .. }), + agent_v2_enabled + && matches!( + self.start_thread_in, + StartThreadIn::NewWorktree { .. } + ), |this| this.child(self.render_new_worktree_branch_selector(cx)), ), ) @@ -4599,6 +4681,13 @@ impl AgentPanel { .gap_1() .pl_1() .pr_1() + .when(show_history_menu && !agent_v2_enabled, |this| { + this.child(self.render_recent_entries_menu( + IconName::MenuAltTemp, + Corner::TopRight, + cx, + )) + }) .child(full_screen_button) .child(self.render_panel_options_menu(window, cx)), ) @@ -4644,6 +4733,13 @@ impl AgentPanel { .pl_1() .pr_1() .child(new_thread_menu) + .when(show_history_menu && !agent_v2_enabled, |this| { + this.child(self.render_recent_entries_menu( + IconName::MenuAltTemp, + Corner::TopRight, + cx, + )) + }) .child(full_screen_button) .child(self.render_panel_options_menu(window, cx)), ) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 74145b4b38422e22d5f5409149d1ee80ab886a87..18075e4e318cf68f0e441cd5f3f06c2c13343c69 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -48,7 +48,7 @@ use agent_settings::{AgentProfileId, AgentSettings}; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; use fs::Fs; -use gpui::{Action, App, Context, Entity, SharedString, Window, actions}; +use gpui::{Action, App, Context, Entity, SharedString, UpdateGlobal as _, Window, actions}; use language::{ LanguageRegistry, language_settings::{AllLanguageSettings, EditPredictionProvider}, @@ -58,9 +58,10 @@ use language_model::{ }; use project::{AgentId, DisableAiSettings}; use prompt_store::PromptBuilder; +use release_channel::ReleaseChannel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{LanguageModelSelection, Settings as _, SettingsStore}; +use settings::{DockPosition, DockSide, LanguageModelSelection, Settings as _, SettingsStore}; use std::any::TypeId; use workspace::Workspace; @@ -514,7 +515,34 @@ pub fn init( }) .detach(); - maybe_backfill_editor_layout(fs, is_new_install, cx); + let agent_v2_enabled = agent_v2_enabled(cx); + if agent_v2_enabled { + maybe_backfill_editor_layout(fs, is_new_install, cx); + } + + SettingsStore::update_global(cx, |store, cx| { + store.update_default_settings(cx, |defaults| { + if agent_v2_enabled { + defaults.agent.get_or_insert_default().dock = Some(DockPosition::Left); + defaults.project_panel.get_or_insert_default().dock = Some(DockSide::Right); + defaults.outline_panel.get_or_insert_default().dock = Some(DockSide::Right); + defaults.collaboration_panel.get_or_insert_default().dock = + Some(DockPosition::Right); + defaults.git_panel.get_or_insert_default().dock = Some(DockPosition::Right); + } else { + defaults.agent.get_or_insert_default().dock = Some(DockPosition::Right); + defaults.project_panel.get_or_insert_default().dock = Some(DockSide::Left); + defaults.outline_panel.get_or_insert_default().dock = Some(DockSide::Left); + defaults.collaboration_panel.get_or_insert_default().dock = + Some(DockPosition::Left); + defaults.git_panel.get_or_insert_default().dock = Some(DockPosition::Left); + } + }); + }); +} + +fn agent_v2_enabled(cx: &App) -> bool { + !matches!(ReleaseChannel::try_global(cx), Some(ReleaseChannel::Stable)) } fn maybe_backfill_editor_layout(fs: Arc, is_new_install: bool, cx: &mut App) { @@ -542,6 +570,7 @@ fn maybe_backfill_editor_layout(fs: Arc, is_new_install: bool, cx: &mut fn update_command_palette_filter(cx: &mut App) { let disable_ai = DisableAiSettings::get_global(cx).disable_ai; let agent_enabled = AgentSettings::get_global(cx).enabled; + let agent_v2_enabled = agent_v2_enabled(cx); let edit_prediction_provider = AllLanguageSettings::get_global(cx) .edit_predictions @@ -611,7 +640,11 @@ fn update_command_palette_filter(cx: &mut App) { filter.show_action_types(&[TypeId::of::()]); } - filter.show_namespace("multi_workspace"); + if agent_v2_enabled { + filter.show_namespace("multi_workspace"); + } else { + filter.hide_namespace("multi_workspace"); + } }); } diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index 01691bdb80b09fd7ebaa3a428a2328c942587b33..ec199c18a3179ef70c9092cb737dd22c4514ceac 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -196,59 +196,60 @@ impl Dismissable for ParallelAgentAnnouncement { } fn announcement_for_version(version: &Version, cx: &App) -> Option { - match (version.major, version.minor, version.patch) { - (0, _, _) => { - if ParallelAgentAnnouncement::dismissed(cx) { - None - } else { - let fs = ::global(cx); - Some(AnnouncementContent { - heading: "Introducing Parallel Agents".into(), - description: "Run multiple agent threads simultaneously across projects." - .into(), - bullet_items: vec![ - "Use your favorite agents in parallel".into(), - "Optionally isolate agents using worktrees".into(), - "Combine multiple projects in one window".into(), - ], - primary_action_label: "Try Now".into(), - primary_action_url: None, - primary_action_callback: Some(Arc::new(move |window, cx| { - let already_agent_layout = - matches!(AgentSettings::get_layout(cx), WindowLayout::Agent(_)); - - let update; - if !already_agent_layout { - update = Some(AgentSettings::set_layout( - WindowLayout::Agent(None), - fs.clone(), - cx, - )); - } else { - update = None; - } + let version_with_parallel_agents = match ReleaseChannel::global(cx) { + ReleaseChannel::Stable => Version::new(0, 233, 0), + ReleaseChannel::Dev | ReleaseChannel::Nightly | ReleaseChannel::Preview => { + Version::new(0, 232, 0) + } + }; - window - .spawn(cx, async move |cx| { - if let Some(update) = update { - update.await.ok(); - } + if *version >= version_with_parallel_agents && !ParallelAgentAnnouncement::dismissed(cx) { + let fs = ::global(cx); + Some(AnnouncementContent { + heading: "Introducing Parallel Agents".into(), + description: "Run multiple agent threads simultaneously across projects.".into(), + bullet_items: vec![ + "Use your favorite agents in parallel".into(), + "Optionally isolate agents using worktrees".into(), + "Combine multiple projects in one window".into(), + ], + primary_action_label: "Try Now".into(), + primary_action_url: None, + primary_action_callback: Some(Arc::new(move |window, cx| { + let already_agent_layout = + matches!(AgentSettings::get_layout(cx), WindowLayout::Agent(_)); + + let update; + if !already_agent_layout { + update = Some(AgentSettings::set_layout( + WindowLayout::Agent(None), + fs.clone(), + cx, + )); + } else { + update = None; + } - cx.update(|window, cx| { - window.dispatch_action(Box::new(FocusWorkspaceSidebar), cx); - window.dispatch_action(Box::new(FocusAgent), cx); - }) - }) - .detach(); - })), - on_dismiss: Some(Arc::new(|cx| { - ParallelAgentAnnouncement::set_dismissed(true, cx) - })), - secondary_action_url: Some("https://zed.dev/blog/".into()), - }) - } - } - _ => None, + window + .spawn(cx, async move |cx| { + if let Some(update) = update { + update.await.ok(); + } + + cx.update(|window, cx| { + window.dispatch_action(Box::new(FocusWorkspaceSidebar), cx); + window.dispatch_action(Box::new(FocusAgent), cx); + }) + }) + .detach(); + })), + on_dismiss: Some(Arc::new(|cx| { + ParallelAgentAnnouncement::set_dismissed(true, cx) + })), + secondary_action_url: Some("https://zed.dev/blog/".into()), + }) + } else { + None } } diff --git a/crates/feature_flags/src/flags.rs b/crates/feature_flags/src/flags.rs index 474f5b35bb536349ce7c4693f5dbedd6ef8b474a..d9541d819626c52861e7679d2e9dff525bfbb1f9 100644 --- a/crates/feature_flags/src/flags.rs +++ b/crates/feature_flags/src/flags.rs @@ -12,6 +12,16 @@ impl FeatureFlag for PanicFeatureFlag { const NAME: &'static str = "panic"; } +pub struct AgentV2FeatureFlag; + +impl FeatureFlag for AgentV2FeatureFlag { + const NAME: &'static str = "agent-v2"; + + fn enabled_for_staff() -> bool { + true + } +} + /// A feature flag for granting access to beta ACP features. /// /// We reuse this feature flag for new betas, so don't delete it if it is not currently in use. diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index f3f44a754035f1b4531f8a0b987e26981c8963df..a4bee8803b7a47581e0bd197cf6c3d723e2dbfe5 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -1,5 +1,6 @@ use gpui::{Action as _, App}; use itertools::Itertools as _; +use release_channel::ReleaseChannel; use settings::{ AudioInputDeviceName, AudioOutputDeviceName, LanguageSettingsContent, SemanticTokens, SettingsContent, @@ -7281,7 +7282,7 @@ fn ai_page(cx: &App) -> SettingsPage { ] } - fn agent_configuration_section(_cx: &App) -> Box<[SettingsPageItem]> { + fn agent_configuration_section(cx: &App) -> Box<[SettingsPageItem]> { let mut items = vec![ SettingsPageItem::SectionHeader("Agent Configuration"), SettingsPageItem::SubPageLink(SubPageLink { @@ -7295,28 +7296,30 @@ fn ai_page(cx: &App) -> SettingsPage { }), ]; - items.push(SettingsPageItem::SettingItem(SettingItem { - title: "New Thread Location", - description: "Whether to start a new thread in the current local project or in a new Git worktree.", - field: Box::new(SettingField { - json_path: Some("agent.new_thread_location"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .new_thread_location - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .new_thread_location = value; - }, - }), - metadata: None, - files: USER, - })); + if !matches!(ReleaseChannel::try_global(cx), Some(ReleaseChannel::Stable)) { + items.push(SettingsPageItem::SettingItem(SettingItem { + title: "New Thread Location", + description: "Whether to start a new thread in the current local project or in a new Git worktree.", + field: Box::new(SettingField { + json_path: Some("agent.new_thread_location"), + pick: |settings_content| { + settings_content + .agent + .as_ref()? + .new_thread_location + .as_ref() + }, + write: |settings_content, value| { + settings_content + .agent + .get_or_insert_default() + .new_thread_location = value; + }, + }), + metadata: None, + files: USER, + })); + } items.extend([ SettingsPageItem::SettingItem(SettingItem { diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 42e64504f348a727d17d2538d06556497fba54df..2014e7ad6f61bad8d2939c0232af92755eccea7f 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -36,7 +36,6 @@ clock.workspace = true collections.workspace = true component.workspace = true db.workspace = true -feature_flags.workspace = true fs.workspace = true futures.workspace = true git.workspace = true @@ -51,6 +50,7 @@ node_runtime.workspace = true parking_lot.workspace = true postage.workspace = true project.workspace = true +release_channel.workspace = true remote.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 4d87a3d4cddaa8263600508cc51b63a666e95f5c..30350a0943bb6cf1f1deb625d5529666766da811 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -7,6 +7,7 @@ use gpui::{ }; pub use project::ProjectGroupKey; use project::{DirectoryLister, DisableAiSettings, Project}; +use release_channel::ReleaseChannel; use remote::RemoteConnectionOptions; use settings::Settings; pub use settings::SidebarSide; @@ -397,7 +398,8 @@ impl MultiWorkspace { } pub fn multi_workspace_enabled(&self, cx: &App) -> bool { - !DisableAiSettings::get_global(cx).disable_ai + !matches!(ReleaseChannel::try_global(cx), Some(ReleaseChannel::Stable)) + && !DisableAiSettings::get_global(cx).disable_ai } pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context) { diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index b13911efa7d249715fd0efca6b20296d2c3fbc9c..92be09286956ab33202391f56a1dec8ef83d0f92 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -2507,7 +2507,7 @@ mod tests { read_multi_workspace_state, }, }; - use feature_flags::FeatureFlagAppExt; + use gpui::AppContext as _; use pretty_assertions::assert_eq; use project::Project; @@ -2527,10 +2527,6 @@ mod tests { async fn test_multi_workspace_serializes_on_add_and_remove(cx: &mut gpui::TestAppContext) { crate::tests::init_test(cx); - cx.update(|cx| { - cx.set_staff(true); - }); - let fs = fs::FakeFs::new(cx.executor()); let project1 = Project::test(fs.clone(), [], cx).await; let project2 = Project::test(fs.clone(), [], cx).await; @@ -4088,10 +4084,6 @@ mod tests { async fn test_flush_serialization_completes_before_quit(cx: &mut gpui::TestAppContext) { crate::tests::init_test(cx); - cx.update(|cx| { - cx.set_staff(true); - }); - let fs = fs::FakeFs::new(cx.executor()); let project = Project::test(fs.clone(), [], cx).await; @@ -4132,10 +4124,6 @@ mod tests { async fn test_create_workspace_serialization(cx: &mut gpui::TestAppContext) { crate::tests::init_test(cx); - cx.update(|cx| { - cx.set_staff(true); - }); - let fs = fs::FakeFs::new(cx.executor()); let project = Project::test(fs.clone(), [], cx).await; @@ -4188,10 +4176,6 @@ mod tests { async fn test_remove_workspace_clears_session_binding(cx: &mut gpui::TestAppContext) { crate::tests::init_test(cx); - cx.update(|cx| { - cx.set_staff(true); - }); - let fs = fs::FakeFs::new(cx.executor()); let dir = unique_test_dir(&fs, "remove").await; let project1 = Project::test(fs.clone(), [], cx).await; @@ -4279,10 +4263,6 @@ mod tests { async fn test_remove_workspace_not_restored_as_zombie(cx: &mut gpui::TestAppContext) { crate::tests::init_test(cx); - cx.update(|cx| { - cx.set_staff(true); - }); - let fs = fs::FakeFs::new(cx.executor()); let dir1 = tempfile::TempDir::with_prefix("zombie_test1").unwrap(); let dir2 = tempfile::TempDir::with_prefix("zombie_test2").unwrap(); @@ -4385,10 +4365,6 @@ mod tests { async fn test_pending_removal_tasks_drained_on_flush(cx: &mut gpui::TestAppContext) { crate::tests::init_test(cx); - cx.update(|cx| { - cx.set_staff(true); - }); - let fs = fs::FakeFs::new(cx.executor()); let dir = unique_test_dir(&fs, "pending-removal").await; let project1 = Project::test(fs.clone(), [], cx).await; @@ -4489,10 +4465,6 @@ mod tests { async fn test_create_workspace_bounds_observer_uses_fresh_id(cx: &mut gpui::TestAppContext) { crate::tests::init_test(cx); - cx.update(|cx| { - cx.set_staff(true); - }); - let fs = fs::FakeFs::new(cx.executor()); let project = Project::test(fs.clone(), [], cx).await; @@ -4545,10 +4517,6 @@ mod tests { async fn test_flush_serialization_writes_bounds(cx: &mut gpui::TestAppContext) { crate::tests::init_test(cx); - cx.update(|cx| { - cx.set_staff(true); - }); - let fs = fs::FakeFs::new(cx.executor()); let dir = tempfile::TempDir::with_prefix("flush_bounds_test").unwrap(); fs.insert_tree(dir.path(), json!({})).await; @@ -4704,10 +4672,6 @@ mod tests { ) { crate::tests::init_test(cx); - cx.update(|cx| { - cx.set_staff(true); - }); - let fs = fs::FakeFs::new(cx.executor()); // Main git repo at /repo @@ -4887,10 +4851,6 @@ mod tests { #[gpui::test] async fn test_remove_project_group_falls_back_to_neighbor(cx: &mut gpui::TestAppContext) { crate::tests::init_test(cx); - cx.update(|cx| { - cx.set_staff(true); - cx.update_flags(true, vec!["agent-v2".to_string()]); - }); let fs = fs::FakeFs::new(cx.executor()); let dir_a = unique_test_dir(&fs, "group-a").await; @@ -5002,10 +4962,6 @@ mod tests { #[gpui::test] async fn test_remove_fallback_skips_excluded_workspaces(cx: &mut gpui::TestAppContext) { crate::tests::init_test(cx); - cx.update(|cx| { - cx.set_staff(true); - cx.update_flags(true, vec!["agent-v2".to_string()]); - }); let fs = fs::FakeFs::new(cx.executor()); let dir = unique_test_dir(&fs, "shared").await;