vigthoria-cli 1.6.24 → 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.d.ts +7 -0
- package/dist/commands/chat.js +74 -24
- package/dist/commands/edit.d.ts +5 -0
- package/dist/commands/edit.js +74 -14
- package/dist/commands/generate.d.ts +5 -0
- package/dist/commands/generate.js +39 -0
- package/dist/index.js +2 -2
- package/dist/utils/api.d.ts +30 -0
- package/dist/utils/api.js +300 -29
- package/package.json +1 -1
package/dist/commands/chat.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export declare class ChatCommand {
|
|
|
75
75
|
private slugifyWorkspaceName;
|
|
76
76
|
private allocateManagedWorkspacePath;
|
|
77
77
|
private initializeSession;
|
|
78
|
+
/**
|
|
79
|
+
* Determine whether a direct --prompt call is simple enough to skip the
|
|
80
|
+
* full V3 Agent planner and answer directly via the simple chat pipeline.
|
|
81
|
+
* This prevents trivial prompts like "Reply with OK" from triggering a
|
|
82
|
+
* multi-step planning workflow with quality_profile: premium-visual-build.
|
|
83
|
+
*/
|
|
84
|
+
private isSimpleDirectPrompt;
|
|
78
85
|
private handleDirectPrompt;
|
|
79
86
|
private formatWorkflowTargetResult;
|
|
80
87
|
private runWorkflowTargetPrompt;
|
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) {
|
|
@@ -576,6 +584,34 @@ class ChatCommand {
|
|
|
576
584
|
this.currentSession = this.sessionManager.create(this.currentProjectPath, this.currentModel, this.agentMode, this.operatorMode);
|
|
577
585
|
this.messages = [...this.currentSession.messages];
|
|
578
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* Determine whether a direct --prompt call is simple enough to skip the
|
|
589
|
+
* full V3 Agent planner and answer directly via the simple chat pipeline.
|
|
590
|
+
* This prevents trivial prompts like "Reply with OK" from triggering a
|
|
591
|
+
* multi-step planning workflow with quality_profile: premium-visual-build.
|
|
592
|
+
*/
|
|
593
|
+
isSimpleDirectPrompt(prompt) {
|
|
594
|
+
const trimmed = prompt.trim();
|
|
595
|
+
const wordCount = trimmed.split(/\s+/).length;
|
|
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) {
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
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;
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
579
615
|
async handleDirectPrompt(prompt) {
|
|
580
616
|
if (!this.jsonOutput) {
|
|
581
617
|
console.log(chalk_1.default.cyan('Running single prompt in direct mode.'));
|
|
@@ -590,8 +626,16 @@ class ChatCommand {
|
|
|
590
626
|
await this.runWorkflowTargetPrompt(prompt);
|
|
591
627
|
return;
|
|
592
628
|
}
|
|
629
|
+
// Smart routing: for agent mode, determine if prompt needs tool access
|
|
593
630
|
if (this.agentMode) {
|
|
594
|
-
|
|
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
|
+
}
|
|
595
639
|
return;
|
|
596
640
|
}
|
|
597
641
|
if (this.operatorMode) {
|
|
@@ -800,30 +844,36 @@ class ChatCommand {
|
|
|
800
844
|
}
|
|
801
845
|
async runSimplePrompt(prompt) {
|
|
802
846
|
this.lastActionableUserInput = prompt;
|
|
803
|
-
//
|
|
804
|
-
//
|
|
805
|
-
|
|
806
|
-
// paths instead of guessing.
|
|
807
|
-
const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
|
|
808
|
-
if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
|
|
809
|
-
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.';
|
|
810
|
-
// Inject a workspace file listing so the model has real paths
|
|
811
|
-
try {
|
|
812
|
-
const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
|
|
813
|
-
if (snapshot && snapshot.paths.length > 0) {
|
|
814
|
-
const listing = snapshot.paths.slice(0, 60).join('\n');
|
|
815
|
-
groundingContent += `\n\nWorkspace: ${this.currentProjectPath}\nFile listing (${snapshot.fileCount} files total):\n${listing}`;
|
|
816
|
-
if (snapshot.fileCount > 60) {
|
|
817
|
-
groundingContent += `\n... and ${snapshot.fileCount - 60} more files.`;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
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')) {
|
|
822
850
|
this.messages.push({
|
|
823
851
|
role: 'system',
|
|
824
|
-
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.',
|
|
825
853
|
});
|
|
826
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
|
+
}
|
|
827
877
|
this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
|
|
828
878
|
(0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: 'chat', model: this.currentModel });
|
|
829
879
|
const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking...', spinner: 'clock' }).start();
|
|
@@ -1611,7 +1661,7 @@ class ChatCommand {
|
|
|
1611
1661
|
if (this.isServerBindableWorkspace(this.currentProjectPath)) {
|
|
1612
1662
|
return false;
|
|
1613
1663
|
}
|
|
1614
|
-
return /(analyse|analyze|audit|overview|inspect|explain|review|debug|diagnos|read|summari[sz]e|investigate)/i.test(prompt);
|
|
1664
|
+
return /(analyse|analyze|audit|overview|inspect|explain|review|debug|diagnos|read|summari[sz]e|investigate|list|show|find|search|check|scan|count|what|describe)/i.test(prompt);
|
|
1615
1665
|
}
|
|
1616
1666
|
shouldRequireV3AgentWorkflow(prompt) {
|
|
1617
1667
|
if (!this.directPromptMode) {
|
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
|
}
|
|
@@ -28,5 +28,10 @@ export declare class GenerateCommand {
|
|
|
28
28
|
private saveToFile;
|
|
29
29
|
private promptForAction;
|
|
30
30
|
private suggestFilename;
|
|
31
|
+
/**
|
|
32
|
+
* Auto-detect the target language from the user's description.
|
|
33
|
+
* Falls back to 'javascript' if no language is recognized.
|
|
34
|
+
*/
|
|
35
|
+
private detectLanguageFromDescription;
|
|
31
36
|
}
|
|
32
37
|
export {};
|
|
@@ -33,6 +33,10 @@ class GenerateCommand {
|
|
|
33
33
|
}
|
|
34
34
|
// Determine mode
|
|
35
35
|
const proMode = options.pro === true;
|
|
36
|
+
// Auto-detect language from description if not explicitly specified
|
|
37
|
+
if (!options.language) {
|
|
38
|
+
options.language = this.detectLanguageFromDescription(description);
|
|
39
|
+
}
|
|
36
40
|
this.logger.section(proMode ? '🚀 Senior Developer Mode' : 'Code Generation');
|
|
37
41
|
console.log(chalk_1.default.gray(`Language: ${options.language}`));
|
|
38
42
|
console.log(chalk_1.default.gray(`Description: ${description}`));
|
|
@@ -214,5 +218,40 @@ class GenerateCommand {
|
|
|
214
218
|
};
|
|
215
219
|
return extensions[language] || 'generated.txt';
|
|
216
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Auto-detect the target language from the user's description.
|
|
223
|
+
* Falls back to 'javascript' if no language is recognized.
|
|
224
|
+
*/
|
|
225
|
+
detectLanguageFromDescription(description) {
|
|
226
|
+
const lower = description.toLowerCase();
|
|
227
|
+
const patterns = [
|
|
228
|
+
[/\b(typescript|\.ts\b)/i, 'typescript'],
|
|
229
|
+
[/\b(javascript|\.js\b|node\.?js|es6|ecmascript)\b/i, 'javascript'],
|
|
230
|
+
[/\b(python|\.py\b|django|flask|fastapi)\b/i, 'python'],
|
|
231
|
+
[/\b(rust|\.rs\b|cargo)\b/i, 'rust'],
|
|
232
|
+
[/\b(go|golang|\.go\b)\b/i, 'go'],
|
|
233
|
+
[/\b(java\b|\.java\b|spring\b)/i, 'java'],
|
|
234
|
+
[/\b(c#|csharp|\.cs\b|dotnet|\.net)\b/i, 'csharp'],
|
|
235
|
+
[/\b(c\+\+|cpp|\.cpp\b|\.hpp\b)\b/i, 'cpp'],
|
|
236
|
+
[/\b(ruby|\.rb\b|rails)\b/i, 'ruby'],
|
|
237
|
+
[/\b(php|\.php\b|laravel)\b/i, 'php'],
|
|
238
|
+
[/\b(swift|\.swift\b|swiftui)\b/i, 'swift'],
|
|
239
|
+
[/\b(kotlin|\.kt\b)\b/i, 'kotlin'],
|
|
240
|
+
[/\b(html|\.html\b|webpage|web page|website|landing page)\b/i, 'html'],
|
|
241
|
+
[/\b(css|\.css\b|stylesheet)\b/i, 'css'],
|
|
242
|
+
[/\b(sql|\.sql\b|database query|select\s+\*)\b/i, 'sql'],
|
|
243
|
+
[/\b(bash|shell|\.sh\b|script)\b/i, 'bash'],
|
|
244
|
+
];
|
|
245
|
+
for (const [regex, lang] of patterns) {
|
|
246
|
+
if (regex.test(lower)) {
|
|
247
|
+
return lang;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// If the description mentions "function" without any language hint, default to javascript
|
|
251
|
+
if (/\bfunction\b/i.test(lower)) {
|
|
252
|
+
return 'javascript';
|
|
253
|
+
}
|
|
254
|
+
return 'javascript';
|
|
255
|
+
}
|
|
217
256
|
}
|
|
218
257
|
exports.GenerateCommand = GenerateCommand;
|
package/dist/index.js
CHANGED
|
@@ -98,7 +98,7 @@ function getVersion() {
|
|
|
98
98
|
catch (e) {
|
|
99
99
|
// Fallback to hardcoded version
|
|
100
100
|
}
|
|
101
|
-
return '1.6.
|
|
101
|
+
return '1.6.26';
|
|
102
102
|
}
|
|
103
103
|
const VERSION = getVersion();
|
|
104
104
|
/**
|
|
@@ -334,7 +334,7 @@ async function main() {
|
|
|
334
334
|
.command('generate <description>')
|
|
335
335
|
.alias('g')
|
|
336
336
|
.description('Generate code from description')
|
|
337
|
-
.option('-l, --language <lang>', 'Target language (
|
|
337
|
+
.option('-l, --language <lang>', 'Target language (auto-detected from description, or specify: javascript, typescript, python, html, etc.)')
|
|
338
338
|
.option('-o, --output <file>', 'Output file path')
|
|
339
339
|
.option('-m, --model <model>', 'Select AI model', 'code')
|
|
340
340
|
.option('-p, --pro', 'Senior Developer Mode: plan, generate, quality check (recommended)', false)
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -320,6 +320,30 @@ export declare class APIClient {
|
|
|
320
320
|
chatStream(messages: ChatMessage[], model: string): AsyncGenerator<StreamChunk>;
|
|
321
321
|
chatWithCallback(messages: ChatMessage[], model: string, onChunk: (chunk: string) => void, onDone: () => void, onError: (error: Error) => void): Promise<void>;
|
|
322
322
|
generateCode(prompt: string, language: string, model: string): Promise<string>;
|
|
323
|
+
/**
|
|
324
|
+
* Ensure code has balanced curly braces by appending missing closing braces.
|
|
325
|
+
*/
|
|
326
|
+
private ensureBalancedBraces;
|
|
327
|
+
/**
|
|
328
|
+
* Extract the first complete function/class from code.
|
|
329
|
+
* Used as last-resort when the model keeps over-producing.
|
|
330
|
+
*/
|
|
331
|
+
private extractFirstFunction;
|
|
332
|
+
/**
|
|
333
|
+
* Detect if code is excessively over-engineered for a short prompt.
|
|
334
|
+
* E.g. a "multiply function" request producing 20+ lines.
|
|
335
|
+
*/
|
|
336
|
+
private codeIsOverEngineered;
|
|
337
|
+
/**
|
|
338
|
+
* Detect if generated code contains DOM/HTML pollution inappropriate
|
|
339
|
+
* for a pure programming language like JavaScript, Python, etc.
|
|
340
|
+
*/
|
|
341
|
+
private codeContainsDomPollution;
|
|
342
|
+
/**
|
|
343
|
+
* Strip DOM pollution from generated code, keeping only the pure logic.
|
|
344
|
+
* Used as last-resort fallback when the model repeatedly ignores constraints.
|
|
345
|
+
*/
|
|
346
|
+
private stripDomPollution;
|
|
323
347
|
generateProject(prompt: string, projectType: string, model: string): Promise<{
|
|
324
348
|
code: string;
|
|
325
349
|
plan?: any;
|
|
@@ -360,6 +384,12 @@ export declare class APIClient {
|
|
|
360
384
|
reason: string;
|
|
361
385
|
}[];
|
|
362
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;
|
|
363
393
|
/**
|
|
364
394
|
* Lightweight client-side syntax error detection.
|
|
365
395
|
* Returns a human-readable description of obvious errors, or empty string.
|
package/dist/utils/api.js
CHANGED
|
@@ -3242,31 +3242,172 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3242
3242
|
}
|
|
3243
3243
|
// Code operations - Using Vigthoria Centralized API
|
|
3244
3244
|
async generateCode(prompt, language, model) {
|
|
3245
|
+
const isNonHtmlLang = !['html', 'css'].includes(language.toLowerCase());
|
|
3246
|
+
const wordCount = prompt.trim().split(/\s+/).length;
|
|
3245
3247
|
// Prepend a forceful scope-enforcement instruction so the model
|
|
3246
3248
|
// doesn't expand a small task into an oversized glossy page.
|
|
3247
|
-
const
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3249
|
+
const buildScopedPrompt = (retry) => {
|
|
3250
|
+
if (retry && isNonHtmlLang) {
|
|
3251
|
+
// Ultra-minimal retry prompt — no long instruction block
|
|
3252
|
+
return `OUTPUT ONLY RAW ${language.toUpperCase()} CODE. MAXIMUM 10 LINES. NO DOM. NO HTML. NO CSS. NO ANIMATION. NO VALIDATION. NO COMMENTS.\n\n${prompt}`;
|
|
3253
|
+
}
|
|
3254
|
+
const lines = [
|
|
3255
|
+
'IMPORTANT — MANDATORY SCOPE CONSTRAINTS (violation = failure):',
|
|
3256
|
+
`1. You MUST output ONLY ${language.toUpperCase()} code. Do NOT output HTML, CSS, or DOM manipulation unless the language is html/css.`,
|
|
3257
|
+
`2. The target language is: ${language}. Do not switch to a different language.`,
|
|
3258
|
+
'3. Output ONLY what the user explicitly asked for. Nothing more.',
|
|
3259
|
+
'4. If the prompt is ≤ 15 words, produce ≤ 80 lines of code maximum.',
|
|
3260
|
+
'5. If the user says "tiny", "small", "simple", "minimal", or "basic", produce ≤ 50 lines.',
|
|
3261
|
+
'6. NEVER add any of these unless the user EXPLICITLY requests them:',
|
|
3262
|
+
' - Hero sections, CTAs, testimonials, pricing tables, footers, navbars',
|
|
3263
|
+
' - CSS animations, gradients, neon effects, glass-morphism, particles',
|
|
3264
|
+
' - Google Fonts, Font Awesome, external CDN links, icon libraries',
|
|
3265
|
+
' - DOM manipulation, document.createElement, innerHTML',
|
|
3266
|
+
' - Responsive breakpoints, media queries (unless asked)',
|
|
3267
|
+
' - Multiple pages or components when one was requested',
|
|
3268
|
+
' - Input validation, error handling beyond what was requested',
|
|
3269
|
+
'7. Prefer inline styles or a small <style> block ONLY if the language is html. No CSS frameworks.',
|
|
3270
|
+
'8. Return raw code only — no markdown fences, no explanations, no comments about what could be added.',
|
|
3271
|
+
'9. Match the complexity of the request: a "hello world" is 1-5 lines, a "multiply function" is 3-5 lines, a "button" is 5-15 lines.',
|
|
3272
|
+
];
|
|
3273
|
+
if (isNonHtmlLang) {
|
|
3274
|
+
lines.push(`10. This is a PURE ${language.toUpperCase()} task. ABSOLUTELY NO document.*, window.*, DOM, createElement, innerHTML, querySelector, addEventListener. Output a plain ${language} function/module ONLY.`);
|
|
3275
|
+
}
|
|
3276
|
+
lines.push('', prompt);
|
|
3277
|
+
return lines.join('\n');
|
|
3278
|
+
};
|
|
3279
|
+
// First attempt
|
|
3280
|
+
let response = await this.client.post('/api/ai/generate', {
|
|
3281
|
+
prompt: buildScopedPrompt(false),
|
|
3266
3282
|
language,
|
|
3267
3283
|
model: this.resolvePermittedModelId(model),
|
|
3268
3284
|
});
|
|
3269
|
-
|
|
3285
|
+
let code = response.data.code || '';
|
|
3286
|
+
// Client-side validation: reject DOM-polluted or over-engineered responses for non-HTML languages
|
|
3287
|
+
const needsRetry = isNonHtmlLang && (this.codeContainsDomPollution(code) ||
|
|
3288
|
+
this.codeIsOverEngineered(code, prompt));
|
|
3289
|
+
if (needsRetry) {
|
|
3290
|
+
// Retry once with stronger constraint
|
|
3291
|
+
response = await this.client.post('/api/ai/generate', {
|
|
3292
|
+
prompt: buildScopedPrompt(true),
|
|
3293
|
+
language,
|
|
3294
|
+
model: this.resolvePermittedModelId(model),
|
|
3295
|
+
});
|
|
3296
|
+
code = response.data.code || '';
|
|
3297
|
+
// If still polluted, strip DOM code client-side
|
|
3298
|
+
if (this.codeContainsDomPollution(code)) {
|
|
3299
|
+
code = this.stripDomPollution(code, language);
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
// Final cleanup: for non-HTML, if still over-engineered after retry,
|
|
3303
|
+
// extract only the first complete function/class definition.
|
|
3304
|
+
if (isNonHtmlLang && this.codeIsOverEngineered(code, prompt)) {
|
|
3305
|
+
code = this.extractFirstFunction(code);
|
|
3306
|
+
}
|
|
3307
|
+
// Ensure balanced braces — model sometimes truncates closing braces
|
|
3308
|
+
code = this.ensureBalancedBraces(code);
|
|
3309
|
+
return code;
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Ensure code has balanced curly braces by appending missing closing braces.
|
|
3313
|
+
*/
|
|
3314
|
+
ensureBalancedBraces(code) {
|
|
3315
|
+
let depth = 0;
|
|
3316
|
+
for (const ch of code) {
|
|
3317
|
+
if (ch === '{')
|
|
3318
|
+
depth++;
|
|
3319
|
+
else if (ch === '}')
|
|
3320
|
+
depth--;
|
|
3321
|
+
}
|
|
3322
|
+
if (depth > 0) {
|
|
3323
|
+
let result = code.trimEnd();
|
|
3324
|
+
for (let i = 0; i < depth; i++) {
|
|
3325
|
+
result += '\n}';
|
|
3326
|
+
}
|
|
3327
|
+
code = result;
|
|
3328
|
+
}
|
|
3329
|
+
return code;
|
|
3330
|
+
}
|
|
3331
|
+
/**
|
|
3332
|
+
* Extract the first complete function/class from code.
|
|
3333
|
+
* Used as last-resort when the model keeps over-producing.
|
|
3334
|
+
*/
|
|
3335
|
+
extractFirstFunction(code) {
|
|
3336
|
+
const lines = code.split('\n');
|
|
3337
|
+
const funcStart = lines.findIndex(l => /^(export\s+)?(const\s+\w+\s*=\s*(\(|function)|function\s+\w+|class\s+\w+|def\s+\w+|fn\s+\w+|func\s+\w+)/.test(l.trim()));
|
|
3338
|
+
if (funcStart === -1)
|
|
3339
|
+
return code;
|
|
3340
|
+
let braceDepth = 0;
|
|
3341
|
+
let foundOpen = false;
|
|
3342
|
+
const result = [];
|
|
3343
|
+
for (let i = funcStart; i < lines.length; i++) {
|
|
3344
|
+
const line = lines[i];
|
|
3345
|
+
result.push(line);
|
|
3346
|
+
braceDepth += (line.match(/{/g) || []).length;
|
|
3347
|
+
braceDepth -= (line.match(/}/g) || []).length;
|
|
3348
|
+
if (braceDepth > 0)
|
|
3349
|
+
foundOpen = true;
|
|
3350
|
+
if (foundOpen && braceDepth <= 0) {
|
|
3351
|
+
break;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
// If we reached the end without balancing braces, add closing
|
|
3355
|
+
if (braceDepth > 0) {
|
|
3356
|
+
result.push('};');
|
|
3357
|
+
}
|
|
3358
|
+
return result.join('\n');
|
|
3359
|
+
}
|
|
3360
|
+
/**
|
|
3361
|
+
* Detect if code is excessively over-engineered for a short prompt.
|
|
3362
|
+
* E.g. a "multiply function" request producing 20+ lines.
|
|
3363
|
+
*/
|
|
3364
|
+
codeIsOverEngineered(code, prompt) {
|
|
3365
|
+
const wordCount = prompt.trim().split(/\s+/).length;
|
|
3366
|
+
const lineCount = code.trim().split('\n').length;
|
|
3367
|
+
// Short prompt (≤15 words) should not produce more than 15 lines
|
|
3368
|
+
if (wordCount <= 15 && lineCount > 15)
|
|
3369
|
+
return true;
|
|
3370
|
+
return false;
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Detect if generated code contains DOM/HTML pollution inappropriate
|
|
3374
|
+
* for a pure programming language like JavaScript, Python, etc.
|
|
3375
|
+
*/
|
|
3376
|
+
codeContainsDomPollution(code) {
|
|
3377
|
+
const domPatterns = /document\.(createElement|querySelector|getElementById|getElementsBy|body|head|addEventListener)|innerHTML|\.style\.(cssText|position|transform|animation)|@keyframes|\.appendChild|\.removeChild|window\.(onload|addEventListener)/;
|
|
3378
|
+
return domPatterns.test(code);
|
|
3379
|
+
}
|
|
3380
|
+
/**
|
|
3381
|
+
* Strip DOM pollution from generated code, keeping only the pure logic.
|
|
3382
|
+
* Used as last-resort fallback when the model repeatedly ignores constraints.
|
|
3383
|
+
*/
|
|
3384
|
+
stripDomPollution(code, language) {
|
|
3385
|
+
const lines = code.split('\n');
|
|
3386
|
+
const cleanLines = [];
|
|
3387
|
+
let insideDomBlock = false;
|
|
3388
|
+
let braceDepth = 0;
|
|
3389
|
+
for (const line of lines) {
|
|
3390
|
+
// Detect start of DOM blocks
|
|
3391
|
+
if (/document\.|\.style\.|\.appendChild|\.removeChild|\.textContent\s*=|@keyframes|addEventListener/.test(line) && !insideDomBlock) {
|
|
3392
|
+
insideDomBlock = true;
|
|
3393
|
+
braceDepth = 0;
|
|
3394
|
+
}
|
|
3395
|
+
if (insideDomBlock) {
|
|
3396
|
+
braceDepth += (line.match(/{/g) || []).length;
|
|
3397
|
+
braceDepth -= (line.match(/}/g) || []).length;
|
|
3398
|
+
if (braceDepth <= 0) {
|
|
3399
|
+
insideDomBlock = false;
|
|
3400
|
+
}
|
|
3401
|
+
continue; // Skip DOM lines
|
|
3402
|
+
}
|
|
3403
|
+
cleanLines.push(line);
|
|
3404
|
+
}
|
|
3405
|
+
const cleaned = cleanLines.join('\n').trim();
|
|
3406
|
+
// If we stripped too aggressively (less than 2 lines left), return a minimal stub
|
|
3407
|
+
if (cleaned.split('\n').length < 2) {
|
|
3408
|
+
return `// Auto-generated: model returned DOM code for a ${language} task.\n// Please regenerate or provide more specific instructions.`;
|
|
3409
|
+
}
|
|
3410
|
+
return cleaned;
|
|
3270
3411
|
}
|
|
3271
3412
|
// Senior Developer Mode - Planning + Generation + Quality Check
|
|
3272
3413
|
async generateProject(prompt, projectType, model) {
|
|
@@ -3389,6 +3530,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3389
3530
|
}
|
|
3390
3531
|
}
|
|
3391
3532
|
}
|
|
3533
|
+
// Accumulator/reducer bug: subtraction used in a reduce/total/sum context
|
|
3534
|
+
// e.g. "sum - item.price" in a function named calculateTotal, getTotal, etc.
|
|
3535
|
+
if (/\w+\s*-\s*\w+\.\s*(price|cost|amount|value|total|quantity|count)\b/.test(line) || /\w+\.\s*(price|cost|amount|value|total)\s*-\s*/.test(line)) {
|
|
3536
|
+
// Check enclosing function/method name for total/sum/aggregate semantics
|
|
3537
|
+
for (let j = i; j >= Math.max(0, i - 10); j--) {
|
|
3538
|
+
if (/(function\s+)?(calculate|calc|get|compute|sum|total|aggregate|accumulate)\s*(Total|Sum|Cost|Price|Amount|All)?\s*[=(]/i.test(lines[j]) || /\.(reduce|forEach|map)\s*\(/.test(lines[j])) {
|
|
3539
|
+
// Also check if this looks like an accumulator pattern (sum - something instead of sum + something)
|
|
3540
|
+
if (/\w+\s*-\s*\w+\.\s*(price|cost|amount|value|total|quantity)\b/.test(line)) {
|
|
3541
|
+
issues.push({ type: 'logic', line: lineNum, message: 'Subtraction used in an accumulation context (total/sum/reduce) — likely should be addition (+).', severity: 'error' });
|
|
3542
|
+
}
|
|
3543
|
+
break;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
// Mismatched function name vs operator: sum/total function using wrong operator
|
|
3548
|
+
if (/\breturn\b/.test(line)) {
|
|
3549
|
+
for (let j = i; j >= Math.max(0, i - 8); j--) {
|
|
3550
|
+
const funcMatch = lines[j].match(/function\s+(\w+)/i) || lines[j].match(/(?:const|let|var)\s+(\w+)\s*=/i);
|
|
3551
|
+
if (funcMatch) {
|
|
3552
|
+
const funcName = funcMatch[1].toLowerCase();
|
|
3553
|
+
if ((funcName.includes('total') || funcName.includes('sum') || funcName.includes('add')) && /\s-\s/.test(line) && !/\s\+\s/.test(line)) {
|
|
3554
|
+
const alreadyReported = issues.some(iss => iss.line === lineNum && iss.type === 'logic');
|
|
3555
|
+
if (!alreadyReported) {
|
|
3556
|
+
issues.push({ type: 'logic', line: lineNum, message: `Subtraction operator in "${funcMatch[1]}" — function name implies addition. Likely should use + instead of -.`, severity: 'error' });
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
break;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3392
3563
|
// Off-by-one: comparing with === where <= or >= may be needed
|
|
3393
3564
|
// (not implemented yet — would require more complex AST analysis)
|
|
3394
3565
|
// Limit to 15 heuristic issues
|
|
@@ -3400,9 +3571,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3400
3571
|
async fixCode(code, language, fixType) {
|
|
3401
3572
|
// Client-side syntax pre-check: detect obvious errors and include
|
|
3402
3573
|
// them in the request so the model has concrete signals.
|
|
3403
|
-
const syntaxHints = this.detectSyntaxErrors(code, language);
|
|
3404
|
-
|
|
3405
|
-
|
|
3574
|
+
const syntaxHints = fixType === 'bugs' || fixType === 'syntax' ? this.detectSyntaxErrors(code, language) : '';
|
|
3575
|
+
// Run heuristic logic analysis, but ONLY pass hints relevant to the fixType
|
|
3576
|
+
const heuristicIssues = this.heuristicCodeIssues(code, language);
|
|
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
|
|
3591
|
+
.map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
|
|
3592
|
+
.join('\n// ');
|
|
3593
|
+
const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
|
|
3594
|
+
const augmentedCode = allHints
|
|
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}`
|
|
3406
3596
|
: code;
|
|
3407
3597
|
const response = await this.client.post('/api/ai/fix', {
|
|
3408
3598
|
code: augmentedCode,
|
|
@@ -3412,26 +3602,107 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3412
3602
|
const raw = response.data ?? {};
|
|
3413
3603
|
let fixed = typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code);
|
|
3414
3604
|
let changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
3415
|
-
// If server returned no changes but we found
|
|
3605
|
+
// If server returned no changes but we found issues, strip
|
|
3416
3606
|
// our injected comment prefix from the returned code and attempt
|
|
3417
3607
|
// a basic client-side repair.
|
|
3418
|
-
if (changes.length === 0 &&
|
|
3608
|
+
if (changes.length === 0 && allHints && fixed === augmentedCode) {
|
|
3419
3609
|
fixed = code; // restore original
|
|
3420
3610
|
}
|
|
3421
3611
|
// Strip the injected comment block if it leaked into the output
|
|
3422
|
-
if (fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT:')) {
|
|
3612
|
+
if (fixed.startsWith('// BUGS DETECTED BY STATIC ANALYSIS') || fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT:')) {
|
|
3423
3613
|
const idx = fixed.indexOf('\n\n');
|
|
3424
3614
|
if (idx !== -1) {
|
|
3425
3615
|
fixed = fixed.slice(idx + 2);
|
|
3426
3616
|
}
|
|
3427
3617
|
}
|
|
3428
|
-
// If there are still no changes but the fixed code differs,
|
|
3429
|
-
//
|
|
3618
|
+
// If there are still no changes but the fixed code differs, compute
|
|
3619
|
+
// a semantic diff using LCS so inserted/removed lines don't cause
|
|
3620
|
+
// every subsequent line to appear as changed.
|
|
3430
3621
|
if (changes.length === 0 && fixed !== code) {
|
|
3431
|
-
changes =
|
|
3622
|
+
changes = this.computeSemanticDiff(code, fixed, allHints);
|
|
3432
3623
|
}
|
|
3433
3624
|
return { fixed, changes };
|
|
3434
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') {
|
|
3675
|
+
changes.push({
|
|
3676
|
+
line: op.origLine,
|
|
3677
|
+
before: op.text,
|
|
3678
|
+
after: ops[idx + 1].text,
|
|
3679
|
+
reason: reason || 'AI-suggested fix',
|
|
3680
|
+
});
|
|
3681
|
+
idx += 2;
|
|
3682
|
+
continue;
|
|
3683
|
+
}
|
|
3684
|
+
changes.push({
|
|
3685
|
+
line: op.origLine,
|
|
3686
|
+
before: op.text,
|
|
3687
|
+
after: '(line removed)',
|
|
3688
|
+
reason: reason || 'AI-suggested fix',
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
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
|
+
});
|
|
3698
|
+
}
|
|
3699
|
+
idx++;
|
|
3700
|
+
}
|
|
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;
|
|
3705
|
+
}
|
|
3435
3706
|
/**
|
|
3436
3707
|
* Lightweight client-side syntax error detection.
|
|
3437
3708
|
* Returns a human-readable description of obvious errors, or empty string.
|