perf: reduce active-turn chat rerenders during streaming
- Keep older assistant messages in the active turn stable while new content streams - Prevent existing tool rows from rerendering when new tool activity is appended - Tighten message and tool memo comparisons to isolate render work to changed rows
This commit is contained in:
@@ -30,7 +30,7 @@ import type { TurnGroupingContext } from './lib/turns/types';
|
|||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
import { FadeInOnReveal } from './message/FadeInOnReveal';
|
import { FadeInOnReveal } from './message/FadeInOnReveal';
|
||||||
import { streamPerfCount } from '@/stores/utils/streamDebug';
|
import { streamPerfCount } from '@/stores/utils/streamDebug';
|
||||||
import { areOptionalRenderRelevantMessagesEqual, areRenderRelevantMessageInfoEqual, areRenderRelevantPartsEqual } from './message/renderCompare';
|
import { areOptionalRenderRelevantMessagesEqual, areRelevantTurnGroupingContextsEqual, areRenderRelevantMessageInfoEqual, areRenderRelevantPartsEqual } from './message/renderCompare';
|
||||||
|
|
||||||
const ToolOutputDialog = React.lazy(() => import('./message/ToolOutputDialog'));
|
const ToolOutputDialog = React.lazy(() => import('./message/ToolOutputDialog'));
|
||||||
|
|
||||||
@@ -1129,12 +1129,14 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(ChatMessage, (prev, next) => {
|
export default React.memo(ChatMessage, (prev, next) => {
|
||||||
|
const prevRole = deriveMessageRole(prev.message.info);
|
||||||
|
|
||||||
return areRenderRelevantMessageInfoEqual(prev.message.info, next.message.info)
|
return areRenderRelevantMessageInfoEqual(prev.message.info, next.message.info)
|
||||||
&& areRenderRelevantPartsEqual(prev.message.parts, next.message.parts)
|
&& areRenderRelevantPartsEqual(prev.message.parts, next.message.parts)
|
||||||
&& areOptionalRenderRelevantMessagesEqual(prev.previousMessage, next.previousMessage)
|
&& areOptionalRenderRelevantMessagesEqual(prev.previousMessage, next.previousMessage)
|
||||||
&& areOptionalRenderRelevantMessagesEqual(prev.nextMessage, next.nextMessage)
|
&& areOptionalRenderRelevantMessagesEqual(prev.nextMessage, next.nextMessage)
|
||||||
&& prev.onContentChange === next.onContentChange
|
&& prev.onContentChange === next.onContentChange
|
||||||
&& prev.turnGroupingContext === next.turnGroupingContext
|
&& areRelevantTurnGroupingContextsEqual(prev.turnGroupingContext, next.turnGroupingContext, prev.message.info.id, prevRole.isUser)
|
||||||
&& prev.assistantHeaderMessageId === next.assistantHeaderMessageId
|
&& prev.assistantHeaderMessageId === next.assistantHeaderMessageId
|
||||||
&& prev.isInActiveTurn === next.isInActiveTurn
|
&& prev.isInActiveTurn === next.isInActiveTurn
|
||||||
&& prev.activeStreamingPhase === next.activeStreamingPhase
|
&& prev.activeStreamingPhase === next.activeStreamingPhase
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Part } from '@opencode-ai/sdk/v2';
|
|||||||
import { measureElement as measureVirtualElement, type VirtualItem, useVirtualizer } from '@tanstack/react-virtual';
|
import { measureElement as measureVirtualElement, type VirtualItem, useVirtualizer } from '@tanstack/react-virtual';
|
||||||
|
|
||||||
import ChatMessage from './ChatMessage';
|
import ChatMessage from './ChatMessage';
|
||||||
import { areOptionalRenderRelevantMessagesEqual, areRenderRelevantMessagesEqual } from './message/renderCompare';
|
import { areOptionalRenderRelevantMessagesEqual, areRelevantTurnGroupingContextsEqual, areRenderRelevantMessagesEqual } from './message/renderCompare';
|
||||||
import TurnItem from './components/TurnItem';
|
import TurnItem from './components/TurnItem';
|
||||||
import type { AnimationHandlers, ContentChangeReason } from '@/hooks/useChatScrollManager';
|
import type { AnimationHandlers, ContentChangeReason } from '@/hooks/useChatScrollManager';
|
||||||
import { filterSyntheticParts } from '@/lib/messages/synthetic';
|
import { filterSyntheticParts } from '@/lib/messages/synthetic';
|
||||||
@@ -480,15 +480,7 @@ const MessageRow = React.memo<MessageRowProps>(({
|
|||||||
&& prev.onUserAnimationConsumed === next.onUserAnimationConsumed
|
&& prev.onUserAnimationConsumed === next.onUserAnimationConsumed
|
||||||
&& prev.onContentChange === next.onContentChange
|
&& prev.onContentChange === next.onContentChange
|
||||||
&& prev.scrollToBottom === next.scrollToBottom
|
&& prev.scrollToBottom === next.scrollToBottom
|
||||||
&& prevTurn?.turnId === nextTurn?.turnId
|
&& areRelevantTurnGroupingContextsEqual(prevTurn, nextTurn, prev.message.info.id, resolveMessageRole(prev.message) === 'user')
|
||||||
&& prevTurn?.isFirstAssistantInTurn === nextTurn?.isFirstAssistantInTurn
|
|
||||||
&& prevTurn?.isLastAssistantInTurn === nextTurn?.isLastAssistantInTurn
|
|
||||||
&& prevTurn?.activityOwnerMessageId === nextTurn?.activityOwnerMessageId
|
|
||||||
&& prevTurn?.isWorking === nextTurn?.isWorking
|
|
||||||
&& prevTurn?.isGroupExpanded === nextTurn?.isGroupExpanded
|
|
||||||
&& prevTurn?.toggleGroup === nextTurn?.toggleGroup
|
|
||||||
&& prevTurn?.activityGroupSegments === nextTurn?.activityGroupSegments
|
|
||||||
&& 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.activeStreamingPhase === next.activeStreamingPhase
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
|||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import ReasoningPart from './ReasoningPart';
|
import ReasoningPart from './ReasoningPart';
|
||||||
import JustificationBlock from './JustificationBlock';
|
import JustificationBlock from './JustificationBlock';
|
||||||
|
import { areRenderRelevantPartsEqual } from '../renderCompare';
|
||||||
|
|
||||||
interface ProgressiveGroupProps {
|
interface ProgressiveGroupProps {
|
||||||
parts: TurnActivityPart[];
|
parts: TurnActivityPart[];
|
||||||
@@ -380,6 +381,115 @@ type AggregatedRow =
|
|||||||
| { type: 'justification'; activity: TurnActivityPart }
|
| { type: 'justification'; activity: TurnActivityPart }
|
||||||
| { type: 'tool-fallback'; activity: TurnActivityPart };
|
| { type: 'tool-fallback'; activity: TurnActivityPart };
|
||||||
|
|
||||||
|
interface ExpandableToolRowProps {
|
||||||
|
activity: TurnActivityPart;
|
||||||
|
isExpanded: boolean;
|
||||||
|
syntaxTheme: Record<string, React.CSSProperties>;
|
||||||
|
isMobile: boolean;
|
||||||
|
onToggleTool: (toolId: string) => void;
|
||||||
|
onShowPopup: (content: ToolPopupContent) => void;
|
||||||
|
onContentChange?: (reason?: ContentChangeReason) => void;
|
||||||
|
animateTailText: boolean;
|
||||||
|
animateRows: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpandableToolRow: React.FC<ExpandableToolRowProps> = ({
|
||||||
|
activity,
|
||||||
|
isExpanded,
|
||||||
|
syntaxTheme,
|
||||||
|
isMobile,
|
||||||
|
onToggleTool,
|
||||||
|
onShowPopup,
|
||||||
|
onContentChange,
|
||||||
|
animateTailText,
|
||||||
|
animateRows,
|
||||||
|
}) => {
|
||||||
|
const handleToggle = React.useCallback(() => {
|
||||||
|
onToggleTool(activity.id);
|
||||||
|
}, [activity.id, onToggleTool]);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<ToolPart
|
||||||
|
part={activity.part as ToolPartType}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
onToggle={handleToggle}
|
||||||
|
syntaxTheme={syntaxTheme}
|
||||||
|
isMobile={isMobile}
|
||||||
|
onContentChange={onContentChange}
|
||||||
|
onShowPopup={onShowPopup}
|
||||||
|
animateTailText={animateTailText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const maybeWrapped = animateTailText ? (
|
||||||
|
<ToolRevealOnMount animate={true} wipe>
|
||||||
|
{content}
|
||||||
|
</ToolRevealOnMount>
|
||||||
|
) : content;
|
||||||
|
|
||||||
|
if (!animateRows) {
|
||||||
|
return maybeWrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FadeInOnReveal>{maybeWrapped}</FadeInOnReveal>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MemoExpandableToolRow = React.memo(ExpandableToolRow, (prev, next) => {
|
||||||
|
return prev.isExpanded === next.isExpanded
|
||||||
|
&& prev.syntaxTheme === next.syntaxTheme
|
||||||
|
&& prev.isMobile === next.isMobile
|
||||||
|
&& prev.onToggleTool === next.onToggleTool
|
||||||
|
&& prev.onShowPopup === next.onShowPopup
|
||||||
|
&& prev.onContentChange === next.onContentChange
|
||||||
|
&& prev.animateTailText === next.animateTailText
|
||||||
|
&& prev.animateRows === next.animateRows
|
||||||
|
&& prev.activity.id === next.activity.id
|
||||||
|
&& prev.activity.kind === next.activity.kind
|
||||||
|
&& prev.activity.endedAt === next.activity.endedAt
|
||||||
|
&& areRenderRelevantPartsEqual([prev.activity.part], [next.activity.part]);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface StaticGroupedToolRowProps {
|
||||||
|
toolName: string;
|
||||||
|
activities: TurnActivityPart[];
|
||||||
|
animateTailText: boolean;
|
||||||
|
animateRows: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StaticGroupedToolRow: React.FC<StaticGroupedToolRowProps> = ({
|
||||||
|
toolName,
|
||||||
|
activities,
|
||||||
|
animateTailText,
|
||||||
|
animateRows,
|
||||||
|
}) => {
|
||||||
|
const content = (
|
||||||
|
<StaticToolRow
|
||||||
|
toolName={toolName}
|
||||||
|
activities={activities}
|
||||||
|
animateTailText={animateTailText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const maybeWrapped = animateTailText ? (
|
||||||
|
<ToolRevealOnMount animate={true} wipe>
|
||||||
|
{content}
|
||||||
|
</ToolRevealOnMount>
|
||||||
|
) : content;
|
||||||
|
|
||||||
|
if (!animateRows) {
|
||||||
|
return maybeWrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FadeInOnReveal>{maybeWrapped}</FadeInOnReveal>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MemoStaticGroupedToolRow = React.memo(StaticGroupedToolRow, (prev, next) => {
|
||||||
|
return prev.toolName === next.toolName
|
||||||
|
&& prev.animateTailText === next.animateTailText
|
||||||
|
&& prev.animateRows === next.animateRows
|
||||||
|
&& areActivityListsEqual(prev.activities, next.activities);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aggregate sorted activity parts into display rows.
|
* Aggregate sorted activity parts into display rows.
|
||||||
* Static tools are rendered as one row per call.
|
* Static tools are rendered as one row per call.
|
||||||
@@ -440,7 +550,36 @@ const aggregateRows = (parts: TurnActivityPart[]): AggregatedRow[] => {
|
|||||||
* Render a static aggregated tool row.
|
* Render a static aggregated tool row.
|
||||||
* Shows: [icon] DisplayName file1.tsx file2.tsx ...
|
* Shows: [icon] DisplayName file1.tsx file2.tsx ...
|
||||||
*/
|
*/
|
||||||
export const StaticToolRow: React.FC<{
|
const areActivityListsEqual = (left: TurnActivityPart[], right: TurnActivityPart[]): boolean => {
|
||||||
|
if (left === right) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.length !== right.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < left.length; index += 1) {
|
||||||
|
const leftActivity = left[index];
|
||||||
|
const rightActivity = right[index];
|
||||||
|
|
||||||
|
if (leftActivity.id !== rightActivity.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftActivity.kind !== rightActivity.kind || leftActivity.endedAt !== rightActivity.endedAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!areRenderRelevantPartsEqual([leftActivity.part], [rightActivity.part])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StaticToolRowInner: React.FC<{
|
||||||
toolName: string;
|
toolName: string;
|
||||||
activities: TurnActivityPart[];
|
activities: TurnActivityPart[];
|
||||||
animateTailText: boolean;
|
animateTailText: boolean;
|
||||||
@@ -590,6 +729,12 @@ export const StaticToolRow: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const StaticToolRow = React.memo(StaticToolRowInner, (prev, next) => {
|
||||||
|
return prev.toolName === next.toolName
|
||||||
|
&& prev.animateTailText === next.animateTailText
|
||||||
|
&& areActivityListsEqual(prev.activities, next.activities);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inline reasoning text block — rendered as dimmed italic markdown.
|
* Inline reasoning text block — rendered as dimmed italic markdown.
|
||||||
*/
|
*/
|
||||||
@@ -683,18 +828,6 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
|||||||
return <FadeInOnReveal key={key}>{content}</FadeInOnReveal>;
|
return <FadeInOnReveal key={key}>{content}</FadeInOnReveal>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderToolRow = (key: string, content: React.ReactNode, animate: boolean) => {
|
|
||||||
if (!animate) {
|
|
||||||
return wrapRow(key, content);
|
|
||||||
}
|
|
||||||
return wrapRow(
|
|
||||||
key,
|
|
||||||
<ToolRevealOnMount animate={true} wipe>
|
|
||||||
{content}
|
|
||||||
</ToolRevealOnMount>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderedRows = shouldRenderRows
|
const renderedRows = shouldRenderRows
|
||||||
? visibleRows.map((row, index) => {
|
? visibleRows.map((row, index) => {
|
||||||
switch (row.type) {
|
switch (row.type) {
|
||||||
@@ -721,52 +854,46 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
case 'tool-expandable':
|
case 'tool-expandable':
|
||||||
return renderToolRow(
|
return (
|
||||||
row.activity.id,
|
<MemoExpandableToolRow
|
||||||
<>
|
key={row.activity.id}
|
||||||
<ToolPart
|
activity={row.activity}
|
||||||
part={row.activity.part as ToolPartType}
|
isExpanded={expandedTools.has(row.activity.id)}
|
||||||
isExpanded={expandedTools.has(row.activity.id)}
|
syntaxTheme={syntaxTheme}
|
||||||
onToggle={() => onToggleTool(row.activity.id)}
|
isMobile={isMobile}
|
||||||
syntaxTheme={syntaxTheme}
|
onToggleTool={onToggleTool}
|
||||||
isMobile={isMobile}
|
onShowPopup={onShowPopup}
|
||||||
onContentChange={onContentChange}
|
onContentChange={onContentChange}
|
||||||
onShowPopup={onShowPopup}
|
animateTailText={Boolean(animatedToolIds?.has(row.activity.id))}
|
||||||
animateTailText={Boolean(animatedToolIds?.has(row.activity.id))}
|
animateRows={animateRows}
|
||||||
/>
|
/>
|
||||||
</>,
|
|
||||||
Boolean(animatedToolIds?.has(row.activity.id))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'tool-static-group':
|
case 'tool-static-group':
|
||||||
return renderToolRow(
|
return (
|
||||||
`static-${row.toolName}-${row.activities[0]?.id ?? index}`,
|
<MemoStaticGroupedToolRow
|
||||||
<>
|
key={`static-${row.toolName}-${row.activities[0]?.id ?? index}`}
|
||||||
<StaticToolRow
|
toolName={row.toolName}
|
||||||
toolName={row.toolName}
|
activities={row.activities}
|
||||||
activities={row.activities}
|
animateTailText={row.activities.some((activity) => animatedToolIds?.has(activity.id))}
|
||||||
animateTailText={row.activities.some((activity) => animatedToolIds?.has(activity.id))}
|
animateRows={animateRows}
|
||||||
/>
|
/>
|
||||||
</>,
|
|
||||||
row.activities.some((activity) => animatedToolIds?.has(activity.id))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'tool-fallback':
|
case 'tool-fallback':
|
||||||
return renderToolRow(
|
return (
|
||||||
row.activity.id,
|
<MemoExpandableToolRow
|
||||||
<>
|
key={row.activity.id}
|
||||||
<ToolPart
|
activity={row.activity}
|
||||||
part={row.activity.part as ToolPartType}
|
isExpanded={expandedTools.has(row.activity.id)}
|
||||||
isExpanded={expandedTools.has(row.activity.id)}
|
syntaxTheme={syntaxTheme}
|
||||||
onToggle={() => onToggleTool(row.activity.id)}
|
isMobile={isMobile}
|
||||||
syntaxTheme={syntaxTheme}
|
onToggleTool={onToggleTool}
|
||||||
isMobile={isMobile}
|
onShowPopup={onShowPopup}
|
||||||
onContentChange={onContentChange}
|
onContentChange={onContentChange}
|
||||||
onShowPopup={onShowPopup}
|
animateTailText={Boolean(animatedToolIds?.has(row.activity.id))}
|
||||||
animateTailText={Boolean(animatedToolIds?.has(row.activity.id))}
|
animateRows={animateRows}
|
||||||
/>
|
/>
|
||||||
</>,
|
|
||||||
Boolean(animatedToolIds?.has(row.activity.id))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { ToolRevealOnMount } from './ToolRevealOnMount';
|
|||||||
import { getToolIcon } from './toolPresentation';
|
import { getToolIcon } from './toolPresentation';
|
||||||
import { useDurationTickerNow } from './useDurationTicker';
|
import { useDurationTickerNow } from './useDurationTicker';
|
||||||
import { resolveFallbackTaskSessionId } from './resolveFallbackTaskSessionId';
|
import { resolveFallbackTaskSessionId } from './resolveFallbackTaskSessionId';
|
||||||
|
import { areRenderRelevantPartsEqual } from '../renderCompare';
|
||||||
|
|
||||||
type ToolStateWithMetadata = ToolStateUnion & { metadata?: Record<string, unknown>; input?: Record<string, unknown>; output?: string; error?: string; time?: { start: number; end?: number } };
|
type ToolStateWithMetadata = ToolStateUnion & { metadata?: Record<string, unknown>; input?: Record<string, unknown>; output?: string; error?: string; time?: { start: number; end?: number } };
|
||||||
|
|
||||||
@@ -2472,4 +2473,12 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ToolPart;
|
export default React.memo(ToolPart, (prev, next) => {
|
||||||
|
return areRenderRelevantPartsEqual([prev.part], [next.part])
|
||||||
|
&& prev.isExpanded === next.isExpanded
|
||||||
|
&& prev.syntaxTheme === next.syntaxTheme
|
||||||
|
&& prev.isMobile === next.isMobile
|
||||||
|
&& prev.onContentChange === next.onContentChange
|
||||||
|
&& prev.onShowPopup === next.onShowPopup
|
||||||
|
&& prev.animateTailText === next.animateTailText;
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Message, Part } from '@opencode-ai/sdk/v2';
|
import type { Message, Part } from '@opencode-ai/sdk/v2';
|
||||||
|
import type { TurnActivityGroup, TurnActivityRecord, TurnDiffStats, TurnGroupingContext } from '../lib/turns/types';
|
||||||
|
|
||||||
type MessageRecord = {
|
type MessageRecord = {
|
||||||
info: Message;
|
info: Message;
|
||||||
@@ -113,3 +114,171 @@ export const areOptionalRenderRelevantMessagesEqual = (left?: MessageRecord, rig
|
|||||||
}
|
}
|
||||||
return areRenderRelevantMessagesEqual(left, right);
|
return areRenderRelevantMessagesEqual(left, right);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const areTurnDiffStatsEqual = (left?: TurnDiffStats, right?: TurnDiffStats): boolean => {
|
||||||
|
if (!left || !right) {
|
||||||
|
return left === right;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left.additions === right.additions
|
||||||
|
&& left.deletions === right.deletions
|
||||||
|
&& left.files === right.files;
|
||||||
|
};
|
||||||
|
|
||||||
|
const areTurnActivityRecordsEqual = (left: TurnActivityRecord, right: TurnActivityRecord): boolean => {
|
||||||
|
return left.id === right.id
|
||||||
|
&& left.messageId === right.messageId
|
||||||
|
&& left.kind === right.kind
|
||||||
|
&& left.partIndex === right.partIndex
|
||||||
|
&& left.endedAt === right.endedAt
|
||||||
|
&& areRenderRelevantPartsEqual([left.part], [right.part]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const areRelevantActivityPartsEqual = (
|
||||||
|
left: TurnActivityRecord[] | undefined,
|
||||||
|
right: TurnActivityRecord[] | undefined,
|
||||||
|
messageId: string,
|
||||||
|
): boolean => {
|
||||||
|
let leftIndex = 0;
|
||||||
|
let rightIndex = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
while (leftIndex < (left?.length ?? 0) && left?.[leftIndex]?.messageId !== messageId) {
|
||||||
|
leftIndex += 1;
|
||||||
|
}
|
||||||
|
while (rightIndex < (right?.length ?? 0) && right?.[rightIndex]?.messageId !== messageId) {
|
||||||
|
rightIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftRecord = left?.[leftIndex];
|
||||||
|
const rightRecord = right?.[rightIndex];
|
||||||
|
|
||||||
|
if (!leftRecord || !rightRecord) {
|
||||||
|
return leftRecord === rightRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!areTurnActivityRecordsEqual(leftRecord, rightRecord)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
leftIndex += 1;
|
||||||
|
rightIndex += 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const areTurnActivityGroupsEqual = (left: TurnActivityGroup, right: TurnActivityGroup): boolean => {
|
||||||
|
if (left.id !== right.id || left.anchorMessageId !== right.anchorMessageId || left.afterToolPartId !== right.afterToolPartId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.parts.length !== right.parts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < left.parts.length; index += 1) {
|
||||||
|
if (!areTurnActivityRecordsEqual(left.parts[index], right.parts[index])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasRelevantActivitySegments = (segments: TurnActivityGroup[] | undefined, messageId: string): boolean => {
|
||||||
|
return Boolean(segments?.some((segment) => segment.anchorMessageId === messageId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const areRelevantActivitySegmentsEqual = (
|
||||||
|
left: TurnActivityGroup[] | undefined,
|
||||||
|
right: TurnActivityGroup[] | undefined,
|
||||||
|
messageId: string,
|
||||||
|
): boolean => {
|
||||||
|
let leftIndex = 0;
|
||||||
|
let rightIndex = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
while (leftIndex < (left?.length ?? 0) && left?.[leftIndex]?.anchorMessageId !== messageId) {
|
||||||
|
leftIndex += 1;
|
||||||
|
}
|
||||||
|
while (rightIndex < (right?.length ?? 0) && right?.[rightIndex]?.anchorMessageId !== messageId) {
|
||||||
|
rightIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftSegment = left?.[leftIndex];
|
||||||
|
const rightSegment = right?.[rightIndex];
|
||||||
|
|
||||||
|
if (!leftSegment || !rightSegment) {
|
||||||
|
return leftSegment === rightSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!areTurnActivityGroupsEqual(leftSegment, rightSegment)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
leftIndex += 1;
|
||||||
|
rightIndex += 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const areRelevantTurnGroupingContextsEqual = (
|
||||||
|
left: TurnGroupingContext | undefined,
|
||||||
|
right: TurnGroupingContext | undefined,
|
||||||
|
messageId: string,
|
||||||
|
isUserMessage: boolean,
|
||||||
|
): boolean => {
|
||||||
|
if (left === right) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!left || !right) {
|
||||||
|
return left === right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUserMessage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.turnId !== right.turnId) return false;
|
||||||
|
if (left.isFirstAssistantInTurn !== right.isFirstAssistantInTurn) return false;
|
||||||
|
if (left.isLastAssistantInTurn !== right.isLastAssistantInTurn) return false;
|
||||||
|
if (left.isWorking !== right.isWorking) return false;
|
||||||
|
if (left.hasTools !== right.hasTools) return false;
|
||||||
|
if (left.hasReasoning !== right.hasReasoning) return false;
|
||||||
|
if (left.userMessageCreatedAt !== right.userMessageCreatedAt) return false;
|
||||||
|
if (left.userMessageVariant !== right.userMessageVariant) return false;
|
||||||
|
|
||||||
|
const headerRelevant = left.headerMessageId === messageId || right.headerMessageId === messageId;
|
||||||
|
if (headerRelevant && left.headerMessageId !== right.headerMessageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerRelevant = left.activityOwnerMessageId === messageId || right.activityOwnerMessageId === messageId;
|
||||||
|
if (ownerRelevant && left.activityOwnerMessageId !== right.activityOwnerMessageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!areRelevantActivityPartsEqual(left.activityParts, right.activityParts, messageId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!areRelevantActivitySegmentsEqual(left.activityGroupSegments, right.activityGroupSegments, messageId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentsRelevant = hasRelevantActivitySegments(left.activityGroupSegments, messageId)
|
||||||
|
|| hasRelevantActivitySegments(right.activityGroupSegments, messageId);
|
||||||
|
|
||||||
|
if ((ownerRelevant || segmentsRelevant) && left.isGroupExpanded !== right.isGroupExpanded) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ownerRelevant || segmentsRelevant) && left.toggleGroup !== right.toggleGroup) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ownerRelevant || segmentsRelevant) && !areTurnDiffStatsEqual(left.diffStats, right.diffStats)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user