diff --git a/.changeset/restore-virtual-history.md b/.changeset/restore-virtual-history.md new file mode 100644 index 00000000000..433d931ca94 --- /dev/null +++ b/.changeset/restore-virtual-history.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Restore earlier chat history when scrolling through virtualized message lists. diff --git a/packages/kilo-vscode/src/kilo-provider/message-page.ts b/packages/kilo-vscode/src/kilo-provider/message-page.ts index b8fef78f2f3..aca1ba8b8a1 100644 --- a/packages/kilo-vscode/src/kilo-provider/message-page.ts +++ b/packages/kilo-vscode/src/kilo-provider/message-page.ts @@ -46,17 +46,11 @@ export async function fetchMessagePage( return { items, cursor } } - const suffix = (items: Awaited>["items"]) => { - const index = [...items].reverse().findIndex((item) => item.info.role === "user") - if (index === -1) return items - return items.slice(items.length - index - 1) - } - const fill = async (page: Awaited>): Promise>> => { if (page.items[0]?.info.role !== "assistant") return page if (!page.cursor || input.signal?.aborted) return page const next = await read(page.cursor) - const items = [...suffix(next.items), ...page.items] + const items = [...next.items, ...page.items] return fill({ items, cursor: next.cursor }) } diff --git a/packages/kilo-vscode/tests/unit/message-page.test.ts b/packages/kilo-vscode/tests/unit/message-page.test.ts index 4fed55e19d0..945ea2d6286 100644 --- a/packages/kilo-vscode/tests/unit/message-page.test.ts +++ b/packages/kilo-vscode/tests/unit/message-page.test.ts @@ -90,7 +90,7 @@ describe("fetchMessagePage / cursor fallback", () => { }) it("synthesized cursor round-trips through the server's before parameter", async () => { - // First page: server strips header, items fill limit → cursor synthesized. + // First page: server strips header, items fill limit -> cursor synthesized. // Next page request uses that cursor and returns more items. const { client, calls } = mockClient([ { @@ -115,4 +115,53 @@ describe("fetchMessagePage / cursor fallback", () => { }) expect(calls[1]?.before).toBe(first.cursor) }) + + it("keeps all fetched older messages when filling a partial assistant turn", async () => { + const { client, calls } = mockClient([ + { + items: [message("m4", "assistant", 40), message("m5", "user", 50)], + cursor: "c1", + }, + { + items: [message("m1", "user", 10), message("m2", "assistant", 20), message("m3", "user", 30)], + cursor: "c2", + }, + ]) + + const page = await fetchMessagePage(client as never, { + sessionID: "s1", + workspaceDir: "/repo", + limit: 3, + }) + + expect(calls.map((call) => call.before)).toEqual([undefined, "c1"]) + expect(page.items.map((item) => item.info.id)).toEqual(["m1", "m2", "m3", "m4", "m5"]) + expect(page.cursor).toBe("c2") + }) + + it("continues fetching until a partial assistant turn reaches the first user message", async () => { + const { client, calls } = mockClient([ + { + items: [message("m4", "assistant", 40), message("m5", "user", 50)], + cursor: "c1", + }, + { + items: [message("m2", "assistant", 20), message("m3", "user", 30)], + cursor: "c2", + }, + { + items: [message("m1", "user", 10)], + }, + ]) + + const page = await fetchMessagePage(client as never, { + sessionID: "s1", + workspaceDir: "/repo", + limit: 2, + }) + + expect(calls.map((call) => call.before)).toEqual([undefined, "c1", "c2"]) + expect(page.items.map((item) => item.info.id)).toEqual(["m1", "m2", "m3", "m4", "m5"]) + expect(page.cursor).toBeUndefined() + }) })