perf: reduce chat streaming rerenders

- Keep historical chat messages stable during streaming
- Isolate active stream state to the current message path
- Reduce chat container churn from sync message updates
This commit is contained in:
Bohdan Triapitsyn
2026-04-05 16:56:05 +03:00
parent e56f2f99cf
commit c88e68e5f7
4 changed files with 93 additions and 30 deletions
@@ -106,6 +106,7 @@ type ChatViewportProps = {
isLoadingOlder: boolean; isLoadingOlder: boolean;
sessionIsWorking: boolean; sessionIsWorking: boolean;
streamingMessageId: string | null; streamingMessageId: string | null;
activeStreamingPhase: import('./message/types').StreamPhase | null;
retryOverlay: { retryOverlay: {
sessionId: string; sessionId: string;
message: string; message: string;
@@ -135,6 +136,7 @@ const ChatViewport = React.memo(({
isLoadingOlder, isLoadingOlder,
sessionIsWorking, sessionIsWorking,
streamingMessageId, streamingMessageId,
activeStreamingPhase,
retryOverlay, retryOverlay,
handleMessageContentChange, handleMessageContentChange,
getAnimationHandlers, getAnimationHandlers,
@@ -173,6 +175,7 @@ const ChatViewport = React.memo(({
messages={renderedMessages} messages={renderedMessages}
sessionIsWorking={sessionIsWorking} sessionIsWorking={sessionIsWorking}
activeStreamingMessageId={streamingMessageId} activeStreamingMessageId={streamingMessageId}
activeStreamingPhase={activeStreamingPhase}
retryOverlay={retryOverlay} retryOverlay={retryOverlay}
onMessageContentChange={handleMessageContentChange} onMessageContentChange={handleMessageContentChange}
getAnimationHandlers={getAnimationHandlers} getAnimationHandlers={getAnimationHandlers}
@@ -218,6 +221,7 @@ const ChatViewport = React.memo(({
&& prev.isLoadingOlder === next.isLoadingOlder && prev.isLoadingOlder === next.isLoadingOlder
&& prev.sessionIsWorking === next.sessionIsWorking && prev.sessionIsWorking === next.sessionIsWorking
&& prev.streamingMessageId === next.streamingMessageId && prev.streamingMessageId === next.streamingMessageId
&& prev.activeStreamingPhase === next.activeStreamingPhase
&& prev.retryOverlay === next.retryOverlay && prev.retryOverlay === next.retryOverlay
&& prev.handleMessageContentChange === next.handleMessageContentChange && prev.handleMessageContentChange === next.handleMessageContentChange
&& prev.getAnimationHandlers === next.getAnimationHandlers && prev.getAnimationHandlers === next.getAnimationHandlers
@@ -296,6 +300,15 @@ export const ChatContainer: React.FC = () => {
[currentSessionId], [currentSessionId],
), ),
); );
const activeStreamingPhase = useStreamingStore(
React.useCallback(
(s) => {
if (!streamingMessageId) return null;
return s.messageStreamStates.get(streamingMessageId)?.phase ?? null;
},
[streamingMessageId],
),
);
const sessionMessageCount = useSessionMessageCount(currentSessionId ?? ''); const sessionMessageCount = useSessionMessageCount(currentSessionId ?? '');
const [suspendDetachedTailUpdates, setSuspendDetachedTailUpdates] = React.useState(false); const [suspendDetachedTailUpdates, setSuspendDetachedTailUpdates] = React.useState(false);
// Messages from sync system // Messages from sync system
@@ -803,6 +816,7 @@ export const ChatContainer: React.FC = () => {
isLoadingOlder={timelineController.isLoadingOlder} isLoadingOlder={timelineController.isLoadingOlder}
sessionIsWorking={sessionIsWorking} sessionIsWorking={sessionIsWorking}
streamingMessageId={streamingMessageId} streamingMessageId={streamingMessageId}
activeStreamingPhase={activeStreamingPhase}
retryOverlay={retryOverlay} retryOverlay={retryOverlay}
handleMessageContentChange={handleMessageContentChange} handleMessageContentChange={handleMessageContentChange}
getAnimationHandlers={getAnimationHandlers} getAnimationHandlers={getAnimationHandlers}
@@ -8,7 +8,6 @@ import { useConfigStore } from '@/stores/useConfigStore';
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore'; import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { useContextStore } from '@/stores/contextStore'; import { useContextStore } from '@/stores/contextStore';
import { useStreamingStore } from '@/sync/streaming';
import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSelectionStore } from '@/sync/selection-store'; import { useSelectionStore } from '@/sync/selection-store';
import * as sessionActions from '@/sync/session-actions'; import * as sessionActions from '@/sync/session-actions';
@@ -131,6 +130,7 @@ interface ChatMessageProps {
turnGroupingContext?: TurnGroupingContext; turnGroupingContext?: TurnGroupingContext;
assistantHeaderMessageId?: string; assistantHeaderMessageId?: string;
isInActiveTurn?: boolean; isInActiveTurn?: boolean;
activeStreamingPhase?: StreamPhase | null;
animateUserOnMount?: boolean; animateUserOnMount?: boolean;
onUserAnimationConsumed?: (messageId: string) => void; onUserAnimationConsumed?: (messageId: string) => void;
} }
@@ -144,6 +144,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
turnGroupingContext, turnGroupingContext,
assistantHeaderMessageId, assistantHeaderMessageId,
isInActiveTurn = false, isInActiveTurn = false,
activeStreamingPhase = null,
animateUserOnMount = false, animateUserOnMount = false,
onUserAnimationConsumed, onUserAnimationConsumed,
}) => { }) => {
@@ -152,13 +153,6 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
const messageContainerRef = React.useRef<HTMLDivElement | null>(null); const messageContainerRef = React.useRef<HTMLDivElement | null>(null);
const currentSessionId = useSessionUIStore((s) => s.currentSessionId); const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
const streamState = useStreamingStore((s) => s.messageStreamStates.get(message.info.id));
const lifecyclePhase = isInActiveTurn ? (streamState?.phase ?? null) : null;
const msgSessionId = (message.info as { sessionID?: string }).sessionID ?? currentSessionId ?? null;
const streamingMsgForSession = useStreamingStore((s) => msgSessionId ? s.streamingMessageIds.get(msgSessionId) ?? null : null);
const isStreamingMessage = isInActiveTurn ? streamingMsgForSession === message.info.id : false;
const hasActiveStreamInSession = typeof streamingMsgForSession === 'string' && streamingMsgForSession.length > 0;
const getAgentModelForSession = useSelectionStore((s) => s.getAgentModelForSession); const getAgentModelForSession = useSelectionStore((s) => s.getAgentModelForSession);
const getSessionModelSelection = useSelectionStore((s) => s.getSessionModelSelection); const getSessionModelSelection = useSelectionStore((s) => s.getSessionModelSelection);
@@ -166,13 +160,8 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
const forkFromMessage = sessionActions.forkFromMessage; const forkFromMessage = sessionActions.forkFromMessage;
streamPerfCount('ui.chat_message.render'); streamPerfCount('ui.chat_message.render');
if (isStreamingMessage) { if (isInActiveTurn) {
streamPerfCount('ui.chat_message.render.streaming'); streamPerfCount('ui.chat_message.render.streaming');
} else if (hasActiveStreamInSession) {
streamPerfCount('ui.chat_message.render.static_during_stream');
if (!isInActiveTurn) {
streamPerfCount('ui.chat_message.render.static_outside_active_turn_during_stream');
}
} }
const providers = useConfigStore.getState().providers; const providers = useConfigStore.getState().providers;
@@ -596,11 +585,11 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
if (isMessageCompleted) { if (isMessageCompleted) {
return 'completed'; return 'completed';
} }
if (lifecyclePhase) { if (isInActiveTurn) {
return lifecyclePhase; return activeStreamingPhase ?? 'streaming';
} }
return isStreamingMessage ? 'streaming' : 'completed'; return 'completed';
}, [isMessageCompleted, lifecyclePhase, isStreamingMessage]); }, [activeStreamingPhase, isInActiveTurn, isMessageCompleted]);
React.useEffect(() => { React.useEffect(() => {
if (!isUser || !animateUserOnMount) { if (!isUser || !animateUserOnMount) {
@@ -1148,6 +1137,7 @@ export default React.memo(ChatMessage, (prev, next) => {
&& prev.turnGroupingContext === next.turnGroupingContext && prev.turnGroupingContext === next.turnGroupingContext
&& prev.assistantHeaderMessageId === next.assistantHeaderMessageId && prev.assistantHeaderMessageId === next.assistantHeaderMessageId
&& prev.isInActiveTurn === next.isInActiveTurn && prev.isInActiveTurn === next.isInActiveTurn
&& prev.activeStreamingPhase === next.activeStreamingPhase
&& prev.animateUserOnMount === next.animateUserOnMount && prev.animateUserOnMount === next.animateUserOnMount
&& prev.onUserAnimationConsumed === next.onUserAnimationConsumed; && prev.onUserAnimationConsumed === next.onUserAnimationConsumed;
}); });
@@ -14,6 +14,7 @@ import { useUIStore } from '@/stores/useUIStore';
import { FadeInDisabledProvider } from './message/FadeInOnReveal'; import { FadeInDisabledProvider } from './message/FadeInOnReveal';
import { hasPendingUserSendAnimation, consumePendingUserSendAnimation } from '@/lib/userSendAnimation'; import { hasPendingUserSendAnimation, consumePendingUserSendAnimation } from '@/lib/userSendAnimation';
import { streamPerfCount, streamPerfMeasure } from '@/stores/utils/streamDebug'; import { streamPerfCount, streamPerfMeasure } from '@/stores/utils/streamDebug';
import type { StreamPhase } from './message/types';
const MESSAGE_LIST_VIRTUALIZE_THRESHOLD = 40; const MESSAGE_LIST_VIRTUALIZE_THRESHOLD = 40;
const MESSAGE_LIST_OVERSCAN = 6; const MESSAGE_LIST_OVERSCAN = 6;
@@ -304,6 +305,7 @@ interface MessageListProps {
messages: ChatMessageEntry[]; messages: ChatMessageEntry[];
sessionIsWorking?: boolean; sessionIsWorking?: boolean;
activeStreamingMessageId?: string | null; activeStreamingMessageId?: string | null;
activeStreamingPhase?: StreamPhase | null;
retryOverlay?: { retryOverlay?: {
sessionId: string; sessionId: string;
message: string; message: string;
@@ -347,6 +349,7 @@ interface MessageRowProps {
turnGroupingContext?: TurnGroupingContext; turnGroupingContext?: TurnGroupingContext;
assistantHeaderMessageId?: string; assistantHeaderMessageId?: string;
isInActiveTurn?: boolean; isInActiveTurn?: boolean;
activeStreamingPhase?: StreamPhase | null;
animateUserOnMount?: boolean; animateUserOnMount?: boolean;
onUserAnimationConsumed?: (messageId: string) => void; onUserAnimationConsumed?: (messageId: string) => void;
onContentChange: (reason?: ContentChangeReason) => void; onContentChange: (reason?: ContentChangeReason) => void;
@@ -361,6 +364,7 @@ const MessageRow = React.memo<MessageRowProps>(({
turnGroupingContext, turnGroupingContext,
assistantHeaderMessageId, assistantHeaderMessageId,
isInActiveTurn, isInActiveTurn,
activeStreamingPhase,
animateUserOnMount, animateUserOnMount,
onUserAnimationConsumed, onUserAnimationConsumed,
onContentChange, onContentChange,
@@ -380,6 +384,7 @@ const MessageRow = React.memo<MessageRowProps>(({
turnGroupingContext={turnGroupingContext} turnGroupingContext={turnGroupingContext}
assistantHeaderMessageId={assistantHeaderMessageId} assistantHeaderMessageId={assistantHeaderMessageId}
isInActiveTurn={isInActiveTurn} isInActiveTurn={isInActiveTurn}
activeStreamingPhase={activeStreamingPhase}
/> />
); );
}, (prev, next) => { }, (prev, next) => {
@@ -404,6 +409,7 @@ const MessageRow = React.memo<MessageRowProps>(({
&& prevTurn?.activityParts === nextTurn?.activityParts && prevTurn?.activityParts === nextTurn?.activityParts
&& prev.assistantHeaderMessageId === next.assistantHeaderMessageId && prev.assistantHeaderMessageId === next.assistantHeaderMessageId
&& prev.isInActiveTurn === next.isInActiveTurn && prev.isInActiveTurn === next.isInActiveTurn
&& prev.activeStreamingPhase === next.activeStreamingPhase
&& prev.animationHandlers?.onChunk === next.animationHandlers?.onChunk && prev.animationHandlers?.onChunk === next.animationHandlers?.onChunk
&& prev.animationHandlers?.onComplete === next.animationHandlers?.onComplete && prev.animationHandlers?.onComplete === next.animationHandlers?.onComplete
&& prev.animationHandlers?.onStreamingCandidate === next.animationHandlers?.onStreamingCandidate && prev.animationHandlers?.onStreamingCandidate === next.animationHandlers?.onStreamingCandidate
@@ -430,6 +436,7 @@ interface TurnBlockProps {
shouldAnimateUserMessage: (message: ChatMessageEntry) => boolean; shouldAnimateUserMessage: (message: ChatMessageEntry) => boolean;
onUserAnimationConsumed: (messageId: string) => void; onUserAnimationConsumed: (messageId: string) => void;
activeStreamingMessageId?: string | null; activeStreamingMessageId?: string | null;
activeStreamingPhase?: StreamPhase | null;
} }
const TurnBlock: React.FC<TurnBlockProps> = ({ const TurnBlock: React.FC<TurnBlockProps> = ({
@@ -447,6 +454,7 @@ const TurnBlock: React.FC<TurnBlockProps> = ({
shouldAnimateUserMessage, shouldAnimateUserMessage,
onUserAnimationConsumed, onUserAnimationConsumed,
activeStreamingMessageId, activeStreamingMessageId,
activeStreamingPhase,
}) => { }) => {
const turnUiState = turnUiStates.get(turn.turnId) ?? { isExpanded: defaultActivityExpanded }; const turnUiState = turnUiStates.get(turn.turnId) ?? { isExpanded: defaultActivityExpanded };
const handleToggleTurnGroup = React.useCallback(() => { const handleToggleTurnGroup = React.useCallback(() => {
@@ -656,6 +664,7 @@ const TurnBlock: React.FC<TurnBlockProps> = ({
turnGroupingContext={turnGroupingContext} turnGroupingContext={turnGroupingContext}
assistantHeaderMessageId={assistantHeaderMessageId} assistantHeaderMessageId={assistantHeaderMessageId}
isInActiveTurn={Boolean(streamingAssistantMessageId) && message.info.id === streamingAssistantMessageId} isInActiveTurn={Boolean(streamingAssistantMessageId) && message.info.id === streamingAssistantMessageId}
activeStreamingPhase={message.info.id === streamingAssistantMessageId ? activeStreamingPhase : null}
animateUserOnMount={shouldAnimateUserMessage(message)} animateUserOnMount={shouldAnimateUserMessage(message)}
onUserAnimationConsumed={onUserAnimationConsumed} onUserAnimationConsumed={onUserAnimationConsumed}
onContentChange={onMessageContentChange} onContentChange={onMessageContentChange}
@@ -681,6 +690,7 @@ const TurnBlock: React.FC<TurnBlockProps> = ({
turnUiState.isExpanded, turnUiState.isExpanded,
turnGroupingContextBase, turnGroupingContextBase,
streamingAssistantMessageId, streamingAssistantMessageId,
activeStreamingPhase,
visibleAssistantMessages, visibleAssistantMessages,
visibleAssistantIds, visibleAssistantIds,
activityOwnerMessageId, activityOwnerMessageId,
@@ -717,6 +727,7 @@ interface UngroupedMessageRowProps {
shouldAnimateUserMessage: (message: ChatMessageEntry) => boolean; shouldAnimateUserMessage: (message: ChatMessageEntry) => boolean;
onUserAnimationConsumed: (messageId: string) => void; onUserAnimationConsumed: (messageId: string) => void;
activeStreamingMessageId?: string | null; activeStreamingMessageId?: string | null;
activeStreamingPhase?: StreamPhase | null;
} }
const UngroupedMessageRow: React.FC<UngroupedMessageRowProps> = React.memo(({ const UngroupedMessageRow: React.FC<UngroupedMessageRowProps> = React.memo(({
@@ -729,6 +740,7 @@ const UngroupedMessageRow: React.FC<UngroupedMessageRowProps> = React.memo(({
shouldAnimateUserMessage, shouldAnimateUserMessage,
onUserAnimationConsumed, onUserAnimationConsumed,
activeStreamingMessageId, activeStreamingMessageId,
activeStreamingPhase,
}) => { }) => {
return ( return (
<MessageRow <MessageRow
@@ -741,6 +753,7 @@ const UngroupedMessageRow: React.FC<UngroupedMessageRowProps> = React.memo(({
animationHandlers={getAnimationHandlers(message.info.id)} animationHandlers={getAnimationHandlers(message.info.id)}
scrollToBottom={scrollToBottom} scrollToBottom={scrollToBottom}
isInActiveTurn={Boolean(activeStreamingMessageId) && message.info.id === activeStreamingMessageId} isInActiveTurn={Boolean(activeStreamingMessageId) && message.info.id === activeStreamingMessageId}
activeStreamingPhase={message.info.id === activeStreamingMessageId ? activeStreamingPhase : null}
/> />
); );
}, (prev, next) => { }, (prev, next) => {
@@ -752,7 +765,8 @@ const UngroupedMessageRow: React.FC<UngroupedMessageRowProps> = React.memo(({
&& prev.scrollToBottom === next.scrollToBottom && prev.scrollToBottom === next.scrollToBottom
&& prev.shouldAnimateUserMessage === next.shouldAnimateUserMessage && prev.shouldAnimateUserMessage === next.shouldAnimateUserMessage
&& prev.onUserAnimationConsumed === next.onUserAnimationConsumed && prev.onUserAnimationConsumed === next.onUserAnimationConsumed
&& prev.activeStreamingMessageId === next.activeStreamingMessageId; && prev.activeStreamingMessageId === next.activeStreamingMessageId
&& prev.activeStreamingPhase === next.activeStreamingPhase;
}); });
UngroupedMessageRow.displayName = 'UngroupedMessageRow'; UngroupedMessageRow.displayName = 'UngroupedMessageRow';
@@ -771,6 +785,7 @@ interface MessageListEntryProps {
shouldAnimateUserMessage: (message: ChatMessageEntry) => boolean; shouldAnimateUserMessage: (message: ChatMessageEntry) => boolean;
onUserAnimationConsumed: (messageId: string) => void; onUserAnimationConsumed: (messageId: string) => void;
activeStreamingMessageId?: string | null; activeStreamingMessageId?: string | null;
activeStreamingPhase?: StreamPhase | null;
} }
const turnContainsMessageId = (turn: TurnRecord, messageId: string | null | undefined): boolean => { const turnContainsMessageId = (turn: TurnRecord, messageId: string | null | undefined): boolean => {
@@ -799,6 +814,7 @@ const MessageListEntry: React.FC<MessageListEntryProps> = React.memo(({
shouldAnimateUserMessage, shouldAnimateUserMessage,
onUserAnimationConsumed, onUserAnimationConsumed,
activeStreamingMessageId, activeStreamingMessageId,
activeStreamingPhase,
}) => { }) => {
if (entry.kind === 'ungrouped') { if (entry.kind === 'ungrouped') {
return ( return (
@@ -812,6 +828,7 @@ const MessageListEntry: React.FC<MessageListEntryProps> = React.memo(({
shouldAnimateUserMessage={shouldAnimateUserMessage} shouldAnimateUserMessage={shouldAnimateUserMessage}
onUserAnimationConsumed={onUserAnimationConsumed} onUserAnimationConsumed={onUserAnimationConsumed}
activeStreamingMessageId={activeStreamingMessageId} activeStreamingMessageId={activeStreamingMessageId}
activeStreamingPhase={activeStreamingPhase}
/> />
); );
} }
@@ -828,6 +845,7 @@ const MessageListEntry: React.FC<MessageListEntryProps> = React.memo(({
shouldAnimateUserMessage={shouldAnimateUserMessage} shouldAnimateUserMessage={shouldAnimateUserMessage}
onUserAnimationConsumed={onUserAnimationConsumed} onUserAnimationConsumed={onUserAnimationConsumed}
activeStreamingMessageId={activeStreamingMessageId} activeStreamingMessageId={activeStreamingMessageId}
activeStreamingPhase={activeStreamingPhase}
onMessageContentChange={onMessageContentChange} onMessageContentChange={onMessageContentChange}
getAnimationHandlers={getAnimationHandlers} getAnimationHandlers={getAnimationHandlers}
scrollToBottom={scrollToBottom} scrollToBottom={scrollToBottom}
@@ -871,6 +889,22 @@ function areMessageListEntryPropsEqual(prevProps: MessageListEntryProps, nextPro
} }
} }
if (prevProps.activeStreamingPhase !== nextProps.activeStreamingPhase) {
const prevAffected = turnContainsMessageId(prevEntry.turn, prevProps.activeStreamingMessageId);
const nextAffected = turnContainsMessageId(nextEntry.turn, nextProps.activeStreamingMessageId);
if (prevAffected || nextAffected) {
return false;
}
}
if (prevProps.activeStreamingPhase !== nextProps.activeStreamingPhase) {
const prevAffected = turnContainsMessageId(prevEntry.turn, prevProps.activeStreamingMessageId);
const nextAffected = turnContainsMessageId(nextEntry.turn, nextProps.activeStreamingMessageId);
if (prevAffected || nextAffected) {
return false;
}
}
return true; return true;
} }
@@ -884,6 +918,20 @@ function areMessageListEntryPropsEqual(prevProps: MessageListEntryProps, nextPro
} }
} }
if (prevProps.activeStreamingPhase !== nextProps.activeStreamingPhase) {
const messageId = prevEntry.message.info.id;
if (prevProps.activeStreamingMessageId === messageId || nextProps.activeStreamingMessageId === messageId) {
return false;
}
}
if (prevProps.activeStreamingPhase !== nextProps.activeStreamingPhase) {
const messageId = prevEntry.message.info.id;
if (prevProps.activeStreamingMessageId === messageId || nextProps.activeStreamingMessageId === messageId) {
return false;
}
}
return ( return (
areRenderRelevantMessagesEqual(prevEntry.message, nextEntry.message) areRenderRelevantMessagesEqual(prevEntry.message, nextEntry.message)
&& areOptionalRenderRelevantMessagesEqual(prevEntry.previousMessage, nextEntry.previousMessage) && areOptionalRenderRelevantMessagesEqual(prevEntry.previousMessage, nextEntry.previousMessage)
@@ -912,7 +960,8 @@ const StaticHistoryList: React.FC<{
chatRenderMode: 'sorted' | 'live'; chatRenderMode: 'sorted' | 'live';
shouldAnimateUserMessage: (message: ChatMessageEntry) => boolean; shouldAnimateUserMessage: (message: ChatMessageEntry) => boolean;
onUserAnimationConsumed: (messageId: string) => void; onUserAnimationConsumed: (messageId: string) => void;
}> = React.memo(({ entries, shouldVirtualize, virtualRows, totalSize, measureElement, contentRef, onMessageContentChange, getAnimationHandlers, scrollToBottom, stickyUserHeader, defaultActivityExpanded, turnUiStates, onToggleTurnGroup, chatRenderMode, shouldAnimateUserMessage, onUserAnimationConsumed }) => { activeStreamingPhase?: StreamPhase | null;
}> = React.memo(({ entries, shouldVirtualize, virtualRows, totalSize, measureElement, contentRef, onMessageContentChange, getAnimationHandlers, scrollToBottom, stickyUserHeader, defaultActivityExpanded, turnUiStates, onToggleTurnGroup, chatRenderMode, shouldAnimateUserMessage, onUserAnimationConsumed, activeStreamingPhase }) => {
const renderEntry = React.useCallback((entry: RenderEntry) => { const renderEntry = React.useCallback((entry: RenderEntry) => {
return ( return (
<MessageListEntry <MessageListEntry
@@ -930,9 +979,10 @@ const StaticHistoryList: React.FC<{
shouldAnimateUserMessage={shouldAnimateUserMessage} shouldAnimateUserMessage={shouldAnimateUserMessage}
onUserAnimationConsumed={onUserAnimationConsumed} onUserAnimationConsumed={onUserAnimationConsumed}
activeStreamingMessageId={null} activeStreamingMessageId={null}
activeStreamingPhase={activeStreamingPhase}
/> />
); );
}, [chatRenderMode, defaultActivityExpanded, getAnimationHandlers, onMessageContentChange, onToggleTurnGroup, onUserAnimationConsumed, scrollToBottom, shouldAnimateUserMessage, stickyUserHeader, turnUiStates]); }, [activeStreamingPhase, chatRenderMode, defaultActivityExpanded, getAnimationHandlers, onMessageContentChange, onToggleTurnGroup, onUserAnimationConsumed, scrollToBottom, shouldAnimateUserMessage, stickyUserHeader, turnUiStates]);
const paddingTop = shouldVirtualize && virtualRows.length > 0 const paddingTop = shouldVirtualize && virtualRows.length > 0
? virtualRows[0]?.start ?? 0 ? virtualRows[0]?.start ?? 0
@@ -995,7 +1045,8 @@ const StaticHistoryList: React.FC<{
&& prevProps.onToggleTurnGroup === nextProps.onToggleTurnGroup && prevProps.onToggleTurnGroup === nextProps.onToggleTurnGroup
&& prevProps.chatRenderMode === nextProps.chatRenderMode && prevProps.chatRenderMode === nextProps.chatRenderMode
&& prevProps.shouldAnimateUserMessage === nextProps.shouldAnimateUserMessage && prevProps.shouldAnimateUserMessage === nextProps.shouldAnimateUserMessage
&& prevProps.onUserAnimationConsumed === nextProps.onUserAnimationConsumed; && prevProps.onUserAnimationConsumed === nextProps.onUserAnimationConsumed
&& prevProps.activeStreamingPhase === nextProps.activeStreamingPhase;
}); });
StaticHistoryList.displayName = 'StaticHistoryList'; StaticHistoryList.displayName = 'StaticHistoryList';
@@ -1014,6 +1065,7 @@ const StreamingTailContent: React.FC<{
shouldAnimateUserMessage: (message: ChatMessageEntry) => boolean; shouldAnimateUserMessage: (message: ChatMessageEntry) => boolean;
onUserAnimationConsumed: (messageId: string) => void; onUserAnimationConsumed: (messageId: string) => void;
activeStreamingMessageId?: string | null; activeStreamingMessageId?: string | null;
activeStreamingPhase?: StreamPhase | null;
}> = React.memo(({ }> = React.memo(({
entry, entry,
onMessageContentChange, onMessageContentChange,
@@ -1028,6 +1080,7 @@ const StreamingTailContent: React.FC<{
shouldAnimateUserMessage, shouldAnimateUserMessage,
onUserAnimationConsumed, onUserAnimationConsumed,
activeStreamingMessageId, activeStreamingMessageId,
activeStreamingPhase,
}) => { }) => {
return ( return (
<MessageListEntry <MessageListEntry
@@ -1044,6 +1097,7 @@ const StreamingTailContent: React.FC<{
shouldAnimateUserMessage={shouldAnimateUserMessage} shouldAnimateUserMessage={shouldAnimateUserMessage}
onUserAnimationConsumed={onUserAnimationConsumed} onUserAnimationConsumed={onUserAnimationConsumed}
activeStreamingMessageId={activeStreamingMessageId} activeStreamingMessageId={activeStreamingMessageId}
activeStreamingPhase={activeStreamingPhase}
/> />
); );
}, (prev, next) => { }, (prev, next) => {
@@ -1059,7 +1113,8 @@ const StreamingTailContent: React.FC<{
&& prev.chatRenderMode === next.chatRenderMode && prev.chatRenderMode === next.chatRenderMode
&& prev.shouldAnimateUserMessage === next.shouldAnimateUserMessage && prev.shouldAnimateUserMessage === next.shouldAnimateUserMessage
&& prev.onUserAnimationConsumed === next.onUserAnimationConsumed && prev.onUserAnimationConsumed === next.onUserAnimationConsumed
&& prev.activeStreamingMessageId === next.activeStreamingMessageId; && prev.activeStreamingMessageId === next.activeStreamingMessageId
&& prev.activeStreamingPhase === next.activeStreamingPhase;
}); });
StreamingTailContent.displayName = 'StreamingTailContent'; StreamingTailContent.displayName = 'StreamingTailContent';
@@ -1071,6 +1126,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
messages, messages,
sessionIsWorking = false, sessionIsWorking = false,
activeStreamingMessageId = null, activeStreamingMessageId = null,
activeStreamingPhase = null,
retryOverlay = null, retryOverlay = null,
onMessageContentChange, onMessageContentChange,
getAnimationHandlers, getAnimationHandlers,
@@ -1760,6 +1816,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
chatRenderMode={chatRenderMode} chatRenderMode={chatRenderMode}
shouldAnimateUserMessage={shouldAnimateUserMessage} shouldAnimateUserMessage={shouldAnimateUserMessage}
onUserAnimationConsumed={onUserAnimationConsumed} onUserAnimationConsumed={onUserAnimationConsumed}
activeStreamingPhase={activeStreamingPhase}
/> />
{trailingStreamingEntry ? ( {trailingStreamingEntry ? (
<StreamingTailContent <StreamingTailContent
@@ -1776,6 +1833,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
shouldAnimateUserMessage={shouldAnimateUserMessage} shouldAnimateUserMessage={shouldAnimateUserMessage}
onUserAnimationConsumed={onUserAnimationConsumed} onUserAnimationConsumed={onUserAnimationConsumed}
activeStreamingMessageId={activeStreamingMessageId} activeStreamingMessageId={activeStreamingMessageId}
activeStreamingPhase={activeStreamingPhase}
/> />
) : null} ) : null}
</div> </div>
+8 -7
View File
@@ -36,7 +36,14 @@ type SyncSystem = {
directory: string directory: string
} }
const SyncContext = createContext<SyncSystem | null>(null) const SYNC_CONTEXT_GLOBAL_KEY = "__openchamber_sync_context__"
type SyncGlobal = typeof globalThis & {
[SYNC_CONTEXT_GLOBAL_KEY]?: React.Context<SyncSystem | null>
}
const syncGlobal = globalThis as SyncGlobal
const SyncContext = syncGlobal[SYNC_CONTEXT_GLOBAL_KEY] ?? createContext<SyncSystem | null>(null)
syncGlobal[SYNC_CONTEXT_GLOBAL_KEY] = SyncContext
function useSyncSystem() { function useSyncSystem() {
const ctx = useContext(SyncContext) const ctx = useContext(SyncContext)
@@ -1072,9 +1079,3 @@ const EMPTY_MESSAGES: Message[] = []
const EMPTY_PARTS: Part[] = [] const EMPTY_PARTS: Part[] = []
const EMPTY_PERMISSION_REQUESTS: PermissionRequest[] = [] const EMPTY_PERMISSION_REQUESTS: PermissionRequest[] = []
const EMPTY_QUESTION_REQUESTS: QuestionRequest[] = [] const EMPTY_QUESTION_REQUESTS: QuestionRequest[] = []
if (import.meta.hot) {
import.meta.hot.accept(() => {
import.meta.hot?.invalidate()
})
}