vibecodingmachine-cli 2025.12.25-25 → 2026.1.22-1441

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.
Files changed (52) hide show
  1. package/__tests__/antigravity-js-handler.test.js +23 -0
  2. package/__tests__/provider-manager.test.js +84 -0
  3. package/__tests__/provider-rate-cache.test.js +27 -0
  4. package/bin/vibecodingmachine.js +92 -118
  5. package/logs/audit/2025-12-27.jsonl +1 -0
  6. package/logs/audit/2026-01-03.jsonl +2 -0
  7. package/package.json +2 -2
  8. package/reset_provider_order.js +21 -0
  9. package/scripts/convert-requirements.js +35 -0
  10. package/scripts/debug-parse.js +24 -0
  11. package/src/commands/auth.js +5 -1
  12. package/src/commands/auto-direct.js +747 -182
  13. package/src/commands/auto.js +206 -48
  14. package/src/commands/computers.js +9 -0
  15. package/src/commands/feature.js +123 -0
  16. package/src/commands/ide.js +108 -3
  17. package/src/commands/repo.js +27 -22
  18. package/src/commands/requirements-remote.js +34 -2
  19. package/src/commands/requirements.js +129 -9
  20. package/src/commands/setup.js +2 -1
  21. package/src/commands/status.js +39 -1
  22. package/src/commands/sync.js +7 -1
  23. package/src/utils/antigravity-js-handler.js +13 -4
  24. package/src/utils/auth.js +56 -25
  25. package/src/utils/compliance-check.js +10 -0
  26. package/src/utils/config.js +42 -1
  27. package/src/utils/date-formatter.js +44 -0
  28. package/src/utils/first-run.js +8 -6
  29. package/src/utils/interactive.js +1363 -334
  30. package/src/utils/kiro-js-handler.js +188 -0
  31. package/src/utils/prompt-helper.js +64 -0
  32. package/src/utils/provider-rate-cache.js +31 -0
  33. package/src/utils/provider-registry.js +42 -1
  34. package/src/utils/requirements-converter.js +107 -0
  35. package/src/utils/requirements-parser.js +144 -0
  36. package/tests/antigravity-js-handler.test.js +23 -0
  37. package/tests/home-bootstrap.test.js +76 -0
  38. package/tests/integration/health-tracking.integration.test.js +284 -0
  39. package/tests/provider-manager.test.js +92 -0
  40. package/tests/rate-limit-display.test.js +44 -0
  41. package/tests/requirements-bullet-parsing.test.js +15 -0
  42. package/tests/requirements-converter.test.js +42 -0
  43. package/tests/requirements-heading-count.test.js +27 -0
  44. package/tests/requirements-legacy-parsing.test.js +15 -0
  45. package/tests/requirements-parse-integration.test.js +44 -0
  46. package/tests/wait-for-ide-completion.test.js +56 -0
  47. package/tests/wait-for-ide-quota-detection-cursor-screenshot.test.js +61 -0
  48. package/tests/wait-for-ide-quota-detection-cursor.test.js +60 -0
  49. package/tests/wait-for-ide-quota-detection-negative.test.js +45 -0
  50. package/tests/wait-for-ide-quota-detection.test.js +59 -0
  51. package/verify_fix.js +36 -0
  52. package/verify_ui.js +38 -0
@@ -5,11 +5,12 @@ const path = require('path');
5
5
  const os = require('os');
6
6
  const fs = require('fs-extra');
7
7
  const readline = require('readline');
8
+ const { execSync } = require('child_process');
8
9
  const repo = require('../commands/repo');
9
10
  const auto = require('../commands/auto');
10
11
  const status = require('../commands/status');
11
12
  const requirements = require('../commands/requirements');
12
- const { getRepoPath, readConfig, writeConfig, getAutoConfig } = require('./config');
13
+ const { getRepoPath, readConfig, writeConfig, getAutoConfig, getProviderCache, setProviderCache } = require('./config');
13
14
  const { getProviderPreferences, saveProviderPreferences, getProviderDefinitions } = require('../utils/provider-registry');
14
15
  const { checkAutoModeStatus } = require('./auto-mode');
15
16
  const {
@@ -20,14 +21,127 @@ const {
20
21
  isComputerNameEnabled,
21
22
  t,
22
23
  detectLocale,
23
- setLocale
24
+ setLocale,
25
+ AppleScriptManager,
26
+ IDEHealthTracker,
27
+ HealthReporter
24
28
  } = require('vibecodingmachine-core');
29
+ const { promptWithDefaultsOnce } = require('./prompt-helper');
30
+ const { formatResetsAtLabel } = require('./date-formatter');
25
31
 
26
32
  // Initialize locale detection for interactive mode
27
33
  const detectedLocale = detectLocale();
28
34
  setLocale(detectedLocale);
29
35
  const pkg = require('../../package.json');
30
36
 
37
+ const { exec } = require('child_process');
38
+ const util = require('util');
39
+ const execAsync = util.promisify(exec);
40
+
41
+ async function checkVSCodeExtension(extensionId) {
42
+ // Check if a VS Code extension is installed
43
+ try {
44
+ const { stdout } = await execAsync('code --list-extensions', { timeout: 5000 });
45
+ const extensions = stdout.toLowerCase().split('\n').map(e => e.trim());
46
+
47
+ // Map common extension IDs to their actual extension IDs
48
+ const extensionMap = {
49
+ 'github-copilot': 'github.copilot',
50
+ 'amazon-q': 'amazonwebservices.amazon-q-vscode'
51
+ };
52
+
53
+ const actualExtensionId = extensionMap[extensionId] || extensionId;
54
+ return extensions.includes(actualExtensionId.toLowerCase());
55
+ } catch (error) {
56
+ // If code CLI is not available or command fails, return false
57
+ return false;
58
+ }
59
+ }
60
+
61
+ async function checkAppOrBinary(names = [], binaries = []) {
62
+ // names: app bundle base names (e.g., 'Cursor' -> /Applications/Cursor.app)
63
+ // binaries: CLI binary names to check on PATH (e.g., 'code')
64
+ const platform = os.platform();
65
+ // Check common application directories
66
+ if (platform === 'darwin') {
67
+ const appDirs = ['/Applications', path.join(os.homedir(), 'Applications')];
68
+ for (const appName of names) {
69
+ for (const dir of appDirs) {
70
+ try {
71
+ const p = path.join(dir, `${appName}.app`);
72
+ if (await fs.pathExists(p)) {
73
+ // Ensure this is a real application bundle (has Contents/MacOS executable)
74
+ try {
75
+ const macosDir = path.join(p, 'Contents', 'MacOS');
76
+ const exists = await fs.pathExists(macosDir);
77
+ if (exists) {
78
+ const files = await fs.readdir(macosDir);
79
+ if (files && files.length > 0) {
80
+ // Prefer to ensure the app is usable: use spctl to assess, fallback to quarantine xattr
81
+ try {
82
+ // spctl returns non-zero on rejected/invalid apps
83
+ // Use async exec to avoid blocking the event loop
84
+ await execAsync(`spctl --assess -v "${p}"`, { timeout: 5000 });
85
+
86
+ // additionally validate codesign quickly (timeout to avoid hangs)
87
+ try {
88
+ await execAsync(`codesign -v --deep --strict "${p}"`, { timeout: 5000 });
89
+ return true;
90
+ } catch (csErr) {
91
+ // codesign failed or timed out — treat as not usable/damaged
92
+ return false;
93
+ }
94
+ } catch (e) {
95
+ // spctl failed or timed out — check if app has quarantine attribute
96
+ try {
97
+ const { stdout } = await execAsync(`xattr -p com.apple.quarantine "${p}" 2>/dev/null || true`, { encoding: 'utf8' });
98
+ const out = stdout.trim();
99
+ if (!out) {
100
+ // no quarantine attribute but spctl failed — be conservative and treat as not installed
101
+ return false;
102
+ }
103
+ // If quarantine attribute exists, treat as not installed (damaged/not allowed)
104
+ return false;
105
+ } catch (e2) {
106
+ return false;
107
+ }
108
+ }
109
+ }
110
+ }
111
+ } catch (e) {
112
+ // if we can't stat inside, be conservative and continue searching
113
+ }
114
+ }
115
+ } catch (e) {
116
+ /* ignore */
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ // Check PATH for known binaries
123
+ for (const bin of binaries) {
124
+ try {
125
+ await execAsync(`which ${bin}`, { timeout: 2000 });
126
+ return true;
127
+ } catch (e) {
128
+ /* not found */
129
+ }
130
+ }
131
+
132
+ // Check common Homebrew bin locations
133
+ const brewPaths = ['/opt/homebrew/bin', '/usr/local/bin'];
134
+ for (const bin of binaries) {
135
+ for (const brew of brewPaths) {
136
+ try {
137
+ if (await fs.pathExists(path.join(brew, bin))) return true;
138
+ } catch (e) { /* ignore */ }
139
+ }
140
+ }
141
+
142
+ return false;
143
+ }
144
+
31
145
  /**
32
146
  * Translate workflow stage names
33
147
  */
@@ -248,6 +362,39 @@ async function countRequirements() {
248
362
  }
249
363
  }
250
364
 
365
+ async function getSyncStatus() {
366
+ try {
367
+ const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
368
+ const syncEngine = new SyncEngine();
369
+ await syncEngine.initialize();
370
+ const status = syncEngine.getStatus();
371
+ syncEngine.stop();
372
+
373
+ if (!status.isOnline && status.queuedChanges > 0) {
374
+ return `[Offline: ${status.queuedChanges} queued]`;
375
+ } else if (!status.isOnline) {
376
+ return '[Offline]';
377
+ } else if (status.isSyncing) {
378
+ return '[Syncing...]';
379
+ } else if (status.lastSyncTime) {
380
+ const timeSinceSync = Date.now() - status.lastSyncTime;
381
+ const minutesAgo = Math.floor(timeSinceSync / (1000 * 60));
382
+ if (minutesAgo < 1) {
383
+ return '[Sync: ✓ just now]';
384
+ } else if (minutesAgo < 60) {
385
+ return `[Sync: ✓ ${minutesAgo}m ago]`;
386
+ } else {
387
+ const hoursAgo = Math.floor(minutesAgo / 60);
388
+ return `[Sync: ✓ ${hoursAgo}h ago]`;
389
+ }
390
+ } else {
391
+ return '[Never synced]';
392
+ }
393
+ } catch (error) {
394
+ return '[Sync unavailable]';
395
+ }
396
+ }
397
+
251
398
  async function getCurrentProgress() {
252
399
  try {
253
400
  const { getRequirementsPath } = require('vibecodingmachine-core');
@@ -319,7 +466,7 @@ async function showWelcomeScreen() {
319
466
 
320
467
  // Display welcome banner with version
321
468
  console.log('\n' + boxen(
322
- chalk.bold.cyan('Vibe Coding Machine') + '\n' +
469
+ chalk.bold.cyan('Vibe Coding Machine!') + '\n' +
323
470
  chalk.gray(version) + '\n' +
324
471
  chalk.gray(t('banner.tagline')),
325
472
  {
@@ -330,9 +477,28 @@ async function showWelcomeScreen() {
330
477
  }
331
478
  ));
332
479
 
480
+ // Display feedback hint at the top of every screen
481
+ console.log(chalk.gray('💡 Press F for feedback - Share your thoughts anytime'));
482
+
333
483
  // Display repository and system info
334
484
  console.log();
335
485
  console.log(chalk.gray(t('system.repo').padEnd(25)), formatPath(repoPath));
486
+
487
+ // Display git branch if in a git repo
488
+ try {
489
+ const { isGitRepo, getCurrentBranch, hasUncommittedChanges } = require('vibecodingmachine-core');
490
+ if (isGitRepo(repoPath)) {
491
+ const branch = getCurrentBranch(repoPath);
492
+ if (branch) {
493
+ const isDirty = hasUncommittedChanges(repoPath);
494
+ const branchDisplay = isDirty ? `${chalk.cyan(branch)} ${chalk.yellow(t('system.git.status.dirty'))}` : chalk.cyan(branch);
495
+ console.log(chalk.gray(t('system.git.branch').padEnd(25)), branchDisplay);
496
+ }
497
+ }
498
+ } catch (error) {
499
+ // Ignore git display errors
500
+ }
501
+
336
502
  console.log(chalk.gray(t('system.computer.name').padEnd(25)), chalk.cyan(hostname));
337
503
 
338
504
  // Display auto mode progress if running
@@ -402,10 +568,14 @@ function indexToLetter(index) {
402
568
  return String.fromCharCode(97 + index); // 97 is 'a'
403
569
  }
404
570
 
571
+ // Parse requirements from file content for a given section (supports ###, PACKAGE:, and '- ' bullets)
572
+ const { parseRequirementsFromContent } = require('./requirements-parser');
573
+
405
574
  // Tree-style requirements navigator
406
575
  async function showRequirementsTree() {
407
576
  console.log(chalk.bold.cyan('\n📋 ' + t('requirements.navigator.title') + '\n'));
408
577
  console.log(chalk.gray(t('requirements.navigator.basic.instructions') + '\n'));
578
+ console.log(chalk.gray('💡 Press F for feedback - Share your thoughts anytime\n'));
409
579
 
410
580
  const tree = {
411
581
  expanded: { root: true },
@@ -432,16 +602,16 @@ async function showRequirementsTree() {
432
602
  };
433
603
 
434
604
  // Create localized labels
435
- const localizedTodoLabel = todoCount > 0 ?
436
- `⏳ ${t('requirements.section.todo')} (${todoCount} - ${Math.round((todoCount / total) * 100)}%)` :
605
+ const localizedTodoLabel = todoCount > 0 ?
606
+ `⏳ ${t('requirements.section.todo')} (${todoCount} - ${Math.round((todoCount / total) * 100)}%)` :
437
607
  `⏳ ${t('requirements.section.todo')} (0 - 0%)`;
438
-
439
- const localizedToVerifyLabel = toVerifyCount > 0 ?
440
- `✅ ${t('requirements.section.to.verify')} (${toVerifyCount} - ${Math.round((toVerifyCount / total) * 100)}%)` :
608
+
609
+ const localizedToVerifyLabel = toVerifyCount > 0 ?
610
+ `✅ ${t('requirements.section.to.verify')} (${toVerifyCount} - ${Math.round((toVerifyCount / total) * 100)}%)` :
441
611
  `✅ ${t('requirements.section.to.verify')} (0 - 0%)`;
442
-
443
- const localizedVerifiedLabel = verifiedCount > 0 ?
444
- `🎉 ${t('requirements.section.verified')} (${verifiedCount} - ${Math.round((verifiedCount / total) * 100)}%)` :
612
+
613
+ const localizedVerifiedLabel = verifiedCount > 0 ?
614
+ `🎉 ${t('requirements.section.verified')} (${verifiedCount} - ${Math.round((verifiedCount / total) * 100)}%)` :
445
615
  `🎉 ${t('requirements.section.verified')} (0 - 0%)`;
446
616
 
447
617
  const verifiedReqs = tree.verifiedReqs || [];
@@ -450,6 +620,10 @@ async function showRequirementsTree() {
450
620
  const todoReqs = tree.todoReqs || [];
451
621
  const recycledReqs = tree.recycledReqs || [];
452
622
 
623
+ // Calculate percentages for clarification and recycled sections
624
+ const clarificationPercent = total > 0 ? Math.round((clarificationReqs.length / total) * 100) : 0;
625
+ const recycledPercent = total > 0 ? Math.round((recycledReqs.length / total) * 100) : 0;
626
+
453
627
  // VERIFIED section (first) - only show if has requirements
454
628
  if (verifiedReqs.length > 0 || verifiedCount > 0) {
455
629
  tree.items.push({ level: 1, type: 'section', label: localizedVerifiedLabel, key: 'verified' });
@@ -517,136 +691,14 @@ async function showRequirementsTree() {
517
691
  }
518
692
 
519
693
  const content = await fs.readFile(reqPath, 'utf8');
520
- const lines = content.split('\n');
521
-
522
- let inSection = false;
523
- const requirements = [];
524
-
525
- // For TO VERIFY section, check multiple possible section titles
526
- const sectionTitles = sectionKey === 'verify'
527
- ? ['🔍 TO VERIFY BY HUMAN', 'TO VERIFY BY HUMAN', '🔍 TO VERIFY', 'TO VERIFY', '✅ Verified by AI screenshot', 'Verified by AI screenshot', 'Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG']
528
- : [sectionTitle];
529
-
530
- // For TO VERIFY, we need to find the exact section header
531
- // The section header is: ## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG
532
- const toVerifySectionHeader = '## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG';
533
694
 
534
- for (let i = 0; i < lines.length; i++) {
535
- const line = lines[i];
536
- const trimmed = line.trim();
537
-
538
- // Check if this line matches any of the section titles
539
- // IMPORTANT: Only check section headers (lines starting with ##), not requirement text
540
- if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
541
- // Reset inSection if we hit a section header that's not our target section
542
- if (sectionKey === 'verify' && inSection) {
543
- // Check if this is still a TO VERIFY section header
544
- if (!isStillToVerify) {
545
- // This will be handled by the "leaving section" check below, but ensure we don't process it as entering
546
- }
547
- }
548
- if (sectionKey === 'verify') {
549
- // For TO VERIFY, check for the specific section header with exact matching
550
- // Must match the exact TO VERIFY section header, not just any line containing "TO VERIFY"
551
- const isToVerifyHeader = trimmed === '## 🔍 TO VERIFY BY HUMAN' ||
552
- trimmed.startsWith('## 🔍 TO VERIFY BY HUMAN') ||
553
- trimmed === '## 🔍 TO VERIFY' ||
554
- trimmed.startsWith('## 🔍 TO VERIFY') ||
555
- trimmed === '## TO VERIFY' ||
556
- trimmed.startsWith('## TO VERIFY') ||
557
- trimmed === '## ✅ TO VERIFY' ||
558
- trimmed.startsWith('## ✅ TO VERIFY') ||
559
- trimmed === toVerifySectionHeader ||
560
- (trimmed.startsWith(toVerifySectionHeader) && trimmed.includes('Needs Human to Verify'));
561
-
562
- if (isToVerifyHeader) {
563
- // Make sure it's not a VERIFIED section (without TO VERIFY)
564
- if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) && !trimmed.includes('📝 VERIFIED')) {
565
- inSection = true;
566
- continue;
567
- }
568
- } else {
569
- // If we hit a different section header and we're looking for TO VERIFY, make sure we're not in section
570
- // This prevents incorrectly reading from TODO or other sections
571
- if (trimmed.includes('⏳ Requirements not yet completed') ||
572
- trimmed.includes('Requirements not yet completed') ||
573
- trimmed === '## 📝 VERIFIED' ||
574
- trimmed.startsWith('## 📝 VERIFIED')) {
575
- // We're in TODO or VERIFIED section, not TO VERIFY - reset
576
- inSection = false;
577
- }
578
- }
579
- } else if (sectionTitles.some(title => trimmed.includes(title))) {
580
- inSection = true;
581
- continue;
582
- }
583
- }
584
-
585
- // Check if we're leaving the section (new section header that doesn't match)
586
- if (inSection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
587
- // If this is a new section header and it's not one of our section titles, we've left the section
588
- if (sectionKey === 'verify') {
589
- // For TO VERIFY, only break if this is clearly a different section
590
- // Check for specific section headers that indicate we've left TO VERIFY
591
-
592
- if (isVerifiedSection || isTodoSection || isRecycledSection || isClarificationSection) {
593
- break; // Different section, we've left TO VERIFY
594
- }
595
- // Otherwise, continue - might be REJECTED or CHANGELOG which are not section boundaries for TO VERIFY
596
- } else {
597
- // For other sections, break if it's a new section header that doesn't match
598
- if (!sectionTitles.some(title => trimmed.includes(title))) {
599
- break;
600
- }
601
- }
602
- }
603
-
604
- // Read requirements in new format (### header)
605
- if (inSection && line.trim().startsWith('###')) {
606
- const title = line.trim().replace(/^###\s*/, '').trim();
607
-
608
- // Skip malformed requirements (title is just a package name, empty, or too short)
609
- // Common package names that shouldn't be requirement titles
610
- const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
611
- if (!title || title.length === 0 || packageNames.includes(title.toLowerCase())) {
612
- continue; // Skip this malformed requirement
613
- }
614
-
615
- const details = [];
616
- let pkg = null;
617
-
618
- // Read package and description
619
- for (let j = i + 1; j < lines.length; j++) {
620
- const nextLine = lines[j].trim();
621
- // Stop if we hit another requirement or section
622
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
623
- break;
624
- }
625
- // Check for PACKAGE line
626
- if (nextLine.startsWith('PACKAGE:')) {
627
- pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
628
- } else if (nextLine && !nextLine.startsWith('PACKAGE:')) {
629
- // Description line
630
- details.push(nextLine);
631
- }
632
- }
633
-
634
- requirements.push({ title, details, pkg, lineIndex: i });
635
- }
636
- }
695
+ // Delegate to reusable parser
696
+ const allReqs = parseRequirementsFromContent(content, sectionKey, sectionTitle);
637
697
 
638
- // Remove duplicates based on title (keep first occurrence)
639
- const seenTitles = new Set();
640
- const uniqueRequirements = [];
641
- for (const req of requirements) {
642
- const normalizedTitle = req.title.replace(/^TRY AGAIN \(\d+(st|nd|rd|th) time\):\s*/i, '').trim();
643
- if (!seenTitles.has(normalizedTitle)) {
644
- seenTitles.add(normalizedTitle);
645
- uniqueRequirements.push(req);
646
- }
647
- }
698
+ // For TODO section, only show primary heading requirements (those marked from '###' titles)
699
+ if (sectionKey === 'todo') return allReqs.filter(r => r.source === 'heading');
648
700
 
649
- return uniqueRequirements;
701
+ return allReqs;
650
702
  };
651
703
 
652
704
  // Load VERIFIED requirements from CHANGELOG
@@ -708,49 +760,99 @@ async function showRequirementsTree() {
708
760
 
709
761
  let inSection = false;
710
762
  const requirements = [];
711
- let currentReq = null;
712
763
 
713
764
  for (let i = 0; i < lines.length; i++) {
714
765
  const line = lines[i];
766
+ const trimmed = line.trim();
715
767
 
716
- if (line.includes(' Requirements needing manual feedback')) {
768
+ // Check if we're entering the clarification section
769
+ if (trimmed.includes('❓ Requirements needing manual feedback')) {
717
770
  inSection = true;
718
771
  continue;
719
772
  }
720
773
 
721
- if (inSection && line.startsWith('##') && !line.includes('❓ Requirements needing manual feedback')) {
722
- if (currentReq) requirements.push(currentReq);
774
+ // Check if we're leaving the section (hit another ## section)
775
+ if (inSection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
723
776
  break;
724
777
  }
725
778
 
726
- if (inSection && line.trim().startsWith('- ')) {
727
- if (currentReq) requirements.push(currentReq);
728
-
729
- const title = line.trim().substring(2);
730
- currentReq = { title, questions: [], findings: null, lineIndex: i };
731
- } else if (inSection && currentReq && line.trim()) {
732
- // Check for AI findings section
733
- if (line.trim().startsWith('**AI found in codebase:**')) {
734
- continue; // Skip the header, capture next line as findings
735
- } else if (!currentReq.findings && !line.trim().startsWith('**Clarifying questions:**') && !line.trim().match(/^\d+\./)) {
736
- // This is the findings content (comes after "AI found in codebase:")
737
- currentReq.findings = line.trim();
738
- } else if (line.trim().startsWith('**Clarifying questions:**')) {
739
- continue; // Skip the questions header
740
- } else if (line.trim().match(/^\d+\./)) {
741
- // This is a question
742
- currentReq.questions.push({ question: line.trim(), response: null });
743
- } else if (currentReq.questions.length > 0) {
744
- // This might be a response to the last question
745
- const lastQuestion = currentReq.questions[currentReq.questions.length - 1];
746
- if (!lastQuestion.response) {
747
- lastQuestion.response = line.trim();
779
+ // Read requirements in new format (### header)
780
+ if (inSection && trimmed.startsWith('###')) {
781
+ const title = trimmed.replace(/^###\s*/, '').trim();
782
+
783
+ // Skip empty titles
784
+ if (!title || title.length === 0) {
785
+ continue;
786
+ }
787
+
788
+ const details = [];
789
+ const questions = [];
790
+ let pkg = null;
791
+ let findings = null;
792
+ let currentQuestion = null;
793
+
794
+ // Read package, description, and clarifying questions
795
+ for (let j = i + 1; j < lines.length; j++) {
796
+ const nextLine = lines[j].trim();
797
+
798
+ // Stop if we hit another requirement or section
799
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
800
+ break;
801
+ }
802
+
803
+ // Check for PACKAGE line
804
+ if (nextLine.startsWith('PACKAGE:')) {
805
+ pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
806
+ }
807
+ // Check for AI findings
808
+ else if (nextLine.startsWith('**AI found in codebase:**')) {
809
+ // Next line will be the findings
810
+ continue;
811
+ }
812
+ else if (nextLine.startsWith('**What went wrong')) {
813
+ // Description line
814
+ details.push(nextLine);
815
+ }
816
+ else if (nextLine.startsWith('**Clarifying questions:**')) {
817
+ // Start of questions section
818
+ continue;
819
+ }
820
+ else if (nextLine.match(/^\d+\./)) {
821
+ // Save previous question if exists
822
+ if (currentQuestion) {
823
+ questions.push(currentQuestion);
824
+ }
825
+ // This is a new question
826
+ currentQuestion = { question: nextLine, response: null };
827
+ }
828
+ else if (currentQuestion && nextLine && !nextLine.startsWith('PACKAGE:') && !nextLine.startsWith('**')) {
829
+ // This might be a response to the current question or description/findings
830
+ if (!findings && !currentQuestion.response && questions.length === 0 && !nextLine.match(/^\d+\./)) {
831
+ // This is findings content
832
+ findings = nextLine;
833
+ } else if (currentQuestion && !currentQuestion.response) {
834
+ // This is a response to the current question
835
+ currentQuestion.response = nextLine;
836
+ } else {
837
+ // Description line
838
+ details.push(nextLine);
839
+ }
840
+ }
841
+ else if (nextLine && !nextLine.startsWith('PACKAGE:') && !nextLine.startsWith('**')) {
842
+ // Description line
843
+ details.push(nextLine);
748
844
  }
749
845
  }
846
+
847
+ // Save last question if exists
848
+ if (currentQuestion) {
849
+ questions.push(currentQuestion);
850
+ }
851
+
852
+ requirements.push({ title, details, pkg, questions, findings, lineIndex: i });
750
853
  }
751
854
  }
752
855
 
753
- if (currentReq) requirements.push(currentReq);
754
856
  return requirements;
755
857
  };
756
858
 
@@ -945,6 +1047,10 @@ async function showRequirementsTree() {
945
1047
  tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
946
1048
  await buildTree();
947
1049
  }
1050
+ } else if (key.name === 'f') {
1051
+ // Feedback button ( megaphone 📣 )
1052
+ await handleFeedbackSubmission();
1053
+ await buildTree();
948
1054
  } else if (key.name === 'r') {
949
1055
  const current = tree.items[tree.selected];
950
1056
  if (!current) continue; // Safety check
@@ -1021,6 +1127,90 @@ async function showRequirementsTree() {
1021
1127
  process.stdin.pause();
1022
1128
  }
1023
1129
 
1130
+ /**
1131
+ * Handle feedback submission
1132
+ */
1133
+ async function handleFeedbackSubmission() {
1134
+ console.log(chalk.bold.cyan('\n📣 ' + t('interactive.feedback.title')));
1135
+ console.log(chalk.gray(t('interactive.feedback.instructions') + '\n'));
1136
+
1137
+ const { getUserProfile } = require('./auth');
1138
+ const userProfile = await getUserProfile();
1139
+ const userEmail = userProfile ? userProfile.email : 'anonymous';
1140
+
1141
+ console.log(chalk.gray('\n' + t('interactive.feedback.comment.instructions') + '\n'));
1142
+
1143
+ const commentLines = [];
1144
+ let emptyLineCount = 0;
1145
+ let isFirstLine = true;
1146
+
1147
+ while (true) {
1148
+ try {
1149
+ const { line } = await inquirer.prompt([{
1150
+ type: 'input',
1151
+ name: 'line',
1152
+ message: isFirstLine ? t('interactive.feedback.comment') : ''
1153
+ }]);
1154
+
1155
+ isFirstLine = false;
1156
+
1157
+ if (line.trim() === '') {
1158
+ emptyLineCount++;
1159
+ if (emptyLineCount >= 2) break;
1160
+ } else {
1161
+ emptyLineCount = 0;
1162
+ commentLines.push(line);
1163
+ }
1164
+ } catch (err) {
1165
+ break;
1166
+ }
1167
+ }
1168
+
1169
+ const comment = commentLines.join('\n');
1170
+
1171
+ if (!comment || comment.trim().length === 0) {
1172
+ console.log(chalk.yellow('\n⚠️ ' + t('interactive.feedback.cancelled')));
1173
+ return;
1174
+ }
1175
+
1176
+ try {
1177
+ console.log(chalk.gray('\n' + t('interactive.feedback.submitting') + '...'));
1178
+
1179
+ const auth = require('./auth');
1180
+ const UserDatabase = require('vibecodingmachine-core/src/database/user-schema');
1181
+ const userDb = new UserDatabase();
1182
+
1183
+ // Set auth token for API requests
1184
+ const token = await auth.getAuthToken();
1185
+ if (token) {
1186
+ userDb.setAuthToken(token);
1187
+ }
1188
+
1189
+ await userDb.submitFeedback({
1190
+ email: userEmail,
1191
+ comment: comment,
1192
+ interface: 'cli',
1193
+ version: pkg.version
1194
+ });
1195
+
1196
+ console.log(chalk.green('\n✓ ' + t('interactive.feedback.success')));
1197
+ } catch (error) {
1198
+ console.log(chalk.red('\n✗ ' + t('interactive.feedback.error') + ': ') + error.message);
1199
+ }
1200
+
1201
+ console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
1202
+ await new Promise(resolve => {
1203
+ const rl = readline.createInterface({
1204
+ input: process.stdin,
1205
+ output: process.stdout
1206
+ });
1207
+ rl.question('', () => {
1208
+ rl.close();
1209
+ resolve();
1210
+ });
1211
+ });
1212
+ }
1213
+
1024
1214
  // Helper to show goodbye message
1025
1215
  function showGoodbyeMessage() {
1026
1216
  const hour = new Date().getHours();
@@ -1369,6 +1559,7 @@ async function showClarificationActions(req, tree, loadClarification) {
1369
1559
  const actions = [
1370
1560
  { label: '✍️ Add/Edit Responses', value: 'edit-responses' },
1371
1561
  { label: '↩️ Move back to TODO (after clarification)', value: 'move-to-todo' },
1562
+ { label: '📣 Feedback', value: 'feedback' },
1372
1563
  { label: '🗑️ Delete', value: 'delete' }
1373
1564
  ];
1374
1565
 
@@ -1399,7 +1590,8 @@ async function showClarificationActions(req, tree, loadClarification) {
1399
1590
  });
1400
1591
 
1401
1592
  // Display menu
1402
- console.log(chalk.gray('\nWhat would you like to do? (↑/↓/Enter to select, ESC/← to go back)\n'));
1593
+ console.log(chalk.gray('\n💡 Press F for feedback - Share your thoughts anytime'));
1594
+ console.log(chalk.gray('What would you like to do? (↑/↓/Enter to select, ESC/← to go back)\n'));
1403
1595
  actions.forEach((action, idx) => {
1404
1596
  if (idx === selected) {
1405
1597
  console.log(chalk.cyan(`❯ ${action.label}`));
@@ -1430,6 +1622,10 @@ async function showClarificationActions(req, tree, loadClarification) {
1430
1622
  selected = Math.max(0, selected - 1);
1431
1623
  } else if (key.name === 'down') {
1432
1624
  selected = Math.min(actions.length - 1, selected + 1);
1625
+ } else if (key.name === 'f') {
1626
+ // Feedback button ( megaphone 📣 )
1627
+ await handleFeedbackSubmission();
1628
+ return;
1433
1629
  } else if (key.name === 'return' || key.name === 'space') {
1434
1630
  const action = actions[selected].value;
1435
1631
 
@@ -1441,6 +1637,9 @@ async function showClarificationActions(req, tree, loadClarification) {
1441
1637
  await moveClarificationToTodo(req, tree);
1442
1638
  tree.clarificationReqs = await loadClarification();
1443
1639
  return;
1640
+ } else if (action === 'feedback') {
1641
+ await handleFeedbackSubmission();
1642
+ return;
1444
1643
  } else if (action === 'delete') {
1445
1644
  await deleteClarification(req, tree);
1446
1645
  tree.clarificationReqs = await loadClarification();
@@ -1458,6 +1657,7 @@ async function showRequirementActions(req, sectionKey, tree) {
1458
1657
  { label: '👎 Thumbs down (demote to TODO)', value: 'thumbs-down' },
1459
1658
  { label: '⬆️ Move up', value: 'move-up' },
1460
1659
  { label: '⬇️ Move down', value: 'move-down' },
1660
+ { label: '📣 Feedback', value: 'feedback' },
1461
1661
  { label: '🗑️ Delete', value: 'delete' }
1462
1662
  ];
1463
1663
 
@@ -1487,6 +1687,7 @@ async function showRequirementActions(req, sectionKey, tree) {
1487
1687
 
1488
1688
  // Display menu (always reprinted)
1489
1689
  console.log();
1690
+ console.log(chalk.gray('💡 Press F for feedback - Share your thoughts anytime'));
1490
1691
  console.log(chalk.gray('What would you like to do? (↑/↓/Enter to select, ESC/← to go back)'));
1491
1692
  console.log();
1492
1693
  menuLines += 3; // Blank line + help text + blank line
@@ -1531,6 +1732,10 @@ async function showRequirementActions(req, sectionKey, tree) {
1531
1732
  process.exit(0);
1532
1733
  } else if (key.name === 'escape' || key.name === 'left') {
1533
1734
  return; // Go back
1735
+ } else if (key.name === 'f') {
1736
+ // Feedback button ( megaphone 📣 )
1737
+ await handleFeedbackSubmission();
1738
+ return;
1534
1739
  } else if (key.name === 'up') {
1535
1740
  selected = Math.max(0, selected - 1);
1536
1741
  } else if (key.name === 'down') {
@@ -1591,6 +1796,9 @@ async function performRequirementAction(action, req, sectionKey, tree) {
1591
1796
  case 'rename':
1592
1797
  await renameRequirement(req, sectionKey, tree);
1593
1798
  break;
1799
+ case 'feedback':
1800
+ await handleFeedbackSubmission();
1801
+ break;
1594
1802
  }
1595
1803
 
1596
1804
  await new Promise(resolve => setTimeout(resolve, 1000));
@@ -2357,7 +2565,7 @@ async function showRequirementsBySection(sectionTitle) {
2357
2565
  }
2358
2566
  break;
2359
2567
  case 'delete':
2360
- const { confirmDelete } = await inquirer.prompt([{
2568
+ const { confirmDelete } = await promptWithDefaultsOnce([{
2361
2569
  type: 'confirm',
2362
2570
  name: 'confirmDelete',
2363
2571
  message: 'Are you sure you want to delete this requirement?',
@@ -2383,6 +2591,104 @@ async function showRequirementsBySection(sectionTitle) {
2383
2591
  }
2384
2592
  }
2385
2593
 
2594
+ // Helper to move a requirement to the Recycled section
2595
+ async function moveToRecycled(reqPath, requirementTitle, fromSectionTitle) {
2596
+ const content = await fs.readFile(reqPath, 'utf8');
2597
+ const lines = content.split('\n');
2598
+
2599
+ let inFromSection = false;
2600
+ let inRecycledSection = false;
2601
+ let reqStartIndex = -1;
2602
+ let reqEndIndex = -1;
2603
+ let recycledSectionIndex = -1;
2604
+ let requirement = null;
2605
+
2606
+ // Find the requirement in the source section
2607
+ for (let i = 0; i < lines.length; i++) {
2608
+ const line = lines[i];
2609
+
2610
+ // Track if we're in the source section
2611
+ if (line.includes(fromSectionTitle)) {
2612
+ inFromSection = true;
2613
+ continue;
2614
+ }
2615
+
2616
+ // Exit source section at next ## header
2617
+ if (inFromSection && line.startsWith('## ') && !line.startsWith('###') && !line.includes(fromSectionTitle)) {
2618
+ inFromSection = false;
2619
+ }
2620
+
2621
+ // Find the requirement
2622
+ if (inFromSection && line.trim() === `### ${requirementTitle}`) {
2623
+ reqStartIndex = i;
2624
+ requirement = { title: requirementTitle, details: [], package: null };
2625
+
2626
+ // Read requirement details
2627
+ for (let j = i + 1; j < lines.length; j++) {
2628
+ const nextLine = lines[j];
2629
+ // Stop at next requirement or section
2630
+ if (nextLine.startsWith('###') || (nextLine.startsWith('## ') && !nextLine.startsWith('###'))) {
2631
+ reqEndIndex = j;
2632
+ break;
2633
+ }
2634
+ // Check for package
2635
+ if (nextLine.startsWith('PACKAGE:')) {
2636
+ requirement.package = nextLine.replace('PACKAGE:', '').trim();
2637
+ } else if (nextLine.trim()) {
2638
+ requirement.details.push(nextLine);
2639
+ }
2640
+ }
2641
+
2642
+ if (reqEndIndex === -1) {
2643
+ reqEndIndex = lines.length;
2644
+ }
2645
+ break;
2646
+ }
2647
+
2648
+ // Find Recycled section
2649
+ if (line.includes('♻️ Recycled') || line.includes('## Recycled')) {
2650
+ recycledSectionIndex = i;
2651
+ inRecycledSection = true;
2652
+ }
2653
+ }
2654
+
2655
+ if (!requirement || reqStartIndex === -1) {
2656
+ throw new Error(`Requirement "${requirementTitle}" not found in ${fromSectionTitle}`);
2657
+ }
2658
+
2659
+ // If Recycled section doesn't exist, create it at the end
2660
+ if (recycledSectionIndex === -1) {
2661
+ recycledSectionIndex = lines.length;
2662
+ lines.push('', '## ♻️ Recycled', '');
2663
+ }
2664
+
2665
+ // Remove requirement from source section
2666
+ const removedLines = lines.splice(reqStartIndex, reqEndIndex - reqStartIndex);
2667
+
2668
+ // Find where to insert in Recycled section (after the section header)
2669
+ let insertIndex = recycledSectionIndex + 1;
2670
+ while (insertIndex < lines.length && lines[insertIndex].trim() === '') {
2671
+ insertIndex++;
2672
+ }
2673
+
2674
+ // Build requirement lines
2675
+ const reqLines = [`### ${requirement.title}`];
2676
+ if (requirement.package && requirement.package !== 'all') {
2677
+ reqLines.push(`PACKAGE: ${requirement.package}`);
2678
+ }
2679
+ requirement.details.forEach(line => {
2680
+ if (line.trim()) {
2681
+ reqLines.push(line);
2682
+ }
2683
+ });
2684
+ reqLines.push('');
2685
+
2686
+ // Insert into Recycled section
2687
+ lines.splice(insertIndex, 0, ...reqLines);
2688
+
2689
+ await fs.writeFile(reqPath, lines.join('\n'));
2690
+ }
2691
+
2386
2692
  // Helper to save reordered requirements back to file
2387
2693
  async function saveRequirementsOrder(reqPath, sectionTitle, requirements) {
2388
2694
  const content = await fs.readFile(reqPath, 'utf8');
@@ -2481,6 +2787,9 @@ async function showRequirementsFromChangelog() {
2481
2787
  }
2482
2788
  }
2483
2789
 
2790
+ // Input suppression timestamp to avoid immediate re-entry into menus after cancel
2791
+ let menuSuppressUntil = 0;
2792
+
2484
2793
  // Custom menu with both arrow keys and letter shortcuts
2485
2794
  async function showQuickMenu(items, initialSelectedIndex = 0) {
2486
2795
  return new Promise((resolve) => {
@@ -2492,6 +2801,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2492
2801
  if (selectedIndex >= items.length) selectedIndex = 0;
2493
2802
 
2494
2803
  let isFirstRender = true;
2804
+ let lastLinesPrinted = 0;
2495
2805
 
2496
2806
  // Helper to calculate visual lines occupied by text
2497
2807
  const getVisualLineCount = (text) => {
@@ -2631,19 +2941,42 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2631
2941
  process.stdin.setRawMode(true);
2632
2942
  }
2633
2943
 
2634
- const onKeypress = async (str, key) => {
2635
- if (!key) return;
2944
+ // Ignore spurious keypresses for a short debounce window after the menu opens
2945
+ // This prevents accidental immediate re-entry when returning from sub-menus.
2946
+ const IGNORE_INPUT_MS = 300; // milliseconds
2947
+ // If a recent menu cancel occurred, extend the ignore window to avoid key repeat re-opening
2948
+ let ignoreInputUntil = Math.max(Date.now() + IGNORE_INPUT_MS, menuSuppressUntil || 0);
2636
2949
 
2637
- // Ctrl+C to exit
2638
- if (key.ctrl && key.name === 'c') {
2950
+ const onKeypress = async (str, key) => {
2951
+ if (process.env.VCM_DEBUG_MENU === '1') {
2952
+ const msg = `[DEBUG-MENU ${new Date().toISOString()}] onKeypress: str=${JSON.stringify(str)} key=${JSON.stringify(key)} now=${Date.now()}\n`;
2953
+ console.log(msg);
2954
+ try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
2955
+ }
2956
+ // Always allow Ctrl+C to pass through immediately
2957
+ if (key && key.ctrl && key.name === 'c') {
2639
2958
  cleanup();
2640
2959
  process.exit(0);
2641
2960
  return;
2642
2961
  }
2643
2962
 
2644
- // ESC or left arrow to exit with confirmation
2645
- if (key.name === 'escape' || key.name === 'left') {
2646
- cleanup();
2963
+ // Debounce: ignore inputs until the short window passes
2964
+ if (Date.now() < ignoreInputUntil) {
2965
+ return;
2966
+ }
2967
+
2968
+ if (!key) return;
2969
+
2970
+ // Ctrl+C to exit
2971
+ if (key.ctrl && key.name === 'c') {
2972
+ cleanup();
2973
+ process.exit(0);
2974
+ return;
2975
+ }
2976
+
2977
+ // ESC or left arrow to exit with confirmation
2978
+ if (key.name === 'escape' || key.name === 'left') {
2979
+ cleanup();
2647
2980
  // Don't clear screen for exit - keep status visible
2648
2981
  resolve({ value: 'exit', selectedIndex });
2649
2982
  return;
@@ -2653,6 +2986,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2653
2986
  if (str && str.length === 1) {
2654
2987
  if (str === 'x') {
2655
2988
  // 'x' always maps to exit (will trigger confirmAndExit)
2989
+ if (process.env.VCM_DEBUG_MENU === '1') console.log(`[DEBUG-MENU ${new Date().toISOString()}] quickMenu: 'x' pressed (exit)`);
2656
2990
  cleanup();
2657
2991
  // Don't clear screen for exit - keep status visible
2658
2992
  resolve({ value: 'exit', selectedIndex });
@@ -2712,6 +3046,11 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2712
3046
  }
2713
3047
 
2714
3048
  async function showProviderManagerMenu() {
3049
+ if (process.env.VCM_DEBUG_MENU === '1') {
3050
+ const msg = `[DEBUG-MENU ${new Date().toISOString()}] showProviderManagerMenu() called\n` + new Error().stack.split('\n').slice(1, 6).join('\n') + '\n';
3051
+ console.log(msg);
3052
+ try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
3053
+ }
2715
3054
  const definitions = getProviderDefinitions();
2716
3055
  const defMap = new Map(definitions.map(def => [def.id, def]));
2717
3056
  const prefs = await getProviderPreferences();
@@ -2721,6 +3060,8 @@ async function showProviderManagerMenu() {
2721
3060
  let dirty = false;
2722
3061
 
2723
3062
  const { fetchQuotaForAgent } = require('vibecodingmachine-core/src/quota-management');
3063
+ const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
3064
+ const providerManager = new ProviderManager();
2724
3065
 
2725
3066
  const debugQuota = process.env.VCM_DEBUG_QUOTA === '1' || process.env.VCM_DEBUG_QUOTA === 'true';
2726
3067
 
@@ -2736,14 +3077,76 @@ async function showProviderManagerMenu() {
2736
3077
  return `${seconds}s`;
2737
3078
  };
2738
3079
 
3080
+ const formatTimeAmPm = (date) => {
3081
+ const hours24 = date.getHours();
3082
+ const minutes = String(date.getMinutes()).padStart(2, '0');
3083
+ const ampm = hours24 >= 12 ? 'pm' : 'am';
3084
+ const hours12 = (hours24 % 12) || 12;
3085
+ return `${hours12}:${minutes} ${ampm}`;
3086
+ };
3087
+
3088
+
3089
+
3090
+ // Pre-fetch data to avoid lag in render loop
3091
+ const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
3092
+
3093
+ // Initialize caches from persistent storage
3094
+ const savedCache = await getProviderCache();
3095
+
3096
+ // Hydrate local maps from saved cache
3097
+ const installationStatus = new Map();
3098
+ const agentQuotas = new Map();
3099
+
3100
+ Object.keys(savedCache).forEach(id => {
3101
+ if (savedCache[id].installed !== undefined) {
3102
+ installationStatus.set(id, savedCache[id].installed);
3103
+ }
3104
+ if (savedCache[id].quota) {
3105
+ agentQuotas.set(id, savedCache[id].quota);
3106
+ }
3107
+ });
3108
+
3109
+ // Also prefill agent quotas from the ProviderManager rate-limit file so the UI can
3110
+ // display `[Resets at ...]` instantly (best-effort, read-only).
3111
+ try {
3112
+ const { getProviderRateLimitedQuotas } = require('./provider-rate-cache');
3113
+ const prefetched = getProviderRateLimitedQuotas(definitions);
3114
+ for (const [id, q] of prefetched) {
3115
+ // Only set if we don't already have a richer quota cached
3116
+ const existing = agentQuotas.get(id);
3117
+ if (!existing || existing.type !== 'rate-limit') {
3118
+ agentQuotas.set(id, q);
3119
+ }
3120
+ }
3121
+ } catch (e) {
3122
+ // Ignore — this is a non-critical optimization
3123
+ }
3124
+
3125
+ // Default values while loading
3126
+ let quotaInfo = { maxIterations: 10, todayUsage: 0 };
3127
+ let autoConfig = {};
3128
+ // Active flag to prevent background render after menu is closed
3129
+ let isMenuActive = true;
3130
+
3131
+ // Load health metrics for all IDEs (load once at startup, not on every render)
3132
+ let healthMetricsMap = new Map();
3133
+ try {
3134
+ const healthTracker = new IDEHealthTracker();
3135
+ await healthTracker.load();
3136
+ healthMetricsMap = await healthTracker.getAllHealthMetrics();
3137
+ } catch (error) {
3138
+ // Silently ignore health loading errors - health display is optional
3139
+ }
3140
+
3141
+ // Clear loading message
3142
+ // process.stdout.write('\r\x1b[K'); // Removed as per diff
3143
+
2739
3144
  const render = async () => {
3145
+ if (!isMenuActive) return; // Prevent rendering after menu closed
2740
3146
  process.stdout.write('\x1Bc');
2741
3147
  console.log(chalk.bold.cyan('⚙ ' + t('provider.title') + '\n'));
2742
3148
 
2743
- // Fetch quota info
2744
- const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
2745
- const autoConfig = await getAutoConfig();
2746
- const quotaInfo = await sharedAuth.canRunAutoMode();
3149
+ // Use cached quota info
2747
3150
  const remaining = Math.max(0, (quotaInfo.maxIterations || 10) - (quotaInfo.todayUsage || 0));
2748
3151
 
2749
3152
  // Calculate time until reset (midnight)
@@ -2754,15 +3157,65 @@ async function showProviderManagerMenu() {
2754
3157
  const hoursUntilReset = Math.floor(msUntilReset / (1000 * 60 * 60));
2755
3158
  const minsUntilReset = Math.floor((msUntilReset % (1000 * 60 * 60)) / (1000 * 60));
2756
3159
 
2757
- // Display quota as time-based instead of numeric (0/1 or 1/1 format)
3160
+ // Display quota, accounting for per-agent rate limits to avoid showing "10/10" when some
3161
+ // providers are rate-limited. We avoid surfacing agent-specific "[Resets at ...]" labels in
3162
+ // the top-level overall quota row and instead show a compact partial-availability message.
2758
3163
  let quotaDisplay;
3164
+
3165
+ // Detect how many agents are currently rate-limited
3166
+ const rateLimitedAgents = Array.from(agentQuotas.values()).filter(q => {
3167
+ if (!q) return false;
3168
+ if (q.type !== 'rate-limit') return false;
3169
+ if (typeof q.isExceeded === 'function') return q.isExceeded();
3170
+ if (q.remaining !== undefined) return q.remaining <= 0;
3171
+ return false;
3172
+ });
3173
+ const rateLimitedCount = rateLimitedAgents.length;
3174
+
2759
3175
  if (remaining === 0) {
2760
- // Rate limit active - show when it resets (in red)
3176
+ // Overall rate limit active - show when it resets (in red)
2761
3177
  quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.red(`⏰ ${t('provider.rate.limit.resets')} ${hoursUntilReset}h ${minsUntilReset}m`);
3178
+ } else if (rateLimitedCount > 0) {
3179
+ // Some providers are rate-limited — show partial availability instead of full 10/10
3180
+ const effectiveRemaining = Math.max(0, remaining - rateLimitedCount);
3181
+
3182
+ // If one or more rate-limited agents provide a specific reset time, show that instead
3183
+ // of a generic "Resets in Xh Ym" message to match per-agent rows (e.g., "[Resets at 5:00 pm]").
3184
+ const earliestReset = rateLimitedAgents.reduce((min, q) => {
3185
+ if (!q || !q.resetsAt) return min;
3186
+ try {
3187
+ const d = new Date(q.resetsAt);
3188
+ if (Number.isNaN(d.getTime())) return min;
3189
+ if (!min) return q.resetsAt;
3190
+ return d.getTime() < new Date(min).getTime() ? q.resetsAt : min;
3191
+ } catch (e) {
3192
+ return min;
3193
+ }
3194
+ }, null);
3195
+
3196
+ const resetLabel = earliestReset ? formatResetsAtLabel(earliestReset) : null;
3197
+
3198
+ quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.yellow(`⚠️ ${t('provider.available')} (${effectiveRemaining}/${quotaInfo.maxIterations})`) + chalk.gray(` • ${rateLimitedCount} provider${rateLimitedCount > 1 ? 's' : ''} limited`);
3199
+
3200
+ if (resetLabel) {
3201
+ quotaDisplay += ' ' + chalk.red(`[${resetLabel}]`);
3202
+ } else {
3203
+ quotaDisplay += chalk.gray(' • ' + t('provider.resets.in') + ' ') + chalk.cyan(`${hoursUntilReset}h ${minsUntilReset}m`);
3204
+ }
2762
3205
  } else {
2763
3206
  // Quota available - show when it resets (in green)
2764
- quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.green(`✓ ${t('provider.available')} (${remaining}/${quotaInfo.maxIterations})`) + chalk.gray(' ' + t('provider.resets.in') + ' ') + chalk.cyan(`${hoursUntilReset}h ${minsUntilReset}m`);
3207
+ // If the quota is full (no usage), don't show the reset time it's unnecessary and
3208
+ // can be confusing when nothing is rate-limited.
3209
+ const fullQuota = remaining === (quotaInfo.maxIterations || 10);
3210
+ quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.green(`✓ ${t('provider.available')} (${remaining}/${quotaInfo.maxIterations})`);
3211
+ if (!fullQuota) {
3212
+ quotaDisplay += chalk.gray(' • ' + t('provider.resets.in') + ' ') + chalk.cyan(`${hoursUntilReset}h ${minsUntilReset}m`);
3213
+ }
2765
3214
  }
3215
+
3216
+ // Display Legend
3217
+ console.log(chalk.gray(' Interface types: 🖥️ GUI Automation ⚡ Direct Automation'));
3218
+ console.log(chalk.gray(' Provider type: ☁️ Cloud 🏠 Local (Slow, Requires lots of memory and CPU, Private)'));
2766
3219
  console.log(quotaDisplay);
2767
3220
  console.log(chalk.gray(' ' + t('provider.instructions') + '\n'));
2768
3221
 
@@ -2773,75 +3226,137 @@ async function showProviderManagerMenu() {
2773
3226
  const isSelected = idx === selectedIndex;
2774
3227
  const isEnabled = enabled[id] !== false;
2775
3228
 
2776
- // Check for Kiro installation
2777
- let isInstalled = true;
2778
- if (id === 'kiro') {
2779
- try {
2780
- const { isKiroInstalled } = require('./kiro-installer');
2781
- isInstalled = isKiroInstalled();
2782
- } catch (e) {
2783
- // Ignore error provider checks
2784
- }
2785
- }
3229
+ // Use cached installation status (default to false if not found in cache)
3230
+ // For LLMs, we consider them always "installed" (cloud)
3231
+ let isInstalled = def.type === 'ide' ? (installationStatus.get(id) === true) : true;
3232
+ let quota = agentQuotas.get(id);
3233
+
3234
+ // Determine status emoji based on user requirements:
3235
+ // (Grey Square): Not installed
3236
+ // ⚠️ (Yellow Triangle): Installed but (Quota limit OR Not verified)
3237
+ // 🚫 (Red Circle): Disabled
3238
+ // 🛑 (Red Stop Sign): Not working/Error
3239
+ // 🟢 (Green Circle): Available/Working
2786
3240
 
2787
- // Determine status emoji: disabled = red alert, rate limited = green circle, enabled = green circle
2788
3241
  let statusEmoji;
2789
- if (id === 'kiro' && !isInstalled) {
2790
- statusEmoji = '🟡'; // Yellow for not installed
3242
+ let statusText = '';
3243
+
3244
+ if (!isEnabled) {
3245
+ statusEmoji = '🚫'; // Disabled
3246
+ } else if (!isInstalled) {
3247
+ statusEmoji = '⬜'; // Not installed
2791
3248
  } else {
2792
- statusEmoji = !isEnabled ? '🚨' : '🟢';
2793
- }
3249
+ // Installed and Enabled, assume Green unless quota or error
3250
+ statusEmoji = '🟢';
2794
3251
 
2795
- const typeLabel = def.type === 'ide' ? chalk.cyan('IDE') : chalk.cyan('LLM');
2796
- const prefix = isSelected ? chalk.cyan('❯') : ' ';
2797
- let line = `${prefix} ${statusEmoji} ${idx + 1}. ${def.name} ${chalk.gray(`(${def.id})`)} ${typeLabel}`;
3252
+ if (def.type === 'ide') {
3253
+ // TODO: Add actual "verified" check if available. For now assuming installed = verified.
3254
+ // If we had a verification check:
3255
+ // if (!isVerified) statusEmoji = '⚠️';
3256
+ }
2798
3257
 
2799
- // Fetch and display specific quota for this agent
2800
- try {
2801
- // Find the active model for this provider if possible
2802
- let model = def.defaultModel || id;
2803
- if (id === 'groq') model = autoConfig.groqModel || model;
2804
- else if (id === 'anthropic') model = autoConfig.anthropicModel || model;
2805
- else if (id === 'ollama') {
2806
- const preferredModel = autoConfig.llmModel && autoConfig.llmModel.includes('ollama/')
2807
- ? autoConfig.llmModel.split('/')[1]
2808
- : autoConfig.llmModel || autoConfig.aiderModel;
2809
- model = (preferredModel && preferredModel !== id) ? preferredModel : model;
3258
+ // Check Quota
3259
+ if (quota) {
3260
+ // Handle both Quota class instances and plain JSON objects from cache
3261
+ let isExceeded = false;
3262
+ if (typeof quota.isExceeded === 'function') {
3263
+ isExceeded = quota.isExceeded();
3264
+ } else if (quota.remaining !== undefined) {
3265
+ isExceeded = quota.remaining <= 0;
3266
+ }
3267
+
3268
+ if (quota.type === 'rate-limit' && isExceeded) {
3269
+ statusEmoji = '⚠️ ';
3270
+ if (quota.resetsAt) {
3271
+ const label = formatResetsAtLabel(quota.resetsAt);
3272
+ statusText = label ? ` ${chalk.red(`⏰ ${label}`)}` : ` ${chalk.red('[Rate limited]')}`;
3273
+ } else {
3274
+ statusText = ` ${chalk.red('[Rate limited]')}`;
3275
+ }
3276
+ }
2810
3277
  }
3278
+ }
2811
3279
 
2812
- const agentId = `${id}:${model}`;
2813
- const quota = await fetchQuotaForAgent(agentId);
3280
+ // Determine Interface Type Icon (GUI vs Direct Automation)
3281
+ let interfaceIcon = '⚡'; // Default to Direct Automation
3282
+ if (def.type === 'ide') {
3283
+ interfaceIcon = '🖥️'; // GUI Automation for IDEs
3284
+ }
3285
+
3286
+ // Determine Provider Type Icon (Cloud vs Local)
3287
+ let providerTypeIcon = '☁️'; // Default to Cloud (IDEs and most LLMs)
3288
+ if (def.type === 'direct' && def.category === 'llm' && def.id === 'ollama') {
3289
+ providerTypeIcon = '🏠'; // Local (only for Ollama)
3290
+ }
2814
3291
 
2815
- if (debugQuota) {
2816
- const resetMs = quota?.resetsAt ? (new Date(quota.resetsAt).getTime() - Date.now()) : null;
2817
- console.error(`[VCM_DEBUG_QUOTA] provider=${id} model=${model} type=${quota?.type} remaining=${quota?.remaining} limit=${quota?.limit} resetsAt=${quota?.resetsAt ? new Date(quota.resetsAt).toISOString() : 'null'} resetIn=${resetMs !== null ? formatDuration(resetMs) : 'null'}`);
3292
+ // Trim emojis to remove any hidden characters and ensure clean spacing
3293
+ interfaceIcon = interfaceIcon.trim();
3294
+ providerTypeIcon = providerTypeIcon.trim();
3295
+ const isWarningEmoji = statusEmoji.includes('⚠️');
3296
+ statusEmoji = statusEmoji.trim();
3297
+
3298
+ // Determine spacing based on interface icon type and provider type
3299
+ // Multi-char icons (🖥️) need double spaces, single-char icons (⚡) use single space after interface icon
3300
+ const isMultiCharInterface = interfaceIcon.length > 1;
3301
+ const spaceAfterInterface = isMultiCharInterface ? ' ' : ' ';
3302
+ // Local provider (🏠) uses single space, others use double space after provider icon
3303
+ const isLocalProvider = providerTypeIcon === '🏠';
3304
+ const spaceAfterProvider = isLocalProvider ? ' ' : ' ';
3305
+
3306
+ // Adjust spacing after status emoji - warning emoji needs extra space
3307
+ const spaceAfterStatus = isWarningEmoji ? ' ' : ' ';
3308
+
3309
+ const prefix = isSelected ? chalk.cyan('❯') + ' ' : ' ';
3310
+
3311
+ // Get health metrics for this IDE (if available)
3312
+ let healthCounters = '';
3313
+ const ideMetrics = healthMetricsMap.get(def.id);
3314
+ if (ideMetrics) {
3315
+ const counters = HealthReporter.formatInlineCounters(ideMetrics);
3316
+ if (counters) {
3317
+ // Color code the counters: green for +N, red for -M
3318
+ const coloredCounters = counters
3319
+ .split(' ')
3320
+ .map(part => part.startsWith('+') ? chalk.green(part) : chalk.red(part))
3321
+ .join(' ');
3322
+ healthCounters = ` ${coloredCounters}`;
2818
3323
  }
3324
+ }
2819
3325
 
2820
- if (quota.type === 'infinite') {
2821
- line += ` ${chalk.gray('[' + t('provider.status.quota.infinite') + ']')}`;
2822
- } else if (quota.type === 'rate-limit') {
2823
- if (quota.isExceeded()) {
2824
- if (quota.resetsAt) {
2825
- const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
2826
- line += ` ${chalk.red(`[⏳ resets in ${formatDuration(msUntilReset)}]`)}`;
3326
+ // Status emoji is now first, followed by interface icon and provider type icon
3327
+ let line = `${prefix}${statusEmoji}${spaceAfterStatus}${interfaceIcon}${spaceAfterInterface}${providerTypeIcon}${spaceAfterProvider} ${idx + 1}. ${def.name} ${chalk.gray(`(${def.id})`)}${healthCounters}${statusText}`;
3328
+ if (debugQuota && quota) {
3329
+ // Append extra debug info if needed
3330
+ const resetMs = quota?.resetsAt ? (new Date(quota.resetsAt).getTime() - Date.now()) : null;
3331
+ line += chalk.gray(` [D: type=${quota?.type} rem=${quota?.remaining} lim=${quota?.limit} resetIn=${resetMs !== null ? formatDuration(resetMs) : 'null'}]`);
3332
+ }
3333
+ console.log(line);
3334
+
3335
+ if (Array.isArray(def.subAgents) && def.subAgents.length > 0) {
3336
+ for (const sub of def.subAgents) {
3337
+ const info = providerManager.getRateLimitInfo(def.id, sub.model);
3338
+ let subStatusEmoji;
3339
+ let subStatusText = '';
3340
+
3341
+ if (!isEnabled) {
3342
+ subStatusEmoji = '🚫';
3343
+ } else if (!isInstalled) {
3344
+ subStatusEmoji = '⬜';
3345
+ } else if (info && info.isRateLimited) {
3346
+ subStatusEmoji = '⚠️';
3347
+ if (info.resetTime) {
3348
+ const label = formatResetsAtLabel(info.resetTime);
3349
+ subStatusText = label ? ` ${chalk.red(`⏰ ${label}`)}` : ` ${chalk.red('[Rate limited]')}`;
2827
3350
  } else {
2828
- line += ` ${chalk.red('[Rate limited]')}`;
3351
+ subStatusText = ` ${chalk.red('[Rate limited]')}`;
2829
3352
  }
2830
3353
  } else {
2831
- // Show time until rate limit starts (when it resets)
2832
- if (quota.resetsAt) {
2833
- const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
2834
- line += ` ${chalk.green(`[✓ ${t('provider.status.available.resets')} ${formatDuration(msUntilReset)}]`)}`;
2835
- } else {
2836
- line += ` ${chalk.green('[' + t('provider.status.available') + ']')}`;
2837
- }
3354
+ subStatusEmoji = '🟢';
2838
3355
  }
3356
+
3357
+ console.log(` ${subStatusEmoji} ${chalk.gray('↳')} ${sub.name} ${chalk.gray(`(${sub.model})`)}${subStatusText}`);
2839
3358
  }
2840
- } catch (e) {
2841
- // Silently skip if quota fetch fails
2842
3359
  }
2843
-
2844
- console.log(line);
2845
3360
  }
2846
3361
 
2847
3362
  console.log();
@@ -2852,6 +3367,143 @@ async function showProviderManagerMenu() {
2852
3367
  }
2853
3368
  };
2854
3369
 
3370
+ // Background loading function
3371
+ const loadDataInBackground = async () => {
3372
+ try {
3373
+ // 1. Load config and auth info
3374
+ const [config, quota] = await Promise.all([
3375
+ getAutoConfig(),
3376
+ sharedAuth.canRunAutoMode()
3377
+ ]);
3378
+ autoConfig = config;
3379
+ quotaInfo = quota;
3380
+ if (typeof render === 'function') await render();
3381
+
3382
+ // 2. Check installation status in parallel
3383
+ const installPromises = definitions.map(async (def) => {
3384
+ let isInstalled = true;
3385
+ if (def.type === 'ide') {
3386
+ try {
3387
+ if (def.id === 'kiro') {
3388
+ const { isKiroInstalled } = require('./kiro-installer');
3389
+ isInstalled = isKiroInstalled();
3390
+ } else if (def.id === 'cursor') {
3391
+ isInstalled = await checkAppOrBinary(['Cursor'], ['cursor']);
3392
+ } else if (def.id === 'windsurf') {
3393
+ isInstalled = await checkAppOrBinary(['Windsurf'], ['windsurf']);
3394
+ } else if (def.id === 'vscode') {
3395
+ isInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
3396
+ } else if (def.id === 'github-copilot' || def.id === 'amazon-q') {
3397
+ // Check if VS Code is installed AND the extension is installed
3398
+ const vscodeInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
3399
+ if (vscodeInstalled) {
3400
+ isInstalled = await checkVSCodeExtension(def.id);
3401
+ } else {
3402
+ isInstalled = false;
3403
+ }
3404
+ } else if (def.id === 'replit') {
3405
+ // Replit is web-based, always considered "installed"
3406
+ isInstalled = true;
3407
+ } else if (def.id === 'antigravity') {
3408
+ isInstalled = await checkAppOrBinary(['Antigravity'], ['antigravity']);
3409
+ } else {
3410
+ isInstalled = await checkAppOrBinary([], [def.id]);
3411
+ }
3412
+ } catch (e) {
3413
+ isInstalled = false;
3414
+ }
3415
+ }
3416
+ installationStatus.set(def.id, isInstalled);
3417
+ return { id: def.id, installed: isInstalled };
3418
+ });
3419
+
3420
+ const installResults = await Promise.all(installPromises);
3421
+
3422
+ // Update cache
3423
+ let newCache = {};
3424
+ installResults.forEach(r => {
3425
+ newCache[r.id] = { ...(savedCache[r.id] || {}), installed: r.installed };
3426
+ });
3427
+ await setProviderCache(newCache);
3428
+
3429
+ if (typeof render === 'function') await render();
3430
+
3431
+ // 3. Check quotas in parallel
3432
+ const quotaPromises = definitions.map(async (def) => {
3433
+ try {
3434
+ let model = def.defaultModel || def.id;
3435
+ if (def.id === 'groq') model = autoConfig.groqModel || model;
3436
+ else if (def.id === 'anthropic') model = autoConfig.anthropicModel || model;
3437
+ else if (def.id === 'ollama') {
3438
+ const preferredModel = autoConfig.llmModel && autoConfig.llmModel.includes('ollama/')
3439
+ ? autoConfig.llmModel.split('/')[1]
3440
+ : autoConfig.llmModel || autoConfig.aiderModel;
3441
+ model = (preferredModel && preferredModel !== def.id) ? preferredModel : model;
3442
+ }
3443
+
3444
+ const agentId = `${def.id}:${model}`;
3445
+ const quota = await fetchQuotaForAgent(agentId);
3446
+
3447
+ // Merge cautiously: if we already have an active rate-limit prefetched from
3448
+ // ProviderManager, avoid overwriting it with a non-rate-limit result which
3449
+ // could make the UI lose the reset information briefly. If both are rate-limits,
3450
+ // prefer the earliest reset time.
3451
+ try {
3452
+ const existing = agentQuotas.get(def.id);
3453
+ const existingIsActiveRateLimit = existing && existing.type === 'rate-limit' && new Date(existing.resetsAt).getTime() > Date.now();
3454
+ const newIsRateLimit = quota && quota.type === 'rate-limit';
3455
+
3456
+ if (existingIsActiveRateLimit && !newIsRateLimit) {
3457
+ // Keep the existing active rate-limit and don't overwrite
3458
+ } else if (existingIsActiveRateLimit && newIsRateLimit) {
3459
+ // Both rate-limited: keep the one with the earliest reset
3460
+ const existingReset = new Date(existing.resetsAt).getTime();
3461
+ const newReset = quota && quota.resetsAt ? new Date(quota.resetsAt).getTime() : null;
3462
+ if (!newReset || existingReset <= newReset) {
3463
+ // keep existing
3464
+ } else {
3465
+ agentQuotas.set(def.id, quota);
3466
+ }
3467
+ } else {
3468
+ // No active existing rate limit — set/overwrite normally
3469
+ agentQuotas.set(def.id, quota);
3470
+ }
3471
+ } catch (e) {
3472
+ // On any error, set the quota to avoid leaving stale state
3473
+ agentQuotas.set(def.id, quota);
3474
+ }
3475
+
3476
+ return { id: def.id, quota };
3477
+ } catch (e) {
3478
+ return null;
3479
+ }
3480
+ });
3481
+
3482
+ const quotaResults = await Promise.all(quotaPromises);
3483
+
3484
+ // Update cache with quotas
3485
+ newCache = {};
3486
+ quotaResults.forEach(r => {
3487
+ if (r) {
3488
+ newCache[r.id] = { ...(savedCache[r.id] || {}), quota: r.quota };
3489
+ // Preserve installed status if we just set it
3490
+ if (installationStatus.has(r.id)) {
3491
+ newCache[r.id].installed = installationStatus.get(r.id);
3492
+ }
3493
+ }
3494
+ });
3495
+ await setProviderCache(newCache);
3496
+
3497
+ if (typeof render === 'function') await render();
3498
+
3499
+ } catch (error) {
3500
+ // Silently fail in background
3501
+ }
3502
+ };
3503
+
3504
+ // Start background loading
3505
+ loadDataInBackground();
3506
+
2855
3507
  if (process.env.VCM_RENDER_ONCE === '1' || process.env.VCM_RENDER_ONCE === 'true') {
2856
3508
  await render();
2857
3509
  return;
@@ -2859,6 +3511,8 @@ async function showProviderManagerMenu() {
2859
3511
 
2860
3512
  return new Promise(async (resolve) => {
2861
3513
  const cleanup = () => {
3514
+ // Mark menu as inactive so background loaders stop rendering
3515
+ isMenuActive = false;
2862
3516
  if (process.stdin.isTTY && process.stdin.setRawMode) {
2863
3517
  process.stdin.setRawMode(false);
2864
3518
  }
@@ -2895,6 +3549,13 @@ async function showProviderManagerMenu() {
2895
3549
  };
2896
3550
 
2897
3551
  const cancel = () => {
3552
+ // Suppress input for a short period to avoid buffered key repeats reopening menus
3553
+ menuSuppressUntil = Date.now() + 1000; // 1 second
3554
+ if (process.env.VCM_DEBUG_MENU === '1') {
3555
+ const msg = `[DEBUG-MENU ${new Date().toISOString()}] menu cancelled; suppress until ${new Date(menuSuppressUntil).toISOString()}\n`;
3556
+ console.log(msg);
3557
+ try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
3558
+ }
2898
3559
  cleanup();
2899
3560
  console.log('\n');
2900
3561
  resolve(null);
@@ -2926,12 +3587,104 @@ async function showProviderManagerMenu() {
2926
3587
  await render();
2927
3588
  };
2928
3589
 
3590
+ const installSelected = async () => {
3591
+ const id = order[selectedIndex];
3592
+ const def = defMap.get(id);
3593
+
3594
+ if (!def) return;
3595
+
3596
+ // Only IDE providers can be installed
3597
+ if (def.type !== 'ide') {
3598
+ console.log(chalk.yellow('\n⚠️ Installation is only available for IDE providers.\n'));
3599
+ await render();
3600
+ return;
3601
+ }
3602
+
3603
+ // Check if already installed
3604
+ const isInstalled = installationStatus.get(id);
3605
+ if (isInstalled) {
3606
+ console.log(chalk.yellow(`\n⚠️ ${def.name} is already installed.\n`));
3607
+ await render();
3608
+ return;
3609
+ }
3610
+
3611
+ console.log(chalk.cyan(`\n🔧 Installing ${def.name}...\n`));
3612
+
3613
+ try {
3614
+ const appleScriptManager = new AppleScriptManager();
3615
+ const repoPath = await getRepoPath();
3616
+
3617
+ // Use openIDE method which handles installation
3618
+ await appleScriptManager.openIDE(id, repoPath);
3619
+
3620
+ // Re-check installation status
3621
+ let newStatus = true;
3622
+ try {
3623
+ if (id === 'kiro') {
3624
+ const { isKiroInstalled } = require('./kiro-installer');
3625
+ newStatus = isKiroInstalled();
3626
+ } else if (id === 'cursor') {
3627
+ newStatus = await checkAppOrBinary(['Cursor'], ['cursor']);
3628
+ } else if (id === 'windsurf') {
3629
+ newStatus = await checkAppOrBinary(['Windsurf'], ['windsurf']);
3630
+ } else if (id === 'vscode') {
3631
+ newStatus = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
3632
+ } else if (id === 'github-copilot' || id === 'amazon-q') {
3633
+ // Check if VS Code is installed AND the extension is installed
3634
+ const vscodeInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
3635
+ if (vscodeInstalled) {
3636
+ newStatus = await checkVSCodeExtension(id);
3637
+ } else {
3638
+ newStatus = false;
3639
+ }
3640
+ } else if (id === 'antigravity') {
3641
+ newStatus = await checkAppOrBinary(['Antigravity'], ['antigravity']);
3642
+ } else if (id === 'replit') {
3643
+ // Replit is web-based, always considered "installed"
3644
+ newStatus = true;
3645
+ } else {
3646
+ newStatus = await checkAppOrBinary([], [id]);
3647
+ }
3648
+ } catch (e) {
3649
+ newStatus = false;
3650
+ }
3651
+
3652
+ installationStatus.set(id, newStatus);
3653
+
3654
+ // Update cache
3655
+ const savedCache = await getProviderCache();
3656
+ const newCache = {
3657
+ ...savedCache,
3658
+ [id]: { ...(savedCache[id] || {}), installed: newStatus }
3659
+ };
3660
+ await setProviderCache(newCache);
3661
+
3662
+ if (newStatus) {
3663
+ console.log(chalk.green(`\n✓ ${def.name} installed successfully!\n`));
3664
+ } else {
3665
+ console.log(chalk.yellow(`\n⚠️ Installation may have completed, but ${def.name} was not detected. Please check manually.\n`));
3666
+ }
3667
+ } catch (error) {
3668
+ console.log(chalk.red(`\n✗ Installation failed: ${error.message}\n`));
3669
+ }
3670
+
3671
+ await render();
3672
+ };
3673
+
3674
+ // Ignore spurious keypresses for a short debounce window after opening the menu
3675
+ const IGNORE_INPUT_MS = 300; // milliseconds
3676
+ let ignoreInputUntil = Date.now() + IGNORE_INPUT_MS;
3677
+
2929
3678
  const onKeypress = async (str, key = {}) => {
3679
+ // Allow immediate Ctrl+C regardless of debounce
2930
3680
  if (key.ctrl && key.name === 'c') {
2931
3681
  cancel();
2932
3682
  return;
2933
3683
  }
2934
3684
 
3685
+ // Debounce: ignore inputs that happen too soon after the menu opens
3686
+ if (Date.now() < ignoreInputUntil) return;
3687
+
2935
3688
  switch (key.name) {
2936
3689
  case 'up':
2937
3690
  await moveSelection(-1);
@@ -2951,15 +3704,22 @@ async function showProviderManagerMenu() {
2951
3704
  case 'd':
2952
3705
  await toggle(false);
2953
3706
  break;
3707
+ case 'i':
3708
+ await installSelected();
3709
+ break;
2954
3710
  case 'space':
2955
3711
  await toggle(!(enabled[order[selectedIndex]] !== false));
2956
3712
  break;
2957
3713
  case 'return':
2958
3714
  saveAndExit(order[selectedIndex]);
2959
3715
  break;
2960
- case 'escape':
2961
3716
  case 'left':
3717
+ // Left arrow: save any pending changes and exit (match Enter's save behavior for order)
3718
+ saveAndExit();
3719
+ break;
3720
+ case 'escape':
2962
3721
  case 'x':
3722
+ // Escape or 'x' should cancel without persisting pending changes
2963
3723
  cancel();
2964
3724
  break;
2965
3725
  default:
@@ -2978,23 +3738,54 @@ async function showProviderManagerMenu() {
2978
3738
  });
2979
3739
  }
2980
3740
 
2981
- /* async function showSettings() {
2982
- console.log(chalk.bold.cyan('\n⚙️ Settings\n'));
3741
+ /**
3742
+ * Get health metrics for IDE choices
3743
+ */
3744
+ async function getIDEHealthMetrics() {
3745
+ try {
3746
+ const healthTracker = new IDEHealthTracker();
3747
+ const allMetrics = await healthTracker.getAllHealthMetrics();
3748
+ return allMetrics;
3749
+ } catch (error) {
3750
+ // Health tracking not available, return empty metrics
3751
+ return new Map();
3752
+ }
3753
+ }
2983
3754
 
3755
+ /**
3756
+ * Format IDE name with health metrics
3757
+ */
3758
+ async function formatIDEChoiceWithHealth(ideName, ideValue, allMetrics) {
3759
+ const metrics = allMetrics.get(ideValue);
3760
+
3761
+ if (!metrics || metrics.totalInteractions === 0) {
3762
+ return ideName; // No health data available
3763
+ }
3764
+
3765
+ const successCount = metrics.successCount || 0;
3766
+ const failureCount = metrics.failureCount || 0;
3767
+ const healthIndicator = metrics.consecutiveFailures === 0 ? '✅' : '⚠️';
3768
+
3769
+ return `${ideName} ${healthIndicator} +${successCount} -${failureCount}`;
3770
+ }
3771
+
3772
+ async function showSettings() {
3773
+ console.log(chalk.bold.cyan('\n⚙️ Settings\n'));
3774
+
2984
3775
  const { setConfigValue } = require('vibecodingmachine-core');
2985
3776
  const { getAutoConfig, setAutoConfig } = require('./config');
2986
3777
  const currentHostnameEnabled = await isComputerNameEnabled();
2987
3778
  const hostname = getHostname();
2988
3779
  const autoConfig = await getAutoConfig();
2989
3780
  const currentIDE = autoConfig.ide || 'claude-code';
2990
-
3781
+
2991
3782
  // Show current settings
2992
3783
  console.log(chalk.gray('Current settings:'));
2993
3784
  console.log(chalk.gray(' Computer Name: '), chalk.cyan(hostname));
2994
3785
  console.log(chalk.gray(' Current IDE: '), chalk.cyan(formatIDEName(currentIDE)));
2995
3786
  console.log(chalk.gray(' Use Hostname in Req File: '), currentHostnameEnabled ? chalk.green('Enabled ✓') : chalk.yellow('Disabled ○'));
2996
3787
  console.log();
2997
-
3788
+
2998
3789
  const { action } = await inquirer.prompt([
2999
3790
  {
3000
3791
  type: 'list',
@@ -3016,54 +3807,59 @@ async function showProviderManagerMenu() {
3016
3807
  ]
3017
3808
  }
3018
3809
  ]);
3810
+
3811
+ /**
3812
+ * Get health metrics for IDE choices
3813
+ */
3814
+ async function getIDEHealthMetrics() {
3815
+ try {
3816
+ const healthTracker = new IDEHealthTracker();
3817
+ const allMetrics = await healthTracker.getAllHealthMetrics();
3818
+ return allMetrics;
3819
+ } catch (error) {
3820
+ // Health tracking not available, return empty metrics
3821
+ return new Map();
3822
+ }
3823
+ }
3019
3824
 
3020
- if (action === 'change-ide') {
3021
- const { ide } = await inquirer.prompt([
3022
- {
3023
- type: 'list',
3024
- name: 'ide',
3025
- message: 'Select IDE:',
3026
- choices: [
3027
- { name: 'Claude Code CLI (recommended - Anthropic Claude)', value: 'claude-code' },
3028
- { name: 'Aider CLI (best for Ollama & Bedrock)', value: 'aider' },
3029
- { name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
3030
- { name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
3031
- { name: 'Cursor', value: 'cursor' },
3032
- { name: 'VS Code', value: 'vscode' },
3033
- { name: 'Windsurf', value: 'windsurf' }
3034
- ],
3035
- default: currentIDE
3036
- }
3037
- ]);
3825
+ /**
3826
+ * Format IDE name with health metrics
3827
+ */
3828
+ async function formatIDEChoiceWithHealth(ideName, ideValue, allMetrics) {
3829
+ const metrics = allMetrics.get(ideValue);
3830
+
3831
+ if (!metrics || metrics.totalInteractions === 0) {
3832
+ return ideName; // No health data available
3833
+ }
3038
3834
 
3039
- // Save to config
3040
- const newConfig = { ...autoConfig, ide };
3041
- await setAutoConfig(newConfig);
3835
+ const successCount = metrics.successCount || 0;
3836
+ const failureCount = metrics.failureCount || 0;
3837
+ const healthIndicator = metrics.consecutiveFailures === 0 ? '✅' : '⚠️';
3838
+
3839
+ return `${ideName} ${healthIndicator} +${successCount} -${failureCount}`;
3840
+ }
3042
3841
 
3043
- console.log(chalk.green('\n✓'), `IDE changed to ${chalk.cyan(formatIDEName(ide))}`);
3044
- console.log(chalk.gray(' Note: This will be used for the next Auto Mode session.'));
3045
- console.log();
3046
- } else if (action === 'toggle-hostname') {
3842
+ async function showSettings() {
3047
3843
  const newValue = !currentHostnameEnabled;
3048
-
3844
+
3049
3845
  // Save to shared config (same location as Electron app)
3050
3846
  await setConfigValue('computerNameEnabled', newValue);
3051
-
3847
+
3052
3848
  const statusText = newValue ? chalk.green('enabled') : chalk.yellow('disabled');
3053
3849
  console.log(chalk.green('\n✓'), `Hostname in requirements file ${statusText}`);
3054
-
3850
+
3055
3851
  if (newValue) {
3056
3852
  console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan(`REQUIREMENTS-${hostname}.md`));
3057
3853
  } else {
3058
3854
  console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan('REQUIREMENTS.md'));
3059
3855
  }
3060
-
3856
+
3061
3857
  console.log(chalk.gray('\n Note: You may need to rename your existing requirements file.'));
3062
3858
  console.log(chalk.gray(' Note: This setting is now synced with the Electron app.'));
3063
3859
  console.log();
3064
3860
  }
3065
3861
  }
3066
-
3862
+
3067
3863
  /**
3068
3864
  * Show cloud sync management menu
3069
3865
  */
@@ -3451,24 +4247,13 @@ async function startInteractive() {
3451
4247
  const provider = displayAgent === 'ollama' ? 'ollama' : displayAgent;
3452
4248
 
3453
4249
  if (checkModel) {
3454
- const timeUntilReset = providerManager.getTimeUntilReset(provider, checkModel);
3455
-
3456
- if (timeUntilReset) {
3457
- // Format time remaining in human-readable format
3458
- const hours = Math.floor(timeUntilReset / (1000 * 60 * 60));
3459
- const minutes = Math.floor((timeUntilReset % (1000 * 60 * 60)) / (1000 * 60));
3460
- const seconds = Math.floor((timeUntilReset % (1000 * 60)) / 1000);
3461
-
3462
- let timeStr = '';
3463
- if (hours > 0) {
3464
- timeStr = `${hours}h ${minutes}m`;
3465
- } else if (minutes > 0) {
3466
- timeStr = `${minutes}m ${seconds}s`;
3467
- } else {
3468
- timeStr = `${seconds}s`;
3469
- }
4250
+ const info = providerManager.getRateLimitInfo(provider, checkModel);
3470
4251
 
3471
- agentDisplay += ` ${chalk.red('⏰ Rate limit resets in ' + timeStr)}`;
4252
+ if (info && info.isRateLimited && info.resetTime) {
4253
+ const label = formatResetsAtLabel(info.resetTime);
4254
+ if (label) {
4255
+ agentDisplay += ` ${chalk.red('⏰ ' + label)}`;
4256
+ }
3472
4257
  }
3473
4258
  }
3474
4259
  }
@@ -3516,7 +4301,9 @@ async function startInteractive() {
3516
4301
 
3517
4302
  // Add Requirements as a selectable setting with counts
3518
4303
  const hasRequirements = await requirementsExists();
3519
- const counts = hasRequirements ? await countRequirements() : null;
4304
+ const { getComputerFilter } = require('./config');
4305
+ const computerFilter = await getComputerFilter();
4306
+ const counts = hasRequirements ? await countRequirements(computerFilter) : null;
3520
4307
  let requirementsText = t('interactive.requirements') + ': ';
3521
4308
  if (counts) {
3522
4309
  // Use actual counts for display (not capped by maxChats)
@@ -3538,11 +4325,19 @@ async function startInteractive() {
3538
4325
  if (requirementsText !== '') {
3539
4326
  items.push({
3540
4327
  type: 'setting',
3541
- name: requirementsText,
4328
+ name: requirementsText + ` ${chalk.blue('Sync Status: ' + (await getSyncStatus()))}`,
3542
4329
  value: 'setting:requirements'
3543
4330
  });
3544
4331
  }
3545
4332
 
4333
+ // Add requirement filtering by computer
4334
+ const filterText = computerFilter ? chalk.cyan(computerFilter) : chalk.gray('All Computers');
4335
+ items.push({
4336
+ type: 'setting',
4337
+ name: ` └─ Filter Requirements by Computer: ${filterText}`,
4338
+ value: 'setting:filter-requirements'
4339
+ });
4340
+
3546
4341
  items.push({
3547
4342
  type: 'setting',
3548
4343
  name: ` └─ ${t('interactive.hostname.enabled')}: ${useHostname ? chalk.green('✓') : chalk.red('🛑')} ${useHostname ? '✓ ENABLED' : '🛑 ' + t('interactive.hostname.disabled')}`,
@@ -3559,44 +4354,33 @@ async function startInteractive() {
3559
4354
  value: 'setting:stages'
3560
4355
  });
3561
4356
 
3562
- // Cloud Sync Status
3563
- try {
3564
- const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
3565
- const syncEngine = new SyncEngine();
4357
+ items.push({
4358
+ type: 'setting',
4359
+ name: `Start Auto: Switch to Console tab and save updated agent list on exit (Left arrow to exit agent list will also save)`,
4360
+ value: 'setting:start-auto'
4361
+ });
3566
4362
 
3567
- // Try to initialize, but don't fail if AWS not configured
3568
- try {
3569
- await syncEngine.initialize();
3570
- const syncStatus = syncEngine.getStatus();
3571
- syncEngine.stop();
3572
-
3573
- const onlineIcon = syncStatus.isOnline ? chalk.green('●') : chalk.red('●');
3574
- const onlineText = syncStatus.isOnline ? t('interactive.online') : t('interactive.offline');
3575
- const queueText = syncStatus.queuedChanges > 0 ? chalk.yellow(` (${syncStatus.queuedChanges} queued)`) : '';
3576
-
3577
- items.push({
3578
- type: 'setting',
3579
- name: `${t('interactive.cloud.sync')}: ${onlineIcon} ${onlineText}${queueText}`,
3580
- value: 'setting:cloud-sync'
3581
- });
3582
- } catch (initError) {
3583
- // Initialization failed - AWS not configured or credentials missing
3584
- items.push({
3585
- type: 'setting',
3586
- name: `Cloud Sync: ${chalk.gray('● Not configured')}`,
3587
- value: 'setting:cloud-sync-setup'
3588
- });
3589
- }
3590
- } catch (error) {
3591
- // Module not found or other error - show disabled
3592
- items.push({
3593
- type: 'setting',
3594
- name: `Cloud Sync: ${chalk.gray('● Disabled')}`,
3595
- value: 'setting:cloud-sync-setup'
3596
- });
3597
- }
4363
+ items.push({
4364
+ type: 'setting',
4365
+ name: ` └─ ${t('interactive.feedback')}: ${chalk.cyan('📣 Press Ctrl+F for feedback')}`,
4366
+ value: 'setting:feedback'
4367
+ });
3598
4368
 
3599
- // Add "Next TODO Requirement" as a separate menu item if there are TODO items
4369
+ // TODO: Implement getNextTodoRequirement function
4370
+ // if (counts && counts.todoCount > 0) {
4371
+ // const { getNextTodoRequirement } = require('./auto-mode');
4372
+ // const nextRequirement = await getNextTodoRequirement();
4373
+ // if (nextRequirement) {
4374
+ // const requirementPreview = nextRequirement.length > 80
4375
+ // ? nextRequirement.substring(0, 77) + '...'
4376
+ // : nextRequirement;
4377
+ // items.push({
4378
+ // type: 'info',
4379
+ // name: ` └─ ${t('interactive.next.requirement')}: ${chalk.cyan(requirementPreview)}`,
4380
+ // value: 'info:next-requirement'
4381
+ // });
4382
+ // }
4383
+ // }
3600
4384
  if (counts && counts.todoCount > 0) {
3601
4385
  // Get the actual next requirement text (new header format)
3602
4386
  let nextReqText = '...';
@@ -3605,6 +4389,9 @@ async function startInteractive() {
3605
4389
  const hostname = await getHostname();
3606
4390
  const reqFilename = await getRequirementsFilename(hostname);
3607
4391
  const reqPath = path.join(repoPath, '.vibecodingmachine', reqFilename);
4392
+ // Increase the delay to prevent flashing and repaint
4393
+ await new Promise(resolve => setTimeout(resolve, 1000));
4394
+
3608
4395
 
3609
4396
  if (await fs.pathExists(reqPath)) {
3610
4397
  const reqContent = await fs.readFile(reqPath, 'utf8');
@@ -3714,10 +4501,20 @@ async function startInteractive() {
3714
4501
 
3715
4502
  switch (actionStr) {
3716
4503
  case 'setting:agent': {
4504
+ if (process.env.VCM_DEBUG_MENU === '1') {
4505
+ const msg = `[DEBUG-MENU ${new Date().toISOString()}] Main menu handling 'setting:agent' - invoking showProviderManagerMenu()\n` + new Error().stack.split('\n').slice(1, 6).join('\n') + '\n';
4506
+ console.log(msg);
4507
+ try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
4508
+ }
3717
4509
  await showProviderManagerMenu();
3718
4510
  await showWelcomeScreen();
3719
4511
  break;
3720
4512
  }
4513
+ case 'setting:feedback': {
4514
+ await handleFeedbackSubmission();
4515
+ await showWelcomeScreen();
4516
+ break;
4517
+ }
3721
4518
  case 'setting:provider': {
3722
4519
  // Switch AI provider - run provider setup only, don't start auto mode
3723
4520
  // Note: Continue CLI doesn't have provider setup - models are configured in ~/.continue/config.yaml
@@ -3920,6 +4717,69 @@ async function startInteractive() {
3920
4717
  await showWelcomeScreen();
3921
4718
  break;
3922
4719
  }
4720
+ case 'setting:filter-requirements': {
4721
+ // Configure requirement filtering by computer
4722
+ const { getComputerFilter, setComputerFilter } = require('./config');
4723
+ const inquirer = require('inquirer');
4724
+ const repoPath = await getRepoPath();
4725
+
4726
+ if (!repoPath) {
4727
+ console.log(chalk.yellow('No repository configured. Use repo:init or repo:set first.'));
4728
+ await showWelcomeScreen();
4729
+ break;
4730
+ }
4731
+
4732
+ // Find all REQUIREMENTS-*.md files to get available computers
4733
+ const vibeDir = path.join(repoPath, '.vibecodingmachine');
4734
+ const files = await fs.pathExists(vibeDir) ? await fs.readdir(vibeDir) : [];
4735
+ const reqFiles = files.filter(f => f.startsWith('REQUIREMENTS') && f.endsWith('.md'));
4736
+
4737
+ const computers = [];
4738
+ for (const file of reqFiles) {
4739
+ const filePath = path.join(vibeDir, file);
4740
+ const content = await fs.readFile(filePath, 'utf8');
4741
+ const hostnameMatch = content.match(/- \*\*Hostname\*\*:\s*(.+)/);
4742
+ if (hostnameMatch) {
4743
+ computers.push(hostnameMatch[1].trim());
4744
+ }
4745
+ }
4746
+
4747
+ if (computers.length === 0) {
4748
+ console.log(chalk.yellow('No computers found in requirements files.'));
4749
+ await showWelcomeScreen();
4750
+ break;
4751
+ }
4752
+
4753
+ const currentFilter = await getComputerFilter();
4754
+
4755
+ console.log(chalk.cyan(`\n🖥️ ${t('interactive.filter.requirements.by.computer')}\n`));
4756
+ console.log(chalk.gray('Select which computer\'s requirements to show, or "All Computers" to show all.\n'));
4757
+
4758
+ const { selectedComputer } = await inquirer.prompt([
4759
+ {
4760
+ type: 'list',
4761
+ name: 'selectedComputer',
4762
+ message: t('interactive.select.computer'),
4763
+ choices: [
4764
+ { name: chalk.gray('All Computers'), value: null },
4765
+ ...computers.map(c => ({
4766
+ name: c === currentFilter ? chalk.cyan(`${c} (current)`) : c,
4767
+ value: c
4768
+ }))
4769
+ ],
4770
+ default: currentFilter || null,
4771
+ loop: false,
4772
+ pageSize: 15
4773
+ }
4774
+ ]);
4775
+
4776
+ await setComputerFilter(selectedComputer);
4777
+
4778
+ const filterText = selectedComputer ? chalk.cyan(selectedComputer) : chalk.gray('All Computers');
4779
+ console.log(chalk.green('\n✓'), `Requirements filter set to: ${filterText}\n`);
4780
+ await showWelcomeScreen();
4781
+ break;
4782
+ }
3923
4783
  case 'setting:stages': {
3924
4784
  // Configure stages
3925
4785
  const { getStages, setStages, DEFAULT_STAGES } = require('./config');
@@ -3927,11 +4787,11 @@ async function startInteractive() {
3927
4787
 
3928
4788
  // Override inquirer's checkbox help text for current locale
3929
4789
  const CheckboxPrompt = require('inquirer/lib/prompts/checkbox');
3930
- const originalGetHelpText = CheckboxPrompt.prototype.getHelp || function() {
4790
+ const originalGetHelpText = CheckboxPrompt.prototype.getHelp || function () {
3931
4791
  return '(Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)';
3932
4792
  };
3933
4793
 
3934
- CheckboxPrompt.prototype.getHelp = function() {
4794
+ CheckboxPrompt.prototype.getHelp = function () {
3935
4795
  const locale = detectLocale();
3936
4796
  if (locale === 'es') {
3937
4797
  return '(Presiona <espacio> para seleccionar, <a> para alternar todo, <i> para invertir selección, y <enter> para proceder)';
@@ -4013,8 +4873,29 @@ async function startInteractive() {
4013
4873
  const { getAutoConfig } = require('./config');
4014
4874
  const currentConfig = await getAutoConfig();
4015
4875
 
4876
+ // Get first enabled agent from provider preferences (this is what user expects)
4877
+ const { getProviderPreferences } = require('../utils/provider-registry');
4878
+ const prefs = await getProviderPreferences();
4879
+ console.log(chalk.gray('[DEBUG] Provider preferences:'), JSON.stringify(prefs, null, 2));
4880
+ let firstEnabledAgent = null;
4881
+ for (const agentId of prefs.order) {
4882
+ if (prefs.enabled[agentId] !== false) {
4883
+ firstEnabledAgent = agentId;
4884
+ console.log(chalk.gray('[DEBUG] Found first enabled agent:'), firstEnabledAgent);
4885
+ break;
4886
+ }
4887
+ }
4888
+ const agentToUse = firstEnabledAgent || currentIDE;
4889
+ console.log(chalk.gray('[DEBUG] Agent to use:'), agentToUse, '(firstEnabled:', firstEnabledAgent, ', fallback:', currentIDE, ')');
4890
+
4016
4891
  // Use saved maxChats/neverStop settings
4017
- const options = { ide: currentIDE };
4892
+ const options = { ide: agentToUse };
4893
+
4894
+ // Set extension for VS Code extensions from saved config
4895
+ if (agentToUse === 'amazon-q' || agentToUse === 'github-copilot') {
4896
+ options.extension = agentToUse;
4897
+ }
4898
+
4018
4899
  if (currentConfig.neverStop) {
4019
4900
  options.neverStop = true;
4020
4901
  } else if (currentConfig.maxChats) {
@@ -4034,10 +4915,9 @@ async function startInteractive() {
4034
4915
  }
4035
4916
 
4036
4917
  // ALWAYS use auto:direct (supports both LLM and IDE agents with proper looping)
4037
- const currentAgent = currentConfig.agent || currentConfig.ide || 'ollama';
4038
- console.log(chalk.gray('[DEBUG] Using auto:direct for agent:', currentAgent));
4918
+ console.log(chalk.gray('[DEBUG] Using auto:direct for agent:', agentToUse));
4039
4919
  const { handleAutoStart: handleDirectAutoStart } = require('../commands/auto-direct');
4040
- await handleDirectAutoStart({ maxChats: options.maxChats });
4920
+ await handleDirectAutoStart(options);
4041
4921
 
4042
4922
  // Prompt user before returning to menu (so they can read the output)
4043
4923
  console.log('');
@@ -4252,8 +5132,11 @@ async function startInteractive() {
4252
5132
  { name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
4253
5133
  { name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
4254
5134
  { name: 'Cursor', value: 'cursor' },
4255
- { name: 'VS Code', value: 'vscode' },
4256
- { name: 'Windsurf', value: 'windsurf' }
5135
+ { name: 'VS Code (with Amazon Q Developer)', value: 'amazon-q' },
5136
+ { name: 'VS Code (with GitHub Copilot)', value: 'github-copilot' },
5137
+ { name: 'VS Code (plain)', value: 'vscode' },
5138
+ { name: 'Windsurf', value: 'windsurf' },
5139
+ { name: 'Google Antigravity', value: 'antigravity' }
4257
5140
  ],
4258
5141
  default: 'claude-code'
4259
5142
  },
@@ -4264,7 +5147,42 @@ async function startInteractive() {
4264
5147
  default: ''
4265
5148
  }
4266
5149
  ]);
5150
+
5151
+ let ideModel = null;
5152
+ if (ide === 'windsurf' || ide === 'antigravity') {
5153
+ const { selectedModel } = await inquirer.prompt([
5154
+ {
5155
+ type: 'list',
5156
+ name: 'selectedModel',
5157
+ message: `Select ${ide === 'windsurf' ? 'Windsurf' : 'Antigravity'} agent/model:`,
5158
+ choices: (ide === 'windsurf' ? [
5159
+ { name: 'Default', value: 'default' },
5160
+ { name: 'SWE-1-lite', value: 'swe-1-lite' }
5161
+ ] : [
5162
+ { name: 'Default', value: 'antigravity' },
5163
+ { name: 'Gemini 3 Pro (Low)', value: 'Gemini 3 Pro (Low)' },
5164
+ { name: 'Claude Sonnet 4.5', value: 'Claude Sonnet 4.5' },
5165
+ { name: 'Claude Sonnet 4.5 (Thinking)', value: 'Claude Sonnet 4.5 (Thinking)' },
5166
+ { name: 'GPT-OSS 120B (Medium)', value: 'GPT-OSS 120B (Medium)' }
5167
+ ]),
5168
+ default: 'default'
5169
+ }
5170
+ ]);
5171
+ ideModel = selectedModel;
5172
+ }
5173
+
4267
5174
  const options = { ide };
5175
+ if (ideModel) {
5176
+ options.ideModel = ideModel;
5177
+ }
5178
+
5179
+ // Set extension for VS Code extensions
5180
+ if (ide === 'amazon-q') {
5181
+ options.extension = 'amazon-q';
5182
+ } else if (ide === 'github-copilot') {
5183
+ options.extension = 'github-copilot';
5184
+ }
5185
+
4268
5186
  if (maxChats && maxChats.trim() !== '0') {
4269
5187
  options.maxChats = parseInt(maxChats);
4270
5188
  } else {
@@ -4286,6 +5204,21 @@ async function startInteractive() {
4286
5204
  const hostname = getHostname();
4287
5205
  console.log('[DEBUG] Repo:', repoPath, 'Hostname:', hostname);
4288
5206
 
5207
+ // Get current git branch if in a git repo
5208
+ let currentBranch = '';
5209
+ if (repoPath) {
5210
+ try {
5211
+ const { execSync } = require('child_process');
5212
+ currentBranch = execSync('git branch --show-current', {
5213
+ cwd: repoPath,
5214
+ encoding: 'utf8'
5215
+ }).trim();
5216
+ } catch (err) {
5217
+ // Not a git repo or git not available
5218
+ currentBranch = '';
5219
+ }
5220
+ }
5221
+
4289
5222
  // Build menu content for header
4290
5223
  const menuContent = `╭───────────────────────────────────────────────────────╮
4291
5224
  │ │
@@ -4295,7 +5228,7 @@ async function startInteractive() {
4295
5228
  ╰───────────────────────────────────────────────────────╯
4296
5229
 
4297
5230
  ${t('system.repo').padEnd(25)} ${repoPath || 'Not set'}
4298
- ${t('system.computer.name').padEnd(25)} ${hostname}
5231
+ ${currentBranch ? t('system.git.branch').padEnd(25) + ' ' + currentBranch + '\n' : ''}${t('system.computer.name').padEnd(25)} ${hostname}
4299
5232
  Current IDE: ${formatIDEName(ide)}
4300
5233
  AI Provider: ${getCurrentAIProvider(ide) || 'N/A'}
4301
5234
  Max Chats: ${maxChats || 'Never stop'}`;
@@ -4343,6 +5276,60 @@ Max Chats: ${maxChats || 'Never stop'}`;
4343
5276
  });
4344
5277
  }
4345
5278
 
5279
+ // Helper to parse requirements file content
5280
+ const parseRequirementsFile = (content) => {
5281
+ const lines = content.split('\n');
5282
+ let currentRequirement = null;
5283
+ let currentStatus = null;
5284
+
5285
+ // Find current requirement (first one under TODO section)
5286
+ let inTodoSection = false;
5287
+ for (let i = 0; i < lines.length; i++) {
5288
+ const line = lines[i];
5289
+ const trimmed = line.trim();
5290
+
5291
+ // Check if we're in TODO section
5292
+ if (trimmed.includes('⏳ Requirements not yet completed') || trimmed.includes('Requirements not yet completed')) {
5293
+ inTodoSection = true;
5294
+ continue;
5295
+ }
5296
+
5297
+ // Exit TODO section at next ## header
5298
+ if (inTodoSection && trimmed.startsWith('## ') && !trimmed.startsWith('###')) {
5299
+ break;
5300
+ }
5301
+
5302
+ // Get first requirement in TODO section
5303
+ if (inTodoSection && trimmed.startsWith('### ')) {
5304
+ currentRequirement = trimmed.replace(/^###\s*/, '').trim();
5305
+ break;
5306
+ }
5307
+ }
5308
+
5309
+ // Find current status
5310
+ for (let i = 0; i < lines.length; i++) {
5311
+ const line = lines[i];
5312
+ const trimmed = line.trim();
5313
+
5314
+ if (trimmed.includes('🚦 Current Status') || trimmed.includes('Current Status')) {
5315
+ // Look for status in next few lines
5316
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
5317
+ const statusLine = lines[j].trim();
5318
+ if (statusLine.match(/^(PREPARE|CREATE|ACT|CLEAN UP|VERIFY|DONE)$/i)) {
5319
+ currentStatus = statusLine.toUpperCase();
5320
+ break;
5321
+ }
5322
+ }
5323
+ break;
5324
+ }
5325
+ }
5326
+
5327
+ return {
5328
+ currentRequirement: currentRequirement || 'No active requirement',
5329
+ currentStatus: currentStatus || 'PREPARE'
5330
+ };
5331
+ };
5332
+
4346
5333
  // Watch requirements file for changes
4347
5334
  const watcher = chokidar.watch(reqPath, { persistent: true });
4348
5335
  watcher.on('change', async () => {
@@ -4466,7 +5453,49 @@ Max Chats: ${maxChats || 'Never stop'}`;
4466
5453
  }
4467
5454
  }
4468
5455
 
4469
- module.exports = { startInteractive };
5456
+ async function bootstrapProjectIfInHomeDir() {
5457
+ const { checkVibeCodingMachineExists, getRequirementsFilename } = require('vibecodingmachine-core');
5458
+ const exists = await checkVibeCodingMachineExists();
5459
+ const home = os.homedir();
5460
+
5461
+ // If a VibeCodingMachine project already exists nearby, do nothing
5462
+ if (exists && (exists.insideExists || exists.siblingExists)) return;
5463
+
5464
+ // Only bootstrap when we're sitting at the user's home directory (resolve symlinks)
5465
+ try {
5466
+ const cwdResolved = await fs.realpath(process.cwd());
5467
+ const homeResolved = await fs.realpath(home);
5468
+ if (cwdResolved !== homeResolved) return;
5469
+ } catch (e) {
5470
+ // If we can't resolve, fallback to path.resolve comparison
5471
+ if (path.resolve(process.cwd()) !== path.resolve(home)) return;
5472
+ }
5473
+
5474
+ const inquirer = require('inquirer');
5475
+ const { shouldCreateCodeDir } = await inquirer.prompt({ type: 'confirm', name: 'shouldCreateCodeDir', message: t('interactive.should.create.code.dir') });
5476
+ if (!shouldCreateCodeDir) return;
5477
+
5478
+ const { projectName } = await inquirer.prompt({ type: 'input', name: 'projectName', message: t('interactive.project.name') });
5479
+ const slug = (projectName || 'my-project-name').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') || 'my-project-name';
5480
+ const codeDir = path.join(home, 'code');
5481
+ await fs.ensureDir(codeDir);
5482
+
5483
+ const projectDir = path.join(codeDir, slug);
5484
+ await fs.ensureDir(projectDir);
5485
+
5486
+ process.chdir(projectDir);
5487
+
5488
+ // Create .vibecodingmachine and REQUIREMENTS file if missing
5489
+ const vcmDir = path.join(projectDir, '.vibecodingmachine');
5490
+ await fs.ensureDir(vcmDir);
5491
+ const reqFilename = await getRequirementsFilename();
5492
+ const reqPath = path.join(vcmDir, reqFilename);
5493
+ if (!await fs.pathExists(reqPath)) {
5494
+ await fs.writeFile(reqPath, `# ${projectName}\n\n## ⏳ Requirements not yet completed\n\n`, 'utf8');
5495
+ }
5496
+ }
5497
+
5498
+ module.exports = { startInteractive, showProviderManagerMenu, /* exported for tests */ parseRequirementsFromContent, bootstrapProjectIfInHomeDir };
4470
5499
 
4471
5500
 
4472
5501