sonance-brand-mcp 1.3.24 → 1.3.25

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.
@@ -55,69 +55,63 @@ interface BackupManifest {
55
55
 
56
56
  const BACKUP_ROOT = ".sonance-backups";
57
57
 
58
- const VISION_SYSTEM_PROMPT = `You are a React/Tailwind CSS expert with vision capabilities for the Sonance brand system.
58
+ const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
59
59
 
60
- You can see screenshots of web pages and understand their visual layout, then modify code to implement requested changes.
60
+ ═══════════════════════════════════════════════════════════════════════════════
61
+ UNDERSTAND THE USER'S REQUEST NATURALLY
62
+ ═══════════════════════════════════════════════════════════════════════════════
63
+
64
+ Read the user's request and do exactly what they ask. You know React, TypeScript, Tailwind CSS, and JSX - use that knowledge naturally.
65
+
66
+ - If they say "add X", ADD X to the code (don't replace or remove anything else)
67
+ - If they say "change X to Y", find X and change it to Y
68
+ - If they say "make X bigger/smaller/different color", adjust the relevant properties
69
+ - If they say "remove X", remove X
70
+ - If they say "wrap X with Y", add Y as a parent around X
71
+
72
+ For any change:
73
+ 1. Read the code context provided
74
+ 2. Understand what the user wants
75
+ 3. Generate patches that accomplish exactly that
76
+ 4. If adding a component/icon requires an import, include a patch for the import too
77
+
78
+ Don't overthink - just make the change the user requested.
61
79
 
62
80
  ═══════════════════════════════════════════════════════════════════════════════
63
- ABSOLUTE RULES - VIOLATION WILL BREAK THE APPLICATION
81
+ PATCH FORMAT
64
82
  ═══════════════════════════════════════════════════════════════════════════════
65
83
 
66
- **SURGICAL EDITING (MOST CRITICAL):**
67
- 1. You are making SURGICAL EDITS, not rewriting files
68
- 2. Change ONLY the specific lines that need modification
69
- 3. The modified file MUST have approximately the same line count as the original
70
- 4. If original file has 200 lines, modified file should have ~195-210 lines
71
- 5. NEVER rewrite a file from scratch - this destroys the application
72
-
73
- **FILE RULES:**
74
- 6. You may ONLY edit files that are provided in the PAGE CONTEXT section
75
- 7. NEVER create new files - only modify existing ones shown to you
76
- 8. The filePath in your response MUST exactly match one of the provided file paths
77
-
78
- **PRESERVATION RULES (ZERO TOLERANCE):**
79
- 9. NEVER delete or remove existing functions, hooks, state, or logic
80
- 10. NEVER remove imports - you may only ADD imports if needed
81
- 11. NEVER remove useEffect, useState, useCallback, or other React hooks
82
- 12. NEVER remove API calls, fetch requests, or data loading logic
83
- 13. NEVER remove error handling, loading states, or conditional rendering
84
- 14. NEVER remove data-sonance-* attributes
85
- 15. NEVER change component structure unless specifically requested
86
- 16. NEVER modify TypeScript types or interfaces unless specifically requested
87
-
88
- **CHANGE RULES:**
89
- 17. Make ONLY the changes requested by the user
90
- 18. Modify the MINIMUM amount of code necessary
91
- 19. Keep all existing className values and ADD to them if needed
92
- 20. Use semantic Tailwind classes (bg-primary, text-foreground, etc.)
93
- 21. Maintain dark mode compatibility with CSS variables
94
- 22. Keep the cn() utility for className merging
95
- 23. CRITICAL: You MUST return the FULL file content in "modifiedContent". Do NOT use comments like "// ... existing code ..." or "// ... rest of file ...". Return every single line of code, even if unchanged.
84
+ Return search/replace patches (NOT full files). The system applies your patches to the original.
85
+
86
+ **Patch Rules:**
87
+ - "search" must match the original code EXACTLY (including whitespace/indentation)
88
+ - "replace" contains your modified version
89
+ - Include 2-4 lines of context in "search" to make it unique
90
+ - You may ONLY edit files provided in the PAGE CONTEXT section
96
91
 
97
92
  **SONANCE BRAND COLORS:**
98
- - Charcoal: #333F48, #343D46 (primary)
99
- - Silver: #E2E2E2, #D1D1D6 (secondary)
93
+ - Charcoal: #333F48 (primary text)
100
94
  - IPORT Orange: #FC4C02
101
- - IPORT Dark: #0F161D
102
95
  - Blaze Blue: #00A3E1
103
- - Blaze Red: #C02B0A
104
96
 
105
97
  **RESPONSE FORMAT:**
106
- Return ONLY a valid JSON object. Do not include any conversational text before or after the JSON.
107
- The JSON must include:
108
- - "reasoning": Brief explanation of what you see in the screenshot and your plan
109
- - "modifications": Array of file modifications, each with:
110
- - "filePath": Path to the file
111
- - "modifiedContent": Complete updated file content (MUST BE FULL CONTENT, NO TRUNCATION)
112
- - "explanation": What changed in this file
113
- - "explanation": Overall summary of changes
114
-
115
- **EXAMPLE OF CORRECT EDIT:**
116
- If user asks to "make buttons smaller", you should:
117
- - Find the button elements in the code
118
- - Change ONLY the size-related classes (e.g., h-10 -> h-8, px-4 -> px-3)
119
- - Keep ALL other code exactly the same
120
- - Return the FULL file with this tiny change`;
98
+ Return ONLY valid JSON:
99
+ {
100
+ "reasoning": "What you understood from the request and your plan",
101
+ "modifications": [
102
+ {
103
+ "filePath": "path/to/file.tsx",
104
+ "patches": [
105
+ {
106
+ "search": "exact code to find",
107
+ "replace": "the replacement code",
108
+ "explanation": "what this patch does"
109
+ }
110
+ ]
111
+ }
112
+ ],
113
+ "explanation": "summary of changes made"
114
+ }`;
121
115
 
122
116
  export async function POST(request: Request) {
123
117
  // Only allow in development
@@ -296,13 +290,15 @@ CRITICAL: Your modified file should have approximately the same number of lines
296
290
  );
297
291
  }
298
292
 
299
- // Parse AI response
293
+ // Parse AI response - now expecting patches instead of full file content
300
294
  let aiResponse: {
301
295
  reasoning?: string;
302
296
  modifications: Array<{
303
297
  filePath: string;
304
- modifiedContent: string;
305
- explanation: string;
298
+ patches?: Patch[];
299
+ // Legacy support for modifiedContent (will be deprecated)
300
+ modifiedContent?: string;
301
+ explanation?: string;
306
302
  }>;
307
303
  explanation?: string;
308
304
  };
@@ -344,10 +340,9 @@ CRITICAL: Your modified file should have approximately the same number of lines
344
340
  });
345
341
  }
346
342
 
347
- // Read original content and prepare modifications
343
+ // Process modifications - apply patches to get modified content
348
344
  const modifications: VisionFileModification[] = [];
349
- const validationErrors: string[] = [];
350
- const validationWarnings: string[] = [];
345
+ const patchErrors: string[] = [];
351
346
 
352
347
  for (const mod of aiResponse.modifications) {
353
348
  const fullPath = path.join(projectRoot, mod.filePath);
@@ -356,42 +351,75 @@ CRITICAL: Your modified file should have approximately the same number of lines
356
351
  originalContent = fs.readFileSync(fullPath, "utf-8");
357
352
  }
358
353
 
359
- // Validate the modification
360
- const validation = validateModification(originalContent, mod.modifiedContent, mod.filePath);
361
-
362
- if (!validation.valid) {
363
- validationErrors.push(validation.error || "Unknown validation error");
364
- continue; // Skip this modification
365
- }
366
-
367
- if (validation.warnings.length > 0) {
368
- validationWarnings.push(...validation.warnings.map(w => `${mod.filePath}: ${w}`));
354
+ let modifiedContent: string;
355
+ let explanation = mod.explanation || "";
356
+
357
+ // Check if AI returned patches (new format) or modifiedContent (legacy)
358
+ if (mod.patches && mod.patches.length > 0) {
359
+ // New patch-based approach
360
+ console.log(`[Apply-First] Applying ${mod.patches.length} patches to ${mod.filePath}`);
361
+
362
+ const patchResult = applyPatches(originalContent, mod.patches);
363
+
364
+ if (!patchResult.success) {
365
+ const failedMessages = patchResult.failedPatches.map(
366
+ (f) => `Patch failed: ${f.error}`
367
+ ).join("\n");
368
+ patchErrors.push(`${mod.filePath}:\n${failedMessages}`);
369
+
370
+ // If some patches succeeded, use partial result
371
+ if (patchResult.appliedPatches > 0) {
372
+ console.warn(`[Apply-First] ${patchResult.appliedPatches}/${mod.patches.length} patches applied to ${mod.filePath}`);
373
+ modifiedContent = patchResult.modifiedContent;
374
+ explanation += ` (${patchResult.appliedPatches}/${mod.patches.length} patches applied)`;
375
+ } else {
376
+ continue; // Skip this file entirely if no patches worked
377
+ }
378
+ } else {
379
+ modifiedContent = patchResult.modifiedContent;
380
+ console.log(`[Apply-First] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
381
+ }
382
+ } else if (mod.modifiedContent) {
383
+ // Legacy: AI returned full file content
384
+ console.warn(`[Apply-First] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
385
+ modifiedContent = mod.modifiedContent;
386
+
387
+ // Validate the modification using legacy validation
388
+ const validation = validateModification(originalContent, modifiedContent, mod.filePath);
389
+ if (!validation.valid) {
390
+ patchErrors.push(`${mod.filePath}: ${validation.error}`);
391
+ continue;
392
+ }
393
+ } else {
394
+ // No patches and no modifiedContent - skip
395
+ console.warn(`[Apply-First] No patches or modifiedContent for ${mod.filePath}`);
396
+ continue;
369
397
  }
370
398
 
371
399
  modifications.push({
372
400
  filePath: mod.filePath,
373
401
  originalContent,
374
- modifiedContent: mod.modifiedContent,
375
- diff: generateSimpleDiff(originalContent, mod.modifiedContent),
376
- explanation: mod.explanation,
402
+ modifiedContent,
403
+ diff: generateSimpleDiff(originalContent, modifiedContent),
404
+ explanation,
377
405
  });
378
406
  }
379
407
 
380
- // If all modifications failed validation, return error
381
- if (validationErrors.length > 0 && modifications.length === 0) {
382
- console.error("All AI modifications failed validation:", validationErrors);
408
+ // If all modifications failed, return error
409
+ if (patchErrors.length > 0 && modifications.length === 0) {
410
+ console.error("All AI patches failed:", patchErrors);
383
411
  return NextResponse.json(
384
412
  {
385
413
  success: false,
386
- error: validationErrors.join("\n\n"),
414
+ error: `Patch application failed:\n\n${patchErrors.join("\n\n")}`,
387
415
  },
388
416
  { status: 400 }
389
417
  );
390
418
  }
391
419
 
392
- // Log warnings for review
393
- if (validationWarnings.length > 0) {
394
- console.warn("Vision apply validation warnings:", validationWarnings);
420
+ // Log patch errors as warnings if some modifications succeeded
421
+ if (patchErrors.length > 0) {
422
+ console.warn("Some patches failed:", patchErrors);
395
423
  }
396
424
 
397
425
  // Create backups and apply changes atomically
@@ -709,6 +737,76 @@ function generateSimpleDiff(original: string, modified: string): string {
709
737
  return diff.join("\n");
710
738
  }
711
739
 
740
+ /**
741
+ * Patch interface for search/replace operations
742
+ */
743
+ interface Patch {
744
+ search: string;
745
+ replace: string;
746
+ explanation: string;
747
+ }
748
+
749
+ /**
750
+ * Result of applying patches to a file
751
+ */
752
+ interface ApplyPatchesResult {
753
+ success: boolean;
754
+ modifiedContent: string;
755
+ appliedPatches: number;
756
+ failedPatches: { patch: Patch; error: string }[];
757
+ }
758
+
759
+ /**
760
+ * Apply search/replace patches to file content
761
+ * This is the core of the patch-based editing system
762
+ */
763
+ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesResult {
764
+ let content = originalContent;
765
+ let appliedPatches = 0;
766
+ const failedPatches: { patch: Patch; error: string }[] = [];
767
+
768
+ for (const patch of patches) {
769
+ // Normalize line endings for matching
770
+ const normalizedSearch = patch.search.replace(/\\n/g, "\n");
771
+ const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
772
+
773
+ // Check if search string exists in content
774
+ if (!content.includes(normalizedSearch)) {
775
+ // Try with different whitespace normalization
776
+ const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
777
+ const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
778
+
779
+ if (!regex.test(content)) {
780
+ failedPatches.push({
781
+ patch,
782
+ error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
783
+ });
784
+ continue;
785
+ }
786
+
787
+ // If regex matched, use regex replace
788
+ content = content.replace(regex, normalizedReplace);
789
+ appliedPatches++;
790
+ } else {
791
+ // Exact match found - apply the replacement
792
+ // Only replace the first occurrence to be safe
793
+ const index = content.indexOf(normalizedSearch);
794
+ content =
795
+ content.substring(0, index) +
796
+ normalizedReplace +
797
+ content.substring(index + normalizedSearch.length);
798
+ appliedPatches++;
799
+ }
800
+ }
801
+
802
+ return {
803
+ success: failedPatches.length === 0,
804
+ modifiedContent: content,
805
+ appliedPatches,
806
+ failedPatches,
807
+ };
808
+ }
809
+
712
810
  /**
713
811
  * Validate that AI modifications are surgical edits, not complete rewrites
714
812
  */
@@ -56,72 +56,65 @@ interface VisionEditResponse {
56
56
  error?: string;
57
57
  }
58
58
 
59
- const VISION_SYSTEM_PROMPT = `You are a React/Tailwind CSS expert with vision capabilities for the Sonance brand system.
59
+ const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
60
60
 
61
- You can see screenshots of web pages and understand their visual layout, then modify code to implement requested changes.
61
+ ═══════════════════════════════════════════════════════════════════════════════
62
+ UNDERSTAND THE USER'S REQUEST NATURALLY
63
+ ═══════════════════════════════════════════════════════════════════════════════
64
+
65
+ Read the user's request and do exactly what they ask. You know React, TypeScript, Tailwind CSS, and JSX - use that knowledge naturally.
66
+
67
+ - If they say "add X", ADD X to the code (don't replace or remove anything else)
68
+ - If they say "change X to Y", find X and change it to Y
69
+ - If they say "make X bigger/smaller/different color", adjust the relevant properties
70
+ - If they say "remove X", remove X
71
+ - If they say "wrap X with Y", add Y as a parent around X
72
+
73
+ For any change:
74
+ 1. Read the code context provided
75
+ 2. Understand what the user wants
76
+ 3. Generate patches that accomplish exactly that
77
+ 4. If adding a component/icon requires an import, include a patch for the import too
78
+
79
+ Don't overthink - just make the change the user requested.
62
80
 
63
81
  ═══════════════════════════════════════════════════════════════════════════════
64
- ABSOLUTE RULES - VIOLATION WILL BREAK THE APPLICATION
82
+ PATCH FORMAT
65
83
  ═══════════════════════════════════════════════════════════════════════════════
66
84
 
67
- **SURGICAL EDITING (MOST CRITICAL):**
68
- 1. You are making SURGICAL EDITS, not rewriting files
69
- 2. Change ONLY the specific lines that need modification
70
- 3. The modified file MUST have approximately the same line count as the original
71
- 4. If original file has 200 lines, modified file should have ~195-210 lines
72
- 5. NEVER rewrite a file from scratch - this destroys the application
73
-
74
- **FILE RULES:**
75
- 6. You may ONLY edit files that are provided in the PAGE CONTEXT section
76
- 7. NEVER create new files - only modify existing ones shown to you
77
- 8. The filePath in your response MUST exactly match one of the provided file paths
78
- 9. If you cannot find the right file to edit, explain this in your response instead of creating a new file
79
-
80
- **PRESERVATION RULES (ZERO TOLERANCE):**
81
- 10. NEVER delete or remove existing functions, hooks, state, or logic
82
- 11. NEVER remove imports - you may only ADD imports if needed
83
- 12. NEVER remove useEffect, useState, useCallback, or other React hooks
84
- 13. NEVER remove API calls, fetch requests, or data loading logic
85
- 14. NEVER remove error handling, loading states, or conditional rendering
86
- 15. NEVER remove data-sonance-* attributes
87
- 16. NEVER change component structure unless specifically requested
88
- 17. NEVER modify TypeScript types or interfaces unless specifically requested
89
-
90
- **CHANGE RULES:**
91
- 18. Make ONLY the changes requested by the user
92
- 19. Modify the MINIMUM amount of code necessary
93
- 20. Keep all existing className values and ADD to them if needed
94
- 21. Use semantic Tailwind classes (bg-primary, text-foreground, etc.)
95
- 22. Maintain dark mode compatibility with CSS variables
96
- 23. Keep the cn() utility for className merging
97
- 24. CRITICAL: You MUST return the FULL file content in "modifiedContent". Do NOT use comments like "// ... existing code ..." or "// ... rest of file ...". Return every single line of code, even if unchanged.
85
+ Return search/replace patches (NOT full files). The system applies your patches to the original.
86
+
87
+ **Patch Rules:**
88
+ - "search" must match the original code EXACTLY (including whitespace/indentation)
89
+ - "replace" contains your modified version
90
+ - Include 2-4 lines of context in "search" to make it unique
91
+ - You may ONLY edit files provided in the PAGE CONTEXT section
98
92
 
99
93
  **SONANCE BRAND COLORS:**
100
- - Charcoal: #333F48, #343D46 (primary)
101
- - Silver: #E2E2E2, #D1D1D6 (secondary)
94
+ - Charcoal: #333F48 (primary text)
102
95
  - IPORT Orange: #FC4C02
103
- - IPORT Dark: #0F161D
104
96
  - Blaze Blue: #00A3E1
105
- - Blaze Red: #C02B0A
106
97
 
107
98
  **RESPONSE FORMAT:**
108
- Return ONLY a valid JSON object. Do not include any conversational text before or after the JSON.
109
- The JSON must include:
110
- - "reasoning": Brief explanation of what you see in the screenshot and your plan
111
- - "modifications": Array of file modifications, each with:
112
- - "filePath": Path to the file
113
- - "modifiedContent": Complete updated file content (MUST BE FULL CONTENT, NO TRUNCATION)
114
- - "explanation": What changed in this file
115
- - "previewCSS": CSS for live preview (use [data-sonance-name="ComponentName"] selectors)
116
- - "aggregatedPreviewCSS": Combined CSS for all changes
117
- - "explanation": Overall summary of changes
118
-
119
- **EXAMPLE OF CORRECT EDIT:**
120
- If user asks to "make buttons smaller", you should:
121
- - Find the button elements in the code
122
- - Change ONLY the size-related classes (e.g., h-10 -> h-8, px-4 -> px-3)
123
- - Keep ALL other code exactly the same
124
- - Return the FULL file with this tiny change`;
99
+ Return ONLY valid JSON:
100
+ {
101
+ "reasoning": "What you understood from the request and your plan",
102
+ "modifications": [
103
+ {
104
+ "filePath": "path/to/file.tsx",
105
+ "patches": [
106
+ {
107
+ "search": "exact code to find",
108
+ "replace": "the replacement code",
109
+ "explanation": "what this patch does"
110
+ }
111
+ ],
112
+ "previewCSS": "optional CSS for live preview"
113
+ }
114
+ ],
115
+ "aggregatedPreviewCSS": "combined CSS for all changes",
116
+ "explanation": "summary of changes made"
117
+ }`;
125
118
 
126
119
  export async function POST(request: Request) {
127
120
  // Only allow in development
@@ -310,13 +303,15 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
310
303
  );
311
304
  }
312
305
 
313
- // Parse AI response
306
+ // Parse AI response - now expecting patches instead of full file content
314
307
  let aiResponse: {
315
308
  reasoning?: string;
316
309
  modifications: Array<{
317
310
  filePath: string;
318
- modifiedContent: string;
319
- explanation: string;
311
+ patches?: Patch[];
312
+ // Legacy support for modifiedContent (will be deprecated)
313
+ modifiedContent?: string;
314
+ explanation?: string;
320
315
  previewCSS?: string;
321
316
  }>;
322
317
  aggregatedPreviewCSS?: string;
@@ -381,10 +376,9 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
381
376
  );
382
377
  }
383
378
 
384
- // Read original content and generate diffs for each modification
379
+ // Process modifications - apply patches to get modified content
385
380
  const modificationsWithOriginals: VisionFileModification[] = [];
386
- const validationErrors: string[] = [];
387
- const validationWarnings: string[] = [];
381
+ const patchErrors: string[] = [];
388
382
 
389
383
  for (const mod of aiResponse.modifications || []) {
390
384
  const fullPath = path.join(projectRoot, mod.filePath);
@@ -393,43 +387,76 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
393
387
  originalContent = fs.readFileSync(fullPath, "utf-8");
394
388
  }
395
389
 
396
- // Validate the modification
397
- const validation = validateModification(originalContent, mod.modifiedContent, mod.filePath);
398
-
399
- if (!validation.valid) {
400
- validationErrors.push(validation.error || "Unknown validation error");
401
- continue; // Skip this modification
402
- }
403
-
404
- if (validation.warnings.length > 0) {
405
- validationWarnings.push(...validation.warnings.map(w => `${mod.filePath}: ${w}`));
390
+ let modifiedContent: string;
391
+ let explanation = mod.explanation || "";
392
+
393
+ // Check if AI returned patches (new format) or modifiedContent (legacy)
394
+ if (mod.patches && mod.patches.length > 0) {
395
+ // New patch-based approach
396
+ console.log(`[Vision Mode] Applying ${mod.patches.length} patches to ${mod.filePath}`);
397
+
398
+ const patchResult = applyPatches(originalContent, mod.patches);
399
+
400
+ if (!patchResult.success) {
401
+ const failedMessages = patchResult.failedPatches.map(
402
+ (f) => `Patch failed: ${f.error}`
403
+ ).join("\n");
404
+ patchErrors.push(`${mod.filePath}:\n${failedMessages}`);
405
+
406
+ // If some patches succeeded, use partial result
407
+ if (patchResult.appliedPatches > 0) {
408
+ console.warn(`[Vision Mode] ${patchResult.appliedPatches}/${mod.patches.length} patches applied to ${mod.filePath}`);
409
+ modifiedContent = patchResult.modifiedContent;
410
+ explanation += ` (${patchResult.appliedPatches}/${mod.patches.length} patches applied)`;
411
+ } else {
412
+ continue; // Skip this file entirely if no patches worked
413
+ }
414
+ } else {
415
+ modifiedContent = patchResult.modifiedContent;
416
+ console.log(`[Vision Mode] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
417
+ }
418
+ } else if (mod.modifiedContent) {
419
+ // Legacy: AI returned full file content
420
+ console.warn(`[Vision Mode] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
421
+ modifiedContent = mod.modifiedContent;
422
+
423
+ // Validate the modification using legacy validation
424
+ const validation = validateModification(originalContent, modifiedContent, mod.filePath);
425
+ if (!validation.valid) {
426
+ patchErrors.push(`${mod.filePath}: ${validation.error}`);
427
+ continue;
428
+ }
429
+ } else {
430
+ // No patches and no modifiedContent - skip
431
+ console.warn(`[Vision Mode] No patches or modifiedContent for ${mod.filePath}`);
432
+ continue;
406
433
  }
407
434
 
408
435
  modificationsWithOriginals.push({
409
436
  filePath: mod.filePath,
410
437
  originalContent,
411
- modifiedContent: mod.modifiedContent,
412
- diff: generateSimpleDiff(originalContent, mod.modifiedContent),
413
- explanation: mod.explanation,
438
+ modifiedContent,
439
+ diff: generateSimpleDiff(originalContent, modifiedContent),
440
+ explanation,
414
441
  previewCSS: mod.previewCSS,
415
442
  });
416
443
  }
417
444
 
418
- // If all modifications failed validation, return error
419
- if (validationErrors.length > 0 && modificationsWithOriginals.length === 0) {
420
- console.error("All AI modifications failed validation:", validationErrors);
445
+ // If all modifications failed, return error
446
+ if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
447
+ console.error("All AI patches failed:", patchErrors);
421
448
  return NextResponse.json(
422
449
  {
423
450
  success: false,
424
- error: validationErrors.join("\n\n"),
451
+ error: `Patch application failed:\n\n${patchErrors.join("\n\n")}`,
425
452
  } as VisionEditResponse,
426
453
  { status: 400 }
427
454
  );
428
455
  }
429
456
 
430
- // Log warnings for review
431
- if (validationWarnings.length > 0) {
432
- console.warn("Vision edit validation warnings:", validationWarnings);
457
+ // Log patch errors as warnings if some modifications succeeded
458
+ if (patchErrors.length > 0) {
459
+ console.warn("Some patches failed:", patchErrors);
433
460
  }
434
461
 
435
462
  // Aggregate preview CSS
@@ -701,6 +728,76 @@ function generateSimpleDiff(original: string, modified: string): string {
701
728
  return diff.join("\n");
702
729
  }
703
730
 
731
+ /**
732
+ * Patch interface for search/replace operations
733
+ */
734
+ interface Patch {
735
+ search: string;
736
+ replace: string;
737
+ explanation: string;
738
+ }
739
+
740
+ /**
741
+ * Result of applying patches to a file
742
+ */
743
+ interface ApplyPatchesResult {
744
+ success: boolean;
745
+ modifiedContent: string;
746
+ appliedPatches: number;
747
+ failedPatches: { patch: Patch; error: string }[];
748
+ }
749
+
750
+ /**
751
+ * Apply search/replace patches to file content
752
+ * This is the core of the patch-based editing system
753
+ */
754
+ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesResult {
755
+ let content = originalContent;
756
+ let appliedPatches = 0;
757
+ const failedPatches: { patch: Patch; error: string }[] = [];
758
+
759
+ for (const patch of patches) {
760
+ // Normalize line endings for matching
761
+ const normalizedSearch = patch.search.replace(/\\n/g, "\n");
762
+ const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
763
+
764
+ // Check if search string exists in content
765
+ if (!content.includes(normalizedSearch)) {
766
+ // Try with different whitespace normalization
767
+ const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
768
+ const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
769
+
770
+ if (!regex.test(content)) {
771
+ failedPatches.push({
772
+ patch,
773
+ error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
774
+ });
775
+ continue;
776
+ }
777
+
778
+ // If regex matched, use regex replace
779
+ content = content.replace(regex, normalizedReplace);
780
+ appliedPatches++;
781
+ } else {
782
+ // Exact match found - apply the replacement
783
+ // Only replace the first occurrence to be safe
784
+ const index = content.indexOf(normalizedSearch);
785
+ content =
786
+ content.substring(0, index) +
787
+ normalizedReplace +
788
+ content.substring(index + normalizedSearch.length);
789
+ appliedPatches++;
790
+ }
791
+ }
792
+
793
+ return {
794
+ success: failedPatches.length === 0,
795
+ modifiedContent: content,
796
+ appliedPatches,
797
+ failedPatches,
798
+ };
799
+ }
800
+
704
801
  /**
705
802
  * Validate that AI modifications are surgical edits, not complete rewrites
706
803
  */
@@ -57,6 +57,7 @@ export function ChatInterface({
57
57
  const [input, setInput] = useState("");
58
58
  const [isProcessing, setIsProcessing] = useState(false);
59
59
  const messagesEndRef = useRef<HTMLDivElement>(null);
60
+ const inputRef = useRef<HTMLInputElement>(null);
60
61
 
61
62
  // Scroll to bottom when messages change
62
63
  useEffect(() => {
@@ -126,6 +127,7 @@ export function ChatInterface({
126
127
 
127
128
  setMessages((prev) => [...prev, userMessage]);
128
129
  setInput("");
130
+ if (inputRef.current) inputRef.current.value = "";
129
131
  setIsProcessing(true);
130
132
 
131
133
  try {
@@ -216,11 +218,14 @@ export function ChatInterface({
216
218
  };
217
219
 
218
220
  const handleSend = async (prompt: string) => {
219
- if (!prompt.trim() || isProcessing) return;
221
+ // Fallback: read from DOM if React state is empty (browser automation compatibility)
222
+ const actualPrompt = prompt || inputRef.current?.value || "";
223
+
224
+ if (!actualPrompt.trim() || isProcessing) return;
220
225
 
221
226
  // Use vision mode handler if vision mode is active
222
227
  if (visionMode) {
223
- return handleVisionEdit(prompt);
228
+ return handleVisionEdit(actualPrompt);
224
229
  }
225
230
 
226
231
  // If no component is selected, intercept the request
@@ -228,11 +233,12 @@ export function ChatInterface({
228
233
  const userMessage: ChatMessage = {
229
234
  id: `msg-${Date.now()}`,
230
235
  role: "user",
231
- content: prompt,
236
+ content: actualPrompt,
232
237
  timestamp: new Date(),
233
238
  };
234
239
  setMessages((prev) => [...prev, userMessage]);
235
240
  setInput("");
241
+ if (inputRef.current) inputRef.current.value = "";
236
242
 
237
243
  setTimeout(() => {
238
244
  const assistantMessage: ChatMessage = {
@@ -249,12 +255,13 @@ export function ChatInterface({
249
255
  const userMessage: ChatMessage = {
250
256
  id: `msg-${Date.now()}`,
251
257
  role: "user",
252
- content: prompt,
258
+ content: actualPrompt,
253
259
  timestamp: new Date(),
254
260
  };
255
261
 
256
262
  setMessages((prev) => [...prev, userMessage]);
257
263
  setInput("");
264
+ if (inputRef.current) inputRef.current.value = "";
258
265
  setIsProcessing(true);
259
266
 
260
267
  try {
@@ -285,7 +292,7 @@ export function ChatInterface({
285
292
  componentType,
286
293
  filePath,
287
294
  currentCode: sourceData.content,
288
- userRequest: prompt,
295
+ userRequest: actualPrompt,
289
296
  // Variant-scoped editing context
290
297
  editScope,
291
298
  variantId: editScope === "variant" ? variantId : undefined,
@@ -390,13 +397,14 @@ export function ChatInterface({
390
397
  {/* Input */}
391
398
  <div className="flex gap-2">
392
399
  <input
400
+ ref={inputRef}
393
401
  type="text"
394
402
  value={input}
395
403
  onChange={(e) => setInput(e.target.value)}
396
404
  onKeyDown={(e) => {
397
405
  if (e.key === "Enter" && !e.shiftKey) {
398
406
  e.preventDefault();
399
- handleSend(input);
407
+ handleSend(input || inputRef.current?.value || "");
400
408
  }
401
409
  }}
402
410
  placeholder={
@@ -418,8 +426,8 @@ export function ChatInterface({
418
426
  )}
419
427
  />
420
428
  <button
421
- onClick={() => handleSend(input)}
422
- disabled={isProcessing || !input.trim()}
429
+ onClick={() => handleSend(input || inputRef.current?.value || "")}
430
+ disabled={isProcessing}
423
431
  className={cn(
424
432
  "px-3 py-2 rounded transition-colors",
425
433
  visionMode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.24",
3
+ "version": "1.3.25",
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",