summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/ui
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-21 02:19:54 +0900
committerAdam Malczewski <[email protected]>2026-06-21 02:19:54 +0900
commitd98a63ce17519983dcf58c27432723e2f4b96e75 (patch)
tree21a4e043d040984aa62fd2ba81ca3349ce01f5c4 /src/features/chat/ui
parent9c90105b6cfede0f3327169718300c649bb0531a (diff)
downloaddispatch-web-d98a63ce17519983dcf58c27432723e2f4b96e75.tar.gz
dispatch-web-d98a63ce17519983dcf58c27432723e2f4b96e75.zip
feat(chat): message queue + steering — mid-turn injection at tool-result boundaries
Consume the message-queue + steering handoff ([email protected], [email protected]). 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.
Diffstat (limited to 'src/features/chat/ui')
-rw-r--r--src/features/chat/ui/Composer.svelte25
1 files changed, 22 insertions, 3 deletions
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"
></textarea>
<button class="btn btn-primary w-20 shrink-0" type="submit" disabled={!hasText}>
- Send
+ {submitLabel}
</button>
</div>