perf(renderer): reduce member card render work

This commit is contained in:
777genius 2026-05-30 20:58:31 +03:00
parent 60d806135c
commit b8a53dcc09
3 changed files with 45 additions and 78 deletions

View file

@ -7,13 +7,10 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { cn } from '@renderer/lib/utils';
import { useStore } from '@renderer/store';
import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice';
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { renderLinkifiedText } from '@renderer/utils/linkifiedText';
import {
agentAvatarUrl,
buildMemberAvatarMap,
buildMemberLaunchPresentation,
displayMemberName,
isOpenCodeRelaunchActionable,
@ -72,8 +69,10 @@ export interface RuntimeTelemetryScale {
}
interface MemberCardProps {
teamName?: string;
member: ResolvedTeamMember;
memberColor: string;
avatarUrl?: string;
fullBleedSurface?: boolean;
runtimeSummary?: string;
runtimeEntry?: TeamAgentRuntimeEntry;
@ -301,53 +300,8 @@ function normalizeRuntimeTelemetrySamples(history: unknown): TeamAgentRuntimeRes
return (Array.isArray(history) ? history : []).filter(isRuntimeTelemetrySampleLike);
}
function buildRuntimeTelemetryTitle(
runtimeEntry: TeamAgentRuntimeEntry | undefined
): string | undefined {
if (!runtimeEntry) {
return undefined;
}
if (normalizeRuntimeTelemetrySamples(runtimeEntry?.resourceHistory).length === 0) {
return undefined;
}
const lines = [
'CPU includes parent + child processes.',
'Local CPU excludes remote LLM inference.',
];
if (runtimeEntry.runtimeLoadScope === 'shared-host') {
lines.push('Shared OpenCode host metric; not exclusive to this member.');
}
if (runtimeEntry.runtimeLoadTruncated) {
lines.push('Process tree was capped for this sample.');
}
const detailParts = [
runtimeEntry.pid ? `root PID ${runtimeEntry.pid}` : undefined,
runtimeEntry.processCount ? `${runtimeEntry.processCount} processes` : undefined,
runtimeEntry.runtimeLoadScope ? `scope ${runtimeEntry.runtimeLoadScope}` : undefined,
'sample 5s',
].filter((part): part is string => Boolean(part));
if (detailParts.length > 0) {
lines.push(detailParts.join(' · '));
}
const aggregateCpuLabel = formatRuntimeTelemetryPercent(runtimeEntry.cpuPercent);
const primaryCpuLabel = formatRuntimeTelemetryPercent(runtimeEntry.primaryCpuPercent);
const childCpuLabel = formatRuntimeTelemetryPercent(runtimeEntry.childCpuPercent);
const rssLabel = formatRuntimeTelemetryBytes(runtimeEntry.rssBytes);
const splitParts = [
aggregateCpuLabel ? `CPU ${aggregateCpuLabel}` : undefined,
primaryCpuLabel ? `root ${primaryCpuLabel}` : undefined,
childCpuLabel ? `children ${childCpuLabel}` : undefined,
rssLabel ? `RSS ${rssLabel}` : undefined,
].filter((part): part is string => Boolean(part));
if (splitParts.length > 0) {
lines.push(splitParts.join(' · '));
}
lines.push('RSS is summed process RSS and can include shared pages.');
return lines.join('\n');
function hasRuntimeTelemetrySamples(history: unknown): boolean {
return Array.isArray(history) && history.some(isRuntimeTelemetrySampleLike);
}
const RuntimeTelemetryTooltipContent = ({
@ -613,8 +567,10 @@ const MemberRuntimeTelemetryStrip = memo(function MemberRuntimeTelemetryStrip({
});
export const MemberCard = memo(function MemberCard({
teamName,
member,
memberColor,
avatarUrl,
fullBleedSurface = true,
runtimeSummary,
runtimeEntry,
@ -654,17 +610,12 @@ export const MemberCard = memo(function MemberCard({
// const leadContext = useStore((s) =>
// member.agentType === 'team-lead' && teamName ? s.leadContextByTeam[teamName] : undefined
// );
const selectedTeamName = useStore((s) => s.selectedTeamName);
const [retryingLaunch, setRetryingLaunch] = useState(false);
const [retryLaunchError, setRetryLaunchError] = useState<string | null>(null);
const [skippingLaunch, setSkippingLaunch] = useState(false);
const [skipLaunchError, setSkipLaunchError] = useState<string | null>(null);
const [restoringMember, setRestoringMember] = useState(false);
const [restoreMemberError, setRestoreMemberError] = useState<string | null>(null);
const teamMembers = useStore((s) =>
selectedTeamName ? selectResolvedMembersForTeamName(s, selectedTeamName) : []
);
const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]);
const bootstrapConfirmedProvisionedButNotAlive =
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
const hasUnsafeBootstrapConfirmedProvisionedButNotAlive =
@ -759,8 +710,10 @@ export const MemberCard = memo(function MemberCard({
: visibleReviewTask
? `Reviewing task: #${deriveTaskDisplayId(visibleReviewTask.id)}`
: undefined;
const runtimeTelemetryTitle = buildRuntimeTelemetryTitle(runtimeEntry);
const showRuntimeTelemetryTooltip = Boolean(runtimeTelemetryTitle);
const showRuntimeTelemetryTooltip = useMemo(
() => hasRuntimeTelemetrySamples(runtimeEntry?.resourceHistory),
[runtimeEntry?.resourceHistory]
);
const rowTitle = showRuntimeTelemetryTooltip ? undefined : activityTitle;
const runtimeTelemetryTooltipIdRef = useRef<string | null>(null);
if (runtimeTelemetryTooltipIdRef.current == null) {
@ -881,7 +834,7 @@ export const MemberCard = memo(function MemberCard({
const launchDiagnosticsPayload = useMemo(
() =>
buildMemberLaunchDiagnosticsPayload({
teamName: selectedTeamName,
teamName,
runId: runtimeRunId,
memberName: member.name,
member,
@ -900,7 +853,7 @@ export const MemberCard = memo(function MemberCard({
runtimeAdvisoryLabel,
runtimeAdvisoryTitle,
runtimeRunId,
selectedTeamName,
teamName,
spawnEntry,
effectiveSpawnLaunchState,
spawnLivenessSource,
@ -1077,7 +1030,7 @@ export const MemberCard = memo(function MemberCard({
}}
>
<img
src={avatarMap.get(member.name) ?? agentAvatarUrl(member.name)}
src={avatarUrl ?? agentAvatarUrl(member.name)}
alt={member.name}
className="size-7 rounded-full bg-[var(--color-surface-raised)]"
loading="lazy"
@ -1585,14 +1538,16 @@ export const MemberCard = memo(function MemberCard({
onOpenChange={handleRuntimeTelemetryTooltipOpenChange}
>
<TooltipTrigger asChild>{cardContent}</TooltipTrigger>
<TooltipContent
side="left"
align="start"
sideOffset={8}
className="border-blue-400/20 bg-[var(--color-surface)] p-3 shadow-xl shadow-black/30"
>
<RuntimeTelemetryTooltipContent runtimeEntry={runtimeEntry} />
</TooltipContent>
{runtimeTelemetryTooltipOpen ? (
<TooltipContent
side="left"
align="start"
sideOffset={8}
className="border-blue-400/20 bg-[var(--color-surface)] p-3 shadow-xl shadow-black/30"
>
<RuntimeTelemetryTooltipContent runtimeEntry={runtimeEntry} />
</TooltipContent>
) : null}
</Tooltip>
);
});

View file

@ -7,7 +7,11 @@ import {
deriveWorkActivityTimerAnchor,
syncMemberActivityTimer,
} from '@renderer/utils/memberActivityTimer';
import { buildMemberColorMap, shouldDisplayMemberCurrentTask } from '@renderer/utils/memberHelpers';
import {
buildMemberAvatarMap,
buildMemberColorMap,
shouldDisplayMemberCurrentTask,
} from '@renderer/utils/memberHelpers';
import { resolveMemberRuntimeSummary } from '@renderer/utils/memberRuntimeSummary';
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
import { isLeadMember } from '@shared/utils/leadDetection';
@ -480,9 +484,11 @@ function areMemberListPropsEqual(
// ---------------------------------------------------------------------------
interface MemberCardRowProps {
teamName: string;
member: ResolvedTeamMember;
isRemoved: boolean;
memberColor: string;
avatarUrl?: string;
fullBleedSurface: boolean;
currentTask: TeamTaskWithKanban | null;
reviewTask: TeamTaskWithKanban | null;
@ -516,9 +522,11 @@ interface MemberCardRowProps {
}
const MemberCardRow = memo(function MemberCardRow({
teamName,
member,
isRemoved,
memberColor,
avatarUrl,
fullBleedSurface,
currentTask,
reviewTask,
@ -567,8 +575,10 @@ const MemberCardRow = memo(function MemberCardRow({
return (
<MemberCard
teamName={teamName}
member={member}
memberColor={memberColor}
avatarUrl={avatarUrl}
fullBleedSurface={fullBleedSurface}
taskCounts={taskCounts}
isTeamAlive={isTeamAlive}
@ -761,6 +771,7 @@ export const MemberList = memo(function MemberList({
[activeMembers]
);
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
const avatarMap = useMemo(() => buildMemberAvatarMap(members), [members]);
const runtimeTelemetryScale = useMemo(
() => buildRuntimeTelemetryScale(activeMembers, memberRuntimeEntries),
[activeMembers, memberRuntimeEntries]
@ -1012,9 +1023,11 @@ export const MemberList = memo(function MemberList({
return (
<MemberCardRow
key={member.name}
teamName={teamName}
member={member}
isRemoved={false}
memberColor={colorMap.get(member.name) ?? 'blue'}
avatarUrl={avatarMap.get(member.name)}
fullBleedSurface={!isWide}
currentTask={currentTask}
reviewTask={reviewTask}
@ -1064,9 +1077,11 @@ export const MemberList = memo(function MemberList({
{removedMembers.map((member) => (
<MemberCardRow
key={member.name}
teamName={teamName}
member={member}
isRemoved={true}
memberColor={colorMap.get(member.name) ?? 'blue'}
avatarUrl={avatarMap.get(member.name)}
fullBleedSurface={!isWide}
currentTask={null}
reviewTask={null}

View file

@ -707,6 +707,7 @@ describe('MemberCard starting-state visuals', () => {
React.createElement(MemberCard, {
member,
memberColor: 'blue',
avatarUrl: 'https://example.com/alice.png',
isTeamAlive: true,
isTeamProvisioning: false,
})
@ -719,6 +720,7 @@ describe('MemberCard starting-state visuals', () => {
const clickableCard = host.querySelector('[role="button"]') as HTMLElement | null;
expect(avatarRing).not.toBeNull();
expect(img?.getAttribute('src')).toBe('https://example.com/alice.png');
expect(avatarRing?.style.borderColor).toBe('#3b82f6');
expect(clickableCard?.style.borderLeft).toBe('');
expect(clickableCard?.style.background).toBe('');
@ -878,18 +880,13 @@ describe('MemberCard starting-state visuals', () => {
const runtimeTooltipContent = Array.from(
host.querySelectorAll('[data-testid="tooltip-content"]')
).find((content) => content.className.includes('border-blue-400/20'));
expect(runtimeTooltipContent?.getAttribute('data-side')).toBe('left');
expect(host.querySelector('[data-testid="tooltip-root"]')?.getAttribute('data-open')).toBe(
'false'
);
expect(host.textContent).toContain('Local runtime load');
expect(host.textContent).toContain('Parent and child processes only.');
expect(host.textContent).toContain('root PID 222');
expect(host.textContent).toContain('3 processes');
expect(host.textContent).toContain('CPU');
expect(host.textContent).toContain('14%');
expect(host.textContent).toContain('Memory');
expect(host.textContent).toContain('238 MB');
expect(runtimeTooltipContent).toBeUndefined();
expect(host.textContent).not.toContain('Local runtime load');
expect(host.textContent).not.toContain('Parent and child processes only.');
expect(host.textContent).not.toContain('root PID 222');
await act(async () => {
root.unmount();