diff options
| author | Adam Malczewski <[email protected]> | 2026-06-07 02:41:37 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-07 02:41:37 +0900 |
| commit | 1144a8027a3d0446e407f98c5cddc3a8c78831d5 (patch) | |
| tree | ac7d0039262cbc73ed418795524d17a16799ba47 /src/features | |
| parent | 1973da082ea69e123433e12a560cf1e3cbb04376 (diff) | |
| download | dispatch-web-1144a8027a3d0446e407f98c5cddc3a8c78831d5.tar.gz dispatch-web-1144a8027a3d0446e407f98c5cddc3a8c78831d5.zip | |
fix: optimistic user message echo + tabs persistence
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.
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/chat/store.svelte.ts | 2 | ||||
| -rw-r--r-- | src/features/chat/store.test.ts | 58 |
2 files changed, 60 insertions, 0 deletions
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(); + }); }); |
