diff --git a/apps/web/src/app/(app)/gastown/[townId]/merges/NeedsAttention.tsx b/apps/web/src/app/(app)/gastown/[townId]/merges/NeedsAttention.tsx
index a7fc670317..cdd73bc7bf 100644
--- a/apps/web/src/app/(app)/gastown/[townId]/merges/NeedsAttention.tsx
+++ b/apps/web/src/app/(app)/gastown/[townId]/merges/NeedsAttention.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useState, useMemo, Fragment } from 'react';
+import { useState, useMemo, Fragment, useCallback } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useSession } from 'next-auth/react';
import { useGastownTRPC } from '@/lib/gastown/trpc';
@@ -15,7 +15,9 @@ import {
Eye,
GitBranch,
GitMerge,
+ Loader2,
RefreshCw,
+ X,
XCircle,
CheckCircle2,
Clock,
@@ -113,6 +115,8 @@ export function NeedsAttention({
}) {
const session = useSession();
const isAdmin = session?.data?.isAdmin ?? false;
+ const trpc = useGastownTRPC();
+ const queryClient = useQueryClient();
const totalCount = data.openPRs.length + data.failedReviews.length + data.stalePRs.length;
// Tag each item with its category for rendering
@@ -139,6 +143,39 @@ export function NeedsAttention({
return map;
}, [allItems]);
+ const failedItems = useMemo(
+ () => allItems.filter(({ category }) => category === 'failed').map(({ item }) => item),
+ [allItems]
+ );
+
+ const [isDismissingAll, setIsDismissingAll] = useState(false);
+ const updateBeadMutation = useMutation(trpc.gastown.updateBead.mutationOptions({}));
+
+ const dismissAllFailed = useCallback(async () => {
+ if (failedItems.length === 0) return;
+ setIsDismissingAll(true);
+ try {
+ await Promise.all(
+ failedItems.map(item =>
+ updateBeadMutation.mutateAsync({
+ rigId: item.mrBead.rig_id ?? '',
+ beadId: item.mrBead.bead_id,
+ status: 'closed',
+ })
+ )
+ );
+ void queryClient.invalidateQueries({
+ queryKey: trpc.gastown.getMergeQueueData.queryKey({ townId }),
+ });
+ toast.success(`Dismissed ${failedItems.length} failed ${failedItems.length === 1 ? 'bead' : 'beads'}`);
+ } catch (err: unknown) {
+ const message = err instanceof Error ? err.message : 'Unknown error';
+ toast.error(`Failed to dismiss all: ${message}`);
+ } finally {
+ setIsDismissingAll(false);
+ }
+ }, [failedItems, updateBeadMutation, queryClient, trpc, townId]);
+
if (totalCount === 0) {
return (
@@ -150,6 +187,24 @@ export function NeedsAttention({
return (
+ {/* Dismiss all failed button */}
+ {failedItems.length > 0 && (
+
+
+
+ )}
+
{/* Convoy groups */}
{convoyGroups.map(group => (
@@ -335,6 +390,18 @@ function AttentionItemRow({
})
);
+ const dismissMutation = useMutation(
+ trpc.gastown.updateBead.mutationOptions({
+ onSuccess: () => {
+ invalidateMergeQueue();
+ toast.success('Bead dismissed');
+ },
+ onError: (err: { message: string }) => {
+ toast.error(`Failed to dismiss: ${err.message}`);
+ },
+ })
+ );
+
// Fail bead mutation: use adminForceFailBead
const failMutation = useMutation(
trpc.gastown.adminForceFailBead.mutationOptions({
@@ -349,7 +416,7 @@ function AttentionItemRow({
})
);
- const isPending = retryMutation.isPending || failMutation.isPending;
+ const isPending = retryMutation.isPending || failMutation.isPending || dismissMutation.isPending;
const handleConfirm = () => {
if (!confirmAction) return;
@@ -388,13 +455,12 @@ function AttentionItemRow({