perf: avoid per-message review metadata checks
Compute review transfer state once per chat render Hide transfer actions when linked review sessions are inactive Remove session-list scans from individual message rows
This commit is contained in:
@@ -31,6 +31,7 @@ import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { FadeInOnReveal } from './message/FadeInOnReveal';
|
||||
import { streamPerfCount } from '@/stores/utils/streamDebug';
|
||||
import { areOptionalRenderRelevantMessagesEqual, areRenderRelevantMessagesEqual, areRelevantTurnGroupingContextsEqual } from './message/renderCompare';
|
||||
import type { ReviewTransferDirection } from '@/lib/reviewFlow';
|
||||
|
||||
const ToolOutputDialog = lazyWithChunkRecovery(() => import('./message/ToolOutputDialog'));
|
||||
|
||||
@@ -133,6 +134,7 @@ interface ChatMessageProps {
|
||||
activeStreamingPhase?: StreamPhase | null;
|
||||
animateUserOnMount?: boolean;
|
||||
onUserAnimationConsumed?: (messageId: string) => void;
|
||||
reviewTransferDirection?: ReviewTransferDirection | null;
|
||||
}
|
||||
|
||||
const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
@@ -147,6 +149,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
activeStreamingPhase = null,
|
||||
animateUserOnMount = false,
|
||||
onUserAnimationConsumed,
|
||||
reviewTransferDirection = null,
|
||||
}) => {
|
||||
const { isMobile, isTablet, hasTouchInput } = useDeviceInfo();
|
||||
const alwaysShowMessageActions = isMobile || isTablet;
|
||||
@@ -1138,6 +1141,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
turnGroupingContext={turnGroupingContext}
|
||||
errorMessage={assistantErrorText}
|
||||
errorVariant={assistantErrorVariant}
|
||||
reviewTransferDirection={reviewTransferDirection}
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -1171,6 +1175,7 @@ export default React.memo(ChatMessage, (prev, next) => {
|
||||
)
|
||||
&& prev.isInActiveTurn === next.isInActiveTurn
|
||||
&& prev.activeStreamingPhase === next.activeStreamingPhase
|
||||
&& prev.reviewTransferDirection === next.reviewTransferDirection
|
||||
&& prev.assistantHeaderMessageId === next.assistantHeaderMessageId
|
||||
&& prev.animateUserOnMount === next.animateUserOnMount
|
||||
&& prev.onUserAnimationConsumed === next.onUserAnimationConsumed
|
||||
|
||||
@@ -16,6 +16,9 @@ import { hasPendingUserSendAnimation, consumePendingUserSendAnimation } from '@/
|
||||
import { streamPerfCount, streamPerfMeasure } from '@/stores/utils/streamDebug';
|
||||
import type { StreamPhase } from './message/types';
|
||||
import { normalizeParts } from './message/partUtils';
|
||||
import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
||||
import { getReviewTransferDirection, type ReviewTransferDirection } from '@/lib/reviewFlow';
|
||||
import { getOriginalSessionID, getReviewSessionID } from '@/lib/sessionReviewMetadata';
|
||||
|
||||
const MESSAGE_LIST_VIRTUALIZE_THRESHOLD = 5;
|
||||
const MESSAGE_LIST_OVERSCAN = 6;
|
||||
@@ -444,6 +447,7 @@ interface MessageRowProps {
|
||||
onContentChange: (reason?: ContentChangeReason) => void;
|
||||
animationHandlers: AnimationHandlers;
|
||||
scrollToBottom?: () => void;
|
||||
reviewTransferDirection?: ReviewTransferDirection | null;
|
||||
}
|
||||
|
||||
const MessageRow = React.memo<MessageRowProps>(({
|
||||
@@ -459,6 +463,7 @@ const MessageRow = React.memo<MessageRowProps>(({
|
||||
onContentChange,
|
||||
animationHandlers,
|
||||
scrollToBottom,
|
||||
reviewTransferDirection,
|
||||
}) => {
|
||||
return (
|
||||
<ChatMessage
|
||||
@@ -474,6 +479,7 @@ const MessageRow = React.memo<MessageRowProps>(({
|
||||
assistantHeaderMessageId={assistantHeaderMessageId}
|
||||
isInActiveTurn={isInActiveTurn}
|
||||
activeStreamingPhase={activeStreamingPhase}
|
||||
reviewTransferDirection={reviewTransferDirection}
|
||||
/>
|
||||
);
|
||||
}, (prev, next) => {
|
||||
@@ -491,6 +497,7 @@ const MessageRow = React.memo<MessageRowProps>(({
|
||||
&& prev.assistantHeaderMessageId === next.assistantHeaderMessageId
|
||||
&& prev.isInActiveTurn === next.isInActiveTurn
|
||||
&& prev.activeStreamingPhase === next.activeStreamingPhase
|
||||
&& prev.reviewTransferDirection === next.reviewTransferDirection
|
||||
&& prev.animationHandlers?.onChunk === next.animationHandlers?.onChunk
|
||||
&& prev.animationHandlers?.onComplete === next.animationHandlers?.onComplete
|
||||
&& prev.animationHandlers?.onStreamingCandidate === next.animationHandlers?.onStreamingCandidate
|
||||
@@ -518,6 +525,7 @@ interface TurnBlockProps {
|
||||
onUserAnimationConsumed: (messageId: string) => void;
|
||||
activeStreamingMessageId?: string | null;
|
||||
activeStreamingPhase?: StreamPhase | null;
|
||||
reviewTransferDirection?: ReviewTransferDirection | null;
|
||||
}
|
||||
|
||||
const TurnBlock = React.memo(({
|
||||
@@ -536,6 +544,7 @@ const TurnBlock = React.memo(({
|
||||
onUserAnimationConsumed,
|
||||
activeStreamingMessageId,
|
||||
activeStreamingPhase,
|
||||
reviewTransferDirection,
|
||||
}: TurnBlockProps) => {
|
||||
const turnUiState = turnUiStates.get(turn.turnId) ?? { isExpanded: defaultActivityExpanded };
|
||||
const handleToggleTurnGroup = React.useCallback(() => {
|
||||
@@ -756,6 +765,7 @@ const TurnBlock = React.memo(({
|
||||
assistantHeaderMessageId={assistantHeaderMessageId}
|
||||
isInActiveTurn={Boolean(streamingAssistantMessageId) && message.info.id === streamingAssistantMessageId}
|
||||
activeStreamingPhase={message.info.id === streamingAssistantMessageId ? activeStreamingPhase : null}
|
||||
reviewTransferDirection={reviewTransferDirection}
|
||||
animateUserOnMount={shouldAnimateUserMessage(message)}
|
||||
onUserAnimationConsumed={onUserAnimationConsumed}
|
||||
onContentChange={onMessageContentChange}
|
||||
@@ -782,6 +792,7 @@ const TurnBlock = React.memo(({
|
||||
turnGroupingContextBase,
|
||||
streamingAssistantMessageId,
|
||||
activeStreamingPhase,
|
||||
reviewTransferDirection,
|
||||
visibleAssistantMessages,
|
||||
visibleAssistantIds,
|
||||
visibleActivitySegments,
|
||||
@@ -820,6 +831,7 @@ interface UngroupedMessageRowProps {
|
||||
onUserAnimationConsumed: (messageId: string) => void;
|
||||
activeStreamingMessageId?: string | null;
|
||||
activeStreamingPhase?: StreamPhase | null;
|
||||
reviewTransferDirection?: ReviewTransferDirection | null;
|
||||
}
|
||||
|
||||
const UngroupedMessageRow = React.memo(({
|
||||
@@ -833,6 +845,7 @@ const UngroupedMessageRow = React.memo(({
|
||||
onUserAnimationConsumed,
|
||||
activeStreamingMessageId,
|
||||
activeStreamingPhase,
|
||||
reviewTransferDirection,
|
||||
}: UngroupedMessageRowProps) => {
|
||||
return (
|
||||
<MessageRow
|
||||
@@ -846,6 +859,7 @@ const UngroupedMessageRow = React.memo(({
|
||||
scrollToBottom={scrollToBottom}
|
||||
isInActiveTurn={Boolean(activeStreamingMessageId) && message.info.id === activeStreamingMessageId}
|
||||
activeStreamingPhase={message.info.id === activeStreamingMessageId ? activeStreamingPhase : null}
|
||||
reviewTransferDirection={reviewTransferDirection}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -867,6 +881,7 @@ interface MessageListEntryProps {
|
||||
onUserAnimationConsumed: (messageId: string) => void;
|
||||
activeStreamingMessageId?: string | null;
|
||||
activeStreamingPhase?: StreamPhase | null;
|
||||
reviewTransferDirection?: ReviewTransferDirection | null;
|
||||
}
|
||||
|
||||
const turnContainsMessageId = (turn: TurnRecord, messageId: string | null | undefined): boolean => {
|
||||
@@ -896,6 +911,7 @@ const MessageListEntry = React.memo(({
|
||||
onUserAnimationConsumed,
|
||||
activeStreamingMessageId,
|
||||
activeStreamingPhase,
|
||||
reviewTransferDirection,
|
||||
}: MessageListEntryProps) => {
|
||||
if (entry.kind === 'ungrouped') {
|
||||
return (
|
||||
@@ -910,6 +926,7 @@ const MessageListEntry = React.memo(({
|
||||
onUserAnimationConsumed={onUserAnimationConsumed}
|
||||
activeStreamingMessageId={activeStreamingMessageId}
|
||||
activeStreamingPhase={activeStreamingPhase}
|
||||
reviewTransferDirection={reviewTransferDirection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -927,6 +944,7 @@ const MessageListEntry = React.memo(({
|
||||
onUserAnimationConsumed={onUserAnimationConsumed}
|
||||
activeStreamingMessageId={activeStreamingMessageId}
|
||||
activeStreamingPhase={activeStreamingPhase}
|
||||
reviewTransferDirection={reviewTransferDirection}
|
||||
onMessageContentChange={onMessageContentChange}
|
||||
getAnimationHandlers={getAnimationHandlers}
|
||||
scrollToBottom={scrollToBottom}
|
||||
@@ -956,9 +974,10 @@ type StaticHistoryListProps = {
|
||||
shouldAnimateUserMessage: (message: ChatMessageEntry) => boolean;
|
||||
onUserAnimationConsumed: (messageId: string) => void;
|
||||
activeStreamingPhase?: StreamPhase | null;
|
||||
reviewTransferDirection?: ReviewTransferDirection | null;
|
||||
};
|
||||
|
||||
const StaticHistoryList = React.memo(({ entries, shouldVirtualize, virtualRows, totalSize, measureElement, contentRef, onMessageContentChange, getAnimationHandlers, scrollToBottom, stickyUserHeader, defaultActivityExpanded, turnUiStates, onToggleTurnGroup, chatRenderMode, shouldAnimateUserMessage, onUserAnimationConsumed, activeStreamingPhase }: StaticHistoryListProps) => {
|
||||
const StaticHistoryList = React.memo(({ entries, shouldVirtualize, virtualRows, totalSize, measureElement, contentRef, onMessageContentChange, getAnimationHandlers, scrollToBottom, stickyUserHeader, defaultActivityExpanded, turnUiStates, onToggleTurnGroup, chatRenderMode, shouldAnimateUserMessage, onUserAnimationConsumed, activeStreamingPhase, reviewTransferDirection }: StaticHistoryListProps) => {
|
||||
const renderEntry = React.useCallback((entry: RenderEntry) => {
|
||||
return (
|
||||
<MessageListEntry
|
||||
@@ -977,9 +996,10 @@ const StaticHistoryList = React.memo(({ entries, shouldVirtualize, virtualRows,
|
||||
onUserAnimationConsumed={onUserAnimationConsumed}
|
||||
activeStreamingMessageId={null}
|
||||
activeStreamingPhase={activeStreamingPhase}
|
||||
reviewTransferDirection={reviewTransferDirection}
|
||||
/>
|
||||
);
|
||||
}, [activeStreamingPhase, chatRenderMode, defaultActivityExpanded, getAnimationHandlers, onMessageContentChange, onToggleTurnGroup, onUserAnimationConsumed, scrollToBottom, shouldAnimateUserMessage, stickyUserHeader, turnUiStates]);
|
||||
}, [activeStreamingPhase, chatRenderMode, defaultActivityExpanded, getAnimationHandlers, onMessageContentChange, onToggleTurnGroup, onUserAnimationConsumed, reviewTransferDirection, scrollToBottom, shouldAnimateUserMessage, stickyUserHeader, turnUiStates]);
|
||||
|
||||
const paddingTop = shouldVirtualize && virtualRows.length > 0
|
||||
? virtualRows[0]?.start ?? 0
|
||||
@@ -1060,6 +1080,7 @@ const StreamingTailContent: React.FC<{
|
||||
onUserAnimationConsumed: (messageId: string) => void;
|
||||
activeStreamingMessageId?: string | null;
|
||||
activeStreamingPhase?: StreamPhase | null;
|
||||
reviewTransferDirection?: ReviewTransferDirection | null;
|
||||
}> = ({
|
||||
entry,
|
||||
onMessageContentChange,
|
||||
@@ -1075,6 +1096,7 @@ const StreamingTailContent: React.FC<{
|
||||
onUserAnimationConsumed,
|
||||
activeStreamingMessageId,
|
||||
activeStreamingPhase,
|
||||
reviewTransferDirection,
|
||||
}) => {
|
||||
return (
|
||||
<MessageListEntry
|
||||
@@ -1092,6 +1114,7 @@ const StreamingTailContent: React.FC<{
|
||||
onUserAnimationConsumed={onUserAnimationConsumed}
|
||||
activeStreamingMessageId={activeStreamingMessageId}
|
||||
activeStreamingPhase={activeStreamingPhase}
|
||||
reviewTransferDirection={reviewTransferDirection}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1118,6 +1141,20 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
||||
const activityRenderMode = useUIStore((state) => state.activityRenderMode);
|
||||
const showTurnChangedFiles = useUIStore((state) => state.showTurnChangedFiles);
|
||||
const defaultActivityExpanded = activityRenderMode === 'summary';
|
||||
const reviewTransferDirection = useGlobalSessionsStore((state) => {
|
||||
const currentSession = state.activeSessions.find((session) => session.id === sessionKey);
|
||||
const direction = getReviewTransferDirection(currentSession);
|
||||
if (!currentSession || !direction) return null;
|
||||
|
||||
const targetSessionId = direction === 'review-to-original'
|
||||
? getOriginalSessionID(currentSession)
|
||||
: getReviewSessionID(currentSession);
|
||||
if (!targetSessionId) return null;
|
||||
|
||||
return state.activeSessions.some((session) => session.id === targetSessionId)
|
||||
? direction
|
||||
: null;
|
||||
});
|
||||
const [turnUiStates, setTurnUiStates] = React.useState<Map<string, TurnUiState>>(() => new Map());
|
||||
const userAnimationRef = React.useRef<{
|
||||
sessionKey: string | undefined;
|
||||
@@ -1692,6 +1729,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
||||
shouldAnimateUserMessage={shouldAnimateUserMessage}
|
||||
onUserAnimationConsumed={onUserAnimationConsumed}
|
||||
activeStreamingPhase={activeStreamingPhase}
|
||||
reviewTransferDirection={reviewTransferDirection}
|
||||
/>
|
||||
{trailingStreamingEntry ? (
|
||||
<StreamingTailContent
|
||||
@@ -1709,6 +1747,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
||||
onUserAnimationConsumed={onUserAnimationConsumed}
|
||||
activeStreamingMessageId={activeStreamingMessageId}
|
||||
activeStreamingPhase={activeStreamingPhase}
|
||||
reviewTransferDirection={reviewTransferDirection}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -48,9 +48,8 @@ import { useI18n } from '@/lib/i18n';
|
||||
import { extractLoopbackUrls } from '@/lib/url';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||
import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
||||
import {
|
||||
getReviewTransferDirection,
|
||||
type ReviewTransferDirection,
|
||||
sendImplementationResponseToReviewer,
|
||||
sendReviewFeedbackToOriginal,
|
||||
} from '@/lib/reviewFlow';
|
||||
@@ -366,6 +365,7 @@ interface MessageBodyProps {
|
||||
errorVariant?: 'error' | 'info';
|
||||
userActionsMode?: 'inline' | 'external-content' | 'external-actions';
|
||||
stickyUserHeaderEnabled?: boolean;
|
||||
reviewTransferDirection?: ReviewTransferDirection | null;
|
||||
}
|
||||
|
||||
const TOOL_REVEAL_CACHE_MAX = 200;
|
||||
@@ -967,6 +967,7 @@ const AssistantMessageBody = React.memo(({
|
||||
turnGroupingContext,
|
||||
errorMessage,
|
||||
errorVariant = 'error',
|
||||
reviewTransferDirection = null,
|
||||
}: Omit<MessageBodyProps, 'isUser'>) => {
|
||||
const { t } = useI18n();
|
||||
const chatSurfaceMode = useChatSurfaceMode();
|
||||
@@ -1121,26 +1122,15 @@ const AssistantMessageBody = React.memo(({
|
||||
const createSessionFromAssistantMessage = useSessionUIStore((state) => state.createSessionFromAssistantMessage);
|
||||
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
||||
const getDirectoryForSession = useSessionUIStore((state) => state.getDirectoryForSession);
|
||||
const currentSession = useGlobalSessionsStore((state) => {
|
||||
if (!sessionId) return null;
|
||||
for (const candidate of state.activeSessions) {
|
||||
if (candidate.id === sessionId) return candidate;
|
||||
}
|
||||
for (const candidate of state.archivedSessions) {
|
||||
if (candidate.id === sessionId) return candidate;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const openMultiRunLauncherWithPrompt = useUIStore((state) => state.openMultiRunLauncherWithPrompt);
|
||||
const projects = useProjectsStore((state) => state.projects);
|
||||
const effectiveDirectory = useEffectiveDirectory();
|
||||
const currentReviewTransferDirection = getReviewTransferDirection(currentSession);
|
||||
const isReviewSessionView = currentReviewTransferDirection === 'review-to-original';
|
||||
const reviewTransferDirection = (!isMobile && !isVSCode) ? currentReviewTransferDirection : null;
|
||||
const isReviewSessionView = reviewTransferDirection === 'review-to-original';
|
||||
const effectiveReviewTransferDirection = (!isMobile && !isVSCode) ? reviewTransferDirection : null;
|
||||
const reviewTransferAction = React.useMemo(() => {
|
||||
const transferText = assistantPlanText.trim();
|
||||
if (!sessionId || !effectiveDirectory || !transferText || !reviewTransferDirection) return undefined;
|
||||
if (reviewTransferDirection === 'review-to-original') {
|
||||
if (!sessionId || !effectiveDirectory || !transferText || !effectiveReviewTransferDirection) return undefined;
|
||||
if (effectiveReviewTransferDirection === 'review-to-original') {
|
||||
return {
|
||||
ariaLabel: t('chat.messageBody.actions.sendReviewFeedback'),
|
||||
tooltip: t('chat.messageBody.actions.sendReviewFeedback'),
|
||||
@@ -1164,7 +1154,7 @@ const AssistantMessageBody = React.memo(({
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [assistantPlanText, effectiveDirectory, reviewTransferDirection, sessionId, t]);
|
||||
}, [assistantPlanText, effectiveDirectory, effectiveReviewTransferDirection, sessionId, t]);
|
||||
const [isPlanDialogOpen, setIsPlanDialogOpen] = React.useState(false);
|
||||
const [isSavingPlan, setIsSavingPlan] = React.useState(false);
|
||||
const [isForkDialogOpen, setIsForkDialogOpen] = React.useState(false);
|
||||
|
||||
@@ -226,7 +226,9 @@ export const sendImplementationResponseToReviewer = async (originalSessionID: st
|
||||
openReviewSessionPanel(directory, reviewSession);
|
||||
};
|
||||
|
||||
export const getReviewTransferDirection = (session: Session | null | undefined): 'review-to-original' | 'original-to-review' | null => {
|
||||
export type ReviewTransferDirection = 'review-to-original' | 'original-to-review';
|
||||
|
||||
export const getReviewTransferDirection = (session: Session | null | undefined): ReviewTransferDirection | null => {
|
||||
if (isReviewSession(session)) return 'review-to-original';
|
||||
if (getReviewSessionID(session)) return 'original-to-review';
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user