authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-01 19:00:53-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-01 20:06:01-08:00
log95f21c1099c9e28b95ee7c6d2f0a6ac49c333485
tree2165c0fb43c4825922675f4bd34ec06c607363d2
parent8cd92150b13a9b129b8d5cb26f91444d60ea6f55

fix: make default-branch confirmation copy action-aware

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 130 insertions(+), 20 deletions(-)

apps/web/src/components/GitActionsControl.logic.test.ts+48
...@@ -5,6 +5,7 @@ import {...@@ -5,6 +5,7 @@ import {
5 buildMenuItems,5 buildMenuItems,
6 requiresDefaultBranchConfirmation,6 requiresDefaultBranchConfirmation,
7 resolveAutoFeatureBranchName,7 resolveAutoFeatureBranchName,
8 resolveDefaultBranchActionDialogCopy,
8 resolveQuickAction,9 resolveQuickAction,
9 summarizeGitResult,10 summarizeGitResult,
10} from "./GitActionsControl.logic";11} from "./GitActionsControl.logic";
...@@ -744,6 +745,53 @@ describe("requiresDefaultBranchConfirmation", () => {...@@ -744,6 +745,53 @@ describe("requiresDefaultBranchConfirmation", () => {
744 });745 });
745});746});
746747
748describe("resolveDefaultBranchActionDialogCopy", () => {
749 it("uses push-only copy when pushing without a commit", () => {
750 const copy = resolveDefaultBranchActionDialogCopy({
751 action: "commit_push",
752 branchName: "main",
753 includesCommit: false,
754 });
755
756 assert.deepEqual(copy, {
757 title: "Push to default branch?",
758 description:
759 'This action will push local commits on "main". You can continue on this branch or create a feature branch and run the same action there.',
760 continueLabel: "Push to main",
761 });
762 });
763
764 it("uses push-and-pr copy when creating a PR without a commit", () => {
765 const copy = resolveDefaultBranchActionDialogCopy({
766 action: "commit_push_pr",
767 branchName: "main",
768 includesCommit: false,
769 });
770
771 assert.deepEqual(copy, {
772 title: "Push & create PR from default branch?",
773 description:
774 'This action will push local commits and create a PR on "main". You can continue on this branch or create a feature branch and run the same action there.',
775 continueLabel: "Push & create PR",
776 });
777 });
778
779 it("keeps commit copy when the action includes a commit", () => {
780 const copy = resolveDefaultBranchActionDialogCopy({
781 action: "commit_push_pr",
782 branchName: "main",
783 includesCommit: true,
784 });
785
786 assert.deepEqual(copy, {
787 title: "Commit, push & create PR from default branch?",
788 description:
789 'This action will commit, push, and create a PR on "main". You can continue on this branch or create a feature branch and run the same action there.',
790 continueLabel: "Commit, push & create PR",
791 });
792 });
793});
794
747describe("buildGitActionProgressStages", () => {795describe("buildGitActionProgressStages", () => {
748 it("shows only push progress when push-only is forced", () => {796 it("shows only push progress when push-only is forced", () => {
749 const stages = buildGitActionProgressStages({797 const stages = buildGitActionProgressStages({
apps/web/src/components/GitActionsControl.logic.ts+51
...@@ -25,6 +25,12 @@ export interface GitQuickAction {...@@ -25,6 +25,12 @@ export interface GitQuickAction {
25 hint?: string;25 hint?: string;
26}26}
2727
28export interface DefaultBranchActionDialogCopy {
29 title: string;
30 description: string;
31 continueLabel: string;
32}
33
28const SHORT_SHA_LENGTH = 7;34const SHORT_SHA_LENGTH = 7;
29const TOAST_DESCRIPTION_MAX = 72;35const TOAST_DESCRIPTION_MAX = 72;
30const AUTO_FEATURE_BRANCH_PREFIX = "feature/stacked";36const AUTO_FEATURE_BRANCH_PREFIX = "feature/stacked";
...@@ -276,6 +282,51 @@ export function requiresDefaultBranchConfirmation(...@@ -276,6 +282,51 @@ export function requiresDefaultBranchConfirmation(
276 return action === "commit_push" || action === "commit_push_pr";282 return action === "commit_push" || action === "commit_push_pr";
277}283}
278284
285export function resolveDefaultBranchActionDialogCopy(input: {
286 action: GitStackedAction;
287 branchName: string;
288 includesCommit: boolean;
289}): DefaultBranchActionDialogCopy {
290 const branchLabel = input.branchName;
291 const suffix = ` on "${branchLabel}". You can continue on this branch or create a feature branch and run the same action there.`;
292
293 if (input.action === "commit") {
294 return {
295 title: "Commit to default branch?",
296 description: `This action will commit changes${suffix}`,
297 continueLabel: `Commit to ${branchLabel}`,
298 };
299 }
300
301 if (input.action === "commit_push") {
302 if (input.includesCommit) {
303 return {
304 title: "Commit & push to default branch?",
305 description: `This action will commit and push changes${suffix}`,
306 continueLabel: `Commit & push to ${branchLabel}`,
307 };
308 }
309 return {
310 title: "Push to default branch?",
311 description: `This action will push local commits${suffix}`,
312 continueLabel: `Push to ${branchLabel}`,
313 };
314 }
315
316 if (input.includesCommit) {
317 return {
318 title: "Commit, push & create PR from default branch?",
319 description: `This action will commit, push, and create a PR${suffix}`,
320 continueLabel: `Commit, push & create PR`,
321 };
322 }
323 return {
324 title: "Push & create PR from default branch?",
325 description: `This action will push local commits and create a PR${suffix}`,
326 continueLabel: "Push & create PR",
327 };
328}
329
279function formatDateForBranch(now: Date): string {330function formatDateForBranch(now: Date): string {
280 const year = now.getUTCFullYear();331 const year = now.getUTCFullYear();
281 const month = String(now.getUTCMonth() + 1).padStart(2, "0");332 const month = String(now.getUTCMonth() + 1).padStart(2, "0");
apps/web/src/components/GitActionsControl.tsx+31-20
...@@ -11,6 +11,7 @@ import {...@@ -11,6 +11,7 @@ import {
11 type GitQuickAction,11 type GitQuickAction,
12 requiresDefaultBranchConfirmation,12 requiresDefaultBranchConfirmation,
13 resolveAutoFeatureBranchName,13 resolveAutoFeatureBranchName,
14 resolveDefaultBranchActionDialogCopy,
14 resolveQuickAction,15 resolveQuickAction,
15 summarizeGitResult,16 summarizeGitResult,
16} from "./GitActionsControl.logic";17} from "./GitActionsControl.logic";
...@@ -50,6 +51,8 @@ interface GitActionsControlProps {...@@ -50,6 +51,8 @@ interface GitActionsControlProps {
5051
51interface PendingDefaultBranchAction {52interface PendingDefaultBranchAction {
52 action: GitStackedAction;53 action: GitStackedAction;
54 branchName: string;
55 includesCommit: boolean;
53 commitMessage?: string;56 commitMessage?: string;
54 forcePushOnlyProgress: boolean;57 forcePushOnlyProgress: boolean;
55 onConfirmed?: () => void;58 onConfirmed?: () => void;
...@@ -115,12 +118,6 @@ const COMMIT_DIALOG_TITLE = "Commit changes";...@@ -115,12 +118,6 @@ const COMMIT_DIALOG_TITLE = "Commit changes";
115const COMMIT_DIALOG_DESCRIPTION =118const COMMIT_DIALOG_DESCRIPTION =
116 "Review and confirm your commit. Leave the message blank to auto-generate one.";119 "Review and confirm your commit. Leave the message blank to auto-generate one.";
117120
118const DEFAULT_BRANCH_ACTION_DESCRIPTION: Record<GitStackedAction, string> = {
119 commit: "commit changes",
120 commit_push: "commit and push changes",
121 commit_push_pr: "commit, push, and create a PR",
122};
123
124function GitActionItemIcon({ icon }: { icon: GitActionIconName }) {121function GitActionItemIcon({ icon }: { icon: GitActionIconName }) {
125 if (icon === "commit") return <GitCommitIcon />;122 if (icon === "commit") return <GitCommitIcon />;
126 if (icon === "push") return <CloudUploadIcon />;123 if (icon === "push") return <CloudUploadIcon />;
...@@ -202,6 +199,13 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -202,6 +199,13 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
202 const quickActionDisabledReason = quickAction.disabled199 const quickActionDisabledReason = quickAction.disabled
203 ? (quickAction.hint ?? "This action is currently unavailable.")200 ? (quickAction.hint ?? "This action is currently unavailable.")
204 : null;201 : null;
202 const pendingDefaultBranchActionCopy = pendingDefaultBranchAction
203 ? resolveDefaultBranchActionDialogCopy({
204 action: pendingDefaultBranchAction.action,
205 branchName: pendingDefaultBranchAction.branchName,
206 includesCommit: pendingDefaultBranchAction.includesCommit,
207 })
208 : null;
205209
206 const openExistingPr = useCallback(async () => {210 const openExistingPr = useCallback(async () => {
207 const api = readNativeApi();211 const api = readNativeApi();
...@@ -253,6 +257,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -253,6 +257,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
253 const actionStatus = statusOverride ?? gitStatusForActions;257 const actionStatus = statusOverride ?? gitStatusForActions;
254 const actionIsDefaultBranch = isDefaultBranchOverride ?? isDefaultBranch;258 const actionIsDefaultBranch = isDefaultBranchOverride ?? isDefaultBranch;
255 const actionBranch = actionStatus?.branch ?? null;259 const actionBranch = actionStatus?.branch ?? null;
260 const includesCommit =
261 !forcePushOnlyProgress && (action === "commit" || !!actionStatus?.hasWorkingTreeChanges);
256 if (262 if (
257 !skipDefaultBranchPrompt &&263 !skipDefaultBranchPrompt &&
258 requiresDefaultBranchConfirmation(action, actionIsDefaultBranch) &&264 requiresDefaultBranchConfirmation(action, actionIsDefaultBranch) &&
...@@ -260,6 +266,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -260,6 +266,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
260 ) {266 ) {
261 setPendingDefaultBranchAction({267 setPendingDefaultBranchAction({
262 action,268 action,
269 branchName: actionBranch,
270 includesCommit,
263 ...(commitMessage ? { commitMessage } : {}),271 ...(commitMessage ? { commitMessage } : {}),
264 forcePushOnlyProgress,272 forcePushOnlyProgress,
265 ...(onConfirmed ? { onConfirmed } : {}),273 ...(onConfirmed ? { onConfirmed } : {}),
...@@ -398,10 +406,13 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -398,10 +406,13 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
398406
399 const continuePendingDefaultBranchAction = useCallback(() => {407 const continuePendingDefaultBranchAction = useCallback(() => {
400 if (!pendingDefaultBranchAction) return;408 if (!pendingDefaultBranchAction) return;
401 const pendingAction = pendingDefaultBranchAction;409 const { action, commitMessage, forcePushOnlyProgress, onConfirmed } = pendingDefaultBranchAction;
402 setPendingDefaultBranchAction(null);410 setPendingDefaultBranchAction(null);
403 void runGitActionWithToast({411 void runGitActionWithToast({
404 ...pendingAction,412 action,
413 ...(commitMessage ? { commitMessage } : {}),
414 forcePushOnlyProgress,
415 ...(onConfirmed ? { onConfirmed } : {}),
405 skipDefaultBranchPrompt: true,416 skipDefaultBranchPrompt: true,
406 });417 });
407 }, [pendingDefaultBranchAction, runGitActionWithToast]);418 }, [pendingDefaultBranchAction, runGitActionWithToast]);
...@@ -456,9 +467,14 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -456,9 +467,14 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
456467
457 const checkoutFeatureBranchAndContinuePendingAction = useCallback(() => {468 const checkoutFeatureBranchAndContinuePendingAction = useCallback(() => {
458 if (!pendingDefaultBranchAction) return;469 if (!pendingDefaultBranchAction) return;
459 const pendingAction = pendingDefaultBranchAction;470 const { action, commitMessage, forcePushOnlyProgress, onConfirmed } = pendingDefaultBranchAction;
460 setPendingDefaultBranchAction(null);471 setPendingDefaultBranchAction(null);
461 checkoutNewBranchAndRunAction(pendingAction);472 checkoutNewBranchAndRunAction({
473 action,
474 ...(commitMessage ? { commitMessage } : {}),
475 forcePushOnlyProgress,
476 ...(onConfirmed ? { onConfirmed } : {}),
477 });
462 }, [pendingDefaultBranchAction, checkoutNewBranchAndRunAction]);478 }, [pendingDefaultBranchAction, checkoutNewBranchAndRunAction]);
463479
464 const runDialogActionOnNewBranch = useCallback(() => {480 const runDialogActionOnNewBranch = useCallback(() => {
...@@ -801,22 +817,17 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -801,22 +817,17 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
801 >817 >
802 <DialogPopup>818 <DialogPopup>
803 <DialogHeader>819 <DialogHeader>
804 <DialogTitle>Commit to default branch?</DialogTitle>820 <DialogTitle>
805 <DialogDescription>821 {pendingDefaultBranchActionCopy?.title ?? "Run action on default branch?"}
806 This action will{" "}822 </DialogTitle>
807 {pendingDefaultBranchAction823 <DialogDescription>{pendingDefaultBranchActionCopy?.description}</DialogDescription>
808 ? DEFAULT_BRANCH_ACTION_DESCRIPTION[pendingDefaultBranchAction.action]
809 : "push changes"}{" "}
810 on "{gitStatusForActions?.branch ?? "default"}". You can continue on this branch or
811 create a feature branch and run the same action there.
812 </DialogDescription>
813 </DialogHeader>824 </DialogHeader>
814 <DialogFooter>825 <DialogFooter>
815 <Button variant="outline" size="sm" onClick={() => setPendingDefaultBranchAction(null)}>826 <Button variant="outline" size="sm" onClick={() => setPendingDefaultBranchAction(null)}>
816 Abort827 Abort
817 </Button>828 </Button>
818 <Button variant="outline" size="sm" onClick={continuePendingDefaultBranchAction}>829 <Button variant="outline" size="sm" onClick={continuePendingDefaultBranchAction}>
819 Commit to {gitStatusForActions?.branch ?? "default"}830 {pendingDefaultBranchActionCopy?.continueLabel ?? "Continue"}
820 </Button>831 </Button>
821 <Button size="sm" onClick={checkoutFeatureBranchAndContinuePendingAction}>832 <Button size="sm" onClick={checkoutFeatureBranchAndContinuePendingAction}>
822 Checkout feature branch & continue833 Checkout feature branch & continue