perf(renderer): memoize chat and sidebar list item components
Wrap SessionItem, SubagentItem, ExecutionTrace, TextItem, ThinkingItem, and DisplayItemList in React.memo. These components render repeatedly in virtualized lists and AI chat groups — memoizing them eliminates redundant renders when their props have not changed, reducing CPU work in active sessions with many messages or long session sidebars.
This commit is contained in:
parent
f764af17d8
commit
fa38b90f9c
6 changed files with 1385 additions and 1356 deletions
|
|
@ -87,7 +87,8 @@ function truncateText(text: string, maxLength: number): string {
|
||||||
*
|
*
|
||||||
* The list is completely flat with no nested toggles or hierarchies.
|
* The list is completely flat with no nested toggles or hierarchies.
|
||||||
*/
|
*/
|
||||||
export const DisplayItemList = ({
|
export const DisplayItemList = React.memo(
|
||||||
|
({
|
||||||
items,
|
items,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
expandedItemIds,
|
expandedItemIds,
|
||||||
|
|
@ -113,7 +114,10 @@ export const DisplayItemList = ({
|
||||||
const isItemInReplyLink = (item: AIGroupDisplayItem): boolean => {
|
const isItemInReplyLink = (item: AIGroupDisplayItem): boolean => {
|
||||||
if (!replyLinkToolId) return false;
|
if (!replyLinkToolId) return false;
|
||||||
if (item.type === 'tool' && item.tool.id === replyLinkToolId) return true;
|
if (item.type === 'tool' && item.tool.id === replyLinkToolId) return true;
|
||||||
if (item.type === 'teammate_message' && item.teammateMessage.replyToToolId === replyLinkToolId)
|
if (
|
||||||
|
item.type === 'teammate_message' &&
|
||||||
|
item.teammateMessage.replyToToolId === replyLinkToolId
|
||||||
|
)
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
@ -354,7 +358,10 @@ export const DisplayItemList = ({
|
||||||
/>
|
/>
|
||||||
<Layers size={14} />
|
<Layers size={14} />
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 text-xs font-medium" style={{ color: TOOL_CALL_TEXT }}>
|
<span
|
||||||
|
className="shrink-0 text-xs font-medium"
|
||||||
|
style={{ color: TOOL_CALL_TEXT }}
|
||||||
|
>
|
||||||
Compacted
|
Compacted
|
||||||
</span>
|
</span>
|
||||||
{item.tokenDelta && (
|
{item.tokenDelta && (
|
||||||
|
|
@ -428,4 +435,5 @@ export const DisplayItemList = ({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ interface ExecutionTraceProps {
|
||||||
// Execution Trace Component
|
// Execution Trace Component
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export const ExecutionTrace: React.FC<ExecutionTraceProps> = ({
|
export const ExecutionTrace: React.FC<ExecutionTraceProps> = React.memo(
|
||||||
|
({
|
||||||
items,
|
items,
|
||||||
aiGroupId: _aiGroupId,
|
aiGroupId: _aiGroupId,
|
||||||
highlightToolUseId,
|
highlightToolUseId,
|
||||||
|
|
@ -217,7 +218,10 @@ export const ExecutionTrace: React.FC<ExecutionTraceProps> = ({
|
||||||
/>
|
/>
|
||||||
<Layers size={14} />
|
<Layers size={14} />
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 text-xs font-medium" style={{ color: TOOL_CALL_TEXT }}>
|
<span
|
||||||
|
className="shrink-0 text-xs font-medium"
|
||||||
|
style={{ color: TOOL_CALL_TEXT }}
|
||||||
|
>
|
||||||
Compacted
|
Compacted
|
||||||
</span>
|
</span>
|
||||||
{item.tokenDelta && (
|
{item.tokenDelta && (
|
||||||
|
|
@ -276,4 +280,5 @@ export const ExecutionTrace: React.FC<ExecutionTraceProps> = ({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,8 @@ interface SubagentItemProps {
|
||||||
// Main Component - Linear-style DevTools Card
|
// Main Component - Linear-style DevTools Card
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export const SubagentItem: React.FC<SubagentItemProps> = ({
|
export const SubagentItem: React.FC<SubagentItemProps> = React.memo(
|
||||||
|
({
|
||||||
step,
|
step,
|
||||||
subagent,
|
subagent,
|
||||||
onClick,
|
onClick,
|
||||||
|
|
@ -483,7 +484,10 @@ export const SubagentItem: React.FC<SubagentItemProps> = ({
|
||||||
{hasIsolated && (
|
{hasIsolated && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CircleDot className="size-3" style={{ color: 'rgba(56, 189, 248, 0.7)' }} />
|
<CircleDot
|
||||||
|
className="size-3"
|
||||||
|
style={{ color: 'rgba(56, 189, 248, 0.7)' }}
|
||||||
|
/>
|
||||||
<span className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }}>
|
<span className="text-xs" style={{ color: COLOR_TEXT_SECONDARY }}>
|
||||||
{subagent.team ? 'Context Window' : 'Subagent Context'}
|
{subagent.team ? 'Context Window' : 'Subagent Context'}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -500,7 +504,10 @@ export const SubagentItem: React.FC<SubagentItemProps> = ({
|
||||||
{/* Per-phase breakdown when multi-phase */}
|
{/* Per-phase breakdown when multi-phase */}
|
||||||
{isMultiPhase &&
|
{isMultiPhase &&
|
||||||
phaseData.phases.map((phase) => (
|
phaseData.phases.map((phase) => (
|
||||||
<div key={phase.phaseNumber} className="flex items-center justify-between pl-5">
|
<div
|
||||||
|
key={phase.phaseNumber}
|
||||||
|
className="flex items-center justify-between pl-5"
|
||||||
|
>
|
||||||
<span className="text-[11px]" style={{ color: CARD_ICON_MUTED }}>
|
<span className="text-[11px]" style={{ color: CARD_ICON_MUTED }}>
|
||||||
Phase {phase.phaseNumber}
|
Phase {phase.phaseNumber}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -589,4 +596,5 @@ export const SubagentItem: React.FC<SubagentItemProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ interface TextItemProps {
|
||||||
titleText?: string;
|
titleText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextItem: React.FC<TextItemProps> = ({
|
export const TextItem: React.FC<TextItemProps> = React.memo(
|
||||||
|
({
|
||||||
step,
|
step,
|
||||||
preview,
|
preview,
|
||||||
onClick,
|
onClick,
|
||||||
|
|
@ -79,4 +80,5 @@ export const TextItem: React.FC<TextItemProps> = ({
|
||||||
/>
|
/>
|
||||||
</BaseItem>
|
</BaseItem>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ interface ThinkingItemProps {
|
||||||
titleText?: string;
|
titleText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThinkingItem: React.FC<ThinkingItemProps> = ({
|
export const ThinkingItem: React.FC<ThinkingItemProps> = React.memo(
|
||||||
|
({
|
||||||
step,
|
step,
|
||||||
preview,
|
preview,
|
||||||
onClick,
|
onClick,
|
||||||
|
|
@ -79,4 +80,5 @@ export const ThinkingItem: React.FC<ThinkingItemProps> = ({
|
||||||
/>
|
/>
|
||||||
</BaseItem>
|
</BaseItem>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* Supports right-click context menu for pane management.
|
* Supports right-click context menu for pane management.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { memo, useCallback, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||||
|
|
@ -156,7 +156,8 @@ const SessionRuntimeBadge = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SessionItem = ({
|
export const SessionItem = memo(
|
||||||
|
({
|
||||||
session,
|
session,
|
||||||
isActive,
|
isActive,
|
||||||
isPinned,
|
isPinned,
|
||||||
|
|
@ -345,7 +346,9 @@ export const SessionItem = ({
|
||||||
{session.messageCount}
|
{session.messageCount}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ opacity: 0.5 }}>·</span>
|
<span style={{ opacity: 0.5 }}>·</span>
|
||||||
<span className="tabular-nums">{formatShortTime(new Date(session.createdAt))}</span>
|
<span className="tabular-nums">
|
||||||
|
{formatShortTime(new Date(session.createdAt))}
|
||||||
|
</span>
|
||||||
{session.model && (
|
{session.model && (
|
||||||
<>
|
<>
|
||||||
<span style={{ opacity: 0.5 }}>·</span>
|
<span style={{ opacity: 0.5 }}>·</span>
|
||||||
|
|
@ -390,4 +393,5 @@ export const SessionItem = ({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue