From 1a6d9dde9e8a8002445cecdd10d7830dffa81257 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 21 Jul 2026 18:53:42 -0700 Subject: [PATCH] [eric] chat: burst commits type out too (fresh whole-message commits route through the same smooth reveal) --- .../src/app/pages/AgentChat/AgentChat.tsx | 47 +++++++++++++++---- .../AgentChat/bubbles/BurstRevealBubble.tsx | 47 +++++++++++++++++++ 2 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 frontend/src/app/pages/AgentChat/bubbles/BurstRevealBubble.tsx diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index a557efbf..b7af3e1c 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -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 = ({ 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 | 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 = ({ 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 = ({ sessionId: sessionIdProp, onClose containIntrinsicSize: 'auto 120px', }} > - + {msg.role === 'assistant' && typeof msg.content === 'string' && !isEditing ? ( + + ) : ( + + )} {!isEditing && (msg.role === 'user' || (msg.role === 'assistant' && lastAssistantIdsInTurn.has(msg.id))) && ( 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 ; + } + return ( + + ); +} + +export default BurstRevealBubble;