@@ -34,10 +34,10 @@ function formatNumber4(n: number): string {
3434
3535function fmtMem ( bytes : number ) : string {
3636 if ( bytes == null ) return "" ;
37- if ( bytes < 1024 ) return formatNumber4 ( bytes ) + " B" ;
38- if ( bytes < 1024 * 1024 ) return formatNumber4 ( bytes / 1024 ) + " K" ;
39- if ( bytes < 1024 * 1024 * 1024 ) return formatNumber4 ( bytes / 1024 / 1024 ) + " M" ;
40- return formatNumber4 ( bytes / 1024 / 1024 / 1024 ) + " G" ;
37+ if ( bytes < 1024 ) return formatNumber4 ( bytes ) + "B" ;
38+ if ( bytes < 1024 * 1024 ) return formatNumber4 ( bytes / 1024 ) + "K" ;
39+ if ( bytes < 1024 * 1024 * 1024 ) return formatNumber4 ( bytes / 1024 / 1024 ) + "M" ;
40+ return formatNumber4 ( bytes / 1024 / 1024 / 1024 ) + "G" ;
4141}
4242
4343function fmtCpu ( cpu : number ) : string {
@@ -251,7 +251,7 @@ const Columns: ColDef[] = [
251251 { key : "command" , label : "Command" , width : "minmax(120px, 4fr)" } ,
252252 { key : "status" , label : "Status" , width : "75px" , hideOnPlatform : [ "windows" , "darwin" ] } ,
253253 { key : "user" , label : "User" , width : "80px" } ,
254- { key : "threads" , label : "NT" , tooltip : "Num Threads" , width : "55px " , align : "right" , hideOnPlatform : [ "windows" ] } ,
254+ { key : "threads" , label : "NT" , tooltip : "Num Threads" , width : "40px " , align : "right" , hideOnPlatform : [ "windows" ] } ,
255255 { key : "cpu" , label : "CPU%" , width : "70px" , align : "right" } ,
256256 { key : "mem" , label : "Memory" , width : "90px" , align : "right" } ,
257257] ;
@@ -403,6 +403,130 @@ const ProcessRow = React.memo(function ProcessRow({
403403} ) ;
404404ProcessRow . displayName = "ProcessRow" ;
405405
406+ type StatusBarProps = {
407+ model : ProcessViewerViewModel ;
408+ data : ProcessListResponse ;
409+ loading : boolean ;
410+ error : string ;
411+ wide : boolean ;
412+ } ;
413+
414+ const StatusBar = React . memo ( function StatusBar ( { model, data, loading, error, wide } : StatusBarProps ) {
415+ const totalCount = data ?. totalcount ?? 0 ;
416+ const filteredCount = data ?. filteredcount ?? 0 ;
417+ const summary = data ?. summary ;
418+ const memUsedFmt = summary ?. memused != null ? fmtMem ( summary . memused ) : null ;
419+ const memTotalFmt = summary ?. memtotal != null ? fmtMem ( summary . memtotal ) : null ;
420+ const cpuPct =
421+ summary ?. cpusum != null && summary ?. numcpu != null && summary . numcpu > 0
422+ ? ( summary . cpusum / summary . numcpu ) . toFixed ( 1 ) . padStart ( 6 , " " )
423+ : null ;
424+
425+ const procCountValue =
426+ totalCount > 0
427+ ? filteredCount < totalCount
428+ ? `${ filteredCount } / ${ totalCount } `
429+ : String ( totalCount ) . padStart ( 5 , " " )
430+ : loading
431+ ? "…"
432+ : error
433+ ? "Err"
434+ : "" ;
435+
436+ const hasSummaryLoad = summary != null && summary . load1 != null ;
437+ const hasSummaryMem = summary != null && memUsedFmt != null ;
438+ const hasSummaryCpu = summary != null && cpuPct != null ;
439+
440+ if ( wide ) {
441+ return (
442+ < div className = "shrink-0 text-xs text-secondary border-b border-white/10 bg-panel flex items-center gap-2 px-2 py-1" >
443+ < div className = "shrink-0 flex items-center" >
444+ < StatusIndicator model = { model } />
445+ </ div >
446+ { hasSummaryLoad && (
447+ < span className = "shrink-0 whitespace-pre" >
448+ Load{ " " }
449+ < span className = "font-mono text-[11px]" >
450+ { fmtLoad ( summary . load1 ) } { fmtLoad ( summary . load5 ) } { fmtLoad ( summary . load15 ) }
451+ </ span >
452+ </ span >
453+ ) }
454+ { hasSummaryMem && (
455+ < >
456+ < div className = "w-px self-stretch bg-white/10 shrink-0" />
457+ < span className = "shrink-0 whitespace-pre" >
458+ Mem{ " " }
459+ < span className = "font-mono text-[11px]" >
460+ { memUsedFmt } / { memTotalFmt }
461+ </ span >
462+ </ span >
463+ </ >
464+ ) }
465+ { hasSummaryCpu && (
466+ < >
467+ < div className = "w-px self-stretch bg-white/10 shrink-0" />
468+ < Tooltip content = { `${ summary . numcpu } cores` } placement = "bottom" >
469+ < span className = "shrink-0 cursor-default whitespace-pre" >
470+ CPU< span className = "font-mono text-[11px]" > x{ summary . numcpu } </ span > { " " }
471+ < span className = "font-mono text-[11px]" > { cpuPct } %</ span >
472+ </ span >
473+ </ Tooltip >
474+ </ >
475+ ) }
476+ < span className = "ml-auto whitespace-pre" >
477+ Procs < span className = "font-mono text-[11px]" > { procCountValue } </ span >
478+ </ span >
479+ </ div >
480+ ) ;
481+ }
482+
483+ return (
484+ < div className = "shrink-0 text-xs text-secondary border-b border-white/10 bg-panel flex items-center px-2 py-1" >
485+ < div className = "shrink-0 flex items-center mr-1" >
486+ < StatusIndicator model = { model } />
487+ </ div >
488+ < div className = "flex-1 max-w-3" />
489+ < div className = "flex flex-row flex-1 min-w-0" >
490+ { hasSummaryLoad && (
491+ < div className = "flex flex-col shrink-0 w-[100px] mr-1" >
492+ < div > Load</ div >
493+ < div className = "font-mono text-[11px] whitespace-pre" >
494+ { fmtLoad ( summary . load1 ) } { fmtLoad ( summary . load5 ) } { fmtLoad ( summary . load15 ) }
495+ </ div >
496+ </ div >
497+ ) }
498+ { hasSummaryLoad && < div className = "flex-1 max-w-3" /> }
499+ { hasSummaryMem && (
500+ < div className = "flex flex-col shrink-0 w-[95px] mr-1" >
501+ < div > Mem</ div >
502+ < div className = "font-mono text-[11px] whitespace-pre" >
503+ { memUsedFmt } / { memTotalFmt }
504+ </ div >
505+ </ div >
506+ ) }
507+ { hasSummaryMem && < div className = "flex-1 max-w-3" /> }
508+ { hasSummaryCpu && (
509+ < div className = "flex flex-col shrink-0 w-[55px] mr-1" >
510+ < Tooltip content = { `${ summary . numcpu } cores` } placement = "bottom" >
511+ < div className = "cursor-default" >
512+ CPU< span className = "font-mono text-[11px]" > x{ summary . numcpu } </ span >
513+ </ div >
514+ </ Tooltip >
515+ < div className = "font-mono text-[11px] whitespace-pre" > { cpuPct } %</ div >
516+ </ div >
517+ ) }
518+ { hasSummaryCpu && < div className = "flex-1 max-w-3" /> }
519+ < div className = "flex-1" />
520+ < div className = "flex flex-col w-[38px] shrink-0" >
521+ < div > Procs</ div >
522+ < div className = "font-mono text-[11px] whitespace-pre" > { procCountValue } </ div >
523+ </ div >
524+ </ div >
525+ </ div >
526+ ) ;
527+ } ) ;
528+ StatusBar . displayName = "StatusBar" ;
529+
406530export const ProcessViewerView : React . FC < ViewComponentProps < ProcessViewerViewModel > > = React . memo (
407531 function ProcessViewerView ( { blockId : _blockId , blockRef : _blockRef , contentRef : _contentRef , model } ) {
408532 const data = jotai . useAtomValue ( model . dataAtom ) ;
@@ -411,111 +535,66 @@ export const ProcessViewerView: React.FC<ViewComponentProps<ProcessViewerViewMod
411535 const loading = jotai . useAtomValue ( model . loadingAtom ) ;
412536 const error = jotai . useAtomValue ( model . errorAtom ) ;
413537 const scrollTop = jotai . useAtomValue ( model . scrollTopAtom ) ;
414- const scrollRef = React . useRef < HTMLDivElement > ( null ) ;
538+ const bodyScrollRef = React . useRef < HTMLDivElement > ( null ) ;
415539 const containerRef = React . useRef < HTMLDivElement > ( null ) ;
540+ const [ wide , setWide ] = React . useState ( false ) ;
416541
542+ const platform = data ?. platform ?? "" ;
543+ const startIdx = Math . max ( 0 , Math . floor ( scrollTop / RowHeight ) - OverscanRows ) ;
417544 const totalCount = data ?. totalcount ?? 0 ;
418- const filteredCount = data ?. filteredcount ?? 0 ;
419545 const processes = data ?. processes ?? [ ] ;
420546 const hasCpu = data ?. hascpu ?? false ;
421- const platform = data ?. platform ?? "" ;
422- const startIdx = Math . max ( 0 , Math . floor ( scrollTop / RowHeight ) - OverscanRows ) ;
423547
424- // track container height
425548 React . useEffect ( ( ) => {
426549 const el = containerRef . current ;
427550 if ( ! el ) return ;
428551 const ro = new ResizeObserver ( ( entries ) => {
429552 for ( const entry of entries ) {
430553 model . setContainerHeight ( entry . contentRect . height ) ;
554+ setWide ( entry . contentRect . width >= 600 ) ;
431555 }
432556 } ) ;
433557 ro . observe ( el ) ;
434558 model . setContainerHeight ( el . clientHeight ) ;
559+ setWide ( el . clientWidth >= 600 ) ;
435560 return ( ) => ro . disconnect ( ) ;
436561 } , [ model ] ) ;
437562
438563 const handleScroll = React . useCallback ( ( ) => {
439- const el = scrollRef . current ;
564+ const el = bodyScrollRef . current ;
440565 if ( ! el ) return ;
441566 model . setScrollTop ( el . scrollTop ) ;
442567 } , [ model ] ) ;
443568
444569 const totalHeight = totalCount * RowHeight ;
445570 const paddingTop = startIdx * RowHeight ;
446571
447- const summary = data ?. summary ;
448- const memUsedFmt = summary ?. memused != null ? fmtMem ( summary . memused ) : null ;
449- const memTotalFmt = summary ?. memtotal != null ? fmtMem ( summary . memtotal ) : null ;
450- const cpuPct =
451- summary ?. cpusum != null && summary ?. numcpu != null && summary . numcpu > 0
452- ? ( summary . cpusum / summary . numcpu ) . toFixed ( 1 ) . padStart ( 6 , " " )
453- : null ;
454-
455- const procCountValue =
456- totalCount > 0
457- ? filteredCount < totalCount
458- ? `${ filteredCount } / ${ totalCount } `
459- : String ( totalCount ) . padStart ( 5 , " " )
460- : loading
461- ? "…"
462- : error
463- ? "Err"
464- : "" ;
465-
466- const hasSummaryLoad = summary != null && summary . load1 != null ;
467- const hasSummaryMem = summary != null && memUsedFmt != null ;
468- const hasSummaryCpu = summary != null && cpuPct != null ;
469-
470572 return (
471573 < div className = "flex flex-col w-full h-full overflow-hidden" ref = { containerRef } >
472- { /* status bar */ }
473- < div className = "shrink-0 text-xs text-secondary border-b border-white/10 bg-panel" >
474- < div className = "flex items-center gap-4 px-2 pt-1 pb-0" >
475- < StatusIndicator model = { model } />
476- { hasSummaryLoad && < span className = "w-[120px] shrink-0" > Load</ span > }
477- { hasSummaryMem && < span className = "w-[120px] shrink-0" > Mem</ span > }
478- { hasSummaryCpu && (
479- < Tooltip content = { `${ summary . numcpu } cores` } placement = "bottom" >
480- < span className = "w-[70px] shrink-0 cursor-default" >
481- CPU< span className = "font-mono text-[11px]" > x{ summary . numcpu } </ span >
482- </ span >
483- </ Tooltip >
484- ) }
485- < span className = "ml-auto" > Procs</ span >
486- </ div >
487- < div className = "flex items-center gap-4 px-2 pb-1 pt-0" >
488- < div className = "w-4 shrink-0" />
489- { hasSummaryLoad && (
490- < span className = "font-mono text-[11px] w-[120px] shrink-0 whitespace-pre" >
491- { fmtLoad ( summary . load1 ) } { fmtLoad ( summary . load5 ) } { fmtLoad ( summary . load15 ) }
492- </ span >
493- ) }
494- { hasSummaryMem && (
495- < span className = "font-mono text-[11px] w-[120px] shrink-0 whitespace-pre" >
496- { memUsedFmt } / { memTotalFmt }
497- </ span >
498- ) }
499- { hasSummaryCpu && (
500- < span className = "font-mono text-[11px] w-[70px] shrink-0 whitespace-pre" > { cpuPct } %</ span >
501- ) }
502- < span className = "ml-auto font-mono text-[11px] whitespace-pre" > { procCountValue } </ span >
503- </ div >
504- </ div >
574+ < StatusBar model = { model } data = { data } loading = { loading } error = { error } wide = { wide } />
505575
506576 { /* error */ }
507577 { error != null && < div className = "px-3 py-2 text-xs text-error shrink-0" > { error } </ div > }
508578
509- { /* header */ }
510- < TableHeader model = { model } sortBy = { sortBy } sortDesc = { sortDesc } platform = { platform } />
511-
512- { /* virtualized rows */ }
513- < div ref = { scrollRef } className = "flex-1 overflow-y-auto overflow-x-auto" onScroll = { handleScroll } >
514- < div style = { { height : totalHeight , position : "relative" , minWidth : "100%" } } >
515- < div style = { { position : "absolute" , top : paddingTop , left : 0 , right : 0 , minWidth : "100%" } } >
516- { processes . map ( ( proc ) => (
517- < ProcessRow key = { proc . pid } proc = { proc } hasCpu = { hasCpu } platform = { platform } />
518- ) ) }
579+ { /* outer h-scroll wrapper */ }
580+ < div className = "flex-1 overflow-x-auto overflow-y-hidden" >
581+ { /* inner column — expands to header's natural width, rows match */ }
582+ < div className = "flex flex-col h-full min-w-full w-max" >
583+ < TableHeader model = { model } sortBy = { sortBy } sortDesc = { sortDesc } platform = { platform } />
584+
585+ { /* virtualized rows — same width as header, scrolls vertically */ }
586+ < div
587+ ref = { bodyScrollRef }
588+ className = "flex-1 overflow-y-auto overflow-x-hidden w-full"
589+ onScroll = { handleScroll }
590+ >
591+ < div style = { { height : totalHeight , position : "relative" } } >
592+ < div style = { { position : "absolute" , top : paddingTop , left : 0 , right : 0 } } >
593+ { processes . map ( ( proc ) => (
594+ < ProcessRow key = { proc . pid } proc = { proc } hasCpu = { hasCpu } platform = { platform } />
595+ ) ) }
596+ </ div >
597+ </ div >
519598 </ div >
520599 </ div >
521600 </ div >
0 commit comments