minovative-mind-cli 1.0.5 → 1.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.
@@ -1,36 +1,107 @@
1
+ /**
2
+ * @fileoverview Core agent execution service.
3
+ *
4
+ * This file coordinates the central execution environment for Minovative Mind,
5
+ * an AI-powered developer CLI. It orchestrates:
6
+ *
7
+ * 1. **Interactive REPL Loop**: Collects multiline user input, supports slash-commands,
8
+ * reverting changes, switching models, auto-commit, and clipboard paste injection.
9
+ * 2. **Context-Aware Investigation**: Gathers repo-wide context and compresses it using
10
+ * low-cost, high-speed LLM summarization.
11
+ * 3. **Autonomous Tool Loop (processResponse)**: Allows the AI model to execute file-system,
12
+ * shell-command, and search operations, feeding outcomes back in a recursive, multi-turn loop.
13
+ * 4. **Asynchronous Interrupt Handling (AsyncInputHandler)**: Intercepts keyboard events
14
+ * on standard input to let users pause execution, queue live feedback, or abort generation.
15
+ * 5. **Self-Correction Pipeline**: Compares modified files against TS/Linter diagnostics,
16
+ * providing feedback on syntax or semantic failures back to the AI for self-healing.
17
+ */
1
18
  import * as p from '@clack/prompts';
2
19
  import pc from 'picocolors';
3
20
  import { promises as fs } from 'node:fs';
4
21
  import path from 'node:path';
5
22
  import { exec } from 'node:child_process';
6
23
  import { promisify } from 'node:util';
24
+ import { debugLog, toggleDebugMode } from '../utils/logger.js';
7
25
  const execAsync = promisify(exec);
26
+ /**
27
+ * Asynchronous Input Handler (AsyncInputHandler)
28
+ *
29
+ * This class intercepts user keyboard input from standard input (stdin) while
30
+ * background processes (e.g., Gemini model generations or long shell tool runs)
31
+ * are executing. It provides seamless execution management by letting users:
32
+ *
33
+ * 1. **Pause execution** at any time by pressing a key.
34
+ * 2. **Queue feedback** ("chained messages") without waiting for the entire run to finish.
35
+ * 3. **Force abort** the current model or tool run by entering "stop" or "stop!".
36
+ *
37
+ * ### Raw Mode & Terminal States
38
+ * In normal CLI execution, Node.js waits for a line feed (Enter) before emitting input.
39
+ * To intercept immediate keystrokes, we put `process.stdin` into *raw mode*.
40
+ * While in raw mode, we listen for direct data buffers.
41
+ * To avoid visual conflicts with our logging output and Clack's CLI spinners,
42
+ * we dynamically pause spinners, detach listeners, disable raw mode, open standard
43
+ * interactive text-prompt forms, and resume raw mode and spinners upon completion.
44
+ */
8
45
  export class AsyncInputHandler {
46
+ /** Queue of pending user feedback/instructions typed during active background execution */
9
47
  queue = [];
48
+ /** Guard flag preventing multiple simultaneous input prompt overlays */
10
49
  isPrompting = false;
50
+ /** Reference to the Clack CLI spinner which must be paused/restarted during prompts */
11
51
  spinner = null;
52
+ /** Stores the terminal's raw mode configuration state before handler activation */
12
53
  originalRawMode = false;
54
+ /** Indicates whether the input handler is currently inactive/stopped */
13
55
  stopped = true;
56
+ /** Reference to the AbortController controlling the active AI request to trigger cancellations */
14
57
  ac = null;
58
+ /**
59
+ * Registers the active AbortController for the current AI request.
60
+ * This is triggered when the user commands a process cancel (e.g., typing "stop").
61
+ *
62
+ * @param ac - The AbortController controlling the current generation.
63
+ */
15
64
  setAbortController(ac) {
16
65
  this.ac = ac;
17
66
  }
67
+ /**
68
+ * Determines if a user-prompt dialog is actively running.
69
+ * Useful for coordinating other console logging output to avoid UI overlap.
70
+ *
71
+ * @returns True if a text prompt is currently displayed, false otherwise.
72
+ */
18
73
  isCurrentlyPrompting() {
19
74
  return this.isPrompting;
20
75
  }
76
+ /**
77
+ * Blocks and waits until any active prompting action is completed.
78
+ * Guarantees terminal stdout is clean before resuming logs.
79
+ */
21
80
  async waitForPrompt() {
22
81
  while (this.isPrompting) {
23
82
  await new Promise((resolve) => setTimeout(resolve, 100));
24
83
  }
25
84
  }
85
+ /**
86
+ * Core stdin event listener. Detects pressed keys, pauses background visual elements,
87
+ * handles exit interrupts, and opens a Clack text dialog for input.
88
+ *
89
+ * Handles recovery of the standard input stream and raw mode states even if
90
+ * errors occur during prompt initialization.
91
+ *
92
+ * @param chunk - The raw terminal buffer containing keypress data.
93
+ * @private
94
+ */
26
95
  onData = async (chunk) => {
27
96
  try {
28
97
  if (this.isPrompting)
29
98
  return;
30
99
  const char = chunk.toString();
100
+ // Handle Ctrl+C (End of Text ASCII 0x03) immediately
31
101
  if (char === '\u0003') {
32
102
  process.exit(0);
33
103
  }
104
+ // Ignore standard non-printable control keys, escape sequences, etc.
34
105
  if (char.charCodeAt(0) < 32 || char === '\u007f')
35
106
  return;
36
107
  this.isPrompting = true;
@@ -38,9 +109,12 @@ export class AsyncInputHandler {
38
109
  if (process.stdin.isTTY) {
39
110
  process.stdin.setRawMode(false);
40
111
  }
112
+ // Hide the background spinner before prompt output to avoid corrupting terminal lines
41
113
  if (this.spinner) {
42
114
  this.spinner.stop('Paused to receive input');
43
115
  }
116
+ // Capture the user feedback. The character typed to trigger this event is passed
117
+ // as the initial value of the prompt to avoid losing the first keystroke.
44
118
  const userInput = await p.text({
45
119
  message: 'Add chained message:',
46
120
  placeholder: '(Leave blank and press Enter to cancel)',
@@ -64,13 +138,14 @@ export class AsyncInputHandler {
64
138
  }
65
139
  }
66
140
  catch (err) {
67
- // Ignore
141
+ // Absorb and suppress errors safely during raw stream intercepts
68
142
  }
69
143
  finally {
70
144
  if (!this.stopped) {
71
145
  if (process.stdin.isTTY) {
72
146
  process.stdin.setRawMode(true);
73
147
  }
148
+ // Small delay before reattaching listener to avoid capturing duplicate keypress frames
74
149
  setTimeout(() => {
75
150
  if (!this.stopped) {
76
151
  process.stdin.on('data', this.onData);
@@ -83,6 +158,12 @@ export class AsyncInputHandler {
83
158
  }
84
159
  }
85
160
  };
161
+ /**
162
+ * Starts intercepting keystrokes and enables raw terminal processing.
163
+ * Saves the original raw mode configuration to ensure a clean restoration later.
164
+ *
165
+ * @param spinner - The current active Clack spinner UI reference, if any.
166
+ */
86
167
  start(spinner) {
87
168
  this.stopped = false;
88
169
  if (spinner) {
@@ -95,6 +176,10 @@ export class AsyncInputHandler {
95
176
  process.stdin.resume();
96
177
  process.stdin.on('data', this.onData);
97
178
  }
179
+ /**
180
+ * Disables raw mode, stops intercepting keystrokes, and restores
181
+ * the terminal stdin stream to its original raw/cooked state.
182
+ */
98
183
  stop() {
99
184
  this.stopped = true;
100
185
  process.stdin.removeListener('data', this.onData);
@@ -103,6 +188,12 @@ export class AsyncInputHandler {
103
188
  }
104
189
  this.spinner = null;
105
190
  }
191
+ /**
192
+ * Retrieves and flushes all user feedback messages accumulated during execution.
193
+ *
194
+ * @returns A concatenated string of all queued user messages separated by newlines,
195
+ * or an empty string if nothing was queued.
196
+ */
106
197
  getAndClear() {
107
198
  if (this.queue.length === 0)
108
199
  return '';
@@ -112,17 +203,20 @@ export class AsyncInputHandler {
112
203
  }
113
204
  }
114
205
  import { consumeSkipOnce, executeTool, getApprovalMode, setApprovalMode } from './agent-tools.js';
115
- import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, ProxyChatSession } from './ai.js';
206
+ import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, ProxyChatSession, compressTextUsingFlashLite } from './ai.js';
116
207
  import { changeLogger } from './changeLogger.js';
117
208
  import { gatherContext, routeIntent } from './contextAgent.js';
118
209
  import { verifyChangedFiles } from './verificationService.js';
119
210
  import { buildContextInjection } from '../utils/contextPrompts.js';
120
- import { debugLog, toggleDebugMode } from '../utils/logger.js';
121
211
  import { readPaste } from '../utils/paste.js';
122
212
  import { marked } from 'marked';
123
213
  import { markedTerminal } from 'marked-terminal';
124
214
  marked.use(markedTerminal());
125
215
  // ─── Constants ───────────────────────────────────────────────────────
216
+ /**
217
+ * Mapping of tool identifiers to user-friendly terminal emojis.
218
+ * Enhances the visual feedback during background tool execution turns.
219
+ */
126
220
  const TOOL_ICONS = {
127
221
  read_file: '📖',
128
222
  write_file: '✏️',
@@ -134,6 +228,10 @@ const TOOL_ICONS = {
134
228
  rename_file: '🚚',
135
229
  find_dependencies: '🔗',
136
230
  };
231
+ /**
232
+ * Human-readable translations for tool actions.
233
+ * Used for constructing clear, active-verb descriptive log headers in the CLI.
234
+ */
137
235
  const TOOL_LABELS = {
138
236
  read_file: 'Reading file',
139
237
  write_file: 'Writing file',
@@ -146,12 +244,33 @@ const TOOL_LABELS = {
146
244
  find_dependencies: 'Tracing dependencies',
147
245
  };
148
246
  // ─── Helpers ─────────────────────────────────────────────────────────
247
+ /**
248
+ * Formats a tool execution call into a beautifully stylized console status line.
249
+ * Extracts context-specific arguments to display relevant parameters in real-time.
250
+ *
251
+ * E.g., for `read_file`, it outputs line-range constraints. For `rename_file`, it prints
252
+ * a path redirection arrow.
253
+ *
254
+ * @param name - The identifier of the tool being called (e.g. 'read_file').
255
+ * @param args - The dictionary of parameters supplied to the tool.
256
+ * @returns A fully colorized and formatted ANSI console string.
257
+ */
149
258
  function formatToolCall(name, args) {
150
259
  const icon = TOOL_ICONS[name] ?? '🔧';
151
260
  const label = TOOL_LABELS[name] ?? name;
152
261
  switch (name) {
153
- case 'read_file':
154
- return `${icon} ${label}: ${pc.cyan(String(args.filePath))}`;
262
+ case 'read_file': {
263
+ let extra = '';
264
+ if (args.startLine !== undefined || args.endLine !== undefined) {
265
+ const start = args.startLine ?? 1;
266
+ const end = args.endLine ?? 'end';
267
+ extra = pc.dim(` (Lines ${start}-${end})`);
268
+ }
269
+ else if (Array.isArray(args.targetElements) && args.targetElements.length > 0) {
270
+ extra = pc.dim(` (Elements: ${args.targetElements.join(', ')})`);
271
+ }
272
+ return `${icon} ${label}${extra}: ${pc.cyan(String(args.filePath))}`;
273
+ }
155
274
  case 'write_file':
156
275
  return `${icon} ${label}: ${pc.cyan(String(args.filePath))}`;
157
276
  case 'modify_file':
@@ -173,10 +292,16 @@ function formatToolCall(name, args) {
173
292
  }
174
293
  }
175
294
  /**
176
- * Prompts the user to approve a shell command, respecting the current
177
- * approval mode (ask / skip-once / skip-all).
295
+ * Prompts the user to approve a pending shell command requested by the AI.
296
+ * Implements a safety gate to protect the host machine's environment from arbitrary code execution.
178
297
  *
179
- * Returns `true` if the command should run, `false` to deny.
298
+ * Supports three approval models:
299
+ * 1. **Ask**: Prompts the user for every shell execution command.
300
+ * 2. **Skip-Once**: Automatically grants permission to the current command, then resets.
301
+ * 3. **Skip-All / Auto-Approve**: Grants permission to all future terminal commands in the current session.
302
+ *
303
+ * @param command - The terminal string requested for execution.
304
+ * @returns A promise resolving to `true` if approved, or `false` if denied/cancelled.
180
305
  */
181
306
  async function requestCommandApproval(command) {
182
307
  const mode = getApprovalMode();
@@ -218,11 +343,49 @@ async function requestCommandApproval(command) {
218
343
  * Processes a single model response that may contain tool calls.
219
344
  * Handles the full tool-call loop: execute → feed results → repeat
220
345
  * until the model produces a final text response.
346
+ *
347
+ * @param chat - The current active proxy chat session.
348
+ * @param result - The current result returned from sending a message to the model.
349
+ * @param workspaceRoot - The absolute/relative path to the workspace root.
350
+ * @param inputHandler - The input handler to check for queued interrupts.
351
+ * @param agentState - State tracker for whether the target agent configuration is CHAT or EXECUTE.
352
+ * @param abortSignal - Signal to detect when the operation has been cancelled.
353
+ * @returns A promise that resolves to the final text response.
354
+ */
355
+ /**
356
+ * Coordinates and executes the recursive model-response tool-execution loop.
357
+ *
358
+ * ### Execution Mechanism:
359
+ * This function processes the initial response from the generative model. If the model determines
360
+ * it needs to execute tools (e.g. read_file, run_command, modify_file) to answer or fulfill the request:
361
+ *
362
+ * 1. **Analyze Tool Requirements**: Parses requested `functionCalls` from the LLM.
363
+ * 2. **Check for User Abortion**: Periodically examines the `AbortSignal` to stop execution if requested.
364
+ * 3. **Manage Safety Approvals**: For high-risk operations (like `run_command`), requests explicit confirmation from the user (or uses auto-approve configurations).
365
+ * 4. **Execute Operations**: Runs requested tool actions via `executeTool` in parallel or series as requested.
366
+ * 5. **Handle Interrupts**: Integrates background user inputs queued in `inputHandler` as new context prompts back into the active LLM context.
367
+ * 6. **Submit Loop Frame**: Feeds execution outcomes back to the Gemini session and recursively repeats this sequence until the LLM produces a final text answer without further tool requests.
368
+ *
369
+ * ### Limits & Recovery Guardrails:
370
+ * - **Turn-Limiting**: Capped at `MAX_TURNS` (25) to prevent infinite loops, API token drain, or excessive billing if the AI gets stuck in a repetitive loop.
371
+ * - **Empty-Response Healing**: If the API returns an empty text response with no tools, the system initiates up to `MAX_EMPTY_RETRIES` (3) system-driven wake-up prompts to re-engage the model.
372
+ *
373
+ * @param chat - The current active proxy chat session.
374
+ * @param result - The current result returned from sending a message to the model.
375
+ * @param workspaceRoot - The absolute/relative path to the workspace root.
376
+ * @param inputHandler - The input handler to check for queued interrupts.
377
+ * @param agentState - State tracker for whether the target agent configuration is CHAT or EXECUTE.
378
+ * @param abortSignal - Signal to detect when the operation has been cancelled.
379
+ * @returns A promise that resolves to the final text response.
221
380
  */
222
381
  async function processResponse(chat, result, workspaceRoot, inputHandler, agentState, abortSignal) {
223
382
  let response = result.response;
383
+ // Upper limit on autonomous sequential tool executions to prevent out-of-control loops
224
384
  let turnCount = 0;
225
385
  const MAX_TURNS = 25;
386
+ // Recovery thresholds for handling unexpected empty API payloads
387
+ let emptyRetryCount = 0;
388
+ const MAX_EMPTY_RETRIES = 3;
226
389
  // Loop while the model keeps requesting tool calls
227
390
  while (true) {
228
391
  turnCount++;
@@ -233,21 +396,50 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
233
396
  const functionCalls = response.functionCalls();
234
397
  if (!functionCalls || functionCalls.length === 0) {
235
398
  // No more tool calls — return the final text response
236
- return response.text() ?? '';
399
+ let finalOutput = response.text() ?? '';
400
+ if (!finalOutput.trim()) {
401
+ if (emptyRetryCount < MAX_EMPTY_RETRIES) {
402
+ emptyRetryCount++;
403
+ p.log.warn(pc.yellow(`AI returned an empty response. Attempting to recover (retry ${emptyRetryCount}/${MAX_EMPTY_RETRIES})...`));
404
+ // System-driven injection to kickstart the LLM's conversation generation
405
+ const wakeUpMessage = 'SYSTEM: You just returned a completely empty response with no tool calls. If you are stuck, please use your tools to explore the workspace, or explain what you are trying to do and ask the user for clarification. Do not return empty responses.';
406
+ try {
407
+ const retry = await chat.sendMessage(wakeUpMessage, undefined, abortSignal);
408
+ response = retry.response;
409
+ continue;
410
+ }
411
+ catch (e) {
412
+ if (e.name === 'AbortError' || e.message?.includes('abort')) {
413
+ p.log.warn(pc.yellow('Generation stopped by user during recovery.'));
414
+ return '[Generation stopped by user]';
415
+ }
416
+ finalOutput = '[System Error: The AI returned an empty response and failed to recover.]';
417
+ }
418
+ }
419
+ else {
420
+ finalOutput =
421
+ '[The AI repeatedly returned empty responses and could not recover. This usually means it hit an API filter/limit or is stuck. Try rewording your prompt.]';
422
+ }
423
+ }
424
+ return finalOutput;
237
425
  }
238
- // Process each tool call
426
+ // Process each tool call requested by the model
239
427
  const toolResponses = [];
240
428
  for (const fc of functionCalls) {
429
+ if (abortSignal.aborted) {
430
+ p.log.warn(pc.yellow('Execution aborted by user.'));
431
+ return '[Generation stopped by user]';
432
+ }
241
433
  const toolName = fc.name;
242
434
  const toolArgs = (fc.args ?? {});
243
- // Display what the agent is doing
435
+ // Log the ongoing tool action to the console
244
436
  p.log.step(formatToolCall(toolName, toolArgs));
245
- // For run_command, request user approval
437
+ // If executing a CLI/shell command, wait for approval
246
438
  if (toolName === 'run_command') {
247
439
  await inputHandler.waitForPrompt();
248
- inputHandler.stop();
440
+ inputHandler.stop(); // Temporarily release raw-mode during blocking interactive prompt
249
441
  const approved = await requestCommandApproval(toolArgs.command);
250
- inputHandler.start();
442
+ inputHandler.start(); // Re-engage raw-mode for continuous background monitoring
251
443
  if (!approved) {
252
444
  toolResponses.push({
253
445
  functionResponse: {
@@ -261,14 +453,17 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
261
453
  continue;
262
454
  }
263
455
  }
264
- // Execute the tool
265
- const toolResult = await executeTool(workspaceRoot, toolName, toolArgs);
456
+ // Execute the underlying filesystem, shell or search tool logic
457
+ const toolResult = await executeTool(workspaceRoot, toolName, toolArgs, abortSignal);
266
458
  if (toolResult.error) {
267
459
  debugLog(`Raw Tool Error for ${toolName}: ${toolResult.error}`);
268
- // Truncate the error message for the terminal UI to prevent console clutter
269
- // (e.g. hiding the large file previews sent to the AI)
270
- const displayError = toolResult.error.split('\n')[0].substring(0, 100);
271
- p.log.warn(`${pc.red('Tool error:')} [${displayError}]`);
460
+ // Do not spam the user with expected chunking warnings
461
+ if (!toolResult.error.includes('File is too large')) {
462
+ // Truncate the error message for the terminal UI to prevent console clutter
463
+ // (e.g. hiding the large file previews sent to the AI)
464
+ const displayError = toolResult.error.split('\n')[0].substring(0, 100);
465
+ p.log.warn(`${pc.red('Tool error:')} [${displayError}]`);
466
+ }
272
467
  }
273
468
  toolResponses.push({
274
469
  functionResponse: {
@@ -280,12 +475,14 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
280
475
  },
281
476
  });
282
477
  }
478
+ // Check if the user entered any feedback or interrupt commands during the tool execution cycle
283
479
  await inputHandler.waitForPrompt();
284
480
  const queuedMsg = inputHandler.getAndClear();
285
481
  let additionalText = undefined;
286
482
  if (queuedMsg) {
287
483
  additionalText = `[USER INTERRUPTION] The user sent the following message during your execution:\n"${queuedMsg}"\n\nPlease incorporate this feedback into your ongoing work. Address the user's message, but DO NOT lose track of your original overall plan or focus. After addressing this interruption, continue with your broader objective.`;
288
484
  p.log.info(pc.cyan(`Sending queued message to AI...`));
485
+ // Dynamically upgrade agent permissions/intent to EXECUTE mode if the interrupted instruction requires system modifications
289
486
  const newIntent = await routeIntent(queuedMsg);
290
487
  if (agentState.targetAgent === 'CHAT') {
291
488
  if (newIntent.targetAgent === 'EXECUTE' || newIntent.needsContext) {
@@ -296,7 +493,7 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
296
493
  }
297
494
  }
298
495
  }
299
- // Feed tool results back to the model
496
+ // Feed tool results and potential interruption text back to the model
300
497
  let followUp;
301
498
  try {
302
499
  followUp = await chat.sendMessage(toolResponses, additionalText, abortSignal);
@@ -318,12 +515,54 @@ async function processResponse(chat, result, workspaceRoot, inputHandler, agentS
318
515
  /**
319
516
  * Starts the interactive agent chat loop.
320
517
  * Runs until the user types "exit", "quit", or presses Ctrl+C.
518
+ *
519
+ * @param workspaceRoot - The root directory of the workspace.
520
+ * @param version - The active version of the CLI utility.
521
+ */
522
+ /**
523
+ * Starts and orchestrates the primary interactive command-line interface (REPL) loop.
524
+ *
525
+ * This is the central control loop of the CLI application. It runs indefinitely until
526
+ * the user explicitly issues an exit directive (e.g., typing "exit", "quit", `/` command menu selections,
527
+ * or using keyboard breaks like Ctrl+C).
528
+ *
529
+ * ### Architectural Pipeline:
530
+ * 1. **User Input Collection**:
531
+ * - Captures multiline text strings by checking for trailing backslash characters (`\`).
532
+ * - Prevents visual terminal glitches during multi-line typing on varying themes.
533
+ * - Automatically handles command menu redirection when users enter a forward slash (`/`).
534
+ * - Intercepts and filters OS clipboard paste buffers to allow inserting massive scripts cleanly.
535
+ *
536
+ * 2. **Slash Commands Processing**:
537
+ * - `/paste`: Interactively captures large copy-pasted blocks using EOF tracking.
538
+ * - `/clear`: Hard-clears active terminal histories and resets session state.
539
+ * - `/models`: Dynamic runtime model hot-swapping (e.g., swapping between Flash and Pro variants).
540
+ * - `/debug`: Toggles active runtime execution telemetry logging.
541
+ * - `/auto-approve`: Grants blanket terminal script permissions to bypass prompt approval blocks.
542
+ * - `/revert`: Pops the most recent change-set log and rolls back mutated files to original states.
543
+ * - `/commit`: Performs git diff staging, executes model summaries to write micro-commits, and executes local git commits.
544
+ *
545
+ * 3. **Workspace Context Gathering & Intention Routing**:
546
+ * - Leverages the Context Agent (`gatherContext`) to perform exploratory scans of the active repository.
547
+ * - Identifies user intention to hot-swap system prompt strategies (`CHAT` for conversational assistance vs. `EXECUTE` for multi-turn modifications).
548
+ * - Compresses gathered repo structures and file contents using high-speed Flash-Lite engines to respect context boundaries and budget costs.
549
+ *
550
+ * 4. **Model Execution & Self-Correction Feedback Loop**:
551
+ * - Passes the target request to Gemini alongside compressed workspace injections.
552
+ * - Recursively processes tool invocations via `processResponse`.
553
+ * - Runs static validation checks (`verifyChangedFiles`) against mutated files to check for compiler/linter bugs.
554
+ * - If files fail validation, enters a self-healing loop by submitting raw diagnostic errors directly back to the AI for remediation.
555
+ *
556
+ * @param workspaceRoot - The relative or absolute path representing the active workspace environment.
557
+ * @param version - The Semantic Version string of the active tool distribution.
321
558
  */
322
559
  export async function startAgentLoop(workspaceRoot, version) {
323
560
  const chat = createSharedChatSession();
324
561
  const inputHandler = new AsyncInputHandler();
325
562
  let isRawPasteMode = false;
326
- // Intercept terminal paste events to prevent accidental early-submission of multi-line pastes
563
+ // Hook process.stdin.emit to intercept fast stream inputs.
564
+ // When large buffers containing newlines arrive rapidly, we interpret them as a clipboard paste,
565
+ // sanitizing and collapsing them to prevent premature command line submission.
327
566
  const originalEmit = process.stdin.emit.bind(process.stdin);
328
567
  process.stdin.emit = function (event, ...args) {
329
568
  if (!isRawPasteMode && event === 'data' && Buffer.isBuffer(args[0])) {
@@ -343,9 +582,10 @@ export async function startAgentLoop(workspaceRoot, version) {
343
582
  let lines = [];
344
583
  let isMultiLine = false;
345
584
  let canceledGlobal = false;
585
+ // Input collection sub-loop (handles multiline line-by-line gathering)
346
586
  while (true) {
347
587
  const userInputRaw = await p.text({
348
- message: isMultiLine ? ' ' : '❯',
588
+ message: isMultiLine ? ' ' : pc.magenta('❯'),
349
589
  placeholder: isMultiLine ? '' : 'Use "\\" for new lines',
350
590
  });
351
591
  if (p.isCancel(userInputRaw)) {
@@ -385,6 +625,7 @@ export async function startAgentLoop(workspaceRoot, version) {
385
625
  if (!userInput) {
386
626
  continue;
387
627
  }
628
+ // Capture standalone forward slash triggers to open the selection console
388
629
  if (userInput === '/') {
389
630
  const commandMenu = await p.select({
390
631
  message: 'Command Menu',
@@ -412,7 +653,10 @@ export async function startAgentLoop(workspaceRoot, version) {
412
653
  if (userInput.length === 0) {
413
654
  continue;
414
655
  }
415
- // Handle special commands
656
+ if (userInput.startsWith('/')) {
657
+ debugLog(`Executing slash command: ${userInput}`);
658
+ }
659
+ // ─── Slash Commands Handling ─────────────────────────────────────
416
660
  if (userInput.toLowerCase() === '/paste') {
417
661
  p.log.info(pc.cyan('Paste mode activated. Paste your text below, then press Ctrl+D on an empty line to submit. (Ctrl+C to cancel)'));
418
662
  try {
@@ -437,7 +681,7 @@ export async function startAgentLoop(workspaceRoot, version) {
437
681
  process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); // Hard clear screen and scrollback
438
682
  p.intro(`${pc.bgCyan(pc.black(' Minovative Mind '))} ${pc.dim('v' + version)}`);
439
683
  p.log.info(`${pc.dim('Workspace:')} ${pc.cyan(workspaceRoot)}`);
440
- p.log.info(`${pc.dim('Commands:')} Type ${pc.yellow('/')} to open the command menu. Type ${pc.yellow('exit')} to leave.`);
684
+ p.log.info(`${pc.dim('Commands:')} Type ${pc.yellow('/')} to open the command menu and "${pc.yellow('stop')}" to stop the ai generation. Type ${pc.yellow('exit')} to leave.`);
441
685
  p.log.success('Chat history cleared.');
442
686
  console.log(pc.dim('\nType your coding request below. Type "exit" or "quit" to leave.\n'));
443
687
  continue;
@@ -446,6 +690,8 @@ export async function startAgentLoop(workspaceRoot, version) {
446
690
  const selectedModel = await p.select({
447
691
  message: 'Select AI Model',
448
692
  options: [
693
+ { value: 'gemini-3.1-pro', label: 'Gemini 3.1 Pro', hint: 'The newest Pro model for complex logic' },
694
+ { value: 'gemini-3.5-flash', label: 'Gemini 3.5 Flash', hint: 'The newest Flash model for high speed' },
449
695
  { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', hint: 'Best for complex coding & large context' },
450
696
  { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', hint: 'Balanced performance' },
451
697
  // {value: 'claude-opus-4-6', label: 'Claude Opus 4.6', hint: 'Anthropic: Highly capable, complex reasoning'},
@@ -459,12 +705,12 @@ export async function startAgentLoop(workspaceRoot, version) {
459
705
  continue;
460
706
  }
461
707
  if (userInput.toLowerCase() === '/debug') {
462
- const isEnabled = toggleDebugMode();
463
- if (isEnabled) {
464
- p.log.success('Debug mode enabled. Internal logs will now be shown.');
708
+ const debugMode = toggleDebugMode();
709
+ if (debugMode) {
710
+ p.log.info('Debug mode enabled. Internal logs will now be shown.');
465
711
  }
466
712
  else {
467
- p.log.success('Debug mode disabled.');
713
+ p.log.info('Debug mode disabled.');
468
714
  }
469
715
  continue;
470
716
  }
@@ -540,6 +786,7 @@ ${diffOut}
540
786
  }
541
787
  continue;
542
788
  }
789
+ // ─── Main Execution Cycle ────────────────────────────────────────
543
790
  const spinner = p.spinner();
544
791
  const ac = new AbortController();
545
792
  inputHandler.setAbortController(ac);
@@ -547,6 +794,7 @@ ${diffOut}
547
794
  inputHandler.start(spinner);
548
795
  changeLogger.startChangeSet(userInput);
549
796
  let finalInput = userInput;
797
+ // Stage 1: Gather Workspace Context and route intentions
550
798
  spinner.start('🔍 Investigating workspace...');
551
799
  const chatHistory = chat.getRecentHistory(3);
552
800
  const gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
@@ -555,27 +803,28 @@ ${diffOut}
555
803
  }
556
804
  });
557
805
  let latestUsage = undefined;
558
- // Also grab any messages queued exactly between gatherContext ending and here
806
+ // Collect any inputs that were queued while the Context Agent was investigating
559
807
  const leftoverMsg = inputHandler.getAndClear();
560
808
  if (leftoverMsg) {
561
809
  gatherRes.chainedMessages.push(leftoverMsg);
562
810
  }
811
+ // Resolve the effective target agent mode based on original intent + any chained commands
563
812
  let effectiveTargetAgent = gatherRes.targetAgent;
564
813
  if (gatherRes.chainedMessages.length > 0) {
565
814
  const chainedContent = gatherRes.chainedMessages.join('\n');
566
815
  const newIntent = await routeIntent(chainedContent);
567
816
  if (gatherRes.contextResult !== null) {
568
- // Was SEARCH: Can change to EXECUTE or CHAT based on chained message
817
+ // If the model previously needed search context, determine if followups change the mode
569
818
  effectiveTargetAgent = newIntent.targetAgent;
570
819
  }
571
820
  else {
572
- // Was NOT SEARCH
821
+ // If not in a context-search flow
573
822
  if (gatherRes.targetAgent === 'EXECUTE') {
574
- // Executing cannot downgrade to CHAT
823
+ // EXECUTE permission sets are immutable during follow-ups to prevent downgrades
575
824
  effectiveTargetAgent = 'EXECUTE';
576
825
  }
577
826
  else {
578
- // Was CHAT: Can change to SEARCH or EXECUTE. Both are treated as EXECUTE to grant tools.
827
+ // Conversational prompts can escalate to EXECUTE if chained instructions dictate
579
828
  if (newIntent.targetAgent === 'EXECUTE' || newIntent.needsContext) {
580
829
  effectiveTargetAgent = 'EXECUTE';
581
830
  }
@@ -583,30 +832,38 @@ ${diffOut}
583
832
  }
584
833
  finalInput += `\n\n[USER FOLLOW-UP INSTRUCTIONS SENT DURING INVESTIGATION]:\n${chainedContent}\n\nPlease incorporate these instructions into your work. Address them appropriately, but ensure you do not lose track of the original request's primary objective.`;
585
834
  }
586
- // Hot-swap the agent configuration based on intent
835
+ // Hot-swap the underlying LLM system instruction context depending on intent (conversational vs execution)
587
836
  debugLog(`Intent Router output: original targetAgent = ${gatherRes.targetAgent}, effective = ${effectiveTargetAgent}`);
588
837
  const config = effectiveTargetAgent === 'CHAT' ? getGeneralChatConfig() : getPlanExecutionConfig();
589
838
  let dynamicSystemInstruction = config.systemInstruction;
839
+ // Inject gathered workspace directories, dependency configurations, and matching search patterns
590
840
  if (gatherRes.contextResult) {
591
- const contextInjection = buildContextInjection(gatherRes.contextResult);
592
- debugLog(`Context injection size: ${contextInjection.length} chars`);
841
+ let contextInjection = buildContextInjection(gatherRes.contextResult);
842
+ debugLog(`Raw context injection size: ${contextInjection.length} chars`);
843
+ // Compress context inputs using Gemini Flash Lite to optimize prompt density and prevent token bloat
844
+ contextInjection = await compressTextUsingFlashLite(contextInjection, 'Summarize the following project context concisely. Preserve all file paths, project types, and the high-level purpose of the read files. Keep it under 2000 characters if possible.');
845
+ debugLog(`Compressed context injection size: ${contextInjection.length} chars`);
593
846
  dynamicSystemInstruction += '\n\n' + contextInjection;
594
847
  }
848
+ // Apply the dynamic prompt updates and tool registrations to the active chat session
595
849
  chat.setAgentConfig(dynamicSystemInstruction, config.tools);
596
850
  if (!inputHandler.isCurrentlyPrompting()) {
597
851
  spinner.message('Thinking...');
598
852
  }
853
+ // Send the formulated prompt payload to the generative model
599
854
  let result;
600
855
  try {
601
856
  result = await chat.sendMessage(finalInput, undefined, ac.signal);
602
857
  }
603
858
  catch (e) {
859
+ spinner.stop('');
604
860
  if (e.name === 'AbortError' || e.message?.includes('abort')) {
605
861
  p.log.warn(pc.yellow('Generation aborted by user.'));
606
- spinner.stop('');
607
- continue;
608
862
  }
609
- throw e;
863
+ else {
864
+ p.log.error(pc.red(`Error communicating with AI: ${e.message || String(e)}`));
865
+ }
866
+ continue;
610
867
  }
611
868
  latestUsage = result.response.usageMetadata?.();
612
869
  const grounding = result.response.groundingMetadata?.();
@@ -620,17 +877,29 @@ ${diffOut}
620
877
  const calls = result.response.functionCalls();
621
878
  debugLog(`Initial functionCalls: ${calls && calls.length > 0 ? JSON.stringify(calls) : 'None'}`);
622
879
  let agentState = { targetAgent: effectiveTargetAgent };
880
+ let previousChangeCount = changeLogger.getCurrentChangeSet()?.changes.length || 0;
881
+ // Stage 2: Recursive Tool Loops and Automated Self-Correction
623
882
  while (correctionAttempts <= MAX_CORRECTIONS) {
624
883
  if (finalText === '[Generation stopped by user]')
625
884
  break;
626
885
  finalText = await processResponse(chat, result, workspaceRoot, inputHandler, agentState, ac.signal);
627
- debugLog(`processResponse returned finalText (length ${finalText.length}): "${finalText.substring(0, 50)}..."`);
886
+ debugLog(`processResponse returned finalText (length ${finalText.length}): "${finalText.substring(0, 10)}..."`);
887
+ if (finalText.startsWith('[The AI repeatedly returned empty responses')) {
888
+ break;
889
+ }
628
890
  const currentChanges = changeLogger.getCurrentChangeSet()?.changes || [];
891
+ // If the correction cycle is active but no new changes were registered, the model gave up
892
+ if (correctionAttempts > 0 && currentChanges.length <= previousChangeCount) {
893
+ p.log.warn('AI failed to modify any files during the correction attempt. Aborting auto-correction.');
894
+ break;
895
+ }
896
+ previousChangeCount = currentChanges.length;
629
897
  const changedFiles = currentChanges
630
898
  .filter((c) => c.action === 'create' || c.action === 'modify')
631
899
  .map((c) => c.filePath);
632
900
  if (changedFiles.length === 0)
633
901
  break;
902
+ // Stage 3: Static code analysis / verification
634
903
  p.log.step('Verifying modified files...');
635
904
  const verificationErrors = await verifyChangedFiles(workspaceRoot, changedFiles);
636
905
  if (!verificationErrors) {
@@ -643,19 +912,22 @@ ${diffOut}
643
912
  break;
644
913
  }
645
914
  p.log.warn(`${pc.yellow('Verification failed. Auto-correcting errors')} (Attempt ${correctionAttempts}/${MAX_CORRECTIONS})...`);
646
- // Optionally show a snippet of the error to the user so they aren't left in the dark
647
915
  const displayError = verificationErrors.split('\n').slice(0, 5).join('\n');
648
916
  console.log(pc.dim(` ${displayError.replace(/\n/g, '\n ')}\n ...`));
649
917
  debugLog(`Verification failed on attempt ${correctionAttempts}/${MAX_CORRECTIONS}. Errors:\n${verificationErrors}`);
918
+ // Compile compilation and syntax diagnostic warnings into an auto-correction prompt
650
919
  const correctionPrompt = `AUTOMATED SYSTEM CHECK: Your previous changes resulted in the following errors:\n\n${verificationErrors}\n\nPlease analyze these errors and use your file modification tools to fix them.`;
651
920
  spinner.start('Thinking (Correction)...');
652
921
  result = await chat.sendMessage(correctionPrompt);
653
922
  spinner.stop('');
654
923
  }
924
+ // Persist the verified changes into our session change ledger
655
925
  changeLogger.commitChangeSet();
656
926
  if (finalText) {
657
927
  console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(${chat.getModel()})`)}\n`);
658
- console.log(marked.parse(finalText));
928
+ // Strip out excessive empty lines generated by LLMs to prevent huge visual gaps in marked-terminal
929
+ const cleanText = finalText.replace(/\n([ \t]*\n){2,}/g, '\n\n');
930
+ console.log(marked.parse(cleanText));
659
931
  }
660
932
  if (latestUsage && latestUsage.remainingBalance !== undefined) {
661
933
  p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(latestUsage.remainingBalance.toLocaleString())}`);