minovative-mind-cli 1.5.1 → 2.1.0

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 (79) hide show
  1. package/README.md +59 -45
  2. package/dist/commands/chat.js +10 -2
  3. package/dist/services/agent/slashCommands.js +369 -42
  4. package/dist/services/agent/toolLoop.d.ts +1 -1
  5. package/dist/services/agent/toolLoop.js +7 -2
  6. package/dist/services/agent/types.d.ts +2 -0
  7. package/dist/services/agent-tools.d.ts +9 -4
  8. package/dist/services/agent-tools.js +272 -34
  9. package/dist/services/agent.d.ts +8 -0
  10. package/dist/services/agent.js +288 -40
  11. package/dist/services/ai.d.ts +19 -5
  12. package/dist/services/ai.js +182 -36
  13. package/dist/services/changeLogger.d.ts +142 -0
  14. package/dist/services/changeLogger.js +132 -3
  15. package/dist/services/contextAgent.d.ts +6 -1
  16. package/dist/services/contextAgent.js +112 -19
  17. package/dist/services/embeddingIndex.d.ts +82 -0
  18. package/dist/services/embeddingIndex.js +613 -0
  19. package/dist/services/investigationComplexity.d.ts +45 -0
  20. package/dist/services/investigationComplexity.js +91 -0
  21. package/dist/services/metrics.d.ts +18 -0
  22. package/dist/services/metrics.js +7 -0
  23. package/dist/services/orchestration/fileLockRegistry.d.ts +125 -0
  24. package/dist/services/orchestration/fileLockRegistry.js +276 -0
  25. package/dist/services/orchestration/investigationAgent.d.ts +85 -0
  26. package/dist/services/orchestration/investigationAgent.js +362 -0
  27. package/dist/services/orchestration/investigationOrchestrator.d.ts +53 -0
  28. package/dist/services/orchestration/investigationOrchestrator.js +180 -0
  29. package/dist/services/orchestration/messageBus.d.ts +162 -0
  30. package/dist/services/orchestration/messageBus.js +225 -0
  31. package/dist/services/orchestration/orchestrator.d.ts +45 -0
  32. package/dist/services/orchestration/orchestrator.js +217 -0
  33. package/dist/services/orchestration/readCache.d.ts +79 -0
  34. package/dist/services/orchestration/readCache.js +108 -0
  35. package/dist/services/orchestration/scopedTools.d.ts +57 -0
  36. package/dist/services/orchestration/scopedTools.js +172 -0
  37. package/dist/services/orchestration/subAgent.d.ts +58 -0
  38. package/dist/services/orchestration/subAgent.js +190 -0
  39. package/dist/services/orchestration/taskGraph.d.ts +129 -0
  40. package/dist/services/orchestration/taskGraph.js +254 -0
  41. package/dist/services/proxyClient.d.ts +25 -0
  42. package/dist/services/proxyClient.js +60 -0
  43. package/dist/services/workspaceRegistry.d.ts +137 -0
  44. package/dist/services/workspaceRegistry.js +270 -0
  45. package/dist/utils/asyncContext.d.ts +16 -0
  46. package/dist/utils/asyncContext.js +25 -0
  47. package/dist/utils/config.d.ts +3 -1
  48. package/dist/utils/config.js +3 -1
  49. package/dist/utils/contextPrompts.js +10 -3
  50. package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
  51. package/dist/utils/dependencyTracer/modules/api.js +62 -0
  52. package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
  53. package/dist/utils/dependencyTracer/modules/graph.js +23 -0
  54. package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
  55. package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
  56. package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
  57. package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
  58. package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
  59. package/dist/utils/dependencyTracer/modules/types.js +1 -0
  60. package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
  61. package/dist/utils/dependencyTracer/modules/walker.js +48 -0
  62. package/dist/utils/dependencyTracer.js +31 -17
  63. package/dist/utils/excludedExtensions.js +0 -1
  64. package/dist/utils/historyPrompt.d.ts +9 -0
  65. package/dist/utils/historyPrompt.js +87 -0
  66. package/dist/utils/logo.js +7 -7
  67. package/dist/utils/paste.d.ts +21 -0
  68. package/dist/utils/paste.js +22 -1
  69. package/dist/utils/pathSecurity.d.ts +31 -0
  70. package/dist/utils/pathSecurity.js +48 -0
  71. package/dist/utils/profiles.d.ts +2 -0
  72. package/dist/utils/profiles.js +44 -0
  73. package/dist/utils/projectStorage.js +10 -7
  74. package/dist/utils/systemPrompts.d.ts +6 -3
  75. package/dist/utils/systemPrompts.js +111 -6
  76. package/dist/utils/types.d.ts +33 -0
  77. package/dist/utils/types.js +1 -0
  78. package/oclif.manifest.json +2 -2
  79. package/package.json +4 -3
@@ -23,17 +23,23 @@ import { marked } from 'marked';
23
23
  import { markedTerminal } from 'marked-terminal';
24
24
  import { debugLog, isDebugOn } from '../utils/logger.js';
25
25
  import { ensureProjectStorage, ensureIgnored, readCache, writeCache, invalidateCacheForDependents, } from '../utils/projectStorage.js';
26
- import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, compressTextUsingFlashLite, generateChatTitle, } from './ai.js';
26
+ import { GEMINI_MODELS } from '../utils/config.js';
27
+ import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, compressTextUsingFlashLite, generateChatTitle, getGlobalActiveModel, getGlobalLatestUsageMetadata, } from './ai.js';
27
28
  import { changeLogger } from './changeLogger.js';
28
29
  import { chatHistoryService } from './chatHistoryService.js';
29
- import { gatherContext, routeIntent } from './contextAgent.js';
30
+ import { gatherContext, routeIntent, evaluateExecutionComplexity } from './contextAgent.js';
30
31
  import { verifyChangedFiles } from './verificationService.js';
31
32
  import { buildContextInjection } from '../utils/contextPrompts.js';
33
+ import { historyText } from '../utils/historyPrompt.js';
32
34
  // Submodule Imports
33
35
  import { AsyncInputHandler } from './agent/inputHandler.js';
34
36
  import { processResponse } from './agent/toolLoop.js';
35
37
  import { handleSlashCommand } from './agent/slashCommands.js';
36
- marked.use(markedTerminal());
38
+ import { getMetricCollector } from './metrics.js';
39
+ import { Orchestrator } from './orchestration/orchestrator.js';
40
+ import { isSubAgentsEnabled } from './agent-tools.js';
41
+ import { runWithAgentId } from '../utils/asyncContext.js';
42
+ marked.use(markedTerminal({ reflowText: false }));
37
43
  // Export submodules for potential external uses if required
38
44
  export { AsyncInputHandler } from './agent/inputHandler.js';
39
45
  /**
@@ -77,7 +83,9 @@ export async function startAgentLoop(workspaceRoot, version) {
77
83
  const chat = createSharedChatSession();
78
84
  const inputHandler = new AsyncInputHandler();
79
85
  const chatSessionState = { id: crypto.randomUUID(), title: '' };
86
+ const sessionInputHistory = [];
80
87
  let isRawPasteMode = false;
88
+ let isPlanMode = false;
81
89
  // Hook process.stdin.emit to intercept fast stream inputs.
82
90
  // When large buffers containing newlines arrive rapidly, we interpret them as a clipboard paste,
83
91
  // sanitizing and collapsing them to prevent premature command line submission.
@@ -102,7 +110,7 @@ export async function startAgentLoop(workspaceRoot, version) {
102
110
  }
103
111
  inputHandler.stop();
104
112
  // Input collection (delegated to a helper function to avoid nested loop warning)
105
- const { userInput: rawInput, canceled } = await collectUserInput();
113
+ const { userInput: rawInput, canceled } = await collectUserInput(sessionInputHistory, isPlanMode);
106
114
  if (canceled) {
107
115
  p.outro(pc.green('Goodbye! Happy coding. 🚀'));
108
116
  return;
@@ -111,16 +119,29 @@ export async function startAgentLoop(workspaceRoot, version) {
111
119
  if (!userInput) {
112
120
  continue;
113
121
  }
122
+ // Push non-empty input to history if it's not the same as the last item
123
+ if (sessionInputHistory[sessionInputHistory.length - 1] !== userInput) {
124
+ sessionInputHistory.push(userInput);
125
+ }
114
126
  // Capture standalone forward slash triggers to open the selection console
115
127
  if (userInput === '/') {
116
128
  const commandMenu = await p['select']({
117
129
  message: 'Command Menu',
118
130
  options: [
119
131
  { value: '/models', label: '/models', hint: 'Change the active AI model' },
132
+ { value: '/plan', label: '/plan', hint: 'Toggle plan mode (build a plan without executing)' },
120
133
  { value: '/paste', label: '/paste', hint: 'Paste large text directly into the CLI (Press Ctrl+D to submit)' },
121
134
  { value: '/clear', label: '/clear', hint: 'Clear chat session history' },
122
135
  { value: '/debug', label: '/debug', hint: 'Toggle internal debug logs' },
123
136
  { value: '/auto-approve', label: '/auto-approve', hint: 'Approve all future terminal commands' },
137
+ { value: '/sub-agents', label: '/sub-agents', hint: 'Toggle the MMAAK Engine for parallel investigation and execution' },
138
+ {
139
+ value: '/semantic-search',
140
+ label: '/semantic-search',
141
+ hint: 'Toggle local vector index capabilities for better search',
142
+ },
143
+ { value: '/workspaces', label: '/workspaces', hint: 'Manage external workspaces for cross-project development' },
144
+ { value: '/stats', label: '/stats', hint: 'View current session statistics and configuration' },
124
145
  { value: '/commit', label: '/commit', hint: 'Auto-commit changes with AI message' },
125
146
  { value: '/revert', label: '/revert', hint: 'Undo last change' },
126
147
  { value: '/chats', label: '/chats', hint: 'View or resume past chat sessions' },
@@ -151,12 +172,16 @@ export async function startAgentLoop(workspaceRoot, version) {
151
172
  workspaceRoot,
152
173
  version,
153
174
  isRawPasteMode,
175
+ isPlanMode,
154
176
  chatSessionState,
155
177
  };
156
178
  const slashResult = await handleSlashCommand(userInput, slashCtx);
157
179
  if (slashResult.isRawPasteMode !== undefined) {
158
180
  isRawPasteMode = slashResult.isRawPasteMode;
159
181
  }
182
+ if (slashResult.isPlanModeOverride !== undefined) {
183
+ isPlanMode = slashResult.isPlanModeOverride;
184
+ }
160
185
  if (slashResult.userInputOverride !== undefined) {
161
186
  userInput = slashResult.userInputOverride;
162
187
  }
@@ -165,6 +190,36 @@ export async function startAgentLoop(workspaceRoot, version) {
165
190
  }
166
191
  }
167
192
  // ─── Main Execution Cycle ────────────────────────────────────────
193
+ let currentInput = userInput;
194
+ let cachedContextResult = null;
195
+ while (true) {
196
+ const execResult = await executeSingleTurn(workspaceRoot, currentInput, chat, inputHandler, chatSessionState, isPlanMode, cachedContextResult);
197
+ const planModeReturn = execResult?.planModeReturn;
198
+ const contextResult = execResult?.contextResult;
199
+ if (isPlanMode && planModeReturn) {
200
+ if (planModeReturn === 'proceed') {
201
+ isPlanMode = false;
202
+ currentInput = 'Please execute the plan we just finalized.';
203
+ cachedContextResult = contextResult;
204
+ p.log.info(pc.cyan('Proceeding with execution...'));
205
+ continue;
206
+ }
207
+ else if (planModeReturn === 'edit') {
208
+ p.log.info(pc.cyan('Please provide additional instructions to revise the plan.'));
209
+ break;
210
+ }
211
+ else if (planModeReturn === 'exit') {
212
+ isPlanMode = false;
213
+ p.log.info(pc.cyan('Exited plan mode. Returning to normal chat.'));
214
+ break;
215
+ }
216
+ }
217
+ break;
218
+ }
219
+ }
220
+ }
221
+ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHandler, chatSessionState, isPlanMode, cachedContextResult) {
222
+ return runWithAgentId('main', async () => {
168
223
  const turnStartTime = Date.now();
169
224
  const spinner = p.spinner();
170
225
  const ac = new AbortController();
@@ -175,12 +230,20 @@ export async function startAgentLoop(workspaceRoot, version) {
175
230
  let finalInput = userInput;
176
231
  // Stage 1: Gather Workspace Context and route intentions
177
232
  spinner.start('🔍 Investigating workspace...');
178
- const chatHistory = chat.getRecentHistory(3);
179
- const gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
180
- if (!inputHandler.isCurrentlyPrompting()) {
181
- spinner.message(`🔍 Investigating workspace... ${pc.dim(msg)}`);
182
- }
183
- });
233
+ let gatherRes = { targetAgent: 'EXECUTE', chainedMessages: [], contextResult: cachedContextResult || null };
234
+ if (!cachedContextResult) {
235
+ const collector = getMetricCollector();
236
+ if (collector)
237
+ collector.startTimer('contextGather');
238
+ const chatHistory = chat.getRecentHistory(3);
239
+ gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
240
+ if (!inputHandler.isCurrentlyPrompting()) {
241
+ spinner.message(`🔍 Investigating workspace... ${pc.dim(msg)}`);
242
+ }
243
+ });
244
+ if (collector)
245
+ collector.stopTimer('contextGather');
246
+ }
184
247
  let latestUsage = undefined;
185
248
  // Collect any inputs that were queued while the Context Agent was investigating
186
249
  const leftoverMsg = inputHandler.getAndClear();
@@ -213,26 +276,66 @@ export async function startAgentLoop(workspaceRoot, version) {
213
276
  }
214
277
  // Hot-swap the underlying LLM system instruction context depending on intent (conversational vs execution)
215
278
  debugLog(`Intent Router output: original targetAgent = ${gatherRes.targetAgent}, effective = ${effectiveTargetAgent}`);
216
- const config = effectiveTargetAgent === 'CHAT' ? getGeneralChatConfig() : getPlanExecutionConfig();
279
+ let config;
280
+ if (isPlanMode) {
281
+ const { getPlanModeConfig } = await import('./ai.js');
282
+ config = getPlanModeConfig();
283
+ }
284
+ else {
285
+ config = effectiveTargetAgent === 'CHAT' ? getGeneralChatConfig() : getPlanExecutionConfig();
286
+ }
217
287
  let dynamicSystemInstruction = config.systemInstruction;
218
288
  // Inject gathered workspace directories, dependency configurations, and matching search patterns
219
289
  if (gatherRes.contextResult) {
220
290
  // Compress each relevant file individually using helper to avoid nested loop warning
221
- gatherRes.contextResult.relevantFiles = await compressContextFiles(workspaceRoot, gatherRes.contextResult);
291
+ if (!cachedContextResult) {
292
+ gatherRes.contextResult.relevantFiles = await compressContextFiles(workspaceRoot, gatherRes.contextResult);
293
+ }
222
294
  // Assemble the final context injection string
223
295
  const contextInjection = buildContextInjection(gatherRes.contextResult);
224
296
  debugLog(`Final compressed context injection size: ${contextInjection.length} chars`);
297
+ const collector = getMetricCollector();
298
+ if (collector)
299
+ collector.recordCompressedContextSize(contextInjection.length);
225
300
  dynamicSystemInstruction += '\n\n' + contextInjection;
226
301
  }
227
302
  // Apply the dynamic prompt updates and tool registrations to the active chat session
228
303
  chat.setAgentConfig(dynamicSystemInstruction, config.tools);
304
+ if (getGlobalActiveModel() === GEMINI_MODELS.AUTO) {
305
+ let selectedModel = GEMINI_MODELS.FLASH_3_5;
306
+ if (effectiveTargetAgent === 'CHAT') {
307
+ selectedModel = GEMINI_MODELS.FLASH_LITE_3_1;
308
+ }
309
+ else {
310
+ spinner.stop(); // MUST clear the investigation spinner first to prevent leaking the setInterval
311
+ spinner.start(pc.blue('🧠 Evaluating execution complexity...'));
312
+ const complexity = await evaluateExecutionComplexity(finalInput, gatherRes.contextResult?.summary, gatherRes.contextResult?.relevantFiles?.size || 0);
313
+ spinner.stop();
314
+ process.stdout.write('\x1b[2K\r');
315
+ selectedModel = complexity === 'EASY' ? GEMINI_MODELS.FLASH_LITE_3_1 : GEMINI_MODELS.FLASH_3_5;
316
+ }
317
+ chat.setModel(selectedModel);
318
+ debugLog(`Auto-routing to model ${selectedModel} based on intent ${effectiveTargetAgent}`);
319
+ }
320
+ else {
321
+ chat.setModel(getGlobalActiveModel());
322
+ }
229
323
  if (gatherRes.contextResult) {
230
- if (!inputHandler.isCurrentlyPrompting()) {
231
- spinner.stop(pc.green('🔍 Investigation completed.'));
232
- spinner.start('Thinking...');
324
+ if (!gatherRes.contextResult.isParallel) {
325
+ if (!inputHandler.isCurrentlyPrompting()) {
326
+ spinner.stop(pc.green('🔍 Investigation completed.'));
327
+ spinner.start('Thinking...');
328
+ }
329
+ else {
330
+ p.log.success(pc.green('🔍 Investigation completed.'));
331
+ }
233
332
  }
234
333
  else {
235
- p.log.success(pc.green('🔍 Investigation completed.'));
334
+ // Parallel investigation already printed its own detailed summary log
335
+ if (!inputHandler.isCurrentlyPrompting()) {
336
+ spinner.stop(); // MUST clear the investigation spinner first to prevent leaking the setInterval
337
+ spinner.start('Thinking...');
338
+ }
236
339
  }
237
340
  }
238
341
  else {
@@ -240,27 +343,106 @@ export async function startAgentLoop(workspaceRoot, version) {
240
343
  spinner.message('Thinking...');
241
344
  }
242
345
  }
346
+ if (effectiveTargetAgent === 'EXECUTE' && isSubAgentsEnabled()) {
347
+ spinner.stop(); // Clear the spinner before delegating
348
+ process.stdout.write('\x1b[2K\r');
349
+ const orchestrator = new Orchestrator(workspaceRoot, chatSessionState.id, inputHandler);
350
+ const handledByOrchestrator = await orchestrator.runOrchestration(finalInput, dynamicSystemInstruction, ac.signal);
351
+ if (typeof handledByOrchestrator === 'string') {
352
+ // The orchestrator successfully decomposed and ran the task.
353
+ // Inject the task context into the main proxy chat session so the agent remembers what happened
354
+ // and so the history state gets saved properly on disk.
355
+ chat.getRawHistory().push({
356
+ role: 'user',
357
+ parts: [{ text: userInput }],
358
+ });
359
+ chat.getRawHistory().push({
360
+ role: 'model',
361
+ parts: [{ text: handledByOrchestrator }],
362
+ });
363
+ // Print the summary text just like single-agent mode
364
+ if (handledByOrchestrator !== '[Generation stopped.]') {
365
+ console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(Orchestrator)`)}\n`);
366
+ const cleanText = handledByOrchestrator.replace(/\n([ \t]*\n){2,}/g, '\n\n');
367
+ console.log(marked.parse(cleanText));
368
+ }
369
+ // Print Usage Stats
370
+ const usage = getGlobalLatestUsageMetadata();
371
+ if (usage) {
372
+ if (usage.cachedTokens > 0) {
373
+ const totalInputTokens = (usage.promptTokens || 0) + usage.cachedTokens;
374
+ const percentSaved = totalInputTokens > 0 ? Math.round((usage.cachedTokens / totalInputTokens) * 100) : 0;
375
+ p.log.info(`${pc.green('⚡')} ${pc.green('Context Cache Hit:')} ${pc.bold(usage.cachedTokens.toLocaleString())} tokens cached ${pc.dim(`(Saved ~${percentSaved}% of input cost)`)}`);
376
+ }
377
+ if (usage.remainingBalance !== undefined) {
378
+ p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(usage.remainingBalance.toLocaleString())}`);
379
+ }
380
+ }
381
+ // Post-execution steps for the UI (saving history, invalidate cache)
382
+ const postExecutionChanges = changeLogger.getCurrentChangeSet()?.changes || [];
383
+ const modifiedFiles = postExecutionChanges
384
+ .filter((c) => c.action === 'modify' || c.action === 'create' || c.action === 'delete')
385
+ .map((c) => path.relative(workspaceRoot, c.filePath));
386
+ if (modifiedFiles.length > 0) {
387
+ await invalidateCacheForDependents(workspaceRoot, modifiedFiles);
388
+ }
389
+ const turnEndTime = Date.now();
390
+ const turnDuration = ((turnEndTime - turnStartTime) / 1000).toFixed(1);
391
+ p.log.info(`${pc.dim('Orchestration finished in')} ${pc.cyan(turnDuration + 's')}`);
392
+ changeLogger.markComplete();
393
+ changeLogger.commitChangeSet();
394
+ // Auto-save chat history for the orchestrator turn
395
+ const history = chat.getRawHistory();
396
+ if (history.length > 0) {
397
+ if (!chatSessionState.title) {
398
+ chatSessionState.title = 'Generating title...';
399
+ try {
400
+ const title = await generateChatTitle(userInput);
401
+ chatSessionState.title = title;
402
+ }
403
+ catch (e) {
404
+ debugLog('Failed to save session with title: ' + e);
405
+ }
406
+ }
407
+ chatHistoryService
408
+ .saveSession({
409
+ id: chatSessionState.id,
410
+ title: chatSessionState.title,
411
+ timestamp: Date.now(),
412
+ history,
413
+ })
414
+ .catch((e) => debugLog('Failed to auto-save session: ' + e));
415
+ }
416
+ return;
417
+ }
418
+ // If it returned false, the graph was too simple (1 task) or failed, so we fall through to the single-agent loop.
419
+ if (!inputHandler.isCurrentlyPrompting()) {
420
+ spinner.start('Thinking (Single-Agent)...');
421
+ }
422
+ }
243
423
  // Send the formulated prompt payload to the generative model
244
424
  let result;
245
425
  try {
246
426
  result = await chat.sendMessage(finalInput, undefined, ac.signal);
247
427
  }
248
428
  catch (e) {
249
- spinner.stop('');
429
+ spinner.stop();
430
+ process.stdout.write('\x1b[2K\r');
250
431
  if (e.name === 'AbortError' || e.message?.includes('abort')) {
251
432
  p.log.warn(pc.yellow('Generation aborted by user.'));
252
433
  }
253
434
  else {
254
435
  p.log.error(pc.red(`Error communicating with AI: ${e.message || String(e)}`));
255
436
  }
256
- continue;
437
+ return;
257
438
  }
258
439
  latestUsage = result.response.usageMetadata?.();
259
440
  const grounding = result.response.groundingMetadata?.();
260
441
  if (grounding?.webSearchQueries && grounding.webSearchQueries.length > 0) {
261
442
  p.log.info(`${pc.blue('🌐')} ${pc.dim('Google Search Queries:')} ${grounding.webSearchQueries.map((q) => pc.cyan(`"${q}"`)).join(', ')}`);
262
443
  }
263
- spinner.stop('');
444
+ spinner.stop();
445
+ process.stdout.write('\x1b[2K\r');
264
446
  const calls = result.response.functionCalls();
265
447
  debugLog(`Initial functionCalls: ${calls && calls.length > 0 ? JSON.stringify(calls) : 'None'}`);
266
448
  // Stage 2: Recursive Tool Loops and Automated Self-Correction (delegated to a helper function to avoid nested loop warning)
@@ -303,44 +485,95 @@ export async function startAgentLoop(workspaceRoot, version) {
303
485
  if (history.length > 0) {
304
486
  if (!chatSessionState.title) {
305
487
  chatSessionState.title = 'Generating title...';
306
- generateChatTitle(userInput).then(title => {
488
+ try {
489
+ const title = await generateChatTitle(userInput);
307
490
  chatSessionState.title = title;
308
- chatHistoryService.saveSession({
491
+ await chatHistoryService.saveSession({
309
492
  id: chatSessionState.id,
310
493
  title: chatSessionState.title,
311
494
  timestamp: Date.now(),
312
- history: chat.getRawHistory()
313
- }).catch(e => debugLog('Failed to save session with title: ' + e));
314
- });
495
+ history: chat.getRawHistory(),
496
+ });
497
+ }
498
+ catch (e) {
499
+ debugLog('Failed to save session with title: ' + e);
500
+ }
315
501
  }
316
- chatHistoryService.saveSession({
502
+ chatHistoryService
503
+ .saveSession({
317
504
  id: chatSessionState.id,
318
505
  title: chatSessionState.title,
319
506
  timestamp: Date.now(),
320
- history
321
- }).catch(e => debugLog('Failed to auto-save session: ' + e));
507
+ history,
508
+ })
509
+ .catch((e) => debugLog('Failed to auto-save session: ' + e));
510
+ }
511
+ let planModeReturn = undefined;
512
+ if (isPlanMode && finalText && finalText !== '[Generation stopped.]') {
513
+ inputHandler.stop(); // MUST stop the async input handler before showing an interactive prompt, or it will intercept stdin and freeze/duplicate
514
+ // Ensure the terminal has fully scrolled and there is a clean line break
515
+ console.log('\n');
516
+ // Small delay to allow stdout to flush and terminal to finish scrolling from the markdown output
517
+ // This prevents @clack/prompts from miscalculating the cursor position and duplicating the prompt on arrow keys
518
+ await new Promise((resolve) => setTimeout(resolve, 200));
519
+ const planAction = await p['select']({
520
+ message: 'Plan generated. What would you like to do?',
521
+ options: [
522
+ { value: 'proceed', label: 'Proceed with plan', hint: 'Executes the generated plan immediately' },
523
+ { value: 'edit', label: 'Edit plan', hint: 'Provide additional feedback to revise the plan' },
524
+ { value: 'exit', label: 'Exit plan mode', hint: 'Return to normal chat without executing' },
525
+ ],
526
+ });
527
+ if (p.isCancel(planAction) || planAction === 'exit') {
528
+ planModeReturn = 'exit';
529
+ }
530
+ else if (planAction === 'edit') {
531
+ planModeReturn = 'edit';
532
+ }
533
+ else if (planAction === 'proceed') {
534
+ planModeReturn = 'proceed';
535
+ }
322
536
  }
537
+ return { planModeReturn, contextResult: gatherRes.contextResult };
323
538
  }
324
539
  catch (err) {
325
- spinner.stop('');
326
540
  const message = err instanceof Error ? err.message : String(err);
327
541
  p.log.error(`${pc.red('Error:')} ${message}`);
328
542
  }
329
543
  finally {
544
+ spinner.stop();
545
+ process.stdout.write('\x1b[2K\r');
330
546
  // Persist any verified or partial changes into our session change ledger.
331
547
  // This ensures that if the process crashes, errors out, or is aborted mid-generation,
332
548
  // the user can still use /revert to undo the partial file mutations.
549
+ const changedFiles = changeLogger.getChangedFiles();
333
550
  changeLogger.commitChangeSet();
551
+ // Delta-update the embedding index if there were changes
552
+ if (changedFiles.length > 0) {
553
+ try {
554
+ const { getEmbeddingIndex } = await import('./embeddingIndex.js');
555
+ const index = getEmbeddingIndex();
556
+ // Only update if it's already loaded in memory
557
+ if (index.isReady()) {
558
+ await index.updateIndex(workspaceRoot, changedFiles);
559
+ await index.save(workspaceRoot);
560
+ }
561
+ }
562
+ catch (e) {
563
+ debugLog(`Failed to delta-update embedding index: ${e.message}`);
564
+ }
565
+ }
334
566
  }
335
- }
567
+ });
336
568
  }
337
- async function collectUserInput() {
569
+ async function collectUserInput(history, isPlanMode) {
338
570
  let lines = [];
339
571
  let isMultiLine = false;
340
572
  while (true) {
341
- const userInputRaw = await p.text({
342
- message: isMultiLine ? ' ' : pc.magenta('❯'),
573
+ const userInputRaw = await historyText({
574
+ message: isMultiLine ? ' ' : isPlanMode ? pc.cyan('(Plan Mode) ❯') : pc.magenta('❯'),
343
575
  placeholder: isMultiLine ? '' : 'Use "\\" for new lines',
576
+ history,
344
577
  });
345
578
  if (p.isCancel(userInputRaw)) {
346
579
  if (isMultiLine) {
@@ -376,10 +609,12 @@ async function compressContextFiles(workspaceRoot, contextResult) {
376
609
  const cachedContext = readCache(workspaceRoot, 'context_cache.json') || {};
377
610
  let cacheUpdated = false;
378
611
  const compressedFiles = new Map();
379
- for (const [filePath, content] of contextResult.relevantFiles.entries()) {
380
- if (content.length < 2000) {
612
+ for (const [filePath, contentObj] of contextResult.relevantFiles.entries()) {
613
+ const content = contentObj.text;
614
+ const inlineData = contentObj.inlineData;
615
+ if (content.length < 2000 && !inlineData) {
381
616
  debugLog(`Bypassing cache and compression for ${filePath} (length ${content.length} < 2000)`);
382
- compressedFiles.set(filePath, content);
617
+ compressedFiles.set(filePath, contentObj);
383
618
  continue;
384
619
  }
385
620
  const fileHash = crypto
@@ -388,13 +623,13 @@ async function compressContextFiles(workspaceRoot, contextResult) {
388
623
  .digest('hex');
389
624
  if (cachedContext[fileHash]) {
390
625
  debugLog(`Context cache HIT for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
391
- compressedFiles.set(filePath, cachedContext[fileHash]);
626
+ compressedFiles.set(filePath, { text: cachedContext[fileHash] });
392
627
  }
393
628
  else {
394
629
  debugLog(`Context cache MISS for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
395
630
  const compressPrompt = `Summarize the following file contents concisely. Preserve all exports, functions, classes, variables, and architectural purpose. Keep it under 2000 characters if possible. File: ${filePath}`;
396
- const summary = await compressTextUsingFlashLite(content, compressPrompt);
397
- compressedFiles.set(filePath, summary);
631
+ const summary = await compressTextUsingFlashLite(content, compressPrompt, inlineData);
632
+ compressedFiles.set(filePath, { text: summary });
398
633
  cachedContext[fileHash] = summary;
399
634
  cacheUpdated = true;
400
635
  }
@@ -441,8 +676,12 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
441
676
  const forcePrompt = `AUTOMATED SYSTEM CHECK: You did not modify any files. You MUST use your file modification tools to apply a fix for the previously mentioned errors. Do not just explain the issue.`;
442
677
  spinner.start('Thinking (Correction)...');
443
678
  result = await chat.sendMessage(forcePrompt);
444
- spinner.stop('');
679
+ spinner.stop();
680
+ process.stdout.write('\x1b[2K\r');
445
681
  correctionAttempts++;
682
+ const collector = getMetricCollector();
683
+ if (collector)
684
+ collector.recordSelfCorrection();
446
685
  continue;
447
686
  }
448
687
  previousChangeCount = currentChanges.length;
@@ -466,9 +705,17 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
466
705
  }
467
706
  if (!hasErrors) {
468
707
  p.log.success('Verification passed.');
708
+ const collector = getMetricCollector();
709
+ if (collector && correctionAttempts === 0)
710
+ collector.recordVerificationResult(true);
469
711
  break;
470
712
  }
713
+ const collector = getMetricCollector();
714
+ if (collector && correctionAttempts === 0)
715
+ collector.recordVerificationResult(false);
471
716
  correctionAttempts++;
717
+ if (collector)
718
+ collector.recordSelfCorrection();
472
719
  if (correctionAttempts > MAX_CORRECTIONS) {
473
720
  p.log.warn('Max self-correction attempts reached. Leaving remaining issues for manual review.');
474
721
  break;
@@ -496,7 +743,8 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
496
743
  const correctionPrompt = `AUTOMATED SYSTEM CHECK: Your previous changes resulted in the following issues:\n\n${combinedIssuesForAI}\n\nPlease analyze these issues and use your file modification tools to fix them.`;
497
744
  spinner.start('Thinking (Correction)...');
498
745
  result = await chat.sendMessage(correctionPrompt);
499
- spinner.stop('');
746
+ spinner.stop();
747
+ process.stdout.write('\x1b[2K\r');
500
748
  }
501
749
  return { finalText };
502
750
  }
@@ -1,4 +1,9 @@
1
1
  import type { Content, FunctionCall } from '@google/generative-ai';
2
+ export declare function setModelOverride(agent: 'context' | 'execution', overrideStr: string): void;
3
+ export declare function clearModelOverrides(): void;
4
+ export declare function setGlobalActiveModel(model: string): void;
5
+ export declare function getGlobalActiveModel(): string;
6
+ export declare function getGlobalLatestUsageMetadata(): any;
2
7
  export declare class ProxyChatSession {
3
8
  private history;
4
9
  private modelName;
@@ -31,7 +36,7 @@ export declare class ProxyChatSession {
31
36
  name: string;
32
37
  response: any;
33
38
  };
34
- }>, additionalText?: string, abortSignal?: AbortSignal): Promise<{
39
+ }>, additionalText?: string, abortSignal?: AbortSignal, onChunk?: (chunk: string) => void): Promise<{
35
40
  response: {
36
41
  text: () => string;
37
42
  functionCalls: () => FunctionCall[] | undefined;
@@ -56,16 +61,25 @@ export declare function getPlanExecutionConfig(): {
56
61
  functionDeclarations: import("@google/generative-ai").FunctionDeclaration[];
57
62
  }[];
58
63
  };
64
+ export declare function getPlanModeConfig(): {
65
+ systemInstruction: string;
66
+ tools: never[];
67
+ };
59
68
  /**
60
69
  * Compresses a large string of text using gemini-3.1-flash-lite.
61
70
  * Used for shrinking context payloads to prevent OOM/choking.
62
71
  */
63
- export declare function compressTextUsingFlashLite(text: string, instruction?: string): Promise<string>;
64
- export declare const CONTEXT_AGENT_MODEL: "gemini-3.5-flash";
72
+ export declare function compressTextUsingFlashLite(text: string, instruction?: string, inlineData?: any): Promise<string>;
73
+ /**
74
+ * Returns the tool declarations for the read-only Context Agent.
75
+ * Extracted as a reusable function so investigation sub-agents can import
76
+ * the same schema without duplicating the declarations.
77
+ */
78
+ export declare function getContextToolDeclarations(): any[];
65
79
  export declare function createContextAgentSession(): any;
66
- export declare const INTENT_ROUTER_MODEL: "gemini-3.1-flash-lite";
67
80
  export declare function createIntentRouterSession(): any;
68
- export declare const WEB_SEARCH_AGENT_MODEL: "gemini-3.1-flash-lite";
81
+ export declare function createExecutionComplexitySession(): any;
82
+ export declare function createInvestigationComplexitySession(): any;
69
83
  export declare function createWebSearchAgentSession(): any;
70
84
  /**
71
85
  * Generates a concise title for a chat session based on the user's first message.