Thread source workspace through worktree switches

This commit is contained in:
Nathan Sobo
2026-04-17 22:02:36 -06:00
parent 631ab75cd3
commit 8835a88a8b
15 changed files with 573 additions and 208 deletions
+198
View File
@@ -0,0 +1,198 @@
# Worktree-switch refactor follow-up hand-off
This document captures the work developed as a follow-up on top of Danilo Leal's original PR:
- Original PR: `#54183`
- Original title: `Move the worktree picker to the title bar + make it always visible`
- Original branch: `worktree-picker-title-bar`
The goal of this follow-up was **not** to change the visible worktree-picker product direction from the original PR. The goal was to finish the worktree-switch refactor so that switching from one workspace/worktree to another exposes the **source workspace directly**, and lets higher layers interpret that directly rather than relying on copied transition state or transient mailboxes.
## Architectural intent implemented in this follow-up
The intended model for worktree switching is now:
- the workspace/worktree switching path knows **which workspace it is leaving**
- generic layers expose only `source_workspace: Option<WeakEntity<Workspace>>`
- feature layers interpret that source workspace directly
- `AgentPanel` initialization inspects the source workspace's `AgentPanel` directly
- there is **no** copied draft mailbox/store and **no** generic transition payload blob
## What was explicitly avoided
The follow-up work intentionally avoided these designs:
- no `WorkspaceTransitionState` / `PanelTransitionState` / `AgentPanelTransitionState`
- no generic payload object passed through workspace APIs
- no `MultiWorkspace.pending_worktree_switch_text`
- no `agent_ui` global draft handoff store
- no side-channel source-stashes / destination-consumes mailbox design
- no agent-specific transfer state in `MultiWorkspace`
- no agent draft transport via `WorktreeCreationChanged`
## Core code changes made
### 1. Canonical activation API
`MultiWorkspace::activate(...)` was changed to the canonical form:
- `activate(workspace, source_workspace, window, cx)`
where:
- `source_workspace: Option<WeakEntity<Workspace>>`
This is the one canonical activation form. Ordinary activation paths now pass `None`. Worktree-switch activation paths pass `Some(source_workspace)`.
Related changes:
- `MultiWorkspaceEvent::ActiveWorkspaceChanged` now carries:
- `source_workspace: Option<WeakEntity<Workspace>>`
- subscribers that do not care about the source just match `ActiveWorkspaceChanged { .. }`
- zed's agent panel setup path consumes the explicit source from that event
### 2. Generic worktree/workspace switching now threads source workspace explicitly
In `crates/git_ui/src/worktree_service.rs`:
- the source workspace is already available as the workspace we are switching away from
- `open_worktree_workspace(...)` now passes that source workspace into the destination activation path explicitly
- after finding or creating the destination workspace, activation is performed with:
- `multi_workspace.activate(new_workspace.clone(), Some(source_workspace.clone()), window, cx)`
This preserves the intended primitive:
- "we are activating this destination workspace"
- "here is the workspace we are coming from"
### 3. Agent panel setup path is now source-workspace aware
In `crates/zed/src/zed.rs`:
- `ensure_agent_panel_for_workspace(...)` now takes:
- `source_workspace: Option<WeakEntity<Workspace>>`
- after ensuring the `AgentPanel` is present, it calls into `AgentPanel` only if a source workspace was explicitly provided
- the `MultiWorkspaceEvent::ActiveWorkspaceChanged { source_workspace }` subscription forwards that explicit source into the panel setup path
This keeps the source interpretation in the panel install/setup flow instead of in runtime draft-transport subscriptions.
### 4. AgentPanel now derives state directly from the source AgentPanel
In `crates/agent_ui/src/agent_panel.rs`:
Added source-aware initialization helpers:
- `destination_has_meaningful_state(...)`
- `active_initial_content(...)`
- `source_panel_initialization(...)`
- `initialize_from_source_workspace_if_needed(...)`
The important behavior is:
- look up the source workspace directly
- find its `AgentPanel` directly
- read the source panel's active draft/editor state directly
- initialize the destination panel only if the destination is effectively fresh/uninitialized
The destination overwrite policy is conservative:
- if destination panel already has meaningful initialized state, do nothing
- if destination is fresh, initialize it from the source panel in one shot
The source-derived data comes from the source `AgentPanel` itself, not from a copied temporary store.
## Transient mechanisms that were removed
These mechanisms were removed from the on-disk diff:
- `PendingWorktreeDraftStore` in `agent_panel.rs`
- `stash_pending_worktree_draft(...)`
- `take_pending_worktree_draft(...)`
- the `AgentPanel` `WorktreeCreationChanged` subscription used only for draft handoff
- the `did_stash_worktree_draft` field
- the temporary `Workspace.pending_source_workspace` approach that was briefly introduced during the refactor attempt
- the separate `activate_with_source_workspace(...)` method, in favor of a single canonical `activate(...)`
There should now be no transient handoff store/mailbox in the codepath described above.
## Files materially changed in this follow-up
These files contain the main architectural changes:
- `crates/git_ui/src/worktree_service.rs`
- `crates/workspace/src/multi_workspace.rs`
- `crates/workspace/src/workspace.rs`
- `crates/agent_ui/src/agent_panel.rs`
- `crates/zed/src/zed.rs`
Other touched files are mostly call-site updates caused by the canonical `activate(...)` signature and related event shape changes:
- `crates/agent_ui/src/conversation_view.rs`
- `crates/call/src/call_impl/mod.rs`
- `crates/recent_projects/src/remote_connections.rs`
- `crates/recent_projects/src/remote_servers.rs`
- `crates/sidebar/src/sidebar.rs`
- `crates/workspace/src/persistence.rs`
- `crates/zed/src/visual_test_runner.rs`
- plus some already-existing branch work in `git_ui` files
## Important current status / remaining work
The architectural refactor is in place, but this follow-up is **not fully mechanically finished**.
### Remaining known work
There are still remaining old 3-argument `activate(...)` test call sites that need to be updated to the canonical 4-argument form:
- `mw.activate(workspace, None, window, cx)`
The most obvious remaining cluster is in:
- `crates/sidebar/src/sidebar_tests.rs`
At the time of hand-off, that file had unsaved editor changes in the environment, so I intentionally did **not** force-save or overwrite it. That means the branch may still contain compile fallout from those unchanged old call sites.
There may also be a few other remaining mechanical call-site updates in tests/helpers that should be cleaned up with a repo-wide search for:
- `mw.activate(`
- `multi_workspace.activate(`
and then converting remaining old-form calls to:
- `activate(workspace, None, window, cx)`
unless the call is a true worktree-switch case that should pass `Some(source_workspace)`.
### Diagnostics status
I ran targeted `cargo check` attempts during the refactor and used those diagnostics to fix the main architectural call sites.
The last known remaining failures were mechanical old-signature call sites after the canonical `activate(...)` change, not new architectural design issues.
I stopped short of further blind edits in files with unsaved buffer state.
## Suggested verification steps for the next person
1. Search for all remaining `activate(...)` call sites and ensure they use the canonical signature.
2. Run a targeted `cargo check` again after finishing those mechanical updates.
3. Verify these behaviors manually or with tests:
- switching from workspace A to workspace B restores workspace-level dock/file/focus state from A
- destination `AgentPanel` initializes from source `AgentPanel` when destination is fresh
- destination `AgentPanel` does **not** overwrite meaningful existing destination state
- no draft text is transported through any copied mailbox/store
4. Consider adding or updating tests for:
- source-aware activation event payload
- conservative destination initialization behavior
- reused destination workspace cases
## Summary of the intended final model
The target model for this follow-up is:
- workspace switching knows what workspace it is leaving
- generic layers only expose that source workspace
- feature layers interpret it themselves
- `AgentPanel` initialization directly inspects the source `AgentPanel`
- no copied draft mailbox/store exists anywhere
If additional cleanup is needed, it should stay within that architecture and avoid reintroducing any transient transfer state.
+97 -83
View File
@@ -59,8 +59,8 @@ use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner,
DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels,
Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext,
Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
};
use language::LanguageRegistry;
use language_model::LanguageModelRegistry;
@@ -672,8 +672,6 @@ pub struct AgentPanel {
_thread_view_subscription: Option<Subscription>,
_active_thread_focus_subscription: Option<Subscription>,
show_trust_workspace_message: bool,
did_stash_worktree_draft: bool,
pending_worktree_draft: Option<String>,
_base_view_observation: Option<Subscription>,
_draft_editor_observation: Option<Subscription>,
}
@@ -1038,73 +1036,11 @@ impl AgentPanel {
_thread_view_subscription: None,
_active_thread_focus_subscription: None,
show_trust_workspace_message: false,
did_stash_worktree_draft: false,
pending_worktree_draft: None,
new_user_onboarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)),
_base_view_observation: None,
_draft_editor_observation: None,
};
// Subscribe to the workspace to detect worktree creation changes.
// When creation starts (label becomes Some): this is the SOURCE panel.
// → Stash draft text via the workspace helper and set a flag.
// When creation ends (label becomes None): this is the DESTINATION panel
// (the flag is false on fresh panels) → pick up the stashed text.
if let Some(workspace_entity) = panel.workspace.upgrade() {
cx.subscribe_in(
&workspace_entity,
window,
|this, workspace, event: &workspace::Event, window, cx| {
if !matches!(event, workspace::Event::WorktreeCreationChanged) {
return;
}
let is_creating = workspace
.read(cx)
.active_worktree_creation()
.label
.is_some();
if is_creating {
let draft_prompt = this.active_thread_view(cx).and_then(|thread_view| {
let text = thread_view.read(cx).message_editor.read(cx).text(cx);
if text.is_empty() { None } else { Some(text) }
});
let multi_workspace = workspace.read(cx).multi_workspace().cloned();
if let Some(multi_workspace) = multi_workspace.and_then(|mw| mw.upgrade()) {
multi_workspace.update(cx, |mw, _cx| {
mw.pending_worktree_switch_text = draft_prompt;
});
}
this.did_stash_worktree_draft = true;
} else if !this.did_stash_worktree_draft {
let multi_workspace = workspace.read(cx).multi_workspace().cloned();
let pending_text =
multi_workspace.and_then(|mw| mw.upgrade()).and_then(|mw| {
mw.update(cx, |mw, _cx| mw.pending_worktree_switch_text.take())
});
if let Some(text) = pending_text {
if let Some(thread_view) = this.active_thread_view(cx) {
thread_view.update(cx, |thread_view, cx| {
thread_view.message_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.insert_text(&text, window, cx);
});
});
} else {
this.pending_worktree_draft = Some(text);
this.ensure_thread_initialized(window, cx);
}
}
}
},
)
.detach();
}
// Initial sync of agent servers from extensions
panel.sync_agent_servers_from_extensions(cx);
panel
@@ -2241,7 +2177,6 @@ impl AgentPanel {
|this, server_view, window, cx| {
this._thread_view_subscription =
Self::subscribe_to_active_thread_view(&server_view, window, cx);
this.apply_pending_worktree_draft(window, cx);
cx.emit(AgentPanelEvent::ActiveViewChanged);
this.serialize(cx);
cx.notify();
@@ -2784,25 +2719,104 @@ impl AgentPanel {
if matches!(self.base_view, BaseView::Uninitialized) {
self.activate_draft(false, window, cx);
}
self.apply_pending_worktree_draft(window, cx);
}
/// Tries to insert any existing draft prompt stashed in `pending_worktree_draft`
/// into the active thread view's message editor.
fn apply_pending_worktree_draft(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(text) = self.pending_worktree_draft.take() {
if let Some(thread_view) = self.active_thread_view(cx) {
thread_view.update(cx, |thread_view, cx| {
thread_view.message_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.insert_text(&text, window, cx);
});
});
} else {
self.pending_worktree_draft = Some(text);
}
fn destination_has_meaningful_state(&self, cx: &App) -> bool {
if !matches!(self.base_view, BaseView::Uninitialized) {
return true;
}
if self.overlay_view.is_some() || !self.retained_threads.is_empty() {
return true;
}
self.draft_thread.as_ref().is_some_and(|conversation_view| {
conversation_view.read(cx).root_thread_view().is_some_and(|thread_view| {
let thread_view = thread_view.read(cx);
thread_view
.thread
.read(cx)
.draft_prompt()
.is_some_and(|draft| !draft.is_empty())
|| !thread_view.message_editor.read(cx).text(cx).trim().is_empty()
})
})
}
fn active_initial_content(&self, cx: &App) -> Option<AgentInitialContent> {
self.active_thread_view(cx).and_then(|thread_view| {
thread_view.read(cx).thread.read(cx).draft_prompt().map(|draft| {
AgentInitialContent::ContentBlock {
blocks: draft.to_vec(),
auto_submit: false,
}
}).filter(|initial_content| match initial_content {
AgentInitialContent::ContentBlock { blocks, .. } => !blocks.is_empty(),
_ => true,
}).or_else(|| {
let text = thread_view.read(cx).message_editor.read(cx).text(cx);
if text.trim().is_empty() {
None
} else {
Some(AgentInitialContent::ContentBlock {
blocks: vec![acp::ContentBlock::Text(acp::TextContent::new(text))],
auto_submit: false,
})
}
})
})
}
fn source_panel_initialization(
source_workspace: &WeakEntity<Workspace>,
cx: &App,
) -> Option<(Agent, AgentInitialContent)> {
let source_workspace = source_workspace.upgrade()?;
let source_panel = source_workspace.read(cx).panel::<AgentPanel>(cx)?;
let source_panel = source_panel.read(cx);
let initial_content = source_panel.active_initial_content(cx)?;
let agent = if source_panel.project.read(cx).is_via_collab() {
Agent::NativeAgent
} else {
source_panel.selected_agent.clone()
};
Some((agent, initial_content))
}
pub fn initialize_from_source_workspace_if_needed(
&mut self,
source_workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
if self.destination_has_meaningful_state(cx) {
return false;
}
let Some((agent, initial_content)) = Self::source_panel_initialization(&source_workspace, cx)
else {
return false;
};
let agent = if self.project.read(cx).is_via_collab() {
Agent::NativeAgent
} else {
agent
};
let thread = self.create_agent_thread(
agent,
None,
None,
None,
Some(initial_content),
"agent_panel",
window,
cx,
);
self.draft_thread = Some(thread.conversation_view.clone());
self.observe_draft_editor(&thread.conversation_view, cx);
self.set_base_view(thread.into(), false, window, cx);
true
}
fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
+6 -1
View File
@@ -2560,7 +2560,12 @@ impl ConversationView {
.update(cx, |multi_workspace, window, cx| {
window.activate_window();
if let Some(workspace) = workspace_handle.upgrade() {
multi_workspace.activate(workspace.clone(), window, cx);
multi_workspace.activate(
workspace.clone(),
None,
window,
cx,
);
workspace.update(cx, |workspace, cx| {
workspace.reveal_panel::<AgentPanel>(window, cx);
if let Some(panel) =
+1 -1
View File
@@ -40,7 +40,7 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
&cx.entity(),
window,
move |multi_workspace, _, event: &MultiWorkspaceEvent, window, cx| {
if !matches!(event, MultiWorkspaceEvent::ActiveWorkspaceChanged)
if !matches!(event, MultiWorkspaceEvent::ActiveWorkspaceChanged { .. })
&& window.is_window_active()
{
return;
+13 -5
View File
@@ -70,17 +70,25 @@ pub fn init(cx: &mut App) {
repository_selector::register(workspace);
git_picker::register(workspace);
workspace.register_action(worktree_service::handle_create_worktree);
workspace.register_action(worktree_service::handle_switch_worktree);
workspace.register_action(|workspace, action: &zed_actions::CreateWorktree, window, cx| {
worktree_service::handle_create_worktree(workspace, action, window, None, cx);
});
workspace.register_action(|workspace, action: &zed_actions::SwitchWorktree, window, cx| {
worktree_service::handle_switch_worktree(workspace, action, window, None, cx);
});
workspace.register_action(|workspace, _: &zed_actions::git::Worktree, window, cx| {
let focused_dock = workspace.focused_dock_position(window, cx);
workspace.set_pre_picker_focused_dock(focused_dock);
let project = workspace.project().clone();
let workspace_handle = workspace.weak_handle();
workspace.toggle_modal(window, cx, |window, cx| {
worktree_picker::WorktreePicker::new_modal(project, workspace_handle, window, cx)
worktree_picker::WorktreePicker::new_modal(
project,
workspace_handle,
focused_dock,
window,
cx,
)
});
});
+72 -39
View File
@@ -19,7 +19,10 @@ use ui::{
};
use util::ResultExt as _;
use util::paths::PathExt;
use workspace::{ModalView, MultiWorkspace, Workspace, notifications::DetachAndPromptErr};
use workspace::{
ModalView, MultiWorkspace, Workspace, dock::DockPosition,
notifications::DetachAndPromptErr,
};
use crate::git_panel::show_error_toast;
use zed_actions::{
@@ -41,27 +44,26 @@ impl WorktreePicker {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
if let Some(workspace) = workspace.upgrade() {
let focused_dock = workspace.read(cx).focused_dock_position(window, cx);
workspace.update(cx, |workspace, _cx| {
workspace.set_pre_picker_focused_dock(focused_dock);
});
}
Self::new_inner(project, workspace, false, window, cx)
let focused_dock = workspace
.upgrade()
.and_then(|workspace| workspace.read(cx).focused_dock_position(window, cx));
Self::new_inner(project, workspace, focused_dock, false, window, cx)
}
pub fn new_modal(
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
focused_dock: Option<DockPosition>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
Self::new_inner(project, workspace, true, window, cx)
Self::new_inner(project, workspace, focused_dock, true, window, cx)
}
fn new_inner(
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
focused_dock: Option<DockPosition>,
show_footer: bool,
window: &mut Window,
cx: &mut Context<Self>,
@@ -104,6 +106,7 @@ impl WorktreePicker {
selected_index: 0,
project,
workspace,
focused_dock,
current_branch_name,
default_branch_name: None,
has_multiple_repositories,
@@ -255,6 +258,7 @@ pub struct WorktreePickerDelegate {
selected_index: usize,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
focused_dock: Option<DockPosition>,
current_branch_name: Option<String>,
default_branch_name: Option<String>,
has_multiple_repositories: bool,
@@ -558,26 +562,40 @@ impl PickerDelegate for WorktreePickerDelegate {
match entry {
WorktreeEntry::Separator => return,
WorktreeEntry::CreateFromCurrentBranch => {
window.dispatch_action(
Box::new(CreateWorktree {
worktree_name: None,
branch_target: NewWorktreeBranchTarget::CurrentBranch,
}),
cx,
);
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
crate::worktree_service::handle_create_worktree(
workspace,
&CreateWorktree {
worktree_name: None,
branch_target: NewWorktreeBranchTarget::CurrentBranch,
},
window,
self.focused_dock,
cx,
);
});
}
}
WorktreeEntry::CreateFromDefaultBranch {
default_branch_name,
} => {
window.dispatch_action(
Box::new(CreateWorktree {
worktree_name: None,
branch_target: NewWorktreeBranchTarget::ExistingBranch {
name: default_branch_name.clone(),
},
}),
cx,
);
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
crate::worktree_service::handle_create_worktree(
workspace,
&CreateWorktree {
worktree_name: None,
branch_target: NewWorktreeBranchTarget::ExistingBranch {
name: default_branch_name.clone(),
},
},
window,
self.focused_dock,
cx,
);
});
}
}
WorktreeEntry::Worktree { worktree, .. } => {
let is_current = self.project_worktree_paths.contains(&worktree.path);
@@ -596,13 +614,20 @@ impl PickerDelegate for WorktreePickerDelegate {
.iter()
.find(|wt| wt.is_main)
.map(|wt| wt.path.as_path());
window.dispatch_action(
Box::new(SwitchWorktree {
path: worktree.path.clone(),
display_name: worktree.directory_name(main_worktree_path),
}),
cx,
);
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
crate::worktree_service::handle_switch_worktree(
workspace,
&SwitchWorktree {
path: worktree.path.clone(),
display_name: worktree.directory_name(main_worktree_path),
},
window,
self.focused_dock,
cx,
);
});
}
}
}
}
@@ -617,13 +642,20 @@ impl PickerDelegate for WorktreePickerDelegate {
},
None => NewWorktreeBranchTarget::CurrentBranch,
};
window.dispatch_action(
Box::new(CreateWorktree {
worktree_name: Some(name.clone()),
branch_target,
}),
cx,
);
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
crate::worktree_service::handle_create_worktree(
workspace,
&CreateWorktree {
worktree_name: Some(name.clone()),
branch_target,
},
window,
self.focused_dock,
cx,
);
});
}
}
WorktreeEntry::CreateNamed {
disabled_reason: Some(_),
@@ -1095,6 +1127,7 @@ pub async fn open_remote_worktree(
app_state,
new_window,
None,
None,
cx,
)
.await?;
+17 -13
View File
@@ -11,7 +11,9 @@ use project::project_settings::ProjectSettings;
use project::trusted_worktrees::{PathTrust, TrustedWorktrees};
use remote::RemoteConnectionOptions;
use settings::Settings;
use workspace::{MultiWorkspace, OpenMode, PreviousWorkspaceState, Workspace};
use workspace::{
MultiWorkspace, OpenMode, PreviousWorkspaceState, Workspace, dock::DockPosition,
};
use zed_actions::NewWorktreeBranchTarget;
use util::ResultExt as _;
@@ -385,6 +387,7 @@ pub fn handle_create_worktree(
workspace: &mut Workspace,
action: &zed_actions::CreateWorktree,
window: &mut gpui::Window,
fallback_focused_dock: Option<DockPosition>,
cx: &mut gpui::Context<Workspace>,
) {
let project = workspace.project().clone();
@@ -403,7 +406,8 @@ pub fn handle_create_worktree(
return;
}
let previous_state = workspace.capture_state_for_worktree_switch(window, cx);
let previous_state =
workspace.capture_state_for_worktree_switch(window, fallback_focused_dock, cx);
let workspace_handle = workspace.weak_handle();
let window_handle = window.window_handle().downcast::<MultiWorkspace>();
let remote_connection_options = project.read(cx).remote_connection_options(cx);
@@ -484,6 +488,7 @@ pub fn handle_switch_worktree(
workspace: &mut Workspace,
action: &zed_actions::SwitchWorktree,
window: &mut gpui::Window,
fallback_focused_dock: Option<DockPosition>,
cx: &mut gpui::Context<Workspace>,
) {
let project = workspace.project().clone();
@@ -502,7 +507,8 @@ pub fn handle_switch_worktree(
return;
}
let previous_state = workspace.capture_state_for_worktree_switch(window, cx);
let previous_state =
workspace.capture_state_for_worktree_switch(window, fallback_focused_dock, cx);
let workspace_handle = workspace.weak_handle();
let window_handle = window.window_handle().downcast::<MultiWorkspace>();
let remote_connection_options = project.read(cx).remote_connection_options(cx);
@@ -719,7 +725,7 @@ async fn open_worktree_workspace(
},
);
let task = multi_workspace.find_or_create_workspace(
let task = multi_workspace.find_or_create_workspace_with_source_workspace(
path_list,
remote_connection_options,
None,
@@ -734,6 +740,7 @@ async fn open_worktree_workspace(
&[],
Some(init),
OpenMode::Add,
Some(workspace.clone()),
window,
cx,
);
@@ -865,20 +872,17 @@ async fn open_worktree_workspace(
.ok();
window_handle.update(cx, |multi_workspace, window, cx| {
multi_workspace.activate(new_workspace.clone(), window, cx);
multi_workspace.activate(
new_workspace.clone(),
Some(workspace.clone()),
window,
cx,
);
new_workspace.update(cx, |workspace, cx| {
workspace.run_create_worktree_tasks(window, cx);
});
// Signal completion on the NEW workspace so its agent panel
// subscriber fires and picks up stashed draft text.
// This must be a separate update so the event is delivered
// before focus restoration triggers ensure_thread_initialized.
new_workspace.update(cx, |workspace, cx| {
workspace.set_active_worktree_creation(None, false, cx);
});
if let Some(dock_position) = focused_dock {
new_workspace.update(cx, |workspace, cx| {
let dock = workspace.dock_at_position(dock_position);
@@ -160,7 +160,7 @@ pub async fn open_remote_project(
let open_results = existing_window
.update(cx, |multi_workspace, window, cx| {
window.activate_window();
multi_workspace.activate(existing_workspace.clone(), window, cx);
multi_workspace.activate(existing_workspace.clone(), None, window, cx);
existing_workspace.update(cx, |workspace, cx| {
workspace.open_paths(
resolved_paths,
+1 -1
View File
@@ -505,7 +505,7 @@ impl ProjectPicker {
}?;
let items = open_remote_project_with_existing_connection(
connection, project, paths, app_state, window, None, cx,
connection, project, paths, app_state, window, None, None, cx,
)
.await
.log_err();
+10 -10
View File
@@ -402,7 +402,7 @@ impl Sidebar {
&multi_workspace,
window,
|this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event {
MultiWorkspaceEvent::ActiveWorkspaceChanged => {
MultiWorkspaceEvent::ActiveWorkspaceChanged { .. } => {
this.sync_active_entry_from_active_workspace(cx);
this.replace_archived_panel_thread(window, cx);
this.update_entries(cx);
@@ -2224,7 +2224,7 @@ impl Sidebar {
}
multi_workspace.update(cx, |multi_workspace, cx| {
multi_workspace.activate(workspace.clone(), window, cx);
multi_workspace.activate(workspace.clone(), None, window, cx);
if retain {
multi_workspace.retain_active_workspace(cx);
}
@@ -2264,7 +2264,7 @@ impl Sidebar {
let activated = target_window
.update(cx, |multi_workspace, window, cx| {
window.activate_window();
multi_workspace.activate(workspace.clone(), window, cx);
multi_workspace.activate(workspace.clone(), None, window, cx);
Self::load_agent_thread_in_workspace(&workspace, &metadata, true, window, cx);
})
.log_err()
@@ -3286,7 +3286,7 @@ impl Sidebar {
) {
if let Some(multi_workspace) = self.multi_workspace.upgrade() {
multi_workspace.update(cx, |mw, cx| {
mw.activate(workspace.clone(), window, cx);
mw.activate(workspace.clone(), None, window, cx);
});
}
}
@@ -3490,7 +3490,7 @@ impl Sidebar {
} => {
if let Some(mw) = weak_multi_workspace.upgrade() {
mw.update(cx, |mw, cx| {
mw.activate(workspace.clone(), window, cx);
mw.activate(workspace.clone(), None, window, cx);
});
}
this.active_entry = Some(ActiveEntry {
@@ -3509,7 +3509,7 @@ impl Sidebar {
} => {
if let Some(mw) = weak_multi_workspace.upgrade() {
mw.update(cx, |mw, cx| {
mw.activate(workspace.clone(), window, cx);
mw.activate(workspace.clone(), None, window, cx);
mw.retain_active_workspace(cx);
});
}
@@ -3527,7 +3527,7 @@ impl Sidebar {
if let Some(mw) = weak_multi_workspace.upgrade() {
if let Some(original_ws) = &original_workspace {
mw.update(cx, |mw, cx| {
mw.activate(original_ws.clone(), window, cx);
mw.activate(original_ws.clone(), None, window, cx);
});
}
}
@@ -3584,7 +3584,7 @@ impl Sidebar {
if let Some((metadata, workspace)) = initial_preview {
if let Some(mw) = self.multi_workspace.upgrade() {
mw.update(cx, |mw, cx| {
mw.activate(workspace.clone(), window, cx);
mw.activate(workspace.clone(), None, window, cx);
});
}
self.active_entry = Some(ActiveEntry {
@@ -3826,7 +3826,7 @@ impl Sidebar {
};
multi_workspace.update(cx, |multi_workspace, cx| {
multi_workspace.activate(workspace.clone(), window, cx);
multi_workspace.activate(workspace.clone(), None, window, cx);
});
let draft_id = workspace.update(cx, |workspace, cx| {
@@ -3953,7 +3953,7 @@ impl Sidebar {
.workspace_for_paths(key.path_list(), key.host().as_ref(), cx)
}) {
multi_workspace.update(cx, |multi_workspace, cx| {
multi_workspace.activate(workspace, window, cx);
multi_workspace.activate(workspace, None, window, cx);
multi_workspace.retain_active_workspace(cx);
});
} else {
+74 -14
View File
@@ -103,7 +103,9 @@ pub fn sidebar_side_context_menu(
}
pub enum MultiWorkspaceEvent {
ActiveWorkspaceChanged,
ActiveWorkspaceChanged {
source_workspace: Option<WeakEntity<Workspace>>,
},
WorkspaceAdded(Entity<Workspace>),
WorkspaceRemoved(EntityId),
ProjectGroupsChanged,
@@ -294,10 +296,6 @@ pub struct MultiWorkspace {
_serialize_task: Option<Task<()>>,
_subscriptions: Vec<Subscription>,
previous_focus_handle: Option<FocusHandle>,
/// Holds unsent agent editor text during a worktree switch.
/// The agent panel in the source workspace stashes its draft text here,
/// and the agent panel in the destination workspace picks it up.
pub pending_worktree_switch_text: Option<String>,
}
impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
@@ -356,7 +354,6 @@ impl MultiWorkspace {
settings_subscription,
],
previous_focus_handle: None,
pending_worktree_switch_text: None,
}
}
@@ -583,7 +580,7 @@ impl MultiWorkspace {
cx.subscribe_in(workspace, window, |this, workspace, event, window, cx| {
if let WorkspaceEvent::Activate = event {
this.activate(workspace.clone(), window, cx);
this.activate(workspace.clone(), None, window, cx);
}
})
.detach();
@@ -1023,19 +1020,52 @@ impl MultiWorkspace {
open_mode: OpenMode,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Workspace>>> {
self.find_or_create_workspace_with_source_workspace(
paths,
host,
provisional_project_group_key,
connect_remote,
excluding,
init,
open_mode,
None,
window,
cx,
)
}
pub fn find_or_create_workspace_with_source_workspace(
&mut self,
paths: PathList,
host: Option<RemoteConnectionOptions>,
provisional_project_group_key: Option<ProjectGroupKey>,
connect_remote: impl FnOnce(
RemoteConnectionOptions,
&mut Window,
&mut Context<Self>,
) -> Task<Result<Option<Entity<remote::RemoteClient>>>>
+ 'static,
excluding: &[Entity<Workspace>],
init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
open_mode: OpenMode,
source_workspace: Option<WeakEntity<Workspace>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Workspace>>> {
if let Some(workspace) = self.workspace_for_paths(&paths, host.as_ref(), cx) {
self.activate(workspace.clone(), window, cx);
self.activate(workspace.clone(), source_workspace, window, cx);
return Task::ready(Ok(workspace));
}
let Some(connection_options) = host else {
return self.find_or_create_local_workspace(
return self.find_or_create_local_workspace_with_source_workspace(
paths,
provisional_project_group_key,
excluding,
init,
open_mode,
source_workspace,
window,
cx,
);
@@ -1074,6 +1104,7 @@ impl MultiWorkspace {
app_state,
window_handle,
provisional_project_group_key,
source_workspace,
cx,
)
.await?;
@@ -1102,10 +1133,33 @@ impl MultiWorkspace {
open_mode: OpenMode,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Workspace>>> {
self.find_or_create_local_workspace_with_source_workspace(
path_list,
project_group,
excluding,
init,
open_mode,
None,
window,
cx,
)
}
pub fn find_or_create_local_workspace_with_source_workspace(
&mut self,
path_list: PathList,
project_group: Option<ProjectGroupKey>,
excluding: &[Entity<Workspace>],
init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
open_mode: OpenMode,
source_workspace: Option<WeakEntity<Workspace>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Workspace>>> {
if let Some(workspace) = self.workspace_for_paths_excluding(&path_list, None, excluding, cx)
{
self.activate(workspace.clone(), window, cx);
self.activate(workspace.clone(), source_workspace, window, cx);
return Task::ready(Ok(workspace));
}
@@ -1150,7 +1204,12 @@ impl MultiWorkspace {
cx,
)
.inspect(|workspace| {
multi_workspace.activate(workspace.clone(), window, cx);
multi_workspace.activate(
workspace.clone(),
source_workspace.clone(),
window,
cx,
);
})
})
.ok()
@@ -1213,6 +1272,7 @@ impl MultiWorkspace {
pub fn activate(
&mut self,
workspace: Entity<Workspace>,
source_workspace: Option<WeakEntity<Workspace>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1245,7 +1305,7 @@ impl MultiWorkspace {
self.detach_workspace(&old_active_workspace, cx);
}
cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged { source_workspace });
self.serialize(cx);
self.focus_active_workspace(window, cx);
cx.notify();
@@ -1675,12 +1735,12 @@ impl MultiWorkspace {
!workspaces.contains(&new_active),
"fallback workspace must not be one of the workspaces being removed"
);
this.activate(new_active, window, cx);
this.activate(new_active, None, window, cx);
})?;
} else {
this.update_in(cx, |this, window, cx| {
if *this.workspace() != original_active {
this.activate(original_active, window, cx);
this.activate(original_active, None, window, cx);
}
})?;
}
+4 -4
View File
@@ -2663,7 +2663,7 @@ mod tests {
let workspace2 = multi_workspace.update_in(cx, |mw, window, cx| {
let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
workspace.update(cx, |ws, _cx| ws.set_random_database_id());
mw.activate(workspace.clone(), window, cx);
mw.activate(workspace.clone(), None, window, cx);
workspace
});
@@ -5066,7 +5066,7 @@ mod tests {
// Activate workspace B so removing its group exercises the fallback.
multi_workspace.update_in(cx, |mw, window, cx| {
mw.activate(workspace_b.clone(), window, cx);
mw.activate(workspace_b.clone(), None, window, cx);
});
cx.run_until_parked();
@@ -5095,7 +5095,7 @@ mod tests {
let workspace_a =
multi_workspace.read_with(cx, |mw, _cx| mw.workspaces().next().unwrap().clone());
multi_workspace.update_in(cx, |mw, window, cx| {
mw.activate(workspace_a.clone(), window, cx);
mw.activate(workspace_a.clone(), None, window, cx);
});
cx.run_until_parked();
@@ -5171,7 +5171,7 @@ mod tests {
// Activate workspace_a so removing it triggers the fallback path.
multi_workspace.update_in(cx, |mw, window, cx| {
mw.activate(workspace_a.clone(), window, cx);
mw.activate(workspace_a.clone(), None, window, cx);
});
cx.run_until_parked();
+13 -18
View File
@@ -1398,7 +1398,6 @@ pub struct Workspace {
sidebar_focus_handle: Option<FocusHandle>,
multi_workspace: Option<WeakEntity<MultiWorkspace>>,
active_worktree_creation: ActiveWorktreeCreation,
pre_picker_focused_dock: Option<DockPosition>,
}
impl EventEmitter<Event> for Workspace {}
@@ -1828,7 +1827,6 @@ impl Workspace {
sidebar_focus_handle: None,
multi_workspace,
active_worktree_creation: ActiveWorktreeCreation::default(),
pre_picker_focused_dock: None,
open_in_dev_container: false,
_dev_container_task: None,
}
@@ -1971,7 +1969,7 @@ impl Workspace {
});
match open_mode {
OpenMode::Activate => {
multi_workspace.activate(workspace.clone(), window, cx);
multi_workspace.activate(workspace.clone(), None, window, cx);
}
OpenMode::Add => {
multi_workspace.add(workspace.clone(), &*window, cx);
@@ -2233,19 +2231,12 @@ impl Workspace {
cx.notify();
}
pub fn pre_picker_focused_dock(&self) -> Option<DockPosition> {
self.pre_picker_focused_dock
}
pub fn set_pre_picker_focused_dock(&mut self, position: Option<DockPosition>) {
self.pre_picker_focused_dock = position;
}
/// Captures the current workspace state for restoring after a worktree switch.
/// This includes dock layout, open file paths, and the active file path.
pub fn capture_state_for_worktree_switch(
&self,
window: &Window,
fallback_focused_dock: Option<DockPosition>,
cx: &App,
) -> PreviousWorkspaceState {
let dock_structure = self.capture_dock_state(window, cx);
@@ -2265,7 +2256,7 @@ impl Workspace {
dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
})
.map(|(position, _)| position)
.or_else(|| self.pre_picker_focused_dock);
.or(fallback_focused_dock);
PreviousWorkspaceState {
dock_structure,
@@ -9724,7 +9715,7 @@ pub fn open_paths(
let open_task = existing
.update(cx, |multi_workspace, window, cx| {
window.activate_window();
multi_workspace.activate(target_workspace.clone(), window, cx);
multi_workspace.activate(target_workspace.clone(), None, window, cx);
target_workspace.update(cx, |workspace, cx| {
if open_in_dev_container {
workspace.set_open_in_dev_container(true);
@@ -9950,6 +9941,7 @@ pub fn open_remote_project_with_new_connection(
app_state,
window,
None,
None,
cx,
)
.await
@@ -9963,6 +9955,7 @@ pub fn open_remote_project_with_existing_connection(
app_state: Arc<AppState>,
window: WindowHandle<MultiWorkspace>,
provisional_project_group_key: Option<ProjectGroupKey>,
source_workspace: Option<WeakEntity<Workspace>>,
cx: &mut AsyncApp,
) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
cx.spawn(async move |cx| {
@@ -9977,6 +9970,7 @@ pub fn open_remote_project_with_existing_connection(
app_state,
window,
provisional_project_group_key,
source_workspace,
cx,
)
.await
@@ -9991,6 +9985,7 @@ async fn open_remote_project_inner(
app_state: Arc<AppState>,
window: WindowHandle<MultiWorkspace>,
provisional_project_group_key: Option<ProjectGroupKey>,
source_workspace: Option<WeakEntity<Workspace>>,
cx: &mut AsyncApp,
) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
let db = cx.update(|cx| WorkspaceDb::global(cx));
@@ -10056,7 +10051,7 @@ async fn open_remote_project_inner(
if let Some(project_group_key) = provisional_project_group_key.clone() {
multi_workspace.retain_workspace(new_workspace.clone(), project_group_key, cx);
}
multi_workspace.activate(new_workspace.clone(), window, cx);
multi_workspace.activate(new_workspace.clone(), source_workspace, window, cx);
new_workspace
})?;
@@ -10143,7 +10138,7 @@ pub fn join_in_room_project(
{
existing_window
.update(cx, |multi_workspace, window, cx| {
multi_workspace.activate(target_workspace, window, cx);
multi_workspace.activate(target_workspace, None, window, cx);
})
.ok();
existing_window
@@ -11083,7 +11078,7 @@ mod tests {
// Activate workspace A
multi_workspace_handle
.update(cx, |mw, window, cx| {
mw.activate(workspace_a.clone(), window, cx);
mw.activate(workspace_a.clone(), None, window, cx);
})
.unwrap();
@@ -11168,7 +11163,7 @@ mod tests {
// Activate workspace A.
multi_workspace_handle
.update(cx, |mw, window, cx| {
mw.activate(workspace_a.clone(), window, cx);
mw.activate(workspace_a.clone(), None, window, cx);
})
.unwrap();
@@ -11220,7 +11215,7 @@ mod tests {
let remove_task = multi_workspace_handle
.update(cx, |mw, window, cx| {
// First switch back to A.
mw.activate(workspace_a.clone(), window, cx);
mw.activate(workspace_a.clone(), None, window, cx);
mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
})
.unwrap();
+3 -3
View File
@@ -2605,7 +2605,7 @@ fn run_multi_workspace_sidebar_visual_tests(
});
cx.new(|cx| {
let mut multi_workspace = MultiWorkspace::new(workspace1, window, cx);
multi_workspace.activate(workspace2, window, cx);
multi_workspace.activate(workspace2, None, window, cx);
multi_workspace
})
},
@@ -2657,7 +2657,7 @@ fn run_multi_workspace_sidebar_visual_tests(
multi_workspace_window
.update(cx, |multi_workspace, window, cx| {
let workspace = multi_workspace.workspaces().next().unwrap().clone();
multi_workspace.activate(workspace, window, cx);
multi_workspace.activate(workspace, None, window, cx);
})
.context("Failed to activate workspace 1")?;
@@ -3393,7 +3393,7 @@ fn open_sidebar_test_window(
let ws = cx.new(|cx| {
Workspace::new(None, project, app_state.clone(), window, cx)
});
mw.activate(ws, window, cx);
mw.activate(ws, None, window, cx);
}
mw
})
+63 -15
View File
@@ -423,6 +423,31 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut App) {
let window_handle = window.window_handle();
let multi_workspace_handle = cx.entity();
cx.subscribe_in(
&multi_workspace_handle,
window,
|_, multi_workspace, event: &workspace::MultiWorkspaceEvent, window, cx| {
let workspace::MultiWorkspaceEvent::ActiveWorkspaceChanged { source_workspace } =
event
else {
return;
};
let active_workspace = multi_workspace.read(cx).workspace().clone();
let source_workspace = source_workspace.clone();
active_workspace.update(cx, |workspace, cx| {
ensure_agent_panel_for_workspace(
workspace,
source_workspace,
window,
cx,
)
.detach_and_log_err(cx);
});
},
)
.detach();
cx.defer(move |cx| {
window_handle
.update(cx, |_, window, cx| {
@@ -735,24 +760,47 @@ fn setup_or_teardown_ai_panel<P: Panel>(
}
}
fn ensure_agent_panel_for_workspace(
workspace: &mut Workspace,
source_workspace: Option<WeakEntity<Workspace>>,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Task<anyhow::Result<()>> {
let task = setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
agent_ui::AgentPanel::load(workspace, cx)
});
cx.spawn_in(window, async move |workspace, cx| {
task.await?;
workspace.update_in(cx, |workspace, window, cx| {
if let Some(source_workspace) = source_workspace.clone()
&& let Some(panel) = workspace.panel::<agent_ui::AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.initialize_from_source_workspace_if_needed(
source_workspace,
window,
cx,
);
});
}
})
})
}
async fn initialize_agent_panel(
workspace_handle: WeakEntity<Workspace>,
mut cx: AsyncWindowContext,
) -> anyhow::Result<()> {
workspace_handle
.update_in(&mut cx, |workspace, window, cx| {
setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
agent_ui::AgentPanel::load(workspace, cx)
})
ensure_agent_panel_for_workspace(workspace, None, window, cx)
})?
.await?;
workspace_handle.update_in(&mut cx, |workspace, window, cx| {
cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
agent_ui::AgentPanel::load(workspace, cx)
})
.detach_and_log_err(cx);
ensure_agent_panel_for_workspace(workspace, None, window, cx).detach_and_log_err(cx);
})
.detach();
@@ -1558,7 +1606,7 @@ fn quit(_: &Quit, cx: &mut App) {
for workspace in workspaces {
if let Some(should_close) = window
.update(cx, |multi_workspace, window, cx| {
multi_workspace.activate(workspace.clone(), window, cx);
multi_workspace.activate(workspace.clone(), None, window, cx);
window.activate_window();
workspace.update(cx, |workspace, cx| {
workspace.prepare_to_close(CloseIntent::Quit, window, cx)
@@ -5673,10 +5721,10 @@ mod tests {
window
.update(cx, |multi_workspace, window, cx| {
multi_workspace.activate(workspace2.clone(), window, cx);
multi_workspace.activate(workspace3.clone(), window, cx);
multi_workspace.activate(workspace2.clone(), None, window, cx);
multi_workspace.activate(workspace3.clone(), None, window, cx);
// Switch back to workspace1 for test setup
multi_workspace.activate(workspace1.clone(), window, cx);
multi_workspace.activate(workspace1.clone(), None, window, cx);
assert_eq!(multi_workspace.workspace(), &workspace1);
})
.unwrap();
@@ -5860,8 +5908,8 @@ mod tests {
window1
.update(cx, |multi_workspace, window, cx| {
multi_workspace.activate(workspace1_2.clone(), window, cx);
multi_workspace.activate(workspace1_1.clone(), window, cx);
multi_workspace.activate(workspace1_2.clone(), None, window, cx);
multi_workspace.activate(workspace1_1.clone(), None, window, cx);
})
.unwrap();
@@ -6180,7 +6228,7 @@ mod tests {
window_a
.update(cx, |multi_workspace, window, cx| {
let workspace = multi_workspace.workspaces().next().unwrap().clone();
multi_workspace.activate(workspace, window, cx);
multi_workspace.activate(workspace, None, window, cx);
})
.unwrap();
@@ -6388,7 +6436,7 @@ mod tests {
})
.expect("workspace_a should exist")
.clone();
mw.activate(workspace_a, window, cx);
mw.activate(workspace_a, None, window, cx);
})
.unwrap();
cx.run_until_parked();