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 { getTeamColorSet } from '@renderer/constants/teamColors';
|
||||||
import { useTheme } from '@renderer/hooks/useTheme';
|
import { useTheme } from '@renderer/hooks/useTheme';
|
||||||
import { cn } from '@renderer/lib/utils';
|
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 { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
||||||
import { renderLinkifiedText } from '@renderer/utils/linkifiedText';
|
import { renderLinkifiedText } from '@renderer/utils/linkifiedText';
|
||||||
import {
|
import {
|
||||||
agentAvatarUrl,
|
agentAvatarUrl,
|
||||||
buildMemberAvatarMap,
|
|
||||||
buildMemberLaunchPresentation,
|
buildMemberLaunchPresentation,
|
||||||
displayMemberName,
|
displayMemberName,
|
||||||
isOpenCodeRelaunchActionable,
|
isOpenCodeRelaunchActionable,
|
||||||
|
|
@ -72,8 +69,10 @@ export interface RuntimeTelemetryScale {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MemberCardProps {
|
interface MemberCardProps {
|
||||||
|
teamName?: string;
|
||||||
member: ResolvedTeamMember;
|
member: ResolvedTeamMember;
|
||||||
memberColor: string;
|
memberColor: string;
|
||||||
|
avatarUrl?: string;
|
||||||
fullBleedSurface?: boolean;
|
fullBleedSurface?: boolean;
|
||||||
runtimeSummary?: string;
|
runtimeSummary?: string;
|
||||||
runtimeEntry?: TeamAgentRuntimeEntry;
|
runtimeEntry?: TeamAgentRuntimeEntry;
|
||||||
|
|
@ -301,53 +300,8 @@ function normalizeRuntimeTelemetrySamples(history: unknown): TeamAgentRuntimeRes
|
||||||
return (Array.isArray(history) ? history : []).filter(isRuntimeTelemetrySampleLike);
|
return (Array.isArray(history) ? history : []).filter(isRuntimeTelemetrySampleLike);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRuntimeTelemetryTitle(
|
function hasRuntimeTelemetrySamples(history: unknown): boolean {
|
||||||
runtimeEntry: TeamAgentRuntimeEntry | undefined
|
return Array.isArray(history) && history.some(isRuntimeTelemetrySampleLike);
|
||||||
): 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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RuntimeTelemetryTooltipContent = ({
|
const RuntimeTelemetryTooltipContent = ({
|
||||||
|
|
@ -613,8 +567,10 @@ const MemberRuntimeTelemetryStrip = memo(function MemberRuntimeTelemetryStrip({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MemberCard = memo(function MemberCard({
|
export const MemberCard = memo(function MemberCard({
|
||||||
|
teamName,
|
||||||
member,
|
member,
|
||||||
memberColor,
|
memberColor,
|
||||||
|
avatarUrl,
|
||||||
fullBleedSurface = true,
|
fullBleedSurface = true,
|
||||||
runtimeSummary,
|
runtimeSummary,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
|
|
@ -654,17 +610,12 @@ export const MemberCard = memo(function MemberCard({
|
||||||
// const leadContext = useStore((s) =>
|
// const leadContext = useStore((s) =>
|
||||||
// member.agentType === 'team-lead' && teamName ? s.leadContextByTeam[teamName] : undefined
|
// member.agentType === 'team-lead' && teamName ? s.leadContextByTeam[teamName] : undefined
|
||||||
// );
|
// );
|
||||||
const selectedTeamName = useStore((s) => s.selectedTeamName);
|
|
||||||
const [retryingLaunch, setRetryingLaunch] = useState(false);
|
const [retryingLaunch, setRetryingLaunch] = useState(false);
|
||||||
const [retryLaunchError, setRetryLaunchError] = useState<string | null>(null);
|
const [retryLaunchError, setRetryLaunchError] = useState<string | null>(null);
|
||||||
const [skippingLaunch, setSkippingLaunch] = useState(false);
|
const [skippingLaunch, setSkippingLaunch] = useState(false);
|
||||||
const [skipLaunchError, setSkipLaunchError] = useState<string | null>(null);
|
const [skipLaunchError, setSkipLaunchError] = useState<string | null>(null);
|
||||||
const [restoringMember, setRestoringMember] = useState(false);
|
const [restoringMember, setRestoringMember] = useState(false);
|
||||||
const [restoreMemberError, setRestoreMemberError] = useState<string | null>(null);
|
const [restoreMemberError, setRestoreMemberError] = useState<string | null>(null);
|
||||||
const teamMembers = useStore((s) =>
|
|
||||||
selectedTeamName ? selectResolvedMembersForTeamName(s, selectedTeamName) : []
|
|
||||||
);
|
|
||||||
const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]);
|
|
||||||
const bootstrapConfirmedProvisionedButNotAlive =
|
const bootstrapConfirmedProvisionedButNotAlive =
|
||||||
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
|
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
|
||||||
const hasUnsafeBootstrapConfirmedProvisionedButNotAlive =
|
const hasUnsafeBootstrapConfirmedProvisionedButNotAlive =
|
||||||
|
|
@ -759,8 +710,10 @@ export const MemberCard = memo(function MemberCard({
|
||||||
: visibleReviewTask
|
: visibleReviewTask
|
||||||
? `Reviewing task: #${deriveTaskDisplayId(visibleReviewTask.id)}`
|
? `Reviewing task: #${deriveTaskDisplayId(visibleReviewTask.id)}`
|
||||||
: undefined;
|
: undefined;
|
||||||
const runtimeTelemetryTitle = buildRuntimeTelemetryTitle(runtimeEntry);
|
const showRuntimeTelemetryTooltip = useMemo(
|
||||||
const showRuntimeTelemetryTooltip = Boolean(runtimeTelemetryTitle);
|
() => hasRuntimeTelemetrySamples(runtimeEntry?.resourceHistory),
|
||||||
|
[runtimeEntry?.resourceHistory]
|
||||||
|
);
|
||||||
const rowTitle = showRuntimeTelemetryTooltip ? undefined : activityTitle;
|
const rowTitle = showRuntimeTelemetryTooltip ? undefined : activityTitle;
|
||||||
const runtimeTelemetryTooltipIdRef = useRef<string | null>(null);
|
const runtimeTelemetryTooltipIdRef = useRef<string | null>(null);
|
||||||
if (runtimeTelemetryTooltipIdRef.current == null) {
|
if (runtimeTelemetryTooltipIdRef.current == null) {
|
||||||
|
|
@ -881,7 +834,7 @@ export const MemberCard = memo(function MemberCard({
|
||||||
const launchDiagnosticsPayload = useMemo(
|
const launchDiagnosticsPayload = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildMemberLaunchDiagnosticsPayload({
|
buildMemberLaunchDiagnosticsPayload({
|
||||||
teamName: selectedTeamName,
|
teamName,
|
||||||
runId: runtimeRunId,
|
runId: runtimeRunId,
|
||||||
memberName: member.name,
|
memberName: member.name,
|
||||||
member,
|
member,
|
||||||
|
|
@ -900,7 +853,7 @@ export const MemberCard = memo(function MemberCard({
|
||||||
runtimeAdvisoryLabel,
|
runtimeAdvisoryLabel,
|
||||||
runtimeAdvisoryTitle,
|
runtimeAdvisoryTitle,
|
||||||
runtimeRunId,
|
runtimeRunId,
|
||||||
selectedTeamName,
|
teamName,
|
||||||
spawnEntry,
|
spawnEntry,
|
||||||
effectiveSpawnLaunchState,
|
effectiveSpawnLaunchState,
|
||||||
spawnLivenessSource,
|
spawnLivenessSource,
|
||||||
|
|
@ -1077,7 +1030,7 @@ export const MemberCard = memo(function MemberCard({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={avatarMap.get(member.name) ?? agentAvatarUrl(member.name)}
|
src={avatarUrl ?? agentAvatarUrl(member.name)}
|
||||||
alt={member.name}
|
alt={member.name}
|
||||||
className="size-7 rounded-full bg-[var(--color-surface-raised)]"
|
className="size-7 rounded-full bg-[var(--color-surface-raised)]"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|
@ -1585,14 +1538,16 @@ export const MemberCard = memo(function MemberCard({
|
||||||
onOpenChange={handleRuntimeTelemetryTooltipOpenChange}
|
onOpenChange={handleRuntimeTelemetryTooltipOpenChange}
|
||||||
>
|
>
|
||||||
<TooltipTrigger asChild>{cardContent}</TooltipTrigger>
|
<TooltipTrigger asChild>{cardContent}</TooltipTrigger>
|
||||||
<TooltipContent
|
{runtimeTelemetryTooltipOpen ? (
|
||||||
side="left"
|
<TooltipContent
|
||||||
align="start"
|
side="left"
|
||||||
sideOffset={8}
|
align="start"
|
||||||
className="border-blue-400/20 bg-[var(--color-surface)] p-3 shadow-xl shadow-black/30"
|
sideOffset={8}
|
||||||
>
|
className="border-blue-400/20 bg-[var(--color-surface)] p-3 shadow-xl shadow-black/30"
|
||||||
<RuntimeTelemetryTooltipContent runtimeEntry={runtimeEntry} />
|
>
|
||||||
</TooltipContent>
|
<RuntimeTelemetryTooltipContent runtimeEntry={runtimeEntry} />
|
||||||
|
</TooltipContent>
|
||||||
|
) : null}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@ import {
|
||||||
deriveWorkActivityTimerAnchor,
|
deriveWorkActivityTimerAnchor,
|
||||||
syncMemberActivityTimer,
|
syncMemberActivityTimer,
|
||||||
} from '@renderer/utils/memberActivityTimer';
|
} 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 { resolveMemberRuntimeSummary } from '@renderer/utils/memberRuntimeSummary';
|
||||||
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
|
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
|
|
@ -480,9 +484,11 @@ function areMemberListPropsEqual(
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
interface MemberCardRowProps {
|
interface MemberCardRowProps {
|
||||||
|
teamName: string;
|
||||||
member: ResolvedTeamMember;
|
member: ResolvedTeamMember;
|
||||||
isRemoved: boolean;
|
isRemoved: boolean;
|
||||||
memberColor: string;
|
memberColor: string;
|
||||||
|
avatarUrl?: string;
|
||||||
fullBleedSurface: boolean;
|
fullBleedSurface: boolean;
|
||||||
currentTask: TeamTaskWithKanban | null;
|
currentTask: TeamTaskWithKanban | null;
|
||||||
reviewTask: TeamTaskWithKanban | null;
|
reviewTask: TeamTaskWithKanban | null;
|
||||||
|
|
@ -516,9 +522,11 @@ interface MemberCardRowProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemberCardRow = memo(function MemberCardRow({
|
const MemberCardRow = memo(function MemberCardRow({
|
||||||
|
teamName,
|
||||||
member,
|
member,
|
||||||
isRemoved,
|
isRemoved,
|
||||||
memberColor,
|
memberColor,
|
||||||
|
avatarUrl,
|
||||||
fullBleedSurface,
|
fullBleedSurface,
|
||||||
currentTask,
|
currentTask,
|
||||||
reviewTask,
|
reviewTask,
|
||||||
|
|
@ -567,8 +575,10 @@ const MemberCardRow = memo(function MemberCardRow({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MemberCard
|
<MemberCard
|
||||||
|
teamName={teamName}
|
||||||
member={member}
|
member={member}
|
||||||
memberColor={memberColor}
|
memberColor={memberColor}
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
fullBleedSurface={fullBleedSurface}
|
fullBleedSurface={fullBleedSurface}
|
||||||
taskCounts={taskCounts}
|
taskCounts={taskCounts}
|
||||||
isTeamAlive={isTeamAlive}
|
isTeamAlive={isTeamAlive}
|
||||||
|
|
@ -761,6 +771,7 @@ export const MemberList = memo(function MemberList({
|
||||||
[activeMembers]
|
[activeMembers]
|
||||||
);
|
);
|
||||||
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
|
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
|
||||||
|
const avatarMap = useMemo(() => buildMemberAvatarMap(members), [members]);
|
||||||
const runtimeTelemetryScale = useMemo(
|
const runtimeTelemetryScale = useMemo(
|
||||||
() => buildRuntimeTelemetryScale(activeMembers, memberRuntimeEntries),
|
() => buildRuntimeTelemetryScale(activeMembers, memberRuntimeEntries),
|
||||||
[activeMembers, memberRuntimeEntries]
|
[activeMembers, memberRuntimeEntries]
|
||||||
|
|
@ -1012,9 +1023,11 @@ export const MemberList = memo(function MemberList({
|
||||||
return (
|
return (
|
||||||
<MemberCardRow
|
<MemberCardRow
|
||||||
key={member.name}
|
key={member.name}
|
||||||
|
teamName={teamName}
|
||||||
member={member}
|
member={member}
|
||||||
isRemoved={false}
|
isRemoved={false}
|
||||||
memberColor={colorMap.get(member.name) ?? 'blue'}
|
memberColor={colorMap.get(member.name) ?? 'blue'}
|
||||||
|
avatarUrl={avatarMap.get(member.name)}
|
||||||
fullBleedSurface={!isWide}
|
fullBleedSurface={!isWide}
|
||||||
currentTask={currentTask}
|
currentTask={currentTask}
|
||||||
reviewTask={reviewTask}
|
reviewTask={reviewTask}
|
||||||
|
|
@ -1064,9 +1077,11 @@ export const MemberList = memo(function MemberList({
|
||||||
{removedMembers.map((member) => (
|
{removedMembers.map((member) => (
|
||||||
<MemberCardRow
|
<MemberCardRow
|
||||||
key={member.name}
|
key={member.name}
|
||||||
|
teamName={teamName}
|
||||||
member={member}
|
member={member}
|
||||||
isRemoved={true}
|
isRemoved={true}
|
||||||
memberColor={colorMap.get(member.name) ?? 'blue'}
|
memberColor={colorMap.get(member.name) ?? 'blue'}
|
||||||
|
avatarUrl={avatarMap.get(member.name)}
|
||||||
fullBleedSurface={!isWide}
|
fullBleedSurface={!isWide}
|
||||||
currentTask={null}
|
currentTask={null}
|
||||||
reviewTask={null}
|
reviewTask={null}
|
||||||
|
|
|
||||||
|
|
@ -707,6 +707,7 @@ describe('MemberCard starting-state visuals', () => {
|
||||||
React.createElement(MemberCard, {
|
React.createElement(MemberCard, {
|
||||||
member,
|
member,
|
||||||
memberColor: 'blue',
|
memberColor: 'blue',
|
||||||
|
avatarUrl: 'https://example.com/alice.png',
|
||||||
isTeamAlive: true,
|
isTeamAlive: true,
|
||||||
isTeamProvisioning: false,
|
isTeamProvisioning: false,
|
||||||
})
|
})
|
||||||
|
|
@ -719,6 +720,7 @@ describe('MemberCard starting-state visuals', () => {
|
||||||
const clickableCard = host.querySelector('[role="button"]') as HTMLElement | null;
|
const clickableCard = host.querySelector('[role="button"]') as HTMLElement | null;
|
||||||
|
|
||||||
expect(avatarRing).not.toBeNull();
|
expect(avatarRing).not.toBeNull();
|
||||||
|
expect(img?.getAttribute('src')).toBe('https://example.com/alice.png');
|
||||||
expect(avatarRing?.style.borderColor).toBe('#3b82f6');
|
expect(avatarRing?.style.borderColor).toBe('#3b82f6');
|
||||||
expect(clickableCard?.style.borderLeft).toBe('');
|
expect(clickableCard?.style.borderLeft).toBe('');
|
||||||
expect(clickableCard?.style.background).toBe('');
|
expect(clickableCard?.style.background).toBe('');
|
||||||
|
|
@ -878,18 +880,13 @@ describe('MemberCard starting-state visuals', () => {
|
||||||
const runtimeTooltipContent = Array.from(
|
const runtimeTooltipContent = Array.from(
|
||||||
host.querySelectorAll('[data-testid="tooltip-content"]')
|
host.querySelectorAll('[data-testid="tooltip-content"]')
|
||||||
).find((content) => content.className.includes('border-blue-400/20'));
|
).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(
|
expect(host.querySelector('[data-testid="tooltip-root"]')?.getAttribute('data-open')).toBe(
|
||||||
'false'
|
'false'
|
||||||
);
|
);
|
||||||
expect(host.textContent).toContain('Local runtime load');
|
expect(runtimeTooltipContent).toBeUndefined();
|
||||||
expect(host.textContent).toContain('Parent and child processes only.');
|
expect(host.textContent).not.toContain('Local runtime load');
|
||||||
expect(host.textContent).toContain('root PID 222');
|
expect(host.textContent).not.toContain('Parent and child processes only.');
|
||||||
expect(host.textContent).toContain('3 processes');
|
expect(host.textContent).not.toContain('root PID 222');
|
||||||
expect(host.textContent).toContain('CPU');
|
|
||||||
expect(host.textContent).toContain('14%');
|
|
||||||
expect(host.textContent).toContain('Memory');
|
|
||||||
expect(host.textContent).toContain('238 MB');
|
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue