mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 04:07:44 +02:00
[eric] chat: glide the streaming tool pill in and keep the commit hand-off seamless
This commit is contained in:
@@ -506,6 +506,23 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
return cleanup;
|
||||
}, [streamingMessageId]);
|
||||
|
||||
// A tool's live pill is already on screen when it commits, so re-running the
|
||||
// mount reveal on the committed bubble flashes the exact same row. Remember the
|
||||
// id that just stopped streaming for a beat and let that one bubble skip its
|
||||
// entrance, so the hand-off is seamless. 500ms is slack for the commit render
|
||||
// to land after the stream clears (they don't always arrive on the same frame).
|
||||
const [justStreamedId, setJustStreamedId] = useState<string | null>(null);
|
||||
const justStreamPrevRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
const prev = justStreamPrevRef.current;
|
||||
justStreamPrevRef.current = streamingMessageId;
|
||||
if (prev && !streamingMessageId) {
|
||||
setJustStreamedId(prev);
|
||||
const t = setTimeout(() => setJustStreamedId(null), 500);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
}, [streamingMessageId]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (scrollRafRef.current != null) {
|
||||
cancelAnimationFrame(scrollRafRef.current);
|
||||
@@ -1297,7 +1314,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
const isPending = item.result === null && sessionRunning;
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
<ToolCallBubble call={item.call} result={item.result} isPending={isPending} sessionId={session.id} />
|
||||
<ToolCallBubble call={item.call} result={item.result} isPending={isPending} sessionId={session.id} suppressReveal={item.call.id === justStreamedId} />
|
||||
{compactionChip}
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -1337,6 +1354,8 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
{!isEditing && (msg.role === 'user' || (msg.role === 'assistant' && lastAssistantIdsInTurn.has(msg.id))) && (
|
||||
<MessageActionBar
|
||||
role={msg.role as 'user' | 'assistant'}
|
||||
sessionId={session.id}
|
||||
messageId={msg.id}
|
||||
onCopy={() => navigator.clipboard.writeText(rawText)}
|
||||
onEdit={msg.role === 'user' ? () => setEditingMessageId(msg.id) : undefined}
|
||||
onRegenerate={msg.role === 'assistant' ? () => handleRegenerate(msg) : undefined}
|
||||
|
||||
@@ -45,21 +45,22 @@ interface DefaultToolBubbleProps {
|
||||
isBrowserAgent: boolean;
|
||||
accentRgb: string;
|
||||
selectAttrs: Record<string, string>;
|
||||
suppressReveal?: boolean;
|
||||
}
|
||||
|
||||
export const DefaultToolBubble: React.FC<DefaultToolBubbleProps> = ({
|
||||
call, input, sessionId, mcpCompact, isPending, isStreaming, isDenied, isError, result,
|
||||
mcpInfo, toolName, inputSummary, formattedInput, promptPrefix, resultSummary, resultElapsedMs,
|
||||
showTimer, showBody, toggle, parsedResult, isBrowserAgent, accentRgb, selectAttrs,
|
||||
showTimer, showBody, toggle, parsedResult, isBrowserAgent, accentRgb, selectAttrs, suppressReveal = false,
|
||||
}) => {
|
||||
const c = useClaudeTokens();
|
||||
const tc = useTermColors();
|
||||
// JS-driven mount reveal (see useMountReveal): replaces the old mount keyframe
|
||||
// that silently no-op'd, which is why standalone tools "appeared out of nowhere".
|
||||
// Skipped while streaming (the live pill has a committed twin; animating both
|
||||
// would flash) and for mcpCompact rows (the group's row-fade already handles them).
|
||||
// JS-driven mount reveal (see useMountReveal). The streaming pill itself glides
|
||||
// in so a tool enters smoothly the moment it starts; when it commits, AgentChat
|
||||
// sets suppressReveal on that same row so the hand-off doesn't re-animate what's
|
||||
// already on screen. mcpCompact rows opt out (the group's row-fade handles them).
|
||||
const reveal = useMountReveal();
|
||||
const enterStyle = (!isStreaming && !mcpCompact) ? reveal : {};
|
||||
const enterStyle = (!mcpCompact && !suppressReveal) ? reveal : {};
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -43,10 +43,11 @@ interface ToolCallBubbleProps {
|
||||
isStreaming?: boolean;
|
||||
mcpCompact?: boolean;
|
||||
sessionId?: string;
|
||||
suppressReveal?: boolean;
|
||||
}
|
||||
|
||||
const ToolCallBubble: React.FC<ToolCallBubbleProps> = React.memo(
|
||||
({ call, result = null, isPending = false, isStreaming = false, mcpCompact = false, sessionId }) => {
|
||||
({ call, result = null, isPending = false, isStreaming = false, mcpCompact = false, sessionId, suppressReveal = false }) => {
|
||||
ensureToolCallKeyframes();
|
||||
|
||||
const c = useClaudeTokens();
|
||||
@@ -295,6 +296,7 @@ const ToolCallBubble: React.FC<ToolCallBubbleProps> = React.memo(
|
||||
isBrowserAgent={isBrowserAgent}
|
||||
accentRgb={accentRgb}
|
||||
selectAttrs={selectAttrs}
|
||||
suppressReveal={suppressReveal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user