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.
- package/index.js +280 -128
- 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
|
-
//
|
|
383
|
-
// Logo
|
|
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
|
-
|
|
386
|
-
this.w.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
402
|
-
this.w.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
424
|
-
this.w.
|
|
425
|
-
this.w.diskValue = blessed.text({ parent: this.w.diskBox, top:
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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
|
-
|
|
435
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
651
|
-
this.settings.
|
|
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
|
|
654
|
-
this.settings.
|
|
794
|
+
case 3: // Toggle Widget 3 (GPU)
|
|
795
|
+
this.settings.showWidget3 = !this.settings.showWidget3;
|
|
796
|
+
this.recalculateLayout();
|
|
655
797
|
break;
|
|
656
|
-
case
|
|
657
|
-
this.settings.
|
|
798
|
+
case 4: // Toggle Widget 4 (Network)
|
|
799
|
+
this.settings.showWidget4 = !this.settings.showWidget4;
|
|
800
|
+
this.recalculateLayout();
|
|
658
801
|
break;
|
|
659
|
-
case
|
|
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
|
-
|
|
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
|
|
742
|
-
|
|
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
|
|
762
|
-
|
|
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
|
|
769
|
-
|
|
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
|
|
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
|
-
|
|
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(`${
|
|
1005
|
+
this.w.memValue.setContent(`${memPercent}%`);
|
|
862
1006
|
this.w.memValue.style.fg = getColor(memPercent);
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
|
892
|
-
if (
|
|
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
|
-
|
|
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 -
|
|
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
|
|
993
|
-
|
|
994
|
-
|
|
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,
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
1054
|
-
this.w.diskValue.setContent(diskText);
|
|
1207
|
+
this.w.diskValue.setContent(`${diskPercent}%`);
|
|
1055
1208
|
this.w.diskValue.style.fg = getColor(diskPercent);
|
|
1056
|
-
this.w.
|
|
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.
|
|
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';
|