summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-22 20:54:19 +0900
committerAdam Malczewski <[email protected]>2026-05-22 20:54:19 +0900
commitc47346cc6237044ecb60ff22c4011d89744af581 (patch)
tree2359a25e687e1290ba5180fd60eae83b03b53a23 /packages/frontend/src/lib/components
parent288b21cec98421fda57028a0c8c9d835cfbb14b0 (diff)
downloaddispatch-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')
-rw-r--r--packages/frontend/src/lib/components/ChatInput.svelte5
-rw-r--r--packages/frontend/src/lib/components/ChatMessage.svelte29
-rw-r--r--packages/frontend/src/lib/components/ChatPanel.svelte3
-rw-r--r--packages/frontend/src/lib/components/ModelSelector.svelte22
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>