diff --git a/apps/mobile/src/features/threads/new-task-flow-provider.tsx b/apps/mobile/src/features/threads/new-task-flow-provider.tsx index ee2bde9d971..682a47901e0 100644 --- a/apps/mobile/src/features/threads/new-task-flow-provider.tsx +++ b/apps/mobile/src/features/threads/new-task-flow-provider.tsx @@ -348,7 +348,10 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { const workspaceMode = selectedProjectDraft.workspaceSelection?.mode ?? "local"; const selectedBranchName = selectedProjectDraft.workspaceSelection?.branch ?? null; const selectedWorktreePath = selectedProjectDraft.workspaceSelection?.worktreePath ?? null; - const startFromOrigin = selectedProjectDraft.workspaceSelection?.startFromOrigin ?? false; + const startFromOrigin = + selectedProjectDraft.workspaceSelection?.startFromOrigin ?? + selectedEnvironmentServerConfig?.settings.newWorktreesStartFromOrigin ?? + true; const runtimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE; const interactionMode = selectedProjectDraft.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE; @@ -598,8 +601,8 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { return; } const preferredBranch = - availableBranches.find((branch) => branch.current) ?? availableBranches.find((branch) => branch.isDefault) ?? + availableBranches.find((branch) => branch.current) ?? null; if (preferredBranch) { selectBranch(preferredBranch); diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index dc58fc2543c..655dfea568c 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -523,6 +523,26 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { }), ); + it.effect("marks the origin default ref as default when no local copy exists", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + const remote = yield* makeTmpDir("git-vcs-driver-remote-"); + const { initialBranch } = yield* initRepoWithCommit(cwd); + yield* git(remote, ["init", "--bare"]); + yield* git(cwd, ["remote", "add", "origin", remote]); + yield* git(cwd, ["push", "-u", "origin", initialBranch]); + yield* git(cwd, ["remote", "set-head", "origin", initialBranch]); + yield* git(cwd, ["checkout", "-b", "feature/only-local"]); + yield* git(cwd, ["branch", "-D", initialBranch]); + const driver = yield* GitVcsDriver.GitVcsDriver; + + const refs = yield* driver.listRefs({ cwd }); + const remoteDefault = refs.refs.find((ref) => ref.name === `origin/${initialBranch}`); + assert.equal(remoteDefault?.isRemote, true); + assert.equal(remoteDefault?.isDefault, true); + }), + ); + it.effect("creates, checks out, renames, and lists refs", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index a406cbce549..0140a25d6f4 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -2202,7 +2202,12 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* name: refName.name, current: false, isRemote: true, - isDefault: false, + // origin/HEAD's target is the repo default even when no local + // copy of the default branch exists. + isDefault: + defaultBranch !== null && + parsedRemoteRef?.remoteName === "origin" && + parsedRemoteRef.branchName === defaultBranch, worktreePath: null, }; if (parsedRemoteRef) { diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index e2ee24c3608..67ae3a8187d 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -422,17 +422,32 @@ export function BranchToolbarBranchSelector({ }); }; + // Default the worktree base to the repo default branch (origin/HEAD), only + // falling back to the checked-out branch when no default is known. + const defaultBranchName = useMemo( + () => refs.find((refName) => refName.isDefault)?.name ?? null, + [refs], + ); + const worktreeBaseBranchCandidate = isInitialBranchesLoadPending + ? null + : (defaultBranchName ?? currentGitBranch); useEffect(() => { if ( effectiveEnvMode !== "worktree" || activeWorktreePath || activeThreadBranch || - !currentGitBranch + !worktreeBaseBranchCandidate ) { return; } - setThreadBranch(currentGitBranch, null); - }, [activeThreadBranch, activeWorktreePath, currentGitBranch, effectiveEnvMode, setThreadBranch]); + setThreadBranch(worktreeBaseBranchCandidate, null); + }, [ + activeThreadBranch, + activeWorktreePath, + effectiveEnvMode, + setThreadBranch, + worktreeBaseBranchCandidate, + ]); // --------------------------------------------------------------------------- // Combobox / list plumbing diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index ac2d47ca336..b1db6005223 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -88,14 +88,14 @@ describe("ServerSettings.providerInstances (slice-2 invariant)", () => { }); describe("ServerSettings worktree defaults", () => { - it("defaults start-from-origin off for legacy configs", () => { - expect(decodeServerSettings({}).newWorktreesStartFromOrigin).toBe(false); + it("defaults start-from-origin on for legacy configs", () => { + expect(decodeServerSettings({}).newWorktreesStartFromOrigin).toBe(true); }); it("accepts start-from-origin updates", () => { expect( - decodeServerSettingsPatch({ newWorktreesStartFromOrigin: true }).newWorktreesStartFromOrigin, - ).toBe(true); + decodeServerSettingsPatch({ newWorktreesStartFromOrigin: false }).newWorktreesStartFromOrigin, + ).toBe(false); }); }); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 6ccd65533dd..e3cac17889a 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -375,7 +375,7 @@ export const ServerSettings = Schema.Struct({ Schema.withDecodingDefault(Effect.succeed("local" as const satisfies ThreadEnvMode)), ), newWorktreesStartFromOrigin: Schema.Boolean.pipe( - Schema.withDecodingDefault(Effect.succeed(false)), + Schema.withDecodingDefault(Effect.succeed(true)), ), addProjectBaseDirectory: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), textGenerationModelSelection: ModelSelection.pipe(