mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-26 15:03:34 +08:00
fix(desktop): respect manual scroll during thinking (#727)
Keep programmatic scroll suppression limited to scroll events that still match the requested auto-scroll target. This lets user scrollbar movement take over while active thinking content is growing. Tested: cd desktop && bun run test -- --run src/components/chat/MessageList.test.tsx Tested: bun run check:desktop Confidence: high Scope-risk: narrow
This commit is contained in:
parent
4b6c855eda
commit
3e6fe7d6bb
@ -2557,6 +2557,83 @@ describe('MessageList nested tool calls', () => {
|
|||||||
expect(screen.queryByRole('button', { name: 'Latest' })).toBeNull()
|
expect(screen.queryByRole('button', { name: 'Latest' })).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('lets the user drag away from active thinking output before the programmatic scroll settles', async () => {
|
||||||
|
let resizeCallback: ResizeObserverCallback | null = null
|
||||||
|
class TestResizeObserver {
|
||||||
|
observe = vi.fn()
|
||||||
|
unobserve = vi.fn()
|
||||||
|
disconnect = vi.fn()
|
||||||
|
|
||||||
|
constructor(callback: ResizeObserverCallback) {
|
||||||
|
resizeCallback = callback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vi.stubGlobal('ResizeObserver', TestResizeObserver)
|
||||||
|
|
||||||
|
useChatStore.setState({
|
||||||
|
sessions: {
|
||||||
|
[ACTIVE_TAB]: makeSessionState({
|
||||||
|
chatState: 'thinking',
|
||||||
|
activeThinkingId: 'thinking-1',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: 'user-1',
|
||||||
|
type: 'user_text',
|
||||||
|
content: '分析一下这段代码',
|
||||||
|
timestamp: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'thinking-1',
|
||||||
|
type: 'thinking',
|
||||||
|
content: '正在阅读代码路径',
|
||||||
|
timestamp: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { container } = render(<MessageList />)
|
||||||
|
const scroller = container.querySelector('.overflow-y-auto') as HTMLDivElement
|
||||||
|
let scrollTop = 600
|
||||||
|
let scrollHeight = 1000
|
||||||
|
Object.defineProperty(scroller, 'scrollHeight', {
|
||||||
|
configurable: true,
|
||||||
|
get: () => scrollHeight,
|
||||||
|
})
|
||||||
|
Object.defineProperty(scroller, 'clientHeight', { configurable: true, value: 400 })
|
||||||
|
Object.defineProperty(scroller, 'scrollTop', {
|
||||||
|
configurable: true,
|
||||||
|
get: () => scrollTop,
|
||||||
|
set: (value) => {
|
||||||
|
scrollTop = value
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(resizeCallback).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
resizeCallback?.([{
|
||||||
|
contentRect: { height: 600 },
|
||||||
|
} as ResizeObserverEntry], {} as ResizeObserver)
|
||||||
|
})
|
||||||
|
|
||||||
|
scrollTop = 200
|
||||||
|
fireEvent.scroll(scroller)
|
||||||
|
expect(screen.getByRole('button', { name: 'Latest' })).toBeTruthy()
|
||||||
|
|
||||||
|
scrollHeight = 1200
|
||||||
|
act(() => {
|
||||||
|
resizeCallback?.([{
|
||||||
|
contentRect: { height: 760 },
|
||||||
|
} as ResizeObserverEntry], {} as ResizeObserver)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(scrollTop).toBe(200)
|
||||||
|
})
|
||||||
|
|
||||||
it('ignores one-pixel content resize jitter while pinned to active thinking output', async () => {
|
it('ignores one-pixel content resize jitter while pinned to active thinking output', async () => {
|
||||||
let resizeCallback: ResizeObserverCallback | null = null
|
let resizeCallback: ResizeObserverCallback | null = null
|
||||||
class TestResizeObserver {
|
class TestResizeObserver {
|
||||||
|
|||||||
@ -1506,11 +1506,16 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
|||||||
// prevent the jump-to-latest button from flickering during auto-scroll.
|
// prevent the jump-to-latest button from flickering during auto-scroll.
|
||||||
const container = scrollContainerRef.current
|
const container = scrollContainerRef.current
|
||||||
if (!container) return
|
if (!container) return
|
||||||
const shouldIgnoreRecentProgrammaticScroll =
|
const matchesProgrammaticScrollTop =
|
||||||
performance.now() < ignoreProgrammaticScrollUntilRef.current &&
|
|
||||||
ignoreProgrammaticScrollTopRef.current !== null &&
|
ignoreProgrammaticScrollTopRef.current !== null &&
|
||||||
Math.abs(container.scrollTop - ignoreProgrammaticScrollTopRef.current) < 1
|
Math.abs(container.scrollTop - ignoreProgrammaticScrollTopRef.current) < 1
|
||||||
if (isProgrammaticScrollingRef.current || shouldIgnoreRecentProgrammaticScroll) {
|
const shouldIgnoreRecentProgrammaticScroll =
|
||||||
|
matchesProgrammaticScrollTop &&
|
||||||
|
(
|
||||||
|
isProgrammaticScrollingRef.current ||
|
||||||
|
performance.now() < ignoreProgrammaticScrollUntilRef.current
|
||||||
|
)
|
||||||
|
if (shouldIgnoreRecentProgrammaticScroll) {
|
||||||
syncVirtualViewportFromContainer(container)
|
syncVirtualViewportFromContainer(container)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user