feat: add GetManPage function and integrate parent process name in ProcessDetail

This commit is contained in:
Jonathan Atta
2026-03-11 17:26:51 +01:00
parent 9ba9b80b8b
commit c2aecae867
6 changed files with 227 additions and 35 deletions

5
app.go
View File

@@ -56,3 +56,8 @@ func (a *App) GetProcessDetail(pid int32) (*backend.ProcessDetail, error) {
func (a *App) KillProcess(pid int32, force bool) error { func (a *App) KillProcess(pid int32, force bool) error {
return backend.KillProcess(pid, force) return backend.KillProcess(pid, force)
} }
// GetManPage returns the plain-text manual page for the given command.
func (a *App) GetManPage(name string) string {
return backend.GetManPage(name)
}

View File

@@ -211,6 +211,7 @@ type ProcessDetail struct {
Username string `json:"username"` Username string `json:"username"`
CreatedAt int64 `json:"created_at"` // unix ms CreatedAt int64 `json:"created_at"` // unix ms
ParentPID int32 `json:"parent_pid"` ParentPID int32 `json:"parent_pid"`
ParentName string `json:"parent_name"`
Nice int32 `json:"nice"` Nice int32 `json:"nice"`
NumThreads int32 `json:"num_threads"` NumThreads int32 `json:"num_threads"`
NumFDs int32 `json:"num_fds"` NumFDs int32 `json:"num_fds"`
@@ -258,6 +259,13 @@ func FetchProcessDetail(pid int32) (*ProcessDetail, error) {
} }
if v, e := p.Ppid(); e == nil { if v, e := p.Ppid(); e == nil {
d.ParentPID = v d.ParentPID = v
if d.ParentPID > 0 {
if pp, err2 := ps.NewProcess(d.ParentPID); err2 == nil {
if pname, err2 := pp.Name(); err2 == nil {
d.ParentName = pname
}
}
}
} }
if v, e := p.Nice(); e == nil { if v, e := p.Nice(); e == nil {
d.Nice = v d.Nice = v
@@ -304,4 +312,30 @@ func KillProcess(pid int32, force bool) error {
return p.Kill() return p.Kill()
} }
return p.Terminate() return p.Terminate()
}
// GetManPage returns the plain-text man page for the given command name.
// Returns an empty string if man is not installed or the page does not exist.
func GetManPage(name string) string {
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "man", "-P", "cat", name)
out, err := cmd.Output()
if err != nil {
return ""
}
// Strip nroff backspace-based bold/underline formatting (char + backspace + char)
raw := []byte(out)
cleaned := make([]byte, 0, len(raw))
for i := 0; i < len(raw); i++ {
if i+1 < len(raw) && raw[i+1] == '\b' {
i++ // skip formatting char and backspace; the real char follows
} else {
cleaned = append(cleaned, raw[i])
}
}
text := strings.TrimSpace(string(cleaned))
if len(text) > 12000 {
text = text[:12000] + "\n…[truncated]"
}
return text
} }

View File

@@ -47,6 +47,7 @@ type ProcessDetail = {
username: string username: string
created_at: number created_at: number
parent_pid: number parent_pid: number
parent_name: string
nice: number nice: number
num_threads: number num_threads: number
num_fds: number num_fds: number
@@ -64,6 +65,7 @@ type ProcessDetail = {
} }
type SortField = 'cpu' | 'mem' type SortField = 'cpu' | 'mem'
type NavItem = { pid: number; name: string }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // 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 ( return (
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5" title={tooltip}>
<span className="text-[10px] font-semibold uppercase tracking-widest text-zinc-600">{label}</span> <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> <span className={cn('text-xs text-zinc-300 break-all', mono && 'font-mono')}>{value || '—'}</span>
</div> </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 ( 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-[10px] text-zinc-600 uppercase tracking-widest mb-1">{label}</p>
<p className="text-sm font-semibold text-zinc-100 tabular-nums">{value}</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>} {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({ function ProcessDetailPanel({
pid, pid,
onClose, onClose,
onNavigate,
navStack,
onNavBack,
gpuType,
}: { }: {
pid: number pid: number
onClose: () => void onClose: () => void
onNavigate: (pid: number, fromName: string) => void
navStack: NavItem[]
onNavBack: (index: number) => void
gpuType?: string
}) { }) {
const [detail, setDetail] = useState<ProcessDetail | null>(null) const [detail, setDetail] = useState<ProcessDetail | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [killing, setKilling] = useState(false) const [killing, setKilling] = useState(false)
const [killConfirm, setKillConfirm] = useState(false) const [killConfirm, setKillConfirm] = useState(false)
const [killError, setKillError] = useState<string | null>(null) 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 intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const lastManNameRef = useRef<string>('')
const load = useCallback(async () => { const load = useCallback(async () => {
try { try {
@@ -204,6 +218,17 @@ function ProcessDetailPanel({
return () => window.removeEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey)
}, [onClose]) }, [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 ( return (
<> <>
{/* Backdrop */} {/* Backdrop */}
@@ -217,10 +242,40 @@ function ProcessDetailPanel({
{/* Panel header */} {/* Panel header */}
<div className="flex items-start justify-between px-5 py-4 border-b border-zinc-800/80 shrink-0"> <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"> <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"> <div className="flex items-center gap-2 mb-1">
<span className="text-sm font-semibold text-zinc-100 truncate"> <span className="text-sm font-semibold text-zinc-100 truncate">
{detail?.name ?? `PID ${pid}`} {detail?.name ?? `PID ${pid}`}
</span> </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 && ( {detail?.status && (
<span <span
className={cn( className={cn(
@@ -315,25 +370,39 @@ function ProcessDetailPanel({
{/* Identity */} {/* Identity */}
<SectionTitle>Identity</SectionTitle> <SectionTitle>Identity</SectionTitle>
<div className="grid grid-cols-3 gap-2 mb-4"> <div className="grid grid-cols-3 gap-2 mb-4">
<StatBlock label="Parent PID" value={detail.parent_pid > 0 ? detail.parent_pid : '—'} /> {detail.parent_pid > 0 ? (
<StatBlock label="Nice / Priority" value={detail.nice} /> <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 <StatBlock
label="Created" label="Created"
value={detail.created_at ? new Date(detail.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '—'} 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} sub={detail.created_at ? fmtRelative(detail.created_at) : undefined}
tooltip="Time when this process was started"
/> />
</div> </div>
{/* Paths */} {/* Paths */}
<SectionTitle>Executable</SectionTitle> <SectionTitle>Executable</SectionTitle>
<div className="space-y-3 mb-4"> <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 <DetailRow
label="Command" label="Command"
value={detail.cmdline || detail.exe} value={detail.cmdline || detail.exe}
mono 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> </div>
{/* CPU & Memory */} {/* CPU & Memory */}
@@ -342,26 +411,31 @@ function ProcessDetailPanel({
<StatBlock <StatBlock
label="CPU" label="CPU"
value={`${detail.cpu_percent.toFixed(1)}%`} value={`${detail.cpu_percent.toFixed(1)}%`}
tooltip="CPU usage as a percentage of one core (can exceed 100% on multi-threaded processes)"
/> />
<StatBlock <StatBlock
label="Threads" label="Threads"
value={detail.num_threads} value={detail.num_threads}
sub={`${detail.num_fds} open FDs`} 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>
<div className="grid grid-cols-3 gap-2 mb-4"> <div className="grid grid-cols-3 gap-2 mb-4">
<StatBlock <StatBlock
label="RSS (physical)" label="RSS (physical)"
value={fmtBytes(detail.rss)} value={fmtBytes(detail.rss)}
tooltip="Resident Set Size — physical RAM pages currently mapped and in use by this process"
/> />
<StatBlock <StatBlock
label="VMS (virtual)" label="VMS (virtual)"
value={fmtBytes(detail.vms)} value={fmtBytes(detail.vms)}
tooltip="Virtual Memory Size — total virtual address space reserved, including shared libraries and mapped files"
/> />
<StatBlock <StatBlock
label="Swap" label="Swap"
value={fmtBytes(detail.swap)} value={fmtBytes(detail.swap)}
sub={`${detail.mem_percent.toFixed(1)}% of RAM`} 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> </div>
@@ -374,11 +448,13 @@ function ProcessDetailPanel({
label="Read" label="Read"
value={fmtBytes(detail.read_bytes)} value={fmtBytes(detail.read_bytes)}
sub={`${detail.read_ops.toLocaleString()} ops`} sub={`${detail.read_ops.toLocaleString()} ops`}
tooltip="Total bytes read from disk since process start. ops = number of read syscalls issued"
/> />
<StatBlock <StatBlock
label="Write" label="Write"
value={fmtBytes(detail.write_bytes)} value={fmtBytes(detail.write_bytes)}
sub={`${detail.write_ops.toLocaleString()} ops`} sub={`${detail.write_ops.toLocaleString()} ops`}
tooltip="Total bytes written to disk since process start. ops = number of write syscalls issued"
/> />
</div> </div>
</> </>
@@ -390,12 +466,39 @@ function ProcessDetailPanel({
<StatBlock <StatBlock
label="Open files" label="Open files"
value={detail.open_files_count} value={detail.open_files_count}
tooltip="Number of open file descriptors: regular files, sockets, pipes, and device handles"
/> />
<StatBlock <StatBlock
label="Connections" label="Connections"
value={detail.conn_count} value={detail.conn_count}
tooltip="Number of active network connections (TCP/UDP sockets) opened by this process"
/> />
</div> </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> </div>
@@ -421,9 +524,15 @@ export default function App() {
const [procOpen, setProcOpen] = useState(true) const [procOpen, setProcOpen] = useState(true)
const [gpuOpen, setGpuOpen] = useState(true) const [gpuOpen, setGpuOpen] = useState(true)
const [gpuSortBy, setGpuSortBy] = useState<'vram' | 'ram'>('vram') const [gpuSortBy, setGpuSortBy] = useState<'vram' | 'ram'>('vram')
const [navStack, setNavStack] = useState<NavItem[]>([])
const PAGE_SIZE = 20 const openDetail = useCallback((pid: number) => {
const GPU_PAGE_SIZE = 4 setSelectedPid(pid)
setNavStack([])
}, [])
const [pageSize, setPageSize] = useState(20)
const [gpuPageSize, setGpuPageSize] = useState(4)
useEffect(() => { useEffect(() => {
const handler = (m: Metrics) => setMetrics(m) const handler = (m: Metrics) => setMetrics(m)
@@ -448,16 +557,16 @@ export default function App() {
const gpuProcesses = [...(metrics?.gpu_processes ?? [])].sort((a, b) => const gpuProcesses = [...(metrics?.gpu_processes ?? [])].sort((a, b) =>
gpuSortBy === 'vram' ? b.vram_mb - a.vram_mb : b.ram - a.ram 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 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 const sortedProcesses = metrics
? [...metrics.processes].sort((a, b) => b[sortBy] - a[sortBy]) ? [...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 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 ( return (
<div className="min-h-screen bg-zinc-950 text-zinc-100 antialiased select-none"> <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"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-zinc-800/80"> <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 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">Name</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">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="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">Memory</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> </tr>
</thead> </thead>
<tbody> <tbody>
{pagedProcesses.map((p) => ( {pagedProcesses.map((p) => (
<tr <tr
key={p.pid} key={p.pid}
onClick={() => setSelectedPid(p.pid)} onClick={() => openDetail(p.pid)}
className={cn( className={cn(
'border-b border-zinc-900 transition-colors cursor-pointer', 'border-b border-zinc-900 transition-colors cursor-pointer',
selectedPid === p.pid selectedPid === p.pid
@@ -602,11 +711,21 @@ export default function App() {
</table> </table>
{/* Pagination */} {/* 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 justify-between px-5 py-3 border-t border-zinc-800/60">
<span className="text-xs text-zinc-600"> <div className="flex items-center gap-2">
{safePage * PAGE_SIZE + 1}{Math.min((safePage + 1) * PAGE_SIZE, sortedProcesses.length)} of {sortedProcesses.length} <span className="text-xs text-zinc-600">
</span> {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"> <div className="flex items-center gap-1">
<button <button
onClick={() => setPage(0)} onClick={() => setPage(0)}
@@ -746,10 +865,10 @@ export default function App() {
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr> <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 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">Type</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">VRAM</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">RAM</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> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -757,13 +876,18 @@ export default function App() {
<tr <tr
key={p.pid} key={p.pid}
className="border-t border-zinc-900 hover:bg-zinc-800/30 transition-colors cursor-pointer" 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 text-zinc-300 text-xs truncate max-w-[180px]">{p.name}</td>
<td className="py-1.5"> <td className="py-1.5">
<span <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( 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 === 'C' ? 'bg-violet-900/50 text-violet-300' :
p.type === 'G' ? 'bg-blue-900/50 text-blue-300' : p.type === 'G' ? 'bg-blue-900/50 text-blue-300' :
'bg-indigo-900/50 text-indigo-300' 'bg-indigo-900/50 text-indigo-300'
@@ -780,11 +904,21 @@ export default function App() {
</table> </table>
{/* GPU pagination */} {/* 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 justify-between pt-2 mt-1 border-t border-zinc-900">
<span className="text-xs text-zinc-600"> <div className="flex items-center gap-2">
{safeGpuPage * GPU_PAGE_SIZE + 1}{Math.min((safeGpuPage + 1) * GPU_PAGE_SIZE, gpuProcesses.length)} of {gpuProcesses.length} <span className="text-xs text-zinc-600">
</span> {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"> <div className="flex items-center gap-1">
<button onClick={() => setGpuPage(0)} disabled={safeGpuPage === 0} <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> 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 && ( {selectedPid !== null && (
<ProcessDetailPanel <ProcessDetailPanel
pid={selectedPid} 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> </div>

View File

@@ -3,6 +3,8 @@
import {backend} from '../models'; import {backend} from '../models';
import {context} from '../models'; import {context} from '../models';
export function GetManPage(arg1:string):Promise<string>;
export function GetMetrics():Promise<backend.Metrics>; export function GetMetrics():Promise<backend.Metrics>;
export function GetProcessDetail(arg1:number):Promise<backend.ProcessDetail>; export function GetProcessDetail(arg1:number):Promise<backend.ProcessDetail>;

View File

@@ -2,6 +2,10 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
export function GetManPage(arg1) {
return window['go']['main']['App']['GetManPage'](arg1);
}
export function GetMetrics() { export function GetMetrics() {
return window['go']['main']['App']['GetMetrics'](); return window['go']['main']['App']['GetMetrics']();
} }

View File

@@ -96,6 +96,7 @@ export namespace backend {
username: string; username: string;
created_at: number; created_at: number;
parent_pid: number; parent_pid: number;
parent_name: string;
nice: number; nice: number;
num_threads: number; num_threads: number;
num_fds: number; num_fds: number;
@@ -126,6 +127,7 @@ export namespace backend {
this.username = source["username"]; this.username = source["username"];
this.created_at = source["created_at"]; this.created_at = source["created_at"];
this.parent_pid = source["parent_pid"]; this.parent_pid = source["parent_pid"];
this.parent_name = source["parent_name"];
this.nice = source["nice"]; this.nice = source["nice"];
this.num_threads = source["num_threads"]; this.num_threads = source["num_threads"];
this.num_fds = source["num_fds"]; this.num_fds = source["num_fds"];