genbox 1.0.212 → 1.0.213
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.
|
@@ -275,9 +275,9 @@ function applyFilters(sessions, filters) {
|
|
|
275
275
|
/**
|
|
276
276
|
* Render TUI header with metrics
|
|
277
277
|
*/
|
|
278
|
-
function renderHeader(metrics, filters) {
|
|
278
|
+
function renderHeader(metrics, filters, termWidth) {
|
|
279
279
|
const timestamp = new Date().toLocaleTimeString();
|
|
280
|
-
|
|
280
|
+
const lineWidth = Math.min(termWidth - 2, 120);
|
|
281
281
|
console.log(chalk_1.default.bold.inverse(' GENBOX SESSION MONITOR ') +
|
|
282
282
|
chalk_1.default.dim(` ↻ ${timestamp}`));
|
|
283
283
|
// Metrics bar
|
|
@@ -311,12 +311,15 @@ function renderHeader(metrics, filters) {
|
|
|
311
311
|
if (activeFilters.length > 0) {
|
|
312
312
|
console.log(chalk_1.default.yellow(' Filters: ') + chalk_1.default.dim(activeFilters.join(', ')));
|
|
313
313
|
}
|
|
314
|
-
console.log(chalk_1.default.dim('─'.repeat(
|
|
314
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
315
315
|
}
|
|
316
316
|
/**
|
|
317
317
|
* Render session table
|
|
318
318
|
*/
|
|
319
|
-
function renderTable(sessions, selectedIndex) {
|
|
319
|
+
function renderTable(sessions, selectedIndex, termHeight, termWidth) {
|
|
320
|
+
const lineWidth = Math.min(termWidth - 2, 120);
|
|
321
|
+
// Calculate max rows we can show (leave room for header ~6 lines, preview ~8 lines, footer ~2 lines)
|
|
322
|
+
const maxRows = Math.max(3, termHeight - 18);
|
|
320
323
|
// Header
|
|
321
324
|
console.log(chalk_1.default.dim(' ') +
|
|
322
325
|
chalk_1.default.bold(' '.padEnd(2)) +
|
|
@@ -327,15 +330,31 @@ function renderTable(sessions, selectedIndex) {
|
|
|
327
330
|
chalk_1.default.bold('STATE'.padEnd(16)) +
|
|
328
331
|
chalk_1.default.bold('TIME'.padEnd(8)) +
|
|
329
332
|
chalk_1.default.bold('TOKENS'));
|
|
330
|
-
console.log(chalk_1.default.dim('─'.repeat(
|
|
333
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
331
334
|
if (sessions.length === 0) {
|
|
332
335
|
console.log(chalk_1.default.dim('\n No sessions match your filters.\n'));
|
|
333
336
|
console.log(chalk_1.default.dim(' Start a session with: ') + chalk_1.default.cyan('gb claude -p -b "your prompt"'));
|
|
334
337
|
console.log(chalk_1.default.dim(' Or clear filters with: ') + chalk_1.default.cyan('[c]'));
|
|
335
338
|
return;
|
|
336
339
|
}
|
|
340
|
+
// Calculate visible range around selected index
|
|
341
|
+
let startIndex = 0;
|
|
342
|
+
let endIndex = Math.min(sessions.length, maxRows);
|
|
343
|
+
if (sessions.length > maxRows) {
|
|
344
|
+
// Keep selected item in view
|
|
345
|
+
const halfView = Math.floor(maxRows / 2);
|
|
346
|
+
startIndex = Math.max(0, selectedIndex - halfView);
|
|
347
|
+
endIndex = Math.min(sessions.length, startIndex + maxRows);
|
|
348
|
+
if (endIndex === sessions.length) {
|
|
349
|
+
startIndex = Math.max(0, endIndex - maxRows);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Show scroll indicator if needed
|
|
353
|
+
if (startIndex > 0) {
|
|
354
|
+
console.log(chalk_1.default.dim(` ↑ ${startIndex} more above`));
|
|
355
|
+
}
|
|
337
356
|
// Rows
|
|
338
|
-
for (let i =
|
|
357
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
339
358
|
const session = sessions[i];
|
|
340
359
|
const isSelected = i === selectedIndex;
|
|
341
360
|
const prefix = isSelected ? chalk_1.default.cyan('▶ ') : ' ';
|
|
@@ -368,17 +387,22 @@ function renderTable(sessions, selectedIndex) {
|
|
|
368
387
|
tokensStr;
|
|
369
388
|
console.log(rowContent);
|
|
370
389
|
}
|
|
390
|
+
// Show scroll indicator if needed
|
|
391
|
+
if (endIndex < sessions.length) {
|
|
392
|
+
console.log(chalk_1.default.dim(` ↓ ${sessions.length - endIndex} more below`));
|
|
393
|
+
}
|
|
371
394
|
}
|
|
372
395
|
/**
|
|
373
396
|
* Render session detail panel
|
|
374
397
|
*/
|
|
375
|
-
function renderPreview(session) {
|
|
376
|
-
|
|
398
|
+
function renderPreview(session, termWidth) {
|
|
399
|
+
const lineWidth = Math.min(termWidth - 2, 120);
|
|
400
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
377
401
|
if (!session) {
|
|
378
|
-
console.log(chalk_1.default.dim('
|
|
402
|
+
console.log(chalk_1.default.dim(' Use ↑↓ to select a session'));
|
|
379
403
|
return;
|
|
380
404
|
}
|
|
381
|
-
console.log(chalk_1.default.bold(
|
|
405
|
+
console.log(chalk_1.default.bold(` ${session.name}`) + chalk_1.default.dim(` (${session.id})`));
|
|
382
406
|
const details = [];
|
|
383
407
|
details.push(`Provider: ${session.provider}`);
|
|
384
408
|
details.push(`Type: ${session.type}`);
|
|
@@ -416,14 +440,15 @@ function renderPreview(session) {
|
|
|
416
440
|
console.log(left + right);
|
|
417
441
|
}
|
|
418
442
|
if (session.lastMessage) {
|
|
419
|
-
console.log(chalk_1.default.dim(
|
|
443
|
+
console.log(chalk_1.default.dim(` Last: "${chalk_1.default.italic(session.lastMessage)}..."`));
|
|
420
444
|
}
|
|
421
445
|
}
|
|
422
446
|
/**
|
|
423
447
|
* Render help footer
|
|
424
448
|
*/
|
|
425
|
-
function renderFooter() {
|
|
426
|
-
|
|
449
|
+
function renderFooter(termWidth) {
|
|
450
|
+
const lineWidth = Math.min(termWidth - 2, 120);
|
|
451
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
427
452
|
const controls = [
|
|
428
453
|
'[q] Quit',
|
|
429
454
|
'[r] Refresh',
|
|
@@ -452,63 +477,109 @@ async function runTuiMode(options) {
|
|
|
452
477
|
status: options.status,
|
|
453
478
|
genbox: options.genbox,
|
|
454
479
|
};
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
480
|
+
// Lock for data fetching (not UI rendering)
|
|
481
|
+
let isFetching = false;
|
|
482
|
+
// Enter alternate screen buffer (like vim/htop)
|
|
483
|
+
process.stdout.write('\x1B[?1049h');
|
|
484
|
+
// Hide cursor
|
|
485
|
+
process.stdout.write('\x1B[?25l');
|
|
486
|
+
// Cache for session data - only refresh on interval or manual refresh
|
|
487
|
+
let allSessionsCache = []; // Unfiltered data
|
|
488
|
+
let cachedSessions = []; // Filtered data for display
|
|
489
|
+
let cachedMetrics = {
|
|
490
|
+
totalSessions: 0,
|
|
491
|
+
activeSessions: 0,
|
|
492
|
+
byProvider: {},
|
|
493
|
+
byStatus: {},
|
|
494
|
+
totalTokens: 0,
|
|
495
|
+
totalCost: 0,
|
|
496
|
+
avgDuration: '--',
|
|
497
|
+
};
|
|
498
|
+
// Apply filters to cached data (fast, no network)
|
|
499
|
+
const reapplyFilters = () => {
|
|
500
|
+
cachedSessions = applyFilters(allSessionsCache, filters);
|
|
501
|
+
lastSessions = cachedSessions;
|
|
502
|
+
cachedMetrics = calculateMetrics(cachedSessions);
|
|
503
|
+
if (selectedIndex >= cachedSessions.length) {
|
|
504
|
+
selectedIndex = Math.max(0, cachedSessions.length - 1);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
// Fetch fresh data from APIs
|
|
508
|
+
const refreshData = async () => {
|
|
509
|
+
allSessionsCache = await collectSessionStates({});
|
|
510
|
+
reapplyFilters();
|
|
511
|
+
};
|
|
512
|
+
// Render UI using cached data (fast)
|
|
513
|
+
const renderUI = () => {
|
|
514
|
+
const termHeight = process.stdout.rows || 24;
|
|
515
|
+
const termWidth = process.stdout.columns || 80;
|
|
516
|
+
// Move cursor to home and clear screen
|
|
517
|
+
process.stdout.write('\x1B[H\x1B[J');
|
|
518
|
+
renderHeader(cachedMetrics, filters, termWidth);
|
|
519
|
+
renderTable(cachedSessions, selectedIndex, termHeight, termWidth);
|
|
520
|
+
renderPreview(cachedSessions[selectedIndex], termWidth);
|
|
521
|
+
renderFooter(termWidth);
|
|
522
|
+
};
|
|
523
|
+
// Quick render: just redraw UI with cached data (instant, for navigation)
|
|
524
|
+
const quickRender = () => {
|
|
525
|
+
// Clamp selected index
|
|
526
|
+
if (selectedIndex >= cachedSessions.length) {
|
|
527
|
+
selectedIndex = Math.max(0, cachedSessions.length - 1);
|
|
528
|
+
}
|
|
529
|
+
renderUI();
|
|
530
|
+
// Show input prompt if in input mode
|
|
531
|
+
if (inputMode === 'search') {
|
|
532
|
+
console.log(chalk_1.default.cyan(`\n Search: ${inputBuffer}_`));
|
|
533
|
+
}
|
|
534
|
+
else if (inputMode === 'filter_provider') {
|
|
535
|
+
console.log(chalk_1.default.cyan(`\n Filter provider [claude/gemini/codex]: ${inputBuffer}_`));
|
|
536
|
+
}
|
|
537
|
+
else if (inputMode === 'filter_status') {
|
|
538
|
+
console.log(chalk_1.default.cyan(`\n Filter status [running/stopped]: ${inputBuffer}_`));
|
|
539
|
+
}
|
|
540
|
+
else if (inputMode === 'filter_genbox') {
|
|
541
|
+
console.log(chalk_1.default.cyan(`\n Filter genbox: ${inputBuffer}_`));
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
// Full refresh: fetch data in background, then render (for interval/manual refresh)
|
|
461
545
|
const render = async () => {
|
|
546
|
+
// Skip if already fetching data
|
|
547
|
+
if (isFetching)
|
|
548
|
+
return;
|
|
549
|
+
isFetching = true;
|
|
462
550
|
try {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
lastSessions = sessions;
|
|
466
|
-
// Clamp selected index
|
|
467
|
-
if (selectedIndex >= sessions.length) {
|
|
468
|
-
selectedIndex = Math.max(0, sessions.length - 1);
|
|
469
|
-
}
|
|
470
|
-
const metrics = calculateMetrics(sessions);
|
|
471
|
-
renderHeader(metrics, filters);
|
|
472
|
-
renderTable(sessions, selectedIndex);
|
|
473
|
-
renderPreview(sessions[selectedIndex]);
|
|
474
|
-
renderFooter();
|
|
475
|
-
// Show input prompt if in input mode
|
|
476
|
-
if (inputMode === 'search') {
|
|
477
|
-
console.log(chalk_1.default.cyan(`\n Search: ${inputBuffer}_`));
|
|
478
|
-
}
|
|
479
|
-
else if (inputMode === 'filter_provider') {
|
|
480
|
-
console.log(chalk_1.default.cyan(`\n Filter provider [claude/gemini/codex]: ${inputBuffer}_`));
|
|
481
|
-
}
|
|
482
|
-
else if (inputMode === 'filter_status') {
|
|
483
|
-
console.log(chalk_1.default.cyan(`\n Filter status [running/stopped]: ${inputBuffer}_`));
|
|
484
|
-
}
|
|
485
|
-
else if (inputMode === 'filter_genbox') {
|
|
486
|
-
console.log(chalk_1.default.cyan(`\n Filter genbox: ${inputBuffer}_`));
|
|
487
|
-
}
|
|
551
|
+
await refreshData();
|
|
552
|
+
quickRender();
|
|
488
553
|
}
|
|
489
554
|
catch (error) {
|
|
490
|
-
|
|
555
|
+
process.stdout.write('\x1B[H\x1B[J');
|
|
491
556
|
console.log(chalk_1.default.red(`\nError: ${error.message}`));
|
|
492
557
|
console.log(chalk_1.default.dim('\nPress any key to retry, q to quit'));
|
|
493
558
|
}
|
|
559
|
+
finally {
|
|
560
|
+
isFetching = false;
|
|
561
|
+
}
|
|
494
562
|
};
|
|
495
563
|
const cleanup = () => {
|
|
496
564
|
if (process.stdin.isTTY) {
|
|
497
565
|
process.stdin.setRawMode(false);
|
|
498
566
|
}
|
|
499
|
-
|
|
500
|
-
|
|
567
|
+
// Show cursor
|
|
568
|
+
process.stdout.write('\x1B[?25h');
|
|
569
|
+
// Exit alternate screen buffer (restores previous screen content)
|
|
570
|
+
process.stdout.write('\x1B[?1049l');
|
|
571
|
+
console.log(chalk_1.default.dim('Exited session watch.\n'));
|
|
501
572
|
};
|
|
502
573
|
const handleInput = async (key) => {
|
|
503
|
-
// Handle input modes
|
|
574
|
+
// Handle input modes (search, filter)
|
|
504
575
|
if (inputMode !== 'normal') {
|
|
505
|
-
if (key === '\u001b') { // Escape
|
|
576
|
+
if (key === '\u001b') { // Escape - cancel input
|
|
506
577
|
inputMode = 'normal';
|
|
507
578
|
inputBuffer = '';
|
|
508
|
-
|
|
579
|
+
quickRender();
|
|
509
580
|
return;
|
|
510
581
|
}
|
|
511
|
-
else if (key === '\r') { // Enter
|
|
582
|
+
else if (key === '\r') { // Enter - apply filter
|
|
512
583
|
if (inputMode === 'search') {
|
|
513
584
|
filters.searchQuery = inputBuffer || undefined;
|
|
514
585
|
}
|
|
@@ -524,17 +595,19 @@ async function runTuiMode(options) {
|
|
|
524
595
|
inputMode = 'normal';
|
|
525
596
|
inputBuffer = '';
|
|
526
597
|
selectedIndex = 0;
|
|
527
|
-
|
|
598
|
+
// Re-apply filters to cached data (instant, no network)
|
|
599
|
+
reapplyFilters();
|
|
600
|
+
quickRender();
|
|
528
601
|
return;
|
|
529
602
|
}
|
|
530
603
|
else if (key === '\u007f') { // Backspace
|
|
531
604
|
inputBuffer = inputBuffer.slice(0, -1);
|
|
532
|
-
|
|
605
|
+
quickRender();
|
|
533
606
|
return;
|
|
534
607
|
}
|
|
535
608
|
else if (key.length === 1 && key >= ' ') {
|
|
536
609
|
inputBuffer += key;
|
|
537
|
-
|
|
610
|
+
quickRender();
|
|
538
611
|
return;
|
|
539
612
|
}
|
|
540
613
|
return;
|
|
@@ -545,16 +618,16 @@ async function runTuiMode(options) {
|
|
|
545
618
|
cleanup();
|
|
546
619
|
process.exit(0);
|
|
547
620
|
}
|
|
548
|
-
else if (key === 'r') { // Refresh
|
|
621
|
+
else if (key === 'r') { // Refresh - full data refresh
|
|
549
622
|
await render();
|
|
550
623
|
}
|
|
551
|
-
else if (key === '\u001b[A') { // Up arrow
|
|
624
|
+
else if (key === '\u001b[A') { // Up arrow - quick navigation
|
|
552
625
|
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
553
|
-
|
|
626
|
+
quickRender();
|
|
554
627
|
}
|
|
555
|
-
else if (key === '\u001b[B') { // Down arrow
|
|
628
|
+
else if (key === '\u001b[B') { // Down arrow - quick navigation
|
|
556
629
|
selectedIndex = Math.min(lastSessions.length - 1, selectedIndex + 1);
|
|
557
|
-
|
|
630
|
+
quickRender();
|
|
558
631
|
}
|
|
559
632
|
else if (key === 'a') { // Attach
|
|
560
633
|
const session = lastSessions[selectedIndex];
|
|
@@ -601,28 +674,28 @@ async function runTuiMode(options) {
|
|
|
601
674
|
else if (key === '/') { // Search
|
|
602
675
|
inputMode = 'search';
|
|
603
676
|
inputBuffer = filters.searchQuery || '';
|
|
604
|
-
|
|
677
|
+
quickRender();
|
|
605
678
|
}
|
|
606
679
|
else if (key === 'f') { // Filter menu
|
|
607
680
|
// Cycle through filter modes
|
|
608
681
|
inputMode = 'filter_provider';
|
|
609
682
|
inputBuffer = filters.provider || '';
|
|
610
|
-
|
|
683
|
+
quickRender();
|
|
611
684
|
}
|
|
612
685
|
else if (key === 'p') { // Quick filter by provider
|
|
613
686
|
inputMode = 'filter_provider';
|
|
614
687
|
inputBuffer = '';
|
|
615
|
-
|
|
688
|
+
quickRender();
|
|
616
689
|
}
|
|
617
690
|
else if (key === 't') { // Quick filter by status
|
|
618
691
|
inputMode = 'filter_status';
|
|
619
692
|
inputBuffer = '';
|
|
620
|
-
|
|
693
|
+
quickRender();
|
|
621
694
|
}
|
|
622
695
|
else if (key === 'g') { // Quick filter by genbox
|
|
623
696
|
inputMode = 'filter_genbox';
|
|
624
697
|
inputBuffer = '';
|
|
625
|
-
|
|
698
|
+
quickRender();
|
|
626
699
|
}
|
|
627
700
|
else if (key === 'c') { // Clear all filters
|
|
628
701
|
filters.provider = undefined;
|
|
@@ -630,19 +703,42 @@ async function runTuiMode(options) {
|
|
|
630
703
|
filters.genbox = undefined;
|
|
631
704
|
filters.searchQuery = undefined;
|
|
632
705
|
selectedIndex = 0;
|
|
633
|
-
|
|
706
|
+
reapplyFilters();
|
|
707
|
+
quickRender();
|
|
634
708
|
}
|
|
635
709
|
};
|
|
636
|
-
|
|
710
|
+
// Setup keyboard input FIRST
|
|
711
|
+
if (process.stdin.isTTY) {
|
|
712
|
+
process.stdin.setRawMode(true);
|
|
713
|
+
process.stdin.resume();
|
|
714
|
+
process.stdin.setEncoding('utf8');
|
|
715
|
+
process.stdin.on('data', handleInput);
|
|
716
|
+
}
|
|
637
717
|
// Initial render
|
|
638
718
|
await render();
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
719
|
+
// Setup refresh interval (event-loop friendly)
|
|
720
|
+
const refreshInterval = setInterval(async () => {
|
|
642
721
|
if (running && inputMode === 'normal') {
|
|
643
722
|
await render();
|
|
644
723
|
}
|
|
645
|
-
}
|
|
724
|
+
}, intervalMs);
|
|
725
|
+
// Keep process alive and handle cleanup
|
|
726
|
+
process.on('SIGINT', () => {
|
|
727
|
+
running = false;
|
|
728
|
+
clearInterval(refreshInterval);
|
|
729
|
+
cleanup();
|
|
730
|
+
process.exit(0);
|
|
731
|
+
});
|
|
732
|
+
// Wait for running to become false
|
|
733
|
+
await new Promise((resolve) => {
|
|
734
|
+
const checkRunning = setInterval(() => {
|
|
735
|
+
if (!running) {
|
|
736
|
+
clearInterval(checkRunning);
|
|
737
|
+
clearInterval(refreshInterval);
|
|
738
|
+
resolve();
|
|
739
|
+
}
|
|
740
|
+
}, 100);
|
|
741
|
+
});
|
|
646
742
|
}
|
|
647
743
|
/**
|
|
648
744
|
* Run JSON snapshot mode with metrics
|
|
@@ -52,6 +52,8 @@ const path = __importStar(require("path"));
|
|
|
52
52
|
const os = __importStar(require("os"));
|
|
53
53
|
const fs = __importStar(require("fs"));
|
|
54
54
|
const child_process_1 = require("child_process");
|
|
55
|
+
const util_1 = require("util");
|
|
56
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
55
57
|
const unified_session_manager_1 = require("./unified-session-manager");
|
|
56
58
|
const api_1 = require("../../api");
|
|
57
59
|
/**
|
|
@@ -99,14 +101,15 @@ async function scanRemoteSessions(genboxes) {
|
|
|
99
101
|
const keyPath = getPrivateSshKey();
|
|
100
102
|
if (!keyPath)
|
|
101
103
|
return sessions;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
// Scan all genboxes in parallel for faster results
|
|
105
|
+
const scanPromises = genboxes
|
|
106
|
+
.filter(g => g.ipAddress && g.status === 'running')
|
|
107
|
+
.map(async (genbox) => {
|
|
105
108
|
try {
|
|
106
109
|
const remoteSocketDir = '/home/dev/.genbox/sockets';
|
|
107
|
-
const
|
|
108
|
-
const lines =
|
|
109
|
-
|
|
110
|
+
const { stdout } = await execAsync(`ssh -i "${keyPath}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=2 -o LogLevel=ERROR dev@${genbox.ipAddress} "ls -1 ${remoteSocketDir}/*.sock 2>/dev/null" 2>/dev/null`, { timeout: 4000 });
|
|
111
|
+
const lines = stdout.trim().split('\n').filter(l => l.includes('.sock'));
|
|
112
|
+
return lines.map(line => {
|
|
110
113
|
const sessionName = path.basename(line, '.sock');
|
|
111
114
|
// Determine provider from session name
|
|
112
115
|
let provider = 'claude';
|
|
@@ -116,20 +119,22 @@ async function scanRemoteSessions(genboxes) {
|
|
|
116
119
|
else if (sessionName.toLowerCase().includes('codex')) {
|
|
117
120
|
provider = 'codex';
|
|
118
121
|
}
|
|
119
|
-
|
|
122
|
+
return {
|
|
120
123
|
name: sessionName,
|
|
121
124
|
provider,
|
|
122
125
|
genboxName: genbox.name,
|
|
123
126
|
genboxId: genbox.id,
|
|
124
127
|
ipAddress: genbox.ipAddress,
|
|
125
|
-
}
|
|
126
|
-
}
|
|
128
|
+
};
|
|
129
|
+
});
|
|
127
130
|
}
|
|
128
131
|
catch {
|
|
129
132
|
// Can't connect or no sessions on this genbox
|
|
133
|
+
return [];
|
|
130
134
|
}
|
|
131
|
-
}
|
|
132
|
-
|
|
135
|
+
});
|
|
136
|
+
const results = await Promise.all(scanPromises);
|
|
137
|
+
return results.flat();
|
|
133
138
|
}
|
|
134
139
|
/**
|
|
135
140
|
* List all sessions (unified function for both commands)
|