agent-ecosystem/src/renderer/components/team/LiveRuntimeStatusSection.tsx

98 lines
3.7 KiB
TypeScript

import { memo } from 'react';
import type { RuntimeDisplayState, TeamRuntimeDisplayRow } from './teamRuntimeDisplayRows';
interface LiveRuntimeStatusSectionProps {
rows: readonly TeamRuntimeDisplayRow[];
}
const STATE_LABELS: Record<RuntimeDisplayState, string> = {
running: 'Running',
starting: 'Starting',
waiting: 'Waiting',
degraded: 'Needs attention',
stopped: 'Stopped',
unknown: 'Unknown',
};
const STATE_CLASS_NAMES: Record<RuntimeDisplayState, string> = {
running: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300',
starting: 'border-sky-500/25 bg-sky-500/10 text-sky-700 dark:text-sky-300',
waiting: 'border-amber-500/25 bg-amber-500/10 text-amber-700 dark:text-amber-300',
degraded: 'border-rose-500/25 bg-rose-500/10 text-rose-700 dark:text-rose-300',
stopped: 'border-zinc-500/25 bg-zinc-500/10 text-zinc-700 dark:text-zinc-300',
unknown: 'border-zinc-500/20 bg-zinc-500/5 text-zinc-600 dark:text-zinc-400',
};
export const LiveRuntimeStatusSection = memo(function LiveRuntimeStatusSection({
rows,
}: LiveRuntimeStatusSectionProps): React.JSX.Element | null {
if (rows.length === 0) return null;
return (
<div className="space-y-3" aria-label="Live runtime status">
<span className="sr-only">Live runtime status</span>
<p className="text-muted-foreground text-xs">
Display-only heartbeat and launch state. Process controls remain below.
</p>
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
{rows.map((row) => (
<article
key={row.memberName}
className="border-border/70 bg-card/50 rounded-lg border p-3 shadow-sm"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-foreground truncate text-sm font-medium">{row.memberName}</div>
<div className="text-muted-foreground mt-1 line-clamp-2 text-xs">
{row.stateReason}
</div>
</div>
<span
className={`shrink-0 rounded-full border px-2 py-0.5 text-[11px] font-medium ${STATE_CLASS_NAMES[row.state]}`}
>
{STATE_LABELS[row.state]}
</span>
</div>
<div className="text-muted-foreground mt-3 flex flex-wrap gap-1.5 text-[11px]">
<span className="bg-muted rounded-full px-2 py-0.5">source: {row.source}</span>
{row.runtimeModel ? (
<span className="bg-muted rounded-full px-2 py-0.5">{row.runtimeModel}</span>
) : null}
{row.laneKind ? (
<span className="bg-muted rounded-full px-2 py-0.5">{row.laneKind} lane</span>
) : null}
{row.pidLabel ? (
<span className="bg-muted rounded-full px-2 py-0.5" title="Diagnostic only">
{row.pidLabel}
</span>
) : null}
{row.updatedAt ? (
<span className="bg-muted rounded-full px-2 py-0.5">
updated {formatRuntimeUpdatedAt(row.updatedAt)}
</span>
) : null}
</div>
</article>
))}
</div>
</div>
);
});
function formatRuntimeUpdatedAt(value: string): string {
const timestamp = Date.parse(value);
if (!Number.isFinite(timestamp)) return value;
const secondsAgo = Math.max(0, Math.round((Date.now() - timestamp) / 1000));
if (secondsAgo < 60) return `${secondsAgo}s ago`;
const minutesAgo = Math.round(secondsAgo / 60);
if (minutesAgo < 60) return `${minutesAgo}m ago`;
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
}