sonance-brand-mcp 1.3.56 → 1.3.58

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.
@@ -40,12 +40,14 @@ interface VisionFileModification {
40
40
  }
41
41
 
42
42
  interface ApplyFirstRequest {
43
- action: "apply" | "accept" | "revert";
43
+ action: "apply" | "accept" | "revert" | "preview";
44
44
  sessionId?: string;
45
45
  screenshot?: string;
46
46
  pageRoute?: string;
47
47
  userPrompt?: string;
48
48
  focusedElements?: VisionFocusedElement[];
49
+ // For applying a previously previewed set of modifications
50
+ previewedModifications?: VisionFileModification[];
49
51
  }
50
52
 
51
53
  interface BackupManifest {
@@ -436,102 +438,41 @@ function searchFilesSmart(
436
438
  return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
437
439
  }
438
440
 
439
- const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
440
-
441
- ═══════════════════════════════════════════════════════════════════════════════
442
- UNDERSTAND THE USER'S REQUEST NATURALLY
443
- ═══════════════════════════════════════════════════════════════════════════════
444
-
445
- Read the user's request and do exactly what they ask. You know React, TypeScript, Tailwind CSS, and JSX - use that knowledge naturally.
446
-
447
- - If they say "add X", ADD X to the code (don't replace or remove anything else)
448
- - If they say "change X to Y", find X and change it to Y
449
- - If they say "make X bigger/smaller/different color", adjust the relevant properties
450
- - If they say "remove X", remove X
451
- - If they say "wrap X with Y", add Y as a parent around X
452
-
453
- For any change:
454
- 1. Read the code context provided
455
- 2. Understand what the user wants
456
- 3. Generate patches that accomplish exactly that
457
- 4. If adding a component/icon requires an import, include a patch for the import too
458
-
459
- Don't overthink - just make the change the user requested.
460
-
461
- ═══════════════════════════════════════════════════════════════════════════════
462
- PATCH FORMAT
463
- ═══════════════════════════════════════════════════════════════════════════════
464
-
465
- Return search/replace patches (NOT full files). The system applies your patches to the original.
466
-
467
- **Patch Rules:**
468
- - "search" must match the original code EXACTLY (including whitespace/indentation)
469
- - "replace" contains your modified version
470
- - Include 2-4 lines of context in "search" to make it unique
471
- - You may ONLY edit files provided in the PAGE CONTEXT section
472
- - CRITICAL: NEVER invent or guess code. Your "search" string MUST be copied EXACTLY from the provided file content. If you cannot find the exact code to modify, return an empty modifications array.
473
- - If the file content appears truncated, only modify code that is visible in the provided content.
474
-
475
- ═══════════════════════════════════════════════════════════════════════════════
476
- SONANCE BRAND COLOR SYSTEM
477
- ═══════════════════════════════════════════════════════════════════════════════
478
-
479
- **Core Colors:**
480
- - Primary (Charcoal): #333F48 - main text, dark backgrounds
481
- - Accent (Cyan "The Beam"): #00D3C8 - highlights, interactive elements, CTAs
482
- - Success: Green tones - positive states, confirmations
483
- - Warning: Amber/Orange tones - caution states, alerts
484
- - Destructive: Red tones - errors, delete actions
485
-
486
- **CRITICAL CONTRAST RULES:**
487
- When using colored backgrounds, ALWAYS use high-contrast text:
488
-
489
- | Background | Use This Text | NEVER Use |
490
- |-------------------|------------------------------------|-----------------------|
491
- | bg-accent | text-white | text-accent-foreground |
492
- | bg-primary | text-primary-foreground, text-white | text-primary |
493
- | bg-success | text-white | text-success |
494
- | bg-warning | text-white | text-warning |
495
- | bg-destructive | text-white | text-destructive |
496
-
497
- **SEMANTIC TOKEN PATTERNS:**
498
- - text-{color} = the color itself (use on NEUTRAL backgrounds like white/gray)
499
- - text-{color}-foreground = INTENDED for text on {color} backgrounds, but MAY have contrast issues
500
- - bg-{color} = background in that color
501
- - WHEN IN DOUBT: Use text-white on any colored background for guaranteed contrast
502
-
503
- **BUTTON PATTERNS (Sonance Standard):**
504
- - Primary CTA: bg-primary text-primary-foreground
505
- - Accent/Highlight: bg-accent text-white (NOT text-accent-foreground)
506
- - Success: bg-success text-white
507
- - Warning: bg-warning text-white
508
- - Destructive: bg-destructive text-white
509
- - Outlined: border-border bg-transparent text-foreground
510
-
511
- **COMMON MISTAKES TO AVOID:**
512
- - WRONG: bg-accent text-accent-foreground (accent-foreground is often dark = invisible text)
513
- - RIGHT: bg-accent text-white (white text on cyan = visible)
514
- - WRONG: bg-primary text-primary (same color = invisible)
515
- - RIGHT: bg-primary text-primary-foreground OR text-white
516
-
517
- **RESPONSE FORMAT:**
518
- CRITICAL: Return ONLY the JSON object below. Do NOT include any text, explanation, or thinking before or after the JSON. No preamble. No "Looking at the screenshot..." No markdown code blocks. Just raw JSON:
441
+ const VISION_SYSTEM_PROMPT = `You are a code editor. Make ONLY the change the user requested.
442
+
443
+ 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):
519
462
  {
520
- "reasoning": "What you understood from the request and your plan",
521
- "modifications": [
522
- {
523
- "filePath": "path/to/file.tsx",
524
- "patches": [
525
- {
526
- "search": "exact code to find",
527
- "replace": "the replacement code",
528
- "explanation": "what this patch does"
529
- }
530
- ]
531
- }
532
- ],
533
- "explanation": "summary of changes made"
534
- }`;
463
+ "reasoning": "brief explanation",
464
+ "modifications": [{
465
+ "filePath": "path/to/file.tsx",
466
+ "patches": [{
467
+ "search": "exact original code (copy from provided file)",
468
+ "replace": "minimal change",
469
+ "explanation": "what this does"
470
+ }]
471
+ }],
472
+ "explanation": "summary"
473
+ }
474
+
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.`;
535
476
 
536
477
  export async function POST(request: Request) {
537
478
  // Only allow in development
@@ -544,7 +485,7 @@ export async function POST(request: Request) {
544
485
 
545
486
  try {
546
487
  const body: ApplyFirstRequest = await request.json();
547
- const { action, sessionId, screenshot, pageRoute, userPrompt, focusedElements } = body;
488
+ const { action, sessionId, screenshot, pageRoute, userPrompt, focusedElements, previewedModifications } = body;
548
489
  const projectRoot = process.cwd();
549
490
 
550
491
  // ========== ACCEPT ACTION ==========
@@ -571,6 +512,8 @@ export async function POST(request: Request) {
571
512
 
572
513
  // ========== REVERT ACTION ==========
573
514
  if (action === "revert") {
515
+ console.log(`[Apply-First] Revert requested for session: ${sessionId}`);
516
+
574
517
  if (!sessionId) {
575
518
  return NextResponse.json(
576
519
  { error: "sessionId is required for revert action" },
@@ -579,6 +522,7 @@ export async function POST(request: Request) {
579
522
  }
580
523
 
581
524
  const result = await revertFromBackups(sessionId, projectRoot);
525
+ console.log(`[Apply-First] Revert result:`, result);
582
526
 
583
527
  return NextResponse.json({
584
528
  success: result.success,
@@ -588,11 +532,44 @@ export async function POST(request: Request) {
588
532
  });
589
533
  }
590
534
 
591
- // ========== APPLY ACTION ==========
592
- if (action === "apply") {
535
+ // ========== APPLY FROM PREVIEW ACTION ==========
536
+ // When user accepts a preview, apply the previously generated modifications
537
+ if (action === "apply" && previewedModifications && previewedModifications.length > 0) {
538
+ debugLog("Applying previewed modifications", {
539
+ count: previewedModifications.length,
540
+ files: previewedModifications.map(m => m.filePath)
541
+ });
542
+
543
+ const newSessionId = randomUUID().slice(0, 8);
544
+
545
+ const applyResult = await applyChangesWithBackup(
546
+ previewedModifications,
547
+ newSessionId,
548
+ projectRoot
549
+ );
550
+
551
+ if (!applyResult.success) {
552
+ return NextResponse.json(
553
+ { error: applyResult.error || "Failed to apply previewed changes" },
554
+ { status: 500 }
555
+ );
556
+ }
557
+
558
+ return NextResponse.json({
559
+ success: true,
560
+ sessionId: newSessionId,
561
+ modifications: previewedModifications,
562
+ backupPaths: applyResult.backupPaths,
563
+ explanation: "Applied previewed changes",
564
+ });
565
+ }
566
+
567
+ // ========== APPLY/PREVIEW ACTION (with LLM) ==========
568
+ // Both actions do LLM work; "preview" returns without writing, "apply" writes to files
569
+ if (action === "apply" || action === "preview") {
593
570
  if (!userPrompt) {
594
571
  return NextResponse.json(
595
- { error: "userPrompt is required for apply action" },
572
+ { error: "userPrompt is required for apply/preview action" },
596
573
  { status: 400 }
597
574
  );
598
575
  }
@@ -699,111 +676,6 @@ export async function POST(request: Request) {
699
676
  });
700
677
  }
701
678
 
702
- // ========== PHASE A: VISUAL PROBLEM ANALYSIS ==========
703
- // Before generating patches, analyze WHAT is visually wrong
704
- // This forces the LLM to articulate the problem before trying to fix it
705
- interface VisualAnalysis {
706
- element: string;
707
- problem: string;
708
- currentState: string;
709
- solution: string;
710
- confidence: "high" | "medium" | "low";
711
- }
712
-
713
- let visualAnalysis: VisualAnalysis | null = null;
714
-
715
- if (screenshot && userPrompt) {
716
- debugLog("Phase A: Starting visual problem analysis");
717
-
718
- const analysisClient = new Anthropic({ apiKey });
719
-
720
- try {
721
- const analysisResponse = await analysisClient.messages.create({
722
- model: "claude-sonnet-4-20250514",
723
- max_tokens: 1024,
724
- messages: [
725
- {
726
- role: "user",
727
- content: [
728
- {
729
- type: "image",
730
- source: {
731
- type: "base64",
732
- media_type: "image/png",
733
- data: base64Data,
734
- },
735
- },
736
- {
737
- type: "text",
738
- text: `You are analyzing a UI screenshot to understand a visual problem.
739
-
740
- User request: "${userPrompt}"
741
-
742
- ${focusedElements && focusedElements.length > 0 ? `User clicked on these elements:
743
- ${focusedElements.map((el) => `- ${el.name} (${el.type}) at position (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
744
- ` : ""}
745
-
746
- BEFORE any code changes can be made, you must analyze the visual problem.
747
-
748
- Answer these questions:
749
- 1. ELEMENT: Which specific UI element has the problem? (describe its location, text content, appearance)
750
- 2. PROBLEM: What exactly is visually wrong? (invisible text, wrong color, poor contrast, wrong size, etc.)
751
- 3. CURRENT STATE: What styling/colors appear to be applied to this element?
752
- 4. SOLUTION: What specific CSS/Tailwind change would fix this problem?
753
- 5. CONFIDENCE: How confident are you that you've identified the correct element and problem? (high/medium/low)
754
-
755
- Return ONLY valid JSON with no other text:
756
- {
757
- "element": "specific description of the UI element",
758
- "problem": "what is visually wrong",
759
- "currentState": "what styling appears to be applied",
760
- "solution": "specific CSS/Tailwind fix needed",
761
- "confidence": "high"
762
- }`,
763
- },
764
- ],
765
- },
766
- ],
767
- });
768
-
769
- const analysisText = analysisResponse.content.find((block) => block.type === "text");
770
- if (analysisText && analysisText.type === "text") {
771
- // Parse the JSON response
772
- let jsonText = analysisText.text.trim();
773
- // Extract JSON if wrapped in code blocks
774
- const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || jsonText.match(/(\{[\s\S]*\})/);
775
- if (jsonMatch) {
776
- jsonText = jsonMatch[1];
777
- }
778
-
779
- try {
780
- visualAnalysis = JSON.parse(jsonText) as VisualAnalysis;
781
- debugLog("Phase A: Visual problem analysis complete", visualAnalysis);
782
-
783
- // If low confidence, return early with clarification request
784
- if (visualAnalysis.confidence === "low") {
785
- debugLog("Phase A: Low confidence - requesting clarification");
786
- return NextResponse.json({
787
- success: false,
788
- needsClarification: true,
789
- analysis: visualAnalysis,
790
- message: `I can see "${visualAnalysis.element}" but I'm not certain about the problem: "${visualAnalysis.problem}". Can you be more specific about what needs to change?`,
791
- });
792
- }
793
- } catch (parseError) {
794
- debugLog("Phase A: Failed to parse analysis response", {
795
- error: String(parseError),
796
- response: jsonText.substring(0, 500)
797
- });
798
- // Continue without analysis if parsing fails
799
- }
800
- }
801
- } catch (analysisError) {
802
- debugLog("Phase A: Analysis call failed", { error: String(analysisError) });
803
- // Continue without analysis - fall back to existing behavior
804
- }
805
- }
806
-
807
679
  // ========== SMART CONTEXT BUDGETING ==========
808
680
  // Claude can handle 200k tokens (~800k chars), so we can safely include large files
809
681
  // Priority: Recommended file (NEVER truncate) > Page file (limited) > Other components (dynamic)
@@ -837,24 +709,6 @@ User Request: "${userPrompt}"
837
709
  textContent += `FOCUSED ELEMENTS (user clicked on these):
838
710
  ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
839
711
 
840
- `;
841
- }
842
-
843
- // ========== VISUAL PROBLEM ANALYSIS (from Phase A) ==========
844
- if (visualAnalysis) {
845
- textContent += `═══════════════════════════════════════════════════════════════════════════════
846
- 🔍 VISUAL PROBLEM ANALYSIS (I analyzed the screenshot first)
847
- ═══════════════════════════════════════════════════════════════════════════════
848
-
849
- **Element:** ${visualAnalysis.element}
850
- **Problem:** ${visualAnalysis.problem}
851
- **Current State:** ${visualAnalysis.currentState}
852
- **Required Fix:** ${visualAnalysis.solution}
853
- **Confidence:** ${visualAnalysis.confidence}
854
-
855
- ⚠️ IMPORTANT: Your patches MUST implement the fix described above for the element described above.
856
- Find the code that renders "${visualAnalysis.element}" and apply "${visualAnalysis.solution}".
857
-
858
712
  `;
859
713
  }
860
714
 
@@ -922,17 +776,27 @@ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
922
776
  }
923
777
  }
924
778
 
925
- // ========== THEME DISCOVERY ==========
779
+ // ========== THEME DISCOVERY (REFERENCE ONLY) ==========
926
780
  // 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
927
782
  const discoveredTheme = await discoverTheme(projectRoot);
928
783
  const themeContext = formatThemeForPrompt(discoveredTheme);
929
784
 
930
785
  if (discoveredTheme.discoveredFiles.length > 0) {
931
786
  textContent += `
932
787
  ═══════════════════════════════════════════════════════════════════════════════
933
- ${themeContext}
788
+ REFERENCE ONLY (do not use this to justify additional changes)
934
789
  ═══════════════════════════════════════════════════════════════════════════════
935
790
 
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)
796
+
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.
799
+
936
800
  `;
937
801
  debugLog("Theme discovery complete", {
938
802
  filesFound: discoveredTheme.discoveredFiles,
@@ -1305,7 +1169,23 @@ This is better than generating patches with made-up code.`,
1305
1169
  break;
1306
1170
  } // End of retry loop
1307
1171
 
1308
- // Create backups and apply changes atomically
1172
+ // ========== PREVIEW MODE: Return modifications without writing ==========
1173
+ if (action === "preview") {
1174
+ debugLog("Preview mode: returning modifications without applying", {
1175
+ fileCount: modifications.length,
1176
+ files: modifications.map(m => m.filePath)
1177
+ });
1178
+
1179
+ return NextResponse.json({
1180
+ success: true,
1181
+ preview: true,
1182
+ modifications,
1183
+ explanation: finalExplanation,
1184
+ reasoning: finalReasoning,
1185
+ });
1186
+ }
1187
+
1188
+ // ========== APPLY MODE: Create backups and apply changes atomically ==========
1309
1189
  const applyResult = await applyChangesWithBackup(
1310
1190
  modifications,
1311
1191
  newSessionId,
@@ -1330,7 +1210,7 @@ This is better than generating patches with made-up code.`,
1330
1210
  }
1331
1211
 
1332
1212
  return NextResponse.json(
1333
- { error: "Invalid action. Use 'apply', 'accept', or 'revert'." },
1213
+ { error: "Invalid action. Use 'apply', 'preview', 'accept', or 'revert'." },
1334
1214
  { status: 400 }
1335
1215
  );
1336
1216
  } catch (error) {
@@ -1350,6 +1230,12 @@ async function applyChangesWithBackup(
1350
1230
  sessionId: string,
1351
1231
  projectRoot: string
1352
1232
  ): Promise<{ success: boolean; backupPaths: string[]; error?: string }> {
1233
+ console.log(`[Apply-First] applyChangesWithBackup called`, {
1234
+ sessionId,
1235
+ fileCount: modifications.length,
1236
+ files: modifications.map(m => m.filePath)
1237
+ });
1238
+
1353
1239
  const backupDir = path.join(projectRoot, BACKUP_ROOT, sessionId);
1354
1240
  const backupPaths: string[] = [];
1355
1241
 
@@ -1364,6 +1250,7 @@ async function applyChangesWithBackup(
1364
1250
 
1365
1251
  try {
1366
1252
  // Step 1: Create backup directory
1253
+ console.log(`[Apply-First] Creating backup directory: ${backupDir}`);
1367
1254
  fs.mkdirSync(backupDir, { recursive: true });
1368
1255
 
1369
1256
  // Step 2: Create ALL backups FIRST (before any writes)
@@ -1377,6 +1264,9 @@ async function applyChangesWithBackup(
1377
1264
  fs.mkdirSync(backupDirForFile, { recursive: true });
1378
1265
  fs.copyFileSync(fullPath, backupPath);
1379
1266
  backupPaths.push(backupPath);
1267
+ console.log(`[Apply-First] Backed up: ${mod.filePath}`);
1268
+ } else {
1269
+ console.log(`[Apply-First] New file (no backup needed): ${mod.filePath}`);
1380
1270
  }
1381
1271
  }
1382
1272
 
@@ -1391,10 +1281,9 @@ async function applyChangesWithBackup(
1391
1281
  })),
1392
1282
  };
1393
1283
 
1394
- fs.writeFileSync(
1395
- path.join(backupDir, "manifest.json"),
1396
- JSON.stringify(manifest, null, 2)
1397
- );
1284
+ const manifestPath = path.join(backupDir, "manifest.json");
1285
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1286
+ console.log(`[Apply-First] Manifest written: ${manifestPath}`);
1398
1287
 
1399
1288
  // Step 4: Apply all changes
1400
1289
  for (const mod of modifications) {
@@ -1412,8 +1301,10 @@ async function applyChangesWithBackup(
1412
1301
  }
1413
1302
 
1414
1303
  fs.writeFileSync(fullPath, mod.modifiedContent, "utf-8");
1304
+ console.log(`[Apply-First] File written: ${mod.filePath}`);
1415
1305
  }
1416
1306
 
1307
+ console.log(`[Apply-First] All changes applied successfully, session: ${sessionId}`);
1417
1308
  return { success: true, backupPaths };
1418
1309
  } catch (error) {
1419
1310
  // Rollback on error - restore all backed up files
@@ -434,104 +434,43 @@ function searchFilesSmart(
434
434
  return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
435
435
  }
436
436
 
437
- const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
438
-
439
- ═══════════════════════════════════════════════════════════════════════════════
440
- UNDERSTAND THE USER'S REQUEST NATURALLY
441
- ═══════════════════════════════════════════════════════════════════════════════
442
-
443
- Read the user's request and do exactly what they ask. You know React, TypeScript, Tailwind CSS, and JSX - use that knowledge naturally.
444
-
445
- - If they say "add X", ADD X to the code (don't replace or remove anything else)
446
- - If they say "change X to Y", find X and change it to Y
447
- - If they say "make X bigger/smaller/different color", adjust the relevant properties
448
- - If they say "remove X", remove X
449
- - If they say "wrap X with Y", add Y as a parent around X
450
-
451
- For any change:
452
- 1. Read the code context provided
453
- 2. Understand what the user wants
454
- 3. Generate patches that accomplish exactly that
455
- 4. If adding a component/icon requires an import, include a patch for the import too
456
-
457
- Don't overthink - just make the change the user requested.
458
-
459
- ═══════════════════════════════════════════════════════════════════════════════
460
- PATCH FORMAT
461
- ═══════════════════════════════════════════════════════════════════════════════
462
-
463
- Return search/replace patches (NOT full files). The system applies your patches to the original.
464
-
465
- **Patch Rules:**
466
- - "search" must match the original code EXACTLY (including whitespace/indentation)
467
- - "replace" contains your modified version
468
- - Include 2-4 lines of context in "search" to make it unique
469
- - You may ONLY edit files provided in the PAGE CONTEXT section
470
- - CRITICAL: NEVER invent or guess code. Your "search" string MUST be copied EXACTLY from the provided file content. If you cannot find the exact code to modify, return an empty modifications array.
471
- - If the file content appears truncated, only modify code that is visible in the provided content.
472
-
473
- ═══════════════════════════════════════════════════════════════════════════════
474
- SONANCE BRAND COLOR SYSTEM
475
- ═══════════════════════════════════════════════════════════════════════════════
476
-
477
- **Core Colors:**
478
- - Primary (Charcoal): #333F48 - main text, dark backgrounds
479
- - Accent (Cyan "The Beam"): #00D3C8 - highlights, interactive elements, CTAs
480
- - Success: Green tones - positive states, confirmations
481
- - Warning: Amber/Orange tones - caution states, alerts
482
- - Destructive: Red tones - errors, delete actions
483
-
484
- **CRITICAL CONTRAST RULES:**
485
- When using colored backgrounds, ALWAYS use high-contrast text:
486
-
487
- | Background | Use This Text | NEVER Use |
488
- |-------------------|------------------------------------|-----------------------|
489
- | bg-accent | text-white | text-accent-foreground |
490
- | bg-primary | text-primary-foreground, text-white | text-primary |
491
- | bg-success | text-white | text-success |
492
- | bg-warning | text-white | text-warning |
493
- | bg-destructive | text-white | text-destructive |
494
-
495
- **SEMANTIC TOKEN PATTERNS:**
496
- - text-{color} = the color itself (use on NEUTRAL backgrounds like white/gray)
497
- - text-{color}-foreground = INTENDED for text on {color} backgrounds, but MAY have contrast issues
498
- - bg-{color} = background in that color
499
- - WHEN IN DOUBT: Use text-white on any colored background for guaranteed contrast
500
-
501
- **BUTTON PATTERNS (Sonance Standard):**
502
- - Primary CTA: bg-primary text-primary-foreground
503
- - Accent/Highlight: bg-accent text-white (NOT text-accent-foreground)
504
- - Success: bg-success text-white
505
- - Warning: bg-warning text-white
506
- - Destructive: bg-destructive text-white
507
- - Outlined: border-border bg-transparent text-foreground
508
-
509
- **COMMON MISTAKES TO AVOID:**
510
- - WRONG: bg-accent text-accent-foreground (accent-foreground is often dark = invisible text)
511
- - RIGHT: bg-accent text-white (white text on cyan = visible)
512
- - WRONG: bg-primary text-primary (same color = invisible)
513
- - RIGHT: bg-primary text-primary-foreground OR text-white
514
-
515
- **RESPONSE FORMAT:**
516
- CRITICAL: Return ONLY the JSON object below. Do NOT include any text, explanation, or thinking before or after the JSON. No preamble. No "Looking at the screenshot..." No markdown code blocks. Just raw JSON:
437
+ const VISION_SYSTEM_PROMPT = `You are a code editor. Make ONLY the change the user requested.
438
+
439
+ 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):
517
458
  {
518
- "reasoning": "What you understood from the request and your plan",
519
- "modifications": [
520
- {
521
- "filePath": "path/to/file.tsx",
522
- "patches": [
523
- {
524
- "search": "exact code to find",
525
- "replace": "the replacement code",
526
- "explanation": "what this patch does"
527
- }
528
- ],
529
- "previewCSS": "optional CSS for live preview"
530
- }
531
- ],
459
+ "reasoning": "brief explanation",
460
+ "modifications": [{
461
+ "filePath": "path/to/file.tsx",
462
+ "patches": [{
463
+ "search": "exact original code (copy from provided file)",
464
+ "replace": "minimal change",
465
+ "explanation": "what this does"
466
+ }],
467
+ "previewCSS": "optional CSS for live preview"
468
+ }],
532
469
  "aggregatedPreviewCSS": "combined CSS for all changes",
533
- "explanation": "summary of changes made"
534
- }`;
470
+ "explanation": "summary"
471
+ }
472
+
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.`;
535
474
 
536
475
  export async function POST(request: Request) {
537
476
  // Only allow in development
@@ -708,111 +647,6 @@ export async function POST(request: Request) {
708
647
  });
709
648
  }
710
649
 
711
- // ========== PHASE A: VISUAL PROBLEM ANALYSIS ==========
712
- // Before generating patches, analyze WHAT is visually wrong
713
- // This forces the LLM to articulate the problem before trying to fix it
714
- interface VisualAnalysis {
715
- element: string;
716
- problem: string;
717
- currentState: string;
718
- solution: string;
719
- confidence: "high" | "medium" | "low";
720
- }
721
-
722
- let visualAnalysis: VisualAnalysis | null = null;
723
-
724
- if (screenshot && userPrompt) {
725
- debugLog("Phase A: Starting visual problem analysis");
726
-
727
- const analysisClient = new Anthropic({ apiKey });
728
-
729
- try {
730
- const analysisResponse = await analysisClient.messages.create({
731
- model: "claude-sonnet-4-20250514",
732
- max_tokens: 1024,
733
- messages: [
734
- {
735
- role: "user",
736
- content: [
737
- {
738
- type: "image",
739
- source: {
740
- type: "base64",
741
- media_type: "image/png",
742
- data: base64Data,
743
- },
744
- },
745
- {
746
- type: "text",
747
- text: `You are analyzing a UI screenshot to understand a visual problem.
748
-
749
- User request: "${userPrompt}"
750
-
751
- ${focusedElements && focusedElements.length > 0 ? `User clicked on these elements:
752
- ${focusedElements.map((el) => `- ${el.name} (${el.type}) at position (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
753
- ` : ""}
754
-
755
- BEFORE any code changes can be made, you must analyze the visual problem.
756
-
757
- Answer these questions:
758
- 1. ELEMENT: Which specific UI element has the problem? (describe its location, text content, appearance)
759
- 2. PROBLEM: What exactly is visually wrong? (invisible text, wrong color, poor contrast, wrong size, etc.)
760
- 3. CURRENT STATE: What styling/colors appear to be applied to this element?
761
- 4. SOLUTION: What specific CSS/Tailwind change would fix this problem?
762
- 5. CONFIDENCE: How confident are you that you've identified the correct element and problem? (high/medium/low)
763
-
764
- Return ONLY valid JSON with no other text:
765
- {
766
- "element": "specific description of the UI element",
767
- "problem": "what is visually wrong",
768
- "currentState": "what styling appears to be applied",
769
- "solution": "specific CSS/Tailwind fix needed",
770
- "confidence": "high"
771
- }`,
772
- },
773
- ],
774
- },
775
- ],
776
- });
777
-
778
- const analysisText = analysisResponse.content.find((block) => block.type === "text");
779
- if (analysisText && analysisText.type === "text") {
780
- // Parse the JSON response
781
- let jsonText = analysisText.text.trim();
782
- // Extract JSON if wrapped in code blocks
783
- const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || jsonText.match(/(\{[\s\S]*\})/);
784
- if (jsonMatch) {
785
- jsonText = jsonMatch[1];
786
- }
787
-
788
- try {
789
- visualAnalysis = JSON.parse(jsonText) as VisualAnalysis;
790
- debugLog("Phase A: Visual problem analysis complete", visualAnalysis);
791
-
792
- // If low confidence, return early with clarification request
793
- if (visualAnalysis.confidence === "low") {
794
- debugLog("Phase A: Low confidence - requesting clarification");
795
- return NextResponse.json({
796
- success: false,
797
- needsClarification: true,
798
- analysis: visualAnalysis,
799
- message: `I can see "${visualAnalysis.element}" but I'm not certain about the problem: "${visualAnalysis.problem}". Can you be more specific about what needs to change?`,
800
- });
801
- }
802
- } catch (parseError) {
803
- debugLog("Phase A: Failed to parse analysis response", {
804
- error: String(parseError),
805
- response: jsonText.substring(0, 500)
806
- });
807
- // Continue without analysis if parsing fails
808
- }
809
- }
810
- } catch (analysisError) {
811
- debugLog("Phase A: Analysis call failed", { error: String(analysisError) });
812
- // Continue without analysis - fall back to existing behavior
813
- }
814
- }
815
-
816
650
  // ========== SMART CONTEXT BUDGETING ==========
817
651
  // Claude can handle 200k tokens (~800k chars), so we can safely include large files
818
652
  // Priority: Recommended file (NEVER truncate) > Page file (limited) > Other components (dynamic)
@@ -846,24 +680,6 @@ User Request: "${userPrompt}"
846
680
  textContent += `FOCUSED ELEMENTS (user clicked on these):
847
681
  ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
848
682
 
849
- `;
850
- }
851
-
852
- // ========== VISUAL PROBLEM ANALYSIS (from Phase A) ==========
853
- if (visualAnalysis) {
854
- textContent += `═══════════════════════════════════════════════════════════════════════════════
855
- 🔍 VISUAL PROBLEM ANALYSIS (I analyzed the screenshot first)
856
- ═══════════════════════════════════════════════════════════════════════════════
857
-
858
- **Element:** ${visualAnalysis.element}
859
- **Problem:** ${visualAnalysis.problem}
860
- **Current State:** ${visualAnalysis.currentState}
861
- **Required Fix:** ${visualAnalysis.solution}
862
- **Confidence:** ${visualAnalysis.confidence}
863
-
864
- ⚠️ IMPORTANT: Your patches MUST implement the fix described above for the element described above.
865
- Find the code that renders "${visualAnalysis.element}" and apply "${visualAnalysis.solution}".
866
-
867
683
  `;
868
684
  }
869
685
 
@@ -931,17 +747,27 @@ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
931
747
  }
932
748
  }
933
749
 
934
- // ========== THEME DISCOVERY ==========
750
+ // ========== THEME DISCOVERY (REFERENCE ONLY) ==========
935
751
  // 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
936
753
  const discoveredTheme = await discoverTheme(projectRoot);
937
754
  const themeContext = formatThemeForPrompt(discoveredTheme);
938
755
 
939
756
  if (discoveredTheme.discoveredFiles.length > 0) {
940
757
  textContent += `
941
758
  ═══════════════════════════════════════════════════════════════════════════════
942
- ${themeContext}
759
+ REFERENCE ONLY (do not use this to justify additional changes)
943
760
  ═══════════════════════════════════════════════════════════════════════════════
944
761
 
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)
767
+
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.
770
+
945
771
  `;
946
772
  debugLog("Theme discovery complete", {
947
773
  filesFound: discoveredTheme.discoveredFiles,
@@ -1,16 +1,148 @@
1
1
  "use client";
2
2
 
3
- import React, { useState, useEffect } from "react";
4
- import { X, Zap, Loader2, Check, Undo, ChevronDown, ChevronRight, FileCode, AlertTriangle, RefreshCw, Info } from "lucide-react";
3
+ import React, { useState, useEffect, useCallback } from "react";
4
+ import { X, Zap, Loader2, Check, ChevronDown, ChevronRight, FileCode, AlertTriangle, Info, Eye, EyeOff } from "lucide-react";
5
5
  import { cn } from "../../../lib/utils";
6
6
  import { ApplyFirstSession, ApplyFirstStatus, VisionFileModification } from "../types";
7
7
 
8
+ // CSS Injection Preview ID
9
+ const PREVIEW_STYLE_ID = "sonance-preview-css";
10
+
11
+ /**
12
+ * Extract className changes from a diff
13
+ * Returns: { removed: string[], added: string[] }
14
+ */
15
+ function extractClassNameChanges(diff: string): { removed: string[]; added: string[] } {
16
+ const removed: string[] = [];
17
+ const added: string[] = [];
18
+
19
+ // Regex to find className="..." patterns
20
+ const classNameRegex = /className=["']([^"']+)["']/g;
21
+
22
+ const lines = diff.split("\n");
23
+ for (const line of lines) {
24
+ if (line.startsWith("-") && !line.startsWith("---")) {
25
+ // Removed line - extract className
26
+ let match;
27
+ while ((match = classNameRegex.exec(line)) !== null) {
28
+ removed.push(...match[1].split(/\s+/));
29
+ }
30
+ } else if (line.startsWith("+") && !line.startsWith("+++")) {
31
+ // Added line - extract className
32
+ let match;
33
+ while ((match = classNameRegex.exec(line)) !== null) {
34
+ added.push(...match[1].split(/\s+/));
35
+ }
36
+ }
37
+ }
38
+
39
+ return { removed, added };
40
+ }
41
+
42
+ /**
43
+ * Common Tailwind class to CSS mapping for preview
44
+ * Only covers the most common styling classes for quick previews
45
+ */
46
+ const TAILWIND_TO_CSS: Record<string, string> = {
47
+ // Text colors
48
+ "text-white": "color: white !important;",
49
+ "text-black": "color: black !important;",
50
+ "text-gray-900": "color: rgb(17, 24, 39) !important;",
51
+ "text-gray-700": "color: rgb(55, 65, 81) !important;",
52
+ "text-gray-500": "color: rgb(107, 114, 128) !important;",
53
+
54
+ // Sonance brand colors
55
+ "text-sonance-charcoal": "color: #333F48 !important;",
56
+ "text-sonance-blue": "color: #00D3C8 !important;",
57
+ "border-sonance-charcoal": "border-color: #333F48 !important;",
58
+ "border-sonance-blue": "border-color: #00D3C8 !important;",
59
+ "bg-sonance-charcoal": "background-color: #333F48 !important;",
60
+ "bg-sonance-blue": "background-color: #00D3C8 !important;",
61
+
62
+ // Semantic colors
63
+ "text-primary": "color: var(--primary) !important;",
64
+ "text-primary-foreground": "color: var(--primary-foreground) !important;",
65
+ "text-accent": "color: var(--accent) !important;",
66
+ "text-accent-foreground": "color: var(--accent-foreground) !important;",
67
+ "bg-primary": "background-color: hsl(var(--primary)) !important;",
68
+ "bg-accent": "background-color: hsl(var(--accent)) !important;",
69
+
70
+ // Common backgrounds
71
+ "bg-white": "background-color: white !important;",
72
+ "bg-black": "background-color: black !important;",
73
+ "bg-gray-100": "background-color: rgb(243, 244, 246) !important;",
74
+ "bg-gray-200": "background-color: rgb(229, 231, 235) !important;",
75
+
76
+ // Borders
77
+ "border-gray-200": "border-color: rgb(229, 231, 235) !important;",
78
+ "border-gray-300": "border-color: rgb(209, 213, 219) !important;",
79
+ };
80
+
81
+ /**
82
+ * Generate CSS for preview based on class changes
83
+ */
84
+ function generatePreviewCSS(modifications: VisionFileModification[]): string {
85
+ const cssRules: string[] = [];
86
+
87
+ for (const mod of modifications) {
88
+ const { added } = extractClassNameChanges(mod.diff);
89
+
90
+ // Generate CSS for added classes
91
+ for (const cls of added) {
92
+ if (TAILWIND_TO_CSS[cls]) {
93
+ // We can't know the exact selector, so we apply to elements with data-sonance-preview
94
+ cssRules.push(TAILWIND_TO_CSS[cls]);
95
+ }
96
+ }
97
+ }
98
+
99
+ if (cssRules.length === 0) return "";
100
+
101
+ // Apply to changed elements (marked by InspectorOverlay)
102
+ return `
103
+ [data-sonance-changed="true"] {
104
+ ${cssRules.join("\n ")}
105
+ outline: 2px dashed #00D3C8 !important;
106
+ outline-offset: 2px !important;
107
+ }
108
+ `;
109
+ }
110
+
111
+ /**
112
+ * Inject preview CSS into the document
113
+ */
114
+ function injectPreviewCSS(css: string): void {
115
+ // Remove existing preview styles first
116
+ removePreviewCSS();
117
+
118
+ if (!css.trim()) return;
119
+
120
+ const style = document.createElement("style");
121
+ style.id = PREVIEW_STYLE_ID;
122
+ style.textContent = css;
123
+ document.head.appendChild(style);
124
+
125
+ console.log("[Preview CSS] Injected:", css.substring(0, 200));
126
+ }
127
+
128
+ /**
129
+ * Remove preview CSS from the document
130
+ */
131
+ function removePreviewCSS(): void {
132
+ const existing = document.getElementById(PREVIEW_STYLE_ID);
133
+ if (existing) {
134
+ existing.remove();
135
+ console.log("[Preview CSS] Removed");
136
+ }
137
+ }
138
+
8
139
  export interface ApplyFirstPreviewProps {
9
140
  session: ApplyFirstSession;
10
141
  status: ApplyFirstStatus;
11
142
  onAccept: () => void;
12
143
  onRevert: () => void;
13
144
  onForceClear?: () => void; // Force clear stale sessions
145
+ onApplyPreview?: () => void; // Apply previewed changes (for preview mode)
14
146
  }
15
147
 
16
148
  function FileModificationCard({
@@ -78,7 +210,7 @@ function HMRStatusBadge({ status }: { status: ApplyFirstStatus }) {
78
210
  if (status === "waiting-hmr") {
79
211
  return (
80
212
  <span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">
81
- <RefreshCw className="h-3 w-3 animate-spin" />
213
+ <Loader2 className="h-3 w-3 animate-spin" />
82
214
  Refreshing...
83
215
  </span>
84
216
  );
@@ -92,6 +224,15 @@ function HMRStatusBadge({ status }: { status: ApplyFirstStatus }) {
92
224
  </span>
93
225
  );
94
226
  }
227
+
228
+ if (status === "previewing") {
229
+ return (
230
+ <span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700">
231
+ <Eye className="h-3 w-3" />
232
+ Preview Mode
233
+ </span>
234
+ );
235
+ }
95
236
 
96
237
  return null;
97
238
  }
@@ -102,8 +243,10 @@ export function ApplyFirstPreview({
102
243
  onAccept,
103
244
  onRevert,
104
245
  onForceClear,
246
+ onApplyPreview,
105
247
  }: ApplyFirstPreviewProps) {
106
248
  const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
249
+ const [cssPreviewEnabled, setCssPreviewEnabled] = useState(true);
107
250
 
108
251
  // Expand first file by default
109
252
  useEffect(() => {
@@ -111,6 +254,32 @@ export function ApplyFirstPreview({
111
254
  setExpandedFiles(new Set([session.modifications[0].filePath]));
112
255
  }
113
256
  }, [session]);
257
+
258
+ // CSS Preview injection for preview mode
259
+ useEffect(() => {
260
+ const isPreviewMode = session.isPreview === true;
261
+
262
+ if (isPreviewMode && cssPreviewEnabled) {
263
+ // Generate and inject preview CSS
264
+ const previewCSS = generatePreviewCSS(session.modifications);
265
+ if (previewCSS) {
266
+ injectPreviewCSS(previewCSS);
267
+ }
268
+ } else {
269
+ // Remove preview CSS when not in preview mode or disabled
270
+ removePreviewCSS();
271
+ }
272
+
273
+ // Cleanup on unmount
274
+ return () => {
275
+ removePreviewCSS();
276
+ };
277
+ }, [session, cssPreviewEnabled]);
278
+
279
+ // Toggle CSS preview
280
+ const toggleCssPreview = useCallback(() => {
281
+ setCssPreviewEnabled(prev => !prev);
282
+ }, []);
114
283
 
115
284
  const toggleFile = (filePath: string) => {
116
285
  setExpandedFiles((prev) => {
@@ -125,26 +294,55 @@ export function ApplyFirstPreview({
125
294
  };
126
295
 
127
296
  const fileCount = session.modifications.length;
128
- const isLoading = status === "accepting" || status === "reverting";
297
+ const isLoading = status === "accepting" || status === "reverting" || status === "applying";
129
298
  const isStaleSession = fileCount === 0 || status === "error";
299
+ const isPreviewMode = session.isPreview === true;
130
300
 
131
301
  return (
132
- <div className="space-y-3 p-3 rounded border border-green-300 bg-green-50">
302
+ <div className={cn(
303
+ "space-y-3 p-3 rounded border",
304
+ isPreviewMode
305
+ ? "border-blue-300 bg-blue-50"
306
+ : "border-green-300 bg-green-50"
307
+ )}>
133
308
  {/* Header */}
134
309
  <div className="flex items-center justify-between">
135
310
  <div className="flex items-center gap-2">
136
- <Zap className="h-4 w-4 text-green-600" />
311
+ <Zap className={cn("h-4 w-4", isPreviewMode ? "text-blue-600" : "text-green-600")} />
137
312
  <span className="text-xs font-semibold text-gray-900">
138
- Changes Applied
313
+ {isPreviewMode ? "Proposed Changes" : "Changes Applied"}
139
314
  </span>
140
- <span className="text-[10px] px-1.5 py-0.5 rounded bg-green-200 text-green-700 font-medium">
315
+ <span className={cn(
316
+ "text-[10px] px-1.5 py-0.5 rounded font-medium",
317
+ isPreviewMode
318
+ ? "bg-blue-200 text-blue-700"
319
+ : "bg-green-200 text-green-700"
320
+ )}>
141
321
  {fileCount} file{fileCount !== 1 ? "s" : ""}
142
322
  </span>
143
323
  </div>
144
- <HMRStatusBadge status={status} />
324
+ <div className="flex items-center gap-2">
325
+ {/* CSS Preview Toggle (only in preview mode) */}
326
+ {isPreviewMode && (
327
+ <button
328
+ onClick={toggleCssPreview}
329
+ className={cn(
330
+ "flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded transition-colors",
331
+ cssPreviewEnabled
332
+ ? "bg-blue-200 text-blue-700"
333
+ : "bg-gray-200 text-gray-500"
334
+ )}
335
+ title={cssPreviewEnabled ? "Hide CSS preview" : "Show CSS preview"}
336
+ >
337
+ {cssPreviewEnabled ? <Eye className="h-3 w-3" /> : <EyeOff className="h-3 w-3" />}
338
+ CSS
339
+ </button>
340
+ )}
341
+ <HMRStatusBadge status={status} />
342
+ </div>
145
343
  </div>
146
344
 
147
- {/* Info Banner - show different message for stale sessions */}
345
+ {/* Info Banner - different messages for preview vs applied vs stale */}
148
346
  {isStaleSession ? (
149
347
  <div className="flex items-start gap-2 p-2 rounded bg-amber-50 border border-amber-200">
150
348
  <AlertTriangle className="h-3.5 w-3.5 text-amber-600 mt-0.5 flex-shrink-0" />
@@ -153,6 +351,14 @@ export function ApplyFirstPreview({
153
351
  Use &quot;Force Clear&quot; to dismiss this panel.
154
352
  </span>
155
353
  </div>
354
+ ) : isPreviewMode ? (
355
+ <div className="flex items-start gap-2 p-2 rounded bg-blue-50 border border-blue-200">
356
+ <Info className="h-3.5 w-3.5 text-blue-600 mt-0.5 flex-shrink-0" />
357
+ <span className="text-xs text-blue-700">
358
+ <strong>Review the changes below.</strong> Click Accept to apply them to your files,
359
+ or Reject to discard.
360
+ </span>
361
+ </div>
156
362
  ) : (
157
363
  <div className="flex items-start gap-2 p-2 rounded bg-blue-50 border border-blue-200">
158
364
  <Info className="h-3.5 w-3.5 text-blue-600 mt-0.5 flex-shrink-0" />
@@ -175,43 +381,47 @@ export function ApplyFirstPreview({
175
381
  ))}
176
382
  </div>
177
383
 
178
- {/* Warning about navigation */}
179
- <div className="flex items-start gap-2 p-2 rounded bg-amber-50 border border-amber-200">
180
- <AlertTriangle className="h-3.5 w-3.5 text-amber-600 mt-0.5 flex-shrink-0" />
181
- <span className="text-xs text-amber-700">
182
- Navigating away will automatically revert changes. Make sure to accept or revert first.
183
- </span>
184
- </div>
384
+ {/* Warning about navigation - only show for applied mode */}
385
+ {!isPreviewMode && (
386
+ <div className="flex items-start gap-2 p-2 rounded bg-amber-50 border border-amber-200">
387
+ <AlertTriangle className="h-3.5 w-3.5 text-amber-600 mt-0.5 flex-shrink-0" />
388
+ <span className="text-xs text-amber-700">
389
+ Navigating away will automatically revert changes. Make sure to accept or revert first.
390
+ </span>
391
+ </div>
392
+ )}
185
393
 
186
394
  {/* Action Buttons */}
187
395
  <div className="flex gap-2">
188
- {/* Accept Button - hide for stale sessions */}
396
+ {/* Accept/Apply Button - behavior depends on mode */}
189
397
  {!isStaleSession && (
190
398
  <button
191
- onClick={onAccept}
399
+ onClick={isPreviewMode && onApplyPreview ? onApplyPreview : onAccept}
192
400
  disabled={isLoading}
193
401
  className={cn(
194
402
  "flex-1 flex items-center justify-center gap-2 py-2.5",
195
403
  "text-xs font-medium rounded transition-colors",
196
- "bg-green-600 text-white hover:bg-green-700",
404
+ isPreviewMode
405
+ ? "bg-blue-600 text-white hover:bg-blue-700"
406
+ : "bg-green-600 text-white hover:bg-green-700",
197
407
  "disabled:opacity-50 disabled:cursor-not-allowed"
198
408
  )}
199
409
  >
200
- {status === "accepting" ? (
410
+ {status === "accepting" || status === "applying" ? (
201
411
  <>
202
412
  <Loader2 className="h-3.5 w-3.5 animate-spin" />
203
- Accepting...
413
+ {isPreviewMode ? "Applying..." : "Accepting..."}
204
414
  </>
205
415
  ) : (
206
416
  <>
207
417
  <Check className="h-3.5 w-3.5" />
208
- Keep Changes
418
+ {isPreviewMode ? "Accept & Apply" : "Keep Changes"}
209
419
  </>
210
420
  )}
211
421
  </button>
212
422
  )}
213
423
 
214
- {/* Revert Button */}
424
+ {/* Reject/Revert Button */}
215
425
  <button
216
426
  onClick={onRevert}
217
427
  disabled={isLoading}
@@ -225,12 +435,12 @@ export function ApplyFirstPreview({
225
435
  {status === "reverting" ? (
226
436
  <>
227
437
  <Loader2 className="h-3.5 w-3.5 animate-spin" />
228
- Reverting...
438
+ {isPreviewMode ? "Discarding..." : "Reverting..."}
229
439
  </>
230
440
  ) : (
231
441
  <>
232
- <Undo className="h-3.5 w-3.5" />
233
- Revert
442
+ <X className="h-3.5 w-3.5" />
443
+ {isPreviewMode ? "Reject" : "Revert"}
234
444
  </>
235
445
  )}
236
446
  </button>
@@ -255,7 +465,7 @@ export function ApplyFirstPreview({
255
465
 
256
466
  {/* Session Info (for debugging) */}
257
467
  <div className="text-[10px] text-gray-400 font-mono">
258
- Session: {session.sessionId} | Applied: {new Date(session.appliedAt).toLocaleTimeString()}
468
+ {isPreviewMode ? "Preview" : "Session"}: {session.sessionId} | {isPreviewMode ? "Generated" : "Applied"}: {new Date(session.appliedAt).toLocaleTimeString()}
259
469
  </div>
260
470
  </div>
261
471
  );
@@ -144,6 +144,7 @@ export function ChatInterface({
144
144
  method: "POST",
145
145
  headers: { "Content-Type": "application/json" },
146
146
  body: JSON.stringify({
147
+ // Apply-First: write files immediately so HMR shows changes
147
148
  action: useApplyFirst ? "apply" : "edit",
148
149
  screenshot,
149
150
  pageRoute: window.location.pathname,
@@ -176,7 +177,7 @@ export function ChatInterface({
176
177
 
177
178
  if (data.success && data.modifications) {
178
179
  if (useApplyFirst && onApplyFirstComplete) {
179
- // Apply-First mode: files are already written
180
+ // Apply-First mode: files are already written, user can see changes via HMR
180
181
  console.log("[Apply-First] Calling onApplyFirstComplete with:", {
181
182
  sessionId: data.sessionId,
182
183
  modifications: data.modifications.map((m: { filePath: string }) => m.filePath),
@@ -50,13 +50,14 @@ export interface ComponentsPanelProps {
50
50
  visionPendingEdit?: VisionPendingEdit | null;
51
51
  onSaveVisionEdit?: () => void;
52
52
  onClearVisionPendingEdit?: () => void;
53
- // Apply-First Mode props
53
+ // Apply-First Mode props (now Preview-First for Cursor-style flow)
54
54
  applyFirstSession?: ApplyFirstSession | null;
55
55
  applyFirstStatus?: ApplyFirstStatus;
56
56
  onApplyFirstComplete?: (session: ApplyFirstSession) => void;
57
57
  onApplyFirstAccept?: () => void;
58
58
  onApplyFirstRevert?: () => void;
59
59
  onApplyFirstForceClear?: () => void;
60
+ onApplyPreview?: () => void; // Apply previewed modifications to files
60
61
  }
61
62
 
62
63
  export function ComponentsPanel({
@@ -97,6 +98,7 @@ export function ComponentsPanel({
97
98
  onApplyFirstAccept,
98
99
  onApplyFirstRevert,
99
100
  onApplyFirstForceClear,
101
+ onApplyPreview,
100
102
  }: ComponentsPanelProps) {
101
103
  // Auto-activate inspector when entering this tab
102
104
  useEffect(() => {
@@ -573,7 +575,7 @@ export function ComponentsPanel({
573
575
  />
574
576
  )}
575
577
 
576
- {/* Apply-First Mode Preview - NEW: Changes already applied */}
578
+ {/* Preview-First Mode - Shows diff, user accepts/rejects before applying */}
577
579
  {applyFirstSession && onApplyFirstAccept && onApplyFirstRevert && (
578
580
  <ApplyFirstPreview
579
581
  session={applyFirstSession}
@@ -581,6 +583,7 @@ export function ComponentsPanel({
581
583
  onAccept={onApplyFirstAccept}
582
584
  onRevert={onApplyFirstRevert}
583
585
  onForceClear={onApplyFirstForceClear}
586
+ onApplyPreview={onApplyPreview}
584
587
  />
585
588
  )}
586
589
 
@@ -255,28 +255,31 @@ export interface VisionPendingEdit {
255
255
  explanation: string;
256
256
  }
257
257
 
258
- // ---- Apply-First Mode Types (Cursor-style instant preview) ----
258
+ // ---- Preview-First Mode Types (Cursor-style) ----
259
+ // Flow: LLM generates patches -> Show diff preview -> User accepts/rejects -> Apply to files
259
260
 
260
261
  export interface ApplyFirstSession {
261
262
  sessionId: string;
262
263
  modifications: VisionFileModification[];
263
264
  appliedAt: number;
264
- status: 'applied' | 'accepted' | 'reverted';
265
+ status: 'preview' | 'applied' | 'accepted' | 'reverted';
265
266
  backupPaths: string[];
267
+ isPreview?: boolean; // True if changes are not yet written to disk
266
268
  }
267
269
 
268
270
  export type ApplyFirstStatus =
269
271
  | 'idle' // No active session
270
272
  | 'generating' // AI is generating changes
273
+ | 'previewing' // User is reviewing proposed changes (not yet applied)
271
274
  | 'applying' // Writing files to disk
272
275
  | 'waiting-hmr' // Waiting for HMR to show changes
273
- | 'reviewing' // User is reviewing actual changes
276
+ | 'reviewing' // User is reviewing actual changes (files written)
274
277
  | 'accepting' // User clicked accept, cleaning up backups
275
278
  | 'reverting' // User clicked revert, restoring backups
276
279
  | 'error'; // Something went wrong
277
280
 
278
281
  export interface ApplyFirstRequest {
279
- action: "apply" | "accept" | "revert";
282
+ action: "apply" | "preview" | "accept" | "revert";
280
283
  sessionId?: string;
281
284
  screenshot?: string;
282
285
  pageRoute?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.56",
3
+ "version": "1.3.58",
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",