| ... | ... | @@ -8,7 +8,6 @@ import { |
| 8 | 8 | buildMenuItems, |
| 9 | 9 | type GitActionIconName, |
| 10 | 10 | type GitActionMenuItem, |
| 11 | | type GitDialogAction, |
| 12 | 11 | type GitQuickAction, |
| 13 | 12 | requiresDefaultBranchConfirmation, |
| 14 | 13 | resolveAutoFeatureBranchName, |
| ... | ... | @@ -88,6 +87,9 @@ function getMenuActionDisabledReason( |
| 88 | 87 | if (isBehind) { |
| 89 | 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 | 93 | if (!isAhead) { |
| 92 | 94 | return "No local commits to push."; |
| 93 | 95 | } |
| ... | ... | @@ -103,29 +105,21 @@ function getMenuActionDisabledReason( |
| 103 | 105 | if (hasChanges) { |
| 104 | 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 | 108 | if (!gitStatus.hasUpstream) { |
| 110 | 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 | 114 | if (isBehind) { |
| 113 | 115 | return "Branch is behind upstream. Pull/rebase before creating a PR."; |
| 114 | 116 | } |
| 115 | 117 | return "Create PR is currently unavailable."; |
| 116 | 118 | } |
| 117 | 119 | |
| 118 | | const DIALOG_TITLE_BY_ACTION = { |
| 119 | | commit: "Commit changes", |
| 120 | | push: "Push branch", |
| 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 | | }; |
| 120 | const COMMIT_DIALOG_TITLE = "Commit changes"; |
| 121 | const COMMIT_DIALOG_DESCRIPTION = |
| 122 | "Review and confirm your commit. Leave the message blank to auto-generate one."; |
| 129 | 123 | |
| 130 | 124 | const DEFAULT_BRANCH_ACTION_DESCRIPTION: Record<GitStackedAction, string> = { |
| 131 | 125 | commit: "commit changes", |
| ... | ... | @@ -158,7 +152,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 158 | 152 | [activeThreadId], |
| 159 | 153 | ); |
| 160 | 154 | const queryClient = useQueryClient(); |
| 161 | | const [activeDialogAction, setActiveDialogAction] = useState<GitDialogAction | null>(null); |
| 155 | const [isCommitDialogOpen, setIsCommitDialogOpen] = useState(false); |
| 162 | 156 | const [dialogCommitMessage, setDialogCommitMessage] = useState(""); |
| 163 | 157 | const [pendingDefaultBranchAction, setPendingDefaultBranchAction] = |
| 164 | 158 | useState<PendingDefaultBranchAction | null>(null); |
| ... | ... | @@ -376,7 +370,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 376 | 370 | children: "Create PR", |
| 377 | 371 | onClick: () => { |
| 378 | 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 | 390 | [ |
| 397 | 391 | isDefaultBranch, |
| 398 | 392 | runImmediateGitActionMutation, |
| 399 | | setActiveDialogAction, |
| 400 | 393 | setPendingDefaultBranchAction, |
| 401 | 394 | threadToastData, |
| 402 | 395 | gitStatusForActions, |
| ... | ... | @@ -455,6 +448,54 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 455 | 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 | 499 | const runQuickAction = useCallback(() => { |
| 459 | 500 | if (quickAction.kind === "open_pr") { |
| 460 | 501 | void openExistingPr(); |
| ... | ... | @@ -506,30 +547,24 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 506 | 547 | void runGitActionWithToast({ action: "commit_push", forcePushOnlyProgress: true }); |
| 507 | 548 | return; |
| 508 | 549 | } |
| 509 | | if (item.dialogAction) { |
| 510 | | setActiveDialogAction(item.dialogAction); |
| 550 | if (item.dialogAction === "create_pr") { |
| 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 | 559 | const runDialogAction = useCallback(() => { |
| 517 | | if (!activeDialogAction) return; |
| 518 | | const action: GitStackedAction = |
| 519 | | activeDialogAction === "commit" |
| 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); |
| 560 | if (!isCommitDialogOpen) return; |
| 561 | const commitMessage = dialogCommitMessage.trim(); |
| 562 | setIsCommitDialogOpen(false); |
| 527 | 563 | void runGitActionWithToast({ |
| 528 | | action, |
| 564 | action: "commit", |
| 529 | 565 | ...(commitMessage ? { commitMessage } : {}), |
| 530 | | ...(forcePushOnlyProgress ? { forcePushOnlyProgress } : {}), |
| 531 | 566 | }); |
| 532 | | }, [activeDialogAction, dialogCommitMessage, runGitActionWithToast]); |
| 567 | }, [dialogCommitMessage, isCommitDialogOpen, runGitActionWithToast, setIsCommitDialogOpen]); |
| 533 | 568 | |
| 534 | 569 | const openChangedFileInEditor = useCallback( |
| 535 | 570 | (filePath: string) => { |
| ... | ... | @@ -685,68 +720,29 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 685 | 720 | )} |
| 686 | 721 | |
| 687 | 722 | <Dialog |
| 688 | | open={activeDialogAction !== null} |
| 723 | open={isCommitDialogOpen} |
| 689 | 724 | onOpenChange={(open) => { |
| 690 | 725 | if (!open) { |
| 691 | | setActiveDialogAction(null); |
| 726 | setIsCommitDialogOpen(false); |
| 692 | 727 | setDialogCommitMessage(""); |
| 693 | 728 | } |
| 694 | 729 | }} |
| 695 | 730 | > |
| 696 | 731 | <DialogPopup> |
| 697 | 732 | <DialogHeader> |
| 698 | | <DialogTitle>{DIALOG_TITLE_BY_ACTION[activeDialogAction ?? "commit"]}</DialogTitle> |
| 699 | | <DialogDescription> |
| 700 | | {DIALOG_DESCRIPTION_BY_ACTION[activeDialogAction ?? "commit"]} |
| 701 | | </DialogDescription> |
| 733 | <DialogTitle>{COMMIT_DIALOG_TITLE}</DialogTitle> |
| 734 | <DialogDescription>{COMMIT_DIALOG_DESCRIPTION}</DialogDescription> |
| 702 | 735 | </DialogHeader> |
| 703 | 736 | <DialogPanel className="space-y-4"> |
| 704 | 737 | <div className="space-y-3 rounded-lg border border-input bg-muted/40 p-3 text-xs"> |
| 705 | 738 | <div className="grid grid-cols-[auto_1fr] items-center gap-x-2 gap-y-1"> |
| 706 | 739 | <span className="text-muted-foreground">Branch</span> |
| 707 | | <span className="font-medium"> |
| 708 | | {gitStatusForActions?.branch ?? "(detached HEAD)"} |
| 740 | <span className="flex items-center justify-between gap-2"> |
| 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 | 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 | 746 | </div> |
| 751 | 747 | <div className="space-y-1"> |
| 752 | 748 | <p className="text-muted-foreground">Files</p> |
| ... | ... | @@ -786,35 +782,32 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions |
| 786 | 782 | )} |
| 787 | 783 | </div> |
| 788 | 784 | </div> |
| 789 | | {activeDialogAction === "commit" && ( |
| 790 | | <div className="space-y-1"> |
| 791 | | <p className="text-xs font-medium">Commit message (optional)</p> |
| 792 | | <Textarea |
| 793 | | value={dialogCommitMessage} |
| 794 | | onChange={(event) => setDialogCommitMessage(event.target.value)} |
| 795 | | placeholder="Leave empty to auto-generate" |
| 796 | | size="sm" |
| 797 | | /> |
| 798 | | </div> |
| 799 | | )} |
| 785 | <div className="space-y-1"> |
| 786 | <p className="text-xs font-medium">Commit message (optional)</p> |
| 787 | <Textarea |
| 788 | value={dialogCommitMessage} |
| 789 | onChange={(event) => setDialogCommitMessage(event.target.value)} |
| 790 | placeholder="Leave empty to auto-generate" |
| 791 | size="sm" |
| 792 | /> |
| 793 | </div> |
| 800 | 794 | </DialogPanel> |
| 801 | 795 | <DialogFooter> |
| 802 | 796 | <Button |
| 803 | 797 | variant="outline" |
| 804 | 798 | size="sm" |
| 805 | 799 | onClick={() => { |
| 806 | | setActiveDialogAction(null); |
| 800 | setIsCommitDialogOpen(false); |
| 807 | 801 | setDialogCommitMessage(""); |
| 808 | 802 | }} |
| 809 | 803 | > |
| 810 | 804 | Cancel |
| 811 | 805 | </Button> |
| 806 | <Button variant="outline" size="sm" onClick={runDialogActionOnNewBranch}> |
| 807 | Commit on new branch |
| 808 | </Button> |
| 812 | 809 | <Button size="sm" onClick={runDialogAction}> |
| 813 | | {activeDialogAction === "commit" |
| 814 | | ? "Commit" |
| 815 | | : activeDialogAction === "push" |
| 816 | | ? "Push" |
| 817 | | : "Create PR"} |
| 810 | Commit |
| 818 | 811 | </Button> |
| 819 | 812 | </DialogFooter> |
| 820 | 813 | </DialogPopup> |