diff --git a/packages/ui/src/components/message-stream-v2.tsx b/packages/ui/src/components/message-stream-v2.tsx index ab93432c..2eab5519 100644 --- a/packages/ui/src/components/message-stream-v2.tsx +++ b/packages/ui/src/components/message-stream-v2.tsx @@ -332,6 +332,8 @@ export default function MessageStreamV2(props: MessageStreamV2Props) { let pendingScrollFrame: number | null = null let userScrollIntentUntil = 0 let detachScrollIntentListeners: (() => void) | undefined + let hasRestoredScroll = false + let hasInitialScroll = false function markUserScrollIntent() { const now = typeof performance !== "undefined" ? performance.now() : Date.now() @@ -405,7 +407,11 @@ export default function MessageStreamV2(props: MessageStreamV2Props) { function scrollToBottomAndClamp(immediate = false) { scrollToBottom(immediate) - requestAnimationFrame(() => clampScrollAfterShrink()) + if (hasInitialScroll) { + requestAnimationFrame(() => clampScrollAfterShrink()) + } else { + hasInitialScroll = true + } } function scrollToTop(immediate = false) { @@ -476,9 +482,13 @@ export default function MessageStreamV2(props: MessageStreamV2Props) { createEffect(() => { const target = containerRef + const loading = props.loading + if (!target) return + if (loading) return + if (hasRestoredScroll) return + scrollCache.restore(target, { - fallback: () => scrollToBottom(true), onApplied: (snapshot) => { if (snapshot) { setAutoScroll(snapshot.atBottom) @@ -490,12 +500,17 @@ export default function MessageStreamV2(props: MessageStreamV2Props) { updateScrollIndicators(target) }, }) + + hasRestoredScroll = true }) let previousToken: string | undefined createEffect(() => { const token = changeToken() + const loading = props.loading + + if (loading) return if (!token || token === previousToken) { return } @@ -507,6 +522,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) { createEffect(() => { preferenceSignature() + if (props.loading) return if (!autoScroll()) { return } diff --git a/packages/ui/src/stores/message-v2/instance-store.ts b/packages/ui/src/stores/message-v2/instance-store.ts index 53a81fac..5aace3d0 100644 --- a/packages/ui/src/stores/message-v2/instance-store.ts +++ b/packages/ui/src/stores/message-v2/instance-store.ts @@ -46,36 +46,13 @@ function ensurePartId(messageId: string, part: ClientPart, index: number): strin const PENDING_PART_MAX_AGE_MS = 30_000 function clonePart(part: ClientPart): ClientPart { - if (!part || typeof part !== "object") { - return part - } - const cloned: Record = { ...part } - if ("renderCache" in cloned) { - cloned.renderCache = undefined - } - if ("text" in cloned) { - cloned.text = cloneStructuredValue(cloned.text) - } - if ("thinking" in cloned && typeof cloned.thinking === "object") { - cloned.thinking = cloneStructuredValue(cloned.thinking) - } - if ("content" in cloned && Array.isArray(cloned.content)) { - cloned.content = cloneStructuredValue(cloned.content) - } - return cloned as ClientPart + // Cloning is intentionally disabled; message parts + // are stored as received from the backend. + return part } function cloneStructuredValue(value: T): T { - if (Array.isArray(value)) { - return value.map((item) => cloneStructuredValue(item)) as T - } - if (value && typeof value === "object") { - const next: Record = {} - Object.entries(value as Record).forEach(([key, nested]) => { - next[key] = cloneStructuredValue(nested) - }) - return next as T - } + // Legacy helper kept as a no-op to avoid deep copies. return value }