fix(app): preload more timeline messages (#36172)

This commit is contained in:
Luke Parker
2026-07-14 21:48:38 +10:00
committed by GitHub
parent 7304bb2751
commit 729bd438e4
3 changed files with 72 additions and 41 deletions
@@ -17,12 +17,14 @@ import { mockOpenCodeServer } from "../utils/mock-server"
import { installSseTransport } from "../utils/sse-transport" import { installSseTransport } from "../utils/sse-transport"
import { expectSessionTitle } from "../utils/waits" import { expectSessionTitle } from "../utils/waits"
const assistants = Array.from({ length: 14 }, (_, index) => const initialPageSize = 20
const historyPageSize = 200
const assistants = Array.from({ length: initialPageSize + 1 }, (_, index) =>
assistantMessage([textPart(`prt_history_root_${index}`, `Assistant response ${index}`)], { assistantMessage([textPart(`prt_history_root_${index}`, `Assistant response ${index}`)], {
id: `msg_${String(index + 1001).padStart(4, "0")}_history_root_assistant`, id: `msg_${String(index + 1001).padStart(4, "0")}_history_root_assistant`,
parentID: userID, parentID: userID,
created: 1700000001000 + index * 1_000, created: 1700000001000 + index * 1_000,
completed: index < 13, completed: index < initialPageSize,
}), }),
) )
const messages = [userMessage(), ...assistants] const messages = [userMessage(), ...assistants]
@@ -46,7 +48,7 @@ const scenarios = [
test.use({ viewport: { width: 646, height: 1385 } }) test.use({ viewport: { width: 646, height: 1385 } })
for (const scenario of scenarios) { for (const scenario of scenarios) {
test(`keeps the latest user turn visible through ${scenario.name}`, async ({ page }) => { test(`keeps visible timeline content visible through ${scenario.name}`, async ({ page }) => {
const requests: { before?: string; phase: "start" | "end" }[] = [] const requests: { before?: string; phase: "start" | "end" }[] = []
const pages: { before?: string; limit: number }[] = [] const pages: { before?: string; limit: number }[] = []
const roots: { sessionID: string; messageID: string }[] = [] const roots: { sessionID: string; messageID: string }[] = []
@@ -101,9 +103,30 @@ for (const scenario of scenarios) {
} }
}, },
}) })
await page.addInitScript( await page.addInitScript(() => {
({ userPartID, lastPartID }) => { const visibleParts = () => {
const state = { armed: false, hidden: false, samples: 0, stop: false } const virtual = document.querySelector<HTMLElement>("[data-timeline-virtual-content]")
const viewport = virtual?.closest<HTMLElement>(".scroll-view__viewport")
const view = viewport?.getBoundingClientRect()
if (!viewport || !view) return []
return [...viewport.querySelectorAll<HTMLElement>("[data-timeline-part-id]")]
.filter((part) => {
const rect = part.getBoundingClientRect()
return rect.width > 0 && rect.height > 0 && rect.bottom > view.top && rect.top < view.bottom
})
.flatMap((part) => (part.dataset.timelinePartId ? [part.dataset.timelinePartId] : []))
}
const state = {
armed: false,
hidden: false,
visibleParts: [] as string[],
samples: 0,
stop: false,
arm() {
state.visibleParts = visibleParts()
state.armed = true
},
}
;(window as Window & { __historyRootProbe?: typeof state }).__historyRootProbe = state ;(window as Window & { __historyRootProbe?: typeof state }).__historyRootProbe = state
const sample = () => { const sample = () => {
if (state.armed) { if (state.armed) {
@@ -111,7 +134,7 @@ for (const scenario of scenarios) {
const viewport = virtual?.closest<HTMLElement>(".scroll-view__viewport") const viewport = virtual?.closest<HTMLElement>(".scroll-view__viewport")
const view = viewport?.getBoundingClientRect() const view = viewport?.getBoundingClientRect()
const visible = (partID: string) => { const visible = (partID: string) => {
const part = viewport?.querySelector<HTMLElement>(`[data-timeline-part-id="${partID}"]`) const part = viewport?.querySelector<HTMLElement>(`[data-timeline-part-id="${CSS.escape(partID)}"]`)
const rect = part?.getBoundingClientRect() const rect = part?.getBoundingClientRect()
return ( return (
!!rect && !!rect &&
@@ -122,15 +145,18 @@ for (const scenario of scenarios) {
rect.top < view.bottom rect.top < view.bottom
) )
} }
if (!virtual || !visible(userPartID) || !visible(lastPartID)) state.hidden = true if (
!virtual ||
state.visibleParts.length === 0 ||
state.visibleParts.some((partID) => !visible(partID))
)
state.hidden = true
state.samples++ state.samples++
} }
if (!state.stop) requestAnimationFrame(() => setTimeout(sample, 0)) if (!state.stop) requestAnimationFrame(() => setTimeout(sample, 0))
} }
requestAnimationFrame(() => setTimeout(sample, 0)) requestAnimationFrame(() => setTimeout(sample, 0))
}, })
{ userPartID, lastPartID },
)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await transport.waitForConnection() await transport.waitForConnection()
@@ -143,23 +169,28 @@ for (const scenario of scenarios) {
"messages:start:latest", "messages:start:latest",
"messages:end:latest", "messages:end:latest",
`message:${userID}`, `message:${userID}`,
`messages:start:${messages.at(-2)!.info.id}`, `messages:start:${messages.at(-initialPageSize)!.info.id}`,
]) ])
await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(initialPageSize)
await page.evaluate(() => { await page.evaluate(() => {
;( ;(
window as Window & { window as Window & {
__historyRootProbe?: { armed: boolean } __historyRootProbe?: { arm(): void }
} }
).__historyRootProbe!.armed = true ).__historyRootProbe!.arm()
}) })
await waitForProbeSamples(page, 0) await waitForProbeSamples(page, 0)
expect(await historyRootHidden(page)).toBe(false) expect(await visibleContentHidden(page)).toBe(false)
const beforeHistory = await probeSamples(page) const beforeHistory = await probeSamples(page)
history.resolve() history.resolve()
await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(14) await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(assistants.length)
await expect.poll(() => requests.filter((request) => request.phase === "end").length).toBe(2)
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible() await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
await waitForProbeSamples(page, beforeHistory) await waitForProbeSamples(page, beforeHistory)
expect(pages[0]).toEqual({ before: undefined, limit: 2 }) expect(pages).toEqual([
{ before: undefined, limit: initialPageSize },
{ before: messages.at(-initialPageSize)!.info.id, limit: historyPageSize },
])
expect(roots).toEqual([{ sessionID, messageID: userID }]) expect(roots).toEqual([{ sessionID, messageID: userID }])
const message = messageUpdated(scenario.info) const message = messageUpdated(scenario.info)
@@ -213,7 +244,7 @@ async function waitForProbeSamples(page: Page, after: number) {
) )
} }
function historyRootHidden(page: Page) { function visibleContentHidden(page: Page) {
return page.evaluate( return page.evaluate(
() => (window as Window & { __historyRootProbe?: { hidden: boolean } }).__historyRootProbe!.hidden, () => (window as Window & { __historyRootProbe?: { hidden: boolean } }).__historyRootProbe!.hidden,
) )
@@ -174,7 +174,7 @@ describe("server session", () => {
await ctx.store.sync("root") await ctx.store.sync("root")
expect(ctx.get).toEqual([{ sessionID: "root" }]) expect(ctx.get).toEqual([{ sessionID: "root" }])
expect(ctx.messages).toEqual([{ sessionID: "root", limit: 2, before: undefined }]) expect(ctx.messages).toEqual([{ sessionID: "root", limit: 20, before: undefined }])
expect(ctx.store.data.message.root).toEqual([]) expect(ctx.store.data.message.root).toEqual([])
}) })
@@ -194,7 +194,7 @@ describe("server session", () => {
await store.sync("child") await store.sync("child")
expect(client.requests).toEqual([{ sessionID: "child", limit: 2, before: undefined }]) expect(client.requests).toEqual([{ sessionID: "child", limit: 20, before: undefined }])
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }]) expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }])
expect(store.data.message.child).toEqual([user, ...assistants]) expect(store.data.message.child).toEqual([user, ...assistants])
expect(store.history.more("child")).toBe(true) expect(store.history.more("child")).toBe(true)
+1 -1
View File
@@ -21,7 +21,7 @@ import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } fro
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id) const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id)
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
const initialMessagePageSize = 2 const initialMessagePageSize = 20
const historyMessagePageSize = 200 const historyMessagePageSize = 200
const sessionInfoLimit = 2_048 const sessionInfoLimit = 2_048
const emptyIDs: ReadonlySet<string> = new Set() const emptyIDs: ReadonlySet<string> = new Set()