vigthoria-cli 1.6.25 → 1.6.26
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.
- package/dist/commands/chat.js +60 -32
- package/dist/commands/edit.d.ts +5 -0
- package/dist/commands/edit.js +74 -14
- package/dist/index.js +1 -1
- package/dist/utils/api.d.ts +6 -0
- package/dist/utils/api.js +99 -22
- package/package.json +1 -1
package/dist/commands/chat.js
CHANGED
|
@@ -422,11 +422,19 @@ class ChatCommand {
|
|
|
422
422
|
}
|
|
423
423
|
if (options.prompt) {
|
|
424
424
|
await this.handleDirectPrompt(options.prompt);
|
|
425
|
-
(0, bridge_client_js_1.getBridgeClient)()
|
|
425
|
+
const bridge = (0, bridge_client_js_1.getBridgeClient)();
|
|
426
|
+
if (bridge) {
|
|
427
|
+
bridge.emitEnd({ reason: 'prompt-complete' });
|
|
428
|
+
bridge.destroy();
|
|
429
|
+
}
|
|
426
430
|
return;
|
|
427
431
|
}
|
|
428
432
|
await this.startInteractiveChat();
|
|
429
|
-
(0, bridge_client_js_1.getBridgeClient)()
|
|
433
|
+
const bridge = (0, bridge_client_js_1.getBridgeClient)();
|
|
434
|
+
if (bridge) {
|
|
435
|
+
bridge.emitEnd({ reason: 'interactive-exit' });
|
|
436
|
+
bridge.destroy();
|
|
437
|
+
}
|
|
430
438
|
}
|
|
431
439
|
/** Handle an inbound admin command from the Commando Bridge. */
|
|
432
440
|
handleAdminCommand(cmd) {
|
|
@@ -584,16 +592,23 @@ class ChatCommand {
|
|
|
584
592
|
*/
|
|
585
593
|
isSimpleDirectPrompt(prompt) {
|
|
586
594
|
const trimmed = prompt.trim();
|
|
587
|
-
// Short prompts (≤ 12 words) that don't reference files, builds, or code tasks
|
|
588
595
|
const wordCount = trimmed.split(/\s+/).length;
|
|
589
|
-
|
|
596
|
+
// Never treat prompts that reference files, dirs, or workspace as simple
|
|
597
|
+
// — these need agent tool access (list_dir, read_file, etc.)
|
|
598
|
+
if (/(file|folder|director|code|project|workspace|repo|module|component|function|class|api|endpoint|route|database|schema|migration|docker|deploy|build|test)\b/i.test(trimmed)) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
// Never treat tool-action verbs as simple
|
|
602
|
+
if (/(list|find|search|show|check|scan|read|inspect|analyze|analyse|audit|review|count|create|build|generate|implement|write.*function|add.*feature|refactor|fix|edit|modify|update|delete|remove)\b/i.test(trimmed)) {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
// Short conversational prompts (≤ 15 words) with no file/tool context
|
|
606
|
+
if (wordCount <= 15) {
|
|
590
607
|
return true;
|
|
591
608
|
}
|
|
592
|
-
// Conversational
|
|
593
|
-
if (/^(what|who|when|where|why|how|is|are|do|does|can|could|would|should|tell|explain|
|
|
594
|
-
|
|
595
|
-
return true;
|
|
596
|
-
}
|
|
609
|
+
// Conversational Q&A starters
|
|
610
|
+
if (/^(what|who|when|where|why|how|is|are|do|does|can|could|would|should|tell|explain|help|reply|say|respond|answer|translate|summarize|define)\b/i.test(trimmed)) {
|
|
611
|
+
return true;
|
|
597
612
|
}
|
|
598
613
|
return false;
|
|
599
614
|
}
|
|
@@ -611,9 +626,16 @@ class ChatCommand {
|
|
|
611
626
|
await this.runWorkflowTargetPrompt(prompt);
|
|
612
627
|
return;
|
|
613
628
|
}
|
|
614
|
-
// Smart routing:
|
|
615
|
-
if (this.agentMode
|
|
616
|
-
|
|
629
|
+
// Smart routing: for agent mode, determine if prompt needs tool access
|
|
630
|
+
if (this.agentMode) {
|
|
631
|
+
if (this.isSimpleDirectPrompt(prompt)) {
|
|
632
|
+
// Simple prompt: downgrade to plain chat model, no agent system prompt
|
|
633
|
+
this.currentModel = this.getDefaultChatModel();
|
|
634
|
+
await this.runSimplePrompt(prompt);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
await this.runAgentTurn(prompt);
|
|
638
|
+
}
|
|
617
639
|
return;
|
|
618
640
|
}
|
|
619
641
|
if (this.operatorMode) {
|
|
@@ -822,30 +844,36 @@ class ChatCommand {
|
|
|
822
844
|
}
|
|
823
845
|
async runSimplePrompt(prompt) {
|
|
824
846
|
this.lastActionableUserInput = prompt;
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
|
|
828
|
-
// paths instead of guessing.
|
|
829
|
-
const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
|
|
830
|
-
if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
|
|
831
|
-
let groundingContent = 'You are in simple chat mode with no direct file access or tools. Do not fabricate file contents, search results, or analysis steps. If the user asks you to modify, edit, or deeply inspect files, advise them to use agent mode (vig chat --agent) for grounded repo analysis.';
|
|
832
|
-
// Inject a workspace file listing so the model has real paths
|
|
833
|
-
try {
|
|
834
|
-
const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
|
|
835
|
-
if (snapshot && snapshot.paths.length > 0) {
|
|
836
|
-
const listing = snapshot.paths.slice(0, 60).join('\n');
|
|
837
|
-
groundingContent += `\n\nWorkspace: ${this.currentProjectPath}\nFile listing (${snapshot.fileCount} files total):\n${listing}`;
|
|
838
|
-
if (snapshot.fileCount > 60) {
|
|
839
|
-
groundingContent += `\n... and ${snapshot.fileCount - 60} more files.`;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
catch { /* ignore snapshot errors */ }
|
|
847
|
+
// For direct --prompt mode with simple prompts, use a minimal system
|
|
848
|
+
// message to avoid polluting the response with tool/platform context.
|
|
849
|
+
if (this.directPromptMode && !this.messages.some(m => m.role === 'system')) {
|
|
844
850
|
this.messages.push({
|
|
845
851
|
role: 'system',
|
|
846
|
-
content:
|
|
852
|
+
content: 'Answer the user\'s question directly and concisely. Do not describe tools, platform constraints, or capabilities unless explicitly asked. If the user\'s instruction is to produce specific output, produce exactly that output with no preamble.',
|
|
847
853
|
});
|
|
848
854
|
}
|
|
855
|
+
else if (!this.directPromptMode) {
|
|
856
|
+
// Interactive mode: inject grounding for file-related queries
|
|
857
|
+
const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
|
|
858
|
+
if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
|
|
859
|
+
let groundingContent = 'You are in simple chat mode with no direct file access or tools. Do not fabricate file contents, search results, or analysis steps. If the user asks you to modify, edit, or deeply inspect files, advise them to use agent mode (vig chat --agent) for grounded repo analysis.';
|
|
860
|
+
try {
|
|
861
|
+
const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
|
|
862
|
+
if (snapshot && snapshot.paths.length > 0) {
|
|
863
|
+
const listing = snapshot.paths.slice(0, 60).join('\n');
|
|
864
|
+
groundingContent += `\n\nWorkspace: ${this.currentProjectPath}\nFile listing (${snapshot.fileCount} files total):\n${listing}`;
|
|
865
|
+
if (snapshot.fileCount > 60) {
|
|
866
|
+
groundingContent += `\n... and ${snapshot.fileCount - 60} more files.`;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
catch { /* ignore snapshot errors */ }
|
|
871
|
+
this.messages.push({
|
|
872
|
+
role: 'system',
|
|
873
|
+
content: groundingContent,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
849
877
|
this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
|
|
850
878
|
(0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: 'chat', model: this.currentModel });
|
|
851
879
|
const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking...', spinner: 'clock' }).start();
|
package/dist/commands/edit.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export declare class EditCommand {
|
|
|
21
21
|
run(filePath: string, options: EditOptions): Promise<void>;
|
|
22
22
|
fix(filePath: string, options: FixOptions): Promise<void>;
|
|
23
23
|
private extractCode;
|
|
24
|
+
/**
|
|
25
|
+
* Detect and remove duplicated content in model output.
|
|
26
|
+
* Models sometimes output the code twice (original + modified).
|
|
27
|
+
*/
|
|
28
|
+
private deduplicateCode;
|
|
24
29
|
private showDiffAndConfirm;
|
|
25
30
|
private showFullDiff;
|
|
26
31
|
private applyFix;
|
package/dist/commands/edit.js
CHANGED
|
@@ -174,10 +174,37 @@ Return the complete modified file content:`,
|
|
|
174
174
|
// If no code block, check if response looks like code
|
|
175
175
|
const trimmed = response.trim();
|
|
176
176
|
if (!trimmed.startsWith('```') && !trimmed.includes('Here') && !trimmed.includes('I ')) {
|
|
177
|
-
return trimmed;
|
|
177
|
+
return this.deduplicateCode(trimmed);
|
|
178
178
|
}
|
|
179
179
|
return null;
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Detect and remove duplicated content in model output.
|
|
183
|
+
* Models sometimes output the code twice (original + modified).
|
|
184
|
+
*/
|
|
185
|
+
deduplicateCode(code) {
|
|
186
|
+
const lines = code.split('\n');
|
|
187
|
+
const len = lines.length;
|
|
188
|
+
if (len < 4)
|
|
189
|
+
return code;
|
|
190
|
+
// Check if the second half is a near-duplicate of the first half
|
|
191
|
+
for (let splitAt = Math.floor(len * 0.4); splitAt <= Math.ceil(len * 0.6); splitAt++) {
|
|
192
|
+
const firstHalf = lines.slice(0, splitAt);
|
|
193
|
+
const secondHalf = lines.slice(splitAt).filter(l => l.trim() !== '');
|
|
194
|
+
if (secondHalf.length < 2)
|
|
195
|
+
continue;
|
|
196
|
+
let matches = 0;
|
|
197
|
+
for (const line of secondHalf) {
|
|
198
|
+
if (firstHalf.some(fl => fl.trim() === line.trim()))
|
|
199
|
+
matches++;
|
|
200
|
+
}
|
|
201
|
+
// If >70% of second half matches first half, it's a duplicate — keep second half
|
|
202
|
+
if (matches / secondHalf.length > 0.7) {
|
|
203
|
+
return secondHalf.join('\n');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return code;
|
|
207
|
+
}
|
|
181
208
|
async showDiffAndConfirm(filePath, original, modified) {
|
|
182
209
|
const diff = this.fileUtils.createDiff(original, modified);
|
|
183
210
|
if (diff.added.length === 0 && diff.removed.length === 0) {
|
|
@@ -240,23 +267,56 @@ Return the complete modified file content:`,
|
|
|
240
267
|
const modifiedLines = modified.split('\n');
|
|
241
268
|
console.log();
|
|
242
269
|
console.log(chalk_1.default.gray('─'.repeat(60)));
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
else {
|
|
252
|
-
if (orig !== undefined) {
|
|
253
|
-
console.log(chalk_1.default.red(`${lineNum} - ${orig}`));
|
|
270
|
+
// Use LCS-based diff to avoid line-shift inflation
|
|
271
|
+
const m = originalLines.length;
|
|
272
|
+
const n = modifiedLines.length;
|
|
273
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
274
|
+
for (let i = 1; i <= m; i++) {
|
|
275
|
+
for (let j = 1; j <= n; j++) {
|
|
276
|
+
if (originalLines[i - 1] === modifiedLines[j - 1]) {
|
|
277
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
254
278
|
}
|
|
255
|
-
|
|
256
|
-
|
|
279
|
+
else {
|
|
280
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
257
281
|
}
|
|
258
282
|
}
|
|
259
283
|
}
|
|
284
|
+
// Backtrack to produce diff ops
|
|
285
|
+
const ops = [];
|
|
286
|
+
let i = m, j = n;
|
|
287
|
+
while (i > 0 || j > 0) {
|
|
288
|
+
if (i > 0 && j > 0 && originalLines[i - 1] === modifiedLines[j - 1]) {
|
|
289
|
+
ops.push({ type: 'keep', text: originalLines[i - 1], lineNum: i });
|
|
290
|
+
i--;
|
|
291
|
+
j--;
|
|
292
|
+
}
|
|
293
|
+
else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
294
|
+
ops.push({ type: 'add', text: modifiedLines[j - 1], lineNum: j });
|
|
295
|
+
j--;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
ops.push({ type: 'remove', text: originalLines[i - 1], lineNum: i });
|
|
299
|
+
i--;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
ops.reverse();
|
|
303
|
+
let displayLine = 0;
|
|
304
|
+
for (const op of ops) {
|
|
305
|
+
if (op.type === 'keep') {
|
|
306
|
+
displayLine++;
|
|
307
|
+
const lineNum = String(displayLine).padStart(4, ' ');
|
|
308
|
+
console.log(chalk_1.default.gray(`${lineNum} │ ${op.text || ''}`));
|
|
309
|
+
}
|
|
310
|
+
else if (op.type === 'remove') {
|
|
311
|
+
displayLine++;
|
|
312
|
+
const lineNum = String(displayLine).padStart(4, ' ');
|
|
313
|
+
console.log(chalk_1.default.red(`${lineNum} - ${op.text}`));
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
const lineNum = ' +';
|
|
317
|
+
console.log(chalk_1.default.green(`${lineNum} + ${op.text}`));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
260
320
|
console.log(chalk_1.default.gray('─'.repeat(60)));
|
|
261
321
|
console.log();
|
|
262
322
|
}
|
package/dist/index.js
CHANGED
package/dist/utils/api.d.ts
CHANGED
|
@@ -384,6 +384,12 @@ export declare class APIClient {
|
|
|
384
384
|
reason: string;
|
|
385
385
|
}[];
|
|
386
386
|
}>;
|
|
387
|
+
/**
|
|
388
|
+
* Compute a semantic diff between original and fixed code using
|
|
389
|
+
* Longest Common Subsequence (LCS) to avoid the line-shift inflation
|
|
390
|
+
* bug where inserting one line flags all subsequent lines as changed.
|
|
391
|
+
*/
|
|
392
|
+
private computeSemanticDiff;
|
|
387
393
|
/**
|
|
388
394
|
* Lightweight client-side syntax error detection.
|
|
389
395
|
* Returns a human-readable description of obvious errors, or empty string.
|
package/dist/utils/api.js
CHANGED
|
@@ -3320,10 +3320,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3320
3320
|
depth--;
|
|
3321
3321
|
}
|
|
3322
3322
|
if (depth > 0) {
|
|
3323
|
-
|
|
3323
|
+
let result = code.trimEnd();
|
|
3324
3324
|
for (let i = 0; i < depth; i++) {
|
|
3325
|
-
|
|
3325
|
+
result += '\n}';
|
|
3326
3326
|
}
|
|
3327
|
+
code = result;
|
|
3327
3328
|
}
|
|
3328
3329
|
return code;
|
|
3329
3330
|
}
|
|
@@ -3570,15 +3571,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3570
3571
|
async fixCode(code, language, fixType) {
|
|
3571
3572
|
// Client-side syntax pre-check: detect obvious errors and include
|
|
3572
3573
|
// them in the request so the model has concrete signals.
|
|
3573
|
-
const syntaxHints = this.detectSyntaxErrors(code, language);
|
|
3574
|
-
//
|
|
3574
|
+
const syntaxHints = fixType === 'bugs' || fixType === 'syntax' ? this.detectSyntaxErrors(code, language) : '';
|
|
3575
|
+
// Run heuristic logic analysis, but ONLY pass hints relevant to the fixType
|
|
3575
3576
|
const heuristicIssues = this.heuristicCodeIssues(code, language);
|
|
3576
|
-
const
|
|
3577
|
+
const relevantIssues = heuristicIssues.filter(issue => {
|
|
3578
|
+
if (fixType === 'bugs' || fixType === 'logic')
|
|
3579
|
+
return issue.type === 'logic' || issue.severity === 'error';
|
|
3580
|
+
if (fixType === 'syntax')
|
|
3581
|
+
return issue.severity === 'error';
|
|
3582
|
+
if (fixType === 'style')
|
|
3583
|
+
return issue.type === 'style' || issue.type === 'quality';
|
|
3584
|
+
if (fixType === 'security')
|
|
3585
|
+
return issue.type === 'security';
|
|
3586
|
+
if (fixType === 'performance')
|
|
3587
|
+
return issue.type === 'performance';
|
|
3588
|
+
return true; // 'all' or unknown fixType: include everything
|
|
3589
|
+
});
|
|
3590
|
+
const logicHints = relevantIssues
|
|
3577
3591
|
.map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
|
|
3578
3592
|
.join('\n// ');
|
|
3579
3593
|
const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
|
|
3580
3594
|
const augmentedCode = allHints
|
|
3581
|
-
? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n\n${code}`
|
|
3595
|
+
? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n// IMPORTANT: Fix ONLY these specific bugs. Do not add comments, do not restructure the code, do not add or remove lines beyond the minimal fix.\n\n${code}`
|
|
3582
3596
|
: code;
|
|
3583
3597
|
const response = await this.client.post('/api/ai/fix', {
|
|
3584
3598
|
code: augmentedCode,
|
|
@@ -3602,29 +3616,92 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3602
3616
|
}
|
|
3603
3617
|
}
|
|
3604
3618
|
// If there are still no changes but the fixed code differs, compute
|
|
3605
|
-
// a
|
|
3619
|
+
// a semantic diff using LCS so inserted/removed lines don't cause
|
|
3620
|
+
// every subsequent line to appear as changed.
|
|
3606
3621
|
if (changes.length === 0 && fixed !== code) {
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3622
|
+
changes = this.computeSemanticDiff(code, fixed, allHints);
|
|
3623
|
+
}
|
|
3624
|
+
return { fixed, changes };
|
|
3625
|
+
}
|
|
3626
|
+
/**
|
|
3627
|
+
* Compute a semantic diff between original and fixed code using
|
|
3628
|
+
* Longest Common Subsequence (LCS) to avoid the line-shift inflation
|
|
3629
|
+
* bug where inserting one line flags all subsequent lines as changed.
|
|
3630
|
+
*/
|
|
3631
|
+
computeSemanticDiff(original, fixed, reason) {
|
|
3632
|
+
const origLines = original.split('\n');
|
|
3633
|
+
const fixedLines = fixed.split('\n');
|
|
3634
|
+
const changes = [];
|
|
3635
|
+
// Build LCS table
|
|
3636
|
+
const m = origLines.length;
|
|
3637
|
+
const n = fixedLines.length;
|
|
3638
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
3639
|
+
for (let i = 1; i <= m; i++) {
|
|
3640
|
+
for (let j = 1; j <= n; j++) {
|
|
3641
|
+
if (origLines[i - 1] === fixedLines[j - 1]) {
|
|
3642
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
3643
|
+
}
|
|
3644
|
+
else {
|
|
3645
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
// Backtrack to find the diff
|
|
3650
|
+
let i = m, j = n;
|
|
3651
|
+
const ops = [];
|
|
3652
|
+
while (i > 0 || j > 0) {
|
|
3653
|
+
if (i > 0 && j > 0 && origLines[i - 1] === fixedLines[j - 1]) {
|
|
3654
|
+
ops.push({ type: 'keep', origLine: i, fixedLine: j, text: origLines[i - 1] });
|
|
3655
|
+
i--;
|
|
3656
|
+
j--;
|
|
3657
|
+
}
|
|
3658
|
+
else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
3659
|
+
ops.push({ type: 'add', fixedLine: j, text: fixedLines[j - 1] });
|
|
3660
|
+
j--;
|
|
3661
|
+
}
|
|
3662
|
+
else {
|
|
3663
|
+
ops.push({ type: 'remove', origLine: i, text: origLines[i - 1] });
|
|
3664
|
+
i--;
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
ops.reverse();
|
|
3668
|
+
// Merge adjacent remove+add pairs into single change entries
|
|
3669
|
+
let idx = 0;
|
|
3670
|
+
while (idx < ops.length) {
|
|
3671
|
+
const op = ops[idx];
|
|
3672
|
+
if (op.type === 'remove') {
|
|
3673
|
+
// Look ahead for a matching 'add' immediately after
|
|
3674
|
+
if (idx + 1 < ops.length && ops[idx + 1].type === 'add') {
|
|
3614
3675
|
changes.push({
|
|
3615
|
-
line:
|
|
3616
|
-
before:
|
|
3617
|
-
after:
|
|
3618
|
-
reason:
|
|
3676
|
+
line: op.origLine,
|
|
3677
|
+
before: op.text,
|
|
3678
|
+
after: ops[idx + 1].text,
|
|
3679
|
+
reason: reason || 'AI-suggested fix',
|
|
3619
3680
|
});
|
|
3681
|
+
idx += 2;
|
|
3682
|
+
continue;
|
|
3620
3683
|
}
|
|
3684
|
+
changes.push({
|
|
3685
|
+
line: op.origLine,
|
|
3686
|
+
before: op.text,
|
|
3687
|
+
after: '(line removed)',
|
|
3688
|
+
reason: reason || 'AI-suggested fix',
|
|
3689
|
+
});
|
|
3621
3690
|
}
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3691
|
+
else if (op.type === 'add') {
|
|
3692
|
+
changes.push({
|
|
3693
|
+
line: op.fixedLine,
|
|
3694
|
+
before: '(new line)',
|
|
3695
|
+
after: op.text,
|
|
3696
|
+
reason: reason || 'AI-suggested fix',
|
|
3697
|
+
});
|
|
3625
3698
|
}
|
|
3699
|
+
idx++;
|
|
3626
3700
|
}
|
|
3627
|
-
|
|
3701
|
+
if (changes.length === 0) {
|
|
3702
|
+
changes.push({ line: 1, before: '(whitespace changes)', after: '(see fixed file)', reason: reason || 'AI-suggested fix' });
|
|
3703
|
+
}
|
|
3704
|
+
return changes;
|
|
3628
3705
|
}
|
|
3629
3706
|
/**
|
|
3630
3707
|
* Lightweight client-side syntax error detection.
|