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.
@@ -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, formatThemeForPrompt } from "../sonance-vision-apply/theme-discovery";
5
+ import { discoverTheme } from "../sonance-vision-apply/theme-discovery";
6
6
 
7
7
  /**
8
8
  * Sonance DevTools API - Vision Mode Editor
@@ -507,83 +507,16 @@ 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 are a code editor like Cursor. Before making changes, REASON about what needs to be done.
511
-
512
- ═══════════════════════════════════════════════════════════════════════════════
513
- STEP 1: ANALYZE (Think like Cursor)
514
- ═══════════════════════════════════════════════════════════════════════════════
515
-
516
- Before writing any patches, analyze the screenshot and answer:
517
-
518
- 1. CURRENT STATE: What do I see in the screenshot?
519
- - Is the element visible/readable right now?
520
- - What colors/styles are currently applied?
521
-
522
- 2. PROBLEM IDENTIFICATION: What's actually wrong?
523
- - Contrast issue? (text blending with background)
524
- - Hidden element? (not showing at all)
525
- - Hover state issue? (only visible on interaction)
526
- - Layout issue? (wrong size/position)
527
- - Or is it already correct?
528
-
529
- 3. FIX STRATEGY: What should I change?
530
- - DEFAULT state only (element needs to look different normally)
531
- - HOVER state only (element needs different hover effect)
532
- - BOTH states (element needs overall visibility improvement)
533
- - ASK for clarification (request is too vague)
534
-
535
- ═══════════════════════════════════════════════════════════════════════════════
536
- STEP 2: GENERATE PATCHES (Only after reasoning)
537
- ═══════════════════════════════════════════════════════════════════════════════
538
-
539
- CRITICAL - FILE SELECTION:
540
- - You MUST modify the file marked "TARGET COMPONENT" - this is the file you have FULL visibility into
541
- - Do NOT try to modify other files listed in context - you only see their imports/exports, not full content
542
- - If the TARGET COMPONENT is not the right file, return clarification_needed
543
-
544
- COPY EXACTLY from the TARGET COMPONENT section:
545
- - Your "search" string must match the file CHARACTER-FOR-CHARACTER
546
- - Include exact indentation and at least 3 lines of context
547
- - If the element looks FINE in the screenshot, say so
510
+ const VISION_SYSTEM_PROMPT = `You edit code. Return ONLY valid JSON - no explanation, no preamble, no markdown.
548
511
 
549
512
  RULES:
550
- 1. Make the SMALLEST possible change
551
- 2. Do NOT refactor or "improve" other code
552
- 3. NEVER invent code - COPY exact code from file
553
- 4. NEVER change data mappings unless user provides new values
554
- 5. If visibility issue: prefer explicit colors (text-white, text-black) over semantic tokens
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
555
517
 
556
- ═══════════════════════════════════════════════════════════════════════════════
557
- RESPONSE FORMAT
558
- ═══════════════════════════════════════════════════════════════════════════════
559
-
560
- Return ONLY raw JSON:
561
- {
562
- "analysis": {
563
- "currentState": "what I see in the screenshot",
564
- "problem": "the actual issue (or 'none - element appears correct')",
565
- "fixStrategy": "default|hover|both|clarification_needed"
566
- },
567
- "reasoning": "my diagnosis and approach",
568
- "modifications": [{
569
- "filePath": "path/to/file.tsx",
570
- "patches": [{
571
- "search": "EXACT copy from file",
572
- "replace": "changed code",
573
- "explanation": "what this changes and why"
574
- }],
575
- "previewCSS": "optional CSS for live preview"
576
- }],
577
- "aggregatedPreviewCSS": "combined CSS for all changes",
578
- "explanation": "summary of what was changed"
579
- }
580
-
581
- If the element looks correct, return:
582
- {
583
- "analysis": { "currentState": "...", "problem": "none", "fixStrategy": "clarification_needed" },
584
- "modifications": [],
585
- "explanation": "The element appears visible/correct in the screenshot. Please specify what needs to change."
586
- }`;
518
+ RESPOND WITH ONLY THIS JSON FORMAT (nothing else):
519
+ {"modifications":[{"filePath":"path","patches":[{"search":"exact code","replace":"changed code"}]}]}`;
587
520
 
588
521
  export async function POST(request: Request) {
589
522
  // Only allow in development
@@ -813,172 +746,92 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
813
746
  `;
814
747
  }
815
748
 
816
- // ========== TARGET COMPONENT (RECOMMENDED FILE) - SHOWN FIRST ==========
749
+ // ========== TARGET COMPONENT ONLY (with line numbers) ==========
750
+ // CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
817
751
  if (recommendedFileContent) {
818
- // Never truncate the recommended file - AI needs full context to avoid hallucination
819
752
  const content = recommendedFileContent.content;
820
753
 
754
+ // Add line numbers to make it easy for LLM to reference exact code
755
+ const linesWithNumbers = content.split('\n').map((line, i) =>
756
+ `${String(i + 1).padStart(4, ' ')}| ${line}`
757
+ ).join('\n');
758
+
821
759
  textContent += `═══════════════════════════════════════════════════════════════════════════════
822
- TARGET COMPONENT - YOU MUST EDIT THIS FILE
760
+ FILE TO EDIT: ${recommendedFileContent.path}
823
761
  ═══════════════════════════════════════════════════════════════════════════════
824
762
 
825
- This is the component that renders the UI you see in the screenshot.
826
- File: ${recommendedFileContent.path}
763
+ IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
764
+ Your "search" string must match the code CHARACTER FOR CHARACTER.
827
765
 
828
766
  \`\`\`tsx
829
- ${content}
767
+ ${linesWithNumbers}
830
768
  \`\`\`
831
769
 
832
770
  `;
833
771
  usedContext += content.length;
834
- debugLog("Added TARGET COMPONENT to context", {
772
+ debugLog("Added TARGET COMPONENT with line numbers", {
835
773
  path: recommendedFileContent.path,
836
- originalSize: recommendedFileContent.content.length,
837
- includedSize: content.length
774
+ lines: content.split('\n').length,
775
+ size: content.length
838
776
  });
839
- }
840
-
841
- // ========== PAGE CONTEXT (wrapper - de-emphasized) ==========
842
- const pageContentTruncated = pageContext.pageContent.substring(0, MAX_PAGE_FILE);
843
- const pageWasTruncated = pageContext.pageContent.length > MAX_PAGE_FILE;
844
-
845
- textContent += `PAGE CONTEXT (wrapper only${recommendedFileContent ? " - DO NOT edit this, edit the TARGET COMPONENT above" : ""}):
846
-
847
- Page File: ${pageContext.pageFile || "Not found"}
848
- ${pageContext.pageContent ? `\`\`\`tsx\n${pageContentTruncated}${pageWasTruncated ? "\n// ... (wrapper truncated)" : ""}\n\`\`\`` : ""}
849
-
850
- `;
851
- usedContext += pageContentTruncated.length;
852
-
853
- // ========== SUPPORTING COMPONENTS (dynamic budget) ==========
854
- if (pageContext.componentSources.length > 0) {
855
- const remainingBudget = TOTAL_CONTEXT_BUDGET - usedContext - MAX_GLOBALS_CSS - 5000; // Reserve 5k for instructions
856
- const filesToInclude = pageContext.componentSources.slice(0, MAX_FILES);
857
- const perFileLimit = Math.max(1000, Math.floor(remainingBudget / Math.max(filesToInclude.length, 1)));
858
-
859
- textContent += `SUPPORTING COMPONENTS (${filesToInclude.length} files, ~${Math.round(perFileLimit/1000)}k chars each):\n`;
777
+ } else if (pageContext.pageContent) {
778
+ // Fallback: use page file if no recommended file
779
+ const content = pageContext.pageContent;
780
+ const linesWithNumbers = content.split('\n').map((line, i) =>
781
+ `${String(i + 1).padStart(4, ' ')}| ${line}`
782
+ ).join('\n');
860
783
 
861
- for (const comp of filesToInclude) {
862
- if (usedContext > TOTAL_CONTEXT_BUDGET - 10000) {
863
- textContent += `\n// ... (remaining files omitted to stay within context limits)\n`;
864
- break;
865
- }
866
-
867
- const truncatedContent = comp.content.substring(0, perFileLimit);
868
- const wasTruncated = comp.content.length > perFileLimit;
869
-
870
- textContent += `
871
- File: ${comp.path}
784
+ textContent += `═══════════════════════════════════════════════════════════════════════════════
785
+ FILE TO EDIT: ${pageContext.pageFile}
786
+ ═══════════════════════════════════════════════════════════════════════════════
787
+
872
788
  \`\`\`tsx
873
- ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
789
+ ${linesWithNumbers}
874
790
  \`\`\`
791
+
875
792
  `;
876
- usedContext += truncatedContent.length;
877
- }
793
+ usedContext += content.length;
878
794
  }
795
+
796
+ // NOTE: We intentionally skip SUPPORTING COMPONENTS to reduce noise
797
+ // The LLM only needs the TARGET file to make accurate edits
879
798
 
880
- // ========== THEME DISCOVERY (REFERENCE ONLY) ==========
881
- // Dynamically discover theme tokens from the target codebase
799
+ // Dynamically discover theme tokens (minimal - just for logging)
882
800
  const discoveredTheme = await discoverTheme(projectRoot);
883
- const themeContext = formatThemeForPrompt(discoveredTheme);
884
-
885
- // Check if this is a visibility-related request
886
- const isVisibilityRequest = /visible|can't see|cant see|not visible|hidden|color|contrast/i.test(userPrompt);
887
801
 
888
802
  if (discoveredTheme.discoveredFiles.length > 0) {
889
- // Only include detailed color guidance for visibility requests
890
- if (isVisibilityRequest) {
891
- // Extract available text color classes from discovered theme
892
- const textColorClasses = Object.keys(discoveredTheme.cssVariables)
893
- .filter(key => key.includes('foreground') || key.includes('text'))
894
- .map(key => `text-${key.replace('--', '').replace(/-/g, '-')}`)
895
- .slice(0, 10);
896
-
897
- textContent += `
898
- ═══════════════════════════════════════════════════════════════════════════════
899
- COLOR FIX GUIDANCE (for visibility issues only)
900
- ═══════════════════════════════════════════════════════════════════════════════
901
-
902
- AVAILABLE TEXT COLORS (use these for contrast fixes):
903
- - text-white (always visible on dark backgrounds)
904
- - text-black (always visible on light backgrounds)
905
- - text-foreground (theme default)
906
- - text-primary-foreground (for bg-primary)
907
- - text-accent-foreground (for bg-accent - CHECK IF THIS HAS GOOD CONTRAST)
908
- - text-destructive-foreground (for bg-destructive)
909
-
910
- SAFE BUTTON PATTERNS:
911
- - bg-accent text-white (cyan button, guaranteed visible)
912
- - bg-primary text-white (charcoal button, guaranteed visible)
913
- - bg-destructive text-white (red button, guaranteed visible)
914
- - bg-muted text-foreground (gray button, guaranteed visible)
915
-
916
- IMPORTANT: If current code has text-accent-foreground or similar semantic colors
917
- that result in poor contrast, REPLACE with text-white or text-black.
918
-
919
- `;
920
- } else {
921
- // For non-visibility requests, minimal reference
922
- textContent += `
923
- ═══════════════════════════════════════════════════════════════════════════════
924
- REFERENCE ONLY (do not use this to justify additional changes)
925
- ═══════════════════════════════════════════════════════════════════════════════
926
- Theme discovered from: ${discoveredTheme.discoveredFiles.join(', ')}
927
- `;
928
- }
929
-
930
803
  debugLog("Theme discovery complete", {
931
804
  filesFound: discoveredTheme.discoveredFiles,
932
- cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
933
- tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
934
- isVisibilityRequest,
935
805
  });
936
806
  }
937
807
 
938
- // ========== GLOBALS CSS ==========
939
- const globalsTruncated = pageContext.globalsCSS.substring(0, MAX_GLOBALS_CSS);
940
- textContent += `
941
- GLOBALS.CSS (theme variables):
942
- \`\`\`css
943
- ${globalsTruncated}${pageContext.globalsCSS.length > MAX_GLOBALS_CSS ? "\n/* ... (truncated) */" : ""}
944
- \`\`\`
945
-
946
- `;
947
-
948
- // ========== VALID FILES LIST ==========
949
- const validFilesList: string[] = [];
950
- const recommendedPath = recommendedFile?.path;
951
-
952
- // Add recommended file first with marker
953
- if (recommendedPath) {
954
- validFilesList.push(`- ${recommendedPath} (*** TARGET - EDIT THIS FILE ***)`);
955
- }
808
+ // ========== SIMPLIFIED INSTRUCTIONS ==========
809
+ const targetPath = recommendedFileContent?.path || pageContext.pageFile || "unknown";
956
810
 
957
- // Add page file (if not the recommended file)
958
- if (pageContext.pageFile && pageContext.pageFile !== recommendedPath) {
959
- validFilesList.push(`- ${pageContext.pageFile} (wrapper only)`);
960
- }
961
-
962
- // Add other component sources (excluding recommended file)
963
- for (const comp of pageContext.componentSources) {
964
- if (comp.path !== recommendedPath) {
965
- validFilesList.push(`- ${comp.path}`);
966
- }
967
- }
811
+ textContent += `═══════════════════════════════════════════════════════════════════════════════
812
+ HOW TO MAKE YOUR EDIT
813
+ ═══════════════════════════════════════════════════════════════════════════════
814
+
815
+ 1. Find the EXACT code in the file above that needs to change
816
+ 2. COPY that code CHARACTER FOR CHARACTER (use the line numbers as reference)
817
+ 3. Make your change to the copied code
818
+ 4. Return a patch with the exact "search" and "replace" strings
968
819
 
969
- textContent += `VALID FILES YOU MAY EDIT:
970
- ${validFilesList.join("\n")}
820
+ EXAMPLE - If line 84 shows:
821
+ 84| <div className="mb-10 md:mb-16 grid grid-cols-2">
971
822
 
972
- INSTRUCTIONS:
973
- 1. Look at the screenshot and identify elements mentioned in the user's request
974
- 2. The TARGET COMPONENT shown first contains the UI - EDIT THAT FILE
975
- 3. Choose which of the VALID FILES above need modifications
976
- 4. Generate complete modified code for each file
977
- 5. Provide previewCSS for immediate visual feedback
978
- 6. Return as JSON in the specified format
979
- 7. DO NOT edit the page wrapper unless the TARGET COMPONENT is unavailable
823
+ Your patch should be:
824
+ {
825
+ "modifications": [{
826
+ "filePath": "${targetPath}",
827
+ "patches": [{
828
+ "search": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2\\">",
829
+ "replace": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2 bg-accent\\">"
830
+ }]
831
+ }]
832
+ }
980
833
 
981
- CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
834
+ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exact code, return empty modifications.`;
982
835
 
983
836
  messageContent.push({
984
837
  type: "text",
@@ -1003,8 +856,6 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
1003
856
  let lastPatchErrors: string[] = [];
1004
857
  let modificationsWithOriginals: VisionFileModification[] = [];
1005
858
  let finalExplanation: string | undefined;
1006
- let finalReasoning: string | undefined;
1007
- let finalAggregatedCSS: string | undefined;
1008
859
 
1009
860
  while (retryCount <= MAX_RETRIES) {
1010
861
  // Build messages for this attempt
@@ -1054,110 +905,116 @@ This is better than generating patches with made-up code.`,
1054
905
  });
1055
906
  }
1056
907
 
1057
- const response = await anthropic.messages.create({
1058
- model: "claude-sonnet-4-20250514",
1059
- max_tokens: 16384,
908
+ const response = await anthropic.messages.create({
909
+ model: "claude-sonnet-4-20250514",
910
+ max_tokens: 16384,
1060
911
  messages: currentMessages,
1061
- system: VISION_SYSTEM_PROMPT,
1062
- });
912
+ system: VISION_SYSTEM_PROMPT,
913
+ });
1063
914
 
1064
- // Extract text content from response
1065
- const textResponse = response.content.find((block) => block.type === "text");
1066
- if (!textResponse || textResponse.type !== "text") {
1067
- return NextResponse.json(
1068
- { error: "No text response from AI" },
1069
- { status: 500 }
1070
- );
1071
- }
915
+ // Extract text content from response
916
+ const textResponse = response.content.find((block) => block.type === "text");
917
+ if (!textResponse || textResponse.type !== "text") {
918
+ return NextResponse.json(
919
+ { error: "No text response from AI" },
920
+ { status: 500 }
921
+ );
922
+ }
1072
923
 
1073
- // Parse AI response - now expecting patches instead of full file content
1074
- let aiResponse: {
1075
- analysis?: {
1076
- currentState?: string;
1077
- problem?: string;
1078
- fixStrategy?: string;
1079
- };
1080
- reasoning?: string;
1081
- modifications: Array<{
1082
- filePath: string;
1083
- patches?: Patch[];
1084
- // Legacy support for modifiedContent (will be deprecated)
1085
- modifiedContent?: string;
1086
- explanation?: string;
1087
- previewCSS?: string;
1088
- }>;
1089
- aggregatedPreviewCSS?: string;
924
+ // Parse AI response - expecting modifications array
925
+ let aiResponse: {
926
+ modifications: Array<{
927
+ filePath: string;
928
+ patches?: Patch[];
1090
929
  explanation?: string;
1091
- };
930
+ previewCSS?: string;
931
+ }>;
932
+ explanation?: string;
933
+ };
1092
934
 
1093
- try {
1094
- let jsonText = textResponse.text.trim();
1095
-
1096
- // Try to extract JSON from markdown code blocks
1097
- const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
1098
- jsonText.match(/```\n([\s\S]*?)\n```/);
1099
-
1100
- if (jsonMatch) {
1101
- jsonText = jsonMatch[1];
1102
- } else if (jsonText.includes("```json")) {
1103
- // Fallback for cases where regex might miss due to newlines
1104
- const start = jsonText.indexOf("```json") + 7;
1105
- const end = jsonText.lastIndexOf("```");
1106
- if (end > start) {
1107
- jsonText = jsonText.substring(start, end);
1108
- }
1109
- }
935
+ try {
936
+ let jsonText = textResponse.text.trim();
937
+
938
+ // Try to extract JSON from markdown code blocks
939
+ const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
940
+ jsonText.match(/```\n([\s\S]*?)\n```/);
941
+
942
+ if (jsonMatch) {
943
+ jsonText = jsonMatch[1];
944
+ } else if (jsonText.includes("```json")) {
945
+ // Fallback for cases where regex might miss due to newlines
946
+ const start = jsonText.indexOf("```json") + 7;
947
+ const end = jsonText.lastIndexOf("```");
948
+ if (end > start) {
949
+ jsonText = jsonText.substring(start, end);
950
+ }
951
+ }
1110
952
 
1111
- // Clean up any remaining whitespace
1112
- jsonText = jsonText.trim();
1113
-
1114
- // Robust JSON extraction: find the first { and last } to extract JSON object
1115
- // This handles cases where the LLM includes preamble text before the JSON
1116
- const firstBrace = jsonText.indexOf('{');
1117
- const lastBrace = jsonText.lastIndexOf('}');
1118
- if (firstBrace !== -1 && lastBrace > firstBrace) {
1119
- jsonText = jsonText.substring(firstBrace, lastBrace + 1);
953
+ // Clean up any remaining whitespace
954
+ jsonText = jsonText.trim();
955
+
956
+ // Robust JSON extraction: look for {"modifications" pattern specifically
957
+ // This handles cases where the LLM includes preamble text with code blocks
958
+ const jsonStartPatterns = ['{"modifications"', '{ "modifications"', '{\n "modifications"'];
959
+ let jsonStart = -1;
960
+
961
+ for (const pattern of jsonStartPatterns) {
962
+ const idx = jsonText.indexOf(pattern);
963
+ if (idx !== -1 && (jsonStart === -1 || idx < jsonStart)) {
964
+ jsonStart = idx;
1120
965
  }
1121
-
1122
- aiResponse = JSON.parse(jsonText);
1123
- } catch {
1124
- console.error("Failed to parse AI response:", textResponse.text);
1125
- return NextResponse.json(
1126
- { error: "Failed to parse AI response. Please try again." },
1127
- { status: 500 }
1128
- );
1129
966
  }
1130
-
1131
- // Log the LLM's analysis/reasoning (like Cursor showing its thought process)
1132
- if (aiResponse.analysis) {
1133
- debugLog("LLM Analysis (Step 1)", {
1134
- currentState: aiResponse.analysis.currentState,
1135
- problem: aiResponse.analysis.problem,
1136
- fixStrategy: aiResponse.analysis.fixStrategy,
1137
- });
967
+
968
+ if (jsonStart !== -1) {
969
+ // Find the matching closing brace by counting braces
970
+ let braceCount = 0;
971
+ let jsonEnd = -1;
972
+ for (let i = jsonStart; i < jsonText.length; i++) {
973
+ if (jsonText[i] === '{') braceCount++;
974
+ if (jsonText[i] === '}') {
975
+ braceCount--;
976
+ if (braceCount === 0) {
977
+ jsonEnd = i;
978
+ break;
979
+ }
980
+ }
981
+ }
982
+ if (jsonEnd !== -1) {
983
+ jsonText = jsonText.substring(jsonStart, jsonEnd + 1);
984
+ }
985
+ } else {
986
+ // Fallback: try first { to last }
987
+ const firstBrace = jsonText.indexOf('{');
988
+ const lastBrace = jsonText.lastIndexOf('}');
989
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
990
+ jsonText = jsonText.substring(firstBrace, lastBrace + 1);
991
+ }
1138
992
  }
993
+
994
+ aiResponse = JSON.parse(jsonText);
995
+ } catch {
996
+ console.error("Failed to parse AI response:", textResponse.text);
997
+ return NextResponse.json(
998
+ { error: "Failed to parse AI response. Please try again." },
999
+ { status: 500 }
1000
+ );
1001
+ }
1139
1002
 
1140
1003
  finalExplanation = aiResponse.explanation;
1141
- finalReasoning = aiResponse.reasoning;
1142
- finalAggregatedCSS = aiResponse.aggregatedPreviewCSS;
1143
1004
 
1144
- // If LLM says clarification is needed, return that to the user
1145
- if (aiResponse.analysis?.fixStrategy === "clarification_needed" &&
1146
- (!aiResponse.modifications || aiResponse.modifications.length === 0)) {
1005
+ if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
1147
1006
  return NextResponse.json({
1148
1007
  success: true,
1149
- needsClarification: true,
1150
- analysis: aiResponse.analysis,
1151
- explanation: aiResponse.explanation || "The element appears correct. Please specify what needs to change.",
1152
1008
  modifications: [],
1009
+ explanation: aiResponse.explanation || "No changes needed.",
1153
1010
  });
1154
- }
1011
+ }
1155
1012
 
1156
- debugLog("VALIDATION: Known file paths from page context", {
1157
- pageFile: pageContext.pageFile,
1158
- knownPaths: Array.from(knownPaths),
1159
- aiRequestedFiles: (aiResponse.modifications || []).map(m => m.filePath)
1160
- });
1013
+ debugLog("VALIDATION: Known file paths from page context", {
1014
+ pageFile: pageContext.pageFile,
1015
+ knownPaths: Array.from(knownPaths),
1016
+ aiRequestedFiles: (aiResponse.modifications || []).map(m => m.filePath)
1017
+ });
1161
1018
 
1162
1019
  // Validate AI response - trust the LLM to identify the correct file
1163
1020
  // Only reject paths that are outside the project or don't exist
@@ -1198,9 +1055,9 @@ This is better than generating patches with made-up code.`,
1198
1055
  }
1199
1056
  }
1200
1057
 
1201
- // Process modifications - apply patches to get modified content
1058
+ // Process modifications - apply patches to get modified content
1202
1059
  modificationsWithOriginals = [];
1203
- const patchErrors: string[] = [];
1060
+ const patchErrors: string[] = [];
1204
1061
 
1205
1062
  for (const mod of aiResponse.modifications || []) {
1206
1063
  const fullPath = path.join(projectRoot, mod.filePath);
@@ -1278,20 +1135,9 @@ This is better than generating patches with made-up code.`,
1278
1135
  modifiedContent = patchResult.modifiedContent;
1279
1136
  console.log(`[Vision Mode] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
1280
1137
  }
1281
- } else if (mod.modifiedContent) {
1282
- // Legacy: AI returned full file content
1283
- console.warn(`[Vision Mode] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
1284
- modifiedContent = mod.modifiedContent;
1285
-
1286
- // Validate the modification using legacy validation
1287
- const validation = validateModification(originalContent, modifiedContent, mod.filePath);
1288
- if (!validation.valid) {
1289
- patchErrors.push(`${mod.filePath}: ${validation.error}`);
1290
- continue;
1291
- }
1292
1138
  } else {
1293
- // No patches and no modifiedContent - skip
1294
- console.warn(`[Vision Mode] No patches or modifiedContent for ${mod.filePath}`);
1139
+ // No patches - skip
1140
+ console.warn(`[Vision Mode] No patches for ${mod.filePath}`);
1295
1141
  continue;
1296
1142
  }
1297
1143
 
@@ -1306,7 +1152,7 @@ This is better than generating patches with made-up code.`,
1306
1152
  }
1307
1153
 
1308
1154
  // If all modifications failed, check if we should retry
1309
- if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
1155
+ if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
1310
1156
  if (retryCount < MAX_RETRIES) {
1311
1157
  console.warn(`[Vision Mode] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
1312
1158
  debugLog("Retry triggered due to patch failures", {
@@ -1321,19 +1167,19 @@ This is better than generating patches with made-up code.`,
1321
1167
 
1322
1168
  // Exhausted retries, return error
1323
1169
  console.error("All AI patches failed after retries:", patchErrors);
1324
- return NextResponse.json(
1325
- {
1326
- success: false,
1170
+ return NextResponse.json(
1171
+ {
1172
+ success: false,
1327
1173
  error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
1328
- } as VisionEditResponse,
1329
- { status: 400 }
1330
- );
1331
- }
1174
+ } as VisionEditResponse,
1175
+ { status: 400 }
1176
+ );
1177
+ }
1332
1178
 
1333
- // Log patch errors as warnings if some modifications succeeded
1334
- if (patchErrors.length > 0) {
1335
- console.warn("Some patches failed:", patchErrors);
1336
- }
1179
+ // Log patch errors as warnings if some modifications succeeded
1180
+ if (patchErrors.length > 0) {
1181
+ console.warn("Some patches failed:", patchErrors);
1182
+ }
1337
1183
 
1338
1184
  // Successfully processed at least some modifications - break out of retry loop
1339
1185
  break;
@@ -1348,9 +1194,8 @@ This is better than generating patches with made-up code.`,
1348
1194
  return NextResponse.json({
1349
1195
  success: true,
1350
1196
  modifications: modificationsWithOriginals,
1351
- aggregatedPreviewCSS: finalAggregatedCSS || aggregatedCSS,
1197
+ aggregatedPreviewCSS: aggregatedCSS,
1352
1198
  explanation: finalExplanation,
1353
- reasoning: finalReasoning,
1354
1199
  } as VisionEditResponse);
1355
1200
  }
1356
1201
 
@@ -2042,13 +1887,13 @@ function getPathAliases(projectRoot: string): Map<string, string> {
2042
1887
 
2043
1888
  // Only log parse error once to avoid log spam
2044
1889
  if (!cachedParseError) {
2045
- const posMatch = errorStr.match(/position (\d+)/);
2046
- let context = "";
2047
- if (posMatch) {
2048
- const pos = parseInt(posMatch[1], 10);
2049
- const content = fs.readFileSync(tsconfigPath, "utf-8");
2050
- context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
2051
- }
1890
+ const posMatch = errorStr.match(/position (\d+)/);
1891
+ let context = "";
1892
+ if (posMatch) {
1893
+ const pos = parseInt(posMatch[1], 10);
1894
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
1895
+ context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
1896
+ }
2052
1897
  debugLog("[edit] Failed to parse tsconfig.json (will use defaults, logging once)", { error: errorStr, context });
2053
1898
  cachedParseError = errorStr;
2054
1899
  }
@@ -2071,14 +1916,14 @@ function getPathAliases(projectRoot: string): Map<string, string> {
2071
1916
  }
2072
1917
  // Only log default alias once (not when using cached error fallback)
2073
1918
  if (!cachedParseError) {
2074
- debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
1919
+ debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
2075
1920
  }
2076
1921
  }
2077
1922
 
2078
1923
  // Cache aliases if parsed successfully, no tsconfig exists, OR we have a parse error (cache fallback)
2079
1924
  if (parsedSuccessfully || !fs.existsSync(tsconfigPath) || cachedParseError) {
2080
- cachedPathAliases = aliases;
2081
- cachedProjectRoot = projectRoot;
1925
+ cachedPathAliases = aliases;
1926
+ cachedProjectRoot = projectRoot;
2082
1927
  }
2083
1928
 
2084
1929
  return aliases;
@@ -2386,105 +2231,3 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
2386
2231
  failedPatches,
2387
2232
  };
2388
2233
  }
2389
-
2390
- /**
2391
- * Validate that AI modifications are surgical edits, not complete rewrites
2392
- */
2393
- interface ValidationResult {
2394
- valid: boolean;
2395
- error?: string;
2396
- warnings: string[];
2397
- }
2398
-
2399
- function validateModification(
2400
- originalContent: string,
2401
- modifiedContent: string,
2402
- filePath: string
2403
- ): ValidationResult {
2404
- const warnings: string[] = [];
2405
-
2406
- // Skip validation for new files (no original content)
2407
- if (!originalContent || originalContent.trim() === "") {
2408
- return { valid: true, warnings: ["New file - no original to compare"] };
2409
- }
2410
-
2411
- const originalLines = originalContent.split("\n");
2412
- const modifiedLines = modifiedContent.split("\n");
2413
-
2414
- // Check 1: Truncation detection - look for placeholder comments
2415
- const truncationPatterns = [
2416
- /\/\/\s*\.\.\.\s*existing/i,
2417
- /\/\/\s*\.\.\.\s*rest\s*of/i,
2418
- /\/\/\s*\.\.\.\s*more\s*code/i,
2419
- /\/\*\s*\.\.\.\s*\*\//,
2420
- /\/\/\s*\.\.\./,
2421
- ];
2422
-
2423
- for (const pattern of truncationPatterns) {
2424
- if (pattern.test(modifiedContent)) {
2425
- return {
2426
- valid: false,
2427
- error: `File ${filePath} contains truncation placeholder (e.g., "// ... existing code"). The AI must return the complete file content. Please try again.`,
2428
- warnings,
2429
- };
2430
- }
2431
- }
2432
-
2433
- // Check 2: Line count shrinkage - reject if file shrinks by more than 30%
2434
- const lineDelta = modifiedLines.length - originalLines.length;
2435
- const shrinkagePercent = (lineDelta / originalLines.length) * 100;
2436
-
2437
- if (shrinkagePercent < -30) {
2438
- return {
2439
- valid: false,
2440
- 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.`,
2441
- warnings,
2442
- };
2443
- }
2444
-
2445
- if (shrinkagePercent < -15) {
2446
- warnings.push(`File shrank by ${Math.abs(shrinkagePercent).toFixed(0)}% - review carefully`);
2447
- }
2448
-
2449
- // Check 3: Change percentage - warn if too many lines are different
2450
- let changedLines = 0;
2451
- const minLines = Math.min(originalLines.length, modifiedLines.length);
2452
-
2453
- for (let i = 0; i < minLines; i++) {
2454
- if (originalLines[i] !== modifiedLines[i]) {
2455
- changedLines++;
2456
- }
2457
- }
2458
-
2459
- // Add lines that were added or removed
2460
- changedLines += Math.abs(originalLines.length - modifiedLines.length);
2461
-
2462
- const changePercent = (changedLines / originalLines.length) * 100;
2463
-
2464
- if (changePercent > 50) {
2465
- return {
2466
- valid: false,
2467
- 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.`,
2468
- warnings,
2469
- };
2470
- }
2471
-
2472
- if (changePercent > 30) {
2473
- warnings.push(`${changePercent.toFixed(0)}% of lines changed - larger than expected for a surgical edit`);
2474
- }
2475
-
2476
- // Check 4: Import preservation - ensure imports aren't removed
2477
- const importRegex = /^import\s+/gm;
2478
- const originalImports = (originalContent.match(importRegex) || []).length;
2479
- const modifiedImports = (modifiedContent.match(importRegex) || []).length;
2480
-
2481
- if (modifiedImports < originalImports * 0.5 && originalImports > 2) {
2482
- return {
2483
- valid: false,
2484
- error: `File ${filePath} went from ${originalImports} imports to ${modifiedImports}. Imports should not be removed. Please try again.`,
2485
- warnings,
2486
- };
2487
- }
2488
-
2489
- return { valid: true, warnings };
2490
- }