perf(renderer): reduce member card render work
This commit is contained in:
parent
60d806135c
commit
b8a53dcc09
3 changed files with 45 additions and 78 deletions
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue