perf(ui-chat): retain the stats resize observer

This commit is contained in:
imccyu committed 2026-09-01 21:09:48 +08:00
1 parent f808112ec8
commit aad1ce0c68
2 files changed
+59 -27

No files matched your search

@@ -2,7 +2,7 @@
// Mounted on 'conversation.composer.dock' so it sticks with the composer in the
// active conversation scrollport (see ConversationRoot data-conversation-scroll).
import { Fragment, memo, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { Fragment, memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
import type { UseProjection } from '@deepseek-ai/dsh-api-session-controller/client'
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
@@ -122,6 +122,44 @@ export interface StatsLineProps {
t: ChatViewSlotProps['t']
}
/** Render and measure one non-empty statistics line. */
const StatsLineContent = memo(function StatsLineContent({
groups,
line,
}: {
readonly groups: readonly string[]
readonly line: string
}) {
const rootRef = useRef<HTMLDivElement | null>(null)
const [truncated, setTruncated] = useState(false)
const measure = useCallback(() => {
const el = rootRef.current
if (el === null) return
const next = el.scrollWidth > el.clientWidth
setTruncated(current => current === next ? current : next)
}, [])
useLayoutEffect(() => {
const el = rootRef.current
if (el === null || typeof ResizeObserver === 'undefined') return
const observer = new ResizeObserver(measure)
observer.observe(el)
return () => { observer.disconnect() }
}, [measure])
useLayoutEffect(measure, [line, measure])
return (
<Tooltip label={line} side="top" delayMs={500} disabled={!truncated}>
<div ref={rootRef} className={css.root}>
{groups.map((group, i) => (
<Fragment key={group}>
{i > 0 && <><span className={css.sep} aria-hidden>|</span>{' '}</>}
<span>{group}</span>
</Fragment>
))}
</div>
</Tooltip>
)
})
export const StatsLine = memo(function StatsLine({ useChat, useProjection, t }: StatsLineProps) {
const settledNodes = useChat(s => s.legacy.nodes)
const usage = useProjection('tokenUsage')
@@ -166,31 +204,6 @@ export const StatsLine = memo(function StatsLine({ useChat, useProjection, t }:
}))
}
const line = groups.join(' | ')
// The row elides with ellipsis when overlong; a delayed hover tooltip carries
// the full line, enabled only while content is actually clipped.
const rootRef = useRef<HTMLDivElement | null>(null)
const [truncated, setTruncated] = useState(false)
useLayoutEffect(() => {
const el = rootRef.current
if (el === null) return
const measure = () => { setTruncated(el.scrollWidth > el.clientWidth) }
measure()
if (typeof ResizeObserver === 'undefined') return
const observer = new ResizeObserver(measure)
observer.observe(el)
return () => { observer.disconnect() }
}, [line])
if (groups.length === 0) return null
return (
<Tooltip label={line} side="top" delayMs={500} disabled={!truncated}>
<div ref={rootRef} className={css.root}>
{groups.map((group, i) => (
<Fragment key={group}>
{i > 0 && <><span className={css.sep} aria-hidden>|</span>{' '}</>}
<span>{group}</span>
</Fragment>
))}
</div>
</Tooltip>
)
return <StatsLineContent groups={groups} line={line} />
})
@@ -162,6 +162,25 @@ describe('StatsLine', () => {
return { uncachedInputTokens, outputTokens: 1, cacheReadTokens, cacheWriteTokens: 0 }
}
it('mounts one size observer only while a non-empty line exists', () => {
let observers = 0
class CountingResizeObserver {
constructor(_callback: ResizeObserverCallback) { observers++ }
observe(): void {}
disconnect(): void {}
}
vi.stubGlobal('ResizeObserver', CountingResizeObserver)
const { set, source } = makeSource()
render(<StatsLine {...props(source, {})} />)
expect(observers).toBe(0)
act(() => { set({ nodes: [assistant(1, 1)] }) })
expect(observers).toBe(1)
act(() => { set({ nodes: [assistant(1, 1), assistant(2, 1)] }) })
expect(observers).toBe(1)
})
it('renders the grouped stats row and hides a brand-new empty session', () => {
const { source } = makeSource({ nodes: [assistant(1, 1)] })
const view = render(<StatsLine {...props(source)} />)