vibecodingmachine-cli 2026.1.3-2209 → 2026.1.23-1010

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 (40) 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 +8 -0
  5. package/package.json +2 -2
  6. package/reset_provider_order.js +21 -0
  7. package/scripts/convert-requirements.js +35 -0
  8. package/scripts/debug-parse.js +24 -0
  9. package/src/commands/auto-direct.js +679 -120
  10. package/src/commands/auto.js +200 -45
  11. package/src/commands/ide.js +108 -3
  12. package/src/commands/requirements-remote.js +10 -1
  13. package/src/commands/status.js +39 -1
  14. package/src/utils/antigravity-js-handler.js +13 -4
  15. package/src/utils/auth.js +37 -13
  16. package/src/utils/compliance-check.js +10 -0
  17. package/src/utils/config.js +29 -1
  18. package/src/utils/date-formatter.js +44 -0
  19. package/src/utils/interactive.js +1006 -537
  20. package/src/utils/kiro-js-handler.js +188 -0
  21. package/src/utils/provider-rate-cache.js +31 -0
  22. package/src/utils/provider-registry.js +42 -1
  23. package/src/utils/requirements-converter.js +107 -0
  24. package/src/utils/requirements-parser.js +144 -0
  25. package/tests/antigravity-js-handler.test.js +23 -0
  26. package/tests/integration/health-tracking.integration.test.js +284 -0
  27. package/tests/provider-manager.test.js +92 -0
  28. package/tests/rate-limit-display.test.js +44 -0
  29. package/tests/requirements-bullet-parsing.test.js +15 -0
  30. package/tests/requirements-converter.test.js +42 -0
  31. package/tests/requirements-heading-count.test.js +27 -0
  32. package/tests/requirements-legacy-parsing.test.js +15 -0
  33. package/tests/requirements-parse-integration.test.js +44 -0
  34. package/tests/wait-for-ide-completion.test.js +56 -0
  35. package/tests/wait-for-ide-quota-detection-cursor-screenshot.test.js +61 -0
  36. package/tests/wait-for-ide-quota-detection-cursor.test.js +60 -0
  37. package/tests/wait-for-ide-quota-detection-negative.test.js +45 -0
  38. package/tests/wait-for-ide-quota-detection.test.js +59 -0
  39. package/verify_fix.js +36 -0
  40. package/verify_ui.js +38 -0
@@ -10,7 +10,7 @@ const repo = require('../commands/repo');
10
10
  const auto = require('../commands/auto');
11
11
  const status = require('../commands/status');
12
12
  const requirements = require('../commands/requirements');
13
- const { getRepoPath, readConfig, writeConfig, getAutoConfig } = require('./config');
13
+ const { getRepoPath, readConfig, writeConfig, getAutoConfig, getProviderCache, setProviderCache } = require('./config');
14
14
  const { getProviderPreferences, saveProviderPreferences, getProviderDefinitions } = require('../utils/provider-registry');
15
15
  const { checkAutoModeStatus } = require('./auto-mode');
16
16
  const {
@@ -21,15 +21,43 @@ const {
21
21
  isComputerNameEnabled,
22
22
  t,
23
23
  detectLocale,
24
- setLocale
24
+ setLocale,
25
+ AppleScriptManager,
26
+ IDEHealthTracker,
27
+ HealthReporter
25
28
  } = require('vibecodingmachine-core');
26
29
  const { promptWithDefaultsOnce } = require('./prompt-helper');
30
+ const { formatResetsAtLabel } = require('./date-formatter');
27
31
 
28
32
  // Initialize locale detection for interactive mode
29
33
  const detectedLocale = detectLocale();
30
34
  setLocale(detectedLocale);
31
35
  const pkg = require('../../package.json');
32
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
+
33
61
  async function checkAppOrBinary(names = [], binaries = []) {
34
62
  // names: app bundle base names (e.g., 'Cursor' -> /Applications/Cursor.app)
35
63
  // binaries: CLI binary names to check on PATH (e.g., 'code')
@@ -52,10 +80,12 @@ async function checkAppOrBinary(names = [], binaries = []) {
52
80
  // Prefer to ensure the app is usable: use spctl to assess, fallback to quarantine xattr
53
81
  try {
54
82
  // spctl returns non-zero on rejected/invalid apps
55
- execSync(`spctl --assess -v "${p}"`, { stdio: 'ignore', timeout: 5000 });
83
+ // Use async exec to avoid blocking the event loop
84
+ await execAsync(`spctl --assess -v "${p}"`, { timeout: 5000 });
85
+
56
86
  // additionally validate codesign quickly (timeout to avoid hangs)
57
87
  try {
58
- execSync(`codesign -v --deep --strict "${p}"`, { stdio: 'ignore', timeout: 5000 });
88
+ await execAsync(`codesign -v --deep --strict "${p}"`, { timeout: 5000 });
59
89
  return true;
60
90
  } catch (csErr) {
61
91
  // codesign failed or timed out — treat as not usable/damaged
@@ -64,7 +94,8 @@ async function checkAppOrBinary(names = [], binaries = []) {
64
94
  } catch (e) {
65
95
  // spctl failed or timed out — check if app has quarantine attribute
66
96
  try {
67
- const out = execSync(`xattr -p com.apple.quarantine "${p}" 2>/dev/null || true`, { encoding: 'utf8' }).trim();
97
+ const { stdout } = await execAsync(`xattr -p com.apple.quarantine "${p}" 2>/dev/null || true`, { encoding: 'utf8' });
98
+ const out = stdout.trim();
68
99
  if (!out) {
69
100
  // no quarantine attribute but spctl failed — be conservative and treat as not installed
70
101
  return false;
@@ -91,7 +122,7 @@ async function checkAppOrBinary(names = [], binaries = []) {
91
122
  // Check PATH for known binaries
92
123
  for (const bin of binaries) {
93
124
  try {
94
- execSync(`which ${bin}`, { stdio: 'ignore' });
125
+ await execAsync(`which ${bin}`, { timeout: 2000 });
95
126
  return true;
96
127
  } catch (e) {
97
128
  /* not found */
@@ -130,78 +161,6 @@ function translateStage(stage) {
130
161
  return key ? t(key) : stage;
131
162
  }
132
163
 
133
- function normalizeProjectDirName(input) {
134
- const s = String(input || '').trim().toLowerCase();
135
- const replaced = s.replace(/\s+/g, '-');
136
- const cleaned = replaced.replace(/[^a-z0-9._-]/g, '-');
137
- const collapsed = cleaned.replace(/-+/g, '-').replace(/^[-._]+|[-._]+$/g, '');
138
- return collapsed || 'my-project';
139
- }
140
-
141
- async function bootstrapProjectIfInHomeDir() {
142
- let cwdResolved = path.resolve(process.cwd());
143
- let homeResolved = path.resolve(os.homedir());
144
-
145
- try {
146
- cwdResolved = await fs.realpath(cwdResolved);
147
- } catch (err) {
148
- // Ignore
149
- }
150
-
151
- try {
152
- homeResolved = await fs.realpath(homeResolved);
153
- } catch (err) {
154
- // Ignore
155
- }
156
-
157
- if (cwdResolved !== homeResolved) {
158
- return;
159
- }
160
-
161
- const codeDir = path.join(homeResolved, 'code');
162
- const codeDirExists = await fs.pathExists(codeDir);
163
-
164
- if (!codeDirExists) {
165
- const { shouldCreateCodeDir } = await inquirer.prompt([
166
- {
167
- type: 'confirm',
168
- name: 'shouldCreateCodeDir',
169
- message: `No code directory found at ${codeDir}. Would you like to create it?`,
170
- default: true
171
- }
172
- ]);
173
-
174
- if (!shouldCreateCodeDir) {
175
- return;
176
- }
177
-
178
- await fs.ensureDir(codeDir);
179
- }
180
-
181
- const { projectName } = await inquirer.prompt([
182
- {
183
- type: 'input',
184
- name: 'projectName',
185
- message: 'What is the project name you want to create?',
186
- validate: (value) => {
187
- const trimmed = String(value || '').trim();
188
- return trimmed.length > 0 || 'Please enter a project name.';
189
- }
190
- }
191
- ]);
192
-
193
- const dirName = normalizeProjectDirName(projectName);
194
- const projectDir = path.join(codeDir, dirName);
195
- await fs.ensureDir(projectDir);
196
-
197
- process.chdir(projectDir);
198
- try {
199
- await writeConfig({ ...(await readConfig()), repoPath: projectDir });
200
- } catch (err) {
201
- // Best-effort: interactive mode will still use process.cwd()
202
- }
203
- }
204
-
205
164
  /**
206
165
  * Format IDE name for display
207
166
  * @param {string} ide - Internal IDE identifier
@@ -507,7 +466,7 @@ async function showWelcomeScreen() {
507
466
 
508
467
  // Display welcome banner with version
509
468
  console.log('\n' + boxen(
510
- chalk.bold.cyan('Vibe Coding Machine') + '\n' +
469
+ chalk.bold.cyan('Vibe Coding Machine!') + '\n' +
511
470
  chalk.gray(version) + '\n' +
512
471
  chalk.gray(t('banner.tagline')),
513
472
  {
@@ -518,6 +477,9 @@ async function showWelcomeScreen() {
518
477
  }
519
478
  ));
520
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
+
521
483
  // Display repository and system info
522
484
  console.log();
523
485
  console.log(chalk.gray(t('system.repo').padEnd(25)), formatPath(repoPath));
@@ -606,10 +568,14 @@ function indexToLetter(index) {
606
568
  return String.fromCharCode(97 + index); // 97 is 'a'
607
569
  }
608
570
 
571
+ // Parse requirements from file content for a given section (supports ###, PACKAGE:, and '- ' bullets)
572
+ const { parseRequirementsFromContent } = require('./requirements-parser');
573
+
609
574
  // Tree-style requirements navigator
610
575
  async function showRequirementsTree() {
611
576
  console.log(chalk.bold.cyan('\n📋 ' + t('requirements.navigator.title') + '\n'));
612
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'));
613
579
 
614
580
  const tree = {
615
581
  expanded: { root: true },
@@ -725,136 +691,14 @@ async function showRequirementsTree() {
725
691
  }
726
692
 
727
693
  const content = await fs.readFile(reqPath, 'utf8');
728
- const lines = content.split('\n');
729
-
730
- let inSection = false;
731
- const requirements = [];
732
-
733
- // For TO VERIFY section, check multiple possible section titles
734
- const sectionTitles = sectionKey === 'verify'
735
- ? ['🔍 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']
736
- : [sectionTitle];
737
-
738
- // For TO VERIFY, we need to find the exact section header
739
- // The section header is: ## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG
740
- const toVerifySectionHeader = '## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG';
741
-
742
- for (let i = 0; i < lines.length; i++) {
743
- const line = lines[i];
744
- const trimmed = line.trim();
745
-
746
- // Check if this line matches any of the section titles
747
- // IMPORTANT: Only check section headers (lines starting with ##), not requirement text
748
- if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
749
- // Reset inSection if we hit a section header that's not our target section
750
- if (sectionKey === 'verify' && inSection) {
751
- // Check if this is still a TO VERIFY section header
752
- if (!isStillToVerify) {
753
- // This will be handled by the "leaving section" check below, but ensure we don't process it as entering
754
- }
755
- }
756
- if (sectionKey === 'verify') {
757
- // For TO VERIFY, check for the specific section header with exact matching
758
- // Must match the exact TO VERIFY section header, not just any line containing "TO VERIFY"
759
- const isToVerifyHeader = trimmed === '## 🔍 TO VERIFY BY HUMAN' ||
760
- trimmed.startsWith('## 🔍 TO VERIFY BY HUMAN') ||
761
- trimmed === '## 🔍 TO VERIFY' ||
762
- trimmed.startsWith('## 🔍 TO VERIFY') ||
763
- trimmed === '## TO VERIFY' ||
764
- trimmed.startsWith('## TO VERIFY') ||
765
- trimmed === '## ✅ TO VERIFY' ||
766
- trimmed.startsWith('## ✅ TO VERIFY') ||
767
- trimmed === toVerifySectionHeader ||
768
- (trimmed.startsWith(toVerifySectionHeader) && trimmed.includes('Needs Human to Verify'));
769
-
770
- if (isToVerifyHeader) {
771
- // Make sure it's not a VERIFIED section (without TO VERIFY)
772
- if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) && !trimmed.includes('📝 VERIFIED')) {
773
- inSection = true;
774
- continue;
775
- }
776
- } else {
777
- // If we hit a different section header and we're looking for TO VERIFY, make sure we're not in section
778
- // This prevents incorrectly reading from TODO or other sections
779
- if (trimmed.includes('⏳ Requirements not yet completed') ||
780
- trimmed.includes('Requirements not yet completed') ||
781
- trimmed === '## 📝 VERIFIED' ||
782
- trimmed.startsWith('## 📝 VERIFIED')) {
783
- // We're in TODO or VERIFIED section, not TO VERIFY - reset
784
- inSection = false;
785
- }
786
- }
787
- } else if (sectionTitles.some(title => trimmed.includes(title))) {
788
- inSection = true;
789
- continue;
790
- }
791
- }
792
-
793
- // Check if we're leaving the section (new section header that doesn't match)
794
- if (inSection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
795
- // If this is a new section header and it's not one of our section titles, we've left the section
796
- if (sectionKey === 'verify') {
797
- // For TO VERIFY, only break if this is clearly a different section
798
- // Check for specific section headers that indicate we've left TO VERIFY
799
-
800
- if (isVerifiedSection || isTodoSection || isRecycledSection || isClarificationSection) {
801
- break; // Different section, we've left TO VERIFY
802
- }
803
- // Otherwise, continue - might be REJECTED or CHANGELOG which are not section boundaries for TO VERIFY
804
- } else {
805
- // For other sections, break if it's a new section header that doesn't match
806
- if (!sectionTitles.some(title => trimmed.includes(title))) {
807
- break;
808
- }
809
- }
810
- }
811
-
812
- // Read requirements in new format (### header)
813
- if (inSection && line.trim().startsWith('###')) {
814
- const title = line.trim().replace(/^###\s*/, '').trim();
815
-
816
- // Skip malformed requirements (title is just a package name, empty, or too short)
817
- // Common package names that shouldn't be requirement titles
818
- const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
819
- if (!title || title.length === 0 || packageNames.includes(title.toLowerCase())) {
820
- continue; // Skip this malformed requirement
821
- }
822
-
823
- const details = [];
824
- let pkg = null;
825
-
826
- // Read package and description
827
- for (let j = i + 1; j < lines.length; j++) {
828
- const nextLine = lines[j].trim();
829
- // Stop if we hit another requirement or section
830
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
831
- break;
832
- }
833
- // Check for PACKAGE line
834
- if (nextLine.startsWith('PACKAGE:')) {
835
- pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
836
- } else if (nextLine && !nextLine.startsWith('PACKAGE:')) {
837
- // Description line
838
- details.push(nextLine);
839
- }
840
- }
841
694
 
842
- requirements.push({ title, details, pkg, lineIndex: i });
843
- }
844
- }
695
+ // Delegate to reusable parser
696
+ const allReqs = parseRequirementsFromContent(content, sectionKey, sectionTitle);
845
697
 
846
- // Remove duplicates based on title (keep first occurrence)
847
- const seenTitles = new Set();
848
- const uniqueRequirements = [];
849
- for (const req of requirements) {
850
- const normalizedTitle = req.title.replace(/^TRY AGAIN \(\d+(st|nd|rd|th) time\):\s*/i, '').trim();
851
- if (!seenTitles.has(normalizedTitle)) {
852
- seenTitles.add(normalizedTitle);
853
- uniqueRequirements.push(req);
854
- }
855
- }
698
+ // For TODO section, only show primary heading requirements (those marked from '###' titles)
699
+ if (sectionKey === 'todo') return allReqs.filter(r => r.source === 'heading');
856
700
 
857
- return uniqueRequirements;
701
+ return allReqs;
858
702
  };
859
703
 
860
704
  // Load VERIFIED requirements from CHANGELOG
@@ -1203,6 +1047,10 @@ async function showRequirementsTree() {
1203
1047
  tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
1204
1048
  await buildTree();
1205
1049
  }
1050
+ } else if (key.name === 'f') {
1051
+ // Feedback button ( megaphone 📣 )
1052
+ await handleFeedbackSubmission();
1053
+ await buildTree();
1206
1054
  } else if (key.name === 'r') {
1207
1055
  const current = tree.items[tree.selected];
1208
1056
  if (!current) continue; // Safety check
@@ -1279,6 +1127,90 @@ async function showRequirementsTree() {
1279
1127
  process.stdin.pause();
1280
1128
  }
1281
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
+
1282
1214
  // Helper to show goodbye message
1283
1215
  function showGoodbyeMessage() {
1284
1216
  const hour = new Date().getHours();
@@ -1627,6 +1559,7 @@ async function showClarificationActions(req, tree, loadClarification) {
1627
1559
  const actions = [
1628
1560
  { label: '✍️ Add/Edit Responses', value: 'edit-responses' },
1629
1561
  { label: '↩️ Move back to TODO (after clarification)', value: 'move-to-todo' },
1562
+ { label: '📣 Feedback', value: 'feedback' },
1630
1563
  { label: '🗑️ Delete', value: 'delete' }
1631
1564
  ];
1632
1565
 
@@ -1657,7 +1590,8 @@ async function showClarificationActions(req, tree, loadClarification) {
1657
1590
  });
1658
1591
 
1659
1592
  // Display menu
1660
- 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'));
1661
1595
  actions.forEach((action, idx) => {
1662
1596
  if (idx === selected) {
1663
1597
  console.log(chalk.cyan(`❯ ${action.label}`));
@@ -1688,6 +1622,10 @@ async function showClarificationActions(req, tree, loadClarification) {
1688
1622
  selected = Math.max(0, selected - 1);
1689
1623
  } else if (key.name === 'down') {
1690
1624
  selected = Math.min(actions.length - 1, selected + 1);
1625
+ } else if (key.name === 'f') {
1626
+ // Feedback button ( megaphone 📣 )
1627
+ await handleFeedbackSubmission();
1628
+ return;
1691
1629
  } else if (key.name === 'return' || key.name === 'space') {
1692
1630
  const action = actions[selected].value;
1693
1631
 
@@ -1699,6 +1637,9 @@ async function showClarificationActions(req, tree, loadClarification) {
1699
1637
  await moveClarificationToTodo(req, tree);
1700
1638
  tree.clarificationReqs = await loadClarification();
1701
1639
  return;
1640
+ } else if (action === 'feedback') {
1641
+ await handleFeedbackSubmission();
1642
+ return;
1702
1643
  } else if (action === 'delete') {
1703
1644
  await deleteClarification(req, tree);
1704
1645
  tree.clarificationReqs = await loadClarification();
@@ -1716,6 +1657,7 @@ async function showRequirementActions(req, sectionKey, tree) {
1716
1657
  { label: '👎 Thumbs down (demote to TODO)', value: 'thumbs-down' },
1717
1658
  { label: '⬆️ Move up', value: 'move-up' },
1718
1659
  { label: '⬇️ Move down', value: 'move-down' },
1660
+ { label: '📣 Feedback', value: 'feedback' },
1719
1661
  { label: '🗑️ Delete', value: 'delete' }
1720
1662
  ];
1721
1663
 
@@ -1745,6 +1687,7 @@ async function showRequirementActions(req, sectionKey, tree) {
1745
1687
 
1746
1688
  // Display menu (always reprinted)
1747
1689
  console.log();
1690
+ console.log(chalk.gray('💡 Press F for feedback - Share your thoughts anytime'));
1748
1691
  console.log(chalk.gray('What would you like to do? (↑/↓/Enter to select, ESC/← to go back)'));
1749
1692
  console.log();
1750
1693
  menuLines += 3; // Blank line + help text + blank line
@@ -1789,6 +1732,10 @@ async function showRequirementActions(req, sectionKey, tree) {
1789
1732
  process.exit(0);
1790
1733
  } else if (key.name === 'escape' || key.name === 'left') {
1791
1734
  return; // Go back
1735
+ } else if (key.name === 'f') {
1736
+ // Feedback button ( megaphone 📣 )
1737
+ await handleFeedbackSubmission();
1738
+ return;
1792
1739
  } else if (key.name === 'up') {
1793
1740
  selected = Math.max(0, selected - 1);
1794
1741
  } else if (key.name === 'down') {
@@ -1849,6 +1796,9 @@ async function performRequirementAction(action, req, sectionKey, tree) {
1849
1796
  case 'rename':
1850
1797
  await renameRequirement(req, sectionKey, tree);
1851
1798
  break;
1799
+ case 'feedback':
1800
+ await handleFeedbackSubmission();
1801
+ break;
1852
1802
  }
1853
1803
 
1854
1804
  await new Promise(resolve => setTimeout(resolve, 1000));
@@ -2641,6 +2591,104 @@ async function showRequirementsBySection(sectionTitle) {
2641
2591
  }
2642
2592
  }
2643
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
+
2644
2692
  // Helper to save reordered requirements back to file
2645
2693
  async function saveRequirementsOrder(reqPath, sectionTitle, requirements) {
2646
2694
  const content = await fs.readFile(reqPath, 'utf8');
@@ -2739,6 +2787,9 @@ async function showRequirementsFromChangelog() {
2739
2787
  }
2740
2788
  }
2741
2789
 
2790
+ // Input suppression timestamp to avoid immediate re-entry into menus after cancel
2791
+ let menuSuppressUntil = 0;
2792
+
2742
2793
  // Custom menu with both arrow keys and letter shortcuts
2743
2794
  async function showQuickMenu(items, initialSelectedIndex = 0) {
2744
2795
  return new Promise((resolve) => {
@@ -2750,6 +2801,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2750
2801
  if (selectedIndex >= items.length) selectedIndex = 0;
2751
2802
 
2752
2803
  let isFirstRender = true;
2804
+ let lastLinesPrinted = 0;
2753
2805
 
2754
2806
  // Helper to calculate visual lines occupied by text
2755
2807
  const getVisualLineCount = (text) => {
@@ -2889,7 +2941,30 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2889
2941
  process.stdin.setRawMode(true);
2890
2942
  }
2891
2943
 
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);
2949
+
2892
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') {
2958
+ cleanup();
2959
+ process.exit(0);
2960
+ return;
2961
+ }
2962
+
2963
+ // Debounce: ignore inputs until the short window passes
2964
+ if (Date.now() < ignoreInputUntil) {
2965
+ return;
2966
+ }
2967
+
2893
2968
  if (!key) return;
2894
2969
 
2895
2970
  // Ctrl+C to exit
@@ -2911,6 +2986,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2911
2986
  if (str && str.length === 1) {
2912
2987
  if (str === 'x') {
2913
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)`);
2914
2990
  cleanup();
2915
2991
  // Don't clear screen for exit - keep status visible
2916
2992
  resolve({ value: 'exit', selectedIndex });
@@ -2970,6 +3046,11 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2970
3046
  }
2971
3047
 
2972
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
+ }
2973
3054
  const definitions = getProviderDefinitions();
2974
3055
  const defMap = new Map(definitions.map(def => [def.id, def]));
2975
3056
  const prefs = await getProviderPreferences();
@@ -2979,6 +3060,8 @@ async function showProviderManagerMenu() {
2979
3060
  let dirty = false;
2980
3061
 
2981
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();
2982
3065
 
2983
3066
  const debugQuota = process.env.VCM_DEBUG_QUOTA === '1' || process.env.VCM_DEBUG_QUOTA === 'true';
2984
3067
 
@@ -2994,14 +3077,76 @@ async function showProviderManagerMenu() {
2994
3077
  return `${seconds}s`;
2995
3078
  };
2996
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
+
2997
3144
  const render = async () => {
3145
+ if (!isMenuActive) return; // Prevent rendering after menu closed
2998
3146
  process.stdout.write('\x1Bc');
2999
3147
  console.log(chalk.bold.cyan('⚙ ' + t('provider.title') + '\n'));
3000
3148
 
3001
- // Fetch quota info
3002
- const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
3003
- const autoConfig = await getAutoConfig();
3004
- const quotaInfo = await sharedAuth.canRunAutoMode();
3149
+ // Use cached quota info
3005
3150
  const remaining = Math.max(0, (quotaInfo.maxIterations || 10) - (quotaInfo.todayUsage || 0));
3006
3151
 
3007
3152
  // Calculate time until reset (midnight)
@@ -3012,15 +3157,65 @@ async function showProviderManagerMenu() {
3012
3157
  const hoursUntilReset = Math.floor(msUntilReset / (1000 * 60 * 60));
3013
3158
  const minsUntilReset = Math.floor((msUntilReset % (1000 * 60 * 60)) / (1000 * 60));
3014
3159
 
3015
- // 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.
3016
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
+
3017
3175
  if (remaining === 0) {
3018
- // Rate limit active - show when it resets (in red)
3176
+ // Overall rate limit active - show when it resets (in red)
3019
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
+ }
3020
3205
  } else {
3021
3206
  // Quota available - show when it resets (in green)
3022
- 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
+ }
3023
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)'));
3024
3219
  console.log(quotaDisplay);
3025
3220
  console.log(chalk.gray(' ' + t('provider.instructions') + '\n'));
3026
3221
 
@@ -3031,91 +3226,137 @@ async function showProviderManagerMenu() {
3031
3226
  const isSelected = idx === selectedIndex;
3032
3227
  const isEnabled = enabled[id] !== false;
3033
3228
 
3034
- // Check installation status for all IDEs
3035
- let isInstalled = true;
3036
- if (def.type === 'ide') {
3037
- try {
3038
- if (id === 'kiro') {
3039
- const { isKiroInstalled } = require('./kiro-installer');
3040
- isInstalled = isKiroInstalled();
3041
- } else if (id === 'cursor') {
3042
- isInstalled = await checkAppOrBinary(['Cursor'], ['cursor']);
3043
- } else if (id === 'windsurf') {
3044
- isInstalled = await checkAppOrBinary(['Windsurf'], ['windsurf']);
3045
- } else if (id === 'vscode') {
3046
- isInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
3047
- } else if (id === 'antigravity') {
3048
- isInstalled = await checkAppOrBinary(['Antigravity'], ['antigravity']);
3049
- } else {
3050
- // For other IDEs, try binary name same as id
3051
- isInstalled = await checkAppOrBinary([], [id]);
3052
- }
3053
- } catch (e) {
3054
- // If detection fails, assume not installed
3055
- isInstalled = false;
3056
- }
3057
- }
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
3058
3240
 
3059
- // Determine status emoji: disabled = red alert, not installed = yellow, enabled = green
3060
3241
  let statusEmoji;
3242
+ let statusText = '';
3243
+
3061
3244
  if (!isEnabled) {
3062
- statusEmoji = '🚨'; // Red for disabled
3063
- } else if (def.type === 'ide' && !isInstalled) {
3064
- statusEmoji = '🟡'; // Yellow for not installed
3245
+ statusEmoji = '🚫'; // Disabled
3246
+ } else if (!isInstalled) {
3247
+ statusEmoji = ''; // Not installed
3065
3248
  } else {
3066
- statusEmoji = '🟢'; // Green for enabled and installed (or LLM)
3067
- }
3249
+ // Installed and Enabled, assume Green unless quota or error
3250
+ statusEmoji = '🟢';
3068
3251
 
3069
- const typeLabel = def.type === 'ide' ? chalk.cyan('IDE') : chalk.cyan('LLM');
3070
- const prefix = isSelected ? chalk.cyan('❯') : ' ';
3071
- 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
+ }
3072
3257
 
3073
- // Fetch and display specific quota for this agent
3074
- try {
3075
- // Find the active model for this provider if possible
3076
- let model = def.defaultModel || id;
3077
- if (id === 'groq') model = autoConfig.groqModel || model;
3078
- else if (id === 'anthropic') model = autoConfig.anthropicModel || model;
3079
- else if (id === 'ollama') {
3080
- const preferredModel = autoConfig.llmModel && autoConfig.llmModel.includes('ollama/')
3081
- ? autoConfig.llmModel.split('/')[1]
3082
- : autoConfig.llmModel || autoConfig.aiderModel;
3083
- 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
+ }
3084
3277
  }
3278
+ }
3085
3279
 
3086
- const agentId = `${id}:${model}`;
3087
- 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
+ }
3088
3285
 
3089
- if (debugQuota) {
3090
- const resetMs = quota?.resetsAt ? (new Date(quota.resetsAt).getTime() - Date.now()) : null;
3091
- 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'}`);
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
+ }
3291
+
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}`;
3092
3323
  }
3324
+ }
3093
3325
 
3094
- if (quota.type === 'infinite') {
3095
- line += ` ${chalk.gray('[' + t('provider.status.quota.infinite') + ']')}`;
3096
- } else if (quota.type === 'rate-limit') {
3097
- if (quota.isExceeded()) {
3098
- if (quota.resetsAt) {
3099
- const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
3100
- 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]')}`;
3101
3350
  } else {
3102
- line += ` ${chalk.red('[Rate limited]')}`;
3351
+ subStatusText = ` ${chalk.red('[Rate limited]')}`;
3103
3352
  }
3104
3353
  } else {
3105
- // Show time until rate limit starts (when it resets)
3106
- if (quota.resetsAt) {
3107
- const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
3108
- line += ` ${chalk.green(`[✓ ${t('provider.status.available.resets')} ${formatDuration(msUntilReset)}]`)}`;
3109
- } else {
3110
- line += ` ${chalk.green('[' + t('provider.status.available') + ']')}`;
3111
- }
3354
+ subStatusEmoji = '🟢';
3112
3355
  }
3356
+
3357
+ console.log(` ${subStatusEmoji} ${chalk.gray('↳')} ${sub.name} ${chalk.gray(`(${sub.model})`)}${subStatusText}`);
3113
3358
  }
3114
- } catch (e) {
3115
- // Silently skip if quota fetch fails
3116
3359
  }
3117
-
3118
- console.log(line);
3119
3360
  }
3120
3361
 
3121
3362
  console.log();
@@ -3126,6 +3367,143 @@ async function showProviderManagerMenu() {
3126
3367
  }
3127
3368
  };
3128
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
+
3129
3507
  if (process.env.VCM_RENDER_ONCE === '1' || process.env.VCM_RENDER_ONCE === 'true') {
3130
3508
  await render();
3131
3509
  return;
@@ -3133,6 +3511,8 @@ async function showProviderManagerMenu() {
3133
3511
 
3134
3512
  return new Promise(async (resolve) => {
3135
3513
  const cleanup = () => {
3514
+ // Mark menu as inactive so background loaders stop rendering
3515
+ isMenuActive = false;
3136
3516
  if (process.stdin.isTTY && process.stdin.setRawMode) {
3137
3517
  process.stdin.setRawMode(false);
3138
3518
  }
@@ -3169,6 +3549,13 @@ async function showProviderManagerMenu() {
3169
3549
  };
3170
3550
 
3171
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
+ }
3172
3559
  cleanup();
3173
3560
  console.log('\n');
3174
3561
  resolve(null);
@@ -3200,12 +3587,104 @@ async function showProviderManagerMenu() {
3200
3587
  await render();
3201
3588
  };
3202
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
+
3203
3678
  const onKeypress = async (str, key = {}) => {
3679
+ // Allow immediate Ctrl+C regardless of debounce
3204
3680
  if (key.ctrl && key.name === 'c') {
3205
3681
  cancel();
3206
3682
  return;
3207
3683
  }
3208
3684
 
3685
+ // Debounce: ignore inputs that happen too soon after the menu opens
3686
+ if (Date.now() < ignoreInputUntil) return;
3687
+
3209
3688
  switch (key.name) {
3210
3689
  case 'up':
3211
3690
  await moveSelection(-1);
@@ -3225,15 +3704,22 @@ async function showProviderManagerMenu() {
3225
3704
  case 'd':
3226
3705
  await toggle(false);
3227
3706
  break;
3707
+ case 'i':
3708
+ await installSelected();
3709
+ break;
3228
3710
  case 'space':
3229
3711
  await toggle(!(enabled[order[selectedIndex]] !== false));
3230
3712
  break;
3231
3713
  case 'return':
3232
3714
  saveAndExit(order[selectedIndex]);
3233
3715
  break;
3234
- case 'escape':
3235
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':
3236
3721
  case 'x':
3722
+ // Escape or 'x' should cancel without persisting pending changes
3237
3723
  cancel();
3238
3724
  break;
3239
3725
  default:
@@ -3252,23 +3738,54 @@ async function showProviderManagerMenu() {
3252
3738
  });
3253
3739
  }
3254
3740
 
3255
- /* async function showSettings() {
3256
- 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
+ }
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
+ }
3257
3771
 
3772
+ async function showSettings() {
3773
+ console.log(chalk.bold.cyan('\n⚙️ Settings\n'));
3774
+
3258
3775
  const { setConfigValue } = require('vibecodingmachine-core');
3259
3776
  const { getAutoConfig, setAutoConfig } = require('./config');
3260
3777
  const currentHostnameEnabled = await isComputerNameEnabled();
3261
3778
  const hostname = getHostname();
3262
3779
  const autoConfig = await getAutoConfig();
3263
3780
  const currentIDE = autoConfig.ide || 'claude-code';
3264
-
3781
+
3265
3782
  // Show current settings
3266
3783
  console.log(chalk.gray('Current settings:'));
3267
3784
  console.log(chalk.gray(' Computer Name: '), chalk.cyan(hostname));
3268
3785
  console.log(chalk.gray(' Current IDE: '), chalk.cyan(formatIDEName(currentIDE)));
3269
3786
  console.log(chalk.gray(' Use Hostname in Req File: '), currentHostnameEnabled ? chalk.green('Enabled ✓') : chalk.yellow('Disabled ○'));
3270
3787
  console.log();
3271
-
3788
+
3272
3789
  const { action } = await inquirer.prompt([
3273
3790
  {
3274
3791
  type: 'list',
@@ -3290,54 +3807,59 @@ async function showProviderManagerMenu() {
3290
3807
  ]
3291
3808
  }
3292
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
+ }
3293
3824
 
3294
- if (action === 'change-ide') {
3295
- const { ide } = await inquirer.prompt([
3296
- {
3297
- type: 'list',
3298
- name: 'ide',
3299
- message: 'Select IDE:',
3300
- choices: [
3301
- { name: 'Claude Code CLI (recommended - Anthropic Claude)', value: 'claude-code' },
3302
- { name: 'Aider CLI (best for Ollama & Bedrock)', value: 'aider' },
3303
- { name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
3304
- { name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
3305
- { name: 'Cursor', value: 'cursor' },
3306
- { name: 'VS Code', value: 'vscode' },
3307
- { name: 'Windsurf', value: 'windsurf' }
3308
- ],
3309
- default: currentIDE
3310
- }
3311
- ]);
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
+ }
3312
3834
 
3313
- // Save to config
3314
- const newConfig = { ...autoConfig, ide };
3315
- 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
+ }
3316
3841
 
3317
- console.log(chalk.green('\n✓'), `IDE changed to ${chalk.cyan(formatIDEName(ide))}`);
3318
- console.log(chalk.gray(' Note: This will be used for the next Auto Mode session.'));
3319
- console.log();
3320
- } else if (action === 'toggle-hostname') {
3842
+ async function showSettings() {
3321
3843
  const newValue = !currentHostnameEnabled;
3322
-
3844
+
3323
3845
  // Save to shared config (same location as Electron app)
3324
3846
  await setConfigValue('computerNameEnabled', newValue);
3325
-
3847
+
3326
3848
  const statusText = newValue ? chalk.green('enabled') : chalk.yellow('disabled');
3327
3849
  console.log(chalk.green('\n✓'), `Hostname in requirements file ${statusText}`);
3328
-
3850
+
3329
3851
  if (newValue) {
3330
3852
  console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan(`REQUIREMENTS-${hostname}.md`));
3331
3853
  } else {
3332
3854
  console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan('REQUIREMENTS.md'));
3333
3855
  }
3334
-
3856
+
3335
3857
  console.log(chalk.gray('\n Note: You may need to rename your existing requirements file.'));
3336
3858
  console.log(chalk.gray(' Note: This setting is now synced with the Electron app.'));
3337
3859
  console.log();
3338
3860
  }
3339
3861
  }
3340
-
3862
+
3341
3863
  /**
3342
3864
  * Show cloud sync management menu
3343
3865
  */
@@ -3646,36 +4168,8 @@ async function startInteractive() {
3646
4168
  const { stopAutoMode } = require('./auto-mode');
3647
4169
  await stopAutoMode('startup');
3648
4170
 
3649
- await bootstrapProjectIfInHomeDir();
3650
-
3651
4171
  await showWelcomeScreen();
3652
4172
 
3653
- // Check if .vibecodingmachine directory exists, offer to create if not
3654
- const allnightStatusCheck = await checkVibeCodingMachineExists();
3655
- if (!allnightStatusCheck.insideExists && !allnightStatusCheck.siblingExists) {
3656
- console.log(chalk.yellow('\n⚠️ No .vibecodingmachine directory found'));
3657
- console.log(chalk.gray('This directory is needed to save requirements and track progress.\n'));
3658
-
3659
- const { shouldInit } = await inquirer.prompt([
3660
- {
3661
- type: 'confirm',
3662
- name: 'shouldInit',
3663
- message: 'Would you like to create the .vibecodingmachine directory now?',
3664
- default: true
3665
- }
3666
- ]);
3667
-
3668
- if (shouldInit) {
3669
- // Use the initRepo function to create the directory
3670
- const { initRepo } = require('../commands/repo');
3671
- await initRepo({ location: 'inside' });
3672
- console.log('');
3673
- } else {
3674
- console.log(chalk.yellow('\n⚠️ Warning: Without the .vibecodingmachine directory, requirements cannot be saved.'));
3675
- console.log(chalk.gray('You can create it later by running: ') + chalk.cyan('vcm repo:init\n'));
3676
- }
3677
- }
3678
-
3679
4173
  if (process.env.VCM_OPEN_PROVIDER_MENU === '1' || process.env.VCM_OPEN_PROVIDER_MENU === 'true') {
3680
4174
  await showProviderManagerMenu();
3681
4175
  return;
@@ -3753,24 +4247,13 @@ async function startInteractive() {
3753
4247
  const provider = displayAgent === 'ollama' ? 'ollama' : displayAgent;
3754
4248
 
3755
4249
  if (checkModel) {
3756
- const timeUntilReset = providerManager.getTimeUntilReset(provider, checkModel);
3757
-
3758
- if (timeUntilReset) {
3759
- // Format time remaining in human-readable format
3760
- const hours = Math.floor(timeUntilReset / (1000 * 60 * 60));
3761
- const minutes = Math.floor((timeUntilReset % (1000 * 60 * 60)) / (1000 * 60));
3762
- const seconds = Math.floor((timeUntilReset % (1000 * 60)) / 1000);
3763
-
3764
- let timeStr = '';
3765
- if (hours > 0) {
3766
- timeStr = `${hours}h ${minutes}m`;
3767
- } else if (minutes > 0) {
3768
- timeStr = `${minutes}m ${seconds}s`;
3769
- } else {
3770
- timeStr = `${seconds}s`;
3771
- }
4250
+ const info = providerManager.getRateLimitInfo(provider, checkModel);
3772
4251
 
3773
- 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
+ }
3774
4257
  }
3775
4258
  }
3776
4259
  }
@@ -3871,181 +4354,18 @@ async function startInteractive() {
3871
4354
  value: 'setting:stages'
3872
4355
  });
3873
4356
 
3874
- // Add Admin Dashboard - Multi-Computer Dashboard
3875
- items.push({
3876
- type: 'setting',
3877
- name: ` └─ Admin Dashboard: ${chalk.cyan('Multi-Computer Dashboard')}`,
3878
- value: 'setting:admin-dashboard-multi-computer'
3879
- });
3880
-
3881
- // Add Admin Dashboard - Compliance and Terms Tracking
3882
- items.push({
3883
- type: 'setting',
3884
- name: ` └─ Admin Dashboard: ${chalk.cyan('Compliance and Terms Tracking')}`,
3885
- value: 'setting:admin-dashboard-compliance'
3886
- });
3887
-
3888
- // Add Admin Dashboard - Authentication and Security
3889
- items.push({
3890
- type: 'setting',
3891
- name: ` └─ Admin Dashboard: ${chalk.cyan('Authentication and Security')}`,
3892
- value: 'setting:admin-dashboard-auth'
3893
- });
3894
-
3895
- // Add Admin Dashboard - Terms and Privacy Policy
3896
- items.push({
3897
- type: 'setting',
3898
- name: ` └─ Admin Dashboard: ${chalk.cyan('Terms and Privacy Policy')}`,
3899
- value: 'setting:admin-dashboard-terms'
3900
- });
3901
-
3902
- // Add Admin Dashboard - Deployment and Infrastructure
3903
- items.push({
3904
- type: 'setting',
3905
- name: ` └─ Admin Dashboard: ${chalk.cyan('Deployment and Infrastructure')}`,
3906
- value: 'setting:admin-dashboard-deployment'
3907
- });
3908
-
3909
- // Add Cloud Sync Infrastructure - AWS DynamoDB Setup
3910
- items.push({
3911
- type: 'setting',
3912
- name: ` └─ Cloud Sync Infrastructure: ${chalk.cyan('AWS DynamoDB Setup')}`,
3913
- value: 'setting:cloud-sync-dynamodb'
3914
- });
3915
-
3916
- // Add Cloud Sync Infrastructure - Lambda Functions
3917
- items.push({
3918
- type: 'setting',
3919
- name: ` └─ Cloud Sync Infrastructure: ${chalk.cyan('Lambda Functions')}`,
3920
- value: 'setting:cloud-sync-lambda'
3921
- });
3922
-
3923
- // Add Cloud Sync Infrastructure - API Gateway Setup
3924
- items.push({
3925
- type: 'setting',
3926
- name: ` └─ Cloud Sync: ${chalk.cyan('AWS DynamoDB Setup')}`,
3927
- value: 'setting:cloud-sync-dynamodb'
3928
- });
3929
-
3930
- // Add Cloud Sync Infrastructure - AWS API Gateway
3931
- items.push({
3932
- type: 'setting',
3933
- name: ` └─ Cloud Sync: ${chalk.cyan('AWS API Gateway')}`,
3934
- value: 'setting:cloud-sync-api-gateway'
3935
- });
3936
-
3937
- // Add Cloud Sync Infrastructure - AWS Cognito Authentication
3938
- items.push({
3939
- type: 'setting',
3940
- name: ` └─ Cloud Sync Infrastructure: ${chalk.cyan('AWS Cognito Authentication')}`,
3941
- value: 'setting:cloud-sync-cognito'
3942
- });
3943
-
3944
- // Add Cloud Sync Infrastructure - AWS DynamoDB Setup
3945
4357
  items.push({
3946
4358
  type: 'setting',
3947
- name: ` └─ Cloud Sync Infrastructure: ${chalk.cyan('AWS DynamoDB Setup')}`,
3948
- value: 'setting:cloud-sync-dynamodb'
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'
3949
4361
  });
3950
4362
 
3951
- // Cloud Sync Status
3952
- try {
3953
- const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
3954
- const syncEngine = new SyncEngine();
3955
-
3956
- // Try to initialize, but don't fail if AWS not configured
3957
- try {
3958
- await syncEngine.initialize();
3959
- const syncStatus = syncEngine.getStatus();
3960
- syncEngine.stop();
3961
-
3962
- const onlineIcon = syncStatus.isOnline ? chalk.green('●') : chalk.red('●');
3963
- const onlineText = syncStatus.isOnline ? t('interactive.online') : t('interactive.offline');
3964
- const queueText = syncStatus.queuedChanges > 0 ? chalk.yellow(` (${syncStatus.queuedChanges} queued)`) : '';
3965
-
3966
- items.push({
3967
- type: 'setting',
3968
- name: `${t('interactive.cloud.sync')}: ${onlineIcon} ${onlineText}${queueText}`,
3969
- value: 'setting:cloud-sync'
3970
- });
3971
- } catch (initError) {
3972
- // Initialization failed - AWS not configured or credentials missing
3973
- items.push({
3974
- type: 'setting',
3975
- name: `Cloud Sync: ${chalk.gray('● Not configured')}`,
3976
- value: 'setting:cloud-sync-setup'
3977
- });
3978
- }
3979
- } catch (error) {
3980
- // Module not found or other error - show disabled
3981
- items.push({
3982
- type: 'setting',
3983
- name: `Cloud Sync: ${chalk.gray('● Disabled')}`,
3984
- value: 'setting:cloud-sync-setup'
3985
- });
3986
- }
3987
-
3988
- // Computer and Usage Tracking Status
3989
- try {
3990
- const os = require('os');
3991
- const hostname = os.hostname();
3992
- const platform = os.platform();
3993
- const arch = os.arch();
3994
- const usageIcon = chalk.blue('●');
3995
- const trackingText = `${hostname} (${platform}/${arch})`;
3996
-
3997
- items.push({
3998
- type: 'setting',
3999
- name: `${t('interactive.usage.tracking') || 'Usage Tracking'}: ${usageIcon} ${chalk.cyan(trackingText)}`,
4000
- value: 'setting:usage-tracking'
4001
- });
4002
- } catch (error) {
4003
- // Tracking info unavailable
4004
- items.push({
4005
- type: 'setting',
4006
- name: `Usage Tracking: ${chalk.gray('● Unavailable')}`,
4007
- value: 'setting:usage-tracking'
4008
- });
4009
- }
4010
-
4011
- // Add requirement number tracking status
4012
4363
  items.push({
4013
4364
  type: 'setting',
4014
- name: ` └─ ${t('interactive.requirements.tracking')}: ${counts ? chalk.green(' ENABLED') : chalk.red('🛑 DISABLED')}`,
4015
- value: 'setting:requirements-tracking'
4365
+ name: ` └─ ${t('interactive.feedback')}: ${chalk.cyan('📣 Press Ctrl+F for feedback')}`,
4366
+ value: 'setting:feedback'
4016
4367
  });
4017
- const { readConfig } = require('./config');
4018
- const config = await readConfig();
4019
- const lastReqNumber = config.lastRequirementNumber || 0;
4020
- items.push({
4021
- type: 'setting',
4022
- name: ` └─ ${t('interactive.last.requirement.number')}: ${chalk.cyan('R' + lastReqNumber)}`,
4023
- value: 'setting:req-number'
4024
- });
4025
-
4026
- // Add "Next TODO Requirement" as a separate menu item if there are TODO items
4027
- if (counts && counts.todoCount > 0) {
4028
- items.push({
4029
- type: 'setting',
4030
- name: ` └─ ${t('interactive.next.todo.requirement')}`,
4031
- value: 'setting:next-todo-requirement'
4032
- });
4033
- }
4034
4368
 
4035
- // Save last tab opened
4036
- const lastTabOpened = config.lastTabOpened;
4037
- if (lastTabOpened) {
4038
- items.push({
4039
- type: 'setting',
4040
- name: ` └─ ${t('interactive.last.tab.opened')}: ${lastTabOpened}`,
4041
- value: 'setting:last-tab-opened'
4042
- });
4043
- }
4044
- items.push({
4045
- type: 'setting',
4046
- name: `Start Auto: Switch to Console tab`,
4047
- value: 'setting:start-auto'
4048
- });
4049
4369
  // TODO: Implement getNextTodoRequirement function
4050
4370
  // if (counts && counts.todoCount > 0) {
4051
4371
  // const { getNextTodoRequirement } = require('./auto-mode');
@@ -4069,6 +4389,9 @@ async function startInteractive() {
4069
4389
  const hostname = await getHostname();
4070
4390
  const reqFilename = await getRequirementsFilename(hostname);
4071
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
+
4072
4395
 
4073
4396
  if (await fs.pathExists(reqPath)) {
4074
4397
  const reqContent = await fs.readFile(reqPath, 'utf8');
@@ -4178,10 +4501,20 @@ async function startInteractive() {
4178
4501
 
4179
4502
  switch (actionStr) {
4180
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
+ }
4181
4509
  await showProviderManagerMenu();
4182
4510
  await showWelcomeScreen();
4183
4511
  break;
4184
4512
  }
4513
+ case 'setting:feedback': {
4514
+ await handleFeedbackSubmission();
4515
+ await showWelcomeScreen();
4516
+ break;
4517
+ }
4185
4518
  case 'setting:provider': {
4186
4519
  // Switch AI provider - run provider setup only, don't start auto mode
4187
4520
  // Note: Continue CLI doesn't have provider setup - models are configured in ~/.continue/config.yaml
@@ -4557,6 +4890,12 @@ async function startInteractive() {
4557
4890
 
4558
4891
  // Use saved maxChats/neverStop settings
4559
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
+
4560
4899
  if (currentConfig.neverStop) {
4561
4900
  options.neverStop = true;
4562
4901
  } else if (currentConfig.maxChats) {
@@ -4578,7 +4917,7 @@ async function startInteractive() {
4578
4917
  // ALWAYS use auto:direct (supports both LLM and IDE agents with proper looping)
4579
4918
  console.log(chalk.gray('[DEBUG] Using auto:direct for agent:', agentToUse));
4580
4919
  const { handleAutoStart: handleDirectAutoStart } = require('../commands/auto-direct');
4581
- await handleDirectAutoStart({ maxChats: options.maxChats });
4920
+ await handleDirectAutoStart(options);
4582
4921
 
4583
4922
  // Prompt user before returning to menu (so they can read the output)
4584
4923
  console.log('');
@@ -4793,8 +5132,11 @@ async function startInteractive() {
4793
5132
  { name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
4794
5133
  { name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
4795
5134
  { name: 'Cursor', value: 'cursor' },
4796
- { name: 'VS Code', value: 'vscode' },
4797
- { 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' }
4798
5140
  ],
4799
5141
  default: 'claude-code'
4800
5142
  },
@@ -4805,7 +5147,42 @@ async function startInteractive() {
4805
5147
  default: ''
4806
5148
  }
4807
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
+
4808
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
+
4809
5186
  if (maxChats && maxChats.trim() !== '0') {
4810
5187
  options.maxChats = parseInt(maxChats);
4811
5188
  } else {
@@ -4899,6 +5276,60 @@ Max Chats: ${maxChats || 'Never stop'}`;
4899
5276
  });
4900
5277
  }
4901
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
+
4902
5333
  // Watch requirements file for changes
4903
5334
  const watcher = chokidar.watch(reqPath, { persistent: true });
4904
5335
  watcher.on('change', async () => {
@@ -5022,11 +5453,49 @@ Max Chats: ${maxChats || 'Never stop'}`;
5022
5453
  }
5023
5454
  }
5024
5455
 
5025
- module.exports = {
5026
- startInteractive,
5027
- bootstrapProjectIfInHomeDir,
5028
- normalizeProjectDirName
5029
- };
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 };
5030
5499
 
5031
5500
 
5032
5501