Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@

import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { PageContainer } from '@/components/layouts/PageContainer';
import { CodeReviewStreamView } from '@/components/code-reviews/CodeReviewStreamView';
import { useRetriggerReview } from '@/components/code-reviews/useRetriggerReview';
import { formatTokenCount } from '@/lib/code-reviews/summary/usage-footer';
import {
ExternalLink,
Expand Down Expand Up @@ -70,21 +81,19 @@ export function CodeReviewDetailClient({ reviewId }: CodeReviewDetailClientProps
},
});

const retriggerMutation = useMutation(
trpc.codeReviews.retrigger.mutationOptions({
onSuccess: async () => {
toast.success('Code review retriggered', {
description: 'The code review has been queued for processing.',
});
await queryClient.invalidateQueries({
queryKey: trpc.codeReviews.get.queryKey({ reviewId }),
});
},
onError: err => {
toast.error('Failed to retrigger code review', { description: err.message });
},
})
);
const {
retriggerReview,
confirmOpen,
setConfirmOpen,
confirmCancelAndRetry,
isPending: isRetriggerPending,
} = useRetriggerReview(reviewId, {
onRetriggered: async () => {
await queryClient.invalidateQueries({
queryKey: trpc.codeReviews.get.queryKey({ reviewId }),
});
},
});

const cancelMutation = useMutation(
trpc.codeReviews.cancel.mutationOptions({
Expand Down Expand Up @@ -303,21 +312,42 @@ export function CodeReviewDetailClient({ reviewId }: CodeReviewDetailClientProps
<Button
variant="outline"
size="sm"
onClick={() => retriggerMutation.mutate({ reviewId })}
disabled={retriggerMutation.isPending}
onClick={() => retriggerReview()}
disabled={isRetriggerPending}
className="gap-2"
>
<RotateCcw
className={`h-3 w-3 ${retriggerMutation.isPending ? 'animate-spin' : ''}`}
/>
{retriggerMutation.isPending ? 'Retrying...' : 'Retry'}
<RotateCcw className={`h-3 w-3 ${isRetriggerPending ? 'animate-spin' : ''}`} />
{isRetriggerPending ? 'Retrying...' : 'Retry'}
</Button>
)}
</div>
)}
</CardContent>
</Card>

<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Cancel previous review and retry?</AlertDialogTitle>
<AlertDialogDescription>
A previous review is still running for this pull request. Cancel that stale review,
then retry this one.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isRetriggerPending}>Keep running</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
onClick={confirmCancelAndRetry}
disabled={isRetriggerPending}
>
{isRetriggerPending && <Loader2 className="h-4 w-4 animate-spin" />}
Cancel and retry
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

{/* Session log / live stream */}
{showStreamView && (
<CodeReviewStreamView
Expand Down
86 changes: 51 additions & 35 deletions apps/web/src/components/code-reviews/CodeReviewJobsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
Dialog,
DialogContent,
Expand Down Expand Up @@ -46,6 +56,7 @@ import { useTRPC } from '@/lib/trpc/utils';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { formatDistanceToNow } from 'date-fns';
import { CodeReviewStreamView } from './CodeReviewStreamView';
import { useRetriggerReview } from './useRetriggerReview';
import { useOrganizationModels } from '@/components/cloud-agent/hooks/useOrganizationModels';
import { ModelCombobox, type ModelOption } from '@/components/shared/ModelCombobox';
import { PRIMARY_DEFAULT_MODEL } from '@/lib/ai-gateway/models';
Expand Down Expand Up @@ -565,34 +576,14 @@ export function CodeReviewJobsCard({
</Dialog>
);

// Retrigger mutation for failed/cancelled/interrupted reviews
const retriggerMutation = useMutation(
trpc.codeReviews.retrigger.mutationOptions({
onSuccess: async () => {
toast.success('Code review retriggered', {
description: 'The code review has been queued for processing.',
});
setActionInProgressId(null);
// Invalidate the query to refetch the list
await queryClient.invalidateQueries({
queryKey: organizationId
? trpc.codeReviews.listForOrganization.queryKey({
organizationId,
limit: PAGE_SIZE,
offset,
platform,
})
: trpc.codeReviews.listForUser.queryKey({ limit: PAGE_SIZE, offset, platform }),
});
},
onError: error => {
toast.error('Failed to retrigger code review', {
description: error.message,
});
setActionInProgressId(null);
},
})
);
const {
retriggerReview,
confirmOpen,
setConfirmOpen,
confirmCancelAndRetry,
isPending: isRetriggerPending,
pendingReviewId: retriggerPendingReviewId,
} = useRetriggerReview(undefined, { onRetriggered: invalidateJobsList });

// Cancel mutation for pending/queued/running reviews
const cancelMutation = useMutation(
Expand Down Expand Up @@ -623,11 +614,37 @@ export function CodeReviewJobsCard({
})
);

const retriggerConfirmDialog = (
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Cancel previous review and retry?</AlertDialogTitle>
<AlertDialogDescription>
A previous review is still running for this pull request. Cancel that stale review, then
retry this one.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isRetriggerPending}>Keep running</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
onClick={confirmCancelAndRetry}
disabled={isRetriggerPending}
>
{isRetriggerPending && <Loader2 className="h-4 w-4 animate-spin" />}
Cancel and retry
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);

if (isLoading) {
return (
<>
<Card>{renderJobsCardHeader('Loading jobs...')}</Card>
{manualJobDialog}
{retriggerConfirmDialog}
</>
);
}
Expand All @@ -652,6 +669,7 @@ export function CodeReviewJobsCard({
</CardContent>
</Card>
{manualJobDialog}
{retriggerConfirmDialog}
</>
);
}
Expand Down Expand Up @@ -854,19 +872,16 @@ export function CodeReviewJobsCard({
<Button
variant="outline"
size="sm"
onClick={() => {
setActionInProgressId(review.id);
retriggerMutation.mutate({ reviewId: review.id });
}}
onClick={() => retriggerReview(review.id)}
disabled={
actionInProgressId === review.id && retriggerMutation.isPending
retriggerPendingReviewId === review.id && isRetriggerPending
}
className="gap-2"
>
<RotateCcw
className={`h-3 w-3 ${actionInProgressId === review.id && retriggerMutation.isPending ? 'animate-spin' : ''}`}
className={`h-3 w-3 ${retriggerPendingReviewId === review.id && isRetriggerPending ? 'animate-spin' : ''}`}
/>
{actionInProgressId === review.id && retriggerMutation.isPending
{retriggerPendingReviewId === review.id && isRetriggerPending
? 'Retrying...'
: 'Retry'}
</Button>
Expand Down Expand Up @@ -923,6 +938,7 @@ export function CodeReviewJobsCard({
</CardContent>
</Card>
{manualJobDialog}
{retriggerConfirmDialog}
</>
);
}
73 changes: 73 additions & 0 deletions apps/web/src/components/code-reviews/useRetriggerReview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { toast } from 'sonner';
import { useTRPC } from '@/lib/trpc/utils';

type UseRetriggerReviewOptions = {
onRetriggered: () => Promise<void> | void;
};

export function useRetriggerReview(
reviewId: string | undefined,
{ onRetriggered }: UseRetriggerReviewOptions
) {
const trpc = useTRPC();
const [confirmOpen, setConfirmOpenState] = useState(false);
const [confirmReviewId, setConfirmReviewId] = useState<string | null>(null);

const retriggerMutation = useMutation(
trpc.codeReviews.retrigger.mutationOptions({
onSuccess: async (result, variables) => {
if (!result.success) {
toast.error('Failed to retrigger code review', {
description: String(result.error ?? 'Failed to retrigger code review'),
});
return;
}

if (result.outcome === 'confirm_cancel_active') {
setConfirmReviewId(variables.reviewId);
setConfirmOpenState(true);
return;
}

setConfirmReviewId(null);
setConfirmOpenState(false);
toast.success('Code review retriggered', {
description: 'The code review has been queued for processing.',
});
await onRetriggered();
},
onError: error => {
toast.error('Failed to retrigger code review', { description: error.message });
},
})
);

function setConfirmOpen(open: boolean) {
setConfirmOpenState(open);
if (!open && !retriggerMutation.isPending) {
setConfirmReviewId(null);
}
}

function retriggerReview(nextReviewId = reviewId) {
if (!nextReviewId) return;
retriggerMutation.mutate({ reviewId: nextReviewId });
}

function confirmCancelAndRetry() {
const nextReviewId = confirmReviewId ?? reviewId;
if (!nextReviewId) return;
retriggerMutation.mutate({ reviewId: nextReviewId, cancelActiveReview: true });
}

return {
retriggerReview,
confirmOpen,
setConfirmOpen,
confirmCancelAndRetry,
isPending: retriggerMutation.isPending,
pendingReviewId: retriggerMutation.variables?.reviewId ?? null,
};
}
1 change: 1 addition & 0 deletions apps/web/src/lib/code-reviews/core/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export const CancelCodeReviewInputSchema = z.object({
*/
export const RetriggerCodeReviewInputSchema = z.object({
reviewId: z.string().uuid(),
cancelActiveReview: z.boolean().optional().default(false),
});

// ============================================================================
Expand Down
Loading