genbox 1.0.212 → 1.0.213

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