mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
perf(app): stop reloading old history every time you return to a session tab (#59166)
This commit is contained in:
committed by
GitHub
Unverified
parent
2bf756e3ab
commit
4e0fca1617
@@ -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"]],
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user