Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
20 changes: 20 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
21 changes: 18 additions & 3 deletions apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(

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.

🟡 Medium components/BranchToolbarBranchSelector.tsx:427

When the repository has origin/HEAD but no local copy of the default branch (e.g. local main was deleted), a new worktree is created from the currently checked-out feature branch instead of the repo's default branch. defaultBranchName only finds refs where isDefault is true, but listRefs only sets isDefault on local branches — remote refs like origin/HEAD -> origin/main always have isDefault set to false. So defaultBranchName is null and the code falls back to currentGitBranch. Consider also checking for an origin/HEAD ref (or the remote default ref) when no local default branch exists.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/BranchToolbarBranchSelector.tsx around line 427:

When the repository has `origin/HEAD` but no local copy of the default branch (e.g. local `main` was deleted), a new worktree is created from the currently checked-out feature branch instead of the repo's default branch. `defaultBranchName` only finds refs where `isDefault` is true, but `listRefs` only sets `isDefault` on local branches — remote refs like `origin/HEAD -> origin/main` always have `isDefault` set to false. So `defaultBranchName` is null and the code falls back to `currentGitBranch`. Consider also checking for an `origin/HEAD` ref (or the remote default ref) when no local default branch exists.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 7e65de2listRefs now flags the origin/<default> remote ref as isDefault (derived from origin/HEAD), so the web selector picks it up when no local copy of the default branch exists. The deduped list only hides the remote ref when a matching local branch is present, so exactly one ref carries isDefault either way. Added a regression test covering the deleted-local-default case.

() => 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
Expand Down
8 changes: 4 additions & 4 deletions packages/contracts/src/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading