Skip to content

Commit 729bd43

Browse files
authored
fix(app): preload more timeline messages (#36172)
1 parent 7304bb2 commit 729bd43

3 files changed

Lines changed: 72 additions & 41 deletions

File tree

packages/app/e2e/regression/session-timeline-history-root.spec.ts

Lines changed: 69 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ import { mockOpenCodeServer } from "../utils/mock-server"
1717
import { installSseTransport } from "../utils/sse-transport"
1818
import { expectSessionTitle } from "../utils/waits"
1919

20-
const assistants = Array.from({ length: 14 }, (_, index) =>
20+
const initialPageSize = 20
21+
const historyPageSize = 200
22+
const assistants = Array.from({ length: initialPageSize + 1 }, (_, index) =>
2123
assistantMessage([textPart(`prt_history_root_${index}`, `Assistant response ${index}`)], {
2224
id: `msg_${String(index + 1001).padStart(4, "0")}_history_root_assistant`,
2325
parentID: userID,
2426
created: 1700000001000 + index * 1_000,
25-
completed: index < 13,
27+
completed: index < initialPageSize,
2628
}),
2729
)
2830
const messages = [userMessage(), ...assistants]
@@ -46,7 +48,7 @@ const scenarios = [
4648
test.use({ viewport: { width: 646, height: 1385 } })
4749

4850
for (const scenario of scenarios) {
49-
test(`keeps the latest user turn visible through ${scenario.name}`, async ({ page }) => {
51+
test(`keeps visible timeline content visible through ${scenario.name}`, async ({ page }) => {
5052
const requests: { before?: string; phase: "start" | "end" }[] = []
5153
const pages: { before?: string; limit: number }[] = []
5254
const roots: { sessionID: string; messageID: string }[] = []
@@ -101,36 +103,60 @@ for (const scenario of scenarios) {
101103
}
102104
},
103105
})
104-
await page.addInitScript(
105-
({ userPartID, lastPartID }) => {
106-
const state = { armed: false, hidden: false, samples: 0, stop: false }
107-
;(window as Window & { __historyRootProbe?: typeof state }).__historyRootProbe = state
108-
const sample = () => {
109-
if (state.armed) {
110-
const virtual = document.querySelector<HTMLElement>("[data-timeline-virtual-content]")
111-
const viewport = virtual?.closest<HTMLElement>(".scroll-view__viewport")
112-
const view = viewport?.getBoundingClientRect()
113-
const visible = (partID: string) => {
114-
const part = viewport?.querySelector<HTMLElement>(`[data-timeline-part-id="${partID}"]`)
115-
const rect = part?.getBoundingClientRect()
116-
return (
117-
!!rect &&
118-
!!view &&
119-
rect.width > 0 &&
120-
rect.height > 0 &&
121-
rect.bottom > view.top &&
122-
rect.top < view.bottom
123-
)
124-
}
125-
if (!virtual || !visible(userPartID) || !visible(lastPartID)) state.hidden = true
126-
state.samples++
106+
await page.addInitScript(() => {
107+
const visibleParts = () => {
108+
const virtual = document.querySelector<HTMLElement>("[data-timeline-virtual-content]")
109+
const viewport = virtual?.closest<HTMLElement>(".scroll-view__viewport")
110+
const view = viewport?.getBoundingClientRect()
111+
if (!viewport || !view) return []
112+
return [...viewport.querySelectorAll<HTMLElement>("[data-timeline-part-id]")]
113+
.filter((part) => {
114+
const rect = part.getBoundingClientRect()
115+
return rect.width > 0 && rect.height > 0 && rect.bottom > view.top && rect.top < view.bottom
116+
})
117+
.flatMap((part) => (part.dataset.timelinePartId ? [part.dataset.timelinePartId] : []))
118+
}
119+
const state = {
120+
armed: false,
121+
hidden: false,
122+
visibleParts: [] as string[],
123+
samples: 0,
124+
stop: false,
125+
arm() {
126+
state.visibleParts = visibleParts()
127+
state.armed = true
128+
},
129+
}
130+
;(window as Window & { __historyRootProbe?: typeof state }).__historyRootProbe = state
131+
const sample = () => {
132+
if (state.armed) {
133+
const virtual = document.querySelector<HTMLElement>("[data-timeline-virtual-content]")
134+
const viewport = virtual?.closest<HTMLElement>(".scroll-view__viewport")
135+
const view = viewport?.getBoundingClientRect()
136+
const visible = (partID: string) => {
137+
const part = viewport?.querySelector<HTMLElement>(`[data-timeline-part-id="${CSS.escape(partID)}"]`)
138+
const rect = part?.getBoundingClientRect()
139+
return (
140+
!!rect &&
141+
!!view &&
142+
rect.width > 0 &&
143+
rect.height > 0 &&
144+
rect.bottom > view.top &&
145+
rect.top < view.bottom
146+
)
127147
}
128-
if (!state.stop) requestAnimationFrame(() => setTimeout(sample, 0))
148+
if (
149+
!virtual ||
150+
state.visibleParts.length === 0 ||
151+
state.visibleParts.some((partID) => !visible(partID))
152+
)
153+
state.hidden = true
154+
state.samples++
129155
}
130-
requestAnimationFrame(() => setTimeout(sample, 0))
131-
},
132-
{ userPartID, lastPartID },
133-
)
156+
if (!state.stop) requestAnimationFrame(() => setTimeout(sample, 0))
157+
}
158+
requestAnimationFrame(() => setTimeout(sample, 0))
159+
})
134160

135161
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
136162
await transport.waitForConnection()
@@ -143,23 +169,28 @@ for (const scenario of scenarios) {
143169
"messages:start:latest",
144170
"messages:end:latest",
145171
`message:${userID}`,
146-
`messages:start:${messages.at(-2)!.info.id}`,
172+
`messages:start:${messages.at(-initialPageSize)!.info.id}`,
147173
])
174+
await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(initialPageSize)
148175
await page.evaluate(() => {
149176
;(
150177
window as Window & {
151-
__historyRootProbe?: { armed: boolean }
178+
__historyRootProbe?: { arm(): void }
152179
}
153-
).__historyRootProbe!.armed = true
180+
).__historyRootProbe!.arm()
154181
})
155182
await waitForProbeSamples(page, 0)
156-
expect(await historyRootHidden(page)).toBe(false)
183+
expect(await visibleContentHidden(page)).toBe(false)
157184
const beforeHistory = await probeSamples(page)
158185
history.resolve()
159-
await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(14)
186+
await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(assistants.length)
187+
await expect.poll(() => requests.filter((request) => request.phase === "end").length).toBe(2)
160188
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
161189
await waitForProbeSamples(page, beforeHistory)
162-
expect(pages[0]).toEqual({ before: undefined, limit: 2 })
190+
expect(pages).toEqual([
191+
{ before: undefined, limit: initialPageSize },
192+
{ before: messages.at(-initialPageSize)!.info.id, limit: historyPageSize },
193+
])
163194
expect(roots).toEqual([{ sessionID, messageID: userID }])
164195

165196
const message = messageUpdated(scenario.info)
@@ -213,7 +244,7 @@ async function waitForProbeSamples(page: Page, after: number) {
213244
)
214245
}
215246

216-
function historyRootHidden(page: Page) {
247+
function visibleContentHidden(page: Page) {
217248
return page.evaluate(
218249
() => (window as Window & { __historyRootProbe?: { hidden: boolean } }).__historyRootProbe!.hidden,
219250
)

packages/app/src/context/server-session.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ describe("server session", () => {
174174
await ctx.store.sync("root")
175175

176176
expect(ctx.get).toEqual([{ sessionID: "root" }])
177-
expect(ctx.messages).toEqual([{ sessionID: "root", limit: 2, before: undefined }])
177+
expect(ctx.messages).toEqual([{ sessionID: "root", limit: 20, before: undefined }])
178178
expect(ctx.store.data.message.root).toEqual([])
179179
})
180180

@@ -194,7 +194,7 @@ describe("server session", () => {
194194

195195
await store.sync("child")
196196

197-
expect(client.requests).toEqual([{ sessionID: "child", limit: 2, before: undefined }])
197+
expect(client.requests).toEqual([{ sessionID: "child", limit: 20, before: undefined }])
198198
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }])
199199
expect(store.data.message.child).toEqual([user, ...assistants])
200200
expect(store.history.more("child")).toBe(true)

packages/app/src/context/server-session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } fro
2121
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
2222
const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id)
2323
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
24-
const initialMessagePageSize = 2
24+
const initialMessagePageSize = 20
2525
const historyMessagePageSize = 200
2626
const sessionInfoLimit = 2_048
2727
const emptyIDs: ReadonlySet<string> = new Set()

0 commit comments

Comments
 (0)