genbox 1.0.226 → 1.0.227
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/dist/commands/session/watch.js +811 -102
- package/package.json +1 -1
|
@@ -119,6 +119,31 @@ function getStateColor(state) {
|
|
|
119
119
|
return chalk_1.default.dim;
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Render progress bar
|
|
124
|
+
*/
|
|
125
|
+
function renderBar(percent, width = 10) {
|
|
126
|
+
const filled = Math.round((percent / 100) * width);
|
|
127
|
+
const empty = width - filled;
|
|
128
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
129
|
+
// Color based on usage level
|
|
130
|
+
if (percent >= 90)
|
|
131
|
+
return chalk_1.default.red(bar);
|
|
132
|
+
if (percent >= 70)
|
|
133
|
+
return chalk_1.default.yellow(bar);
|
|
134
|
+
return chalk_1.default.green(bar);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Format time remaining
|
|
138
|
+
*/
|
|
139
|
+
function formatTimeRemaining(mins) {
|
|
140
|
+
if (mins >= 60) {
|
|
141
|
+
const hours = Math.floor(mins / 60);
|
|
142
|
+
const remainingMins = mins % 60;
|
|
143
|
+
return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
|
|
144
|
+
}
|
|
145
|
+
return `${mins} min`;
|
|
146
|
+
}
|
|
122
147
|
/**
|
|
123
148
|
* Format state for display
|
|
124
149
|
*/
|
|
@@ -352,6 +377,165 @@ fi
|
|
|
352
377
|
}
|
|
353
378
|
return null;
|
|
354
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Query genbox daemon for session messages via JSONL reading
|
|
382
|
+
* This reads from ~/.claude/projects/ JSONL files for structured message data
|
|
383
|
+
*/
|
|
384
|
+
async function queryGenboxDaemonMessages(genboxName, sessionId = 'latest', limit = 50) {
|
|
385
|
+
try {
|
|
386
|
+
// Try each daemon port
|
|
387
|
+
const { stdout } = await execAsync(`ssh -o ConnectTimeout=5 -o BatchMode=yes genbox-${genboxName} 'curl -s "http://127.0.0.1:47191/api/sessions/${sessionId}/messages?limit=${limit}" 2>/dev/null || curl -s "http://127.0.0.1:47192/api/sessions/${sessionId}/messages?limit=${limit}" 2>/dev/null || curl -s "http://127.0.0.1:47193/api/sessions/${sessionId}/messages?limit=${limit}" 2>/dev/null' 2>/dev/null`, { encoding: 'utf8', timeout: 15000 });
|
|
388
|
+
if (stdout && stdout.trim()) {
|
|
389
|
+
const data = JSON.parse(stdout);
|
|
390
|
+
return data.messages || [];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// Fallback if daemon doesn't have messages endpoint yet
|
|
395
|
+
}
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Format a single message into display lines
|
|
400
|
+
*/
|
|
401
|
+
function formatMessageLine(msg, termWidth) {
|
|
402
|
+
const lines = [];
|
|
403
|
+
const maxContentWidth = termWidth - 8;
|
|
404
|
+
for (const block of msg.content) {
|
|
405
|
+
if (block.type === 'text' && block.text) {
|
|
406
|
+
// Assistant text response
|
|
407
|
+
if (msg.role === 'assistant' && !msg.toolName) {
|
|
408
|
+
const text = block.text.trim();
|
|
409
|
+
const preview = text.length > maxContentWidth
|
|
410
|
+
? text.substring(0, maxContentWidth - 12) + '...(truncated)'
|
|
411
|
+
: text.split('\n')[0].substring(0, maxContentWidth);
|
|
412
|
+
lines.push(chalk_1.default.yellow('🤖 ') + chalk_1.default.yellow(preview));
|
|
413
|
+
// Token info
|
|
414
|
+
if (msg.inputTokens || msg.outputTokens) {
|
|
415
|
+
lines.push(chalk_1.default.dim(` tokens: ${msg.inputTokens || 0}↓ ${msg.outputTokens || 0}↑`));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// User message (usually tool result context)
|
|
419
|
+
else if (msg.role === 'user' && !msg.isToolResult) {
|
|
420
|
+
const preview = block.text.substring(0, maxContentWidth);
|
|
421
|
+
lines.push(chalk_1.default.green('👤 ') + chalk_1.default.green(preview));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else if (block.type === 'tool_use' && block.name) {
|
|
425
|
+
// Tool call
|
|
426
|
+
let toolName = block.name
|
|
427
|
+
.replace(/^mcp__playwright__/, 'pw:')
|
|
428
|
+
.replace(/^mcp__plugin_playwright_playwright__/, 'pw:');
|
|
429
|
+
// Extract useful info from input
|
|
430
|
+
let inputPreview = '';
|
|
431
|
+
if (block.input) {
|
|
432
|
+
if (block.input.file_path) {
|
|
433
|
+
inputPreview = block.input.file_path;
|
|
434
|
+
}
|
|
435
|
+
else if (block.input.pattern) {
|
|
436
|
+
inputPreview = JSON.stringify({ pattern: block.input.pattern, path: block.input.path });
|
|
437
|
+
}
|
|
438
|
+
else if (block.input.command) {
|
|
439
|
+
inputPreview = block.input.command.split('\n')[0].substring(0, 60);
|
|
440
|
+
}
|
|
441
|
+
else if (block.input.content) {
|
|
442
|
+
inputPreview = `"${block.input.content.substring(0, 40)}..."`;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
const preview = JSON.stringify(block.input);
|
|
446
|
+
inputPreview = preview.length > 80 ? preview.substring(0, 77) + '...' : preview;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
lines.push(chalk_1.default.cyan('⚙ ') + chalk_1.default.bold(toolName) + ' ' + chalk_1.default.dim(inputPreview));
|
|
450
|
+
}
|
|
451
|
+
else if (block.type === 'tool_result') {
|
|
452
|
+
// Tool result (shown under the tool call that triggered it)
|
|
453
|
+
const resultText = typeof block.content === 'string' ? block.content : '';
|
|
454
|
+
const preview = resultText.split('\n')[0].substring(0, maxContentWidth - 5);
|
|
455
|
+
const icon = msg.isError ? chalk_1.default.red('✗') : chalk_1.default.green('✓');
|
|
456
|
+
lines.push(` ${icon} ` + chalk_1.default.dim(preview || '(result)'));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return lines;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Render messages view with pagination like logs view
|
|
463
|
+
*/
|
|
464
|
+
function renderMessagesView(session, messages, scrollOffset, termHeight, termWidth) {
|
|
465
|
+
const lineWidth = Math.min(termWidth - 2, 140);
|
|
466
|
+
const maxLogLines = Math.max(5, termHeight - 8);
|
|
467
|
+
if (!session) {
|
|
468
|
+
console.log(chalk_1.default.dim('\n No session selected.\n'));
|
|
469
|
+
return { totalLines: 0, visibleLines: 0 };
|
|
470
|
+
}
|
|
471
|
+
// Header with session info
|
|
472
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
473
|
+
const stateIcon = session.state === 'thinking' ? '💭' :
|
|
474
|
+
session.state === 'executing_tool' ? '⚙️' :
|
|
475
|
+
session.state === 'waiting_for_input' ? '⌛' :
|
|
476
|
+
session.state === 'responding' ? '📝' : '●';
|
|
477
|
+
const stateText = session.currentTool || session.state?.replace(/_/g, ' ') || 'running';
|
|
478
|
+
console.log(chalk_1.default.bold.inverse(` ${session.name} `) + chalk_1.default.dim(` ${session.genbox} • ${stateIcon} ${stateText} • ${timestamp}`));
|
|
479
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
480
|
+
if (!messages || messages.length === 0) {
|
|
481
|
+
console.log(chalk_1.default.dim('\n Waiting for messages...\n'));
|
|
482
|
+
return { totalLines: 0, visibleLines: 0 };
|
|
483
|
+
}
|
|
484
|
+
// Format all messages into lines
|
|
485
|
+
const formattedLines = [];
|
|
486
|
+
for (const msg of messages) {
|
|
487
|
+
const msgLines = formatMessageLine(msg, termWidth);
|
|
488
|
+
formattedLines.push(...msgLines);
|
|
489
|
+
}
|
|
490
|
+
const totalLines = formattedLines.length;
|
|
491
|
+
// Calculate visible window with pagination
|
|
492
|
+
// scrollOffset 0 = show latest (bottom), positive = scroll up (older)
|
|
493
|
+
const endIndex = totalLines - scrollOffset;
|
|
494
|
+
const startIndex = Math.max(0, endIndex - maxLogLines);
|
|
495
|
+
const actualEnd = Math.min(endIndex, totalLines);
|
|
496
|
+
// Position info
|
|
497
|
+
const showingStart = startIndex + 1;
|
|
498
|
+
const showingEnd = actualEnd;
|
|
499
|
+
const positionInfo = `${showingStart}-${showingEnd} of ${totalLines}`;
|
|
500
|
+
console.log(chalk_1.default.bold(' Messages') + chalk_1.default.dim(` [${positionInfo}] `) + chalk_1.default.dim(`(${messages.length} turns)`));
|
|
501
|
+
// Scroll up indicator
|
|
502
|
+
if (startIndex > 0) {
|
|
503
|
+
console.log(chalk_1.default.dim(` ↑ ${startIndex} more above [↑ to scroll]`));
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
console.log('');
|
|
507
|
+
}
|
|
508
|
+
// Display visible lines
|
|
509
|
+
const visibleLines = formattedLines.slice(startIndex, actualEnd);
|
|
510
|
+
for (const line of visibleLines) {
|
|
511
|
+
console.log(' ' + line);
|
|
512
|
+
}
|
|
513
|
+
// Scroll down indicator
|
|
514
|
+
if (actualEnd < totalLines) {
|
|
515
|
+
console.log(chalk_1.default.dim(` ↓ ${totalLines - actualEnd} more below [↓ to scroll]`));
|
|
516
|
+
}
|
|
517
|
+
else if (scrollOffset > 0) {
|
|
518
|
+
console.log(chalk_1.default.cyan(' ↓ [↓ or End to jump to latest]'));
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
console.log(chalk_1.default.dim(' ─ latest'));
|
|
522
|
+
}
|
|
523
|
+
// Fill remaining height
|
|
524
|
+
const usedLines = 6 + visibleLines.length;
|
|
525
|
+
const remaining = termHeight - usedLines - 1;
|
|
526
|
+
for (let i = 0; i < remaining; i++) {
|
|
527
|
+
console.log('');
|
|
528
|
+
}
|
|
529
|
+
// Footer
|
|
530
|
+
const controls = chalk_1.default.dim('[Esc]') + ' Back ' +
|
|
531
|
+
chalk_1.default.dim('[↑/↓]') + ' Scroll ' +
|
|
532
|
+
chalk_1.default.dim('[a]') + ' Attach ' +
|
|
533
|
+
chalk_1.default.dim('[s]') + ' Send ' +
|
|
534
|
+
chalk_1.default.dim('[r]') + ' Refresh ' +
|
|
535
|
+
chalk_1.default.dim('[q]') + ' Quit';
|
|
536
|
+
console.log(chalk_1.default.inverse(' ' + controls + ' '.repeat(Math.max(0, termWidth - stripAnsi(controls).length - 1))));
|
|
537
|
+
return { totalLines, visibleLines: visibleLines.length };
|
|
538
|
+
}
|
|
355
539
|
/**
|
|
356
540
|
* Query genbox for BOTH socket files AND daemon session data in a SINGLE SSH call
|
|
357
541
|
* This is much faster than making two separate SSH connections
|
|
@@ -459,13 +643,18 @@ async function collectSessionStates(options) {
|
|
|
459
643
|
includeEnded: false,
|
|
460
644
|
});
|
|
461
645
|
debugLog(`collectSessionStates: got ${localSessions.length} local sessions (instant)`);
|
|
462
|
-
// Step 2: Fetch cloud genboxes from API (single API call)
|
|
646
|
+
// Step 2: Fetch cloud genboxes from API (single API call) with full details
|
|
463
647
|
const apiStart = Date.now();
|
|
464
648
|
let cloudGenboxes = [];
|
|
649
|
+
const genboxDetailsMap = new Map(); // Store full genbox details by name
|
|
465
650
|
try {
|
|
466
651
|
const response = await (0, api_1.fetchApi)('/genboxes');
|
|
467
|
-
|
|
468
|
-
|
|
652
|
+
const allGenboxes = (Array.isArray(response) ? response : response?.genboxes || []);
|
|
653
|
+
cloudGenboxes = allGenboxes.filter((g) => g.status === 'running');
|
|
654
|
+
// Store full genbox details for later use
|
|
655
|
+
for (const genbox of cloudGenboxes) {
|
|
656
|
+
genboxDetailsMap.set(genbox.name, genbox);
|
|
657
|
+
}
|
|
469
658
|
}
|
|
470
659
|
catch {
|
|
471
660
|
// Ignore API errors
|
|
@@ -540,6 +729,25 @@ async function collectSessionStates(options) {
|
|
|
540
729
|
displayStatus = daemonSession.status === 'active' ? 'running' :
|
|
541
730
|
daemonSession.status === 'idle' ? 'idle' : session.status;
|
|
542
731
|
}
|
|
732
|
+
// Get full genbox details for cloud sessions
|
|
733
|
+
let genboxDetails = undefined;
|
|
734
|
+
if (session.type === 'cloud' && genboxName) {
|
|
735
|
+
const fullGenbox = genboxDetailsMap.get(genboxName);
|
|
736
|
+
if (fullGenbox) {
|
|
737
|
+
genboxDetails = {
|
|
738
|
+
size: fullGenbox.size,
|
|
739
|
+
creditsPerHour: fullGenbox.creditsPerHour,
|
|
740
|
+
totalCreditsUsed: fullGenbox.totalCreditsUsed,
|
|
741
|
+
totalHoursUsed: fullGenbox.totalHoursUsed,
|
|
742
|
+
currentHourEnd: fullGenbox.currentHourEnd,
|
|
743
|
+
lastActivityAt: fullGenbox.lastActivityAt,
|
|
744
|
+
autoDestroyOnInactivity: fullGenbox.autoDestroyOnInactivity,
|
|
745
|
+
protectedUntil: fullGenbox.protectedUntil,
|
|
746
|
+
systemStats: fullGenbox.systemStats,
|
|
747
|
+
urls: fullGenbox.urls,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
}
|
|
543
751
|
states.push({
|
|
544
752
|
id: session.id.substring(0, 8),
|
|
545
753
|
name: session.name,
|
|
@@ -560,6 +768,7 @@ async function collectSessionStates(options) {
|
|
|
560
768
|
cost: daemonSession?.estimatedCostUsd,
|
|
561
769
|
toolCalls: daemonSession?.toolUseCount,
|
|
562
770
|
messagesCount: daemonSession?.messageCount,
|
|
771
|
+
genboxDetails,
|
|
563
772
|
});
|
|
564
773
|
}
|
|
565
774
|
// Process cloud genboxes with combined socket + daemon data
|
|
@@ -599,6 +808,20 @@ async function collectSessionStates(options) {
|
|
|
599
808
|
displayStatus = daemonSession.status === 'active' ? 'running' :
|
|
600
809
|
daemonSession.status === 'idle' ? 'idle' : 'running';
|
|
601
810
|
}
|
|
811
|
+
// Get full genbox details for this genbox
|
|
812
|
+
const fullGenbox = genboxDetailsMap.get(genbox.name);
|
|
813
|
+
const genboxDetails = fullGenbox ? {
|
|
814
|
+
size: fullGenbox.size,
|
|
815
|
+
creditsPerHour: fullGenbox.creditsPerHour,
|
|
816
|
+
totalCreditsUsed: fullGenbox.totalCreditsUsed,
|
|
817
|
+
totalHoursUsed: fullGenbox.totalHoursUsed,
|
|
818
|
+
currentHourEnd: fullGenbox.currentHourEnd,
|
|
819
|
+
lastActivityAt: fullGenbox.lastActivityAt,
|
|
820
|
+
autoDestroyOnInactivity: fullGenbox.autoDestroyOnInactivity,
|
|
821
|
+
protectedUntil: fullGenbox.protectedUntil,
|
|
822
|
+
systemStats: fullGenbox.systemStats,
|
|
823
|
+
urls: fullGenbox.urls,
|
|
824
|
+
} : undefined;
|
|
602
825
|
states.push({
|
|
603
826
|
id: sessionName.substring(0, 8),
|
|
604
827
|
name: sessionName,
|
|
@@ -623,12 +846,27 @@ async function collectSessionStates(options) {
|
|
|
623
846
|
cost: daemonSession?.estimatedCostUsd,
|
|
624
847
|
toolCalls: daemonSession?.toolUseCount,
|
|
625
848
|
messagesCount: daemonSession?.messageCount,
|
|
849
|
+
genboxDetails,
|
|
626
850
|
});
|
|
627
851
|
}
|
|
628
852
|
// If no sockets found but genbox is running, still show the genbox
|
|
629
853
|
if (sockets.length === 0) {
|
|
630
854
|
const hasSession = states.some(s => s.genbox === genbox.name);
|
|
631
855
|
if (!hasSession && genbox.status === 'running' && options.status !== 'stopped') {
|
|
856
|
+
// Get full genbox details for this genbox
|
|
857
|
+
const fullGenbox = genboxDetailsMap.get(genbox.name);
|
|
858
|
+
const genboxDetails = fullGenbox ? {
|
|
859
|
+
size: fullGenbox.size,
|
|
860
|
+
creditsPerHour: fullGenbox.creditsPerHour,
|
|
861
|
+
totalCreditsUsed: fullGenbox.totalCreditsUsed,
|
|
862
|
+
totalHoursUsed: fullGenbox.totalHoursUsed,
|
|
863
|
+
currentHourEnd: fullGenbox.currentHourEnd,
|
|
864
|
+
lastActivityAt: fullGenbox.lastActivityAt,
|
|
865
|
+
autoDestroyOnInactivity: fullGenbox.autoDestroyOnInactivity,
|
|
866
|
+
protectedUntil: fullGenbox.protectedUntil,
|
|
867
|
+
systemStats: fullGenbox.systemStats,
|
|
868
|
+
urls: fullGenbox.urls,
|
|
869
|
+
} : undefined;
|
|
632
870
|
states.push({
|
|
633
871
|
id: genbox.id?.substring(0, 8) || 'unknown',
|
|
634
872
|
name: genbox.name,
|
|
@@ -638,6 +876,7 @@ async function collectSessionStates(options) {
|
|
|
638
876
|
status: 'running',
|
|
639
877
|
duration: formatDuration(genbox.createdAt),
|
|
640
878
|
createdAt: genbox.createdAt,
|
|
879
|
+
genboxDetails,
|
|
641
880
|
});
|
|
642
881
|
}
|
|
643
882
|
}
|
|
@@ -895,8 +1134,8 @@ function renderFooter(termWidth) {
|
|
|
895
1134
|
'[q] Quit',
|
|
896
1135
|
'[r] Refresh',
|
|
897
1136
|
'[↑↓] Navigate',
|
|
898
|
-
'[⏎]
|
|
899
|
-
'[
|
|
1137
|
+
'[⏎] Messages',
|
|
1138
|
+
'[t] Terminal',
|
|
900
1139
|
'[l] Logs',
|
|
901
1140
|
'[a] Attach',
|
|
902
1141
|
'[s] Send',
|
|
@@ -906,83 +1145,278 @@ function renderFooter(termWidth) {
|
|
|
906
1145
|
console.log(chalk_1.default.dim(' ' + controls.join(' ')));
|
|
907
1146
|
}
|
|
908
1147
|
/**
|
|
909
|
-
* Render detail view for a session
|
|
1148
|
+
* Render detail view for a session (with scrolling)
|
|
910
1149
|
*/
|
|
911
|
-
function renderDetailView(session, termHeight, termWidth) {
|
|
1150
|
+
function renderDetailView(session, scrollOffset, termHeight, termWidth) {
|
|
912
1151
|
const lineWidth = Math.min(termWidth - 2, 120);
|
|
913
1152
|
if (!session) {
|
|
914
1153
|
console.log(chalk_1.default.dim('\n No session selected.\n'));
|
|
915
1154
|
return;
|
|
916
1155
|
}
|
|
917
|
-
// Header
|
|
1156
|
+
// Header (not scrollable - always shown at top)
|
|
918
1157
|
console.log(chalk_1.default.bold.inverse(' SESSION DETAILS ') + chalk_1.default.dim(' Press [Esc] or [Backspace] to go back'));
|
|
919
1158
|
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
920
1159
|
console.log('');
|
|
921
|
-
//
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
[chalk_1.default.dim('Total Tokens:'), session.tokens ? `${session.tokens}k` : '--', chalk_1.default.dim('Cost:'), session.cost ? `$${session.cost.toFixed(2)}` : '--'],
|
|
954
|
-
[chalk_1.default.dim('Files Modified:'), `${session.filesModified || 0}`, '', ''],
|
|
955
|
-
];
|
|
956
|
-
for (const row of metricsRows) {
|
|
957
|
-
const left = ` ${row[0].padEnd(16)} ${String(row[1]).padEnd(col1Width - 18)}`;
|
|
958
|
-
const right = row[2] ? `${row[2].padEnd(16)} ${row[3]}` : '';
|
|
959
|
-
console.log(left + right);
|
|
960
|
-
}
|
|
961
|
-
// Current tool if executing
|
|
962
|
-
if (session.state === 'executing_tool' && session.currentTool) {
|
|
1160
|
+
// Capture all scrollable content to lines array
|
|
1161
|
+
const lines = [];
|
|
1162
|
+
const originalLog = console.log;
|
|
1163
|
+
try {
|
|
1164
|
+
// Override console.log to capture output
|
|
1165
|
+
console.log = (...args) => {
|
|
1166
|
+
lines.push(args.join(' '));
|
|
1167
|
+
};
|
|
1168
|
+
// Session name and ID
|
|
1169
|
+
const statusIcon = session.status === 'running' ? chalk_1.default.green('●') :
|
|
1170
|
+
session.status === 'idle' ? chalk_1.default.yellow('●') :
|
|
1171
|
+
session.status === 'stopped' ? chalk_1.default.red('●') : chalk_1.default.dim('○');
|
|
1172
|
+
const stateIcon = session.state === 'thinking' ? '💭' :
|
|
1173
|
+
session.state === 'executing_tool' ? '⚙️' :
|
|
1174
|
+
session.state === 'waiting_for_input' ? '⌛' :
|
|
1175
|
+
session.state === 'responding' ? '📝' : '';
|
|
1176
|
+
console.log(chalk_1.default.bold(` ${session.name}`) + chalk_1.default.dim(` (${session.id})`));
|
|
1177
|
+
console.log('');
|
|
1178
|
+
// Two-column layout
|
|
1179
|
+
const col1Width = 30;
|
|
1180
|
+
const col2Width = 40;
|
|
1181
|
+
const rows = [
|
|
1182
|
+
[chalk_1.default.dim('Provider:'), chalk_1.default.magenta(session.provider), chalk_1.default.dim('Status:'), `${statusIcon} ${session.status}`],
|
|
1183
|
+
[chalk_1.default.dim('Type:'), session.type, chalk_1.default.dim('State:'), session.state ? `${stateIcon} ${session.state}` : chalk_1.default.dim('--')],
|
|
1184
|
+
[chalk_1.default.dim('Genbox:'), chalk_1.default.cyan(session.genbox), chalk_1.default.dim('Duration:'), session.duration || '--'],
|
|
1185
|
+
[chalk_1.default.dim('Created:'), session.createdAt ? new Date(session.createdAt).toLocaleString() : '--', '', ''],
|
|
1186
|
+
];
|
|
1187
|
+
for (const row of rows) {
|
|
1188
|
+
const left = ` ${row[0].padEnd(12)} ${String(row[1]).padEnd(col1Width - 14)}`;
|
|
1189
|
+
const right = row[2] ? `${row[2].padEnd(12)} ${row[3]}` : '';
|
|
1190
|
+
console.log(left + right);
|
|
1191
|
+
}
|
|
963
1192
|
console.log('');
|
|
964
1193
|
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
965
|
-
console.log(chalk_1.default.bold('
|
|
966
|
-
console.log(
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1194
|
+
console.log(chalk_1.default.bold(' Metrics'));
|
|
1195
|
+
console.log('');
|
|
1196
|
+
// Metrics section
|
|
1197
|
+
const metricsRows = [
|
|
1198
|
+
[chalk_1.default.dim('Messages:'), `${session.messagesCount || 0}`, chalk_1.default.dim('Tool Calls:'), `${session.toolCalls || 0}`],
|
|
1199
|
+
[chalk_1.default.dim('Input Tokens:'), session.inputTokens ? session.inputTokens.toLocaleString() : '--', chalk_1.default.dim('Output Tokens:'), session.outputTokens ? session.outputTokens.toLocaleString() : '--'],
|
|
1200
|
+
[chalk_1.default.dim('Total Tokens:'), session.tokens ? `${session.tokens}k` : '--', chalk_1.default.dim('Cost:'), session.cost ? `$${session.cost.toFixed(2)}` : '--'],
|
|
1201
|
+
[chalk_1.default.dim('Files Modified:'), `${session.filesModified || 0}`, '', ''],
|
|
1202
|
+
];
|
|
1203
|
+
for (const row of metricsRows) {
|
|
1204
|
+
const left = ` ${row[0].padEnd(16)} ${String(row[1]).padEnd(col1Width - 18)}`;
|
|
1205
|
+
const right = row[2] ? `${row[2].padEnd(16)} ${row[3]}` : '';
|
|
1206
|
+
console.log(left + right);
|
|
1207
|
+
}
|
|
1208
|
+
// Current tool if executing
|
|
1209
|
+
if (session.state === 'executing_tool' && session.currentTool) {
|
|
1210
|
+
console.log('');
|
|
1211
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
1212
|
+
console.log(chalk_1.default.bold(' Current Tool'));
|
|
1213
|
+
console.log(` ${chalk_1.default.cyan(session.currentTool)}`);
|
|
1214
|
+
}
|
|
1215
|
+
// Last message preview
|
|
1216
|
+
if (session.lastMessage) {
|
|
1217
|
+
console.log('');
|
|
1218
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
1219
|
+
console.log(chalk_1.default.bold(' Last Message'));
|
|
1220
|
+
console.log(chalk_1.default.dim(` "${session.lastMessage}..."`));
|
|
1221
|
+
}
|
|
1222
|
+
// Genbox details (for cloud sessions)
|
|
1223
|
+
if (session.type === 'cloud' && session.genboxDetails) {
|
|
1224
|
+
const details = session.genboxDetails;
|
|
1225
|
+
// Billing section
|
|
1226
|
+
if (details.size || details.creditsPerHour !== undefined) {
|
|
1227
|
+
console.log('');
|
|
1228
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
1229
|
+
console.log(chalk_1.default.bold(' Billing'));
|
|
1230
|
+
console.log('');
|
|
1231
|
+
if (details.size && details.creditsPerHour !== undefined) {
|
|
1232
|
+
const plural = details.creditsPerHour > 1 ? 's' : '';
|
|
1233
|
+
console.log(` ${chalk_1.default.dim('Size:')} ${details.size} (${details.creditsPerHour} credit${plural}/hr)`);
|
|
1234
|
+
}
|
|
1235
|
+
if (details.currentHourEnd) {
|
|
1236
|
+
const now = new Date();
|
|
1237
|
+
const currentHourEnd = new Date(details.currentHourEnd);
|
|
1238
|
+
const minutesUntilBilling = Math.max(0, Math.ceil((currentHourEnd.getTime() - now.getTime()) / (60 * 1000)));
|
|
1239
|
+
console.log(` ${chalk_1.default.dim('Current hour ends in:')} ${chalk_1.default.cyan(formatTimeRemaining(minutesUntilBilling))}`);
|
|
1240
|
+
}
|
|
1241
|
+
if (details.lastActivityAt) {
|
|
1242
|
+
const now = new Date();
|
|
1243
|
+
const lastActivity = new Date(details.lastActivityAt);
|
|
1244
|
+
const minutesInactive = Math.floor((now.getTime() - lastActivity.getTime()) / (60 * 1000));
|
|
1245
|
+
const activityStr = minutesInactive < 1 ? 'just now' : minutesInactive + ' min ago';
|
|
1246
|
+
console.log(` ${chalk_1.default.dim('Last activity:')} ${activityStr}`);
|
|
1247
|
+
}
|
|
1248
|
+
if (details.totalHoursUsed !== undefined && details.totalCreditsUsed !== undefined) {
|
|
1249
|
+
const hourPlural = details.totalHoursUsed !== 1 ? 's' : '';
|
|
1250
|
+
const creditPlural = details.totalCreditsUsed !== 1 ? 's' : '';
|
|
1251
|
+
console.log(` ${chalk_1.default.dim('Total:')} ${details.totalHoursUsed} hour${hourPlural}, ${details.totalCreditsUsed} credit${creditPlural}`);
|
|
1252
|
+
}
|
|
1253
|
+
// Auto-destroy status
|
|
1254
|
+
if (details.autoDestroyOnInactivity !== undefined) {
|
|
1255
|
+
const now = new Date();
|
|
1256
|
+
// Check if protected from auto-destroy
|
|
1257
|
+
if (details.protectedUntil) {
|
|
1258
|
+
const protectedTime = new Date(details.protectedUntil);
|
|
1259
|
+
const minutesUntilProtectionEnds = Math.ceil((protectedTime.getTime() - now.getTime()) / (60 * 1000));
|
|
1260
|
+
if (minutesUntilProtectionEnds > 0) {
|
|
1261
|
+
const timeStr = formatTimeRemaining(minutesUntilProtectionEnds);
|
|
1262
|
+
console.log(` ${chalk_1.default.green('Protected from auto-destroy for:')} ${timeStr}`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
// Auto-destroy status (only show if NOT protected and enabled)
|
|
1266
|
+
if (details.autoDestroyOnInactivity && details.currentHourEnd) {
|
|
1267
|
+
const isProtected = details.protectedUntil && new Date(details.protectedUntil).getTime() > now.getTime();
|
|
1268
|
+
if (!isProtected) {
|
|
1269
|
+
const currentHourEnd = new Date(details.currentHourEnd);
|
|
1270
|
+
const currentHourStart = new Date(currentHourEnd.getTime() - 60 * 60 * 1000);
|
|
1271
|
+
const minutesIntoBillingHour = Math.floor((now.getTime() - currentHourStart.getTime()) / (60 * 1000));
|
|
1272
|
+
const minutesUntilDestroy = Math.max(0, 58 - minutesIntoBillingHour);
|
|
1273
|
+
// Check if there was activity after the 50 min mark
|
|
1274
|
+
const fiftyMinMark = new Date(currentHourStart.getTime() + 50 * 60 * 1000);
|
|
1275
|
+
const lastActivity = details.lastActivityAt ? new Date(details.lastActivityAt) : null;
|
|
1276
|
+
const wasActiveAfter50Min = lastActivity ? lastActivity.getTime() >= fiftyMinMark.getTime() : false;
|
|
1277
|
+
// Auto-destroy is paused if we're past 50min mark AND there was activity after 50min
|
|
1278
|
+
const isAutoDestroyPaused = minutesIntoBillingHour >= 50 && wasActiveAfter50Min;
|
|
1279
|
+
if (isAutoDestroyPaused) {
|
|
1280
|
+
console.log(` ${chalk_1.default.green('Auto-destroy:')} paused`);
|
|
1281
|
+
}
|
|
1282
|
+
else {
|
|
1283
|
+
const lastActivityMinutes = lastActivity ? Math.floor((now.getTime() - lastActivity.getTime()) / (60 * 1000)) : 0;
|
|
1284
|
+
if (lastActivityMinutes >= 5) {
|
|
1285
|
+
console.log(` ${chalk_1.default.yellow('Auto-destroy in:')} ${minutesUntilDestroy} min (inactive)`);
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
console.log(` ${chalk_1.default.dim('Auto-destroy:')} ${minutesUntilDestroy} min`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
else if (!details.autoDestroyOnInactivity) {
|
|
1294
|
+
console.log(` ${chalk_1.default.dim('Auto-destroy:')} disabled`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
// System Stats section
|
|
1299
|
+
if (details.systemStats) {
|
|
1300
|
+
const stats = details.systemStats;
|
|
1301
|
+
console.log('');
|
|
1302
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
1303
|
+
console.log(chalk_1.default.bold(' System Stats'));
|
|
1304
|
+
console.log('');
|
|
1305
|
+
// Load average
|
|
1306
|
+
if (stats.loadAvg && stats.loadAvg.length >= 3) {
|
|
1307
|
+
const [load1, load5, load15] = stats.loadAvg;
|
|
1308
|
+
console.log(` ${chalk_1.default.dim('Load Avg:')} ${load1.toFixed(2)}, ${load5.toFixed(2)}, ${load15.toFixed(2)}`);
|
|
1309
|
+
}
|
|
1310
|
+
// Memory
|
|
1311
|
+
if (stats.memoryUsagePercent !== undefined) {
|
|
1312
|
+
const memUsed = stats.memoryUsedMb ? `${(stats.memoryUsedMb / 1024).toFixed(1)}G` : '?';
|
|
1313
|
+
const memTotal = stats.memoryTotalMb ? `${(stats.memoryTotalMb / 1024).toFixed(1)}G` : '?';
|
|
1314
|
+
const memBar = renderBar(stats.memoryUsagePercent);
|
|
1315
|
+
console.log(` ${chalk_1.default.dim('Memory:')} ${memBar} ${stats.memoryUsagePercent}% (${memUsed}/${memTotal})`);
|
|
1316
|
+
}
|
|
1317
|
+
// Disk
|
|
1318
|
+
if (stats.diskUsagePercent !== undefined) {
|
|
1319
|
+
const diskUsed = stats.diskUsedGb !== undefined ? `${stats.diskUsedGb}G` : '?';
|
|
1320
|
+
const diskTotal = stats.diskTotalGb !== undefined ? `${stats.diskTotalGb}G` : '?';
|
|
1321
|
+
const diskBar = renderBar(stats.diskUsagePercent);
|
|
1322
|
+
console.log(` ${chalk_1.default.dim('Disk:')} ${diskBar} ${stats.diskUsagePercent}% (${diskUsed}/${diskTotal})`);
|
|
1323
|
+
}
|
|
1324
|
+
// Uptime
|
|
1325
|
+
if (stats.uptimeSeconds !== undefined) {
|
|
1326
|
+
const days = Math.floor(stats.uptimeSeconds / 86400);
|
|
1327
|
+
const hours = Math.floor((stats.uptimeSeconds % 86400) / 3600);
|
|
1328
|
+
const mins = Math.floor((stats.uptimeSeconds % 3600) / 60);
|
|
1329
|
+
const uptimeStr = days > 0 ? `${days}d ${hours}h ${mins}m` : hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
1330
|
+
console.log(` ${chalk_1.default.dim('Uptime:')} ${uptimeStr}`);
|
|
1331
|
+
}
|
|
1332
|
+
// Process count
|
|
1333
|
+
if (stats.processCount !== undefined) {
|
|
1334
|
+
console.log(` ${chalk_1.default.dim('Processes:')} ${stats.processCount}`);
|
|
1335
|
+
}
|
|
1336
|
+
// Stats age
|
|
1337
|
+
if (stats.updatedAt) {
|
|
1338
|
+
const now = new Date();
|
|
1339
|
+
const statsAge = Math.floor((now.getTime() - new Date(stats.updatedAt).getTime()) / 1000);
|
|
1340
|
+
const ageStr = statsAge < 60 ? 'just now' : Math.floor(statsAge / 60) + 'm ago';
|
|
1341
|
+
console.log(chalk_1.default.dim(` (updated ${ageStr})`));
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
// Docker Services section
|
|
1345
|
+
if (details.systemStats?.dockerServices && details.systemStats.dockerServices.length > 0) {
|
|
1346
|
+
console.log('');
|
|
1347
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
1348
|
+
console.log(chalk_1.default.bold(' Docker Services'));
|
|
1349
|
+
console.log('');
|
|
1350
|
+
console.log(' ' + 'NAME'.padEnd(30) + 'STATUS');
|
|
1351
|
+
for (const svc of details.systemStats.dockerServices) {
|
|
1352
|
+
const healthBadge = svc.health
|
|
1353
|
+
? (svc.health === 'healthy' ? chalk_1.default.green(' ✓') : chalk_1.default.red(` (${svc.health})`))
|
|
1354
|
+
: '';
|
|
1355
|
+
console.log(` ${svc.name.padEnd(30)}${svc.status}${healthBadge}`);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
// PM2 Services section
|
|
1359
|
+
if (details.systemStats?.pm2Services && details.systemStats.pm2Services.length > 0) {
|
|
1360
|
+
console.log('');
|
|
1361
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
1362
|
+
console.log(chalk_1.default.bold(' PM2 Services'));
|
|
1363
|
+
console.log('');
|
|
1364
|
+
console.log(' ' + 'NAME'.padEnd(20) + 'STATUS'.padEnd(16) + 'CPU'.padEnd(8) + 'MEM');
|
|
1365
|
+
for (const app of details.systemStats.pm2Services) {
|
|
1366
|
+
const statusColor = app.status === 'online' ? chalk_1.default.green :
|
|
1367
|
+
app.status === 'stopped' ? chalk_1.default.yellow : chalk_1.default.red;
|
|
1368
|
+
const memoryMb = app.memory ? (app.memory > 10000 ? Math.round(app.memory / (1024 * 1024)) : app.memory) : 0;
|
|
1369
|
+
console.log(` ${app.name.padEnd(20)}${statusColor(app.status.padEnd(16))}${(app.cpu || 0) + '%'}`.padEnd(28) + `${memoryMb}MB`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
// Service URLs section
|
|
1373
|
+
if (details.urls && Object.keys(details.urls).length > 0) {
|
|
1374
|
+
console.log('');
|
|
1375
|
+
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
1376
|
+
console.log(chalk_1.default.bold(' Service URLs'));
|
|
1377
|
+
console.log('');
|
|
1378
|
+
for (const [service, url] of Object.entries(details.urls)) {
|
|
1379
|
+
console.log(` ${service}: ${chalk_1.default.cyan(url)}`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
// Actions
|
|
970
1384
|
console.log('');
|
|
971
1385
|
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
972
|
-
console.log(chalk_1.default.bold('
|
|
973
|
-
console.log(
|
|
1386
|
+
console.log(chalk_1.default.bold(' Quick Actions'));
|
|
1387
|
+
console.log('');
|
|
1388
|
+
console.log(chalk_1.default.dim(' Press: ') +
|
|
1389
|
+
chalk_1.default.cyan('[a]') + chalk_1.default.dim(' Attach ') +
|
|
1390
|
+
chalk_1.default.cyan('[l]') + chalk_1.default.dim(' Logs ') +
|
|
1391
|
+
chalk_1.default.cyan('[s]') + chalk_1.default.dim(' Send prompt ') +
|
|
1392
|
+
chalk_1.default.cyan('[Esc]') + chalk_1.default.dim(' Back'));
|
|
1393
|
+
console.log('');
|
|
1394
|
+
}
|
|
1395
|
+
finally {
|
|
1396
|
+
// Always restore console.log
|
|
1397
|
+
console.log = originalLog;
|
|
1398
|
+
}
|
|
1399
|
+
// Calculate visible area (reserve 5 lines for header/footer)
|
|
1400
|
+
const headerLines = 3;
|
|
1401
|
+
const footerLines = 2;
|
|
1402
|
+
const availableLines = termHeight - headerLines - footerLines;
|
|
1403
|
+
// Calculate total content height and visible window
|
|
1404
|
+
const totalLines = lines.length;
|
|
1405
|
+
const maxScrollOffset = Math.max(0, totalLines - availableLines);
|
|
1406
|
+
// Clamp scroll offset
|
|
1407
|
+
const clampedOffset = Math.min(scrollOffset, maxScrollOffset);
|
|
1408
|
+
// Display visible portion of content
|
|
1409
|
+
const visibleLines = lines.slice(clampedOffset, clampedOffset + availableLines);
|
|
1410
|
+
for (const line of visibleLines) {
|
|
1411
|
+
console.log(line);
|
|
1412
|
+
}
|
|
1413
|
+
// Show scroll indicator if content exceeds visible area
|
|
1414
|
+
if (totalLines > availableLines) {
|
|
1415
|
+
const scrollPercentage = Math.round((clampedOffset / maxScrollOffset) * 100);
|
|
1416
|
+
const scrollIndicator = chalk_1.default.dim(`[${clampedOffset + 1}-${Math.min(clampedOffset + availableLines, totalLines)}/${totalLines}] ${scrollPercentage}% ↑↓ to scroll`);
|
|
1417
|
+
console.log('');
|
|
1418
|
+
console.log(scrollIndicator);
|
|
974
1419
|
}
|
|
975
|
-
// Actions
|
|
976
|
-
console.log('');
|
|
977
|
-
console.log(chalk_1.default.dim('─'.repeat(lineWidth)));
|
|
978
|
-
console.log(chalk_1.default.bold(' Quick Actions'));
|
|
979
|
-
console.log('');
|
|
980
|
-
console.log(chalk_1.default.dim(' Press: ') +
|
|
981
|
-
chalk_1.default.cyan('[a]') + chalk_1.default.dim(' Attach ') +
|
|
982
|
-
chalk_1.default.cyan('[l]') + chalk_1.default.dim(' Logs ') +
|
|
983
|
-
chalk_1.default.cyan('[s]') + chalk_1.default.dim(' Send prompt ') +
|
|
984
|
-
chalk_1.default.cyan('[Esc]') + chalk_1.default.dim(' Back'));
|
|
985
|
-
console.log('');
|
|
986
1420
|
}
|
|
987
1421
|
/**
|
|
988
1422
|
* Render detail view footer
|
|
@@ -1099,8 +1533,8 @@ function processTerminalOutput(output, maxLines) {
|
|
|
1099
1533
|
return lines;
|
|
1100
1534
|
}
|
|
1101
1535
|
/**
|
|
1102
|
-
* Process terminal output for full display -
|
|
1103
|
-
* This is used for live view where we want
|
|
1536
|
+
* Process terminal output for full display - aggressive deduplication of status lines
|
|
1537
|
+
* This is used for live view where we want clean, readable output
|
|
1104
1538
|
*/
|
|
1105
1539
|
function processTerminalOutputFull(output, maxLines) {
|
|
1106
1540
|
// Remove script headers
|
|
@@ -1152,48 +1586,100 @@ function processTerminalOutputFull(output, maxLines) {
|
|
|
1152
1586
|
.replace(/\[\d*J/g, '')
|
|
1153
1587
|
.replace(/\r\n/g, '\n')
|
|
1154
1588
|
.replace(/\r/g, '\n')
|
|
1155
|
-
.replace(/\n{
|
|
1589
|
+
.replace(/\n{3,}/g, '\n\n'); // Reduce multiple blank lines
|
|
1156
1590
|
// Split into lines
|
|
1157
1591
|
const rawLines = lastFrame.split('\n');
|
|
1158
|
-
// Create a signature for deduplication
|
|
1159
|
-
// Status lines like "
|
|
1592
|
+
// Create a signature for deduplication - extract just the core content
|
|
1593
|
+
// Status lines like "* Fixing foo... (esc to interrupt · 14m 54s · ↓ 11.4k tokens · thinking)"
|
|
1594
|
+
// should all dedupe to the same signature
|
|
1160
1595
|
const getLineSignature = (line) => {
|
|
1161
|
-
|
|
1596
|
+
let sig = line
|
|
1162
1597
|
// Remove ANSI codes for comparison
|
|
1163
1598
|
.replace(/\x1B\[[0-9;]*m/g, '')
|
|
1164
|
-
// Remove
|
|
1165
|
-
.replace(
|
|
1599
|
+
// Remove leading spinner characters (*, +, ·, -, etc.)
|
|
1600
|
+
.replace(/^[*+·\-•○●◐◑◒◓⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s*/, '')
|
|
1601
|
+
// Remove everything in parentheses (timing, token counts, state, etc.)
|
|
1602
|
+
.replace(/\([^)]*\)/g, '')
|
|
1603
|
+
// Remove timing info anywhere: 14m 54s, 43s, etc.
|
|
1166
1604
|
.replace(/\d+m\s*\d*s?/g, '')
|
|
1167
|
-
|
|
1605
|
+
.replace(/\d+s\b/g, '')
|
|
1606
|
+
// Remove token counts: ↓ 11.4k tokens, 1.2k, etc.
|
|
1607
|
+
.replace(/[↓↑]\s*[\d.]+k?\s*tokens?/gi, '')
|
|
1168
1608
|
.replace(/[\d.]+k\s*tokens?/gi, '')
|
|
1169
|
-
// Remove
|
|
1170
|
-
.replace(/[
|
|
1609
|
+
// Remove special status characters
|
|
1610
|
+
.replace(/[↓↑·│├└─]/g, '')
|
|
1611
|
+
// Remove "timeout: Xm Xs" pattern
|
|
1612
|
+
.replace(/timeout:\s*\d+m?\s*\d*s?/gi, '')
|
|
1171
1613
|
// Normalize whitespace
|
|
1172
1614
|
.replace(/\s+/g, ' ')
|
|
1173
1615
|
.trim();
|
|
1616
|
+
return sig;
|
|
1174
1617
|
};
|
|
1175
|
-
//
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1618
|
+
// Check if a line is just noise (empty, only punctuation, or prompt chars)
|
|
1619
|
+
const isNoiseLine = (line) => {
|
|
1620
|
+
const stripped = line
|
|
1621
|
+
.replace(/\x1B\[[0-9;]*m/g, '')
|
|
1622
|
+
.trim();
|
|
1623
|
+
// Empty or just whitespace
|
|
1624
|
+
if (!stripped)
|
|
1625
|
+
return true;
|
|
1626
|
+
// Just prompt characters like ">", "$ ", etc.
|
|
1627
|
+
if (/^[>$%#│├└─\s]*$/.test(stripped))
|
|
1628
|
+
return true;
|
|
1629
|
+
// Just a single character
|
|
1630
|
+
if (stripped.length <= 1)
|
|
1631
|
+
return true;
|
|
1632
|
+
return false;
|
|
1633
|
+
};
|
|
1634
|
+
// Build result keeping only unique meaningful lines (latest occurrence)
|
|
1184
1635
|
const lines = [];
|
|
1185
|
-
const usedSignatures =
|
|
1186
|
-
// Process in reverse to keep the LAST occurrence
|
|
1636
|
+
const usedSignatures = []; // Array of signatures we've seen
|
|
1637
|
+
// Process in reverse to keep the LAST occurrence of each unique line
|
|
1187
1638
|
for (let i = rawLines.length - 1; i >= 0 && lines.length < maxLines; i--) {
|
|
1188
1639
|
const line = rawLines[i];
|
|
1640
|
+
// Skip noise lines
|
|
1641
|
+
if (isNoiseLine(line))
|
|
1642
|
+
continue;
|
|
1189
1643
|
const sig = getLineSignature(line);
|
|
1190
|
-
//
|
|
1191
|
-
if (sig.length
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1644
|
+
// Skip completely empty signatures
|
|
1645
|
+
if (!sig || sig.length === 0) {
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
// Check for exact match or similar prefix (aggressive deduplication)
|
|
1649
|
+
// Use shorter prefix match (30 chars) to catch more duplicates
|
|
1650
|
+
const prefixLen = Math.min(30, sig.length);
|
|
1651
|
+
const shortSig = sig.substring(0, prefixLen);
|
|
1652
|
+
let isDuplicate = false;
|
|
1653
|
+
for (const existingSig of usedSignatures) {
|
|
1654
|
+
// Exact match
|
|
1655
|
+
if (existingSig === sig) {
|
|
1656
|
+
isDuplicate = true;
|
|
1657
|
+
break;
|
|
1195
1658
|
}
|
|
1659
|
+
// Prefix match - if both sigs are long enough, compare first 30 chars
|
|
1660
|
+
if (existingSig.length >= prefixLen && sig.length >= prefixLen) {
|
|
1661
|
+
if (existingSig.substring(0, prefixLen) === shortSig) {
|
|
1662
|
+
isDuplicate = true;
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
// Fuzzy match - check if one sig contains the other (for very similar lines)
|
|
1667
|
+
if (sig.length > 10 && existingSig.length > 10) {
|
|
1668
|
+
if (existingSig.includes(sig) || sig.includes(existingSig)) {
|
|
1669
|
+
isDuplicate = true;
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
if (isDuplicate) {
|
|
1675
|
+
continue;
|
|
1196
1676
|
}
|
|
1677
|
+
lines.unshift(line);
|
|
1678
|
+
usedSignatures.push(sig);
|
|
1679
|
+
}
|
|
1680
|
+
// If we filtered too aggressively and have nothing, return original (limited)
|
|
1681
|
+
if (lines.length === 0 && rawLines.length > 0) {
|
|
1682
|
+
return rawLines.filter(l => !isNoiseLine(l)).slice(-maxLines);
|
|
1197
1683
|
}
|
|
1198
1684
|
return lines;
|
|
1199
1685
|
}
|
|
@@ -1262,6 +1748,7 @@ function renderLiveView(session, output, activityLog, termHeight, termWidth) {
|
|
|
1262
1748
|
}
|
|
1263
1749
|
// Footer at bottom - compact status bar
|
|
1264
1750
|
const controls = chalk_1.default.dim('[Esc]') + ' Back ' +
|
|
1751
|
+
chalk_1.default.dim('[m]') + ' Messages ' +
|
|
1265
1752
|
chalk_1.default.dim('[l]') + ' Logs ' +
|
|
1266
1753
|
chalk_1.default.dim('[a]') + ' Attach ' +
|
|
1267
1754
|
chalk_1.default.dim('[s]') + ' Send ' +
|
|
@@ -1527,9 +2014,13 @@ async function runTuiMode(options) {
|
|
|
1527
2014
|
let liveOutputCache = '';
|
|
1528
2015
|
let liveActivityCache = '';
|
|
1529
2016
|
let liveViewPoller = null;
|
|
2017
|
+
let messagesCache = [];
|
|
2018
|
+
let messagesViewPoller = null;
|
|
2019
|
+
let messagesScrollOffset = 0;
|
|
1530
2020
|
let logsCache = [];
|
|
1531
2021
|
let logsViewPoller = null;
|
|
1532
2022
|
let logsScrollOffset = 0; // For pagination - 0 means show latest
|
|
2023
|
+
let detailScrollOffset = 0; // For detail view scrolling - 0 means show from top
|
|
1533
2024
|
const filters = {
|
|
1534
2025
|
provider: options.provider,
|
|
1535
2026
|
status: options.status,
|
|
@@ -1575,14 +2066,17 @@ async function runTuiMode(options) {
|
|
|
1575
2066
|
const termWidth = process.stdout.columns || 80;
|
|
1576
2067
|
// Move cursor to home and clear screen
|
|
1577
2068
|
process.stdout.write('\x1B[H\x1B[J');
|
|
1578
|
-
if (viewMode === '
|
|
2069
|
+
if (viewMode === 'messages') {
|
|
2070
|
+
renderMessagesView(cachedSessions[selectedIndex], messagesCache, messagesScrollOffset, termHeight, termWidth);
|
|
2071
|
+
}
|
|
2072
|
+
else if (viewMode === 'live') {
|
|
1579
2073
|
renderLiveView(cachedSessions[selectedIndex], liveOutputCache, liveActivityCache, termHeight, termWidth);
|
|
1580
2074
|
}
|
|
1581
2075
|
else if (viewMode === 'logs') {
|
|
1582
2076
|
renderLogsView(cachedSessions[selectedIndex], logsCache, logsScrollOffset, termHeight, termWidth);
|
|
1583
2077
|
}
|
|
1584
2078
|
else if (viewMode === 'detail') {
|
|
1585
|
-
renderDetailView(cachedSessions[selectedIndex], termHeight, termWidth);
|
|
2079
|
+
renderDetailView(cachedSessions[selectedIndex], detailScrollOffset, termHeight, termWidth);
|
|
1586
2080
|
renderDetailFooter(termWidth);
|
|
1587
2081
|
}
|
|
1588
2082
|
else {
|
|
@@ -1687,6 +2181,51 @@ async function runTuiMode(options) {
|
|
|
1687
2181
|
logsViewPoller = null;
|
|
1688
2182
|
}
|
|
1689
2183
|
};
|
|
2184
|
+
// Start messages polling for current session
|
|
2185
|
+
const startMessagesPolling = () => {
|
|
2186
|
+
if (messagesViewPoller)
|
|
2187
|
+
return;
|
|
2188
|
+
const session = cachedSessions[selectedIndex];
|
|
2189
|
+
if (!session || !session.genbox)
|
|
2190
|
+
return;
|
|
2191
|
+
messagesViewPoller = setInterval(async () => {
|
|
2192
|
+
if (viewMode !== 'messages') {
|
|
2193
|
+
stopMessagesPolling();
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
try {
|
|
2197
|
+
const messages = await queryGenboxDaemonMessages(session.genbox, 'latest', 200);
|
|
2198
|
+
if (messages && messages.length > 0) {
|
|
2199
|
+
// Only update if at latest (not scrolled up) or if new messages
|
|
2200
|
+
if (messagesScrollOffset === 0 || messages.length > messagesCache.length) {
|
|
2201
|
+
messagesCache = messages;
|
|
2202
|
+
quickRender();
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
catch {
|
|
2207
|
+
// Ignore errors, keep trying
|
|
2208
|
+
}
|
|
2209
|
+
}, 2000);
|
|
2210
|
+
// Fetch immediately
|
|
2211
|
+
(async () => {
|
|
2212
|
+
try {
|
|
2213
|
+
const messages = await queryGenboxDaemonMessages(session.genbox, 'latest', 200);
|
|
2214
|
+
messagesCache = messages || [];
|
|
2215
|
+
quickRender();
|
|
2216
|
+
}
|
|
2217
|
+
catch {
|
|
2218
|
+
// Ignore
|
|
2219
|
+
}
|
|
2220
|
+
})();
|
|
2221
|
+
};
|
|
2222
|
+
// Stop messages polling
|
|
2223
|
+
const stopMessagesPolling = () => {
|
|
2224
|
+
if (messagesViewPoller) {
|
|
2225
|
+
clearInterval(messagesViewPoller);
|
|
2226
|
+
messagesViewPoller = null;
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
1690
2229
|
// Quick render: just redraw UI with cached data (instant, for navigation)
|
|
1691
2230
|
const quickRender = () => {
|
|
1692
2231
|
// Clamp selected index
|
|
@@ -1743,6 +2282,7 @@ async function runTuiMode(options) {
|
|
|
1743
2282
|
const cleanup = () => {
|
|
1744
2283
|
stopLivePolling();
|
|
1745
2284
|
stopLogsPolling();
|
|
2285
|
+
stopMessagesPolling();
|
|
1746
2286
|
if (process.stdin.isTTY) {
|
|
1747
2287
|
process.stdin.setRawMode(false);
|
|
1748
2288
|
}
|
|
@@ -1866,11 +2406,124 @@ async function runTuiMode(options) {
|
|
|
1866
2406
|
quickRender();
|
|
1867
2407
|
return;
|
|
1868
2408
|
}
|
|
2409
|
+
else if (key === 'm') { // Switch to messages view (JSONL based)
|
|
2410
|
+
stopLivePolling();
|
|
2411
|
+
viewMode = 'messages';
|
|
2412
|
+
messagesCache = [];
|
|
2413
|
+
startMessagesPolling();
|
|
2414
|
+
quickRender();
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
// Handle messages view mode
|
|
2420
|
+
if (viewMode === 'messages') {
|
|
2421
|
+
if (key === '\u001b' || key === '\u007f' || key === '\u001b[D') { // Escape, Backspace, or Left arrow
|
|
2422
|
+
stopMessagesPolling();
|
|
2423
|
+
viewMode = 'list';
|
|
2424
|
+
quickRender();
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
else if (key === 'q' || key === '\u0003') { // q or Ctrl+C
|
|
2428
|
+
stopMessagesPolling();
|
|
2429
|
+
running = false;
|
|
2430
|
+
cleanup();
|
|
2431
|
+
process.exit(0);
|
|
2432
|
+
}
|
|
2433
|
+
else if (key === 'r') { // Refresh messages
|
|
2434
|
+
const session = cachedSessions[selectedIndex];
|
|
2435
|
+
if (session && session.genbox) {
|
|
2436
|
+
messagesScrollOffset = 0; // Reset to latest on refresh
|
|
2437
|
+
(async () => {
|
|
2438
|
+
try {
|
|
2439
|
+
const messages = await queryGenboxDaemonMessages(session.genbox, 'latest', 200);
|
|
2440
|
+
messagesCache = messages || [];
|
|
2441
|
+
quickRender();
|
|
2442
|
+
}
|
|
2443
|
+
catch {
|
|
2444
|
+
// Ignore
|
|
2445
|
+
}
|
|
2446
|
+
})();
|
|
2447
|
+
}
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
else if (key === '\u001b[A') { // Up arrow - scroll up (older)
|
|
2451
|
+
const totalLines = messagesCache.reduce((acc, msg) => acc + formatMessageLine(msg, process.stdout.columns || 80).length, 0);
|
|
2452
|
+
messagesScrollOffset = Math.min(messagesScrollOffset + 5, Math.max(0, totalLines - 5));
|
|
2453
|
+
quickRender();
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
else if (key === '\u001b[B') { // Down arrow - scroll down (newer)
|
|
2457
|
+
messagesScrollOffset = Math.max(0, messagesScrollOffset - 5);
|
|
2458
|
+
quickRender();
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
else if (key === '\u001b[H' || key === '\u001b[1~') { // Home - jump to oldest
|
|
2462
|
+
const totalLines = messagesCache.reduce((acc, msg) => acc + formatMessageLine(msg, process.stdout.columns || 80).length, 0);
|
|
2463
|
+
messagesScrollOffset = Math.max(0, totalLines - 10);
|
|
2464
|
+
quickRender();
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
else if (key === '\u001b[F' || key === '\u001b[4~') { // End - jump to latest
|
|
2468
|
+
messagesScrollOffset = 0;
|
|
2469
|
+
quickRender();
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
else if (key === 's') { // Send prompt
|
|
2473
|
+
const session = lastSessions[selectedIndex];
|
|
2474
|
+
if (session) {
|
|
2475
|
+
stopMessagesPolling();
|
|
2476
|
+
cleanup();
|
|
2477
|
+
console.log(chalk_1.default.cyan(`\nSending prompt to ${session.name}...`));
|
|
2478
|
+
try {
|
|
2479
|
+
(0, child_process_1.execSync)(`gb session send ${session.name} -e`, { stdio: 'inherit' });
|
|
2480
|
+
}
|
|
2481
|
+
catch {
|
|
2482
|
+
// Session might have ended
|
|
2483
|
+
}
|
|
2484
|
+
process.exit(0);
|
|
2485
|
+
}
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
else if (key === 'l') { // Switch to logs view from messages view
|
|
2489
|
+
stopMessagesPolling();
|
|
2490
|
+
logsScrollOffset = 0;
|
|
2491
|
+
viewMode = 'logs';
|
|
2492
|
+
logsCache = [];
|
|
2493
|
+
startLogsPolling();
|
|
2494
|
+
quickRender();
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
else if (key === 't') { // Switch to terminal view (live dtach)
|
|
2498
|
+
stopMessagesPolling();
|
|
2499
|
+
viewMode = 'live';
|
|
2500
|
+
liveOutputCache = '';
|
|
2501
|
+
startLivePolling();
|
|
2502
|
+
quickRender();
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
else if (key === 'a') { // Attach from messages view
|
|
2506
|
+
const session = lastSessions[selectedIndex];
|
|
2507
|
+
if (session) {
|
|
2508
|
+
stopMessagesPolling();
|
|
2509
|
+
cleanup();
|
|
2510
|
+
console.log(chalk_1.default.cyan(`\nAttaching to ${session.name}...`));
|
|
2511
|
+
try {
|
|
2512
|
+
(0, child_process_1.execSync)(`gb session attach ${session.name}`, { stdio: 'inherit' });
|
|
2513
|
+
}
|
|
2514
|
+
catch {
|
|
2515
|
+
// Session might have ended
|
|
2516
|
+
}
|
|
2517
|
+
process.exit(0);
|
|
2518
|
+
}
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
1869
2521
|
return;
|
|
1870
2522
|
}
|
|
1871
2523
|
// Handle detail view mode
|
|
1872
2524
|
if (viewMode === 'detail') {
|
|
1873
2525
|
if (key === '\u001b' || key === '\u007f' || key === '\u001b[D') { // Escape, Backspace, or Left arrow
|
|
2526
|
+
detailScrollOffset = 0; // Reset scroll on exit
|
|
1874
2527
|
viewMode = 'list';
|
|
1875
2528
|
quickRender();
|
|
1876
2529
|
return;
|
|
@@ -1880,7 +2533,40 @@ async function runTuiMode(options) {
|
|
|
1880
2533
|
cleanup();
|
|
1881
2534
|
process.exit(0);
|
|
1882
2535
|
}
|
|
2536
|
+
else if (key === '\u001b[A') { // Up arrow - scroll up
|
|
2537
|
+
detailScrollOffset = Math.max(0, detailScrollOffset - 1);
|
|
2538
|
+
quickRender();
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
else if (key === '\u001b[B') { // Down arrow - scroll down
|
|
2542
|
+
detailScrollOffset = detailScrollOffset + 1;
|
|
2543
|
+
quickRender();
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
else if (key === '\u001b[5~') { // Page Up - scroll up one page
|
|
2547
|
+
const pageSize = Math.max(10, (process.stdout.rows || 24) - 10);
|
|
2548
|
+
detailScrollOffset = Math.max(0, detailScrollOffset - pageSize);
|
|
2549
|
+
quickRender();
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
else if (key === '\u001b[6~') { // Page Down - scroll down one page
|
|
2553
|
+
const pageSize = Math.max(10, (process.stdout.rows || 24) - 10);
|
|
2554
|
+
detailScrollOffset = detailScrollOffset + pageSize;
|
|
2555
|
+
quickRender();
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
else if (key === '\u001b[H' || key === '\u001bOH' || key === '\u001b[1~') { // Home - jump to top
|
|
2559
|
+
detailScrollOffset = 0;
|
|
2560
|
+
quickRender();
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
else if (key === '\u001b[F' || key === '\u001bOF' || key === '\u001b[4~') { // End - jump to bottom
|
|
2564
|
+
detailScrollOffset = 999999; // Will be clamped in renderDetailView
|
|
2565
|
+
quickRender();
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
1883
2568
|
else if (key === 'r') { // Refresh
|
|
2569
|
+
detailScrollOffset = 0;
|
|
1884
2570
|
await render(true); // Force refresh
|
|
1885
2571
|
}
|
|
1886
2572
|
else if (key === 'a') { // Attach from detail view
|
|
@@ -1921,6 +2607,16 @@ async function runTuiMode(options) {
|
|
|
1921
2607
|
process.exit(0);
|
|
1922
2608
|
}
|
|
1923
2609
|
}
|
|
2610
|
+
else if (key === 'm') { // Messages view from detail view
|
|
2611
|
+
const session = cachedSessions[selectedIndex];
|
|
2612
|
+
if (session && session.genbox) {
|
|
2613
|
+
viewMode = 'messages';
|
|
2614
|
+
messagesCache = [];
|
|
2615
|
+
startMessagesPolling();
|
|
2616
|
+
quickRender();
|
|
2617
|
+
}
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
1924
2620
|
return;
|
|
1925
2621
|
}
|
|
1926
2622
|
// Handle logs view mode
|
|
@@ -2013,23 +2709,35 @@ async function runTuiMode(options) {
|
|
|
2013
2709
|
cleanup();
|
|
2014
2710
|
process.exit(0);
|
|
2015
2711
|
}
|
|
2016
|
-
else if (key === '\r') { // Enter - show
|
|
2712
|
+
else if (key === '\r') { // Enter - show messages view (JSONL based)
|
|
2017
2713
|
const session = cachedSessions[selectedIndex];
|
|
2018
2714
|
if (session && session.genbox) {
|
|
2019
|
-
viewMode = '
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2715
|
+
viewMode = 'messages';
|
|
2716
|
+
messagesScrollOffset = 0;
|
|
2717
|
+
messagesCache = [];
|
|
2718
|
+
startMessagesPolling();
|
|
2023
2719
|
quickRender();
|
|
2024
2720
|
}
|
|
2025
2721
|
else if (session) {
|
|
2026
2722
|
// Local session - fall back to detail view
|
|
2723
|
+
detailScrollOffset = 0;
|
|
2027
2724
|
viewMode = 'detail';
|
|
2028
2725
|
quickRender();
|
|
2029
2726
|
}
|
|
2030
2727
|
}
|
|
2728
|
+
else if (key === 't') { // 't' for terminal/live view
|
|
2729
|
+
const session = cachedSessions[selectedIndex];
|
|
2730
|
+
if (session && session.genbox) {
|
|
2731
|
+
viewMode = 'live';
|
|
2732
|
+
liveOutputCache = '';
|
|
2733
|
+
liveActivityCache = '';
|
|
2734
|
+
startLivePolling();
|
|
2735
|
+
quickRender();
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2031
2738
|
else if (key === 'd') { // 'd' for detail view
|
|
2032
2739
|
if (cachedSessions[selectedIndex]) {
|
|
2740
|
+
detailScrollOffset = 0;
|
|
2033
2741
|
viewMode = 'detail';
|
|
2034
2742
|
quickRender();
|
|
2035
2743
|
}
|
|
@@ -2070,6 +2778,7 @@ async function runTuiMode(options) {
|
|
|
2070
2778
|
}
|
|
2071
2779
|
else if (session) {
|
|
2072
2780
|
// Local session without genbox - fall back to detail view
|
|
2781
|
+
detailScrollOffset = 0;
|
|
2073
2782
|
viewMode = 'detail';
|
|
2074
2783
|
quickRender();
|
|
2075
2784
|
}
|