mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +07:00
Support creating sibling threads in a new git worktree
When create_thread is called with use_new_worktree, open a new background workspace backed by linked git worktrees of the project's git repos (reusing the worktree picker's create flow) and spawn the thread there, rather than only sharing the parent's worktree. - Add git_ui::worktree_service::create_worktree_workspace, a Task- returning variant of handle_create_worktree that hands back the new workspace so the agent tool can create a thread in it. - Open the agent's worktree workspace in the background via a new MultiWorkspace::add_background_workspace, leaving the user where they are and giving the new workspace a clean checkout (no inherited open files or dock layout). This matches how non-worktree sibling threads are created in the background. - Dedup linked worktrees of the same underlying repo in start_worktree_creations so they don't collide on the same target path (previously a hard 'already exists' failure for both this flow and the manual Create worktree UI). - Report a non-fatal warning back to the calling agent when the project had multiple worktrees of the same repo and they were consolidated. - Add optional worktree_name to the create_thread tool input. Release Notes: - N/A
This commit is contained in:
parent
64c0c8be2f
commit
497bf37ea5
6 changed files with 307 additions and 56 deletions
|
|
@ -713,9 +713,12 @@ pub struct SiblingThreadRequest {
|
|||
/// Optional model override, as `provider/model-id`.
|
||||
/// Defaults to the user's configured default model for the agent.
|
||||
pub model: Option<String>,
|
||||
/// Whether to create the thread in a new git worktree.
|
||||
/// Not yet supported; passing `true` will return an error.
|
||||
/// Whether to create the thread in a new git worktree workspace.
|
||||
pub use_new_worktree: bool,
|
||||
/// Optional worktree directory name. When `None`, the UI generates a
|
||||
/// random non-colliding name (matching the manual "Create worktree"
|
||||
/// flow). Only relevant when `use_new_worktree` is true.
|
||||
pub worktree_name: Option<String>,
|
||||
/// Git ref (branch, tag, or commit) to base the new worktree on.
|
||||
/// Only relevant when `use_new_worktree` is true.
|
||||
pub base_ref: Option<String>,
|
||||
|
|
@ -730,6 +733,11 @@ pub struct SiblingThreadInfo {
|
|||
pub agent_id: String,
|
||||
/// The model ID used for the thread, if known.
|
||||
pub model: Option<String>,
|
||||
/// An optional, non-fatal heads-up about the created thread that the
|
||||
/// caller should relay or take into account (e.g., the project had an
|
||||
/// unusual worktree layout that affected how the new worktree was set
|
||||
/// up). Empty when nothing noteworthy happened.
|
||||
pub warning: Option<String>,
|
||||
}
|
||||
|
||||
/// A list of agents and, for each, the models available for use.
|
||||
|
|
|
|||
|
|
@ -37,8 +37,25 @@ use crate::{AgentTool, SiblingThreadRequest, ThreadEnvironment, ToolCallEventStr
|
|||
/// - Leave `agent` and `model` unset to use the user's current defaults.
|
||||
///
|
||||
/// ### Worktree support
|
||||
/// Not yet implemented. Setting `use_new_worktree` to true currently returns an
|
||||
/// error. Sibling threads always share the parent thread's worktree for now.
|
||||
/// Set `use_new_worktree` to true to spawn the sibling inside a brand-new
|
||||
/// workspace (a new tab) backed by linked git worktrees of each git
|
||||
/// repository in the current project. This mirrors what the user gets when
|
||||
/// they manually pick "Create worktree" from the worktree picker.
|
||||
///
|
||||
/// - The new workspace opens in its own tab; switch to it manually to see
|
||||
/// the sibling's progress.
|
||||
/// - The new worktrees start in detached HEAD state. Use `base_ref` to base
|
||||
/// them off a specific branch, tag, or commit; omit it to base off `HEAD`.
|
||||
/// The agent in the sibling thread can attach to a branch by running
|
||||
/// `git switch -c <branch>` in its terminal if needed.
|
||||
/// - `worktree_name` overrides the autogenerated directory name. Omit it to
|
||||
/// let the editor pick a random non-colliding name.
|
||||
/// - The project must contain at least one git repository, otherwise the
|
||||
/// call fails.
|
||||
///
|
||||
/// Use this when the sibling needs to make changes that shouldn't touch the
|
||||
/// user's current working tree (e.g., risky refactors, parallel experiments,
|
||||
/// or work the user wants to review independently).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct CreateThreadToolInput {
|
||||
|
|
@ -62,13 +79,20 @@ pub struct CreateThreadToolInput {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
|
||||
/// If true, create the thread in a new git worktree rather than sharing the
|
||||
/// parent's worktree. NOT YET SUPPORTED — passing true currently fails.
|
||||
/// If true, create the thread in a new git worktree rather than sharing
|
||||
/// the parent's worktree. The project must contain a git repository.
|
||||
#[serde(default)]
|
||||
pub use_new_worktree: bool,
|
||||
|
||||
/// Git ref to base the new worktree on. Only used when `use_new_worktree`
|
||||
/// is true.
|
||||
/// Optional name for the new worktree directory. When omitted, the
|
||||
/// editor generates a random non-colliding name (matching the
|
||||
/// manual "Create worktree" UI behavior). Only used when
|
||||
/// `use_new_worktree` is true.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub worktree_name: Option<String>,
|
||||
|
||||
/// Git ref (branch, tag, or commit) to base the new worktree on. Only
|
||||
/// used when `use_new_worktree` is true. Defaults to `HEAD`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_ref: Option<String>,
|
||||
}
|
||||
|
|
@ -81,6 +105,11 @@ pub enum CreateThreadToolOutput {
|
|||
agent_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
model: Option<String>,
|
||||
/// A non-fatal heads-up about the created thread (e.g., the project's
|
||||
/// worktree layout was unusual and the new worktree may not match
|
||||
/// expectations). Present only when there's something to flag.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
warning: Option<String>,
|
||||
},
|
||||
Error {
|
||||
error: String,
|
||||
|
|
@ -137,17 +166,12 @@ impl AgentTool for CreateThreadTool {
|
|||
cx: &mut App,
|
||||
) -> Task<Result<Self::Output, Self::Output>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let input = input.recv().await.map_err(|e| CreateThreadToolOutput::Error {
|
||||
error: format!("Failed to receive tool input: {e}"),
|
||||
})?;
|
||||
|
||||
if input.use_new_worktree {
|
||||
return Err(CreateThreadToolOutput::Error {
|
||||
error: "Creating sibling threads in a new git worktree is not yet supported. \
|
||||
Set `use_new_worktree` to false to create the thread in the current worktree."
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
let input = input
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|e| CreateThreadToolOutput::Error {
|
||||
error: format!("Failed to receive tool input: {e}"),
|
||||
})?;
|
||||
|
||||
let title: SharedString = input.title.clone().into();
|
||||
let request = SiblingThreadRequest {
|
||||
|
|
@ -156,6 +180,7 @@ impl AgentTool for CreateThreadTool {
|
|||
agent_id: input.agent,
|
||||
model: input.model,
|
||||
use_new_worktree: input.use_new_worktree,
|
||||
worktree_name: input.worktree_name,
|
||||
base_ref: input.base_ref,
|
||||
};
|
||||
|
||||
|
|
@ -165,6 +190,7 @@ impl AgentTool for CreateThreadTool {
|
|||
title: info.title.to_string(),
|
||||
agent_id: info.agent_id,
|
||||
model: info.model,
|
||||
warning: info.warning,
|
||||
}),
|
||||
Err(error) => Err(CreateThreadToolOutput::Error {
|
||||
error: error.to_string(),
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ file_icons.workspace = true
|
|||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
git.workspace = true
|
||||
git_ui.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
gpui_tokio.workspace = true
|
||||
|
|
@ -125,7 +126,6 @@ clock = { workspace = true, features = ["test-support"] }
|
|||
db = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
eval_utils.workspace = true
|
||||
git_ui.workspace = true
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ use crate::{
|
|||
};
|
||||
use agent_settings::AgentSettings;
|
||||
use ai_onboarding::AgentPanelOnboarding;
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
#[cfg(feature = "audio")]
|
||||
use audio::{Audio, Sound};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
|
@ -880,6 +880,10 @@ pub struct CreateThreadOptions {
|
|||
/// Model override, as `provider/model-id`. Only applied when the thread
|
||||
/// uses the native Zed agent.
|
||||
pub model: Option<String>,
|
||||
/// Working directories to attach to the new thread (e.g., the path of a
|
||||
/// freshly-created sibling worktree). When `None`, the thread inherits
|
||||
/// the project's default path list.
|
||||
pub work_dirs: Option<PathList>,
|
||||
}
|
||||
|
||||
pub(crate) struct AgentThread {
|
||||
|
|
@ -3013,7 +3017,7 @@ impl AgentPanel {
|
|||
agent,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
options.work_dirs,
|
||||
options.title.clone(),
|
||||
options.initial_content,
|
||||
options.model,
|
||||
|
|
@ -4661,14 +4665,85 @@ impl agent::SiblingThreadHost for AgentPanelSiblingHost {
|
|||
initial_content: Some(initial_content),
|
||||
agent: agent_choice.clone(),
|
||||
model: request.model.clone(),
|
||||
work_dirs: None,
|
||||
};
|
||||
|
||||
// If the caller asked for a fresh worktree, open a new workspace
|
||||
// backed by a linked git worktree of each git repo in the parent
|
||||
// project — the same flow the user gets when they pick "Create
|
||||
// worktree" from the worktree picker. The sibling thread is then
|
||||
// created inside the new workspace's agent panel, so it lives
|
||||
// alongside any threads the user would create there manually.
|
||||
let mut worktree_warning: Option<String> = None;
|
||||
let target_panel = if request.use_new_worktree {
|
||||
let workspace = panel.read_with(cx, |panel, _cx| panel.workspace.clone())?;
|
||||
let workspace = workspace
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("Source workspace is no longer available"))?;
|
||||
// The branch target follows the existing UI semantics: when
|
||||
// `base_ref` is set, treat it as the ref to base off of
|
||||
// (resolved like `git switch --detach <ref>`); otherwise base
|
||||
// off the current HEAD. Either way the new worktrees are in
|
||||
// detached HEAD state — the agent can attach to a branch via
|
||||
// git afterwards.
|
||||
let branch_target = match request.base_ref.as_ref() {
|
||||
Some(ref_name) => zed_actions::NewWorktreeBranchTarget::ExistingBranch {
|
||||
name: ref_name.clone(),
|
||||
},
|
||||
None => zed_actions::NewWorktreeBranchTarget::CurrentBranch,
|
||||
};
|
||||
let action = zed_actions::CreateWorktree {
|
||||
worktree_name: request.worktree_name.clone(),
|
||||
branch_target,
|
||||
};
|
||||
let creation = window.update(cx, |_root, window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
git_ui::worktree_service::create_worktree_workspace(
|
||||
workspace, &action, window, None, cx,
|
||||
)
|
||||
})
|
||||
})?;
|
||||
let created = creation
|
||||
.await
|
||||
.context("failed to create worktree workspace")?;
|
||||
// The creation flow tells us when the project had multiple
|
||||
// worktrees of the same underlying repo, which it consolidates
|
||||
// into one new worktree — flag it so the calling agent knows
|
||||
// the result may not reflect every source worktree's state.
|
||||
if created.consolidated_worktrees {
|
||||
worktree_warning = Some(
|
||||
"The project contained multiple worktrees backed by the same git \
|
||||
repository, so they were consolidated into a single new worktree. \
|
||||
The new thread's worktree is based on one of them and may not \
|
||||
reflect the exact state of the others."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
// Locate the agent panel on the new workspace. We rely on
|
||||
// the panel having registered by the time
|
||||
// `create_worktree_workspace` returns — `open_worktree_workspace`
|
||||
// explicitly awaits `take_panels_task` and the initial scan.
|
||||
created
|
||||
.workspace
|
||||
.read_with(cx, |workspace, cx| workspace.panel::<AgentPanel>(cx))
|
||||
.ok_or_else(|| anyhow!("new workspace did not register an agent panel"))?
|
||||
.downgrade()
|
||||
} else {
|
||||
panel.clone()
|
||||
};
|
||||
// Both the source panel and any newly-opened worktree workspace
|
||||
// live in the same OS window (the new workspace is a tab on the
|
||||
// existing MultiWorkspace), so the original window handle is
|
||||
// still the right context for the `create_thread_with_options`
|
||||
// call regardless of which panel ends up the target.
|
||||
let target_window = window;
|
||||
|
||||
// We deliberately don't wait for the new thread's session to
|
||||
// become available here: there are currently no agent tools that
|
||||
// operate on sibling threads by session ID, so requiring one would
|
||||
// just introduce a race for no benefit.
|
||||
let resolved_agent_id = window.update(cx, |_root, window, cx| {
|
||||
panel.update(cx, |panel, cx| {
|
||||
let resolved_agent_id = target_window.update(cx, |_root, window, cx| {
|
||||
target_panel.update(cx, |panel, cx| {
|
||||
panel.create_thread_with_options(
|
||||
options,
|
||||
AgentThreadSource::AgentPanel,
|
||||
|
|
@ -4686,6 +4761,7 @@ impl agent::SiblingThreadHost for AgentPanelSiblingHost {
|
|||
title,
|
||||
agent_id: resolved_agent_id.0.to_string(),
|
||||
model: request.model,
|
||||
warning: worktree_warning,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use collections::HashSet;
|
|||
use fs::Fs;
|
||||
use gpui::{
|
||||
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
|
||||
TaskExt, WeakEntity,
|
||||
Task, TaskExt, WeakEntity,
|
||||
};
|
||||
use project::Project;
|
||||
use project::git_store::Repository;
|
||||
|
|
@ -177,7 +177,7 @@ impl Render for WorktreeFetchFailedToast {
|
|||
cx.emit(DismissEvent);
|
||||
if let Some(workspace) = workspace_for_retry.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
handle_create_worktree_inner(
|
||||
let task = create_worktree_workspace_inner(
|
||||
workspace,
|
||||
&zed_actions::CreateWorktree {
|
||||
worktree_name: worktree_name.clone(),
|
||||
|
|
@ -186,8 +186,11 @@ impl Render for WorktreeFetchFailedToast {
|
|||
window,
|
||||
focused_dock,
|
||||
RemoteBranchFetchMode::UseLocal,
|
||||
// User-initiated retry of a foreground create.
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
task.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
})),
|
||||
|
|
@ -350,6 +353,14 @@ async fn fetch_remote_for_worktree_base(
|
|||
///
|
||||
/// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuples.
|
||||
/// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs for remapping editor tabs.
|
||||
///
|
||||
/// Multiple entries in `git_repos` can be linked worktrees of the *same*
|
||||
/// underlying repository (e.g. a project that has both the main checkout and
|
||||
/// one of its linked worktrees open as separate Zed worktrees). Those entries
|
||||
/// resolve to the same target path via [`Repository::path_for_new_linked_worktree`],
|
||||
/// so we create the new worktree only once and remap every contributing
|
||||
/// work directory onto it. Without this dedup, the second `git worktree add`
|
||||
/// fails with "already exists".
|
||||
fn start_worktree_creations(
|
||||
git_repos: &[Entity<Repository>],
|
||||
worktree_name: Option<String>,
|
||||
|
|
@ -369,6 +380,7 @@ fn start_worktree_creations(
|
|||
)> {
|
||||
let mut creation_infos = Vec::new();
|
||||
let mut path_remapping = Vec::new();
|
||||
let mut scheduled_paths: HashSet<PathBuf> = HashSet::default();
|
||||
|
||||
let worktree_name = worktree_name.unwrap_or_else(|| {
|
||||
let existing_refs: Vec<&str> = existing_worktree_names.iter().map(|s| s.as_str()).collect();
|
||||
|
|
@ -383,15 +395,25 @@ fn start_worktree_creations(
|
|||
if existing_worktree_paths.contains(&new_path) {
|
||||
anyhow::bail!("A worktree already exists at {}", new_path.display());
|
||||
}
|
||||
let target = git::repository::CreateWorktreeTarget::Detached {
|
||||
base_sha: base_ref.clone(),
|
||||
};
|
||||
let receiver = repo.create_worktree(target, new_path.clone());
|
||||
let work_dir = repo.work_directory_abs_path.clone();
|
||||
// Only the first repo that resolves to a given target path
|
||||
// actually creates the worktree; subsequent linked worktrees of
|
||||
// the same repository just contribute a path remapping.
|
||||
let receiver = if scheduled_paths.contains(&new_path) {
|
||||
None
|
||||
} else {
|
||||
let target = git::repository::CreateWorktreeTarget::Detached {
|
||||
base_sha: base_ref.clone(),
|
||||
};
|
||||
Some(repo.create_worktree(target, new_path.clone()))
|
||||
};
|
||||
anyhow::Ok((work_dir, new_path, receiver))
|
||||
})?;
|
||||
path_remapping.push((work_dir.to_path_buf(), new_path.clone()));
|
||||
creation_infos.push((repo.clone(), new_path, receiver));
|
||||
if let Some(receiver) = receiver {
|
||||
scheduled_paths.insert(new_path.clone());
|
||||
creation_infos.push((repo.clone(), new_path, receiver));
|
||||
}
|
||||
}
|
||||
|
||||
Ok((creation_infos, path_remapping))
|
||||
|
|
@ -561,6 +583,9 @@ fn maybe_propagate_worktree_trust(
|
|||
|
||||
/// Handles the `CreateWorktree` action generically, without any agent panel involvement.
|
||||
/// Creates a new git worktree, opens the workspace, restores layout and files.
|
||||
/// Errors are surfaced to the user via toasts; the new workspace handle is
|
||||
/// discarded. Use [`create_worktree_workspace`] when you need the resulting
|
||||
/// workspace (e.g., the `create_thread` agent tool spawns a thread in it).
|
||||
pub fn handle_create_worktree(
|
||||
workspace: &mut Workspace,
|
||||
action: &zed_actions::CreateWorktree,
|
||||
|
|
@ -568,38 +593,94 @@ pub fn handle_create_worktree(
|
|||
fallback_focused_dock: Option<DockPosition>,
|
||||
cx: &mut gpui::Context<Workspace>,
|
||||
) {
|
||||
handle_create_worktree_inner(
|
||||
let task = create_worktree_workspace_inner(
|
||||
workspace,
|
||||
action,
|
||||
window,
|
||||
fallback_focused_dock,
|
||||
RemoteBranchFetchMode::Fetch,
|
||||
// The user explicitly asked to create a worktree, so foreground it.
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_create_worktree_inner(
|
||||
/// Outcome of [`create_worktree_workspace`].
|
||||
pub struct CreatedWorktreeWorkspace {
|
||||
/// The newly opened workspace.
|
||||
pub workspace: Entity<Workspace>,
|
||||
/// True when the project contained more than one Zed worktree backed by
|
||||
/// the same underlying git repository, so they were consolidated into a
|
||||
/// single new worktree (they resolve to the same target path). Callers
|
||||
/// that care — like the `create_thread` agent tool — can use this to warn
|
||||
/// that the result may not reflect every source worktree's state.
|
||||
pub consolidated_worktrees: bool,
|
||||
}
|
||||
|
||||
/// Same as [`handle_create_worktree`], but returns a `Task` that resolves to
|
||||
/// the new workspace once worktree creation and post-open setup are
|
||||
/// complete. The caller receives errors as `Result`s and is expected to
|
||||
/// handle them. Note that a small set of early failures (no git repositories,
|
||||
/// disconnected remote, mid-creation `git fetch` failure) still surface a
|
||||
/// toast on the source workspace so the user understands why the action
|
||||
/// didn't take effect; the same error is also returned to the caller.
|
||||
///
|
||||
/// Used by the `create_thread` agent tool to spawn a sibling thread inside
|
||||
/// the newly-opened workspace.
|
||||
///
|
||||
/// The new workspace is opened in the **background** (added as a retained
|
||||
/// tab without switching to it or moving focus), and it's a clean checkout
|
||||
/// rather than inheriting the source workspace's open files and dock layout.
|
||||
/// This mirrors how the agent's non-worktree threads are created in the
|
||||
/// background rather than yanking the user away from what they're doing.
|
||||
pub fn create_worktree_workspace(
|
||||
workspace: &mut Workspace,
|
||||
action: &zed_actions::CreateWorktree,
|
||||
window: &mut gpui::Window,
|
||||
fallback_focused_dock: Option<DockPosition>,
|
||||
cx: &mut gpui::Context<Workspace>,
|
||||
) -> Task<anyhow::Result<CreatedWorktreeWorkspace>> {
|
||||
create_worktree_workspace_inner(
|
||||
workspace,
|
||||
action,
|
||||
window,
|
||||
fallback_focused_dock,
|
||||
RemoteBranchFetchMode::Fetch,
|
||||
// Agent-created worktree workspaces open in the background.
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_worktree_workspace_inner(
|
||||
workspace: &mut Workspace,
|
||||
action: &zed_actions::CreateWorktree,
|
||||
window: &mut gpui::Window,
|
||||
fallback_focused_dock: Option<DockPosition>,
|
||||
remote_branch_fetch_mode: RemoteBranchFetchMode,
|
||||
activate: bool,
|
||||
cx: &mut gpui::Context<Workspace>,
|
||||
) {
|
||||
) -> Task<anyhow::Result<CreatedWorktreeWorkspace>> {
|
||||
let project = workspace.project().clone();
|
||||
|
||||
if project.read(cx).repositories(cx).is_empty() {
|
||||
log::error!("create_worktree: no git repository in the project");
|
||||
return;
|
||||
return Task::ready(Err(anyhow!(
|
||||
"create_worktree: no git repository in the project"
|
||||
)));
|
||||
}
|
||||
if project.read(cx).is_via_collab() {
|
||||
log::error!("create_worktree: not supported in collab projects");
|
||||
return;
|
||||
return Task::ready(Err(anyhow!(
|
||||
"create_worktree: not supported in collab projects"
|
||||
)));
|
||||
}
|
||||
|
||||
// Guard against concurrent creation
|
||||
// Guard against concurrent creation. We treat a concurrent creation as
|
||||
// a hard error here so the caller can surface it; the user-facing
|
||||
// wrapper [`handle_create_worktree`] swallows the error via
|
||||
// `detach_and_log_err`, matching the pre-existing silent return.
|
||||
if workspace.active_worktree_creation().label.is_some() {
|
||||
return;
|
||||
return Task::ready(Err(anyhow!("A worktree creation is already in progress")));
|
||||
}
|
||||
|
||||
let previous_state =
|
||||
|
|
@ -611,13 +692,14 @@ fn handle_create_worktree_inner(
|
|||
let (git_repos, non_git_paths) = classify_worktrees(project.read(cx), cx);
|
||||
|
||||
if git_repos.is_empty() {
|
||||
let toast_workspace = cx.entity();
|
||||
show_error_toast(
|
||||
cx.entity(),
|
||||
toast_workspace,
|
||||
"worktree create",
|
||||
anyhow!("No git repositories found in the project"),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
return Task::ready(Err(anyhow!("No git repositories found in the project")));
|
||||
}
|
||||
|
||||
if remote_connection_options.is_some() {
|
||||
|
|
@ -626,13 +708,16 @@ fn handle_create_worktree_inner(
|
|||
.remote_client()
|
||||
.is_some_and(|client| client.read(cx).is_disconnected());
|
||||
if is_disconnected {
|
||||
let toast_workspace = cx.entity();
|
||||
show_error_toast(
|
||||
cx.entity(),
|
||||
toast_workspace,
|
||||
"worktree create",
|
||||
anyhow!("Cannot create worktree: remote connection is not active"),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Cannot create worktree: remote connection is not active"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -677,6 +762,7 @@ fn handle_create_worktree_inner(
|
|||
workspace_handle.clone(),
|
||||
window_handle,
|
||||
remote_connection_options,
|
||||
activate,
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
|
|
@ -707,7 +793,6 @@ fn handle_create_worktree_inner(
|
|||
|
||||
result
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn handle_switch_worktree(
|
||||
|
|
@ -791,8 +876,9 @@ async fn do_create_worktree(
|
|||
workspace: WeakEntity<Workspace>,
|
||||
window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
|
||||
remote_connection_options: Option<RemoteConnectionOptions>,
|
||||
activate: bool,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> anyhow::Result<CreatedWorktreeWorkspace> {
|
||||
// List existing worktrees from all repos to detect name collisions
|
||||
let worktree_receivers: Vec<_> = cx.update(|_, cx| {
|
||||
git_repos
|
||||
|
|
@ -874,11 +960,17 @@ async fn do_create_worktree(
|
|||
|
||||
let created_paths = await_and_rollback_on_failure(creation_infos, fs, cx).await?;
|
||||
|
||||
// `path_remapping` has one entry per source git repo, while `created_paths`
|
||||
// has one per *unique* target worktree. When the former is larger, two or
|
||||
// more source repos were linked worktrees of the same underlying
|
||||
// repository and `start_worktree_creations` consolidated them.
|
||||
let consolidated_worktrees = path_remapping.len() > created_paths.len();
|
||||
|
||||
let mut all_paths = created_paths;
|
||||
let has_non_git = !non_git_paths.is_empty();
|
||||
all_paths.extend(non_git_paths.iter().cloned());
|
||||
|
||||
open_worktree_workspace(
|
||||
let workspace = open_worktree_workspace(
|
||||
all_paths,
|
||||
path_remapping,
|
||||
non_git_paths,
|
||||
|
|
@ -888,9 +980,15 @@ async fn do_create_worktree(
|
|||
window_handle,
|
||||
remote_connection_options,
|
||||
WorktreeOperation::Create,
|
||||
activate,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
Ok(CreatedWorktreeWorkspace {
|
||||
workspace,
|
||||
consolidated_worktrees,
|
||||
})
|
||||
}
|
||||
|
||||
async fn do_switch_worktree(
|
||||
|
|
@ -902,7 +1000,7 @@ async fn do_switch_worktree(
|
|||
window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
|
||||
remote_connection_options: Option<RemoteConnectionOptions>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> anyhow::Result<Entity<Workspace>> {
|
||||
let path_remapping: Vec<(PathBuf, PathBuf)> = git_repo_work_dirs
|
||||
.iter()
|
||||
.map(|work_dir| (work_dir.clone(), worktree_path.clone()))
|
||||
|
|
@ -922,12 +1020,16 @@ async fn do_switch_worktree(
|
|||
window_handle,
|
||||
remote_connection_options,
|
||||
WorktreeOperation::Switch,
|
||||
// Switching is always an explicit, foreground user action.
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Core workspace opening logic shared by both create and switch flows.
|
||||
/// Returns the newly opened workspace entity so callers can do post-open
|
||||
/// work (e.g., the `create_thread` agent tool spawns a thread inside it).
|
||||
async fn open_worktree_workspace(
|
||||
all_paths: Vec<PathBuf>,
|
||||
path_remapping: Vec<(PathBuf, PathBuf)>,
|
||||
|
|
@ -938,8 +1040,9 @@ async fn open_worktree_workspace(
|
|||
window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
|
||||
remote_connection_options: Option<RemoteConnectionOptions>,
|
||||
operation: WorktreeOperation,
|
||||
activate: bool,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> anyhow::Result<Entity<Workspace>> {
|
||||
let window_handle = window_handle
|
||||
.ok_or_else(|| anyhow!("No window handle available for workspace creation"))?;
|
||||
|
||||
|
|
@ -947,7 +1050,14 @@ async fn open_worktree_workspace(
|
|||
|
||||
let is_creating_new_worktree = matches!(operation, WorktreeOperation::Create);
|
||||
|
||||
let source_for_transfer = if is_creating_new_worktree {
|
||||
// When `activate` is false the new workspace is opened in the background
|
||||
// (e.g. the agent's `create_thread` tool), so it should be a clean
|
||||
// checkout rather than inheriting the source workspace's open files and
|
||||
// dock layout. The state transfer only applies when we're foregrounding
|
||||
// a freshly-created worktree for the user.
|
||||
let transfer_state = is_creating_new_worktree && activate;
|
||||
|
||||
let source_for_transfer = if transfer_state {
|
||||
Some(workspace.clone())
|
||||
} else {
|
||||
None
|
||||
|
|
@ -964,7 +1074,7 @@ async fn open_worktree_workspace(
|
|||
dyn FnOnce(&mut Workspace, &mut gpui::Window, &mut gpui::Context<Workspace>)
|
||||
+ Send,
|
||||
>,
|
||||
> = if is_creating_new_worktree {
|
||||
> = if transfer_state {
|
||||
let dock_structure = previous_state.dock_structure;
|
||||
Some(Box::new(
|
||||
move |workspace: &mut Workspace,
|
||||
|
|
@ -1034,7 +1144,7 @@ async fn open_worktree_workspace(
|
|||
|
||||
maybe_propagate_worktree_trust(&workspace, &new_workspace, &all_paths, cx);
|
||||
|
||||
if is_creating_new_worktree {
|
||||
if transfer_state {
|
||||
window_handle.update(cx, |_multi_workspace, window, cx| {
|
||||
new_workspace.update(cx, |workspace, cx| {
|
||||
if has_non_git {
|
||||
|
|
@ -1133,13 +1243,21 @@ async fn open_worktree_workspace(
|
|||
.ok();
|
||||
|
||||
window_handle.update(cx, |multi_workspace, window, cx| {
|
||||
multi_workspace.activate(new_workspace.clone(), source_for_transfer, window, cx);
|
||||
if activate {
|
||||
multi_workspace.activate(new_workspace.clone(), source_for_transfer, window, cx);
|
||||
} else {
|
||||
// Background open: register the new workspace as a retained tab
|
||||
// but leave the user where they are.
|
||||
multi_workspace.add_background_workspace(new_workspace.clone(), window, cx);
|
||||
}
|
||||
|
||||
if is_creating_new_worktree {
|
||||
new_workspace.update(cx, |workspace, cx| {
|
||||
// Run create-worktree setup hooks regardless of foreground vs
|
||||
// background — the worktree was created either way.
|
||||
workspace.run_create_worktree_tasks(window, cx);
|
||||
|
||||
if let Some(dock_position) = focused_dock {
|
||||
if activate && let Some(dock_position) = focused_dock {
|
||||
let dock = workspace.dock_at_position(dock_position);
|
||||
if let Some(panel) = dock.read(cx).active_panel() {
|
||||
panel.panel_focus_handle(cx).focus(window, cx);
|
||||
|
|
@ -1149,7 +1267,7 @@ async fn open_worktree_workspace(
|
|||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
Ok(new_workspace)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -1504,6 +1504,29 @@ impl MultiWorkspace {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
/// Adds `workspace` as a retained background tab without switching the
|
||||
/// active workspace to it or moving focus. Mirrors the registration and
|
||||
/// retention bookkeeping `activate` performs for the incoming workspace,
|
||||
/// but leaves the currently-active workspace focused.
|
||||
///
|
||||
/// Used when something opens a workspace the user should not be yanked
|
||||
/// into — e.g. the agent's `create_thread` tool spawning a sibling
|
||||
/// worktree in the background.
|
||||
pub fn add_background_workspace(
|
||||
&mut self,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.workspace() == &workspace || self.is_workspace_retained(&workspace) {
|
||||
return;
|
||||
}
|
||||
self.register_workspace(&workspace, window, cx);
|
||||
let key = workspace.read(cx).project_group_key(cx);
|
||||
self.retain_workspace(workspace, key, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Promotes the currently active workspace to persistent if it is
|
||||
/// transient, so it is retained across workspace switches even when
|
||||
/// the sidebar is closed. No-op if the workspace is already persistent.
|
||||
|
|
|
|||
Loading…
Reference in a new issue