sonance-brand-mcp 1.3.99 → 1.3.101

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.
@@ -84,6 +84,40 @@ interface VisionEditResponse {
84
84
 
85
85
  const DEBUG_LOG_FILE = "sonance-debug.log";
86
86
 
87
+ /**
88
+ * TWO-PHASE DESIGN ANALYSIS TYPES
89
+ * Phase 1 identifies problems, Phase 2 generates patches that reference those problems
90
+ */
91
+
92
+ /** Categories of visual/UX problems that can be identified */
93
+ type DesignProblemCategory = 'hierarchy' | 'spacing' | 'alignment' | 'feedback' | 'grouping';
94
+
95
+ /** A specific visual problem identified in Phase 1 analysis */
96
+ interface DesignProblem {
97
+ /** Unique identifier (e.g., "hierarchy-1", "spacing-2") */
98
+ id: string;
99
+ /** Problem category */
100
+ category: DesignProblemCategory;
101
+ /** Specific description of the issue */
102
+ description: string;
103
+ /** Visual impact severity */
104
+ severity: 'high' | 'medium' | 'low';
105
+ /** Hint for what to look for in the code */
106
+ codeHint?: string;
107
+ }
108
+
109
+ /** Extended patch that references a specific problem from Phase 1 */
110
+ interface DesignPatch {
111
+ /** Reference to the problem this patch fixes */
112
+ problemId: string;
113
+ /** Exact code to find */
114
+ search: string;
115
+ /** Replacement code */
116
+ replace: string;
117
+ /** Explanation of how this fixes the problem */
118
+ rationale: string;
119
+ }
120
+
87
121
  /**
88
122
  * Debug logging utility - writes to sonance-debug.log in project root
89
123
  * This helps diagnose issues with file discovery, validation, and revert
@@ -885,6 +919,139 @@ Be smart - use your knowledge of React patterns to identify what type of element
885
919
  }
886
920
  }
887
921
 
922
+ /**
923
+ * PHASE 1: Design Problem Analysis
924
+ *
925
+ * Analyzes the screenshot to identify specific visual/UX problems.
926
+ * This is a SEPARATE LLM call that ONLY identifies problems - no code changes.
927
+ * The problems are then passed to Phase 2 to generate targeted patches.
928
+ */
929
+ async function analyzeDesignProblems(
930
+ screenshot: string,
931
+ fileContent: string,
932
+ userPrompt: string,
933
+ apiKey: string
934
+ ): Promise<DesignProblem[]> {
935
+ const anthropic = new Anthropic({ apiKey });
936
+ const base64Data = screenshot.split(",")[1] || screenshot;
937
+
938
+ debugLog("Phase 1 Design Analysis: Starting visual problem identification", {
939
+ promptPreview: userPrompt?.substring(0, 50),
940
+ fileContentLength: fileContent.length
941
+ });
942
+
943
+ try {
944
+ const response = await anthropic.messages.create({
945
+ model: "claude-sonnet-4-20250514",
946
+ max_tokens: 2048,
947
+ messages: [
948
+ {
949
+ role: "user",
950
+ content: [
951
+ {
952
+ type: "image",
953
+ source: {
954
+ type: "base64",
955
+ media_type: "image/png",
956
+ data: base64Data,
957
+ },
958
+ },
959
+ {
960
+ type: "text",
961
+ text: `You are a senior UI/UX designer. Analyze this screenshot and identify SPECIFIC visual problems.
962
+
963
+ User's request: "${userPrompt}"
964
+
965
+ Examine the UI for issues in these categories:
966
+
967
+ 1. HIERARCHY: Is there a clear visual focus? Are elements competing for attention?
968
+ - Look for: competing headings, unclear primary actions, visual clutter
969
+
970
+ 2. SPACING: Are related elements grouped? Is there awkward empty space?
971
+ - Look for: disconnected labels/values, uneven gaps, cramped or overly spread content
972
+
973
+ 3. ALIGNMENT: Do elements align naturally? Are there visual disconnects?
974
+ - Look for: misaligned text, floating elements, broken visual flow
975
+
976
+ 4. FEEDBACK: Are progress indicators, states, and actions clear?
977
+ - Look for: weak progress visualization, unclear states, orphaned status indicators
978
+
979
+ 5. GROUPING: Are related items visually connected? Do containers make sense?
980
+ - Look for: related content that appears separate, missing visual boundaries
981
+
982
+ For each problem found, provide:
983
+ - id: unique identifier (e.g., "hierarchy-1", "spacing-2")
984
+ - category: one of [hierarchy, spacing, alignment, feedback, grouping]
985
+ - description: specific description of what's wrong
986
+ - severity: high/medium/low based on visual impact
987
+ - codeHint: what to look for in the code to fix it
988
+
989
+ Return ONLY a JSON array of problems. Example:
990
+ [
991
+ {
992
+ "id": "grouping-1",
993
+ "category": "grouping",
994
+ "description": "The 'Essence Quality' label and '0% COMPLETE' badge appear visually disconnected - they should be grouped together",
995
+ "severity": "high",
996
+ "codeHint": "Check if label and badge are in separate containers or using absolute positioning"
997
+ },
998
+ {
999
+ "id": "spacing-1",
1000
+ "category": "spacing",
1001
+ "description": "Too much vertical gap between the header section and the quality indicator",
1002
+ "severity": "medium",
1003
+ "codeHint": "Look for large margin or padding values like mt-6, mb-8, py-6"
1004
+ }
1005
+ ]
1006
+
1007
+ IMPORTANT:
1008
+ - Only identify problems that are actually visible in the screenshot
1009
+ - Be specific - vague problems lead to vague fixes
1010
+ - Maximum 5 problems - focus on the most impactful ones
1011
+ - If no significant problems are found, return an empty array []
1012
+ - Do NOT suggest code changes - only identify problems
1013
+
1014
+ Return ONLY the JSON array, no other text.`,
1015
+ },
1016
+ ],
1017
+ },
1018
+ ],
1019
+ });
1020
+
1021
+ const textBlock = response.content.find((block) => block.type === "text");
1022
+ if (!textBlock || textBlock.type !== "text") {
1023
+ debugLog("Phase 1 Design Analysis: No text response from LLM");
1024
+ return [];
1025
+ }
1026
+
1027
+ let jsonText = textBlock.text.trim();
1028
+
1029
+ // Extract JSON from code fences if present
1030
+ const jsonMatch = jsonText.match(/```(?:json)?\n?([\s\S]*?)\n?```/) ||
1031
+ jsonText.match(/\[[\s\S]*\]/);
1032
+ if (jsonMatch) {
1033
+ jsonText = jsonMatch[1] || jsonMatch[0];
1034
+ }
1035
+
1036
+ const problems: DesignProblem[] = JSON.parse(jsonText);
1037
+
1038
+ // Validate the structure
1039
+ const validatedProblems = problems.filter(p =>
1040
+ p.id && p.category && p.description && p.severity
1041
+ ).slice(0, 5); // Maximum 5 problems
1042
+
1043
+ debugLog("Phase 1 Design Analysis: Identified problems", {
1044
+ count: validatedProblems.length,
1045
+ problems: validatedProblems.map(p => ({ id: p.id, category: p.category, severity: p.severity }))
1046
+ });
1047
+
1048
+ return validatedProblems;
1049
+ } catch (e) {
1050
+ debugLog("Phase 1 Design Analysis: Failed to analyze", { error: String(e) });
1051
+ return [];
1052
+ }
1053
+ }
1054
+
888
1055
  /**
889
1056
  * Search candidate files for JSX code matching the focused element
890
1057
  * This helps identify which file actually contains the element the user clicked on
@@ -1338,91 +1505,166 @@ Output format:
1338
1505
  The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
1339
1506
 
1340
1507
  /**
1341
- * DESIGNER ANALYSIS PROTOCOL
1342
- * Forces the LLM to analyze the UI like a senior designer before making changes.
1343
- * Identifies specific visual problems, maps them to code, then makes targeted fixes.
1508
+ * PHASE 2: Targeted Patch Generation Prompt
1509
+ *
1510
+ * This prompt is used AFTER Phase 1 has identified specific problems.
1511
+ * It requires each patch to reference a problem ID from Phase 1.
1344
1512
  */
1345
- const DESIGN_REASONING_PROMPT = `
1513
+ function buildPhase2DesignPrompt(problems: DesignProblem[]): string {
1514
+ if (problems.length === 0) {
1515
+ return ''; // No problems identified, skip design protocol
1516
+ }
1517
+
1518
+ const problemsJson = JSON.stringify(problems, null, 2);
1519
+
1520
+ return `
1346
1521
  ═══════════════════════════════════════════════════════════════════════════════
1347
- DESIGNER ANALYSIS PROTOCOL
1522
+ PHASE 2: TARGETED PATCH GENERATION
1348
1523
  ═══════════════════════════════════════════════════════════════════════════════
1349
1524
 
1350
- You are a senior UI/UX designer reviewing this component. Before generating any code changes, you MUST complete this analysis:
1525
+ A visual analysis identified these specific problems in the UI:
1351
1526
 
1352
- STEP 1 - VISUAL AUDIT (analyze the screenshot)
1353
- Examine the UI and identify specific issues in these categories:
1527
+ ${problemsJson}
1354
1528
 
1355
- HIERARCHY: Is there a clear visual focus? Are elements competing for attention?
1356
- - Look for: competing headings, unclear primary actions, visual clutter
1357
-
1358
- • SPACING: Are related elements grouped? Is there awkward empty space?
1359
- - Look for: disconnected labels/values, uneven gaps, cramped or overly spread content
1360
-
1361
- • ALIGNMENT: Do elements align naturally? Are there visual disconnects?
1362
- - Look for: misaligned text, floating elements, broken visual flow
1363
-
1364
- • FEEDBACK: Are progress indicators, states, and actions clear?
1365
- - Look for: weak progress visualization, unclear states, orphaned status indicators
1366
-
1367
- • GROUPING: Are related items visually connected? Do containers make sense?
1368
- - Look for: related content that appears separate, missing visual boundaries
1529
+ YOUR TASK: Generate patches that fix ONLY these specific problems.
1369
1530
 
1370
- List each problem you find with a specific description.
1531
+ STRICT RULES:
1532
+ 1. Each patch MUST include a "problemId" field referencing one of the problems above
1533
+ 2. Do NOT make changes unrelated to the identified problems
1534
+ 3. Make the SMALLEST change that fixes each problem
1535
+ 4. Do NOT change layout systems (flex→grid) unless absolutely necessary to fix a problem
1536
+ 5. Preserve existing structure - adjust properties, don't restructure
1371
1537
 
1372
- STEP 2 - CODE MAPPING
1373
- For each problem identified in Step 1, find the EXACT code in the file that causes it:
1538
+ For each problem, find the exact code that causes it and create a targeted fix.
1374
1539
 
1375
- Example format:
1376
- - Problem: "Badge disconnected from its label"
1377
- - Code: Line 45 shows \`<Badge className="absolute right-0">\` positioned separately from label at Line 42
1378
- - Fix needed: Move Badge inside the same flex container as the label
1379
-
1380
- Do this for EACH problem you identified.
1381
-
1382
- STEP 3 - PRIORITIZED FIXES
1383
- Rank your fixes by visual impact. Address the most impactful issues first.
1384
- Each fix should:
1385
- • Target a specific identified problem from Step 1
1386
- • Reference the exact code location from Step 2
1387
- Make focused changes that fix THAT specific problem
1540
+ OUTPUT FORMAT (note the problemId and rationale fields):
1541
+ {
1542
+ "modifications": [{
1543
+ "filePath": "components/Example.tsx",
1544
+ "patches": [
1545
+ {
1546
+ "problemId": "grouping-1",
1547
+ "search": "<Badge className=\"absolute right-4\">",
1548
+ "replace": "<Badge className=\"ml-2\">",
1549
+ "rationale": "Removes absolute positioning to group badge with label"
1550
+ },
1551
+ {
1552
+ "problemId": "spacing-1",
1553
+ "search": "className=\"mt-8\"",
1554
+ "replace": "className=\"mt-4\"",
1555
+ "rationale": "Reduces excessive vertical gap"
1556
+ }
1557
+ ]
1558
+ }]
1559
+ }
1388
1560
 
1389
- STEP 4 - GENERATE PATCHES
1390
- Now generate patches that implement your fixes.
1391
- Each patch should be traceable to a specific problem from Step 1
1392
- Multiple small patches are better than one large rewrite
1393
- • Every change must have a clear reason tied to your analysis
1561
+ VALIDATION:
1562
+ - Patches without a valid problemId will be REJECTED
1563
+ - Patches that don't trace to an identified problem will be REJECTED
1564
+ - Each patch must have search, replace, problemId, and rationale fields
1394
1565
  `;
1566
+ }
1395
1567
 
1396
1568
  /**
1397
- * DESIGN DECISION RULES
1398
- * Informed decision rules that allow fixing real problems while preventing unnecessary rewrites.
1569
+ * DESIGN DECISION RULES for Phase 2
1399
1570
  */
1400
1571
  const DESIGN_GUARDRAILS = `
1401
1572
  ═══════════════════════════════════════════════════════════════════════════════
1402
- DESIGN DECISION RULES
1573
+ DESIGN CONSTRAINTS
1403
1574
  ═══════════════════════════════════════════════════════════════════════════════
1404
1575
 
1405
1576
  ✓ ALLOWED when fixing identified problems:
1406
- • Adjust spacing, padding, margins to fix grouping issues
1407
- Reposition elements to improve visual hierarchy (move badge next to its label, etc.)
1408
- Add containers/wrappers to group related content that appears disconnected
1409
- Change flex direction or alignment to fix layout issues
1410
- Adjust typography for better hierarchy (size, weight, line-height)
1411
- • Improve progress indicators or status displays for clarity
1412
-
1413
- STILL AVOID:
1414
- Complete rewrites when targeted fixes would work
1415
- Changing functionality or interactive behavior
1416
- • Removing content or features the user didn't ask to remove
1417
- Changing color schemes unless specifically identified as a problem
1418
- • Making changes that aren't traceable to a specific visual problem you identified
1577
+ • Adjust spacing (margin, padding) - e.g., mt-4 mt-2
1578
+ Adjust alignment within containers - e.g., items-start items-center
1579
+ Move elements within the same container to improve grouping
1580
+ Adjust typography - font size, weight, line-height
1581
+ Add/remove whitespace classes
1582
+
1583
+ ✗ FORBIDDEN (will cause rejection):
1584
+ Changing layout system (flex → grid, grid → flex)
1585
+ Restructuring component hierarchy
1586
+ Wrapping elements in new containers (unless specifically needed for a grouping problem)
1587
+ • Removing or changing functionality
1588
+ Changes without a problemId reference
1419
1589
 
1420
- ═══════════════════════════════════════════════════════════════════════════════
1421
- PRINCIPLE: Every change must trace back to a specific visual problem you identified in Step 1.
1422
- If you can't explain WHY a change fixes a specific problem, don't make that change.
1423
1590
  ═══════════════════════════════════════════════════════════════════════════════
1424
1591
  `;
1425
1592
 
1593
+ /**
1594
+ * Validate patches against identified problems
1595
+ * Rejects patches that don't reference a valid problem ID
1596
+ */
1597
+ function validateDesignPatches(
1598
+ patches: Array<{ search: string; replace: string; problemId?: string; rationale?: string }>,
1599
+ problems: DesignProblem[]
1600
+ ): { valid: Array<{ search: string; replace: string; problemId: string; rationale: string }>; rejected: Array<{ patch: unknown; reason: string }> } {
1601
+ const validProblemIds = new Set(problems.map(p => p.id));
1602
+ const valid: Array<{ search: string; replace: string; problemId: string; rationale: string }> = [];
1603
+ const rejected: Array<{ patch: unknown; reason: string }> = [];
1604
+
1605
+ for (const patch of patches) {
1606
+ // Check for required fields
1607
+ if (!patch.search || !patch.replace) {
1608
+ rejected.push({ patch, reason: "Missing search or replace field" });
1609
+ continue;
1610
+ }
1611
+
1612
+ // For design-heavy requests, require problemId
1613
+ if (!patch.problemId) {
1614
+ rejected.push({ patch, reason: "Missing problemId - patch doesn't reference an identified problem" });
1615
+ continue;
1616
+ }
1617
+
1618
+ // Validate problemId exists in the problems list
1619
+ if (!validProblemIds.has(patch.problemId)) {
1620
+ rejected.push({ patch, reason: `Invalid problemId "${patch.problemId}" - not in identified problems list` });
1621
+ continue;
1622
+ }
1623
+
1624
+ // Check for forbidden structural changes
1625
+ const searchLower = patch.search.toLowerCase();
1626
+ const replaceLower = patch.replace.toLowerCase();
1627
+
1628
+ // Detect flex → grid or grid → flex conversions
1629
+ const hadFlex = searchLower.includes('flex') || searchLower.includes('items-') || searchLower.includes('justify-');
1630
+ const hasGrid = replaceLower.includes('grid') && !searchLower.includes('grid');
1631
+ const hadGrid = searchLower.includes('grid');
1632
+ const hasFlex = (replaceLower.includes('flex') || replaceLower.includes('items-') || replaceLower.includes('justify-')) && !hadFlex;
1633
+
1634
+ if ((hadFlex && hasGrid) || (hadGrid && hasFlex)) {
1635
+ rejected.push({ patch, reason: "Forbidden: Changing layout system (flex ↔ grid)" });
1636
+ continue;
1637
+ }
1638
+
1639
+ // Detect space-y/space-x additions that fundamentally change layout
1640
+ const addedSpaceY = replaceLower.includes('space-y') && !searchLower.includes('space-y');
1641
+ const addedSpaceX = replaceLower.includes('space-x') && !searchLower.includes('space-x');
1642
+ const hadJustifyBetween = searchLower.includes('justify-between');
1643
+
1644
+ if ((addedSpaceY || addedSpaceX) && hadJustifyBetween) {
1645
+ rejected.push({ patch, reason: "Forbidden: Converting flex layout to stack layout" });
1646
+ continue;
1647
+ }
1648
+
1649
+ valid.push({
1650
+ search: patch.search,
1651
+ replace: patch.replace,
1652
+ problemId: patch.problemId,
1653
+ rationale: patch.rationale || 'No rationale provided'
1654
+ });
1655
+ }
1656
+
1657
+ if (rejected.length > 0) {
1658
+ debugLog("Design patch validation: Some patches rejected", {
1659
+ validCount: valid.length,
1660
+ rejectedCount: rejected.length,
1661
+ rejected: rejected.map(r => ({ reason: r.reason, searchPreview: String(r.patch).substring(0, 50) }))
1662
+ });
1663
+ }
1664
+
1665
+ return { valid, rejected };
1666
+ }
1667
+
1426
1668
  /**
1427
1669
  * Detect if a user request is design-heavy and needs the reasoning protocol.
1428
1670
  * Simple requests like "change button color to blue" don't need it.
@@ -1814,151 +2056,175 @@ export async function POST(request: Request) {
1814
2056
  }
1815
2057
  }
1816
2058
 
1817
- // Build text content
1818
- let textContent = `VISION MODE EDIT REQUEST
1819
-
1820
- Page Route: ${pageRoute}
1821
- User Request: "${userPrompt}"
1822
-
1823
- `;
1824
-
1825
- // ========== DESIGN REASONING PROTOCOL ==========
1826
- // For design-heavy requests, add structured reasoning and guardrails
1827
- // This prevents over-engineering and ensures thoughtful changes
1828
- const isDesignRequest = isDesignHeavyRequest(userPrompt || '');
2059
+ // ========== FILE REDIRECT LOGIC (runs BEFORE building instructions) ==========
2060
+ // This determines the ACTUAL target file by checking for text matches in imported components
2061
+ // Must run first so that all subsequent code uses the correct file path
2062
+ let actualTargetFile = recommendedFileContent || (pageContext.pageContent ? { path: pageContext.pageFile, content: pageContext.pageContent } : null);
2063
+ let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
1829
2064
 
1830
- if (isDesignRequest) {
1831
- debugLog("Design-heavy request detected, adding reasoning protocol", {
1832
- prompt: userPrompt?.substring(0, 50),
1833
- triggerKeywords: ['redesign', 'better', 'improve', 'cleaner', 'layout', 'modernize']
1834
- .filter(k => userPrompt?.toLowerCase().includes(k))
1835
- });
1836
-
1837
- textContent += DESIGN_REASONING_PROMPT;
1838
- textContent += DESIGN_GUARDRAILS;
1839
- textContent += '\n';
1840
- }
1841
-
1842
- // ========== TARGET COMPONENT ONLY (with line numbers) ==========
1843
- // CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
1844
- if (recommendedFileContent) {
1845
- const content = recommendedFileContent.content;
2065
+ if (actualTargetFile && focusedElements && focusedElements.length > 0) {
2066
+ const content = actualTargetFile.content;
1846
2067
 
1847
2068
  // Search for focused element in the file using multiple strategies
1848
2069
  // Priority: DOM id > textContent > className patterns
1849
- let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
1850
- let actualTargetFile = recommendedFileContent; // May change if we redirect
1851
-
1852
- if (focusedElements && focusedElements.length > 0) {
1853
- for (const el of focusedElements) {
1854
- elementLocation = findElementLineInFile(content, el);
1855
- if (elementLocation) {
1856
- debugLog("Found focused element in file", {
1857
- matchedBy: elementLocation.matchedBy,
1858
- lineNumber: elementLocation.lineNumber,
1859
- confidence: elementLocation.confidence,
1860
- file: recommendedFileContent.path,
1861
- });
1862
- break;
1863
- }
1864
- }
1865
-
1866
- // TEXT SCORING REDIRECT: Score ALL imported files by how many discovered
1867
- // text strings they contain. The file with the highest score is the TRUE target.
1868
- // This handles parent/child component cases accurately.
1869
- if (elementLocation && elementLocation.confidence !== 'high') {
1870
- // Collect ALL text content from focused elements
1871
- const allTextContent = focusedElements
1872
- .filter(e => e.textContent && e.textContent.length > 5)
1873
- .map(e => e.textContent!);
1874
-
1875
- if (allTextContent.length > 0) {
1876
- debugLog("Medium/low confidence match - scoring imports for all text", {
1877
- currentFile: recommendedFileContent.path,
1878
- currentConfidence: elementLocation.confidence,
1879
- textCount: allTextContent.length,
1880
- texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
1881
- });
1882
-
1883
- // Score all imported files
1884
- const bestMatch = scoreFilesForTextContent(
1885
- allTextContent,
1886
- pageContext.componentSources
1887
- );
1888
-
1889
- // Also score the current file for comparison
1890
- const currentFileScore = allTextContent.filter(text =>
1891
- recommendedFileContent.content.includes(text.substring(0, 20))
1892
- ).length;
1893
-
1894
- debugLog("Text scoring comparison", {
1895
- currentFile: recommendedFileContent.path,
1896
- currentScore: currentFileScore,
1897
- bestImport: bestMatch?.path || 'none',
1898
- bestImportScore: bestMatch?.score || 0
1899
- });
1900
-
1901
- // Redirect only if imported file has MORE matches than current file
1902
- if (bestMatch && bestMatch.score > currentFileScore) {
1903
- debugLog("TEXT REDIRECT: Imported file has more text matches", {
1904
- originalFile: recommendedFileContent.path,
1905
- originalScore: currentFileScore,
1906
- redirectTo: bestMatch.path,
1907
- redirectScore: bestMatch.score,
1908
- matchedTexts: bestMatch.matchedTexts
1909
- });
1910
-
1911
- // Switch target file to where most text content lives
1912
- actualTargetFile = {
1913
- path: bestMatch.path,
1914
- content: bestMatch.content
1915
- };
1916
-
1917
- // Find a line with matched text for element location
1918
- const lines = bestMatch.content.split('\n');
1919
- const snippetStart = Math.max(0, bestMatch.firstMatchLine - 4);
1920
- const snippetEnd = Math.min(lines.length, bestMatch.firstMatchLine + 5);
1921
-
1922
- elementLocation = {
1923
- lineNumber: bestMatch.firstMatchLine,
1924
- snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
1925
- confidence: 'high',
1926
- matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
1927
- };
1928
- }
1929
- }
2070
+ for (const el of focusedElements) {
2071
+ elementLocation = findElementLineInFile(content, el);
2072
+ if (elementLocation) {
2073
+ debugLog("Found focused element in file", {
2074
+ matchedBy: elementLocation.matchedBy,
2075
+ lineNumber: elementLocation.lineNumber,
2076
+ confidence: elementLocation.confidence,
2077
+ file: actualTargetFile.path,
2078
+ });
2079
+ break;
1930
2080
  }
2081
+ }
2082
+
2083
+ // TEXT SCORING REDIRECT: Score ALL imported files by how many discovered
2084
+ // text strings they contain. The file with the highest score is the TRUE target.
2085
+ if (elementLocation && elementLocation.confidence !== 'high') {
2086
+ const allTextContent = focusedElements
2087
+ .filter(e => e.textContent && e.textContent.length > 5)
2088
+ .map(e => e.textContent!);
1931
2089
 
1932
- // DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
1933
- if (!elementLocation) {
1934
- debugLog("Element not in main file, searching imported components...", {
1935
- mainFile: recommendedFileContent.path,
1936
- importedFilesCount: pageContext.componentSources.length
2090
+ if (allTextContent.length > 0) {
2091
+ debugLog("Medium/low confidence match - scoring imports for all text", {
2092
+ currentFile: actualTargetFile.path,
2093
+ currentConfidence: elementLocation.confidence,
2094
+ textCount: allTextContent.length,
2095
+ texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
1937
2096
  });
1938
2097
 
1939
- const importedMatch = findElementInImportedFiles(
1940
- focusedElements[0],
2098
+ const bestMatch = scoreFilesForTextContent(
2099
+ allTextContent,
1941
2100
  pageContext.componentSources
1942
2101
  );
1943
2102
 
1944
- if (importedMatch) {
1945
- debugLog("REDIRECT: Element found in imported component", {
1946
- originalFile: recommendedFileContent.path,
1947
- redirectTo: importedMatch.path,
1948
- matchedBy: importedMatch.matchedBy,
1949
- lineNumber: importedMatch.lineNumber
2103
+ const currentFileScore = allTextContent.filter(text =>
2104
+ actualTargetFile!.content.includes(text.substring(0, 20))
2105
+ ).length;
2106
+
2107
+ debugLog("Text scoring comparison", {
2108
+ currentFile: actualTargetFile.path,
2109
+ currentScore: currentFileScore,
2110
+ bestImport: bestMatch?.path || 'none',
2111
+ bestImportScore: bestMatch?.score || 0
2112
+ });
2113
+
2114
+ if (bestMatch && bestMatch.score > currentFileScore) {
2115
+ debugLog("TEXT REDIRECT: Imported file has more text matches", {
2116
+ originalFile: actualTargetFile.path,
2117
+ originalScore: currentFileScore,
2118
+ redirectTo: bestMatch.path,
2119
+ redirectScore: bestMatch.score,
2120
+ matchedTexts: bestMatch.matchedTexts
1950
2121
  });
1951
2122
 
1952
- // Switch target file to where element actually is
1953
2123
  actualTargetFile = {
1954
- path: importedMatch.path,
1955
- content: importedMatch.content
2124
+ path: bestMatch.path,
2125
+ content: bestMatch.content
2126
+ };
2127
+
2128
+ const lines = bestMatch.content.split('\n');
2129
+ const snippetStart = Math.max(0, bestMatch.firstMatchLine - 4);
2130
+ const snippetEnd = Math.min(lines.length, bestMatch.firstMatchLine + 5);
2131
+
2132
+ elementLocation = {
2133
+ lineNumber: bestMatch.firstMatchLine,
2134
+ snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
2135
+ confidence: 'high',
2136
+ matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
1956
2137
  };
1957
- elementLocation = findElementLineInFile(importedMatch.content, focusedElements[0]);
1958
2138
  }
1959
2139
  }
1960
2140
  }
1961
2141
 
2142
+ // DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
2143
+ if (!elementLocation) {
2144
+ debugLog("Element not in main file, searching imported components...", {
2145
+ mainFile: actualTargetFile.path,
2146
+ importedFilesCount: pageContext.componentSources.length
2147
+ });
2148
+
2149
+ const importedMatch = findElementInImportedFiles(
2150
+ focusedElements[0],
2151
+ pageContext.componentSources
2152
+ );
2153
+
2154
+ if (importedMatch) {
2155
+ debugLog("REDIRECT: Element found in imported component", {
2156
+ originalFile: actualTargetFile.path,
2157
+ redirectTo: importedMatch.path,
2158
+ matchedBy: importedMatch.matchedBy,
2159
+ lineNumber: importedMatch.lineNumber
2160
+ });
2161
+
2162
+ actualTargetFile = {
2163
+ path: importedMatch.path,
2164
+ content: importedMatch.content
2165
+ };
2166
+ elementLocation = findElementLineInFile(importedMatch.content, focusedElements[0]);
2167
+ }
2168
+ }
2169
+ }
2170
+
2171
+ debugLog("File redirect complete", {
2172
+ originalRecommended: recommendedFileContent?.path || 'none',
2173
+ actualTarget: actualTargetFile?.path || 'none',
2174
+ wasRedirected: actualTargetFile?.path !== recommendedFileContent?.path
2175
+ });
2176
+
2177
+ // Build text content
2178
+ let textContent = `VISION MODE EDIT REQUEST
2179
+
2180
+ Page Route: ${pageRoute}
2181
+ User Request: "${userPrompt}"
2182
+
2183
+ `;
2184
+
2185
+ // ========== TWO-PHASE DESIGN ANALYSIS ==========
2186
+ // Phase 1: Identify specific visual problems (separate LLM call)
2187
+ // Phase 2: Generate patches that reference those problems
2188
+ const isDesignRequest = isDesignHeavyRequest(userPrompt || '');
2189
+ let identifiedProblems: DesignProblem[] = [];
2190
+
2191
+ if (isDesignRequest && screenshot) {
2192
+ debugLog("Design-heavy request detected, running Phase 1 analysis", {
2193
+ prompt: userPrompt?.substring(0, 50),
2194
+ triggerKeywords: ['redesign', 'better', 'improve', 'cleaner', 'layout', 'modernize']
2195
+ .filter(k => userPrompt?.toLowerCase().includes(k))
2196
+ });
2197
+
2198
+ // Phase 1: Analyze the screenshot to identify specific problems
2199
+ // This is a SEPARATE LLM call that only identifies problems
2200
+ // Use actualTargetFile since redirect has already happened
2201
+ const fileContentForAnalysis = actualTargetFile?.content || pageContext.pageContent || '';
2202
+ identifiedProblems = await analyzeDesignProblems(
2203
+ screenshot,
2204
+ fileContentForAnalysis,
2205
+ userPrompt || '',
2206
+ apiKey
2207
+ );
2208
+
2209
+ debugLog("Phase 1 complete: Problems identified", {
2210
+ problemCount: identifiedProblems.length,
2211
+ problems: identifiedProblems.map(p => `${p.id}: ${p.description.substring(0, 50)}`)
2212
+ });
2213
+
2214
+ // Add Phase 2 prompt with the identified problems
2215
+ if (identifiedProblems.length > 0) {
2216
+ textContent += buildPhase2DesignPrompt(identifiedProblems);
2217
+ textContent += DESIGN_GUARDRAILS;
2218
+ textContent += '\n';
2219
+ } else {
2220
+ debugLog("No problems identified - proceeding with standard edit");
2221
+ }
2222
+ }
2223
+
2224
+ // ========== TARGET COMPONENT ONLY (with line numbers) ==========
2225
+ // CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
2226
+ // Note: Redirect logic already ran above, so actualTargetFile is the correct file
2227
+ if (actualTargetFile) {
1962
2228
  // Build focused elements section with precise targeting info
1963
2229
  if (focusedElements && focusedElements.length > 0) {
1964
2230
  textContent += `FOCUSED ELEMENTS (user clicked on these):\n`;
@@ -2022,7 +2288,7 @@ ${elementLocation.snippet}
2022
2288
  // Element NOT found in main file OR any imported components
2023
2289
  // BLOCK the LLM from guessing - require empty modifications
2024
2290
  debugLog("BLOCK: Could not locate focused element anywhere", {
2025
- mainFile: recommendedFileContent.path,
2291
+ mainFile: actualTargetFile.path,
2026
2292
  searchedImports: pageContext.componentSources.length,
2027
2293
  focusedElements: focusedElements.map(el => ({
2028
2294
  name: el.name,
@@ -2038,7 +2304,7 @@ ${elementLocation.snippet}
2038
2304
  ⛔ STOP: CANNOT LOCATE THE CLICKED ELEMENT
2039
2305
 
2040
2306
  The user clicked on a specific element, but it could NOT be found in:
2041
- - ${recommendedFileContent.path} (main target file)
2307
+ - ${actualTargetFile.path} (main target file)
2042
2308
  - Any of the ${pageContext.componentSources.length} imported component files
2043
2309
 
2044
2310
  The element may be:
@@ -2080,7 +2346,7 @@ ${linesWithNumbers}
2080
2346
  path: actualTargetFile.path,
2081
2347
  lines: targetContent.split('\n').length,
2082
2348
  size: targetContent.length,
2083
- wasRedirected: actualTargetFile.path !== recommendedFileContent.path
2349
+ wasRedirected: actualTargetFile.path !== recommendedFileContent?.path
2084
2350
  });
2085
2351
  } else if (pageContext.pageContent) {
2086
2352
  // Fallback: use page file if no recommended file
@@ -2153,7 +2419,8 @@ ${variantsMatch[0]}
2153
2419
  }
2154
2420
 
2155
2421
  // ========== SIMPLIFIED INSTRUCTIONS ==========
2156
- const targetPath = recommendedFileContent?.path || pageContext.pageFile || "unknown";
2422
+ // Use actualTargetFile which may have been redirected to an imported component
2423
+ const targetPath = actualTargetFile?.path || pageContext.pageFile || "unknown";
2157
2424
 
2158
2425
  textContent += `═══════════════════════════════════════════════════════════════════════════════
2159
2426
  HOW TO MAKE YOUR EDIT
@@ -2365,9 +2632,46 @@ This is better than generating patches with made-up code.`,
2365
2632
  // New patch-based approach
2366
2633
  console.log(`[Vision Mode] Applying ${mod.patches.length} patches to ${mod.filePath}`);
2367
2634
 
2635
+ // DESIGN VALIDATION: For design-heavy requests, validate patches have problemIds
2636
+ let patchesToApply = mod.patches;
2637
+ if (isDesignRequest && identifiedProblems.length > 0) {
2638
+ const validationResult = validateDesignPatches(mod.patches, identifiedProblems);
2639
+
2640
+ if (validationResult.rejected.length > 0) {
2641
+ debugLog("Design patch validation rejected some patches", {
2642
+ filePath: mod.filePath,
2643
+ validCount: validationResult.valid.length,
2644
+ rejectedCount: validationResult.rejected.length,
2645
+ rejectedReasons: validationResult.rejected.map(r => r.reason)
2646
+ });
2647
+ console.warn(`[Vision Mode] ${validationResult.rejected.length} patches rejected for missing/invalid problemId`);
2648
+ }
2649
+
2650
+ // Only use validated patches
2651
+ if (validationResult.valid.length === 0) {
2652
+ patchErrors.push(`${mod.filePath}: All patches were rejected - they don't reference identified problems`);
2653
+ continue;
2654
+ }
2655
+
2656
+ // Log which problems are being fixed
2657
+ const problemsBeingFixed = [...new Set(validationResult.valid.map(p => p.problemId))];
2658
+ debugLog("Design patches validated", {
2659
+ filePath: mod.filePath,
2660
+ problemsBeingFixed,
2661
+ patchCount: validationResult.valid.length
2662
+ });
2663
+
2664
+ // Map validated patches to Patch interface (rationale -> explanation)
2665
+ patchesToApply = validationResult.valid.map(p => ({
2666
+ search: p.search,
2667
+ replace: p.replace,
2668
+ explanation: `[${p.problemId}] ${p.rationale}`
2669
+ }));
2670
+ }
2671
+
2368
2672
  // PRE-VALIDATION: Check if all search strings exist in the file BEFORE applying
2369
2673
  const preValidationErrors: string[] = [];
2370
- for (const patch of mod.patches) {
2674
+ for (const patch of patchesToApply) {
2371
2675
  const normalizedSearch = patch.search.replace(/\\n/g, "\n");
2372
2676
  if (!originalContent.includes(normalizedSearch)) {
2373
2677
  // Try fuzzy match as fallback
@@ -2406,7 +2710,7 @@ This is better than generating patches with made-up code.`,
2406
2710
  continue;
2407
2711
  }
2408
2712
 
2409
- const patchResult = applyPatches(originalContent, mod.patches);
2713
+ const patchResult = applyPatches(originalContent, patchesToApply);
2410
2714
 
2411
2715
  if (!patchResult.success) {
2412
2716
  const failedMessages = patchResult.failedPatches.map(
@@ -2416,15 +2720,15 @@ This is better than generating patches with made-up code.`,
2416
2720
 
2417
2721
  // If some patches succeeded, use partial result
2418
2722
  if (patchResult.appliedPatches > 0) {
2419
- console.warn(`[Vision Mode] ${patchResult.appliedPatches}/${mod.patches.length} patches applied to ${mod.filePath}`);
2723
+ console.warn(`[Vision Mode] ${patchResult.appliedPatches}/${patchesToApply.length} patches applied to ${mod.filePath}`);
2420
2724
  modifiedContent = patchResult.modifiedContent;
2421
- explanation += ` (${patchResult.appliedPatches}/${mod.patches.length} patches applied)`;
2725
+ explanation += ` (${patchResult.appliedPatches}/${patchesToApply.length} patches applied)`;
2422
2726
  } else {
2423
2727
  continue; // Skip this file entirely if no patches worked
2424
2728
  }
2425
2729
  } else {
2426
2730
  modifiedContent = patchResult.modifiedContent;
2427
- console.log(`[Vision Mode] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
2731
+ console.log(`[Vision Mode] All ${patchesToApply.length} patches applied successfully to ${mod.filePath}`);
2428
2732
  }
2429
2733
 
2430
2734
  // AST VALIDATION: Use Babel parser to catch actual syntax errors