sonance-brand-mcp 1.3.88 → 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,242 @@ 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
|
+
|
|
365
|
+
/**
|
|
366
|
+
* PHASE 0: Deterministic Element ID Search (Cursor-style)
|
|
367
|
+
* Grep entire codebase for the element ID. If found in multiple files,
|
|
368
|
+
* use the current route to disambiguate (prioritize page file for route).
|
|
369
|
+
*
|
|
370
|
+
* This is the most reliable signal - like Cursor knowing which file you're in.
|
|
371
|
+
* Element IDs should be unique, so if we find one, that's THE file.
|
|
372
|
+
*/
|
|
373
|
+
function findFilesByElementId(
|
|
374
|
+
projectRoot: string,
|
|
375
|
+
elementId: string,
|
|
376
|
+
currentRoute: string,
|
|
377
|
+
discoverPageFileFn: (route: string, projectRoot: string) => string | null
|
|
378
|
+
): { path: string; lineNumber: number; isRouteMatch: boolean }[] {
|
|
379
|
+
const pattern = new RegExp(`id=["'\`]${elementId}["'\`]`);
|
|
380
|
+
const matches: { path: string; lineNumber: number; isRouteMatch: boolean }[] = [];
|
|
381
|
+
|
|
382
|
+
// Determine expected page file from route
|
|
383
|
+
const routePageFile = discoverPageFileFn(currentRoute, projectRoot);
|
|
384
|
+
|
|
385
|
+
// Search all common project directories that exist
|
|
386
|
+
// This supports: src/app, app/, pages/, components/, lib/ structures
|
|
387
|
+
const commonDirs = ['src', 'app', 'pages', 'components', 'lib'];
|
|
388
|
+
const searchDirs = commonDirs
|
|
389
|
+
.map(dir => path.join(projectRoot, dir))
|
|
390
|
+
.filter(dir => {
|
|
391
|
+
try {
|
|
392
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// If no standard dirs found, search project root (excluding node_modules)
|
|
399
|
+
if (searchDirs.length === 0) {
|
|
400
|
+
searchDirs.push(projectRoot);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
debugLog("PHASE 0: Searching for element ID", {
|
|
404
|
+
elementId,
|
|
405
|
+
currentRoute,
|
|
406
|
+
routePageFile,
|
|
407
|
+
searchDirs: searchDirs.map(d => d.replace(projectRoot + '/', ''))
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
function searchDirRecursive(dir: string): void {
|
|
411
|
+
try {
|
|
412
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
413
|
+
for (const entry of entries) {
|
|
414
|
+
const fullPath = path.join(dir, entry.name);
|
|
415
|
+
|
|
416
|
+
// Skip node_modules, hidden directories, and build outputs
|
|
417
|
+
if (entry.isDirectory()) {
|
|
418
|
+
if (
|
|
419
|
+
entry.name.includes('node_modules') ||
|
|
420
|
+
entry.name.startsWith('.') ||
|
|
421
|
+
entry.name === 'dist' ||
|
|
422
|
+
entry.name === 'build' ||
|
|
423
|
+
entry.name === '.next'
|
|
424
|
+
) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
searchDirRecursive(fullPath);
|
|
428
|
+
} else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
429
|
+
try {
|
|
430
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
431
|
+
const lines = content.split('\n');
|
|
432
|
+
for (let i = 0; i < lines.length; i++) {
|
|
433
|
+
if (pattern.test(lines[i])) {
|
|
434
|
+
const relativePath = fullPath.replace(projectRoot + '/', '');
|
|
435
|
+
const isRouteMatch = relativePath === routePageFile;
|
|
436
|
+
matches.push({
|
|
437
|
+
path: relativePath,
|
|
438
|
+
lineNumber: i + 1,
|
|
439
|
+
isRouteMatch
|
|
440
|
+
});
|
|
441
|
+
debugLog("PHASE 0: Found ID match", {
|
|
442
|
+
file: relativePath,
|
|
443
|
+
line: i + 1,
|
|
444
|
+
isRouteMatch
|
|
445
|
+
});
|
|
446
|
+
break; // One match per file is enough
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
// Skip files that can't be read
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} catch {
|
|
455
|
+
// Skip directories that can't be read
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Search all directories
|
|
460
|
+
for (const searchDir of searchDirs) {
|
|
461
|
+
searchDirRecursive(searchDir);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Sort: route matches first
|
|
465
|
+
return matches.sort((a, b) => (b.isRouteMatch ? 1 : 0) - (a.isRouteMatch ? 1 : 0));
|
|
466
|
+
}
|
|
467
|
+
|
|
232
468
|
/**
|
|
233
469
|
* Result of LLM screenshot analysis for smart file discovery
|
|
234
470
|
*/
|
|
@@ -927,12 +1163,73 @@ export async function POST(request: Request) {
|
|
|
927
1163
|
// Generate a unique session ID
|
|
928
1164
|
const newSessionId = randomUUID().slice(0, 8);
|
|
929
1165
|
|
|
930
|
-
//
|
|
931
|
-
// The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
|
|
1166
|
+
// Initialize file discovery variables
|
|
932
1167
|
let smartSearchFiles: { path: string; content: string }[] = [];
|
|
933
1168
|
let recommendedFile: { path: string; reason: string } | null = null;
|
|
934
|
-
|
|
935
|
-
|
|
1169
|
+
let deterministicMatch: { path: string; lineNumber: number } | null = null;
|
|
1170
|
+
|
|
1171
|
+
// ========================================================================
|
|
1172
|
+
// PHASE 0: Deterministic Element ID Search (Cursor-style explicit selection)
|
|
1173
|
+
// If we have an element ID, find it directly - no scoring, no heuristics
|
|
1174
|
+
// This is the most reliable signal, like Cursor knowing which file you're in
|
|
1175
|
+
// ========================================================================
|
|
1176
|
+
if (focusedElements?.some(el => el.elementId)) {
|
|
1177
|
+
const elementWithId = focusedElements.find(el => el.elementId)!;
|
|
1178
|
+
debugLog("PHASE 0: Element has ID, starting deterministic search", {
|
|
1179
|
+
elementId: elementWithId.elementId,
|
|
1180
|
+
route: pageRoute
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
const matches = findFilesByElementId(
|
|
1184
|
+
projectRoot,
|
|
1185
|
+
elementWithId.elementId!,
|
|
1186
|
+
pageRoute || "/",
|
|
1187
|
+
discoverPageFile
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
if (matches.length === 1) {
|
|
1191
|
+
// Single match - 100% confidence
|
|
1192
|
+
deterministicMatch = matches[0];
|
|
1193
|
+
recommendedFile = {
|
|
1194
|
+
path: matches[0].path,
|
|
1195
|
+
reason: `Deterministic ID match: id="${elementWithId.elementId}" (unique in codebase)`
|
|
1196
|
+
};
|
|
1197
|
+
debugLog("PHASE 0 SUCCESS: Single ID match - skipping smart search", deterministicMatch);
|
|
1198
|
+
} else if (matches.length > 1) {
|
|
1199
|
+
// Multiple matches - use route to disambiguate
|
|
1200
|
+
const routeMatch = matches.find(m => m.isRouteMatch);
|
|
1201
|
+
if (routeMatch) {
|
|
1202
|
+
deterministicMatch = routeMatch;
|
|
1203
|
+
recommendedFile = {
|
|
1204
|
+
path: routeMatch.path,
|
|
1205
|
+
reason: `Deterministic ID match: id="${elementWithId.elementId}" (route "${pageRoute}" disambiguated from ${matches.length} files)`
|
|
1206
|
+
};
|
|
1207
|
+
debugLog("PHASE 0 SUCCESS: ID found in multiple files, using route match", {
|
|
1208
|
+
match: deterministicMatch,
|
|
1209
|
+
otherFiles: matches.filter(m => !m.isRouteMatch).map(m => m.path)
|
|
1210
|
+
});
|
|
1211
|
+
} else {
|
|
1212
|
+
debugLog("PHASE 0 FALLBACK: ID found in multiple files, no route match", {
|
|
1213
|
+
elementId: elementWithId.elementId,
|
|
1214
|
+
matchCount: matches.length,
|
|
1215
|
+
files: matches.map(m => m.path),
|
|
1216
|
+
route: pageRoute
|
|
1217
|
+
});
|
|
1218
|
+
// Fall through to smart search
|
|
1219
|
+
}
|
|
1220
|
+
} else {
|
|
1221
|
+
debugLog("PHASE 0 FALLBACK: Element ID not found in codebase", {
|
|
1222
|
+
elementId: elementWithId.elementId
|
|
1223
|
+
});
|
|
1224
|
+
// Fall through to smart search
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// ========================================================================
|
|
1229
|
+
// PHASE 1+2: LLM-driven smart file discovery (only if Phase 0 didn't match)
|
|
1230
|
+
// The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
|
|
1231
|
+
// ========================================================================
|
|
1232
|
+
if (!deterministicMatch && screenshot) {
|
|
936
1233
|
debugLog("Starting Phase 1: LLM screenshot analysis");
|
|
937
1234
|
const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
|
|
938
1235
|
|
|
@@ -1024,8 +1321,12 @@ export async function POST(request: Request) {
|
|
|
1024
1321
|
return bestMatch;
|
|
1025
1322
|
};
|
|
1026
1323
|
|
|
1324
|
+
// ====================================================================
|
|
1325
|
+
// FALLBACK PRIORITY LOGIC (only reached when Phase 0 didn't find a match)
|
|
1326
|
+
// This handles cases: no element ID, dynamic IDs, or ambiguous ID matches
|
|
1327
|
+
// ====================================================================
|
|
1027
1328
|
if (phase2aConfirmed) {
|
|
1028
|
-
// PRIORITY 1: Phase 2a match confirmed by element search
|
|
1329
|
+
// FALLBACK PRIORITY 1: Phase 2a match confirmed by element search
|
|
1029
1330
|
const confirmedPath = phase2aMatches.find(p =>
|
|
1030
1331
|
focusedElementHints.some(h => h.path === p && h.score > 0)
|
|
1031
1332
|
);
|
|
@@ -1033,33 +1334,47 @@ export async function POST(request: Request) {
|
|
|
1033
1334
|
path: confirmedPath!,
|
|
1034
1335
|
reason: `Phase 2a component-name match confirmed by element search`
|
|
1035
1336
|
};
|
|
1036
|
-
debugLog("PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
|
|
1337
|
+
debugLog("FALLBACK PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
|
|
1037
1338
|
} else if (focusedTopScore > smartSearchTopScore * 0.5 && focusedTopScore >= 400) {
|
|
1038
|
-
// PRIORITY 2: Focused element has strong
|
|
1339
|
+
// FALLBACK PRIORITY 2: Focused element has strong score
|
|
1039
1340
|
recommendedFile = {
|
|
1040
1341
|
path: focusedTopPath!,
|
|
1041
1342
|
reason: `Focused element match (score: ${focusedTopScore}, exceeds threshold)`
|
|
1042
1343
|
};
|
|
1043
|
-
debugLog("PRIORITY 2: Strong focused element match", recommendedFile);
|
|
1344
|
+
debugLog("FALLBACK PRIORITY 2: Strong focused element match", recommendedFile);
|
|
1044
1345
|
} else if (phase2aMatches.length > 0) {
|
|
1045
|
-
// PRIORITY
|
|
1346
|
+
// FALLBACK PRIORITY 3: Phase 2a match WITHOUT focused element
|
|
1046
1347
|
// Use prompt-aware selection to pick the best match
|
|
1047
1348
|
const bestMatch = findBestPhase2aMatch()!;
|
|
1048
1349
|
recommendedFile = {
|
|
1049
1350
|
path: bestMatch,
|
|
1050
1351
|
reason: `Phase 2a component-name match (prompt-aware selection from ${phase2aMatches.length} candidates)`
|
|
1051
1352
|
};
|
|
1052
|
-
debugLog("PRIORITY
|
|
1353
|
+
debugLog("FALLBACK PRIORITY 3: Phase 2a match (no focused element)", {
|
|
1053
1354
|
selectedPath: bestMatch,
|
|
1054
1355
|
allCandidates: phase2aMatches
|
|
1055
1356
|
});
|
|
1056
|
-
} else
|
|
1057
|
-
// PRIORITY
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1357
|
+
} else {
|
|
1358
|
+
// FALLBACK PRIORITY 4: Use the page file from the current route
|
|
1359
|
+
const routePageFile = discoverPageFile(pageRoute || "/", projectRoot);
|
|
1360
|
+
|
|
1361
|
+
if (routePageFile && fs.existsSync(path.join(projectRoot, routePageFile))) {
|
|
1362
|
+
recommendedFile = {
|
|
1363
|
+
path: routePageFile,
|
|
1364
|
+
reason: `Page file from route "${pageRoute || "/"}"`
|
|
1365
|
+
};
|
|
1366
|
+
debugLog("FALLBACK PRIORITY 4: Using page file from route", {
|
|
1367
|
+
route: pageRoute || "/",
|
|
1368
|
+
pageFile: routePageFile
|
|
1369
|
+
});
|
|
1370
|
+
} else if (smartSearchTopPath) {
|
|
1371
|
+
// FALLBACK PRIORITY 5: Smart search top result - last resort
|
|
1372
|
+
recommendedFile = {
|
|
1373
|
+
path: smartSearchTopPath,
|
|
1374
|
+
reason: `Smart search top result (score: ${smartSearchTopScore})`
|
|
1375
|
+
};
|
|
1376
|
+
debugLog("FALLBACK PRIORITY 5: Using smart search top result", recommendedFile);
|
|
1377
|
+
}
|
|
1063
1378
|
}
|
|
1064
1379
|
|
|
1065
1380
|
debugLog("Phase 1+2 complete", {
|
|
@@ -1148,12 +1463,21 @@ User Request: "${userPrompt}"
|
|
|
1148
1463
|
if (recommendedFileContent) {
|
|
1149
1464
|
const content = recommendedFileContent.content;
|
|
1150
1465
|
|
|
1151
|
-
// Search for element
|
|
1152
|
-
|
|
1153
|
-
|
|
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) {
|
|
1154
1470
|
for (const el of focusedElements) {
|
|
1155
|
-
|
|
1156
|
-
if (
|
|
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
|
+
}
|
|
1157
1481
|
}
|
|
1158
1482
|
}
|
|
1159
1483
|
|
|
@@ -1168,22 +1492,36 @@ User Request: "${userPrompt}"
|
|
|
1168
1492
|
textContent += `\n`;
|
|
1169
1493
|
}
|
|
1170
1494
|
|
|
1171
|
-
// Add precise targeting
|
|
1172
|
-
if (
|
|
1495
|
+
// Add precise targeting with line number and snippet
|
|
1496
|
+
if (elementLocation) {
|
|
1173
1497
|
textContent += `
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
→
|
|
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.
|
|
1178
1511
|
|
|
1179
1512
|
`;
|
|
1180
|
-
debugLog("Found element ID in file", {
|
|
1181
|
-
matchedId: idMatch.matchedId,
|
|
1182
|
-
lineNumber: idMatch.lineNumber,
|
|
1183
|
-
file: recommendedFileContent.path,
|
|
1184
|
-
});
|
|
1185
1513
|
} else {
|
|
1186
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
|
+
});
|
|
1187
1525
|
}
|
|
1188
1526
|
}
|
|
1189
1527
|
|
|
@@ -225,6 +225,242 @@ 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
|
+
|
|
361
|
+
/**
|
|
362
|
+
* PHASE 0: Deterministic Element ID Search (Cursor-style)
|
|
363
|
+
* Grep entire codebase for the element ID. If found in multiple files,
|
|
364
|
+
* use the current route to disambiguate (prioritize page file for route).
|
|
365
|
+
*
|
|
366
|
+
* This is the most reliable signal - like Cursor knowing which file you're in.
|
|
367
|
+
* Element IDs should be unique, so if we find one, that's THE file.
|
|
368
|
+
*/
|
|
369
|
+
function findFilesByElementId(
|
|
370
|
+
projectRoot: string,
|
|
371
|
+
elementId: string,
|
|
372
|
+
currentRoute: string,
|
|
373
|
+
discoverPageFileFn: (route: string, projectRoot: string) => string | null
|
|
374
|
+
): { path: string; lineNumber: number; isRouteMatch: boolean }[] {
|
|
375
|
+
const pattern = new RegExp(`id=["'\`]${elementId}["'\`]`);
|
|
376
|
+
const matches: { path: string; lineNumber: number; isRouteMatch: boolean }[] = [];
|
|
377
|
+
|
|
378
|
+
// Determine expected page file from route
|
|
379
|
+
const routePageFile = discoverPageFileFn(currentRoute, projectRoot);
|
|
380
|
+
|
|
381
|
+
// Search all common project directories that exist
|
|
382
|
+
// This supports: src/app, app/, pages/, components/, lib/ structures
|
|
383
|
+
const commonDirs = ['src', 'app', 'pages', 'components', 'lib'];
|
|
384
|
+
const searchDirs = commonDirs
|
|
385
|
+
.map(dir => path.join(projectRoot, dir))
|
|
386
|
+
.filter(dir => {
|
|
387
|
+
try {
|
|
388
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// If no standard dirs found, search project root (excluding node_modules)
|
|
395
|
+
if (searchDirs.length === 0) {
|
|
396
|
+
searchDirs.push(projectRoot);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
debugLog("PHASE 0: Searching for element ID", {
|
|
400
|
+
elementId,
|
|
401
|
+
currentRoute,
|
|
402
|
+
routePageFile,
|
|
403
|
+
searchDirs: searchDirs.map(d => d.replace(projectRoot + '/', ''))
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
function searchDirRecursive(dir: string): void {
|
|
407
|
+
try {
|
|
408
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
409
|
+
for (const entry of entries) {
|
|
410
|
+
const fullPath = path.join(dir, entry.name);
|
|
411
|
+
|
|
412
|
+
// Skip node_modules, hidden directories, and build outputs
|
|
413
|
+
if (entry.isDirectory()) {
|
|
414
|
+
if (
|
|
415
|
+
entry.name.includes('node_modules') ||
|
|
416
|
+
entry.name.startsWith('.') ||
|
|
417
|
+
entry.name === 'dist' ||
|
|
418
|
+
entry.name === 'build' ||
|
|
419
|
+
entry.name === '.next'
|
|
420
|
+
) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
searchDirRecursive(fullPath);
|
|
424
|
+
} else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
425
|
+
try {
|
|
426
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
427
|
+
const lines = content.split('\n');
|
|
428
|
+
for (let i = 0; i < lines.length; i++) {
|
|
429
|
+
if (pattern.test(lines[i])) {
|
|
430
|
+
const relativePath = fullPath.replace(projectRoot + '/', '');
|
|
431
|
+
const isRouteMatch = relativePath === routePageFile;
|
|
432
|
+
matches.push({
|
|
433
|
+
path: relativePath,
|
|
434
|
+
lineNumber: i + 1,
|
|
435
|
+
isRouteMatch
|
|
436
|
+
});
|
|
437
|
+
debugLog("PHASE 0: Found ID match", {
|
|
438
|
+
file: relativePath,
|
|
439
|
+
line: i + 1,
|
|
440
|
+
isRouteMatch
|
|
441
|
+
});
|
|
442
|
+
break; // One match per file is enough
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
// Skip files that can't be read
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
// Skip directories that can't be read
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Search all directories
|
|
456
|
+
for (const searchDir of searchDirs) {
|
|
457
|
+
searchDirRecursive(searchDir);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Sort: route matches first
|
|
461
|
+
return matches.sort((a, b) => (b.isRouteMatch ? 1 : 0) - (a.isRouteMatch ? 1 : 0));
|
|
462
|
+
}
|
|
463
|
+
|
|
228
464
|
/**
|
|
229
465
|
* Result of LLM screenshot analysis for smart file discovery
|
|
230
466
|
*/
|
|
@@ -896,12 +1132,73 @@ export async function POST(request: Request) {
|
|
|
896
1132
|
);
|
|
897
1133
|
}
|
|
898
1134
|
|
|
899
|
-
//
|
|
900
|
-
// The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
|
|
1135
|
+
// Initialize file discovery variables
|
|
901
1136
|
let smartSearchFiles: { path: string; content: string }[] = [];
|
|
902
1137
|
let recommendedFile: { path: string; reason: string } | null = null;
|
|
903
|
-
|
|
904
|
-
|
|
1138
|
+
let deterministicMatch: { path: string; lineNumber: number } | null = null;
|
|
1139
|
+
|
|
1140
|
+
// ========================================================================
|
|
1141
|
+
// PHASE 0: Deterministic Element ID Search (Cursor-style explicit selection)
|
|
1142
|
+
// If we have an element ID, find it directly - no scoring, no heuristics
|
|
1143
|
+
// This is the most reliable signal, like Cursor knowing which file you're in
|
|
1144
|
+
// ========================================================================
|
|
1145
|
+
if (focusedElements?.some(el => el.elementId)) {
|
|
1146
|
+
const elementWithId = focusedElements.find(el => el.elementId)!;
|
|
1147
|
+
debugLog("PHASE 0: Element has ID, starting deterministic search", {
|
|
1148
|
+
elementId: elementWithId.elementId,
|
|
1149
|
+
route: pageRoute
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
const matches = findFilesByElementId(
|
|
1153
|
+
projectRoot,
|
|
1154
|
+
elementWithId.elementId!,
|
|
1155
|
+
pageRoute || "/",
|
|
1156
|
+
discoverPageFile
|
|
1157
|
+
);
|
|
1158
|
+
|
|
1159
|
+
if (matches.length === 1) {
|
|
1160
|
+
// Single match - 100% confidence
|
|
1161
|
+
deterministicMatch = matches[0];
|
|
1162
|
+
recommendedFile = {
|
|
1163
|
+
path: matches[0].path,
|
|
1164
|
+
reason: `Deterministic ID match: id="${elementWithId.elementId}" (unique in codebase)`
|
|
1165
|
+
};
|
|
1166
|
+
debugLog("PHASE 0 SUCCESS: Single ID match - skipping smart search", deterministicMatch);
|
|
1167
|
+
} else if (matches.length > 1) {
|
|
1168
|
+
// Multiple matches - use route to disambiguate
|
|
1169
|
+
const routeMatch = matches.find(m => m.isRouteMatch);
|
|
1170
|
+
if (routeMatch) {
|
|
1171
|
+
deterministicMatch = routeMatch;
|
|
1172
|
+
recommendedFile = {
|
|
1173
|
+
path: routeMatch.path,
|
|
1174
|
+
reason: `Deterministic ID match: id="${elementWithId.elementId}" (route "${pageRoute}" disambiguated from ${matches.length} files)`
|
|
1175
|
+
};
|
|
1176
|
+
debugLog("PHASE 0 SUCCESS: ID found in multiple files, using route match", {
|
|
1177
|
+
match: deterministicMatch,
|
|
1178
|
+
otherFiles: matches.filter(m => !m.isRouteMatch).map(m => m.path)
|
|
1179
|
+
});
|
|
1180
|
+
} else {
|
|
1181
|
+
debugLog("PHASE 0 FALLBACK: ID found in multiple files, no route match", {
|
|
1182
|
+
elementId: elementWithId.elementId,
|
|
1183
|
+
matchCount: matches.length,
|
|
1184
|
+
files: matches.map(m => m.path),
|
|
1185
|
+
route: pageRoute
|
|
1186
|
+
});
|
|
1187
|
+
// Fall through to smart search
|
|
1188
|
+
}
|
|
1189
|
+
} else {
|
|
1190
|
+
debugLog("PHASE 0 FALLBACK: Element ID not found in codebase", {
|
|
1191
|
+
elementId: elementWithId.elementId
|
|
1192
|
+
});
|
|
1193
|
+
// Fall through to smart search
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// ========================================================================
|
|
1198
|
+
// PHASE 1+2: LLM-driven smart file discovery (only if Phase 0 didn't match)
|
|
1199
|
+
// The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
|
|
1200
|
+
// ========================================================================
|
|
1201
|
+
if (!deterministicMatch && screenshot) {
|
|
905
1202
|
debugLog("Starting Phase 1: LLM screenshot analysis");
|
|
906
1203
|
const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
|
|
907
1204
|
|
|
@@ -993,8 +1290,12 @@ export async function POST(request: Request) {
|
|
|
993
1290
|
return bestMatch;
|
|
994
1291
|
};
|
|
995
1292
|
|
|
1293
|
+
// ====================================================================
|
|
1294
|
+
// FALLBACK PRIORITY LOGIC (only reached when Phase 0 didn't find a match)
|
|
1295
|
+
// This handles cases: no element ID, dynamic IDs, or ambiguous ID matches
|
|
1296
|
+
// ====================================================================
|
|
996
1297
|
if (phase2aConfirmed) {
|
|
997
|
-
// PRIORITY 1: Phase 2a match confirmed by element search
|
|
1298
|
+
// FALLBACK PRIORITY 1: Phase 2a match confirmed by element search
|
|
998
1299
|
const confirmedPath = phase2aMatches.find(p =>
|
|
999
1300
|
focusedElementHints.some(h => h.path === p && h.score > 0)
|
|
1000
1301
|
);
|
|
@@ -1002,33 +1303,47 @@ export async function POST(request: Request) {
|
|
|
1002
1303
|
path: confirmedPath!,
|
|
1003
1304
|
reason: `Phase 2a component-name match confirmed by element search`
|
|
1004
1305
|
};
|
|
1005
|
-
debugLog("PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
|
|
1306
|
+
debugLog("FALLBACK PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
|
|
1006
1307
|
} else if (focusedTopScore > smartSearchTopScore * 0.5 && focusedTopScore >= 400) {
|
|
1007
|
-
// PRIORITY 2: Focused element has strong
|
|
1308
|
+
// FALLBACK PRIORITY 2: Focused element has strong score
|
|
1008
1309
|
recommendedFile = {
|
|
1009
1310
|
path: focusedTopPath!,
|
|
1010
1311
|
reason: `Focused element match (score: ${focusedTopScore}, exceeds threshold)`
|
|
1011
1312
|
};
|
|
1012
|
-
debugLog("PRIORITY 2: Strong focused element match", recommendedFile);
|
|
1313
|
+
debugLog("FALLBACK PRIORITY 2: Strong focused element match", recommendedFile);
|
|
1013
1314
|
} else if (phase2aMatches.length > 0) {
|
|
1014
|
-
// PRIORITY
|
|
1315
|
+
// FALLBACK PRIORITY 3: Phase 2a match WITHOUT focused element
|
|
1015
1316
|
// Use prompt-aware selection to pick the best match
|
|
1016
1317
|
const bestMatch = findBestPhase2aMatch()!;
|
|
1017
1318
|
recommendedFile = {
|
|
1018
1319
|
path: bestMatch,
|
|
1019
1320
|
reason: `Phase 2a component-name match (prompt-aware selection from ${phase2aMatches.length} candidates)`
|
|
1020
1321
|
};
|
|
1021
|
-
debugLog("PRIORITY
|
|
1322
|
+
debugLog("FALLBACK PRIORITY 3: Phase 2a match (no focused element)", {
|
|
1022
1323
|
selectedPath: bestMatch,
|
|
1023
1324
|
allCandidates: phase2aMatches
|
|
1024
1325
|
});
|
|
1025
|
-
} else
|
|
1026
|
-
// PRIORITY
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1326
|
+
} else {
|
|
1327
|
+
// FALLBACK PRIORITY 4: Use the page file from the current route
|
|
1328
|
+
const routePageFile = discoverPageFile(pageRoute || "/", projectRoot);
|
|
1329
|
+
|
|
1330
|
+
if (routePageFile && fs.existsSync(path.join(projectRoot, routePageFile))) {
|
|
1331
|
+
recommendedFile = {
|
|
1332
|
+
path: routePageFile,
|
|
1333
|
+
reason: `Page file from route "${pageRoute || "/"}"`
|
|
1334
|
+
};
|
|
1335
|
+
debugLog("FALLBACK PRIORITY 4: Using page file from route", {
|
|
1336
|
+
route: pageRoute || "/",
|
|
1337
|
+
pageFile: routePageFile
|
|
1338
|
+
});
|
|
1339
|
+
} else if (smartSearchTopPath) {
|
|
1340
|
+
// FALLBACK PRIORITY 5: Smart search top result - last resort
|
|
1341
|
+
recommendedFile = {
|
|
1342
|
+
path: smartSearchTopPath,
|
|
1343
|
+
reason: `Smart search top result (score: ${smartSearchTopScore})`
|
|
1344
|
+
};
|
|
1345
|
+
debugLog("FALLBACK PRIORITY 5: Using smart search top result", recommendedFile);
|
|
1346
|
+
}
|
|
1032
1347
|
}
|
|
1033
1348
|
|
|
1034
1349
|
debugLog("Phase 1+2 complete", {
|
|
@@ -1117,12 +1432,21 @@ User Request: "${userPrompt}"
|
|
|
1117
1432
|
if (recommendedFileContent) {
|
|
1118
1433
|
const content = recommendedFileContent.content;
|
|
1119
1434
|
|
|
1120
|
-
// Search for element
|
|
1121
|
-
|
|
1122
|
-
|
|
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) {
|
|
1123
1439
|
for (const el of focusedElements) {
|
|
1124
|
-
|
|
1125
|
-
if (
|
|
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
|
+
}
|
|
1126
1450
|
}
|
|
1127
1451
|
}
|
|
1128
1452
|
|
|
@@ -1137,22 +1461,36 @@ User Request: "${userPrompt}"
|
|
|
1137
1461
|
textContent += `\n`;
|
|
1138
1462
|
}
|
|
1139
1463
|
|
|
1140
|
-
// Add precise targeting
|
|
1141
|
-
if (
|
|
1464
|
+
// Add precise targeting with line number and snippet
|
|
1465
|
+
if (elementLocation) {
|
|
1142
1466
|
textContent += `
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
→
|
|
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.
|
|
1147
1480
|
|
|
1148
1481
|
`;
|
|
1149
|
-
debugLog("Found element ID in file", {
|
|
1150
|
-
matchedId: idMatch.matchedId,
|
|
1151
|
-
lineNumber: idMatch.lineNumber,
|
|
1152
|
-
file: recommendedFileContent.path,
|
|
1153
|
-
});
|
|
1154
1482
|
} else {
|
|
1155
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
|
+
});
|
|
1156
1494
|
}
|
|
1157
1495
|
}
|
|
1158
1496
|
|
|
@@ -1215,10 +1215,11 @@ export function SonanceDevTools() {
|
|
|
1215
1215
|
console.warn("[Apply-First] Failed to persist session:", e);
|
|
1216
1216
|
}
|
|
1217
1217
|
|
|
1218
|
-
//
|
|
1218
|
+
// Force page refresh to ensure changes are visible
|
|
1219
|
+
// Session is already persisted to localStorage, so it survives refresh
|
|
1219
1220
|
setTimeout(() => {
|
|
1220
|
-
|
|
1221
|
-
},
|
|
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.
|
|
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",
|