perf(app): stop reloading old history every time you return to a session tab (#59166)

This commit is contained in:
Kit Langton
2026-10-26 10:48:41 -07:00
committed by GitHub
Unverified
parent 2bf756e3ab
commit 4e0fca1617
3 changed files with 194 additions and 7 deletions
@@ -13,10 +13,70 @@ import {
userMessage,
} from "../utils/timeline"
import { expectSessionTitle } from "../utils/waits"
import { openSession } from "../utils/workspace"
test.use({ serviceWorkers: "block" })
test.describe("timeline history", () => {
test("restoring a paged transcript waits for another upward scroll before loading history", async ({ page }) => {
const id = "ses_history_restore"
const other = "ses_history_other"
const text = Array.from({ length: 40 }, (_, index) => `Paragraph ${index}.`).join("\n\n")
const messages = [
userMessage([textPart("prt_restore_0", "Original prompt")], { id: "msg_restore_0", created: 1 }),
assistantMessage([textPart("prt_restore_1", text)], { id: "msg_restore_1", created: 2 }),
userMessage([textPart("prt_restore_2", "Middle prompt")], { id: "msg_restore_2", created: 3 }),
assistantMessage([textPart("prt_restore_3", text)], { id: "msg_restore_3", created: 4 }),
userMessage([textPart("prt_restore_4", "Another prompt")], { id: "msg_restore_4", created: 5 }),
assistantMessage([textPart("prt_restore_5", text)], { id: "msg_restore_5", created: 6 }),
userMessage([textPart("prt_restore_6", "Latest prompt")], { id: "msg_restore_6", created: 7 }),
assistantMessage([textPart("prt_restore_7", text)], { id: "msg_restore_7", created: 8 }),
]
const requests: (string | undefined)[] = []
await page.clock.install()
await openSession(page, {
name: "HistoryRestore",
sessions: [{ id }, { id: other, directory: "C:/OpenCode/HistoryOther" }],
pageMessages: (session, _limit, before) => {
if (session === other)
return { items: [userMessage([textPart("prt_other", "Other session")], { id: "msg_other" })] }
requests.push(before)
if (before === "oldest") return { items: messages.slice(0, 1) }
if (before === "older") return { items: messages.slice(1, -2), cursor: "oldest" }
return { items: messages.slice(-2), cursor: "older" }
},
})
const scroller = timelineScroller(page)
await expect(page.locator("[data-timeline-virtual-content]")).toHaveCSS("visibility", "visible")
// One upward gesture, without wheel momentum carrying across the page prepend.
await scroller.evaluate((element) => {
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, deltaY: -1 }))
element.scrollTop = 0
})
await expect.poll(() => requests).toEqual([undefined, "older"])
await expect.poll(() => scroller.evaluate((element) => element.scrollTop)).toBeGreaterThan(200)
await page.locator(`[data-slot="titlebar-tabs"] a[href="${sessionHref(other)}"]`).click()
await expect(page.getByText("Other session", { exact: true })).toBeVisible()
await page.locator(`[data-slot="titlebar-tabs"] a[href="${sessionHref(id)}"]`).click()
await expect(page.locator("[data-timeline-virtual-content]")).toHaveCSS("visibility", "visible")
// Exhaust the delayed leading-turn enrichment without a real-time sleep.
await page.clock.runFor(1_000)
expect(requests).toEqual([undefined, "older"])
await scroller.evaluate((element) => {
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, deltaY: -1 }))
element.scrollTop = 0
})
await expect(page.getByText("Original prompt", { exact: true })).toBeInViewport()
expect(requests).toEqual([undefined, "older", "oldest"])
})
test("keeps visible rows fixed while an older page prepends", async ({ page }) => {
const older = Promise.withResolvers<void>()
const requests: { before?: string; phase: "start" | "end" }[] = []
@@ -149,6 +209,7 @@ test.describe("timeline history", () => {
const roots: { sessionID: string; messageID: string }[] = []
const sequence: string[] = []
const messages = rootHistory()
// SAFETY: rootHistory ends each generated turn with an assistant message.
const last = messages.at(-1) as SessionMessageAssistant
await page.addInitScript(installVisibilityProbe)
@@ -199,9 +260,10 @@ test.describe("timeline history", () => {
const completed = {
...last,
time: { ...last.time, completed: last.time.created + 15_000 },
...(scenario.error ? { error: scenario.error } : {}),
}
if (scenario.error) completed.error = scenario.error
const message = messageUpdated(completed)
const idle = status("idle")
// Idle ends the working turn, so its last text part shows the response actions. A completed step changes
@@ -244,16 +306,16 @@ test.describe("timeline history", () => {
})
}
for (const shape of ["assistant-only", "mixed"] as const) {
test(`renders the ${shape} tail before parent hydration and preserves it afterward`, async ({ page }) => {
const session = { ...fixture.sessions[0]!, id: `ses_hydration_${shape}` }
for (const composition of ["assistant-only", "mixed"] as const) {
test(`renders the ${composition} tail before parent hydration and preserves it afterward`, async ({ page }) => {
const session = { ...fixture.sessions[0]!, id: `ses_hydration_${composition}` }
// Compact's initial 40 and the next 20 begin with an assistant; page three supplies its parent.
const messages = Array.from({ length: 61 }, (_, index): SessionMessageInfo => {
const id = `msg_hydration_${index}`
const time = { created: 1700000000000 + index * 1_000 }
if (index === 0 || (shape === "mixed" && index === 59))
if (index === 0 || (composition === "mixed" && index === 59))
return { id, type: "user", time, text: `Prompt ${index}` }
return {
@@ -324,7 +386,7 @@ test.describe("timeline history", () => {
await expectReadyTail()
await expect(orphan).toHaveAttribute("data-message-id", "msg_hydration_21")
if (shape === "mixed")
if (composition === "mixed")
await expect(
page.locator('[data-timeline-row="UserMessage"][data-message-id="msg_hydration_59"]'),
).toBeInViewport()
@@ -490,6 +552,7 @@ function installVisibilityProbe() {
.flatMap((part) => (part.dataset.timelinePartId ? [part.dataset.timelinePartId] : []))
}
// SAFETY: arm populates parts with visible part IDs; settle supplies the optional final state.
const state = {
armed: false,
hidden: false,
@@ -14,6 +14,122 @@ const assistant = (id: string): SessionMessageAssistant => ({
})
describe("timeline model", () => {
test("does not enrich a different leading turn after prepending to a retained transcript", async () => {
const messages: SessionMessageInfo[] = [user("msg_user"), assistant("msg_latest")]
let loads = 0
const input = {
current: () => true,
messages: () => messages,
more: () => true,
loading: () => false,
loadMore: async () => {
loads += 1
},
pause: async () => undefined,
maxPages: 3,
}
await enrichLeadingTurn(input)
messages.unshift(assistant("msg_older"))
await enrichLeadingTurn(input)
expect(loads).toBe(0)
// An evicted/reloaded transcript gets its own initial enrichment budget.
const reloaded = [...messages]
await enrichLeadingTurn({ ...input, messages: () => reloaded })
expect(loads).toBe(3)
})
const resumed = ["msg_previous_turn", "msg_parent", "msg_latest"]
test("resumes the original leading turn after switching away", async () => {
const messages: SessionMessageInfo[] = [assistant("msg_latest")]
let current = true
let loads = 0
const input = {
current: () => current,
messages: () => messages,
more: () => true,
loading: () => false,
loadMore: async () => {
loads += 1
messages.unshift(assistant("msg_previous_turn"), user("msg_parent"))
},
pause: async () => {
current = false
},
maxPages: 3,
}
await enrichLeadingTurn(input)
expect(loads).toBe(0)
current = true
await enrichLeadingTurn({ ...input, pause: async () => undefined })
expect(messages.map((message) => message.id)).toEqual(resumed)
expect(loads).toBe(1)
await enrichLeadingTurn({ ...input, pause: async () => undefined })
expect(loads).toBe(1)
})
test("resumes the original leading turn after a failed page", async () => {
const messages: SessionMessageInfo[] = [assistant("msg_latest")]
let loads = 0
const input = {
current: () => true,
messages: () => messages,
more: () => true,
loading: () => false,
loadMore: async () => {
loads += 1
if (loads === 1) throw new Error("history failed")
messages.unshift(assistant("msg_previous_turn"), user("msg_parent"))
},
pause: async () => undefined,
maxPages: 3,
}
await expect(enrichLeadingTurn(input)).rejects.toThrow("history failed")
await enrichLeadingTurn(input)
expect(messages.map((message) => message.id)).toEqual(resumed)
expect(loads).toBe(2)
await enrichLeadingTurn(input)
expect(loads).toBe(2)
})
test("resumes the original leading turn after the transcript reconnects", async () => {
const messages: SessionMessageInfo[] = [assistant("msg_latest")]
let loads = 0
const input = {
current: () => true,
messages: () => messages,
more: () => true,
loading: () => false,
loadMore: async () => {
loads += 1
messages.unshift(assistant("msg_previous_turn"), user("msg_parent"))
},
pause: async () => undefined,
maxPages: 3,
}
await enrichLeadingTurn(input)
messages.splice(0, messages.length, assistant("msg_latest"))
await enrichLeadingTurn(input)
expect(messages.map((message) => message.id)).toEqual(resumed)
expect(loads).toBe(2)
await enrichLeadingTurn(input)
expect(loads).toBe(2)
})
test.each([
["restores the anchor after one opaque cursor page", "ok", ["before", "load", "after", true]],
["does not restore an anchor after the session changes", "switch", ["before", "load"]],
+9 -1
View File
@@ -8,6 +8,10 @@ const leadingTurnPageDelay = 200
const leadingTurnPageLimit = 3
// The store preserves the transcript array across prepends and tab switches, and replaces it on eviction.
// Remember the original turn so interrupted enrichment can resume without chasing newly prepended turns.
const enrichmentTurns = new WeakMap<SessionMessageInfo[], string>()
export function createTimelineModel(input: { session: Pick<SessionModel, "identity" | "history" | "ownership"> }) {
const data = useData()
@@ -86,9 +90,13 @@ export async function enrichLeadingTurn(input: {
pause: () => Promise<void>
maxPages: number
}) {
const assistant = input.messages().find((message) => message.type === "assistant")?.id
const messages = input.messages()
if (!input.current()) return
const assistant = enrichmentTurns.get(messages) ?? messages.find((message) => message.type === "assistant")?.id
if (!assistant) return
enrichmentTurns.set(messages, assistant)
const load = async (pages: number): Promise<void> => {
if (