| @@ -1,5 +1,6 @@ |
| 1 | 1 | import { |
| 2 | 2 | PROVIDER_SEND_TURN_MAX_INPUT_CHARS, |
| 3 | + type GitRunStackedActionResult, |
| 3 | 4 | type GitStackedAction, |
| 4 | 5 | type GitStatusResult, |
| 5 | 6 | type ProviderApprovalDecision, |
| @@ -40,7 +41,16 @@ import BranchToolbar from "./BranchToolbar"; · 34 unmodified lines |
| 40 | 41 | import { isTerminalToggleShortcut } from "../terminal-shortcuts"; |
| 41 | 42 | import ChatMarkdown from "./ChatMarkdown"; |
| 42 | 43 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; |
| 43 | | −import { CloudUploadIcon, GitCommitIcon, GithubIcon } from "lucide-react"; |
| 44 | +import { |
| 45 | + CheckIcon, |
| 46 | + CircleIcon, |
| 47 | + CloudUploadIcon, |
| 48 | + GitCommitIcon, |
| 49 | + GithubIcon, |
| 50 | + Loader2Icon, |
| 51 | + MinusIcon, |
| 52 | + XIcon, |
| 53 | +} from "lucide-react"; |
| 44 | 54 | |
| 45 | 55 | function formatMessageMeta(createdAt: string, duration: string | null): string { |
| 46 | 56 | if (!duration) return formatTimestamp(createdAt); |
| @@ -81,6 +91,88 @@ function GitActionIcon(props: { icon: GitActionMenuItem["icon"]; disabled: boole · 34 unmodified lines |
| 81 | 91 | return <GithubIcon className={`h-5 w-5 shrink-0 ${toneClass}`} />; |
| 82 | 92 | } |
| 83 | 93 | |
| 94 | +type GitProgressStepStatus = |
| 95 | + | "pending" |
| 96 | + | "running" |
| 97 | + | "completed" |
| 98 | + | "skipped" |
| 99 | + | "failed"; |
| 100 | + |
| 101 | +interface GitProgressStep { |
| 102 | + id: "generate" | "commit" | "push" | "pr"; |
| 103 | + label: string; |
| 104 | + status: GitProgressStepStatus; |
| 105 | + detail?: string; |
| 106 | +} |
| 107 | + |
| 108 | +function gitActionModalTitle(action: GitStackedAction): string { |
| 109 | + if (action === "commit") return "Commit your changes"; |
| 110 | + if (action === "commit_push") return "Commit and push changes"; |
| 111 | + return "Commit, push and open PR"; |
| 112 | +} |
| 113 | + |
| 114 | +function initialGitProgressSteps( |
| 115 | + action: GitStackedAction, |
| 116 | + commitMessage: string, |
| 117 | +): GitProgressStep[] { |
| 118 | + const hasCustomMessage = commitMessage.trim().length > 0; |
| 119 | + const steps: GitProgressStep[] = []; |
| 120 | + |
| 121 | + if (!hasCustomMessage) { |
| 122 | + steps.push({ |
| 123 | + id: "generate", |
| 124 | + label: "Generate commit message", |
| 125 | + status: "pending", |
| 126 | + }); |
| 127 | + } |
| 128 | + |
| 129 | + steps.push({ |
| 130 | + id: "commit", |
| 131 | + label: "Commit changes", |
| 132 | + status: "pending", |
| 133 | + }); |
| 134 | + |
| 135 | + if (action !== "commit") { |
| 136 | + steps.push({ |
| 137 | + id: "push", |
| 138 | + label: "Push branch", |
| 139 | + status: "pending", |
| 140 | + }); |
| 141 | + } |
| 142 | + |
| 143 | + if (action === "commit_push_pr") { |
| 144 | + steps.push({ |
| 145 | + id: "pr", |
| 146 | + label: "Create or open PR", |
| 147 | + status: "pending", |
| 148 | + }); |
| 149 | + } |
| 150 | + |
| 151 | + return steps; |
| 152 | +} |
| 153 | + |
| 154 | +function updateProgressStep( |
| 155 | + steps: GitProgressStep[], |
| 156 | + id: GitProgressStep["id"], |
| 157 | + status: GitProgressStepStatus, |
| 158 | + detail?: string, |
| 159 | +): GitProgressStep[] { |
| 160 | + return steps.map((step) => { |
| 161 | + if (step.id !== id) return step; |
| 162 | + return { |
| 163 | + ...step, |
| 164 | + status, |
| 165 | + ...(detail ? { detail } : {}), |
| 166 | + }; |
| 167 | + }); |
| 168 | +} |
| 169 | + |
| 170 | +function runActionLabel(action: GitStackedAction): string { |
| 171 | + if (action === "commit") return "Commit"; |
| 172 | + if (action === "commit_push") return "Commit & Push"; |
| 173 | + return "Commit, Push & Open PR"; |
| 174 | +} |
| 175 | + |
| 84 | 176 | function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { |
| 85 | 177 | if (tone === "error") return "text-rose-300/50 dark:text-rose-300/50"; |
| 86 | 178 | if (tone === "tool") return "text-muted-foreground/70"; |
| @@ -166,6 +258,11 @@ export default function ChatView() { · 79 unmodified lines |
| 166 | 258 | const [isGitActionRunning, setIsGitActionRunning] = useState(false); |
| 167 | 259 | const [gitStatus, setGitStatus] = useState<GitStatusResult | null>(null); |
| 168 | 260 | const [gitActionError, setGitActionError] = useState<string | null>(null); |
| 261 | + const [gitModalAction, setGitModalAction] = useState<GitStackedAction | null>(null); |
| 262 | + const [gitModalCommitMessage, setGitModalCommitMessage] = useState(""); |
| 263 | + const [gitModalProgress, setGitModalProgress] = useState<GitProgressStep[]>([]); |
| 264 | + const [gitModalError, setGitModalError] = useState<string | null>(null); |
| 265 | + const [gitModalResult, setGitModalResult] = useState<GitRunStackedActionResult | null>(null); |
| 169 | 266 | const [lastEditor, setLastEditor] = useState<EditorId>(() => { |
| 170 | 267 | const stored = localStorage.getItem(LAST_EDITOR_KEY); |
| 171 | 268 | return EDITORS.some((e) => e.id === stored) ? (stored as EditorId) : EDITORS[0].id; |
| @@ -325,6 +422,16 @@ export default function ChatView() { · 153 unmodified lines |
| 325 | 422 | }, |
| 326 | 423 | ]; |
| 327 | 424 | }, [gitBaseDisabled, gitStatus]); |
| 425 | + const isGitModalOpen = gitModalAction !== null; |
| 426 | + const gitModalPreviewSteps = useMemo( |
| 427 | + () => |
| 428 | + gitModalAction |
| 429 | + ? initialGitProgressSteps(gitModalAction, gitModalCommitMessage) |
| 430 | + : ([] as GitProgressStep[]), |
| 431 | + [gitModalAction, gitModalCommitMessage], |
| 432 | + ); |
| 433 | + const gitModalSteps = |
| 434 | + gitModalProgress.length > 0 ? gitModalProgress : gitModalPreviewSteps; |
| 328 | 435 | |
| 329 | 436 | useEffect(() => { |
| 330 | 437 | latestGitCwdRef.current = gitCwd; |
| @@ -343,34 +450,161 @@ export default function ChatView() { · 12 unmodified lines |
| 343 | 450 | setGitActionError(null); |
| 344 | 451 | }, [api, gitCwd]); |
| 345 | 452 | |
| 346 | | − const runGitAction = useCallback( |
| 347 | | − async (action: GitStackedAction) => { |
| 348 | | − if (!api || !gitCwd) return; |
| 349 | | − const actionCwd = gitCwd; |
| 453 | + const openGitActionModal = useCallback((action: GitStackedAction) => { |
| 454 | + setIsGitMenuOpen(false); |
| 455 | + setGitModalAction(action); |
| 456 | + setGitModalCommitMessage(""); |
| 457 | + setGitModalProgress([]); |
| 458 | + setGitModalError(null); |
| 459 | + setGitModalResult(null); |
| 460 | + setGitActionError(null); |
| 461 | + }, []); |
| 350 | 462 | |
| 351 | | − setIsGitMenuOpen(false); |
| 352 | | − setIsGitActionRunning(true); |
| 353 | | − setGitActionError(null); |
| 354 | | − try { |
| 355 | | − await api.git.runStackedAction({ |
| 463 | + const closeGitActionModal = useCallback(() => { |
| 464 | + if (isGitActionRunning) return; |
| 465 | + setGitModalAction(null); |
| 466 | + setGitModalCommitMessage(""); |
| 467 | + setGitModalProgress([]); |
| 468 | + setGitModalError(null); |
| 469 | + setGitModalResult(null); |
| 470 | + }, [isGitActionRunning]); |
| 471 | + |
| 472 | + const runGitAction = useCallback(async () => { |
| 473 | + if (!api || !gitCwd || !gitModalAction) return; |
| 474 | + const actionCwd = gitCwd; |
| 475 | + const action = gitModalAction; |
| 476 | + const commitMessage = gitModalCommitMessage.trim(); |
| 477 | + const includeGeneratedCommitMessage = commitMessage.length === 0; |
| 478 | + |
| 479 | + setIsGitActionRunning(true); |
| 480 | + setGitModalError(null); |
| 481 | + setGitActionError(null); |
| 482 | + setGitModalResult(null); |
| 483 | + setGitModalProgress(initialGitProgressSteps(action, commitMessage)); |
| 484 | + |
| 485 | + let commit: GitRunStackedActionResult["commit"] = { |
| 486 | + status: "skipped_no_changes", |
| 487 | + }; |
| 488 | + let push: GitRunStackedActionResult["push"] = { |
| 489 | + status: "skipped_not_requested", |
| 490 | + }; |
| 491 | + let pr: GitRunStackedActionResult["pr"] = { |
| 492 | + status: "skipped_not_requested", |
| 493 | + }; |
| 494 | + |
| 495 | + const updateStep = ( |
| 496 | + id: GitProgressStep["id"], |
| 497 | + status: GitProgressStepStatus, |
| 498 | + detail?: string, |
| 499 | + ) => { |
| 500 | + setGitModalProgress((steps) => updateProgressStep(steps, id, status, detail)); |
| 501 | + }; |
| 502 | + |
| 503 | + try { |
| 504 | + if (includeGeneratedCommitMessage) { |
| 505 | + updateStep("generate", "running"); |
| 506 | + } else { |
| 507 | + updateStep("commit", "running"); |
| 508 | + } |
| 509 | + |
| 510 | + const commitRun = await api.git.runStackedAction({ |
| 511 | + cwd: actionCwd, |
| 512 | + action: "commit", |
| 513 | + ...(commitMessage.length > 0 ? { commitMessage } : {}), |
| 514 | + }); |
| 515 | + commit = commitRun.commit; |
| 516 | + |
| 517 | + if (includeGeneratedCommitMessage) { |
| 518 | + if (commitRun.commit.status === "created") { |
| 519 | + updateStep( |
| 520 | + "generate", |
| 521 | + "completed", |
| 522 | + commitRun.commit.subject |
| 523 | + ? `Generated: ${commitRun.commit.subject}` |
| 524 | + : "Generated commit message.", |
| 525 | + ); |
| 526 | + } else { |
| 527 | + updateStep("generate", "skipped", "No local changes to commit."); |
| 528 | + } |
| 529 | + } |
| 530 | + |
| 531 | + if (commitRun.commit.status === "created") { |
| 532 | + updateStep( |
| 533 | + "commit", |
| 534 | + "completed", |
| 535 | + commitRun.commit.subject ?? "Committed local changes.", |
| 536 | + ); |
| 537 | + } else { |
| 538 | + updateStep("commit", "skipped", "No local changes to commit."); |
| 539 | + } |
| 540 | + |
| 541 | + if (action !== "commit") { |
| 542 | + updateStep("push", "running"); |
| 543 | + const pushRun = await api.git.runStackedAction({ |
| 356 | 544 | cwd: actionCwd, |
| 357 | | − action, |
| 545 | + action: "commit_push", |
| 358 | 546 | }); |
| 359 | | − } catch (error) { |
| 360 | | − setGitActionError(error instanceof Error ? error.message : "Git action failed."); |
| 361 | | − } finally { |
| 362 | | − setIsGitActionRunning(false); |
| 363 | | − try { |
| 364 | | − if (latestGitCwdRef.current === actionCwd) { |
| 365 | | − await refreshGitStatus(); |
| 366 | | − } |
| 367 | | − } catch { |
| 368 | | − setGitStatus(null); |
| 547 | + push = pushRun.push; |
| 548 | + if (pushRun.push.status === "pushed") { |
| 549 | + updateStep( |
| 550 | + "push", |
| 551 | + "completed", |
| 552 | + pushRun.push.upstreamBranch |
| 553 | + ? `Pushed to ${pushRun.push.upstreamBranch}.` |
| 554 | + : "Pushed latest commits.", |
| 555 | + ); |
| 556 | + } else { |
| 557 | + updateStep("push", "skipped", "Branch already up to date."); |
| 369 | 558 | } |
| 370 | 559 | } |
| 371 | | − }, |
| 372 | | − [api, gitCwd, refreshGitStatus], |
| 373 | | − ); |
| 560 | + |
| 561 | + if (action === "commit_push_pr") { |
| 562 | + updateStep("pr", "running"); |
| 563 | + const prRun = await api.git.runStackedAction({ |
| 564 | + cwd: actionCwd, |
| 565 | + action: "commit_push_pr", |
| 566 | + }); |
| 567 | + pr = prRun.pr; |
| 568 | + if (prRun.pr.status === "opened_existing") { |
| 569 | + updateStep( |
| 570 | + "pr", |
| 571 | + "completed", |
| 572 | + prRun.pr.number |
| 573 | + ? `Opened existing PR #${prRun.pr.number}.` |
| 574 | + : "Opened existing PR.", |
| 575 | + ); |
| 576 | + } else if (prRun.pr.status === "created") { |
| 577 | + updateStep( |
| 578 | + "pr", |
| 579 | + "completed", |
| 580 | + prRun.pr.number ? `Created PR #${prRun.pr.number}.` : "Created PR.", |
| 581 | + ); |
| 582 | + } else { |
| 583 | + updateStep("pr", "skipped", "PR step was not requested."); |
| 584 | + } |
| 585 | + } |
| 586 | + |
| 587 | + setGitModalResult({ action, commit, push, pr }); |
| 588 | + } catch (error) { |
| 589 | + const message = error instanceof Error ? error.message : "Git action failed."; |
| 590 | + setGitModalError(message); |
| 591 | + setGitActionError(message); |
| 592 | + setGitModalProgress((steps) => { |
| 593 | + const active = steps.find((step) => step.status === "running"); |
| 594 | + if (!active) return steps; |
| 595 | + return updateProgressStep(steps, active.id, "failed", message); |
| 596 | + }); |
| 597 | + } finally { |
| 598 | + setIsGitActionRunning(false); |
| 599 | + try { |
| 600 | + if (latestGitCwdRef.current === actionCwd) { |
| 601 | + await refreshGitStatus(); |
| 602 | + } |
| 603 | + } catch { |
| 604 | + setGitStatus(null); |
| 605 | + } |
| 606 | + } |
| 607 | + }, [api, gitCwd, gitModalAction, gitModalCommitMessage, refreshGitStatus]); |
| 374 | 608 | |
| 375 | 609 | const envLocked = Boolean( |
| 376 | 610 | activeThread && |
| @@ -436,6 +670,11 @@ export default function ChatView() { · 59 unmodified lines |
| 436 | 670 | |
| 437 | 671 | useEffect(() => { |
| 438 | 672 | setGitActionError(null); |
| 673 | + setGitModalError(null); |
| 674 | + setGitModalAction(null); |
| 675 | + setGitModalCommitMessage(""); |
| 676 | + setGitModalProgress([]); |
| 677 | + setGitModalResult(null); |
| 439 | 678 | }, [activeProject?.id, activeWorktreePath]); |
| 440 | 679 | |
| 441 | 680 | useEffect(() => { |
| @@ -537,6 +776,21 @@ export default function ChatView() { · 95 unmodified lines |
| 537 | 776 | }; |
| 538 | 777 | }, [isGitMenuOpen]); |
| 539 | 778 | |
| 779 | + useEffect(() => { |
| 780 | + if (!isGitModalOpen) return; |
| 781 | + |
| 782 | + const handleEscape = (event: globalThis.KeyboardEvent) => { |
| 783 | + if (event.key !== "Escape") return; |
| 784 | + if (isGitActionRunning) return; |
| 785 | + closeGitActionModal(); |
| 786 | + }; |
| 787 | + |
| 788 | + window.addEventListener("keydown", handleEscape); |
| 789 | + return () => { |
| 790 | + window.removeEventListener("keydown", handleEscape); |
| 791 | + }; |
| 792 | + }, [closeGitActionModal, isGitActionRunning, isGitModalOpen]); |
| 793 | + |
| 540 | 794 | // Cmd+O / Ctrl+O to open in last-used editor |
| 541 | 795 | useEffect(() => { |
| 542 | 796 | const handler = (e: globalThis.KeyboardEvent) => { |
| @@ -882,7 +1136,7 @@ export default function ChatView() { · 339 unmodified lines |
| 882 | 1136 | className="mb-1.5 flex w-full items-center gap-3 rounded-2xl px-3 py-2.5 text-left text-[14px] text-foreground transition-colors duration-150 hover:bg-accent disabled:cursor-not-allowed disabled:text-muted-foreground/65" |
| 883 | 1137 | disabled={item.disabled} |
| 884 | 1138 | onClick={() => { |
| 885 | | − void runGitAction(item.id); |
| 1139 | + openGitActionModal(item.id); |
| 886 | 1140 | }} |
| 887 | 1141 | > |
| 888 | 1142 | <GitActionIcon icon={item.icon} disabled={item.disabled} /> |
| @@ -941,12 +1195,6 @@ export default function ChatView() { · 52 unmodified lines |
| 941 | 1195 | </div> |
| 942 | 1196 | )} |
| 943 | 1197 | |
| 944 | | − {gitActionError && ( |
| 945 | | − <div className="mx-4 mt-3 rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-800 dark:border-rose-400/20 dark:bg-rose-900/20 dark:text-rose-200"> |
| 946 | | − {gitActionError} |
| 947 | | − </div> |
| 948 | | − )} |
| 949 | | − |
| 950 | 1198 | {pendingApprovals.length > 0 && ( |
| 951 | 1199 | <div className="mx-4 mt-3 space-y-2"> |
| 952 | 1200 | {pendingApprovals.map((approval) => { |
| @@ -1430,6 +1678,180 @@ export default function ChatView() { · 477 unmodified lines |
| 1430 | 1678 | </form> |
| 1431 | 1679 | </div> |
| 1432 | 1680 | |
| 1681 | + {isGitModalOpen && gitModalAction && ( |
| 1682 | + <div |
| 1683 | + className="fixed inset-0 z-[80] flex items-center justify-center bg-black/35 px-4 py-6" |
| 1684 | + onMouseDown={() => { |
| 1685 | + closeGitActionModal(); |
| 1686 | + }} |
| 1687 | + > |
| 1688 | + <div |
| 1689 | + className="w-full max-w-[640px] rounded-3xl border border-border bg-popover p-6 shadow-2xl" |
| 1690 | + role="dialog" |
| 1691 | + aria-modal="true" |
| 1692 | + aria-label="Git action confirmation" |
| 1693 | + onMouseDown={(event) => { |
| 1694 | + event.stopPropagation(); |
| 1695 | + }} |
| 1696 | + > |
| 1697 | + <div className="flex items-start justify-between gap-4"> |
| 1698 | + <div className="flex items-center gap-3"> |
| 1699 | + <div className="rounded-2xl bg-accent p-3"> |
| 1700 | + <GitActionIcon |
| 1701 | + icon={ |
| 1702 | + gitModalAction === "commit" |
| 1703 | + ? "commit" |
| 1704 | + : gitModalAction === "commit_push" |
| 1705 | + ? "push" |
| 1706 | + : "pr" |
| 1707 | + } |
| 1708 | + disabled={false} |
| 1709 | + /> |
| 1710 | + </div> |
| 1711 | + <div> |
| 1712 | + <p className="text-xs uppercase tracking-[0.14em] text-muted-foreground/70"> |
| 1713 | + Git actions |
| 1714 | + </p> |
| 1715 | + <h3 className="text-3xl font-semibold tracking-tight text-foreground"> |
| 1716 | + {gitActionModalTitle(gitModalAction)} |
| 1717 | + </h3> |
| 1718 | + </div> |
| 1719 | + </div> |
| 1720 | + <button |
| 1721 | + type="button" |
| 1722 | + className="rounded-md p-1 text-muted-foreground/60 transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50" |
| 1723 | + onClick={closeGitActionModal} |
| 1724 | + disabled={isGitActionRunning} |
| 1725 | + aria-label="Close git action dialog" |
| 1726 | + > |
| 1727 | + <XIcon className="h-5 w-5" /> |
| 1728 | + </button> |
| 1729 | + </div> |
| 1730 | + |
| 1731 | + <div className="mt-6 space-y-2 rounded-2xl border border-border/80 bg-card/40 px-4 py-3"> |
| 1732 | + <div className="flex items-center justify-between gap-4 text-sm"> |
| 1733 | + <span className="text-muted-foreground/70">Branch</span> |
| 1734 | + <span className="font-mono text-foreground"> |
| 1735 | + {gitStatus?.branch ?? "(detached HEAD)"} |
| 1736 | + </span> |
| 1737 | + </div> |
| 1738 | + <div className="flex items-center justify-between gap-4 text-sm"> |
| 1739 | + <span className="text-muted-foreground/70">Changes</span> |
| 1740 | + <span className="text-foreground"> |
| 1741 | + {gitStatus?.hasWorkingTreeChanges ? "Working tree has changes" : "No local changes"} |
| 1742 | + </span> |
| 1743 | + </div> |
| 1744 | + </div> |
| 1745 | + |
| 1746 | + <div className="mt-6"> |
| 1747 | + <label |
| 1748 | + htmlFor="git-commit-message" |
| 1749 | + className="mb-2 block text-sm font-medium text-foreground" |
| 1750 | + > |
| 1751 | + Commit message |
| 1752 | + </label> |
| 1753 | + <textarea |
| 1754 | + id="git-commit-message" |
| 1755 | + rows={3} |
| 1756 | + className="w-full resize-none rounded-2xl border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground/50 focus:border-ring focus:outline-none disabled:cursor-not-allowed disabled:opacity-70" |
| 1757 | + placeholder="Leave blank to autogenerate a commit message" |
| 1758 | + value={gitModalCommitMessage} |
| 1759 | + onChange={(event) => setGitModalCommitMessage(event.target.value)} |
| 1760 | + disabled={isGitActionRunning || gitModalResult !== null} |
| 1761 | + /> |
| 1762 | + <p className="mt-1.5 text-xs text-muted-foreground/65"> |
| 1763 | + Leave this empty to use AI-generated commit text. |
| 1764 | + </p> |
| 1765 | + </div> |
| 1766 | + |
| 1767 | + <div className="mt-6"> |
| 1768 | + <p className="text-sm font-medium text-foreground">Next steps</p> |
| 1769 | + <div className="mt-2 overflow-hidden rounded-2xl border border-border"> |
| 1770 | + {gitModalSteps.map((step, index) => { |
| 1771 | + const borderClass = index < gitModalSteps.length - 1 ? "border-b border-border/70" : ""; |
| 1772 | + const statusTextClass = |
| 1773 | + step.status === "failed" |
| 1774 | + ? "text-rose-500 dark:text-rose-300" |
| 1775 | + : step.status === "completed" |
| 1776 | + ? "text-emerald-600 dark:text-emerald-300" |
| 1777 | + : "text-muted-foreground/70"; |
| 1778 | + |
| 1779 | + return ( |
| 1780 | + <div |
| 1781 | + key={step.id} |
| 1782 | + className={`flex items-start gap-3 bg-card/45 px-4 py-3 ${borderClass}`} |
| 1783 | + > |
| 1784 | + <span className="mt-0.5"> |
| 1785 | + {step.status === "running" ? ( |
| 1786 | + <Loader2Icon className="h-4 w-4 animate-spin text-foreground" /> |
| 1787 | + ) : step.status === "completed" ? ( |
| 1788 | + <CheckIcon className="h-4 w-4 text-emerald-600 dark:text-emerald-300" /> |
| 1789 | + ) : step.status === "skipped" ? ( |
| 1790 | + <MinusIcon className="h-4 w-4 text-muted-foreground/70" /> |
| 1791 | + ) : step.status === "failed" ? ( |
| 1792 | + <XIcon className="h-4 w-4 text-rose-500 dark:text-rose-300" /> |
| 1793 | + ) : ( |
| 1794 | + <CircleIcon className="h-4 w-4 text-muted-foreground/60" /> |
| 1795 | + )} |
| 1796 | + </span> |
| 1797 | + <div className="min-w-0"> |
| 1798 | + <p className="text-sm text-foreground">{step.label}</p> |
| 1799 | + {step.detail && ( |
| 1800 | + <p className={`mt-0.5 text-xs ${statusTextClass}`}>{step.detail}</p> |
| 1801 | + )} |
| 1802 | + </div> |
| 1803 | + </div> |
| 1804 | + ); |
| 1805 | + })} |
| 1806 | + </div> |
| 1807 | + </div> |
| 1808 | + |
| 1809 | + {(gitModalError ?? gitActionError) && ( |
| 1810 | + <div className="mt-4 rounded-lg border border-rose-300/50 bg-rose-500/10 px-3 py-2 text-xs text-rose-600 dark:text-rose-200"> |
| 1811 | + {gitModalError ?? gitActionError} |
| 1812 | + </div> |
| 1813 | + )} |
| 1814 | + |
| 1815 | + {gitModalResult?.pr.url && ( |
| 1816 | + <div className="mt-4 rounded-lg border border-border bg-card/40 px-3 py-2 text-xs text-muted-foreground/80"> |
| 1817 | + PR:{" "} |
| 1818 | + <a |
| 1819 | + href={gitModalResult.pr.url} |
| 1820 | + target="_blank" |
| 1821 | + rel="noreferrer" |
| 1822 | + className="text-foreground underline underline-offset-2" |
| 1823 | + > |
| 1824 | + {gitModalResult.pr.url} |
| 1825 | + </a> |
| 1826 | + </div> |
| 1827 | + )} |
| 1828 | + |
| 1829 | + <div className="mt-6 flex justify-end gap-2"> |
| 1830 | + <button |
| 1831 | + type="button" |
| 1832 | + className="rounded-xl border border-border px-4 py-2 text-sm text-foreground transition-colors duration-150 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-60" |
| 1833 | + onClick={closeGitActionModal} |
| 1834 | + disabled={isGitActionRunning} |
| 1835 | + > |
| 1836 | + {gitModalResult ? "Done" : "Cancel"} |
| 1837 | + </button> |
| 1838 | + {!gitModalResult && ( |
| 1839 | + <button |
| 1840 | + type="button" |
| 1841 | + className="rounded-xl bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors duration-150 hover:bg-foreground/90 disabled:cursor-not-allowed disabled:opacity-60" |
| 1842 | + onClick={() => { |
| 1843 | + void runGitAction(); |
| 1844 | + }} |
| 1845 | + disabled={isGitActionRunning} |
| 1846 | + > |
| 1847 | + {isGitActionRunning ? "Running..." : runActionLabel(gitModalAction)} |
| 1848 | + </button> |
| 1849 | + )} |
| 1850 | + </div> |
| 1851 | + </div> |
| 1852 | + </div> |
| 1853 | + )} |
| 1854 | + |
| 1433 | 1855 | <BranchToolbar envMode={envMode} onEnvModeChange={setEnvMode} envLocked={envLocked} /> |
| 1434 | 1856 | |
| 1435 | 1857 | {activeThread.terminalOpen && api && activeProject && ( |