sonance-brand-mcp 1.3.89 → 1.3.90

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.
@@ -229,6 +229,139 @@ function findElementIdInFile(
229
229
  return null;
230
230
  }
231
231
 
232
+ /**
233
+ * Find the line number of a focused element in the source code
234
+ * Uses multiple strategies in priority order:
235
+ * 1. DOM id (highest confidence)
236
+ * 2. Text content
237
+ * 3. ClassName patterns
238
+ *
239
+ * This extends the basic ID matching to handle elements without IDs.
240
+ */
241
+ function findElementLineInFile(
242
+ fileContent: string,
243
+ focusedElement: VisionFocusedElement
244
+ ): { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null {
245
+ if (!fileContent) return null;
246
+
247
+ const lines = fileContent.split('\n');
248
+
249
+ // PRIORITY 1: DOM id - highest confidence (exact match)
250
+ if (focusedElement.elementId) {
251
+ const idPattern = new RegExp(`id=["'\`]${focusedElement.elementId}["'\`]`);
252
+ for (let i = 0; i < lines.length; i++) {
253
+ if (idPattern.test(lines[i])) {
254
+ return {
255
+ lineNumber: i + 1,
256
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
257
+ confidence: 'high',
258
+ matchedBy: `DOM id="${focusedElement.elementId}"`
259
+ };
260
+ }
261
+ }
262
+ }
263
+
264
+ // Also try child IDs with high confidence
265
+ if (focusedElement.childIds && focusedElement.childIds.length > 0) {
266
+ for (const childId of focusedElement.childIds) {
267
+ const idPattern = new RegExp(`id=["'\`]${childId}["'\`]`);
268
+ for (let i = 0; i < lines.length; i++) {
269
+ if (idPattern.test(lines[i])) {
270
+ return {
271
+ lineNumber: i + 1,
272
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
273
+ confidence: 'high',
274
+ matchedBy: `child id="${childId}"`
275
+ };
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ // PRIORITY 2: Exact text content in JSX
282
+ if (focusedElement.textContent && focusedElement.textContent.trim().length >= 2) {
283
+ const text = focusedElement.textContent.trim();
284
+ // Escape special regex characters in the text
285
+ const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
286
+
287
+ for (let i = 0; i < lines.length; i++) {
288
+ const line = lines[i];
289
+ // Match patterns like: >Text<, >Text</tag, "Text", 'Text', {`Text`}
290
+ const textPatterns = [
291
+ `>${escapedText}<`, // JSX content: >Text<
292
+ `"${escapedText}"`, // String literal
293
+ `'${escapedText}'`, // String literal
294
+ `\`${escapedText}\``, // Template literal
295
+ ];
296
+
297
+ for (const pattern of textPatterns) {
298
+ if (line.includes(pattern.replace(/\\/g, ''))) {
299
+ return {
300
+ lineNumber: i + 1,
301
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
302
+ confidence: 'high',
303
+ matchedBy: `textContent="${text.substring(0, 30)}${text.length > 30 ? '...' : ''}"`
304
+ };
305
+ }
306
+ }
307
+ }
308
+
309
+ // Try partial match for longer text (first 15+ chars)
310
+ if (text.length > 15) {
311
+ const partialText = text.substring(0, 15);
312
+ for (let i = 0; i < lines.length; i++) {
313
+ if (lines[i].includes(`>${partialText}`) || lines[i].includes(`"${partialText}`)) {
314
+ return {
315
+ lineNumber: i + 1,
316
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
317
+ confidence: 'medium',
318
+ matchedBy: `partial textContent starting with "${partialText}..."`
319
+ };
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ // PRIORITY 3: Distinctive className patterns
326
+ if (focusedElement.className) {
327
+ // Extract distinctive class names (long, not hover/focus pseudo-classes)
328
+ const classes = focusedElement.className.split(/\s+/)
329
+ .filter(c => c.length > 8 && !c.startsWith('hover:') && !c.startsWith('focus:') && !c.startsWith('active:'));
330
+
331
+ for (const cls of classes) {
332
+ for (let i = 0; i < lines.length; i++) {
333
+ if (lines[i].includes(cls)) {
334
+ return {
335
+ lineNumber: i + 1,
336
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
337
+ confidence: 'medium',
338
+ matchedBy: `className contains "${cls}"`
339
+ };
340
+ }
341
+ }
342
+ }
343
+
344
+ // Try shorter but unique-looking class names (not utility classes)
345
+ const uniqueClasses = focusedElement.className.split(/\s+/)
346
+ .filter(c => c.length > 4 && !c.match(/^(p[xytblr]?-|m[xytblr]?-|w-|h-|bg-|text-|flex|grid|block|hidden)/));
347
+
348
+ for (const cls of uniqueClasses) {
349
+ for (let i = 0; i < lines.length; i++) {
350
+ if (lines[i].includes(cls)) {
351
+ return {
352
+ lineNumber: i + 1,
353
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
354
+ confidence: 'low',
355
+ matchedBy: `className contains "${cls}"`
356
+ };
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ return null;
363
+ }
364
+
232
365
  /**
233
366
  * PHASE 0: Deterministic Element ID Search (Cursor-style)
234
367
  * Grep entire codebase for the element ID. If found in multiple files,
@@ -1330,12 +1463,21 @@ User Request: "${userPrompt}"
1330
1463
  if (recommendedFileContent) {
1331
1464
  const content = recommendedFileContent.content;
1332
1465
 
1333
- // Search for element IDs in the file to enable precise targeting
1334
- let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
1335
- if (focusedElements && focusedElements.length > 0) {
1466
+ // Search for focused element in the file using multiple strategies
1467
+ // Priority: DOM id > textContent > className patterns
1468
+ let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
1469
+ if (focusedElements && focusedElements.length > 0) {
1336
1470
  for (const el of focusedElements) {
1337
- idMatch = findElementIdInFile(content, el.elementId, el.childIds);
1338
- if (idMatch) break;
1471
+ elementLocation = findElementLineInFile(content, el);
1472
+ if (elementLocation) {
1473
+ debugLog("Found focused element in file", {
1474
+ matchedBy: elementLocation.matchedBy,
1475
+ lineNumber: elementLocation.lineNumber,
1476
+ confidence: elementLocation.confidence,
1477
+ file: recommendedFileContent.path,
1478
+ });
1479
+ break;
1480
+ }
1339
1481
  }
1340
1482
  }
1341
1483
 
@@ -1350,22 +1492,36 @@ User Request: "${userPrompt}"
1350
1492
  textContent += `\n`;
1351
1493
  }
1352
1494
 
1353
- // Add precise targeting if we found an ID match
1354
- if (idMatch) {
1495
+ // Add precise targeting with line number and snippet
1496
+ if (elementLocation) {
1355
1497
  textContent += `
1356
- PRECISE TARGET (found by element ID):
1357
- ID: "${idMatch.matchedId}"
1358
- → Line: ${idMatch.lineNumber}
1359
- Look for this ID in the code and modify the element that contains it.
1498
+ ══════════════════════════════════════════════════════════════════════════════
1499
+ PRECISE TARGET LOCATION (${elementLocation.confidence} confidence)
1500
+ ══════════════════════════════════════════════════════════════════════════════
1501
+ Matched by: ${elementLocation.matchedBy}
1502
+ → Line: ${elementLocation.lineNumber}
1503
+
1504
+ THE USER CLICKED ON THE ELEMENT AT LINE ${elementLocation.lineNumber}.
1505
+ Here is the exact code around that element:
1506
+ \`\`\`
1507
+ ${elementLocation.snippet}
1508
+ \`\`\`
1509
+
1510
+ ⚠️ IMPORTANT: Modify ONLY the element at line ${elementLocation.lineNumber}, NOT other similar elements in the file.
1360
1511
 
1361
1512
  `;
1362
- debugLog("Found element ID in file", {
1363
- matchedId: idMatch.matchedId,
1364
- lineNumber: idMatch.lineNumber,
1365
- file: recommendedFileContent.path,
1366
- });
1367
1513
  } else {
1368
1514
  textContent += `\n`;
1515
+ debugLog("Could not locate focused element in file - no ID, textContent, or className match", {
1516
+ file: recommendedFileContent.path,
1517
+ focusedElements: focusedElements.map(el => ({
1518
+ name: el.name,
1519
+ type: el.type,
1520
+ textContent: el.textContent?.substring(0, 30),
1521
+ className: el.className?.substring(0, 50),
1522
+ elementId: el.elementId,
1523
+ }))
1524
+ });
1369
1525
  }
1370
1526
  }
1371
1527
 
@@ -225,6 +225,139 @@ function findElementIdInFile(
225
225
  return null;
226
226
  }
227
227
 
228
+ /**
229
+ * Find the line number of a focused element in the source code
230
+ * Uses multiple strategies in priority order:
231
+ * 1. DOM id (highest confidence)
232
+ * 2. Text content
233
+ * 3. ClassName patterns
234
+ *
235
+ * This extends the basic ID matching to handle elements without IDs.
236
+ */
237
+ function findElementLineInFile(
238
+ fileContent: string,
239
+ focusedElement: VisionFocusedElement
240
+ ): { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null {
241
+ if (!fileContent) return null;
242
+
243
+ const lines = fileContent.split('\n');
244
+
245
+ // PRIORITY 1: DOM id - highest confidence (exact match)
246
+ if (focusedElement.elementId) {
247
+ const idPattern = new RegExp(`id=["'\`]${focusedElement.elementId}["'\`]`);
248
+ for (let i = 0; i < lines.length; i++) {
249
+ if (idPattern.test(lines[i])) {
250
+ return {
251
+ lineNumber: i + 1,
252
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
253
+ confidence: 'high',
254
+ matchedBy: `DOM id="${focusedElement.elementId}"`
255
+ };
256
+ }
257
+ }
258
+ }
259
+
260
+ // Also try child IDs with high confidence
261
+ if (focusedElement.childIds && focusedElement.childIds.length > 0) {
262
+ for (const childId of focusedElement.childIds) {
263
+ const idPattern = new RegExp(`id=["'\`]${childId}["'\`]`);
264
+ for (let i = 0; i < lines.length; i++) {
265
+ if (idPattern.test(lines[i])) {
266
+ return {
267
+ lineNumber: i + 1,
268
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
269
+ confidence: 'high',
270
+ matchedBy: `child id="${childId}"`
271
+ };
272
+ }
273
+ }
274
+ }
275
+ }
276
+
277
+ // PRIORITY 2: Exact text content in JSX
278
+ if (focusedElement.textContent && focusedElement.textContent.trim().length >= 2) {
279
+ const text = focusedElement.textContent.trim();
280
+ // Escape special regex characters in the text
281
+ const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
282
+
283
+ for (let i = 0; i < lines.length; i++) {
284
+ const line = lines[i];
285
+ // Match patterns like: >Text<, >Text</tag, "Text", 'Text', {`Text`}
286
+ const textPatterns = [
287
+ `>${escapedText}<`, // JSX content: >Text<
288
+ `"${escapedText}"`, // String literal
289
+ `'${escapedText}'`, // String literal
290
+ `\`${escapedText}\``, // Template literal
291
+ ];
292
+
293
+ for (const pattern of textPatterns) {
294
+ if (line.includes(pattern.replace(/\\/g, ''))) {
295
+ return {
296
+ lineNumber: i + 1,
297
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
298
+ confidence: 'high',
299
+ matchedBy: `textContent="${text.substring(0, 30)}${text.length > 30 ? '...' : ''}"`
300
+ };
301
+ }
302
+ }
303
+ }
304
+
305
+ // Try partial match for longer text (first 15+ chars)
306
+ if (text.length > 15) {
307
+ const partialText = text.substring(0, 15);
308
+ for (let i = 0; i < lines.length; i++) {
309
+ if (lines[i].includes(`>${partialText}`) || lines[i].includes(`"${partialText}`)) {
310
+ return {
311
+ lineNumber: i + 1,
312
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
313
+ confidence: 'medium',
314
+ matchedBy: `partial textContent starting with "${partialText}..."`
315
+ };
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ // PRIORITY 3: Distinctive className patterns
322
+ if (focusedElement.className) {
323
+ // Extract distinctive class names (long, not hover/focus pseudo-classes)
324
+ const classes = focusedElement.className.split(/\s+/)
325
+ .filter(c => c.length > 8 && !c.startsWith('hover:') && !c.startsWith('focus:') && !c.startsWith('active:'));
326
+
327
+ for (const cls of classes) {
328
+ for (let i = 0; i < lines.length; i++) {
329
+ if (lines[i].includes(cls)) {
330
+ return {
331
+ lineNumber: i + 1,
332
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
333
+ confidence: 'medium',
334
+ matchedBy: `className contains "${cls}"`
335
+ };
336
+ }
337
+ }
338
+ }
339
+
340
+ // Try shorter but unique-looking class names (not utility classes)
341
+ const uniqueClasses = focusedElement.className.split(/\s+/)
342
+ .filter(c => c.length > 4 && !c.match(/^(p[xytblr]?-|m[xytblr]?-|w-|h-|bg-|text-|flex|grid|block|hidden)/));
343
+
344
+ for (const cls of uniqueClasses) {
345
+ for (let i = 0; i < lines.length; i++) {
346
+ if (lines[i].includes(cls)) {
347
+ return {
348
+ lineNumber: i + 1,
349
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
350
+ confidence: 'low',
351
+ matchedBy: `className contains "${cls}"`
352
+ };
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ return null;
359
+ }
360
+
228
361
  /**
229
362
  * PHASE 0: Deterministic Element ID Search (Cursor-style)
230
363
  * Grep entire codebase for the element ID. If found in multiple files,
@@ -1299,12 +1432,21 @@ User Request: "${userPrompt}"
1299
1432
  if (recommendedFileContent) {
1300
1433
  const content = recommendedFileContent.content;
1301
1434
 
1302
- // Search for element IDs in the file to enable precise targeting
1303
- let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
1304
- if (focusedElements && focusedElements.length > 0) {
1435
+ // Search for focused element in the file using multiple strategies
1436
+ // Priority: DOM id > textContent > className patterns
1437
+ let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
1438
+ if (focusedElements && focusedElements.length > 0) {
1305
1439
  for (const el of focusedElements) {
1306
- idMatch = findElementIdInFile(content, el.elementId, el.childIds);
1307
- if (idMatch) break;
1440
+ elementLocation = findElementLineInFile(content, el);
1441
+ if (elementLocation) {
1442
+ debugLog("Found focused element in file", {
1443
+ matchedBy: elementLocation.matchedBy,
1444
+ lineNumber: elementLocation.lineNumber,
1445
+ confidence: elementLocation.confidence,
1446
+ file: recommendedFileContent.path,
1447
+ });
1448
+ break;
1449
+ }
1308
1450
  }
1309
1451
  }
1310
1452
 
@@ -1319,22 +1461,36 @@ User Request: "${userPrompt}"
1319
1461
  textContent += `\n`;
1320
1462
  }
1321
1463
 
1322
- // Add precise targeting if we found an ID match
1323
- if (idMatch) {
1464
+ // Add precise targeting with line number and snippet
1465
+ if (elementLocation) {
1324
1466
  textContent += `
1325
- PRECISE TARGET (found by element ID):
1326
- ID: "${idMatch.matchedId}"
1327
- → Line: ${idMatch.lineNumber}
1328
- Look for this ID in the code and modify the element that contains it.
1467
+ ══════════════════════════════════════════════════════════════════════════════
1468
+ PRECISE TARGET LOCATION (${elementLocation.confidence} confidence)
1469
+ ══════════════════════════════════════════════════════════════════════════════
1470
+ Matched by: ${elementLocation.matchedBy}
1471
+ → Line: ${elementLocation.lineNumber}
1472
+
1473
+ THE USER CLICKED ON THE ELEMENT AT LINE ${elementLocation.lineNumber}.
1474
+ Here is the exact code around that element:
1475
+ \`\`\`
1476
+ ${elementLocation.snippet}
1477
+ \`\`\`
1478
+
1479
+ ⚠️ IMPORTANT: Modify ONLY the element at line ${elementLocation.lineNumber}, NOT other similar elements in the file.
1329
1480
 
1330
1481
  `;
1331
- debugLog("Found element ID in file", {
1332
- matchedId: idMatch.matchedId,
1333
- lineNumber: idMatch.lineNumber,
1334
- file: recommendedFileContent.path,
1335
- });
1336
1482
  } else {
1337
1483
  textContent += `\n`;
1484
+ debugLog("Could not locate focused element in file - no ID, textContent, or className match", {
1485
+ file: recommendedFileContent.path,
1486
+ focusedElements: focusedElements.map(el => ({
1487
+ name: el.name,
1488
+ type: el.type,
1489
+ textContent: el.textContent?.substring(0, 30),
1490
+ className: el.className?.substring(0, 50),
1491
+ elementId: el.elementId,
1492
+ }))
1493
+ });
1338
1494
  }
1339
1495
  }
1340
1496
 
@@ -1215,10 +1215,11 @@ export function SonanceDevTools() {
1215
1215
  console.warn("[Apply-First] Failed to persist session:", e);
1216
1216
  }
1217
1217
 
1218
- // After a brief delay, assume HMR has completed
1218
+ // Force page refresh to ensure changes are visible
1219
+ // Session is already persisted to localStorage, so it survives refresh
1219
1220
  setTimeout(() => {
1220
- setApplyFirstStatus("reviewing");
1221
- }, 1500);
1221
+ window.location.reload();
1222
+ }, 500);
1222
1223
  }, [visionFocusedElements]);
1223
1224
 
1224
1225
  // Accept changes - delete backups
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.89",
3
+ "version": "1.3.90",
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",