Add worktree session creation to the agents overview (#45276)

## What changed

- Add a configurable `new_worktree` action, bound to `w`, for local sessions with worktree support enabled.
- Create the worktree from the cached project default branch, preferring remote `HEAD` and falling back to conventional `main` or `master` refs without fetching. Preserve the selected subdirectory and source checkout edits.
- Start and bind a blank session in the new worktree, preserving the selected approval reviewer without sending an initial turn.
- Show creation progress, pause conflicting actions, clean up unclaimed clean worktrees, and report retained checkout paths when session startup fails. Clear pending creation state on reconnect.

## Testing

Add coverage for default-branch selection, source checkout preservation, session ownership and approval settings, cleanup, startup failures, reconnect state, and busy-state rendering. Extend Git environment isolation coverage and test unrelated non-UTF-8 refs.

GitOrigin-RevId: 771ddc12d04dbf0d12e77e9c96c8386cf31e67c5
This commit is contained in:
Eric Traut
2026-09-13 18:53:06 +00:00
committed by copyberry
parent 44b9011611
commit 6f39a47bb3
49 changed files with 734 additions and 57 deletions
+45
View File
@@ -94,6 +94,51 @@ pub(crate) fn git_path_from_bytes(bytes: &[u8]) -> Result<PathBuf> {
}
}
/// Resolves the cached repository default without fetching or inheriting Git selectors.
/// Prefer a remote HEAD (origin first), then conventional main/master refs.
pub fn default_worktree_base(cwd: &Path) -> Result<String> {
let output = git_output(
cwd,
GitOperation::Metadata,
[
"for-each-ref",
"--format=%(refname) %(symref)",
"refs/remotes",
"refs/heads",
],
)?;
// Unrelated refs may contain non-UTF-8 bytes. Decode only the chosen base,
// because CreateWorktree accepts a UTF-8 revision.
let branches = output
.stdout
.split(|byte| *byte == b'\n')
.filter_map(|line| {
let line = line.strip_suffix(b"\r").unwrap_or(line);
let separator = line.iter().position(|byte| *byte == b' ')?;
Some((&line[..separator], &line[separator + 1..]))
})
.collect::<Vec<_>>();
let base = branches
.iter()
.filter(|(name, target)| name.ends_with(b"/HEAD") && !target.is_empty())
.min_by_key(|(name, _)| (!name.starts_with(b"refs/remotes/origin/"), *name))
.map(|(_, target)| *target)
.or_else(|| {
[
b"refs/remotes/origin/main".as_slice(),
b"refs/remotes/origin/master".as_slice(),
b"refs/heads/main".as_slice(),
b"refs/heads/master".as_slice(),
]
.into_iter()
.find(|candidate| branches.iter().any(|(branch, _)| branch == candidate))
})
.context("Could not determine the project's default branch.")?;
std::str::from_utf8(base)
.context("The project's default branch is not valid UTF-8.")
.map(str::to_owned)
}
fn base_git_command(cwd: &Path) -> Command {
let mut command = Command::new("git");
// Git wrappers export repository-local state to their children. Select this
+30
View File
@@ -166,3 +166,33 @@ fn write_marker_script(path: &Path, marker: &Path) -> Result<()> {
fs::set_permissions(path, fs::Permissions::from_mode(0o755))?;
Ok(())
}
#[cfg(unix)]
#[test]
fn default_base_ignores_unrelated_non_utf8_refs() -> Result<()> {
let (_directory, root) = repository()?;
let head = git_stdout(&root, ["rev-parse", "HEAD"])?;
// Packed refs work even on filesystems that cannot store non-UTF-8 filenames.
fs::write(
root.join(".git/packed-refs"),
[head.as_bytes(), b" refs/heads/unrelated-\xff\n"].concat(),
)?;
assert_eq!(super::default_worktree_base(&root)?, "refs/heads/main");
setup_git(
&root,
&["update-ref", "refs/remotes/origin/release/stable", "HEAD"],
)?;
setup_git(
&root,
&[
"symbolic-ref",
"refs/remotes/origin/HEAD",
"refs/remotes/origin/release/stable",
],
)?;
assert_eq!(
super::default_worktree_base(&root)?,
"refs/remotes/origin/release/stable"
);
Ok(())
}
+1
View File
@@ -20,6 +20,7 @@ use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
pub use git::default_worktree_base;
pub use settings::DEFAULT_WORKTREE_KEEP_COUNT;
pub use settings::WorktreeSettings;