sonance-brand-mcp 1.3.58 → 1.3.60

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.
@@ -171,6 +171,73 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
171
171
  }
172
172
  }
173
173
 
174
+ /**
175
+ * Search candidate files for JSX code matching the focused element
176
+ * This helps identify which file actually contains the element the user clicked on
177
+ */
178
+ function findFilesContainingElement(
179
+ focusedElements: VisionFocusedElement[] | undefined,
180
+ candidateFiles: { path: string; content: string }[]
181
+ ): { path: string; score: number; matches: string[] }[] {
182
+ if (!focusedElements || focusedElements.length === 0) {
183
+ return [];
184
+ }
185
+
186
+ const results: { path: string; score: number; matches: string[] }[] = [];
187
+
188
+ for (const file of candidateFiles) {
189
+ let score = 0;
190
+ const matches: string[] = [];
191
+ const content = file.content.toLowerCase();
192
+
193
+ for (const el of focusedElements) {
194
+ // Extract element type from the name (e.g., "button #123" -> "button")
195
+ const elementType = el.name.split(/[\s#]/)[0].toLowerCase();
196
+ const componentType = el.type.toLowerCase();
197
+
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}>`);
203
+ }
204
+
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}>`);
210
+ }
211
+
212
+ // Check for component imports
213
+ if (content.includes(`import`) && content.includes(componentType)) {
214
+ score += 5;
215
+ }
216
+
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;
221
+ }
222
+ }
223
+
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}`);
229
+ }
230
+ }
231
+
232
+ if (score > 0) {
233
+ results.push({ path: file.path, score, matches });
234
+ }
235
+ }
236
+
237
+ // Sort by score descending
238
+ return results.sort((a, b) => b.score - a.score);
239
+ }
240
+
174
241
  /**
175
242
  * Phase 3: Ask LLM to select the best file from actual file list
176
243
  * This replaces guessing - the LLM sees the real filenames and picks one
@@ -179,7 +246,8 @@ async function selectBestFileFromList(
179
246
  screenshot: string,
180
247
  userPrompt: string,
181
248
  candidateFiles: string[],
182
- apiKey: string
249
+ apiKey: string,
250
+ focusedElementHints?: { path: string; score: number; matches: string[] }[]
183
251
  ): Promise<string | null> {
184
252
  if (candidateFiles.length === 0) return null;
185
253
 
@@ -203,7 +271,12 @@ async function selectBestFileFromList(
203
271
 
204
272
  Here are the actual component files found in this codebase:
205
273
  ${candidateFiles.map((f, i) => `${i + 1}. ${f}`).join('\n')}
274
+ ${focusedElementHints && focusedElementHints.length > 0 ? `
275
+ FOCUSED ELEMENT ANALYSIS - Files that contain the element the user clicked on:
276
+ ${focusedElementHints.slice(0, 3).map(h => `- ${h.path} (confidence: ${h.score}, found: ${h.matches.join(", ")})`).join('\n')}
206
277
 
278
+ IMPORTANT: Prefer files from the FOCUSED ELEMENT ANALYSIS above, as they contain the actual element the user wants to modify.
279
+ ` : ''}
207
280
  Looking at the screenshot, which file MOST LIKELY contains the UI elements the user wants to modify?
208
281
 
209
282
  IMPORTANT: Return ONLY the exact file path from the list above (e.g., "components/ProcessCatalogue/ProcessDetailPanel.tsx").
@@ -438,41 +511,81 @@ function searchFilesSmart(
438
511
  return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
439
512
  }
440
513
 
441
- const VISION_SYSTEM_PROMPT = `You are a code editor. Make ONLY the change the user requested.
514
+ const VISION_SYSTEM_PROMPT = `You are a code editor like Cursor. Before making changes, REASON about what needs to be done.
515
+
516
+ ═══════════════════════════════════════════════════════════════════════════════
517
+ STEP 1: ANALYZE (Think like Cursor)
518
+ ═══════════════════════════════════════════════════════════════════════════════
519
+
520
+ Before writing any patches, analyze the screenshot and answer:
521
+
522
+ 1. CURRENT STATE: What do I see in the screenshot?
523
+ - Is the element visible/readable right now?
524
+ - What colors/styles are currently applied?
525
+
526
+ 2. PROBLEM IDENTIFICATION: What's actually wrong?
527
+ - Contrast issue? (text blending with background)
528
+ - Hidden element? (not showing at all)
529
+ - Hover state issue? (only visible on interaction)
530
+ - Layout issue? (wrong size/position)
531
+ - Or is it already correct?
532
+
533
+ 3. FIX STRATEGY: What should I change?
534
+ - DEFAULT state only (element needs to look different normally)
535
+ - HOVER state only (element needs different hover effect)
536
+ - BOTH states (element needs overall visibility improvement)
537
+ - ASK for clarification (request is too vague)
538
+
539
+ ═══════════════════════════════════════════════════════════════════════════════
540
+ STEP 2: GENERATE PATCHES (Only after reasoning)
541
+ ═══════════════════════════════════════════════════════════════════════════════
542
+
543
+ CRITICAL - FILE SELECTION:
544
+ - You MUST modify the file marked "TARGET COMPONENT" - this is the file you have FULL visibility into
545
+ - Do NOT try to modify other files listed in context - you only see their imports/exports, not full content
546
+ - If the TARGET COMPONENT is not the right file, return clarification_needed
547
+
548
+ COPY EXACTLY from the TARGET COMPONENT section:
549
+ - Your "search" string must match the file CHARACTER-FOR-CHARACTER
550
+ - Include exact indentation and at least 3 lines of context
551
+ - If the element looks FINE in the screenshot, say so
442
552
 
443
553
  RULES:
444
- 1. Make the SMALLEST possible change to accomplish the request
445
- 2. Do NOT refactor, rebrand, or "improve" anything else
446
- 3. Do NOT change import statements unless explicitly required
447
- 4. Do NOT change component libraries (e.g., heroui to shadcn)
448
- 5. If fixing a color/visibility issue, change ONLY that element's classes
449
- 6. Your patch should typically be 1-5 lines, not 50+
450
- 7. NEVER invent or guess code - your "search" string MUST match the file EXACTLY
451
- 8. NEVER change data mappings (icon names, keys, enum values) unless user explicitly provides the new values
452
- 9. If you don't know what values exist in a database or data source, DO NOT guess - ask for clarification
453
-
454
- CRITICAL - DATA INTEGRITY:
455
- - If code references database values (like icon_name, type, status), DO NOT change the mapping keys
456
- - You cannot see database content - only change STRUCTURE, not DATA MAPPINGS
457
- - Example: If you see iconMap["EyeOff"] = SomeIcon, do NOT change "EyeOff" to something else
458
- - If user wants different icons, they must tell you the EXACT icon names they want
459
-
460
- PATCH FORMAT:
461
- Return ONLY raw JSON (no markdown, no preamble):
554
+ 1. Make the SMALLEST possible change
555
+ 2. Do NOT refactor or "improve" other code
556
+ 3. NEVER invent code - COPY exact code from file
557
+ 4. NEVER change data mappings unless user provides new values
558
+ 5. If visibility issue: prefer explicit colors (text-white, text-black) over semantic tokens
559
+
560
+ ═══════════════════════════════════════════════════════════════════════════════
561
+ RESPONSE FORMAT
562
+ ═══════════════════════════════════════════════════════════════════════════════
563
+
564
+ Return ONLY raw JSON:
462
565
  {
463
- "reasoning": "brief explanation",
566
+ "analysis": {
567
+ "currentState": "what I see in the screenshot",
568
+ "problem": "the actual issue (or 'none - element appears correct')",
569
+ "fixStrategy": "default|hover|both|clarification_needed"
570
+ },
571
+ "reasoning": "my diagnosis and approach",
464
572
  "modifications": [{
465
573
  "filePath": "path/to/file.tsx",
466
574
  "patches": [{
467
- "search": "exact original code (copy from provided file)",
468
- "replace": "minimal change",
469
- "explanation": "what this does"
575
+ "search": "EXACT copy from file",
576
+ "replace": "changed code",
577
+ "explanation": "what this changes and why"
470
578
  }]
471
579
  }],
472
- "explanation": "summary"
580
+ "explanation": "summary of what was changed"
473
581
  }
474
582
 
475
- If you cannot find the exact code to modify, OR if you would need to guess data values, return empty modifications array with explanation.`;
583
+ If the element looks correct, return:
584
+ {
585
+ "analysis": { "currentState": "...", "problem": "none", "fixStrategy": "clarification_needed" },
586
+ "modifications": [],
587
+ "explanation": "The element appears visible/correct in the screenshot. Please specify what needs to change."
588
+ }`;
476
589
 
477
590
  export async function POST(request: Request) {
478
591
  // Only allow in development
@@ -604,6 +717,22 @@ export async function POST(request: Request) {
604
717
  const searchResults = searchFilesSmart(analysis, projectRoot, 10);
605
718
  smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
606
719
 
720
+ // PHASE 2.5: Find which files contain the focused element
721
+ const focusedElementHints = findFilesContainingElement(
722
+ focusedElements,
723
+ searchResults.map(r => ({ path: r.path, content: r.content }))
724
+ );
725
+
726
+ if (focusedElementHints.length > 0) {
727
+ debugLog("Focused element search results", {
728
+ topMatches: focusedElementHints.slice(0, 3).map(h => ({
729
+ path: h.path,
730
+ score: h.score,
731
+ matches: h.matches
732
+ }))
733
+ });
734
+ }
735
+
607
736
  // PHASE 3: Ask LLM to pick the best file from actual file list
608
737
  if (searchResults.length > 0) {
609
738
  const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
@@ -611,7 +740,8 @@ export async function POST(request: Request) {
611
740
  screenshot,
612
741
  userPrompt,
613
742
  candidateFiles,
614
- apiKey
743
+ apiKey,
744
+ focusedElementHints
615
745
  );
616
746
 
617
747
  if (selectedFile) {
@@ -778,30 +908,59 @@ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
778
908
 
779
909
  // ========== THEME DISCOVERY (REFERENCE ONLY) ==========
780
910
  // Dynamically discover theme tokens from the target codebase
781
- // This is marked as REFERENCE ONLY so the LLM doesn't use it to justify extra changes
782
911
  const discoveredTheme = await discoverTheme(projectRoot);
783
912
  const themeContext = formatThemeForPrompt(discoveredTheme);
784
913
 
914
+ // Check if this is a visibility-related request
915
+ const isVisibilityRequest = /visible|can't see|cant see|not visible|hidden|color|contrast/i.test(userPrompt);
916
+
785
917
  if (discoveredTheme.discoveredFiles.length > 0) {
786
- textContent += `
918
+ // Only include detailed color guidance for visibility requests
919
+ if (isVisibilityRequest) {
920
+ // Extract available text color classes from discovered theme
921
+ const textColorClasses = Object.keys(discoveredTheme.cssVariables)
922
+ .filter(key => key.includes('foreground') || key.includes('text'))
923
+ .map(key => `text-${key.replace('--', '').replace(/-/g, '-')}`)
924
+ .slice(0, 10);
925
+
926
+ textContent += `
787
927
  ═══════════════════════════════════════════════════════════════════════════════
788
- REFERENCE ONLY (do not use this to justify additional changes)
928
+ COLOR FIX GUIDANCE (for visibility issues only)
789
929
  ═══════════════════════════════════════════════════════════════════════════════
790
930
 
791
- If you need to pick a color for a VISIBILITY fix, these are safe choices:
792
- - bg-accent text-white (cyan button with white text)
793
- - bg-primary text-white (charcoal button with white text)
794
- - bg-success text-white (green button with white text)
795
- - bg-destructive text-white (red button with white text)
931
+ AVAILABLE TEXT COLORS (use these for contrast fixes):
932
+ - text-white (always visible on dark backgrounds)
933
+ - text-black (always visible on light backgrounds)
934
+ - text-foreground (theme default)
935
+ - text-primary-foreground (for bg-primary)
936
+ - text-accent-foreground (for bg-accent - CHECK IF THIS HAS GOOD CONTRAST)
937
+ - text-destructive-foreground (for bg-destructive)
938
+
939
+ SAFE BUTTON PATTERNS:
940
+ - bg-accent text-white (cyan button, guaranteed visible)
941
+ - bg-primary text-white (charcoal button, guaranteed visible)
942
+ - bg-destructive text-white (red button, guaranteed visible)
943
+ - bg-muted text-foreground (gray button, guaranteed visible)
796
944
 
797
- But ONLY use these if the user is asking for a color/visibility change.
798
- Do NOT rebrand or change other elements to match.
945
+ IMPORTANT: If current code has text-accent-foreground or similar semantic colors
946
+ that result in poor contrast, REPLACE with text-white or text-black.
799
947
 
800
948
  `;
949
+ } else {
950
+ // For non-visibility requests, minimal reference
951
+ textContent += `
952
+ ═══════════════════════════════════════════════════════════════════════════════
953
+ REFERENCE ONLY (do not use this to justify additional changes)
954
+ ═══════════════════════════════════════════════════════════════════════════════
955
+ Theme discovered from: ${discoveredTheme.discoveredFiles.join(', ')}
956
+ `;
957
+ }
958
+
801
959
  debugLog("Theme discovery complete", {
802
960
  filesFound: discoveredTheme.discoveredFiles,
803
961
  cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
804
962
  tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
963
+ isVisibilityRequest,
805
964
  });
806
965
  }
807
966
 
@@ -946,6 +1105,11 @@ This is better than generating patches with made-up code.`,
946
1105
 
947
1106
  // Parse AI response - now expecting patches instead of full file content
948
1107
  let aiResponse: {
1108
+ analysis?: {
1109
+ currentState?: string;
1110
+ problem?: string;
1111
+ fixStrategy?: string;
1112
+ };
949
1113
  reasoning?: string;
950
1114
  modifications: Array<{
951
1115
  filePath: string;
@@ -993,9 +1157,31 @@ This is better than generating patches with made-up code.`,
993
1157
  );
994
1158
  }
995
1159
 
1160
+ // Log the LLM's analysis/reasoning (like Cursor showing its thought process)
1161
+ if (aiResponse.analysis) {
1162
+ debugLog("LLM Analysis (Step 1)", {
1163
+ currentState: aiResponse.analysis.currentState,
1164
+ problem: aiResponse.analysis.problem,
1165
+ fixStrategy: aiResponse.analysis.fixStrategy,
1166
+ });
1167
+ }
1168
+
996
1169
  finalExplanation = aiResponse.explanation;
997
1170
  finalReasoning = aiResponse.reasoning;
998
1171
 
1172
+ // If LLM says clarification is needed, return that to the user
1173
+ if (aiResponse.analysis?.fixStrategy === "clarification_needed" &&
1174
+ (!aiResponse.modifications || aiResponse.modifications.length === 0)) {
1175
+ return NextResponse.json({
1176
+ success: true,
1177
+ sessionId: newSessionId,
1178
+ needsClarification: true,
1179
+ analysis: aiResponse.analysis,
1180
+ explanation: aiResponse.explanation || "The element appears correct. Please specify what needs to change.",
1181
+ modifications: [],
1182
+ });
1183
+ }
1184
+
999
1185
  if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
1000
1186
  return NextResponse.json({
1001
1187
  success: true,
@@ -1034,6 +1220,19 @@ This is better than generating patches with made-up code.`,
1034
1220
  continue;
1035
1221
  }
1036
1222
 
1223
+ // CRITICAL: Warn if LLM is trying to modify a file OTHER than the TARGET COMPONENT
1224
+ // This usually means the LLM is trying to modify a file it doesn't have full visibility into
1225
+ const targetComponentPath = recommendedFileContent?.path;
1226
+ if (targetComponentPath && mod.filePath !== targetComponentPath) {
1227
+ debugLog("WARNING: LLM trying to modify non-target file", {
1228
+ targetComponent: targetComponentPath,
1229
+ attemptedFile: mod.filePath,
1230
+ warning: "LLM may be hallucinating code since it only has full content of the TARGET COMPONENT"
1231
+ });
1232
+ console.warn(`[Apply-First] ⚠️ LLM is modifying ${mod.filePath} but TARGET COMPONENT is ${targetComponentPath}`);
1233
+ console.warn(`[Apply-First] ⚠️ This may cause hallucination since LLM only has full visibility into the TARGET COMPONENT`);
1234
+ }
1235
+
1037
1236
  const fullPath = path.join(projectRoot, mod.filePath);
1038
1237
  let originalContent = "";
1039
1238
  if (fs.existsSync(fullPath)) {
@@ -1109,6 +1308,34 @@ This is better than generating patches with made-up code.`,
1109
1308
  modifiedContent = patchResult.modifiedContent;
1110
1309
  console.log(`[Apply-First] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
1111
1310
  }
1311
+
1312
+ // SYNTAX VALIDATION: Check for common JSX/HTML tag mismatches
1313
+ // This catches cases where the LLM changes opening tags but not closing tags
1314
+ if (mod.filePath.endsWith('.tsx') || mod.filePath.endsWith('.jsx')) {
1315
+ const openDivs = (modifiedContent.match(/<div[\s>]/g) || []).length;
1316
+ const closeDivs = (modifiedContent.match(/<\/div>/g) || []).length;
1317
+ const openSpans = (modifiedContent.match(/<span[\s>]/g) || []).length;
1318
+ const closeSpans = (modifiedContent.match(/<\/span>/g) || []).length;
1319
+
1320
+ if (openDivs !== closeDivs || openSpans !== closeSpans) {
1321
+ debugLog("SYNTAX WARNING: Tag mismatch detected", {
1322
+ filePath: mod.filePath,
1323
+ divs: { open: openDivs, close: closeDivs },
1324
+ spans: { open: openSpans, close: closeSpans },
1325
+ });
1326
+ console.warn(`[Apply-First] ⚠️ SYNTAX WARNING: Tag mismatch in ${mod.filePath}`);
1327
+ console.warn(`[Apply-First] divs: ${openDivs} open, ${closeDivs} close`);
1328
+ console.warn(`[Apply-First] spans: ${openSpans} open, ${closeSpans} close`);
1329
+
1330
+ // If there's a significant mismatch, reject the change
1331
+ const divDiff = Math.abs(openDivs - closeDivs);
1332
+ const spanDiff = Math.abs(openSpans - closeSpans);
1333
+ if (divDiff > 0 || spanDiff > 0) {
1334
+ patchErrors.push(`${mod.filePath}: LLM introduced syntax error - tag mismatch detected (${divDiff} div, ${spanDiff} span). Change rejected.`);
1335
+ continue;
1336
+ }
1337
+ }
1338
+ }
1112
1339
  } else if (mod.modifiedContent) {
1113
1340
  // Legacy: AI returned full file content
1114
1341
  console.warn(`[Apply-First] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
@@ -167,6 +167,73 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
167
167
  }
168
168
  }
169
169
 
170
+ /**
171
+ * Search candidate files for JSX code matching the focused element
172
+ * This helps identify which file actually contains the element the user clicked on
173
+ */
174
+ function findFilesContainingElement(
175
+ focusedElements: VisionFocusedElement[] | undefined,
176
+ candidateFiles: { path: string; content: string }[]
177
+ ): { path: string; score: number; matches: string[] }[] {
178
+ if (!focusedElements || focusedElements.length === 0) {
179
+ return [];
180
+ }
181
+
182
+ const results: { path: string; score: number; matches: string[] }[] = [];
183
+
184
+ for (const file of candidateFiles) {
185
+ let score = 0;
186
+ const matches: string[] = [];
187
+ const content = file.content.toLowerCase();
188
+
189
+ for (const el of focusedElements) {
190
+ // Extract element type from the name (e.g., "button #123" -> "button")
191
+ const elementType = el.name.split(/[\s#]/)[0].toLowerCase();
192
+ const componentType = el.type.toLowerCase();
193
+
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}>`);
199
+ }
200
+
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}>`);
206
+ }
207
+
208
+ // Check for component imports
209
+ if (content.includes(`import`) && content.includes(componentType)) {
210
+ score += 5;
211
+ }
212
+
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;
217
+ }
218
+ }
219
+
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}`);
225
+ }
226
+ }
227
+
228
+ if (score > 0) {
229
+ results.push({ path: file.path, score, matches });
230
+ }
231
+ }
232
+
233
+ // Sort by score descending
234
+ return results.sort((a, b) => b.score - a.score);
235
+ }
236
+
170
237
  /**
171
238
  * Phase 3: Ask LLM to select the best file from actual file list
172
239
  * This replaces guessing - the LLM sees the real filenames and picks one
@@ -175,7 +242,8 @@ async function selectBestFileFromList(
175
242
  screenshot: string,
176
243
  userPrompt: string,
177
244
  candidateFiles: string[],
178
- apiKey: string
245
+ apiKey: string,
246
+ focusedElementHints?: { path: string; score: number; matches: string[] }[]
179
247
  ): Promise<string | null> {
180
248
  if (candidateFiles.length === 0) return null;
181
249
 
@@ -199,7 +267,12 @@ async function selectBestFileFromList(
199
267
 
200
268
  Here are the actual component files found in this codebase:
201
269
  ${candidateFiles.map((f, i) => `${i + 1}. ${f}`).join('\n')}
270
+ ${focusedElementHints && focusedElementHints.length > 0 ? `
271
+ FOCUSED ELEMENT ANALYSIS - Files that contain the element the user clicked on:
272
+ ${focusedElementHints.slice(0, 3).map(h => `- ${h.path} (confidence: ${h.score}, found: ${h.matches.join(", ")})`).join('\n')}
202
273
 
274
+ IMPORTANT: Prefer files from the FOCUSED ELEMENT ANALYSIS above, as they contain the actual element the user wants to modify.
275
+ ` : ''}
203
276
  Looking at the screenshot, which file MOST LIKELY contains the UI elements the user wants to modify?
204
277
 
205
278
  IMPORTANT: Return ONLY the exact file path from the list above (e.g., "components/ProcessCatalogue/ProcessDetailPanel.tsx").
@@ -434,43 +507,83 @@ function searchFilesSmart(
434
507
  return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
435
508
  }
436
509
 
437
- const VISION_SYSTEM_PROMPT = `You are a code editor. Make ONLY the change the user requested.
510
+ const VISION_SYSTEM_PROMPT = `You are a code editor like Cursor. Before making changes, REASON about what needs to be done.
511
+
512
+ ═══════════════════════════════════════════════════════════════════════════════
513
+ STEP 1: ANALYZE (Think like Cursor)
514
+ ═══════════════════════════════════════════════════════════════════════════════
515
+
516
+ Before writing any patches, analyze the screenshot and answer:
517
+
518
+ 1. CURRENT STATE: What do I see in the screenshot?
519
+ - Is the element visible/readable right now?
520
+ - What colors/styles are currently applied?
521
+
522
+ 2. PROBLEM IDENTIFICATION: What's actually wrong?
523
+ - Contrast issue? (text blending with background)
524
+ - Hidden element? (not showing at all)
525
+ - Hover state issue? (only visible on interaction)
526
+ - Layout issue? (wrong size/position)
527
+ - Or is it already correct?
528
+
529
+ 3. FIX STRATEGY: What should I change?
530
+ - DEFAULT state only (element needs to look different normally)
531
+ - HOVER state only (element needs different hover effect)
532
+ - BOTH states (element needs overall visibility improvement)
533
+ - ASK for clarification (request is too vague)
534
+
535
+ ═══════════════════════════════════════════════════════════════════════════════
536
+ STEP 2: GENERATE PATCHES (Only after reasoning)
537
+ ═══════════════════════════════════════════════════════════════════════════════
538
+
539
+ CRITICAL - FILE SELECTION:
540
+ - You MUST modify the file marked "TARGET COMPONENT" - this is the file you have FULL visibility into
541
+ - Do NOT try to modify other files listed in context - you only see their imports/exports, not full content
542
+ - If the TARGET COMPONENT is not the right file, return clarification_needed
543
+
544
+ COPY EXACTLY from the TARGET COMPONENT section:
545
+ - Your "search" string must match the file CHARACTER-FOR-CHARACTER
546
+ - Include exact indentation and at least 3 lines of context
547
+ - If the element looks FINE in the screenshot, say so
438
548
 
439
549
  RULES:
440
- 1. Make the SMALLEST possible change to accomplish the request
441
- 2. Do NOT refactor, rebrand, or "improve" anything else
442
- 3. Do NOT change import statements unless explicitly required
443
- 4. Do NOT change component libraries (e.g., heroui to shadcn)
444
- 5. If fixing a color/visibility issue, change ONLY that element's classes
445
- 6. Your patch should typically be 1-5 lines, not 50+
446
- 7. NEVER invent or guess code - your "search" string MUST match the file EXACTLY
447
- 8. NEVER change data mappings (icon names, keys, enum values) unless user explicitly provides the new values
448
- 9. If you don't know what values exist in a database or data source, DO NOT guess - ask for clarification
449
-
450
- CRITICAL - DATA INTEGRITY:
451
- - If code references database values (like icon_name, type, status), DO NOT change the mapping keys
452
- - You cannot see database content - only change STRUCTURE, not DATA MAPPINGS
453
- - Example: If you see iconMap["EyeOff"] = SomeIcon, do NOT change "EyeOff" to something else
454
- - If user wants different icons, they must tell you the EXACT icon names they want
455
-
456
- PATCH FORMAT:
457
- Return ONLY raw JSON (no markdown, no preamble):
550
+ 1. Make the SMALLEST possible change
551
+ 2. Do NOT refactor or "improve" other code
552
+ 3. NEVER invent code - COPY exact code from file
553
+ 4. NEVER change data mappings unless user provides new values
554
+ 5. If visibility issue: prefer explicit colors (text-white, text-black) over semantic tokens
555
+
556
+ ═══════════════════════════════════════════════════════════════════════════════
557
+ RESPONSE FORMAT
558
+ ═══════════════════════════════════════════════════════════════════════════════
559
+
560
+ Return ONLY raw JSON:
458
561
  {
459
- "reasoning": "brief explanation",
562
+ "analysis": {
563
+ "currentState": "what I see in the screenshot",
564
+ "problem": "the actual issue (or 'none - element appears correct')",
565
+ "fixStrategy": "default|hover|both|clarification_needed"
566
+ },
567
+ "reasoning": "my diagnosis and approach",
460
568
  "modifications": [{
461
569
  "filePath": "path/to/file.tsx",
462
570
  "patches": [{
463
- "search": "exact original code (copy from provided file)",
464
- "replace": "minimal change",
465
- "explanation": "what this does"
571
+ "search": "EXACT copy from file",
572
+ "replace": "changed code",
573
+ "explanation": "what this changes and why"
466
574
  }],
467
575
  "previewCSS": "optional CSS for live preview"
468
576
  }],
469
577
  "aggregatedPreviewCSS": "combined CSS for all changes",
470
- "explanation": "summary"
578
+ "explanation": "summary of what was changed"
471
579
  }
472
580
 
473
- If you cannot find the exact code to modify, OR if you would need to guess data values, return empty modifications array with explanation.`;
581
+ If the element looks correct, return:
582
+ {
583
+ "analysis": { "currentState": "...", "problem": "none", "fixStrategy": "clarification_needed" },
584
+ "modifications": [],
585
+ "explanation": "The element appears visible/correct in the screenshot. Please specify what needs to change."
586
+ }`;
474
587
 
475
588
  export async function POST(request: Request) {
476
589
  // Only allow in development
@@ -575,6 +688,22 @@ export async function POST(request: Request) {
575
688
  const searchResults = searchFilesSmart(analysis, projectRoot, 10);
576
689
  smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
577
690
 
691
+ // PHASE 2.5: Find which files contain the focused element
692
+ const focusedElementHints = findFilesContainingElement(
693
+ focusedElements,
694
+ searchResults.map(r => ({ path: r.path, content: r.content }))
695
+ );
696
+
697
+ if (focusedElementHints.length > 0) {
698
+ debugLog("Focused element search results", {
699
+ topMatches: focusedElementHints.slice(0, 3).map(h => ({
700
+ path: h.path,
701
+ score: h.score,
702
+ matches: h.matches
703
+ }))
704
+ });
705
+ }
706
+
578
707
  // PHASE 3: Ask LLM to pick the best file from actual file list
579
708
  if (searchResults.length > 0) {
580
709
  const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
@@ -582,7 +711,8 @@ export async function POST(request: Request) {
582
711
  screenshot,
583
712
  userPrompt,
584
713
  candidateFiles,
585
- apiKey
714
+ apiKey,
715
+ focusedElementHints
586
716
  );
587
717
 
588
718
  if (selectedFile) {
@@ -749,30 +879,59 @@ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
749
879
 
750
880
  // ========== THEME DISCOVERY (REFERENCE ONLY) ==========
751
881
  // Dynamically discover theme tokens from the target codebase
752
- // This is marked as REFERENCE ONLY so the LLM doesn't use it to justify extra changes
753
882
  const discoveredTheme = await discoverTheme(projectRoot);
754
883
  const themeContext = formatThemeForPrompt(discoveredTheme);
755
884
 
885
+ // Check if this is a visibility-related request
886
+ const isVisibilityRequest = /visible|can't see|cant see|not visible|hidden|color|contrast/i.test(userPrompt);
887
+
756
888
  if (discoveredTheme.discoveredFiles.length > 0) {
757
- textContent += `
889
+ // Only include detailed color guidance for visibility requests
890
+ if (isVisibilityRequest) {
891
+ // Extract available text color classes from discovered theme
892
+ const textColorClasses = Object.keys(discoveredTheme.cssVariables)
893
+ .filter(key => key.includes('foreground') || key.includes('text'))
894
+ .map(key => `text-${key.replace('--', '').replace(/-/g, '-')}`)
895
+ .slice(0, 10);
896
+
897
+ textContent += `
758
898
  ═══════════════════════════════════════════════════════════════════════════════
759
- REFERENCE ONLY (do not use this to justify additional changes)
899
+ COLOR FIX GUIDANCE (for visibility issues only)
760
900
  ═══════════════════════════════════════════════════════════════════════════════
761
901
 
762
- If you need to pick a color for a VISIBILITY fix, these are safe choices:
763
- - bg-accent text-white (cyan button with white text)
764
- - bg-primary text-white (charcoal button with white text)
765
- - bg-success text-white (green button with white text)
766
- - bg-destructive text-white (red button with white text)
902
+ AVAILABLE TEXT COLORS (use these for contrast fixes):
903
+ - text-white (always visible on dark backgrounds)
904
+ - text-black (always visible on light backgrounds)
905
+ - text-foreground (theme default)
906
+ - text-primary-foreground (for bg-primary)
907
+ - text-accent-foreground (for bg-accent - CHECK IF THIS HAS GOOD CONTRAST)
908
+ - text-destructive-foreground (for bg-destructive)
909
+
910
+ SAFE BUTTON PATTERNS:
911
+ - bg-accent text-white (cyan button, guaranteed visible)
912
+ - bg-primary text-white (charcoal button, guaranteed visible)
913
+ - bg-destructive text-white (red button, guaranteed visible)
914
+ - bg-muted text-foreground (gray button, guaranteed visible)
767
915
 
768
- But ONLY use these if the user is asking for a color/visibility change.
769
- Do NOT rebrand or change other elements to match.
916
+ IMPORTANT: If current code has text-accent-foreground or similar semantic colors
917
+ that result in poor contrast, REPLACE with text-white or text-black.
770
918
 
771
919
  `;
920
+ } else {
921
+ // For non-visibility requests, minimal reference
922
+ textContent += `
923
+ ═══════════════════════════════════════════════════════════════════════════════
924
+ REFERENCE ONLY (do not use this to justify additional changes)
925
+ ═══════════════════════════════════════════════════════════════════════════════
926
+ Theme discovered from: ${discoveredTheme.discoveredFiles.join(', ')}
927
+ `;
928
+ }
929
+
772
930
  debugLog("Theme discovery complete", {
773
931
  filesFound: discoveredTheme.discoveredFiles,
774
932
  cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
775
933
  tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
934
+ isVisibilityRequest,
776
935
  });
777
936
  }
778
937
 
@@ -913,6 +1072,11 @@ This is better than generating patches with made-up code.`,
913
1072
 
914
1073
  // Parse AI response - now expecting patches instead of full file content
915
1074
  let aiResponse: {
1075
+ analysis?: {
1076
+ currentState?: string;
1077
+ problem?: string;
1078
+ fixStrategy?: string;
1079
+ };
916
1080
  reasoning?: string;
917
1081
  modifications: Array<{
918
1082
  filePath: string;
@@ -964,10 +1128,31 @@ This is better than generating patches with made-up code.`,
964
1128
  );
965
1129
  }
966
1130
 
1131
+ // Log the LLM's analysis/reasoning (like Cursor showing its thought process)
1132
+ if (aiResponse.analysis) {
1133
+ debugLog("LLM Analysis (Step 1)", {
1134
+ currentState: aiResponse.analysis.currentState,
1135
+ problem: aiResponse.analysis.problem,
1136
+ fixStrategy: aiResponse.analysis.fixStrategy,
1137
+ });
1138
+ }
1139
+
967
1140
  finalExplanation = aiResponse.explanation;
968
1141
  finalReasoning = aiResponse.reasoning;
969
1142
  finalAggregatedCSS = aiResponse.aggregatedPreviewCSS;
970
1143
 
1144
+ // If LLM says clarification is needed, return that to the user
1145
+ if (aiResponse.analysis?.fixStrategy === "clarification_needed" &&
1146
+ (!aiResponse.modifications || aiResponse.modifications.length === 0)) {
1147
+ return NextResponse.json({
1148
+ success: true,
1149
+ needsClarification: true,
1150
+ analysis: aiResponse.analysis,
1151
+ explanation: aiResponse.explanation || "The element appears correct. Please specify what needs to change.",
1152
+ modifications: [],
1153
+ });
1154
+ }
1155
+
971
1156
  debugLog("VALIDATION: Known file paths from page context", {
972
1157
  pageFile: pageContext.pageFile,
973
1158
  knownPaths: Array.from(knownPaths),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.58",
3
+ "version": "1.3.60",
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",