import { AlertTriangleIcon, ChevronDownIcon, ChevronRightIcon, CopyIcon, FolderOpenIcon, InfoIcon, RefreshCwIcon, } from "lucide-react"; import { useAtomValue } from "@effect/atom-react"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { useCallback, useMemo, 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 { resolveAndPersistPreferredEditor } from "../../editorPreferences"; import { formatRelativeTime } 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 { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { toastManager } from "../ui/toast"; 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"; const relative = formatRelativeTime(DateTime.formatIso(value)); return relative.suffix ? `${relative.value} ${relative.suffix}` : relative.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 (
{label} {tooltip ? ( } /> {tooltip} ) : null}
{value}
); } function StatsGrid({ children }: { children: ReactNode }) { return (
{children}
); } function EmptyRows({ label }: { label: string }) { return
{label}
; } function ExpandableText({ text, className, collapsedClassName = "line-clamp-3", expandLabel = "Show full error", }: { text: string; className?: string; collapsedClassName?: string; expandLabel?: string; }) { const [expanded, setExpanded] = useState(false); const canExpand = text.length > 180 || text.includes("\n"); return (
{text}
{canExpand ? ( ) : null}
); } function DiagnosticsTable({ headers, children, minTableWidth = "min-w-[640px]", columnWidths, }: { headers: ReadonlyArray; children: ReactNode; minTableWidth?: string; columnWidths?: ReadonlyArray; }) { return ( {columnWidths ? ( {headers.map((header, index) => ( ))} ) : null} {headers.map((header, index) => ( ))} {children}
{header.replaceAll(" ", "\u00a0")}
); } function TraceIdCell({ traceId }: { traceId: string }) { const { copyToClipboard, isCopied: copied } = useCopyToClipboard({ target: "trace ID", timeout: 1_200, }); return (
{shortenTraceId(traceId)} } /> {traceId} copyToClipboard(traceId)} > } /> {copied ? "Copied" : "Copy full trace ID"}
); } 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 (
{hasChildren ? ( ) : (
); } function ProcessSignalActions({ process, isSignaling, onSignal, }: { process: ServerProcessDiagnosticsEntry; isSignaling: boolean; onSignal: (pid: number, signal: ServerProcessSignal) => void; }) { return (
onSignal(process.pid, "SIGINT")} > INT } /> Send SIGINT onSignal(process.pid, "SIGKILL")} > KILL } /> Send SIGKILL
); } function ProcessDiagnosticsTable({ processes, signalingPid, onSignal, emptyLabel, }: { processes: ReadonlyArray; signalingPid: number | null; onSignal: (pid: number, signal: ServerProcessSignal) => void; emptyLabel?: string; }) { const [collapsedPids, setCollapsedPids] = useState>(() => 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 ( {visibleProcesses.length === 0 ? ( ) : null} {visibleProcesses.map((process) => ( ))}
Name CPU Memory Command PID Type Kill
{emptyLabel ?? "No live descendant processes found."}
{process.cpuPercent.toFixed(1)}% {formatBytes(process.rssBytes)} {process.command}} /> {process.command} {process.pid} {formatProcessType(process)}
); } 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`; return `${(minutes / 60).toFixed(2)}h`; } function formatShortProcessName(command: string): string { const name = formatProcessName(command); return name.length > 42 ? `${name.slice(0, 39)}...` : name; } function ResourceHistoryProcessNameCell({ process, visualDepth, }: { process: ServerProcessResourceHistorySummary; visualDepth: number; }) { const name = formatShortProcessName(process.command); return (
); } function ProcessResourceHistoryChart({ buckets, }: { buckets: ReadonlyArray<{ readonly startedAt: DateTime.Utc; readonly avgCpuPercent: number; readonly maxCpuPercent: number; }>; }) { const maxCpuPercent = Math.max(1, ...buckets.map((bucket) => bucket.maxCpuPercent)); return (
{buckets.map((bucket) => { const peakHeight = Math.max(2, (bucket.maxCpuPercent / maxCpuPercent) * 100); const averageHeight = Math.max(2, (bucket.avgCpuPercent / maxCpuPercent) * 100); return (
} /> Avg {bucket.avgCpuPercent.toFixed(1)}%, peak {bucket.maxCpuPercent.toFixed(1)}% ); })}
); } function ResourceHistoryWindowSelector({ selectedWindowMs, onSelect, }: { selectedWindowMs: number; onSelect: (windowMs: number) => void; }) { return (
{RESOURCE_HISTORY_WINDOWS.map((option) => ( ))}
); } function ProcessResourceHistoryTable({ processes, emptyLabel, }: { processes: ReadonlyArray; emptyLabel: string; }) { const shallowestChildDepth = processes.reduce((minDepth, process) => { if (process.isServerRoot) return minDepth; return minDepth === null ? process.depth : Math.min(minDepth, process.depth); }, null); return ( {processes.length === 0 ? ( ) : null} {processes.map((process) => ( ))}
Process CPU Time Current Average Peak Max Mem Command PID
{emptyLabel}
{formatCpuTime(process.cpuSecondsApprox)} {process.currentCpuPercent.toFixed(1)}% {process.avgCpuPercent.toFixed(1)}% {process.maxCpuPercent.toFixed(1)}% {formatBytes(process.maxRssBytes)} {process.command}} /> {process.command} {process.pid}
); } function DiagnosticsLastChecked({ checkedAt }: { checkedAt: DateTime.Utc | null }) { useRelativeTimeTick(); const relative = checkedAt ? formatRelativeTime(DateTime.formatIso(checkedAt)) : null; if (!relative) { return Checking; } return ( {relative.suffix ? ( <> Checked {relative.value} {relative.suffix} ) : ( <>Checked {relative.value} )} ); } function DiagnosticsRefreshButton({ isPending, label, onClick, }: { isPending: boolean; label: string; onClick: () => void; }) { return ( } /> {label} ); } export function DiagnosticsSettingsPanel() { const observability = useAtomValue(primaryServerObservabilityAtom); const availableEditors = useAtomValue(primaryServerAvailableEditorsAtom); const primaryEnvironment = usePrimaryEnvironment(); const environmentId = primaryEnvironment?.environmentId ?? null; const signalServerProcess = useAtomCommand(serverEnvironment.signalProcess, { reportFailure: false, }); const openInEditor = useAtomCommand(shellEnvironment.openInEditor, { reportFailure: false, }); const [resourceWindowMs, setResourceWindowMs] = useState(15 * 60_000); const selectedResourceWindow = RESOURCE_HISTORY_WINDOWS.find((option) => option.windowMs === resourceWindowMs) ?? RESOURCE_HISTORY_WINDOWS[1]; const { data, error, isPending, refresh } = useEnvironmentQuery( environmentId === null ? null : serverEnvironment.traceDiagnostics({ environmentId, input: {} }), ); const { data: processData, error: processError, isPending: isProcessPending, refresh: refreshProcesses, } = useEnvironmentQuery( environmentId === null ? null : serverEnvironment.processDiagnostics({ environmentId, input: {} }), ); const { data: resourceData, error: resourceError, isPending: isResourcePending, refresh: refreshResources, } = useEnvironmentQuery( environmentId === null ? null : serverEnvironment.processResourceHistory({ environmentId, input: { windowMs: selectedResourceWindow.windowMs, bucketMs: selectedResourceWindow.bucketMs, }, }), ); const [isOpeningLogsDirectory, setIsOpeningLogsDirectory] = useState(false); const [openLogsDirectoryError, setOpenLogsDirectoryError] = useState(null); const [signalingPid, setSignalingPid] = useState(null); const openLogsDirectory = useCallback(() => { const logsDirectoryPath = observability?.logsDirectoryPath ?? null; if (!logsDirectoryPath) return; const editor = resolveAndPersistPreferredEditor(availableEditors ?? []); if (!editor) { setOpenLogsDirectoryError("No available editors found."); return; } if (environmentId === null) { setOpenLogsDirectoryError("No environment is selected."); return; } setIsOpeningLogsDirectory(true); setOpenLogsDirectoryError(null); void (async () => { const result = await openInEditor({ environmentId, input: { cwd: logsDirectoryPath, editor, }, }); setIsOpeningLogsDirectory(false); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); setOpenLogsDirectoryError( error instanceof Error ? error.message : "Unable to open logs folder.", ); } })(); }, [availableEditors, environmentId, observability?.logsDirectoryPath, openInEditor]); const isInitialLoading = isPending && data === null; const isProcessInitialLoading = isProcessPending && processData === null; const signalProcess = useCallback( (pid: number, signal: ServerProcessSignal) => { if ( signal === "SIGKILL" && !window.confirm(`Send SIGKILL to process ${pid}? This cannot be handled by the process.`) ) { return; } if (environmentId === null) { return; } setSignalingPid(pid); void (async () => { const result = await signalServerProcess({ environmentId, input: { pid, signal }, }); setSignalingPid(null); if (result._tag === "Failure") { if (!isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); toastManager.add({ type: "error", title: `Could not send ${signal}`, description: error instanceof Error ? error.message : `Failed to send ${signal}.`, }); } return; } if (!result.value.signaled) { const message = Option.getOrUndefined(result.value.message); refreshProcesses(); if (isStaleProcessSignalMessage(message)) { toastManager.add({ type: "info", title: "Process already exited", description: "The process is not a child of the T3 Server. It might already have exited.", }); return; } toastManager.add({ type: "error", title: `Could not send ${signal}`, description: message ?? `Failed to send ${signal}.`, }); return; } refreshProcesses(); })(); }, [environmentId, refreshProcesses, signalServerProcess], ); const processDiagnosticsError = processData ? Option.getOrNull(processData.error) : null; const processResourceError = resourceData ? Option.getOrNull(resourceData.error) : null; const traceDiagnosticsError = data ? Option.getOrNull(data.error) : null; const traceDiagnosticsPartialFailure = data ? Option.getOrElse(data.partialFailure, () => false) : false; return (
} > {processDiagnosticsError || processError ? (
{processDiagnosticsError ? (
{processDiagnosticsError.message}
) : null} {processError ? (
{processError}
) : null}
) : null} } > {processResourceError || resourceError ? (
{processResourceError ? (
{processResourceError.message}
) : null} {resourceError ? (
{resourceError}
) : null}
) : null}
} /> Open logs folder } > 0 ? "danger" : "default"} /> 0 ? "warning" : "default"} /> 0 ? "warning" : "default"} /> {openLogsDirectoryError || traceDiagnosticsError || error ? (
{openLogsDirectoryError ? (
{openLogsDirectoryError}
) : null} {traceDiagnosticsError ? (
{traceDiagnosticsPartialFailure ? `Some trace files could not be read, so diagnostics may be incomplete. ${traceDiagnosticsError.message}` : traceDiagnosticsError.message}
) : null} {error ? (
{error}
) : null}
) : null}
{data && data.latestFailures.length > 0 ? ( {data.latestFailures.map((failure) => ( {failure.name} {formatDuration(failure.durationMs)} {formatRelativeNoWrap(failure.endedAt)} ))} ) : ( )} {data && data.commonFailures.length > 0 ? ( {data.commonFailures.map((failure) => ( {failure.name} {formatCount(failure.count)} {formatRelativeNoWrap(failure.lastSeenAt)} ))} ) : ( )} {data && data.slowestSpans.length > 0 ? ( {data.slowestSpans.map((span) => ( {span.name} {formatDuration(span.durationMs)} {formatRelativeNoWrap(span.endedAt)} ))} ) : ( )} {data && data.latestWarningAndErrorLogs.length > 0 ? ( {data.latestWarningAndErrorLogs.map((event) => ( ))}
Time Level Span Message Trace
{formatRelativeNoWrap(event.seenAt)} {event.level}
{event.spanName}
) : ( )}
{data && data.topSpansByCount.length > 0 ? ( {data.topSpansByCount.map((span) => ( {span.name} {formatCount(span.count)} {formatCount(span.failureCount)} {formatDuration(span.averageDurationMs)} {formatDuration(span.maxDurationMs)} ))} ) : ( )} ); }