fix(app): preload more timeline messages (#36172)
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user