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.
- package/index.js +279 -130
- 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
|
-
//
|
|
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 } });
|
|
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
|
-
|
|
386
|
-
this.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const boxHeight = 5;
|
|
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
|
-
|
|
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
459
|
|
|
411
|
-
|
|
412
|
-
this.w.
|
|
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
|
-
|
|
425
|
-
this.w.
|
|
426
|
-
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 } });
|
|
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
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
436
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
652
|
-
this.settings.
|
|
786
|
+
case 1: // Toggle Widget 1 (CPU)
|
|
787
|
+
this.settings.showWidget1 = !this.settings.showWidget1;
|
|
788
|
+
this.recalculateLayout();
|
|
653
789
|
break;
|
|
654
|
-
case 2: // Toggle
|
|
655
|
-
this.settings.
|
|
790
|
+
case 2: // Toggle Widget 2 (Memory)
|
|
791
|
+
this.settings.showWidget2 = !this.settings.showWidget2;
|
|
792
|
+
this.recalculateLayout();
|
|
656
793
|
break;
|
|
657
|
-
case 3: // Toggle
|
|
658
|
-
this.settings.
|
|
794
|
+
case 3: // Toggle Widget 3 (GPU)
|
|
795
|
+
this.settings.showWidget3 = !this.settings.showWidget3;
|
|
796
|
+
this.recalculateLayout();
|
|
659
797
|
break;
|
|
660
|
-
case 4: //
|
|
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
|
-
|
|
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
|
|
743
|
-
|
|
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
|
|
763
|
-
|
|
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
|
|
770
|
-
|
|
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
|
|
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
|
-
|
|
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(`${
|
|
1005
|
+
this.w.memValue.setContent(`${memPercent}%`);
|
|
863
1006
|
this.w.memValue.style.fg = getColor(memPercent);
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
|
893
|
-
if (
|
|
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
|
-
|
|
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.
|
|
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 -
|
|
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
|
|
996
|
-
|
|
997
|
-
|
|
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,
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
1057
|
-
this.w.diskValue.setContent(diskText);
|
|
1207
|
+
this.w.diskValue.setContent(`${diskPercent}%`);
|
|
1058
1208
|
this.w.diskValue.style.fg = getColor(diskPercent);
|
|
1059
|
-
this.w.
|
|
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.
|
|
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';
|