Skip to content

Commit 6a4ef53

Browse files
tlgimenesclaude
andcommitted
fix(chat): exclude the active head from the queue, not just PENDING
The queue panel was duplicating the current message: a just-sent message sits ENQUEUED ("accepted and queued") for a window before it flips to PENDING, and the previous filter only excluded PENDING — so the active head leaked into the panel while also rendering in the chat body. Define the active run as the *oldest non-terminal* gate workflow (PENDING or ENQUEUED) and exclude it: new pure `selectWaitingQueueItems(items)` sorts by enqueuedAt and drops the head. The current message keeps rendering in the body as before; the panel shows only what comes next. Verified live: a lone ENQUEUED head shows no panel (only the body), and a head + one waiting item shows just the waiting one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ee0962c commit 6a4ef53

3 files changed

Lines changed: 78 additions & 4 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { selectWaitingQueueItems } from "./queue-items";
3+
import type { QueueItemDTO } from "./use-thread-queue";
4+
5+
const item = (
6+
id: string,
7+
status: "running" | "queued",
8+
enqueuedAt: number,
9+
): QueueItemDTO => ({
10+
workflowId: `thread-run:t:${id}`,
11+
messageId: id,
12+
text: id,
13+
status,
14+
enqueuedAt,
15+
});
16+
17+
describe("selectWaitingQueueItems", () => {
18+
it("returns nothing for an empty queue", () => {
19+
expect(selectWaitingQueueItems([])).toEqual([]);
20+
});
21+
22+
it("hides a lone PENDING head (running, in the body)", () => {
23+
expect(selectWaitingQueueItems([item("a", "running", 1)])).toEqual([]);
24+
});
25+
26+
it("hides a lone ENQUEUED head — the 'accepted and queued' window", () => {
27+
// The reported bug: a just-sent first message sits ENQUEUED before it
28+
// flips to PENDING; it's the active run shown in the body, not a wait.
29+
expect(selectWaitingQueueItems([item("massa", "queued", 1)])).toEqual([]);
30+
});
31+
32+
it("shows the messages waiting behind a running head", () => {
33+
const out = selectWaitingQueueItems([
34+
item("head", "running", 1),
35+
item("q1", "queued", 2),
36+
item("q2", "queued", 3),
37+
]);
38+
expect(out.map((i) => i.messageId)).toEqual(["q1", "q2"]);
39+
});
40+
41+
it("drops the oldest when none is running yet (all ENQUEUED)", () => {
42+
const out = selectWaitingQueueItems([
43+
item("head", "queued", 1),
44+
item("q1", "queued", 2),
45+
]);
46+
expect(out.map((i) => i.messageId)).toEqual(["q1"]);
47+
});
48+
49+
it("sorts by enqueuedAt before dropping the head", () => {
50+
const out = selectWaitingQueueItems([
51+
item("q2", "queued", 3),
52+
item("head", "running", 1),
53+
item("q1", "queued", 2),
54+
]);
55+
expect(out.map((i) => i.messageId)).toEqual(["q1", "q2"]);
56+
});
57+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { QueueItemDTO } from "./use-thread-queue";
2+
3+
/**
4+
* The messages *waiting behind* the active run, for the queue panel.
5+
*
6+
* The active run is the oldest non-terminal gate workflow — whether it's
7+
* already PENDING ("running") or still ENQUEUED in the brief "accepted and
8+
* queued" window before it flips to PENDING. That message is the one being
9+
* processed and is already rendered in the chat body, so the panel must
10+
* exclude it and show only what comes next. Pure + total.
11+
*/
12+
export function selectWaitingQueueItems(items: QueueItemDTO[]): QueueItemDTO[] {
13+
if (items.length <= 1) return [];
14+
const sorted = [...items].sort((a, b) => a.enqueuedAt - b.enqueuedAt);
15+
return sorted.slice(1);
16+
}

apps/mesh/src/web/components/chat/queue-panel.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import { Button } from "@deco/ui/components/button.tsx";
22
import { X } from "@untitledui/icons";
3+
import { selectWaitingQueueItems } from "./queue-items";
34
import { useCancelQueuedMessage, useThreadQueue } from "./use-thread-queue";
45

56
/**
67
* Pending-message queue above the composer. Lists only the messages *waiting*
7-
* behind the active run — the running head is excluded here because it's
8-
* already rendered in the chat body. Each row cancels its own gate workflow.
9-
* Hidden when nothing is queued.
8+
* behind the active run — the message currently being processed (the oldest
9+
* non-terminal gate workflow) is excluded because it's already rendered in the
10+
* chat body. Each row cancels its own gate workflow. Hidden when nothing waits.
1011
*/
1112
export function ThreadQueuePanel({ taskId }: { taskId: string }) {
1213
const { items } = useThreadQueue(taskId);
1314
const cancel = useCancelQueuedMessage(taskId);
14-
const queued = items.filter((i) => i.status === "queued");
15+
const queued = selectWaitingQueueItems(items);
1516
if (queued.length === 0) return null;
1617

1718
return (

0 commit comments

Comments
 (0)