From f8b7070e234d4222e74b88bed3eba3649c968ea8 Mon Sep 17 00:00:00 2001 From: Mnigos Date: Mon, 12 Oct 2026 18:26:10 +0200 Subject: [PATCH 1/2] fix(web): the default-branch dialog no longer offers a feature branch it cannot create On a clean default branch with unpushed commits, Push and Create PR ask for confirmation and offer "Check out feature branch & continue". The server names that branch from the uncommitted changes, so with a clean tree it always fails with "Cannot create a feature branch because there are no changes to commit". Disable the option when the working tree is clean and say why. Mobile already handles this case by branching at HEAD. --- .../components/GitActionsControl.logic.test.ts | 16 ++++++++++++++++ .../src/components/GitActionsControl.logic.ts | 12 ++++++++++++ apps/web/src/components/GitActionsControl.tsx | 11 ++++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/GitActionsControl.logic.test.ts b/apps/web/src/components/GitActionsControl.logic.test.ts index 12aee26c757ed..35f1f565aeb77 100644 --- a/apps/web/src/components/GitActionsControl.logic.test.ts +++ b/apps/web/src/components/GitActionsControl.logic.test.ts @@ -8,6 +8,7 @@ import { requiresDefaultBranchConfirmation, resolveAutoFeatureBranchName, resolveDefaultBranchActionDialogCopy, + resolveFeatureBranchUnavailableReason, resolveGitActionProgressPresentation, resolveGitActionResultToastTiming, resolveLiveThreadBranchUpdate, @@ -1025,6 +1026,21 @@ describe("resolveDefaultBranchActionDialogCopy", () => { }); }); +describe("resolveFeatureBranchUnavailableReason", () => { + it("blocks the feature branch when there is nothing to commit", () => { + assert.equal( + resolveFeatureBranchUnavailableReason({ branchName: "main", hasWorkingTreeChanges: false }), + "There are no changes to commit, so a feature branch can't be named. Create a branch yourself or continue on main.", + ); + }); + + it("allows the feature branch when there are changes to commit", () => { + assert.isNull( + resolveFeatureBranchUnavailableReason({ branchName: "main", hasWorkingTreeChanges: true }), + ); + }); +}); + describe("buildGitActionProgressStages", () => { it("shows only push progress for explicit push actions", () => { const stages = buildGitActionProgressStages({ diff --git a/apps/web/src/components/GitActionsControl.logic.ts b/apps/web/src/components/GitActionsControl.logic.ts index af6206625c411..d59753db1ed12 100644 --- a/apps/web/src/components/GitActionsControl.logic.ts +++ b/apps/web/src/components/GitActionsControl.logic.ts @@ -432,6 +432,18 @@ export function resolveDefaultBranchActionDialogCopy(input: { }; } +/** + * Why the default-ref dialog cannot check out a feature branch, or null when it can. + * The server names that branch from the uncommitted changes, so a clean tree cannot use it. + */ +export function resolveFeatureBranchUnavailableReason(input: { + branchName: string; + hasWorkingTreeChanges: boolean; +}): string | null { + if (input.hasWorkingTreeChanges) return null; + return `There are no changes to commit, so a feature branch can't be named. Create a branch yourself or continue on ${input.branchName}.`; +} + export function resolveThreadBranchUpdate( result: GitRunStackedActionResult, ): { branch: string } | null { diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 6419ce2a779c3..2b1d1adcf3875 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -60,6 +60,7 @@ import { type DefaultBranchConfirmableAction, requiresDefaultBranchConfirmation, resolveDefaultBranchActionDialogCopy, + resolveFeatureBranchUnavailableReason, resolveGitActionProgressPresentation, resolveGitActionResultToastTiming, resolveLiveThreadBranchUpdate, @@ -147,6 +148,7 @@ interface PendingDefaultBranchAction { action: DefaultBranchConfirmableAction; branchName: string; includesCommit: boolean; + hasWorkingTreeChanges: boolean; commitMessage?: string; onConfirmed?: () => void; filePaths?: string[]; @@ -1224,6 +1226,9 @@ export default function GitActionsControl({ terminology: changeRequestTerminology, }) : null; + const featureBranchUnavailableReason = pendingDefaultBranchAction + ? resolveFeatureBranchUnavailableReason(pendingDefaultBranchAction) + : null; useEffect(() => { if (gitCwd === null) { @@ -1301,6 +1306,7 @@ export default function GitActionsControl({ action, branchName: actionBranch, includesCommit, + hasWorkingTreeChanges: !!actionStatus?.hasWorkingTreeChanges, ...(commitMessage ? { commitMessage } : {}), ...(onConfirmed ? { onConfirmed } : {}), ...(filePaths ? { filePaths } : {}), @@ -2117,6 +2123,9 @@ export default function GitActionsControl({ {pendingDefaultBranchActionCopy?.title ?? "Run action on default branch?"} {pendingDefaultBranchActionCopy?.description} + {featureBranchUnavailableReason && ( +

{featureBranchUnavailableReason}

+ )} From c2ab5cd0cf854c9d1b14e61b9b02861a23188298 Mon Sep 17 00:00:00 2001 From: Mnigos Date: Mon, 12 Oct 2026 19:07:27 +0200 Subject: [PATCH 2/2] fix(web): the clean-tree check in the default-branch dialog follows the live git status The disabled state was snapshotted when the dialog opened, so a tree that changed while it stayed open could leave the option stale. Read the component's live status instead; until it loads the option stays enabled and the server remains the authority. --- apps/web/src/components/GitActionsControl.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 2b1d1adcf3875..436c8370aa288 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -148,7 +148,6 @@ interface PendingDefaultBranchAction { action: DefaultBranchConfirmableAction; branchName: string; includesCommit: boolean; - hasWorkingTreeChanges: boolean; commitMessage?: string; onConfirmed?: () => void; filePaths?: string[]; @@ -1226,8 +1225,14 @@ export default function GitActionsControl({ terminology: changeRequestTerminology, }) : null; + // Follows the live status so the button tracks the tree while the dialog is open. Until the + // status loads, keep the option enabled: the server stays the authority and rejects a stale + // request with its own error. const featureBranchUnavailableReason = pendingDefaultBranchAction - ? resolveFeatureBranchUnavailableReason(pendingDefaultBranchAction) + ? resolveFeatureBranchUnavailableReason({ + branchName: pendingDefaultBranchAction.branchName, + hasWorkingTreeChanges: gitStatus?.hasWorkingTreeChanges ?? true, + }) : null; useEffect(() => { @@ -1306,7 +1311,6 @@ export default function GitActionsControl({ action, branchName: actionBranch, includesCommit, - hasWorkingTreeChanges: !!actionStatus?.hasWorkingTreeChanges, ...(commitMessage ? { commitMessage } : {}), ...(onConfirmed ? { onConfirmed } : {}), ...(filePaths ? { filePaths } : {}),