sonance-brand-mcp 1.3.60 → 1.3.62
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.
|
@@ -3,7 +3,7 @@ import * as fs from "fs";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import Anthropic from "@anthropic-ai/sdk";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
|
-
import { discoverTheme
|
|
6
|
+
import { discoverTheme } from "./theme-discovery";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Sonance DevTools API - Apply-First Vision Mode
|
|
@@ -511,81 +511,16 @@ function searchFilesSmart(
|
|
|
511
511
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
-
const VISION_SYSTEM_PROMPT = `You
|
|
515
|
-
|
|
516
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
517
|
-
STEP 1: ANALYZE (Think like Cursor)
|
|
518
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
519
|
-
|
|
520
|
-
Before writing any patches, analyze the screenshot and answer:
|
|
521
|
-
|
|
522
|
-
1. CURRENT STATE: What do I see in the screenshot?
|
|
523
|
-
- Is the element visible/readable right now?
|
|
524
|
-
- What colors/styles are currently applied?
|
|
525
|
-
|
|
526
|
-
2. PROBLEM IDENTIFICATION: What's actually wrong?
|
|
527
|
-
- Contrast issue? (text blending with background)
|
|
528
|
-
- Hidden element? (not showing at all)
|
|
529
|
-
- Hover state issue? (only visible on interaction)
|
|
530
|
-
- Layout issue? (wrong size/position)
|
|
531
|
-
- Or is it already correct?
|
|
532
|
-
|
|
533
|
-
3. FIX STRATEGY: What should I change?
|
|
534
|
-
- DEFAULT state only (element needs to look different normally)
|
|
535
|
-
- HOVER state only (element needs different hover effect)
|
|
536
|
-
- BOTH states (element needs overall visibility improvement)
|
|
537
|
-
- ASK for clarification (request is too vague)
|
|
538
|
-
|
|
539
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
540
|
-
STEP 2: GENERATE PATCHES (Only after reasoning)
|
|
541
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
542
|
-
|
|
543
|
-
CRITICAL - FILE SELECTION:
|
|
544
|
-
- You MUST modify the file marked "TARGET COMPONENT" - this is the file you have FULL visibility into
|
|
545
|
-
- Do NOT try to modify other files listed in context - you only see their imports/exports, not full content
|
|
546
|
-
- If the TARGET COMPONENT is not the right file, return clarification_needed
|
|
547
|
-
|
|
548
|
-
COPY EXACTLY from the TARGET COMPONENT section:
|
|
549
|
-
- Your "search" string must match the file CHARACTER-FOR-CHARACTER
|
|
550
|
-
- Include exact indentation and at least 3 lines of context
|
|
551
|
-
- If the element looks FINE in the screenshot, say so
|
|
514
|
+
const VISION_SYSTEM_PROMPT = `You edit code. Return ONLY valid JSON - no explanation, no preamble, no markdown.
|
|
552
515
|
|
|
553
516
|
RULES:
|
|
554
|
-
1.
|
|
555
|
-
2.
|
|
556
|
-
3.
|
|
557
|
-
4.
|
|
558
|
-
5. If visibility issue: prefer explicit colors (text-white, text-black) over semantic tokens
|
|
559
|
-
|
|
560
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
561
|
-
RESPONSE FORMAT
|
|
562
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
517
|
+
1. Copy code EXACTLY from the file - character for character
|
|
518
|
+
2. Make the SMALLEST possible change
|
|
519
|
+
3. For color changes, just change the className
|
|
520
|
+
4. Do not restructure or reorganize code
|
|
563
521
|
|
|
564
|
-
|
|
565
|
-
{
|
|
566
|
-
"analysis": {
|
|
567
|
-
"currentState": "what I see in the screenshot",
|
|
568
|
-
"problem": "the actual issue (or 'none - element appears correct')",
|
|
569
|
-
"fixStrategy": "default|hover|both|clarification_needed"
|
|
570
|
-
},
|
|
571
|
-
"reasoning": "my diagnosis and approach",
|
|
572
|
-
"modifications": [{
|
|
573
|
-
"filePath": "path/to/file.tsx",
|
|
574
|
-
"patches": [{
|
|
575
|
-
"search": "EXACT copy from file",
|
|
576
|
-
"replace": "changed code",
|
|
577
|
-
"explanation": "what this changes and why"
|
|
578
|
-
}]
|
|
579
|
-
}],
|
|
580
|
-
"explanation": "summary of what was changed"
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
If the element looks correct, return:
|
|
584
|
-
{
|
|
585
|
-
"analysis": { "currentState": "...", "problem": "none", "fixStrategy": "clarification_needed" },
|
|
586
|
-
"modifications": [],
|
|
587
|
-
"explanation": "The element appears visible/correct in the screenshot. Please specify what needs to change."
|
|
588
|
-
}`;
|
|
522
|
+
RESPOND WITH ONLY THIS JSON FORMAT (nothing else):
|
|
523
|
+
{"modifications":[{"filePath":"path","patches":[{"search":"exact code","replace":"changed code"}]}]}`;
|
|
589
524
|
|
|
590
525
|
export async function POST(request: Request) {
|
|
591
526
|
// Only allow in development
|
|
@@ -842,172 +777,93 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
|
|
|
842
777
|
`;
|
|
843
778
|
}
|
|
844
779
|
|
|
845
|
-
// ========== TARGET COMPONENT (
|
|
780
|
+
// ========== TARGET COMPONENT ONLY (with line numbers) ==========
|
|
781
|
+
// CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
|
|
846
782
|
if (recommendedFileContent) {
|
|
847
|
-
// Never truncate the recommended file - AI needs full context to avoid hallucination
|
|
848
783
|
const content = recommendedFileContent.content;
|
|
849
784
|
|
|
785
|
+
// Add line numbers to make it easy for LLM to reference exact code
|
|
786
|
+
const linesWithNumbers = content.split('\n').map((line, i) =>
|
|
787
|
+
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
788
|
+
).join('\n');
|
|
789
|
+
|
|
850
790
|
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
851
|
-
|
|
791
|
+
FILE TO EDIT: ${recommendedFileContent.path}
|
|
852
792
|
═══════════════════════════════════════════════════════════════════════════════
|
|
853
793
|
|
|
854
|
-
|
|
855
|
-
|
|
794
|
+
IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
|
|
795
|
+
Your "search" string must match the code CHARACTER FOR CHARACTER.
|
|
856
796
|
|
|
857
797
|
\`\`\`tsx
|
|
858
|
-
${
|
|
798
|
+
${linesWithNumbers}
|
|
859
799
|
\`\`\`
|
|
860
800
|
|
|
861
801
|
`;
|
|
862
802
|
usedContext += content.length;
|
|
863
|
-
debugLog("Added TARGET COMPONENT
|
|
803
|
+
debugLog("Added TARGET COMPONENT with line numbers", {
|
|
864
804
|
path: recommendedFileContent.path,
|
|
865
|
-
|
|
866
|
-
|
|
805
|
+
lines: content.split('\n').length,
|
|
806
|
+
size: content.length
|
|
867
807
|
});
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
textContent += `PAGE CONTEXT (wrapper only${recommendedFileContent ? " - DO NOT edit this, edit the TARGET COMPONENT above" : ""}):
|
|
875
|
-
|
|
876
|
-
Page File: ${pageContext.pageFile || "Not found"}
|
|
877
|
-
${pageContext.pageContent ? `\`\`\`tsx\n${pageContentTruncated}${pageWasTruncated ? "\n// ... (wrapper truncated)" : ""}\n\`\`\`` : ""}
|
|
878
|
-
|
|
879
|
-
`;
|
|
880
|
-
usedContext += pageContentTruncated.length;
|
|
881
|
-
|
|
882
|
-
// ========== SUPPORTING COMPONENTS (dynamic budget) ==========
|
|
883
|
-
if (pageContext.componentSources.length > 0) {
|
|
884
|
-
const remainingBudget = TOTAL_CONTEXT_BUDGET - usedContext - MAX_GLOBALS_CSS - 5000; // Reserve 5k for instructions
|
|
885
|
-
const filesToInclude = pageContext.componentSources.slice(0, MAX_FILES);
|
|
886
|
-
const perFileLimit = Math.max(1000, Math.floor(remainingBudget / Math.max(filesToInclude.length, 1)));
|
|
887
|
-
|
|
888
|
-
textContent += `SUPPORTING COMPONENTS (${filesToInclude.length} files, ~${Math.round(perFileLimit/1000)}k chars each):\n`;
|
|
808
|
+
} else if (pageContext.pageContent) {
|
|
809
|
+
// Fallback: use page file if no recommended file
|
|
810
|
+
const content = pageContext.pageContent;
|
|
811
|
+
const linesWithNumbers = content.split('\n').map((line, i) =>
|
|
812
|
+
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
813
|
+
).join('\n');
|
|
889
814
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const truncatedContent = comp.content.substring(0, perFileLimit);
|
|
897
|
-
const wasTruncated = comp.content.length > perFileLimit;
|
|
898
|
-
|
|
899
|
-
textContent += `
|
|
900
|
-
File: ${comp.path}
|
|
815
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
816
|
+
FILE TO EDIT: ${pageContext.pageFile}
|
|
817
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
818
|
+
|
|
901
819
|
\`\`\`tsx
|
|
902
|
-
${
|
|
820
|
+
${linesWithNumbers}
|
|
903
821
|
\`\`\`
|
|
822
|
+
|
|
904
823
|
`;
|
|
905
|
-
|
|
906
|
-
}
|
|
824
|
+
usedContext += content.length;
|
|
907
825
|
}
|
|
826
|
+
|
|
827
|
+
// NOTE: We intentionally skip SUPPORTING COMPONENTS to reduce noise
|
|
828
|
+
// The LLM only needs the TARGET file to make accurate edits
|
|
908
829
|
|
|
909
830
|
// ========== THEME DISCOVERY (REFERENCE ONLY) ==========
|
|
910
|
-
// Dynamically discover theme tokens
|
|
831
|
+
// Dynamically discover theme tokens (minimal - just for logging)
|
|
911
832
|
const discoveredTheme = await discoverTheme(projectRoot);
|
|
912
|
-
const themeContext = formatThemeForPrompt(discoveredTheme);
|
|
913
|
-
|
|
914
|
-
// Check if this is a visibility-related request
|
|
915
|
-
const isVisibilityRequest = /visible|can't see|cant see|not visible|hidden|color|contrast/i.test(userPrompt);
|
|
916
833
|
|
|
917
834
|
if (discoveredTheme.discoveredFiles.length > 0) {
|
|
918
|
-
// Only include detailed color guidance for visibility requests
|
|
919
|
-
if (isVisibilityRequest) {
|
|
920
|
-
// Extract available text color classes from discovered theme
|
|
921
|
-
const textColorClasses = Object.keys(discoveredTheme.cssVariables)
|
|
922
|
-
.filter(key => key.includes('foreground') || key.includes('text'))
|
|
923
|
-
.map(key => `text-${key.replace('--', '').replace(/-/g, '-')}`)
|
|
924
|
-
.slice(0, 10);
|
|
925
|
-
|
|
926
|
-
textContent += `
|
|
927
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
928
|
-
COLOR FIX GUIDANCE (for visibility issues only)
|
|
929
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
930
|
-
|
|
931
|
-
AVAILABLE TEXT COLORS (use these for contrast fixes):
|
|
932
|
-
- text-white (always visible on dark backgrounds)
|
|
933
|
-
- text-black (always visible on light backgrounds)
|
|
934
|
-
- text-foreground (theme default)
|
|
935
|
-
- text-primary-foreground (for bg-primary)
|
|
936
|
-
- text-accent-foreground (for bg-accent - CHECK IF THIS HAS GOOD CONTRAST)
|
|
937
|
-
- text-destructive-foreground (for bg-destructive)
|
|
938
|
-
|
|
939
|
-
SAFE BUTTON PATTERNS:
|
|
940
|
-
- bg-accent text-white (cyan button, guaranteed visible)
|
|
941
|
-
- bg-primary text-white (charcoal button, guaranteed visible)
|
|
942
|
-
- bg-destructive text-white (red button, guaranteed visible)
|
|
943
|
-
- bg-muted text-foreground (gray button, guaranteed visible)
|
|
944
|
-
|
|
945
|
-
IMPORTANT: If current code has text-accent-foreground or similar semantic colors
|
|
946
|
-
that result in poor contrast, REPLACE with text-white or text-black.
|
|
947
|
-
|
|
948
|
-
`;
|
|
949
|
-
} else {
|
|
950
|
-
// For non-visibility requests, minimal reference
|
|
951
|
-
textContent += `
|
|
952
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
953
|
-
REFERENCE ONLY (do not use this to justify additional changes)
|
|
954
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
955
|
-
Theme discovered from: ${discoveredTheme.discoveredFiles.join(', ')}
|
|
956
|
-
`;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
835
|
debugLog("Theme discovery complete", {
|
|
960
836
|
filesFound: discoveredTheme.discoveredFiles,
|
|
961
|
-
cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
|
|
962
|
-
tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
|
|
963
|
-
isVisibilityRequest,
|
|
964
837
|
});
|
|
965
838
|
}
|
|
966
839
|
|
|
967
|
-
// ==========
|
|
968
|
-
const
|
|
969
|
-
textContent += `
|
|
970
|
-
GLOBALS.CSS (theme variables):
|
|
971
|
-
\`\`\`css
|
|
972
|
-
${globalsTruncated}${pageContext.globalsCSS.length > MAX_GLOBALS_CSS ? "\n/* ... (truncated) */" : ""}
|
|
973
|
-
\`\`\`
|
|
974
|
-
|
|
975
|
-
`;
|
|
976
|
-
|
|
977
|
-
// ========== VALID FILES LIST ==========
|
|
978
|
-
const validFilesList: string[] = [];
|
|
979
|
-
const recommendedPath = recommendedFile?.path;
|
|
840
|
+
// ========== SIMPLIFIED INSTRUCTIONS ==========
|
|
841
|
+
const targetPath = recommendedFileContent?.path || pageContext.pageFile || "unknown";
|
|
980
842
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// Add page file (if not the recommended file)
|
|
987
|
-
if (pageContext.pageFile && pageContext.pageFile !== recommendedPath) {
|
|
988
|
-
validFilesList.push(`- ${pageContext.pageFile} (wrapper only)`);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// Add other component sources (excluding recommended file)
|
|
992
|
-
for (const comp of pageContext.componentSources) {
|
|
993
|
-
if (comp.path !== recommendedPath) {
|
|
994
|
-
validFilesList.push(`- ${comp.path}`);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
843
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
844
|
+
HOW TO MAKE YOUR EDIT
|
|
845
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
997
846
|
|
|
998
|
-
|
|
999
|
-
|
|
847
|
+
1. Find the EXACT code in the file above that needs to change
|
|
848
|
+
2. COPY that code CHARACTER FOR CHARACTER (use the line numbers as reference)
|
|
849
|
+
3. Make your change to the copied code
|
|
850
|
+
4. Return a patch with the exact "search" and "replace" strings
|
|
1000
851
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
2. The TARGET COMPONENT shown first contains the UI - EDIT THAT FILE
|
|
1004
|
-
3. Make SURGICAL EDITS - change only the specific lines needed
|
|
1005
|
-
4. PRESERVE all existing logic, hooks, API calls, and error handling
|
|
1006
|
-
5. Return patches in the specified format (search/replace)
|
|
1007
|
-
6. Only use file paths from the VALID FILES list above
|
|
1008
|
-
7. DO NOT edit the page wrapper unless the TARGET COMPONENT is unavailable
|
|
852
|
+
EXAMPLE - If line 84 shows:
|
|
853
|
+
84| <div className="mb-10 md:mb-16 grid grid-cols-2">
|
|
1009
854
|
|
|
1010
|
-
|
|
855
|
+
Your patch should be:
|
|
856
|
+
{
|
|
857
|
+
"modifications": [{
|
|
858
|
+
"filePath": "${targetPath}",
|
|
859
|
+
"patches": [{
|
|
860
|
+
"search": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2\\">",
|
|
861
|
+
"replace": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2 bg-accent\\">"
|
|
862
|
+
}]
|
|
863
|
+
}]
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
CRITICAL: Your "search" string MUST exist in the file. If you can't find the exact code, return empty modifications.`;
|
|
1011
867
|
|
|
1012
868
|
messageContent.push({
|
|
1013
869
|
type: "text",
|
|
@@ -1037,7 +893,6 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
|
|
|
1037
893
|
let lastPatchErrors: string[] = [];
|
|
1038
894
|
let modifications: VisionFileModification[] = [];
|
|
1039
895
|
let finalExplanation: string | undefined;
|
|
1040
|
-
let finalReasoning: string | undefined;
|
|
1041
896
|
|
|
1042
897
|
while (retryCount <= MAX_RETRIES) {
|
|
1043
898
|
// Build messages for this attempt
|
|
@@ -1087,120 +942,118 @@ This is better than generating patches with made-up code.`,
|
|
|
1087
942
|
});
|
|
1088
943
|
}
|
|
1089
944
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
945
|
+
const response = await anthropic.messages.create({
|
|
946
|
+
model: "claude-sonnet-4-20250514",
|
|
947
|
+
max_tokens: 16384,
|
|
1093
948
|
messages: currentMessages,
|
|
1094
|
-
|
|
1095
|
-
|
|
949
|
+
system: VISION_SYSTEM_PROMPT,
|
|
950
|
+
});
|
|
1096
951
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
952
|
+
// Extract text content from response
|
|
953
|
+
const textResponse = response.content.find((block) => block.type === "text");
|
|
954
|
+
if (!textResponse || textResponse.type !== "text") {
|
|
955
|
+
return NextResponse.json(
|
|
956
|
+
{ error: "No text response from AI" },
|
|
957
|
+
{ status: 500 }
|
|
958
|
+
);
|
|
959
|
+
}
|
|
1105
960
|
|
|
1106
|
-
// Parse AI response -
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
fixStrategy?: string;
|
|
1112
|
-
};
|
|
1113
|
-
reasoning?: string;
|
|
1114
|
-
modifications: Array<{
|
|
1115
|
-
filePath: string;
|
|
1116
|
-
patches?: Patch[];
|
|
1117
|
-
// Legacy support for modifiedContent (will be deprecated)
|
|
1118
|
-
modifiedContent?: string;
|
|
1119
|
-
explanation?: string;
|
|
1120
|
-
}>;
|
|
961
|
+
// Parse AI response - expecting modifications array
|
|
962
|
+
let aiResponse: {
|
|
963
|
+
modifications: Array<{
|
|
964
|
+
filePath: string;
|
|
965
|
+
patches?: Patch[];
|
|
1121
966
|
explanation?: string;
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
let jsonText = textResponse.text.trim();
|
|
1126
|
-
|
|
1127
|
-
// Try to extract JSON from markdown code blocks
|
|
1128
|
-
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
1129
|
-
jsonText.match(/```\n([\s\S]*?)\n```/);
|
|
967
|
+
}>;
|
|
968
|
+
explanation?: string;
|
|
969
|
+
};
|
|
1130
970
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
971
|
+
try {
|
|
972
|
+
let jsonText = textResponse.text.trim();
|
|
973
|
+
|
|
974
|
+
// Try to extract JSON from markdown code blocks
|
|
975
|
+
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
976
|
+
jsonText.match(/```\n([\s\S]*?)\n```/);
|
|
977
|
+
|
|
978
|
+
if (jsonMatch) {
|
|
979
|
+
jsonText = jsonMatch[1];
|
|
980
|
+
} else if (jsonText.includes("```json")) {
|
|
981
|
+
const start = jsonText.indexOf("```json") + 7;
|
|
982
|
+
const end = jsonText.lastIndexOf("```");
|
|
983
|
+
if (end > start) {
|
|
984
|
+
jsonText = jsonText.substring(start, end);
|
|
1139
985
|
}
|
|
986
|
+
}
|
|
1140
987
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
988
|
+
jsonText = jsonText.trim();
|
|
989
|
+
|
|
990
|
+
// Robust JSON extraction: look for {"modifications" pattern specifically
|
|
991
|
+
// This handles cases where the LLM includes preamble text with code blocks
|
|
992
|
+
const jsonStartPatterns = ['{"modifications"', '{ "modifications"', '{\n "modifications"'];
|
|
993
|
+
let jsonStart = -1;
|
|
994
|
+
|
|
995
|
+
for (const pattern of jsonStartPatterns) {
|
|
996
|
+
const idx = jsonText.indexOf(pattern);
|
|
997
|
+
if (idx !== -1 && (jsonStart === -1 || idx < jsonStart)) {
|
|
998
|
+
jsonStart = idx;
|
|
1149
999
|
}
|
|
1150
|
-
|
|
1151
|
-
aiResponse = JSON.parse(jsonText);
|
|
1152
|
-
} catch {
|
|
1153
|
-
console.error("Failed to parse AI response:", textResponse.text);
|
|
1154
|
-
return NextResponse.json(
|
|
1155
|
-
{ error: "Failed to parse AI response. Please try again." },
|
|
1156
|
-
{ status: 500 }
|
|
1157
|
-
);
|
|
1158
1000
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1001
|
+
|
|
1002
|
+
if (jsonStart !== -1) {
|
|
1003
|
+
// Find the matching closing brace by counting braces
|
|
1004
|
+
let braceCount = 0;
|
|
1005
|
+
let jsonEnd = -1;
|
|
1006
|
+
for (let i = jsonStart; i < jsonText.length; i++) {
|
|
1007
|
+
if (jsonText[i] === '{') braceCount++;
|
|
1008
|
+
if (jsonText[i] === '}') {
|
|
1009
|
+
braceCount--;
|
|
1010
|
+
if (braceCount === 0) {
|
|
1011
|
+
jsonEnd = i;
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (jsonEnd !== -1) {
|
|
1017
|
+
jsonText = jsonText.substring(jsonStart, jsonEnd + 1);
|
|
1018
|
+
}
|
|
1019
|
+
} else {
|
|
1020
|
+
// Fallback: try first { to last }
|
|
1021
|
+
const firstBrace = jsonText.indexOf('{');
|
|
1022
|
+
const lastBrace = jsonText.lastIndexOf('}');
|
|
1023
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
1024
|
+
jsonText = jsonText.substring(firstBrace, lastBrace + 1);
|
|
1025
|
+
}
|
|
1167
1026
|
}
|
|
1027
|
+
|
|
1028
|
+
aiResponse = JSON.parse(jsonText);
|
|
1029
|
+
} catch {
|
|
1030
|
+
console.error("Failed to parse AI response:", textResponse.text);
|
|
1031
|
+
return NextResponse.json(
|
|
1032
|
+
{ error: "Failed to parse AI response. Please try again." },
|
|
1033
|
+
{ status: 500 }
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1168
1036
|
|
|
1169
1037
|
finalExplanation = aiResponse.explanation;
|
|
1170
|
-
finalReasoning = aiResponse.reasoning;
|
|
1171
|
-
|
|
1172
|
-
// If LLM says clarification is needed, return that to the user
|
|
1173
|
-
if (aiResponse.analysis?.fixStrategy === "clarification_needed" &&
|
|
1174
|
-
(!aiResponse.modifications || aiResponse.modifications.length === 0)) {
|
|
1175
|
-
return NextResponse.json({
|
|
1176
|
-
success: true,
|
|
1177
|
-
sessionId: newSessionId,
|
|
1178
|
-
needsClarification: true,
|
|
1179
|
-
analysis: aiResponse.analysis,
|
|
1180
|
-
explanation: aiResponse.explanation || "The element appears correct. Please specify what needs to change.",
|
|
1181
|
-
modifications: [],
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
1038
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
reasoning: aiResponse.reasoning,
|
|
1039
|
+
if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
|
|
1040
|
+
return NextResponse.json({
|
|
1041
|
+
success: true,
|
|
1042
|
+
sessionId: newSessionId,
|
|
1043
|
+
modifications: [],
|
|
1044
|
+
explanation: aiResponse.explanation || "No changes needed.",
|
|
1192
1045
|
});
|
|
1193
1046
|
}
|
|
1194
1047
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1048
|
+
debugLog("VALIDATION: Valid file paths from page context", {
|
|
1049
|
+
pageFile: pageContext.pageFile,
|
|
1050
|
+
validFilePaths: Array.from(validFilePaths),
|
|
1051
|
+
aiRequestedFiles: aiResponse.modifications.map(m => m.filePath)
|
|
1052
|
+
});
|
|
1200
1053
|
|
|
1201
|
-
|
|
1054
|
+
// Process modifications - apply patches to get modified content
|
|
1202
1055
|
modifications = [];
|
|
1203
|
-
|
|
1056
|
+
const patchErrors: string[] = [];
|
|
1204
1057
|
|
|
1205
1058
|
for (const mod of aiResponse.modifications) {
|
|
1206
1059
|
// Validate that the file path is in the page context
|
|
@@ -1332,24 +1185,13 @@ This is better than generating patches with made-up code.`,
|
|
|
1332
1185
|
const spanDiff = Math.abs(openSpans - closeSpans);
|
|
1333
1186
|
if (divDiff > 0 || spanDiff > 0) {
|
|
1334
1187
|
patchErrors.push(`${mod.filePath}: LLM introduced syntax error - tag mismatch detected (${divDiff} div, ${spanDiff} span). Change rejected.`);
|
|
1335
|
-
|
|
1188
|
+
continue;
|
|
1336
1189
|
}
|
|
1337
1190
|
}
|
|
1338
1191
|
}
|
|
1339
|
-
} else if (mod.modifiedContent) {
|
|
1340
|
-
// Legacy: AI returned full file content
|
|
1341
|
-
console.warn(`[Apply-First] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
|
|
1342
|
-
modifiedContent = mod.modifiedContent;
|
|
1343
|
-
|
|
1344
|
-
// Validate the modification using legacy validation
|
|
1345
|
-
const validation = validateModification(originalContent, modifiedContent, mod.filePath);
|
|
1346
|
-
if (!validation.valid) {
|
|
1347
|
-
patchErrors.push(`${mod.filePath}: ${validation.error}`);
|
|
1348
|
-
continue;
|
|
1349
|
-
}
|
|
1350
1192
|
} else {
|
|
1351
|
-
// No patches
|
|
1352
|
-
console.warn(`[Apply-First] No patches
|
|
1193
|
+
// No patches - skip
|
|
1194
|
+
console.warn(`[Apply-First] No patches for ${mod.filePath}`);
|
|
1353
1195
|
continue;
|
|
1354
1196
|
}
|
|
1355
1197
|
|
|
@@ -1363,7 +1205,7 @@ This is better than generating patches with made-up code.`,
|
|
|
1363
1205
|
}
|
|
1364
1206
|
|
|
1365
1207
|
// If all modifications failed, check if we should retry
|
|
1366
|
-
|
|
1208
|
+
if (patchErrors.length > 0 && modifications.length === 0) {
|
|
1367
1209
|
if (retryCount < MAX_RETRIES) {
|
|
1368
1210
|
console.warn(`[Apply-First] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
|
|
1369
1211
|
debugLog("Retry triggered due to patch failures", {
|
|
@@ -1378,20 +1220,20 @@ This is better than generating patches with made-up code.`,
|
|
|
1378
1220
|
|
|
1379
1221
|
// Exhausted retries, return error
|
|
1380
1222
|
console.error("All AI patches failed after retries:", patchErrors);
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1223
|
+
return NextResponse.json(
|
|
1224
|
+
{
|
|
1225
|
+
success: false,
|
|
1384
1226
|
error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1227
|
+
},
|
|
1228
|
+
{ status: 400 }
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Log patch errors as warnings if some modifications succeeded
|
|
1233
|
+
if (patchErrors.length > 0) {
|
|
1234
|
+
console.warn("Some patches failed:", patchErrors);
|
|
1235
|
+
}
|
|
1389
1236
|
|
|
1390
|
-
// Log patch errors as warnings if some modifications succeeded
|
|
1391
|
-
if (patchErrors.length > 0) {
|
|
1392
|
-
console.warn("Some patches failed:", patchErrors);
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
1237
|
// Successfully processed at least some modifications - break out of retry loop
|
|
1396
1238
|
break;
|
|
1397
1239
|
} // End of retry loop
|
|
@@ -1408,7 +1250,6 @@ This is better than generating patches with made-up code.`,
|
|
|
1408
1250
|
preview: true,
|
|
1409
1251
|
modifications,
|
|
1410
1252
|
explanation: finalExplanation,
|
|
1411
|
-
reasoning: finalReasoning,
|
|
1412
1253
|
});
|
|
1413
1254
|
}
|
|
1414
1255
|
|
|
@@ -1432,7 +1273,6 @@ This is better than generating patches with made-up code.`,
|
|
|
1432
1273
|
modifications,
|
|
1433
1274
|
backupPaths: applyResult.backupPaths,
|
|
1434
1275
|
explanation: finalExplanation,
|
|
1435
|
-
reasoning: finalReasoning,
|
|
1436
1276
|
});
|
|
1437
1277
|
}
|
|
1438
1278
|
|
|
@@ -2244,13 +2084,13 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
2244
2084
|
|
|
2245
2085
|
// Only log parse error once to avoid log spam
|
|
2246
2086
|
if (!cachedParseError) {
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2087
|
+
const posMatch = errorStr.match(/position (\d+)/);
|
|
2088
|
+
let context = "";
|
|
2089
|
+
if (posMatch) {
|
|
2090
|
+
const pos = parseInt(posMatch[1], 10);
|
|
2091
|
+
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
|
2092
|
+
context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
|
|
2093
|
+
}
|
|
2254
2094
|
debugLog("[apply] Failed to parse tsconfig.json (will use defaults, logging once)", { error: errorStr, context });
|
|
2255
2095
|
cachedParseError = errorStr;
|
|
2256
2096
|
}
|
|
@@ -2273,14 +2113,14 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
2273
2113
|
}
|
|
2274
2114
|
// Only log default alias once (not when using cached error fallback)
|
|
2275
2115
|
if (!cachedParseError) {
|
|
2276
|
-
|
|
2116
|
+
debugLog("[apply] Using default @/ alias", { alias: aliases.get("@/") });
|
|
2277
2117
|
}
|
|
2278
2118
|
}
|
|
2279
2119
|
|
|
2280
2120
|
// Cache aliases if parsed successfully, no tsconfig exists, OR we have a parse error (cache fallback)
|
|
2281
2121
|
if (parsedSuccessfully || !fs.existsSync(tsconfigPath) || cachedParseError) {
|
|
2282
|
-
|
|
2283
|
-
|
|
2122
|
+
cachedPathAliases = aliases;
|
|
2123
|
+
cachedProjectRoot = projectRoot;
|
|
2284
2124
|
}
|
|
2285
2125
|
|
|
2286
2126
|
return aliases;
|
|
@@ -2582,105 +2422,3 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
|
|
|
2582
2422
|
failedPatches,
|
|
2583
2423
|
};
|
|
2584
2424
|
}
|
|
2585
|
-
|
|
2586
|
-
/**
|
|
2587
|
-
* Validate that AI modifications are surgical edits, not complete rewrites
|
|
2588
|
-
*/
|
|
2589
|
-
interface ValidationResult {
|
|
2590
|
-
valid: boolean;
|
|
2591
|
-
error?: string;
|
|
2592
|
-
warnings: string[];
|
|
2593
|
-
}
|
|
2594
|
-
|
|
2595
|
-
function validateModification(
|
|
2596
|
-
originalContent: string,
|
|
2597
|
-
modifiedContent: string,
|
|
2598
|
-
filePath: string
|
|
2599
|
-
): ValidationResult {
|
|
2600
|
-
const warnings: string[] = [];
|
|
2601
|
-
|
|
2602
|
-
// Skip validation for new files (no original content)
|
|
2603
|
-
if (!originalContent || originalContent.trim() === "") {
|
|
2604
|
-
return { valid: true, warnings: ["New file - no original to compare"] };
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
const originalLines = originalContent.split("\n");
|
|
2608
|
-
const modifiedLines = modifiedContent.split("\n");
|
|
2609
|
-
|
|
2610
|
-
// Check 1: Truncation detection - look for placeholder comments
|
|
2611
|
-
const truncationPatterns = [
|
|
2612
|
-
/\/\/\s*\.\.\.\s*existing/i,
|
|
2613
|
-
/\/\/\s*\.\.\.\s*rest\s*of/i,
|
|
2614
|
-
/\/\/\s*\.\.\.\s*more\s*code/i,
|
|
2615
|
-
/\/\*\s*\.\.\.\s*\*\//,
|
|
2616
|
-
/\/\/\s*\.\.\./,
|
|
2617
|
-
];
|
|
2618
|
-
|
|
2619
|
-
for (const pattern of truncationPatterns) {
|
|
2620
|
-
if (pattern.test(modifiedContent)) {
|
|
2621
|
-
return {
|
|
2622
|
-
valid: false,
|
|
2623
|
-
error: `File ${filePath} contains truncation placeholder (e.g., "// ... existing code"). The AI must return the complete file content. Please try again.`,
|
|
2624
|
-
warnings,
|
|
2625
|
-
};
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
// Check 2: Line count shrinkage - reject if file shrinks by more than 30%
|
|
2630
|
-
const lineDelta = modifiedLines.length - originalLines.length;
|
|
2631
|
-
const shrinkagePercent = (lineDelta / originalLines.length) * 100;
|
|
2632
|
-
|
|
2633
|
-
if (shrinkagePercent < -30) {
|
|
2634
|
-
return {
|
|
2635
|
-
valid: false,
|
|
2636
|
-
error: `File ${filePath} shrank from ${originalLines.length} to ${modifiedLines.length} lines (${Math.abs(shrinkagePercent).toFixed(0)}% reduction). This suggests the AI rewrote the file instead of making surgical edits. Please try a more specific request.`,
|
|
2637
|
-
warnings,
|
|
2638
|
-
};
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
if (shrinkagePercent < -15) {
|
|
2642
|
-
warnings.push(`File shrank by ${Math.abs(shrinkagePercent).toFixed(0)}% - review carefully`);
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
// Check 3: Change percentage - warn if too many lines are different
|
|
2646
|
-
let changedLines = 0;
|
|
2647
|
-
const minLines = Math.min(originalLines.length, modifiedLines.length);
|
|
2648
|
-
|
|
2649
|
-
for (let i = 0; i < minLines; i++) {
|
|
2650
|
-
if (originalLines[i] !== modifiedLines[i]) {
|
|
2651
|
-
changedLines++;
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
// Add lines that were added or removed
|
|
2656
|
-
changedLines += Math.abs(originalLines.length - modifiedLines.length);
|
|
2657
|
-
|
|
2658
|
-
const changePercent = (changedLines / originalLines.length) * 100;
|
|
2659
|
-
|
|
2660
|
-
if (changePercent > 50) {
|
|
2661
|
-
return {
|
|
2662
|
-
valid: false,
|
|
2663
|
-
error: `File ${filePath} has ${changePercent.toFixed(0)}% of lines changed. This suggests the AI rewrote the file instead of making surgical edits. For safety, this change has been rejected. Please try a more specific request.`,
|
|
2664
|
-
warnings,
|
|
2665
|
-
};
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
|
-
if (changePercent > 30) {
|
|
2669
|
-
warnings.push(`${changePercent.toFixed(0)}% of lines changed - larger than expected for a surgical edit`);
|
|
2670
|
-
}
|
|
2671
|
-
|
|
2672
|
-
// Check 4: Import preservation - ensure imports aren't removed
|
|
2673
|
-
const importRegex = /^import\s+/gm;
|
|
2674
|
-
const originalImports = (originalContent.match(importRegex) || []).length;
|
|
2675
|
-
const modifiedImports = (modifiedContent.match(importRegex) || []).length;
|
|
2676
|
-
|
|
2677
|
-
if (modifiedImports < originalImports * 0.5 && originalImports > 2) {
|
|
2678
|
-
return {
|
|
2679
|
-
valid: false,
|
|
2680
|
-
error: `File ${filePath} went from ${originalImports} imports to ${modifiedImports}. Imports should not be removed. Please try again.`,
|
|
2681
|
-
warnings,
|
|
2682
|
-
};
|
|
2683
|
-
}
|
|
2684
|
-
|
|
2685
|
-
return { valid: true, warnings };
|
|
2686
|
-
}
|