import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Badge } from '@renderer/components/ui/badge'; import { SyncedLoader2 } from '@renderer/components/ui/SyncedLoader2'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; 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, shouldDisplayMemberCurrentTask, } from '@renderer/utils/memberHelpers'; import { buildMemberLaunchDiagnosticsPayload, hasMemberLaunchDiagnosticsDetails, hasMemberLaunchDiagnosticsError, normalizeMemberLaunchFailureReason, } from '@renderer/utils/memberLaunchDiagnostics'; import { getRuntimeMemorySourceLabel } from '@renderer/utils/memberRuntimeSummary'; import { isLeadMember } from '@shared/utils/leadDetection'; import { deriveTaskDisplayId } from '@shared/utils/taskIdentity'; import { Activity, AlertTriangle, Ban, Cpu, GitBranch, HardDrive, Info, Layers3, MessageSquare, Plus, RotateCcw, Server, Undo2, } from 'lucide-react'; import { CurrentTaskIndicator } from './CurrentTaskIndicator'; import { MemberLaunchDiagnosticsButton } from './MemberLaunchDiagnosticsButton'; import { MemberPresenceDot } from './MemberPresenceDot'; import type { MemberActivityTimerAnchor } from '@renderer/utils/memberActivityTimer'; import type { TaskStatusCounts } from '@renderer/utils/pathNormalize'; import type { LeadActivityState, MemberLaunchState, MemberSpawnLivenessSource, MemberSpawnStatus, MemberSpawnStatusEntry, ResolvedTeamMember, TeamAgentRuntimeEntry, TeamAgentRuntimeResourceSample, TeamTaskWithKanban, } from '@shared/types'; export interface RuntimeTelemetryScale { memoryCapBytes?: number; cpuCapPercent?: number; } interface MemberCardProps { member: ResolvedTeamMember; memberColor: string; fullBleedSurface?: boolean; runtimeSummary?: string; runtimeEntry?: TeamAgentRuntimeEntry; runtimeRunId?: string | null; taskCounts?: TaskStatusCounts | null; isTeamAlive?: boolean; isTeamProvisioning?: boolean; leadActivity?: LeadActivityState; currentTask?: TeamTaskWithKanban | null; reviewTask?: TeamTaskWithKanban | null; currentTaskTimer?: MemberActivityTimerAnchor | null; reviewTaskTimer?: MemberActivityTimerAnchor | null; currentTaskTimerRunning?: boolean; reviewTaskTimerRunning?: boolean; isAwaitingReply?: boolean; isRemoved?: boolean; spawnStatus?: MemberSpawnStatus; spawnEntry?: MemberSpawnStatusEntry; spawnError?: string; spawnLivenessSource?: MemberSpawnLivenessSource; spawnLaunchState?: MemberLaunchState; spawnRuntimeAlive?: boolean; isLaunchSettling?: boolean; runtimeTelemetryScale?: RuntimeTelemetryScale; onOpenTask?: () => void; onOpenReviewTask?: () => void; onClick?: () => void; onSendMessage?: () => void; onAssignTask?: () => void; onRestartMember?: (memberName: string) => Promise | void; onSkipMemberForLaunch?: (memberName: string) => Promise | void; onRestoreMember?: (memberName: string) => Promise | void; } const MEMBER_ROW_SURFACE_BLEED_CLASS = '-mx-[calc(1rem-5px)] px-[calc(1rem-5px)]'; const RUNTIME_TELEMETRY_TOOLTIP_DELAY_MS = 1000; const RUNTIME_TELEMETRY_TOOLTIP_OPEN_EVENT = 'member-runtime-telemetry-tooltip-open'; let runtimeTelemetryTooltipIdSequence = 0; function createRuntimeTelemetryTooltipId(): string { runtimeTelemetryTooltipIdSequence += 1; return `runtime-telemetry-tooltip-${runtimeTelemetryTooltipIdSequence}`; } function notifyRuntimeTelemetryTooltipOpen(id: string): void { if (typeof window === 'undefined') { return; } window.dispatchEvent( new CustomEvent<{ id: string }>(RUNTIME_TELEMETRY_TOOLTIP_OPEN_EVENT, { detail: { id }, }) ); } function isRuntimeTelemetryTooltipBlockedTarget( currentTarget: EventTarget, target: EventTarget | null ): boolean { if (!(currentTarget instanceof Element) || !(target instanceof Element)) { return false; } const blockedTarget = target.closest('button,a,[title],[data-runtime-telemetry-exempt="true"]'); return Boolean(blockedTarget && blockedTarget !== currentTarget); } function splitRuntimeSummaryMemory(runtimeSummary: string | undefined): { summary: string | undefined; memory: string | undefined; } { const trimmed = runtimeSummary?.trim(); if (!trimmed) { return { summary: undefined, memory: undefined }; } const match = /^(.*?)(?:\s·\s(\d+(?:\.\d+)?\s(?:B|KB|MB|GB|TB)))$/.exec(trimmed); if (!match) { return { summary: trimmed, memory: undefined }; } return { summary: match[1]?.trim() || undefined, memory: match[2]?.trim() || undefined, }; } function getLaunchFailureLinkLabel(url: string): string { try { const parsed = new URL(url); if (parsed.hostname === 'openrouter.ai' && parsed.pathname === '/settings/credits') { return 'OpenRouter credits'; } } catch { return url; } return url; } const RUNTIME_TELEMETRY_SAMPLE_LIMIT = 48; const RUNTIME_TELEMETRY_WIDTH = 100; const RUNTIME_TELEMETRY_HEIGHT = 18; const RUNTIME_TELEMETRY_BASELINE_Y = 16.5; interface TelemetryPoint { x: number; y: number; } interface RuntimeTelemetryPaths { memoryAreaPath?: string; memoryLinePath?: string; cpuLinePath?: string; } function isFiniteNonNegative(value: number | undefined): value is number { return typeof value === 'number' && Number.isFinite(value) && value >= 0; } function formatTelemetryCoordinate(value: number): string { return Number.isInteger(value) ? String(value) : value.toFixed(2); } function buildLinePath(points: readonly TelemetryPoint[]): string | undefined { if (points.length < 2) { return undefined; } return points .map((point, index) => { const command = index === 0 ? 'M' : 'L'; return `${command}${formatTelemetryCoordinate(point.x)} ${formatTelemetryCoordinate(point.y)}`; }) .join(' '); } function buildAreaPath(points: readonly TelemetryPoint[]): string | undefined { if (points.length < 2) { return undefined; } const first = points[0]; const last = points[points.length - 1]; return [ `M${formatTelemetryCoordinate(first.x)} ${formatTelemetryCoordinate(RUNTIME_TELEMETRY_BASELINE_Y)}`, `L${formatTelemetryCoordinate(first.x)} ${formatTelemetryCoordinate(first.y)}`, ...points .slice(1) .map( (point) => `L${formatTelemetryCoordinate(point.x)} ${formatTelemetryCoordinate(point.y)}` ), `L${formatTelemetryCoordinate(last.x)} ${formatTelemetryCoordinate(RUNTIME_TELEMETRY_BASELINE_Y)}`, 'Z', ].join(' '); } function getRelativeTelemetryY( value: number, values: readonly number[], options: { bottomY: number; amplitude: number; fallbackRatio: number; minimumSpan?: number; } ): number { const min = Math.min(...values); const max = Math.max(...values); const span = max - min; if (span <= 0) { return options.bottomY - options.fallbackRatio * options.amplitude; } const effectiveSpan = Math.max(span, options.minimumSpan ?? 0); const ratio = Math.max(0, Math.min(1, (value - min) / effectiveSpan)); return options.bottomY - ratio * options.amplitude; } function getCappedTelemetryY( value: number, cap: number | undefined, options: { bottomY: number; amplitude: number; curve?: 'linear' | 'sqrt'; } ): number | undefined { if (!isFiniteNonNegative(cap) || cap <= 0) { return undefined; } const rawRatio = Math.max(0, Math.min(1, value / cap)); const ratio = options.curve === 'sqrt' ? Math.sqrt(rawRatio) : rawRatio; return options.bottomY - ratio * options.amplitude; } function formatRuntimeTelemetryPercent(value: number | undefined): string | undefined { if (!isFiniteNonNegative(value)) { return undefined; } return `${value >= 10 ? Math.round(value) : value.toFixed(1)}%`; } function formatRuntimeTelemetryBytes(value: number | undefined): string | undefined { if (!isFiniteNonNegative(value)) { return undefined; } const mib = value / (1024 * 1024); if (mib < 1024) { return `${mib.toFixed(mib >= 10 ? 0 : 1)} MB`; } return `${(mib / 1024).toFixed(1)} GB`; } function isRuntimeTelemetrySampleLike(value: unknown): value is TeamAgentRuntimeResourceSample { if (!value || typeof value !== 'object') { return false; } const sample = value as Partial; return ( typeof sample.timestamp === 'string' || isFiniteNonNegative(sample.cpuPercent) || isFiniteNonNegative(sample.rssBytes) ); } function normalizeRuntimeTelemetrySamples(history: unknown): TeamAgentRuntimeResourceSample[] { 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'); } const RuntimeTelemetryTooltipContent = ({ runtimeEntry, }: Readonly<{ runtimeEntry: TeamAgentRuntimeEntry | undefined; }>): React.JSX.Element | null => { if (!runtimeEntry) { return null; } const aggregateCpuLabel = formatRuntimeTelemetryPercent(runtimeEntry.cpuPercent); const primaryCpuLabel = formatRuntimeTelemetryPercent(runtimeEntry.primaryCpuPercent); const childCpuLabel = formatRuntimeTelemetryPercent(runtimeEntry.childCpuPercent); const rssLabel = formatRuntimeTelemetryBytes(runtimeEntry.rssBytes); 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)); const cpuSplit = [ primaryCpuLabel ? `root ${primaryCpuLabel}` : undefined, childCpuLabel ? `children ${childCpuLabel}` : undefined, ].filter((part): part is string => Boolean(part)); return (
Local runtime load
Parent and child processes only. Remote LLM inference is not included.
CPU
{aggregateCpuLabel ?? 'unknown'}
{cpuSplit.length > 0 ? (
{cpuSplit.join(' · ')}
) : null}
Memory
{rssLabel ?? 'unknown'}
summed RSS
{detailParts.length > 0 ? (
{detailParts.map((part) => ( {part} ))}
) : null} {runtimeEntry.runtimeLoadScope === 'shared-host' ? (
Shared OpenCode host metric. It is not exclusive to this member.
) : null} {runtimeEntry.runtimeLoadTruncated ? (
Process tree was capped for this sample.
) : null}
RSS can include shared pages, so it is best read as a load signal, not exclusive memory.
); }; function buildTelemetryPoints( samples: readonly TeamAgentRuntimeResourceSample[], getValue: (sample: TeamAgentRuntimeResourceSample) => number | undefined, getY: (value: number, values: readonly number[]) => number ): TelemetryPoint[] { const values = samples.map(getValue).filter(isFiniteNonNegative); if (values.length < 2 || samples.length < 2) { return []; } return samples.flatMap((sample, index) => { const value = getValue(sample); if (!isFiniteNonNegative(value)) { return []; } return [ { x: (index / (samples.length - 1)) * RUNTIME_TELEMETRY_WIDTH, y: getY(value, values), }, ]; }); } function buildRuntimeTelemetryPaths( history: readonly TeamAgentRuntimeResourceSample[] | undefined, scale?: RuntimeTelemetryScale ): RuntimeTelemetryPaths | undefined { const samples = normalizeRuntimeTelemetrySamples(history).slice(-RUNTIME_TELEMETRY_SAMPLE_LIMIT); if (samples.length < 2) { return undefined; } const memoryPoints = buildTelemetryPoints( samples, (sample) => sample.rssBytes, (value, values) => { const cappedY = getCappedTelemetryY(value, scale?.memoryCapBytes, { bottomY: 15.25, amplitude: 4.4, }); return ( cappedY ?? getRelativeTelemetryY(value, values, { bottomY: 15.25, amplitude: 4.4, fallbackRatio: 0.32, }) ); } ); const cpuPoints = buildTelemetryPoints( samples, (sample) => sample.cpuPercent, (value, values) => { const cappedY = getCappedTelemetryY(value, scale?.cpuCapPercent, { bottomY: 16.1, amplitude: 5.2, curve: 'sqrt', }); return ( cappedY ?? getRelativeTelemetryY(value, values, { bottomY: 16.1, amplitude: 5.2, fallbackRatio: 0, minimumSpan: 0.5, }) ); } ); const memoryAreaPath = buildAreaPath(memoryPoints); const memoryLinePath = buildLinePath(memoryPoints); const cpuLinePath = buildLinePath(cpuPoints); if (!memoryAreaPath && !cpuLinePath) { return undefined; } return { memoryAreaPath, memoryLinePath, cpuLinePath, }; } const MemberRuntimeTelemetryStrip = memo(function MemberRuntimeTelemetryStrip({ runtimeEntry, scale, }: { runtimeEntry?: TeamAgentRuntimeEntry; scale?: RuntimeTelemetryScale; }): React.JSX.Element | null { const paths = useMemo( () => buildRuntimeTelemetryPaths(runtimeEntry?.resourceHistory, scale), [runtimeEntry?.resourceHistory, scale] ); if (!paths) { return null; } return (