mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-10-21 05:06:37 +00:00
fix(history): preserve manual timeline browsing during pending jumps
Cancel deferred active-marker reveals when the reader scrolls or interacts with the global rail, and reveal without a smooth animation. A previous selection must not move or unmount the next click target while its distant window is still loading. Extend the Chromium latest-intent regression to keep the next destination visible beyond the reveal delay. Update the isolated native pruning UI fixture to proxy history routes and provide the same service-location inventory used by whole-session cleanup. Validated all five browser navigation cases, the UI typecheck, packaged-resource smoke checks, and the complete isolated OpenCode 2.0.5 native/UI pruning fixture, including whole-session confirmation and persistence after reload.
This commit is contained in:
parent
d9de25b360
commit
009a0aaf29
3 changed files with 29 additions and 10 deletions
|
|
@ -383,22 +383,27 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||
|
||||
const [scrollElement, setScrollElement] = createSignal<HTMLDivElement | undefined>()
|
||||
const [virtualizerHandle, setVirtualizerHandle] = createSignal<VirtualizerHandle | undefined>()
|
||||
let revealTimer: ReturnType<typeof setTimeout> | undefined
|
||||
const cancelReveal = () => {
|
||||
if (revealTimer !== undefined) clearTimeout(revealTimer)
|
||||
revealTimer = undefined
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
cancelReveal()
|
||||
if (hoveredSegment()) clearHoverPreview()
|
||||
}
|
||||
|
||||
createEffect(on(() => props.activeSegmentId, (activeId) => {
|
||||
if (!activeId) return
|
||||
const timer = typeof window !== "undefined" ? window.setTimeout(() => {
|
||||
revealTimer = setTimeout(() => {
|
||||
revealTimer = undefined
|
||||
const index = segmentIndexById().get(activeId)
|
||||
if (index !== undefined) virtualizerHandle()?.scrollToIndex(index, { align: "nearest", smooth: true })
|
||||
}, 120) : null
|
||||
onCleanup(() => {
|
||||
if (timer !== null && typeof window !== "undefined") {
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
})
|
||||
// The rail is globally virtualized: an animated reveal can keep moving
|
||||
// under the pointer while the reader chooses another distant destination.
|
||||
if (index !== undefined) virtualizerHandle()?.scrollToIndex(index, { align: "nearest" })
|
||||
}, 120)
|
||||
onCleanup(cancelReveal)
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -569,6 +574,9 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||
role="navigation"
|
||||
aria-label={t("messageTimeline.ariaLabel")}
|
||||
onScroll={handleScroll}
|
||||
onWheel={cancelReveal}
|
||||
onPointerDown={cancelReveal}
|
||||
onKeyDown={cancelReveal}
|
||||
>
|
||||
<Virtualizer ref={setVirtualizerHandle} data={props.segments} scrollRef={scrollElement()} bufferSize={TIMELINE_VIRTUALIZER_BUFFER_PX}>
|
||||
{(segment) => {
|
||||
|
|
|
|||
|
|
@ -74,10 +74,18 @@ async function fixture() {
|
|||
}
|
||||
}
|
||||
|
||||
async function clickTimeline(page: Page, index: number) {
|
||||
async function clickTimeline(page: Page, index: number, checkReaderPosition = false) {
|
||||
await page.locator(".message-timeline").evaluate((element, index) => {
|
||||
element.scrollTop = element.scrollHeight * index / 1500
|
||||
}, index)
|
||||
if (checkReaderPosition) {
|
||||
// Let the prior selection's 120ms reveal expire while the reader is
|
||||
// browsing elsewhere. It must not reclaim the rail or animate the target.
|
||||
await page.waitForTimeout(250)
|
||||
const target = await page.locator(`.message-timeline-segment[data-message-id="${navigationMessageId(index)}"]`).boundingBox()
|
||||
const rail = await page.locator(".message-timeline").boundingBox()
|
||||
assert(target && rail && target.y >= rail.y && target.y < rail.y + rail.height, "reader's destination remains visible before clicking")
|
||||
}
|
||||
await page.locator(`.message-timeline-segment[data-message-id="${navigationMessageId(index)}"]`).click()
|
||||
}
|
||||
const snapshot = (page: Page) => page.evaluate(() => (window as any).fixture.snapshot())
|
||||
|
|
@ -119,7 +127,7 @@ test("latest timeline intent supersedes a slow far jump, including a resident de
|
|||
const release = f.hold(250)
|
||||
await clickTimeline(f.page, 250)
|
||||
await f.page.locator(".history-navigation-status").waitFor()
|
||||
await clickTimeline(f.page, 1450)
|
||||
await clickTimeline(f.page, 1450, true)
|
||||
await f.page.locator(".history-navigation-status").waitFor({ state: "hidden" })
|
||||
release()
|
||||
await f.page.waitForTimeout(150)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ export async function testPruningUI({ client, baseUrl, root, location, generate,
|
|||
...(connection ? { getSharedServiceConnection: async () => connection } : {}),
|
||||
getInstanceAuthorizationHeader: () => `Basic ${Buffer.from("opencode:isolated-pruning-fixture").toString("base64")}`,
|
||||
getServiceDirectory: () => location.directory,
|
||||
getServiceLocation: () => location,
|
||||
getWorktrees: async () => ({ worktrees: [] }),
|
||||
getSharedServiceClient: async () => client,
|
||||
getSessionEnvironment: async () => {
|
||||
const { sessionEnvironment } = await tsImport("../packages/server/src/workspaces/session-environment.ts", import.meta.url)
|
||||
|
|
@ -71,6 +73,7 @@ export async function testPruningUI({ client, baseUrl, root, location, generate,
|
|||
server: { host: "127.0.0.1", port: 0, hmr: false, watch: null, proxy: {
|
||||
"/workspaces/pruning-ui/instance": { target: `http://127.0.0.1:${broker.server.address().port}` },
|
||||
"/api/workspaces/pruning-ui/session-pruning": { target: `http://127.0.0.1:${broker.server.address().port}` },
|
||||
"/api/workspaces/pruning-ui/session-history": { target: `http://127.0.0.1:${broker.server.address().port}` },
|
||||
"/fixture-events": { target: `http://127.0.0.1:${broker.server.address().port}` },
|
||||
} },
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue