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
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
PATCH FORMAT
|
|
64
82
|
═══════════════════════════════════════════════════════════════════════════════
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
305
|
-
|
|
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
|
-
//
|
|
343
|
+
// Process modifications - apply patches to get modified content
|
|
348
344
|
const modifications: VisionFileModification[] = [];
|
|
349
|
-
const
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
375
|
-
diff: generateSimpleDiff(originalContent,
|
|
376
|
-
explanation
|
|
402
|
+
modifiedContent,
|
|
403
|
+
diff: generateSimpleDiff(originalContent, modifiedContent),
|
|
404
|
+
explanation,
|
|
377
405
|
});
|
|
378
406
|
}
|
|
379
407
|
|
|
380
|
-
// If all modifications failed
|
|
381
|
-
if (
|
|
382
|
-
console.error("All AI
|
|
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:
|
|
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
|
|
393
|
-
if (
|
|
394
|
-
console.warn("
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
PATCH FORMAT
|
|
65
83
|
═══════════════════════════════════════════════════════════════════════════════
|
|
66
84
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
//
|
|
379
|
+
// Process modifications - apply patches to get modified content
|
|
385
380
|
const modificationsWithOriginals: VisionFileModification[] = [];
|
|
386
|
-
const
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
412
|
-
diff: generateSimpleDiff(originalContent,
|
|
413
|
-
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
|
|
419
|
-
if (
|
|
420
|
-
console.error("All AI
|
|
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:
|
|
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
|
|
431
|
-
if (
|
|
432
|
-
console.warn("
|
|
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 (
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
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",
|