summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 02:41:37 +0900
committerAdam Malczewski <[email protected]>2026-06-07 02:41:37 +0900
commit1144a8027a3d0446e407f98c5cddc3a8c78831d5 (patch)
treeac7d0039262cbc73ed418795524d17a16799ba47 /src/features/chat
parent1973da082ea69e123433e12a560cf1e3cbb04376 (diff)
downloaddispatch-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/chat')
-rw-r--r--src/features/chat/store.svelte.ts2
-rw-r--r--src/features/chat/store.test.ts58
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();
+ });
});