@@ -47,6 +47,7 @@ type ProcessDetail = {
username : string
created_at : number
parent_pid : number
parent_name : string
nice : number
num_threads : number
num_fds : number
@@ -64,6 +65,7 @@ type ProcessDetail = {
}
type SortField = 'cpu' | 'mem'
type NavItem = { pid : number ; name : string }
// ---------------------------------------------------------------------------
// Helpers
@@ -119,10 +121,10 @@ function MetricValue({ value, pct }: { value: string; pct: number }) {
)
}
function DetailRow ( { label , value , mono = false } : { label : string ; value : React.ReactNode ; mono? : boolean } ) {
function DetailRow ( { label , value , mono = false , tooltip } : { label : string ; value : React.ReactNode ; mono? : boolean ; tooltip? : string } ) {
return (
< div className = "flex flex-col gap-0.5" >
< span className = "text-[10px] font-semibold uppercase tracking-widest text-zinc-600" > { label } < / span >
< div className = "flex flex-col gap-0.5" title = { tooltip } >
< span className = "text-[10px] font-semibold uppercase tracking-widest text-zinc-600 cursor-help " > { label } < / span >
< span className = { cn ( 'text-xs text-zinc-300 break-all' , mono && 'font-mono' ) } > { value || '—' } < / span >
< / div >
)
@@ -136,9 +138,9 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
)
}
function StatBlock ( { label , value , sub } : { label : string ; value : React.ReactNode ; sub? : string } ) {
function StatBlock ( { label , value , sub , tooltip } : { label : string ; value : React.ReactNode ; sub? : string ; tooltip? : string } ) {
return (
< div className = "bg-zinc-800/40 rounded-lg px-3 py-2.5" >
< div className = "bg-zinc-800/40 rounded-lg px-3 py-2.5 cursor-help" title = { tooltip } >
< p className = "text-[10px] text-zinc-600 uppercase tracking-widest mb-1" > { label } < / p >
< p className = "text-sm font-semibold text-zinc-100 tabular-nums" > { value } < / p >
{ sub && < p className = "text-[10px] text-zinc-600 mt-0.5" > { sub } < / p > }
@@ -153,16 +155,28 @@ function StatBlock({ label, value, sub }: { label: string; value: React.ReactNod
function ProcessDetailPanel ( {
pid ,
onClose ,
onNavigate ,
navStack ,
onNavBack ,
gpuType ,
} : {
pid : number
onClose : ( ) = > void
onNavigate : ( pid : number , fromName : string ) = > void
navStack : NavItem [ ]
onNavBack : ( index : number ) = > void
gpuType? : string
} ) {
const [ detail , setDetail ] = useState < ProcessDetail | null > ( null )
const [ loading , setLoading ] = useState ( true )
const [ killing , setKilling ] = useState ( false )
const [ killConfirm , setKillConfirm ] = useState ( false )
const [ killError , setKillError ] = useState < string | null > ( null )
const [ manPage , setManPage ] = useState < string | null > ( null )
const [ manPageLoading , setManPageLoading ] = useState ( false )
const [ manOpen , setManOpen ] = useState ( false )
const intervalRef = useRef < ReturnType < typeof setInterval > | null > ( null )
const lastManNameRef = useRef < string > ( '' )
const load = useCallback ( async ( ) = > {
try {
@@ -204,6 +218,17 @@ function ProcessDetailPanel({
return ( ) = > window . removeEventListener ( 'keydown' , onKey )
} , [ onClose ] )
useEffect ( ( ) = > {
if ( ! detail ? . name || detail . name === lastManNameRef . current ) return
lastManNameRef . current = detail . name
setManPageLoading ( true )
setManPage ( null )
; ( window as any ) [ 'go' ] [ 'main' ] [ 'App' ] [ 'GetManPage' ] ( detail . name )
. then ( ( text : string ) = > { setManPage ( text || null ) } )
. catch ( ( ) = > setManPage ( null ) )
. finally ( ( ) = > setManPageLoading ( false ) )
} , [ detail ? . name ] )
return (
< >
{ /* Backdrop */ }
@@ -217,10 +242,40 @@ function ProcessDetailPanel({
{ /* Panel header */ }
< div className = "flex items-start justify-between px-5 py-4 border-b border-zinc-800/80 shrink-0" >
< div className = "min-w-0 flex-1 pr-4" >
{ navStack . length > 0 && (
< div className = "flex items-center gap-1 flex-wrap mb-1.5 min-w-0" >
{ navStack . map ( ( item , i ) = > (
< React.Fragment key = { i } >
< button
onClick = { ( ) = > onNavBack ( i ) }
className = "text-[10px] text-zinc-500 hover:text-zinc-300 transition-colors hover:underline truncate max-w-[100px]"
title = { ` Back to ${ item . name || ` PID ${ item . pid } ` } (PID ${ item . pid } ) ` }
>
{ item . name || ` PID ${ item . pid } ` }
< / button >
< ChevronRight className = "w-2.5 h-2.5 text-zinc-700 shrink-0" / >
< / React.Fragment >
) ) }
< span className = "text-[10px] text-zinc-400 truncate max-w-[100px]" > { detail ? . name ? ? ` PID ${ pid } ` } < / span >
< / div >
) }
< div className = "flex items-center gap-2 mb-1" >
< span className = "text-sm font-semibold text-zinc-100 truncate" >
{ detail ? . name ? ? ` PID ${ pid } ` }
< / span >
{ gpuType && (
< span
title = { gpuType === 'C' ? 'GPU Compute — CUDA/OpenCL/Vulkan Compute' : gpuType === 'G' ? 'GPU Graphics — rendering & display' : 'GPU Compute + Graphics' }
className = { cn (
'shrink-0 text-[10px] font-mono px-1.5 py-0.5 rounded cursor-help' ,
gpuType === 'C' ? 'bg-violet-900/50 text-violet-300' :
gpuType === 'G' ? 'bg-blue-900/50 text-blue-300' :
'bg-indigo-900/50 text-indigo-300'
) }
>
GPU · { gpuType }
< / span >
) }
{ detail ? . status && (
< span
className = { cn (
@@ -315,25 +370,39 @@ function ProcessDetailPanel({
{ /* Identity */ }
< SectionTitle > Identity < / SectionTitle >
< div className = "grid grid-cols-3 gap-2 mb-4" >
< StatBlock label = "Parent PID" value = { detail . parent_pid > 0 ? detail . parent_pid : '—' } / >
< StatBlock label = "Nice / Priority" value = { detail . nice } / >
{ detail . parent_pid > 0 ? (
< div
className = "bg-zinc-800/40 rounded-lg px-3 py-2.5 cursor-pointer hover:bg-zinc-700/50 transition-colors group"
title = { ` Navigate to parent process ${ detail . parent_name ? ` ( ${ detail . parent_name } ) ` : '' } ` }
onClick = { ( ) = > onNavigate ( detail . parent_pid , detail . name ) }
>
< p className = "text-[10px] text-zinc-600 uppercase tracking-widest mb-1 group-hover:text-zinc-400" > Parent PID < / p >
< p className = "text-sm font-semibold tabular-nums text-emerald-400/80 group-hover:text-emerald-400 transition-colors" > { detail . parent_pid } < / p >
{ detail . parent_name && < p className = "text-[10px] text-zinc-500 mt-0.5 truncate" > { detail . parent_name } < / p > }
< / div >
) : (
< StatBlock label = "Parent PID" value = "—" tooltip = "No parent process (root process)" / >
) }
< StatBlock label = "Nice / Priority" value = { detail . nice } tooltip = "Scheduling priority: -20 = highest, +19 = lowest. Default is 0. Lower values get more CPU time" / >
< StatBlock
label = "Created"
value = { detail . created_at ? new Date ( detail . created_at ) . toLocaleTimeString ( [ ] , { hour : '2-digit' , minute : '2-digit' } ) : '—' }
sub = { detail . created_at ? fmtRelative ( detail . created_at ) : undefined }
tooltip = "Time when this process was started"
/ >
< / div >
{ /* Paths */ }
< SectionTitle > Executable < / SectionTitle >
< div className = "space-y-3 mb-4" >
< DetailRow label = "Path" value = { detail . exe } mono / >
< DetailRow label = "Path" value = { detail . exe } mono tooltip = "Full filesystem path to the executable binary" / >
< DetailRow
label = "Command"
value = { detail . cmdline || detail . exe }
mono
tooltip = "Full command line including arguments used to launch this process"
/ >
{ detail . cwd && < DetailRow label = "Working directory" value = { detail . cwd } mono / > }
{ detail . cwd && < DetailRow label = "Working directory" value = { detail . cwd } mono tooltip = "Current working directory of the process at the time of sampling" / > }
< / div >
{ /* CPU & Memory */ }
@@ -342,26 +411,31 @@ function ProcessDetailPanel({
< StatBlock
label = "CPU"
value = { ` ${ detail . cpu_percent . toFixed ( 1 ) } % ` }
tooltip = "CPU usage as a percentage of one core (can exceed 100% on multi-threaded processes)"
/ >
< StatBlock
label = "Threads"
value = { detail . num_threads }
sub = { ` ${ detail . num_fds } open FDs ` }
tooltip = "Number of OS threads running in this process. Open FDs = open file descriptors (files, sockets, pipes, devices)"
/ >
< / div >
< div className = "grid grid-cols-3 gap-2 mb-4" >
< StatBlock
label = "RSS (physical)"
value = { fmtBytes ( detail . rss ) }
tooltip = "Resident Set Size — physical RAM pages currently mapped and in use by this process"
/ >
< StatBlock
label = "VMS (virtual)"
value = { fmtBytes ( detail . vms ) }
tooltip = "Virtual Memory Size — total virtual address space reserved, including shared libraries and mapped files"
/ >
< StatBlock
label = "Swap"
value = { fmtBytes ( detail . swap ) }
sub = { ` ${ detail . mem_percent . toFixed ( 1 ) } % of RAM ` }
tooltip = "Memory paged out to disk swap. % = share of total system RAM used by this process"
/ >
< / div >
@@ -374,11 +448,13 @@ function ProcessDetailPanel({
label = "Read"
value = { fmtBytes ( detail . read_bytes ) }
sub = { ` ${ detail . read_ops . toLocaleString ( ) } ops ` }
tooltip = "Total bytes read from disk since process start. ops = number of read syscalls issued"
/ >
< StatBlock
label = "Write"
value = { fmtBytes ( detail . write_bytes ) }
sub = { ` ${ detail . write_ops . toLocaleString ( ) } ops ` }
tooltip = "Total bytes written to disk since process start. ops = number of write syscalls issued"
/ >
< / div >
< / >
@@ -390,12 +466,39 @@ function ProcessDetailPanel({
< StatBlock
label = "Open files"
value = { detail . open_files_count }
tooltip = "Number of open file descriptors: regular files, sockets, pipes, and device handles"
/ >
< StatBlock
label = "Connections"
value = { detail . conn_count }
tooltip = "Number of active network connections (TCP/UDP sockets) opened by this process"
/ >
< / div >
{ /* Man page */ }
{ ( manPage !== null || manPageLoading ) && (
< >
< SectionTitle > Manual page < / SectionTitle >
{ manPageLoading ? (
< p className = "text-xs text-zinc-700 mb-4" > Loading … < / p >
) : (
< div className = "mb-4" >
< button
onClick = { ( ) = > setManOpen ( o = > ! o ) }
className = "flex items-center gap-1.5 text-xs text-zinc-600 hover:text-zinc-400 transition-colors mb-2"
>
{ manOpen ? < ChevronDown className = "w-3 h-3" / > : < ChevronRight className = "w-3 h-3" / > }
{ manOpen ? 'Collapse' : 'Show' } man { detail . name }
< / button >
{ manOpen && (
< pre className = "text-[10px] font-mono text-zinc-500 whitespace-pre-wrap break-words bg-zinc-900/60 rounded-lg p-3 max-h-80 overflow-y-auto leading-relaxed border border-zinc-800/60" >
{ manPage }
< / pre >
) }
< / div >
) }
< / >
) }
< / >
) }
< / div >
@@ -421,9 +524,15 @@ export default function App() {
const [ procOpen , setProcOpen ] = useState ( true )
const [ gpuOpen , setGpuOpen ] = useState ( true )
const [ gpuSortBy , setGpuSortBy ] = useState < 'vram' | 'ram' > ( 'vram' )
const [ navStack , setNavStack ] = useState < NavItem [ ] > ( [ ] )
const PAGE_SIZE = 20
const GPU_PAGE_SIZE = 4
const openDetail = useCallback ( ( pid : number ) = > {
setSelectedPid ( pid )
setNavStack ( [ ] )
} , [ ] )
const [ pageSize , setPageSize ] = useState ( 20 )
const [ gpuPageSize , setGpuPageSize ] = useState ( 4 )
useEffect ( ( ) = > {
const handler = ( m : Metrics ) = > setMetrics ( m )
@@ -448,16 +557,16 @@ export default function App() {
const gpuProcesses = [ . . . ( metrics ? . gpu_processes ? ? [ ] ) ] . sort ( ( a , b ) = >
gpuSortBy === 'vram' ? b . vram_mb - a.vram_mb : b.ram - a . ram
)
const gpuTotalPages = Math . ceil ( gpuProcesses . length / GPU_PAGE_SIZE )
const gpuTotalPages = Math . ceil ( gpuProcesses . length / gpuPageSize )
const safeGpuPage = Math . min ( gpuPage , Math . max ( 0 , gpuTotalPages - 1 ) )
const pagedGpuProcesses = gpuProcesses . slice ( safeGpuPage * GPU_PAGE_SIZE , ( safeGpuPage + 1 ) * GPU_PAGE_SIZE )
const pagedGpuProcesses = gpuProcesses . slice ( safeGpuPage * gpuPageSize , ( safeGpuPage + 1 ) * gpuPageSize )
const sortedProcesses = metrics
? [ . . . metrics . processes ] . sort ( ( a , b ) = > b [ sortBy ] - a [ sortBy ] )
: [ ]
const totalPages = Math . ceil ( sortedProcesses . length / PAGE_SIZE )
const totalPages = Math . ceil ( sortedProcesses . length / pageSize )
const safePage = Math . min ( page , Math . max ( 0 , totalPages - 1 ) )
const pagedProcesses = sortedProcesses . slice ( safePage * PAGE_SIZE , ( safePage + 1 ) * PAGE_SIZE )
const pagedProcesses = sortedProcesses . slice ( safePage * pageSize , ( safePage + 1 ) * pageSize )
return (
< div className = "min-h-screen bg-zinc-950 text-zinc-100 antialiased select-none" >
@@ -554,17 +663,17 @@ export default function App() {
< table className = "w-full text-sm" >
< thead >
< tr className = "border-b border-zinc-800/80" >
< th className = "py-2 px-5 text-left text-xs font-medium text-zinc-600 uppercase tracking-wider w-16" > PID < / th >
< th className = "py-2 text-left text-xs font-medium text-zinc-600 uppercase tracking-wider" > Name < / th >
< th className = "py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28" > CPU < / th >
< th className = "py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28" > Memory < / th >
< th className = "py-2 px-5 text-left text-xs font-medium text-zinc-600 uppercase tracking-wider w-16 cursor-help" title = "Process ID — unique identifier assigned by the OS " > PID < / th >
< th className = "py-2 text-left text-xs font-medium text-zinc-600 uppercase tracking-wider cursor-help" title = "Executable name of the process " > Name < / th >
< th className = "py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28 cursor-help" title = "CPU usage as a % of a single core (can exceed 100% on multi-threaded processes) " > CPU < / th >
< th className = "py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28 cursor-help" title = "Physical RAM (RSS) currently used by this process " > Memory < / th >
< / tr >
< / thead >
< tbody >
{ pagedProcesses . map ( ( p ) = > (
< tr
key = { p . pid }
onClick = { ( ) = > setSelectedPid ( p . pid ) }
onClick = { ( ) = > openDetail ( p . pid ) }
className = { cn (
'border-b border-zinc-900 transition-colors cursor-pointer' ,
selectedPid === p . pid
@@ -602,11 +711,21 @@ export default function App() {
< / table >
{ /* Pagination */ }
{ totalPages > 1 && (
{ totalPages > 0 && (
< div className = "flex items-center justify-between px-5 py-3 border-t border-zinc-800/60" >
< div className = "flex items-center gap-2" >
< span className = "text-xs text-zinc-600" >
{ safePage * PAGE_SIZE + 1 } – { Math . min ( ( safePage + 1 ) * PAGE_SIZE , sortedProcesses . length ) } of { sortedProcesses . length }
{ safePage * pageSize + 1 } – { Math . min ( ( safePage + 1 ) * pageSize , sortedProcesses . length ) } of { sortedProcesses . length }
< / span >
< select
value = { pageSize }
onChange = { e = > { setPageSize ( Number ( e . target . value ) ) ; setPage ( 0 ) } }
className = "text-xs bg-zinc-900 border border-zinc-800 text-zinc-400 rounded px-1.5 py-0.5 cursor-pointer hover:border-zinc-600 transition-colors"
title = "Items per page"
>
{ [ 10 , 20 , 50 , 100 ] . map ( n = > < option key = { n } value = { n } > { n } / page < / option > ) }
< / select >
< / div >
< div className = "flex items-center gap-1" >
< button
onClick = { ( ) = > setPage ( 0 ) }
@@ -746,10 +865,10 @@ export default function App() {
< table className = "w-full" >
< thead >
< tr >
< th className = "pb-1.5 text-left text-xs font-medium text-zinc-700 uppercase tracking-wider" > Name < / th >
< th className = "pb-1.5 text-left text-xs font-medium text-zinc-700 uppercase tracking-wider w-16" > Type < / th >
< th className = "pb-1.5 text-right text-xs font-medium text-zinc-700 uppercase tracking-wider w-24" > VRAM < / th >
< th className = "pb-1.5 text-right text-xs font-medium text-zinc-700 uppercase tracking-wider w-28" > RAM < / th >
< th className = "pb-1.5 text-left text-xs font-medium text-zinc-700 uppercase tracking-wider cursor-help" title = "Process name using this GPU " > Name < / th >
< th className = "pb-1.5 text-left text-xs font-medium text-zinc-700 uppercase tracking-wider w-16 cursor-help" title = "C = Compute (CUDA/OpenCL), G = Graphics (rendering), C+G = both " > Type < / th >
< th className = "pb-1.5 text-right text-xs font-medium text-zinc-700 uppercase tracking-wider w-24 cursor-help" title = "GPU VRAM used by this process (video memory on the graphics card) " > VRAM < / th >
< th className = "pb-1.5 text-right text-xs font-medium text-zinc-700 uppercase tracking-wider w-28 cursor-help" title = "System RAM (CPU-side memory) used by this process " > RAM < / th >
< / tr >
< / thead >
< tbody >
@@ -757,13 +876,18 @@ export default function App() {
< tr
key = { p . pid }
className = "border-t border-zinc-900 hover:bg-zinc-800/30 transition-colors cursor-pointer"
onClick = { ( ) = > setSelectedPid ( p . pid ) }
onClick = { ( ) = > openDetail ( p . pid ) }
>
< td className = "py-1.5 text-zinc-300 text-xs truncate max-w-[180px]" > { p . name } < / td >
< td className = "py-1.5" >
< span
title = {
p . type === 'C' ? 'C — Compute: uses the GPU for compute workloads (CUDA, OpenCL, Vulkan Compute…)' :
p . type === 'G' ? 'G — Graphics: uses the GPU for rendering and display output' :
'C+G — Compute + Graphics: uses the GPU for both compute and rendering workloads'
}
className = { cn (
'inline-block px-1.5 py-0.5 rounded text-xs font-mono font-semibold' ,
'inline-block px-1.5 py-0.5 rounded text-xs font-mono font-semibold cursor-help ' ,
p . type === 'C' ? 'bg-violet-900/50 text-violet-300' :
p . type === 'G' ? 'bg-blue-900/50 text-blue-300' :
'bg-indigo-900/50 text-indigo-300'
@@ -780,11 +904,21 @@ export default function App() {
< / table >
{ /* GPU pagination */ }
{ gpuTotalPages > 1 && (
{ gpuTotalPages > 0 && (
< div className = "flex items-center justify-between pt-2 mt-1 border-t border-zinc-900" >
< div className = "flex items-center gap-2" >
< span className = "text-xs text-zinc-600" >
{ safeGpuPage * GPU_PAGE_SIZE + 1 } – { Math . min ( ( safeGpuPage + 1 ) * GPU_PAGE_SIZE , gpuProcesses . length ) } of { gpuProcesses . length }
{ safeGpuPage * gpuPageSize + 1 } – { Math . min ( ( safeGpuPage + 1 ) * gpuPageSize , gpuProcesses . length ) } of { gpuProcesses . length }
< / span >
< select
value = { gpuPageSize }
onChange = { e = > { setGpuPageSize ( Number ( e . target . value ) ) ; setGpuPage ( 0 ) } }
className = "text-xs bg-zinc-900 border border-zinc-800 text-zinc-400 rounded px-1.5 py-0.5 cursor-pointer hover:border-zinc-600 transition-colors"
title = "Items per page"
>
{ [ 4 , 10 , 20 , 50 ] . map ( n = > < option key = { n } value = { n } > { n } / page < / option > ) }
< / select >
< / div >
< div className = "flex items-center gap-1" >
< button onClick = { ( ) = > setGpuPage ( 0 ) } disabled = { safeGpuPage === 0 }
className = "px-2 py-1 rounded text-xs text-zinc-600 hover:text-zinc-300 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" > « < / button >
@@ -827,7 +961,18 @@ export default function App() {
{ selectedPid !== null && (
< ProcessDetailPanel
pid = { selectedPid }
onClose = { ( ) = > setSelectedPid ( null ) }
onClose = { ( ) = > { setSelectedPid ( null ) ; setNavStack ( [ ] ) } }
onNavigate = { ( targetPid , fromName ) = > {
setNavStack ( s = > [ . . . s , { pid : selectedPid , name : fromName } ] )
setSelectedPid ( targetPid )
} }
navStack = { navStack }
onNavBack = { ( index ) = > {
const target = navStack [ index ]
setNavStack ( s = > s . slice ( 0 , index ) )
setSelectedPid ( target . pid )
} }
gpuType = { metrics ? . gpu_processes ? . find ( gp = > gp . pid === selectedPid ) ? . type }
/ >
) }
< / div >