Skip to content

Commit d8498ec

Browse files
tlgimenesclaude
andcommitted
feat(chat): queued-messages panel above the composer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 82866ea commit d8498ec

2 files changed

Lines changed: 74 additions & 0 deletions

File tree

apps/mesh/src/web/components/chat/input.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import { ConnectionsBanner } from "./connections-banner";
6767
import { useVoiceInput } from "@/web/hooks/use-voice-input.ts";
6868
import { VoiceWaveform } from "./voice-input";
6969
import { shouldRenderInlineModeRow } from "./input-mode-row";
70+
import { ThreadQueuePanel } from "./queue-panel";
7071

7172
// ============================================================================
7273
// useWindowFileDrop - Reusable hook for window-level file drag & drop
@@ -460,6 +461,13 @@ export function ChatInput({
460461
absent on the home composer. */}
461462
{stream && taskCtx && <ChatHighlight />}
462463

464+
{stream && taskCtx && taskId ? (
465+
<ThreadQueuePanel
466+
taskId={taskId}
467+
active={isStreaming || isRunInProgress}
468+
/>
469+
) : null}
470+
463471
<TiptapProvider
464472
key={taskId}
465473
tiptapDoc={tiptapDoc}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Button } from "@deco/ui/components/button.tsx";
2+
import { cn } from "@deco/ui/lib/utils.ts";
3+
import { X } from "@untitledui/icons";
4+
import { useCancelQueuedMessage, useThreadQueue } from "./use-thread-queue";
5+
6+
/**
7+
* Pending-message queue above the composer. Lists the thread's running head +
8+
* queued messages; each row cancels its own gate workflow. Hidden when empty.
9+
*/
10+
export function ThreadQueuePanel({
11+
taskId,
12+
active,
13+
}: {
14+
taskId: string;
15+
active: boolean;
16+
}) {
17+
const { items } = useThreadQueue(taskId, { active });
18+
const cancel = useCancelQueuedMessage(taskId);
19+
if (items.length === 0) return null;
20+
21+
const queuedCount = items.filter((i) => i.status === "queued").length;
22+
23+
return (
24+
<div className="mb-1.5 rounded-xl border border-border bg-muted/50 p-2 text-sm">
25+
<div className="px-1 pb-1 text-xs text-muted-foreground">
26+
{queuedCount > 0
27+
? `${queuedCount} queued message${queuedCount === 1 ? "" : "s"}`
28+
: "Running"}
29+
</div>
30+
<ul className="flex flex-col gap-1">
31+
{items.map((item) => (
32+
<li
33+
key={item.workflowId}
34+
className="flex items-center gap-2 rounded-lg bg-background px-2 py-1.5"
35+
>
36+
<span
37+
className={cn(
38+
"size-1.5 shrink-0 rounded-full",
39+
item.status === "running"
40+
? "bg-primary animate-pulse"
41+
: "bg-muted-foreground/40",
42+
)}
43+
/>
44+
<span className="min-w-0 flex-1 truncate text-foreground">
45+
{item.text || (
46+
<span className="text-muted-foreground">(no text)</span>
47+
)}
48+
</span>
49+
<Button
50+
type="button"
51+
variant="ghost"
52+
size="icon"
53+
className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
54+
title={
55+
item.status === "running" ? "Cancel run" : "Remove from queue"
56+
}
57+
onClick={() => cancel(item.workflowId)}
58+
>
59+
<X size={14} />
60+
</Button>
61+
</li>
62+
))}
63+
</ul>
64+
</div>
65+
);
66+
}

0 commit comments

Comments
 (0)