[eric] chat: burst commits type out too (fresh whole-message commits route through the same smooth reveal)

This commit is contained in:
ciregenz
2026-07-21 18:53:42 -07:00
parent c5efd8fd57
commit 1a6d9dde9e
2 changed files with 85 additions and 9 deletions
+38 -9
View File
@@ -53,6 +53,7 @@ import WelcomeQuickReplies from './WelcomeQuickReplies';
import { useWelcomeGreeting } from './useWelcomeGreeting';
import { THINKING_LABELS } from './thinkingLabels';
import MessageBubble from './bubbles/MessageBubble';
import BurstRevealBubble from './bubbles/BurstRevealBubble';
import { estimateRenderedTextHeight, RECHECK_VISIBILITY_EVENT } from './bubbles/markdownMeasure';
import CompactionMarker from './bubbles/CompactionMarker';
import MessageActionBar from './shell/MessageActionBar';
@@ -630,7 +631,12 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
};
}, [id, session?.id, scheduleWindowRecompute]);
// Burst-reveal bookkeeping: ids present at open (or after a session/branch hop) are HISTORY and
// never animate; only ids that appear later, while the agent is working, type themselves out.
const seenMessageIdsRef = useRef<Set<string> | null>(null);
React.useLayoutEffect(() => {
seenMessageIdsRef.current = null;
const seed = initialSeedItems(viewportHeight);
const total = renderItemsLengthRef.current;
const end = total;
@@ -1715,6 +1721,18 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
}
const msg = item;
const isEditing = editingMessageId === msg.id;
// First pass after open/hop seeds the set; later unseen assistant ids arrived live.
if (seenMessageIdsRef.current === null) {
seenMessageIdsRef.current = new Set(renderItems.map((it) => it.id));
}
const burstAnimate =
msg.role === 'assistant' &&
typeof msg.content === 'string' &&
!isEditing &&
!seenMessageIdsRef.current.has(msg.id) &&
msg.id !== justStreamedId &&
(sessionRunning || awaitingResponse);
seenMessageIdsRef.current.add(msg.id);
const siblings = getSiblingBranches(msg.id);
const hasBranches = siblings.length > 0;
const currentBranchIdx = hasBranches
@@ -1732,15 +1750,26 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
containIntrinsicSize: 'auto 120px',
}}
>
<MessageBubble
message={msg}
editing={isEditing}
onSaveEdit={handleSaveEdit}
onCancelEdit={handleCancelEdit}
viewportHeight={viewportHeight}
viewportWidth={viewportWidth}
scrollRoot={scrollRoot}
/>
{msg.role === 'assistant' && typeof msg.content === 'string' && !isEditing ? (
<BurstRevealBubble
message={msg}
animate={burstAnimate}
onGrew={stickToBottomIfNeeded}
viewportHeight={viewportHeight}
viewportWidth={viewportWidth}
scrollRoot={scrollRoot}
/>
) : (
<MessageBubble
message={msg}
editing={isEditing}
onSaveEdit={handleSaveEdit}
onCancelEdit={handleCancelEdit}
viewportHeight={viewportHeight}
viewportWidth={viewportWidth}
scrollRoot={scrollRoot}
/>
)}
{!isEditing && (msg.role === 'user' || (msg.role === 'assistant' && lastAssistantIdsInTurn.has(msg.id))) && (
<MessageActionBar
role={msg.role as 'user' | 'assistant'}
@@ -0,0 +1,47 @@
import React, { useEffect, useState } from 'react';
import { AgentMessage } from '@/shared/state/agentsSlice';
import MessageBubble from './MessageBubble';
import { useSmoothText } from './useSmoothText';
interface Props {
message: AgentMessage;
// Captured once at mount: a message that arrived whole mid-run types itself out; history never re-animates.
animate: boolean;
onGrew?: () => void;
viewportHeight?: number;
viewportWidth?: number;
scrollRoot?: Element | null;
}
/** Short post-tool answers often COMMIT whole and skip the streaming slice entirely, so they popped
while true streams typed. Route fresh commits through the same smooth reveal (assistant-ui's
drain pattern), then settle into the identical committed render so the handoff can't flash. */
function BurstRevealBubble({ message, animate, onGrew, viewportHeight, viewportWidth, scrollRoot }: Props): React.ReactElement {
const [shouldAnimate] = useState(animate);
const full = typeof message.content === 'string' ? message.content : '';
const [done, setDone] = useState(!shouldAnimate || full.length === 0);
const { text, revealRef } = useSmoothText(full, !done);
useEffect(() => {
if (!done && text.length >= full.length) setDone(true);
}, [done, text.length, full.length]);
const grewRef = React.useRef(onGrew);
grewRef.current = onGrew;
useEffect(() => {
if (!done) grewRef.current?.();
}, [text.length, done]);
if (done) {
return <MessageBubble message={message} viewportHeight={viewportHeight} viewportWidth={viewportWidth} scrollRoot={scrollRoot} />;
}
return (
<MessageBubble
message={{ ...message, content: text }}
isStreaming
revealRef={revealRef}
viewportHeight={viewportHeight}
viewportWidth={viewportWidth}
scrollRoot={scrollRoot}
/>
);
}
export default BurstRevealBubble;