diff --git a/src/renderer/hooks/usePrWriteActions.ts b/src/renderer/hooks/usePrWriteActions.ts index 9e856884..d8042521 100644 --- a/src/renderer/hooks/usePrWriteActions.ts +++ b/src/renderer/hooks/usePrWriteActions.ts @@ -14,8 +14,12 @@ export interface UsePrWriteActionsArgs { onRefresh: () => void; } +/** Which PR write action is currently in flight, so only its button shows a spinner. */ +export type PrWriteAction = "merge" | "close" | "ready" | "update"; + export interface UsePrWriteActionsResult { prLoading: boolean; + pendingAction: PrWriteAction | null; handleMergePr: (method: "merge" | "squash" | "rebase", admin?: boolean) => Promise; handleClosePr: () => Promise; handleMarkPrReady: () => Promise; @@ -30,7 +34,8 @@ export interface UsePrWriteActionsResult { */ export function usePrWriteActions(args: UsePrWriteActionsArgs): UsePrWriteActionsResult { const { projectLocation, localSyncLocation, prKey, onRefresh } = args; - const [prLoading, setPrLoading] = useState(false); + const [pendingAction, setPendingAction] = useState(null); + const prLoading = pendingAction !== null; function getCurrentPrData() { if (!prKey) return null; @@ -43,7 +48,7 @@ export function usePrWriteActions(args: UsePrWriteActionsArgs): UsePrWriteAction ): Promise { const prData = getCurrentPrData(); if (!prData) return; - setPrLoading(true); + setPendingAction("merge"); try { await readBridge().ghMergePr({ projectLocation, @@ -72,14 +77,14 @@ export function usePrWriteActions(args: UsePrWriteActionsArgs): UsePrWriteAction toast.danger(message); } } finally { - setPrLoading(false); + setPendingAction(null); } } async function handleClosePr(): Promise { const prData = getCurrentPrData(); if (!prData) return; - setPrLoading(true); + setPendingAction("close"); try { await readBridge().ghClosePr({ projectLocation, prNumber: prData.number }); if (prKey) { @@ -89,14 +94,14 @@ export function usePrWriteActions(args: UsePrWriteActionsArgs): UsePrWriteAction console.error("[git] close PR failed", err); toast.danger(friendlyError(err)); } finally { - setPrLoading(false); + setPendingAction(null); } } async function handleMarkPrReady(): Promise { const prData = getCurrentPrData(); if (!prData) return; - setPrLoading(true); + setPendingAction("ready"); try { await readBridge().ghMarkPrReady({ projectLocation, prNumber: prData.number }); if (prKey) { @@ -107,14 +112,14 @@ export function usePrWriteActions(args: UsePrWriteActionsArgs): UsePrWriteAction console.error("[git] mark PR ready failed", err); toast.danger(friendlyError(err)); } finally { - setPrLoading(false); + setPendingAction(null); } } async function handleUpdatePrBranch(rebase = false): Promise { const prData = getCurrentPrData(); if (!prData) return; - setPrLoading(true); + setPendingAction("update"); try { await readBridge().ghUpdatePrBranch({ projectLocation, @@ -132,9 +137,16 @@ export function usePrWriteActions(args: UsePrWriteActionsArgs): UsePrWriteAction console.error("[git] update PR branch failed", err); toast.danger(friendlyError(err)); } finally { - setPrLoading(false); + setPendingAction(null); } } - return { prLoading, handleMergePr, handleClosePr, handleMarkPrReady, handleUpdatePrBranch }; + return { + prLoading, + pendingAction, + handleMergePr, + handleClosePr, + handleMarkPrReady, + handleUpdatePrBranch, + }; } diff --git a/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/GitReviewSidebar.tsx b/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/GitReviewSidebar.tsx index f137f050..22800f4a 100644 --- a/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/GitReviewSidebar.tsx +++ b/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/GitReviewSidebar.tsx @@ -150,6 +150,7 @@ export function GitReviewSidebar(props: { prTargetBranch, setPrTargetBranch, prLoading, + prPendingAction, isGeneratingPr, handleCommit, handleGenerateMessage, @@ -463,6 +464,7 @@ export function GitReviewSidebar(props: { projectId={project.id} worktreePath={worktreePath} prLoading={prLoading} + pendingAction={prPendingAction} handleMergePr={handleMergePr} handleClosePr={handleClosePr} handleMarkPrReady={handleMarkPrReady} diff --git a/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/parts/PrSection.tsx b/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/parts/PrSection.tsx index 977ae96d..7fd87f08 100644 --- a/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/parts/PrSection.tsx +++ b/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/parts/PrSection.tsx @@ -21,6 +21,7 @@ import { } from "@heroui/react"; import { readBridge } from "@/renderer/bridge"; import { PixelLoader } from "@/renderer/components/common"; +import type { PrWriteAction } from "@/renderer/hooks/usePrWriteActions"; import { usePrMergeStateStatus, usePrMergeable, @@ -47,6 +48,8 @@ export function PrSection(props: { projectId: string; worktreePath?: string | undefined; prLoading: boolean; + /** Which write action is in flight, so only its button spins (others stay disabled). */ + pendingAction?: PrWriteAction | null | undefined; handleMergePr: (method: "merge" | "squash" | "rebase", admin?: boolean) => Promise; handleClosePr: () => Promise; handleMarkPrReady: () => Promise; @@ -57,6 +60,7 @@ export function PrSection(props: { projectId, worktreePath, prLoading, + pendingAction, handleMergePr, handleClosePr, handleMarkPrReady, @@ -133,7 +137,7 @@ export function PrSection(props: { variant="tertiary" className="flex-1" isDisabled={prLoading} - isPending={prLoading} + isPending={pendingAction === "ready"} onPress={() => void handleMarkPrReady()} > {({ isPending }) => ( @@ -204,7 +208,7 @@ export function PrSection(props: { variant="tertiary" className="flex-1" isDisabled={prLoading} - isPending={prLoading} + isPending={pendingAction === "update"} onPress={() => void handleUpdatePrBranch(false)} > {({ isPending }) => ( @@ -248,7 +252,7 @@ export function PrSection(props: { variant="tertiary" className="flex-1" isDisabled={prLoading || (isBlocked && !bypass)} - isPending={prLoading} + isPending={pendingAction === "merge"} onPress={() => void handleMergePr("squash", bypass)} > {({ isPending }) => ( diff --git a/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/parts/useGitReviewActions.ts b/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/parts/useGitReviewActions.ts index e3ac0a7b..46d0943c 100644 --- a/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/parts/useGitReviewActions.ts +++ b/src/renderer/views/GitReviewOverlay/parts/GitReviewSidebar/parts/useGitReviewActions.ts @@ -593,6 +593,7 @@ export function useGitReviewActions(args: UseGitReviewActionsArgs) { prTargetBranch, setPrTargetBranch, prLoading, + prPendingAction: writeActions.pendingAction, isGeneratingPr, handleCommit, handleGenerateMessage, diff --git a/src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx b/src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx index c34b4726..2c1c1cf6 100644 --- a/src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx +++ b/src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx @@ -48,12 +48,18 @@ export function PrReviewSidebar(props: { } = props; const { isCollapsed, collapse, expand } = useSidebar(); const [expanded, setExpanded] = useState(true); - const { prLoading, handleMergePr, handleClosePr, handleMarkPrReady, handleUpdatePrBranch } = - usePrWriteActions({ - projectLocation, - prKey, - onRefresh, - }); + const { + prLoading, + pendingAction, + handleMergePr, + handleClosePr, + handleMarkPrReady, + handleUpdatePrBranch, + } = usePrWriteActions({ + projectLocation, + prKey, + onRefresh, + }); const sorted = files.toSorted((a, b) => compareFilesByDirThenName({ path: a.path }, { path: b.path }), @@ -148,6 +154,7 @@ export function PrReviewSidebar(props: { projectId={projectId} worktreePath={worktreePath} prLoading={prLoading} + pendingAction={pendingAction} handleMergePr={handleMergePr} handleClosePr={handleClosePr} handleMarkPrReady={handleMarkPrReady}