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.
@@ -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
- cloudGenboxes = (Array.isArray(response) ? response : response?.genboxes || [])
468
- .filter((g) => g.status === 'running');
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
- '[⏎] Live',
899
- '[d] Details',
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
- // Session name and ID
922
- const statusIcon = session.status === 'running' ? chalk_1.default.green('●') :
923
- session.status === 'idle' ? chalk_1.default.yellow('●') :
924
- session.status === 'stopped' ? chalk_1.default.red('●') : chalk_1.default.dim('○');
925
- const stateIcon = session.state === 'thinking' ? '💭' :
926
- session.state === 'executing_tool' ? '⚙️' :
927
- session.state === 'waiting_for_input' ? '⌛' :
928
- session.state === 'responding' ? '📝' : '';
929
- console.log(chalk_1.default.bold(` ${session.name}`) + chalk_1.default.dim(` (${session.id})`));
930
- console.log('');
931
- // Two-column layout
932
- const col1Width = 30;
933
- const col2Width = 40;
934
- const rows = [
935
- [chalk_1.default.dim('Provider:'), chalk_1.default.magenta(session.provider), chalk_1.default.dim('Status:'), `${statusIcon} ${session.status}`],
936
- [chalk_1.default.dim('Type:'), session.type, chalk_1.default.dim('State:'), session.state ? `${stateIcon} ${session.state}` : chalk_1.default.dim('--')],
937
- [chalk_1.default.dim('Genbox:'), chalk_1.default.cyan(session.genbox), chalk_1.default.dim('Duration:'), session.duration || '--'],
938
- [chalk_1.default.dim('Created:'), session.createdAt ? new Date(session.createdAt).toLocaleString() : '--', '', ''],
939
- ];
940
- for (const row of rows) {
941
- const left = ` ${row[0].padEnd(12)} ${String(row[1]).padEnd(col1Width - 14)}`;
942
- const right = row[2] ? `${row[2].padEnd(12)} ${row[3]}` : '';
943
- console.log(left + right);
944
- }
945
- console.log('');
946
- console.log(chalk_1.default.dim(''.repeat(lineWidth)));
947
- console.log(chalk_1.default.bold(' Metrics'));
948
- console.log('');
949
- // Metrics section
950
- const metricsRows = [
951
- [chalk_1.default.dim('Messages:'), `${session.messagesCount || 0}`, chalk_1.default.dim('Tool Calls:'), `${session.toolCalls || 0}`],
952
- [chalk_1.default.dim('Input Tokens:'), session.inputTokens ? session.inputTokens.toLocaleString() : '--', chalk_1.default.dim('Output Tokens:'), session.outputTokens ? session.outputTokens.toLocaleString() : '--'],
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(' Current Tool'));
966
- console.log(` ${chalk_1.default.cyan(session.currentTool)}`);
967
- }
968
- // Last message preview
969
- if (session.lastMessage) {
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(' Last Message'));
973
- console.log(chalk_1.default.dim(` "${session.lastMessage}..."`));
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 - minimal deduplication, preserve all content
1103
- * This is used for live view where we want to show as much as possible
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{4,}/g, '\n\n\n'); // Keep some blank lines for spacing
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 that ignores timestamps and token counts
1159
- // Status lines like "Computing... (esc to interrupt · 43s · ↓ 1.2k tokens)" should dedupe
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
- return line
1596
+ let sig = line
1162
1597
  // Remove ANSI codes for comparison
1163
1598
  .replace(/\x1B\[[0-9;]*m/g, '')
1164
- // Remove timing info (43s, 1m 30s, etc.)
1165
- .replace(/\d+s\b/g, '')
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
- // Remove token counts (1.2k, 20.0k, etc.)
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 arrows and special chars used in status
1170
- .replace(/[↓↑·]/g, '')
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
- // Deduplicate similar lines (keep only the last occurrence of each pattern)
1176
- const seenSignatures = new Map(); // signature -> last index
1177
- for (let i = 0; i < rawLines.length; i++) {
1178
- const sig = getLineSignature(rawLines[i]);
1179
- if (sig.length > 5) { // Only dedupe lines with meaningful content
1180
- seenSignatures.set(sig, i);
1181
- }
1182
- }
1183
- // Build result keeping only unique lines or last occurrence of duplicates
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 = new Set();
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
- // Keep if: short signature (spacing), empty, or first time seeing this signature
1191
- if (sig.length <= 5 || !usedSignatures.has(sig)) {
1192
- lines.unshift(line);
1193
- if (sig.length > 5) {
1194
- usedSignatures.add(sig);
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 === 'live') {
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 live terminal output in TUI
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 = 'live';
2020
- liveOutputCache = '';
2021
- liveActivityCache = '';
2022
- startLivePolling();
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.226",
3
+ "version": "1.0.227",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {