701 lines · 28.6 KB · Markdown
RawDownload

Multi-Agent Git Worktree Support — Plan (Option C: Hybrid)

Goal: Allow users to start agent threads in isolated git worktrees so that multiple agents can work on the same codebase in parallel without interfering with each other.

Approach: Hybrid — automatic by default, fully configurable. The user picks "New Worktree" from a dropdown before starting a thread. Zed creates a git worktree, opens it as a new workspace inside the MultiWorkspace, and starts the agent thread there.

Scope and Rollout

  • Primary UI target: AgentPanel in crates/agent_ui.
  • agent_ui_v2 / AgentsPanel is out of scope and will be removed.
  • Local + remote support are both in scope.
  • Remote plumbing is the final implementation milestone, after local behavior is complete and stable.

Terminology

Throughout this document:

Term Meaning
Zed Worktree A folder open in a Zed project (project::Worktree / worktree::Worktree). This is Zed's concept of a directory tree, not a git worktree.
Git Worktree A git worktree add checkout — an additional working directory sharing the same .git object store (git::repository::Worktree).
Workspace A workspace::Workspace entity — one project context inside a Zed window.
MultiWorkspace The root view of a Zed window (workspace::MultiWorkspace). Contains one or more Workspace entities and can switch between them.

The git worktree APIs live in crates/git/src/repository.rs on the GitRepository trait:

  • fn worktrees() -> Vec<git::repository::Worktree> — lists git worktrees
  • fn create_worktree(name, directory, from_commit) -> Result<()> — creates a git worktree
  • fn rename_branch(branch, new_name) -> Result<()> — already exists

These must not be confused with project::Worktree / worktree::Worktree which represent open folders in the editor.


Feature Flag

All work is gated behind the existing agent-v2 feature flag (AgentV2FeatureFlag in crates/feature_flags/src/flags.rs).

Use cx.has_flag::<AgentV2FeatureFlag>() to gate UI and behavior. No new flag is needed.


Phase 1: UI — Thread Target Selector

1.1 "Start Thread In…" Dropdown

Where: The agent panel toolbar, adjacent to the existing "New Thread" (+) menu.

What the user sees:

┌──────────────────────┐
│ Start Thread In...   │
│ Local Project        │  ← default, current workspace
│ New Worktree         │  ← creates a git worktree
└──────────────────────┘

With a tooltip on "New Worktree":

You currently have "main" checked out and by default, Zed creates a new branch for worktrees based off the selected branch.

Reuse existing UI components:

  • PopoverMenu + ContextMenu — same pattern as the mode selector (crates/agent_ui/src/acp/mode_selector.rs) and the existing new-thread menu in AgentPanel::render_toolbar().
  • ContextMenuEntry with .icon(), .icon_color(), .handler(), .documentation_aside() for the tooltip — all already used in the new-thread menu.
  • Tooltip::for_action_in() for keyboard shortcut hints.
  • Button with .label_size(LabelSize::Small) + .icon(IconName::ChevronDown) as the trigger — same pattern as ModeSelector's trigger button.

Implementation location: crates/agent_ui/src/agent_panel.rs — modify render_toolbar() and the new-thread menu construction.

Behavior:

  • "Local Project" is always available and is the default.
  • "New Worktree" is only shown when cx.has_flag::<AgentV2FeatureFlag>().
  • "New Worktree" is disabled (grayed out with tooltip) when:
    • The project has no git repository.
    • The project is remote/collab during the local-only implementation milestones.
  • Selecting "New Worktree" does not immediately create the worktree. It sets the panel's next thread target to ThreadTarget::NewWorktree.
  • Worktree creation happens when the user explicitly starts a new ACP thread from the toolbar/menu. It does not wait for first message send.

New types:

#[derive(Clone, Debug, Default, PartialEq)]
pub enum ThreadTarget {
    #[default]
    LocalProject,
    NewWorktree,
    ExistingWorktree {
        /// Path to the git worktree on disk.
        path: PathBuf,
        /// The branch name checked out in the worktree.
        branch: String,
    },
}

Store ThreadTarget on the AgentPanel or on the individual thread view state.

1.2 Worktree Indicator on Running Threads

Where: ACP thread history rendering in crates/agent_ui/src/acp/thread_history.rs (render_history_entry()).

Current ACP history uses ListItem, not ThreadItem. We should add a secondary line in the existing ListItem entry that shows branch/worktree data from session metadata.

What the user sees:

┌──────────────────────────────────────┐
│ ⑀  link-agent-panel                  │
└──────────────────────────────────────┘

The branch icon (⑀) + branch/worktree name is shown beneath the thread title.

Wiring up:

  • Extend the thread history row model to include optional worktree display text.
  • If session metadata contains worktree info, render a second muted line with branch icon + branch/worktree label.
  • Use ThreadItem style as visual reference, but keep implementation in AcpThreadHistory for now to avoid broad UI refactors.

1.3 Loading / Indeterminate States

When "New Worktree" is selected and the user starts a new ACP thread, several async operations happen before session creation:

  1. Creating the git worktree (git worktree add ...)
  2. Opening the worktree path as a new project
  3. Adding the workspace to MultiWorkspace
  4. Starting the agent thread in the new workspace

During this time, the UI needs to show indeterminate/loading states.

Reuse existing components:

  • SpinnerLabel (crates/ui/src/components/label/spinner_label.rs) for creation progress.
  • Existing error notification/prompt patterns from workspace/git UI for failures that occur before ACP thread creation.
  • AcpThreadView::thread_error can still be used for failures after thread view exists.

States to support:

  • The toolbar selector + new-thread trigger should be disabled while creation is in progress for that action.
  • Show SpinnerLabel status text during worktree creation (e.g., "Creating worktree…").
  • If creation fails before thread creation, show an actionable error and keep the user in the current workspace without creating a partial thread.

Note: The exact visual design of these loading states needs further discussion with the team. The infrastructure to support them should be built, but the specific UI treatment is TBD.


Phase 2: Git Repository API Changes

2.1 Add remove_worktree() to GitRepository

The GitRepository trait (crates/git/src/repository.rs) currently has worktrees() and create_worktree() but no way to remove a worktree.

Add to the trait:

fn remove_worktree(
    &self,
    path: PathBuf,
    force: bool,
) -> BoxFuture<'_, Result<()>>;

Implementation on RealGitRepository: run git worktree remove <path> [--force].

Implementation on FakeGitRepository (crates/fs/src/fake_git_repo.rs): track created worktrees in FakeGitRepositoryState and allow removal. The current implementations of worktrees() and create_worktree() on FakeGitRepository are unimplemented!() — these need to be filled in as part of this work to support testing.

2.2 Add rename_worktree() to GitRepository

Git worktrees can be moved/renamed via git worktree move <worktree> <new-path>. This is useful if we want to rename the worktree directory to match the thread title after it's generated.

Add to the trait:

fn rename_worktree(
    &self,
    old_path: PathBuf,
    new_path: PathBuf,
) -> BoxFuture<'_, Result<()>>;

Implementation on RealGitRepository: run git worktree move <old_path> <new_path>.

Implementation on FakeGitRepository: update the tracked worktree entry.

2.3 Repository Handle + Proto Plumbing

GitRepository trait changes are not enough on their own. AgentPanel uses project-level repository handles (project::Repository in crates/project/src/git_store.rs), which handle both local and remote paths.

Add matching methods on project::Repository:

  • remove_worktree(path, force)
  • rename_worktree(old_path, new_path)

Add corresponding RPC plumbing:

  • New messages in crates/proto/proto/git.proto
  • Message registration in crates/proto/src/proto.rs
  • Request handlers in GitStore for remote mode
  • Local + remote branches in repository job execution logic

2.4 Branch Naming

When creating a git worktree for an agent thread, Zed needs to generate a branch name. The default pattern:

zed/agent/<short-id>

Where <short-id> is a random 5-character alphanumeric string (e.g., a4Xiu).

The branch can be renamed via the existing GitRepository::rename_branch() once the agent generates a thread title (similar to how Conductor handles it — start with a placeholder, rename once context is available).

2.5 Worktree Storage Location

Git worktrees are created via git worktree add <path> -b <branch> <base>. The <path> determines where the working directory lives on disk.

Default location: A managed directory under the Zed data directory.

On macOS:

~/Library/Application Support/Zed/agent-worktrees/<repo-name>/<branch-name>/

On Linux:

~/.local/share/zed/agent-worktrees/<repo-name>/<branch-name>/

This keeps worktrees out of the project directory and avoids .gitignore issues.

Project-level override: Users can configure a custom worktree path via the git section of project settings. This requires changes in both:

  • crates/settings_content/src/project.rs (settings_content::GitSettings)
  • crates/project/src/project_settings.rs (project::GitSettings)

Add a new field to both GitSettings structs:

pub struct GitSettings {
    // ... existing fields ...

    /// Directory where agent worktrees are created.
    /// If not set, defaults to the Zed data directory.
    ///
    /// Can be an absolute path or relative to the project root.
    ///
    /// Default: null (uses system default)
    pub agent_worktree_directory: Option<String>,
}

Because this field is not Copy, existing Copy derives on Git settings types must be removed where necessary.

This allows teams to colocate worktrees with the project (e.g., .worktrees/) or use a shared location. Users set this in .zed/settings.json:

{
  "git": {
    "agent_worktree_directory": ".worktrees"
  }
}

2.6 Creation Flow

Use the existing GitRepository::create_worktree() method:

fn create_worktree(
    &self,
    name: String,        // branch name
    directory: PathBuf,  // parent directory for the worktree
    from_commit: Option<String>, // base commit/branch (e.g., "main")
) -> BoxFuture<'_, Result<()>>;

This runs git worktree add <directory>/<name> -b <name> <from_commit>.

The base commit should be the currently checked-out branch HEAD in the main worktree so the agent starts from the same state the user is looking at.

2.7 Trust Management

The existing worktree picker (crates/git_ui/src/worktree_picker.rs) already handles trust for newly created worktrees via TrustedWorktrees. The same pattern should be followed: if the parent project is trusted, automatically trust the new worktree path. See WorktreeListDelegate::create_worktree() for the reference implementation.


Phase 3: Workspace Integration

3.1 Sequence of Operations

When the user starts a new ACP thread while ThreadTarget::NewWorktree is selected:

  1. Create the git worktree (async)

    • Generate branch name (zed/agent/<id>)
    • Determine storage path (default or from GitSettings.agent_worktree_directory)
    • Call GitRepository::create_worktree()
    • Trust the new path (following worktree_picker.rs pattern)
  2. Open the worktree as a new Zed project (async)

    • Use the workspace open path flow with open_new_workspace: Some(true) so we deterministically create a new workspace instead of reusing an existing one.
    • Reference behavior: workspace::open_paths in crates/workspace/src/workspace.rs
  3. Attach workspace to the current MultiWorkspace

    • If open-path flow returns a workspace not yet attached to this window's MultiWorkspace, call add_workspace() explicitly.
  4. Activate the new workspace

    • Ensure the new workspace becomes active (activate() / activate_index() as appropriate).
  5. Start the thread

    • Call AgentPanel::new_agent_thread() in the new workspace's agent panel.
    • The thread runs against the new workspace's project, which points at the git worktree on disk
    • Persist AgentGitWorktreeInfo as soon as the session id is known
  6. Failure rollback

    • If worktree creation succeeds but workspace open or thread startup fails, clean up the created git worktree and surface an error to the user.

3.2 Implementation Location

The orchestration logic for steps 1–6 should live in a new module: crates/agent_ui/src/agent_worktree.rs. This keeps it close to the UI that triggers it while being separate enough to test independently.

The async work should be spawned via cx.spawn() from the AgentPanel, with WeakEntity<Workspace> and the MultiWorkspace root captured for the later workspace management steps.

3.3 Workspace ↔ Thread Association

Each thread needs to know which git worktree (if any) it belongs to. Use persisted thread data as source of truth, with ACP meta as a transport copy.

pub struct AgentGitWorktreeInfo {
    /// The branch name in the git worktree.
    pub branch: String,
    /// Absolute path to the git worktree on disk.
    pub worktree_path: PathBuf,
    /// The base branch/commit the worktree was created from.
    pub base_ref: String,
}

Storage model:

  • Persist AgentGitWorktreeInfo in the agent thread DB model (DbThread) so it survives restarts.
  • Expose enough metadata in thread list results so history rendering can display branch/worktree labels without loading every full thread.
  • Mirror this metadata into AgentSessionInfo.meta when returning session lists.
  • Keep AcpServerView state as runtime cache only, not source of truth.

This enables:

  • History rows to display branch/worktree labels
  • Cleanup to find and remove git worktrees reliably
  • Resume flow to reopen the correct workspace path

Phase 4: Cleanup & Lifecycle

4.1 When to Clean Up

Git worktrees should be cleaned up (removed) when:

  • The user explicitly discards a thread that was running in a worktree
  • The user applies/merges changes back (future work)
  • The worktree's workspace is closed and the thread is complete

Worktrees should not be cleaned up when:

  • The thread is still running
  • The user might want to resume the thread later
  • The workspace is just temporarily not active in MultiWorkspace

4.2 Cleanup Operations

  1. Look up persisted AgentGitWorktreeInfo for the session.
  2. Remove the workspace from MultiWorkspace::remove_workspace() if present.
  3. Run remove_worktree(path, force) through project::Repository.
  4. Optionally delete the branch via existing branch APIs when safe.

4.3 Orphan Detection

On startup, compare:

  • Persisted AgentGitWorktreeInfo entries from thread storage.
  • Actual git worktrees from repository APIs.

If a managed worktree exists without an associated live thread/session record, mark it orphaned and offer cleanup (or auto-cleanup after policy delay).


Phase 5: Testing

5.1 Unit Tests — Git Repository API

Location: crates/git/src/repository.rs (in the existing mod tests block)

  • test_parse_worktrees_from_str: Verify parsing of git worktree list --porcelain output. Test with zero, one, and multiple worktrees. Test with detached HEAD entries and bare repos.
  • test_create_and_list_worktrees: Create a real git repo in a temp dir, add a worktree via the API, verify worktrees() returns it.
  • test_remove_worktree: Create a worktree, remove it, verify it's gone from worktrees() and the directory is deleted.
  • test_remove_worktree_force: Create a worktree with uncommitted changes, verify non-force removal fails, force removal succeeds.
  • test_rename_worktree: Create a worktree, move it to a new path, verify the old path is gone and the new path exists.

5.2 Unit Tests — FakeGitRepository

Location: crates/fs/src/fake_git_repo.rs

Implement worktrees(), create_worktree(), remove_worktree(), and rename_worktree() on FakeGitRepository backed by FakeGitRepositoryState. These are currently unimplemented!().

  • test_fake_worktree_lifecycle: Create, list, remove worktrees on the fake to verify the test infrastructure works.

5.3 Unit Tests — Branch Name Generation

Location: crates/agent_ui/src/agent_worktree.rs

  • test_branch_name_generation: Verify generated names match the zed/agent/<id> pattern, are valid git branch names, and are unique across

Lines 1–500 of 701.