diff --git a/packages/app/e2e/regression/new-session.spec.ts b/packages/app/e2e/regression/new-session.spec.ts index f03b2fe547d..3bcd725a0d8 100644 --- a/packages/app/e2e/regression/new-session.spec.ts +++ b/packages/app/e2e/regression/new-session.spec.ts @@ -92,6 +92,35 @@ test("the location shortcuts choose worktrees from every state and keep cycling" await expect(view.editor).toBeFocused() }) +test("Local shows and targets the main checkout when the draft opened in a worktree", async ({ page }) => { + const root = "C:/OpenCode/LocalFromWorktree" + + const worktree = `${root}/.worktrees/quiet-river` + + const created: unknown[] = [] + + const view = await openDraft(page, { + name: "LocalFromWorktree", + project: { sandboxes: [worktree] }, + worktrees: () => [{ directory: root }, { directory: worktree, strategy: "git" }], + vcs: (directory) => + directory === worktree + ? { current: "worktree-branch", default: "dev" } + : { current: "checkout-branch", default: "dev" }, + onSessionCreate: (body) => void created.push(body.location), + seed: { tabs: [{ draft: "draft_localfromworktree", directory: worktree }] }, + }) + + const screen = page.locator('[data-component="new-session"]') + + await expect(newSessionWorktree(page)).toHaveText("Local") + await expect(screen.getByText("checkout-branch", { exact: true })).toBeVisible() + await expect(screen.getByText("worktree-branch", { exact: true })).toHaveCount(0) + await view.editor.fill("Run in the main checkout") + await submit(page).click() + await expect.poll(() => created).toEqual([{ directory: root }]) +}) + for (const binding of ["custom", "none"]) { test(`the picker tooltips reflect ${binding} shortcuts`, async ({ page }) => { const view = await openDraft(page, { diff --git a/packages/app/src/new-session/composer-adapter.ts b/packages/app/src/new-session/composer-adapter.ts index 8ea95f8a998..1d4d178e03d 100644 --- a/packages/app/src/new-session/composer-adapter.ts +++ b/packages/app/src/new-session/composer-adapter.ts @@ -11,7 +11,6 @@ import { useLocal } from "@/providers/models/selection" import { useData, useServer } from "@/runtime/server/current" import { useServerSDK } from "@/runtime/server/client" import { useTabs } from "@/shell/tabs/tabs" -import { useWorkspaceLocation } from "@/workspaces/location" import { createWorktree } from "@/workspaces/create" import { showToast } from "@/shell/notifications/toast" import type { DraftMcpControls } from "./mcp" @@ -72,6 +71,7 @@ type Started = Awaited> export function createNewSessionComposerAdapter(props: { draftID: string worktree: () => string + root: () => string branch: () => string | undefined submitted: () => void mcp: DraftMcpControls @@ -84,7 +84,6 @@ export function createNewSessionComposerAdapter(props: { const server = useServer() const serverSDK = useServerSDK() const tabs = useTabs() - const location = useWorkspaceLocation() const language = useLanguage() const model = createComposerModelSelection({ agent: () => local.agent.current() }) const controls = createComposerControls({ model }) @@ -97,11 +96,7 @@ export function createNewSessionComposerAdapter(props: { working: () => false, submitted: props.submitted, start(selection, submission, message) { - const currentDirectory = location().directory - - const projectDirectory = - data.location.info({ directory: currentDirectory })?.project.canonical ?? currentDirectory - + const projectDirectory = props.root() const worktree = props.worktree() const directory = worktree === "main" ? projectDirectory : worktree const sessionID = Session.ID.create() diff --git a/packages/app/src/new-session/screen.tsx b/packages/app/src/new-session/screen.tsx index 96ba967ade4..9e505420950 100644 --- a/packages/app/src/new-session/screen.tsx +++ b/packages/app/src/new-session/screen.tsx @@ -44,6 +44,7 @@ export default function NewSessionPage(props: { draftId: string }) { const composer = createNewSessionComposerAdapter({ draftID: props.draftId, worktree: workspace.selection.value, + root: workspace.project.root, branch: workspace.bar.branch, submitted: workspace.selection.remember, mcp, diff --git a/packages/app/src/new-session/workspace/controller.test.ts b/packages/app/src/new-session/workspace/controller.test.ts index c4e0feaa49f..0e29cf7144d 100644 --- a/packages/app/src/new-session/workspace/controller.test.ts +++ b/packages/app/src/new-session/workspace/controller.test.ts @@ -38,19 +38,24 @@ describe("new session workspace selection", () => { expect(resolveNewSessionWorktree(input)).toBe(expected) }) - const branch = (worktree: string) => (worktree === "/project/feature" ? "feature" : undefined) + const branches = new Map([ + ["/project", "dev"], + ["/project/feature", "feature"], + ]) + + const branch = (worktree: string) => branches.get(worktree) test.each([ - { worktree: "main", directory: "/project/feature", createBranch: undefined, expected: "feature" }, - { worktree: "create", directory: "/project/feature", createBranch: undefined, expected: "feature" }, - { worktree: "/project/feature", directory: "/project", createBranch: undefined, expected: "feature" }, - { worktree: "/missing", directory: "/project/feature", createBranch: undefined, expected: undefined }, - { worktree: "create", directory: "/project/feature", createBranch: "release", expected: "release" }, - ])("resolves the branch for $worktree in $directory (new branch: $createBranch)", (row) => { + { worktree: "main", root: "/project", createBranch: undefined, expected: "dev" }, + { worktree: "create", root: "/project", createBranch: undefined, expected: "dev" }, + { worktree: "/project/feature", root: "/project", createBranch: undefined, expected: "feature" }, + { worktree: "/missing", root: "/project", createBranch: undefined, expected: undefined }, + { worktree: "create", root: "/project", createBranch: "release", expected: "release" }, + ])("resolves the branch for $worktree from $root (new branch: $createBranch)", (row) => { expect( resolveNewSessionBranch({ worktree: row.worktree, - directory: row.directory, + root: row.root, createBranch: row.createBranch, worktreeBranch: branch, }), diff --git a/packages/app/src/new-session/workspace/controller.ts b/packages/app/src/new-session/workspace/controller.ts index aec07897723..77ff4ecb2ad 100644 --- a/packages/app/src/new-session/workspace/controller.ts +++ b/packages/app/src/new-session/workspace/controller.ts @@ -26,12 +26,12 @@ export function resolveNewSessionWorktree(input: { enabled: boolean; selected?: export function resolveNewSessionBranch(input: { worktree: string - directory: string + root: string createBranch?: string worktreeBranch: (worktree: string) => string | undefined }) { if (input.worktree === "create" && input.createBranch) return input.createBranch - const directory = input.worktree === "main" || input.worktree === "create" ? input.directory : input.worktree + const directory = input.worktree === "main" || input.worktree === "create" ? input.root : input.worktree return input.worktreeBranch(directory) } @@ -167,8 +167,13 @@ export function createNewSessionWorkspaceController(input: { }), ) - const projectRoot = createMemo(() => currentProject()?.worktree ?? sdk().directory) - const branchDirectory = () => (value() === "create" ? projectRoot() : sdk().directory) + // A draft opened from a worktree session keeps that worktree as its directory; Local still runs in the main checkout. + const projectRoot = createMemo( + () => + data.location.info({ directory: sdk().directory })?.project.canonical ?? + currentProject()?.worktree ?? + sdk().directory, + ) createEffect(() => { void Promise.all([ @@ -178,7 +183,7 @@ export function createNewSessionWorkspaceController(input: { ]).catch(() => undefined) }) createEffect(() => { - if (value() !== "create" || sameDirectory(projectRoot(), sdk().directory)) return + if ((value() !== "main" && value() !== "create") || sameDirectory(projectRoot(), sdk().directory)) return void data.location.vcs.sync({ directory: projectRoot() }).catch(() => undefined) }) // Only the selected worktree feeds the branch label. Syncing every worktree in the inventory boots @@ -197,7 +202,7 @@ export function createNewSessionWorkspaceController(input: { const branch = createMemo(() => resolveNewSessionBranch({ worktree: value(), - directory: branchDirectory(), + root: projectRoot(), createBranch: input.selectedBranch(), worktreeBranch: (worktree) => data.location.vcs.info({ directory: worktree })?.branch.current, }),