import { RefreshIcon } from "~/components/ui/refresh-icon";import { AlertTriangleIcon, ChevronDownIcon, ChevronRightIcon, CopyIcon, FolderOpenIcon, InfoIcon,} from "lucide-react";import { useAtomValue } from "@effect/atom-react";import { isAtomCommandInterrupted, squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime";import { useCallback, useMemo, useRef, useState, type ReactNode } from "react";import type { ServerProcessDiagnosticsEntry, ServerProcessResourceHistorySummary, ServerProcessSignal,} from "@t3tools/contracts";import * as DateTime from "effect/DateTime";import * as Option from "effect/Option"; import { cn } from "../../lib/utils";import { ensureLocalApi } from "../../localApi";import { resolveAndPersistPreferredEditor } from "../../editorPreferences";import { formatRelativeTimeLabel, getRelativeTimeState } from "../../timestampFormat";import { useEnvironmentQuery } from "../../state/query";import { primaryServerAvailableEditorsAtom, primaryServerObservabilityAtom, serverEnvironment,} from "../../state/server";import { shellEnvironment } from "../../state/shell";import { usePrimaryEnvironment } from "../../state/environments";import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";import { Button } from "../ui/button";import { ScrollArea } from "../ui/scroll-area";import { Toggle, ToggleGroup } from "../ui/toggle-group";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import { toastManager } from "../ui/toast";import { ExpandableText } from "./ExpandableText";import { ResourceTelemetryDiagnostics } from "./ResourceTelemetryDiagnostics";import { SettingsPageContainer, SettingsSection, useRelativeTimeTick } from "./settingsLayout";import { useAtomCommand } from "../../state/use-atom-command"; const NUMBER_FORMAT = new Intl.NumberFormat(); function formatCount(value: number): string { return NUMBER_FORMAT.format(value);} function formatDuration(value: number): string { if (value < 1_000) return `${Math.round(value)} ms`; return `${(value / 1_000).toFixed(value >= 10_000 ? 1 : 2)} s`;} function formatBytes(value: number): string { if (value < 1024) return `${value} B`; const units = ["KB", "MB", "GB"] as const; let unitIndex = -1; let next = value; do { next /= 1024; unitIndex += 1; } while (next >= 1024 && unitIndex < units.length - 1); return `${next.toFixed(next >= 10 ? 1 : 2)} ${units[unitIndex]}`;} function formatRelative(value: DateTime.Utc | null): string { if (!value) return "No trace records"; return formatRelativeTimeLabel(DateTime.formatIso(value));} function formatRelativeNoWrap(value: DateTime.Utc | null): string { return formatRelative(value).replaceAll(" ", "\u00a0");} function shortenTraceId(traceId: string): string { if (traceId.length <= 32) return traceId; return `${traceId.slice(0, 18)}...${traceId.slice(-10)}`;} function isStaleProcessSignalMessage(message: string | undefined): boolean { return message?.includes("not a live descendant") ?? false;} function StatBlock({ label, value, tooltip, tone = "default",}: { label: string; value: string; tooltip?: ReactNode; tone?: "default" | "warning" | "danger";}) { return ( <div className="min-w-0 border-border/60 px-4 py-3 sm:px-5"> <div className="flex min-w-0 items-center gap-1.5 text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground/70"> <span className="min-w-0 truncate">{label}</span> {tooltip ? ( <Tooltip> <TooltipTrigger render={ <button type="button" className="cursor-pointer inline-flex size-3.5 shrink-0 items-center justify-center rounded-sm text-muted-foreground/60 hover:text-foreground" aria-label={`${label} details`} > <InfoIcon className="size-3" /> </button> } /> <TooltipPopup side="top" className="max-w-[min(300px,calc(100vw-2rem))] whitespace-normal text-left text-[11px] leading-relaxed text-wrap" > {tooltip} </TooltipPopup> </Tooltip> ) : null} </div> <div className={cn( "mt-1 truncate font-mono text-lg font-semibold tabular-nums text-foreground", tone === "warning" && "text-amber-600 dark:text-amber-400", tone === "danger" && "text-destructive", )} > {value} </div> </div> );} function StatsGrid({ children }: { children: ReactNode }) { return ( <div className="relative grid grid-cols-2 sm:grid-cols-4"> <span className="pointer-events-none absolute inset-y-0 left-1/2 w-px bg-border/60" aria-hidden /> <span className="pointer-events-none absolute inset-x-0 top-1/2 h-px bg-border/60 sm:hidden" aria-hidden /> <span className="pointer-events-none absolute inset-y-0 left-1/4 hidden w-px bg-border/60 sm:block" aria-hidden /> <span className="pointer-events-none absolute inset-y-0 left-3/4 hidden w-px bg-border/60 sm:block" aria-hidden /> {children} </div> );} function EmptyRows({ label }: { label: string }) { return <div className="px-4 py-4 text-xs text-muted-foreground sm:px-5">{label}</div>;} function DiagnosticsTable({ headers, children, minTableWidth = "min-w-[640px]", columnWidths,}: { headers: ReadonlyArray<string>; children: ReactNode; minTableWidth?: string; columnWidths?: ReadonlyArray<string>;}) { return ( <ScrollArea chainVerticalScroll scrollFade hideScrollbars className="w-full max-w-full rounded-none" > <table className={cn("w-full text-left text-xs", minTableWidth, columnWidths && "table-fixed")} > {columnWidths ? ( <colgroup> {headers.map((header, index) => ( <col key={header} className={columnWidths[index]} /> ))} </colgroup> ) : null} <thead className="border-b border-border/60 text-[11px] uppercase tracking-[0.08em] text-muted-foreground/70"> <tr> {headers.map((header, index) => ( <th key={header} className={cn( "whitespace-nowrap px-4 py-2.5 font-semibold first:sm:pl-5 last:sm:pr-5", !columnWidths && index === headers.length - 1 && "w-px", )} > {header.replaceAll(" ", "\u00a0")} </th> ))} </tr> </thead> <tbody className="divide-y divide-border/60">{children}</tbody> </table> </ScrollArea> );} function TraceIdCell({ traceId }: { traceId: string }) { const { copyToClipboard, isCopied: copied } = useCopyToClipboard({ target: "trace ID", timeout: 1_200, }); return ( <div className="flex w-full min-w-0 max-w-full items-center gap-2"> <Tooltip> <TooltipTrigger render={ <span className="min-w-0 flex-1 truncate font-mono text-[11px]"> {shortenTraceId(traceId)} </span> } /> <TooltipPopup side="top" className="max-w-[min(520px,calc(100vw-2rem))] break-all font-mono text-[11px]" > {traceId} </TooltipPopup> </Tooltip> <Tooltip> <TooltipTrigger render={ <Button size="icon-micro" variant="ghost-muted" aria-label={copied ? "Copied trace ID" : "Copy trace ID"} onClick={() => copyToClipboard(traceId)} > <CopyIcon className="size-3" /> </Button> } /> <TooltipPopup side="top">{copied ? "Copied" : "Copy full trace ID"}</TooltipPopup> </Tooltip> </div> );} function formatProcessName(command: string): string { const firstToken = command.trim().split(/\s+/)[0]; if (!firstToken) return command; const normalized = firstToken.replace(/^['"]|['"]$/g, ""); const segments = normalized.split(/[\\/]/).filter(Boolean); return segments.at(-1) ?? normalized;} function formatProcessType(process: ServerProcessDiagnosticsEntry): string { if (process.depth > 0) return "Subprocess"; if (/\b(codex|claude|opencode|cursor)\b/i.test(process.command)) return "Agent"; return "Process";} function ProcessNameCell({ process, isExpanded, onToggle,}: { process: ServerProcessDiagnosticsEntry; isExpanded: boolean; onToggle: (pid: number) => void;}) { const name = formatProcessName(process.command); const hasChildren = process.childPids.length > 0; const ChevronIcon = isExpanded ? ChevronDownIcon : ChevronRightIcon; return ( <div className="grid min-w-0 grid-cols-[1.25rem_0.375rem_minmax(0,1fr)] items-center gap-2" style={{ paddingLeft: `${Math.min(process.depth, 6) * 10}px` }} > {hasChildren ? ( <Button size="icon-micro" variant="ghost-muted" aria-label={isExpanded ? `Collapse ${name}` : `Expand ${name}`} onClick={() => onToggle(process.pid)} > <ChevronIcon className="size-3.5" /> </Button> ) : ( <span className="size-5 shrink-0" aria-hidden="true" /> )} <span className="size-1.5 shrink-0 rounded-full bg-emerald-500/80" /> <Tooltip> <TooltipTrigger render={<span className="min-w-0 truncate font-medium text-foreground">{name}</span>} /> <TooltipPopup side="top" className="max-w-[min(440px,calc(100vw-2rem))] whitespace-normal break-words text-left font-mono text-[11px] leading-relaxed text-wrap" > {process.command} </TooltipPopup> </Tooltip> </div> );} function ProcessSignalActions({ process, isSignaling, onSignal,}: { process: ServerProcessDiagnosticsEntry; isSignaling: boolean; onSignal: (pid: number, signal: ServerProcessSignal) => void;}) { return ( <div className="flex items-center justify-end gap-1.5"> <Tooltip> <TooltipTrigger render={ <button type="button" disabled={isSignaling} className="cursor-pointer text-[11px] font-medium text-muted-foreground underline-offset-2 hover:text-foreground hover:underline disabled:pointer-events-none disabled:opacity-50" onClick={() => onSignal(process.pid, "SIGINT")} > INT </button> } /> <TooltipPopup side="top">Send SIGINT</TooltipPopup> </Tooltip> <Tooltip> <TooltipTrigger render={ <button type="button" disabled={isSignaling} className="cursor-pointer text-[11px] font-medium text-destructive underline-offset-2 hover:underline disabled:pointer-events-none disabled:opacity-50" onClick={() => onSignal(process.pid, "SIGKILL")} > KILL </button> } /> <TooltipPopup side="top">Send SIGKILL</TooltipPopup> </Tooltip> </div> );} function ProcessDiagnosticsTable({ processes, signalingPid, onSignal, emptyLabel,}: { processes: ReadonlyArray<ServerProcessDiagnosticsEntry>; signalingPid: number | null; onSignal: (pid: number, signal: ServerProcessSignal) => void; emptyLabel?: string;}) { const [collapsedPids, setCollapsedPids] = useState<ReadonlySet<number>>(() => new Set()); const visibleProcesses = useMemo(() => { const visible: ServerProcessDiagnosticsEntry[] = []; let hiddenChildDepth: number | null = null; for (const process of processes) { if (hiddenChildDepth !== null) { if (process.depth > hiddenChildDepth) continue; hiddenChildDepth = null; } visible.push(process); if (collapsedPids.has(process.pid)) { hiddenChildDepth = process.depth; } } return visible; }, [collapsedPids, processes]); const toggleProcess = useCallback((pid: number) => { setCollapsedPids((previous) => { const next = new Set(previous); if (next.has(pid)) { next.delete(pid); } else { next.add(pid); } return next; }); }, []); return ( <ScrollArea chainVerticalScroll scrollFade hideScrollbars className="max-h-[min(64vh,44rem)] w-full max-w-full rounded-none border-t border-border/60" > <table className="w-full min-w-[1040px] table-fixed text-left text-xs"> <colgroup> <col className="w-[24%]" /> <col className="w-[8%]" /> <col className="w-[10%]" /> <col className="w-[33%]" /> <col className="w-[8%]" /> <col className="w-[11%]" /> <col className="w-[6%]" /> </colgroup> <thead className="sticky top-0 z-10 border-b border-border/60 bg-card text-[11px] uppercase tracking-[0.08em] text-muted-foreground/70"> <tr> <th className="px-4 py-2 font-semibold sm:pl-5">Name</th> <th className="px-3 py-2 text-right font-semibold">CPU</th> <th className="px-3 py-2 text-right font-semibold">Memory</th> <th className="px-3 py-2 font-semibold">Command</th> <th className="px-3 py-2 text-right font-semibold">PID</th> <th className="px-3 py-2 font-semibold">Type</th> <th className="p-2 text-right font-semibold sm:pr-4">Kill</th> </tr> </thead> <tbody className="divide-y divide-border/50"> {visibleProcesses.length === 0 ? ( <tr> <td colSpan={7} className="px-4 py-4 text-xs text-muted-foreground sm:px-5"> {emptyLabel ?? "No live descendant processes found."} </td> </tr> ) : null} {visibleProcesses.map((process) => ( <tr key={process.pid} className="hover:bg-muted/20"> <td className="px-4 py-2 align-middle sm:pl-5"> <ProcessNameCell process={process} isExpanded={!collapsedPids.has(process.pid)} onToggle={toggleProcess} /> </td> <td className="px-3 py-2 text-right align-middle font-mono tabular-nums"> {process.cpuPercent.toFixed(1)}% </td> <td className="px-3 py-2 text-right align-middle font-mono tabular-nums"> {formatBytes(process.rssBytes)} </td> <td className="px-3 py-2 align-middle text-muted-foreground"> <Tooltip> <TooltipTrigger render={<span className="block truncate">{process.command}</span>} /> <TooltipPopup side="top" className="max-w-[min(440px,calc(100vw-2rem))] whitespace-normal break-words text-left font-mono text-[11px] leading-relaxed text-wrap" > {process.command} </TooltipPopup> </Tooltip> </td> <td className="px-3 py-2 text-right align-middle font-mono tabular-nums text-muted-foreground"> {process.pid} </td> <td className="truncate px-3 py-2 align-middle text-muted-foreground"> {formatProcessType(process)} </td> <td className="p-2 align-middle sm:pr-4"> <ProcessSignalActions process={process} isSignaling={signalingPid === process.pid} onSignal={onSignal} /> </td> </tr> ))} </tbody> </table> </ScrollArea> );} const RESOURCE_HISTORY_WINDOWS = [ { label: "5m", windowMs: 5 * 60_000, bucketMs: 30_000 }, { label: "15m", windowMs: 15 * 60_000, bucketMs: 60_000 }, { label: "30m", windowMs: 30 * 60_000, bucketMs: 2 * 60_000 }, { label: "1h", windowMs: 60 * 60_000, bucketMs: 5 * 60_000 },] as const; function formatCpuTime(seconds: number): string { if (seconds < 60) return `${seconds.toFixed(seconds >= 10 ? 1 : 2)}s`; const minutes = seconds / 60; if (minutes < 60) return `${minutes.toFixed(minutes >= 10 ? 1 : 2)}m`;Lines 1–500 of 1358. Load more
import { RefreshIcon } from "~/components/ui/refresh-icon"; import { AlertTriangleIcon, ChevronDownIcon, ChevronRightIcon, CopyIcon, FolderOpenIcon, InfoIcon, } from "lucide-react"; import { useAtomValue } from "@effect/atom-react"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { useCallback, useMemo, useRef, useState, type ReactNode } from "react"; import type { ServerProcessDiagnosticsEntry, ServerProcessResourceHistorySummary, ServerProcessSignal, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Option from "effect/Option"; import { cn } from "../../lib/utils"; import { ensureLocalApi } from "../../localApi"; import { resolveAndPersistPreferredEditor } from "../../editorPreferences"; import { formatRelativeTimeLabel, getRelativeTimeState } from "../../timestampFormat"; import { useEnvironmentQuery } from "../../state/query"; import { primaryServerAvailableEditorsAtom, primaryServerObservabilityAtom, serverEnvironment, } from "../../state/server"; import { shellEnvironment } from "../../state/shell"; import { usePrimaryEnvironment } from "../../state/environments"; import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; import { Button } from "../ui/button"; import { ScrollArea } from "../ui/scroll-area"; import { Toggle, ToggleGroup } from "../ui/toggle-group"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { toastManager } from "../ui/toast"; import { ExpandableText } from "./ExpandableText"; import { ResourceTelemetryDiagnostics } from "./ResourceTelemetryDiagnostics"; import { SettingsPageContainer, SettingsSection, useRelativeTimeTick } from "./settingsLayout"; import { useAtomCommand } from "../../state/use-atom-command"; const NUMBER_FORMAT = new Intl.NumberFormat(); function formatCount(value: number): string { return NUMBER_FORMAT.format(value); } function formatDuration(value: number): string { if (value < 1_000) return `${Math.round(value)} ms`; return `${(value / 1_000).toFixed(value >= 10_000 ? 1 : 2)} s`; } function formatBytes(value: number): string { if (value < 1024) return `${value} B`; const units = ["KB", "MB", "GB"] as const; let unitIndex = -1; let next = value; do { next /= 1024; unitIndex += 1; } while (next >= 1024 && unitIndex < units.length - 1); return `${next.toFixed(next >= 10 ? 1 : 2)} ${units[unitIndex]}`; } function formatRelative(value: DateTime.Utc | null): string { if (!value) return "No trace records"; return formatRelativeTimeLabel(DateTime.formatIso(value)); } function formatRelativeNoWrap(value: DateTime.Utc | null): string { return formatRelative(value).replaceAll(" ", "\u00a0"); } function shortenTraceId(traceId: string): string { if (traceId.length <= 32) return traceId; return `${traceId.slice(0, 18)}...${traceId.slice(-10)}`; } function isStaleProcessSignalMessage(message: string | undefined): boolean { return message?.includes("not a live descendant") ?? false; } function StatBlock({ label, value, tooltip, tone = "default", }: { label: string; value: string; tooltip?: ReactNode; tone?: "default" | "warning" | "danger"; }) { return ( <div className="min-w-0 border-border/60 px-4 py-3 sm:px-5"> <div className="flex min-w-0 items-center gap-1.5 text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground/70"> <span className="min-w-0 truncate">{label}</span> {tooltip ? ( <Tooltip> <TooltipTrigger render={ <button type="button" className="cursor-pointer inline-flex size-3.5 shrink-0 items-center justify-center rounded-sm text-muted-foreground/60 hover:text-foreground" aria-label={`${label} details`} > <InfoIcon className="size-3" /> </button> } /> <TooltipPopup side="top" className="max-w-[min(300px,calc(100vw-2rem))] whitespace-normal text-left text-[11px] leading-relaxed text-wrap" > {tooltip} </TooltipPopup> </Tooltip> ) : null} </div> <div className={cn( "mt-1 truncate font-mono text-lg font-semibold tabular-nums text-foreground", tone === "warning" && "text-amber-600 dark:text-amber-400", tone === "danger" && "text-destructive", )} > {value} </div> </div> ); } function StatsGrid({ children }: { children: ReactNode }) { return ( <div className="relative grid grid-cols-2 sm:grid-cols-4"> <span className="pointer-events-none absolute inset-y-0 left-1/2 w-px bg-border/60" aria-hidden /> <span className="pointer-events-none absolute inset-x-0 top-1/2 h-px bg-border/60 sm:hidden" aria-hidden /> <span className="pointer-events-none absolute inset-y-0 left-1/4 hidden w-px bg-border/60 sm:block" aria-hidden /> <span className="pointer-events-none absolute inset-y-0 left-3/4 hidden w-px bg-border/60 sm:block" aria-hidden /> {children} </div> ); } function EmptyRows({ label }: { label: string }) { return <div className="px-4 py-4 text-xs text-muted-foreground sm:px-5">{label}</div>; } function DiagnosticsTable({ headers, children, minTableWidth = "min-w-[640px]", columnWidths, }: { headers: ReadonlyArray<string>; children: ReactNode; minTableWidth?: string; columnWidths?: ReadonlyArray<string>; }) { return ( <ScrollArea chainVerticalScroll scrollFade hideScrollbars className="w-full max-w-full rounded-none" > <table className={cn("w-full text-left text-xs", minTableWidth, columnWidths && "table-fixed")} > {columnWidths ? ( <colgroup> {headers.map((header, index) => ( <col key={header} className={columnWidths[index]} /> ))} </colgroup> ) : null} <thead className="border-b border-border/60 text-[11px] uppercase tracking-[0.08em] text-muted-foreground/70"> <tr> {headers.map((header, index) => ( <th key={header} className={cn( "whitespace-nowrap px-4 py-2.5 font-semibold first:sm:pl-5 last:sm:pr-5", !columnWidths && index === headers.length - 1 && "w-px", )} > {header.replaceAll(" ", "\u00a0")} </th> ))} </tr> </thead> <tbody className="divide-y divide-border/60">{children}</tbody> </table> </ScrollArea> ); } function TraceIdCell({ traceId }: { traceId: string }) { const { copyToClipboard, isCopied: copied } = useCopyToClipboard({ target: "trace ID", timeout: 1_200, }); return ( <div className="flex w-full min-w-0 max-w-full items-center gap-2"> <Tooltip> <TooltipTrigger render={ <span className="min-w-0 flex-1 truncate font-mono text-[11px]"> {shortenTraceId(traceId)} </span> } /> <TooltipPopup side="top" className="max-w-[min(520px,calc(100vw-2rem))] break-all font-mono text-[11px]" > {traceId} </TooltipPopup> </Tooltip> <Tooltip> <TooltipTrigger render={ <Button size="icon-micro" variant="ghost-muted" aria-label={copied ? "Copied trace ID" : "Copy trace ID"} onClick={() => copyToClipboard(traceId)} > <CopyIcon className="size-3" /> </Button> } /> <TooltipPopup side="top">{copied ? "Copied" : "Copy full trace ID"}</TooltipPopup> </Tooltip> </div> ); } function formatProcessName(command: string): string { const firstToken = command.trim().split(/\s+/)[0]; if (!firstToken) return command; const normalized = firstToken.replace(/^['"]|['"]$/g, ""); const segments = normalized.split(/[\\/]/).filter(Boolean); return segments.at(-1) ?? normalized; } function formatProcessType(process: ServerProcessDiagnosticsEntry): string { if (process.depth > 0) return "Subprocess"; if (/\b(codex|claude|opencode|cursor)\b/i.test(process.command)) return "Agent"; return "Process"; } function ProcessNameCell({ process, isExpanded, onToggle, }: { process: ServerProcessDiagnosticsEntry; isExpanded: boolean; onToggle: (pid: number) => void; }) { const name = formatProcessName(process.command); const hasChildren = process.childPids.length > 0; const ChevronIcon = isExpanded ? ChevronDownIcon : ChevronRightIcon; return ( <div className="grid min-w-0 grid-cols-[1.25rem_0.375rem_minmax(0,1fr)] items-center gap-2" style={{ paddingLeft: `${Math.min(process.depth, 6) * 10}px` }} > {hasChildren ? ( <Button size="icon-micro" variant="ghost-muted" aria-label={isExpanded ? `Collapse ${name}` : `Expand ${name}`} onClick={() => onToggle(process.pid)} > <ChevronIcon className="size-3.5" /> </Button> ) : ( <span className="size-5 shrink-0" aria-hidden="true" /> )} <span className="size-1.5 shrink-0 rounded-full bg-emerald-500/80" /> <Tooltip> <TooltipTrigger render={<span className="min-w-0 truncate font-medium text-foreground">{name}</span>} /> <TooltipPopup side="top" className="max-w-[min(440px,calc(100vw-2rem))] whitespace-normal break-words text-left font-mono text-[11px] leading-relaxed text-wrap" > {process.command} </TooltipPopup> </Tooltip> </div> ); } function ProcessSignalActions({ process, isSignaling, onSignal, }: { process: ServerProcessDiagnosticsEntry; isSignaling: boolean; onSignal: (pid: number, signal: ServerProcessSignal) => void; }) { return ( <div className="flex items-center justify-end gap-1.5"> <Tooltip> <TooltipTrigger render={ <button type="button" disabled={isSignaling} className="cursor-pointer text-[11px] font-medium text-muted-foreground underline-offset-2 hover:text-foreground hover:underline disabled:pointer-events-none disabled:opacity-50" onClick={() => onSignal(process.pid, "SIGINT")} > INT </button> } /> <TooltipPopup side="top">Send SIGINT</TooltipPopup> </Tooltip> <Tooltip> <TooltipTrigger render={ <button type="button" disabled={isSignaling} className="cursor-pointer text-[11px] font-medium text-destructive underline-offset-2 hover:underline disabled:pointer-events-none disabled:opacity-50" onClick={() => onSignal(process.pid, "SIGKILL")} > KILL </button> } /> <TooltipPopup side="top">Send SIGKILL</TooltipPopup> </Tooltip> </div> ); } function ProcessDiagnosticsTable({ processes, signalingPid, onSignal, emptyLabel, }: { processes: ReadonlyArray<ServerProcessDiagnosticsEntry>; signalingPid: number | null; onSignal: (pid: number, signal: ServerProcessSignal) => void; emptyLabel?: string; }) { const [collapsedPids, setCollapsedPids] = useState<ReadonlySet<number>>(() => new Set()); const visibleProcesses = useMemo(() => { const visible: ServerProcessDiagnosticsEntry[] = []; let hiddenChildDepth: number | null = null; for (const process of processes) { if (hiddenChildDepth !== null) { if (process.depth > hiddenChildDepth) continue; hiddenChildDepth = null; } visible.push(process); if (collapsedPids.has(process.pid)) { hiddenChildDepth = process.depth; } } return visible; }, [collapsedPids, processes]); const toggleProcess = useCallback((pid: number) => { setCollapsedPids((previous) => { const next = new Set(previous); if (next.has(pid)) { next.delete(pid); } else { next.add(pid); } return next; }); }, []); return ( <ScrollArea chainVerticalScroll scrollFade hideScrollbars className="max-h-[min(64vh,44rem)] w-full max-w-full rounded-none border-t border-border/60" > <table className="w-full min-w-[1040px] table-fixed text-left text-xs"> <colgroup> <col className="w-[24%]" /> <col className="w-[8%]" /> <col className="w-[10%]" /> <col className="w-[33%]" /> <col className="w-[8%]" /> <col className="w-[11%]" /> <col className="w-[6%]" /> </colgroup> <thead className="sticky top-0 z-10 border-b border-border/60 bg-card text-[11px] uppercase tracking-[0.08em] text-muted-foreground/70"> <tr> <th className="px-4 py-2 font-semibold sm:pl-5">Name</th> <th className="px-3 py-2 text-right font-semibold">CPU</th> <th className="px-3 py-2 text-right font-semibold">Memory</th> <th className="px-3 py-2 font-semibold">Command</th> <th className="px-3 py-2 text-right font-semibold">PID</th> <th className="px-3 py-2 font-semibold">Type</th> <th className="p-2 text-right font-semibold sm:pr-4">Kill</th> </tr> </thead> <tbody className="divide-y divide-border/50"> {visibleProcesses.length === 0 ? ( <tr> <td colSpan={7} className="px-4 py-4 text-xs text-muted-foreground sm:px-5"> {emptyLabel ?? "No live descendant processes found."} </td> </tr> ) : null} {visibleProcesses.map((process) => ( <tr key={process.pid} className="hover:bg-muted/20"> <td className="px-4 py-2 align-middle sm:pl-5"> <ProcessNameCell process={process} isExpanded={!collapsedPids.has(process.pid)} onToggle={toggleProcess} /> </td> <td className="px-3 py-2 text-right align-middle font-mono tabular-nums"> {process.cpuPercent.toFixed(1)}% </td> <td className="px-3 py-2 text-right align-middle font-mono tabular-nums"> {formatBytes(process.rssBytes)} </td> <td className="px-3 py-2 align-middle text-muted-foreground"> <Tooltip> <TooltipTrigger render={<span className="block truncate">{process.command}</span>} /> <TooltipPopup side="top" className="max-w-[min(440px,calc(100vw-2rem))] whitespace-normal break-words text-left font-mono text-[11px] leading-relaxed text-wrap" > {process.command} </TooltipPopup> </Tooltip> </td> <td className="px-3 py-2 text-right align-middle font-mono tabular-nums text-muted-foreground"> {process.pid} </td> <td className="truncate px-3 py-2 align-middle text-muted-foreground"> {formatProcessType(process)} </td> <td className="p-2 align-middle sm:pr-4"> <ProcessSignalActions process={process} isSignaling={signalingPid === process.pid} onSignal={onSignal} /> </td> </tr> ))} </tbody> </table> </ScrollArea> ); } const RESOURCE_HISTORY_WINDOWS = [ { label: "5m", windowMs: 5 * 60_000, bucketMs: 30_000 }, { label: "15m", windowMs: 15 * 60_000, bucketMs: 60_000 }, { label: "30m", windowMs: 30 * 60_000, bucketMs: 2 * 60_000 }, { label: "1h", windowMs: 60 * 60_000, bucketMs: 5 * 60_000 }, ] as const; function formatCpuTime(seconds: number): string { if (seconds < 60) return `${seconds.toFixed(seconds >= 10 ? 1 : 2)}s`; const minutes = seconds / 60; if (minutes < 60) return `${minutes.toFixed(minutes >= 10 ? 1 : 2)}m`;