From 1144a8027a3d0446e407f98c5cddc3a8c78831d5 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sun, 7 Jun 2026 02:41:37 +0900 Subject: fix: optimistic user message echo + tabs persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1 (sent message didn't appear until turn end): the transcript only folded assistant AgentEvents, so the user's own message showed only after turn-sealed resync. Add core/chunks appendUserMessage() (provisional user chunk, superseded on history sync) and call it in chat send() — the message now renders instantly. Bug 2 (tabs didn't persist on refresh): the app passed { storage: undefined } to createLocalStore, which the adapter treats as a no-op store, so nothing was saved. Default to globalThis.localStorage. Regression test exercises the non-injected path. Also updated app store tests for the echo (assistant-vs-user chunk filtering). Verified: svelte-check 0/0, vitest 288 (stable x2), biome clean, build ok. --- src/features/chat/store.svelte.ts | 2 ++ src/features/chat/store.test.ts | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) (limited to 'src/features') diff --git a/src/features/chat/store.svelte.ts b/src/features/chat/store.svelte.ts index e997f49..1d8ab17 100644 --- a/src/features/chat/store.svelte.ts +++ b/src/features/chat/store.svelte.ts @@ -6,6 +6,7 @@ import type { import type { ChatMessage } from "@dispatch/wire"; import type { RenderedChunk, TranscriptState } from "../../core/chunks"; import { + appendUserMessage, applyHistory, foldEvent, initialState, @@ -94,6 +95,7 @@ export function createChatStore(deps: ChatStoreDependencies): ChatStore { }, send(text: string): void { + transcript = appendUserMessage(transcript, text); const msg: ChatSendMessage = { type: "chat.send", conversationId: deps.conversationId, diff --git a/src/features/chat/store.test.ts b/src/features/chat/store.test.ts index 4ec40a9..de60b14 100644 --- a/src/features/chat/store.test.ts +++ b/src/features/chat/store.test.ts @@ -436,4 +436,62 @@ describe("createChatStore", () => { store.dispose(); }); + + it("send optimistically shows the user message immediately", () => { + const transport = createFakeTransport(); + const historySync = createFakeHistorySync(); + const cache = createFakeCache(); + const store = createChatStore({ + conversationId: CONV_ID, + transport: transport.impl, + historySync: historySync.impl, + cache: cache.impl, + }); + + store.send("hi"); + + expect(store.messages).toHaveLength(1); + expect(store.messages[0]?.role).toBe("user"); + expect(store.messages[0]?.chunks).toHaveLength(1); + expect(store.messages[0]?.chunks[0]?.type).toBe("text"); + expect((store.messages[0]?.chunks[0] as { type: "text"; text: string }).text).toBe("hi"); + + store.dispose(); + }); + + it("the optimistic user message is replaced after turn-sealed + history sync", async () => { + const transport = createFakeTransport(); + const historySync = createFakeHistorySync(); + const cache = createFakeCache(); + const store = createChatStore({ + conversationId: CONV_ID, + transport: transport.impl, + historySync: historySync.impl, + cache: cache.impl, + }); + + historySync.returnChunks = [ + { seq: 1, role: "user", chunk: { type: "text", text: "hi" } }, + { seq: 2, role: "assistant", chunk: { type: "text", text: "hello!" } }, + ]; + + store.send("hi"); + expect(store.messages).toHaveLength(1); + expect(store.messages[0]?.role).toBe("user"); + + store.handleDelta(deltaEvent({ type: "turn-start", conversationId: CONV_ID, turnId: "t1" })); + store.handleDelta( + deltaEvent({ type: "text-delta", conversationId: CONV_ID, turnId: "t1", delta: "hello!" }), + ); + store.handleDelta(deltaEvent({ type: "turn-sealed", conversationId: CONV_ID, turnId: "t1" })); + + await vi.waitFor(() => { + expect(store.messages.length).toBe(2); + }); + + expect(store.messages[0]?.role).toBe("user"); + expect(store.messages[1]?.role).toBe("assistant"); + + store.dispose(); + }); }); -- cgit v1.2.3