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.
@@ -88,6 +88,40 @@ interface BackupManifest {
88
88
  const BACKUP_ROOT = ".sonance-backups";
89
89
  const DEBUG_LOG_FILE = "sonance-debug.log";
90
90
 
91
+ /**
92
+ * TWO-PHASE DESIGN ANALYSIS TYPES
93
+ * Phase 1 identifies problems, Phase 2 generates patches that reference those problems
94
+ */
95
+
96
+ /** Categories of visual/UX problems that can be identified */
97
+ type DesignProblemCategory = 'hierarchy' | 'spacing' | 'alignment' | 'feedback' | 'grouping';
98
+
99
+ /** A specific visual problem identified in Phase 1 analysis */
100
+ interface DesignProblem {
101
+ /** Unique identifier (e.g., "hierarchy-1", "spacing-2") */
102
+ id: string;
103
+ /** Problem category */
104
+ category: DesignProblemCategory;
105
+ /** Specific description of the issue */
106
+ description: string;
107
+ /** Visual impact severity */
108
+ severity: 'high' | 'medium' | 'low';
109
+ /** Hint for what to look for in the code */
110
+ codeHint?: string;
111
+ }
112
+
113
+ /** Extended patch that references a specific problem from Phase 1 */
114
+ interface DesignPatch {
115
+ /** Reference to the problem this patch fixes */
116
+ problemId: string;
117
+ /** Exact code to find */
118
+ search: string;
119
+ /** Replacement code */
120
+ replace: string;
121
+ /** Explanation of how this fixes the problem */
122
+ rationale: string;
123
+ }
124
+
91
125
  /**
92
126
  * Debug logging utility - writes to sonance-debug.log in project root
93
127
  * This helps diagnose issues with file discovery, validation, and revert
@@ -889,6 +923,139 @@ Be smart - use your knowledge of React patterns to identify what type of element
889
923
  }
890
924
  }
891
925
 
926
+ /**
927
+ * PHASE 1: Design Problem Analysis
928
+ *
929
+ * Analyzes the screenshot to identify specific visual/UX problems.
930
+ * This is a SEPARATE LLM call that ONLY identifies problems - no code changes.
931
+ * The problems are then passed to Phase 2 to generate targeted patches.
932
+ */
933
+ async function analyzeDesignProblems(
934
+ screenshot: string,
935
+ fileContent: string,
936
+ userPrompt: string,
937
+ apiKey: string
938
+ ): Promise<DesignProblem[]> {
939
+ const anthropic = new Anthropic({ apiKey });
940
+ const base64Data = screenshot.split(",")[1] || screenshot;
941
+
942
+ debugLog("Phase 1 Design Analysis: Starting visual problem identification", {
943
+ promptPreview: userPrompt?.substring(0, 50),
944
+ fileContentLength: fileContent.length
945
+ });
946
+
947
+ try {
948
+ const response = await anthropic.messages.create({
949
+ model: "claude-sonnet-4-20250514",
950
+ max_tokens: 2048,
951
+ messages: [
952
+ {
953
+ role: "user",
954
+ content: [
955
+ {
956
+ type: "image",
957
+ source: {
958
+ type: "base64",
959
+ media_type: "image/png",
960
+ data: base64Data,
961
+ },
962
+ },
963
+ {
964
+ type: "text",
965
+ text: `You are a senior UI/UX designer. Analyze this screenshot and identify SPECIFIC visual problems.
966
+
967
+ User's request: "${userPrompt}"
968
+
969
+ Examine the UI for issues in these categories:
970
+
971
+ 1. HIERARCHY: Is there a clear visual focus? Are elements competing for attention?
972
+ - Look for: competing headings, unclear primary actions, visual clutter
973
+
974
+ 2. SPACING: Are related elements grouped? Is there awkward empty space?
975
+ - Look for: disconnected labels/values, uneven gaps, cramped or overly spread content
976
+
977
+ 3. ALIGNMENT: Do elements align naturally? Are there visual disconnects?
978
+ - Look for: misaligned text, floating elements, broken visual flow
979
+
980
+ 4. FEEDBACK: Are progress indicators, states, and actions clear?
981
+ - Look for: weak progress visualization, unclear states, orphaned status indicators
982
+
983
+ 5. GROUPING: Are related items visually connected? Do containers make sense?
984
+ - Look for: related content that appears separate, missing visual boundaries
985
+
986
+ For each problem found, provide:
987
+ - id: unique identifier (e.g., "hierarchy-1", "spacing-2")
988
+ - category: one of [hierarchy, spacing, alignment, feedback, grouping]
989
+ - description: specific description of what's wrong
990
+ - severity: high/medium/low based on visual impact
991
+ - codeHint: what to look for in the code to fix it
992
+
993
+ Return ONLY a JSON array of problems. Example:
994
+ [
995
+ {
996
+ "id": "grouping-1",
997
+ "category": "grouping",
998
+ "description": "The 'Essence Quality' label and '0% COMPLETE' badge appear visually disconnected - they should be grouped together",
999
+ "severity": "high",
1000
+ "codeHint": "Check if label and badge are in separate containers or using absolute positioning"
1001
+ },
1002
+ {
1003
+ "id": "spacing-1",
1004
+ "category": "spacing",
1005
+ "description": "Too much vertical gap between the header section and the quality indicator",
1006
+ "severity": "medium",
1007
+ "codeHint": "Look for large margin or padding values like mt-6, mb-8, py-6"
1008
+ }
1009
+ ]
1010
+
1011
+ IMPORTANT:
1012
+ - Only identify problems that are actually visible in the screenshot
1013
+ - Be specific - vague problems lead to vague fixes
1014
+ - Maximum 5 problems - focus on the most impactful ones
1015
+ - If no significant problems are found, return an empty array []
1016
+ - Do NOT suggest code changes - only identify problems
1017
+
1018
+ Return ONLY the JSON array, no other text.`,
1019
+ },
1020
+ ],
1021
+ },
1022
+ ],
1023
+ });
1024
+
1025
+ const textBlock = response.content.find((block) => block.type === "text");
1026
+ if (!textBlock || textBlock.type !== "text") {
1027
+ debugLog("Phase 1 Design Analysis: No text response from LLM");
1028
+ return [];
1029
+ }
1030
+
1031
+ let jsonText = textBlock.text.trim();
1032
+
1033
+ // Extract JSON from code fences if present
1034
+ const jsonMatch = jsonText.match(/```(?:json)?\n?([\s\S]*?)\n?```/) ||
1035
+ jsonText.match(/\[[\s\S]*\]/);
1036
+ if (jsonMatch) {
1037
+ jsonText = jsonMatch[1] || jsonMatch[0];
1038
+ }
1039
+
1040
+ const problems: DesignProblem[] = JSON.parse(jsonText);
1041
+
1042
+ // Validate the structure
1043
+ const validatedProblems = problems.filter(p =>
1044
+ p.id && p.category && p.description && p.severity
1045
+ ).slice(0, 5); // Maximum 5 problems
1046
+
1047
+ debugLog("Phase 1 Design Analysis: Identified problems", {
1048
+ count: validatedProblems.length,
1049
+ problems: validatedProblems.map(p => ({ id: p.id, category: p.category, severity: p.severity }))
1050
+ });
1051
+
1052
+ return validatedProblems;
1053
+ } catch (e) {
1054
+ debugLog("Phase 1 Design Analysis: Failed to analyze", { error: String(e) });
1055
+ return [];
1056
+ }
1057
+ }
1058
+
892
1059
  /**
893
1060
  * Search candidate files for JSX code matching the focused element
894
1061
  * This helps identify which file actually contains the element the user clicked on
@@ -1342,91 +1509,166 @@ Output format:
1342
1509
  The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
1343
1510
 
1344
1511
  /**
1345
- * DESIGNER ANALYSIS PROTOCOL
1346
- * Forces the LLM to analyze the UI like a senior designer before making changes.
1347
- * Identifies specific visual problems, maps them to code, then makes targeted fixes.
1512
+ * PHASE 2: Targeted Patch Generation Prompt
1513
+ *
1514
+ * This prompt is used AFTER Phase 1 has identified specific problems.
1515
+ * It requires each patch to reference a problem ID from Phase 1.
1348
1516
  */
1349
- const DESIGN_REASONING_PROMPT = `
1517
+ function buildPhase2DesignPrompt(problems: DesignProblem[]): string {
1518
+ if (problems.length === 0) {
1519
+ return ''; // No problems identified, skip design protocol
1520
+ }
1521
+
1522
+ const problemsJson = JSON.stringify(problems, null, 2);
1523
+
1524
+ return `
1350
1525
  ═══════════════════════════════════════════════════════════════════════════════
1351
- DESIGNER ANALYSIS PROTOCOL
1526
+ PHASE 2: TARGETED PATCH GENERATION
1352
1527
  ═══════════════════════════════════════════════════════════════════════════════
1353
1528
 
1354
- You are a senior UI/UX designer reviewing this component. Before generating any code changes, you MUST complete this analysis:
1355
-
1356
- STEP 1 - VISUAL AUDIT (analyze the screenshot)
1357
- Examine the UI and identify specific issues in these categories:
1358
-
1359
- • HIERARCHY: Is there a clear visual focus? Are elements competing for attention?
1360
- - Look for: competing headings, unclear primary actions, visual clutter
1361
-
1362
- • SPACING: Are related elements grouped? Is there awkward empty space?
1363
- - Look for: disconnected labels/values, uneven gaps, cramped or overly spread content
1364
-
1365
- • ALIGNMENT: Do elements align naturally? Are there visual disconnects?
1366
- - Look for: misaligned text, floating elements, broken visual flow
1367
-
1368
- • FEEDBACK: Are progress indicators, states, and actions clear?
1369
- - Look for: weak progress visualization, unclear states, orphaned status indicators
1370
-
1371
- • GROUPING: Are related items visually connected? Do containers make sense?
1372
- - Look for: related content that appears separate, missing visual boundaries
1529
+ A visual analysis identified these specific problems in the UI:
1373
1530
 
1374
- List each problem you find with a specific description.
1531
+ ${problemsJson}
1375
1532
 
1376
- STEP 2 - CODE MAPPING
1377
- For each problem identified in Step 1, find the EXACT code in the file that causes it:
1533
+ YOUR TASK: Generate patches that fix ONLY these specific problems.
1378
1534
 
1379
- Example format:
1380
- - Problem: "Badge disconnected from its label"
1381
- - Code: Line 45 shows \`<Badge className="absolute right-0">\` positioned separately from label at Line 42
1382
- - Fix needed: Move Badge inside the same flex container as the label
1535
+ STRICT RULES:
1536
+ 1. Each patch MUST include a "problemId" field referencing one of the problems above
1537
+ 2. Do NOT make changes unrelated to the identified problems
1538
+ 3. Make the SMALLEST change that fixes each problem
1539
+ 4. Do NOT change layout systems (flex→grid) unless absolutely necessary to fix a problem
1540
+ 5. Preserve existing structure - adjust properties, don't restructure
1383
1541
 
1384
- Do this for EACH problem you identified.
1542
+ For each problem, find the exact code that causes it and create a targeted fix.
1385
1543
 
1386
- STEP 3 - PRIORITIZED FIXES
1387
- Rank your fixes by visual impact. Address the most impactful issues first.
1388
- Each fix should:
1389
- Target a specific identified problem from Step 1
1390
- Reference the exact code location from Step 2
1391
- • Make focused changes that fix THAT specific problem
1544
+ OUTPUT FORMAT (note the problemId and rationale fields):
1545
+ {
1546
+ "modifications": [{
1547
+ "filePath": "components/Example.tsx",
1548
+ "patches": [
1549
+ {
1550
+ "problemId": "grouping-1",
1551
+ "search": "<Badge className=\"absolute right-4\">",
1552
+ "replace": "<Badge className=\"ml-2\">",
1553
+ "rationale": "Removes absolute positioning to group badge with label"
1554
+ },
1555
+ {
1556
+ "problemId": "spacing-1",
1557
+ "search": "className=\"mt-8\"",
1558
+ "replace": "className=\"mt-4\"",
1559
+ "rationale": "Reduces excessive vertical gap"
1560
+ }
1561
+ ]
1562
+ }]
1563
+ }
1392
1564
 
1393
- STEP 4 - GENERATE PATCHES
1394
- Now generate patches that implement your fixes.
1395
- Each patch should be traceable to a specific problem from Step 1
1396
- Multiple small patches are better than one large rewrite
1397
- • Every change must have a clear reason tied to your analysis
1565
+ VALIDATION:
1566
+ - Patches without a valid problemId will be REJECTED
1567
+ - Patches that don't trace to an identified problem will be REJECTED
1568
+ - Each patch must have search, replace, problemId, and rationale fields
1398
1569
  `;
1570
+ }
1399
1571
 
1400
1572
  /**
1401
- * DESIGN DECISION RULES
1402
- * Informed decision rules that allow fixing real problems while preventing unnecessary rewrites.
1573
+ * DESIGN DECISION RULES for Phase 2
1403
1574
  */
1404
1575
  const DESIGN_GUARDRAILS = `
1405
1576
  ═══════════════════════════════════════════════════════════════════════════════
1406
- DESIGN DECISION RULES
1577
+ DESIGN CONSTRAINTS
1407
1578
  ═══════════════════════════════════════════════════════════════════════════════
1408
1579
 
1409
1580
  ✓ ALLOWED when fixing identified problems:
1410
- • Adjust spacing, padding, margins to fix grouping issues
1411
- Reposition elements to improve visual hierarchy (move badge next to its label, etc.)
1412
- Add containers/wrappers to group related content that appears disconnected
1413
- Change flex direction or alignment to fix layout issues
1414
- Adjust typography for better hierarchy (size, weight, line-height)
1415
- • Improve progress indicators or status displays for clarity
1416
-
1417
- STILL AVOID:
1418
- Complete rewrites when targeted fixes would work
1419
- Changing functionality or interactive behavior
1420
- • Removing content or features the user didn't ask to remove
1421
- Changing color schemes unless specifically identified as a problem
1422
- • Making changes that aren't traceable to a specific visual problem you identified
1581
+ • Adjust spacing (margin, padding) - e.g., mt-4 mt-2
1582
+ Adjust alignment within containers - e.g., items-start items-center
1583
+ Move elements within the same container to improve grouping
1584
+ Adjust typography - font size, weight, line-height
1585
+ Add/remove whitespace classes
1586
+
1587
+ ✗ FORBIDDEN (will cause rejection):
1588
+ Changing layout system (flex → grid, grid → flex)
1589
+ Restructuring component hierarchy
1590
+ Wrapping elements in new containers (unless specifically needed for a grouping problem)
1591
+ • Removing or changing functionality
1592
+ Changes without a problemId reference
1423
1593
 
1424
- ═══════════════════════════════════════════════════════════════════════════════
1425
- PRINCIPLE: Every change must trace back to a specific visual problem you identified in Step 1.
1426
- If you can't explain WHY a change fixes a specific problem, don't make that change.
1427
1594
  ═══════════════════════════════════════════════════════════════════════════════
1428
1595
  `;
1429
1596
 
1597
+ /**
1598
+ * Validate patches against identified problems
1599
+ * Rejects patches that don't reference a valid problem ID
1600
+ */
1601
+ function validateDesignPatches(
1602
+ patches: Array<{ search: string; replace: string; problemId?: string; rationale?: string }>,
1603
+ problems: DesignProblem[]
1604
+ ): { valid: Array<{ search: string; replace: string; problemId: string; rationale: string }>; rejected: Array<{ patch: unknown; reason: string }> } {
1605
+ const validProblemIds = new Set(problems.map(p => p.id));
1606
+ const valid: Array<{ search: string; replace: string; problemId: string; rationale: string }> = [];
1607
+ const rejected: Array<{ patch: unknown; reason: string }> = [];
1608
+
1609
+ for (const patch of patches) {
1610
+ // Check for required fields
1611
+ if (!patch.search || !patch.replace) {
1612
+ rejected.push({ patch, reason: "Missing search or replace field" });
1613
+ continue;
1614
+ }
1615
+
1616
+ // For design-heavy requests, require problemId
1617
+ if (!patch.problemId) {
1618
+ rejected.push({ patch, reason: "Missing problemId - patch doesn't reference an identified problem" });
1619
+ continue;
1620
+ }
1621
+
1622
+ // Validate problemId exists in the problems list
1623
+ if (!validProblemIds.has(patch.problemId)) {
1624
+ rejected.push({ patch, reason: `Invalid problemId "${patch.problemId}" - not in identified problems list` });
1625
+ continue;
1626
+ }
1627
+
1628
+ // Check for forbidden structural changes
1629
+ const searchLower = patch.search.toLowerCase();
1630
+ const replaceLower = patch.replace.toLowerCase();
1631
+
1632
+ // Detect flex → grid or grid → flex conversions
1633
+ const hadFlex = searchLower.includes('flex') || searchLower.includes('items-') || searchLower.includes('justify-');
1634
+ const hasGrid = replaceLower.includes('grid') && !searchLower.includes('grid');
1635
+ const hadGrid = searchLower.includes('grid');
1636
+ const hasFlex = (replaceLower.includes('flex') || replaceLower.includes('items-') || replaceLower.includes('justify-')) && !hadFlex;
1637
+
1638
+ if ((hadFlex && hasGrid) || (hadGrid && hasFlex)) {
1639
+ rejected.push({ patch, reason: "Forbidden: Changing layout system (flex ↔ grid)" });
1640
+ continue;
1641
+ }
1642
+
1643
+ // Detect space-y/space-x additions that fundamentally change layout
1644
+ const addedSpaceY = replaceLower.includes('space-y') && !searchLower.includes('space-y');
1645
+ const addedSpaceX = replaceLower.includes('space-x') && !searchLower.includes('space-x');
1646
+ const hadJustifyBetween = searchLower.includes('justify-between');
1647
+
1648
+ if ((addedSpaceY || addedSpaceX) && hadJustifyBetween) {
1649
+ rejected.push({ patch, reason: "Forbidden: Converting flex layout to stack layout" });
1650
+ continue;
1651
+ }
1652
+
1653
+ valid.push({
1654
+ search: patch.search,
1655
+ replace: patch.replace,
1656
+ problemId: patch.problemId,
1657
+ rationale: patch.rationale || 'No rationale provided'
1658
+ });
1659
+ }
1660
+
1661
+ if (rejected.length > 0) {
1662
+ debugLog("Design patch validation: Some patches rejected", {
1663
+ validCount: valid.length,
1664
+ rejectedCount: rejected.length,
1665
+ rejected: rejected.map(r => ({ reason: r.reason, searchPreview: String(r.patch).substring(0, 50) }))
1666
+ });
1667
+ }
1668
+
1669
+ return { valid, rejected };
1670
+ }
1671
+
1430
1672
  /**
1431
1673
  * Detect if a user request is design-heavy and needs the reasoning protocol.
1432
1674
  * Simple requests like "change button color to blue" don't need it.
@@ -1845,151 +2087,175 @@ export async function POST(request: Request) {
1845
2087
  }
1846
2088
  }
1847
2089
 
1848
- // Build text content
1849
- let textContent = `VISION MODE EDIT REQUEST
1850
-
1851
- Page Route: ${pageRoute}
1852
- User Request: "${userPrompt}"
1853
-
1854
- `;
1855
-
1856
- // ========== DESIGN REASONING PROTOCOL ==========
1857
- // For design-heavy requests, add structured reasoning and guardrails
1858
- // This prevents over-engineering and ensures thoughtful changes
1859
- const isDesignRequest = isDesignHeavyRequest(userPrompt || '');
2090
+ // ========== FILE REDIRECT LOGIC (runs BEFORE building instructions) ==========
2091
+ // This determines the ACTUAL target file by checking for text matches in imported components
2092
+ // Must run first so that all subsequent code uses the correct file path
2093
+ let actualTargetFile = recommendedFileContent || (pageContext.pageContent ? { path: pageContext.pageFile, content: pageContext.pageContent } : null);
2094
+ let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
1860
2095
 
1861
- if (isDesignRequest) {
1862
- debugLog("Design-heavy request detected, adding reasoning protocol", {
1863
- prompt: userPrompt?.substring(0, 50),
1864
- triggerKeywords: ['redesign', 'better', 'improve', 'cleaner', 'layout', 'modernize']
1865
- .filter(k => userPrompt?.toLowerCase().includes(k))
1866
- });
1867
-
1868
- textContent += DESIGN_REASONING_PROMPT;
1869
- textContent += DESIGN_GUARDRAILS;
1870
- textContent += '\n';
1871
- }
1872
-
1873
- // ========== TARGET COMPONENT ONLY (with line numbers) ==========
1874
- // CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
1875
- if (recommendedFileContent) {
1876
- const content = recommendedFileContent.content;
2096
+ if (actualTargetFile && focusedElements && focusedElements.length > 0) {
2097
+ const content = actualTargetFile.content;
1877
2098
 
1878
2099
  // Search for focused element in the file using multiple strategies
1879
2100
  // Priority: DOM id > textContent > className patterns
1880
- let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
1881
- let actualTargetFile = recommendedFileContent; // May change if we redirect
1882
-
1883
- if (focusedElements && focusedElements.length > 0) {
1884
- for (const el of focusedElements) {
1885
- elementLocation = findElementLineInFile(content, el);
1886
- if (elementLocation) {
1887
- debugLog("Found focused element in file", {
1888
- matchedBy: elementLocation.matchedBy,
1889
- lineNumber: elementLocation.lineNumber,
1890
- confidence: elementLocation.confidence,
1891
- file: recommendedFileContent.path,
1892
- });
1893
- break;
1894
- }
1895
- }
1896
-
1897
- // TEXT SCORING REDIRECT: Score ALL imported files by how many discovered
1898
- // text strings they contain. The file with the highest score is the TRUE target.
1899
- // This handles parent/child component cases accurately.
1900
- if (elementLocation && elementLocation.confidence !== 'high') {
1901
- // Collect ALL text content from focused elements
1902
- const allTextContent = focusedElements
1903
- .filter(e => e.textContent && e.textContent.length > 5)
1904
- .map(e => e.textContent!);
1905
-
1906
- if (allTextContent.length > 0) {
1907
- debugLog("Medium/low confidence match - scoring imports for all text", {
1908
- currentFile: recommendedFileContent.path,
1909
- currentConfidence: elementLocation.confidence,
1910
- textCount: allTextContent.length,
1911
- texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
1912
- });
1913
-
1914
- // Score all imported files
1915
- const bestMatch = scoreFilesForTextContent(
1916
- allTextContent,
1917
- pageContext.componentSources
1918
- );
1919
-
1920
- // Also score the current file for comparison
1921
- const currentFileScore = allTextContent.filter(text =>
1922
- recommendedFileContent.content.includes(text.substring(0, 20))
1923
- ).length;
1924
-
1925
- debugLog("Text scoring comparison", {
1926
- currentFile: recommendedFileContent.path,
1927
- currentScore: currentFileScore,
1928
- bestImport: bestMatch?.path || 'none',
1929
- bestImportScore: bestMatch?.score || 0
1930
- });
1931
-
1932
- // Redirect only if imported file has MORE matches than current file
1933
- if (bestMatch && bestMatch.score > currentFileScore) {
1934
- debugLog("TEXT REDIRECT: Imported file has more text matches", {
1935
- originalFile: recommendedFileContent.path,
1936
- originalScore: currentFileScore,
1937
- redirectTo: bestMatch.path,
1938
- redirectScore: bestMatch.score,
1939
- matchedTexts: bestMatch.matchedTexts
1940
- });
1941
-
1942
- // Switch target file to where most text content lives
1943
- actualTargetFile = {
1944
- path: bestMatch.path,
1945
- content: bestMatch.content
1946
- };
1947
-
1948
- // Find a line with matched text for element location
1949
- const lines = bestMatch.content.split('\n');
1950
- const snippetStart = Math.max(0, bestMatch.firstMatchLine - 4);
1951
- const snippetEnd = Math.min(lines.length, bestMatch.firstMatchLine + 5);
1952
-
1953
- elementLocation = {
1954
- lineNumber: bestMatch.firstMatchLine,
1955
- snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
1956
- confidence: 'high',
1957
- matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
1958
- };
1959
- }
1960
- }
2101
+ for (const el of focusedElements) {
2102
+ elementLocation = findElementLineInFile(content, el);
2103
+ if (elementLocation) {
2104
+ debugLog("Found focused element in file", {
2105
+ matchedBy: elementLocation.matchedBy,
2106
+ lineNumber: elementLocation.lineNumber,
2107
+ confidence: elementLocation.confidence,
2108
+ file: actualTargetFile.path,
2109
+ });
2110
+ break;
1961
2111
  }
2112
+ }
2113
+
2114
+ // TEXT SCORING REDIRECT: Score ALL imported files by how many discovered
2115
+ // text strings they contain. The file with the highest score is the TRUE target.
2116
+ if (elementLocation && elementLocation.confidence !== 'high') {
2117
+ const allTextContent = focusedElements
2118
+ .filter(e => e.textContent && e.textContent.length > 5)
2119
+ .map(e => e.textContent!);
1962
2120
 
1963
- // DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
1964
- if (!elementLocation) {
1965
- debugLog("Element not in main file, searching imported components...", {
1966
- mainFile: recommendedFileContent.path,
1967
- importedFilesCount: pageContext.componentSources.length
2121
+ if (allTextContent.length > 0) {
2122
+ debugLog("Medium/low confidence match - scoring imports for all text", {
2123
+ currentFile: actualTargetFile.path,
2124
+ currentConfidence: elementLocation.confidence,
2125
+ textCount: allTextContent.length,
2126
+ texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
1968
2127
  });
1969
2128
 
1970
- const importedMatch = findElementInImportedFiles(
1971
- focusedElements[0],
2129
+ const bestMatch = scoreFilesForTextContent(
2130
+ allTextContent,
1972
2131
  pageContext.componentSources
1973
2132
  );
1974
2133
 
1975
- if (importedMatch) {
1976
- debugLog("REDIRECT: Element found in imported component", {
1977
- originalFile: recommendedFileContent.path,
1978
- redirectTo: importedMatch.path,
1979
- matchedBy: importedMatch.matchedBy,
1980
- lineNumber: importedMatch.lineNumber
2134
+ const currentFileScore = allTextContent.filter(text =>
2135
+ actualTargetFile!.content.includes(text.substring(0, 20))
2136
+ ).length;
2137
+
2138
+ debugLog("Text scoring comparison", {
2139
+ currentFile: actualTargetFile.path,
2140
+ currentScore: currentFileScore,
2141
+ bestImport: bestMatch?.path || 'none',
2142
+ bestImportScore: bestMatch?.score || 0
2143
+ });
2144
+
2145
+ if (bestMatch && bestMatch.score > currentFileScore) {
2146
+ debugLog("TEXT REDIRECT: Imported file has more text matches", {
2147
+ originalFile: actualTargetFile.path,
2148
+ originalScore: currentFileScore,
2149
+ redirectTo: bestMatch.path,
2150
+ redirectScore: bestMatch.score,
2151
+ matchedTexts: bestMatch.matchedTexts
1981
2152
  });
1982
2153
 
1983
- // Switch target file to where element actually is
1984
2154
  actualTargetFile = {
1985
- path: importedMatch.path,
1986
- content: importedMatch.content
2155
+ path: bestMatch.path,
2156
+ content: bestMatch.content
2157
+ };
2158
+
2159
+ const lines = bestMatch.content.split('\n');
2160
+ const snippetStart = Math.max(0, bestMatch.firstMatchLine - 4);
2161
+ const snippetEnd = Math.min(lines.length, bestMatch.firstMatchLine + 5);
2162
+
2163
+ elementLocation = {
2164
+ lineNumber: bestMatch.firstMatchLine,
2165
+ snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
2166
+ confidence: 'high',
2167
+ matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
1987
2168
  };
1988
- elementLocation = findElementLineInFile(importedMatch.content, focusedElements[0]);
1989
2169
  }
1990
2170
  }
1991
2171
  }
1992
2172
 
2173
+ // DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
2174
+ if (!elementLocation) {
2175
+ debugLog("Element not in main file, searching imported components...", {
2176
+ mainFile: actualTargetFile.path,
2177
+ importedFilesCount: pageContext.componentSources.length
2178
+ });
2179
+
2180
+ const importedMatch = findElementInImportedFiles(
2181
+ focusedElements[0],
2182
+ pageContext.componentSources
2183
+ );
2184
+
2185
+ if (importedMatch) {
2186
+ debugLog("REDIRECT: Element found in imported component", {
2187
+ originalFile: actualTargetFile.path,
2188
+ redirectTo: importedMatch.path,
2189
+ matchedBy: importedMatch.matchedBy,
2190
+ lineNumber: importedMatch.lineNumber
2191
+ });
2192
+
2193
+ actualTargetFile = {
2194
+ path: importedMatch.path,
2195
+ content: importedMatch.content
2196
+ };
2197
+ elementLocation = findElementLineInFile(importedMatch.content, focusedElements[0]);
2198
+ }
2199
+ }
2200
+ }
2201
+
2202
+ debugLog("File redirect complete", {
2203
+ originalRecommended: recommendedFileContent?.path || 'none',
2204
+ actualTarget: actualTargetFile?.path || 'none',
2205
+ wasRedirected: actualTargetFile?.path !== recommendedFileContent?.path
2206
+ });
2207
+
2208
+ // Build text content
2209
+ let textContent = `VISION MODE EDIT REQUEST
2210
+
2211
+ Page Route: ${pageRoute}
2212
+ User Request: "${userPrompt}"
2213
+
2214
+ `;
2215
+
2216
+ // ========== TWO-PHASE DESIGN ANALYSIS ==========
2217
+ // Phase 1: Identify specific visual problems (separate LLM call)
2218
+ // Phase 2: Generate patches that reference those problems
2219
+ const isDesignRequest = isDesignHeavyRequest(userPrompt || '');
2220
+ let identifiedProblems: DesignProblem[] = [];
2221
+
2222
+ if (isDesignRequest && screenshot) {
2223
+ debugLog("Design-heavy request detected, running Phase 1 analysis", {
2224
+ prompt: userPrompt?.substring(0, 50),
2225
+ triggerKeywords: ['redesign', 'better', 'improve', 'cleaner', 'layout', 'modernize']
2226
+ .filter(k => userPrompt?.toLowerCase().includes(k))
2227
+ });
2228
+
2229
+ // Phase 1: Analyze the screenshot to identify specific problems
2230
+ // This is a SEPARATE LLM call that only identifies problems
2231
+ // Use actualTargetFile since redirect has already happened
2232
+ const fileContentForAnalysis = actualTargetFile?.content || pageContext.pageContent || '';
2233
+ identifiedProblems = await analyzeDesignProblems(
2234
+ screenshot,
2235
+ fileContentForAnalysis,
2236
+ userPrompt || '',
2237
+ apiKey
2238
+ );
2239
+
2240
+ debugLog("Phase 1 complete: Problems identified", {
2241
+ problemCount: identifiedProblems.length,
2242
+ problems: identifiedProblems.map(p => `${p.id}: ${p.description.substring(0, 50)}`)
2243
+ });
2244
+
2245
+ // Add Phase 2 prompt with the identified problems
2246
+ if (identifiedProblems.length > 0) {
2247
+ textContent += buildPhase2DesignPrompt(identifiedProblems);
2248
+ textContent += DESIGN_GUARDRAILS;
2249
+ textContent += '\n';
2250
+ } else {
2251
+ debugLog("No problems identified - proceeding with standard edit");
2252
+ }
2253
+ }
2254
+
2255
+ // ========== TARGET COMPONENT ONLY (with line numbers) ==========
2256
+ // CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
2257
+ // Note: Redirect logic already ran above, so actualTargetFile is the correct file
2258
+ if (actualTargetFile) {
1993
2259
  // Build focused elements section with precise targeting info
1994
2260
  if (focusedElements && focusedElements.length > 0) {
1995
2261
  textContent += `FOCUSED ELEMENTS (user clicked on these):\n`;
@@ -2053,7 +2319,7 @@ ${elementLocation.snippet}
2053
2319
  // Element NOT found in main file OR any imported components
2054
2320
  // BLOCK the LLM from guessing - require empty modifications
2055
2321
  debugLog("BLOCK: Could not locate focused element anywhere", {
2056
- mainFile: recommendedFileContent.path,
2322
+ mainFile: actualTargetFile.path,
2057
2323
  searchedImports: pageContext.componentSources.length,
2058
2324
  focusedElements: focusedElements.map(el => ({
2059
2325
  name: el.name,
@@ -2069,7 +2335,7 @@ ${elementLocation.snippet}
2069
2335
  ⛔ STOP: CANNOT LOCATE THE CLICKED ELEMENT
2070
2336
 
2071
2337
  The user clicked on a specific element, but it could NOT be found in:
2072
- - ${recommendedFileContent.path} (main target file)
2338
+ - ${actualTargetFile.path} (main target file)
2073
2339
  - Any of the ${pageContext.componentSources.length} imported component files
2074
2340
 
2075
2341
  The element may be:
@@ -2184,7 +2450,8 @@ ${variantsMatch[0]}
2184
2450
  }
2185
2451
 
2186
2452
  // ========== SIMPLIFIED INSTRUCTIONS ==========
2187
- const targetPath = recommendedFileContent?.path || pageContext.pageFile || "unknown";
2453
+ // Use actualTargetFile which may have been redirected to an imported component
2454
+ const targetPath = actualTargetFile?.path || pageContext.pageFile || "unknown";
2188
2455
 
2189
2456
  textContent += `═══════════════════════════════════════════════════════════════════════════════
2190
2457
  HOW TO MAKE YOUR EDIT
@@ -2228,10 +2495,14 @@ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exa
2228
2495
  validFilePaths.add(comp.path);
2229
2496
  }
2230
2497
 
2231
- // FIX: Add the recommended file to validFilePaths (it was spliced out for display purposes)
2498
+ // FIX: Add the recommended file and actual target file to validFilePaths
2499
+ // (recommended was spliced out for display, actual may be different due to redirect)
2232
2500
  if (recommendedFileContent) {
2233
2501
  validFilePaths.add(recommendedFileContent.path);
2234
2502
  }
2503
+ if (actualTargetFile && actualTargetFile.path !== recommendedFileContent?.path) {
2504
+ validFilePaths.add(actualTargetFile.path);
2505
+ }
2235
2506
 
2236
2507
  // Retry loop for handling patch failures
2237
2508
  const MAX_RETRIES = 1;
@@ -2367,7 +2638,8 @@ This is better than generating patches with made-up code.`,
2367
2638
 
2368
2639
  // CRITICAL: Warn if LLM is trying to modify a file OTHER than the TARGET COMPONENT
2369
2640
  // This usually means the LLM is trying to modify a file it doesn't have full visibility into
2370
- const targetComponentPath = recommendedFileContent?.path;
2641
+ // Use actualTargetFile since it may have been redirected from recommendedFileContent
2642
+ const targetComponentPath = actualTargetFile?.path;
2371
2643
  if (targetComponentPath && mod.filePath !== targetComponentPath) {
2372
2644
  debugLog("WARNING: LLM trying to modify non-target file", {
2373
2645
  targetComponent: targetComponentPath,
@@ -2392,9 +2664,41 @@ This is better than generating patches with made-up code.`,
2392
2664
  // New patch-based approach
2393
2665
  console.log(`[Apply-First] Applying ${mod.patches.length} patches to ${mod.filePath}`);
2394
2666
 
2667
+ // DESIGN VALIDATION: For design-heavy requests, validate patches have problemIds
2668
+ let patchesToApply = mod.patches;
2669
+ if (isDesignRequest && identifiedProblems.length > 0) {
2670
+ const validationResult = validateDesignPatches(mod.patches, identifiedProblems);
2671
+
2672
+ if (validationResult.rejected.length > 0) {
2673
+ debugLog("Design patch validation rejected some patches", {
2674
+ filePath: mod.filePath,
2675
+ validCount: validationResult.valid.length,
2676
+ rejectedCount: validationResult.rejected.length,
2677
+ rejectedReasons: validationResult.rejected.map(r => r.reason)
2678
+ });
2679
+ console.warn(`[Apply-First] ${validationResult.rejected.length} patches rejected for missing/invalid problemId`);
2680
+ }
2681
+
2682
+ // Only use validated patches
2683
+ if (validationResult.valid.length === 0) {
2684
+ patchErrors.push(`${mod.filePath}: All patches were rejected - they don't reference identified problems`);
2685
+ continue;
2686
+ }
2687
+
2688
+ // Log which problems are being fixed
2689
+ const problemsBeingFixed = [...new Set(validationResult.valid.map(p => p.problemId))];
2690
+ debugLog("Design patches validated", {
2691
+ filePath: mod.filePath,
2692
+ problemsBeingFixed,
2693
+ patchCount: validationResult.valid.length
2694
+ });
2695
+
2696
+ patchesToApply = validationResult.valid;
2697
+ }
2698
+
2395
2699
  // PRE-VALIDATION: Check if all search strings exist in the file BEFORE applying
2396
2700
  const preValidationErrors: string[] = [];
2397
- for (const patch of mod.patches) {
2701
+ for (const patch of patchesToApply) {
2398
2702
  const normalizedSearch = patch.search.replace(/\\n/g, "\n");
2399
2703
  if (!originalContent.includes(normalizedSearch)) {
2400
2704
  // Try fuzzy match as fallback
@@ -2433,7 +2737,7 @@ This is better than generating patches with made-up code.`,
2433
2737
  continue;
2434
2738
  }
2435
2739
 
2436
- const patchResult = applyPatches(originalContent, mod.patches);
2740
+ const patchResult = applyPatches(originalContent, patchesToApply);
2437
2741
 
2438
2742
  if (!patchResult.success) {
2439
2743
  const failedMessages = patchResult.failedPatches.map(
@@ -2443,15 +2747,15 @@ This is better than generating patches with made-up code.`,
2443
2747
 
2444
2748
  // If some patches succeeded, use partial result
2445
2749
  if (patchResult.appliedPatches > 0) {
2446
- console.warn(`[Apply-First] ${patchResult.appliedPatches}/${mod.patches.length} patches applied to ${mod.filePath}`);
2750
+ console.warn(`[Apply-First] ${patchResult.appliedPatches}/${patchesToApply.length} patches applied to ${mod.filePath}`);
2447
2751
  modifiedContent = patchResult.modifiedContent;
2448
- explanation += ` (${patchResult.appliedPatches}/${mod.patches.length} patches applied)`;
2752
+ explanation += ` (${patchResult.appliedPatches}/${patchesToApply.length} patches applied)`;
2449
2753
  } else {
2450
2754
  continue; // Skip this file entirely if no patches worked
2451
2755
  }
2452
2756
  } else {
2453
2757
  modifiedContent = patchResult.modifiedContent;
2454
- console.log(`[Apply-First] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
2758
+ console.log(`[Apply-First] All ${patchesToApply.length} patches applied successfully to ${mod.filePath}`);
2455
2759
  }
2456
2760
 
2457
2761
  // AST VALIDATION: Use Babel parser to catch actual syntax errors