next-ai-editor 0.1.1 → 0.1.2

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.
Files changed (50) hide show
  1. package/dist/{AIEditorProvider-D-w9-GZb.js → AIEditorProvider-CFFnEtEB.js} +849 -402
  2. package/dist/AIEditorProvider-CFFnEtEB.js.map +1 -0
  3. package/dist/{AIEditorProvider-Bs9zUVrL.cjs → AIEditorProvider-CmiACRfw.cjs} +825 -378
  4. package/dist/AIEditorProvider-CmiACRfw.cjs.map +1 -0
  5. package/dist/client/AIEditorProvider.d.ts +8 -16
  6. package/dist/client/AIEditorProvider.d.ts.map +1 -1
  7. package/dist/client/fiber-utils.d.ts +35 -0
  8. package/dist/client/fiber-utils.d.ts.map +1 -0
  9. package/dist/client/index.d.ts +1 -1
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/query-params.d.ts +9 -0
  12. package/dist/client/query-params.d.ts.map +1 -0
  13. package/dist/client.cjs +1 -1
  14. package/dist/client.js +1 -1
  15. package/dist/{index-BFa7H-uO.js → index-3OMXRwpD.js} +601 -224
  16. package/dist/index-3OMXRwpD.js.map +1 -0
  17. package/dist/{index-DnoYi4f8.cjs → index-9QODCOgD.cjs} +595 -218
  18. package/dist/index-9QODCOgD.cjs.map +1 -0
  19. package/dist/index.cjs +6 -2
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.js +14 -10
  22. package/dist/path-utils-Bai2xKx9.js +36 -0
  23. package/dist/path-utils-Bai2xKx9.js.map +1 -0
  24. package/dist/path-utils-DYzEWUGy.cjs +35 -0
  25. package/dist/path-utils-DYzEWUGy.cjs.map +1 -0
  26. package/dist/server/handlers/edit.d.ts.map +1 -1
  27. package/dist/server/handlers/index.d.ts +1 -0
  28. package/dist/server/handlers/index.d.ts.map +1 -1
  29. package/dist/server/handlers/read.d.ts.map +1 -1
  30. package/dist/server/handlers/resolve.d.ts.map +1 -1
  31. package/dist/server/handlers/suggestions.d.ts +3 -0
  32. package/dist/server/handlers/suggestions.d.ts.map +1 -0
  33. package/dist/server/index.d.ts +1 -0
  34. package/dist/server/index.d.ts.map +1 -1
  35. package/dist/server/utils/ast.d.ts +10 -0
  36. package/dist/server/utils/ast.d.ts.map +1 -1
  37. package/dist/server/utils/source-map.d.ts +5 -0
  38. package/dist/server/utils/source-map.d.ts.map +1 -1
  39. package/dist/server.cjs +5 -1
  40. package/dist/server.cjs.map +1 -1
  41. package/dist/server.js +13 -9
  42. package/dist/shared/path-utils.d.ts +24 -0
  43. package/dist/shared/path-utils.d.ts.map +1 -0
  44. package/dist/shared/types.d.ts +30 -0
  45. package/dist/shared/types.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/dist/AIEditorProvider-Bs9zUVrL.cjs.map +0 -1
  48. package/dist/AIEditorProvider-D-w9-GZb.js.map +0 -1
  49. package/dist/index-BFa7H-uO.js.map +0 -1
  50. package/dist/index-DnoYi4f8.cjs.map +0 -1
@@ -1,12 +1,13 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import fs from "fs/promises";
3
- import * as parser from "@babel/parser";
4
3
  import { ChatAnthropic } from "@langchain/anthropic";
5
4
  import { SystemMessage, HumanMessage } from "@langchain/core/messages";
6
5
  import path from "path";
6
+ import * as parser from "@babel/parser";
7
7
  import traverse from "@babel/traverse";
8
8
  import * as t from "@babel/types";
9
9
  import { SourceMapConsumer } from "@jridgewell/source-map";
10
+ import { c as cleanPath, s as shouldSkipPath, n as normalizeSourcePath } from "./path-utils-Bai2xKx9.js";
10
11
  import crypto from "crypto";
11
12
  function validateDevMode() {
12
13
  if (process.env.NODE_ENV !== "development") {
@@ -65,9 +66,74 @@ function parseFile(content) {
65
66
  return null;
66
67
  }
67
68
  }
69
+ function extractComponentName(ast) {
70
+ let componentName = null;
71
+ traverse(ast, {
72
+ ExportDefaultDeclaration(path2) {
73
+ var _a;
74
+ if (t.isFunctionDeclaration(path2.node.declaration)) {
75
+ componentName = ((_a = path2.node.declaration.id) == null ? void 0 : _a.name) || null;
76
+ } else if (t.isArrowFunctionExpression(path2.node.declaration)) {
77
+ componentName = "default";
78
+ } else if (t.isIdentifier(path2.node.declaration)) {
79
+ componentName = path2.node.declaration.name;
80
+ }
81
+ },
82
+ ExportNamedDeclaration(path2) {
83
+ var _a;
84
+ if (t.isFunctionDeclaration(path2.node.declaration)) {
85
+ componentName = ((_a = path2.node.declaration.id) == null ? void 0 : _a.name) || null;
86
+ } else if (t.isVariableDeclaration(path2.node.declaration)) {
87
+ const declarator = path2.node.declaration.declarations[0];
88
+ if (t.isIdentifier(declarator.id)) {
89
+ componentName = declarator.id.name;
90
+ }
91
+ }
92
+ }
93
+ });
94
+ return componentName;
95
+ }
96
+ function validateGeneratedCode(newCode, originalCode, fileContent) {
97
+ try {
98
+ const isFullComponent = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?(default\s+)?const\s+\w+\s*=/.test(newCode.trim());
99
+ if (isFullComponent) {
100
+ let codeToValidate = newCode;
101
+ if (fileContent) {
102
+ const interfaceMatches = fileContent.match(
103
+ /^(interface|type)\s+\w+[^}]*\}/gm
104
+ );
105
+ if (interfaceMatches) {
106
+ codeToValidate = interfaceMatches.join("\n\n") + "\n\n" + newCode;
107
+ }
108
+ }
109
+ parser.parse(codeToValidate, {
110
+ sourceType: "module",
111
+ plugins: ["jsx", "typescript"]
112
+ });
113
+ } else {
114
+ const wrapped = `function _() { return (${newCode}); }`;
115
+ parser.parse(wrapped, {
116
+ sourceType: "module",
117
+ plugins: ["jsx", "typescript"]
118
+ });
119
+ }
120
+ } catch (e) {
121
+ console.error("Generated code parse error:", e);
122
+ return false;
123
+ }
124
+ const origBraces = (originalCode.match(/[{}]/g) || []).length;
125
+ const newBraces = (newCode.match(/[{}]/g) || []).length;
126
+ const origTags = (originalCode.match(/[<>]/g) || []).length;
127
+ const newTags = (newCode.match(/[<>]/g) || []).length;
128
+ if (Math.abs(origBraces - newBraces) > 4 || Math.abs(origTags - newTags) > 4) {
129
+ console.warn(
130
+ `Structure changed significantly: braces ${origBraces}->${newBraces}, tags ${origTags}->${newTags}`
131
+ );
132
+ }
133
+ return true;
134
+ }
68
135
  function findTargetElement(ast, fileContent, options) {
69
136
  const { componentName, lineNumber, elementContext } = options;
70
- const matches = [];
71
137
  let componentNode = null;
72
138
  let componentStart = 0;
73
139
  let componentEnd = Infinity;
@@ -118,6 +184,7 @@ function findTargetElement(ast, fileContent, options) {
118
184
  componentEnd = fallback.end;
119
185
  }
120
186
  const allElementsByTag = /* @__PURE__ */ new Map();
187
+ const elementsAtLine = [];
121
188
  traverse(ast, {
122
189
  JSXElement(path2) {
123
190
  const loc = path2.node.loc;
@@ -133,59 +200,89 @@ function findTargetElement(ast, fileContent, options) {
133
200
  }
134
201
  allElementsByTag.get(tagName).push({ node: path2.node, startLine, endLine, score: 0 });
135
202
  }
136
- if (elementContext) {
137
- const score = scoreElementMatch(path2.node, elementContext, fileContent);
138
- if (score > 0) {
139
- matches.push({ node: path2.node, startLine, endLine, score });
140
- }
141
- } else if (Math.abs(startLine - lineNumber) <= 5) {
142
- matches.push({
143
- node: path2.node,
144
- startLine,
145
- endLine,
146
- score: 100 - Math.abs(startLine - lineNumber)
147
- });
203
+ if (startLine === lineNumber) {
204
+ elementsAtLine.push({ node: path2.node, startLine, endLine, score: 0 });
148
205
  }
149
206
  }
150
207
  });
151
- if (matches.length === 0) {
152
- if (componentNode && componentStart > 0) {
208
+ if (elementsAtLine.length > 0) {
209
+ if (elementsAtLine.length === 1) {
210
+ const target = elementsAtLine[0];
153
211
  return {
154
- startLine: componentStart,
155
- endLine: componentEnd,
212
+ startLine: target.startLine,
213
+ endLine: target.endLine,
156
214
  componentStart,
157
215
  componentEnd
158
216
  };
159
217
  }
160
- return null;
218
+ if (elementContext) {
219
+ for (const elem of elementsAtLine) {
220
+ if (t.isJSXElement(elem.node)) {
221
+ const score = scoreElementMatch(elem.node, elementContext, fileContent);
222
+ elem.score = score;
223
+ }
224
+ }
225
+ elementsAtLine.sort((a, b) => b.score - a.score);
226
+ if (elementsAtLine[0].score > 0) {
227
+ return {
228
+ startLine: elementsAtLine[0].startLine,
229
+ endLine: elementsAtLine[0].endLine,
230
+ componentStart,
231
+ componentEnd
232
+ };
233
+ }
234
+ }
235
+ return {
236
+ startLine: elementsAtLine[0].startLine,
237
+ endLine: elementsAtLine[0].endLine,
238
+ componentStart,
239
+ componentEnd
240
+ };
161
241
  }
162
242
  if ((elementContext == null ? void 0 : elementContext.nthOfType) && elementContext.tagName) {
163
243
  const allOfTag = allElementsByTag.get(elementContext.tagName);
164
244
  if (allOfTag && allOfTag.length >= elementContext.nthOfType) {
165
245
  const target = allOfTag[elementContext.nthOfType - 1];
166
- console.log(
167
- ` Using nthOfType=${elementContext.nthOfType}: found ${allOfTag.length} <${elementContext.tagName}> elements`
168
- );
169
246
  return {
170
247
  startLine: target.startLine,
171
248
  endLine: target.endLine,
172
249
  componentStart,
173
250
  componentEnd
174
251
  };
175
- } else {
176
- console.log(
177
- ` nthOfType=${elementContext.nthOfType} but only found ${(allOfTag == null ? void 0 : allOfTag.length) || 0} <${elementContext.tagName}> elements`
178
- );
179
252
  }
180
253
  }
181
- matches.sort((a, b) => b.score - a.score);
182
- const best = matches[0];
183
- return {
184
- startLine: best.startLine,
185
- endLine: best.endLine,
186
- componentStart,
187
- componentEnd
188
- };
254
+ const nearbyElements = [];
255
+ traverse(ast, {
256
+ JSXElement(path2) {
257
+ const loc = path2.node.loc;
258
+ if (!loc) return;
259
+ const startLine = loc.start.line;
260
+ const endLine = loc.end.line;
261
+ if (startLine < componentStart || endLine > componentEnd) return;
262
+ if (Math.abs(startLine - lineNumber) <= 5) {
263
+ const score = elementContext ? scoreElementMatch(path2.node, elementContext, fileContent) : 100 - Math.abs(startLine - lineNumber);
264
+ nearbyElements.push({ node: path2.node, startLine, endLine, score });
265
+ }
266
+ }
267
+ });
268
+ if (nearbyElements.length > 0) {
269
+ nearbyElements.sort((a, b) => b.score - a.score);
270
+ return {
271
+ startLine: nearbyElements[0].startLine,
272
+ endLine: nearbyElements[0].endLine,
273
+ componentStart,
274
+ componentEnd
275
+ };
276
+ }
277
+ if (componentNode && componentStart > 0) {
278
+ return {
279
+ startLine: componentStart,
280
+ endLine: componentEnd,
281
+ componentStart,
282
+ componentEnd
283
+ };
284
+ }
285
+ return null;
189
286
  }
190
287
  function scoreElementMatch(node, context, fileContent) {
191
288
  let score = 0;
@@ -269,7 +366,8 @@ async function handleEdit(req) {
269
366
  componentName,
270
367
  suggestion,
271
368
  elementContext,
272
- editHistory
369
+ editHistory,
370
+ parentInstance
273
371
  } = body;
274
372
  const projectRoot = process.cwd();
275
373
  const normalizedPath = normalizePath(filePath);
@@ -281,7 +379,6 @@ async function handleEdit(req) {
281
379
  );
282
380
  }
283
381
  const fileContent = await fs.readFile(absolutePath, "utf-8");
284
- console.log(`[/edit] componentName="${componentName}", filePath="${filePath}"`);
285
382
  const ast = parseFile(fileContent);
286
383
  if (!ast) {
287
384
  return NextResponse.json(
@@ -300,20 +397,10 @@ async function handleEdit(req) {
300
397
  { status: 400 }
301
398
  );
302
399
  }
303
- console.log(
304
- `📍 Found element <${(elementContext == null ? void 0 : elementContext.tagName) || "component"}> at lines ${target.startLine}-${target.endLine} (component: ${target.componentStart}-${target.componentEnd})`
305
- );
306
- console.log(
307
- ` Element context: tagName=${elementContext == null ? void 0 : elementContext.tagName}, nthOfType=${elementContext == null ? void 0 : elementContext.nthOfType}, textContent="${elementContext == null ? void 0 : elementContext.textContent}"`
308
- );
309
400
  const lines = fileContent.split("\n");
310
- const foundElementCode = lines.slice(target.startLine - 1, Math.min(target.startLine + 2, target.endLine)).join("\n");
311
- console.log(` Found element preview:
312
- ${foundElementCode}`);
313
401
  const targetCode = lines.slice(target.startLine - 1, target.endLine).join("\n");
314
402
  const baseIndentation = ((_a = lines[target.startLine - 1].match(/^(\s*)/)) == null ? void 0 : _a[1]) || "";
315
403
  if (target.componentStart <= 0 || target.componentEnd === Infinity) {
316
- console.error("❌ Invalid component bounds detected");
317
404
  return NextResponse.json({
318
405
  success: false,
319
406
  error: `Could not determine component bounds. Component: ${target.componentStart}-${target.componentEnd}`
@@ -351,7 +438,8 @@ ${foundElementCode}`);
351
438
  targetEndLine: target.endLine,
352
439
  componentStart: target.componentStart,
353
440
  componentEnd: target.componentEnd,
354
- editHistory: editHistory || []
441
+ editHistory: editHistory || [],
442
+ parentInstance
355
443
  });
356
444
  if (!newCode) {
357
445
  return NextResponse.json({
@@ -359,38 +447,56 @@ ${foundElementCode}`);
359
447
  error: "AI failed to generate valid edit"
360
448
  });
361
449
  }
362
- console.log("Raw AI response length:", newCode.length);
363
- console.log("Looking for // FULL_COMPONENT marker...");
450
+ const parentInstanceMatch = newCode.match(/\/\/ EDIT_PARENT_INSTANCE\s*\n([\s\S]+)/);
451
+ if (parentInstanceMatch && parentInstance) {
452
+ const parentCode = parentInstanceMatch[1].trim();
453
+ const parentNormalizedPath = normalizePath(parentInstance.filePath);
454
+ const parentAbsolutePath = await resolveFilePath(projectRoot, parentNormalizedPath);
455
+ if (!parentAbsolutePath) {
456
+ return NextResponse.json({
457
+ success: false,
458
+ error: `Parent file not found: ${parentNormalizedPath}`
459
+ });
460
+ }
461
+ const parentFileContent = await fs.readFile(parentAbsolutePath, "utf-8");
462
+ const parentLines = parentFileContent.split("\n");
463
+ const newParentLines = [...parentLines];
464
+ newParentLines.splice(
465
+ parentInstance.lineStart - 1,
466
+ parentInstance.lineEnd - parentInstance.lineStart + 1,
467
+ ...parentCode.split("\n")
468
+ );
469
+ await fs.writeFile(parentAbsolutePath, newParentLines.join("\n"), "utf-8");
470
+ return NextResponse.json({
471
+ success: true,
472
+ fileSnapshot: parentFileContent,
473
+ generatedCode: parentCode,
474
+ modifiedLines: {
475
+ start: parentInstance.lineStart,
476
+ end: parentInstance.lineEnd
477
+ },
478
+ editedFile: parentInstance.filePath
479
+ // Indicate which file was edited
480
+ });
481
+ }
364
482
  const fullComponentMatch = newCode.match(/\/\/ FULL_COMPONENT\s*\n([\s\S]+)/);
365
483
  let codeToApply = newCode;
366
484
  let startLineToReplace = target.startLine;
367
485
  let endLineToReplace = target.endLine;
486
+ const isFullComponentDeclaration = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?const\s+\w+\s*=/.test(newCode.trim());
368
487
  if (fullComponentMatch) {
369
- console.log("Found // FULL_COMPONENT marker, extracting full component code");
370
488
  codeToApply = fullComponentMatch[1].trim();
371
489
  startLineToReplace = target.componentStart;
372
490
  endLineToReplace = target.componentEnd;
373
- console.log(
374
- `🔄 AI returned full component modification (lines ${startLineToReplace}-${endLineToReplace})`
375
- );
376
- console.log("Extracted component code length:", codeToApply.length);
377
- console.log("Extracted component code (first 300 chars):", codeToApply.substring(0, 300));
378
- } else {
379
- console.log("No // FULL_COMPONENT marker found, treating as target element modification");
380
- console.log(
381
- `🔄 AI returned target element modification (lines ${startLineToReplace}-${endLineToReplace})`
382
- );
491
+ } else if (isFullComponentDeclaration && target.startLine !== target.componentStart) {
492
+ codeToApply = newCode;
493
+ startLineToReplace = target.componentStart;
494
+ endLineToReplace = target.componentEnd;
383
495
  }
384
- console.log("Code to apply (first 200 chars):", codeToApply.substring(0, 200));
385
496
  if (!validateGeneratedCode(codeToApply, targetCode, fileContent)) {
386
- console.error("❌ Generated code failed validation");
387
- console.error("=== Generated code START ===");
388
- console.error(codeToApply);
389
- console.error("=== Generated code END ===");
390
- console.error(`Length: ${codeToApply.length} chars, ${codeToApply.split("\n").length} lines`);
391
497
  return NextResponse.json({
392
498
  success: false,
393
- error: "Generated code is invalid - check server logs for details"
499
+ error: "Generated code is invalid"
394
500
  });
395
501
  }
396
502
  const newLines = [...lines];
@@ -400,7 +506,6 @@ ${foundElementCode}`);
400
506
  ...codeToApply.split("\n")
401
507
  );
402
508
  await fs.writeFile(absolutePath, newLines.join("\n"), "utf-8");
403
- console.log(`✅ Updated ${normalizedPath}`);
404
509
  return NextResponse.json({
405
510
  success: true,
406
511
  fileSnapshot: fileContent,
@@ -410,10 +515,11 @@ ${foundElementCode}`);
410
515
  modifiedLines: {
411
516
  start: startLineToReplace,
412
517
  end: endLineToReplace
413
- }
518
+ },
519
+ editedFile: filePath
520
+ // Indicate which file was edited
414
521
  });
415
522
  } catch (error) {
416
- console.error("AI Editor error:", error);
417
523
  return NextResponse.json(
418
524
  { success: false, error: String(error) },
419
525
  { status: 500 }
@@ -425,7 +531,8 @@ async function generateEdit(options) {
425
531
  fullComponentCode,
426
532
  suggestion,
427
533
  baseIndentation,
428
- editHistory
534
+ editHistory,
535
+ parentInstance
429
536
  } = options;
430
537
  const apiKey = process.env.ANTHROPIC_API_KEY;
431
538
  if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set");
@@ -438,25 +545,29 @@ async function generateEdit(options) {
438
545
  const systemPrompt = `You are a precise code editor for React/JSX components.
439
546
 
440
547
  WHAT YOU'LL SEE:
441
- - Full component code with line annotations
442
- - The clicked element is marked with "// ← CLICKED ELEMENT STARTS" and "// ← CLICKED ELEMENT ENDS"
443
- - Lines within the element have "// (clicked element)"
444
- - These are just annotations to help you - they are NOT part of the actual code
548
+ - Component Definition: Full code of the clicked component with line annotations
549
+ * The clicked element is marked with "// ← CLICKED ELEMENT STARTS" and "// ← CLICKED ELEMENT ENDS"
550
+ * These are just annotations - NOT part of the actual code
551
+ ${parentInstance ? `- Parent Instance: Where this component is used, with "// COMPONENT USAGE" marking the usage line` : ""}
445
552
 
446
- YOUR TASK:
447
- Modify ONLY the marked element (unless the request requires changing its parent/wrapper).
553
+ YOUR DECISION - Choose ONE approach based on the user's request:
448
554
 
449
- OUTPUT:
450
- - Just the modified element code (WITHOUT the annotation comments)
451
- - OR if broader changes needed: "// FULL_COMPONENT\\n" + complete modified component (WITHOUT annotation comments)
555
+ 1. EDIT COMPONENT DEFINITION (most common):
556
+ - Modify styling, layout, or behavior that should apply to ALL instances
557
+ - Example requests: "make the button blue", "add padding", "change font size"
558
+ - Output: Just the modified element OR "// FULL_COMPONENT\\n" + complete modified component
559
+
560
+ ${parentInstance ? `2. EDIT PARENT INSTANCE (when user wants to modify THIS specific usage):
561
+ - Change props, text, or configuration for THIS specific instance only
562
+ - Example requests: "change this title to...", "remove this card", "add another button here"
563
+ - Output: "// EDIT_PARENT_INSTANCE\\n" + complete modified parent component` : ""}
452
564
 
453
565
  RULES:
454
566
  - Output ONLY code, no explanations
455
567
  - No markdown fences
456
- - Do NOT include the "// ← CLICKED ELEMENT" annotation comments in your output
568
+ - Do NOT include annotation comments in your output
457
569
  - Preserve indentation
458
- - Make minimal changes
459
- - Do NOT modify unrelated elements`;
570
+ - Make minimal changes`;
460
571
  let userPrompt = "";
461
572
  if (editHistory.length > 0) {
462
573
  userPrompt += "Previous edits made to this element:\n";
@@ -466,16 +577,36 @@ RULES:
466
577
  });
467
578
  userPrompt += "\n";
468
579
  }
469
- userPrompt += `Component (clicked element is annotated with "// ← CLICKED ELEMENT" comments):
580
+ userPrompt += `COMPONENT DEFINITION (clicked element is annotated):
470
581
 
471
582
  \`\`\`jsx
472
583
  ${fullComponentCode}
473
584
  \`\`\`
585
+ `;
586
+ if (parentInstance) {
587
+ const parentLines = parentInstance.content.split("\n");
588
+ const annotatedParentLines = parentLines.map((line, idx) => {
589
+ const lineNum = parentInstance.lineStart + idx;
590
+ const isUsageLine = lineNum >= parentInstance.usageLineStart && lineNum <= parentInstance.usageLineEnd;
591
+ return line + (isUsageLine ? " // ← COMPONENT USAGE" : "");
592
+ });
593
+ userPrompt += `
594
+ PARENT INSTANCE (where component is used - in ${parentInstance.filePath}):
474
595
 
596
+ \`\`\`jsx
597
+ ${annotatedParentLines.join("\n")}
598
+ \`\`\`
599
+ `;
600
+ }
601
+ userPrompt += `
475
602
  User request: "${suggestion}"
476
- ${editHistory.length > 0 ? "\n(Build upon previous changes)" : ""}
603
+ ${editHistory.length > 0 ? "(Build upon previous changes)" : ""}
604
+
605
+ ${parentInstance ? `Decide whether to:
606
+ 1. Edit the component definition (for changes that affect ALL instances)
607
+ 2. Edit the parent instance (for changes specific to THIS usage)
477
608
 
478
- Modify the annotated element to fulfill this request. Remember: do NOT include the annotation comments in your output.`;
609
+ Then output the appropriate code with the correct marker.` : `Modify the annotated element to fulfill this request. Remember: do NOT include the annotation comments in your output.`}`;
479
610
  try {
480
611
  const response = await model.invoke([
481
612
  new SystemMessage(systemPrompt),
@@ -485,12 +616,18 @@ Modify the annotated element to fulfill this request. Remember: do NOT include t
485
616
  code = cleanGeneratedCode(code, baseIndentation);
486
617
  return code || null;
487
618
  } catch (e) {
488
- console.error("AI generation error:", e);
489
619
  return null;
490
620
  }
491
621
  }
492
622
  function cleanGeneratedCode(code, baseIndentation) {
493
623
  var _a, _b, _c;
624
+ const isParentEdit = code.trim().startsWith("// EDIT_PARENT_INSTANCE");
625
+ if (isParentEdit) {
626
+ const marker2 = "// EDIT_PARENT_INSTANCE\n";
627
+ code = code.replace(/^\/\/ EDIT_PARENT_INSTANCE\n?/, "");
628
+ code = code.replace(/^```[\w]*\n?/gm, "").replace(/\n?```$/gm, "").replace(/\s*\/\/ ← COMPONENT USAGE/g, "").trim();
629
+ return marker2 + code;
630
+ }
494
631
  const isFullComponent = code.trim().startsWith("// FULL_COMPONENT");
495
632
  let marker = "";
496
633
  if (isFullComponent) {
@@ -521,43 +658,6 @@ function cleanGeneratedCode(code, baseIndentation) {
521
658
  }
522
659
  return marker + code;
523
660
  }
524
- function validateGeneratedCode(newCode, originalCode, fileContent) {
525
- try {
526
- const isFullComponent = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?(default\s+)?const\s+\w+\s*=/.test(newCode.trim());
527
- if (isFullComponent) {
528
- let codeToValidate = newCode;
529
- if (fileContent) {
530
- const interfaceMatches = fileContent.match(/^(interface|type)\s+\w+[^}]*\}/gm);
531
- if (interfaceMatches) {
532
- codeToValidate = interfaceMatches.join("\n\n") + "\n\n" + newCode;
533
- }
534
- }
535
- parser.parse(codeToValidate, {
536
- sourceType: "module",
537
- plugins: ["jsx", "typescript"]
538
- });
539
- } else {
540
- const wrapped = `function _() { return (${newCode}); }`;
541
- parser.parse(wrapped, {
542
- sourceType: "module",
543
- plugins: ["jsx", "typescript"]
544
- });
545
- }
546
- } catch (e) {
547
- console.error("Generated code parse error:", e);
548
- return false;
549
- }
550
- const origBraces = (originalCode.match(/[{}]/g) || []).length;
551
- const newBraces = (newCode.match(/[{}]/g) || []).length;
552
- const origTags = (originalCode.match(/[<>]/g) || []).length;
553
- const newTags = (newCode.match(/[<>]/g) || []).length;
554
- if (Math.abs(origBraces - newBraces) > 4 || Math.abs(origTags - newTags) > 4) {
555
- console.warn(
556
- `Structure changed significantly: braces ${origBraces}->${newBraces}, tags ${origTags}->${newTags}`
557
- );
558
- }
559
- return true;
560
- }
561
661
  function parseDebugStack(stack) {
562
662
  if (!stack) return null;
563
663
  const stackStr = typeof stack === "string" ? stack : stack.stack || String(stack);
@@ -582,8 +682,8 @@ function parseDebugStack(stack) {
582
682
  chunkId = chunkMatch[1];
583
683
  filePath = filePath.replace(/\?[^:]*$/, "");
584
684
  }
585
- filePath = cleanPathTurbopack(filePath);
586
- if (!shouldSkip(filePath)) {
685
+ filePath = cleanPath(filePath);
686
+ if (!shouldSkipPath(filePath)) {
587
687
  console.log("parseDebugStack extracted:", { filePath, line, column });
588
688
  return { filePath, line, column, chunkId };
589
689
  }
@@ -595,46 +695,49 @@ function parseDebugStack(stack) {
595
695
  );
596
696
  return null;
597
697
  }
598
- function cleanPathTurbopack(p) {
599
- let cleaned = p;
600
- cleaned = cleaned.replace(/^about:\/\/React\/Server\//, "");
601
- if (cleaned.startsWith("file://")) {
602
- cleaned = cleaned.replace(/^file:\/\//, "");
603
- }
604
- try {
605
- if (cleaned.includes("%")) {
606
- cleaned = decodeURIComponent(cleaned);
698
+ function parseDebugStackFrames(stack) {
699
+ if (!stack) return [];
700
+ const stackStr = typeof stack === "string" ? stack : stack.stack || String(stack);
701
+ const frames = stackStr.split("\n");
702
+ const skipPatterns = [
703
+ "node_modules",
704
+ "SegmentViewNode",
705
+ "LayoutRouter",
706
+ "ErrorBoundary",
707
+ "fakeJSXCallSite",
708
+ "react_stack_bottom_frame"
709
+ ];
710
+ const positions = [];
711
+ for (const frame of frames) {
712
+ if (skipPatterns.some((p) => frame.includes(p))) continue;
713
+ const match = frame.match(/at\s+(\w+)\s+\((.+?):(\d+):(\d+)\)?$/);
714
+ if (match) {
715
+ match[1];
716
+ let filePath = match[2];
717
+ const line = parseInt(match[3], 10);
718
+ const column = parseInt(match[4], 10);
719
+ let chunkId;
720
+ const chunkMatch = filePath.match(/\?([^:]+)$/);
721
+ if (chunkMatch) {
722
+ chunkId = chunkMatch[1];
723
+ filePath = filePath.replace(/\?[^:]*$/, "");
724
+ }
725
+ filePath = cleanPath(filePath);
726
+ if (!shouldSkipPath(filePath)) {
727
+ positions.push({ filePath, line, column, chunkId });
728
+ if (positions.length >= 2) break;
729
+ }
607
730
  }
608
- } catch (e) {
609
- console.warn("Failed to decode URL-encoded path:", cleaned, e);
610
- }
611
- cleaned = cleaned.replace(/^webpack-internal:\/\/\/\([^)]+\)\/\.\//, "").replace(/^webpack-internal:\/\/\/\([^)]+\)\//, "").replace(/^webpack-internal:\/\//, "").replace(/^webpack:\/\/[^/]*\//, "").replace(/^\([^)]+\)\//, "").replace(/\?.*$/, "");
612
- if (cleaned.startsWith(".next/") || path.isAbsolute(cleaned)) {
613
- return cleaned;
614
731
  }
615
- return cleaned;
616
- }
617
- function cleanPath(p) {
618
- return cleanPathTurbopack(p);
619
- }
620
- function normalizeSourcePath(source, projectRoot) {
621
- let cleaned = cleanPath(source);
622
- cleaned = cleaned.replace(/\\/g, "/");
623
- if (cleaned.startsWith(projectRoot)) {
624
- cleaned = cleaned.substring(projectRoot.length + 1);
625
- }
626
- return cleaned.replace(/^\/+/, "");
627
- }
628
- function shouldSkip(p) {
629
- if (!p) return true;
630
- return ["node_modules", "next/dist", "react-dom"].some((s) => p.includes(s));
732
+ console.log(`parseDebugStackFrames extracted ${positions.length} frames:`, positions);
733
+ return positions;
631
734
  }
632
735
  async function resolveOriginalPosition(compiledPos, projectRoot) {
633
736
  try {
634
737
  console.log("resolveOriginalPosition called with:", compiledPos);
635
738
  let compiledFilePath = compiledPos.filePath;
636
- compiledFilePath = cleanPathTurbopack(compiledFilePath);
637
- console.log("After cleanPathTurbopack:", compiledFilePath);
739
+ compiledFilePath = cleanPath(compiledFilePath);
740
+ console.log("After cleanPath:", compiledFilePath);
638
741
  if (compiledFilePath.startsWith("http://") || compiledFilePath.startsWith("https://")) {
639
742
  const url = new URL(compiledFilePath);
640
743
  const pathname = url.pathname;
@@ -807,7 +910,6 @@ async function getOriginalPositionFromDebugStack(debugStack, projectRoot) {
807
910
  return await resolveOriginalPosition(compiledPos, projectRoot);
808
911
  }
809
912
  async function handleRead(req) {
810
- var _a;
811
913
  const devModeError = validateDevMode();
812
914
  if (devModeError) return devModeError;
813
915
  const { searchParams } = new URL(req.url);
@@ -824,6 +926,11 @@ async function handleRead(req) {
824
926
  textContent: textContent || void 0,
825
927
  className: className || void 0
826
928
  } : void 0;
929
+ const parentFilePath = searchParams.get("parentFilePath") || "";
930
+ const parentLine = parseInt(searchParams.get("parentLine") || "0");
931
+ const parentComponentName = searchParams.get("parentComponentName") || "";
932
+ const parentDebugStack = searchParams.get("parentDebugStack") || "";
933
+ const childKey = searchParams.get("childKey") || "";
827
934
  const projectRoot = process.cwd();
828
935
  if (debugStack) {
829
936
  const compiledPos = parseDebugStack(debugStack);
@@ -854,21 +961,7 @@ async function handleRead(req) {
854
961
  { status: 400 }
855
962
  );
856
963
  }
857
- let componentName = "";
858
- traverse(ast, {
859
- ExportDefaultDeclaration(path2) {
860
- var _a2;
861
- if (t.isFunctionDeclaration(path2.node.declaration)) {
862
- componentName = ((_a2 = path2.node.declaration.id) == null ? void 0 : _a2.name) || "";
863
- }
864
- },
865
- ExportNamedDeclaration(path2) {
866
- var _a2;
867
- if (t.isFunctionDeclaration(path2.node.declaration)) {
868
- componentName = ((_a2 = path2.node.declaration.id) == null ? void 0 : _a2.name) || "";
869
- }
870
- }
871
- });
964
+ const componentName = extractComponentName(ast);
872
965
  if (!componentName) {
873
966
  return NextResponse.json({
874
967
  success: true,
@@ -890,28 +983,79 @@ async function handleRead(req) {
890
983
  lineEnd: content.split("\n").length
891
984
  });
892
985
  }
893
- console.log(`[/read] Found element:`);
894
- console.log(
895
- ` Component: ${componentName} (lines ${target.componentStart}-${target.componentEnd})`
896
- );
897
- console.log(` Target element: lines ${target.startLine}-${target.endLine}`);
898
- console.log(
899
- ` Element context: tagName=${elementContext == null ? void 0 : elementContext.tagName}, nthOfType=${elementContext == null ? void 0 : elementContext.nthOfType}`
900
- );
901
- const foundLines = content.split("\n").slice(
902
- target.startLine - 1,
903
- Math.min(target.startLine + 2, target.endLine)
904
- );
905
- console.log(` Preview: ${(_a = foundLines[0]) == null ? void 0 : _a.trim()}`);
906
- console.log(
907
- ` textContent="${elementContext == null ? void 0 : elementContext.textContent}", className="${elementContext == null ? void 0 : elementContext.className}"`
908
- );
909
986
  const lines = content.split("\n");
910
987
  const componentLines = lines.slice(
911
988
  target.componentStart - 1,
912
989
  target.componentEnd
913
990
  );
914
991
  const preview = componentLines.join("\n");
992
+ let parentInstance = null;
993
+ if (parentDebugStack) {
994
+ try {
995
+ let resolvedParentPath = parentFilePath;
996
+ let resolvedParentLine = parentLine;
997
+ let resolvedParentComponentName = parentComponentName;
998
+ if (resolvedParentComponentName && parentDebugStack) {
999
+ const compiledPos = parseDebugStack(parentDebugStack);
1000
+ if (compiledPos) {
1001
+ const originalPos = await resolveOriginalPosition(
1002
+ compiledPos,
1003
+ projectRoot
1004
+ );
1005
+ if (originalPos) {
1006
+ resolvedParentPath = originalPos.source;
1007
+ resolvedParentLine = originalPos.line;
1008
+ }
1009
+ }
1010
+ }
1011
+ const normalizedParentPath = normalizePath(resolvedParentPath);
1012
+ const absoluteParentPath = await resolveFilePath(
1013
+ projectRoot,
1014
+ normalizedParentPath
1015
+ );
1016
+ if (absoluteParentPath && resolvedParentComponentName) {
1017
+ const parentContent = await fs.readFile(absoluteParentPath, "utf-8");
1018
+ const parentAst = parseFile(parentContent);
1019
+ if (parentAst) {
1020
+ let nthOfType2 = void 0;
1021
+ if (childKey) {
1022
+ const keyAsNumber = parseInt(childKey, 10);
1023
+ if (!isNaN(keyAsNumber)) {
1024
+ nthOfType2 = keyAsNumber + 1;
1025
+ }
1026
+ }
1027
+ const parentTarget = findTargetElement(parentAst, parentContent, {
1028
+ componentName: resolvedParentComponentName,
1029
+ lineNumber: 0,
1030
+ // Don't use line number - rely on nthOfType to find correct instance
1031
+ elementContext: {
1032
+ tagName: componentName,
1033
+ // Search for child component usage
1034
+ nthOfType: nthOfType2
1035
+ // Find specific instance if key is numeric
1036
+ }
1037
+ });
1038
+ if (parentTarget) {
1039
+ const parentLines = parentContent.split("\n");
1040
+ const parentComponentLines = parentLines.slice(
1041
+ parentTarget.componentStart - 1,
1042
+ parentTarget.componentEnd
1043
+ );
1044
+ parentInstance = {
1045
+ filePath: resolvedParentPath,
1046
+ content: parentComponentLines.join("\n"),
1047
+ lineStart: parentTarget.componentStart,
1048
+ lineEnd: parentTarget.componentEnd,
1049
+ usageLineStart: parentTarget.startLine,
1050
+ usageLineEnd: parentTarget.endLine,
1051
+ componentName: resolvedParentComponentName
1052
+ };
1053
+ }
1054
+ }
1055
+ }
1056
+ } catch (error) {
1057
+ }
1058
+ }
915
1059
  return NextResponse.json({
916
1060
  success: true,
917
1061
  content: preview,
@@ -919,8 +1063,10 @@ async function handleRead(req) {
919
1063
  lineEnd: target.componentEnd,
920
1064
  targetStartLine: target.startLine,
921
1065
  targetEndLine: target.endLine,
922
- componentName
1066
+ componentName,
923
1067
  // Return the actual component name parsed from code
1068
+ parentInstance
1069
+ // Optional: where this component is used
924
1070
  });
925
1071
  }
926
1072
  async function handleUndo(req) {
@@ -976,37 +1122,58 @@ async function handleResolve(req) {
976
1122
  { status: 400 }
977
1123
  );
978
1124
  }
979
- const compiledPos = parseDebugStack(debugStack);
980
- if (!compiledPos) {
981
- console.error("Could not parse debug stack:", debugStack);
982
- return NextResponse.json(
983
- { success: false, error: "Could not parse stack" },
984
- { status: 422 }
1125
+ const compiledFrames = parseDebugStackFrames(debugStack);
1126
+ if (compiledFrames.length === 0) {
1127
+ const compiledPos = parseDebugStack(debugStack);
1128
+ if (!compiledPos) {
1129
+ console.error("Could not parse debug stack:", debugStack);
1130
+ return NextResponse.json(
1131
+ { success: false, error: "Could not parse stack" },
1132
+ { status: 422 }
1133
+ );
1134
+ }
1135
+ const originalPos = await resolveOriginalPosition(
1136
+ compiledPos,
1137
+ process.cwd()
985
1138
  );
1139
+ if (!originalPos) {
1140
+ return NextResponse.json(
1141
+ { success: false, error: "Source map lookup failed" },
1142
+ { status: 404 }
1143
+ );
1144
+ }
1145
+ return NextResponse.json({
1146
+ success: true,
1147
+ filePath: originalPos.source,
1148
+ lineNumber: originalPos.line,
1149
+ columnNumber: originalPos.column ?? 0
1150
+ });
986
1151
  }
987
- console.log("Parsed compiled position:", compiledPos);
988
- const originalPos = await resolveOriginalPosition(
989
- compiledPos,
990
- process.cwd()
991
- );
992
- if (!originalPos) {
993
- console.error(
994
- "Source map lookup failed for:",
995
- compiledPos.filePath,
996
- "at line",
997
- compiledPos.line
998
- );
1152
+ const resolvedFrames = [];
1153
+ for (const frame of compiledFrames) {
1154
+ const originalPos = await resolveOriginalPosition(frame, process.cwd());
1155
+ if (originalPos) {
1156
+ resolvedFrames.push({
1157
+ filePath: originalPos.source,
1158
+ lineNumber: originalPos.line,
1159
+ columnNumber: originalPos.column ?? 0
1160
+ });
1161
+ }
1162
+ }
1163
+ if (resolvedFrames.length === 0) {
999
1164
  return NextResponse.json(
1000
- { success: false, error: "Source map lookup failed" },
1165
+ { success: false, error: "Source map lookup failed for all frames" },
1001
1166
  { status: 404 }
1002
1167
  );
1003
1168
  }
1004
- console.log("Resolved original position:", originalPos);
1169
+ console.log("Resolved frames:", resolvedFrames);
1005
1170
  return NextResponse.json({
1006
1171
  success: true,
1007
- filePath: originalPos.source,
1008
- lineNumber: originalPos.line,
1009
- columnNumber: originalPos.column ?? 0
1172
+ filePath: resolvedFrames[0].filePath,
1173
+ lineNumber: resolvedFrames[0].lineNumber,
1174
+ columnNumber: resolvedFrames[0].columnNumber,
1175
+ frames: resolvedFrames
1176
+ // [componentDefinition, parentInstance]
1010
1177
  });
1011
1178
  } catch (error) {
1012
1179
  console.error("Source resolve error:", error);
@@ -1091,6 +1258,209 @@ async function handleValidateSession(req) {
1091
1258
  );
1092
1259
  }
1093
1260
  }
1261
+ const suggestionCache = /* @__PURE__ */ new Map();
1262
+ const CACHE_TTL = 3e4;
1263
+ function getCacheKey(params) {
1264
+ return `${params.componentName}:${params.elementTag || "div"}:${params.lastSuggestion || ""}`;
1265
+ }
1266
+ function getCachedSuggestions(key) {
1267
+ const cached = suggestionCache.get(key);
1268
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
1269
+ return cached.suggestions;
1270
+ }
1271
+ suggestionCache.delete(key);
1272
+ return null;
1273
+ }
1274
+ function cacheSuggestions(key, suggestions) {
1275
+ suggestionCache.set(key, { suggestions, timestamp: Date.now() });
1276
+ if (suggestionCache.size > 100) {
1277
+ const oldestKeys = Array.from(suggestionCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp).slice(0, 20).map(([key2]) => key2);
1278
+ oldestKeys.forEach((key2) => suggestionCache.delete(key2));
1279
+ }
1280
+ }
1281
+ const DEFAULT_SUGGESTIONS = [
1282
+ "Add padding",
1283
+ "Change color",
1284
+ "Make larger",
1285
+ "Add shadow",
1286
+ "Round corners",
1287
+ "Center content",
1288
+ "Add hover effect",
1289
+ "Adjust spacing"
1290
+ ];
1291
+ async function handleSuggestions(req) {
1292
+ const devModeError = validateDevMode();
1293
+ if (devModeError) return devModeError;
1294
+ try {
1295
+ const { searchParams } = new URL(req.url);
1296
+ const componentName = searchParams.get("componentName") || "Component";
1297
+ const elementTag = searchParams.get("elementTag") || void 0;
1298
+ const className = searchParams.get("className") || void 0;
1299
+ const textContent = searchParams.get("textContent") || void 0;
1300
+ const lastSuggestion = searchParams.get("lastSuggestion") || void 0;
1301
+ const editHistoryStr = searchParams.get("editHistory") || void 0;
1302
+ const excludedSuggestionsStr = searchParams.get("excludedSuggestions") || void 0;
1303
+ let editHistory = [];
1304
+ if (editHistoryStr) {
1305
+ try {
1306
+ editHistory = JSON.parse(editHistoryStr);
1307
+ } catch (e) {
1308
+ }
1309
+ }
1310
+ let excludedSuggestions = [];
1311
+ if (excludedSuggestionsStr) {
1312
+ try {
1313
+ excludedSuggestions = JSON.parse(excludedSuggestionsStr);
1314
+ } catch (e) {
1315
+ }
1316
+ }
1317
+ const cacheKey = getCacheKey({ componentName, elementTag, lastSuggestion });
1318
+ const cached = excludedSuggestions.length === 0 ? getCachedSuggestions(cacheKey) : null;
1319
+ if (cached) {
1320
+ return NextResponse.json({
1321
+ success: true,
1322
+ suggestions: cached
1323
+ });
1324
+ }
1325
+ const suggestions = await generateSuggestions({
1326
+ componentName,
1327
+ elementTag,
1328
+ className,
1329
+ textContent,
1330
+ editHistory,
1331
+ lastSuggestion,
1332
+ excludedSuggestions
1333
+ });
1334
+ if (suggestions && suggestions.length > 0) {
1335
+ cacheSuggestions(cacheKey, suggestions);
1336
+ return NextResponse.json({
1337
+ success: true,
1338
+ suggestions
1339
+ });
1340
+ }
1341
+ return NextResponse.json({
1342
+ success: true,
1343
+ suggestions: DEFAULT_SUGGESTIONS
1344
+ });
1345
+ } catch (error) {
1346
+ return NextResponse.json(
1347
+ {
1348
+ success: false,
1349
+ error: String(error),
1350
+ suggestions: DEFAULT_SUGGESTIONS
1351
+ // Fallback
1352
+ },
1353
+ { status: 500 }
1354
+ );
1355
+ }
1356
+ }
1357
+ async function generateSuggestions(options) {
1358
+ const {
1359
+ componentName,
1360
+ elementTag,
1361
+ className,
1362
+ textContent,
1363
+ editHistory,
1364
+ lastSuggestion,
1365
+ excludedSuggestions = []
1366
+ } = options;
1367
+ const apiKey = process.env.ANTHROPIC_API_KEY;
1368
+ if (!apiKey) {
1369
+ return null;
1370
+ }
1371
+ const model = new ChatAnthropic({
1372
+ apiKey,
1373
+ modelName: "claude-haiku-4-5-20251001",
1374
+ maxTokens: 1024,
1375
+ temperature: 0.3
1376
+ // Slightly creative for variety
1377
+ });
1378
+ const systemPrompt = `You are a UI/UX expert suggesting quick edits for React components.
1379
+
1380
+ Generate 6-8 concise, actionable suggestions that a developer might want to make next.
1381
+
1382
+ RULES:
1383
+ - Each suggestion must be 2-6 words (e.g., "Add padding", "Make text larger")
1384
+ - Focus on common UI improvements: spacing, colors, sizing, layout, shadows, borders, typography
1385
+ - Consider the element type (${elementTag || "element"})
1386
+ - Output ONLY a JSON array of strings, no explanations, no markdown fences
1387
+ ${excludedSuggestions.length > 0 ? `- DO NOT suggest any of these (user wants different options): ${excludedSuggestions.join(
1388
+ ", "
1389
+ )}` : ""}
1390
+
1391
+ ${lastSuggestion ? `IMPORTANT - LAST EDIT CONTEXT:
1392
+ The user just applied: "${lastSuggestion}"
1393
+ Your suggestions MUST be direct follow-ups/refinements of this last change:
1394
+ - If it was "Add padding" → suggest "More padding", "Less padding", "Add vertical padding only"
1395
+ - If it was "Make it blue" → suggest "Darker blue", "Lighter blue", "Change to navy blue"
1396
+ - If it was "Increase font size" → suggest "Decrease font size", "Make even larger", "Adjust line height"
1397
+ - 4-6 suggestions should be variations/refinements of the last edit
1398
+ - 2-4 suggestions can be other related improvements
1399
+
1400
+ Generate follow-up suggestions that let the user iteratively refine what they just did.` : `Generate varied initial suggestions covering different aspects: layout, colors, spacing, shadows, typography, etc.`}
1401
+
1402
+ Example output format:
1403
+ ["Add hover effect", "Increase padding", "Make corners rounder", "Change to flex row", "Add drop shadow", "Adjust font size"]`;
1404
+ let userPrompt = `Element: <${elementTag || "div"}>`;
1405
+ if (className) {
1406
+ userPrompt += `
1407
+ Classes: ${className}`;
1408
+ }
1409
+ if (textContent) {
1410
+ userPrompt += `
1411
+ Text: "${textContent.slice(0, 50)}"`;
1412
+ }
1413
+ userPrompt += `
1414
+ Component: ${componentName}`;
1415
+ if (editHistory && editHistory.length > 0) {
1416
+ userPrompt += `
1417
+
1418
+ Recent edits:
1419
+ `;
1420
+ editHistory.slice(-3).forEach((item, idx) => {
1421
+ userPrompt += `${idx + 1}. ${item.suggestion} ${item.success ? "✓" : "✗"}
1422
+ `;
1423
+ });
1424
+ } else {
1425
+ userPrompt += `
1426
+
1427
+ No previous edits.`;
1428
+ }
1429
+ if (lastSuggestion) {
1430
+ userPrompt += `
1431
+
1432
+ **LAST EDIT APPLIED:** "${lastSuggestion}"`;
1433
+ userPrompt += `
1434
+
1435
+ Generate 6-8 follow-up suggestions (mostly variations of the last edit):`;
1436
+ } else {
1437
+ userPrompt += `
1438
+
1439
+ Generate 6-8 initial suggestions:`;
1440
+ }
1441
+ try {
1442
+ const response = await model.invoke([
1443
+ new SystemMessage(systemPrompt),
1444
+ new HumanMessage(userPrompt)
1445
+ ]);
1446
+ let content = typeof response.content === "string" ? response.content : String(response.content);
1447
+ content = content.trim();
1448
+ content = content.replace(/^```json?\s*/gm, "").replace(/\s*```$/gm, "");
1449
+ const suggestions = JSON.parse(content);
1450
+ if (Array.isArray(suggestions)) {
1451
+ const validSuggestions = suggestions.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => {
1452
+ const words = s.split(/\s+/).length;
1453
+ return words >= 1 && words <= 15;
1454
+ }).slice(0, 8);
1455
+ if (validSuggestions.length >= 4) {
1456
+ return validSuggestions;
1457
+ }
1458
+ }
1459
+ return null;
1460
+ } catch (e) {
1461
+ return null;
1462
+ }
1463
+ }
1094
1464
  async function handleAIEditorRequest(req, context) {
1095
1465
  const { path: path2 } = await context.params;
1096
1466
  const endpoint = path2[0];
@@ -1114,6 +1484,9 @@ async function handleAIEditorRequest(req, context) {
1114
1484
  case "validate-session":
1115
1485
  if (method === "POST") return handleValidateSession(req);
1116
1486
  break;
1487
+ case "suggestions":
1488
+ if (method === "GET") return handleSuggestions(req);
1489
+ break;
1117
1490
  }
1118
1491
  return NextResponse.json(
1119
1492
  { error: `Unknown endpoint: ${endpoint}` },
@@ -1127,19 +1500,23 @@ export {
1127
1500
  handleResolve as d,
1128
1501
  handleAbsolutePath as e,
1129
1502
  handleValidateSession as f,
1130
- fileExists$1 as g,
1503
+ handleSuggestions as g,
1131
1504
  handleAIEditorRequest as h,
1132
1505
  isPathSecure as i,
1133
- findTargetElement as j,
1134
- getJSXMemberName as k,
1135
- getAttributeValue as l,
1136
- parseDebugStack as m,
1506
+ fileExists$1 as j,
1507
+ extractComponentName as k,
1508
+ validateGeneratedCode as l,
1509
+ findTargetElement as m,
1137
1510
  normalizePath as n,
1138
- resolveOriginalPosition as o,
1511
+ getJSXMemberName as o,
1139
1512
  parseFile as p,
1140
- getOriginalPositionFromDebugStack as q,
1513
+ getAttributeValue as q,
1141
1514
  resolveFilePath as r,
1142
1515
  scoreElementMatch as s,
1143
- validateDevMode as v
1516
+ parseDebugStack as t,
1517
+ parseDebugStackFrames as u,
1518
+ validateDevMode as v,
1519
+ resolveOriginalPosition as w,
1520
+ getOriginalPositionFromDebugStack as x
1144
1521
  };
1145
- //# sourceMappingURL=index-BFa7H-uO.js.map
1522
+ //# sourceMappingURL=index-3OMXRwpD.js.map