From 7f679f28507237ff0a9f08b512835c6528b21f22 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 18 Aug 2026 20:54:51 +0000 Subject: [PATCH] fix(ui): guard retry action while recovery resolves Co-Authored-By: Paperclip --- ui/src/components/IssueChatThread.tsx | 1 + ui/src/components/TaskChatThread.test.tsx | 23 +++++++++++++++++++++++ ui/src/components/TaskChatThread.tsx | 4 +++- ui/src/pages/IssueDetail.tsx | 4 ++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx index 057b09bed70..7879cebd1af 100644 --- a/ui/src/components/IssueChatThread.tsx +++ b/ui/src/components/IssueChatThread.tsx @@ -456,6 +456,7 @@ interface IssueChatThreadProps { scheduledRetry?: IssueScheduledRetry | null; recoveryAction?: IssueRecoveryAction | null; onResolveRecoveryAction?: (outcome: RecoveryResolveOutcome) => void; + recoveryActionPending?: boolean; onReissueIsolatedRecoveryAction?: (request: RecoveryReissueRequest) => void; reissueIsolatedRecoveryActionPending?: boolean; onReconcileForwardRecoveryAction?: () => void; diff --git a/ui/src/components/TaskChatThread.test.tsx b/ui/src/components/TaskChatThread.test.tsx index 1ebbbcf43d7..8abadf4216c 100644 --- a/ui/src/components/TaskChatThread.test.tsx +++ b/ui/src/components/TaskChatThread.test.tsx @@ -233,6 +233,29 @@ describe("TaskChatThread recovery actions", () => { expect(container.querySelector('[data-testid="task-chat-recovery-try-again"]')).toBeNull(); }); + + it("disables Try again while recovery resolution is pending", () => { + const onResolveRecoveryAction = vi.fn(); + render( + {}} + issueId="issue-1" + issueStatus="blocked" + recoveryAction={recoveryAction} + onResolveRecoveryAction={onResolveRecoveryAction} + recoveryActionPending + />, + ); + + const retry = container.querySelector( + '[data-testid="task-chat-recovery-try-again"]', + )!; + expect(retry.disabled).toBe(true); + + flushSync(() => retry.click()); + expect(onResolveRecoveryAction).not.toHaveBeenCalled(); + }); }); describe("TaskChatThread composer alignment (PAP-498)", () => { diff --git a/ui/src/components/TaskChatThread.tsx b/ui/src/components/TaskChatThread.tsx index 3c7c4feb5c6..8cef08de099 100644 --- a/ui/src/components/TaskChatThread.tsx +++ b/ui/src/components/TaskChatThread.tsx @@ -149,6 +149,7 @@ export function TaskChatThread(props: TaskChatThreadProps) { liveIssueIds, recoveryAction, onResolveRecoveryAction, + recoveryActionPending, } = props; const liveWorkLinks = useMemo( @@ -644,13 +645,14 @@ export function TaskChatThread(props: TaskChatThreadProps) { variant="outline" size="xs" data-testid="task-chat-recovery-try-again" + disabled={recoveryActionPending} onClick={() => onResolveRecoveryAction("todo")} > Try again ); }, - [onResolveRecoveryAction, recoveryAction], + [onResolveRecoveryAction, recoveryAction, recoveryActionPending], ); const renderInteraction = useCallback( diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 135b438c217..8735b6720fd 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -961,6 +961,7 @@ type IssueDetailChatTabProps = { scheduledRetry: Issue["scheduledRetry"] | null; recoveryAction: Issue["activeRecoveryAction"]; onResolveRecoveryAction?: (outcome: import("../components/IssueRecoveryActionCard").RecoveryResolveOutcome) => void; + recoveryActionPending?: boolean; onReissueIsolatedRecoveryAction?: (request: import("../components/IssueRecoveryActionCard").RecoveryReissueRequest) => void; reissueIsolatedRecoveryActionPending?: boolean; onReconcileForwardRecoveryAction?: () => void; @@ -1067,6 +1068,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({ scheduledRetry, recoveryAction, onResolveRecoveryAction, + recoveryActionPending, onReissueIsolatedRecoveryAction, reissueIsolatedRecoveryActionPending, onReconcileForwardRecoveryAction, @@ -1342,6 +1344,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({ scheduledRetry={scheduledRetry} recoveryAction={recoveryAction ?? null} onResolveRecoveryAction={onResolveRecoveryAction} + recoveryActionPending={recoveryActionPending} onReissueIsolatedRecoveryAction={onReissueIsolatedRecoveryAction} reissueIsolatedRecoveryActionPending={reissueIsolatedRecoveryActionPending} onReconcileForwardRecoveryAction={onReconcileForwardRecoveryAction} @@ -5285,6 +5288,7 @@ export function IssueDetail() { scheduledRetry={issue.scheduledRetry ?? null} recoveryAction={issue.activeRecoveryAction ?? null} onResolveRecoveryAction={handleResolveRecoveryAction} + recoveryActionPending={resolveRecoveryAction.isPending} onReissueIsolatedRecoveryAction={handleReissueIsolatedRecoveryAction} reissueIsolatedRecoveryActionPending={reissueIsolatedRecoveryAction.isPending} onReconcileForwardRecoveryAction={handleReconcileForwardRecoveryAction}