[eric] chat: keep tool body collapsed mid-stream, ease bubbles in, stop scroll-anchor fighting autoscroll

This commit is contained in:
eric
2026-06-01 11:55:27 -07:00
parent d2d0589501
commit 4e2077a6b7
4 changed files with 50 additions and 12 deletions
+19 -10
View File
@@ -1307,19 +1307,28 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
</Box>
);
})}
{/* overflow-anchor: none on the two elements that grow every frame
(live stream + thinking dots) keeps Chromium's scroll anchoring
from fighting our jam-to-bottom for the scroll position. The
committed messages above keep the default anchor, so resizing a
tool row while the user has scrolled up still holds their view. */}
{id && (
<StreamingBubble
sessionId={id}
activeBranchId={session.active_branch_id || 'main'}
turnLabel={session.turn_label?.label}
onStreamGrew={stickToBottomIfNeeded}
/>
<Box sx={{ overflowAnchor: 'none' }}>
<StreamingBubble
sessionId={id}
activeBranchId={session.active_branch_id || 'main'}
turnLabel={session.turn_label?.label}
onStreamGrew={stickToBottomIfNeeded}
/>
</Box>
)}
{(awaitingResponse || (session.status === 'running' && !streamingMessageId)) && (
<ThinkingBubble
label={session.turn_label?.label}
seedKey={`${session.id}:${session.messages?.length ?? 0}`}
/>
<Box sx={{ overflowAnchor: 'none' }}>
<ThinkingBubble
label={session.turn_label?.label}
seedKey={`${session.id}:${session.messages?.length ?? 0}`}
/>
</Box>
)}
{showResumeBubble && session.status === 'stopped' && (
<Box sx={{ display: 'flex', justifyContent: 'flex-start', my: 0.75 }}>
@@ -55,7 +55,24 @@ export const DefaultToolBubble: React.FC<DefaultToolBubbleProps> = ({
const tc = useTermColors();
return (
<Box {...selectAttrs} sx={{ maxWidth: mcpCompact ? '100%' : '85%', my: mcpCompact ? 0 : 0.5 }}>
<Box
{...selectAttrs}
sx={{
maxWidth: mcpCompact ? '100%' : '85%',
my: mcpCompact ? 0 : 0.5,
// Ease standalone tool calls in instead of popping. Skipped while
// streaming (the live pill has a committed twin, so animating both
// would flash) and when mcpCompact (rows inside a group already fade
// via toolRowFadeIn). Transform+opacity only, so layout/scroll are untouched.
...(!isStreaming && !mcpCompact ? {
animation: 'toolBubbleEnter 160ms ease-out',
'@keyframes toolBubbleEnter': {
from: { opacity: 0, transform: 'translateY(4px)' },
to: { opacity: 1, transform: 'translateY(0)' },
},
} : {}),
}}
>
<Box
sx={{
'--glow-rgb': accentRgb,
@@ -65,7 +65,11 @@ const ToolCallBubble: React.FC<ToolCallBubbleProps> = React.memo(
const isInvokeAgent = isInvokeAgentTool(toolName);
const isCreateAgent = isCreateAgentTool(toolName);
const browserAgentAutoExpand = isBrowserAgent && isPending && !isStreaming;
const showBody = expanded || isStreaming || browserAgentAutoExpand;
// While the call is still streaming we keep the body CLOSED: the args land in
// bursty clumps and force-painting them mid-stream is the jitter the user feels.
// The header pill (tool name + glow) is the calm "what's running" signal; the
// full args/output live behind the chevron once the call lands and is expanded.
const showBody = expanded || browserAgentAutoExpand;
const resultContent = result?.content;
const hasStructuredResult =
@@ -99,6 +99,14 @@ const ToolGroupBubble: React.FC<Props> = React.memo(({ group, isSessionRunning =
my: 0.5,
// contain: stops new tool rows from reflowing the whole transcript.
contain: 'layout style',
// Ease in instead of popping when a tool group appears mid-turn.
// Transform+opacity only, so it rides the compositor and never nudges
// layout or the scroll position. No streaming twin, so no handoff flash.
animation: 'toolGroupEnter 160ms ease-out',
'@keyframes toolGroupEnter': {
from: { opacity: 0, transform: 'translateY(4px)' },
to: { opacity: 1, transform: 'translateY(0)' },
},
}}
>
<Box