Skip to content

fix(worktree): stop offering worktree actions for projects without a repository - #55771

Open
YueSmith wants to merge 2 commits into
anomalyco:devfrom
YueSmith:worktree-boundary
Open

YueSmith wants to merge 2 commits into
anomalyco:devfrom
YueSmith:worktree-boundary

Conversation

@YueSmith

@YueSmith YueSmith commented Oct 15, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #55776
Related to #55215

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Worktree actions are offered for a project whose directory holds no repository.
Creating one there can only fail: POST /api/worktree returns 400. The same
strategy also walked up to an ancestor repository, so a plain directory under a
repository ran list/create/remove against that repository.

  • core: the git strategy requires the discovered repository root to be the
    requested directory, so list/create/remove stay inside that checkout.
  • gui-extensions: the session details menu offers worktree actions only for a
    git project. Creating, switching and returning to Local all work from the
    project's own checkouts, so nothing about the session's directory gates it.

The TUI half of #55215 is #55326.

How did you verify your code works?

bun typecheck in packages/core and packages/gui-extensions; bun run lint
exits 0.

suite pass skip failing
packages/core 6289 368 6
packages/gui-extensions 175 1 0

The same core numbers come out of dev (4397ee4) with these commits
reverted.

core skips: the shell-scan suites enumerate the shells installed on the machine
and skip the cases for the ones that are missing.

core failing (already failing on dev):

  • Core test environment > isolates global home and XDG roots
  • creates, lists, and removes worktrees for the plugin's project
  • Git trees > refresh handles check-ignore exit 128
  • util.effect-flock > fails on unwritable lock roots
  • util.flock > fails clearly on unwritable lock roots
  • Worktree > requires force to remove a dirty git worktree

By hand: in a directory that is not a repository the session details menu has no
worktree actions; in a repository it still does.

Screenshots / recordings

Web — left: the session details menu for nogit2, a directory that holds no
repository. Right: the same panel after this change.

F6UYUXV@% G14_8X3YXA%C9

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

git.repo.discover searches ancestors, so a plain directory that only sits under a repository ran worktree list/create/remove against that repository. Require the discovered repository root to be the requested directory so operations stay inside the caller's working directory.
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@YueSmith
YueSmith marked this pull request as ready for review October 15, 2026 16:26

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TUI part works. In a folder with no VCS, /worktrees is gone ("No matching commands"); on the base it opens a dialog whose new action always fails. In a git project it still opens normally. Core worktree tests (37 pass, same as base) and typechecks for core, tui and gui-extensions pass. Public API unchanged (no SDK, HTTP, plugin or config changes; core's git strategy is internal).

Two concerns, details inline:

  1. Regression in the web details panel. movable() also hides the workspace menu for git-project sessions that aren't at a checkout root, e.g. a session in a monorepo subfolder. Creating, switching and returning to Local don't depend on the session's directory, so these sessions lose working actions.
  2. Gating on vcs === "git" in the client. Plugins can register worktree strategies (see #51672), and this hides /worktrees, including listing and deleting, for any non-git project such a plugin serves. A per-project capability from the server would be more accurate. If you keep the client-side check, consider hiding only the new action instead of the whole command.

Note that #55326 addresses the same issue in a narrower way (only the dialog's new action).

Related: #51672

Before

After

// Worktrees belong to a git project, and a session may manage them only from a
// checkout root. From a nested directory the menu would reach out to the
// repository above the session's working directory, so it stays a plain label.
const movable = () =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hides the menu for any git-project session whose directory isn't the project root or a sandbox, e.g. a session started in repo/packages/foo. The comment's reason doesn't hold: SessionWorkspaceMenu.createWorktree creates from project.canonical, and "Local" moves to project.worktree, so neither depends on the session's directory. Those sessions currently get a working menu (new worktree, switch, back to Local) and would lose it, along with the "move" prompt below. Checking props.project.vcs === "git" alone is enough for #55215.

name: "session.move",
category: "Session",
slash: { name: "worktrees" },
enabled: canManageWorktrees,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things about this gate:

Ideally the server would say whether the project's selected strategy can create worktrees. Short of that, hiding only the new action in the dialog (as #55326 does) has less impact.

The session details menu offered worktree actions for a project whose directory
holds no git repository, where creating one can only fail. Offer the menu only
for a git project.
@YueSmith

Copy link
Copy Markdown
Author

Thanks. Both points are addressed in 03d304b:

This PR is now core + gui-extensions only: Closes #55776, Related to #55215.

@YueSmith

Copy link
Copy Markdown
Author

Ran the reproduction script from #55776 against this branch (OC_DEV=<checkout> bash repro.sh).

Part 2 — a plain directory whose parent later becomes a repository:

worktree list for the plain project (expect nothing outside .../parent/child):
[]

create worktree for the plain project from the listed checkout:
WorktreeError: Worktree source not found: .../parent        (HTTP 400)

git worktree list in the PARENT repository:
.../parent  6eba60e [master]

Before this change that list showed parent, creating from it added a real worktree to the parent repository, and git worktree list there showed it.

Part 1 — a plain directory with no repository — still returns 400 over the API, which is expected: that part is the UI affordance, not the API. It is handled by the gui-extensions change here and by #55326 in the TUI.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Worktree actions are offered without a repository, and reach an ancestor repository

1 participant