sonance-brand-mcp 1.3.24 → 1.3.26

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.
@@ -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
@@ -250,14 +243,38 @@ ${pageContext.pageContent ? `\`\`\`tsx\n${pageContext.pageContent}\n\`\`\`` : ""
250
243
  `;
251
244
 
252
245
  if (pageContext.componentSources.length > 0) {
253
- textContent += `IMPORTED COMPONENTS:\n`;
254
- for (const comp of pageContext.componentSources) {
246
+ // Smart truncation: prioritize first components (direct imports) and limit total context
247
+ const MAX_TOTAL_CONTEXT = 80000; // ~80k chars to stay well under Claude's limit
248
+ const MAX_PER_FILE_PRIORITY = 4000; // First 10 files get more space
249
+ const MAX_PER_FILE_SECONDARY = 1500; // Remaining files get less
250
+ const MAX_FILES = 30; // Limit total number of files
251
+
252
+ let usedContext = pageContext.pageContent.length + pageContext.globalsCSS.length;
253
+ const truncatedComponents = pageContext.componentSources.slice(0, MAX_FILES);
254
+
255
+ textContent += `IMPORTED COMPONENTS (${truncatedComponents.length} files, ${pageContext.componentSources.length > MAX_FILES ? `${pageContext.componentSources.length - MAX_FILES} omitted` : 'complete'}):\n`;
256
+
257
+ for (let i = 0; i < truncatedComponents.length; i++) {
258
+ const comp = truncatedComponents[i];
259
+ const isPriority = i < 10; // First 10 files are priority (direct imports)
260
+ const maxSize = isPriority ? MAX_PER_FILE_PRIORITY : MAX_PER_FILE_SECONDARY;
261
+
262
+ // Stop if we've used too much context
263
+ if (usedContext > MAX_TOTAL_CONTEXT) {
264
+ textContent += `\n// ... (${truncatedComponents.length - i} more files omitted to stay within context limits)\n`;
265
+ break;
266
+ }
267
+
268
+ const truncatedContent = comp.content.substring(0, maxSize);
269
+ const wasTruncated = comp.content.length > maxSize;
270
+
255
271
  textContent += `
256
- File: ${comp.path}
272
+ File: ${comp.path}${isPriority ? '' : ' (nested)'}
257
273
  \`\`\`tsx
258
- ${comp.content.substring(0, 3000)}${comp.content.length > 3000 ? "\n// ... (truncated)" : ""}
274
+ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
259
275
  \`\`\`
260
276
  `;
277
+ usedContext += truncatedContent.length;
261
278
  }
262
279
  }
263
280
 
@@ -310,13 +327,15 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
310
327
  );
311
328
  }
312
329
 
313
- // Parse AI response
330
+ // Parse AI response - now expecting patches instead of full file content
314
331
  let aiResponse: {
315
332
  reasoning?: string;
316
333
  modifications: Array<{
317
334
  filePath: string;
318
- modifiedContent: string;
319
- explanation: string;
335
+ patches?: Patch[];
336
+ // Legacy support for modifiedContent (will be deprecated)
337
+ modifiedContent?: string;
338
+ explanation?: string;
320
339
  previewCSS?: string;
321
340
  }>;
322
341
  aggregatedPreviewCSS?: string;
@@ -381,10 +400,9 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
381
400
  );
382
401
  }
383
402
 
384
- // Read original content and generate diffs for each modification
403
+ // Process modifications - apply patches to get modified content
385
404
  const modificationsWithOriginals: VisionFileModification[] = [];
386
- const validationErrors: string[] = [];
387
- const validationWarnings: string[] = [];
405
+ const patchErrors: string[] = [];
388
406
 
389
407
  for (const mod of aiResponse.modifications || []) {
390
408
  const fullPath = path.join(projectRoot, mod.filePath);
@@ -393,43 +411,76 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
393
411
  originalContent = fs.readFileSync(fullPath, "utf-8");
394
412
  }
395
413
 
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}`));
414
+ let modifiedContent: string;
415
+ let explanation = mod.explanation || "";
416
+
417
+ // Check if AI returned patches (new format) or modifiedContent (legacy)
418
+ if (mod.patches && mod.patches.length > 0) {
419
+ // New patch-based approach
420
+ console.log(`[Vision Mode] Applying ${mod.patches.length} patches to ${mod.filePath}`);
421
+
422
+ const patchResult = applyPatches(originalContent, mod.patches);
423
+
424
+ if (!patchResult.success) {
425
+ const failedMessages = patchResult.failedPatches.map(
426
+ (f) => `Patch failed: ${f.error}`
427
+ ).join("\n");
428
+ patchErrors.push(`${mod.filePath}:\n${failedMessages}`);
429
+
430
+ // If some patches succeeded, use partial result
431
+ if (patchResult.appliedPatches > 0) {
432
+ console.warn(`[Vision Mode] ${patchResult.appliedPatches}/${mod.patches.length} patches applied to ${mod.filePath}`);
433
+ modifiedContent = patchResult.modifiedContent;
434
+ explanation += ` (${patchResult.appliedPatches}/${mod.patches.length} patches applied)`;
435
+ } else {
436
+ continue; // Skip this file entirely if no patches worked
437
+ }
438
+ } else {
439
+ modifiedContent = patchResult.modifiedContent;
440
+ console.log(`[Vision Mode] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
441
+ }
442
+ } else if (mod.modifiedContent) {
443
+ // Legacy: AI returned full file content
444
+ console.warn(`[Vision Mode] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
445
+ modifiedContent = mod.modifiedContent;
446
+
447
+ // Validate the modification using legacy validation
448
+ const validation = validateModification(originalContent, modifiedContent, mod.filePath);
449
+ if (!validation.valid) {
450
+ patchErrors.push(`${mod.filePath}: ${validation.error}`);
451
+ continue;
452
+ }
453
+ } else {
454
+ // No patches and no modifiedContent - skip
455
+ console.warn(`[Vision Mode] No patches or modifiedContent for ${mod.filePath}`);
456
+ continue;
406
457
  }
407
458
 
408
459
  modificationsWithOriginals.push({
409
460
  filePath: mod.filePath,
410
461
  originalContent,
411
- modifiedContent: mod.modifiedContent,
412
- diff: generateSimpleDiff(originalContent, mod.modifiedContent),
413
- explanation: mod.explanation,
462
+ modifiedContent,
463
+ diff: generateSimpleDiff(originalContent, modifiedContent),
464
+ explanation,
414
465
  previewCSS: mod.previewCSS,
415
466
  });
416
467
  }
417
468
 
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);
469
+ // If all modifications failed, return error
470
+ if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
471
+ console.error("All AI patches failed:", patchErrors);
421
472
  return NextResponse.json(
422
473
  {
423
474
  success: false,
424
- error: validationErrors.join("\n\n"),
475
+ error: `Patch application failed:\n\n${patchErrors.join("\n\n")}`,
425
476
  } as VisionEditResponse,
426
477
  { status: 400 }
427
478
  );
428
479
  }
429
480
 
430
- // Log warnings for review
431
- if (validationWarnings.length > 0) {
432
- console.warn("Vision edit validation warnings:", validationWarnings);
481
+ // Log patch errors as warnings if some modifications succeeded
482
+ if (patchErrors.length > 0) {
483
+ console.warn("Some patches failed:", patchErrors);
433
484
  }
434
485
 
435
486
  // Aggregate preview CSS
@@ -524,8 +575,109 @@ function findComponentFileByName(
524
575
  return null;
525
576
  }
526
577
 
578
+ /**
579
+ * Recursively gather all imports from a file up to a max depth
580
+ * This builds a complete component graph for the AI to understand
581
+ */
582
+ function gatherAllImports(
583
+ filePath: string,
584
+ projectRoot: string,
585
+ visited: Set<string> = new Set(),
586
+ maxDepth: number = 4
587
+ ): { path: string; content: string }[] {
588
+ // Prevent infinite loops and limit total files
589
+ if (visited.has(filePath) || visited.size > 50) return [];
590
+ visited.add(filePath);
591
+
592
+ const results: { path: string; content: string }[] = [];
593
+ const fullPath = path.join(projectRoot, filePath);
594
+
595
+ if (!fs.existsSync(fullPath)) return results;
596
+
597
+ try {
598
+ const content = fs.readFileSync(fullPath, "utf-8");
599
+ results.push({ path: filePath, content });
600
+
601
+ // Continue recursing if we haven't hit max depth
602
+ if (maxDepth > 0) {
603
+ const imports = extractImports(content);
604
+ for (const imp of imports) {
605
+ const resolved = resolveImportPath(imp, filePath, projectRoot);
606
+ if (resolved && !visited.has(resolved)) {
607
+ const nestedImports = gatherAllImports(resolved, projectRoot, visited, maxDepth - 1);
608
+ results.push(...nestedImports);
609
+ }
610
+ }
611
+ }
612
+ } catch {
613
+ // Skip files that can't be read
614
+ }
615
+
616
+ return results;
617
+ }
618
+
619
+ /**
620
+ * Discover layout files that wrap the page
621
+ * App Router: layout.tsx in same and parent directories
622
+ * Pages Router: _app.tsx and _document.tsx
623
+ */
624
+ function discoverLayoutFiles(pageFile: string | null, projectRoot: string): string[] {
625
+ const layoutFiles: string[] = [];
626
+
627
+ if (!pageFile) return layoutFiles;
628
+
629
+ // Determine if App Router or Pages Router
630
+ const isAppRouter = pageFile.includes("/app/") || pageFile.startsWith("app/");
631
+ const isPagesRouter = pageFile.includes("/pages/") || pageFile.startsWith("pages/");
632
+
633
+ if (isAppRouter) {
634
+ // App Router: Check for layout.tsx in same directory and parent directories
635
+ let currentDir = path.dirname(pageFile);
636
+ const appRoot = pageFile.includes("src/app") ? "src/app" : "app";
637
+
638
+ while (currentDir.includes(appRoot)) {
639
+ const layoutPatterns = [
640
+ path.join(currentDir, "layout.tsx"),
641
+ path.join(currentDir, "layout.jsx"),
642
+ ];
643
+
644
+ for (const layoutPath of layoutPatterns) {
645
+ if (fs.existsSync(path.join(projectRoot, layoutPath))) {
646
+ layoutFiles.push(layoutPath);
647
+ }
648
+ }
649
+
650
+ // Move to parent directory
651
+ const parentDir = path.dirname(currentDir);
652
+ if (parentDir === currentDir) break;
653
+ currentDir = parentDir;
654
+ }
655
+ }
656
+
657
+ if (isPagesRouter) {
658
+ // Pages Router: Check for _app.tsx and _document.tsx
659
+ const pagesRoot = pageFile.includes("src/pages") ? "src/pages" : "pages";
660
+
661
+ const pagesRouterLayouts = [
662
+ `${pagesRoot}/_app.tsx`,
663
+ `${pagesRoot}/_app.jsx`,
664
+ `${pagesRoot}/_document.tsx`,
665
+ `${pagesRoot}/_document.jsx`,
666
+ ];
667
+
668
+ for (const layoutPath of pagesRouterLayouts) {
669
+ if (fs.existsSync(path.join(projectRoot, layoutPath))) {
670
+ layoutFiles.push(layoutPath);
671
+ }
672
+ }
673
+ }
674
+
675
+ return layoutFiles;
676
+ }
677
+
527
678
  /**
528
679
  * Gather context about the current page for AI analysis
680
+ * Uses recursive import resolution to build complete component graph
529
681
  */
530
682
  function gatherPageContext(
531
683
  pageRoute: string,
@@ -541,23 +693,30 @@ function gatherPageContext(
541
693
  const pageFile = discoverPageFile(pageRoute, projectRoot);
542
694
  let pageContent = "";
543
695
  const componentSources: { path: string; content: string }[] = [];
696
+ const visited = new Set<string>();
544
697
 
545
698
  if (pageFile) {
546
699
  const fullPath = path.join(projectRoot, pageFile);
547
700
  if (fs.existsSync(fullPath)) {
548
701
  pageContent = fs.readFileSync(fullPath, "utf-8");
702
+ visited.add(pageFile);
549
703
 
550
- // Extract imports and read component files
704
+ // Recursively gather all imported components (up to 4 levels deep)
551
705
  const imports = extractImports(pageContent);
552
706
  for (const importPath of imports) {
553
707
  const resolvedPath = resolveImportPath(importPath, pageFile, projectRoot);
554
- if (resolvedPath && fs.existsSync(path.join(projectRoot, resolvedPath))) {
555
- try {
556
- const content = fs.readFileSync(path.join(projectRoot, resolvedPath), "utf-8");
557
- componentSources.push({ path: resolvedPath, content });
558
- } catch {
559
- // Skip files that can't be read
560
- }
708
+ if (resolvedPath && !visited.has(resolvedPath)) {
709
+ const nestedComponents = gatherAllImports(resolvedPath, projectRoot, visited, 3);
710
+ componentSources.push(...nestedComponents);
711
+ }
712
+ }
713
+
714
+ // Also include layout files
715
+ const layoutFiles = discoverLayoutFiles(pageFile, projectRoot);
716
+ for (const layoutFile of layoutFiles) {
717
+ if (!visited.has(layoutFile)) {
718
+ const layoutComponents = gatherAllImports(layoutFile, projectRoot, visited, 2);
719
+ componentSources.push(...layoutComponents);
561
720
  }
562
721
  }
563
722
  }
@@ -568,25 +727,30 @@ function gatherPageContext(
568
727
  for (const el of focusedElements) {
569
728
  // Try to find component file by name
570
729
  const foundPath = findComponentFileByName(el.name, projectRoot);
571
- if (foundPath && !componentSources.some((c) => c.path === foundPath)) {
572
- try {
573
- const content = fs.readFileSync(
574
- path.join(projectRoot, foundPath),
575
- "utf-8"
576
- );
577
- componentSources.push({ path: foundPath, content });
578
- } catch {
579
- /* skip if unreadable */
580
- }
730
+ if (foundPath && !visited.has(foundPath) && !componentSources.some((c) => c.path === foundPath)) {
731
+ const focusedComponents = gatherAllImports(foundPath, projectRoot, visited, 2);
732
+ componentSources.push(...focusedComponents);
581
733
  }
582
734
  }
583
735
  }
584
736
 
585
- // Read globals.css
737
+ // Read globals.css - check multiple possible locations
586
738
  let globalsCSS = "";
587
- const globalsPath = path.join(projectRoot, "src/app/globals.css");
588
- if (fs.existsSync(globalsPath)) {
589
- globalsCSS = fs.readFileSync(globalsPath, "utf-8");
739
+ const globalsCSSPatterns = [
740
+ "src/app/globals.css",
741
+ "app/globals.css",
742
+ "src/styles/globals.css",
743
+ "styles/globals.css",
744
+ "src/styles/global.css",
745
+ "styles/global.css",
746
+ ];
747
+
748
+ for (const cssPattern of globalsCSSPatterns) {
749
+ const globalsPath = path.join(projectRoot, cssPattern);
750
+ if (fs.existsSync(globalsPath)) {
751
+ globalsCSS = fs.readFileSync(globalsPath, "utf-8");
752
+ break;
753
+ }
590
754
  }
591
755
 
592
756
  return { pageFile, pageContent, componentSources, globalsCSS };
@@ -594,13 +758,28 @@ function gatherPageContext(
594
758
 
595
759
  /**
596
760
  * Discover the page file for a given route
761
+ * Supports both App Router (src/app/) and Pages Router (src/pages/, pages/)
597
762
  */
598
763
  function discoverPageFile(route: string, projectRoot: string): string | null {
599
764
  // Handle root route
600
765
  if (route === "/" || route === "") {
601
- const rootPage = "src/app/page.tsx";
602
- if (fs.existsSync(path.join(projectRoot, rootPage))) {
603
- return rootPage;
766
+ const rootPatterns = [
767
+ // App Router patterns
768
+ "src/app/page.tsx",
769
+ "src/app/page.jsx",
770
+ "app/page.tsx",
771
+ "app/page.jsx",
772
+ // Pages Router patterns
773
+ "src/pages/index.tsx",
774
+ "src/pages/index.jsx",
775
+ "pages/index.tsx",
776
+ "pages/index.jsx",
777
+ ];
778
+
779
+ for (const pattern of rootPatterns) {
780
+ if (fs.existsSync(path.join(projectRoot, pattern))) {
781
+ return pattern;
782
+ }
604
783
  }
605
784
  return null;
606
785
  }
@@ -608,11 +787,26 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
608
787
  // Remove leading slash
609
788
  const cleanRoute = route.replace(/^\//, "");
610
789
 
611
- // Try different patterns
790
+ // First, try exact match patterns
612
791
  const patterns = [
792
+ // App Router patterns (with src)
613
793
  `src/app/${cleanRoute}/page.tsx`,
614
794
  `src/app/${cleanRoute}/page.jsx`,
615
- `src/app/${cleanRoute}.tsx`,
795
+ // App Router patterns (without src)
796
+ `app/${cleanRoute}/page.tsx`,
797
+ `app/${cleanRoute}/page.jsx`,
798
+ // Pages Router patterns (with src) - file-based
799
+ `src/pages/${cleanRoute}.tsx`,
800
+ `src/pages/${cleanRoute}.jsx`,
801
+ // Pages Router patterns (with src) - folder-based
802
+ `src/pages/${cleanRoute}/index.tsx`,
803
+ `src/pages/${cleanRoute}/index.jsx`,
804
+ // Pages Router patterns (without src) - file-based
805
+ `pages/${cleanRoute}.tsx`,
806
+ `pages/${cleanRoute}.jsx`,
807
+ // Pages Router patterns (without src) - folder-based
808
+ `pages/${cleanRoute}/index.tsx`,
809
+ `pages/${cleanRoute}/index.jsx`,
616
810
  ];
617
811
 
618
812
  for (const pattern of patterns) {
@@ -621,6 +815,99 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
621
815
  }
622
816
  }
623
817
 
818
+ // If exact match not found, try dynamic route matching
819
+ // e.g., /processes/123 -> src/pages/processes/[id].tsx
820
+ const dynamicResult = findDynamicRoute(cleanRoute, projectRoot);
821
+ if (dynamicResult) {
822
+ return dynamicResult;
823
+ }
824
+
825
+ return null;
826
+ }
827
+
828
+ /**
829
+ * Find dynamic route files when exact match fails
830
+ * Maps runtime routes like "/processes/123" to file paths like "src/pages/processes/[id].tsx"
831
+ */
832
+ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | null {
833
+ const segments = cleanRoute.split("/");
834
+
835
+ // Try replacing the last segment with dynamic patterns
836
+ const baseDirs = [
837
+ "src/app",
838
+ "app",
839
+ "src/pages",
840
+ "pages",
841
+ ];
842
+
843
+ const dynamicPatterns = ["[id]", "[slug]", "[...slug]", "[[...slug]]"];
844
+ const extensions = [".tsx", ".jsx"];
845
+
846
+ for (const baseDir of baseDirs) {
847
+ const basePath = path.join(projectRoot, baseDir);
848
+ if (!fs.existsSync(basePath)) continue;
849
+
850
+ // Build path with all segments except the last one
851
+ const parentSegments = segments.slice(0, -1);
852
+ const parentPath = parentSegments.length > 0
853
+ ? path.join(basePath, ...parentSegments)
854
+ : basePath;
855
+
856
+ if (!fs.existsSync(parentPath)) continue;
857
+
858
+ // Check for dynamic route files
859
+ for (const dynPattern of dynamicPatterns) {
860
+ for (const ext of extensions) {
861
+ // App Router: parent/[id]/page.tsx
862
+ if (baseDir.includes("app")) {
863
+ const appRouterPath = path.join(parentPath, dynPattern, `page${ext}`);
864
+ if (fs.existsSync(appRouterPath)) {
865
+ return path.relative(projectRoot, appRouterPath);
866
+ }
867
+ }
868
+
869
+ // Pages Router: parent/[id].tsx
870
+ const pagesRouterFile = path.join(parentPath, `${dynPattern}${ext}`);
871
+ if (fs.existsSync(pagesRouterFile)) {
872
+ return path.relative(projectRoot, pagesRouterFile);
873
+ }
874
+
875
+ // Pages Router: parent/[id]/index.tsx
876
+ const pagesRouterDir = path.join(parentPath, dynPattern, `index${ext}`);
877
+ if (fs.existsSync(pagesRouterDir)) {
878
+ return path.relative(projectRoot, pagesRouterDir);
879
+ }
880
+ }
881
+ }
882
+
883
+ // Also scan directory for any dynamic segment pattern [...]
884
+ try {
885
+ const entries = fs.readdirSync(parentPath, { withFileTypes: true });
886
+ for (const entry of entries) {
887
+ if (entry.name.startsWith("[") && entry.name.includes("]")) {
888
+ for (const ext of extensions) {
889
+ if (entry.isDirectory()) {
890
+ // App Router or Pages Router with folder
891
+ const pagePath = path.join(parentPath, entry.name, `page${ext}`);
892
+ const indexPath = path.join(parentPath, entry.name, `index${ext}`);
893
+ if (fs.existsSync(pagePath)) {
894
+ return path.relative(projectRoot, pagePath);
895
+ }
896
+ if (fs.existsSync(indexPath)) {
897
+ return path.relative(projectRoot, indexPath);
898
+ }
899
+ } else if (entry.isFile() && entry.name.endsWith(ext)) {
900
+ // Pages Router file-based
901
+ return path.relative(projectRoot, path.join(parentPath, entry.name));
902
+ }
903
+ }
904
+ }
905
+ }
906
+ } catch {
907
+ // Skip if directory can't be read
908
+ }
909
+ }
910
+
624
911
  return null;
625
912
  }
626
913
 
@@ -701,6 +988,76 @@ function generateSimpleDiff(original: string, modified: string): string {
701
988
  return diff.join("\n");
702
989
  }
703
990
 
991
+ /**
992
+ * Patch interface for search/replace operations
993
+ */
994
+ interface Patch {
995
+ search: string;
996
+ replace: string;
997
+ explanation: string;
998
+ }
999
+
1000
+ /**
1001
+ * Result of applying patches to a file
1002
+ */
1003
+ interface ApplyPatchesResult {
1004
+ success: boolean;
1005
+ modifiedContent: string;
1006
+ appliedPatches: number;
1007
+ failedPatches: { patch: Patch; error: string }[];
1008
+ }
1009
+
1010
+ /**
1011
+ * Apply search/replace patches to file content
1012
+ * This is the core of the patch-based editing system
1013
+ */
1014
+ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesResult {
1015
+ let content = originalContent;
1016
+ let appliedPatches = 0;
1017
+ const failedPatches: { patch: Patch; error: string }[] = [];
1018
+
1019
+ for (const patch of patches) {
1020
+ // Normalize line endings for matching
1021
+ const normalizedSearch = patch.search.replace(/\\n/g, "\n");
1022
+ const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
1023
+
1024
+ // Check if search string exists in content
1025
+ if (!content.includes(normalizedSearch)) {
1026
+ // Try with different whitespace normalization
1027
+ const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
1028
+ const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
1029
+
1030
+ if (!regex.test(content)) {
1031
+ failedPatches.push({
1032
+ patch,
1033
+ error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
1034
+ });
1035
+ continue;
1036
+ }
1037
+
1038
+ // If regex matched, use regex replace
1039
+ content = content.replace(regex, normalizedReplace);
1040
+ appliedPatches++;
1041
+ } else {
1042
+ // Exact match found - apply the replacement
1043
+ // Only replace the first occurrence to be safe
1044
+ const index = content.indexOf(normalizedSearch);
1045
+ content =
1046
+ content.substring(0, index) +
1047
+ normalizedReplace +
1048
+ content.substring(index + normalizedSearch.length);
1049
+ appliedPatches++;
1050
+ }
1051
+ }
1052
+
1053
+ return {
1054
+ success: failedPatches.length === 0,
1055
+ modifiedContent: content,
1056
+ appliedPatches,
1057
+ failedPatches,
1058
+ };
1059
+ }
1060
+
704
1061
  /**
705
1062
  * Validate that AI modifications are surgical edits, not complete rewrites
706
1063
  */