sonance-brand-mcp 1.3.103 → 1.3.105

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.
@@ -141,6 +141,132 @@ function debugLog(message: string, data?: unknown) {
141
141
  }
142
142
  }
143
143
 
144
+ /**
145
+ * Sanitize a JSON string by finding the correct end point using bracket balancing.
146
+ * Handles cases where LLM outputs trailing garbage like extra ]} characters.
147
+ */
148
+ function sanitizeJsonString(text: string): string {
149
+ const trimmed = text.trim();
150
+
151
+ debugLog("[sanitizeJsonString] Starting", {
152
+ inputLength: trimmed.length,
153
+ first100: trimmed.substring(0, 100),
154
+ last100: trimmed.substring(trimmed.length - 100)
155
+ });
156
+
157
+ // Try parsing as-is first
158
+ try {
159
+ JSON.parse(trimmed);
160
+ debugLog("[sanitizeJsonString] Parsed as-is successfully");
161
+ return trimmed;
162
+ } catch (e) {
163
+ const error = e as Error;
164
+ debugLog("[sanitizeJsonString] Initial parse failed", {
165
+ error: error.message,
166
+ // Try to find where the error is
167
+ errorPosition: error.message.match(/position (\d+)/)?.[1]
168
+ });
169
+ }
170
+
171
+ // Find the correct end using balanced bracket counting
172
+ let braceCount = 0;
173
+ let bracketCount = 0;
174
+ let inString = false;
175
+ let escapeNext = false;
176
+ let endIndex = -1;
177
+
178
+ for (let i = 0; i < trimmed.length; i++) {
179
+ const char = trimmed[i];
180
+
181
+ if (escapeNext) {
182
+ escapeNext = false;
183
+ continue;
184
+ }
185
+
186
+ if (char === '\\' && inString) {
187
+ escapeNext = true;
188
+ continue;
189
+ }
190
+
191
+ if (char === '"') {
192
+ inString = !inString;
193
+ continue;
194
+ }
195
+
196
+ if (inString) continue;
197
+
198
+ if (char === '{') braceCount++;
199
+ if (char === '}') braceCount--;
200
+ if (char === '[') bracketCount++;
201
+ if (char === ']') bracketCount--;
202
+
203
+ // Found the end of the root object
204
+ if (braceCount === 0 && bracketCount === 0 && char === '}') {
205
+ endIndex = i;
206
+ break;
207
+ }
208
+ }
209
+
210
+ debugLog("[sanitizeJsonString] Bracket counting complete", {
211
+ endIndex,
212
+ finalBraceCount: braceCount,
213
+ finalBracketCount: bracketCount,
214
+ inString
215
+ });
216
+
217
+ if (endIndex !== -1) {
218
+ const sanitized = trimmed.slice(0, endIndex + 1);
219
+ const removed = trimmed.slice(endIndex + 1);
220
+ debugLog("[sanitizeJsonString] Attempting parse of balanced portion", {
221
+ sanitizedLength: sanitized.length,
222
+ removedLength: removed.length,
223
+ removedChars: removed.substring(0, 50)
224
+ });
225
+ try {
226
+ JSON.parse(sanitized);
227
+ debugLog("[sanitizeJsonString] SUCCESS: Removed trailing garbage", {
228
+ originalLength: trimmed.length,
229
+ sanitizedLength: sanitized.length,
230
+ removedChars: removed
231
+ });
232
+ return sanitized;
233
+ } catch (e) {
234
+ const error = e as Error;
235
+ debugLog("[sanitizeJsonString] Balanced portion still invalid", {
236
+ error: error.message
237
+ });
238
+ }
239
+ }
240
+
241
+ // Last resort: progressively remove trailing characters
242
+ debugLog("[sanitizeJsonString] Starting progressive trimming");
243
+ let attempt = trimmed;
244
+ let iterations = 0;
245
+ const maxIterations = Math.min(1000, trimmed.length); // Safety limit
246
+
247
+ while (attempt.length > 1 && iterations < maxIterations) {
248
+ iterations++;
249
+ try {
250
+ JSON.parse(attempt);
251
+ debugLog("[sanitizeJsonString] SUCCESS via progressive trimming", {
252
+ originalLength: trimmed.length,
253
+ sanitizedLength: attempt.length,
254
+ iterations
255
+ });
256
+ return attempt;
257
+ } catch {
258
+ attempt = attempt.slice(0, -1).trim();
259
+ }
260
+ }
261
+
262
+ debugLog("[sanitizeJsonString] FAILED: Could not sanitize JSON", {
263
+ originalLength: trimmed.length,
264
+ iterations
265
+ });
266
+
267
+ return trimmed; // Return original if all else fails
268
+ }
269
+
144
270
  /**
145
271
  * Extract JSON from LLM response that may contain preamble text
146
272
  * Handles: pure JSON, markdown code fences, and text with embedded JSON
@@ -154,14 +280,14 @@ function debugLog(message: string, data?: unknown) {
154
280
  function extractJsonFromResponse(text: string): string {
155
281
  // Try direct parse first - if it starts with {, it's likely pure JSON
156
282
  const trimmed = text.trim();
157
- if (trimmed.startsWith('{')) return trimmed;
283
+ if (trimmed.startsWith('{')) return sanitizeJsonString(trimmed);
158
284
 
159
285
  // PRIORITY 1: Look specifically for ```json fence (most reliable)
160
286
  const jsonFenceMatch = text.match(/```json\s*([\s\S]*?)```/);
161
287
  if (jsonFenceMatch) {
162
288
  const extracted = jsonFenceMatch[1].trim();
163
289
  debugLog("Extracted JSON from explicit json fence", { previewLength: extracted.length });
164
- return extracted;
290
+ return sanitizeJsonString(extracted);
165
291
  }
166
292
 
167
293
  // PRIORITY 2: Find raw JSON object in text (handles prose + JSON at end)
@@ -169,7 +295,7 @@ function extractJsonFromResponse(text: string): string {
169
295
  const modMatch = text.match(/(\{"modifications"\s*:\s*\[[\s\S]*\](?:\s*,\s*"explanation"\s*:\s*"[^"]*")?\s*\})/);
170
296
  if (modMatch) {
171
297
  debugLog("Extracted JSON via modifications pattern", { length: modMatch[1].length });
172
- return modMatch[1];
298
+ return sanitizeJsonString(modMatch[1]);
173
299
  }
174
300
 
175
301
  // PRIORITY 3: Find any JSON object starting with {"
@@ -181,7 +307,7 @@ function extractJsonFromResponse(text: string): string {
181
307
  preambleLength: jsonStart,
182
308
  jsonLength: extracted.length
183
309
  });
184
- return extracted;
310
+ return sanitizeJsonString(extracted);
185
311
  }
186
312
 
187
313
  // PRIORITY 4 (last resort): Try generic code fence
@@ -192,12 +318,12 @@ function extractJsonFromResponse(text: string): string {
192
318
  // Only use if it looks like JSON
193
319
  if (extracted.startsWith('{')) {
194
320
  debugLog("Extracted JSON from generic fence", { previewLength: extracted.length });
195
- return extracted;
321
+ return sanitizeJsonString(extracted);
196
322
  }
197
323
  }
198
324
 
199
- // Return as-is if no JSON structure found
200
- return text;
325
+ // Return as-is if no JSON structure found (sanitizeJsonString will handle validation)
326
+ return sanitizeJsonString(text);
201
327
  }
202
328
 
203
329
  /**
@@ -328,9 +454,19 @@ function findElementLineInFile(
328
454
  }
329
455
  }
330
456
 
457
+ // Helper: Normalize text for searching (handle bullets, whitespace, special chars)
458
+ const normalizeTextForSearch = (text: string): string => {
459
+ return text
460
+ .replace(/[•·●○◦‣⁃▪▫→←↓↑]/g, '') // Remove bullet/arrow characters
461
+ .replace(/\s+/g, ' ') // Collapse whitespace
462
+ .replace(/[""'']/g, '"') // Normalize quotes
463
+ .trim();
464
+ };
465
+
331
466
  // PRIORITY 2: Exact text content in JSX
332
467
  if (focusedElement.textContent && focusedElement.textContent.trim().length >= 2) {
333
468
  const text = focusedElement.textContent.trim();
469
+ const normalizedText = normalizeTextForSearch(text);
334
470
  // Escape special regex characters in the text
335
471
  const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
336
472
 
@@ -370,6 +506,34 @@ function findElementLineInFile(
370
506
  }
371
507
  }
372
508
  }
509
+
510
+ // PRIORITY 2c: Word-based matching for text with special characters (bullets, etc.)
511
+ // Extract significant words and find lines containing multiple of them
512
+ if (normalizedText.length > 10) {
513
+ const commonWords = ['the', 'this', 'that', 'with', 'from', 'have', 'will', 'your', 'for', 'and', 'use'];
514
+ const significantWords = normalizedText.split(' ')
515
+ .filter(word => word.length >= 4 && !commonWords.includes(word.toLowerCase()))
516
+ .slice(0, 5); // Take up to 5 significant words
517
+
518
+ if (significantWords.length >= 2) {
519
+ for (let i = 0; i < lines.length; i++) {
520
+ const lineLower = lines[i].toLowerCase();
521
+ const matchCount = significantWords.filter(word =>
522
+ lineLower.includes(word.toLowerCase())
523
+ ).length;
524
+
525
+ // Require at least 2 word matches for confidence
526
+ if (matchCount >= 2) {
527
+ return {
528
+ lineNumber: i + 1,
529
+ snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
530
+ confidence: 'medium',
531
+ matchedBy: `word match: ${significantWords.slice(0, 3).join(', ')}... (${matchCount} words)`
532
+ };
533
+ }
534
+ }
535
+ }
536
+ }
373
537
  }
374
538
 
375
539
  // PRIORITY 2b: Look for icon patterns if type is button and no text (like X buttons)
@@ -390,159 +554,72 @@ function findElementLineInFile(
390
554
  }
391
555
  }
392
556
 
393
- // PRIORITY 3: Distinctive className patterns (ONLY semantic/custom classes)
557
+ // PRIORITY 3: Distinctive className patterns (semantic classes from design system)
394
558
  if (focusedElement.className) {
395
- // Filter OUT all Tailwind utility patterns - these are too generic and match wrong elements
396
- const tailwindPatterns = [
397
- /^p[xytblr]?-/, // padding: px-4, py-2, pt-1
398
- /^m[xytblr]?-/, // margin: mx-auto, my-4
399
- /^-?m[xytblr]?-/, // negative margins: -mt-4
400
- /^w-/, // width: w-full, w-4
401
- /^h-/, // height: h-10, h-full
402
- /^min-/, // min-width/height
403
- /^max-/, // max-width/height
404
- /^size-/, // size utilities
405
- /^bg-/, // background
406
- /^text-/, // text color/size
407
- /^font-/, // font-weight
408
- /^leading-/, // line-height
409
- /^tracking-/, // letter-spacing
410
- /^rounded/, // border-radius: rounded-sm, rounded-lg
411
- /^border/, // border
412
- /^outline/, // outline
413
- /^ring/, // ring
414
- /^shadow/, // shadow
415
- /^flex/, // flex utilities
416
- /^grow/, // flex-grow
417
- /^shrink/, // flex-shrink
418
- /^basis-/, // flex-basis
419
- /^grid/, // grid utilities
420
- /^col-/, // grid columns
421
- /^row-/, // grid rows
422
- /^gap-/, // gap
423
- /^items-/, // align-items
424
- /^justify-/, // justify-content
425
- /^self-/, // align-self
426
- /^place-/, // place utilities
427
- /^space-/, // space-x, space-y
428
- /^overflow/, // overflow
429
- /^scroll/, // scroll utilities
430
- /^cursor-/, // cursor
431
- /^pointer-/, // pointer-events
432
- /^select-/, // user-select
433
- /^opacity-/, // opacity
434
- /^visible/, // visibility
435
- /^invisible/, // visibility
436
- /^z-/, // z-index
437
- /^inset/, // inset utilities
438
- /^top-/, // positioning
439
- /^right-/, // positioning
440
- /^bottom-/, // positioning
441
- /^left-/, // positioning
442
- /^static/, // position
443
- /^fixed/, // position
444
- /^absolute/, // position
445
- /^relative/, // position
446
- /^sticky/, // position
447
- /^transition/, // transitions
448
- /^duration-/, // duration
449
- /^ease-/, // timing function
450
- /^delay-/, // delay
451
- /^animate-/, // animations
452
- /^transform/, // transform
453
- /^scale-/, // scale
454
- /^rotate-/, // rotate
455
- /^translate-/, // translate
456
- /^skew-/, // skew
457
- /^origin-/, // transform-origin
458
- /^appearance-/, // appearance
459
- /^accent-/, // accent-color
460
- /^caret-/, // caret-color
461
- /^fill-/, // SVG fill
462
- /^stroke-/, // SVG stroke
463
- /^object-/, // object-fit/position
464
- /^aspect-/, // aspect-ratio
465
- /^container/, // container
466
- /^columns-/, // columns
467
- /^break-/, // word-break
468
- /^truncate/, // text-overflow
469
- /^whitespace-/, // whitespace
470
- /^list-/, // list-style
471
- /^decoration-/, // text-decoration
472
- /^underline/, // underline
473
- /^overline/, // overline
474
- /^line-through/, // line-through
475
- /^no-underline/, // no underline
476
- /^antialiased/, // font-smoothing
477
- /^subpixel/, // subpixel-antialiased
478
- /^italic/, // font-style
479
- /^not-italic/, // font-style
480
- /^uppercase/, // text-transform
481
- /^lowercase/, // text-transform
482
- /^capitalize/, // text-transform
483
- /^normal-case/, // text-transform
484
- /^align-/, // vertical-align
485
- /^indent-/, // text-indent
486
- /^content-/, // content
487
- /^will-change/, // will-change
488
- /^hover:/, // hover states
489
- /^focus:/, // focus states
490
- /^focus-within:/, // focus-within states
491
- /^focus-visible:/, // focus-visible states
492
- /^active:/, // active states
493
- /^visited:/, // visited states
494
- /^disabled:/, // disabled states
495
- /^checked:/, // checked states
496
- /^group-/, // group utilities
497
- /^peer-/, // peer utilities
498
- /^dark:/, // dark mode
499
- /^light:/, // light mode
500
- /^motion-/, // motion utilities
501
- /^print:/, // print utilities
502
- /^portrait:/, // orientation
503
- /^landscape:/, // orientation
504
- /^sm:|^md:|^lg:|^xl:|^2xl:/, // responsive prefixes
505
- /^data-\[/, // data attribute variants
506
- /^aria-/, // aria variants
507
- /^supports-/, // supports variants
508
- /^has-/, // has variants
509
- /^group$/, // group class itself
510
- /^peer$/, // peer class itself
511
- /^sr-only/, // screen reader only
512
- /^not-sr-only/, // not screen reader only
513
- /^isolate/, // isolation
514
- /^block$/, // display
515
- /^inline/, // display
516
- /^hidden$/, // display
517
- /^table/, // display table
559
+ // SEMANTIC CLASS DETECTION: Instead of filtering OUT utilities, filter IN semantics
560
+ // These patterns identify classes that reference CSS variables (design tokens)
561
+ const semanticClassPatterns = [
562
+ // Background colors referencing CSS variables
563
+ /^bg-(card|background|foreground|primary|secondary|accent|destructive|muted|popover|input|sidebar)/,
564
+ // Text colors referencing CSS variables
565
+ /^text-(foreground|muted|primary|secondary|accent|destructive|card)/,
566
+ // Border colors referencing CSS variables
567
+ /^border-(border|card|input|primary|accent|destructive|muted)/,
568
+ // Any class containing these semantic tokens (catches hover:bg-card-hover, etc.)
569
+ /-card($|-)/,
570
+ /-foreground($|-)/,
571
+ /-background($|-)/,
572
+ /-border($|-)/,
573
+ /-primary($|-)/,
574
+ /-secondary($|-)/,
575
+ /-accent($|-)/,
576
+ /-muted($|-)/,
577
+ /-destructive($|-)/,
578
+ /-popover($|-)/,
579
+ /-sidebar($|-)/,
580
+ // Component-specific classes (likely custom)
581
+ /^(btn|card|modal|dialog|panel|sidebar|nav|menu|dropdown|tooltip|badge|chip|tag|avatar|icon)-/,
582
+ // Data attribute classes (often semantic)
583
+ /^\[data-/,
518
584
  ];
519
585
 
520
- const isTailwindUtility = (cls: string) =>
521
- tailwindPatterns.some(pattern => pattern.test(cls));
586
+ const isSemanticClass = (cls: string): boolean => {
587
+ // Remove variant prefixes for checking (hover:, focus:, dark:, etc.)
588
+ const baseClass = cls.replace(/^(hover:|focus:|active:|dark:|light:|md:|lg:|xl:|2xl:|sm:)+/, '');
589
+ return semanticClassPatterns.some(pattern => pattern.test(baseClass));
590
+ };
522
591
 
523
- // Only use classes that are NOT Tailwind utilities (likely custom/semantic)
592
+ // Find semantic classes
524
593
  const semanticClasses = focusedElement.className.split(/\s+/)
525
- .filter(c => c.length > 3 && !isTailwindUtility(c));
594
+ .filter(c => c.length > 3 && isSemanticClass(c));
526
595
 
527
596
  // Only proceed if we found semantic classes
528
597
  if (semanticClasses.length > 0) {
598
+ debugLog("Found semantic classes for element search", {
599
+ semanticClasses,
600
+ originalClasses: focusedElement.className.substring(0, 80)
601
+ });
602
+
529
603
  for (const cls of semanticClasses) {
604
+ // Remove variant prefix for searching (hover:bg-card -> bg-card)
605
+ const searchClass = cls.replace(/^(hover:|focus:|active:|dark:|light:|md:|lg:|xl:|2xl:|sm:)+/, '');
606
+
530
607
  for (let i = 0; i < lines.length; i++) {
531
- if (lines[i].includes(cls)) {
608
+ if (lines[i].includes(searchClass)) {
532
609
  return {
533
610
  lineNumber: i + 1,
534
611
  snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
535
612
  confidence: 'medium',
536
- matchedBy: `semantic className "${cls}"`
613
+ matchedBy: `semantic className "${searchClass}"`
537
614
  };
538
615
  }
539
616
  }
540
617
  }
541
618
  }
542
619
 
543
- // Log when we filtered out all classes (helpful for debugging)
620
+ // Log when we couldn't find semantic classes (helpful for debugging)
544
621
  if (semanticClasses.length === 0 && focusedElement.className.trim().length > 0) {
545
- console.log('[findElementLineInFile] All classes filtered as Tailwind utilities:',
622
+ console.log('[findElementLineInFile] No semantic classes found in:',
546
623
  focusedElement.className.substring(0, 100));
547
624
  }
548
625
  }
@@ -651,39 +728,105 @@ function scoreFilesForTextContent(
651
728
  */
652
729
  function findElementInImportedFiles(
653
730
  focusedElement: VisionFocusedElement,
654
- importedFiles: { path: string; content: string }[]
731
+ importedFiles: { path: string; content: string }[],
732
+ pageFilePath?: string // Pass page file for proximity scoring
655
733
  ): { path: string; lineNumber: number; matchedBy: string; content: string; confidence: string } | null {
656
734
  debugLog("Searching imported components for element", {
657
735
  elementType: focusedElement.type,
658
736
  textContent: focusedElement.textContent?.substring(0, 30),
659
- filesCount: importedFiles.length
737
+ filesCount: importedFiles.length,
738
+ pageFilePath
660
739
  });
661
740
 
741
+ // Collect ALL matches with scores instead of returning first match
742
+ const matches: Array<{
743
+ path: string;
744
+ lineNumber: number;
745
+ matchedBy: string;
746
+ content: string;
747
+ confidence: string;
748
+ score: number;
749
+ }> = [];
750
+
751
+ // Extract route/directory info from page file for proximity scoring
752
+ const pageDir = pageFilePath ? path.dirname(pageFilePath) : '';
753
+ const routeName = pageDir.split('/').pop() || ''; // e.g., "typography"
754
+
662
755
  for (const file of importedFiles) {
663
756
  // Focus on component files (where UI elements live)
664
757
  // Skip types, stores, utils, hooks - they don't contain JSX elements
665
- if (!file.path.includes('components/') && !file.path.includes('/ui/')) continue;
758
+ if (!file.path.includes('components/') && !file.path.includes('/ui/') && !file.path.includes('/_components/')) continue;
666
759
 
667
760
  const result = findElementLineInFile(file.content, focusedElement);
668
761
  if (result && result.confidence !== 'low') {
669
- debugLog("Found element in imported component", {
670
- path: file.path,
671
- lineNumber: result.lineNumber,
672
- matchedBy: result.matchedBy,
673
- confidence: result.confidence
674
- });
675
- return {
762
+ let score = 0;
763
+
764
+ // Text content match is highest priority (+100)
765
+ if (result.matchedBy.includes('textContent') || result.matchedBy.includes('word match')) {
766
+ score += 100;
767
+ }
768
+
769
+ // Directory proximity - same directory tree as page file (+50)
770
+ // e.g., for page src/app/typography/page.tsx, prefer src/app/typography/_components/
771
+ if (pageDir && file.path.includes(pageDir)) {
772
+ score += 50;
773
+ }
774
+
775
+ // Route name in filename (+30)
776
+ // e.g., for /typography route, prefer DigitalTypography.tsx
777
+ if (routeName && routeName.length > 2 && file.path.toLowerCase().includes(routeName.toLowerCase())) {
778
+ score += 30;
779
+ }
780
+
781
+ // Confidence bonus
782
+ score += result.confidence === 'high' ? 20 : 10;
783
+
784
+ matches.push({
676
785
  path: file.path,
677
786
  lineNumber: result.lineNumber,
678
787
  matchedBy: result.matchedBy,
679
788
  content: file.content,
680
- confidence: result.confidence
681
- };
789
+ confidence: result.confidence,
790
+ score
791
+ });
682
792
  }
683
793
  }
684
794
 
685
- debugLog("Element not found in any imported component files");
686
- return null;
795
+ // Return highest-scoring match
796
+ if (matches.length === 0) {
797
+ debugLog("Element not found in any imported component files");
798
+ return null;
799
+ }
800
+
801
+ // Sort by score descending
802
+ matches.sort((a, b) => b.score - a.score);
803
+
804
+ debugLog("Scored imported component matches", {
805
+ matchCount: matches.length,
806
+ topMatches: matches.slice(0, 3).map(m => ({
807
+ path: m.path,
808
+ score: m.score,
809
+ matchedBy: m.matchedBy,
810
+ confidence: m.confidence
811
+ }))
812
+ });
813
+
814
+ const bestMatch = matches[0];
815
+ debugLog("Found element in imported component (best match)", {
816
+ path: bestMatch.path,
817
+ lineNumber: bestMatch.lineNumber,
818
+ matchedBy: bestMatch.matchedBy,
819
+ confidence: bestMatch.confidence,
820
+ score: bestMatch.score
821
+ });
822
+
823
+ return {
824
+ path: bestMatch.path,
825
+ lineNumber: bestMatch.lineNumber,
826
+ matchedBy: bestMatch.matchedBy,
827
+ content: bestMatch.content,
828
+ confidence: bestMatch.confidence
829
+ };
687
830
  }
688
831
 
689
832
  /**
@@ -1532,6 +1675,27 @@ DESIGN ISSUES TO FIX (${problems.length} problems identified)
1532
1675
 
1533
1676
  ${problemsList}
1534
1677
 
1678
+ BEFORE GENERATING PATCHES, analyze each problem:
1679
+
1680
+ For EACH problem, determine:
1681
+ 1. What is the current DOM structure for this element?
1682
+ 2. Can this be fixed by adjusting className values (spacing, alignment, colors)?
1683
+ 3. Or does the DOM structure itself prevent the fix?
1684
+
1685
+ DECISION RULES:
1686
+ - If a problem is about SPACING: adjust margin/padding classes (mt-X, mb-X, gap-X, p-X)
1687
+ - If a problem is about ALIGNMENT: adjust flex/grid alignment classes (items-center, justify-between)
1688
+ - If a problem is about HIERARCHY: adjust font-size, font-weight, or color classes
1689
+ - If a problem is about GROUPING: check if elements are in the same container first
1690
+ → If yes: adjust gap/spacing classes
1691
+ → If no: consider minimal restructuring
1692
+
1693
+ ONLY restructure elements when:
1694
+ - Elements are in separate containers that prevent proper grouping
1695
+ - The current container type (div, flex, grid) fundamentally can't achieve the layout
1696
+
1697
+ PREFER the minimal change. A good designer makes surgical fixes, not rewrites.
1698
+
1535
1699
  YOUR TASK: Generate AT LEAST ONE PATCH for EACH problem listed above.
1536
1700
  Expected minimum patches: ${problems.length}
1537
1701
 
@@ -1550,12 +1714,14 @@ DO NOT:
1550
1714
  - Add or remove classes/attributes
1551
1715
  - Invent code that "should" exist
1552
1716
  - Guess at the structure
1717
+ - Restructure when a className change would work
1553
1718
 
1554
1719
  DO:
1555
1720
  - Copy the exact text from the numbered lines
1556
1721
  - Make small, targeted changes in the "replace" field
1557
1722
  - Keep patches focused on one change each
1558
1723
  - Generate one patch per problem minimum
1724
+ - Prefer adjusting existing classes over moving elements
1559
1725
  `;
1560
1726
  }
1561
1727
 
@@ -1794,6 +1960,7 @@ export async function POST(request: Request) {
1794
1960
  let smartSearchFiles: { path: string; content: string }[] = [];
1795
1961
  let recommendedFile: { path: string; reason: string } | null = null;
1796
1962
  let deterministicMatch: { path: string; lineNumber: number } | null = null;
1963
+ let phase1VisibleText: string[] = []; // Store Phase 1 visible text for use in file scoring
1797
1964
 
1798
1965
  // ========================================================================
1799
1966
  // PHASE 0: Deterministic Element ID Search (Cursor-style explicit selection)
@@ -1859,6 +2026,9 @@ export async function POST(request: Request) {
1859
2026
  if (!deterministicMatch && screenshot) {
1860
2027
  debugLog("Starting Phase 1: LLM screenshot analysis");
1861
2028
  const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
2029
+
2030
+ // Store visible text for use in file scoring when no element is focused
2031
+ phase1VisibleText = analysis.visibleText || [];
1862
2032
 
1863
2033
  const hasSearchTerms = analysis.visibleText.length > 0 ||
1864
2034
  analysis.componentNames.length > 0 ||
@@ -2169,7 +2339,8 @@ export async function POST(request: Request) {
2169
2339
 
2170
2340
  const importedMatch = findElementInImportedFiles(
2171
2341
  focusedElements[0],
2172
- pageContext.componentSources
2342
+ pageContext.componentSources,
2343
+ actualTargetFile?.path // Pass page file for proximity scoring
2173
2344
  );
2174
2345
 
2175
2346
  if (importedMatch) {
@@ -2188,6 +2359,48 @@ export async function POST(request: Request) {
2188
2359
  }
2189
2360
  }
2190
2361
  }
2362
+ // ========== AUTO TEXT SCORING (no focused element) ==========
2363
+ // When no element is clicked but we have Phase 1 visible text, use it to find the best file
2364
+ else if (actualTargetFile && (!focusedElements || focusedElements.length === 0) && phase1VisibleText.length > 0) {
2365
+ debugLog("No focused element - using Phase 1 visible text for file scoring", {
2366
+ textCount: phase1VisibleText.length,
2367
+ texts: phase1VisibleText.slice(0, 5)
2368
+ });
2369
+
2370
+ // Score all imported files based on visible text from Phase 1
2371
+ const bestMatch = scoreFilesForTextContent(
2372
+ phase1VisibleText,
2373
+ pageContext.componentSources
2374
+ );
2375
+
2376
+ // Check how well the current target file matches
2377
+ const currentFileScore = phase1VisibleText.filter(text =>
2378
+ actualTargetFile!.content.includes(text.substring(0, 20))
2379
+ ).length;
2380
+
2381
+ debugLog("Phase 1 text scoring comparison", {
2382
+ currentFile: actualTargetFile.path,
2383
+ currentScore: currentFileScore,
2384
+ bestImport: bestMatch?.path || 'none',
2385
+ bestImportScore: bestMatch?.score || 0
2386
+ });
2387
+
2388
+ // Redirect if an imported file has significantly more text matches
2389
+ if (bestMatch && bestMatch.score > currentFileScore && bestMatch.score >= 2) {
2390
+ debugLog("TEXT REDIRECT (no element): Phase 1 visible text found better file", {
2391
+ originalFile: actualTargetFile.path,
2392
+ originalScore: currentFileScore,
2393
+ redirectTo: bestMatch.path,
2394
+ redirectScore: bestMatch.score,
2395
+ matchedTexts: bestMatch.matchedTexts
2396
+ });
2397
+
2398
+ actualTargetFile = {
2399
+ path: bestMatch.path,
2400
+ content: bestMatch.content
2401
+ };
2402
+ }
2403
+ }
2191
2404
 
2192
2405
  debugLog("File redirect complete", {
2193
2406
  originalRecommended: recommendedFileContent?.path || 'none',
@@ -2578,11 +2791,24 @@ This is better than generating patches with made-up code.`,
2578
2791
  try {
2579
2792
  // Use robust JSON extraction that handles preamble text, code fences, etc.
2580
2793
  const jsonText = extractJsonFromResponse(textResponse.text);
2794
+ debugLog("[JSON Parse] Attempting to parse extracted JSON", {
2795
+ extractedLength: jsonText.length,
2796
+ extractedFirst100: jsonText.substring(0, 100),
2797
+ extractedLast100: jsonText.substring(jsonText.length - 100)
2798
+ });
2581
2799
  aiResponse = JSON.parse(jsonText);
2582
- } catch {
2800
+ debugLog("[JSON Parse] Successfully parsed AI response");
2801
+ } catch (parseError) {
2802
+ const error = parseError as Error;
2583
2803
  console.error("Failed to parse AI response:", textResponse.text);
2804
+ debugLog("[JSON Parse] FAILED to parse", {
2805
+ error: error.message,
2806
+ originalLength: textResponse.text.length,
2807
+ originalFirst100: textResponse.text.substring(0, 100),
2808
+ originalLast100: textResponse.text.substring(textResponse.text.length - 100)
2809
+ });
2584
2810
  return NextResponse.json(
2585
- { error: "Failed to parse AI response. Please try again." },
2811
+ { error: `Failed to parse AI response: ${error.message}` },
2586
2812
  { status: 500 }
2587
2813
  );
2588
2814
  }