From 0ace2f960f634c27199468c214cebd0154c998db Mon Sep 17 00:00:00 2001 From: Marius Date: Mon, 20 Apr 2026 10:09:38 +0200 Subject: [PATCH] fix(vscode): preserve older messages when paginating virtualized chat (#9194) The fill() helper that repairs pages starting with an assistant turn kept only the user-turn suffix of each older page, so earlier messages fetched mid-stitch were dropped while the pagination cursor still advanced past them. That made the top of the virtualized history land after the real first message. Keep the full older page when stitching partial turns so fill() terminates naturally once a user message reaches the top, and add a regression test covering multi-page fills. --- .changeset/restore-virtual-history.md | 5 ++ .../src/kilo-provider/message-page.ts | 8 +-- .../tests/unit/message-page.test.ts | 51 ++++++++++++++++++- 3 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 .changeset/restore-virtual-history.md 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() + }) })