From 7cf2edd0a91ce7135f19892820d1efe6d52571d0 Mon Sep 17 00:00:00 2001 From: ethan Date: Mon, 17 Nov 2025 13:23:41 +1100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A4=96=20feat:=20queue=20messages=20d?= =?UTF-8?q?uring=20streaming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/browser/App.stories.tsx | 3 + src/browser/api.ts | 1 + src/browser/components/AIView.tsx | 33 +- src/browser/components/ChatInput/index.tsx | 40 +- src/browser/components/ChatInput/types.ts | 2 + src/browser/components/CommandPalette.tsx | 7 +- .../components/Messages/MessageRenderer.tsx | 1 + .../components/Messages/MessageWindow.tsx | 4 +- .../components/Messages/QueuedMessage.tsx | 55 ++ src/browser/stores/WorkspaceStore.ts | 37 +- src/common/constants/events.ts | 5 +- src/common/constants/ipc-constants.ts | 1 + src/common/types/ipc.ts | 39 +- src/common/types/message.ts | 9 +- src/desktop/preload.ts | 2 + src/node/services/agentSession.ts | 56 +- src/node/services/ipcMain.ts | 22 +- src/node/services/messageQueue.test.ts | 214 ++++++++ src/node/services/messageQueue.ts | 110 ++++ tests/ipcMain/helpers.ts | 19 +- tests/ipcMain/queuedMessages.test.ts | 493 ++++++++++++++++++ tests/ipcMain/sendMessage.test.ts | 6 +- 22 files changed, 1127 insertions(+), 32 deletions(-) create mode 100644 src/browser/components/Messages/QueuedMessage.tsx create mode 100644 src/node/services/messageQueue.test.ts create mode 100644 src/node/services/messageQueue.ts create mode 100644 tests/ipcMain/queuedMessages.test.ts diff --git a/src/browser/App.stories.tsx b/src/browser/App.stories.tsx index b04b16cf25..fba7a7f5b3 100644 --- a/src/browser/App.stories.tsx +++ b/src/browser/App.stories.tsx @@ -67,6 +67,7 @@ function setupMockAPI(options: { sendMessage: () => Promise.resolve({ success: true, data: undefined }), resumeStream: () => Promise.resolve({ success: true, data: undefined }), interruptStream: () => Promise.resolve({ success: true, data: undefined }), + clearQueue: () => Promise.resolve({ success: true, data: undefined }), truncateHistory: () => Promise.resolve({ success: true, data: undefined }), replaceChatHistory: () => Promise.resolve({ success: true, data: undefined }), getInfo: () => Promise.resolve(null), @@ -1118,6 +1119,7 @@ export const ActiveWorkspaceWithChat: Story = { sendMessage: () => Promise.resolve({ success: true, data: undefined }), resumeStream: () => Promise.resolve({ success: true, data: undefined }), interruptStream: () => Promise.resolve({ success: true, data: undefined }), + clearQueue: () => Promise.resolve({ success: true, data: undefined }), truncateHistory: () => Promise.resolve({ success: true, data: undefined }), replaceChatHistory: () => Promise.resolve({ success: true, data: undefined }), getInfo: () => Promise.resolve(null), @@ -1408,6 +1410,7 @@ These tables should render cleanly without any disruptive copy or download actio sendMessage: () => Promise.resolve({ success: true, data: undefined }), resumeStream: () => Promise.resolve({ success: true, data: undefined }), interruptStream: () => Promise.resolve({ success: true, data: undefined }), + clearQueue: () => Promise.resolve({ success: true, data: undefined }), truncateHistory: () => Promise.resolve({ success: true, data: undefined }), replaceChatHistory: () => Promise.resolve({ success: true, data: undefined }), getInfo: () => Promise.resolve(null), diff --git a/src/browser/api.ts b/src/browser/api.ts index 449e57e238..4314b5c908 100644 --- a/src/browser/api.ts +++ b/src/browser/api.ts @@ -225,6 +225,7 @@ const webApi: IPCApi = { invokeIPC(IPC_CHANNELS.WORKSPACE_RESUME_STREAM, workspaceId, options), interruptStream: (workspaceId, options) => invokeIPC(IPC_CHANNELS.WORKSPACE_INTERRUPT_STREAM, workspaceId, options), + clearQueue: (workspaceId) => invokeIPC(IPC_CHANNELS.WORKSPACE_QUEUE_CLEAR, workspaceId), truncateHistory: (workspaceId, percentage) => invokeIPC(IPC_CHANNELS.WORKSPACE_TRUNCATE_HISTORY, workspaceId, percentage), replaceChatHistory: (workspaceId, summaryMessage) => diff --git a/src/browser/components/AIView.tsx b/src/browser/components/AIView.tsx index 556d6260cc..cb43414f31 100644 --- a/src/browser/components/AIView.tsx +++ b/src/browser/components/AIView.tsx @@ -27,6 +27,7 @@ import type { DisplayedMessage } from "@/common/types/message"; import type { RuntimeConfig } from "@/common/types/runtime"; import { useAIViewKeybinds } from "@/browser/hooks/useAIViewKeybinds"; import { evictModelFromLRU } from "@/browser/hooks/useModelLRU"; +import { QueuedMessage } from "./Messages/QueuedMessage"; interface AIViewProps { workspaceId: string; @@ -141,8 +142,28 @@ const AIViewInner: React.FC = ({ setEditingMessage({ id: messageId, content }); }, []); - const handleEditLastUserMessage = useCallback(() => { + const handleEditQueuedMessage = useCallback(async () => { + const queuedMessage = workspaceState?.queuedMessage; + if (!queuedMessage) return; + + await window.api.workspace.clearQueue(workspaceId); + chatInputAPI.current?.restoreText(queuedMessage.content); + + // Restore images if present + if (queuedMessage.imageParts && queuedMessage.imageParts.length > 0) { + chatInputAPI.current?.restoreImages(queuedMessage.imageParts); + } + }, [workspaceId, workspaceState?.queuedMessage, chatInputAPI]); + + const handleEditLastUserMessage = useCallback(async () => { if (!workspaceState) return; + + if (workspaceState.queuedMessage) { + await handleEditQueuedMessage(); + return; + } + + // Otherwise, edit last user message const mergedMessages = mergeConsecutiveStreamErrors(workspaceState.messages); const lastUserMessage = [...mergedMessages] .reverse() @@ -159,7 +180,7 @@ const AIViewInner: React.FC = ({ element?.scrollIntoView({ behavior: "smooth", block: "center" }); }); } - }, [workspaceState, contentRef, setAutoScroll]); + }, [workspaceState, contentRef, setAutoScroll, handleEditQueuedMessage]); const handleCancelEdit = useCallback(() => { setEditingMessage(undefined); @@ -458,6 +479,12 @@ const AIViewInner: React.FC = ({ /> )} + {workspaceState?.queuedMessage && ( + void handleEditQueuedMessage()} + /> + )} {!autoScroll && (