vigthoria-cli 1.6.25 → 1.6.27
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 +82 -32
- package/dist/commands/edit.d.ts +7 -0
- package/dist/commands/edit.js +103 -14
- package/dist/index.js +4 -5
- package/dist/utils/api.d.ts +6 -0
- package/dist/utils/api.js +118 -26
- package/package.json +1 -1
package/dist/commands/chat.js
CHANGED
|
@@ -421,12 +421,42 @@ class ChatCommand {
|
|
|
421
421
|
return;
|
|
422
422
|
}
|
|
423
423
|
if (options.prompt) {
|
|
424
|
+
// Wrap in a timeout to guarantee bridge cleanup even if the agent
|
|
425
|
+
// loop hangs (e.g. model takes forever on a turn).
|
|
426
|
+
const BRIDGE_TIMEOUT_MS = 180_000; // 3 minutes max for a single prompt
|
|
427
|
+
let timedOut = false;
|
|
428
|
+
const timeoutId = options.bridge
|
|
429
|
+
? setTimeout(() => {
|
|
430
|
+
timedOut = true;
|
|
431
|
+
const b = (0, bridge_client_js_1.getBridgeClient)();
|
|
432
|
+
if (b) {
|
|
433
|
+
b.emitEnd({ reason: 'timeout' });
|
|
434
|
+
b.destroy();
|
|
435
|
+
}
|
|
436
|
+
if (!this.jsonOutput) {
|
|
437
|
+
this.logger.error('Bridge prompt timed out after 3 minutes.');
|
|
438
|
+
}
|
|
439
|
+
process.exitCode = 1;
|
|
440
|
+
}, BRIDGE_TIMEOUT_MS)
|
|
441
|
+
: null;
|
|
424
442
|
await this.handleDirectPrompt(options.prompt);
|
|
425
|
-
|
|
443
|
+
if (timeoutId)
|
|
444
|
+
clearTimeout(timeoutId);
|
|
445
|
+
if (!timedOut) {
|
|
446
|
+
const bridge = (0, bridge_client_js_1.getBridgeClient)();
|
|
447
|
+
if (bridge) {
|
|
448
|
+
bridge.emitEnd({ reason: 'prompt-complete' });
|
|
449
|
+
bridge.destroy();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
426
452
|
return;
|
|
427
453
|
}
|
|
428
454
|
await this.startInteractiveChat();
|
|
429
|
-
(0, bridge_client_js_1.getBridgeClient)()
|
|
455
|
+
const bridge = (0, bridge_client_js_1.getBridgeClient)();
|
|
456
|
+
if (bridge) {
|
|
457
|
+
bridge.emitEnd({ reason: 'interactive-exit' });
|
|
458
|
+
bridge.destroy();
|
|
459
|
+
}
|
|
430
460
|
}
|
|
431
461
|
/** Handle an inbound admin command from the Commando Bridge. */
|
|
432
462
|
handleAdminCommand(cmd) {
|
|
@@ -584,16 +614,23 @@ class ChatCommand {
|
|
|
584
614
|
*/
|
|
585
615
|
isSimpleDirectPrompt(prompt) {
|
|
586
616
|
const trimmed = prompt.trim();
|
|
587
|
-
// Short prompts (≤ 12 words) that don't reference files, builds, or code tasks
|
|
588
617
|
const wordCount = trimmed.split(/\s+/).length;
|
|
589
|
-
|
|
618
|
+
// Never treat prompts that reference files, dirs, or workspace as simple
|
|
619
|
+
// — these need agent tool access (list_dir, read_file, etc.)
|
|
620
|
+
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)) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
// Never treat tool-action verbs as simple
|
|
624
|
+
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)) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
// Short conversational prompts (≤ 15 words) with no file/tool context
|
|
628
|
+
if (wordCount <= 15) {
|
|
590
629
|
return true;
|
|
591
630
|
}
|
|
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
|
-
}
|
|
631
|
+
// Conversational Q&A starters
|
|
632
|
+
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)) {
|
|
633
|
+
return true;
|
|
597
634
|
}
|
|
598
635
|
return false;
|
|
599
636
|
}
|
|
@@ -611,9 +648,16 @@ class ChatCommand {
|
|
|
611
648
|
await this.runWorkflowTargetPrompt(prompt);
|
|
612
649
|
return;
|
|
613
650
|
}
|
|
614
|
-
// Smart routing:
|
|
615
|
-
if (this.agentMode
|
|
616
|
-
|
|
651
|
+
// Smart routing: for agent mode, determine if prompt needs tool access
|
|
652
|
+
if (this.agentMode) {
|
|
653
|
+
if (this.isSimpleDirectPrompt(prompt)) {
|
|
654
|
+
// Simple prompt: downgrade to plain chat model, no agent system prompt
|
|
655
|
+
this.currentModel = this.getDefaultChatModel();
|
|
656
|
+
await this.runSimplePrompt(prompt);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
await this.runAgentTurn(prompt);
|
|
660
|
+
}
|
|
617
661
|
return;
|
|
618
662
|
}
|
|
619
663
|
if (this.operatorMode) {
|
|
@@ -822,30 +866,36 @@ class ChatCommand {
|
|
|
822
866
|
}
|
|
823
867
|
async runSimplePrompt(prompt) {
|
|
824
868
|
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 */ }
|
|
869
|
+
// For direct --prompt mode with simple prompts, use a minimal system
|
|
870
|
+
// message to avoid polluting the response with tool/platform context.
|
|
871
|
+
if (this.directPromptMode && !this.messages.some(m => m.role === 'system')) {
|
|
844
872
|
this.messages.push({
|
|
845
873
|
role: 'system',
|
|
846
|
-
content:
|
|
874
|
+
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
875
|
});
|
|
848
876
|
}
|
|
877
|
+
else if (!this.directPromptMode) {
|
|
878
|
+
// Interactive mode: inject grounding for file-related queries
|
|
879
|
+
const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
|
|
880
|
+
if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
|
|
881
|
+
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.';
|
|
882
|
+
try {
|
|
883
|
+
const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
|
|
884
|
+
if (snapshot && snapshot.paths.length > 0) {
|
|
885
|
+
const listing = snapshot.paths.slice(0, 60).join('\n');
|
|
886
|
+
groundingContent += `\n\nWorkspace: ${this.currentProjectPath}\nFile listing (${snapshot.fileCount} files total):\n${listing}`;
|
|
887
|
+
if (snapshot.fileCount > 60) {
|
|
888
|
+
groundingContent += `\n... and ${snapshot.fileCount - 60} more files.`;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
catch { /* ignore snapshot errors */ }
|
|
893
|
+
this.messages.push({
|
|
894
|
+
role: 'system',
|
|
895
|
+
content: groundingContent,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
}
|
|
849
899
|
this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
|
|
850
900
|
(0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: 'chat', model: this.currentModel });
|
|
851
901
|
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,13 @@ 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 or three times.
|
|
27
|
+
* Applies iteratively until no further duplicates are found.
|
|
28
|
+
*/
|
|
29
|
+
private deduplicateCode;
|
|
30
|
+
private deduplicateOnce;
|
|
24
31
|
private showDiffAndConfirm;
|
|
25
32
|
private showFullDiff;
|
|
26
33
|
private applyFix;
|
package/dist/commands/edit.js
CHANGED
|
@@ -133,7 +133,24 @@ Return the complete modified file content:`,
|
|
|
133
133
|
try {
|
|
134
134
|
const result = await this.api.fixCode(file.content, file.language, options.type);
|
|
135
135
|
spinner.stop();
|
|
136
|
+
// Deduplicate the fixed output — models sometimes repeat code 2-3x
|
|
137
|
+
result.fixed = this.deduplicateCode(result.fixed);
|
|
136
138
|
if (result.changes.length === 0) {
|
|
139
|
+
// Even if the diff engine found no structured changes, the fixed
|
|
140
|
+
// code may still differ (e.g. single-char operator swap). Show a
|
|
141
|
+
// diff and let the user decide instead of silently discarding.
|
|
142
|
+
if (result.fixed && result.fixed !== file.content) {
|
|
143
|
+
this.logger.section('Found 1 issue(s)');
|
|
144
|
+
console.log(chalk_1.default.yellow('1. Operator/character-level fix detected'));
|
|
145
|
+
console.log();
|
|
146
|
+
if (options.apply) {
|
|
147
|
+
await this.applyFix(file.path, file.content, result.fixed);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
await this.showDiffAndConfirm(file.path, file.content, result.fixed);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
137
154
|
this.logger.success('No issues found!');
|
|
138
155
|
return;
|
|
139
156
|
}
|
|
@@ -174,10 +191,49 @@ Return the complete modified file content:`,
|
|
|
174
191
|
// If no code block, check if response looks like code
|
|
175
192
|
const trimmed = response.trim();
|
|
176
193
|
if (!trimmed.startsWith('```') && !trimmed.includes('Here') && !trimmed.includes('I ')) {
|
|
177
|
-
return trimmed;
|
|
194
|
+
return this.deduplicateCode(trimmed);
|
|
178
195
|
}
|
|
179
196
|
return null;
|
|
180
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Detect and remove duplicated content in model output.
|
|
200
|
+
* Models sometimes output the code twice or three times.
|
|
201
|
+
* Applies iteratively until no further duplicates are found.
|
|
202
|
+
*/
|
|
203
|
+
deduplicateCode(code) {
|
|
204
|
+
let result = code;
|
|
205
|
+
// Iterate up to 3 times to handle triple+ duplication
|
|
206
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
207
|
+
const deduped = this.deduplicateOnce(result);
|
|
208
|
+
if (deduped === result)
|
|
209
|
+
break;
|
|
210
|
+
result = deduped;
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
deduplicateOnce(code) {
|
|
215
|
+
const lines = code.split('\n');
|
|
216
|
+
const len = lines.length;
|
|
217
|
+
if (len < 4)
|
|
218
|
+
return code;
|
|
219
|
+
// Check if the second half is a near-duplicate of the first half
|
|
220
|
+
for (let splitAt = Math.floor(len * 0.4); splitAt <= Math.ceil(len * 0.6); splitAt++) {
|
|
221
|
+
const firstHalf = lines.slice(0, splitAt);
|
|
222
|
+
const secondHalf = lines.slice(splitAt).filter(l => l.trim() !== '');
|
|
223
|
+
if (secondHalf.length < 2)
|
|
224
|
+
continue;
|
|
225
|
+
let matches = 0;
|
|
226
|
+
for (const line of secondHalf) {
|
|
227
|
+
if (firstHalf.some(fl => fl.trim() === line.trim()))
|
|
228
|
+
matches++;
|
|
229
|
+
}
|
|
230
|
+
// If >70% of second half matches first half, it's a duplicate — keep second half
|
|
231
|
+
if (matches / secondHalf.length > 0.7) {
|
|
232
|
+
return secondHalf.join('\n');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return code;
|
|
236
|
+
}
|
|
181
237
|
async showDiffAndConfirm(filePath, original, modified) {
|
|
182
238
|
const diff = this.fileUtils.createDiff(original, modified);
|
|
183
239
|
if (diff.added.length === 0 && diff.removed.length === 0) {
|
|
@@ -240,23 +296,56 @@ Return the complete modified file content:`,
|
|
|
240
296
|
const modifiedLines = modified.split('\n');
|
|
241
297
|
console.log();
|
|
242
298
|
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}`));
|
|
299
|
+
// Use LCS-based diff to avoid line-shift inflation
|
|
300
|
+
const m = originalLines.length;
|
|
301
|
+
const n = modifiedLines.length;
|
|
302
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
303
|
+
for (let i = 1; i <= m; i++) {
|
|
304
|
+
for (let j = 1; j <= n; j++) {
|
|
305
|
+
if (originalLines[i - 1] === modifiedLines[j - 1]) {
|
|
306
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
254
307
|
}
|
|
255
|
-
|
|
256
|
-
|
|
308
|
+
else {
|
|
309
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
257
310
|
}
|
|
258
311
|
}
|
|
259
312
|
}
|
|
313
|
+
// Backtrack to produce diff ops
|
|
314
|
+
const ops = [];
|
|
315
|
+
let i = m, j = n;
|
|
316
|
+
while (i > 0 || j > 0) {
|
|
317
|
+
if (i > 0 && j > 0 && originalLines[i - 1] === modifiedLines[j - 1]) {
|
|
318
|
+
ops.push({ type: 'keep', text: originalLines[i - 1], lineNum: i });
|
|
319
|
+
i--;
|
|
320
|
+
j--;
|
|
321
|
+
}
|
|
322
|
+
else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
323
|
+
ops.push({ type: 'add', text: modifiedLines[j - 1], lineNum: j });
|
|
324
|
+
j--;
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
ops.push({ type: 'remove', text: originalLines[i - 1], lineNum: i });
|
|
328
|
+
i--;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
ops.reverse();
|
|
332
|
+
let displayLine = 0;
|
|
333
|
+
for (const op of ops) {
|
|
334
|
+
if (op.type === 'keep') {
|
|
335
|
+
displayLine++;
|
|
336
|
+
const lineNum = String(displayLine).padStart(4, ' ');
|
|
337
|
+
console.log(chalk_1.default.gray(`${lineNum} │ ${op.text || ''}`));
|
|
338
|
+
}
|
|
339
|
+
else if (op.type === 'remove') {
|
|
340
|
+
displayLine++;
|
|
341
|
+
const lineNum = String(displayLine).padStart(4, ' ');
|
|
342
|
+
console.log(chalk_1.default.red(`${lineNum} - ${op.text}`));
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const lineNum = ' +';
|
|
346
|
+
console.log(chalk_1.default.green(`${lineNum} + ${op.text}`));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
260
349
|
console.log(chalk_1.default.gray('─'.repeat(60)));
|
|
261
350
|
console.log();
|
|
262
351
|
}
|
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.27';
|
|
102
102
|
}
|
|
103
103
|
const VERSION = getVersion();
|
|
104
104
|
/**
|
|
@@ -576,10 +576,9 @@ async function main() {
|
|
|
576
576
|
force: options.force
|
|
577
577
|
});
|
|
578
578
|
});
|
|
579
|
-
// Default repo action shows
|
|
580
|
-
repoCommand.action(
|
|
581
|
-
|
|
582
|
-
await repo.list({});
|
|
579
|
+
// Default repo action shows help with available subcommands
|
|
580
|
+
repoCommand.action(() => {
|
|
581
|
+
repoCommand.outputHelp();
|
|
583
582
|
});
|
|
584
583
|
// ==================== DEPLOY COMMANDS ====================
|
|
585
584
|
// Deploy command - Host projects on Vigthoria
|
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
|
@@ -50,7 +50,7 @@ class APIClient {
|
|
|
50
50
|
// Main Vigthoria Coder API (coder.vigthoria.io)
|
|
51
51
|
this.client = axios_1.default.create({
|
|
52
52
|
baseURL: config.get('apiUrl'),
|
|
53
|
-
timeout:
|
|
53
|
+
timeout: 300000, // 5 minutes — covers review, fix, and agent relay flows
|
|
54
54
|
httpsAgent,
|
|
55
55
|
headers: {
|
|
56
56
|
'Content-Type': 'application/json',
|
|
@@ -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
|
}
|
|
@@ -3373,7 +3374,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3373
3374
|
* for a pure programming language like JavaScript, Python, etc.
|
|
3374
3375
|
*/
|
|
3375
3376
|
codeContainsDomPollution(code) {
|
|
3376
|
-
const domPatterns = /document\.(createElement|querySelector|getElementById|getElementsBy|body|head|addEventListener)|innerHTML|\.style\.(cssText|position|transform|animation)|@keyframes|\.appendChild|\.removeChild|window\.(onload|addEventListener)/;
|
|
3377
|
+
const domPatterns = /document\.(createElement|querySelector|getElementById|getElementsBy|body|head|addEventListener)|innerHTML|\.style\.(cssText|position|transform|animation)|@keyframes|\.appendChild|\.removeChild|window\.(onload|addEventListener)|CSSAnimation|new\s+Animation\b|\.getAnimations\s*\(|\.animate\s*\(/;
|
|
3377
3378
|
return domPatterns.test(code);
|
|
3378
3379
|
}
|
|
3379
3380
|
/**
|
|
@@ -3387,7 +3388,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3387
3388
|
let braceDepth = 0;
|
|
3388
3389
|
for (const line of lines) {
|
|
3389
3390
|
// Detect start of DOM blocks
|
|
3390
|
-
if (/document\.|\.style\.|\.appendChild|\.removeChild|\.textContent\s*=|@keyframes|addEventListener/.test(line) && !insideDomBlock) {
|
|
3391
|
+
if (/document\.|\.style\.|\.appendChild|\.removeChild|\.textContent\s*=|@keyframes|addEventListener|CSSAnimation|\.getAnimations|\.animate\s*\(/.test(line) && !insideDomBlock) {
|
|
3391
3392
|
insideDomBlock = true;
|
|
3392
3393
|
braceDepth = 0;
|
|
3393
3394
|
}
|
|
@@ -3455,7 +3456,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3455
3456
|
// even when the server only reports style issues like console.log.
|
|
3456
3457
|
const heuristic = this.heuristicCodeIssues(code, language);
|
|
3457
3458
|
for (const h of heuristic) {
|
|
3458
|
-
//
|
|
3459
|
+
// Always include critical logic bugs (severity error) from heuristics
|
|
3460
|
+
// regardless of server results — these catch wrong-operator bugs the
|
|
3461
|
+
// server frequently misses.
|
|
3462
|
+
if (h.severity === 'error') {
|
|
3463
|
+
const exactDuplicate = issues.some((existing) => existing.line === h.line && existing.message === h.message);
|
|
3464
|
+
if (!exactDuplicate) {
|
|
3465
|
+
issues.push(h);
|
|
3466
|
+
}
|
|
3467
|
+
continue;
|
|
3468
|
+
}
|
|
3469
|
+
// For non-critical heuristics, avoid duplicating issues the server
|
|
3470
|
+
// already reported on the same line with the same type.
|
|
3459
3471
|
const isDuplicate = issues.some((existing) => existing.line === h.line && existing.type === h.type);
|
|
3460
3472
|
if (!isDuplicate) {
|
|
3461
3473
|
issues.push(h);
|
|
@@ -3570,15 +3582,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3570
3582
|
async fixCode(code, language, fixType) {
|
|
3571
3583
|
// Client-side syntax pre-check: detect obvious errors and include
|
|
3572
3584
|
// them in the request so the model has concrete signals.
|
|
3573
|
-
const syntaxHints = this.detectSyntaxErrors(code, language);
|
|
3574
|
-
//
|
|
3585
|
+
const syntaxHints = fixType === 'bugs' || fixType === 'syntax' ? this.detectSyntaxErrors(code, language) : '';
|
|
3586
|
+
// Run heuristic logic analysis, but ONLY pass hints relevant to the fixType
|
|
3575
3587
|
const heuristicIssues = this.heuristicCodeIssues(code, language);
|
|
3576
|
-
const
|
|
3588
|
+
const relevantIssues = heuristicIssues.filter(issue => {
|
|
3589
|
+
if (fixType === 'bugs' || fixType === 'logic')
|
|
3590
|
+
return issue.type === 'logic' || issue.severity === 'error';
|
|
3591
|
+
if (fixType === 'syntax')
|
|
3592
|
+
return issue.severity === 'error';
|
|
3593
|
+
if (fixType === 'style')
|
|
3594
|
+
return issue.type === 'style' || issue.type === 'quality';
|
|
3595
|
+
if (fixType === 'security')
|
|
3596
|
+
return issue.type === 'security';
|
|
3597
|
+
if (fixType === 'performance')
|
|
3598
|
+
return issue.type === 'performance';
|
|
3599
|
+
return true; // 'all' or unknown fixType: include everything
|
|
3600
|
+
});
|
|
3601
|
+
const logicHints = relevantIssues
|
|
3577
3602
|
.map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
|
|
3578
3603
|
.join('\n// ');
|
|
3579
3604
|
const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
|
|
3580
3605
|
const augmentedCode = allHints
|
|
3581
|
-
? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n\n${code}`
|
|
3606
|
+
? `// 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
3607
|
: code;
|
|
3583
3608
|
const response = await this.client.post('/api/ai/fix', {
|
|
3584
3609
|
code: augmentedCode,
|
|
@@ -3602,29 +3627,96 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3602
3627
|
}
|
|
3603
3628
|
}
|
|
3604
3629
|
// If there are still no changes but the fixed code differs, compute
|
|
3605
|
-
// a
|
|
3630
|
+
// a semantic diff using LCS so inserted/removed lines don't cause
|
|
3631
|
+
// every subsequent line to appear as changed.
|
|
3606
3632
|
if (changes.length === 0 && fixed !== code) {
|
|
3607
|
-
|
|
3608
|
-
const
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3633
|
+
// Use a clean reason string — strip verbose static-analysis hints
|
|
3634
|
+
const cleanReason = relevantIssues.length > 0
|
|
3635
|
+
? relevantIssues.map(i => i.message).join('; ')
|
|
3636
|
+
: 'AI-suggested fix';
|
|
3637
|
+
changes = this.computeSemanticDiff(code, fixed, cleanReason);
|
|
3638
|
+
}
|
|
3639
|
+
return { fixed, changes };
|
|
3640
|
+
}
|
|
3641
|
+
/**
|
|
3642
|
+
* Compute a semantic diff between original and fixed code using
|
|
3643
|
+
* Longest Common Subsequence (LCS) to avoid the line-shift inflation
|
|
3644
|
+
* bug where inserting one line flags all subsequent lines as changed.
|
|
3645
|
+
*/
|
|
3646
|
+
computeSemanticDiff(original, fixed, reason) {
|
|
3647
|
+
const origLines = original.split('\n');
|
|
3648
|
+
const fixedLines = fixed.split('\n');
|
|
3649
|
+
const changes = [];
|
|
3650
|
+
// Build LCS table
|
|
3651
|
+
const m = origLines.length;
|
|
3652
|
+
const n = fixedLines.length;
|
|
3653
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
3654
|
+
for (let i = 1; i <= m; i++) {
|
|
3655
|
+
for (let j = 1; j <= n; j++) {
|
|
3656
|
+
if (origLines[i - 1] === fixedLines[j - 1]) {
|
|
3657
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
3658
|
+
}
|
|
3659
|
+
else {
|
|
3660
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
// Backtrack to find the diff
|
|
3665
|
+
let i = m, j = n;
|
|
3666
|
+
const ops = [];
|
|
3667
|
+
while (i > 0 || j > 0) {
|
|
3668
|
+
if (i > 0 && j > 0 && origLines[i - 1] === fixedLines[j - 1]) {
|
|
3669
|
+
ops.push({ type: 'keep', origLine: i, fixedLine: j, text: origLines[i - 1] });
|
|
3670
|
+
i--;
|
|
3671
|
+
j--;
|
|
3672
|
+
}
|
|
3673
|
+
else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
3674
|
+
ops.push({ type: 'add', fixedLine: j, text: fixedLines[j - 1] });
|
|
3675
|
+
j--;
|
|
3676
|
+
}
|
|
3677
|
+
else {
|
|
3678
|
+
ops.push({ type: 'remove', origLine: i, text: origLines[i - 1] });
|
|
3679
|
+
i--;
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
ops.reverse();
|
|
3683
|
+
// Merge adjacent remove+add pairs into single change entries
|
|
3684
|
+
let idx = 0;
|
|
3685
|
+
while (idx < ops.length) {
|
|
3686
|
+
const op = ops[idx];
|
|
3687
|
+
if (op.type === 'remove') {
|
|
3688
|
+
// Look ahead for a matching 'add' immediately after
|
|
3689
|
+
if (idx + 1 < ops.length && ops[idx + 1].type === 'add') {
|
|
3614
3690
|
changes.push({
|
|
3615
|
-
line:
|
|
3616
|
-
before:
|
|
3617
|
-
after:
|
|
3618
|
-
reason:
|
|
3691
|
+
line: op.origLine,
|
|
3692
|
+
before: op.text,
|
|
3693
|
+
after: ops[idx + 1].text,
|
|
3694
|
+
reason: reason || 'AI-suggested fix',
|
|
3619
3695
|
});
|
|
3696
|
+
idx += 2;
|
|
3697
|
+
continue;
|
|
3620
3698
|
}
|
|
3699
|
+
changes.push({
|
|
3700
|
+
line: op.origLine,
|
|
3701
|
+
before: op.text,
|
|
3702
|
+
after: '(line removed)',
|
|
3703
|
+
reason: reason || 'AI-suggested fix',
|
|
3704
|
+
});
|
|
3621
3705
|
}
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3706
|
+
else if (op.type === 'add') {
|
|
3707
|
+
changes.push({
|
|
3708
|
+
line: op.fixedLine,
|
|
3709
|
+
before: '(new line)',
|
|
3710
|
+
after: op.text,
|
|
3711
|
+
reason: reason || 'AI-suggested fix',
|
|
3712
|
+
});
|
|
3625
3713
|
}
|
|
3714
|
+
idx++;
|
|
3626
3715
|
}
|
|
3627
|
-
|
|
3716
|
+
if (changes.length === 0) {
|
|
3717
|
+
changes.push({ line: 1, before: '(whitespace changes)', after: '(see fixed file)', reason: reason || 'AI-suggested fix' });
|
|
3718
|
+
}
|
|
3719
|
+
return changes;
|
|
3628
3720
|
}
|
|
3629
3721
|
/**
|
|
3630
3722
|
* Lightweight client-side syntax error detection.
|