claw-dashboard 1.8.4 → 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 +279 -130
  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,61 +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 } });
417
+
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 } });
384
436
 
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
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;
395
444
 
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 } } });
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
459
 
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
- this.w.sessCount = blessed.text({ parent: this.w.sessBox, top: 0, right: 2, content: '', style: { fg: C.gray } });
415
-
416
- 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 } } });
417
- this.w.sysInfoLine1 = blessed.text({ parent: this.w.sysBox, top: 0, left: 'center', content: '...', style: { fg: C.gray } });
418
- this.w.sysInfoLine2 = blessed.text({ parent: this.w.sysBox, top: 1, left: 'center', content: '', style: { fg: C.gray } });
419
-
420
- 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 } } });
421
462
  this.w.netValue = blessed.text({ parent: this.w.netBox, top: 0, left: 'center', content: 'Loading...', style: { fg: C.brightCyan, bold: true } });
422
463
  this.w.netDetail = blessed.text({ parent: this.w.netBox, top: 1, left: 'center', content: '', style: { fg: C.gray } });
423
464
 
424
- 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 } } });
425
- this.w.diskGauge = blessed.text({ parent: this.w.diskBox, top: 0, left: 'center', content: '', style: { fg: C.green } });
426
- 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 } });
469
+
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 } });
427
474
 
428
- 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 } } });
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 } } });
429
477
  this.w.uptimeSys = blessed.text({ parent: this.w.uptimeBox, top: 0, left: 'center', content: 'Sys: --', style: { fg: C.brightMagenta, bold: true } });
430
478
  this.w.uptimeClaw = blessed.text({ parent: this.w.uptimeBox, top: 1, left: 'center', content: 'Claw: --', style: { fg: C.brightMagenta, bold: true } });
479
+ }
431
480
 
432
- 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 });
433
- 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
+ });
434
541
 
435
- this.w.footer = blessed.box({ parent: this.screen, bottom: 0, left: 0, width: '100%', height: 1, style: { bg: C.black, fg: C.gray } });
436
- 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
+ }
437
556
  }
438
557
 
439
558
  setupKeys() {
@@ -443,6 +562,22 @@ class Dashboard {
443
562
  this.screen.key(['s', 'S'], () => this.toggleSettings());
444
563
  this.screen.key(['p', ' '], () => this.togglePause());
445
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();
446
581
  }
447
582
 
448
583
  cycleSessionSort() {
@@ -487,6 +622,8 @@ class Dashboard {
487
622
  ' {cyan-fg}?{/cyan-fg} or {cyan-fg}h{/cyan-fg} Toggle this help panel',
488
623
  ' {cyan-fg}s{/cyan-fg} or {cyan-fg}S{/cyan-fg} Open settings panel',
489
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
+ '',
490
627
  '{center}{gray-fg}Press ? or h to close this help{/gray-fg}{/center}'
491
628
  ].join('\n');
492
629
 
@@ -544,7 +681,7 @@ class Dashboard {
544
681
  top: 'center',
545
682
  left: 'center',
546
683
  width: 56,
547
- height: 16,
684
+ height: 18,
548
685
  border: { type: 'line' },
549
686
  style: {
550
687
  border: { fg: C.brightGreen },
@@ -571,19 +708,25 @@ class Dashboard {
571
708
  tags: true
572
709
  });
573
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
+
574
723
  this.w.settingsList = blessed.list({
575
724
  parent: this.w.settingsBox,
576
725
  top: 5,
577
726
  left: 2,
578
727
  width: 52,
579
- height: 7,
580
- items: [
581
- `Refresh Interval: ${refreshSec}s (1s/2s/5s/10s)`,
582
- `Show Network: ${this.settings.showNetwork ? 'ON' : 'OFF'}`,
583
- `Show GPU: ${this.settings.showGPU ? 'ON' : 'OFF'}`,
584
- `Show Disk: ${this.settings.showDisk ? 'ON' : 'OFF'}`,
585
- `Log Level Filter: ${this.settings.logLevelFilter.toUpperCase()}`
586
- ],
728
+ height: 9,
729
+ items: getSettingsItems(),
587
730
  style: {
588
731
  fg: C.white,
589
732
  bg: C.black,
@@ -609,15 +752,7 @@ class Dashboard {
609
752
  this.w.settingsList.on('select', (item, index) => {
610
753
  this.toggleSettingOption(index);
611
754
  // Refresh the list items
612
- const newRefreshMs = this.settings.refreshInterval;
613
- const newRefreshSec = newRefreshMs / 1000;
614
- this.w.settingsList.setItems([
615
- `Refresh Interval: ${newRefreshSec}s (1s/2s/5s/10s)`,
616
- `Show Network: ${this.settings.showNetwork ? 'ON' : 'OFF'}`,
617
- `Show GPU: ${this.settings.showGPU ? 'ON' : 'OFF'}`,
618
- `Show Disk: ${this.settings.showDisk ? 'ON' : 'OFF'}`,
619
- `Log Level Filter: ${this.settings.logLevelFilter.toUpperCase()}`
620
- ]);
755
+ this.w.settingsList.setItems(getSettingsItems());
621
756
  this.w.settingsList.select(index);
622
757
  this.screen.render();
623
758
  });
@@ -648,24 +783,42 @@ class Dashboard {
648
783
  clearInterval(this.timer);
649
784
  this.timer = setInterval(() => this.refresh(), this.settings.refreshInterval);
650
785
  break;
651
- case 1: // Toggle network
652
- this.settings.showNetwork = !this.settings.showNetwork;
786
+ case 1: // Toggle Widget 1 (CPU)
787
+ this.settings.showWidget1 = !this.settings.showWidget1;
788
+ this.recalculateLayout();
653
789
  break;
654
- case 2: // Toggle GPU
655
- this.settings.showGPU = !this.settings.showGPU;
790
+ case 2: // Toggle Widget 2 (Memory)
791
+ this.settings.showWidget2 = !this.settings.showWidget2;
792
+ this.recalculateLayout();
656
793
  break;
657
- case 3: // Toggle disk
658
- this.settings.showDisk = !this.settings.showDisk;
794
+ case 3: // Toggle Widget 3 (GPU)
795
+ this.settings.showWidget3 = !this.settings.showWidget3;
796
+ this.recalculateLayout();
659
797
  break;
660
- case 4: // Cycle log level filter: all -> debug -> info -> warn -> error -> all
798
+ case 4: // Toggle Widget 4 (Network)
799
+ this.settings.showWidget4 = !this.settings.showWidget4;
800
+ this.recalculateLayout();
801
+ break;
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
661
815
  const levels = ['all', 'debug', 'info', 'warn', 'error'];
662
816
  const currentLevel = levels.indexOf(this.settings.logLevelFilter);
663
817
  this.settings.logLevelFilter = levels[(currentLevel + 1) % levels.length];
664
818
  break;
665
819
  }
666
820
  saveSettings(this.settings);
667
- // Re-render main dashboard to apply visibility changes
668
- this.render();
821
+ this.screen.render();
669
822
  }
670
823
 
671
824
  // Fetch sessions directly from sessions.json (like openclaw CLI does)
@@ -739,10 +892,8 @@ class Dashboard {
739
892
  this.data.system = `${os.distro || 'macOS'} ${os.release} (${os.arch}) Node v${ver.node}`;
740
893
  this.data.systemUptime = time.uptime;
741
894
 
742
- // Fetch disk stats for root partition (if enabled)
743
- if (!this.settings.showDisk) {
744
- this.data.disk = null;
745
- } else try {
895
+ // Fetch disk stats for root partition
896
+ try {
746
897
  const fsSize = await si.fsSize();
747
898
  const rootFs = fsSize.find(f => f.mount === '/') || fsSize[0];
748
899
  if (rootFs) {
@@ -759,17 +910,11 @@ class Dashboard {
759
910
  this.data.disk = null;
760
911
  }
761
912
 
762
- // Fetch GPU stats (if enabled)
763
- if (this.settings.showGPU) {
764
- this.data.gpu = await getMacGPU();
765
- } else {
766
- this.data.gpu = null;
767
- }
913
+ // Fetch GPU stats
914
+ this.data.gpu = await getMacGPU();
768
915
 
769
- // Fetch network stats (if enabled)
770
- if (!this.settings.showNetwork) {
771
- this.data.network = null;
772
- } else try {
916
+ // Fetch network stats
917
+ try {
773
918
  const netStats = await si.networkStats();
774
919
  const primaryInterface = netStats.find(n => n.operstate === 'up' && !n.internal) || netStats[0];
775
920
  if (primaryInterface) {
@@ -830,12 +975,12 @@ class Dashboard {
830
975
 
831
976
  // Fetch recent logs
832
977
  try {
833
- 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 });
834
979
  const filterFn = getLogFilterFn(this.settings.logLevelFilter || 'all');
835
980
  const lines = stdout.trim().split('\n')
836
981
  .filter(line => !line.includes('plugin CLI register skipped'))
837
- .filter(line => filterFn(line))
838
- .slice(-12);
982
+ .filter(line => filterFn(line));
983
+ // Store all filtered logs - dynamic slicing happens in render()
839
984
  if (lines.length > 0 && lines[0]) {
840
985
  this.logLines = lines;
841
986
  }
@@ -855,24 +1000,14 @@ class Dashboard {
855
1000
  this.w.cpuValue.setContent(`${cpuPercent}%`);
856
1001
  this.w.cpuValue.style.fg = getColor(cpuPercent);
857
1002
  this.w.cpuDetail.setContent(`${this.data.cpu?.length || 0} cores`);
858
- this.w.cpuSpark.setContent(sparkline(this.history.cpu));
859
- this.w.cpuSpark.style.fg = cpuPercent > 60 ? C.yellow : C.cyan;
860
1003
 
861
1004
  const memPercent = this.data.memory.percent || 0;
862
- this.w.memValue.setContent(`${this.data.memory.usedGB}GB / ${this.data.memory.totalGB}GB`);
1005
+ this.w.memValue.setContent(`${memPercent}%`);
863
1006
  this.w.memValue.style.fg = getColor(memPercent);
864
- // Show cache info if significant (>1GB)
865
- const cacheInfo = this.data.memory.cachedGB > 1 ? ` (${this.data.memory.cachedGB}GB cache)` : '';
866
- this.w.memDetail.setContent(`${memPercent}% used${cacheInfo}`);
867
- this.w.memSpark.setContent(sparkline(this.history.memory));
868
- this.w.memSpark.style.fg = memPercent > 60 ? C.yellow : C.magenta;
869
-
870
- if (!this.settings.showGPU) {
871
- this.w.gpuValue.setContent('[Disabled]');
872
- this.w.gpuValue.style.fg = C.gray;
873
- this.w.gpuDetail.setContent('');
874
- this.w.gpuSpark.setContent('');
875
- } 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) {
876
1011
  this.w.gpuValue.setContent(this.data.gpu.short);
877
1012
  this.w.gpuValue.style.fg = C.brightYellow;
878
1013
  let details = [];
@@ -880,21 +1015,14 @@ class Dashboard {
880
1015
  if (this.data.gpu.frequency) details.push(`${this.data.gpu.frequency}MHz`);
881
1016
  this.w.gpuDetail.setContent(details.join(' ') || 'Apple Silicon');
882
1017
  this.w.gpuDetail.style.fg = C.gray;
883
- this.w.gpuSpark.setContent(gauge(this.data.gpu.utilization || 0, 12));
884
- this.w.gpuSpark.style.fg = C.yellow;
885
1018
  } else {
886
1019
  this.w.gpuValue.setContent('Not Detected');
887
1020
  this.w.gpuValue.style.fg = C.gray;
888
1021
  this.w.gpuDetail.setContent('');
889
- this.w.gpuSpark.setContent('');
890
1022
  }
891
1023
 
892
- // Render network widget (compact version in bottom row)
893
- if (!this.settings.showNetwork) {
894
- this.w.netValue.setContent('[Disabled]');
895
- this.w.netValue.style.fg = C.gray;
896
- this.w.netDetail.setContent('');
897
- } else if (this.data.network) {
1024
+ // Render network widget
1025
+ if (this.data.network) {
898
1026
  const rxStr = formatBitsPerSecond(this.data.network.rxSec);
899
1027
  const txStr = formatBitsPerSecond(this.data.network.txSec);
900
1028
  const netText = `▼${rxStr} ▲${txStr}`;
@@ -935,7 +1063,10 @@ class Dashboard {
935
1063
  }
936
1064
  });
937
1065
 
938
- 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 => {
939
1070
  // Calculate idle time
940
1071
  const idleMs = s.updatedAt ? Date.now() - s.updatedAt : 0;
941
1072
 
@@ -981,20 +1112,45 @@ class Dashboard {
981
1112
 
982
1113
  return `${statusStr} ${agentName} ${model} ${context} ${idle} ${channel}`;
983
1114
  });
984
- this.w.sessList.setContent(lines.join('\n'));
985
- this.w.sessCount.setContent(`${this.data.sessions.length} sessions`);
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}`);
986
1119
  } else {
987
1120
  this.w.sessList.setContent('No active sessions');
988
1121
  this.w.sessCount.setContent('0 sessions');
989
1122
  }
990
1123
 
991
- // Update logs - colorize by level and filter
1124
+ // Update logs - dynamically fill available space with wrapping calculation
992
1125
  if (this.logLines.length) {
993
1126
  const filter = this.settings.logLevelFilter || 'all';
994
1127
  const filterFn = getLogFilterFn(filter);
995
- const coloredLines = this.logLines
996
- .filter(line => filterFn(line))
997
- .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));
998
1154
  this.w.logContent.setContent(coloredLines.join('\n'));
999
1155
  } else {
1000
1156
  this.w.logContent.setContent('No log output');
@@ -1027,7 +1183,7 @@ class Dashboard {
1027
1183
  }
1028
1184
  this.w.title.setContent(`Dashboard ${DASHBOARD_VERSION}, ${openclawText}`);
1029
1185
 
1030
- // Update clock - show current local time, with PAUSED indicator to the left
1186
+ // Update clock - show current local time, PAUSED indicator on the right
1031
1187
  const now = new Date();
1032
1188
  const timeStr = now.toLocaleTimeString('en-US', {
1033
1189
  hour: '2-digit',
@@ -1040,29 +1196,22 @@ class Dashboard {
1040
1196
  day: 'numeric'
1041
1197
  });
1042
1198
  if (this.isPaused) {
1043
- this.w.clock.setContent(`{yellow-fg}[PAUSED]{/yellow-fg} ${timeStr} ${dateStr}`);
1199
+ this.w.clock.setContent(`${timeStr} ${dateStr} {yellow-fg}[PAUSED]{/yellow-fg}`);
1044
1200
  } else {
1045
1201
  this.w.clock.setContent(`${timeStr} ${dateStr}`);
1046
1202
  }
1047
1203
 
1048
1204
  // Render disk widget
1049
- if (!this.settings.showDisk) {
1050
- this.w.diskValue.setContent('[Disabled]');
1051
- this.w.diskValue.style.fg = C.gray;
1052
- this.w.diskGauge.setContent('');
1053
- this.w.diskBox.style.border.fg = C.gray;
1054
- } else if (this.data.disk) {
1205
+ if (this.data.disk) {
1055
1206
  const diskPercent = this.data.disk.percent || 0;
1056
- const diskText = `${this.data.disk.usedGB}GB / ${this.data.disk.totalGB}GB`;
1057
- this.w.diskValue.setContent(diskText);
1207
+ this.w.diskValue.setContent(`${diskPercent}%`);
1058
1208
  this.w.diskValue.style.fg = getColor(diskPercent);
1059
- this.w.diskGauge.setContent(gauge(diskPercent, 10));
1060
- this.w.diskGauge.style.fg = getColor(diskPercent);
1209
+ this.w.diskDetail.setContent(`${this.data.disk.usedGB}/${this.data.disk.totalGB}`);
1061
1210
  this.w.diskBox.style.border.fg = getColor(diskPercent);
1062
1211
  } else {
1063
1212
  this.w.diskValue.setContent('No disk info');
1064
1213
  this.w.diskValue.style.fg = C.gray;
1065
- this.w.diskGauge.setContent('');
1214
+ this.w.diskDetail.setContent('');
1066
1215
  }
1067
1216
 
1068
1217
  // Render uptime widget - Sys on line 1, Claw on line 2
@@ -1089,7 +1238,7 @@ class Dashboard {
1089
1238
  const refreshSec = Math.round(this.settings.refreshInterval / 1000);
1090
1239
  const pauseIndicator = this.isPaused ? '▶ running' : 'p pause';
1091
1240
  const sortMode = this.settings.sessionSortMode;
1092
- 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`);
1093
1242
 
1094
1243
  // Update session box label to show sort mode
1095
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.4",
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": {