Skip to content

desktop(v2): selecting Local in New Session shows branch from last selected worktree instead of main worktree #55327

Closed

Description

@gh-ppathi

Description

When working with git worktrees in OpenCode Desktop v2, selecting Local (the main worktree) on the New Session screen displays the branch from the last selected/active worktree instead of the main worktree's branch.

PR #49668 updated packages/app/src/new-session/composer-adapter.ts so worktree === "main" (Local) resolves to project.canonical (the main worktree), but packages/app/src/new-session/workspace/controller.ts still resolves and syncs the displayed branch for "main" against sdk().directory (the draft's inherited worktree directory) instead of projectRoot():

  • packages/app/src/new-session/workspace/controller.ts (branch memo, lines 215–222) passes directory: sdk().directory instead of projectRoot() when value() is "main".
  • The VCS sync effect (lines 195–201) only syncs sdk().directory and does not sync projectRoot() when projectRoot() !== sdk().directory.

Plugins

None

OpenCode version

OpenCode Desktop 2.0.28

Steps to reproduce

  1. Open a project that has a main checkout (e.g. on main) and a secondary worktree on another branch (e.g. feature/foo).
  2. Select/open a session in the secondary worktree (feature/foo).
  3. Open a New Session tab (+ / Cmd+T) and select Local in the workspace picker.
  4. Observe the branch pill next to Local: it shows feature/foo (from the last selected worktree) instead of main (the main worktree's branch).

Screenshot and/or share link

N/A

Operating System

macOS 26.6.2 (arm64)

Terminal

OpenCode Desktop GUI

Activity

  1. opencode-agent commented on Oct 14, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the pointers to the code. I could confirm this at the code level; there is no desktop app in this sandbox, so I couldn't watch the branch pill itself.

    What I did:

    • Set up a repo with the main checkout on dev and a second worktree on feature.
    • Called resolveNewSessionBranch from packages/app/src/new-session/workspace/controller.ts the same way the New Session controller does when the draft came from the feature worktree and Local is selected (worktree: "main", directory: sdk().directory).

    It returns feature instead of dev, both in 2.0.28 and on the latest dev (165cd2e). As you pointed out:

    • The branch memo uses sdk().directory instead of the main checkout.
    • The selection sync effect skips "main".
    • The composer already sends Local to project.canonical, so the label can disagree with where the session actually starts.

    The existing unit test in controller.test.ts also asserts this behaviour (main in /project/feature → feature), so it will need updating along with the fix. The script below reproduces it.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #55327 at the logic level: the New Session "Local" branch label is computed by
    # resolveNewSessionBranch() in packages/app/src/new-session/workspace/controller.ts, which the
    # controller calls with directory = sdk().directory (the draft's inherited worktree).
    # Usage: REPO=/path/to/opencode/checkout (deps installed) [REF=v2.0.28] ./repro.sh
    set -euo pipefail
    REPO=${REPO:-/workspace/repo}
    REF=${REF:-}
    WORK=$(mktemp -d)
    
    # 1. Real git project: main checkout on "dev", secondary worktree on "feature".
    git init -q -b dev "$WORK/project"
    git -C "$WORK/project" -c user.email=a@b -c user.name=a commit -q --allow-empty -m init
    git -C "$WORK/project" worktree add -q -b feature "$WORK/project-feature"
    
    # 2. Load controller.ts from the requested ref (or the working tree).
    SRC="$REPO/packages/app/src/new-session/workspace/controller.ts"
    if [ -n "$REF" ]; then
      SRC="$REPO/packages/app/src/new-session/workspace/.controller-$REF.ts"
      git -C "$REPO" show "$REF:packages/app/src/new-session/workspace/controller.ts" > "$SRC"
    fi
    TEST="$REPO/packages/app/src/new-session/workspace/repro-55327.test.ts"
    cat > "$TEST" <<EOF
    import { test, expect } from "bun:test"
    import { execSync } from "node:child_process"
    import { resolveNewSessionBranch } from "$SRC"
    const main = "$WORK/project", feature = "$WORK/project-feature"
    const gitBranch = (dir: string) => execSync("git branch --show-current", { cwd: dir }).toString().trim()
    test("Local shows the main worktree branch", () => {
      // Same arguments the controller's branch memo passes when the New Session draft was
      // opened from a session in the secondary worktree (sdk().directory === feature) and Local is selected.
      const label = resolveNewSessionBranch({ worktree: "main", directory: feature, worktreeBranch: gitBranch })
      console.log("main checkout branch:", gitBranch(main), "| label shown for Local:", label)
      expect(label).toBe(gitBranch(main))
    })
    EOF
    cd "$REPO/packages/app"
    bun test --conditions=solid --isolate --preload ./happydom.ts "$TEST" || STATUS=$?
    rm -f "$TEST" ${REF:+"$SRC"}
    exit ${STATUS:-0}

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions