sonance-brand-mcp 1.3.59 → 1.3.61
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.
|
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import Anthropic from "@anthropic-ai/sdk";
|
|
5
|
-
import { discoverTheme
|
|
5
|
+
import { discoverTheme } from "../sonance-vision-apply/theme-discovery";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Sonance DevTools API - Vision Mode Editor
|
|
@@ -507,50 +507,21 @@ function searchFilesSmart(
|
|
|
507
507
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
-
const VISION_SYSTEM_PROMPT = `You
|
|
510
|
+
const VISION_SYSTEM_PROMPT = `You edit code. Make ONLY the change requested.
|
|
511
511
|
|
|
512
512
|
RULES:
|
|
513
|
-
1.
|
|
514
|
-
2.
|
|
515
|
-
3.
|
|
516
|
-
4. Do
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
7. NEVER invent or guess code - your "search" string MUST match the file EXACTLY
|
|
520
|
-
8. NEVER change data mappings (icon names, keys, enum values) unless user explicitly provides the new values
|
|
521
|
-
9. If you don't know what values exist in a database or data source, DO NOT guess - ask for clarification
|
|
522
|
-
|
|
523
|
-
CRITICAL - ELEMENT VERIFICATION:
|
|
524
|
-
- When a focused element is mentioned, SEARCH the provided file content for that EXACT element
|
|
525
|
-
- Look for the actual JSX/HTML code: <button, <Button, <div, className, onClick, etc.
|
|
526
|
-
- If you cannot find the element in the TARGET COMPONENT section, it may be in a child component
|
|
527
|
-
- NEVER guess what element code looks like - find it in the file or report "element not found"
|
|
528
|
-
- If the element is not in the provided file, return: {"modifications": [], "explanation": "The focused element appears to be in a child component, not in [filename]"}
|
|
529
|
-
|
|
530
|
-
CRITICAL - DATA INTEGRITY:
|
|
531
|
-
- If code references database values (like icon_name, type, status), DO NOT change the mapping keys
|
|
532
|
-
- You cannot see database content - only change STRUCTURE, not DATA MAPPINGS
|
|
533
|
-
- Example: If you see iconMap["EyeOff"] = SomeIcon, do NOT change "EyeOff" to something else
|
|
534
|
-
- If user wants different icons, they must tell you the EXACT icon names they want
|
|
535
|
-
|
|
536
|
-
PATCH FORMAT:
|
|
537
|
-
Return ONLY raw JSON (no markdown, no preamble):
|
|
513
|
+
1. Copy code EXACTLY from the file - character for character
|
|
514
|
+
2. Make the SMALLEST possible change
|
|
515
|
+
3. For color changes, just change the className
|
|
516
|
+
4. Do not restructure or reorganize code
|
|
517
|
+
|
|
518
|
+
Return JSON:
|
|
538
519
|
{
|
|
539
|
-
"reasoning": "brief explanation",
|
|
540
520
|
"modifications": [{
|
|
541
|
-
"filePath": "path
|
|
542
|
-
"patches": [{
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
"explanation": "what this does"
|
|
546
|
-
}],
|
|
547
|
-
"previewCSS": "optional CSS for live preview"
|
|
548
|
-
}],
|
|
549
|
-
"aggregatedPreviewCSS": "combined CSS for all changes",
|
|
550
|
-
"explanation": "summary"
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
If you cannot find the exact code to modify, OR if you would need to guess data values, return empty modifications array with explanation.`;
|
|
521
|
+
"filePath": "path",
|
|
522
|
+
"patches": [{ "search": "exact code from file", "replace": "changed code" }]
|
|
523
|
+
}]
|
|
524
|
+
}`;
|
|
554
525
|
|
|
555
526
|
export async function POST(request: Request) {
|
|
556
527
|
// Only allow in development
|
|
@@ -780,143 +751,92 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
|
|
|
780
751
|
`;
|
|
781
752
|
}
|
|
782
753
|
|
|
783
|
-
// ========== TARGET COMPONENT (
|
|
754
|
+
// ========== TARGET COMPONENT ONLY (with line numbers) ==========
|
|
755
|
+
// CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
|
|
784
756
|
if (recommendedFileContent) {
|
|
785
|
-
// Never truncate the recommended file - AI needs full context to avoid hallucination
|
|
786
757
|
const content = recommendedFileContent.content;
|
|
787
758
|
|
|
759
|
+
// Add line numbers to make it easy for LLM to reference exact code
|
|
760
|
+
const linesWithNumbers = content.split('\n').map((line, i) =>
|
|
761
|
+
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
762
|
+
).join('\n');
|
|
763
|
+
|
|
788
764
|
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
789
|
-
|
|
765
|
+
FILE TO EDIT: ${recommendedFileContent.path}
|
|
790
766
|
═══════════════════════════════════════════════════════════════════════════════
|
|
791
767
|
|
|
792
|
-
|
|
793
|
-
|
|
768
|
+
IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
|
|
769
|
+
Your "search" string must match the code CHARACTER FOR CHARACTER.
|
|
794
770
|
|
|
795
771
|
\`\`\`tsx
|
|
796
|
-
${
|
|
772
|
+
${linesWithNumbers}
|
|
797
773
|
\`\`\`
|
|
798
774
|
|
|
799
775
|
`;
|
|
800
776
|
usedContext += content.length;
|
|
801
|
-
debugLog("Added TARGET COMPONENT
|
|
777
|
+
debugLog("Added TARGET COMPONENT with line numbers", {
|
|
802
778
|
path: recommendedFileContent.path,
|
|
803
|
-
|
|
804
|
-
|
|
779
|
+
lines: content.split('\n').length,
|
|
780
|
+
size: content.length
|
|
805
781
|
});
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
textContent += `PAGE CONTEXT (wrapper only${recommendedFileContent ? " - DO NOT edit this, edit the TARGET COMPONENT above" : ""}):
|
|
813
|
-
|
|
814
|
-
Page File: ${pageContext.pageFile || "Not found"}
|
|
815
|
-
${pageContext.pageContent ? `\`\`\`tsx\n${pageContentTruncated}${pageWasTruncated ? "\n// ... (wrapper truncated)" : ""}\n\`\`\`` : ""}
|
|
816
|
-
|
|
817
|
-
`;
|
|
818
|
-
usedContext += pageContentTruncated.length;
|
|
819
|
-
|
|
820
|
-
// ========== SUPPORTING COMPONENTS (dynamic budget) ==========
|
|
821
|
-
if (pageContext.componentSources.length > 0) {
|
|
822
|
-
const remainingBudget = TOTAL_CONTEXT_BUDGET - usedContext - MAX_GLOBALS_CSS - 5000; // Reserve 5k for instructions
|
|
823
|
-
const filesToInclude = pageContext.componentSources.slice(0, MAX_FILES);
|
|
824
|
-
const perFileLimit = Math.max(1000, Math.floor(remainingBudget / Math.max(filesToInclude.length, 1)));
|
|
782
|
+
} else if (pageContext.pageContent) {
|
|
783
|
+
// Fallback: use page file if no recommended file
|
|
784
|
+
const content = pageContext.pageContent;
|
|
785
|
+
const linesWithNumbers = content.split('\n').map((line, i) =>
|
|
786
|
+
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
787
|
+
).join('\n');
|
|
825
788
|
|
|
826
|
-
textContent +=
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
textContent += `\n// ... (remaining files omitted to stay within context limits)\n`;
|
|
831
|
-
break;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
const truncatedContent = comp.content.substring(0, perFileLimit);
|
|
835
|
-
const wasTruncated = comp.content.length > perFileLimit;
|
|
836
|
-
|
|
837
|
-
textContent += `
|
|
838
|
-
File: ${comp.path}
|
|
789
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
790
|
+
FILE TO EDIT: ${pageContext.pageFile}
|
|
791
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
792
|
+
|
|
839
793
|
\`\`\`tsx
|
|
840
|
-
${
|
|
794
|
+
${linesWithNumbers}
|
|
841
795
|
\`\`\`
|
|
796
|
+
|
|
842
797
|
`;
|
|
843
|
-
|
|
844
|
-
}
|
|
798
|
+
usedContext += content.length;
|
|
845
799
|
}
|
|
800
|
+
|
|
801
|
+
// NOTE: We intentionally skip SUPPORTING COMPONENTS to reduce noise
|
|
802
|
+
// The LLM only needs the TARGET file to make accurate edits
|
|
846
803
|
|
|
847
|
-
//
|
|
848
|
-
// Dynamically discover theme tokens from the target codebase
|
|
849
|
-
// This is marked as REFERENCE ONLY so the LLM doesn't use it to justify extra changes
|
|
804
|
+
// Dynamically discover theme tokens (minimal - just for logging)
|
|
850
805
|
const discoveredTheme = await discoverTheme(projectRoot);
|
|
851
|
-
const themeContext = formatThemeForPrompt(discoveredTheme);
|
|
852
806
|
|
|
853
807
|
if (discoveredTheme.discoveredFiles.length > 0) {
|
|
854
|
-
textContent += `
|
|
855
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
856
|
-
REFERENCE ONLY (do not use this to justify additional changes)
|
|
857
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
858
|
-
|
|
859
|
-
If you need to pick a color for a VISIBILITY fix, these are safe choices:
|
|
860
|
-
- bg-accent text-white (cyan button with white text)
|
|
861
|
-
- bg-primary text-white (charcoal button with white text)
|
|
862
|
-
- bg-success text-white (green button with white text)
|
|
863
|
-
- bg-destructive text-white (red button with white text)
|
|
864
|
-
|
|
865
|
-
But ONLY use these if the user is asking for a color/visibility change.
|
|
866
|
-
Do NOT rebrand or change other elements to match.
|
|
867
|
-
|
|
868
|
-
`;
|
|
869
808
|
debugLog("Theme discovery complete", {
|
|
870
809
|
filesFound: discoveredTheme.discoveredFiles,
|
|
871
|
-
cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
|
|
872
|
-
tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
|
|
873
810
|
});
|
|
874
811
|
}
|
|
875
812
|
|
|
876
|
-
// ==========
|
|
877
|
-
const
|
|
878
|
-
textContent += `
|
|
879
|
-
GLOBALS.CSS (theme variables):
|
|
880
|
-
\`\`\`css
|
|
881
|
-
${globalsTruncated}${pageContext.globalsCSS.length > MAX_GLOBALS_CSS ? "\n/* ... (truncated) */" : ""}
|
|
882
|
-
\`\`\`
|
|
883
|
-
|
|
884
|
-
`;
|
|
885
|
-
|
|
886
|
-
// ========== VALID FILES LIST ==========
|
|
887
|
-
const validFilesList: string[] = [];
|
|
888
|
-
const recommendedPath = recommendedFile?.path;
|
|
889
|
-
|
|
890
|
-
// Add recommended file first with marker
|
|
891
|
-
if (recommendedPath) {
|
|
892
|
-
validFilesList.push(`- ${recommendedPath} (*** TARGET - EDIT THIS FILE ***)`);
|
|
893
|
-
}
|
|
813
|
+
// ========== SIMPLIFIED INSTRUCTIONS ==========
|
|
814
|
+
const targetPath = recommendedFileContent?.path || pageContext.pageFile || "unknown";
|
|
894
815
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
validFilesList.push(`- ${comp.path}`);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
816
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
817
|
+
HOW TO MAKE YOUR EDIT
|
|
818
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
819
|
+
|
|
820
|
+
1. Find the EXACT code in the file above that needs to change
|
|
821
|
+
2. COPY that code CHARACTER FOR CHARACTER (use the line numbers as reference)
|
|
822
|
+
3. Make your change to the copied code
|
|
823
|
+
4. Return a patch with the exact "search" and "replace" strings
|
|
906
824
|
|
|
907
|
-
|
|
908
|
-
|
|
825
|
+
EXAMPLE - If line 84 shows:
|
|
826
|
+
84| <div className="mb-10 md:mb-16 grid grid-cols-2">
|
|
909
827
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
828
|
+
Your patch should be:
|
|
829
|
+
{
|
|
830
|
+
"modifications": [{
|
|
831
|
+
"filePath": "${targetPath}",
|
|
832
|
+
"patches": [{
|
|
833
|
+
"search": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2\\">",
|
|
834
|
+
"replace": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2 bg-accent\\">"
|
|
835
|
+
}]
|
|
836
|
+
}]
|
|
837
|
+
}
|
|
918
838
|
|
|
919
|
-
CRITICAL:
|
|
839
|
+
CRITICAL: Your "search" string MUST exist in the file. If you can't find the exact code, return empty modifications.`;
|
|
920
840
|
|
|
921
841
|
messageContent.push({
|
|
922
842
|
type: "text",
|
|
@@ -941,8 +861,6 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
|
|
|
941
861
|
let lastPatchErrors: string[] = [];
|
|
942
862
|
let modificationsWithOriginals: VisionFileModification[] = [];
|
|
943
863
|
let finalExplanation: string | undefined;
|
|
944
|
-
let finalReasoning: string | undefined;
|
|
945
|
-
let finalAggregatedCSS: string | undefined;
|
|
946
864
|
|
|
947
865
|
while (retryCount <= MAX_RETRIES) {
|
|
948
866
|
// Build messages for this attempt
|
|
@@ -992,84 +910,86 @@ This is better than generating patches with made-up code.`,
|
|
|
992
910
|
});
|
|
993
911
|
}
|
|
994
912
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
913
|
+
const response = await anthropic.messages.create({
|
|
914
|
+
model: "claude-sonnet-4-20250514",
|
|
915
|
+
max_tokens: 16384,
|
|
998
916
|
messages: currentMessages,
|
|
999
|
-
|
|
1000
|
-
|
|
917
|
+
system: VISION_SYSTEM_PROMPT,
|
|
918
|
+
});
|
|
1001
919
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
920
|
+
// Extract text content from response
|
|
921
|
+
const textResponse = response.content.find((block) => block.type === "text");
|
|
922
|
+
if (!textResponse || textResponse.type !== "text") {
|
|
923
|
+
return NextResponse.json(
|
|
924
|
+
{ error: "No text response from AI" },
|
|
925
|
+
{ status: 500 }
|
|
926
|
+
);
|
|
927
|
+
}
|
|
1010
928
|
|
|
1011
|
-
// Parse AI response -
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
patches?: Patch[];
|
|
1017
|
-
// Legacy support for modifiedContent (will be deprecated)
|
|
1018
|
-
modifiedContent?: string;
|
|
1019
|
-
explanation?: string;
|
|
1020
|
-
previewCSS?: string;
|
|
1021
|
-
}>;
|
|
1022
|
-
aggregatedPreviewCSS?: string;
|
|
929
|
+
// Parse AI response - expecting modifications array
|
|
930
|
+
let aiResponse: {
|
|
931
|
+
modifications: Array<{
|
|
932
|
+
filePath: string;
|
|
933
|
+
patches?: Patch[];
|
|
1023
934
|
explanation?: string;
|
|
1024
|
-
|
|
935
|
+
previewCSS?: string;
|
|
936
|
+
}>;
|
|
937
|
+
explanation?: string;
|
|
938
|
+
};
|
|
1025
939
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
940
|
+
try {
|
|
941
|
+
let jsonText = textResponse.text.trim();
|
|
942
|
+
|
|
943
|
+
// Try to extract JSON from markdown code blocks
|
|
944
|
+
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
945
|
+
jsonText.match(/```\n([\s\S]*?)\n```/);
|
|
946
|
+
|
|
947
|
+
if (jsonMatch) {
|
|
948
|
+
jsonText = jsonMatch[1];
|
|
949
|
+
} else if (jsonText.includes("```json")) {
|
|
950
|
+
// Fallback for cases where regex might miss due to newlines
|
|
951
|
+
const start = jsonText.indexOf("```json") + 7;
|
|
952
|
+
const end = jsonText.lastIndexOf("```");
|
|
953
|
+
if (end > start) {
|
|
954
|
+
jsonText = jsonText.substring(start, end);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
1043
957
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
aiResponse = JSON.parse(jsonText);
|
|
1056
|
-
} catch {
|
|
1057
|
-
console.error("Failed to parse AI response:", textResponse.text);
|
|
1058
|
-
return NextResponse.json(
|
|
1059
|
-
{ error: "Failed to parse AI response. Please try again." },
|
|
1060
|
-
{ status: 500 }
|
|
1061
|
-
);
|
|
958
|
+
// Clean up any remaining whitespace
|
|
959
|
+
jsonText = jsonText.trim();
|
|
960
|
+
|
|
961
|
+
// Robust JSON extraction: find the first { and last } to extract JSON object
|
|
962
|
+
// This handles cases where the LLM includes preamble text before the JSON
|
|
963
|
+
const firstBrace = jsonText.indexOf('{');
|
|
964
|
+
const lastBrace = jsonText.lastIndexOf('}');
|
|
965
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
966
|
+
jsonText = jsonText.substring(firstBrace, lastBrace + 1);
|
|
1062
967
|
}
|
|
968
|
+
|
|
969
|
+
aiResponse = JSON.parse(jsonText);
|
|
970
|
+
} catch {
|
|
971
|
+
console.error("Failed to parse AI response:", textResponse.text);
|
|
972
|
+
return NextResponse.json(
|
|
973
|
+
{ error: "Failed to parse AI response. Please try again." },
|
|
974
|
+
{ status: 500 }
|
|
975
|
+
);
|
|
976
|
+
}
|
|
1063
977
|
|
|
1064
978
|
finalExplanation = aiResponse.explanation;
|
|
1065
|
-
finalReasoning = aiResponse.reasoning;
|
|
1066
|
-
finalAggregatedCSS = aiResponse.aggregatedPreviewCSS;
|
|
1067
979
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
980
|
+
if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
|
|
981
|
+
return NextResponse.json({
|
|
982
|
+
success: true,
|
|
983
|
+
modifications: [],
|
|
984
|
+
explanation: aiResponse.explanation || "No changes needed.",
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
debugLog("VALIDATION: Known file paths from page context", {
|
|
989
|
+
pageFile: pageContext.pageFile,
|
|
990
|
+
knownPaths: Array.from(knownPaths),
|
|
991
|
+
aiRequestedFiles: (aiResponse.modifications || []).map(m => m.filePath)
|
|
992
|
+
});
|
|
1073
993
|
|
|
1074
994
|
// Validate AI response - trust the LLM to identify the correct file
|
|
1075
995
|
// Only reject paths that are outside the project or don't exist
|
|
@@ -1110,9 +1030,9 @@ This is better than generating patches with made-up code.`,
|
|
|
1110
1030
|
}
|
|
1111
1031
|
}
|
|
1112
1032
|
|
|
1113
|
-
|
|
1033
|
+
// Process modifications - apply patches to get modified content
|
|
1114
1034
|
modificationsWithOriginals = [];
|
|
1115
|
-
|
|
1035
|
+
const patchErrors: string[] = [];
|
|
1116
1036
|
|
|
1117
1037
|
for (const mod of aiResponse.modifications || []) {
|
|
1118
1038
|
const fullPath = path.join(projectRoot, mod.filePath);
|
|
@@ -1190,20 +1110,9 @@ This is better than generating patches with made-up code.`,
|
|
|
1190
1110
|
modifiedContent = patchResult.modifiedContent;
|
|
1191
1111
|
console.log(`[Vision Mode] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
|
|
1192
1112
|
}
|
|
1193
|
-
} else if (mod.modifiedContent) {
|
|
1194
|
-
// Legacy: AI returned full file content
|
|
1195
|
-
console.warn(`[Vision Mode] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
|
|
1196
|
-
modifiedContent = mod.modifiedContent;
|
|
1197
|
-
|
|
1198
|
-
// Validate the modification using legacy validation
|
|
1199
|
-
const validation = validateModification(originalContent, modifiedContent, mod.filePath);
|
|
1200
|
-
if (!validation.valid) {
|
|
1201
|
-
patchErrors.push(`${mod.filePath}: ${validation.error}`);
|
|
1202
|
-
continue;
|
|
1203
|
-
}
|
|
1204
1113
|
} else {
|
|
1205
|
-
// No patches
|
|
1206
|
-
console.warn(`[Vision Mode] No patches
|
|
1114
|
+
// No patches - skip
|
|
1115
|
+
console.warn(`[Vision Mode] No patches for ${mod.filePath}`);
|
|
1207
1116
|
continue;
|
|
1208
1117
|
}
|
|
1209
1118
|
|
|
@@ -1218,7 +1127,7 @@ This is better than generating patches with made-up code.`,
|
|
|
1218
1127
|
}
|
|
1219
1128
|
|
|
1220
1129
|
// If all modifications failed, check if we should retry
|
|
1221
|
-
|
|
1130
|
+
if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
|
|
1222
1131
|
if (retryCount < MAX_RETRIES) {
|
|
1223
1132
|
console.warn(`[Vision Mode] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
|
|
1224
1133
|
debugLog("Retry triggered due to patch failures", {
|
|
@@ -1233,19 +1142,19 @@ This is better than generating patches with made-up code.`,
|
|
|
1233
1142
|
|
|
1234
1143
|
// Exhausted retries, return error
|
|
1235
1144
|
console.error("All AI patches failed after retries:", patchErrors);
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1145
|
+
return NextResponse.json(
|
|
1146
|
+
{
|
|
1147
|
+
success: false,
|
|
1239
1148
|
error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1149
|
+
} as VisionEditResponse,
|
|
1150
|
+
{ status: 400 }
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1244
1153
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1154
|
+
// Log patch errors as warnings if some modifications succeeded
|
|
1155
|
+
if (patchErrors.length > 0) {
|
|
1156
|
+
console.warn("Some patches failed:", patchErrors);
|
|
1157
|
+
}
|
|
1249
1158
|
|
|
1250
1159
|
// Successfully processed at least some modifications - break out of retry loop
|
|
1251
1160
|
break;
|
|
@@ -1260,9 +1169,8 @@ This is better than generating patches with made-up code.`,
|
|
|
1260
1169
|
return NextResponse.json({
|
|
1261
1170
|
success: true,
|
|
1262
1171
|
modifications: modificationsWithOriginals,
|
|
1263
|
-
aggregatedPreviewCSS:
|
|
1172
|
+
aggregatedPreviewCSS: aggregatedCSS,
|
|
1264
1173
|
explanation: finalExplanation,
|
|
1265
|
-
reasoning: finalReasoning,
|
|
1266
1174
|
} as VisionEditResponse);
|
|
1267
1175
|
}
|
|
1268
1176
|
|
|
@@ -1954,13 +1862,13 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1954
1862
|
|
|
1955
1863
|
// Only log parse error once to avoid log spam
|
|
1956
1864
|
if (!cachedParseError) {
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1865
|
+
const posMatch = errorStr.match(/position (\d+)/);
|
|
1866
|
+
let context = "";
|
|
1867
|
+
if (posMatch) {
|
|
1868
|
+
const pos = parseInt(posMatch[1], 10);
|
|
1869
|
+
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
|
1870
|
+
context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
|
|
1871
|
+
}
|
|
1964
1872
|
debugLog("[edit] Failed to parse tsconfig.json (will use defaults, logging once)", { error: errorStr, context });
|
|
1965
1873
|
cachedParseError = errorStr;
|
|
1966
1874
|
}
|
|
@@ -1983,14 +1891,14 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1983
1891
|
}
|
|
1984
1892
|
// Only log default alias once (not when using cached error fallback)
|
|
1985
1893
|
if (!cachedParseError) {
|
|
1986
|
-
|
|
1894
|
+
debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
|
|
1987
1895
|
}
|
|
1988
1896
|
}
|
|
1989
1897
|
|
|
1990
1898
|
// Cache aliases if parsed successfully, no tsconfig exists, OR we have a parse error (cache fallback)
|
|
1991
1899
|
if (parsedSuccessfully || !fs.existsSync(tsconfigPath) || cachedParseError) {
|
|
1992
|
-
|
|
1993
|
-
|
|
1900
|
+
cachedPathAliases = aliases;
|
|
1901
|
+
cachedProjectRoot = projectRoot;
|
|
1994
1902
|
}
|
|
1995
1903
|
|
|
1996
1904
|
return aliases;
|
|
@@ -2298,105 +2206,3 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
|
|
|
2298
2206
|
failedPatches,
|
|
2299
2207
|
};
|
|
2300
2208
|
}
|
|
2301
|
-
|
|
2302
|
-
/**
|
|
2303
|
-
* Validate that AI modifications are surgical edits, not complete rewrites
|
|
2304
|
-
*/
|
|
2305
|
-
interface ValidationResult {
|
|
2306
|
-
valid: boolean;
|
|
2307
|
-
error?: string;
|
|
2308
|
-
warnings: string[];
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
function validateModification(
|
|
2312
|
-
originalContent: string,
|
|
2313
|
-
modifiedContent: string,
|
|
2314
|
-
filePath: string
|
|
2315
|
-
): ValidationResult {
|
|
2316
|
-
const warnings: string[] = [];
|
|
2317
|
-
|
|
2318
|
-
// Skip validation for new files (no original content)
|
|
2319
|
-
if (!originalContent || originalContent.trim() === "") {
|
|
2320
|
-
return { valid: true, warnings: ["New file - no original to compare"] };
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
const originalLines = originalContent.split("\n");
|
|
2324
|
-
const modifiedLines = modifiedContent.split("\n");
|
|
2325
|
-
|
|
2326
|
-
// Check 1: Truncation detection - look for placeholder comments
|
|
2327
|
-
const truncationPatterns = [
|
|
2328
|
-
/\/\/\s*\.\.\.\s*existing/i,
|
|
2329
|
-
/\/\/\s*\.\.\.\s*rest\s*of/i,
|
|
2330
|
-
/\/\/\s*\.\.\.\s*more\s*code/i,
|
|
2331
|
-
/\/\*\s*\.\.\.\s*\*\//,
|
|
2332
|
-
/\/\/\s*\.\.\./,
|
|
2333
|
-
];
|
|
2334
|
-
|
|
2335
|
-
for (const pattern of truncationPatterns) {
|
|
2336
|
-
if (pattern.test(modifiedContent)) {
|
|
2337
|
-
return {
|
|
2338
|
-
valid: false,
|
|
2339
|
-
error: `File ${filePath} contains truncation placeholder (e.g., "// ... existing code"). The AI must return the complete file content. Please try again.`,
|
|
2340
|
-
warnings,
|
|
2341
|
-
};
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
// Check 2: Line count shrinkage - reject if file shrinks by more than 30%
|
|
2346
|
-
const lineDelta = modifiedLines.length - originalLines.length;
|
|
2347
|
-
const shrinkagePercent = (lineDelta / originalLines.length) * 100;
|
|
2348
|
-
|
|
2349
|
-
if (shrinkagePercent < -30) {
|
|
2350
|
-
return {
|
|
2351
|
-
valid: false,
|
|
2352
|
-
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.`,
|
|
2353
|
-
warnings,
|
|
2354
|
-
};
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
if (shrinkagePercent < -15) {
|
|
2358
|
-
warnings.push(`File shrank by ${Math.abs(shrinkagePercent).toFixed(0)}% - review carefully`);
|
|
2359
|
-
}
|
|
2360
|
-
|
|
2361
|
-
// Check 3: Change percentage - warn if too many lines are different
|
|
2362
|
-
let changedLines = 0;
|
|
2363
|
-
const minLines = Math.min(originalLines.length, modifiedLines.length);
|
|
2364
|
-
|
|
2365
|
-
for (let i = 0; i < minLines; i++) {
|
|
2366
|
-
if (originalLines[i] !== modifiedLines[i]) {
|
|
2367
|
-
changedLines++;
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
// Add lines that were added or removed
|
|
2372
|
-
changedLines += Math.abs(originalLines.length - modifiedLines.length);
|
|
2373
|
-
|
|
2374
|
-
const changePercent = (changedLines / originalLines.length) * 100;
|
|
2375
|
-
|
|
2376
|
-
if (changePercent > 50) {
|
|
2377
|
-
return {
|
|
2378
|
-
valid: false,
|
|
2379
|
-
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.`,
|
|
2380
|
-
warnings,
|
|
2381
|
-
};
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
if (changePercent > 30) {
|
|
2385
|
-
warnings.push(`${changePercent.toFixed(0)}% of lines changed - larger than expected for a surgical edit`);
|
|
2386
|
-
}
|
|
2387
|
-
|
|
2388
|
-
// Check 4: Import preservation - ensure imports aren't removed
|
|
2389
|
-
const importRegex = /^import\s+/gm;
|
|
2390
|
-
const originalImports = (originalContent.match(importRegex) || []).length;
|
|
2391
|
-
const modifiedImports = (modifiedContent.match(importRegex) || []).length;
|
|
2392
|
-
|
|
2393
|
-
if (modifiedImports < originalImports * 0.5 && originalImports > 2) {
|
|
2394
|
-
return {
|
|
2395
|
-
valid: false,
|
|
2396
|
-
error: `File ${filePath} went from ${originalImports} imports to ${modifiedImports}. Imports should not be removed. Please try again.`,
|
|
2397
|
-
warnings,
|
|
2398
|
-
};
|
|
2399
|
-
}
|
|
2400
|
-
|
|
2401
|
-
return { valid: true, warnings };
|
|
2402
|
-
}
|