claw-dashboard 1.8.3 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +280 -128
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -32,11 +32,16 @@ const SETTINGS_PATH = process.env.HOME + '/.openclaw/dashboard-settings.json';
32
32
 
33
33
  const DEFAULT_SETTINGS = {
34
34
  refreshInterval: DEFAULT_REFRESH_INTERVAL,
35
- showNetwork: true,
36
- showGPU: true,
37
- showDisk: true,
38
35
  logLevelFilter: 'all',
39
- sessionSortMode: 'time' // 'time' | 'tokens' | 'idle' | 'name'
36
+ sessionSortMode: 'time', // 'time' | 'tokens' | 'idle' | 'name'
37
+ // Widget visibility toggles (1-7 keys) - all 7 small widgets
38
+ showWidget1: true, // CPU
39
+ showWidget2: true, // Memory
40
+ showWidget3: true, // GPU
41
+ showWidget4: true, // Network
42
+ showWidget5: true, // Disk
43
+ showWidget6: true, // System
44
+ showWidget7: true, // Uptime
40
45
  };
41
46
 
42
47
  function loadSettings() {
@@ -182,6 +187,24 @@ function getLogFilterFn(filter) {
182
187
  };
183
188
  }
184
189
 
190
+ // Calculate how many lines a text will wrap to given a width
191
+ function calculateWrappedLines(text, width) {
192
+ if (!text || width <= 0) return 1;
193
+ const words = text.split(' ');
194
+ let lines = 1;
195
+ let currentLineLength = 0;
196
+
197
+ for (const word of words) {
198
+ if (currentLineLength + word.length + 1 > width) {
199
+ lines++;
200
+ currentLineLength = word.length;
201
+ } else {
202
+ currentLineLength += word.length + 1;
203
+ }
204
+ }
205
+ return lines;
206
+ }
207
+
185
208
  const ASCII_LOGO = [
186
209
  ' ██████╗██╗ █████╗ ██╗ ██╗ ',
187
210
  ' ██╔════╝██║ ██╔══██╗██║ ██║ ',
@@ -379,60 +402,157 @@ class Dashboard {
379
402
  createWidgets() {
380
403
  this.w = {};
381
404
 
382
- // Header area: logo on left, 3 stat boxes in a horizontal row on right
383
- // Logo is ~39 chars wide, dashboard version + clawbot version stacked under
405
+ // COMPACT HEADER LAYOUT:
406
+ // Row 0-5: Logo on left (40 cols), widgets flow on right
407
+ // Row 6: Title line below logo
408
+ // Row 7+: Sessions, then logs
409
+
410
+ const LOGO_WIDTH = 40;
411
+
412
+ // Logo on left side of header
413
+ this.w.logo = blessed.text({ parent: this.screen, top: 2, left: 1, width: LOGO_WIDTH, content: ASCII_LOGO.join('\n'), style: { fg: C.brightCyan, bold: true } });
414
+
415
+ // Title below logo (spans full width)
416
+ this.w.title = blessed.text({ parent: this.screen, top: 8, left: 3, content: `Dashboard ${DASHBOARD_VERSION}, openclaw checking...`, style: { fg: C.brightWhite, bold: true } });
384
417
 
385
- this.w.logo = blessed.text({ parent: this.screen, top: 0, left: 1, width: 40, content: ASCII_LOGO.join('\n'), style: { fg: C.brightCyan, bold: true } });
386
- this.w.title = blessed.text({ parent: this.screen, top: 6, left: 3, content: `Dashboard ${DASHBOARD_VERSION}, openclaw checking...`, style: { fg: C.brightWhite, bold: true } });
387
- this.w.clock = blessed.text({ parent: this.screen, top: 0, left: '100%-30', content: '--:--', style: { fg: C.brightCyan, bold: true }, align: 'right', tags: true });
388
-
389
- // 3 stat boxes in a horizontal row
390
- // Fixed positioning: logo ends ~col 42, remaining space split evenly
391
- const boxHeight = 5; // removed blank row at bottom
392
- const startCol = 42;
393
- const boxWidth = 32; // wider to prevent wrapping
394
- const boxTop = 1; // moved down one line
418
+ // Clock positioned at top-left corner
419
+ this.w.clock = blessed.text({ parent: this.screen, top: 0, left: 0, width: 26, content: '--:--', style: { fg: C.brightCyan, bold: true }, align: 'left', tags: true });
420
+
421
+ // All 7 small widgets: positioned to the RIGHT of logo in header area
422
+ this.createWidgetBoxes();
423
+
424
+ // Sessions always below header area (row 10), height 9 to span rows 10-18
425
+ this.w.sessBox = blessed.box({ parent: this.screen, left: 0, width: '100%', height: 9, border: { type: 'line' }, label: ' SESSIONS ', style: { border: { fg: C.blue } }, tags: true, overflow: 'hidden', scrollable: false });
426
+ this.w.sessHeader = blessed.text({ parent: this.w.sessBox, top: 0, left: 1, width: '98%', content: 'STATUS AGENT MODEL CONTEXT IDLE CHAN', style: { fg: C.brightWhite, bold: true }, overflow: 'hidden' });
427
+ this.w.sessList = blessed.text({ parent: this.w.sessBox, top: 1, left: 1, width: '98%', height: 6, content: '', style: { fg: C.white }, tags: true, overflow: 'hidden', scrollable: false });
428
+ this.w.sessCount = blessed.text({ parent: this.w.sessBox, top: 0, right: 2, content: '', style: { fg: C.gray } });
429
+
430
+ // Logs always below sessions
431
+ this.w.logBox = blessed.box({ parent: this.screen, left: 0, width: '100%', height: 19, border: { type: 'line' }, label: ' OPENCLAW LOGS ', style: { border: { fg: C.cyan } }, scrollable: true, alwaysScroll: true });
432
+ this.w.logContent = blessed.text({ parent: this.w.logBox, top: 0, left: 1, width: '95%-2', content: 'Loading logs...', style: { fg: C.gray }, tags: true });
433
+
434
+ this.w.footer = blessed.box({ parent: this.screen, bottom: 0, left: 0, width: '100%', height: 1, style: { bg: C.black, fg: C.gray } });
435
+ this.w.footerText = blessed.text({ parent: this.w.footer, top: 0, left: 'center', content: '', style: { fg: C.gray } });
395
436
 
396
- this.w.cpuBox = blessed.box({ parent: this.screen, top: boxTop, left: startCol, width: boxWidth, height: boxHeight, border: { type: 'line' }, label: ' CPU ', style: { border: { fg: C.cyan } } });
437
+ // Initial layout calculation
438
+ this.recalculateLayout();
439
+ }
440
+
441
+ // Create the 7 widget boxes (always created, visibility toggled)
442
+ createWidgetBoxes() {
443
+ const boxHeight = 5;
444
+
445
+ // Widget 1: CPU (priority)
446
+ this.w.cpuBox = blessed.box({ parent: this.screen, height: boxHeight, border: { type: 'line' }, label: ' CPU ', style: { border: { fg: C.cyan } } });
397
447
  this.w.cpuValue = blessed.text({ parent: this.w.cpuBox, top: 0, left: 'center', content: '0%', style: { fg: C.brightGreen, bold: true } });
398
448
  this.w.cpuDetail = blessed.text({ parent: this.w.cpuBox, top: 1, left: 'center', content: '', style: { fg: C.gray } });
399
- this.w.cpuSpark = blessed.text({ parent: this.w.cpuBox, top: 2, left: 'center', content: sparkline(this.history.cpu), style: { fg: C.cyan } });
400
449
 
401
- this.w.memBox = blessed.box({ parent: this.screen, top: boxTop, left: startCol + boxWidth, width: boxWidth, height: boxHeight, border: { type: 'line' }, label: ' MEMORY ', style: { border: { fg: C.magenta } } });
402
- this.w.memValue = blessed.text({ parent: this.w.memBox, top: 0, left: 'center', content: '0GB', style: { fg: C.brightMagenta, bold: true } });
450
+ // Widget 2: MEMORY (priority)
451
+ this.w.memBox = blessed.box({ parent: this.screen, height: boxHeight, border: { type: 'line' }, label: ' MEMORY ', style: { border: { fg: C.magenta } } });
452
+ this.w.memValue = blessed.text({ parent: this.w.memBox, top: 0, left: 'center', content: '0%', style: { fg: C.brightMagenta, bold: true } });
403
453
  this.w.memDetail = blessed.text({ parent: this.w.memBox, top: 1, left: 'center', content: '', style: { fg: C.gray } });
404
- this.w.memSpark = blessed.text({ parent: this.w.memBox, top: 2, left: 'center', content: sparkline(this.history.memory), style: { fg: C.magenta } });
405
454
 
406
- this.w.gpuBox = blessed.box({ parent: this.screen, top: boxTop, left: startCol + boxWidth * 2, width: boxWidth, height: boxHeight, border: { type: 'line' }, label: ' GPU ', style: { border: { fg: C.yellow } } });
455
+ // Widget 3: GPU (priority)
456
+ this.w.gpuBox = blessed.box({ parent: this.screen, height: boxHeight, border: { type: 'line' }, label: ' GPU ', style: { border: { fg: C.yellow } } });
407
457
  this.w.gpuValue = blessed.text({ parent: this.w.gpuBox, top: 0, left: 'center', content: 'Detecting...', style: { fg: C.brightYellow, bold: true } });
408
458
  this.w.gpuDetail = blessed.text({ parent: this.w.gpuBox, top: 1, left: 'center', content: '', style: { fg: C.gray } });
409
- this.w.gpuSpark = blessed.text({ parent: this.w.gpuBox, top: 2, left: 'center', content: '', style: { fg: C.yellow } });
410
-
411
- this.w.sessBox = blessed.box({ parent: this.screen, top: 8, left: 0, width: '100%', height: 10, border: { type: 'line' }, label: ' SESSIONS ', style: { border: { fg: C.blue } }, tags: true });
412
- this.w.sessHeader = blessed.text({ parent: this.w.sessBox, top: 0, left: 1, content: 'STATUS AGENT MODEL CONTEXT IDLE CHAN', style: { fg: C.brightWhite, bold: true } });
413
- this.w.sessList = blessed.text({ parent: this.w.sessBox, top: 1, left: 1, width: '98%', height: 7, content: '', style: { fg: C.white }, tags: true });
414
-
415
- this.w.sysBox = blessed.box({ parent: this.screen, top: 18, left: 0, width: '25%', height: 4, border: { type: 'line' }, label: ' SYSTEM ', style: { border: { fg: C.gray } } });
416
- this.w.sysInfoLine1 = blessed.text({ parent: this.w.sysBox, top: 0, left: 'center', content: '...', style: { fg: C.gray } });
417
- this.w.sysInfoLine2 = blessed.text({ parent: this.w.sysBox, top: 1, left: 'center', content: '', style: { fg: C.gray } });
418
459
 
419
- this.w.netBox = blessed.box({ parent: this.screen, top: 18, left: '25%', width: '25%', height: 4, border: { type: 'line' }, label: ' NETWORK ', style: { border: { fg: C.brightCyan } } });
460
+ // Widget 4: NETWORK
461
+ this.w.netBox = blessed.box({ parent: this.screen, height: boxHeight, border: { type: 'line' }, label: ' NETWORK ', style: { border: { fg: C.brightCyan } } });
420
462
  this.w.netValue = blessed.text({ parent: this.w.netBox, top: 0, left: 'center', content: 'Loading...', style: { fg: C.brightCyan, bold: true } });
421
463
  this.w.netDetail = blessed.text({ parent: this.w.netBox, top: 1, left: 'center', content: '', style: { fg: C.gray } });
422
464
 
423
- this.w.diskBox = blessed.box({ parent: this.screen, top: 18, left: '50%', width: '25%', height: 4, border: { type: 'line' }, label: ' DISK ', style: { border: { fg: C.green } } });
424
- this.w.diskGauge = blessed.text({ parent: this.w.diskBox, top: 0, left: 'center', content: '', style: { fg: C.green } });
425
- this.w.diskValue = blessed.text({ parent: this.w.diskBox, top: 1, left: 'center', content: 'Loading...', style: { fg: C.brightGreen, bold: true } });
465
+ // Widget 5: DISK
466
+ this.w.diskBox = blessed.box({ parent: this.screen, height: boxHeight, border: { type: 'line' }, label: ' DISK ', style: { border: { fg: C.green } } });
467
+ this.w.diskValue = blessed.text({ parent: this.w.diskBox, top: 0, left: 'center', content: '0%', style: { fg: C.brightGreen, bold: true } });
468
+ this.w.diskDetail = blessed.text({ parent: this.w.diskBox, top: 1, left: 'center', content: '', style: { fg: C.gray } });
426
469
 
427
- this.w.uptimeBox = blessed.box({ parent: this.screen, top: 18, left: '75%', width: '25%', height: 4, border: { type: 'line' }, label: ' UPTIME ', style: { border: { fg: C.brightMagenta } } });
470
+ // Widget 6: SYSTEM
471
+ this.w.sysBox = blessed.box({ parent: this.screen, height: boxHeight, border: { type: 'line' }, label: ' SYSTEM ', style: { border: { fg: C.gray } } });
472
+ this.w.sysInfoLine1 = blessed.text({ parent: this.w.sysBox, top: 0, left: 'center', content: '...', style: { fg: C.gray } });
473
+ this.w.sysInfoLine2 = blessed.text({ parent: this.w.sysBox, top: 1, left: 'center', content: '', style: { fg: C.gray } });
474
+
475
+ // Widget 7: UPTIME
476
+ this.w.uptimeBox = blessed.box({ parent: this.screen, height: boxHeight, border: { type: 'line' }, label: ' UPTIME ', style: { border: { fg: C.brightMagenta } } });
428
477
  this.w.uptimeSys = blessed.text({ parent: this.w.uptimeBox, top: 0, left: 'center', content: 'Sys: --', style: { fg: C.brightMagenta, bold: true } });
429
478
  this.w.uptimeClaw = blessed.text({ parent: this.w.uptimeBox, top: 1, left: 'center', content: 'Claw: --', style: { fg: C.brightMagenta, bold: true } });
479
+ }
430
480
 
431
- this.w.logBox = blessed.box({ parent: this.screen, top: 22, left: 0, width: '100%', height: '100%-23', border: { type: 'line' }, label: ' OPENCLAW LOGS ', style: { border: { fg: C.cyan } }, scrollable: true, alwaysScroll: true });
432
- this.w.logContent = blessed.text({ parent: this.w.logBox, top: 0, left: 1, width: '95%-2', content: 'Loading logs...', style: { fg: C.gray }, tags: true });
481
+ // Recalculate layout positions - COMPACT DESIGN
482
+ // Widgets flow to the right of logo in header area (rows 0-5)
483
+ // Sessions below at row 7, logs below sessions
484
+ recalculateLayout() {
485
+ const boxHeight = 5;
486
+ const LOGO_COLS = 42; // Logo takes roughly 42 cols on left
487
+ const HEADER_ROWS = 10; // Clock moved to top-left, sessions start at row 10
488
+ const SESSIONS_HEIGHT = 9; // rows 10-18 inclusive = 9 rows to ensure bottom border visible
489
+
490
+ // Determine which widgets are visible
491
+ const widgets = [
492
+ { name: 'cpu', box: this.w.cpuBox, visible: this.settings.showWidget1 },
493
+ { name: 'mem', box: this.w.memBox, visible: this.settings.showWidget2 },
494
+ { name: 'gpu', box: this.w.gpuBox, visible: this.settings.showWidget3 },
495
+ { name: 'net', box: this.w.netBox, visible: this.settings.showWidget4 },
496
+ { name: 'disk', box: this.w.diskBox, visible: this.settings.showWidget5 },
497
+ { name: 'sys', box: this.w.sysBox, visible: this.settings.showWidget6 },
498
+ { name: 'uptime', box: this.w.uptimeBox, visible: this.settings.showWidget7 },
499
+ ];
500
+
501
+ const visibleWidgets = widgets.filter(w => w.visible);
502
+ const numVisible = visibleWidgets.length;
503
+
504
+ if (numVisible === 0) {
505
+ // All widgets hidden - position sessions at top
506
+ this.w.sessBox.position = { top: HEADER_ROWS };
507
+ this.w.sessBox.height = SESSIONS_HEIGHT;
508
+ const logTop = Math.max(19, HEADER_ROWS + SESSIONS_HEIGHT); // Sessions is now 9 rows, ensure min 19
509
+ this.w.logBox.position = { top: logTop };
510
+ this.w.logBox.height = '100%-' + (logTop + 1); // -1 for footer
511
+ } else {
512
+ // BALANCED LAYOUT: Split visible widgets evenly between 2 rows
513
+ // Algorithm: row1Count = Math.ceil(visibleCount / 2), row2Count = visibleCount - row1Count
514
+ // 5 widgets -> 3 on top, 2 on bottom
515
+ // 4 widgets -> 2 on top, 2 on bottom
516
+ // 3 widgets -> 2 on top, 1 on bottom
517
+ // 6 widgets -> 3 on top, 3 on bottom
518
+
519
+ const row1Count = Math.ceil(numVisible / 2);
520
+ const row2Count = numVisible - row1Count;
521
+
522
+ // Calculate width percentage for each widget
523
+ // Available space is roughly (100% - logo offset)
524
+ // Logo is about 42 chars wide in ~120 char terminal = ~35%
525
+ const logoWidthPercent = 35;
526
+ const availablePercent = 100 - logoWidthPercent;
527
+
528
+ visibleWidgets.forEach((widget, index) => {
529
+ const row = index < row1Count ? 0 : 1;
530
+ const colInRow = row === 0 ? index : index - row1Count;
531
+ const widgetsInThisRow = row === 0 ? row1Count : row2Count;
532
+
533
+ const widthPercent = Math.floor(availablePercent / widgetsInThisRow);
534
+ const leftPercent = logoWidthPercent + (colInRow * widthPercent);
535
+
536
+ widget.box.top = row * boxHeight;
537
+ widget.box.left = leftPercent + '%';
538
+ widget.box.width = widthPercent + '%';
539
+ widget.box.show();
540
+ });
433
541
 
434
- this.w.footer = blessed.box({ parent: this.screen, bottom: 0, left: 0, width: '100%', height: 1, style: { bg: C.black, fg: C.gray } });
435
- this.w.footerText = blessed.text({ parent: this.w.footer, top: 0, left: 'center', content: '', style: { fg: C.gray } });
542
+ // Hide invisible widgets
543
+ widgets.filter(w => !w.visible).forEach(widget => {
544
+ widget.box.hide();
545
+ });
546
+
547
+ // Position sessions below header area (row 10), spanning rows 10-18 (height 9)
548
+ this.w.sessBox.position = { top: HEADER_ROWS };
549
+ this.w.sessBox.height = SESSIONS_HEIGHT;
550
+
551
+ // Position logs below sessions (minimum row 19, fill remaining space, account for footer)
552
+ const logTop = Math.max(19, HEADER_ROWS + SESSIONS_HEIGHT); // Sessions is 9 rows, ensure min 19
553
+ this.w.logBox.position = { top: logTop };
554
+ this.w.logBox.height = '100%-' + (logTop + 1); // -1 for footer
555
+ }
436
556
  }
437
557
 
438
558
  setupKeys() {
@@ -442,6 +562,22 @@ class Dashboard {
442
562
  this.screen.key(['s', 'S'], () => this.toggleSettings());
443
563
  this.screen.key(['p', ' '], () => this.togglePause());
444
564
  this.screen.key('o', () => this.cycleSessionSort());
565
+
566
+ // Widget toggle keys 1-7
567
+ this.screen.key('1', () => this.toggleWidget('showWidget1'));
568
+ this.screen.key('2', () => this.toggleWidget('showWidget2'));
569
+ this.screen.key('3', () => this.toggleWidget('showWidget3'));
570
+ this.screen.key('4', () => this.toggleWidget('showWidget4'));
571
+ this.screen.key('5', () => this.toggleWidget('showWidget5'));
572
+ this.screen.key('6', () => this.toggleWidget('showWidget6'));
573
+ this.screen.key('7', () => this.toggleWidget('showWidget7'));
574
+ }
575
+
576
+ toggleWidget(settingKey) {
577
+ this.settings[settingKey] = !this.settings[settingKey];
578
+ saveSettings(this.settings);
579
+ this.recalculateLayout();
580
+ this.screen.render();
445
581
  }
446
582
 
447
583
  cycleSessionSort() {
@@ -486,6 +622,8 @@ class Dashboard {
486
622
  ' {cyan-fg}?{/cyan-fg} or {cyan-fg}h{/cyan-fg} Toggle this help panel',
487
623
  ' {cyan-fg}s{/cyan-fg} or {cyan-fg}S{/cyan-fg} Open settings panel',
488
624
  '',
625
+ ' {cyan-fg}1-7{/cyan-fg} Toggle widgets (1:CPU 2:MEM 3:GPU 4:NET 5:DISK 6:SYS 7:UP)',
626
+ '',
489
627
  '{center}{gray-fg}Press ? or h to close this help{/gray-fg}{/center}'
490
628
  ].join('\n');
491
629
 
@@ -543,7 +681,7 @@ class Dashboard {
543
681
  top: 'center',
544
682
  left: 'center',
545
683
  width: 56,
546
- height: 16,
684
+ height: 18,
547
685
  border: { type: 'line' },
548
686
  style: {
549
687
  border: { fg: C.brightGreen },
@@ -570,19 +708,25 @@ class Dashboard {
570
708
  tags: true
571
709
  });
572
710
 
711
+ const getSettingsItems = () => [
712
+ `Refresh Interval: ${refreshSec}s (1s/2s/5s/10s)`,
713
+ `1 CPU: ${this.settings.showWidget1 ? 'ON' : 'OFF'}`,
714
+ `2 Memory: ${this.settings.showWidget2 ? 'ON' : 'OFF'}`,
715
+ `3 GPU: ${this.settings.showWidget3 ? 'ON' : 'OFF'}`,
716
+ `4 Network: ${this.settings.showWidget4 ? 'ON' : 'OFF'}`,
717
+ `5 Disk: ${this.settings.showWidget5 ? 'ON' : 'OFF'}`,
718
+ `6 System: ${this.settings.showWidget6 ? 'ON' : 'OFF'}`,
719
+ `7 Uptime: ${this.settings.showWidget7 ? 'ON' : 'OFF'}`,
720
+ `Log Level Filter: ${this.settings.logLevelFilter.toUpperCase()}`
721
+ ];
722
+
573
723
  this.w.settingsList = blessed.list({
574
724
  parent: this.w.settingsBox,
575
725
  top: 5,
576
726
  left: 2,
577
727
  width: 52,
578
- height: 7,
579
- items: [
580
- `Refresh Interval: ${refreshSec}s (1s/2s/5s/10s)`,
581
- `Show Network: ${this.settings.showNetwork ? 'ON' : 'OFF'}`,
582
- `Show GPU: ${this.settings.showGPU ? 'ON' : 'OFF'}`,
583
- `Show Disk: ${this.settings.showDisk ? 'ON' : 'OFF'}`,
584
- `Log Level Filter: ${this.settings.logLevelFilter.toUpperCase()}`
585
- ],
728
+ height: 9,
729
+ items: getSettingsItems(),
586
730
  style: {
587
731
  fg: C.white,
588
732
  bg: C.black,
@@ -608,15 +752,7 @@ class Dashboard {
608
752
  this.w.settingsList.on('select', (item, index) => {
609
753
  this.toggleSettingOption(index);
610
754
  // Refresh the list items
611
- const newRefreshMs = this.settings.refreshInterval;
612
- const newRefreshSec = newRefreshMs / 1000;
613
- this.w.settingsList.setItems([
614
- `Refresh Interval: ${newRefreshSec}s (1s/2s/5s/10s)`,
615
- `Show Network: ${this.settings.showNetwork ? 'ON' : 'OFF'}`,
616
- `Show GPU: ${this.settings.showGPU ? 'ON' : 'OFF'}`,
617
- `Show Disk: ${this.settings.showDisk ? 'ON' : 'OFF'}`,
618
- `Log Level Filter: ${this.settings.logLevelFilter.toUpperCase()}`
619
- ]);
755
+ this.w.settingsList.setItems(getSettingsItems());
620
756
  this.w.settingsList.select(index);
621
757
  this.screen.render();
622
758
  });
@@ -647,24 +783,42 @@ class Dashboard {
647
783
  clearInterval(this.timer);
648
784
  this.timer = setInterval(() => this.refresh(), this.settings.refreshInterval);
649
785
  break;
650
- case 1: // Toggle network
651
- this.settings.showNetwork = !this.settings.showNetwork;
786
+ case 1: // Toggle Widget 1 (CPU)
787
+ this.settings.showWidget1 = !this.settings.showWidget1;
788
+ this.recalculateLayout();
789
+ break;
790
+ case 2: // Toggle Widget 2 (Memory)
791
+ this.settings.showWidget2 = !this.settings.showWidget2;
792
+ this.recalculateLayout();
652
793
  break;
653
- case 2: // Toggle GPU
654
- this.settings.showGPU = !this.settings.showGPU;
794
+ case 3: // Toggle Widget 3 (GPU)
795
+ this.settings.showWidget3 = !this.settings.showWidget3;
796
+ this.recalculateLayout();
655
797
  break;
656
- case 3: // Toggle disk
657
- this.settings.showDisk = !this.settings.showDisk;
798
+ case 4: // Toggle Widget 4 (Network)
799
+ this.settings.showWidget4 = !this.settings.showWidget4;
800
+ this.recalculateLayout();
658
801
  break;
659
- case 4: // Cycle log level filter: all -> debug -> info -> warn -> error -> all
802
+ case 5: // Toggle Widget 5 (Disk)
803
+ this.settings.showWidget5 = !this.settings.showWidget5;
804
+ this.recalculateLayout();
805
+ break;
806
+ case 6: // Toggle Widget 6 (System)
807
+ this.settings.showWidget6 = !this.settings.showWidget6;
808
+ this.recalculateLayout();
809
+ break;
810
+ case 7: // Toggle Widget 7 (Uptime)
811
+ this.settings.showWidget7 = !this.settings.showWidget7;
812
+ this.recalculateLayout();
813
+ break;
814
+ case 8: // Cycle log level filter: all -> debug -> info -> warn -> error -> all
660
815
  const levels = ['all', 'debug', 'info', 'warn', 'error'];
661
816
  const currentLevel = levels.indexOf(this.settings.logLevelFilter);
662
817
  this.settings.logLevelFilter = levels[(currentLevel + 1) % levels.length];
663
818
  break;
664
819
  }
665
820
  saveSettings(this.settings);
666
- // Re-render main dashboard to apply visibility changes
667
- this.render();
821
+ this.screen.render();
668
822
  }
669
823
 
670
824
  // Fetch sessions directly from sessions.json (like openclaw CLI does)
@@ -738,10 +892,8 @@ class Dashboard {
738
892
  this.data.system = `${os.distro || 'macOS'} ${os.release} (${os.arch}) Node v${ver.node}`;
739
893
  this.data.systemUptime = time.uptime;
740
894
 
741
- // Fetch disk stats for root partition (if enabled)
742
- if (!this.settings.showDisk) {
743
- this.data.disk = null;
744
- } else try {
895
+ // Fetch disk stats for root partition
896
+ try {
745
897
  const fsSize = await si.fsSize();
746
898
  const rootFs = fsSize.find(f => f.mount === '/') || fsSize[0];
747
899
  if (rootFs) {
@@ -758,17 +910,11 @@ class Dashboard {
758
910
  this.data.disk = null;
759
911
  }
760
912
 
761
- // Fetch GPU stats (if enabled)
762
- if (this.settings.showGPU) {
763
- this.data.gpu = await getMacGPU();
764
- } else {
765
- this.data.gpu = null;
766
- }
913
+ // Fetch GPU stats
914
+ this.data.gpu = await getMacGPU();
767
915
 
768
- // Fetch network stats (if enabled)
769
- if (!this.settings.showNetwork) {
770
- this.data.network = null;
771
- } else try {
916
+ // Fetch network stats
917
+ try {
772
918
  const netStats = await si.networkStats();
773
919
  const primaryInterface = netStats.find(n => n.operstate === 'up' && !n.internal) || netStats[0];
774
920
  if (primaryInterface) {
@@ -829,12 +975,12 @@ class Dashboard {
829
975
 
830
976
  // Fetch recent logs
831
977
  try {
832
- const { stdout } = await execAsync('openclaw logs --limit 100 --plain 2>/dev/null', { timeout: 5000 });
978
+ const { stdout } = await execAsync('openclaw logs --limit 200 --plain 2>/dev/null', { timeout: 5000 });
833
979
  const filterFn = getLogFilterFn(this.settings.logLevelFilter || 'all');
834
980
  const lines = stdout.trim().split('\n')
835
981
  .filter(line => !line.includes('plugin CLI register skipped'))
836
- .filter(line => filterFn(line))
837
- .slice(-12);
982
+ .filter(line => filterFn(line));
983
+ // Store all filtered logs - dynamic slicing happens in render()
838
984
  if (lines.length > 0 && lines[0]) {
839
985
  this.logLines = lines;
840
986
  }
@@ -854,24 +1000,14 @@ class Dashboard {
854
1000
  this.w.cpuValue.setContent(`${cpuPercent}%`);
855
1001
  this.w.cpuValue.style.fg = getColor(cpuPercent);
856
1002
  this.w.cpuDetail.setContent(`${this.data.cpu?.length || 0} cores`);
857
- this.w.cpuSpark.setContent(sparkline(this.history.cpu));
858
- this.w.cpuSpark.style.fg = cpuPercent > 60 ? C.yellow : C.cyan;
859
1003
 
860
1004
  const memPercent = this.data.memory.percent || 0;
861
- this.w.memValue.setContent(`${this.data.memory.usedGB}GB / ${this.data.memory.totalGB}GB`);
1005
+ this.w.memValue.setContent(`${memPercent}%`);
862
1006
  this.w.memValue.style.fg = getColor(memPercent);
863
- // Show cache info if significant (>1GB)
864
- const cacheInfo = this.data.memory.cachedGB > 1 ? ` (${this.data.memory.cachedGB}GB cache)` : '';
865
- this.w.memDetail.setContent(`${memPercent}% used${cacheInfo}`);
866
- this.w.memSpark.setContent(sparkline(this.history.memory));
867
- this.w.memSpark.style.fg = memPercent > 60 ? C.yellow : C.magenta;
868
-
869
- if (!this.settings.showGPU) {
870
- this.w.gpuValue.setContent('[Disabled]');
871
- this.w.gpuValue.style.fg = C.gray;
872
- this.w.gpuDetail.setContent('');
873
- this.w.gpuSpark.setContent('');
874
- } else if (this.data.gpu) {
1007
+ this.w.memDetail.setContent(`${this.data.memory.usedGB}/${this.data.memory.totalGB}`);
1008
+
1009
+ // GPU widget content
1010
+ if (this.data.gpu) {
875
1011
  this.w.gpuValue.setContent(this.data.gpu.short);
876
1012
  this.w.gpuValue.style.fg = C.brightYellow;
877
1013
  let details = [];
@@ -879,21 +1015,14 @@ class Dashboard {
879
1015
  if (this.data.gpu.frequency) details.push(`${this.data.gpu.frequency}MHz`);
880
1016
  this.w.gpuDetail.setContent(details.join(' ') || 'Apple Silicon');
881
1017
  this.w.gpuDetail.style.fg = C.gray;
882
- this.w.gpuSpark.setContent(gauge(this.data.gpu.utilization || 0, 12));
883
- this.w.gpuSpark.style.fg = C.yellow;
884
1018
  } else {
885
1019
  this.w.gpuValue.setContent('Not Detected');
886
1020
  this.w.gpuValue.style.fg = C.gray;
887
1021
  this.w.gpuDetail.setContent('');
888
- this.w.gpuSpark.setContent('');
889
1022
  }
890
1023
 
891
- // Render network widget (compact version in bottom row)
892
- if (!this.settings.showNetwork) {
893
- this.w.netValue.setContent('[Disabled]');
894
- this.w.netValue.style.fg = C.gray;
895
- this.w.netDetail.setContent('');
896
- } else if (this.data.network) {
1024
+ // Render network widget
1025
+ if (this.data.network) {
897
1026
  const rxStr = formatBitsPerSecond(this.data.network.rxSec);
898
1027
  const txStr = formatBitsPerSecond(this.data.network.txSec);
899
1028
  const netText = `▼${rxStr} ▲${txStr}`;
@@ -934,7 +1063,10 @@ class Dashboard {
934
1063
  }
935
1064
  });
936
1065
 
937
- const lines = sortedSessions.map(s => {
1066
+ // Show 6 sessions within 9-row box (header + 6 lines + footer/border)
1067
+ const displaySessions = sortedSessions.slice(0, 6);
1068
+
1069
+ const lines = displaySessions.map(s => {
938
1070
  // Calculate idle time
939
1071
  const idleMs = s.updatedAt ? Date.now() - s.updatedAt : 0;
940
1072
 
@@ -980,18 +1112,45 @@ class Dashboard {
980
1112
 
981
1113
  return `${statusStr} ${agentName} ${model} ${context} ${idle} ${channel}`;
982
1114
  });
983
- this.w.sessList.setContent(lines.join('\n'));
1115
+ this.w.sessList.setContent(lines.join('\n').replace(/\n$/, ''));
1116
+ const totalCount = this.data.sessions.length;
1117
+ const showingCount = displaySessions.length;
1118
+ this.w.sessCount.setContent(showingCount < totalCount ? `${showingCount}/${totalCount}` : `${totalCount}`);
984
1119
  } else {
985
1120
  this.w.sessList.setContent('No active sessions');
1121
+ this.w.sessCount.setContent('0 sessions');
986
1122
  }
987
1123
 
988
- // Update logs - colorize by level and filter
1124
+ // Update logs - dynamically fill available space with wrapping calculation
989
1125
  if (this.logLines.length) {
990
1126
  const filter = this.settings.logLevelFilter || 'all';
991
1127
  const filterFn = getLogFilterFn(filter);
992
- const coloredLines = this.logLines
993
- .filter(line => filterFn(line))
994
- .map(line => colorizeLogLine(line));
1128
+ const filteredLogs = this.logLines.filter(line => filterFn(line));
1129
+
1130
+ // Calculate available space for logs
1131
+ const logHeight = this.w.logBox.height || 15;
1132
+ const logWidth = (this.w.logBox.width || 80) - 4; // account for borders/padding
1133
+ const availableLines = Math.max(1, logHeight - 2); // subtract header/border space
1134
+
1135
+ // Fill from bottom (latest first) accounting for wrapped lines
1136
+ let usedLines = 0;
1137
+ const logsToShow = [];
1138
+
1139
+ // Iterate from end (newest) backwards
1140
+ for (let i = filteredLogs.length - 1; i >= 0; i--) {
1141
+ const log = filteredLogs[i];
1142
+ const lineCount = calculateWrappedLines(log, logWidth);
1143
+
1144
+ if (usedLines + lineCount <= availableLines) {
1145
+ logsToShow.unshift(log); // add to beginning (oldest of shown)
1146
+ usedLines += lineCount;
1147
+ } else {
1148
+ break; // no more space
1149
+ }
1150
+ }
1151
+
1152
+ // Colorize and display
1153
+ const coloredLines = logsToShow.map(line => colorizeLogLine(line));
995
1154
  this.w.logContent.setContent(coloredLines.join('\n'));
996
1155
  } else {
997
1156
  this.w.logContent.setContent('No log output');
@@ -1024,7 +1183,7 @@ class Dashboard {
1024
1183
  }
1025
1184
  this.w.title.setContent(`Dashboard ${DASHBOARD_VERSION}, ${openclawText}`);
1026
1185
 
1027
- // Update clock - show current local time, with PAUSED indicator to the left
1186
+ // Update clock - show current local time, PAUSED indicator on the right
1028
1187
  const now = new Date();
1029
1188
  const timeStr = now.toLocaleTimeString('en-US', {
1030
1189
  hour: '2-digit',
@@ -1037,29 +1196,22 @@ class Dashboard {
1037
1196
  day: 'numeric'
1038
1197
  });
1039
1198
  if (this.isPaused) {
1040
- this.w.clock.setContent(`{yellow-fg}[PAUSED]{/yellow-fg} ${timeStr} ${dateStr}`);
1199
+ this.w.clock.setContent(`${timeStr} ${dateStr} {yellow-fg}[PAUSED]{/yellow-fg}`);
1041
1200
  } else {
1042
1201
  this.w.clock.setContent(`${timeStr} ${dateStr}`);
1043
1202
  }
1044
1203
 
1045
1204
  // Render disk widget
1046
- if (!this.settings.showDisk) {
1047
- this.w.diskValue.setContent('[Disabled]');
1048
- this.w.diskValue.style.fg = C.gray;
1049
- this.w.diskGauge.setContent('');
1050
- this.w.diskBox.style.border.fg = C.gray;
1051
- } else if (this.data.disk) {
1205
+ if (this.data.disk) {
1052
1206
  const diskPercent = this.data.disk.percent || 0;
1053
- const diskText = `${this.data.disk.usedGB}GB / ${this.data.disk.totalGB}GB`;
1054
- this.w.diskValue.setContent(diskText);
1207
+ this.w.diskValue.setContent(`${diskPercent}%`);
1055
1208
  this.w.diskValue.style.fg = getColor(diskPercent);
1056
- this.w.diskGauge.setContent(gauge(diskPercent, 10));
1057
- this.w.diskGauge.style.fg = getColor(diskPercent);
1209
+ this.w.diskDetail.setContent(`${this.data.disk.usedGB}/${this.data.disk.totalGB}`);
1058
1210
  this.w.diskBox.style.border.fg = getColor(diskPercent);
1059
1211
  } else {
1060
1212
  this.w.diskValue.setContent('No disk info');
1061
1213
  this.w.diskValue.style.fg = C.gray;
1062
- this.w.diskGauge.setContent('');
1214
+ this.w.diskDetail.setContent('');
1063
1215
  }
1064
1216
 
1065
1217
  // Render uptime widget - Sys on line 1, Claw on line 2
@@ -1086,7 +1238,7 @@ class Dashboard {
1086
1238
  const refreshSec = Math.round(this.settings.refreshInterval / 1000);
1087
1239
  const pauseIndicator = this.isPaused ? '▶ running' : 'p pause';
1088
1240
  const sortMode = this.settings.sessionSortMode;
1089
- this.w.footerText.setContent(`q quit r refresh ${pauseIndicator} o sort:${sortMode} ? help s settings • ${refreshSec}s refresh`);
1241
+ this.w.footerText.setContent(`q quit r refresh ${pauseIndicator} o sort:${sortMode} 1-7 toggle ? help s settings • ${refreshSec}s refresh`);
1090
1242
 
1091
1243
  // Update session box label to show sort mode
1092
1244
  const sortLabel = sortMode === 'time' ? 'TIME' : sortMode === 'tokens' ? 'TOKENS' : sortMode === 'idle' ? 'IDLE' : 'NAME';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-dashboard",
3
- "version": "1.8.3",
3
+ "version": "1.9.0",
4
4
  "description": "A beautiful console dashboard for monitoring OpenClaw instances — inspired by htop/btop/mactop",
5
5
  "main": "index.js",
6
6
  "bin": {