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.
This commit is contained in:
Marius
2026-04-20 10:09:38 +02:00
committed by GitHub
parent 7038ce0612
commit 0ace2f960f
3 changed files with 56 additions and 8 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Restore earlier chat history when scrolling through virtualized message lists.
@@ -46,17 +46,11 @@ export async function fetchMessagePage(
return { items, cursor }
}
const suffix = (items: Awaited<ReturnType<typeof read>>["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<ReturnType<typeof read>>): Promise<Awaited<ReturnType<typeof read>>> => {
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 })
}
@@ -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()
})
})