mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 12:10:45 +03:00
fix(app): preserve bottom scroll anchoring (#44077)
This commit is contained in:
committed by
GitHub
Unverified
parent
6258c1943a
commit
97536add75
@@ -1,4 +1,5 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { createTwoFilesPatch } from "diff"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
@@ -13,10 +14,63 @@ import {
|
||||
setupTimeline,
|
||||
shell,
|
||||
textPart,
|
||||
toolPart,
|
||||
userMessage,
|
||||
type TimelineMessage,
|
||||
} from "./fixture"
|
||||
|
||||
test("follows an expanded patch that arrives as the user reaches the bottom", async ({ page }) => {
|
||||
const toolID = "prt_bottom_follow_patch"
|
||||
const input = { patchText: "Update src/edit.ts" }
|
||||
const timeline = await setupTimeline(page, {
|
||||
messages: [
|
||||
...history(20),
|
||||
userMessage(),
|
||||
assistantMessage([textPart("prt_bottom_follow_text", "Working")], { completed: false }),
|
||||
],
|
||||
settings: { editToolPartsExpanded: true },
|
||||
reducedMotion: true,
|
||||
})
|
||||
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
|
||||
await scroller.evaluate((element) => {
|
||||
element.scrollTop = Math.max(0, element.scrollHeight - element.clientHeight - 300)
|
||||
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: 300 }))
|
||||
element.scrollTop = element.scrollHeight
|
||||
})
|
||||
await expect
|
||||
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
|
||||
.toBeLessThanOrEqual(1)
|
||||
|
||||
await timeline.send(partUpdated(toolPart(toolID, "patch", "running", input)))
|
||||
await timeline.send(
|
||||
partUpdated(
|
||||
toolPart(toolID, "patch", "completed", input, {
|
||||
metadata: {
|
||||
files: [
|
||||
{
|
||||
file: "src/edit.ts",
|
||||
status: "modified",
|
||||
patch: createTwoFilesPatch(
|
||||
"a/src/edit.ts",
|
||||
"b/src/edit.ts",
|
||||
Array.from({ length: 40 }, (_, index) => `export const value${index} = ${index}\n`).join(""),
|
||||
Array.from({ length: 40 }, (_, index) => `export const value${index} = ${index + 1}\n`).join(""),
|
||||
),
|
||||
additions: 40,
|
||||
deletions: 40,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
await timeline.waitForPart(toolID)
|
||||
await expect
|
||||
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
|
||||
.toBeLessThanOrEqual(1)
|
||||
})
|
||||
|
||||
test("does not reverse visible rows when the user wheels during shell remeasurement", async ({ page }, testInfo) => {
|
||||
const shellID = "prt_wheel_01_shell"
|
||||
const followingID = "prt_wheel_02_following"
|
||||
|
||||
@@ -74,7 +74,9 @@ export function createTimelineVirtualizer(input: Input) {
|
||||
let prependLoading = false
|
||||
let resizePinnedIndexes: number[] = []
|
||||
let resizePinFrame: number | undefined
|
||||
let gestureAnchorFrame: number | undefined
|
||||
let virtualContent: HTMLDivElement | undefined
|
||||
let scrollTop = 0
|
||||
|
||||
const clearPrependAnchor = () => {
|
||||
prependLoading = false
|
||||
@@ -178,12 +180,28 @@ export function createTimelineVirtualizer(input: Input) {
|
||||
})
|
||||
const resizeItem = virtualizer.resizeItem
|
||||
let resizeAnchorScheduled = false
|
||||
const anchorAfterGesture = () => {
|
||||
if (gestureAnchorFrame !== undefined) return
|
||||
const apply = () => {
|
||||
gestureAnchorFrame = undefined
|
||||
if (input.hasScrollGesture()) {
|
||||
gestureAnchorFrame = requestAnimationFrame(apply)
|
||||
return
|
||||
}
|
||||
if (input.shouldAnchorBottom()) virtualizer.scrollToEnd()
|
||||
}
|
||||
gestureAnchorFrame = requestAnimationFrame(apply)
|
||||
}
|
||||
const anchorResizedBottom = () => {
|
||||
if (resizeAnchorScheduled || input.hasScrollGesture()) return
|
||||
if (resizeAnchorScheduled) return
|
||||
resizeAnchorScheduled = true
|
||||
queueMicrotask(() => {
|
||||
resizeAnchorScheduled = false
|
||||
if (!input.shouldAnchorBottom() || input.hasScrollGesture()) return
|
||||
if (input.hasScrollGesture()) {
|
||||
anchorAfterGesture()
|
||||
return
|
||||
}
|
||||
if (!input.shouldAnchorBottom()) return
|
||||
virtualizer.scrollToEnd()
|
||||
})
|
||||
}
|
||||
@@ -244,7 +262,11 @@ export function createTimelineVirtualizer(input: Input) {
|
||||
|
||||
const maybeAnchorBottom = () => {
|
||||
if (rows().length === 0) return
|
||||
if (!input.shouldAnchorBottom() || input.hasScrollGesture()) return
|
||||
if (input.hasScrollGesture()) {
|
||||
anchorAfterGesture()
|
||||
return
|
||||
}
|
||||
if (!input.shouldAnchorBottom()) return
|
||||
if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame)
|
||||
clearPrependAnchor()
|
||||
if (prependAnchorFrame !== undefined) cancelAnimationFrame(prependAnchorFrame)
|
||||
@@ -265,6 +287,7 @@ export function createTimelineVirtualizer(input: Input) {
|
||||
const bindListRoot = (root: HTMLDivElement) => {
|
||||
if (root === listRoot()) return
|
||||
setListRoot(root)
|
||||
scrollTop = root.scrollTop
|
||||
input.setScrollRef(root)
|
||||
}
|
||||
|
||||
@@ -324,13 +347,17 @@ export function createTimelineVirtualizer(input: Input) {
|
||||
}
|
||||
|
||||
const handleListScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
|
||||
const root = event.currentTarget
|
||||
const movedUp = root.scrollTop < scrollTop - 0.5
|
||||
scrollTop = root.scrollTop
|
||||
if (prependLoading) updatePrependAnchor()
|
||||
input.onScheduleScrollState(event.currentTarget)
|
||||
input.onScheduleScrollState(root)
|
||||
input.onHistoryScroll()
|
||||
if (!input.hasScrollGesture()) return
|
||||
if (!movedUp && root.scrollHeight - root.clientHeight - root.scrollTop >= 10) return
|
||||
input.onUserScroll()
|
||||
input.onAutoScrollHandleScroll()
|
||||
input.onMarkScrollGesture(event.currentTarget)
|
||||
input.onMarkScrollGesture(root)
|
||||
}
|
||||
|
||||
function View(props: ViewProps) {
|
||||
@@ -463,6 +490,7 @@ export function createTimelineVirtualizer(input: Input) {
|
||||
cache.set(ownerSessionKey, { measurements: virtualizer.takeSnapshot(), toolOpen: { ...toolOpen } })
|
||||
while (cache.size > 16) cache.delete(cache.keys().next().value!)
|
||||
if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame)
|
||||
if (gestureAnchorFrame !== undefined) cancelAnimationFrame(gestureAnchorFrame)
|
||||
if (overscanFrame !== undefined) cancelAnimationFrame(overscanFrame)
|
||||
input.setScrollRef(undefined)
|
||||
input.setRevealMessage?.(() => {})
|
||||
|
||||
Reference in New Issue
Block a user