Skip to content

Commit 82866ea

Browse files
tlgimenesclaude
andcommitted
feat(chat): thread queue data hooks + invalidate on send
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0cdc3d2 commit 82866ea

3 files changed

Lines changed: 72 additions & 0 deletions

File tree

apps/mesh/src/web/components/chat/chat-context.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,9 @@ export function ActiveTaskProvider({
11061106
}),
11071107
},
11081108
);
1109+
queryClient.invalidateQueries({
1110+
queryKey: KEYS.threadQueue(capturedTaskId),
1111+
});
11091112
}
11101113

11111114
// Cancel run
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2+
import { useProjectContext } from "@decocms/mesh-sdk";
3+
import { toast } from "sonner";
4+
import { KEYS } from "../../lib/query-keys";
5+
6+
export interface QueueItemDTO {
7+
workflowId: string;
8+
messageId: string;
9+
text: string;
10+
status: "running" | "queued";
11+
enqueuedAt: number;
12+
}
13+
14+
/**
15+
* Poll this thread's pending gate queue. Polls every 3s while the run is active
16+
* or items exist (cheap, bounded), otherwise idles (re-fetched on send via the
17+
* KEYS.threadQueue invalidation in chat-context). No useEffect (lint ban).
18+
*/
19+
export function useThreadQueue(
20+
taskId: string,
21+
opts: { active: boolean },
22+
): { items: QueueItemDTO[] } {
23+
const { org } = useProjectContext();
24+
const { data } = useQuery({
25+
queryKey: KEYS.threadQueue(taskId),
26+
enabled: Boolean(taskId),
27+
staleTime: 0,
28+
refetchInterval: (query) => {
29+
const n =
30+
(query.state.data as { items?: QueueItemDTO[] } | undefined)?.items
31+
?.length ?? 0;
32+
return opts.active || n > 0 ? 3000 : false;
33+
},
34+
queryFn: async (): Promise<{ items: QueueItemDTO[] }> => {
35+
const res = await fetch(`/api/${org.slug}/decopilot/queue/${taskId}`, {
36+
credentials: "include",
37+
});
38+
if (!res.ok) return { items: [] };
39+
return (await res.json()) as { items: QueueItemDTO[] };
40+
},
41+
});
42+
return { items: data?.items ?? [] };
43+
}
44+
45+
/** Cancel one queued/running gate item, then refresh the queue. */
46+
export function useCancelQueuedMessage(
47+
taskId: string,
48+
): (workflowId: string) => void {
49+
const { org } = useProjectContext();
50+
const queryClient = useQueryClient();
51+
const mutation = useMutation({
52+
mutationFn: async (workflowId: string) => {
53+
const res = await fetch(
54+
`/api/${org.slug}/decopilot/queue/${taskId}/cancel/${encodeURIComponent(workflowId)}`,
55+
{ method: "POST", credentials: "include" },
56+
);
57+
if (!res.ok && res.status !== 404) {
58+
throw new Error(`Cancel failed: ${res.status}`);
59+
}
60+
},
61+
onSettled: () =>
62+
queryClient.invalidateQueries({ queryKey: KEYS.threadQueue(taskId) }),
63+
onError: (err) =>
64+
toast.error(err instanceof Error ? err.message : "Failed to cancel"),
65+
});
66+
return (workflowId: string) => mutation.mutate(workflowId);
67+
}

apps/mesh/src/web/lib/query-keys.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ export const KEYS = {
188188
threadSandbox: (orgKey: string, taskId: string | undefined) =>
189189
["thread-sandbox", "v2", orgKey, taskId] as const,
190190
threadOutputs: (threadId: string) => ["thread-outputs", threadId] as const,
191+
threadQueue: (threadId: string) =>
192+
["decopilot", "thread-queue", threadId] as const,
191193
// Fetched text content of a previewed file (FilePreview), keyed by URL.
192194
fileText: (downloadUrl: string) => ["file-text", downloadUrl] as const,
193195
// First bytes of a CSV/TSV file for the card thumbnail (range request).

0 commit comments

Comments
 (0)