diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 0d7059704..2b15e1125 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -73,6 +73,7 @@ import { makeTimer } from "@solid-primitives/timer" import { scheduleConnectedMeasure } from "./measure" import { createTimelineProjection } from "./projection" import { MessageComment, SummaryDiff, TimelineRow, TimelineRowMap } from "./rows" +import { filterVirtualIndexes } from "./virtual-items" const emptyMessages: MessageType[] = [] const emptyParts: PartType[] = [] @@ -452,7 +453,10 @@ export function MessageTimeline(props: { const id = activeMessageID() const active = id ? (messageLastRowIndex().get(id) ?? -1) : -1 const indexes = defaultRangeExtractor({ ...range, overscan: renderOverscan() }) - return [...new Set([...resizePinnedIndexes, ...indexes, ...(active < 0 ? [] : [active])])].sort((a, b) => a - b) + return filterVirtualIndexes( + [...new Set([...resizePinnedIndexes, ...indexes, ...(active < 0 ? [] : [active])])].sort((a, b) => a - b), + range.count, + ) }, }) const resizeItem = virtualizer.resizeItem diff --git a/packages/app/src/pages/session/timeline/virtual-items.ts b/packages/app/src/pages/session/timeline/virtual-items.ts new file mode 100644 index 000000000..358bf4601 --- /dev/null +++ b/packages/app/src/pages/session/timeline/virtual-items.ts @@ -0,0 +1,3 @@ +export function filterVirtualIndexes(indexes: number[], count: number) { + return indexes.filter((index) => index >= 0 && index < count) +} diff --git a/packages/app/test-browser/solid-virtual.test.ts b/packages/app/test-browser/solid-virtual.test.ts index 445fc799c..727248d60 100644 --- a/packages/app/test-browser/solid-virtual.test.ts +++ b/packages/app/test-browser/solid-virtual.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test" -import { createVirtualizer } from "@tanstack/solid-virtual" +import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual" import { createRoot, createSignal } from "solid-js" +import { filterVirtualIndexes } from "@/pages/session/timeline/virtual-items" test("reactive count updates preserve measured row sizes", () => { createRoot((dispose) => { @@ -44,3 +45,26 @@ test("logical scroll offset includes pending measurement adjustments", () => { dispose() }) }) + +test("stale pinned indexes do not produce missing virtual items after count shrinks", () => { + createRoot((dispose) => { + const [count, setCount] = createSignal(2) + const pinned = [1] + const virtualizer = createVirtualizer({ + get count() { + return count() + }, + getScrollElement: () => null, + estimateSize: () => 60, + initialRect: { width: 800, height: 600 }, + rangeExtractor: (range) => + filterVirtualIndexes([...new Set([...defaultRangeExtractor(range), ...pinned])], range.count), + }) + + expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([0, 1]) + setCount(1) + expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([0]) + expect(() => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item]))).not.toThrow() + dispose() + }) +}) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index a01cc9d12..3481a853c 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -1242,7 +1242,7 @@ it.instance( } }), { git: true }, - 3_000, + 10_000, ) // Queue semantics @@ -1669,7 +1669,7 @@ it.instance( expect(yield* llm.calls).toBe(1) }), { git: true }, - 3_000, + 10_000, ) unix(