diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index 57312a50..4139f9e8 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -506,6 +506,23 @@ const AgentChat: React.FC = ({ 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(null); + const justStreamPrevRef = useRef(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 = ({ sessionId: sessionIdProp, onClose const isPending = item.result === null && sessionRunning; return ( - + {compactionChip} ); @@ -1337,6 +1354,8 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose {!isEditing && (msg.role === 'user' || (msg.role === 'assistant' && lastAssistantIdsInTurn.has(msg.id))) && ( navigator.clipboard.writeText(rawText)} onEdit={msg.role === 'user' ? () => setEditingMessageId(msg.id) : undefined} onRegenerate={msg.role === 'assistant' ? () => handleRegenerate(msg) : undefined} diff --git a/frontend/src/app/pages/AgentChat/tool-bubbles/DefaultToolBubble.tsx b/frontend/src/app/pages/AgentChat/tool-bubbles/DefaultToolBubble.tsx index 1c54cd8a..12f7eac6 100644 --- a/frontend/src/app/pages/AgentChat/tool-bubbles/DefaultToolBubble.tsx +++ b/frontend/src/app/pages/AgentChat/tool-bubbles/DefaultToolBubble.tsx @@ -45,21 +45,22 @@ interface DefaultToolBubbleProps { isBrowserAgent: boolean; accentRgb: string; selectAttrs: Record; + suppressReveal?: boolean; } export const DefaultToolBubble: React.FC = ({ 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 ( = 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 = React.memo( isBrowserAgent={isBrowserAgent} accentRgb={accentRgb} selectAttrs={selectAttrs} + suppressReveal={suppressReveal} /> ); }