sonance-brand-mcp 1.3.62 → 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 {
@@ -106,7 +110,7 @@ async function analyzeScreenshotForSearch(
106
110
 
107
111
  const response = await anthropic.messages.create({
108
112
  model: "claude-sonnet-4-20250514",
109
- max_tokens: 1024,
113
+ max_tokens: 2048, // Increased for better analysis
110
114
  messages: [
111
115
  {
112
116
  role: "user",
@@ -174,10 +178,16 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
174
178
  /**
175
179
  * Search candidate files for JSX code matching the focused element
176
180
  * This helps identify which file actually contains the element the user clicked on
181
+ *
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
177
186
  */
178
187
  function findFilesContainingElement(
179
188
  focusedElements: VisionFocusedElement[] | undefined,
180
- candidateFiles: { path: string; content: string }[]
189
+ candidateFiles: { path: string; content: string }[],
190
+ phase2aMatches: string[] = [] // Files that Phase 2a identified by component name
181
191
  ): { path: string; score: number; matches: string[] }[] {
182
192
  if (!focusedElements || focusedElements.length === 0) {
183
193
  return [];
@@ -188,44 +198,88 @@ function findFilesContainingElement(
188
198
  for (const file of candidateFiles) {
189
199
  let score = 0;
190
200
  const matches: string[] = [];
191
- const content = file.content.toLowerCase();
201
+ const content = file.content;
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
+ }
192
209
 
193
210
  for (const el of focusedElements) {
194
211
  // Extract element type from the name (e.g., "button #123" -> "button")
195
212
  const elementType = el.name.split(/[\s#]/)[0].toLowerCase();
196
213
  const componentType = el.type.toLowerCase();
214
+ const capitalizedType = el.type.charAt(0).toUpperCase() + el.type.slice(1);
197
215
 
198
- // Search for JSX patterns
199
- // Check for HTML tags like <button, <div, etc.
200
- if (content.includes(`<${elementType}`)) {
201
- score += 10;
202
- matches.push(`<${elementType}>`);
216
+ // HIGH VALUE: Component is EXPORTED from this file (this is likely THE file)
217
+ if (content.includes(`export function ${capitalizedType}`) ||
218
+ content.includes(`export const ${capitalizedType}`) ||
219
+ content.includes(`export default function ${capitalizedType}`)) {
220
+ score += 100;
221
+ matches.push(`EXPORTS ${capitalizedType}`);
203
222
  }
204
223
 
205
- // Check for React component like <Button, <Card, etc.
206
- const capitalizedType = el.type.charAt(0).toUpperCase() + el.type.slice(1);
207
- if (file.content.includes(`<${capitalizedType}`)) {
208
- score += 15;
209
- matches.push(`<${capitalizedType}>`);
224
+ // HIGH VALUE: Component is DEFINED in this file
225
+ if (content.includes(`function ${capitalizedType}(`) ||
226
+ content.includes(`const ${capitalizedType} =`) ||
227
+ content.includes(`const ${capitalizedType}:`) ||
228
+ content.includes(`function ${capitalizedType} (`)) {
229
+ score += 50;
230
+ matches.push(`defines ${capitalizedType}`);
210
231
  }
211
232
 
212
- // Check for component imports
213
- if (content.includes(`import`) && content.includes(componentType)) {
214
- score += 5;
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
+ }
215
256
  }
216
257
 
217
- // Check for event handlers commonly associated with buttons
218
- if (elementType === "button" || componentType === "button") {
219
- if (content.includes("onclick") || content.includes("onpress")) {
220
- score += 3;
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
+ }
221
269
  }
222
270
  }
223
271
 
224
- // Check for the component being defined in this file
225
- if (file.content.includes(`function ${capitalizedType}`) ||
226
- file.content.includes(`const ${capitalizedType}`)) {
227
- score += 20;
228
- matches.push(`defines ${capitalizedType}`);
272
+ // BONUS: File name contains the element type
273
+ const fileName = file.path.toLowerCase();
274
+ if (fileName.includes(elementType) || fileName.includes(componentType)) {
275
+ score += 30;
276
+ matches.push(`filename contains ${elementType}`);
277
+ }
278
+
279
+ // BONUS: File is a Row/Cell component (likely contains action buttons)
280
+ if (fileName.includes('row') || fileName.includes('cell') || fileName.includes('item')) {
281
+ score += 15;
282
+ matches.push('row/cell component');
229
283
  }
230
284
  }
231
285
 
@@ -386,7 +440,7 @@ function searchFilesSmart(
386
440
  analysis: ScreenshotAnalysis,
387
441
  projectRoot: string,
388
442
  maxFiles: number = 10
389
- ): { path: string; content: string; score: number }[] {
443
+ ): { path: string; content: string; score: number; filenameMatch: boolean }[] {
390
444
  const { visibleText, componentNames, codePatterns } = analysis;
391
445
 
392
446
  // Combine all search terms
@@ -508,7 +562,7 @@ function searchFilesSmart(
508
562
  }))
509
563
  });
510
564
 
511
- 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 }));
512
566
  }
513
567
 
514
568
  const VISION_SYSTEM_PROMPT = `You edit code. Return ONLY valid JSON - no explanation, no preamble, no markdown.
@@ -652,10 +706,19 @@ export async function POST(request: Request) {
652
706
  const searchResults = searchFilesSmart(analysis, projectRoot, 10);
653
707
  smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
654
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
+
655
717
  // PHASE 2.5: Find which files contain the focused element
656
718
  const focusedElementHints = findFilesContainingElement(
657
719
  focusedElements,
658
- 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
659
722
  );
660
723
 
661
724
  if (focusedElementHints.length > 0) {
@@ -668,8 +731,19 @@ export async function POST(request: Request) {
668
731
  });
669
732
  }
670
733
 
671
- // PHASE 3: Ask LLM to pick the best file from actual file list
672
- if (searchResults.length > 0) {
734
+ // CURSOR-STYLE: If focused element search has high confidence, use it directly
735
+ // Skip the LLM guessing phase - we found the file that contains the code
736
+ const HIGH_CONFIDENCE_THRESHOLD = 50; // Score of 50+ means we found exact matches
737
+
738
+ if (focusedElementHints.length > 0 && focusedElementHints[0].score >= HIGH_CONFIDENCE_THRESHOLD) {
739
+ // HIGH CONFIDENCE: Use focused element match directly
740
+ recommendedFile = {
741
+ path: focusedElementHints[0].path,
742
+ reason: `Content search found element (score: ${focusedElementHints[0].score}, matches: ${focusedElementHints[0].matches.join(', ')})`
743
+ };
744
+ debugLog("HIGH CONFIDENCE: Using focused element match directly", recommendedFile);
745
+ } else if (searchResults.length > 0) {
746
+ // LOW CONFIDENCE: Fall back to LLM selection
673
747
  const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
674
748
  const selectedFile = await selectBestFileFromList(
675
749
  screenshot,
@@ -2422,3 +2496,4 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
2422
2496
  failedPatches,
2423
2497
  };
2424
2498
  }
2499
+
@@ -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 {
@@ -102,7 +106,7 @@ async function analyzeScreenshotForSearch(
102
106
 
103
107
  const response = await anthropic.messages.create({
104
108
  model: "claude-sonnet-4-20250514",
105
- max_tokens: 1024,
109
+ max_tokens: 2048, // Increased for better analysis
106
110
  messages: [
107
111
  {
108
112
  role: "user",
@@ -170,10 +174,16 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
170
174
  /**
171
175
  * Search candidate files for JSX code matching the focused element
172
176
  * This helps identify which file actually contains the element the user clicked on
177
+ *
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
173
182
  */
174
183
  function findFilesContainingElement(
175
184
  focusedElements: VisionFocusedElement[] | undefined,
176
- candidateFiles: { path: string; content: string }[]
185
+ candidateFiles: { path: string; content: string }[],
186
+ phase2aMatches: string[] = [] // Files that Phase 2a identified by component name
177
187
  ): { path: string; score: number; matches: string[] }[] {
178
188
  if (!focusedElements || focusedElements.length === 0) {
179
189
  return [];
@@ -184,44 +194,88 @@ function findFilesContainingElement(
184
194
  for (const file of candidateFiles) {
185
195
  let score = 0;
186
196
  const matches: string[] = [];
187
- const content = file.content.toLowerCase();
197
+ const content = file.content;
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
+ }
188
205
 
189
206
  for (const el of focusedElements) {
190
207
  // Extract element type from the name (e.g., "button #123" -> "button")
191
208
  const elementType = el.name.split(/[\s#]/)[0].toLowerCase();
192
209
  const componentType = el.type.toLowerCase();
210
+ const capitalizedType = el.type.charAt(0).toUpperCase() + el.type.slice(1);
193
211
 
194
- // Search for JSX patterns
195
- // Check for HTML tags like <button, <div, etc.
196
- if (content.includes(`<${elementType}`)) {
197
- score += 10;
198
- matches.push(`<${elementType}>`);
212
+ // HIGH VALUE: Component is EXPORTED from this file (this is likely THE file)
213
+ if (content.includes(`export function ${capitalizedType}`) ||
214
+ content.includes(`export const ${capitalizedType}`) ||
215
+ content.includes(`export default function ${capitalizedType}`)) {
216
+ score += 100;
217
+ matches.push(`EXPORTS ${capitalizedType}`);
199
218
  }
200
219
 
201
- // Check for React component like <Button, <Card, etc.
202
- const capitalizedType = el.type.charAt(0).toUpperCase() + el.type.slice(1);
203
- if (file.content.includes(`<${capitalizedType}`)) {
204
- score += 15;
205
- matches.push(`<${capitalizedType}>`);
220
+ // HIGH VALUE: Component is DEFINED in this file
221
+ if (content.includes(`function ${capitalizedType}(`) ||
222
+ content.includes(`const ${capitalizedType} =`) ||
223
+ content.includes(`const ${capitalizedType}:`) ||
224
+ content.includes(`function ${capitalizedType} (`)) {
225
+ score += 50;
226
+ matches.push(`defines ${capitalizedType}`);
206
227
  }
207
228
 
208
- // Check for component imports
209
- if (content.includes(`import`) && content.includes(componentType)) {
210
- score += 5;
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
+ }
211
252
  }
212
253
 
213
- // Check for event handlers commonly associated with buttons
214
- if (elementType === "button" || componentType === "button") {
215
- if (content.includes("onclick") || content.includes("onpress")) {
216
- score += 3;
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
+ }
217
265
  }
218
266
  }
219
267
 
220
- // Check for the component being defined in this file
221
- if (file.content.includes(`function ${capitalizedType}`) ||
222
- file.content.includes(`const ${capitalizedType}`)) {
223
- score += 20;
224
- matches.push(`defines ${capitalizedType}`);
268
+ // BONUS: File name contains the element type
269
+ const fileName = file.path.toLowerCase();
270
+ if (fileName.includes(elementType) || fileName.includes(componentType)) {
271
+ score += 30;
272
+ matches.push(`filename contains ${elementType}`);
273
+ }
274
+
275
+ // BONUS: File is a Row/Cell component (likely contains action buttons)
276
+ if (fileName.includes('row') || fileName.includes('cell') || fileName.includes('item')) {
277
+ score += 15;
278
+ matches.push('row/cell component');
225
279
  }
226
280
  }
227
281
 
@@ -382,7 +436,7 @@ function searchFilesSmart(
382
436
  analysis: ScreenshotAnalysis,
383
437
  projectRoot: string,
384
438
  maxFiles: number = 10
385
- ): { path: string; content: string; score: number }[] {
439
+ ): { path: string; content: string; score: number; filenameMatch: boolean }[] {
386
440
  const { visibleText, componentNames, codePatterns } = analysis;
387
441
 
388
442
  // Combine all search terms
@@ -504,7 +558,7 @@ function searchFilesSmart(
504
558
  }))
505
559
  });
506
560
 
507
- 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 }));
508
562
  }
509
563
 
510
564
  const VISION_SYSTEM_PROMPT = `You edit code. Return ONLY valid JSON - no explanation, no preamble, no markdown.
@@ -621,10 +675,19 @@ export async function POST(request: Request) {
621
675
  const searchResults = searchFilesSmart(analysis, projectRoot, 10);
622
676
  smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
623
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
+
624
686
  // PHASE 2.5: Find which files contain the focused element
625
687
  const focusedElementHints = findFilesContainingElement(
626
688
  focusedElements,
627
- 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
628
691
  );
629
692
 
630
693
  if (focusedElementHints.length > 0) {
@@ -637,8 +700,19 @@ export async function POST(request: Request) {
637
700
  });
638
701
  }
639
702
 
640
- // PHASE 3: Ask LLM to pick the best file from actual file list
641
- if (searchResults.length > 0) {
703
+ // CURSOR-STYLE: If focused element search has high confidence, use it directly
704
+ // Skip the LLM guessing phase - we found the file that contains the code
705
+ const HIGH_CONFIDENCE_THRESHOLD = 50; // Score of 50+ means we found exact matches
706
+
707
+ if (focusedElementHints.length > 0 && focusedElementHints[0].score >= HIGH_CONFIDENCE_THRESHOLD) {
708
+ // HIGH CONFIDENCE: Use focused element match directly
709
+ recommendedFile = {
710
+ path: focusedElementHints[0].path,
711
+ reason: `Content search found element (score: ${focusedElementHints[0].score}, matches: ${focusedElementHints[0].matches.join(', ')})`
712
+ };
713
+ debugLog("HIGH CONFIDENCE: Using focused element match directly", recommendedFile);
714
+ } else if (searchResults.length > 0) {
715
+ // LOW CONFIDENCE: Fall back to LLM selection
642
716
  const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
643
717
  const selectedFile = await selectBestFileFromList(
644
718
  screenshot,
@@ -2231,3 +2305,4 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
2231
2305
  failedPatches,
2232
2306
  };
2233
2307
  }
2308
+
@@ -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.62",
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",