perf(desktop): move settled-preview derivation off the message root
The last status-dependent read at the message root, and the most expensive one: the completedText selector flipped between '' while running and a full messageContentText(content) join once settled, so every running <-> settled transition re-ran the join for the whole message AND re-rendered the root. At stream breadth N that is N joins plus N root re-renders per flip. completedText and the previewTargets memo it feeds now live in a new AssistantPreviewEmbeds leaf, mounted at the same position inside [data-slot='aui_assistant-message-content']. Verified before moving that previewTargets fed nothing else at the root -- its only consumer was its own render block. The leaf renders the same wrapper div with the same classes, or null when there are no targets, so the DOM is byte-identical; a component boundary adds no node, so unlike StreamingMarker this needed no placement care around the :first-child/:last-child rules. The '' branch is preserved deliberately: it is the streaming-side optimization that keeps the selector referentially stable so per-token flushes skip the regex scan. AssistantMessageBody now holds no status-dependent subscription at all -- what remains is messageId, hasVisibleText, isInterim and turnDurationS, none of which move on a pending flip. Adds preview-embeds.test.tsx. The embed had no coverage, and the two cases are written as a matched pair on the same selector -- present once settled, absent while running -- so neither can pass vacuously. Behavior-identical; invalidation scope only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -172,21 +172,6 @@ const AssistantMessageBody: FC<AssistantMessageProps> = ({ onBranchInNewChat, on
|
||||
// stable across the 30 Hz delta stream, so this adds no per-token renders).
|
||||
const turnDurationS = useAuiState(s => s.message.metadata?.custom?.durationS as number | undefined)
|
||||
|
||||
// Preview targets only materialize once the turn completes — while running
|
||||
// the selector returns '' (stable), so per-token flushes skip the regex
|
||||
// scan and the re-render it would cause.
|
||||
const completedText = useAuiState(s =>
|
||||
s.message.status?.type === 'running' ? '' : messageContentText(s.message.content)
|
||||
)
|
||||
|
||||
const previewTargets = useMemo(() => {
|
||||
if (!completedText || !/(https?:\/\/|file:\/\/)/i.test(completedText)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return pickPrimaryPreviewTarget(extractPreviewTargets(completedText))
|
||||
}, [completedText])
|
||||
|
||||
const getMessageText = useCallback(() => messageContentText(messageRuntime.getState().content), [messageRuntime])
|
||||
|
||||
// useEnterAnimation consults `enabled` ONLY when its callback ref fires,
|
||||
@@ -219,13 +204,7 @@ const AssistantMessageBody: FC<AssistantMessageProps> = ({ onBranchInNewChat, on
|
||||
{/* Todos render in the composer status stack now, not inline. */}
|
||||
{MESSAGE_PARTS}
|
||||
<AssistantStatusSlot />
|
||||
{previewTargets.length > 0 && (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{previewTargets.map(target => (
|
||||
<PreviewAttachment key={target} source="explicit-link" target={target} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<AssistantPreviewEmbeds />
|
||||
<MessagePrimitive.Error>
|
||||
<ErrorPrimitive.Root
|
||||
className="mt-1.5 flex items-start gap-1.5 text-[0.78rem] leading-5 text-[color-mix(in_srgb,var(--dt-destructive)_78%,var(--ui-text-secondary))]"
|
||||
@@ -302,6 +281,53 @@ const AssistantStatusSlot: FC = () => {
|
||||
return <TurnActivityIndicator />
|
||||
}
|
||||
|
||||
/**
|
||||
* PERF leaf: owns the settled-text selector that feeds the link previews.
|
||||
*
|
||||
* This was the last status-dependent read at the message root, and the most
|
||||
* expensive one: the selector flips between '' while running and the full
|
||||
* `messageContentText(content)` join once settled, so every running <-> settled
|
||||
* transition re-ran the join for the whole message AND re-rendered the root.
|
||||
* At stream breadth N that is N joins plus N root re-renders per flip. Reading
|
||||
* it here confines both to this leaf, which renders nothing at all in the
|
||||
* common case.
|
||||
*
|
||||
* The streaming-side optimization is unchanged and still the point of the ''
|
||||
* branch: preview targets only materialize once the turn completes, so while
|
||||
* running the selector returns a stable '' and per-token flushes skip the
|
||||
* regex scan and the re-render it would cause.
|
||||
*
|
||||
* Renders exactly what the root used to render at this position — the same
|
||||
* wrapper div with the same classes, or nothing when there are no targets —
|
||||
* so the DOM is byte-identical either way. A component boundary adds no node
|
||||
* of its own, so unlike StreamingMarker this needs no placement care.
|
||||
*/
|
||||
const AssistantPreviewEmbeds: FC = () => {
|
||||
const completedText = useAuiState(s =>
|
||||
s.message.status?.type === 'running' ? '' : messageContentText(s.message.content)
|
||||
)
|
||||
|
||||
const previewTargets = useMemo(() => {
|
||||
if (!completedText || !/(https?:\/\/|file:\/\/)/i.test(completedText)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return pickPrimaryPreviewTarget(extractPreviewTargets(completedText))
|
||||
}, [completedText])
|
||||
|
||||
if (previewTargets.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{previewTargets.map(target => (
|
||||
<PreviewAttachment key={target} source="explicit-link" target={target} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* PERF leaf: owns the `settledParts` selector so the tail's settle stops
|
||||
* re-rendering the message root. This is the one status-derived selector that
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Link previews moved off the message root into the AssistantPreviewEmbeds
|
||||
// leaf, because the selector behind them (`'' while running`, the full
|
||||
// `messageContentText(content)` join once settled) flipped on every
|
||||
// running <-> settled transition and re-rendered the root with it.
|
||||
//
|
||||
// Two things are pinned here. That the embed still renders at all — the move
|
||||
// was verbatim JSX and had no coverage before. And that it renders ONLY once
|
||||
// the turn settles: the '' branch is a deliberate streaming optimization (it
|
||||
// keeps the selector referentially stable so per-token flushes skip the regex
|
||||
// scan), so a rewrite that drops it would make previews flicker in mid-stream
|
||||
// with nothing to catch it.
|
||||
import { AssistantRuntimeProvider, type ThreadMessage, useExternalStoreRuntime } from '@assistant-ui/react'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { Thread } from '.'
|
||||
|
||||
const createdAt = new Date('2026-05-01T00:00:00.000Z')
|
||||
|
||||
class TestResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
vi.stubGlobal('ResizeObserver', TestResizeObserver)
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
window.setTimeout(() => callback(performance.now()), 0)
|
||||
)
|
||||
vi.stubGlobal('cancelAnimationFrame', (id: number) => window.clearTimeout(id))
|
||||
vi.stubGlobal('CSS', { escape: (str: string) => str })
|
||||
|
||||
Element.prototype.scrollTo = function scrollTo() {}
|
||||
|
||||
Element.prototype.animate = function animate() {
|
||||
return { cancel() {}, finished: Promise.resolve() } as unknown as Animation
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
const assistantMetadata = { unstable_state: null, unstable_annotations: [], unstable_data: [], steps: [], custom: {} }
|
||||
|
||||
function user(id: string, text: string): ThreadMessage {
|
||||
return {
|
||||
id,
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text }],
|
||||
attachments: [],
|
||||
createdAt,
|
||||
metadata: { custom: {} }
|
||||
} as ThreadMessage
|
||||
}
|
||||
|
||||
function assistant(id: string, text: string, running: boolean): ThreadMessage {
|
||||
return {
|
||||
id,
|
||||
role: 'assistant',
|
||||
content: text ? [{ type: 'text', text }] : [],
|
||||
status: running ? { type: 'running' } : { type: 'complete', reason: 'stop' },
|
||||
createdAt,
|
||||
metadata: assistantMetadata
|
||||
} as ThreadMessage
|
||||
}
|
||||
|
||||
function Harness({ messages }: { messages: ThreadMessage[] }) {
|
||||
const runtime = useExternalStoreRuntime<ThreadMessage>({
|
||||
messages,
|
||||
isRunning: messages.at(-1)?.status?.type === 'running',
|
||||
onNew: async () => {}
|
||||
})
|
||||
|
||||
return (
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
<Thread />
|
||||
</AssistantRuntimeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const TARGET = 'https://example.com/docs'
|
||||
const WITH_PREVIEW = `Serving now: [Preview: example](#preview/${TARGET})`
|
||||
|
||||
describe('settled-turn link previews', () => {
|
||||
it('renders the embed once the turn has settled', async () => {
|
||||
const { container } = render(<Harness messages={[user('u1', 'start it'), assistant('a1', WITH_PREVIEW, false)]} />)
|
||||
|
||||
await screen.findByText('Serving now:', { exact: false })
|
||||
|
||||
expect(container.querySelector(`[title="${TARGET}"]`)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not render the embed while the turn is still running', async () => {
|
||||
const { container } = render(<Harness messages={[user('u1', 'start it'), assistant('a1', WITH_PREVIEW, true)]} />)
|
||||
|
||||
await screen.findByText('Serving now:', { exact: false })
|
||||
|
||||
expect(container.querySelector(`[title="${TARGET}"]`)).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user