sonance-brand-mcp 1.3.63 → 1.3.64

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.
@@ -29,6 +29,10 @@ interface VisionFocusedElement {
29
29
  height: number;
30
30
  };
31
31
  description?: string;
32
+ /** The actual text content of the element (e.g., "REFRESH", "ADD PROCESS") */
33
+ textContent?: string;
34
+ /** The className string of the element for pattern matching */
35
+ className?: string;
32
36
  }
33
37
 
34
38
  interface VisionFileModification {
@@ -176,10 +180,14 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
176
180
  * This helps identify which file actually contains the element the user clicked on
177
181
  *
178
182
  * CURSOR-STYLE: Search file CONTENTS for patterns, not just file names
183
+ *
184
+ * @param phase2aMatches - File paths that Phase 2a identified by component name matching visible text
185
+ * These get a MASSIVE bonus because they're contextually relevant
179
186
  */
180
187
  function findFilesContainingElement(
181
188
  focusedElements: VisionFocusedElement[] | undefined,
182
- candidateFiles: { path: string; content: string }[]
189
+ candidateFiles: { path: string; content: string }[],
190
+ phase2aMatches: string[] = [] // Files that Phase 2a identified by component name
183
191
  ): { path: string; score: number; matches: string[] }[] {
184
192
  if (!focusedElements || focusedElements.length === 0) {
185
193
  return [];
@@ -191,7 +199,13 @@ function findFilesContainingElement(
191
199
  let score = 0;
192
200
  const matches: string[] = [];
193
201
  const content = file.content;
194
- const contentLower = content.toLowerCase();
202
+
203
+ // HIGHEST VALUE: Phase 2a identified this file by matching component name to visible text
204
+ // This means the LLM saw "ProcessRow" in the screenshot and this file is ProcessRow.tsx
205
+ if (phase2aMatches.includes(file.path)) {
206
+ score += 200;
207
+ matches.push('Phase2a component match');
208
+ }
195
209
 
196
210
  for (const el of focusedElements) {
197
211
  // Extract element type from the name (e.g., "button #123" -> "button")
@@ -216,28 +230,42 @@ function findFilesContainingElement(
216
230
  matches.push(`defines ${capitalizedType}`);
217
231
  }
218
232
 
219
- // MEDIUM VALUE: JSX usage of this element type (rendered in this file)
220
- const jsxTagPattern = new RegExp(`<${capitalizedType}[\\s/>]`, 'g');
221
- const jsxMatches = content.match(jsxTagPattern);
222
- if (jsxMatches) {
223
- score += 20 * jsxMatches.length;
224
- matches.push(`<${capitalizedType}> x${jsxMatches.length}`);
225
- }
226
-
227
- // MEDIUM VALUE: HTML tag usage
228
- const htmlTagPattern = new RegExp(`<${elementType}[\\s>]`, 'gi');
229
- const htmlMatches = content.match(htmlTagPattern);
230
- if (htmlMatches) {
231
- score += 10 * htmlMatches.length;
232
- matches.push(`<${elementType}> x${htmlMatches.length}`);
233
+ // NOTE: We intentionally DO NOT count buttons, onClick handlers, or JSX tags
234
+ // because files with MORE of these are often "kitchen sink" components,
235
+ // not the focused component the user is looking at.
236
+ // Instead, we rely on Phase 2a matches and textContent matching.
237
+
238
+ // VERY HIGH VALUE: Exact text content match (e.g., button says "REFRESH")
239
+ // This is THE file if it contains the exact text the user clicked on
240
+ if (el.textContent && el.textContent.length >= 2) {
241
+ // Search for the text in JSX patterns like >REFRESH< or {`REFRESH`} or {"REFRESH"}
242
+ const textPatterns = [
243
+ `>${el.textContent}<`, // JSX text content
244
+ `"${el.textContent}"`, // String literal
245
+ `'${el.textContent}'`, // Single-quoted string
246
+ `\`${el.textContent}\``, // Template literal
247
+ ];
248
+
249
+ for (const pattern of textPatterns) {
250
+ if (content.includes(pattern)) {
251
+ score += 150; // Very high - exact match!
252
+ matches.push(`contains "${el.textContent.substring(0, 20)}"`);
253
+ break; // Only add once per element
254
+ }
255
+ }
233
256
  }
234
257
 
235
- // LOW VALUE: Event handlers (onClick, onPress, etc.)
236
- if (elementType === "button" || componentType === "button") {
237
- const clickHandlers = content.match(/onClick\s*=/gi);
238
- if (clickHandlers) {
239
- score += 5 * clickHandlers.length;
240
- matches.push(`onClick x${clickHandlers.length}`);
258
+ // MEDIUM VALUE: className pattern match
259
+ // If the file contains the same className string, it likely defines this element
260
+ if (el.className && el.className.length >= 10) {
261
+ // Extract the first few unique class names (not all to avoid noise)
262
+ const classNames = el.className.split(/\s+/).filter(c => c.length > 3).slice(0, 5);
263
+ for (const cls of classNames) {
264
+ if (content.includes(cls)) {
265
+ score += 25;
266
+ matches.push(`has class "${cls.substring(0, 20)}"`);
267
+ break; // Only add once per element
268
+ }
241
269
  }
242
270
  }
243
271
 
@@ -412,7 +440,7 @@ function searchFilesSmart(
412
440
  analysis: ScreenshotAnalysis,
413
441
  projectRoot: string,
414
442
  maxFiles: number = 10
415
- ): { path: string; content: string; score: number }[] {
443
+ ): { path: string; content: string; score: number; filenameMatch: boolean }[] {
416
444
  const { visibleText, componentNames, codePatterns } = analysis;
417
445
 
418
446
  // Combine all search terms
@@ -534,7 +562,7 @@ function searchFilesSmart(
534
562
  }))
535
563
  });
536
564
 
537
- return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
565
+ return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score, filenameMatch: r.filenameMatch }));
538
566
  }
539
567
 
540
568
  const VISION_SYSTEM_PROMPT = `You edit code. Return ONLY valid JSON - no explanation, no preamble, no markdown.
@@ -678,10 +706,19 @@ export async function POST(request: Request) {
678
706
  const searchResults = searchFilesSmart(analysis, projectRoot, 10);
679
707
  smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
680
708
 
709
+ // Extract Phase 2a matches - files identified by component name matching visible text
710
+ // These are contextually relevant (e.g., "ProcessRow" seen in screenshot -> ProcessRow.tsx)
711
+ const phase2aMatches = searchResults
712
+ .filter(r => r.filenameMatch)
713
+ .map(r => r.path);
714
+
715
+ debugLog("Phase 2a component-name matches", { phase2aMatches });
716
+
681
717
  // PHASE 2.5: Find which files contain the focused element
682
718
  const focusedElementHints = findFilesContainingElement(
683
719
  focusedElements,
684
- searchResults.map(r => ({ path: r.path, content: r.content }))
720
+ searchResults.map(r => ({ path: r.path, content: r.content })),
721
+ phase2aMatches // Pass Phase 2a matches for massive bonus
685
722
  );
686
723
 
687
724
  if (focusedElementHints.length > 0) {
@@ -28,6 +28,10 @@ interface VisionFocusedElement {
28
28
  height: number;
29
29
  };
30
30
  description?: string;
31
+ /** The actual text content of the element (e.g., "REFRESH", "ADD PROCESS") */
32
+ textContent?: string;
33
+ /** The className string of the element for pattern matching */
34
+ className?: string;
31
35
  }
32
36
 
33
37
  interface VisionFileModification {
@@ -172,10 +176,14 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
172
176
  * This helps identify which file actually contains the element the user clicked on
173
177
  *
174
178
  * CURSOR-STYLE: Search file CONTENTS for patterns, not just file names
179
+ *
180
+ * @param phase2aMatches - File paths that Phase 2a identified by component name matching visible text
181
+ * These get a MASSIVE bonus because they're contextually relevant
175
182
  */
176
183
  function findFilesContainingElement(
177
184
  focusedElements: VisionFocusedElement[] | undefined,
178
- candidateFiles: { path: string; content: string }[]
185
+ candidateFiles: { path: string; content: string }[],
186
+ phase2aMatches: string[] = [] // Files that Phase 2a identified by component name
179
187
  ): { path: string; score: number; matches: string[] }[] {
180
188
  if (!focusedElements || focusedElements.length === 0) {
181
189
  return [];
@@ -187,7 +195,13 @@ function findFilesContainingElement(
187
195
  let score = 0;
188
196
  const matches: string[] = [];
189
197
  const content = file.content;
190
- const contentLower = content.toLowerCase();
198
+
199
+ // HIGHEST VALUE: Phase 2a identified this file by matching component name to visible text
200
+ // This means the LLM saw "ProcessRow" in the screenshot and this file is ProcessRow.tsx
201
+ if (phase2aMatches.includes(file.path)) {
202
+ score += 200;
203
+ matches.push('Phase2a component match');
204
+ }
191
205
 
192
206
  for (const el of focusedElements) {
193
207
  // Extract element type from the name (e.g., "button #123" -> "button")
@@ -212,28 +226,42 @@ function findFilesContainingElement(
212
226
  matches.push(`defines ${capitalizedType}`);
213
227
  }
214
228
 
215
- // MEDIUM VALUE: JSX usage of this element type (rendered in this file)
216
- const jsxTagPattern = new RegExp(`<${capitalizedType}[\\s/>]`, 'g');
217
- const jsxMatches = content.match(jsxTagPattern);
218
- if (jsxMatches) {
219
- score += 20 * jsxMatches.length;
220
- matches.push(`<${capitalizedType}> x${jsxMatches.length}`);
221
- }
222
-
223
- // MEDIUM VALUE: HTML tag usage
224
- const htmlTagPattern = new RegExp(`<${elementType}[\\s>]`, 'gi');
225
- const htmlMatches = content.match(htmlTagPattern);
226
- if (htmlMatches) {
227
- score += 10 * htmlMatches.length;
228
- matches.push(`<${elementType}> x${htmlMatches.length}`);
229
+ // NOTE: We intentionally DO NOT count buttons, onClick handlers, or JSX tags
230
+ // because files with MORE of these are often "kitchen sink" components,
231
+ // not the focused component the user is looking at.
232
+ // Instead, we rely on Phase 2a matches and textContent matching.
233
+
234
+ // VERY HIGH VALUE: Exact text content match (e.g., button says "REFRESH")
235
+ // This is THE file if it contains the exact text the user clicked on
236
+ if (el.textContent && el.textContent.length >= 2) {
237
+ // Search for the text in JSX patterns like >REFRESH< or {`REFRESH`} or {"REFRESH"}
238
+ const textPatterns = [
239
+ `>${el.textContent}<`, // JSX text content
240
+ `"${el.textContent}"`, // String literal
241
+ `'${el.textContent}'`, // Single-quoted string
242
+ `\`${el.textContent}\``, // Template literal
243
+ ];
244
+
245
+ for (const pattern of textPatterns) {
246
+ if (content.includes(pattern)) {
247
+ score += 150; // Very high - exact match!
248
+ matches.push(`contains "${el.textContent.substring(0, 20)}"`);
249
+ break; // Only add once per element
250
+ }
251
+ }
229
252
  }
230
253
 
231
- // LOW VALUE: Event handlers (onClick, onPress, etc.)
232
- if (elementType === "button" || componentType === "button") {
233
- const clickHandlers = content.match(/onClick\s*=/gi);
234
- if (clickHandlers) {
235
- score += 5 * clickHandlers.length;
236
- matches.push(`onClick x${clickHandlers.length}`);
254
+ // MEDIUM VALUE: className pattern match
255
+ // If the file contains the same className string, it likely defines this element
256
+ if (el.className && el.className.length >= 10) {
257
+ // Extract the first few unique class names (not all to avoid noise)
258
+ const classNames = el.className.split(/\s+/).filter(c => c.length > 3).slice(0, 5);
259
+ for (const cls of classNames) {
260
+ if (content.includes(cls)) {
261
+ score += 25;
262
+ matches.push(`has class "${cls.substring(0, 20)}"`);
263
+ break; // Only add once per element
264
+ }
237
265
  }
238
266
  }
239
267
 
@@ -408,7 +436,7 @@ function searchFilesSmart(
408
436
  analysis: ScreenshotAnalysis,
409
437
  projectRoot: string,
410
438
  maxFiles: number = 10
411
- ): { path: string; content: string; score: number }[] {
439
+ ): { path: string; content: string; score: number; filenameMatch: boolean }[] {
412
440
  const { visibleText, componentNames, codePatterns } = analysis;
413
441
 
414
442
  // Combine all search terms
@@ -530,7 +558,7 @@ function searchFilesSmart(
530
558
  }))
531
559
  });
532
560
 
533
- return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
561
+ return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score, filenameMatch: r.filenameMatch }));
534
562
  }
535
563
 
536
564
  const VISION_SYSTEM_PROMPT = `You edit code. Return ONLY valid JSON - no explanation, no preamble, no markdown.
@@ -647,10 +675,19 @@ export async function POST(request: Request) {
647
675
  const searchResults = searchFilesSmart(analysis, projectRoot, 10);
648
676
  smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
649
677
 
678
+ // Extract Phase 2a matches - files identified by component name matching visible text
679
+ // These are contextually relevant (e.g., "ProcessRow" seen in screenshot -> ProcessRow.tsx)
680
+ const phase2aMatches = searchResults
681
+ .filter(r => r.filenameMatch)
682
+ .map(r => r.path);
683
+
684
+ debugLog("Phase 2a component-name matches", { phase2aMatches });
685
+
650
686
  // PHASE 2.5: Find which files contain the focused element
651
687
  const focusedElementHints = findFilesContainingElement(
652
688
  focusedElements,
653
- searchResults.map(r => ({ path: r.path, content: r.content }))
689
+ searchResults.map(r => ({ path: r.path, content: r.content })),
690
+ phase2aMatches // Pass Phase 2a matches for massive bonus
654
691
  );
655
692
 
656
693
  if (focusedElementHints.length > 0) {
@@ -787,7 +787,11 @@ export function SonanceDevTools() {
787
787
  el.setAttribute("data-sonance-variant", variantId);
788
788
  }
789
789
 
790
- newTagged.push({ name, rect, type: "component", variantId });
790
+ // Capture textContent and className for dynamic element matching
791
+ const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
792
+ const className = el.className?.toString() || "";
793
+
794
+ newTagged.push({ name, rect, type: "component", variantId, textContent, className });
791
795
  }
792
796
  }
793
797
  });
@@ -826,7 +830,11 @@ export function SonanceDevTools() {
826
830
  el.setAttribute("data-sonance-name", genericName);
827
831
  }
828
832
 
829
- newTagged.push({ name: genericName, rect, type: "component", variantId });
833
+ // Capture textContent and className for dynamic element matching
834
+ const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
835
+ const elClassName = el.className?.toString() || "";
836
+
837
+ newTagged.push({ name: genericName, rect, type: "component", variantId, textContent, className: elClassName });
830
838
  }
831
839
  });
832
840
  });
@@ -1017,6 +1025,9 @@ export function SonanceDevTools() {
1017
1025
  width: element.rect.width,
1018
1026
  height: element.rect.height,
1019
1027
  },
1028
+ // NEW: Capture text and className for dynamic file matching
1029
+ textContent: element.textContent,
1030
+ className: element.className,
1020
1031
  };
1021
1032
 
1022
1033
  setVisionFocusedElements((prev) => {
@@ -22,8 +22,10 @@ export interface DetectedElement {
22
22
  logoId?: string;
23
23
  /** Unique ID for text elements (for selection/editing) */
24
24
  textId?: string;
25
- /** The actual text content (for text elements) */
25
+ /** The actual text content (for all elements - button text, link text, etc.) */
26
26
  textContent?: string;
27
+ /** The className string for pattern matching in code */
28
+ className?: string;
27
29
  /** Component variant ID (hash of styles/classes) to distinguish visual styles */
28
30
  variantId?: string;
29
31
  }
@@ -220,6 +222,10 @@ export interface VisionFocusedElement {
220
222
  height: number;
221
223
  };
222
224
  description?: string;
225
+ /** The actual text content of the element (e.g., "REFRESH", "ADD PROCESS") */
226
+ textContent?: string;
227
+ /** The className string of the element for pattern matching */
228
+ className?: string;
223
229
  }
224
230
 
225
231
  export interface VisionEditRequest {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.63",
3
+ "version": "1.3.64",
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",