sonance-brand-mcp 1.3.60 → 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.
@@ -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, formatThemeForPrompt } from "./theme-discovery";
6
+ import { discoverTheme } from "./theme-discovery";
7
7
 
8
8
  /**
9
9
  * Sonance DevTools API - Apply-First Vision Mode
@@ -511,80 +511,20 @@ 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 are a code editor like Cursor. Before making changes, REASON about what needs to be done.
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. Make ONLY the change requested.
552
515
 
553
516
  RULES:
554
- 1. Make the SMALLEST possible change
555
- 2. Do NOT refactor or "improve" other code
556
- 3. NEVER invent code - COPY exact code from file
557
- 4. NEVER change data mappings unless user provides new values
558
- 5. If visibility issue: prefer explicit colors (text-white, text-black) over semantic tokens
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
559
521
 
560
- ═══════════════════════════════════════════════════════════════════════════════
561
- RESPONSE FORMAT
562
- ═══════════════════════════════════════════════════════════════════════════════
563
-
564
- Return ONLY raw JSON:
522
+ Return JSON:
565
523
  {
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
524
  "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."
525
+ "filePath": "path",
526
+ "patches": [{ "search": "exact code from file", "replace": "changed code" }]
527
+ }]
588
528
  }`;
589
529
 
590
530
  export async function POST(request: Request) {
@@ -842,172 +782,93 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
842
782
  `;
843
783
  }
844
784
 
845
- // ========== TARGET COMPONENT (RECOMMENDED FILE) - SHOWN FIRST ==========
785
+ // ========== TARGET COMPONENT ONLY (with line numbers) ==========
786
+ // CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
846
787
  if (recommendedFileContent) {
847
- // Never truncate the recommended file - AI needs full context to avoid hallucination
848
788
  const content = recommendedFileContent.content;
849
789
 
790
+ // Add line numbers to make it easy for LLM to reference exact code
791
+ const linesWithNumbers = content.split('\n').map((line, i) =>
792
+ `${String(i + 1).padStart(4, ' ')}| ${line}`
793
+ ).join('\n');
794
+
850
795
  textContent += `═══════════════════════════════════════════════════════════════════════════════
851
- TARGET COMPONENT - YOU MUST EDIT THIS FILE
796
+ FILE TO EDIT: ${recommendedFileContent.path}
852
797
  ═══════════════════════════════════════════════════════════════════════════════
853
798
 
854
- This is the component that renders the UI you see in the screenshot.
855
- File: ${recommendedFileContent.path}
799
+ IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
800
+ Your "search" string must match the code CHARACTER FOR CHARACTER.
856
801
 
857
802
  \`\`\`tsx
858
- ${content}
803
+ ${linesWithNumbers}
859
804
  \`\`\`
860
805
 
861
806
  `;
862
807
  usedContext += content.length;
863
- debugLog("Added TARGET COMPONENT to context", {
808
+ debugLog("Added TARGET COMPONENT with line numbers", {
864
809
  path: recommendedFileContent.path,
865
- originalSize: recommendedFileContent.content.length,
866
- includedSize: content.length
810
+ lines: content.split('\n').length,
811
+ size: content.length
867
812
  });
868
- }
869
-
870
- // ========== PAGE CONTEXT (wrapper - de-emphasized) ==========
871
- const pageContentTruncated = pageContext.pageContent.substring(0, MAX_PAGE_FILE);
872
- const pageWasTruncated = pageContext.pageContent.length > MAX_PAGE_FILE;
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`;
813
+ } else if (pageContext.pageContent) {
814
+ // Fallback: use page file if no recommended file
815
+ const content = pageContext.pageContent;
816
+ const linesWithNumbers = content.split('\n').map((line, i) =>
817
+ `${String(i + 1).padStart(4, ' ')}| ${line}`
818
+ ).join('\n');
889
819
 
890
- for (const comp of filesToInclude) {
891
- if (usedContext > TOTAL_CONTEXT_BUDGET - 10000) {
892
- textContent += `\n// ... (remaining files omitted to stay within context limits)\n`;
893
- break;
894
- }
895
-
896
- const truncatedContent = comp.content.substring(0, perFileLimit);
897
- const wasTruncated = comp.content.length > perFileLimit;
898
-
899
- textContent += `
900
- File: ${comp.path}
820
+ textContent += `═══════════════════════════════════════════════════════════════════════════════
821
+ FILE TO EDIT: ${pageContext.pageFile}
822
+ ═══════════════════════════════════════════════════════════════════════════════
823
+
901
824
  \`\`\`tsx
902
- ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
825
+ ${linesWithNumbers}
903
826
  \`\`\`
827
+
904
828
  `;
905
- usedContext += truncatedContent.length;
906
- }
829
+ usedContext += content.length;
907
830
  }
831
+
832
+ // NOTE: We intentionally skip SUPPORTING COMPONENTS to reduce noise
833
+ // The LLM only needs the TARGET file to make accurate edits
908
834
 
909
835
  // ========== THEME DISCOVERY (REFERENCE ONLY) ==========
910
- // Dynamically discover theme tokens from the target codebase
836
+ // Dynamically discover theme tokens (minimal - just for logging)
911
837
  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
838
 
917
839
  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
840
  debugLog("Theme discovery complete", {
960
841
  filesFound: discoveredTheme.discoveredFiles,
961
- cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
962
- tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
963
- isVisibilityRequest,
964
842
  });
965
843
  }
966
844
 
967
- // ========== GLOBALS CSS ==========
968
- const globalsTruncated = pageContext.globalsCSS.substring(0, MAX_GLOBALS_CSS);
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;
845
+ // ========== SIMPLIFIED INSTRUCTIONS ==========
846
+ const targetPath = recommendedFileContent?.path || pageContext.pageFile || "unknown";
980
847
 
981
- // Add recommended file first with marker
982
- if (recommendedPath) {
983
- validFilesList.push(`- ${recommendedPath} (*** TARGET - EDIT THIS FILE ***)`);
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
- }
848
+ textContent += `═══════════════════════════════════════════════════════════════════════════════
849
+ HOW TO MAKE YOUR EDIT
850
+ ═══════════════════════════════════════════════════════════════════════════════
997
851
 
998
- textContent += `VALID FILES YOU MAY EDIT:
999
- ${validFilesList.join("\n")}
852
+ 1. Find the EXACT code in the file above that needs to change
853
+ 2. COPY that code CHARACTER FOR CHARACTER (use the line numbers as reference)
854
+ 3. Make your change to the copied code
855
+ 4. Return a patch with the exact "search" and "replace" strings
1000
856
 
1001
- INSTRUCTIONS:
1002
- 1. Look at the screenshot and identify elements mentioned in the user's request
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
857
+ EXAMPLE - If line 84 shows:
858
+ 84| <div className="mb-10 md:mb-16 grid grid-cols-2">
859
+
860
+ Your patch should be:
861
+ {
862
+ "modifications": [{
863
+ "filePath": "${targetPath}",
864
+ "patches": [{
865
+ "search": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2\\">",
866
+ "replace": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2 bg-accent\\">"
867
+ }]
868
+ }]
869
+ }
1009
870
 
1010
- CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
871
+ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exact code, return empty modifications.`;
1011
872
 
1012
873
  messageContent.push({
1013
874
  type: "text",
@@ -1037,7 +898,6 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
1037
898
  let lastPatchErrors: string[] = [];
1038
899
  let modifications: VisionFileModification[] = [];
1039
900
  let finalExplanation: string | undefined;
1040
- let finalReasoning: string | undefined;
1041
901
 
1042
902
  while (retryCount <= MAX_RETRIES) {
1043
903
  // Build messages for this attempt
@@ -1087,120 +947,88 @@ This is better than generating patches with made-up code.`,
1087
947
  });
1088
948
  }
1089
949
 
1090
- const response = await anthropic.messages.create({
1091
- model: "claude-sonnet-4-20250514",
1092
- max_tokens: 16384,
950
+ const response = await anthropic.messages.create({
951
+ model: "claude-sonnet-4-20250514",
952
+ max_tokens: 16384,
1093
953
  messages: currentMessages,
1094
- system: VISION_SYSTEM_PROMPT,
1095
- });
954
+ system: VISION_SYSTEM_PROMPT,
955
+ });
1096
956
 
1097
- // Extract text content from response
1098
- const textResponse = response.content.find((block) => block.type === "text");
1099
- if (!textResponse || textResponse.type !== "text") {
1100
- return NextResponse.json(
1101
- { error: "No text response from AI" },
1102
- { status: 500 }
1103
- );
1104
- }
957
+ // Extract text content from response
958
+ const textResponse = response.content.find((block) => block.type === "text");
959
+ if (!textResponse || textResponse.type !== "text") {
960
+ return NextResponse.json(
961
+ { error: "No text response from AI" },
962
+ { status: 500 }
963
+ );
964
+ }
1105
965
 
1106
- // Parse AI response - now expecting patches instead of full file content
1107
- let aiResponse: {
1108
- analysis?: {
1109
- currentState?: string;
1110
- problem?: string;
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
- }>;
966
+ // Parse AI response - expecting modifications array
967
+ let aiResponse: {
968
+ modifications: Array<{
969
+ filePath: string;
970
+ patches?: Patch[];
1121
971
  explanation?: string;
1122
- };
1123
-
1124
- try {
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```/);
1130
-
1131
- if (jsonMatch) {
1132
- jsonText = jsonMatch[1];
1133
- } else if (jsonText.includes("```json")) {
1134
- const start = jsonText.indexOf("```json") + 7;
1135
- const end = jsonText.lastIndexOf("```");
1136
- if (end > start) {
1137
- jsonText = jsonText.substring(start, end);
1138
- }
1139
- }
972
+ }>;
973
+ explanation?: string;
974
+ };
1140
975
 
1141
- jsonText = jsonText.trim();
1142
-
1143
- // Robust JSON extraction: find the first { and last } to extract JSON object
1144
- // This handles cases where the LLM includes preamble text before the JSON
1145
- const firstBrace = jsonText.indexOf('{');
1146
- const lastBrace = jsonText.lastIndexOf('}');
1147
- if (firstBrace !== -1 && lastBrace > firstBrace) {
1148
- jsonText = jsonText.substring(firstBrace, lastBrace + 1);
976
+ try {
977
+ let jsonText = textResponse.text.trim();
978
+
979
+ // Try to extract JSON from markdown code blocks
980
+ const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
981
+ jsonText.match(/```\n([\s\S]*?)\n```/);
982
+
983
+ if (jsonMatch) {
984
+ jsonText = jsonMatch[1];
985
+ } else if (jsonText.includes("```json")) {
986
+ const start = jsonText.indexOf("```json") + 7;
987
+ const end = jsonText.lastIndexOf("```");
988
+ if (end > start) {
989
+ jsonText = jsonText.substring(start, end);
1149
990
  }
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
991
  }
1159
992
 
1160
- // Log the LLM's analysis/reasoning (like Cursor showing its thought process)
1161
- if (aiResponse.analysis) {
1162
- debugLog("LLM Analysis (Step 1)", {
1163
- currentState: aiResponse.analysis.currentState,
1164
- problem: aiResponse.analysis.problem,
1165
- fixStrategy: aiResponse.analysis.fixStrategy,
1166
- });
993
+ jsonText = jsonText.trim();
994
+
995
+ // Robust JSON extraction: find the first { and last } to extract JSON object
996
+ // This handles cases where the LLM includes preamble text before the JSON
997
+ const firstBrace = jsonText.indexOf('{');
998
+ const lastBrace = jsonText.lastIndexOf('}');
999
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
1000
+ jsonText = jsonText.substring(firstBrace, lastBrace + 1);
1167
1001
  }
1002
+
1003
+ aiResponse = JSON.parse(jsonText);
1004
+ } catch {
1005
+ console.error("Failed to parse AI response:", textResponse.text);
1006
+ return NextResponse.json(
1007
+ { error: "Failed to parse AI response. Please try again." },
1008
+ { status: 500 }
1009
+ );
1010
+ }
1168
1011
 
1169
1012
  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
1013
 
1185
- if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
1186
- return NextResponse.json({
1187
- success: true,
1188
- sessionId: newSessionId,
1189
- modifications: [],
1190
- explanation: aiResponse.explanation || "No changes needed.",
1191
- reasoning: aiResponse.reasoning,
1014
+ if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
1015
+ return NextResponse.json({
1016
+ success: true,
1017
+ sessionId: newSessionId,
1018
+ modifications: [],
1019
+ explanation: aiResponse.explanation || "No changes needed.",
1192
1020
  });
1193
1021
  }
1194
1022
 
1195
- debugLog("VALIDATION: Valid file paths from page context", {
1196
- pageFile: pageContext.pageFile,
1197
- validFilePaths: Array.from(validFilePaths),
1198
- aiRequestedFiles: aiResponse.modifications.map(m => m.filePath)
1199
- });
1023
+ debugLog("VALIDATION: Valid file paths from page context", {
1024
+ pageFile: pageContext.pageFile,
1025
+ validFilePaths: Array.from(validFilePaths),
1026
+ aiRequestedFiles: aiResponse.modifications.map(m => m.filePath)
1027
+ });
1200
1028
 
1201
- // Process modifications - apply patches to get modified content
1029
+ // Process modifications - apply patches to get modified content
1202
1030
  modifications = [];
1203
- const patchErrors: string[] = [];
1031
+ const patchErrors: string[] = [];
1204
1032
 
1205
1033
  for (const mod of aiResponse.modifications) {
1206
1034
  // Validate that the file path is in the page context
@@ -1332,24 +1160,13 @@ This is better than generating patches with made-up code.`,
1332
1160
  const spanDiff = Math.abs(openSpans - closeSpans);
1333
1161
  if (divDiff > 0 || spanDiff > 0) {
1334
1162
  patchErrors.push(`${mod.filePath}: LLM introduced syntax error - tag mismatch detected (${divDiff} div, ${spanDiff} span). Change rejected.`);
1335
- continue;
1163
+ continue;
1336
1164
  }
1337
1165
  }
1338
1166
  }
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
1167
  } else {
1351
- // No patches and no modifiedContent - skip
1352
- console.warn(`[Apply-First] No patches or modifiedContent for ${mod.filePath}`);
1168
+ // No patches - skip
1169
+ console.warn(`[Apply-First] No patches for ${mod.filePath}`);
1353
1170
  continue;
1354
1171
  }
1355
1172
 
@@ -1363,7 +1180,7 @@ This is better than generating patches with made-up code.`,
1363
1180
  }
1364
1181
 
1365
1182
  // If all modifications failed, check if we should retry
1366
- if (patchErrors.length > 0 && modifications.length === 0) {
1183
+ if (patchErrors.length > 0 && modifications.length === 0) {
1367
1184
  if (retryCount < MAX_RETRIES) {
1368
1185
  console.warn(`[Apply-First] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
1369
1186
  debugLog("Retry triggered due to patch failures", {
@@ -1378,20 +1195,20 @@ This is better than generating patches with made-up code.`,
1378
1195
 
1379
1196
  // Exhausted retries, return error
1380
1197
  console.error("All AI patches failed after retries:", patchErrors);
1381
- return NextResponse.json(
1382
- {
1383
- success: false,
1198
+ return NextResponse.json(
1199
+ {
1200
+ success: false,
1384
1201
  error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
1385
- },
1386
- { status: 400 }
1387
- );
1388
- }
1202
+ },
1203
+ { status: 400 }
1204
+ );
1205
+ }
1206
+
1207
+ // Log patch errors as warnings if some modifications succeeded
1208
+ if (patchErrors.length > 0) {
1209
+ console.warn("Some patches failed:", patchErrors);
1210
+ }
1389
1211
 
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
1212
  // Successfully processed at least some modifications - break out of retry loop
1396
1213
  break;
1397
1214
  } // End of retry loop
@@ -1408,7 +1225,6 @@ This is better than generating patches with made-up code.`,
1408
1225
  preview: true,
1409
1226
  modifications,
1410
1227
  explanation: finalExplanation,
1411
- reasoning: finalReasoning,
1412
1228
  });
1413
1229
  }
1414
1230
 
@@ -1432,7 +1248,6 @@ This is better than generating patches with made-up code.`,
1432
1248
  modifications,
1433
1249
  backupPaths: applyResult.backupPaths,
1434
1250
  explanation: finalExplanation,
1435
- reasoning: finalReasoning,
1436
1251
  });
1437
1252
  }
1438
1253
 
@@ -2244,13 +2059,13 @@ function getPathAliases(projectRoot: string): Map<string, string> {
2244
2059
 
2245
2060
  // Only log parse error once to avoid log spam
2246
2061
  if (!cachedParseError) {
2247
- const posMatch = errorStr.match(/position (\d+)/);
2248
- let context = "";
2249
- if (posMatch) {
2250
- const pos = parseInt(posMatch[1], 10);
2251
- const content = fs.readFileSync(tsconfigPath, "utf-8");
2252
- context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
2253
- }
2062
+ const posMatch = errorStr.match(/position (\d+)/);
2063
+ let context = "";
2064
+ if (posMatch) {
2065
+ const pos = parseInt(posMatch[1], 10);
2066
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
2067
+ context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
2068
+ }
2254
2069
  debugLog("[apply] Failed to parse tsconfig.json (will use defaults, logging once)", { error: errorStr, context });
2255
2070
  cachedParseError = errorStr;
2256
2071
  }
@@ -2273,14 +2088,14 @@ function getPathAliases(projectRoot: string): Map<string, string> {
2273
2088
  }
2274
2089
  // Only log default alias once (not when using cached error fallback)
2275
2090
  if (!cachedParseError) {
2276
- debugLog("[apply] Using default @/ alias", { alias: aliases.get("@/") });
2091
+ debugLog("[apply] Using default @/ alias", { alias: aliases.get("@/") });
2277
2092
  }
2278
2093
  }
2279
2094
 
2280
2095
  // Cache aliases if parsed successfully, no tsconfig exists, OR we have a parse error (cache fallback)
2281
2096
  if (parsedSuccessfully || !fs.existsSync(tsconfigPath) || cachedParseError) {
2282
- cachedPathAliases = aliases;
2283
- cachedProjectRoot = projectRoot;
2097
+ cachedPathAliases = aliases;
2098
+ cachedProjectRoot = projectRoot;
2284
2099
  }
2285
2100
 
2286
2101
  return aliases;
@@ -2582,105 +2397,3 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
2582
2397
  failedPatches,
2583
2398
  };
2584
2399
  }
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
- }