From c47346cc6237044ecb60ff22c4011d89744af581 Mon Sep 17 00:00:00 2001
From: Adam Malczewski <github@tradam.dev>
Date: Fri, 22 May 2026 20:54:19 +0900
Subject: 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
---
 .../frontend/src/lib/components/ChatMessage.svelte | 29 ++++++++++++++++++++--
 1 file changed, 27 insertions(+), 2 deletions(-)

(limited to 'packages/frontend/src/lib/components/ChatMessage.svelte')

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}
-- 
cgit v1.2.3

