From d98a63ce17519983dcf58c27432723e2f4b96e75 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sun, 21 Jun 2026 02:19:54 +0900 Subject: feat(chat): message queue + steering — mid-turn injection at tool-result boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consume the message-queue + steering handoff (wire@0.8.0, transport-contract@0.12.0). Re-pinned file: deps + re-mirrored .dispatch/*.reference.md. - fold steering AgentEvent into the transcript as a provisional user bubble (after the tool-result it followed; no de-dup — the queue surface carried it) - add rendererId: "message-queue" custom renderer (pure parser + MessageQueueList) rendered as a compact panel above the Composer (hidden when queue is empty) - add ChatStore.queueMessage / AppStore.queueMessage — sends chat.queue WS op (trim/validate non-empty; auto-starts a turn if idle) - Composer switches to chat.queue while generating (button → Queue, placeholder → Steer the conversation...) - exhaustiveness guards updated for steering + chat.queue - carry-to-new-turn needs no special handling (normal new turn) 664 tests green. --- src/features/chat/ui/Composer.svelte | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) (limited to 'src/features/chat/ui') diff --git a/src/features/chat/ui/Composer.svelte b/src/features/chat/ui/Composer.svelte index 24c2c19..d519efc 100644 --- a/src/features/chat/ui/Composer.svelte +++ b/src/features/chat/ui/Composer.svelte @@ -8,10 +8,18 @@ let { onSend, + onQueue, contextSize = undefined, status = "idle", }: { onSend: (text: string) => void; + /** + * Enqueue a steering message (`chat.queue`). When provided AND the status + * is `running`, the send button becomes a "Queue" button that steers the + * in-flight turn instead of starting a new one. When absent, `onSend` is + * used regardless (tests / non-steering contexts). + */ + onQueue?: (text: string) => void; // Current context occupancy (latest turn's contextSize), or `undefined` // when unknown — the status bar then shows "— tokens", never 0%. contextSize?: number | undefined; @@ -26,6 +34,13 @@ const usage = $derived(computeContextUsage(contextSize, MAX_CONTEXT)); const hasUsage = $derived(contextSize !== undefined); + // While a turn is generating, the send button becomes a "Queue" button that + // enqueues a steering message (`chat.queue`) instead of starting a new turn + // (`chat.send`). Falls back to `onSend` when no `onQueue` is wired. + const steering = $derived(status === "running" && onQueue !== undefined); + const submitLabel = $derived(steering ? "Queue" : "Send"); + const placeholder = $derived(steering ? "Steer the conversation..." : "Type a message..."); + // As the window fills, escalate color: calm → warning → danger. function fillClass(pct: number): string { if (pct >= 90) return "progress-error"; @@ -58,7 +73,11 @@ function handleSubmit(): void { const trimmed = text.trim(); if (trimmed.length === 0) return; - onSend(trimmed); + if (steering) { + onQueue?.(trimmed); + } else { + onSend(trimmed); + } text = ""; } @@ -84,12 +103,12 @@ class="textarea textarea-bordered flex-1 resize-none leading-normal !min-h-0 h-auto" bind:value={text} onkeydown={handleKeydown} - placeholder="Type a message..." + placeholder={placeholder} rows="1" aria-label="Message input" > -- cgit v1.2.3