| ... | @@ -8,7 +8,6 @@ import { | ... | @@ -8,7 +8,6 @@ import { |
| 8 | buildMenuItems, | 8 | buildMenuItems, |
| 9 | type GitActionIconName, | 9 | type GitActionIconName, |
| 10 | type GitActionMenuItem, | 10 | type GitActionMenuItem, |
| 11 | type GitDialogAction, | | |
| 12 | type GitQuickAction, | 11 | type GitQuickAction, |
| 13 | requiresDefaultBranchConfirmation, | 12 | requiresDefaultBranchConfirmation, |
| 14 | resolveAutoFeatureBranchName, | 13 | resolveAutoFeatureBranchName, |
| ... | @@ -88,6 +87,9 @@ function getMenuActionDisabledReason( | ... | @@ -88,6 +87,9 @@ function getMenuActionDisabledReason( |
| 88 | if (isBehind) { | 87 | if (isBehind) { |
| 89 | return "Branch is behind upstream. Pull/rebase before pushing."; | 88 | return "Branch is behind upstream. Pull/rebase before pushing."; |
| 90 | } | 89 | } |
| | 90 | if (!gitStatus.hasUpstream) { |
| | 91 | return "Branch has no upstream. Push to publish it."; |
| | 92 | } |
| 91 | if (!isAhead) { | 93 | if (!isAhead) { |
| 92 | return "No local commits to push."; | 94 | return "No local commits to push."; |
| 93 | } | 95 | } |
| ... | @@ -103,29 +105,21 @@ function getMenuActionDisabledReason( | ... | @@ -103,29 +105,21 @@ function getMenuActionDisabledReason( |
| 103 | if (hasChanges) { | 105 | if (hasChanges) { |
| 104 | return "Commit local changes before creating a PR."; | 106 | return "Commit local changes before creating a PR."; |
| 105 | } | 107 | } |
| 106 | if (!isAhead) { | | |
| 107 | return "No local commits to include in a PR."; | | |
| 108 | } | | |
| 109 | if (!gitStatus.hasUpstream) { | 108 | if (!gitStatus.hasUpstream) { |
| 110 | return "Set an upstream branch before creating a PR."; | 109 | return "Set an upstream branch before creating a PR."; |
| 111 | } | 110 | } |
| | 111 | if (!isAhead) { |
| | 112 | return "No local commits to include in a PR."; |
| | 113 | } |
| 112 | if (isBehind) { | 114 | if (isBehind) { |
| 113 | return "Branch is behind upstream. Pull/rebase before creating a PR."; | 115 | return "Branch is behind upstream. Pull/rebase before creating a PR."; |
| 114 | } | 116 | } |
| 115 | return "Create PR is currently unavailable."; | 117 | return "Create PR is currently unavailable."; |
| 116 | } | 118 | } |
| 117 | | 119 | |
| 118 | const DIALOG_TITLE_BY_ACTION = { | 120 | const COMMIT_DIALOG_TITLE = "Commit changes"; |
| 119 | commit: "Commit changes", | 121 | const COMMIT_DIALOG_DESCRIPTION = |
| 120 | push: "Push branch", | 122 | "Review and confirm your commit. Leave the message blank to auto-generate one."; |
| 121 | create_pr: "Create pull request", | | |
| 122 | }; | | |
| 123 | | | |
| 124 | const DIALOG_DESCRIPTION_BY_ACTION = { | | |
| 125 | commit: "Review and confirm your commit. Leave the message blank to auto-generate one.", | | |
| 126 | push: "Push this branch now.", | | |
| 127 | create_pr: "Create a pull request using generated title/body content.", | | |
| 128 | }; | | |
| 129 | | 123 | |
| 130 | const DEFAULT_BRANCH_ACTION_DESCRIPTION: Record<GitStackedAction, string> = { | 124 | const DEFAULT_BRANCH_ACTION_DESCRIPTION: Record<GitStackedAction, string> = { |
| 131 | commit: "commit changes", | 125 | commit: "commit changes", |
| ... | @@ -158,7 +152,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions | ... | @@ -158,7 +152,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 158 | [activeThreadId], | 152 | [activeThreadId], |
| 159 | ); | 153 | ); |
| 160 | const queryClient = useQueryClient(); | 154 | const queryClient = useQueryClient(); |
| 161 | const [activeDialogAction, setActiveDialogAction] = useState<GitDialogAction | null>(null); | 155 | const [isCommitDialogOpen, setIsCommitDialogOpen] = useState(false); |
| 162 | const [dialogCommitMessage, setDialogCommitMessage] = useState(""); | 156 | const [dialogCommitMessage, setDialogCommitMessage] = useState(""); |
| 163 | const [pendingDefaultBranchAction, setPendingDefaultBranchAction] = | 157 | const [pendingDefaultBranchAction, setPendingDefaultBranchAction] = |
| 164 | useState<PendingDefaultBranchAction | null>(null); | 158 | useState<PendingDefaultBranchAction | null>(null); |
| ... | @@ -376,7 +370,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions | ... | @@ -376,7 +370,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 376 | children: "Create PR", | 370 | children: "Create PR", |
| 377 | onClick: () => { | 371 | onClick: () => { |
| 378 | closeResultToast(); | 372 | closeResultToast(); |
| 379 | setActiveDialogAction("create_pr"); | 373 | void runGitActionWithToast({ action: "commit_push_pr" }); |
| 380 | }, | 374 | }, |
| 381 | }, | 375 | }, |
| 382 | } | 376 | } |
| ... | @@ -396,7 +390,6 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions | ... | @@ -396,7 +390,6 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 396 | [ | 390 | [ |
| 397 | isDefaultBranch, | 391 | isDefaultBranch, |
| 398 | runImmediateGitActionMutation, | 392 | runImmediateGitActionMutation, |
| 399 | setActiveDialogAction, | | |
| 400 | setPendingDefaultBranchAction, | 393 | setPendingDefaultBranchAction, |
| 401 | threadToastData, | 394 | threadToastData, |
| 402 | gitStatusForActions, | 395 | gitStatusForActions, |
| ... | @@ -455,6 +448,54 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions | ... | @@ -455,6 +448,54 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 455 | threadToastData, | 448 | threadToastData, |
| 456 | ]); | 449 | ]); |
| 457 | | 450 | |
| | 451 | const runDialogActionOnNewBranch = useCallback(() => { |
| | 452 | if (!isCommitDialogOpen) return; |
| | 453 | const action: GitStackedAction = "commit"; |
| | 454 | const commitMessage = dialogCommitMessage.trim(); |
| | 455 | const branchName = resolveAutoFeatureBranchName(branchList?.branches.map((branch) => branch.name) ?? []); |
| | 456 | |
| | 457 | setIsCommitDialogOpen(false); |
| | 458 | setDialogCommitMessage(""); |
| | 459 | |
| | 460 | const checkoutPromise = createBranchAndCheckoutMutation.mutateAsync(branchName); |
| | 461 | toastManager.promise(checkoutPromise, { |
| | 462 | loading: { title: `Creating ${branchName}...`, data: threadToastData }, |
| | 463 | success: () => ({ |
| | 464 | title: `Checked out ${branchName}`, |
| | 465 | data: threadToastData, |
| | 466 | }), |
| | 467 | error: (error) => ({ |
| | 468 | title: "Failed to checkout feature branch", |
| | 469 | description: error instanceof Error ? error.message : "An error occurred.", |
| | 470 | data: threadToastData, |
| | 471 | }), |
| | 472 | }); |
| | 473 | |
| | 474 | void checkoutPromise |
| | 475 | .then(() => { |
| | 476 | const statusOverride = gitStatusForActions |
| | 477 | ? { ...gitStatusForActions, branch: branchName, pr: null } |
| | 478 | : null; |
| | 479 | return runGitActionWithToast({ |
| | 480 | action, |
| | 481 | ...(commitMessage ? { commitMessage } : {}), |
| | 482 | skipDefaultBranchPrompt: true, |
| | 483 | statusOverride, |
| | 484 | isDefaultBranchOverride: false, |
| | 485 | }); |
| | 486 | }) |
| | 487 | .catch(() => undefined); |
| | 488 | }, [ |
| | 489 | branchList?.branches, |
| | 490 | createBranchAndCheckoutMutation, |
| | 491 | dialogCommitMessage, |
| | 492 | gitStatusForActions, |
| | 493 | isCommitDialogOpen, |
| | 494 | runGitActionWithToast, |
| | 495 | setIsCommitDialogOpen, |
| | 496 | threadToastData, |
| | 497 | ]); |
| | 498 | |
| 458 | const runQuickAction = useCallback(() => { | 499 | const runQuickAction = useCallback(() => { |
| 459 | if (quickAction.kind === "open_pr") { | 500 | if (quickAction.kind === "open_pr") { |
| 460 | void openExistingPr(); | 501 | void openExistingPr(); |
| ... | @@ -506,30 +547,24 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions | ... | @@ -506,30 +547,24 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 506 | void runGitActionWithToast({ action: "commit_push", forcePushOnlyProgress: true }); | 547 | void runGitActionWithToast({ action: "commit_push", forcePushOnlyProgress: true }); |
| 507 | return; | 548 | return; |
| 508 | } | 549 | } |
| 509 | if (item.dialogAction) { | 550 | if (item.dialogAction === "create_pr") { |
| 510 | setActiveDialogAction(item.dialogAction); | 551 | void runGitActionWithToast({ action: "commit_push_pr" }); |
| | 552 | return; |
| 511 | } | 553 | } |
| | 554 | setIsCommitDialogOpen(true); |
| 512 | }, | 555 | }, |
| 513 | [openExistingPr, runGitActionWithToast], | 556 | [openExistingPr, runGitActionWithToast, setIsCommitDialogOpen], |
| 514 | ); | 557 | ); |
| 515 | | 558 | |
| 516 | const runDialogAction = useCallback(() => { | 559 | const runDialogAction = useCallback(() => { |
| 517 | if (!activeDialogAction) return; | 560 | if (!isCommitDialogOpen) return; |
| 518 | const action: GitStackedAction = | 561 | const commitMessage = dialogCommitMessage.trim(); |
| 519 | activeDialogAction === "commit" | 562 | setIsCommitDialogOpen(false); |
| 520 | ? "commit" | | |
| 521 | : activeDialogAction === "push" | | |
| 522 | ? "commit_push" | | |
| 523 | : "commit_push_pr"; | | |
| 524 | const commitMessage = activeDialogAction === "commit" ? dialogCommitMessage.trim() : ""; | | |
| 525 | const forcePushOnlyProgress = activeDialogAction === "push"; | | |
| 526 | setActiveDialogAction(null); | | |
| 527 | void runGitActionWithToast({ | 563 | void runGitActionWithToast({ |
| 528 | action, | 564 | action: "commit", |
| 529 | ...(commitMessage ? { commitMessage } : {}), | 565 | ...(commitMessage ? { commitMessage } : {}), |
| 530 | ...(forcePushOnlyProgress ? { forcePushOnlyProgress } : {}), | | |
| 531 | }); | 566 | }); |
| 532 | }, [activeDialogAction, dialogCommitMessage, runGitActionWithToast]); | 567 | }, [dialogCommitMessage, isCommitDialogOpen, runGitActionWithToast, setIsCommitDialogOpen]); |
| 533 | | 568 | |
| 534 | const openChangedFileInEditor = useCallback( | 569 | const openChangedFileInEditor = useCallback( |
| 535 | (filePath: string) => { | 570 | (filePath: string) => { |
| ... | @@ -685,68 +720,29 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions | ... | @@ -685,68 +720,29 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 685 | )} | 720 | )} |
| 686 | | 721 | |
| 687 | <Dialog | 722 | <Dialog |
| 688 | open={activeDialogAction !== null} | 723 | open={isCommitDialogOpen} |
| 689 | onOpenChange={(open) => { | 724 | onOpenChange={(open) => { |
| 690 | if (!open) { | 725 | if (!open) { |
| 691 | setActiveDialogAction(null); | 726 | setIsCommitDialogOpen(false); |
| 692 | setDialogCommitMessage(""); | 727 | setDialogCommitMessage(""); |
| 693 | } | 728 | } |
| 694 | }} | 729 | }} |
| 695 | > | 730 | > |
| 696 | <DialogPopup> | 731 | <DialogPopup> |
| 697 | <DialogHeader> | 732 | <DialogHeader> |
| 698 | <DialogTitle>{DIALOG_TITLE_BY_ACTION[activeDialogAction ?? "commit"]}</DialogTitle> | 733 | <DialogTitle>{COMMIT_DIALOG_TITLE}</DialogTitle> |
| 699 | <DialogDescription> | 734 | <DialogDescription>{COMMIT_DIALOG_DESCRIPTION}</DialogDescription> |
| 700 | {DIALOG_DESCRIPTION_BY_ACTION[activeDialogAction ?? "commit"]} | | |
| 701 | </DialogDescription> | | |
| 702 | </DialogHeader> | 735 | </DialogHeader> |
| 703 | <DialogPanel className="space-y-4"> | 736 | <DialogPanel className="space-y-4"> |
| 704 | <div className="space-y-3 rounded-lg border border-input bg-muted/40 p-3 text-xs"> | 737 | <div className="space-y-3 rounded-lg border border-input bg-muted/40 p-3 text-xs"> |
| 705 | <div className="grid grid-cols-[auto_1fr] items-center gap-x-2 gap-y-1"> | 738 | <div className="grid grid-cols-[auto_1fr] items-center gap-x-2 gap-y-1"> |
| 706 | <span className="text-muted-foreground">Branch</span> | 739 | <span className="text-muted-foreground">Branch</span> |
| 707 | <span className="font-medium"> | 740 | <span className="flex items-center justify-between gap-2"> |
| 708 | {gitStatusForActions?.branch ?? "(detached HEAD)"} | 741 | <span className="font-medium">{gitStatusForActions?.branch ?? "(detached HEAD)"}</span> |
| | 742 | {isDefaultBranch && ( |
| | 743 | <span className="text-right text-warning text-xs">Warning: default branch</span> |
| | 744 | )} |
| 709 | </span> | 745 | </span> |
| 710 | {activeDialogAction !== "commit" && ( | | |
| 711 | <> | | |
| 712 | <span className="text-muted-foreground">Upstream</span> | | |
| 713 | <span className="font-medium"> | | |
| 714 | {!gitStatusForActions || !gitStatusForActions.hasUpstream | | |
| 715 | ? "No upstream configured" | | |
| 716 | : gitStatusForActions.aheadCount === 0 && | | |
| 717 | gitStatusForActions.behindCount === 0 | | |
| 718 | ? "Up to date" | | |
| 719 | : gitStatusForActions.aheadCount > 0 && | | |
| 720 | gitStatusForActions.behindCount > 0 | | |
| 721 | ? `Diverged (+${gitStatusForActions.aheadCount} / -${gitStatusForActions.behindCount})` | | |
| 722 | : gitStatusForActions.aheadCount > 0 | | |
| 723 | ? `Ahead by ${gitStatusForActions.aheadCount}` | | |
| 724 | : `Behind by ${gitStatusForActions.behindCount}`} | | |
| 725 | </span> | | |
| 726 | <span className="text-muted-foreground">Working tree</span> | | |
| 727 | <span className="font-medium"> | | |
| 728 | {!gitStatusForActions || !gitStatusForActions.hasWorkingTreeChanges | | |
| 729 | ? "Clean" | | |
| 730 | : `${gitStatusForActions.workingTree.files.length} file(s)`} | | |
| 731 | </span> | | |
| 732 | <span className="text-muted-foreground">Diff</span> | | |
| 733 | <span className="font-mono"> | | |
| 734 | {!gitStatusForActions || !gitStatusForActions.hasWorkingTreeChanges ? ( | | |
| 735 | "none" | | |
| 736 | ) : ( | | |
| 737 | <> | | |
| 738 | <span className="text-success"> | | |
| 739 | +{gitStatusForActions.workingTree.insertions} | | |
| 740 | </span> | | |
| 741 | <span className="text-muted-foreground"> / </span> | | |
| 742 | <span className="text-destructive"> | | |
| 743 | -{gitStatusForActions.workingTree.deletions} | | |
| 744 | </span> | | |
| 745 | </> | | |
| 746 | )} | | |
| 747 | </span> | | |
| 748 | </> | | |
| 749 | )} | | |
| 750 | </div> | 746 | </div> |
| 751 | <div className="space-y-1"> | 747 | <div className="space-y-1"> |
| 752 | <p className="text-muted-foreground">Files</p> | 748 | <p className="text-muted-foreground">Files</p> |
| ... | @@ -786,35 +782,32 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions | ... | @@ -786,35 +782,32 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 786 | )} | 782 | )} |
| 787 | </div> | 783 | </div> |
| 788 | </div> | 784 | </div> |
| 789 | {activeDialogAction === "commit" && ( | 785 | <div className="space-y-1"> |
| 790 | <div className="space-y-1"> | 786 | <p className="text-xs font-medium">Commit message (optional)</p> |
| 791 | <p className="text-xs font-medium">Commit message (optional)</p> | 787 | <Textarea |
| 792 | <Textarea | 788 | value={dialogCommitMessage} |
| 793 | value={dialogCommitMessage} | 789 | onChange={(event) => setDialogCommitMessage(event.target.value)} |
| 794 | onChange={(event) => setDialogCommitMessage(event.target.value)} | 790 | placeholder="Leave empty to auto-generate" |
| 795 | placeholder="Leave empty to auto-generate" | 791 | size="sm" |
| 796 | size="sm" | 792 | /> |
| 797 | /> | 793 | </div> |
| 798 | </div> | | |
| 799 | )} | | |
| 800 | </DialogPanel> | 794 | </DialogPanel> |
| 801 | <DialogFooter> | 795 | <DialogFooter> |
| 802 | <Button | 796 | <Button |
| 803 | variant="outline" | 797 | variant="outline" |
| 804 | size="sm" | 798 | size="sm" |
| 805 | onClick={() => { | 799 | onClick={() => { |
| 806 | setActiveDialogAction(null); | 800 | setIsCommitDialogOpen(false); |
| 807 | setDialogCommitMessage(""); | 801 | setDialogCommitMessage(""); |
| 808 | }} | 802 | }} |
| 809 | > | 803 | > |
| 810 | Cancel | 804 | Cancel |
| 811 | </Button> | 805 | </Button> |
| | 806 | <Button variant="outline" size="sm" onClick={runDialogActionOnNewBranch}> |
| | 807 | Commit on new branch |
| | 808 | </Button> |
| 812 | <Button size="sm" onClick={runDialogAction}> | 809 | <Button size="sm" onClick={runDialogAction}> |
| 813 | {activeDialogAction === "commit" | 810 | Commit |
| 814 | ? "Commit" | | |
| 815 | : activeDialogAction === "push" | | |
| 816 | ? "Push" | | |
| 817 | : "Create PR"} | | |
| 818 | </Button> | 811 | </Button> |
| 819 | </DialogFooter> | 812 | </DialogFooter> |
| 820 | </DialogPopup> | 813 | </DialogPopup> |