fix: restore the jump-to-bottom glide and keep user state, selections and scroll anchors in windowed lists

Pause scroll writes while the jump-to-bottom smooth scroll runs; keep rows with pending questions, approvals, recent interactions and selection ends mounted; cache Mermaid results; copy text spanning unmounted rows; anchor the sidebar on id changes; disable native overflow-anchor for windowed lists; write terminal output on the leading edge and before clear; forward remote events every 16 ms; flush streaming text before copy/export; add performance principle P10 (no UX regressions).
This commit is contained in:
mesalogo
2026-10-03 16:19:56 +08:00
parent a620c22676
commit 805acc3fce
25 changed files with 834 additions and 63 deletions
+45 -2
View File
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { RemoteEventBatcher } from './remote-event-batcher'
import { REMOTE_EVENT_BATCH_FLUSH_INTERVAL_MS, RemoteEventBatcher } from './remote-event-batcher'
const provenance = (eventIndex: number) => ({
source: 'remote-semantic-transcript' as const,
@@ -46,6 +46,49 @@ describe('RemoteEventBatcher', () => {
batcher.dispose()
})
it('commits (and so forwards) streamed events within one frame by default', () => {
vi.useFakeTimers()
expect(REMOTE_EVENT_BATCH_FLUSH_INTERVAL_MS).toBe(16)
const persist = vi.fn((entries: readonly unknown[]) => entries.map(() => true))
const committed: string[] = []
const batcher = new RemoteEventBatcher<string>({ persist })
batcher.add(provenance(0), 'a', (event) => committed.push(event))
batcher.add(provenance(1), 'b', (event) => committed.push(event))
vi.advanceTimersByTime(15)
// Nothing is forwarded before it is persisted.
expect(committed).toEqual([])
vi.advanceTimersByTime(1)
expect(persist).toHaveBeenCalledOnce()
expect(committed).toEqual(['a', 'b'])
batcher.dispose()
})
it('reports a timer flush after its events committed, and not after a failed one', () => {
vi.useFakeTimers()
const order: string[] = []
let fail = false
const batcher = new RemoteEventBatcher<string>({
persist: (entries) => {
if (fail) throw new Error('disk full')
return entries.map(() => true)
},
onTimerFlushed: () => order.push('flushed'),
onError: () => order.push('error')
})
batcher.add(provenance(0), 'a', (event) => order.push(event))
vi.advanceTimersByTime(REMOTE_EVENT_BATCH_FLUSH_INTERVAL_MS)
expect(order).toEqual(['a', 'flushed'])
// Explicit flushes leave forwarding to the caller.
batcher.add(provenance(1), 'b', (event) => order.push(event))
batcher.flush()
expect(order).toEqual(['a', 'flushed', 'b'])
fail = true
batcher.add(provenance(2), 'c', (event) => order.push(event))
vi.advanceTimersByTime(REMOTE_EVENT_BATCH_FLUSH_INTERVAL_MS)
expect(order).toEqual(['a', 'flushed', 'b', 'error'])
batcher.dispose()
})
it('rethrows a failed commit and stays failed so later checkpoints are never committed', () => {
vi.useFakeTimers()
const onError = vi.fn()
@@ -55,7 +98,7 @@ describe('RemoteEventBatcher', () => {
const onCommitted = vi.fn()
const batcher = new RemoteEventBatcher<string>({ persist, onError })
batcher.add(provenance(0), 'a', onCommitted)
vi.advanceTimersByTime(50)
vi.advanceTimersByTime(REMOTE_EVENT_BATCH_FLUSH_INTERVAL_MS)
expect(onError).toHaveBeenCalledWith(new Error('disk full'))
expect(() => batcher.add(provenance(1), 'checkpoint')).toThrow('disk full')
expect(() => batcher.flush()).toThrow('disk full')