[eric] chat: glide the streaming tool pill in and keep the commit hand-off seamless

This commit is contained in:
ciregenz
2026-06-01 15:01:13 -07:00
parent 06006cd54a
commit 0b4d41ad92
3 changed files with 30 additions and 8 deletions
+20 -1
View File
@@ -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}
/>
);
}