sonance-brand-mcp 1.3.91 → 1.3.93

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.
@@ -19,6 +19,20 @@ import * as babelParser from "@babel/parser";
19
19
  * DEVELOPMENT ONLY.
20
20
  */
21
21
 
22
+ /** Parent section context for section-level targeting */
23
+ interface ParentSectionInfo {
24
+ /** Semantic container type (section, article, form, card, dialog, etc.) */
25
+ type: string;
26
+ /** Key text content in the section (first heading, labels) */
27
+ sectionText?: string;
28
+ /** The parent container's className for code matching */
29
+ className?: string;
30
+ /** Coordinates of the parent section */
31
+ coordinates?: { x: number; y: number; width: number; height: number };
32
+ /** Parent's element ID if available */
33
+ elementId?: string;
34
+ }
35
+
22
36
  interface VisionFocusedElement {
23
37
  name: string;
24
38
  type: string;
@@ -38,6 +52,8 @@ interface VisionFocusedElement {
38
52
  elementId?: string;
39
53
  /** IDs of child elements for more precise targeting */
40
54
  childIds?: string[];
55
+ /** Parent section context for section-level changes */
56
+ parentSection?: ParentSectionInfo;
41
57
  }
42
58
 
43
59
  interface VisionFileModification {
@@ -500,6 +516,50 @@ function findElementLineInFile(
500
516
  return null;
501
517
  }
502
518
 
519
+ /**
520
+ * Search imported component files for the focused element
521
+ * Reuses findElementLineInFile() for consistent detection
522
+ *
523
+ * This is called when the element cannot be found in the main target file,
524
+ * allowing us to redirect to the actual file containing the element.
525
+ */
526
+ function findElementInImportedFiles(
527
+ focusedElement: VisionFocusedElement,
528
+ importedFiles: { path: string; content: string }[]
529
+ ): { path: string; lineNumber: number; matchedBy: string; content: string; confidence: string } | null {
530
+ debugLog("Searching imported components for element", {
531
+ elementType: focusedElement.type,
532
+ textContent: focusedElement.textContent?.substring(0, 30),
533
+ filesCount: importedFiles.length
534
+ });
535
+
536
+ for (const file of importedFiles) {
537
+ // Focus on component files (where UI elements live)
538
+ // Skip types, stores, utils, hooks - they don't contain JSX elements
539
+ if (!file.path.includes('components/') && !file.path.includes('/ui/')) continue;
540
+
541
+ const result = findElementLineInFile(file.content, focusedElement);
542
+ if (result && result.confidence !== 'low') {
543
+ debugLog("Found element in imported component", {
544
+ path: file.path,
545
+ lineNumber: result.lineNumber,
546
+ matchedBy: result.matchedBy,
547
+ confidence: result.confidence
548
+ });
549
+ return {
550
+ path: file.path,
551
+ lineNumber: result.lineNumber,
552
+ matchedBy: result.matchedBy,
553
+ content: file.content,
554
+ confidence: result.confidence
555
+ };
556
+ }
557
+ }
558
+
559
+ debugLog("Element not found in any imported component files");
560
+ return null;
561
+ }
562
+
503
563
  /**
504
564
  * PHASE 0: Deterministic Element ID Search (Cursor-style)
505
565
  * Grep entire codebase for the element ID. If found in multiple files,
@@ -1604,6 +1664,8 @@ User Request: "${userPrompt}"
1604
1664
  // Search for focused element in the file using multiple strategies
1605
1665
  // Priority: DOM id > textContent > className patterns
1606
1666
  let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
1667
+ let actualTargetFile = recommendedFileContent; // May change if we redirect
1668
+
1607
1669
  if (focusedElements && focusedElements.length > 0) {
1608
1670
  for (const el of focusedElements) {
1609
1671
  elementLocation = findElementLineInFile(content, el);
@@ -1617,6 +1679,35 @@ User Request: "${userPrompt}"
1617
1679
  break;
1618
1680
  }
1619
1681
  }
1682
+
1683
+ // DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
1684
+ if (!elementLocation) {
1685
+ debugLog("Element not in main file, searching imported components...", {
1686
+ mainFile: recommendedFileContent.path,
1687
+ importedFilesCount: pageContext.componentSources.length
1688
+ });
1689
+
1690
+ const importedMatch = findElementInImportedFiles(
1691
+ focusedElements[0],
1692
+ pageContext.componentSources
1693
+ );
1694
+
1695
+ if (importedMatch) {
1696
+ debugLog("REDIRECT: Element found in imported component", {
1697
+ originalFile: recommendedFileContent.path,
1698
+ redirectTo: importedMatch.path,
1699
+ matchedBy: importedMatch.matchedBy,
1700
+ lineNumber: importedMatch.lineNumber
1701
+ });
1702
+
1703
+ // Switch target file to where element actually is
1704
+ actualTargetFile = {
1705
+ path: importedMatch.path,
1706
+ content: importedMatch.content
1707
+ };
1708
+ elementLocation = findElementLineInFile(importedMatch.content, focusedElements[0]);
1709
+ }
1710
+ }
1620
1711
  }
1621
1712
 
1622
1713
  // Build focused elements section with precise targeting info
@@ -1628,6 +1719,36 @@ User Request: "${userPrompt}"
1628
1719
  textContent += ` with text "${el.textContent.substring(0, 30)}"`;
1629
1720
  }
1630
1721
  textContent += `\n`;
1722
+
1723
+ // Include parent section context for section-level targeting
1724
+ if (el.parentSection) {
1725
+ textContent += ` └─ Parent Section: <${el.parentSection.type}>`;
1726
+ if (el.parentSection.sectionText) {
1727
+ textContent += ` containing "${el.parentSection.sectionText.substring(0, 60)}${el.parentSection.sectionText.length > 60 ? '...' : ''}"`;
1728
+ }
1729
+ if (el.parentSection.elementId) {
1730
+ textContent += ` (id="${el.parentSection.elementId}")`;
1731
+ }
1732
+ if (el.parentSection.className) {
1733
+ // Show first 80 chars of className for context
1734
+ const truncatedClass = el.parentSection.className.length > 80
1735
+ ? el.parentSection.className.substring(0, 80) + '...'
1736
+ : el.parentSection.className;
1737
+ textContent += `\n className="${truncatedClass}"`;
1738
+ }
1739
+ textContent += `\n`;
1740
+ }
1741
+ }
1742
+
1743
+ // Detect section-level prompts and provide guidance
1744
+ const sectionKeywords = ['section', 'area', 'container', 'card', 'panel', 'header', 'form', 'region', 'compact', 'modernize', 'redesign', 'layout'];
1745
+ const isSectionLevelChange = sectionKeywords.some(kw => userPrompt?.toLowerCase().includes(kw));
1746
+ if (isSectionLevelChange && focusedElements.some(el => el.parentSection)) {
1747
+ textContent += `
1748
+ 📍 SECTION-LEVEL CHANGE DETECTED: The user's prompt mentions section/area keywords.
1749
+ If modifying the parent section (not just the clicked element), target the container
1750
+ mentioned above. Look for elements matching the section className or id.
1751
+ `;
1631
1752
  }
1632
1753
 
1633
1754
  // Add precise targeting with line number and snippet
@@ -1649,10 +1770,11 @@ ${elementLocation.snippet}
1649
1770
 
1650
1771
  `;
1651
1772
  } else {
1652
- textContent += `\n`;
1653
- debugLog("WARNING: Could not locate focused element in file", {
1654
- file: recommendedFileContent.path,
1655
- hint: "The clicked element may be rendered by a child component (e.g., DialogContent, Button component). Check the component's source file instead.",
1773
+ // Element NOT found in main file OR any imported components
1774
+ // BLOCK the LLM from guessing - require empty modifications
1775
+ debugLog("BLOCK: Could not locate focused element anywhere", {
1776
+ mainFile: recommendedFileContent.path,
1777
+ searchedImports: pageContext.componentSources.length,
1656
1778
  focusedElements: focusedElements.map(el => ({
1657
1779
  name: el.name,
1658
1780
  type: el.type,
@@ -1662,27 +1784,38 @@ ${elementLocation.snippet}
1662
1784
  }))
1663
1785
  });
1664
1786
 
1665
- // Add a hint to the LLM that we couldn't precisely locate the element
1787
+ // STRONG BLOCK instruction - tell LLM to NOT guess
1666
1788
  textContent += `
1667
- ⚠️ WARNING: Could not precisely locate the clicked element in this file.
1789
+ ⛔ STOP: CANNOT LOCATE THE CLICKED ELEMENT
1790
+
1791
+ The user clicked on a specific element, but it could NOT be found in:
1792
+ - ${recommendedFileContent.path} (main target file)
1793
+ - Any of the ${pageContext.componentSources.length} imported component files
1794
+
1668
1795
  The element may be:
1669
- - Rendered by a child component (e.g., DialogContent, Button)
1670
- - Dynamically generated
1671
- - In a different file imported by this component
1796
+ - Deeply nested in a component not in the import tree
1797
+ - Dynamically generated at runtime
1798
+ - Part of a third-party library component
1672
1799
 
1673
- Please proceed with caution and verify you're modifying the correct element.
1800
+ DO NOT GUESS. Return this exact response:
1801
+ {
1802
+ "modifications": [],
1803
+ "explanation": "Could not locate the clicked element in this file or any of its ${pageContext.componentSources.length} imported components. The element may be rendered by a deeply nested or third-party component."
1804
+ }
1674
1805
 
1675
1806
  `;
1676
1807
  }
1677
1808
  }
1678
1809
 
1679
1810
  // Add line numbers to make it easy for LLM to reference exact code
1680
- const linesWithNumbers = content.split('\n').map((line, i) =>
1811
+ // Use actualTargetFile which may have been redirected to an imported component
1812
+ const targetContent = actualTargetFile.content;
1813
+ const linesWithNumbers = targetContent.split('\n').map((line, i) =>
1681
1814
  `${String(i + 1).padStart(4, ' ')}| ${line}`
1682
1815
  ).join('\n');
1683
1816
 
1684
1817
  textContent += `═══════════════════════════════════════════════════════════════════════════════
1685
- FILE TO EDIT: ${recommendedFileContent.path}
1818
+ FILE TO EDIT: ${actualTargetFile.path}
1686
1819
  ═══════════════════════════════════════════════════════════════════════════════
1687
1820
 
1688
1821
  IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
@@ -1693,11 +1826,12 @@ ${linesWithNumbers}
1693
1826
  \`\`\`
1694
1827
 
1695
1828
  `;
1696
- usedContext += content.length;
1829
+ usedContext += targetContent.length;
1697
1830
  debugLog("Added TARGET COMPONENT with line numbers", {
1698
- path: recommendedFileContent.path,
1699
- lines: content.split('\n').length,
1700
- size: content.length
1831
+ path: actualTargetFile.path,
1832
+ lines: targetContent.split('\n').length,
1833
+ size: targetContent.length,
1834
+ wasRedirected: actualTargetFile.path !== recommendedFileContent.path
1701
1835
  });
1702
1836
  } else if (pageContext.pageContent) {
1703
1837
  // Fallback: use page file if no recommended file
@@ -18,6 +18,20 @@ import * as babelParser from "@babel/parser";
18
18
  * DEVELOPMENT ONLY.
19
19
  */
20
20
 
21
+ /** Parent section context for section-level targeting */
22
+ interface ParentSectionInfo {
23
+ /** Semantic container type (section, article, form, card, dialog, etc.) */
24
+ type: string;
25
+ /** Key text content in the section (first heading, labels) */
26
+ sectionText?: string;
27
+ /** The parent container's className for code matching */
28
+ className?: string;
29
+ /** Coordinates of the parent section */
30
+ coordinates?: { x: number; y: number; width: number; height: number };
31
+ /** Parent's element ID if available */
32
+ elementId?: string;
33
+ }
34
+
21
35
  interface VisionFocusedElement {
22
36
  name: string;
23
37
  type: string;
@@ -37,6 +51,8 @@ interface VisionFocusedElement {
37
51
  elementId?: string;
38
52
  /** IDs of child elements for more precise targeting */
39
53
  childIds?: string[];
54
+ /** Parent section context for section-level changes */
55
+ parentSection?: ParentSectionInfo;
40
56
  }
41
57
 
42
58
  interface VisionFileModification {
@@ -496,6 +512,50 @@ function findElementLineInFile(
496
512
  return null;
497
513
  }
498
514
 
515
+ /**
516
+ * Search imported component files for the focused element
517
+ * Reuses findElementLineInFile() for consistent detection
518
+ *
519
+ * This is called when the element cannot be found in the main target file,
520
+ * allowing us to redirect to the actual file containing the element.
521
+ */
522
+ function findElementInImportedFiles(
523
+ focusedElement: VisionFocusedElement,
524
+ importedFiles: { path: string; content: string }[]
525
+ ): { path: string; lineNumber: number; matchedBy: string; content: string; confidence: string } | null {
526
+ debugLog("Searching imported components for element", {
527
+ elementType: focusedElement.type,
528
+ textContent: focusedElement.textContent?.substring(0, 30),
529
+ filesCount: importedFiles.length
530
+ });
531
+
532
+ for (const file of importedFiles) {
533
+ // Focus on component files (where UI elements live)
534
+ // Skip types, stores, utils, hooks - they don't contain JSX elements
535
+ if (!file.path.includes('components/') && !file.path.includes('/ui/')) continue;
536
+
537
+ const result = findElementLineInFile(file.content, focusedElement);
538
+ if (result && result.confidence !== 'low') {
539
+ debugLog("Found element in imported component", {
540
+ path: file.path,
541
+ lineNumber: result.lineNumber,
542
+ matchedBy: result.matchedBy,
543
+ confidence: result.confidence
544
+ });
545
+ return {
546
+ path: file.path,
547
+ lineNumber: result.lineNumber,
548
+ matchedBy: result.matchedBy,
549
+ content: file.content,
550
+ confidence: result.confidence
551
+ };
552
+ }
553
+ }
554
+
555
+ debugLog("Element not found in any imported component files");
556
+ return null;
557
+ }
558
+
499
559
  /**
500
560
  * PHASE 0: Deterministic Element ID Search (Cursor-style)
501
561
  * Grep entire codebase for the element ID. If found in multiple files,
@@ -1573,6 +1633,8 @@ User Request: "${userPrompt}"
1573
1633
  // Search for focused element in the file using multiple strategies
1574
1634
  // Priority: DOM id > textContent > className patterns
1575
1635
  let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
1636
+ let actualTargetFile = recommendedFileContent; // May change if we redirect
1637
+
1576
1638
  if (focusedElements && focusedElements.length > 0) {
1577
1639
  for (const el of focusedElements) {
1578
1640
  elementLocation = findElementLineInFile(content, el);
@@ -1586,6 +1648,35 @@ User Request: "${userPrompt}"
1586
1648
  break;
1587
1649
  }
1588
1650
  }
1651
+
1652
+ // DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
1653
+ if (!elementLocation) {
1654
+ debugLog("Element not in main file, searching imported components...", {
1655
+ mainFile: recommendedFileContent.path,
1656
+ importedFilesCount: pageContext.componentSources.length
1657
+ });
1658
+
1659
+ const importedMatch = findElementInImportedFiles(
1660
+ focusedElements[0],
1661
+ pageContext.componentSources
1662
+ );
1663
+
1664
+ if (importedMatch) {
1665
+ debugLog("REDIRECT: Element found in imported component", {
1666
+ originalFile: recommendedFileContent.path,
1667
+ redirectTo: importedMatch.path,
1668
+ matchedBy: importedMatch.matchedBy,
1669
+ lineNumber: importedMatch.lineNumber
1670
+ });
1671
+
1672
+ // Switch target file to where element actually is
1673
+ actualTargetFile = {
1674
+ path: importedMatch.path,
1675
+ content: importedMatch.content
1676
+ };
1677
+ elementLocation = findElementLineInFile(importedMatch.content, focusedElements[0]);
1678
+ }
1679
+ }
1589
1680
  }
1590
1681
 
1591
1682
  // Build focused elements section with precise targeting info
@@ -1597,6 +1688,36 @@ User Request: "${userPrompt}"
1597
1688
  textContent += ` with text "${el.textContent.substring(0, 30)}"`;
1598
1689
  }
1599
1690
  textContent += `\n`;
1691
+
1692
+ // Include parent section context for section-level targeting
1693
+ if (el.parentSection) {
1694
+ textContent += ` └─ Parent Section: <${el.parentSection.type}>`;
1695
+ if (el.parentSection.sectionText) {
1696
+ textContent += ` containing "${el.parentSection.sectionText.substring(0, 60)}${el.parentSection.sectionText.length > 60 ? '...' : ''}"`;
1697
+ }
1698
+ if (el.parentSection.elementId) {
1699
+ textContent += ` (id="${el.parentSection.elementId}")`;
1700
+ }
1701
+ if (el.parentSection.className) {
1702
+ // Show first 80 chars of className for context
1703
+ const truncatedClass = el.parentSection.className.length > 80
1704
+ ? el.parentSection.className.substring(0, 80) + '...'
1705
+ : el.parentSection.className;
1706
+ textContent += `\n className="${truncatedClass}"`;
1707
+ }
1708
+ textContent += `\n`;
1709
+ }
1710
+ }
1711
+
1712
+ // Detect section-level prompts and provide guidance
1713
+ const sectionKeywords = ['section', 'area', 'container', 'card', 'panel', 'header', 'form', 'region', 'compact', 'modernize', 'redesign', 'layout'];
1714
+ const isSectionLevelChange = sectionKeywords.some(kw => userPrompt?.toLowerCase().includes(kw));
1715
+ if (isSectionLevelChange && focusedElements.some(el => el.parentSection)) {
1716
+ textContent += `
1717
+ 📍 SECTION-LEVEL CHANGE DETECTED: The user's prompt mentions section/area keywords.
1718
+ If modifying the parent section (not just the clicked element), target the container
1719
+ mentioned above. Look for elements matching the section className or id.
1720
+ `;
1600
1721
  }
1601
1722
 
1602
1723
  // Add precise targeting with line number and snippet
@@ -1618,10 +1739,11 @@ ${elementLocation.snippet}
1618
1739
 
1619
1740
  `;
1620
1741
  } else {
1621
- textContent += `\n`;
1622
- debugLog("WARNING: Could not locate focused element in file", {
1623
- file: recommendedFileContent.path,
1624
- hint: "The clicked element may be rendered by a child component (e.g., DialogContent, Button component). Check the component's source file instead.",
1742
+ // Element NOT found in main file OR any imported components
1743
+ // BLOCK the LLM from guessing - require empty modifications
1744
+ debugLog("BLOCK: Could not locate focused element anywhere", {
1745
+ mainFile: recommendedFileContent.path,
1746
+ searchedImports: pageContext.componentSources.length,
1625
1747
  focusedElements: focusedElements.map(el => ({
1626
1748
  name: el.name,
1627
1749
  type: el.type,
@@ -1631,27 +1753,38 @@ ${elementLocation.snippet}
1631
1753
  }))
1632
1754
  });
1633
1755
 
1634
- // Add a hint to the LLM that we couldn't precisely locate the element
1756
+ // STRONG BLOCK instruction - tell LLM to NOT guess
1635
1757
  textContent += `
1636
- ⚠️ WARNING: Could not precisely locate the clicked element in this file.
1758
+ ⛔ STOP: CANNOT LOCATE THE CLICKED ELEMENT
1759
+
1760
+ The user clicked on a specific element, but it could NOT be found in:
1761
+ - ${recommendedFileContent.path} (main target file)
1762
+ - Any of the ${pageContext.componentSources.length} imported component files
1763
+
1637
1764
  The element may be:
1638
- - Rendered by a child component (e.g., DialogContent, Button)
1639
- - Dynamically generated
1640
- - In a different file imported by this component
1765
+ - Deeply nested in a component not in the import tree
1766
+ - Dynamically generated at runtime
1767
+ - Part of a third-party library component
1641
1768
 
1642
- Please proceed with caution and verify you're modifying the correct element.
1769
+ DO NOT GUESS. Return this exact response:
1770
+ {
1771
+ "modifications": [],
1772
+ "explanation": "Could not locate the clicked element in this file or any of its ${pageContext.componentSources.length} imported components. The element may be rendered by a deeply nested or third-party component."
1773
+ }
1643
1774
 
1644
1775
  `;
1645
1776
  }
1646
1777
  }
1647
1778
 
1648
1779
  // Add line numbers to make it easy for LLM to reference exact code
1649
- const linesWithNumbers = content.split('\n').map((line, i) =>
1780
+ // Use actualTargetFile which may have been redirected to an imported component
1781
+ const targetContent = actualTargetFile.content;
1782
+ const linesWithNumbers = targetContent.split('\n').map((line, i) =>
1650
1783
  `${String(i + 1).padStart(4, ' ')}| ${line}`
1651
1784
  ).join('\n');
1652
1785
 
1653
1786
  textContent += `═══════════════════════════════════════════════════════════════════════════════
1654
- FILE TO EDIT: ${recommendedFileContent.path}
1787
+ FILE TO EDIT: ${actualTargetFile.path}
1655
1788
  ═══════════════════════════════════════════════════════════════════════════════
1656
1789
 
1657
1790
  IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
@@ -1662,11 +1795,12 @@ ${linesWithNumbers}
1662
1795
  \`\`\`
1663
1796
 
1664
1797
  `;
1665
- usedContext += content.length;
1798
+ usedContext += targetContent.length;
1666
1799
  debugLog("Added TARGET COMPONENT with line numbers", {
1667
- path: recommendedFileContent.path,
1668
- lines: content.split('\n').length,
1669
- size: content.length
1800
+ path: actualTargetFile.path,
1801
+ lines: targetContent.split('\n').length,
1802
+ size: targetContent.length,
1803
+ wasRedirected: actualTargetFile.path !== recommendedFileContent.path
1670
1804
  });
1671
1805
  } else if (pageContext.pageContent) {
1672
1806
  // Fallback: use page file if no recommended file
@@ -48,6 +48,7 @@ import {
48
48
  ApplyFirstStatus,
49
49
  TextOverride,
50
50
  OriginalTextState,
51
+ ParentSectionInfo,
51
52
  } from "./types";
52
53
  import {
53
54
  tabs,
@@ -71,6 +72,7 @@ import { InspectorOverlay } from "./components/InspectorOverlay";
71
72
  import { ChatInterface } from "./components/ChatInterface";
72
73
  import { DiffPreview } from "./components/DiffPreview";
73
74
  import { VisionModeBorder } from "./components/VisionModeBorder";
75
+ import { SectionHighlight } from "./components/SectionHighlight";
74
76
  import { AnalysisPanel } from "./panels/AnalysisPanel";
75
77
  import { TextPanel } from "./panels/TextPanel";
76
78
  import { LogoToolsPanel } from "./panels/LogoToolsPanel";
@@ -82,6 +84,100 @@ import { ComponentsPanel } from "./panels/ComponentsPanel";
82
84
  // A floating development overlay for brand theming
83
85
  // ============================================
84
86
 
87
+ // ---- Helper Functions ----
88
+
89
+ /**
90
+ * Extract the first meaningful heading or text content from a section
91
+ */
92
+ function extractSectionHeading(container: Element): string | undefined {
93
+ // Look for headings first
94
+ const heading = container.querySelector('h1, h2, h3, h4, h5, h6');
95
+ if (heading && heading.textContent) {
96
+ const text = heading.textContent.trim();
97
+ // Return first 100 chars max
98
+ return text.length > 100 ? text.substring(0, 100) + '...' : text;
99
+ }
100
+
101
+ // Look for labels or title elements
102
+ const label = container.querySelector('label, [class*="title"], [class*="heading"]');
103
+ if (label && label.textContent) {
104
+ const text = label.textContent.trim();
105
+ return text.length > 100 ? text.substring(0, 100) + '...' : text;
106
+ }
107
+
108
+ // Look for aria-label or title attribute
109
+ const ariaLabel = container.getAttribute('aria-label');
110
+ if (ariaLabel) return ariaLabel;
111
+
112
+ const title = container.getAttribute('title');
113
+ if (title) return title;
114
+
115
+ return undefined;
116
+ }
117
+
118
+ /**
119
+ * Find the nearest meaningful parent section/container for an element
120
+ * This helps the AI understand the broader context when making section-level changes
121
+ */
122
+ function findParentSection(element: Element): ParentSectionInfo | undefined {
123
+ // Selectors for common section-like containers
124
+ const sectionSelectors = [
125
+ 'section', 'article', 'form', 'dialog', 'aside', 'header', 'footer', 'main',
126
+ '[role="dialog"]', '[role="region"]', '[role="form"]', '[role="main"]',
127
+ '[role="complementary"]', '[role="banner"]', '[role="contentinfo"]'
128
+ ];
129
+
130
+ // Class patterns that indicate section-like containers
131
+ const sectionClassPatterns = /\b(card|panel|section|container|modal|drawer|header|footer|sidebar|content|wrapper|box|block|group|region|area)\b/i;
132
+
133
+ let current = element.parentElement;
134
+ let depth = 0;
135
+ const maxDepth = 10; // Don't traverse too far up
136
+
137
+ while (current && current !== document.body && depth < maxDepth) {
138
+ // Skip DevTools elements
139
+ if (current.hasAttribute('data-sonance-devtools')) {
140
+ current = current.parentElement;
141
+ depth++;
142
+ continue;
143
+ }
144
+
145
+ // Check if current matches a section-like container
146
+ const matchesSelector = sectionSelectors.some(sel => {
147
+ try {
148
+ return current!.matches(sel);
149
+ } catch {
150
+ return false;
151
+ }
152
+ });
153
+
154
+ const matchesClassPattern = current.className && sectionClassPatterns.test(current.className);
155
+
156
+ if (matchesSelector || matchesClassPattern) {
157
+ const rect = current.getBoundingClientRect();
158
+ // Store document-relative coordinates (add scroll offset)
159
+ // This ensures the highlight stays with the section when scrolling
160
+ return {
161
+ type: current.tagName.toLowerCase(),
162
+ sectionText: extractSectionHeading(current),
163
+ className: current.className || undefined,
164
+ coordinates: {
165
+ x: rect.left + window.scrollX,
166
+ y: rect.top + window.scrollY,
167
+ width: rect.width,
168
+ height: rect.height,
169
+ },
170
+ elementId: current.id || undefined,
171
+ };
172
+ }
173
+
174
+ current = current.parentElement;
175
+ depth++;
176
+ }
177
+
178
+ return undefined;
179
+ }
180
+
85
181
  // ---- Main Component ----
86
182
 
87
183
  export function SonanceDevTools() {
@@ -1079,6 +1175,24 @@ export function SonanceDevTools() {
1079
1175
  const handleVisionElementClick = useCallback((element: DetectedElement) => {
1080
1176
  if (!visionModeActive) return;
1081
1177
 
1178
+ // Find the actual DOM element using coordinates (center of the element)
1179
+ const centerX = element.rect.left + element.rect.width / 2;
1180
+ const centerY = element.rect.top + element.rect.height / 2;
1181
+ const domElement = document.elementFromPoint(centerX, centerY);
1182
+
1183
+ // Find parent section context for section-level targeting
1184
+ let parentSection: ParentSectionInfo | undefined;
1185
+ if (domElement) {
1186
+ parentSection = findParentSection(domElement);
1187
+ if (parentSection) {
1188
+ console.log("[Vision Mode] Captured parent section:", {
1189
+ type: parentSection.type,
1190
+ sectionText: parentSection.sectionText?.substring(0, 50),
1191
+ elementId: parentSection.elementId,
1192
+ });
1193
+ }
1194
+ }
1195
+
1082
1196
  const focusedElement: VisionFocusedElement = {
1083
1197
  name: element.name,
1084
1198
  type: element.type,
@@ -1095,6 +1209,8 @@ export function SonanceDevTools() {
1095
1209
  // Capture element ID and child IDs for precise code targeting
1096
1210
  elementId: element.elementId,
1097
1211
  childIds: element.childIds,
1212
+ // Parent section context for section-level changes
1213
+ parentSection,
1098
1214
  };
1099
1215
 
1100
1216
  setVisionFocusedElements((prev) => {
@@ -2707,6 +2823,11 @@ export function SonanceDevTools() {
2707
2823
  active={visionModeActive}
2708
2824
  focusedCount={visionFocusedElements.length}
2709
2825
  />
2826
+ {/* Section Highlight - shows detected parent section when element is clicked */}
2827
+ <SectionHighlight
2828
+ active={visionModeActive}
2829
+ focusedElements={visionFocusedElements}
2830
+ />
2710
2831
  {/* Visual Inspector Overlay - switches to preview mode when AI changes are pending */}
2711
2832
  {(inspectorEnabled || isPreviewActive || visionModeActive || changedElements.length > 0 || (viewMode === "inspector" && selectedComponentType !== "all")) && filteredOverlayElements.length > 0 && (
2712
2833
  <InspectorOverlay
@@ -1,11 +1,59 @@
1
1
  "use client";
2
2
 
3
3
  import React, { useState, useEffect, useCallback, useRef } from "react";
4
- import { Loader2, Send, Sparkles, Eye } from "lucide-react";
4
+ import { Loader2, Send, Sparkles, Eye, AlertCircle, X } from "lucide-react";
5
5
  import { cn } from "../../../lib/utils";
6
6
  import { ChatMessage, AIEditResult, PendingEdit, VisionFocusedElement, VisionPendingEdit, ApplyFirstSession } from "../types";
7
7
  import html2canvas from "html2canvas-pro";
8
8
 
9
+ // Helper to detect location failure in explanation
10
+ function isLocationFailure(explanation: string | undefined): boolean {
11
+ if (!explanation) return false;
12
+ const lowerExplanation = explanation.toLowerCase();
13
+ return (
14
+ lowerExplanation.includes('could not locate') ||
15
+ lowerExplanation.includes('element_not_found') ||
16
+ lowerExplanation.includes('cannot find the clicked element') ||
17
+ lowerExplanation.includes('unable to locate') ||
18
+ lowerExplanation.includes('could not find the element')
19
+ );
20
+ }
21
+
22
+ /**
23
+ * Draw a section highlight border on a screenshot image
24
+ * This helps the LLM visually identify the target section for modifications
25
+ */
26
+ function drawSectionHighlight(
27
+ screenshotDataUrl: string,
28
+ sectionCoords: { x: number; y: number; width: number; height: number }
29
+ ): Promise<string> {
30
+ return new Promise((resolve) => {
31
+ const img = new Image();
32
+ img.onload = () => {
33
+ const canvas = document.createElement('canvas');
34
+ canvas.width = img.width;
35
+ canvas.height = img.height;
36
+ const ctx = canvas.getContext('2d')!;
37
+
38
+ // Draw original screenshot
39
+ ctx.drawImage(img, 0, 0);
40
+
41
+ // Draw section highlight border (teal/cyan to match Sonance brand)
42
+ ctx.strokeStyle = '#00D3C8';
43
+ ctx.lineWidth = 3;
44
+ ctx.setLineDash([8, 4]); // Dashed line for visibility
45
+ ctx.strokeRect(sectionCoords.x, sectionCoords.y, sectionCoords.width, sectionCoords.height);
46
+
47
+ // Semi-transparent fill to subtly highlight the area
48
+ ctx.fillStyle = 'rgba(0, 211, 200, 0.08)';
49
+ ctx.fillRect(sectionCoords.x, sectionCoords.y, sectionCoords.width, sectionCoords.height);
50
+
51
+ resolve(canvas.toDataURL('image/png', 0.8));
52
+ };
53
+ img.src = screenshotDataUrl;
54
+ });
55
+ }
56
+
9
57
  // Variant styles captured from the DOM
10
58
  export interface VariantStyles {
11
59
  backgroundColor: string;
@@ -56,8 +104,17 @@ export function ChatInterface({
56
104
  const [messages, setMessages] = useState<ChatMessage[]>([]);
57
105
  const [input, setInput] = useState("");
58
106
  const [isProcessing, setIsProcessing] = useState(false);
107
+ const [toastMessage, setToastMessage] = useState<{ message: string; type: 'error' | 'warning' } | null>(null);
59
108
  const messagesEndRef = useRef<HTMLDivElement>(null);
60
109
  const inputRef = useRef<HTMLInputElement>(null);
110
+
111
+ // Auto-dismiss toast after 5 seconds
112
+ useEffect(() => {
113
+ if (toastMessage) {
114
+ const timer = setTimeout(() => setToastMessage(null), 5000);
115
+ return () => clearTimeout(timer);
116
+ }
117
+ }, [toastMessage]);
61
118
 
62
119
  // Scroll to bottom when messages change
63
120
  useEffect(() => {
@@ -133,8 +190,22 @@ export function ChatInterface({
133
190
  try {
134
191
  // Capture screenshot
135
192
  console.log("[Vision Mode] Capturing screenshot...");
136
- const screenshot = await captureScreenshot();
137
- console.log("[Vision Mode] Screenshot captured:", screenshot ? `${screenshot.length} bytes` : "null");
193
+ const rawScreenshot = await captureScreenshot();
194
+ console.log("[Vision Mode] Screenshot captured:", rawScreenshot ? `${rawScreenshot.length} bytes` : "null");
195
+
196
+ // Annotate screenshot with section highlight if parent section exists
197
+ // This helps the LLM visually identify the target area for modifications
198
+ let screenshot = rawScreenshot;
199
+ if (rawScreenshot && visionFocusedElements.length > 0) {
200
+ const parentSection = visionFocusedElements[0].parentSection;
201
+ if (parentSection?.coordinates) {
202
+ screenshot = await drawSectionHighlight(rawScreenshot, parentSection.coordinates);
203
+ console.log("[Vision Mode] Added section highlight to screenshot:", {
204
+ sectionType: parentSection.type,
205
+ sectionText: parentSection.sectionText?.substring(0, 30),
206
+ });
207
+ }
208
+ }
138
209
 
139
210
  // Choose API endpoint based on mode
140
211
  const endpoint = useApplyFirst ? "/api/sonance-vision-apply" : "/api/sonance-vision-edit";
@@ -162,20 +233,49 @@ export function ChatInterface({
162
233
  error: data.error,
163
234
  });
164
235
 
236
+ // Check if this is a "location failure" case - element could not be found in code
237
+ const hasLocationFailure = isLocationFailure(data.explanation);
238
+ const hasNoModifications = !data.modifications || data.modifications.length === 0;
239
+ const isElementNotFound = hasLocationFailure && hasNoModifications;
240
+
241
+ // Build appropriate message based on result
242
+ let messageContent: string;
243
+ if (isElementNotFound) {
244
+ // Element not found - provide helpful guidance
245
+ messageContent = (data.explanation || "Could not locate the clicked element in the source code.") +
246
+ "\n\nTry clicking on a different element or describe what you want to change in more detail.";
247
+ } else if (data.success) {
248
+ messageContent = useApplyFirst
249
+ ? data.explanation || "Changes applied! Review and accept or revert."
250
+ : data.explanation || "Vision mode changes ready for preview.";
251
+ } else {
252
+ messageContent = data.error || "Failed to generate changes.";
253
+ }
254
+
165
255
  const assistantMessage: ChatMessage = {
166
256
  id: `msg-${Date.now()}-response`,
167
257
  role: "assistant",
168
- content: data.success
169
- ? useApplyFirst
170
- ? data.explanation || "Changes applied! Review and accept or revert."
171
- : data.explanation || "Vision mode changes ready for preview."
172
- : data.error || "Failed to generate changes.",
258
+ content: messageContent,
173
259
  timestamp: new Date(),
174
260
  };
175
261
 
176
262
  setMessages((prev) => [...prev, assistantMessage]);
177
263
 
178
- if (data.success && data.modifications) {
264
+ // Handle element not found case - show toast and do NOT trigger page refresh
265
+ if (isElementNotFound) {
266
+ console.log("[Vision Mode] Element not found - blocking page refresh:", {
267
+ explanation: data.explanation,
268
+ modifications: data.modifications?.length || 0,
269
+ });
270
+ setToastMessage({
271
+ message: "Could not locate the clicked element in the source code",
272
+ type: 'warning'
273
+ });
274
+ // Do NOT call onApplyFirstComplete - this prevents page refresh
275
+ return;
276
+ }
277
+
278
+ if (data.success && data.modifications && data.modifications.length > 0) {
179
279
  if (useApplyFirst && onApplyFirstComplete) {
180
280
  // Apply-First mode: files are already written, user can see changes via HMR
181
281
  console.log("[Apply-First] Calling onApplyFirstComplete with:", {
@@ -344,6 +444,27 @@ export function ChatInterface({
344
444
 
345
445
  return (
346
446
  <div className="space-y-3">
447
+ {/* Toast Notification */}
448
+ {toastMessage && (
449
+ <div
450
+ className={cn(
451
+ "flex items-center gap-2 p-3 rounded-md text-sm animate-in slide-in-from-top-2",
452
+ toastMessage.type === 'error'
453
+ ? "bg-red-50 border border-red-200 text-red-700"
454
+ : "bg-amber-50 border border-amber-200 text-amber-700"
455
+ )}
456
+ >
457
+ <AlertCircle className="h-4 w-4 flex-shrink-0" />
458
+ <span className="flex-1">{toastMessage.message}</span>
459
+ <button
460
+ onClick={() => setToastMessage(null)}
461
+ className="p-0.5 hover:bg-black/5 rounded"
462
+ >
463
+ <X className="h-3 w-3" />
464
+ </button>
465
+ </div>
466
+ )}
467
+
347
468
  {/* Vision Mode Banner */}
348
469
  {visionMode && (
349
470
  <div className="p-2 bg-purple-50 border border-purple-200 rounded-md">
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { Box } from "lucide-react";
6
+ import type { VisionFocusedElement } from "../types";
7
+
8
+ interface SectionHighlightProps {
9
+ /** Whether vision mode is active */
10
+ active: boolean;
11
+ /** The focused elements that may have parent section info */
12
+ focusedElements: VisionFocusedElement[];
13
+ }
14
+
15
+ /**
16
+ * SectionHighlight renders a visible dashed border around the detected
17
+ * parent section when a user clicks an element in vision mode.
18
+ * This provides visual feedback so users can verify the correct section
19
+ * will be targeted by the AI.
20
+ */
21
+ export function SectionHighlight({ active, focusedElements }: SectionHighlightProps) {
22
+ const [mounted, setMounted] = useState(false);
23
+
24
+ useEffect(() => {
25
+ setMounted(true);
26
+ return () => setMounted(false);
27
+ }, []);
28
+
29
+ if (!active || !mounted) return null;
30
+
31
+ // Find the first focused element with parent section coordinates
32
+ const elementWithSection = focusedElements.find(
33
+ (el) => el.parentSection?.coordinates
34
+ );
35
+
36
+ if (!elementWithSection?.parentSection?.coordinates) return null;
37
+
38
+ const { coordinates, type, sectionText, elementId } = elementWithSection.parentSection;
39
+
40
+ // Coordinates are already document-relative (scroll offset added at capture time)
41
+ // so we can use them directly with absolute positioning
42
+
43
+ // Build the label text
44
+ let labelText = `<${type}>`;
45
+ if (sectionText) {
46
+ const truncatedText = sectionText.length > 40
47
+ ? sectionText.substring(0, 40) + "..."
48
+ : sectionText;
49
+ labelText += ` "${truncatedText}"`;
50
+ }
51
+ if (elementId) {
52
+ labelText += ` #${elementId}`;
53
+ }
54
+
55
+ return createPortal(
56
+ <>
57
+ {/* Section border overlay - uses absolute positioning to stay with content on scroll */}
58
+ <div
59
+ data-section-highlight="true"
60
+ style={{
61
+ position: "absolute",
62
+ top: coordinates.y,
63
+ left: coordinates.x,
64
+ width: coordinates.width,
65
+ height: coordinates.height,
66
+ pointerEvents: "none",
67
+ zIndex: 9995, // Below VisionModeBorder (9996)
68
+ border: "3px dashed #00D3C8",
69
+ borderRadius: "4px",
70
+ backgroundColor: "rgba(0, 211, 200, 0.06)",
71
+ boxShadow: "0 0 0 1px rgba(0, 211, 200, 0.3), inset 0 0 20px rgba(0, 211, 200, 0.05)",
72
+ transition: "all 0.2s ease-out",
73
+ }}
74
+ >
75
+ {/* Section label */}
76
+ <div
77
+ style={{
78
+ position: "absolute",
79
+ top: "-28px",
80
+ left: "0",
81
+ display: "flex",
82
+ alignItems: "center",
83
+ gap: "6px",
84
+ backgroundColor: "#00D3C8",
85
+ color: "#1a1a1a",
86
+ padding: "4px 10px",
87
+ borderRadius: "4px",
88
+ fontSize: "11px",
89
+ fontWeight: 600,
90
+ fontFamily: "'Montserrat', system-ui, -apple-system, sans-serif",
91
+ boxShadow: "0 2px 8px rgba(0, 211, 200, 0.4)",
92
+ whiteSpace: "nowrap",
93
+ maxWidth: "350px",
94
+ overflow: "hidden",
95
+ textOverflow: "ellipsis",
96
+ }}
97
+ >
98
+ <Box size={12} />
99
+ <span>Section: {labelText}</span>
100
+ </div>
101
+
102
+ {/* Corner markers for emphasis */}
103
+ {[
104
+ { top: -2, left: -2, borderTop: "3px solid #00D3C8", borderLeft: "3px solid #00D3C8" },
105
+ { top: -2, right: -2, borderTop: "3px solid #00D3C8", borderRight: "3px solid #00D3C8" },
106
+ { bottom: -2, left: -2, borderBottom: "3px solid #00D3C8", borderLeft: "3px solid #00D3C8" },
107
+ { bottom: -2, right: -2, borderBottom: "3px solid #00D3C8", borderRight: "3px solid #00D3C8" },
108
+ ].map((style, i) => (
109
+ <div
110
+ key={i}
111
+ style={{
112
+ position: "absolute",
113
+ width: "12px",
114
+ height: "12px",
115
+ ...style,
116
+ }}
117
+ />
118
+ ))}
119
+ </div>
120
+ </>,
121
+ document.body
122
+ );
123
+ }
124
+
@@ -215,6 +215,20 @@ export type ComponentsViewMode = "visual" | "inspector";
215
215
 
216
216
  // ---- Vision Mode Types ----
217
217
 
218
+ /** Parent section/container context for section-level targeting */
219
+ export interface ParentSectionInfo {
220
+ /** Semantic container type (section, article, form, card, dialog, etc.) */
221
+ type: string;
222
+ /** Key text content in the section (first heading, labels) */
223
+ sectionText?: string;
224
+ /** The parent container's className for code matching */
225
+ className?: string;
226
+ /** Coordinates of the parent section */
227
+ coordinates?: { x: number; y: number; width: number; height: number };
228
+ /** Parent's element ID if available */
229
+ elementId?: string;
230
+ }
231
+
218
232
  export interface VisionFocusedElement {
219
233
  name: string;
220
234
  type: DetectedElementType;
@@ -234,6 +248,8 @@ export interface VisionFocusedElement {
234
248
  elementId?: string;
235
249
  /** IDs of child elements for more precise targeting */
236
250
  childIds?: string[];
251
+ /** Parent section context for section-level targeting */
252
+ parentSection?: ParentSectionInfo;
237
253
  }
238
254
 
239
255
  export interface VisionEditRequest {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.91",
3
+ "version": "1.3.93",
4
4
  "description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",