fix(chat): prevent graph collapse during streaming and strip thinking from reply bubble
- Prevent execution graph from auto-collapsing while reply is still streaming by excluding from autoCollapsedRunKeys and keeping expanded=true via controlled prop - Strip thinking blocks from the streaming ChatMessage when the reply renders as a separate bubble, so thinking content doesn't duplicate alongside the response text Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -378,12 +378,12 @@ export function Chat() {
|
|||||||
const autoCollapsedRunKeys = useMemo(() => {
|
const autoCollapsedRunKeys = useMemo(() => {
|
||||||
const keys = new Set<string>();
|
const keys = new Set<string>();
|
||||||
for (const card of userRunCards) {
|
for (const card of userRunCards) {
|
||||||
// Auto-collapse once the reply is visible — either the streaming
|
// Only auto-collapse after the run is fully complete — not while
|
||||||
// reply bubble is already rendering (streamingReplyText != null)
|
// the reply is still streaming, otherwise the graph jumps to a
|
||||||
// or the run finished and we have a reply text override.
|
// collapsed summary mid-stream.
|
||||||
const hasStreamingReply = card.streamingReplyText != null;
|
const isStillStreaming = card.streamingReplyText != null;
|
||||||
const hasHistoricalReply = card.replyIndex != null && replyTextOverrides.has(card.replyIndex);
|
const shouldCollapse = !isStillStreaming
|
||||||
const shouldCollapse = hasStreamingReply || hasHistoricalReply;
|
&& (card.replyIndex != null && replyTextOverrides.has(card.replyIndex));
|
||||||
if (!shouldCollapse) continue;
|
if (!shouldCollapse) continue;
|
||||||
const triggerMsg = messages[card.triggerIndex];
|
const triggerMsg = messages[card.triggerIndex];
|
||||||
const runKey = triggerMsg?.id
|
const runKey = triggerMsg?.id
|
||||||
@@ -492,11 +492,18 @@ export function Chat() {
|
|||||||
? `msg-${triggerMsg.id}`
|
? `msg-${triggerMsg.id}`
|
||||||
: `${currentSessionKey}:trigger-${card.triggerIndex}`;
|
: `${currentSessionKey}:trigger-${card.triggerIndex}`;
|
||||||
const userOverride = graphExpandedOverrides[runKey];
|
const userOverride = graphExpandedOverrides[runKey];
|
||||||
|
// Keep the graph expanded while the streaming reply
|
||||||
|
// renders — `active` flips to false once the reply
|
||||||
|
// bubble appears, which would trigger auto-collapse
|
||||||
|
// inside ExecutionGraphCard's uncontrolled path.
|
||||||
|
const isStreamingReply = card.streamingReplyText != null;
|
||||||
const expanded = userOverride != null
|
const expanded = userOverride != null
|
||||||
? userOverride
|
? userOverride
|
||||||
: autoCollapsedRunKeys.has(runKey)
|
: autoCollapsedRunKeys.has(runKey)
|
||||||
? false
|
? false
|
||||||
: undefined;
|
: isStreamingReply
|
||||||
|
? true
|
||||||
|
: undefined;
|
||||||
return (
|
return (
|
||||||
<ExecutionGraphCard
|
<ExecutionGraphCard
|
||||||
key={`graph-${runKey}`}
|
key={`graph-${runKey}`}
|
||||||
@@ -517,18 +524,33 @@ export function Chat() {
|
|||||||
{/* Streaming message */}
|
{/* Streaming message */}
|
||||||
{shouldRenderStreaming && !hasActiveExecutionGraph && (
|
{shouldRenderStreaming && !hasActiveExecutionGraph && (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
message={(streamMsg
|
message={(() => {
|
||||||
? {
|
const base = streamMsg
|
||||||
...(streamMsg as Record<string, unknown>),
|
? {
|
||||||
role: (typeof streamMsg.role === 'string' ? streamMsg.role : 'assistant') as RawMessage['role'],
|
...(streamMsg as Record<string, unknown>),
|
||||||
content: streamMsg.content ?? streamText,
|
role: (typeof streamMsg.role === 'string' ? streamMsg.role : 'assistant') as RawMessage['role'],
|
||||||
timestamp: streamMsg.timestamp ?? streamingTimestamp,
|
content: streamMsg.content ?? streamText,
|
||||||
}
|
timestamp: streamMsg.timestamp ?? streamingTimestamp,
|
||||||
: {
|
}
|
||||||
role: 'assistant',
|
: {
|
||||||
content: streamText,
|
role: 'assistant' as const,
|
||||||
timestamp: streamingTimestamp,
|
content: streamText,
|
||||||
}) as RawMessage}
|
timestamp: streamingTimestamp,
|
||||||
|
};
|
||||||
|
// When the reply renders as a separate bubble, strip
|
||||||
|
// thinking blocks from the message — they belong to
|
||||||
|
// the execution phase and are already omitted from
|
||||||
|
// the graph via omitLastStreamingMessageSegment.
|
||||||
|
if (streamingReplyText != null && Array.isArray(base.content)) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
content: (base.content as Array<{ type?: string }>).filter(
|
||||||
|
(block) => block.type !== 'thinking',
|
||||||
|
),
|
||||||
|
} as RawMessage;
|
||||||
|
}
|
||||||
|
return base as RawMessage;
|
||||||
|
})()}
|
||||||
textOverride={streamingReplyText ?? undefined}
|
textOverride={streamingReplyText ?? undefined}
|
||||||
isStreaming
|
isStreaming
|
||||||
streamingTools={streamingReplyText != null ? [] : streamingTools}
|
streamingTools={streamingReplyText != null ? [] : streamingTools}
|
||||||
|
|||||||
Reference in New Issue
Block a user