@pheem49/mint 1.5.2 → 1.5.3

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,17 +1,19 @@
1
1
  const fs = require('fs');
2
+ const os = require('os');
2
3
  const path = require('path');
3
- const { execFile } = require('child_process');
4
+ const { execFile, execFileSync } = require('child_process');
4
5
  const { promisify } = require('util');
5
- const { GoogleGenAI } = require('@google/genai');
6
6
  const axios = require('axios');
7
7
  const cheerio = require('cheerio');
8
- const { readConfig, getAvailableProviders } = require('../System/config_manager');
8
+ const { readConfig, getAvailableProviders, CONFIG_DIR } = require('../System/config_manager');
9
9
  const safetyManager = require('../System/safety_manager');
10
10
  const memoryStore = require('../AI_Brain/memory_store');
11
11
  const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
12
12
  const { executeAction } = require('../System/action_executor');
13
13
  const toolRegistry = require('../System/tool_registry');
14
14
  const sandboxRunner = require('../System/sandbox_runner');
15
+ const providerAdapter = require('../AI_Brain/provider_adapter');
16
+ const taskManager = require('../System/task_manager');
15
17
 
16
18
  async function webSearch(query, onProgress = () => {}) {
17
19
  if (!query) throw new Error('Search query required.');
@@ -118,10 +120,13 @@ async function webSearch(query, onProgress = () => {}) {
118
120
 
119
121
 
120
122
  const execFileAsync = promisify(execFile);
121
- const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
122
123
  const MAX_TOOL_OUTPUT = 12000;
123
124
  const MAX_AGENT_STEPS = 16;
124
125
  const MAX_JSON_REPAIR_ATTEMPTS = 2;
126
+ const DEFAULT_VERIFICATION_BUDGET = 2;
127
+ const MINT_CONFIG_DIR = CONFIG_DIR || path.join(os.homedir(), '.config', 'mint');
128
+ const PLAN_FILE_PATH = path.join(MINT_CONFIG_DIR, 'mint_plan.md');
129
+ const PLAN_FILE_LABEL = path.join('~', '.config', 'mint', 'mint_plan.md');
125
130
  const SUPPORTED_CODE_PROVIDERS = ['gemini', 'anthropic', 'openai', 'local_openai'];
126
131
 
127
132
  const CODE_AGENT_PROMPT = `You are "Mint" (มิ้นท์), a pragmatic, polite, and highly helpful AI assistant that can chat, reason, write code, and search the web.
@@ -134,10 +139,14 @@ PERSONALITY & TONE:
134
139
  - If the latest user message contains Thai characters, respond in Thai.
135
140
  - If the latest user message is English, ASCII-only, or a short English greeting such as "hi", "hello", "ok", or "thanks", respond in English.
136
141
  - Do not use Thai just because your persona mentions Mint/มิ้นท์, previous history was Thai, or app settings use th-TH.
142
+ - This language routing applies to user-facing final answers and ask_user questions.
143
+ - Internal progress notes, the JSON "thought" field, and "plan" action bullet text MUST be written in English.
137
144
  - Politeness:
138
145
  - **WHEN RESPONDING IN THAI:** Use natural female polite particles such as "ค่ะ" or "นะคะ" where appropriate. Refer to yourself as "มิ้นท์" when it sounds natural.
139
146
  - **WHEN RESPONDING IN ENGLISH:** Use a polite, concise, professional tone.
140
147
  - Emojis: Avoid emojis in technical, review, debugging, and code-editing responses unless the user explicitly uses or asks for them.
148
+ - For technical/code/debugging tasks, keep progress notes and final summaries factual and compact. Do not cheerlead, over-apologize, roleplay, or add affectionate language.
149
+ - For code edits, final summaries should lead with changed files/behavior and verification. Avoid "เรียบร้อยแล้วค่ะ" repetition and decorative closing lines.
141
150
 
142
151
  Rules:
143
152
  1. Respond with valid JSON only.
@@ -148,13 +157,34 @@ Rules:
148
157
  6. Use shell commands for inspection, tests, and formatting when useful.
149
158
  6. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
150
159
  7. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
151
- 8. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
152
- 9. When you are done, return "finish" with your final response to the user in the "summary" field.
160
+ 8. Before editing more than one file, you MUST first use the "plan" action and wait for user approval. The plan must be written in English, start with "Plan:", and include one bullet per file, for example "- Update src/CLI/agent.js". After approval, make the edits.
161
+ 9. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
162
+ 10. Before any "apply_patch" or "write_file" action, the "thought" field MUST explicitly name the file you will edit and why that file is the right target. If the file is under "scratch/" or "tests/fixtures/", call that out and explain why editing disposable/test fixture content is intentional.
163
+ 11. When you are done, return "finish" with your final response to the user in the "summary" field.
164
+
165
+ Action safety and intent discipline:
166
+ - The latest user message is authoritative. Do not continue an older unfinished task unless the latest message explicitly asks you to continue or clearly refers to that task.
167
+ - For greetings, name-calls, acknowledgements, or backchannels such as "มิ้น", "มิ้นๆ", "อ๋อ", "โอเค", "ขอบคุณ", "hi", "hello", "ok", or "thanks": use "finish" only. Do not inspect files, run shell commands, search code, or claim you checked anything.
168
+ - If the user asks for a command to type, provide the command in "finish". Do not run it unless the user explicitly asks you to run it.
169
+ - If the user asks not to edit or says this is read-only analysis (for example "ห้ามแก้ไฟล์", "ไม่ต้องแก้", "แค่อ่าน", "แค่สรุป", "do not edit", "no edits", "read only"), do not use "plan", "apply_patch", "write_file", "create_folder", "delete_file", "clipboard_write", or system-changing actions. Inspect with read/search tools and finish with a summary only.
170
+ - If the user explicitly asks to search keywords, method names, class names, or symbols, use "search_code" before repeatedly reading more file ranges. Prefer a scoped search with input.path instead of scanning the whole workspace when the likely area is clear.
171
+ - Search scope heuristics: choose input.path only when that path is visible in the current workspace context or was named by the user. If the repo layout is unclear, use list_files on "." first, then choose the narrowest existing directory. Common scopes include "src", "app", "lib", "packages", "tests", and project-specific folders; in this Mint repo, CLI/terminal/command/approval/chat agent questions usually start in "src/CLI", desktop UI/renderer/settings/widget questions in "src/UI", system/config/safety questions in "src/System", and plugin questions in "src/Plugins". If a scoped search path is missing or finds no useful matches, search the whole workspace.
172
+ - If the user explicitly asks you to run a command or provided code, such as "รันคำสั่ง npm test ให้หน่อย", "รันโค้ดนี้หน่อย", or "run npm test", choose "run_shell" with the exact command when it is clear. The app will ask the user for approval before execution.
173
+ - If the user asks you to run something but no exact command/code is provided, use "ask_user" to request the command instead of guessing.
174
+ - If the user asks what is inside a folder and a concrete path is present in the latest message or recent context, use "list_files" for that path. If no concrete target is clear, ask for clarification instead of guessing.
175
+ - Never say you opened, checked, inspected, or verified a file/folder unless a tool observation in this turn actually supports it.
176
+
177
+ Progress updates:
178
+ - The "thought" field is shown to the user as a live progress note. Do not put private chain-of-thought there.
179
+ - Write "thought" as one short, concrete status sentence in English, even when the user writes in Thai or another language.
180
+ - Mention what you just learned from the previous observation when it matters, then say what you will inspect or change next.
181
+ - Before editing, explain the specific file and behavior you are about to change.
182
+ - Before verifying, explain what check you are running and why.
153
183
 
154
184
  Response format:
155
185
  {
156
186
  "thought": "short reasoning about what to do next",
157
- "action": "web_search" | "list_files" | "read_file" | "search_code" | "find_path" | "run_shell" | "apply_patch" | "write_file" | "ask_user" | "open_url" | "open_app" | "open_file" | "open_folder" | "create_folder" | "system_info" | "system_automation" | "finish",
187
+ "action": "web_search" | "list_files" | "read_file" | "search_code" | "find_path" | "run_shell" | "verify" | "plan" | "apply_patch" | "write_file" | "ask_user" | "open_url" | "search" | "open_app" | "web_automation" | "open_file" | "open_folder" | "create_folder" | "delete_file" | "clipboard_write" | "learn_file" | "learn_folder" | "system_info" | "plugin" | "mcp_tool" | "mouse_move" | "mouse_click" | "type_text" | "key_tap" | "system_automation" | "finish",
158
188
  "input": {
159
189
  "question": "your question to the user for ask_user",
160
190
  "query": "search text for web_search, search_code, or find_path",
@@ -162,9 +192,12 @@ Response format:
162
192
  "path": "relative/path",
163
193
  "type": "file" | "dir" | "any",
164
194
  "command": "shell command",
195
+ "commands": ["npm test", "npm run build"],
165
196
  "startLine": 1,
166
197
  "endLine": 120,
167
198
  "content": "full file content for write_file",
199
+ "plan": ["- Update relative/path.js", "- Add tests in tests/example.test.js"],
200
+ "files": ["relative/path.js", "tests/example.test.js"],
168
201
  "summary": "your final conversational or technical response to the user (Matches user language and uses polite particles)",
169
202
  "verification": "tests or checks (if applicable)",
170
203
  "sessionSummary": "brief persistent summary for the workspace",
@@ -184,9 +217,11 @@ Tool notes:
184
217
  - "web_search": search the internet for information when you lack knowledge.
185
218
  - "list_files": inspect the workspace or a subdirectory.
186
219
  - "read_file": read a file, optionally with startLine/endLine.
187
- - "search_code": search by text or regex-like pattern.
220
+ - "search_code": search by text or regex-like pattern. Optionally set input.path to a relative file or directory to avoid scanning the whole workspace; use the search scope heuristics above when the user did not name a path.
188
221
  - "find_path": find files or directories by path/name when the user is looking for a folder, filename, or location.
189
222
  - "run_shell": run a non-destructive command in the workspace.
223
+ - "verify": run the detected or provided test/build/lint commands. If verification fails, inspect the output, patch the issue, and verify again within the remaining budget.
224
+ - "plan": present a user-visible multi-file edit plan before changing more than one file. Use English input.plan bullet strings and input.files as the expected touched files.
190
225
  - "apply_patch": update an existing file using one or more exact replacement hunks.
191
226
  - "write_file": create a new file or fully rewrite a file when replacement is not practical.
192
227
  - "ask_user": ask the user for clarification, preference, or more information before proceeding.
@@ -194,6 +229,8 @@ Tool notes:
194
229
  - "open_app": open a local application on the user's computer.
195
230
  - "system_info": get system information like CPU, memory, date, or weather.
196
231
  - "system_automation": control system settings like volume, brightness, or power.
232
+ - "plugin": run a configured Mint plugin.
233
+ - "mcp_tool": call a configured MCP tool.
197
234
  - "finish": stop and reply to the user using the "summary" field.
198
235
  `;
199
236
 
@@ -231,6 +268,10 @@ function normalizeExecutorAction(action, input = {}) {
231
268
  }
232
269
 
233
270
  function formatActionPreview(action, input = {}) {
271
+ if (action === 'search_code') {
272
+ const query = input.query || 'search';
273
+ return input.path ? `${query} in ${input.path}` : query;
274
+ }
234
275
  if (input.command) return input.command;
235
276
  if (input.path) return input.path;
236
277
  if (input.target) return input.target;
@@ -264,75 +305,32 @@ function evaluateActionResult(action, toolResult = '') {
264
305
  };
265
306
  }
266
307
 
267
- function splitDataUri(dataUri = '') {
268
- const match = String(dataUri).match(/^data:([^;]+);base64,([\s\S]+)$/);
269
- if (!match) return null;
270
- return {
271
- mimeType: match[1],
272
- data: match[2]
273
- };
274
- }
275
-
276
- function contentToText(content) {
277
- if (content && typeof content === 'object' && !Array.isArray(content)) {
278
- return String(content.text || '');
308
+ function getToolCallStatus(action, toolResult = '', evaluation = null) {
309
+ const text = String(toolResult || '');
310
+ if (/^Error:|User denied|blocked|denied|failed|exception|not found/i.test(text)) {
311
+ return 'failed';
279
312
  }
280
- return String(content || '');
281
- }
282
-
283
- function contentToGeminiParts(content) {
284
- const text = contentToText(content);
285
- const parts = text ? [{ text }] : [];
286
- if (content && typeof content === 'object' && content.imageDataUri) {
287
- const image = splitDataUri(content.imageDataUri);
288
- if (image) {
289
- parts.push({ inlineData: { mimeType: image.mimeType, data: image.data } });
290
- }
313
+ if (evaluation && evaluation.status === 'failed') {
314
+ return 'failed';
291
315
  }
292
- return parts.length > 0 ? parts : [{ text: '' }];
293
- }
294
-
295
- function contentToOpenAIContent(content) {
296
- const text = contentToText(content) || 'Analyze this input.';
297
- if (content && typeof content === 'object' && content.imageDataUri) {
298
- return [
299
- { type: 'text', text },
300
- { type: 'image_url', image_url: { url: content.imageDataUri } }
301
- ];
316
+ if (action === 'run_shell' && /(ERR!|Error:|FAIL|failed|not found|permission denied)/i.test(text)) {
317
+ return 'failed';
302
318
  }
303
- return text;
319
+ return 'success';
304
320
  }
305
321
 
306
- function contentToAnthropicContent(content) {
307
- const text = contentToText(content) || 'Analyze this input.';
308
- if (content && typeof content === 'object' && content.imageDataUri) {
309
- const image = splitDataUri(content.imageDataUri);
310
- if (image) {
311
- return [
312
- { type: 'image', source: { type: 'base64', media_type: image.mimeType, data: image.data } },
313
- { type: 'text', text }
314
- ];
315
- }
316
- }
317
- return text;
322
+ function summarizeToolTarget(action, input = {}) {
323
+ if (action === 'plan') return 'Multi-file plan';
324
+ return formatActionPreview(action, input);
318
325
  }
319
326
 
320
327
  function getSupportedCodeProviderOrder(config, availableProviders = getAvailableProviders(config || {}), requestedOverride = null) {
321
- const requestedProvider = requestedOverride || (config && config.aiProvider) || 'gemini';
322
- const priority = ['anthropic', 'openai', 'gemini', 'local_openai'];
323
- const ordered = [];
324
-
325
- if (SUPPORTED_CODE_PROVIDERS.includes(requestedProvider) && availableProviders.includes(requestedProvider)) {
326
- ordered.push(requestedProvider);
327
- }
328
-
329
- for (const provider of priority) {
330
- if (availableProviders.includes(provider) && !ordered.includes(provider)) {
331
- ordered.push(provider);
332
- }
333
- }
334
-
335
- return ordered.length > 0 ? ordered : ['gemini'];
328
+ return providerAdapter.getProviderAttemptOrder(config || {}, {
329
+ supported: SUPPORTED_CODE_PROVIDERS,
330
+ availableProviders,
331
+ requested: requestedOverride || (config && config.aiProvider) || 'gemini',
332
+ priority: ['anthropic', 'openai', 'gemini', 'local_openai']
333
+ });
336
334
  }
337
335
 
338
336
  function selectSupportedCodeProvider(config, availableProviders = getAvailableProviders(config || {})) {
@@ -340,17 +338,7 @@ function selectSupportedCodeProvider(config, availableProviders = getAvailablePr
340
338
  }
341
339
 
342
340
  function getCodeProviderModel(provider, config = {}) {
343
- switch (provider) {
344
- case 'anthropic':
345
- return config.anthropicModel || 'claude-3-5-sonnet-latest';
346
- case 'openai':
347
- return config.openaiModel || 'gpt-4o';
348
- case 'local_openai':
349
- return config.localModelName || 'local-model';
350
- case 'gemini':
351
- default:
352
- return config.geminiModel || DEFAULT_GEMINI_MODEL;
353
- }
341
+ return providerAdapter.getProviderModel(provider, config);
354
342
  }
355
343
 
356
344
  function resolveWorkspacePath(workspaceRoot, targetPath = '.') {
@@ -432,12 +420,16 @@ function readFileRange(workspaceRoot, targetPath, startLine = 1, endLine = 200)
432
420
  .join('\n');
433
421
  }
434
422
 
435
- async function searchCode(workspaceRoot, query) {
423
+ async function searchCode(workspaceRoot, query, targetPath = '.') {
436
424
  if (!query || !query.trim()) {
437
425
  throw new Error('Search query is required.');
438
426
  }
427
+ const searchRoot = resolveWorkspacePath(workspaceRoot, targetPath || '.');
428
+ if (!fs.existsSync(searchRoot)) {
429
+ throw new Error(`Search path does not exist: ${targetPath}`);
430
+ }
439
431
  try {
440
- const { stdout } = await execFileAsync('rg', ['-n', '--hidden', '--glob', '!.git', query, workspaceRoot], {
432
+ const { stdout } = await execFileAsync('rg', ['-n', '--hidden', '--glob', '!.git', query, searchRoot], {
441
433
  cwd: workspaceRoot,
442
434
  maxBuffer: 1024 * 1024 * 4
443
435
  });
@@ -527,6 +519,66 @@ async function runShell(workspaceRoot, command) {
527
519
  return truncate([stdout, stderr].filter(Boolean).join('\n') || '(no output)');
528
520
  }
529
521
 
522
+ async function runVerificationCommands(workspaceRoot, commands = [], options = {}) {
523
+ const detected = detectTestCommands(workspaceRoot);
524
+ const requested = Array.isArray(commands)
525
+ ? commands.map(command => String(command || '').trim()).filter(Boolean)
526
+ : [];
527
+ const commandList = requested.length > 0 ? requested : detected;
528
+
529
+ if (commandList.length === 0) {
530
+ return {
531
+ passed: true,
532
+ output: 'No verification commands detected.'
533
+ };
534
+ }
535
+
536
+ const requestApproval = typeof options.requestApproval === 'function'
537
+ ? options.requestApproval
538
+ : async () => true;
539
+ const budget = Number.isFinite(options.budget) ? options.budget : DEFAULT_VERIFICATION_BUDGET;
540
+ const attempt = Number.isFinite(options.attempt) ? options.attempt : 1;
541
+ const lines = [
542
+ `Verification attempt ${attempt}/${budget}`,
543
+ `Commands: ${commandList.join(' && ')}`
544
+ ];
545
+
546
+ for (const command of commandList) {
547
+ const approved = await requestApproval({
548
+ type: 'verify',
549
+ label: command,
550
+ preview: command
551
+ });
552
+ if (!approved) {
553
+ lines.push(`SKIP ${command}: User denied verification command.`);
554
+ return {
555
+ passed: false,
556
+ output: lines.join('\n')
557
+ };
558
+ }
559
+
560
+ try {
561
+ const output = await runShell(workspaceRoot, command);
562
+ lines.push(`PASS ${command}`);
563
+ if (output && output !== '(no output)') {
564
+ lines.push(truncate(output, 4000));
565
+ }
566
+ } catch (error) {
567
+ lines.push(`FAIL ${command}`);
568
+ lines.push(truncate([error.stdout, error.stderr, error.message].filter(Boolean).join('\n'), 6000));
569
+ return {
570
+ passed: false,
571
+ output: lines.join('\n')
572
+ };
573
+ }
574
+ }
575
+
576
+ return {
577
+ passed: true,
578
+ output: lines.join('\n')
579
+ };
580
+ }
581
+
530
582
  function splitDiffLines(text) {
531
583
  const normalized = String(text || '').replace(/\r\n/g, '\n');
532
584
  const lines = normalized.split('\n');
@@ -536,11 +588,66 @@ function splitDiffLines(text) {
536
588
  return lines;
537
589
  }
538
590
 
539
- function formatDiffRange(startLine, count) {
540
- return count === 1 ? `${startLine}` : `${startLine},${count}`;
591
+ function normalizeGitNoIndexDiff(stdout, targetPath) {
592
+ const lines = String(stdout || '').replace(/\r\n/g, '\n').split('\n');
593
+ const filtered = [];
594
+ for (const line of lines) {
595
+ if (!line) continue;
596
+ if (line.startsWith('diff --git ') || line.startsWith('index ')) continue;
597
+ if (line.startsWith('--- ')) {
598
+ filtered.push(`--- a/${targetPath}`);
599
+ continue;
600
+ }
601
+ if (line.startsWith('+++ ')) {
602
+ filtered.push(`+++ b/${targetPath}`);
603
+ continue;
604
+ }
605
+ filtered.push(line);
606
+ }
607
+ return filtered.join('\n');
608
+ }
609
+
610
+ function buildSimpleFullFileDiff(targetPath, previousContent = '', nextContent = '') {
611
+ const previousLines = splitDiffLines(previousContent);
612
+ const nextLines = splitDiffLines(nextContent || '');
613
+ const oldRange = previousLines.length || 0;
614
+ const newRange = nextLines.length || 0;
615
+ const output = [
616
+ `--- a/${targetPath}`,
617
+ `+++ b/${targetPath}`,
618
+ `@@ -1,${oldRange} +1,${newRange} @@`
619
+ ];
620
+
621
+ previousLines.forEach(line => output.push(`-${line}`));
622
+ nextLines.forEach(line => output.push(`+${line}`));
623
+ return output.join('\n');
541
624
  }
542
625
 
543
- function buildUnifiedDiffPreview(workspaceRoot, patchInput, options = {}) {
626
+ function buildContentDiffPreview(targetPath, previousContent = '', nextContent = '') {
627
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mint-diff-'));
628
+ const oldPath = path.join(tempDir, 'old');
629
+ const newPath = path.join(tempDir, 'new');
630
+
631
+ try {
632
+ fs.writeFileSync(oldPath, previousContent || '', 'utf8');
633
+ fs.writeFileSync(newPath, nextContent || '', 'utf8');
634
+ try {
635
+ const stdout = execFileSync('git', ['diff', '--no-index', '--', oldPath, newPath], {
636
+ encoding: 'utf8',
637
+ maxBuffer: 1024 * 1024 * 4
638
+ });
639
+ return normalizeGitNoIndexDiff(stdout, targetPath);
640
+ } catch (error) {
641
+ const stdout = error.stdout || '';
642
+ if (stdout) return normalizeGitNoIndexDiff(stdout, targetPath);
643
+ return buildSimpleFullFileDiff(targetPath, previousContent, nextContent);
644
+ }
645
+ } finally {
646
+ fs.rmSync(tempDir, { recursive: true, force: true });
647
+ }
648
+ }
649
+
650
+ function buildPatchedContent(workspaceRoot, patchInput) {
544
651
  if (!patchInput || !patchInput.path) {
545
652
  throw new Error('Patch path is required.');
546
653
  }
@@ -555,47 +662,16 @@ function buildUnifiedDiffPreview(workspaceRoot, patchInput, options = {}) {
555
662
  throw new Error('Patch hunks are required.');
556
663
  }
557
664
 
558
- const contextLines = Number.isFinite(options.contextLines) ? options.contextLines : 3;
559
- let content = fs.readFileSync(resolved, 'utf8');
560
- const output = [
561
- `--- a/${patchInput.path}`,
562
- `+++ b/${patchInput.path}`
563
- ];
564
-
565
- hunks.forEach((hunk, index) => {
566
- if (typeof hunk.oldText !== 'string' || typeof hunk.newText !== 'string') {
567
- throw new Error(`Patch hunk ${index + 1} is invalid.`);
568
- }
569
-
570
- const offset = content.indexOf(hunk.oldText);
571
- if (offset === -1) {
572
- throw new Error(`Patch hunk ${index + 1} oldText not found in ${patchInput.path}`);
573
- }
574
-
575
- const beforeText = content.slice(0, offset);
576
- const oldStartLine = beforeText.length === 0 ? 1 : splitDiffLines(beforeText).length + 1;
577
- const fileLines = splitDiffLines(content);
578
- const oldLines = splitDiffLines(hunk.oldText);
579
- const newLines = splitDiffLines(hunk.newText);
580
- const oldStartIndex = oldStartLine - 1;
581
- const contextStartIndex = Math.max(0, oldStartIndex - contextLines);
582
- const contextEndIndex = Math.min(fileLines.length, oldStartIndex + oldLines.length + contextLines);
583
- const beforeContext = fileLines.slice(contextStartIndex, oldStartIndex);
584
- const afterContext = fileLines.slice(oldStartIndex + oldLines.length, contextEndIndex);
585
- const oldRangeStart = contextStartIndex + 1;
586
- const oldRangeCount = beforeContext.length + oldLines.length + afterContext.length;
587
- const newRangeCount = beforeContext.length + newLines.length + afterContext.length;
588
-
589
- output.push(`@@ -${formatDiffRange(oldRangeStart, oldRangeCount)} +${formatDiffRange(oldRangeStart, newRangeCount)} @@`);
590
- beforeContext.forEach(line => output.push(` ${line}`));
591
- oldLines.forEach(line => output.push(`-${line}`));
592
- newLines.forEach(line => output.push(`+${line}`));
593
- afterContext.forEach(line => output.push(` ${line}`));
594
-
595
- content = `${content.slice(0, offset)}${hunk.newText}${content.slice(offset + hunk.oldText.length)}`;
596
- });
665
+ const previousContent = fs.readFileSync(resolved, 'utf8');
666
+ return {
667
+ previousContent,
668
+ nextContent: applyHunksToContent(previousContent, hunks, patchInput.path)
669
+ };
670
+ }
597
671
 
598
- return output.join('\n');
672
+ function buildUnifiedDiffPreview(workspaceRoot, patchInput, options = {}) {
673
+ const { previousContent, nextContent } = buildPatchedContent(workspaceRoot, patchInput);
674
+ return buildContentDiffPreview(patchInput.path, previousContent, nextContent);
599
675
  }
600
676
 
601
677
  function formatPatchPreview(workspaceRoot, patchInput) {
@@ -606,6 +682,281 @@ function formatPatchPreview(workspaceRoot, patchInput) {
606
682
  }
607
683
  }
608
684
 
685
+ function buildFullFileDiffPreview(workspaceRoot, targetPath, nextContent = '') {
686
+ if (!targetPath) {
687
+ throw new Error('Write path is required.');
688
+ }
689
+
690
+ const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
691
+ const previousContent = fs.existsSync(resolved) ? fs.readFileSync(resolved, 'utf8') : '';
692
+ return buildContentDiffPreview(targetPath, previousContent, nextContent || '');
693
+ }
694
+
695
+ function formatWritePreview(workspaceRoot, targetPath, content) {
696
+ try {
697
+ return buildFullFileDiffPreview(workspaceRoot, targetPath, content);
698
+ } catch (error) {
699
+ return `Write preview failed: ${error.message}\n${targetPath}\n${truncate(content || '', 800)}`;
700
+ }
701
+ }
702
+
703
+ function normalizeRelativePathForWarning(targetPath = '') {
704
+ return String(targetPath || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
705
+ }
706
+
707
+ function contentLooksLikeGuide(text = '') {
708
+ return /(guide|installation|publish|npm|registry|setup|documentation|คู่มือ|ติดตั้ง|เผยแพร่)/i.test(String(text || ''));
709
+ }
710
+
711
+ function contentLooksLikeBio(text = '') {
712
+ return /(bio|biography|profile|created by|assistant|ประวัติ|โปรไฟล์)/i.test(String(text || ''));
713
+ }
714
+
715
+ function contentLooksLikeConfig(text = '') {
716
+ return /(apiKey|token|secret|config|settings|\.env|clientSecret|refreshToken)/i.test(String(text || ''));
717
+ }
718
+
719
+ function buildApprovalWarnings(targetPath = '', nextContent = '') {
720
+ const normalized = normalizeRelativePathForWarning(targetPath);
721
+ const basename = path.basename(normalized).toLowerCase();
722
+ const warnings = [];
723
+
724
+ if (normalized.startsWith('scratch/')) {
725
+ warnings.push('Target is under scratch/, which is usually disposable/test content. Confirm this is intentional.');
726
+ }
727
+ if (normalized.startsWith('tests/fixtures/') || normalized.includes('/tests/fixtures/')) {
728
+ warnings.push('Target is under tests/fixtures/, so this may change test fixture behavior.');
729
+ }
730
+ if (/bio|profile|about/.test(basename) && contentLooksLikeGuide(nextContent)) {
731
+ warnings.push('File name looks like profile/bio content, but the new content looks like a guide or publishing document.');
732
+ }
733
+ if (/(guide|readme|docs?|manual)/.test(basename) && contentLooksLikeBio(nextContent)) {
734
+ warnings.push('File name looks like documentation, but the new content looks like biography/profile content.');
735
+ }
736
+ if (!/(config|settings|env|secret|token)/.test(basename) && contentLooksLikeConfig(nextContent)) {
737
+ warnings.push('New content appears to include config/secret-like terms; verify this file is the right place.');
738
+ }
739
+
740
+ return warnings;
741
+ }
742
+
743
+ function normalizePlanItems(plan) {
744
+ if (Array.isArray(plan)) {
745
+ return plan
746
+ .map(item => String(item || '').trim())
747
+ .filter(Boolean);
748
+ }
749
+ return String(plan || '')
750
+ .split('\n')
751
+ .map(line => line.trim())
752
+ .filter(Boolean);
753
+ }
754
+
755
+ function normalizePlanItemLanguage(item) {
756
+ let text = String(item || '').trim();
757
+ const hasBullet = text.startsWith('- ');
758
+ if (hasBullet) text = text.slice(2).trim();
759
+
760
+ const replacements = [
761
+ [/^แก้\s+(.+)$/i, 'Update $1'],
762
+ [/^แก้ไข\s+(.+)$/i, 'Update $1'],
763
+ [/^อัปเดต\s+(.+)$/i, 'Update $1'],
764
+ [/^ปรับ\s+(.+)$/i, 'Update $1'],
765
+ [/^สร้าง\s+(.+)$/i, 'Create $1'],
766
+ [/^เพิ่ม\s+(.+)$/i, 'Add $1'],
767
+ [/^ลบ\s+(.+)$/i, 'Remove $1'],
768
+ [/^ตรวจสอบ\s+(.+)$/i, 'Verify $1'],
769
+ [/^ทดสอบ\s+(.+)$/i, 'Test $1']
770
+ ];
771
+
772
+ for (const [pattern, replacement] of replacements) {
773
+ if (pattern.test(text)) {
774
+ text = text.replace(pattern, replacement);
775
+ break;
776
+ }
777
+ }
778
+
779
+ return hasBullet ? `- ${text}` : text;
780
+ }
781
+
782
+ function formatPlanPreview(input = {}) {
783
+ const items = normalizePlanItems(input.plan);
784
+ const files = Array.isArray(input.files)
785
+ ? input.files.map(file => String(file || '').trim()).filter(Boolean)
786
+ : [];
787
+ const lines = ['Plan:'];
788
+
789
+ if (items.length > 0) {
790
+ items.forEach(item => {
791
+ const normalizedItem = normalizePlanItemLanguage(item);
792
+ lines.push(normalizedItem.startsWith('- ') ? normalizedItem : `- ${normalizedItem}`);
793
+ });
794
+ } else {
795
+ files.forEach(file => lines.push(`- Update ${file}`));
796
+ }
797
+
798
+ return lines.join('\n');
799
+ }
800
+
801
+ function formatPlanApprovalSummary(input = {}) {
802
+ const items = normalizePlanItems(input.plan);
803
+ const files = Array.isArray(input.files)
804
+ ? input.files.map(file => String(file || '').trim()).filter(Boolean)
805
+ : [];
806
+ if (files.length > 0) {
807
+ return `${items.length || files.length} planned changes across ${files.length} files.`;
808
+ }
809
+ return `${items.length || 1} planned change${(items.length || 1) === 1 ? '' : 's'} prepared.`;
810
+ }
811
+
812
+ function formatPlanMarkdown(input = {}, context = {}) {
813
+ const preview = formatPlanPreview(input);
814
+ const files = Array.isArray(input.files)
815
+ ? input.files.map(file => String(file || '').trim()).filter(Boolean)
816
+ : [];
817
+ const task = String(context.task || input.task || '').trim();
818
+ const createdAt = context.createdAt || new Date().toISOString();
819
+ const approvalStatus = context.approvalStatus || 'Pending user approval';
820
+ const approvalTime = context.approvalTime || '';
821
+ const lines = [
822
+ '# Mint Plan',
823
+ '',
824
+ `Created: ${createdAt}`
825
+ ];
826
+
827
+ if (task) {
828
+ lines.push('', '## Task', '', task);
829
+ }
830
+
831
+ lines.push('', '## Plan', '', preview);
832
+
833
+ if (files.length > 0) {
834
+ lines.push('', '## Expected Files', '');
835
+ files.forEach(file => lines.push(`- ${file}`));
836
+ }
837
+
838
+ lines.push(
839
+ '',
840
+ '## Approval',
841
+ '',
842
+ `Status: ${approvalStatus}`
843
+ );
844
+
845
+ if (approvalTime) {
846
+ lines.push(`${approvalStatus}: ${approvalTime}`);
847
+ }
848
+
849
+ lines.push('');
850
+
851
+ return lines.join('\n');
852
+ }
853
+
854
+ function writePlanFile(workspaceRoot, input = {}, context = {}) {
855
+ const planPath = context.planPath || PLAN_FILE_PATH;
856
+ const content = formatPlanMarkdown(input, context);
857
+ fs.mkdirSync(path.dirname(planPath), { recursive: true });
858
+ fs.writeFileSync(planPath, content, 'utf8');
859
+ return {
860
+ path: planPath,
861
+ content
862
+ };
863
+ }
864
+
865
+ function updatePlanApprovalStatus(planFile, input = {}, context = {}) {
866
+ const content = formatPlanMarkdown(input, context);
867
+ fs.mkdirSync(path.dirname(planFile.path), { recursive: true });
868
+ fs.writeFileSync(planFile.path, content, 'utf8');
869
+ return {
870
+ ...planFile,
871
+ content
872
+ };
873
+ }
874
+
875
+ function getEditTargetPath(action, input = {}) {
876
+ if (action === 'apply_patch') {
877
+ return input.patch && input.patch.path ? String(input.patch.path) : '';
878
+ }
879
+ if (action === 'write_file') {
880
+ return input.path ? String(input.path) : '';
881
+ }
882
+ return '';
883
+ }
884
+
885
+ function requiresMultiFilePlan(action, input = {}, editPlanState = {}) {
886
+ const targetPath = getEditTargetPath(action, input);
887
+ if (!targetPath || editPlanState.approved) {
888
+ return false;
889
+ }
890
+
891
+ const touchedFiles = editPlanState.touchedFiles instanceof Set
892
+ ? editPlanState.touchedFiles
893
+ : new Set(editPlanState.touchedFiles || []);
894
+ return touchedFiles.size > 0 && !touchedFiles.has(targetPath);
895
+ }
896
+
897
+ function getMissingPlanFiles(editPlanState = {}) {
898
+ const expectedFiles = editPlanState.expectedFiles instanceof Set
899
+ ? editPlanState.expectedFiles
900
+ : new Set(editPlanState.expectedFiles || []);
901
+ const touchedFiles = editPlanState.touchedFiles instanceof Set
902
+ ? editPlanState.touchedFiles
903
+ : new Set(editPlanState.touchedFiles || []);
904
+
905
+ return Array.from(expectedFiles).filter(file => file && !touchedFiles.has(file));
906
+ }
907
+
908
+ function isReadOnlyTask(task = '') {
909
+ const text = String(task || '').toLowerCase();
910
+ return /(?:ห้ามแก้|ไม่ต้องแก้|อย่าแก้|ไม่แก้ไฟล์|ห้ามเขียน|แค่อ่าน|อ่านอย่างเดียว|แค่สรุป|สรุปอย่างเดียว|แค่อธิบาย|อธิบายอย่างเดียว|do not edit|don't edit|no edits?|read[-\s]?only|only read|only summarize|summari[sz]e only|do not modify|don't modify|no changes?|analysis only)/i.test(text);
911
+ }
912
+
913
+ function isWriteLikeAction(action) {
914
+ return new Set([
915
+ 'plan',
916
+ 'apply_patch',
917
+ 'write_file',
918
+ 'create_folder',
919
+ 'delete_file',
920
+ 'clipboard_write',
921
+ 'system_automation',
922
+ 'mouse_move',
923
+ 'mouse_click',
924
+ 'type_text',
925
+ 'key_tap'
926
+ ]).has(action);
927
+ }
928
+
929
+ function validateEditExplanation(action, input = {}, thought = '') {
930
+ const targetPath = getEditTargetPath(action, input);
931
+ if (!targetPath) return { ok: true };
932
+
933
+ const text = String(thought || '').toLowerCase();
934
+ const normalized = normalizeRelativePathForWarning(targetPath).toLowerCase();
935
+ const basename = path.basename(normalized).toLowerCase();
936
+ const mentionsTarget = text.includes(normalized) || (basename && text.includes(basename));
937
+ const explainsWhy = /(because|why|so that|in order|to update|to change|to edit|เพื่อ|เพราะ|เนื่องจาก|จะปรับ|จะแก้|อัปเดต|แก้ไข)/i.test(thought || '');
938
+ if (!mentionsTarget || !explainsWhy) {
939
+ return {
940
+ ok: false,
941
+ message: `Before editing ${targetPath}, explain in the thought field which file you will edit and why this is the correct target.`
942
+ };
943
+ }
944
+
945
+ const sensitiveScratchPath = normalized.startsWith('scratch/') ||
946
+ normalized.startsWith('tests/fixtures/') ||
947
+ normalized.includes('/tests/fixtures/');
948
+ const mentionsSensitiveLocation = /(scratch|fixture|test fixture|tests\/fixtures|ทดลอง|fixture)/i.test(thought || '');
949
+ const marksIntentional = /(intentional|intentionally|disposable|test content|test fixture|ตั้งใจ|ชั่วคราว|เนื้อหาทดลอง|ไฟล์ทดสอบ)/i.test(thought || '');
950
+ if (sensitiveScratchPath && !(mentionsSensitiveLocation && marksIntentional)) {
951
+ return {
952
+ ok: false,
953
+ message: `Before editing ${targetPath}, explicitly mention that it is under scratch/ or tests/fixtures/ and why editing that disposable/test fixture content is intentional.`
954
+ };
955
+ }
956
+
957
+ return { ok: true };
958
+ }
959
+
609
960
  function applyHunksToContent(content, hunks, filePath) {
610
961
  let nextContent = content;
611
962
  hunks.forEach((hunk, index) => {
@@ -647,125 +998,6 @@ function writeFile(workspaceRoot, targetPath, content) {
647
998
  return `Wrote ${targetPath}`;
648
999
  }
649
1000
 
650
- class UnifiedAgentClient {
651
- constructor(provider, config, providerOrder = [provider]) {
652
- this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
653
- this.providerOrder = providerOrder.length > 0 ? providerOrder : [this.provider];
654
- this.config = config;
655
- this.history = [];
656
- this.systemInstruction = CODE_AGENT_PROMPT;
657
- this.lastSuccessfulProvider = null;
658
- }
659
-
660
- async sendMessage(observation) {
661
- this.history.push({ role: 'user', content: observation });
662
-
663
- const failures = [];
664
- for (const provider of this.providerOrder) {
665
- this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
666
- try {
667
- let responseText = '';
668
- if (this.provider === 'anthropic') {
669
- responseText = await this._callAnthropic();
670
- } else if (this.provider === 'openai' || this.provider === 'local_openai') {
671
- responseText = await this._callOpenAI();
672
- } else {
673
- responseText = await this._callGemini();
674
- }
675
-
676
- this.history.push({ role: 'assistant', content: responseText });
677
- this.lastSuccessfulProvider = this.provider;
678
- return responseText;
679
- } catch (error) {
680
- const message = error.message || error.code || 'unknown error';
681
- failures.push(`${this.provider}: ${message}`);
682
- if (process.env.MINT_DEBUG === '1') {
683
- console.error(`[Code Agent Fallback] Provider '${this.provider}' failed: ${message}`);
684
- }
685
- }
686
- }
687
-
688
- throw new Error(`All code agent providers failed. ${failures.join(' | ')}`);
689
- }
690
-
691
- async _callAnthropic() {
692
- const apiKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
693
- const messages = this.history.map(m => ({
694
- role: m.role,
695
- content: contentToAnthropicContent(m.content)
696
- }));
697
-
698
- const response = await axios.post('https://api.anthropic.com/v1/messages', {
699
- model: this.config.anthropicModel || 'claude-3-5-sonnet-latest',
700
- max_tokens: 8192,
701
- system: this.systemInstruction,
702
- messages: messages
703
- }, {
704
- headers: {
705
- 'x-api-key': apiKey,
706
- 'anthropic-version': '2023-06-01',
707
- 'content-type': 'application/json'
708
- }
709
- });
710
- return response.data.content[0].text;
711
- }
712
-
713
- async _callOpenAI() {
714
- const isLocal = this.provider === 'local_openai';
715
- const apiKey = isLocal ? 'not-needed' : (this.config.openaiApiKey || process.env.OPENAI_API_KEY);
716
- const baseUrl = isLocal ? (this.config.localApiBaseUrl || 'http://localhost:1234/v1') : 'https://api.openai.com/v1';
717
- const model = isLocal ? (this.config.localModelName || 'local-model') : (this.config.openaiModel || 'gpt-4o');
718
-
719
- const messages = [
720
- { role: 'system', content: this.systemInstruction },
721
- ...this.history.map(m => ({
722
- role: m.role,
723
- content: contentToOpenAIContent(m.content)
724
- }))
725
- ];
726
-
727
- const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
728
- model: model,
729
- messages: messages,
730
- response_format: isLocal ? undefined : { type: "json_object" }
731
- }, {
732
- headers: {
733
- 'Authorization': `Bearer ${apiKey}`,
734
- 'Content-Type': 'application/json'
735
- }
736
- });
737
- return response.data.choices[0].message.content;
738
- }
739
-
740
- async _callGemini() {
741
- const apiKey = this.config.apiKey || process.env.GEMINI_API_KEY;
742
- const model = this.config.geminiModel || DEFAULT_GEMINI_MODEL;
743
- const ai = new GoogleGenAI({ apiKey });
744
-
745
- const recentHistory = this.history.slice(-16);
746
- const priorHistory = recentHistory.slice(0, -1);
747
- const lastEntry = recentHistory[recentHistory.length - 1] || { content: '' };
748
-
749
- // Convert history for Gemini, ensuring parts are correctly structured
750
- const geminiHistory = priorHistory.map(m => ({
751
- role: m.role === 'assistant' ? 'model' : 'user',
752
- parts: contentToGeminiParts(m.content)
753
- }));
754
-
755
- const chat = ai.chats.create({
756
- model,
757
- config: {
758
- systemInstruction: this.systemInstruction,
759
- responseMimeType: 'application/json'
760
- },
761
- history: geminiHistory
762
- });
763
-
764
- const response = await chat.sendMessage({ message: contentToGeminiParts(lastEntry.content) });
765
- return typeof response.text === 'function' ? response.text() : response.text;
766
- }
767
- }
768
-
769
1001
  async function getAgentDecision(client, observation, options = {}) {
770
1002
  const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
771
1003
  const step = options.step || 0;
@@ -877,7 +1109,14 @@ async function executeCodeTask(task, options = {}) {
877
1109
  const availableProviders = getAvailableProviders(config);
878
1110
  const providerOrder = getSupportedCodeProviderOrder(config, availableProviders, options.provider);
879
1111
  const provider = providerOrder[0];
880
- const client = new UnifiedAgentClient(provider, config, providerOrder);
1112
+ const client = new providerAdapter.AgentProviderClient({
1113
+ provider,
1114
+ config,
1115
+ providerOrder,
1116
+ systemInstruction: CODE_AGENT_PROMPT,
1117
+ responseMimeType: 'application/json',
1118
+ maxTokens: 8192
1119
+ });
881
1120
 
882
1121
  const initialObservationText = await buildInitialObservation(task, workspaceRoot, history);
883
1122
  const relevantMemoryCount = memoryStore.searchInteractions(task, 5).length;
@@ -902,6 +1141,25 @@ async function executeCodeTask(task, options = {}) {
902
1141
  let finalVerification = '';
903
1142
  let finalSessionSummary = '';
904
1143
  let executedSteps = 0;
1144
+ const readOnlyTask = isReadOnlyTask(task);
1145
+ const editPlanState = {
1146
+ approved: false,
1147
+ touchedFiles: new Set(),
1148
+ expectedFiles: new Set()
1149
+ };
1150
+ let verificationAttempts = 0;
1151
+ const verificationBudget = Number.isFinite(options.verificationBudget)
1152
+ ? options.verificationBudget
1153
+ : DEFAULT_VERIFICATION_BUDGET;
1154
+
1155
+ if (options.taskId) {
1156
+ taskManager.addCheckpoint(options.taskId, {
1157
+ phase: 'code_agent_start',
1158
+ message: task,
1159
+ provider,
1160
+ providerOrder
1161
+ });
1162
+ }
905
1163
 
906
1164
  for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
907
1165
  executedSteps = step;
@@ -921,28 +1179,24 @@ async function executeCodeTask(task, options = {}) {
921
1179
  continue;
922
1180
  }
923
1181
 
924
- // Immediately show the agent's thought/reasoning
925
- onProgress({
926
- step,
927
- phase: 'acting',
928
- action: 'thinking',
929
- thought: decision.thought
930
- });
931
-
932
1182
  if (action === 'finish') {
1183
+ const missingPlanFiles = getMissingPlanFiles(editPlanState);
1184
+ if (missingPlanFiles.length > 0) {
1185
+ observation = [
1186
+ `Previous thought: ${decision.thought || '(none)'}`,
1187
+ 'Action: finish',
1188
+ 'Observation:',
1189
+ [
1190
+ 'Error: Approved plan is not complete yet.',
1191
+ `Missing planned file edits: ${missingPlanFiles.join(', ')}`,
1192
+ 'Complete every file listed in the approved plan before finishing, or create a new plan if the scope changed.'
1193
+ ].join('\n')
1194
+ ].join('\n');
1195
+ continue;
1196
+ }
933
1197
  finalSessionSummary = input.sessionSummary || input.summary || task;
934
1198
  finalSummary = input.summary || 'Task complete.';
935
1199
  finalVerification = input.verification || 'Not specified.';
936
- if (onFinalSummary) {
937
- await onFinalSummary({
938
- summary: finalSummary,
939
- verification: finalVerification,
940
- providerInfo: {
941
- provider: client.lastSuccessfulProvider || client.provider || provider,
942
- model: getCodeProviderModel(client.lastSuccessfulProvider || client.provider || provider, config)
943
- }
944
- });
945
- }
946
1200
  writeWorkspaceSession(workspaceRoot, {
947
1201
  summary: finalSessionSummary,
948
1202
  lastTask: task,
@@ -953,6 +1207,57 @@ async function executeCodeTask(task, options = {}) {
953
1207
 
954
1208
  let toolResult = '';
955
1209
  try {
1210
+ if (readOnlyTask && isWriteLikeAction(action)) {
1211
+ observation = [
1212
+ `Previous thought: ${decision.thought || '(none)'}`,
1213
+ `Action: ${action}`,
1214
+ 'Observation:',
1215
+ [
1216
+ 'Error: The latest user request is read-only and explicitly forbids edits or changes.',
1217
+ 'Do not create a plan or request approval for edits.',
1218
+ 'Use read_file/search_code/find_path as needed, then finish with an analysis summary.'
1219
+ ].join('\n')
1220
+ ].join('\n');
1221
+ continue;
1222
+ }
1223
+
1224
+ if (requiresMultiFilePlan(action, input, editPlanState)) {
1225
+ const nextPath = getEditTargetPath(action, input);
1226
+ observation = [
1227
+ `Previous thought: ${decision.thought || '(none)'}`,
1228
+ `Action: ${action}`,
1229
+ 'Observation:',
1230
+ [
1231
+ 'Error: Multi-file edit plan required before editing another file.',
1232
+ 'Use the "plan" action first with input.plan starting with "Plan:" bullets and input.files listing every file you expect to touch.',
1233
+ `Already edited: ${Array.from(editPlanState.touchedFiles).join(', ')}`,
1234
+ `Next requested file: ${nextPath}`
1235
+ ].join('\n')
1236
+ ].join('\n');
1237
+ continue;
1238
+ }
1239
+
1240
+ if (action === 'apply_patch' || action === 'write_file') {
1241
+ const explanation = validateEditExplanation(action, input, decision.thought);
1242
+ if (!explanation.ok) {
1243
+ observation = [
1244
+ `Previous thought: ${decision.thought || '(none)'}`,
1245
+ `Action: ${action}`,
1246
+ 'Observation:',
1247
+ `Error: ${explanation.message}`
1248
+ ].join('\n');
1249
+ continue;
1250
+ }
1251
+ }
1252
+
1253
+ // Show progress only after the action passes local validation, so retry attempts do not spam near-duplicate notes.
1254
+ onProgress({
1255
+ step,
1256
+ phase: 'acting',
1257
+ action: 'thinking',
1258
+ thought: decision.thought
1259
+ });
1260
+
956
1261
  switch (action) {
957
1262
  case 'web_search':
958
1263
  toolResult = await webSearch(input.query, onProgress);
@@ -964,7 +1269,7 @@ async function executeCodeTask(task, options = {}) {
964
1269
  toolResult = readFileRange(workspaceRoot, input.path, input.startLine, input.endLine);
965
1270
  break;
966
1271
  case 'search_code':
967
- toolResult = await searchCode(workspaceRoot, input.query);
1272
+ toolResult = await searchCode(workspaceRoot, input.query, input.path || '.');
968
1273
  break;
969
1274
  case 'find_path':
970
1275
  toolResult = await findPaths(workspaceRoot, input.query, input.type);
@@ -995,12 +1300,85 @@ async function executeCodeTask(task, options = {}) {
995
1300
  toolResult = await runShell(workspaceRoot, input.command);
996
1301
  break;
997
1302
  }
1303
+ case 'verify': {
1304
+ verificationAttempts += 1;
1305
+ const result = await runVerificationCommands(workspaceRoot, input.commands, {
1306
+ requestApproval,
1307
+ budget: verificationBudget,
1308
+ attempt: verificationAttempts
1309
+ });
1310
+ toolResult = result.output;
1311
+ if (options.taskId) {
1312
+ taskManager.addCheckpoint(options.taskId, {
1313
+ phase: 'verification',
1314
+ attempt: verificationAttempts,
1315
+ passed: result.passed,
1316
+ output: truncate(result.output, 4000)
1317
+ });
1318
+ }
1319
+ if (!result.passed && verificationAttempts >= verificationBudget) {
1320
+ toolResult += '\nVerification budget exhausted. Finish with the remaining failure clearly explained.';
1321
+ }
1322
+ break;
1323
+ }
1324
+ case 'plan': {
1325
+ const createdAt = new Date().toISOString();
1326
+ let planFile = writePlanFile(workspaceRoot, input, { task, createdAt });
1327
+ const approved = await requestApproval({
1328
+ type: 'plan',
1329
+ label: PLAN_FILE_LABEL,
1330
+ preview: planFile.content,
1331
+ summary: formatPlanApprovalSummary(input),
1332
+ openPath: planFile.path
1333
+ });
1334
+ if (!approved) {
1335
+ planFile = updatePlanApprovalStatus(planFile, input, {
1336
+ task,
1337
+ createdAt,
1338
+ approvalStatus: 'Denied',
1339
+ approvalTime: new Date().toISOString()
1340
+ });
1341
+ toolResult = 'User denied multi-file plan.';
1342
+ break;
1343
+ }
1344
+ planFile = updatePlanApprovalStatus(planFile, input, {
1345
+ task,
1346
+ createdAt,
1347
+ approvalStatus: 'Approved',
1348
+ approvalTime: new Date().toISOString()
1349
+ });
1350
+ editPlanState.approved = true;
1351
+ editPlanState.expectedFiles = new Set(
1352
+ Array.isArray(input.files)
1353
+ ? input.files.map(file => String(file || '').trim()).filter(Boolean)
1354
+ : []
1355
+ );
1356
+ safetyManager.appendActionLog({
1357
+ source: 'code_agent',
1358
+ action: 'plan',
1359
+ path: planFile.path,
1360
+ preview: planFile.content,
1361
+ approved
1362
+ });
1363
+ toolResult = `User approved multi-file plan at ${PLAN_FILE_LABEL}:\n${planFile.content}`;
1364
+ break;
1365
+ }
998
1366
  case 'apply_patch': {
999
1367
  const patchInput = input.patch || {};
1368
+ let patchWarnings = [];
1369
+ try {
1370
+ patchWarnings = buildApprovalWarnings(
1371
+ patchInput.path,
1372
+ buildPatchedContent(workspaceRoot, patchInput).nextContent
1373
+ );
1374
+ } catch (_) {
1375
+ patchWarnings = buildApprovalWarnings(patchInput.path, '');
1376
+ }
1000
1377
  const approved = await requestApproval({
1001
1378
  type: 'patch',
1002
1379
  label: patchInput.path,
1003
- preview: formatPatchPreview(workspaceRoot, patchInput)
1380
+ preview: formatPatchPreview(workspaceRoot, patchInput),
1381
+ warnings: patchWarnings
1004
1382
  });
1005
1383
  if (!approved) {
1006
1384
  toolResult = `User denied patch for ${patchInput.path}`;
@@ -1013,13 +1391,15 @@ async function executeCodeTask(task, options = {}) {
1013
1391
  approved
1014
1392
  });
1015
1393
  toolResult = applyPatch(workspaceRoot, patchInput);
1394
+ editPlanState.touchedFiles.add(patchInput.path);
1016
1395
  break;
1017
1396
  }
1018
1397
  case 'write_file': {
1019
1398
  const approved = await requestApproval({
1020
1399
  type: 'write_file',
1021
1400
  label: input.path,
1022
- preview: `${input.path}\n${truncate(input.content || '', 800)}`
1401
+ preview: formatWritePreview(workspaceRoot, input.path, input.content),
1402
+ warnings: buildApprovalWarnings(input.path, input.content)
1023
1403
  });
1024
1404
  if (!approved) {
1025
1405
  toolResult = `User denied full file write for ${input.path}`;
@@ -1032,6 +1412,7 @@ async function executeCodeTask(task, options = {}) {
1032
1412
  approved
1033
1413
  });
1034
1414
  toolResult = writeFile(workspaceRoot, input.path, input.content);
1415
+ editPlanState.touchedFiles.add(input.path);
1035
1416
  break;
1036
1417
  }
1037
1418
  case 'ask_user': {
@@ -1040,11 +1421,23 @@ async function executeCodeTask(task, options = {}) {
1040
1421
  break;
1041
1422
  }
1042
1423
  case 'open_url':
1424
+ case 'search':
1043
1425
  case 'open_app':
1426
+ case 'web_automation':
1044
1427
  case 'open_file':
1045
1428
  case 'open_folder':
1046
1429
  case 'create_folder':
1430
+ case 'delete_file':
1431
+ case 'clipboard_write':
1432
+ case 'learn_file':
1433
+ case 'learn_folder':
1047
1434
  case 'system_info':
1435
+ case 'plugin':
1436
+ case 'mcp_tool':
1437
+ case 'mouse_move':
1438
+ case 'mouse_click':
1439
+ case 'type_text':
1440
+ case 'key_tap':
1048
1441
  case 'system_automation': {
1049
1442
  const executorAction = normalizeExecutorAction(action, input);
1050
1443
  const safety = safetyManager.classifyAction(executorAction);
@@ -1077,6 +1470,7 @@ async function executeCodeTask(task, options = {}) {
1077
1470
  }
1078
1471
 
1079
1472
  const evaluation = evaluateActionResult(action, toolResult);
1473
+ const toolStatus = getToolCallStatus(action, toolResult, evaluation);
1080
1474
  if (evaluation) {
1081
1475
  onProgress({
1082
1476
  step,
@@ -1092,6 +1486,14 @@ async function executeCodeTask(task, options = {}) {
1092
1486
  ].join('\n');
1093
1487
  }
1094
1488
 
1489
+ onProgress({
1490
+ step,
1491
+ phase: 'tool_call',
1492
+ action,
1493
+ status: toolStatus,
1494
+ target: summarizeToolTarget(action, input)
1495
+ });
1496
+
1095
1497
  // Log the finished step with result
1096
1498
  let resultSummary = '';
1097
1499
  if (action === 'search_code') {
@@ -1105,7 +1507,7 @@ async function executeCodeTask(task, options = {}) {
1105
1507
  step,
1106
1508
  phase: 'finished',
1107
1509
  action,
1108
- target: (input.path || input.command || input.query || '') + resultSummary
1510
+ target: summarizeToolTarget(action, input) + resultSummary
1109
1511
  });
1110
1512
 
1111
1513
  // Format tool result to be more readable and structured for the agent
@@ -1122,7 +1524,7 @@ async function executeCodeTask(task, options = {}) {
1122
1524
  ].join('\n'); }
1123
1525
 
1124
1526
  // Check for Agent Collaboration (Review) - Disabled by default to save tokens
1125
- if (config.enableAgentCollaboration === true && executedSteps > 8 && finalSummary) {
1527
+ if (config.enableAgentCollaboration === true && !readOnlyTask && executedSteps > 8 && finalSummary) {
1126
1528
  const availableProviders = getAvailableProviders(config);
1127
1529
  // Exclude providers that often need special local setup or are slow/unreliable for tiny reviews
1128
1530
  const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface' && p !== 'local_openai');
@@ -1135,7 +1537,14 @@ async function executeCodeTask(task, options = {}) {
1135
1537
  if (reviewerProvider && finalSummary) {
1136
1538
  onProgress({ phase: 'reviewing', action: 'reviewer_start', message: `Invoking Reviewer Agent (${reviewerProvider})...` });
1137
1539
 
1138
- const reviewerClient = new UnifiedAgentClient(reviewerProvider, config);
1540
+ const reviewerClient = new providerAdapter.AgentProviderClient({
1541
+ provider: reviewerProvider,
1542
+ config,
1543
+ providerOrder: [reviewerProvider],
1544
+ systemInstruction: CODE_AGENT_PROMPT,
1545
+ responseMimeType: 'application/json',
1546
+ maxTokens: 4096
1547
+ });
1139
1548
  reviewerClient.systemInstruction = CODE_AGENT_PROMPT + "\n\nYou are the Reviewer Agent. Review the primary agent's changes, test output, and verification. If you spot a critical bug, point it out. Otherwise, confirm it looks good. Return JSON with action: 'finish' and your review in the 'summary' field.";
1140
1549
 
1141
1550
  const reviewPrompt = `The primary agent (${provider}) just completed the task: "${task}".\nSummary: ${finalSummary}\nVerification: ${finalVerification}\nGit Status: ${(await getGitContext(workspaceRoot)).status}\n\nPlease review this. Return JSON with action: 'finish'.`;
@@ -1155,15 +1564,20 @@ async function executeCodeTask(task, options = {}) {
1155
1564
  if (finalSummary) {
1156
1565
  memoryStore.recordInteraction(task, finalSummary);
1157
1566
  const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
1158
- return {
1567
+ const result = {
1159
1568
  summary: finalSummary,
1160
1569
  verification: finalVerification,
1161
1570
  steps: executedSteps,
1162
1571
  providerInfo: {
1163
1572
  provider: answeredProvider,
1164
- model: getCodeProviderModel(answeredProvider, config)
1573
+ model: getCodeProviderModel(answeredProvider, config),
1574
+ usage: client.getUsageSummary()
1165
1575
  }
1166
1576
  };
1577
+ if (onFinalSummary) {
1578
+ await onFinalSummary(result);
1579
+ }
1580
+ return result;
1167
1581
  }
1168
1582
 
1169
1583
  writeWorkspaceSession(workspaceRoot, {
@@ -1179,7 +1593,8 @@ async function executeCodeTask(task, options = {}) {
1179
1593
  steps: executedSteps || MAX_AGENT_STEPS,
1180
1594
  providerInfo: {
1181
1595
  provider: answeredProvider,
1182
- model: getCodeProviderModel(answeredProvider, config)
1596
+ model: getCodeProviderModel(answeredProvider, config),
1597
+ usage: client.getUsageSummary()
1183
1598
  }
1184
1599
  };
1185
1600
  }
@@ -1193,8 +1608,26 @@ module.exports = {
1193
1608
  findPaths,
1194
1609
  listFiles,
1195
1610
  searchCode,
1611
+ runVerificationCommands,
1196
1612
  walkDirectory,
1197
1613
  buildUnifiedDiffPreview,
1198
- formatPatchPreview
1614
+ buildFullFileDiffPreview,
1615
+ buildApprovalWarnings,
1616
+ validateEditExplanation,
1617
+ formatPatchPreview,
1618
+ formatWritePreview,
1619
+ formatPlanPreview,
1620
+ formatPlanApprovalSummary,
1621
+ formatPlanMarkdown,
1622
+ writePlanFile,
1623
+ updatePlanApprovalStatus,
1624
+ normalizePlanItems,
1625
+ normalizePlanItemLanguage,
1626
+ requiresMultiFilePlan,
1627
+ getMissingPlanFiles,
1628
+ isReadOnlyTask,
1629
+ isWriteLikeAction,
1630
+ getEditTargetPath,
1631
+ PLAN_FILE_PATH
1199
1632
  }
1200
1633
  };