From cdb00f529391af6ba8b9ead948ea17ff12e64176 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:15:10 +0000 Subject: [PATCH] Add each panel to the workspace as soon as it's ready (#43414) (cherry-pick to stable) (#43426) Cherry-pick of #43414 to stable ---- We'll now add panels to the workspace as soon as they're ready rather than waiting for all the rest to complete. We should strive to make all panels fast, but given that their load tasks are fallible and do IO, this approach seems more resilient. Additionally, we'll now start loading the agent panel at the same time as the rest. Release Notes: - workspace: Add panels as soon as they are ready Co-authored-by: Agus Zubiaga --- crates/collab/src/tests/following_tests.rs | 2 +- crates/workspace/src/dock.rs | 17 +- crates/workspace/src/workspace.rs | 12 +- crates/zed/src/zed.rs | 195 +++++++++++---------- 4 files changed, 122 insertions(+), 104 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 07cf866a3513d27894307216e904b130eb023e22..4b04e955d98c499eecc7281df5088e2c9c643cca 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -523,7 +523,7 @@ async fn test_basic_following( }); // Client B activates a panel, and the previously-opened screen-sharing item gets activated. - let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, cx)); + let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, 100, cx)); workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.add_panel(panel, window, cx); workspace.toggle_panel_focus::(window, cx); diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 05af5d080c4c965f3d53f61b5af144a456ce0074..dfc341db9c71fd1059853b9480a7e679109ead40 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -560,7 +560,16 @@ impl Dock { .binary_search_by_key(&panel.read(cx).activation_priority(), |entry| { entry.panel.activation_priority(cx) }) { - Ok(ix) => ix, + Ok(ix) => { + if cfg!(debug_assertions) { + panic!( + "Panels `{}` and `{}` have the same activation priority. Each panel must have a unique priority so the status bar order is deterministic.", + T::panel_key(), + self.panel_entries[ix].panel.panel_key() + ); + } + ix + } Err(ix) => ix, }; if let Some(active_index) = self.active_panel_index.as_mut() @@ -994,19 +1003,21 @@ pub mod test { pub active: bool, pub focus_handle: FocusHandle, pub size: Pixels, + pub activation_priority: u32, } actions!(test_only, [ToggleTestPanel]); impl EventEmitter for TestPanel {} impl TestPanel { - pub fn new(position: DockPosition, cx: &mut App) -> Self { + pub fn new(position: DockPosition, activation_priority: u32, cx: &mut App) -> Self { Self { position, zoomed: false, active: false, focus_handle: cx.focus_handle(), size: px(300.), + activation_priority, } } } @@ -1072,7 +1083,7 @@ pub mod test { } fn activation_priority(&self) -> u32 { - 100 + self.activation_priority } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 316969812ac34e84f4019a191fda225e255700f0..47301951bac2a5f9ddf5f2785cee4966ace1b924 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9174,7 +9174,7 @@ mod tests { cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let panel = workspace.update_in(cx, |workspace, window, cx| { - let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx)); + let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); workspace.add_panel(panel.clone(), window, cx); workspace @@ -9407,10 +9407,10 @@ mod tests { // Open two docks (left and right) with one panel each let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| { - let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, cx)); + let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx)); workspace.add_panel(left_panel.clone(), window, cx); - let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx)); + let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx)); workspace.add_panel(right_panel.clone(), window, cx); workspace.toggle_dock(DockPosition::Left, window, cx); @@ -9838,10 +9838,10 @@ mod tests { cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| { - let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx)); + let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx)); workspace.add_panel(panel_1.clone(), window, cx); workspace.toggle_dock(DockPosition::Left, window, cx); - let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx)); + let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx)); workspace.add_panel(panel_2.clone(), window, cx); workspace.toggle_dock(DockPosition::Right, window, cx); @@ -10748,7 +10748,7 @@ mod tests { // Add a new panel to the right dock, opening the dock and setting the // focus to the new panel. let panel = workspace.update_in(cx, |workspace, window, cx| { - let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx)); + let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); workspace.add_panel(panel.clone(), window, cx); workspace diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cbd6fba694136c87c64c4d6ca2bfee6d1e1072dd..96ec2408b33fece5b221975e2c015715e4d579e0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -22,16 +22,17 @@ use editor::{Editor, MultiBuffer}; use extension_host::ExtensionStore; use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag}; use fs::Fs; +use futures::FutureExt as _; use futures::future::Either; use futures::{StreamExt, channel::mpsc, select_biased}; use git_ui::commit_view::CommitViewToolbar; use git_ui::git_panel::GitPanel; use git_ui::project_diff::ProjectDiffToolbar; use gpui::{ - Action, App, AppContext as _, Context, DismissEvent, Element, Entity, Focusable, KeyBinding, - ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Styled, Task, - TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, image_cache, point, - px, retain_all, + Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity, + Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, + Styled, Task, TitlebarOptions, UpdateGlobal, WeakEntity, Window, WindowKind, WindowOptions, + actions, image_cache, point, px, retain_all, }; use image_viewer::ImageInfo; use language::Capability; @@ -602,105 +603,111 @@ fn initialize_panels( ); let debug_panel = DebugPanel::load(workspace_handle.clone(), cx); - let ( - project_panel, - outline_panel, - terminal_panel, - git_panel, - channels_panel, - notification_panel, - debug_panel, - ) = futures::try_join!( - project_panel, - outline_panel, - git_panel, - terminal_panel, - channels_panel, - notification_panel, - debug_panel, - )?; - - workspace_handle.update_in(cx, |workspace, window, cx| { - workspace.add_panel(project_panel, window, cx); - workspace.add_panel(outline_panel, window, cx); - workspace.add_panel(terminal_panel, window, cx); - workspace.add_panel(git_panel, window, cx); - workspace.add_panel(channels_panel, window, cx); - workspace.add_panel(notification_panel, window, cx); - workspace.add_panel(debug_panel, window, cx); - })?; - - fn setup_or_teardown_agent_panel( - workspace: &mut Workspace, - prompt_builder: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let disable_ai = SettingsStore::global(cx) - .get::(None) - .disable_ai - || cfg!(test); - let existing_panel = workspace.panel::(cx); - match (disable_ai, existing_panel) { - (false, None) => cx.spawn_in(window, async move |workspace, cx| { - let panel = - agent_ui::AgentPanel::load(workspace.clone(), prompt_builder, cx.clone()) - .await?; - workspace.update_in(cx, |workspace, window, cx| { - let disable_ai = SettingsStore::global(cx) - .get::(None) - .disable_ai; - let have_panel = workspace.panel::(cx).is_some(); - if !disable_ai && !have_panel { - workspace.add_panel(panel, window, cx); - } + async fn add_panel_when_ready( + panel_task: impl Future>> + 'static, + workspace_handle: WeakEntity, + mut cx: gpui::AsyncWindowContext, + ) { + if let Some(panel) = panel_task.await.context("failed to load panel").log_err() + { + workspace_handle + .update_in(&mut cx, |workspace, window, cx| { + workspace.add_panel(panel, window, cx); }) - }), - (true, Some(existing_panel)) => { - workspace.remove_panel::(&existing_panel, window, cx); - Task::ready(Ok(())) - } - _ => Task::ready(Ok(())), + .log_err(); } } - workspace_handle - .update_in(cx, |workspace, window, cx| { - setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx) - })? - .await?; + futures::join!( + add_panel_when_ready(project_panel, workspace_handle.clone(), cx.clone()), + add_panel_when_ready(outline_panel, workspace_handle.clone(), cx.clone()), + add_panel_when_ready(terminal_panel, workspace_handle.clone(), cx.clone()), + add_panel_when_ready(git_panel, workspace_handle.clone(), cx.clone()), + add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()), + add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()), + add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()), + initialize_agent_panel(workspace_handle, prompt_builder, cx.clone()).map(|r| r.log_err()) + ); - workspace_handle.update_in(cx, |workspace, window, cx| { - cx.observe_global_in::(window, { - let prompt_builder = prompt_builder.clone(); - move |workspace, window, cx| { - setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx) - .detach_and_log_err(cx); - } - }) - .detach(); + anyhow::Ok(()) + }) + .detach(); +} - // Register the actions that are shared between `assistant` and `assistant2`. - // - // We need to do this here instead of within the individual `init` - // functions so that we only register the actions once. - // - // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`. - if !cfg!(test) { - ::set_global( - Arc::new(agent_ui::ConcreteAssistantPanelDelegate), - cx, - ); +async fn initialize_agent_panel( + workspace_handle: WeakEntity, + prompt_builder: Arc, + mut cx: AsyncWindowContext, +) -> anyhow::Result<()> { + fn setup_or_teardown_agent_panel( + workspace: &mut Workspace, + prompt_builder: Arc, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let disable_ai = SettingsStore::global(cx) + .get::(None) + .disable_ai + || cfg!(test); + let existing_panel = workspace.panel::(cx); + match (disable_ai, existing_panel) { + (false, None) => cx.spawn_in(window, async move |workspace, cx| { + let panel = + agent_ui::AgentPanel::load(workspace.clone(), prompt_builder, cx.clone()) + .await?; + workspace.update_in(cx, |workspace, window, cx| { + let disable_ai = SettingsStore::global(cx) + .get::(None) + .disable_ai; + let have_panel = workspace.panel::(cx).is_some(); + if !disable_ai && !have_panel { + workspace.add_panel(panel, window, cx); + } + }) + }), + (true, Some(existing_panel)) => { + workspace.remove_panel::(&existing_panel, window, cx); + Task::ready(Ok(())) + } + _ => Task::ready(Ok(())), + } + } - workspace - .register_action(agent_ui::AgentPanel::toggle_focus) - .register_action(agent_ui::InlineAssistant::inline_assist); + workspace_handle + .update_in(&mut cx, |workspace, window, cx| { + setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx) + })? + .await?; + + workspace_handle.update_in(&mut cx, |workspace, window, cx| { + cx.observe_global_in::(window, { + let prompt_builder = prompt_builder.clone(); + move |workspace, window, cx| { + setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx) + .detach_and_log_err(cx); } - })?; + }) + .detach(); - anyhow::Ok(()) - }) - .detach(); + // Register the actions that are shared between `assistant` and `assistant2`. + // + // We need to do this here instead of within the individual `init` + // functions so that we only register the actions once. + // + // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`. + if !cfg!(test) { + ::set_global( + Arc::new(agent_ui::ConcreteAssistantPanelDelegate), + cx, + ); + + workspace + .register_action(agent_ui::AgentPanel::toggle_focus) + .register_action(agent_ui::InlineAssistant::inline_assist); + } + })?; + + anyhow::Ok(()) } fn register_actions(