mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-08 18:57:43 +02:00
[eric] chat: burst commits type out too (fresh whole-message commits route through the same smooth reveal)
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user