import { RefreshIcon } from "~/components/ui/refresh-icon";import { ActivityIcon, AlertTriangleIcon, BatteryIcon, ChevronDownIcon, ChevronRightIcon, CpuIcon, DatabaseIcon, GaugeIcon, HardDriveIcon, MemoryStickIcon,} from "lucide-react";import type { BackgroundBooleanState, ResourceAttributionEntry, ResourceTelemetryAggregate, ResourceTelemetryHistoryBucket, ResourceTelemetryIoSemantics, ResourceTelemetryProcess, ResourceTelemetryProcessCategory, ResourceTelemetryProcessSummary, ResourceTelemetrySourceHealth, ResourceTelemetrySourceStatus, ServerProcessSignal,} from "@t3tools/contracts";import * as DateTime from "effect/DateTime";import * as Option from "effect/Option";import { useCallback, useMemo, useRef, useState, type ReactNode } from "react";import { isAtomCommandInterrupted, squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime"; import { useResourceTelemetry, useResourceTelemetryHistory,} from "../../lib/resourceTelemetryState";import { cn } from "../../lib/utils";import { ensureLocalApi } from "../../localApi";import { usePrimaryEnvironment } from "../../state/environments";import { serverEnvironment } from "../../state/server";import { useAtomCommand } from "../../state/use-atom-command";import { formatRelativeTime } from "../../timestampFormat";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 { resourceHistoryBarHeight, resourceHistoryCpuScaleMax, shouldShowResourceMonitorRetry, visibleResourceTelemetryProcesses,} from "./ResourceTelemetryDiagnostics.logic";import { SettingsSection, useRelativeTimeTick } from "./settingsLayout"; const HISTORY_WINDOWS = [ { label: "5m", windowMs: 5 * 60_000, bucketMs: 15_000 }, { label: "15m", windowMs: 15 * 60_000, bucketMs: 30_000 }, { label: "30m", windowMs: 30 * 60_000, bucketMs: 60_000 }, { label: "1h", windowMs: 60 * 60_000, bucketMs: 2 * 60_000 },] as const; function formatBytes(value: number): string { if (value < 1_024) return `${Math.round(value)} B`; const units = ["KB", "MB", "GB", "TB"] as const; let next = value; let unitIndex = -1; do { next /= 1_024; unitIndex += 1; } while (next >= 1_024 && unitIndex < units.length - 1); return `${next.toFixed(next >= 100 ? 0 : next >= 10 ? 1 : 2)} ${units[unitIndex]}`;} function formatRate(value: number): string { return `${formatBytes(value)}/s`;} function formatCpuTime(valueMs: number): string { const seconds = valueMs / 1_000; 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 formatDurationMicros(value: number): string { if (value < 1_000) return `${Math.round(value)} µs`; if (value < 1_000_000) return `${(value / 1_000).toFixed(2)} ms`; return `${(value / 1_000_000).toFixed(2)} s`;} function formatSampleInterval(valueMs: number): string { if (valueMs < 1_000) return `${Math.max(0, Math.round(valueMs))} ms`; const seconds = valueMs / 1_000; return `${seconds.toLocaleString(undefined, { maximumFractionDigits: 1 })} ${ seconds === 1 ? "second" : "seconds" }`;} function processIdentityKey(process: ResourceTelemetryProcess): string { return `${process.identity.pid}:${process.identity.startTimeMs}`;} function processSummaryIdentityKey(process: ResourceTelemetryProcessSummary): string { return `${process.identity.pid}:${process.identity.startTimeMs}`;} function formatProcessName(process: Pick<ResourceTelemetryProcess, "command" | "name">): string { if (process.name.trim()) return process.name; const firstToken = process.command.trim().split(/\s+/)[0] ?? process.command; const normalized = firstToken.replace(/^['"]|['"]$/g, ""); return normalized.split(/[\\/]/).findLast((segment) => segment.length > 0) ?? normalized;} function categoryLabel(category: ResourceTelemetryProcessCategory): string { switch (category) { case "server": return "Server"; case "server-child": return "Backend child"; case "provider-root": return "Provider"; case "terminal-root": return "Terminal"; case "electron-main": return "Electron main"; case "electron-renderer": return "Renderer"; case "electron-gpu": return "GPU"; case "electron-utility": return "Electron utility"; case "resource-monitor": return "Monitor"; case "unknown-t3": return "T3 process"; }} function categoryDotClass(category: ResourceTelemetryProcessCategory): string { if (category === "resource-monitor") return "bg-amber-500"; if (category.startsWith("electron-")) return "bg-sky-500"; if (category === "server") return "bg-violet-500"; return "bg-emerald-500";} function ioSemanticsLabel(semantics: ResourceTelemetryIoSemantics): string { switch (semantics) { case "storage": return "Storage bytes"; case "logical": return "Logical bytes"; case "all-io": return "All I/O bytes"; case "unavailable": return "Unavailable"; }} function booleanStateLabel( value: BackgroundBooleanState, labels: { readonly true: string; readonly false: string },): string { if (value === "true") return labels.true; if (value === "false") return labels.false; return "Unknown";} function sourceStatusTone(status: ResourceTelemetrySourceStatus): "default" | "warning" | "danger" { if (status === "healthy") return "default"; if (status === "starting" || status === "degraded") return "warning"; return "danger";} function SourceStatusBadge({ label, status, presentation,}: { label: string; status: ResourceTelemetrySourceStatus; presentation?: | { readonly label: string; readonly tone: "neutral"; } | undefined;}) { const tone = presentation?.tone ?? sourceStatusTone(status); return ( <span className={cn( "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.08em]", tone === "neutral" && "border-border/70 bg-muted/45 text-muted-foreground", tone === "default" && "border-emerald-500/25 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", tone === "warning" && "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300", tone === "danger" && "border-destructive/30 bg-destructive/10 text-destructive", )} > <span className={cn( "size-1.5 rounded-full", tone === "neutral" && "bg-muted-foreground/55", tone === "default" && "bg-emerald-500", tone === "warning" && "bg-amber-500", tone === "danger" && "bg-destructive", )} /> {label} {presentation?.label ?? status} </span> );} function LastSampleLabel({ sampledAt }: { sampledAt: DateTime.Utc | null }) { useRelativeTimeTick(); if (!sampledAt) { return <span className="text-[11px] text-muted-foreground/55">Waiting for sample</span>; } const relative = formatRelativeTime(DateTime.formatIso(sampledAt)); if (!relative) { return <span className="text-[11px] text-muted-foreground/55">Waiting for sample</span>; } return ( <span className="text-[11px] text-muted-foreground/60"> Updated <span className="font-mono tabular-nums">{relative.value}</span> {relative.suffix ? ` ${relative.suffix}` : ""} </span> );} function IconStat({ icon, label, value, detail, tone = "default",}: { icon: ReactNode; label: string; value: string; detail?: string | undefined; tone?: "default" | "warning" | "danger";}) { return ( <div className="group min-w-0 px-4 py-4 sm:px-5"> <div className="flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.11em] text-muted-foreground/70"> <span className="text-muted-foreground/55 transition-colors group-hover:text-foreground/65"> {icon} </span> <span className="truncate">{label}</span> </div> <div className={cn( "mt-2.5 truncate font-mono text-2xl font-semibold tracking-[-0.05em] tabular-nums text-foreground", tone === "warning" && "text-amber-600 dark:text-amber-300", tone === "danger" && "text-destructive", )} > {value} </div> {detail ? ( <div className="mt-1.5 truncate text-[10px] text-muted-foreground/60">{detail}</div> ) : null} </div> );} function AggregateCard({ label, accentClass, aggregate,}: { label: string; accentClass: string; aggregate: ResourceTelemetryAggregate;}) { return ( <div className="relative overflow-hidden border-t border-border/60 px-4 py-4 first:border-t-0 md:border-t-0 md:border-l md:first:border-l-0 sm:px-5"> <span className={cn("absolute inset-x-5 top-0 h-0.5 rounded-full opacity-75", accentClass)} /> <div className="flex items-center justify-between gap-3"> <div className="text-[10px] font-semibold uppercase tracking-[0.11em] text-muted-foreground/75"> {label} </div> <div className="rounded-md bg-muted/55 px-1.5 py-0.5 font-mono text-[9px] tabular-nums text-muted-foreground/70"> {aggregate.processCount} {aggregate.processCount === 1 ? "process" : "processes"} </div> </div> <div className="mt-3.5 grid grid-cols-2 gap-x-4 gap-y-2.5"> <MetricPair label="CPU" value={`${aggregate.currentCpuPercent.toFixed(1)}%`} /> <MetricPair label="Memory" value={formatBytes(aggregate.currentRssBytes)} /> <MetricPair label="Read" value={formatRate(aggregate.ioReadBytesPerSecond)} /> <MetricPair label="Write" value={formatRate(aggregate.ioWriteBytesPerSecond)} /> </div> </div> );} function MetricPair({ label, value }: { label: string; value: string }) { return ( <div className="min-w-0"> <div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-muted-foreground/45"> {label} </div> <div className="truncate font-mono text-xs font-medium tabular-nums text-foreground/90"> {value} </div> </div> );} function HealthSource({ label, health }: { label: string; health: ResourceTelemetrySourceHealth }) { const expectedInBrowser = health.status === "unavailable" && Option.exists(health.lastError, (error) => error.includes("'web' mode")); return ( <div className="flex items-start justify-between gap-4 border-t border-border/50 py-3 first:border-t-0"> <div className="min-w-0"> <div className="text-[13px] font-medium text-foreground">{label}</div> <div className="mt-1 text-[11px] leading-relaxed text-muted-foreground/65"> {expectedInBrowser ? "Available when this page runs inside the desktop app." : Option.match(health.lastError, { onNone: () => "No reported errors", onSome: (error) => error, })} </div> </div> <SourceStatusBadge label="" status={health.status} presentation={ expectedInBrowser ? { label: "Desktop only", tone: "neutral", } : undefined } /> </div> );} function DetailRow({ label, value, valueClassName,}: { label: string; value: ReactNode; valueClassName?: string | undefined;}) { return ( <div className="flex items-center justify-between gap-4 border-t border-border/50 py-2.5 first:border-t-0"> <span className="text-[11px] text-muted-foreground/75">{label}</span> <span className={cn( "min-w-0 truncate text-right font-mono text-[11px] tabular-nums text-foreground/85", valueClassName, )} > {value} </span> </div> );} function HistoryWindowSelector({ selectedWindowMs, onSelect,}: { selectedWindowMs: number; onSelect: (windowMs: number) => void;}) { return ( <ToggleGroup aria-label="Resource history period" variant="segmented" value={[String(selectedWindowMs)]} onValueChange={(next) => { const selected = HISTORY_WINDOWS.find((option) => String(option.windowMs) === next[0]); if (selected) onSelect(selected.windowMs); }} > {HISTORY_WINDOWS.map((option) => ( <Toggle key={option.windowMs} value={String(option.windowMs)}> {option.label} </Toggle> ))} </ToggleGroup> );} function ResourceHistoryChart({ buckets,}: { buckets: ReadonlyArray<ResourceTelemetryHistoryBucket>;}) { const maxCpu = resourceHistoryCpuScaleMax(buckets); const maxIo = Math.max(1, ...buckets.map((bucket) => bucket.ioReadBytes + bucket.ioWriteBytes)); return ( <div className="border-t border-border/60 px-4 py-4 sm:px-5"> <div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-[10px] text-muted-foreground/65"> <span className="inline-flex items-center gap-1.5"> <span className="h-1.5 w-3 rounded-full bg-foreground/70" /> CPU average </span> <span className="inline-flex items-center gap-1.5"> <span className="h-1.5 w-3 rounded-full bg-sky-500/70" /> I/O reads </span> <span className="inline-flex items-center gap-1.5"> <span className="h-1.5 w-3 rounded-full bg-amber-500/80" /> I/O writes </span> </div> <div className="flex h-32 items-end gap-1 overflow-hidden rounded-lg border border-border/40 bg-muted/8 px-2 pt-3 pb-2"> {buckets.map((bucket) => { const cpuHeight = resourceHistoryBarHeight({ value: bucket.avgCpuPercent, max: maxCpu, minimumVisiblePercent: 2, }); const readHeight = resourceHistoryBarHeight({ value: bucket.ioReadBytes, max: maxIo, minimumVisiblePercent: 1, }); const writeHeight = resourceHistoryBarHeight({ value: bucket.ioWriteBytes, max: maxIo, minimumVisiblePercent: 1, }); return ( <Tooltip key={DateTime.formatIso(bucket.startedAt)}> <TooltipTrigger render={ <div className="grid h-full min-w-1 flex-1 grid-cols-3 items-end gap-px"> <span className="block rounded-t-sm bg-foreground/65" style={{ height: `${cpuHeight}%` }} /> <span className="block rounded-t-sm bg-sky-500/70" style={{ height: `${readHeight}%` }} /> <span className="block rounded-t-sm bg-amber-500/80" style={{ height: `${writeHeight}%` }} /> </div> } /> <TooltipPopup side="top" className="space-y-0.5 text-left"> <div>CPU avg {bucket.avgCpuPercent.toFixed(1)}%</div> <div>CPU peak {bucket.maxCpuPercent.toFixed(1)}%</div> <div>Read {formatBytes(bucket.ioReadBytes)}</div> <div>Write {formatBytes(bucket.ioWriteBytes)}</div> </TooltipPopup> </Tooltip> ); })} </div> </div> );} function ProcessTreeName({ process, collapsed, onToggle,}: { process: ResourceTelemetryProcess; collapsed: boolean; onToggle: (process: ResourceTelemetryProcess) => void;}) { const name = formatProcessName(process); const hasChildren = process.childPids.length > 0; const ChevronIcon = collapsed ? ChevronRightIcon : ChevronDownIcon; 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, 7) * 10}px` }} > {hasChildren ? ( <Button size="icon-micro" variant="ghost-muted" onClick={() => onToggle(process)} aria-label={collapsed ? `Expand ${name}` : `Collapse ${name}`} > <ChevronIcon className="size-3.5" /> </Button> ) : ( <span className="size-5" aria-hidden /> )} <span className={cn("size-1.5 rounded-full", categoryDotClass(process.category))} />Lines 1–500 of 1288. Load more
import { RefreshIcon } from "~/components/ui/refresh-icon"; import { ActivityIcon, AlertTriangleIcon, BatteryIcon, ChevronDownIcon, ChevronRightIcon, CpuIcon, DatabaseIcon, GaugeIcon, HardDriveIcon, MemoryStickIcon, } from "lucide-react"; import type { BackgroundBooleanState, ResourceAttributionEntry, ResourceTelemetryAggregate, ResourceTelemetryHistoryBucket, ResourceTelemetryIoSemantics, ResourceTelemetryProcess, ResourceTelemetryProcessCategory, ResourceTelemetryProcessSummary, ResourceTelemetrySourceHealth, ResourceTelemetrySourceStatus, ServerProcessSignal, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Option from "effect/Option"; import { useCallback, useMemo, useRef, useState, type ReactNode } from "react"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { useResourceTelemetry, useResourceTelemetryHistory, } from "../../lib/resourceTelemetryState"; import { cn } from "../../lib/utils"; import { ensureLocalApi } from "../../localApi"; import { usePrimaryEnvironment } from "../../state/environments"; import { serverEnvironment } from "../../state/server"; import { useAtomCommand } from "../../state/use-atom-command"; import { formatRelativeTime } from "../../timestampFormat"; 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 { resourceHistoryBarHeight, resourceHistoryCpuScaleMax, shouldShowResourceMonitorRetry, visibleResourceTelemetryProcesses, } from "./ResourceTelemetryDiagnostics.logic"; import { SettingsSection, useRelativeTimeTick } from "./settingsLayout"; const HISTORY_WINDOWS = [ { label: "5m", windowMs: 5 * 60_000, bucketMs: 15_000 }, { label: "15m", windowMs: 15 * 60_000, bucketMs: 30_000 }, { label: "30m", windowMs: 30 * 60_000, bucketMs: 60_000 }, { label: "1h", windowMs: 60 * 60_000, bucketMs: 2 * 60_000 }, ] as const; function formatBytes(value: number): string { if (value < 1_024) return `${Math.round(value)} B`; const units = ["KB", "MB", "GB", "TB"] as const; let next = value; let unitIndex = -1; do { next /= 1_024; unitIndex += 1; } while (next >= 1_024 && unitIndex < units.length - 1); return `${next.toFixed(next >= 100 ? 0 : next >= 10 ? 1 : 2)} ${units[unitIndex]}`; } function formatRate(value: number): string { return `${formatBytes(value)}/s`; } function formatCpuTime(valueMs: number): string { const seconds = valueMs / 1_000; 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 formatDurationMicros(value: number): string { if (value < 1_000) return `${Math.round(value)} µs`; if (value < 1_000_000) return `${(value / 1_000).toFixed(2)} ms`; return `${(value / 1_000_000).toFixed(2)} s`; } function formatSampleInterval(valueMs: number): string { if (valueMs < 1_000) return `${Math.max(0, Math.round(valueMs))} ms`; const seconds = valueMs / 1_000; return `${seconds.toLocaleString(undefined, { maximumFractionDigits: 1 })} ${ seconds === 1 ? "second" : "seconds" }`; } function processIdentityKey(process: ResourceTelemetryProcess): string { return `${process.identity.pid}:${process.identity.startTimeMs}`; } function processSummaryIdentityKey(process: ResourceTelemetryProcessSummary): string { return `${process.identity.pid}:${process.identity.startTimeMs}`; } function formatProcessName(process: Pick<ResourceTelemetryProcess, "command" | "name">): string { if (process.name.trim()) return process.name; const firstToken = process.command.trim().split(/\s+/)[0] ?? process.command; const normalized = firstToken.replace(/^['"]|['"]$/g, ""); return normalized.split(/[\\/]/).findLast((segment) => segment.length > 0) ?? normalized; } function categoryLabel(category: ResourceTelemetryProcessCategory): string { switch (category) { case "server": return "Server"; case "server-child": return "Backend child"; case "provider-root": return "Provider"; case "terminal-root": return "Terminal"; case "electron-main": return "Electron main"; case "electron-renderer": return "Renderer"; case "electron-gpu": return "GPU"; case "electron-utility": return "Electron utility"; case "resource-monitor": return "Monitor"; case "unknown-t3": return "T3 process"; } } function categoryDotClass(category: ResourceTelemetryProcessCategory): string { if (category === "resource-monitor") return "bg-amber-500"; if (category.startsWith("electron-")) return "bg-sky-500"; if (category === "server") return "bg-violet-500"; return "bg-emerald-500"; } function ioSemanticsLabel(semantics: ResourceTelemetryIoSemantics): string { switch (semantics) { case "storage": return "Storage bytes"; case "logical": return "Logical bytes"; case "all-io": return "All I/O bytes"; case "unavailable": return "Unavailable"; } } function booleanStateLabel( value: BackgroundBooleanState, labels: { readonly true: string; readonly false: string }, ): string { if (value === "true") return labels.true; if (value === "false") return labels.false; return "Unknown"; } function sourceStatusTone(status: ResourceTelemetrySourceStatus): "default" | "warning" | "danger" { if (status === "healthy") return "default"; if (status === "starting" || status === "degraded") return "warning"; return "danger"; } function SourceStatusBadge({ label, status, presentation, }: { label: string; status: ResourceTelemetrySourceStatus; presentation?: | { readonly label: string; readonly tone: "neutral"; } | undefined; }) { const tone = presentation?.tone ?? sourceStatusTone(status); return ( <span className={cn( "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.08em]", tone === "neutral" && "border-border/70 bg-muted/45 text-muted-foreground", tone === "default" && "border-emerald-500/25 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", tone === "warning" && "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300", tone === "danger" && "border-destructive/30 bg-destructive/10 text-destructive", )} > <span className={cn( "size-1.5 rounded-full", tone === "neutral" && "bg-muted-foreground/55", tone === "default" && "bg-emerald-500", tone === "warning" && "bg-amber-500", tone === "danger" && "bg-destructive", )} /> {label} {presentation?.label ?? status} </span> ); } function LastSampleLabel({ sampledAt }: { sampledAt: DateTime.Utc | null }) { useRelativeTimeTick(); if (!sampledAt) { return <span className="text-[11px] text-muted-foreground/55">Waiting for sample</span>; } const relative = formatRelativeTime(DateTime.formatIso(sampledAt)); if (!relative) { return <span className="text-[11px] text-muted-foreground/55">Waiting for sample</span>; } return ( <span className="text-[11px] text-muted-foreground/60"> Updated <span className="font-mono tabular-nums">{relative.value}</span> {relative.suffix ? ` ${relative.suffix}` : ""} </span> ); } function IconStat({ icon, label, value, detail, tone = "default", }: { icon: ReactNode; label: string; value: string; detail?: string | undefined; tone?: "default" | "warning" | "danger"; }) { return ( <div className="group min-w-0 px-4 py-4 sm:px-5"> <div className="flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.11em] text-muted-foreground/70"> <span className="text-muted-foreground/55 transition-colors group-hover:text-foreground/65"> {icon} </span> <span className="truncate">{label}</span> </div> <div className={cn( "mt-2.5 truncate font-mono text-2xl font-semibold tracking-[-0.05em] tabular-nums text-foreground", tone === "warning" && "text-amber-600 dark:text-amber-300", tone === "danger" && "text-destructive", )} > {value} </div> {detail ? ( <div className="mt-1.5 truncate text-[10px] text-muted-foreground/60">{detail}</div> ) : null} </div> ); } function AggregateCard({ label, accentClass, aggregate, }: { label: string; accentClass: string; aggregate: ResourceTelemetryAggregate; }) { return ( <div className="relative overflow-hidden border-t border-border/60 px-4 py-4 first:border-t-0 md:border-t-0 md:border-l md:first:border-l-0 sm:px-5"> <span className={cn("absolute inset-x-5 top-0 h-0.5 rounded-full opacity-75", accentClass)} /> <div className="flex items-center justify-between gap-3"> <div className="text-[10px] font-semibold uppercase tracking-[0.11em] text-muted-foreground/75"> {label} </div> <div className="rounded-md bg-muted/55 px-1.5 py-0.5 font-mono text-[9px] tabular-nums text-muted-foreground/70"> {aggregate.processCount} {aggregate.processCount === 1 ? "process" : "processes"} </div> </div> <div className="mt-3.5 grid grid-cols-2 gap-x-4 gap-y-2.5"> <MetricPair label="CPU" value={`${aggregate.currentCpuPercent.toFixed(1)}%`} /> <MetricPair label="Memory" value={formatBytes(aggregate.currentRssBytes)} /> <MetricPair label="Read" value={formatRate(aggregate.ioReadBytesPerSecond)} /> <MetricPair label="Write" value={formatRate(aggregate.ioWriteBytesPerSecond)} /> </div> </div> ); } function MetricPair({ label, value }: { label: string; value: string }) { return ( <div className="min-w-0"> <div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-muted-foreground/45"> {label} </div> <div className="truncate font-mono text-xs font-medium tabular-nums text-foreground/90"> {value} </div> </div> ); } function HealthSource({ label, health }: { label: string; health: ResourceTelemetrySourceHealth }) { const expectedInBrowser = health.status === "unavailable" && Option.exists(health.lastError, (error) => error.includes("'web' mode")); return ( <div className="flex items-start justify-between gap-4 border-t border-border/50 py-3 first:border-t-0"> <div className="min-w-0"> <div className="text-[13px] font-medium text-foreground">{label}</div> <div className="mt-1 text-[11px] leading-relaxed text-muted-foreground/65"> {expectedInBrowser ? "Available when this page runs inside the desktop app." : Option.match(health.lastError, { onNone: () => "No reported errors", onSome: (error) => error, })} </div> </div> <SourceStatusBadge label="" status={health.status} presentation={ expectedInBrowser ? { label: "Desktop only", tone: "neutral", } : undefined } /> </div> ); } function DetailRow({ label, value, valueClassName, }: { label: string; value: ReactNode; valueClassName?: string | undefined; }) { return ( <div className="flex items-center justify-between gap-4 border-t border-border/50 py-2.5 first:border-t-0"> <span className="text-[11px] text-muted-foreground/75">{label}</span> <span className={cn( "min-w-0 truncate text-right font-mono text-[11px] tabular-nums text-foreground/85", valueClassName, )} > {value} </span> </div> ); } function HistoryWindowSelector({ selectedWindowMs, onSelect, }: { selectedWindowMs: number; onSelect: (windowMs: number) => void; }) { return ( <ToggleGroup aria-label="Resource history period" variant="segmented" value={[String(selectedWindowMs)]} onValueChange={(next) => { const selected = HISTORY_WINDOWS.find((option) => String(option.windowMs) === next[0]); if (selected) onSelect(selected.windowMs); }} > {HISTORY_WINDOWS.map((option) => ( <Toggle key={option.windowMs} value={String(option.windowMs)}> {option.label} </Toggle> ))} </ToggleGroup> ); } function ResourceHistoryChart({ buckets, }: { buckets: ReadonlyArray<ResourceTelemetryHistoryBucket>; }) { const maxCpu = resourceHistoryCpuScaleMax(buckets); const maxIo = Math.max(1, ...buckets.map((bucket) => bucket.ioReadBytes + bucket.ioWriteBytes)); return ( <div className="border-t border-border/60 px-4 py-4 sm:px-5"> <div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-[10px] text-muted-foreground/65"> <span className="inline-flex items-center gap-1.5"> <span className="h-1.5 w-3 rounded-full bg-foreground/70" /> CPU average </span> <span className="inline-flex items-center gap-1.5"> <span className="h-1.5 w-3 rounded-full bg-sky-500/70" /> I/O reads </span> <span className="inline-flex items-center gap-1.5"> <span className="h-1.5 w-3 rounded-full bg-amber-500/80" /> I/O writes </span> </div> <div className="flex h-32 items-end gap-1 overflow-hidden rounded-lg border border-border/40 bg-muted/8 px-2 pt-3 pb-2"> {buckets.map((bucket) => { const cpuHeight = resourceHistoryBarHeight({ value: bucket.avgCpuPercent, max: maxCpu, minimumVisiblePercent: 2, }); const readHeight = resourceHistoryBarHeight({ value: bucket.ioReadBytes, max: maxIo, minimumVisiblePercent: 1, }); const writeHeight = resourceHistoryBarHeight({ value: bucket.ioWriteBytes, max: maxIo, minimumVisiblePercent: 1, }); return ( <Tooltip key={DateTime.formatIso(bucket.startedAt)}> <TooltipTrigger render={ <div className="grid h-full min-w-1 flex-1 grid-cols-3 items-end gap-px"> <span className="block rounded-t-sm bg-foreground/65" style={{ height: `${cpuHeight}%` }} /> <span className="block rounded-t-sm bg-sky-500/70" style={{ height: `${readHeight}%` }} /> <span className="block rounded-t-sm bg-amber-500/80" style={{ height: `${writeHeight}%` }} /> </div> } /> <TooltipPopup side="top" className="space-y-0.5 text-left"> <div>CPU avg {bucket.avgCpuPercent.toFixed(1)}%</div> <div>CPU peak {bucket.maxCpuPercent.toFixed(1)}%</div> <div>Read {formatBytes(bucket.ioReadBytes)}</div> <div>Write {formatBytes(bucket.ioWriteBytes)}</div> </TooltipPopup> </Tooltip> ); })} </div> </div> ); } function ProcessTreeName({ process, collapsed, onToggle, }: { process: ResourceTelemetryProcess; collapsed: boolean; onToggle: (process: ResourceTelemetryProcess) => void; }) { const name = formatProcessName(process); const hasChildren = process.childPids.length > 0; const ChevronIcon = collapsed ? ChevronRightIcon : ChevronDownIcon; 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, 7) * 10}px` }} > {hasChildren ? ( <Button size="icon-micro" variant="ghost-muted" onClick={() => onToggle(process)} aria-label={collapsed ? `Expand ${name}` : `Collapse ${name}`} > <ChevronIcon className="size-3.5" /> </Button> ) : ( <span className="size-5" aria-hidden /> )} <span className={cn("size-1.5 rounded-full", categoryDotClass(process.category))} />