mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
fix(chat): keep the status slide while fold room takes growth
Fold room takes output growth before any scroll, like the empty space below content that fits, so the tail is pinned only when no room remains. Refs #6637
This commit is contained in:
1 parent
6c35093a4b
commit
62338ac27d
2 files changed
+78
-6
No files matched your search
@@ -781,11 +781,13 @@ export class ChatViewport {
|
||||
|
||||
/**
|
||||
* Mark the column while the tail is pinned: the reader follows the floor with no unsampled reader
|
||||
* movement, the test fold batches use for a tail landing, and the content overflows, so following scrolls
|
||||
* absorb output growth and the status keeps its on-screen position. The first-output slide runs at zero
|
||||
* duration there. A resize calls this after its own follow scroll, before paint, so a slide that starts in
|
||||
* that frame finishes before it is drawn; scroll and scrollend call it after delivering the sample, and
|
||||
* follow-intent commits call it with the latest measurement. The attribute changes only with the pin.
|
||||
* movement, the test fold batches use for a tail landing, and the content overflows with no fold room
|
||||
* below it, so following scrolls absorb output growth and the status keeps its on-screen position. Fold
|
||||
* room, like the empty space below content that fits, takes growth before any scroll, so the status moves
|
||||
* there. The first-output slide runs at zero duration while pinned. A resize calls this after its own
|
||||
* follow scroll, before paint, so a slide that starts in that frame finishes before it is drawn; scroll and
|
||||
* scrollend call it after delivering the sample, and follow-intent commits call it with the latest
|
||||
* measurement. The attribute changes only with the pin.
|
||||
* @param overflowing - a fresh overflow measurement; omitted, the latest one is reused.
|
||||
*/
|
||||
refreshTailPin(overflowing = this.overflowing): void {
|
||||
@@ -793,7 +795,7 @@ export class ChatViewport {
|
||||
const elements = this.elements
|
||||
if (elements === null) return
|
||||
const reader = this.reader
|
||||
const pinned = overflowing && reader !== null && reader.follow.active && !reader.pending()
|
||||
const pinned = overflowing && this.spacerHeight() === 0 && reader !== null && reader.follow.active && !reader.pending()
|
||||
if (pinned === this.tailPinned) return
|
||||
this.tailPinned = pinned
|
||||
if (pinned) elements.column.dataset.chatTailPinned = ''
|
||||
|
||||
@@ -655,3 +655,73 @@ it('pins the tail only while sampled follow intent holds over overflowing conten
|
||||
viewport.detach()
|
||||
expect(pinned()).toBe(false)
|
||||
})
|
||||
|
||||
it('leaves the tail unpinned while fold room takes growth, then pins once the room is used', () => {
|
||||
let notifyResize = () => {}
|
||||
class Observer implements ResizeObserver {
|
||||
constructor(callback: ResizeObserverCallback) { notifyResize = () => { callback([], this) } }
|
||||
observe(): void {}
|
||||
unobserve(): void {}
|
||||
disconnect(): void {}
|
||||
}
|
||||
vi.stubGlobal('ResizeObserver', Observer)
|
||||
const scroller = document.createElement('div')
|
||||
const column = document.createElement('div')
|
||||
const spacer = document.createElement('div')
|
||||
spacer.dataset.chatTurnSpacer = ''
|
||||
scroller.append(column, spacer)
|
||||
document.body.append(scroller)
|
||||
let content = 900
|
||||
let top = 0
|
||||
const room = () => Number.parseFloat(spacer.style.height) || 0
|
||||
const floor = () => Math.max(0, content + room() - 300)
|
||||
Object.defineProperties(scroller, {
|
||||
clientHeight: { configurable: true, get: () => 300 },
|
||||
scrollHeight: { configurable: true, get: () => Math.max(300, content + room()) },
|
||||
scrollTop: {
|
||||
configurable: true,
|
||||
get: () => Math.min(top, floor()),
|
||||
set: (value: number) => { top = Math.max(0, Math.min(value, floor())) },
|
||||
},
|
||||
})
|
||||
const viewport = new ChatViewport()
|
||||
viewport.attach(scroller, column)
|
||||
const follow = new ScrollFollow(true, 4)
|
||||
const reading = new ChatReading(viewport, { read: () => null, save: vi.fn() }, {
|
||||
initialized: true, followingTail: true, activeTurn: 1,
|
||||
}, vi.fn(), follow)
|
||||
const unbind = reading.bindFolds()
|
||||
viewport.connect({
|
||||
scroll: reading.onScroll, scrollEnd: reading.onScrollEnd, interact: () => {},
|
||||
resize: () => { reading.onResize() },
|
||||
})
|
||||
onTestFinished(() => {
|
||||
unbind()
|
||||
reading.dispose()
|
||||
viewport.detach()
|
||||
scroller.remove()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
const pinned = () => column.hasAttribute('data-chat-tail-pinned')
|
||||
|
||||
notifyResize()
|
||||
expect(scroller.scrollTop).toBe(600)
|
||||
expect(pinned()).toBe(true)
|
||||
// A fold landing leaves room below the content and commits the tail landing.
|
||||
spacer.style.height = '200px'
|
||||
scroller.scrollTop = 800
|
||||
reading.followTail()
|
||||
expect(pinned()).toBe(false)
|
||||
// Growth fills the room without scrolling, so the status moves on screen and keeps its slide.
|
||||
content = 940
|
||||
notifyResize()
|
||||
expect(room()).toBe(160)
|
||||
expect(scroller.scrollTop).toBe(800)
|
||||
expect(pinned()).toBe(false)
|
||||
// Once growth has used the room, following scrolls absorb it again.
|
||||
content = 1_100
|
||||
notifyResize()
|
||||
expect(room()).toBe(0)
|
||||
expect(scroller.scrollTop).toBe(800)
|
||||
expect(pinned()).toBe(true)
|
||||
})
|
||||
Reference in new issue
Block a user