diff --git a/src/client/src/appState.ts b/src/client/src/appState.ts index 9882631..f196641 100644 --- a/src/client/src/appState.ts +++ b/src/client/src/appState.ts @@ -10,6 +10,7 @@ export interface AppState { messagePageStart: number; messagePageTotal: number; isLoadingEarlierMessages: boolean; + isReceivingPartialStream: boolean; selectedProject: Project | undefined; selectedWorkspace: Workspace | undefined; selectedSession: SessionInfo | undefined; @@ -44,6 +45,7 @@ export function initialAppState(): AppState { messagePageStart: 0, messagePageTotal: 0, isLoadingEarlierMessages: false, + isReceivingPartialStream: false, selectedProject: undefined, selectedWorkspace: undefined, selectedSession: undefined, diff --git a/src/client/src/chatHistoryCache.test.ts b/src/client/src/chatHistoryCache.test.ts index a4ed906..5d57a16 100644 --- a/src/client/src/chatHistoryCache.test.ts +++ b/src/client/src/chatHistoryCache.test.ts @@ -12,9 +12,22 @@ describe("mergeChatHistory", () => { expect(merged).toEqual(page(0, 5, ["a", "b", "c", "d", "e"])); }); - it("uses incoming history when totals changed", () => { + it("keeps cached history when new messages were appended", () => { + const existing = page(0, 3, ["a", "b", "c"]); + const incoming = page(1, 4, ["b", "c", "d"]); + + expect(mergeChatHistory(existing, incoming)).toEqual(page(0, 4, ["a", "b", "c", "d"])); + }); + + it("uses incoming history when totals shrink", () => { const incoming = page(0, 2, ["fresh-a", "fresh-b"]); - expect(mergeChatHistory(page(0, 1, ["stale"]), incoming)).toEqual(incoming); + expect(mergeChatHistory(page(0, 3, ["stale-a", "stale-b", "stale-c"]), incoming)).toEqual(incoming); + }); + + it("uses incoming history instead of creating a gapped page", () => { + const incoming = page(8, 10, ["i", "j"]); + + expect(mergeChatHistory(page(0, 10, ["a", "b"]), incoming)).toEqual(incoming); }); }); diff --git a/src/client/src/chatHistoryCache.ts b/src/client/src/chatHistoryCache.ts index 78bd196..97891f6 100644 --- a/src/client/src/chatHistoryCache.ts +++ b/src/client/src/chatHistoryCache.ts @@ -36,7 +36,8 @@ export function writeChatHistoryCache(sessionId: string, page: RawMessagePage): } export function mergeChatHistory(existing: RawMessagePage | undefined, incoming: RawMessagePage): RawMessagePage { - if (existing?.total !== incoming.total) return incoming; + if (existing === undefined) return incoming; + if (existing.total > incoming.total) return incoming; const start = Math.min(existing.start, incoming.start); const end = Math.max(existing.start + existing.messages.length, incoming.start + incoming.messages.length); @@ -44,7 +45,15 @@ export function mergeChatHistory(existing: RawMessagePage | undefined, incoming: copyInto(messages, start, existing); copyInto(messages, start, incoming); - return { start, total: incoming.total, messages: messages.filter((message) => message !== undefined) }; + if (hasSparseEntries(messages)) return incoming; + return { start, total: incoming.total, messages }; +} + +function hasSparseEntries(messages: unknown[]): boolean { + for (let index = 0; index < messages.length; index += 1) { + if (!(index in messages) || messages[index] === undefined) return true; + } + return false; } function copyInto(target: unknown[], targetStart: number, page: RawMessagePage): void { diff --git a/src/client/src/components/ChatView.ts b/src/client/src/components/ChatView.ts index 009439b..3aecde5 100644 --- a/src/client/src/components/ChatView.ts +++ b/src/client/src/components/ChatView.ts @@ -28,6 +28,7 @@ export class ChatView extends LitElement { @property({ type: Number }) messageTotal = 0; @property({ type: Boolean }) hasMore = false; @property({ type: Boolean }) loadingMore = false; + @property({ type: Boolean }) isReceivingPartialStream = false; @property({ type: Boolean }) isCompacting = false; @property({ type: Number }) pendingMessageCount = 0; @property({ attribute: false }) status?: SessionStatus; @@ -85,6 +86,12 @@ export class ChatView extends LitElement { } private renderSessionActivity() { + if (this.isReceivingPartialStream) return html` + + `; if (!this.isCompacting) return null; return html`