summaryrefslogtreecommitdiffhomepage
path: root/src/app
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-21 15:36:54 +0900
committerAdam Malczewski <[email protected]>2026-10-21 15:36:54 +0900
commitac43e92d174ed1f081c387944e9044643ee9efc2 (patch)
tree3110ac7c1fa722ad6c092200ea02aead5384b5d3 /src/app
parente9786bf1dbf258b68f793870d02378c390939d3d (diff)
downloaddispatch-web-ac43e92d174ed1f081c387944e9044643ee9efc2.tar.gz
dispatch-web-ac43e92d174ed1f081c387944e9044643ee9efc2.zip
fix(review-followup-fee): recover omitted chat payloads through history
Diffstat (limited to 'src/app')
-rw-r--r--src/app/store.test.ts53
-rw-r--r--src/app/views/ChatPane.svelte1
-rw-r--r--src/app/ws-handlers.ts7
3 files changed, 59 insertions, 2 deletions
diff --git a/src/app/store.test.ts b/src/app/store.test.ts
index 40a0fa7..33f93bb 100644
--- a/src/app/store.test.ts
+++ b/src/app/store.test.ts
@@ -3076,3 +3076,56 @@ describe("compaction queued acknowledgement routing", () => {
store.dispose();
});
});
+
+it("routes omitted replay only to the matching watch/tab without WS resend or an error banner", async () => {
+ const ws = fakeSocket();
+ const store = createAppStore({
+ socketFactory: () => ws,
+ fetchImpl: fakeFetchImpl(),
+ indexedDB: new IDBFactory(),
+ localStorage: createFakeStorage(),
+ chatFlushScheduler: null,
+ });
+ try {
+ ws.resolveOpen();
+ const tab = store.activeChat;
+ const watch = store.watchConversation("omitted-watch");
+ const sentBefore = ws.sent.length;
+ ws.feedServerMessage({
+ type: "chat.resync-required",
+ conversationId: "unknown",
+ turnId: "unknown-turn",
+ reason: "replay-limit",
+ });
+ expect(watch.recovering).toBe(false);
+ expect(tab.recovering).toBe(false);
+ ws.feedServerMessage({
+ type: "chat.resync-required",
+ conversationId: "omitted-watch",
+ turnId: "t1",
+ reason: "replay-limit",
+ });
+ expect(watch.recovering).toBe(true);
+ expect(watch.generating).toBe(true);
+ expect(tab.recovering).toBe(false);
+ expect(watch.error).toBeNull();
+ expect(tab.error).toBeNull();
+ expect(store.fatalError).toBeNull();
+ expect(ws.sent).toHaveLength(sentBefore);
+ ws.feedServerMessage({
+ type: "chat.delta",
+ event: {
+ type: "turn-ended",
+ conversationId: "omitted-watch",
+ turnId: "t1",
+ reason: "aborted",
+ },
+ });
+ await vi.waitFor(() => expect(watch.pendingSync).toBe(false));
+ expect(watch.recovering).toBe(false);
+ expect(watch.generating).toBe(false);
+ expect(ws.sent).toHaveLength(sentBefore);
+ } finally {
+ store.dispose();
+ }
+});
diff --git a/src/app/views/ChatPane.svelte b/src/app/views/ChatPane.svelte
index d1630af..62b9344 100644
--- a/src/app/views/ChatPane.svelte
+++ b/src/app/views/ChatPane.svelte
@@ -80,6 +80,7 @@
{#key store.activeConversationId}
<ChatView
chunks={store.activeChat.chunks}
+ recovering={store.activeChat.recovering}
turnMetrics={store.activeChat.turnMetrics}
hasEarlier={store.activeChat.hasEarlier}
onShowEarlier={handleShowEarlier}
diff --git a/src/app/ws-handlers.ts b/src/app/ws-handlers.ts
index aa1e03e..b9adb30 100644
--- a/src/app/ws-handlers.ts
+++ b/src/app/ws-handlers.ts
@@ -2,6 +2,7 @@ import type {
ChatDeltaMessage,
ChatErrorMessage,
ChatQueuedMessage,
+ ChatResyncRequiredMessage,
ConversationCompactedMessage,
ConversationOpenMessage,
ConversationStatusChangedMessage,
@@ -53,7 +54,9 @@ export function createWsHandlers(deps: {
layoutStore,
tileLayoutStore,
} = deps;
- function handleChatMessage(msg: ChatDeltaMessage | ChatErrorMessage | ChatQueuedMessage): void {
+ function handleChatMessage(
+ msg: ChatDeltaMessage | ChatErrorMessage | ChatQueuedMessage | ChatResyncRequiredMessage,
+ ): void {
let targetId: string | undefined;
if (msg.type === "chat.delta") {
targetId = msg.event.conversationId;
@@ -69,7 +72,7 @@ export function createWsHandlers(deps: {
}
}
- if (msg.type === "chat.queued") return;
+ if (msg.type === "chat.queued" || msg.type === "chat.resync-required") return;
// fallback: try all stores (chat.error without conversationId)
for (const store of registry.chatStores.values()) {