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/app | |
| 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/app')
| -rw-r--r-- | src/app/store.svelte.ts | 2 | ||||
| -rw-r--r-- | src/app/store.test.ts | 66 |
2 files changed, 60 insertions, 8 deletions
diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts index 07d850b..760c390 100644 --- a/src/app/store.svelte.ts +++ b/src/app/store.svelte.ts @@ -100,7 +100,7 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { const fetchImpl = opts?.fetchImpl ?? globalThis.fetch.bind(globalThis); const indexedDBFactory = opts?.indexedDB ?? globalThis.indexedDB; - const localStorageOpt = opts?.localStorage; + const localStorageOpt = opts?.localStorage ?? globalThis.localStorage; const storageAdapter = createLocalStore<TabsState>("dispatch.tabs", { storage: localStorageOpt, diff --git a/src/app/store.test.ts b/src/app/store.test.ts index dabc80d..86a21d6 100644 --- a/src/app/store.test.ts +++ b/src/app/store.test.ts @@ -388,9 +388,11 @@ describe("createAppStore", () => { }); expect(store.activeChat.chunks.length).toBeGreaterThan(0); - const textChunks = store.activeChat.chunks.filter((c) => c.chunk.type === "text"); - expect(textChunks).toHaveLength(1); - expect((textChunks[0]?.chunk as { type: "text"; text: string }).text).toBe("Hello world"); + const assistantChunks = store.activeChat.chunks.filter( + (c) => c.role === "assistant" && c.chunk.type === "text", + ); + expect(assistantChunks).toHaveLength(1); + expect((assistantChunks[0]?.chunk as { type: "text"; text: string }).text).toBe("Hello world"); store.dispose(); }); @@ -580,14 +582,19 @@ describe("createAppStore", () => { }); store.selectTab(convId1); - const textChunks1 = store.activeChat.chunks.filter((c) => c.chunk.type === "text"); - expect(textChunks1).toHaveLength(1); - expect((textChunks1[0]?.chunk as { type: "text"; text: string }).text).toBe( + const assistantChunks1 = store.activeChat.chunks.filter( + (c) => c.role === "assistant" && c.chunk.type === "text", + ); + expect(assistantChunks1).toHaveLength(1); + expect((assistantChunks1[0]?.chunk as { type: "text"; text: string }).text).toBe( "response to first", ); store.selectTab(convId2); - expect(store.activeChat.chunks).toEqual([]); + const assistantChunks2 = store.activeChat.chunks.filter( + (c) => c.role === "assistant" && c.chunk.type === "text", + ); + expect(assistantChunks2).toEqual([]); store.dispose(); }); @@ -654,6 +661,51 @@ describe("createAppStore", () => { store2.dispose(); }); + it("tabs persist to globalThis.localStorage when no storage is injected", () => { + const realLs = globalThis.localStorage; + const memLs = createFakeStorage(); + globalThis.localStorage = memLs; + try { + const ws1 = fakeSocket(); + const store = createAppStore({ + socketFactory: () => ws1, + fetchImpl: fakeFetchImpl(), + }); + ws1.resolveOpen(); + + store.send("persist via default"); + const convId = store.tabs[0]?.conversationId; + const title = store.tabs[0]?.title; + expect(convId).toBeDefined(); + expect(title).toBeDefined(); + + const raw = globalThis.localStorage.getItem("dispatch.tabs"); + expect(raw).not.toBeNull(); + const parsed = JSON.parse(raw as string); + expect(parsed.tabs).toHaveLength(1); + expect(parsed.tabs[0].conversationId).toBe(convId); + expect(parsed.tabs[0].title).toBe(title); + + store.dispose(); + + const ws2 = fakeSocket(); + const store2 = createAppStore({ + socketFactory: () => ws2, + fetchImpl: fakeFetchImpl(), + }); + ws2.resolveOpen(); + + expect(store2.tabs).toHaveLength(1); + expect(store2.tabs[0]?.conversationId).toBe(convId); + expect(store2.tabs[0]?.title).toBe(title); + expect(store2.activeConversationId).toBe(convId); + + store2.dispose(); + } finally { + globalThis.localStorage = realLs; + } + }); + it("newDraft resets to draft mode", () => { const ws = fakeSocket(); const store = createAppStore({ |
