diff options
| author | Adam Malczewski <[email protected]> | 2026-05-22 20:54:19 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-22 20:54:19 +0900 |
| commit | c47346cc6237044ecb60ff22c4011d89744af581 (patch) | |
| tree | 2359a25e687e1290ba5180fd60eae83b03b53a23 /packages/frontend/src/lib/components | |
| parent | 288b21cec98421fda57028a0c8c9d835cfbb14b0 (diff) | |
| download | dispatch-c47346cc6237044ecb60ff22c4011d89744af581.tar.gz dispatch-c47346cc6237044ecb60ff22c4011d89744af581.zip | |
feat: message queue/interrupt system, CORS fix, mobile fixes, chat splitting
- Add message queue allowing users to send messages while agent is running
- Queue messages are injected into tool results as [USER INTERRUPT]
- Retrieve tool interrupted via Promise.race when user message arrives
- Queued messages show with 'queued' badge and cancel button
- Consumed messages repositioned and chat splits at interrupt point
- New assistant message block created after interrupt for clean flow
- Add POST /chat/cancel endpoint for cancelling queued messages
- Fix CORS to allow any origin (Tailscale/LAN access)
- Fix crypto.randomUUID fallback for non-secure contexts (HTTP)
- Fix frontend API URL derivation from page hostname
- Auto-create DB tab if missing on processMessage (foreign key fix)
- Add error logging to processMessage catch block
- Fix working directory input sync on agent switch
- Fix agent mode button to re-apply agent settings
Diffstat (limited to 'packages/frontend/src/lib/components')
4 files changed, 50 insertions, 9 deletions
diff --git a/packages/frontend/src/lib/components/ChatInput.svelte b/packages/frontend/src/lib/components/ChatInput.svelte index 21c85fe..dac7a3a 100644 --- a/packages/frontend/src/lib/components/ChatInput.svelte +++ b/packages/frontend/src/lib/components/ChatInput.svelte @@ -3,7 +3,6 @@ import { tabStore } from "../tabs.svelte.js"; let inputEl: HTMLInputElement | undefined; let inputValue = $state(""); -const isDisabled = $derived((tabStore.activeTab?.agentStatus ?? "idle") === "running"); $effect(() => { inputEl?.focus(); @@ -18,7 +17,7 @@ function handleKeydown(e: KeyboardEvent) { function submit() { const text = inputValue.trim(); - if (!text || isDisabled) return; + if (!text) return; inputValue = ""; tabStore.sendMessage(text); } @@ -36,7 +35,7 @@ function submit() { <button type="button" class="btn btn-primary" - disabled={isDisabled || !inputValue.trim()} + disabled={!inputValue.trim()} onclick={submit} > Send diff --git a/packages/frontend/src/lib/components/ChatMessage.svelte b/packages/frontend/src/lib/components/ChatMessage.svelte index 308e6ba..c6b6034 100644 --- a/packages/frontend/src/lib/components/ChatMessage.svelte +++ b/packages/frontend/src/lib/components/ChatMessage.svelte @@ -1,13 +1,26 @@ <script lang="ts"> import { appSettings } from "../settings.svelte.js"; +import { tabStore } from "../tabs.svelte.js"; import type { ChatMessage } from "../types.js"; import MarkdownRenderer from "./MarkdownRenderer.svelte"; import ToolCallDisplay from "./ToolCallDisplay.svelte"; -const { message }: { message: ChatMessage } = $props(); +const { message, tabId }: { message: ChatMessage; tabId?: string } = $props(); const isUser = $derived(message.role === "user"); const isSystem = $derived(message.role === "system"); + +// Check if this message is queued: its id starts with "queued-" +const queuedMessageId = $derived( + isUser && message.id.startsWith("queued-") ? message.id.slice("queued-".length) : null, +); +const isQueued = $derived(queuedMessageId !== null); + +function cancelQueued() { + if (tabId && queuedMessageId) { + void tabStore.cancelQueuedMessage(tabId, queuedMessageId); + } +} </script> {#if isSystem} @@ -21,7 +34,7 @@ const isSystem = $derived(message.role === "system"); </div> </div> {:else} -<div class="chat chat-start mb-2 [&>.chat-bubble]:max-w-full"> +<div class="chat chat-start mb-2 [&>.chat-bubble]:max-w-full {isQueued ? 'opacity-60' : ''}"> <div class="chat-bubble break-words {isUser ? 'chat-bubble-primary w-fit' : 'bg-transparent w-full'}"> {#if message.thinking} <div class="collapse collapse-arrow mb-2 p-1"> @@ -43,5 +56,17 @@ const isSystem = $derived(message.role === "system"); <span class="inline-block w-1.5 h-4 bg-current animate-pulse ml-0.5 align-middle rounded-sm"></span> {/if} </div> + {#if isQueued} + <div class="flex items-center gap-1 mt-0.5 ml-1"> + <span class="badge badge-ghost badge-xs text-base-content/40">queued</span> + <button + class="btn btn-xs btn-ghost text-base-content/40 hover:text-error px-1 min-h-0 h-auto" + onclick={cancelQueued} + title="Cancel queued message" + > + ✕ + </button> + </div> + {/if} </div> {/if} diff --git a/packages/frontend/src/lib/components/ChatPanel.svelte b/packages/frontend/src/lib/components/ChatPanel.svelte index 8c92e4f..82bd4ee 100644 --- a/packages/frontend/src/lib/components/ChatPanel.svelte +++ b/packages/frontend/src/lib/components/ChatPanel.svelte @@ -8,6 +8,7 @@ let userScrolledUp = $state(false); let isAutoScrolling = false; const messages = $derived(tabStore.activeTab?.messages ?? []); +const activeTabId = $derived(tabStore.activeTab?.id); function isNearBottom(el: HTMLElement): boolean { return el.scrollHeight - el.scrollTop - el.clientHeight < 64; @@ -60,7 +61,7 @@ $effect(() => { </div> {/if} {#each messages as message (message.id)} - <ChatMessageComponent {message} /> + <ChatMessageComponent {message} tabId={activeTabId} /> {/each} </div> diff --git a/packages/frontend/src/lib/components/ModelSelector.svelte b/packages/frontend/src/lib/components/ModelSelector.svelte index 21364f9..5949e71 100644 --- a/packages/frontend/src/lib/components/ModelSelector.svelte +++ b/packages/frontend/src/lib/components/ModelSelector.svelte @@ -175,7 +175,7 @@ const modelCache = new Map<string, string[]>(); class="input input-bordered input-sm font-mono text-xs flex-1" placeholder="default (project root)" value={workingDirectory ?? ""} - oninput={(e) => { + onchange={(e) => { const val = e.currentTarget.value.trim(); onWorkingDirectoryChange(val || null); }} @@ -202,7 +202,19 @@ const modelCache = new Map<string, string[]>(); </button> <button class="btn btn-xs {mode === 'agent' ? 'btn-primary' : 'btn-ghost'}" - onclick={() => { modeOverride = "agent"; fetchAgents(); }} + onclick={async () => { + modeOverride = "agent"; + await fetchAgents(); + // Re-apply the active agent's settings (including cwd) + const current = agents.find(a => a.slug === activeAgentSlug); + const agentToApply = current ?? agents[0] ?? null; + if (agentToApply) { + onAgentChange(agentToApply); + // Force-update the input since the prop may not change (already set) + const cwdEl = document.getElementById("cwd-input") as HTMLInputElement | null; + if (cwdEl) cwdEl.value = agentToApply.cwd ?? ""; + } + }} > Agent </button> @@ -253,7 +265,11 @@ const modelCache = new Map<string, string[]>(); {#each agents as agent (agent.slug + ":" + agent.scope)} <button class="w-full text-left rounded-lg px-3 py-2 transition-colors {activeAgentSlug === agent.slug ? 'bg-primary text-primary-content' : 'bg-base-300 hover:bg-base-200'}" - onclick={() => onAgentChange(agent)} + onclick={() => { + onAgentChange(agent); + const cwdEl = document.getElementById("cwd-input") as HTMLInputElement | null; + if (cwdEl) cwdEl.value = agent.cwd ?? ""; + }} > <div class="flex items-center justify-between gap-2"> <span class="font-medium text-sm">{agent.name}</span> |
