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`