Skip to content
Merged
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
83 changes: 67 additions & 16 deletions apps/web/app/(app)/[emailAccountId]/assistant/BulkRunRules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export function BulkRunRules() {
const { emailAccountId } = useAccount();

const [isOpen, setIsOpen] = useState(false);
const [totalThreads, setTotalThreads] = useState(0);
const [processedThreadIds, setProcessedThreadIds] = useState<Set<string>>(
new Set(),
);

const { data, isLoading, error } = useThreads({ type: "inbox" });

Expand All @@ -40,9 +42,17 @@ export function BulkRunRules() {

const [startDate, setStartDate] = useState<Date | undefined>();
const [endDate, setEndDate] = useState<Date | undefined>();
const [runResult, setRunResult] = useState<{
count: number;
} | null>(null);

const abortRef = useRef<() => void>(undefined);

const remaining = new Set(
[...processedThreadIds].filter((id) => queue.has(id)),
).size;
const completed = processedThreadIds.size - remaining;

return (
<div>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
Expand All @@ -63,11 +73,12 @@ export function BulkRunRules() {
(that have not been previously processed).
</SectionDescription>

{!!queue.size && (
{processedThreadIds.size > 0 && (
<div className="rounded-md border border-green-200 bg-green-50 px-2 py-1.5 dark:border-green-800 dark:bg-green-950">
<SectionDescription className="mt-0">
Progress: {totalThreads - queue.size}/{totalThreads}{" "}
emails completed
{remaining > 0
? `Progress: ${completed}/${processedThreadIds.size} emails completed`
: `Success: Processed ${processedThreadIds.size} emails`}
</SectionDescription>
</div>
)}
Expand All @@ -76,13 +87,21 @@ export function BulkRunRules() {
<div className="flex flex-col space-y-2">
<div className="grid grid-cols-2 gap-2">
<SetDateDropdown
onChange={setStartDate}
onChange={(date) => {
setStartDate(date);
setRunResult(null);
setProcessedThreadIds(new Set());
}}
value={startDate}
placeholder="Set start date"
disabled={running}
/>
<SetDateDropdown
onChange={setEndDate}
onChange={(date) => {
setEndDate(date);
setRunResult(null);
setProcessedThreadIds(new Set());
}}
value={endDate}
placeholder="Set end date (optional)"
disabled={running}
Expand All @@ -94,6 +113,8 @@ export function BulkRunRules() {
disabled={running || !startDate || !emailAccountId}
loading={running}
onClick={async () => {
setRunResult(null);
setProcessedThreadIds(new Set());
if (!startDate) {
toastError({
description: "Please select a start date",
Expand All @@ -111,9 +132,21 @@ export function BulkRunRules() {
abortRef.current = await onRun(
emailAccountId,
{ startDate, endDate },
(count) =>
setTotalThreads((total) => total + count),
() => setRunning(false),
(ids) => {
setProcessedThreadIds((prev) => {
const next = new Set(prev);
for (const id of ids) {
next.add(id);
}
return next;
});
},
(status, count) => {
setRunning(false);
if (status === "success" && count === 0) {
setRunResult({ count });
}
},
);
}}
>
Expand All @@ -127,6 +160,12 @@ export function BulkRunRules() {
Cancel
</Button>
)}

{runResult && runResult.count === 0 && (
<div className="mt-4 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-sm text-blue-800 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200">
No unread emails found in the selected date range.
</div>
)}
</div>
) : (
<PremiumAlertWithData />
Expand All @@ -145,11 +184,15 @@ export function BulkRunRules() {
async function onRun(
emailAccountId: string,
{ startDate, endDate }: { startDate: Date; endDate?: Date },
incrementThreadsQueued: (count: number) => void,
onComplete: () => void,
onThreadsQueued: (threadIds: string[]) => void,
onComplete: (
status: "success" | "error" | "cancelled",
count: number,
) => void,
) {
let nextPageToken = "";
const LIMIT = 25;
let totalProcessed = 0;

let aborted = false;

Expand Down Expand Up @@ -186,7 +229,8 @@ async function onRun(
? errorData.error
: `Error: ${res.status}`,
});
break;
onComplete("error", totalProcessed);
return;
}

const data: ThreadsResponse = await res.json();
Expand All @@ -197,25 +241,32 @@ async function onRun(
title: "Invalid response",
description: "Failed to process emails. Please try again.",
});
break;
onComplete("error", totalProcessed);
return;
}

nextPageToken = data.nextPageToken || "";

const threadsWithoutPlan = data.threads.filter((t) => !t.plan);

incrementThreadsQueued(threadsWithoutPlan.length);
onThreadsQueued(threadsWithoutPlan.map((t) => t.id));
totalProcessed += threadsWithoutPlan.length;

runAiRules(emailAccountId, threadsWithoutPlan, false);

if (!nextPageToken || aborted) break;
if (aborted) {
onComplete("cancelled", totalProcessed);
return;
}

if (!nextPageToken) break;

// avoid gmail api rate limits
// ai takes longer anyway
await sleep(threadsWithoutPlan.length ? 5000 : 2000);
}

onComplete();
onComplete("success", totalProcessed);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runAiRules function, which is called by this component, does not handle errors within its queued jobs. This can lead to a permanent inconsistency between the AI processing queue state and the UI, causing progress indicators to freeze on an error, even though this component reports that the queuing was successful.

Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/assistant/BulkRunRules.tsx at line 269:

<comment>The `runAiRules` function, which is called by this component, does not handle errors within its queued jobs. This can lead to a permanent inconsistency between the AI processing queue state and the UI, causing progress indicators to freeze on an error, even though this component reports that the queuing was successful.</comment>

<file context>
@@ -249,14 +254,19 @@ async function onRun(
     }
 
-    onComplete(totalProcessed);
+    onComplete(&quot;success&quot;, totalProcessed);
   }
 
</file context>
Fix with Cubic

}

run();
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.20.9
v2.20.10
Loading