@agent-native/core 0.106.0 → 0.106.1

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 (67) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/application-state/index.ts +2 -0
  5. package/corpus/core/src/application-state/script-helpers.ts +12 -0
  6. package/corpus/core/src/application-state/store.ts +38 -0
  7. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +10 -3
  8. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +22 -0
  9. package/corpus/templates/design/AGENTS.md +16 -0
  10. package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +54 -0
  11. package/corpus/templates/design/actions/propose-node-rewrite.ts +290 -0
  12. package/corpus/templates/design/actions/resolve-node-rewrite.ts +257 -0
  13. package/corpus/templates/design/actions/view-screen.ts +98 -2
  14. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +71 -1
  15. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +19 -0
  16. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -0
  17. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +194 -0
  18. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +21 -4
  19. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +18 -0
  20. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  21. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +638 -0
  22. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +535 -54
  23. package/corpus/templates/design/app/components/visual-editor/index.ts +5 -1
  24. package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +21 -3
  25. package/corpus/templates/design/app/i18n/ar-SA.ts +35 -0
  26. package/corpus/templates/design/app/i18n/de-DE.ts +35 -0
  27. package/corpus/templates/design/app/i18n/es-ES.ts +35 -0
  28. package/corpus/templates/design/app/i18n/fr-FR.ts +35 -0
  29. package/corpus/templates/design/app/i18n/hi-IN.ts +35 -0
  30. package/corpus/templates/design/app/i18n/ja-JP.ts +35 -0
  31. package/corpus/templates/design/app/i18n/ko-KR.ts +35 -0
  32. package/corpus/templates/design/app/i18n/pt-BR.ts +35 -0
  33. package/corpus/templates/design/app/i18n/zh-CN.ts +35 -0
  34. package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
  35. package/corpus/templates/design/app/i18n-data.ts +32 -0
  36. package/corpus/templates/design/app/lib/node-reprompt.ts +110 -0
  37. package/corpus/templates/design/changelog/2026-07-16-design-editing-and-review-are-now-more-predictable.md +6 -0
  38. package/corpus/templates/design/server/lib/reprompt-action-guard.ts +107 -0
  39. package/corpus/templates/design/server/plugins/agent-chat.ts +9 -1
  40. package/corpus/templates/design/shared/code-layer.ts +15 -0
  41. package/corpus/templates/design/shared/node-rewrite.ts +213 -0
  42. package/corpus/templates/design/shared/review-anchor.ts +26 -3
  43. package/dist/application-state/index.d.ts +2 -2
  44. package/dist/application-state/index.d.ts.map +1 -1
  45. package/dist/application-state/index.js +2 -2
  46. package/dist/application-state/index.js.map +1 -1
  47. package/dist/application-state/script-helpers.d.ts +1 -0
  48. package/dist/application-state/script-helpers.d.ts.map +1 -1
  49. package/dist/application-state/script-helpers.js +7 -1
  50. package/dist/application-state/script-helpers.js.map +1 -1
  51. package/dist/application-state/store.d.ts +1 -0
  52. package/dist/application-state/store.d.ts.map +1 -1
  53. package/dist/application-state/store.js +28 -0
  54. package/dist/application-state/store.js.map +1 -1
  55. package/dist/client/review/ReviewCommentComposer.d.ts +4 -1
  56. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  57. package/dist/client/review/ReviewCommentComposer.js +3 -3
  58. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  59. package/dist/collab/awareness.d.ts +2 -2
  60. package/dist/collab/awareness.d.ts.map +1 -1
  61. package/dist/collab/struct-routes.d.ts +1 -1
  62. package/dist/notifications/routes.d.ts +2 -2
  63. package/dist/observability/routes.d.ts +5 -5
  64. package/dist/resources/handlers.d.ts +1 -1
  65. package/dist/secrets/routes.d.ts +9 -9
  66. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  67. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  buildReviewThreads,
3
+ callAction,
3
4
  ReviewCommentComposer,
5
+ setClientAppState,
4
6
  useCreateReviewComment,
5
7
  useReplyReviewComment,
6
8
  useResolveReviewThread,
@@ -9,11 +11,17 @@ import {
9
11
  type ReviewThread,
10
12
  } from "@agent-native/core/client";
11
13
  import type { ReviewComment } from "@agent-native/core/review";
14
+ import {
15
+ designRepromptPendingStateKey,
16
+ type NodeRewriteTarget,
17
+ } from "@shared/node-rewrite";
12
18
  import {
13
19
  IconArrowUp,
20
+ IconChevronDown,
14
21
  IconCircleCheck,
15
22
  IconMessageCircle,
16
23
  IconRobot,
24
+ IconSend,
17
25
  IconX,
18
26
  } from "@tabler/icons-react";
19
27
  import {
@@ -28,8 +36,24 @@ import { toast } from "sonner";
28
36
 
29
37
  import { Avatar, AvatarFallback } from "@/components/ui/avatar";
30
38
  import { Button } from "@/components/ui/button";
39
+ import {
40
+ DropdownMenu,
41
+ DropdownMenuContent,
42
+ DropdownMenuRadioGroup,
43
+ DropdownMenuRadioItem,
44
+ DropdownMenuTrigger,
45
+ } from "@/components/ui/dropdown-menu";
31
46
  import { Input } from "@/components/ui/input";
32
47
  import { Spinner } from "@/components/ui/spinner";
48
+ import { sendToDesignAgentChatAndConfirm } from "@/lib/agent-chat";
49
+ import {
50
+ formatNodeSelectionQuestion,
51
+ formatNodeRepromptSubmission,
52
+ inferNodeRepromptSendMode,
53
+ NODE_REPROMPT_PRESENTED_EVENT,
54
+ NODE_REPROMPT_RESOLVED_EVENT,
55
+ type NodeRepromptSendMode,
56
+ } from "@/lib/node-reprompt";
33
57
  import { cn } from "@/lib/utils";
34
58
 
35
59
  import {
@@ -38,11 +62,19 @@ import {
38
62
  type ReviewAnchorPoint,
39
63
  } from "../../../shared/review-anchor";
40
64
  import {
65
+ getReviewPinPosition,
41
66
  getReviewPopoverPlacement,
42
67
  placeReviewDraftPin,
43
68
  type ReviewDraftPin,
44
69
  } from "./review-canvas-state";
45
70
 
71
+ export interface RepromptDraftRequest {
72
+ nonce: number;
73
+ fileId: string;
74
+ target: NodeRewriteTarget;
75
+ point?: ReviewAnchorPoint;
76
+ }
77
+
46
78
  export interface ReviewFocusRequest {
47
79
  nonce: number;
48
80
  anchor: unknown;
@@ -63,11 +95,10 @@ interface ReviewCanvasPinsProps {
63
95
  onDispatchCommentToAgent?: (comment: ReviewComment) => void;
64
96
  onSendThreadToAgent?: (thread: ReviewThread) => void;
65
97
  sendingThreadId?: string | null;
66
- }
67
-
68
- interface PinPosition {
69
- point: ReviewAnchorPoint;
70
- source: "node" | "point";
98
+ sourceType?: "inline" | "localhost" | "fusion";
99
+ sourceVersionHash?: string;
100
+ repromptDraftRequest?: RepromptDraftRequest | null;
101
+ onRepromptDraftConsumed?: (nonce: number) => void;
71
102
  }
72
103
 
73
104
  interface ReviewFrameNodeGeometry {
@@ -114,12 +145,11 @@ function findNodeElement(canvas: HTMLElement, nodeId: string): Element | null {
114
145
  }
115
146
  }
116
147
 
117
- function nodePoint(
148
+ function elementPoint(
118
149
  canvas: HTMLElement,
119
- nodeId: string,
150
+ element: Element | null,
120
151
  frameGeometry?: ReviewFrameNodeGeometry,
121
152
  ): ReviewAnchorPoint | null {
122
- const element = findNodeElement(canvas, nodeId);
123
153
  const iframe = canvas.querySelector<HTMLIFrameElement>(
124
154
  "iframe[data-design-preview-iframe]",
125
155
  );
@@ -153,11 +183,69 @@ function nodePoint(
153
183
  };
154
184
  }
155
185
 
186
+ function nodePoint(
187
+ canvas: HTMLElement,
188
+ nodeId: string,
189
+ frameGeometry?: ReviewFrameNodeGeometry,
190
+ ): ReviewAnchorPoint | null {
191
+ return elementPoint(canvas, findNodeElement(canvas, nodeId), frameGeometry);
192
+ }
193
+
194
+ function selectorPoint(
195
+ canvas: HTMLElement,
196
+ selector: string,
197
+ ): ReviewAnchorPoint | null {
198
+ const iframe = canvas.querySelector<HTMLIFrameElement>(
199
+ "iframe[data-design-preview-iframe]",
200
+ );
201
+ try {
202
+ return elementPoint(
203
+ canvas,
204
+ iframe?.contentDocument?.querySelector(selector) ?? null,
205
+ );
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+
211
+ function structuralSelector(element: Element, document: Document): string {
212
+ if (element === document.body || element === document.documentElement) {
213
+ return "";
214
+ }
215
+ const parts: string[] = [];
216
+ let current: Element | null = element;
217
+ while (
218
+ current &&
219
+ current !== document.body &&
220
+ current !== document.documentElement
221
+ ) {
222
+ const tag = current.tagName.toLowerCase();
223
+ const siblings = current.parentElement
224
+ ? Array.from(current.parentElement.children).filter(
225
+ (candidate) => candidate.tagName === current?.tagName,
226
+ )
227
+ : [];
228
+ const index = siblings.indexOf(current);
229
+ parts.unshift(
230
+ siblings.length > 1 && index >= 0
231
+ ? `${tag}:nth-of-type(${index + 1})`
232
+ : tag,
233
+ );
234
+ current = current.parentElement;
235
+ }
236
+ return parts.length > 0 ? `body > ${parts.join(" > ")}` : "";
237
+ }
238
+
156
239
  function elementAnchorAtPoint(
157
240
  canvas: HTMLElement,
158
241
  clientX: number,
159
242
  clientY: number,
160
- ): { nodeId?: string; layerName?: string; tagName?: string } {
243
+ ): {
244
+ nodeId?: string;
245
+ targetSelector?: string;
246
+ layerName?: string;
247
+ tagName?: string;
248
+ } {
161
249
  const iframe = canvas.querySelector<HTMLIFrameElement>(
162
250
  "iframe[data-design-preview-iframe]",
163
251
  );
@@ -171,10 +259,22 @@ function elementAnchorAtPoint(
171
259
  (clientX - iframeRect.left) * scaleX,
172
260
  (clientY - iframeRect.top) * scaleY,
173
261
  );
174
- const anchor = element?.closest(
262
+ const identifiedAncestor = element?.closest(
175
263
  "[data-agent-native-node-id],[data-code-layer-id],[data-layer-id],[data-builder-id],[id]",
176
264
  );
177
- if (!anchor) return {};
265
+ const anchor =
266
+ identifiedAncestor &&
267
+ identifiedAncestor !== document.body &&
268
+ identifiedAncestor !== document.documentElement
269
+ ? identifiedAncestor
270
+ : element;
271
+ if (
272
+ !anchor ||
273
+ anchor === document.body ||
274
+ anchor === document.documentElement
275
+ ) {
276
+ return {};
277
+ }
178
278
  const nodeId =
179
279
  anchor.getAttribute("data-agent-native-node-id") ??
180
280
  anchor.getAttribute("data-code-layer-id") ??
@@ -184,8 +284,12 @@ function elementAnchorAtPoint(
184
284
  undefined;
185
285
  const layerName =
186
286
  anchor.getAttribute("data-agent-native-layer-name") ?? undefined;
287
+ const targetSelector = nodeId
288
+ ? undefined
289
+ : structuralSelector(anchor, document) || undefined;
187
290
  return {
188
291
  ...(nodeId ? { nodeId } : {}),
292
+ ...(targetSelector ? { targetSelector } : {}),
189
293
  ...(layerName ? { layerName } : {}),
190
294
  tagName: anchor.tagName.toLowerCase(),
191
295
  };
@@ -208,26 +312,19 @@ function anchorAtPoint(
208
312
  return {
209
313
  anchor: {
210
314
  ...(element.nodeId ? { nodeId: element.nodeId } : {}),
315
+ ...(element.targetSelector ? { selector: element.targetSelector } : {}),
211
316
  point: { xPct, yPct },
212
317
  },
213
318
  metadata: {
214
319
  ...(element.layerName ? { layerName: element.layerName } : {}),
215
320
  ...(element.tagName ? { tagName: element.tagName } : {}),
321
+ ...(element.targetSelector
322
+ ? { targetSelector: element.targetSelector }
323
+ : {}),
216
324
  },
217
325
  };
218
326
  }
219
327
 
220
- function resolvePinPosition(
221
- canvas: HTMLElement,
222
- anchor: unknown,
223
- frameGeometry: Record<string, ReviewFrameNodeGeometry>,
224
- ): PinPosition | null {
225
- const resolved = resolveReviewAnchor(anchor, (nodeId) =>
226
- nodePoint(canvas, nodeId, frameGeometry[nodeId]),
227
- );
228
- return resolved ? { point: resolved.point, source: resolved.source } : null;
229
- }
230
-
231
328
  export function ReviewCanvasPins({
232
329
  active,
233
330
  hidden = false,
@@ -242,6 +339,10 @@ export function ReviewCanvasPins({
242
339
  onDispatchCommentToAgent,
243
340
  onSendThreadToAgent,
244
341
  sendingThreadId,
342
+ sourceType = "inline",
343
+ sourceVersionHash,
344
+ repromptDraftRequest,
345
+ onRepromptDraftConsumed,
245
346
  }: ReviewCanvasPinsProps) {
246
347
  const t = useT();
247
348
  const comments = useReviewComments(
@@ -265,11 +366,17 @@ export function ReviewCanvasPins({
265
366
  const [draftComposerOpen, setDraftComposerOpen] = useState(false);
266
367
  const [activeThreadId, setActiveThreadId] = useState<string | null>(null);
267
368
  const [replyDraft, setReplyDraft] = useState("");
369
+ const [draftMode, setDraftMode] = useState<"comment" | "reprompt">("comment");
370
+ const [pendingRepromptId, setPendingRepromptId] = useState<string | null>(
371
+ null,
372
+ );
373
+ const [agentSubmitting, setAgentSubmitting] = useState(false);
268
374
  const [frameNodeGeometry, setFrameNodeGeometry] = useState<
269
375
  Record<string, ReviewFrameNodeGeometry>
270
376
  >({});
271
377
  const lastFocusNonceRef = useRef<number | null>(null);
272
378
  const pendingFocusNonceRef = useRef<number | null>(null);
379
+ const lastRepromptDraftNonceRef = useRef<number | null>(null);
273
380
  const frameCallbacksRef = useRef<
274
381
  Map<string, (payload: Record<string, unknown>) => void>
275
382
  >(new Map());
@@ -277,8 +384,32 @@ export function ReviewCanvasPins({
277
384
  const cancelDraft = useCallback(() => {
278
385
  setDraftPin(null);
279
386
  setDraftComposerOpen(false);
387
+ setDraftMode("comment");
388
+ setPendingRepromptId(null);
280
389
  }, []);
281
390
 
391
+ useEffect(() => {
392
+ const onRepromptSettled = (event: Event) => {
393
+ const detail = (event as CustomEvent<{ repromptId?: string }>).detail;
394
+ if (!pendingRepromptId || detail?.repromptId !== pendingRepromptId)
395
+ return;
396
+ cancelDraft();
397
+ onClose();
398
+ };
399
+ window.addEventListener(NODE_REPROMPT_PRESENTED_EVENT, onRepromptSettled);
400
+ window.addEventListener(NODE_REPROMPT_RESOLVED_EVENT, onRepromptSettled);
401
+ return () => {
402
+ window.removeEventListener(
403
+ NODE_REPROMPT_PRESENTED_EVENT,
404
+ onRepromptSettled,
405
+ );
406
+ window.removeEventListener(
407
+ NODE_REPROMPT_RESOLVED_EVENT,
408
+ onRepromptSettled,
409
+ );
410
+ };
411
+ }, [cancelDraft, onClose, pendingRepromptId]);
412
+
282
413
  const threads = useMemo(
283
414
  () => buildReviewThreads(comments.data?.comments ?? []),
284
415
  [comments.data?.comments],
@@ -414,12 +545,29 @@ export function ReviewCanvasPins({
414
545
  const focusAnchor = useCallback(
415
546
  (anchor: unknown, nonce: number): boolean => {
416
547
  if (!canvas) return false;
417
- const resolved = resolveReviewAnchor(anchor, (nodeId) =>
418
- nodePoint(canvas, nodeId, frameNodeGeometry[nodeId]),
548
+ const resolved = resolveReviewAnchor(
549
+ anchor,
550
+ (nodeId) => nodePoint(canvas, nodeId, frameNodeGeometry[nodeId]),
551
+ (selector) => selectorPoint(canvas, selector),
419
552
  );
420
553
  if (!resolved) return true;
421
- if (resolved.anchor.nodeId) {
422
- const element = findNodeElement(canvas, resolved.anchor.nodeId);
554
+ if (resolved.anchor.nodeId || resolved.anchor.selector) {
555
+ const element = resolved.anchor.nodeId
556
+ ? findNodeElement(canvas, resolved.anchor.nodeId)
557
+ : (() => {
558
+ const iframe = canvas.querySelector<HTMLIFrameElement>(
559
+ "iframe[data-design-preview-iframe]",
560
+ );
561
+ try {
562
+ return (
563
+ iframe?.contentDocument?.querySelector(
564
+ resolved.anchor.selector!,
565
+ ) ?? null
566
+ );
567
+ } catch {
568
+ return null;
569
+ }
570
+ })();
423
571
  if (element instanceof HTMLElement || element instanceof SVGElement) {
424
572
  element.scrollIntoView({ block: "center", inline: "center" });
425
573
  const previousBoxShadow = element.style.boxShadow;
@@ -430,6 +578,7 @@ export function ReviewCanvasPins({
430
578
  }, 700);
431
579
  return true;
432
580
  }
581
+ if (!resolved.anchor.nodeId) return true;
433
582
  if (pendingFocusNonceRef.current === nonce) return false;
434
583
  const iframe = canvas.querySelector<HTMLIFrameElement>(
435
584
  "iframe[data-design-preview-iframe]",
@@ -475,6 +624,50 @@ export function ReviewCanvasPins({
475
624
  if (!active) cancelDraft();
476
625
  }, [active, cancelDraft]);
477
626
 
627
+ useEffect(() => {
628
+ if (
629
+ !canvas ||
630
+ !repromptDraftRequest ||
631
+ repromptDraftRequest.fileId !== targetId ||
632
+ repromptDraftRequest.nonce === lastRepromptDraftNonceRef.current
633
+ ) {
634
+ return;
635
+ }
636
+ lastRepromptDraftNonceRef.current = repromptDraftRequest.nonce;
637
+ const nodeId = repromptDraftRequest.target.nodeId;
638
+ const point = repromptDraftRequest.point ??
639
+ (nodeId
640
+ ? nodePoint(canvas, nodeId, frameNodeGeometry[nodeId])
641
+ : null) ?? { xPct: 50, yPct: 50 };
642
+ setActiveThreadId(null);
643
+ setReplyDraft("");
644
+ setPendingRepromptId(null);
645
+ setDraftMode("reprompt");
646
+ setDraftPin({
647
+ id: crypto.randomUUID(),
648
+ anchor: {
649
+ ...(nodeId ? { nodeId } : {}),
650
+ point,
651
+ },
652
+ draft: "",
653
+ resolutionTarget: "agent",
654
+ metadata: {
655
+ mode: "reprompt",
656
+ ...(repromptDraftRequest.target.selector
657
+ ? { targetSelector: repromptDraftRequest.target.selector }
658
+ : {}),
659
+ },
660
+ });
661
+ setDraftComposerOpen(true);
662
+ onRepromptDraftConsumed?.(repromptDraftRequest.nonce);
663
+ }, [
664
+ canvas,
665
+ frameNodeGeometry,
666
+ onRepromptDraftConsumed,
667
+ repromptDraftRequest,
668
+ targetId,
669
+ ]);
670
+
478
671
  useEffect(() => {
479
672
  if (!active && !activeThreadId && !draftComposerOpen) return;
480
673
  const onKeyDown = (event: KeyboardEvent) => {
@@ -525,6 +718,8 @@ export function ReviewCanvasPins({
525
718
  if (!next) return;
526
719
  setActiveThreadId(null);
527
720
  setReplyDraft("");
721
+ setDraftMode("comment");
722
+ setPendingRepromptId(null);
528
723
  setDraftPin((current) =>
529
724
  placeReviewDraftPin(current, {
530
725
  id: crypto.randomUUID(),
@@ -534,7 +729,6 @@ export function ReviewCanvasPins({
534
729
  );
535
730
  setDraftComposerOpen(true);
536
731
 
537
- if (next.anchor.nodeId) return;
538
732
  const iframe = canvas.querySelector<HTMLIFrameElement>(
539
733
  "iframe[data-design-preview-iframe]",
540
734
  );
@@ -546,11 +740,15 @@ export function ReviewCanvasPins({
546
740
  frameCallbacksRef.current.set(correlationId, (payload) => {
547
741
  const nodeId =
548
742
  typeof payload.nodeId === "string" ? payload.nodeId : undefined;
743
+ const targetSelector =
744
+ typeof payload.targetSelector === "string"
745
+ ? payload.targetSelector
746
+ : undefined;
549
747
  const layerName =
550
748
  typeof payload.layerName === "string" ? payload.layerName : undefined;
551
749
  const tagName =
552
750
  typeof payload.tagName === "string" ? payload.tagName : undefined;
553
- if (!nodeId && !layerName && !tagName) return;
751
+ if (!nodeId && !targetSelector && !layerName && !tagName) return;
554
752
  setDraftPin((current) => {
555
753
  if (
556
754
  !current ||
@@ -563,12 +761,14 @@ export function ReviewCanvasPins({
563
761
  ...current,
564
762
  anchor: {
565
763
  ...(nodeId ? { nodeId } : {}),
764
+ ...(targetSelector ? { selector: targetSelector } : {}),
566
765
  point: current.anchor.point,
567
766
  },
568
767
  metadata: {
569
768
  ...current.metadata,
570
769
  ...(layerName ? { layerName } : {}),
571
770
  ...(tagName ? { tagName } : {}),
771
+ ...(targetSelector ? { targetSelector } : {}),
572
772
  },
573
773
  };
574
774
  });
@@ -631,6 +831,172 @@ export function ReviewCanvasPins({
631
831
  ],
632
832
  );
633
833
 
834
+ const submitReprompt = useCallback(
835
+ async (pin: ReviewDraftPin) => {
836
+ const instruction = pin.draft.trim();
837
+ const resolved = resolveReviewAnchor(pin.anchor, () => null);
838
+ const nodeId = resolved?.anchor.nodeId;
839
+ const targetSelector =
840
+ typeof pin.metadata.targetSelector === "string"
841
+ ? pin.metadata.targetSelector
842
+ : undefined;
843
+ if (
844
+ !instruction ||
845
+ (!nodeId && !targetSelector) ||
846
+ !sourceVersionHash ||
847
+ sourceType !== "inline" ||
848
+ agentSubmitting
849
+ ) {
850
+ return;
851
+ }
852
+ const target: NodeRewriteTarget = {
853
+ ...(nodeId ? { nodeId } : {}),
854
+ ...(targetSelector ? { selector: targetSelector } : {}),
855
+ };
856
+ const repromptId = crypto.randomUUID();
857
+ const pending = {
858
+ repromptId,
859
+ designId: resourceId,
860
+ fileId: targetId,
861
+ target,
862
+ baseVersionHash: sourceVersionHash,
863
+ instruction,
864
+ createdAt: new Date().toISOString(),
865
+ };
866
+ let element = nodeId ? findNodeElement(canvas!, nodeId) : null;
867
+ if (!element && targetSelector) {
868
+ const iframe = canvas?.querySelector<HTMLIFrameElement>(
869
+ "iframe[data-design-preview-iframe]",
870
+ );
871
+ try {
872
+ element =
873
+ iframe?.contentDocument?.querySelector(targetSelector) ?? null;
874
+ } catch {
875
+ element = null;
876
+ }
877
+ }
878
+ const stateKey = designRepromptPendingStateKey(resourceId, targetId);
879
+ setAgentSubmitting(true);
880
+ try {
881
+ await setClientAppState(stateKey, pending);
882
+ const submission = formatNodeRepromptSubmission({
883
+ ...pending,
884
+ subtreeHtml:
885
+ element instanceof HTMLElement || element instanceof SVGElement
886
+ ? element.outerHTML
887
+ : undefined,
888
+ });
889
+ const delivery = await sendToDesignAgentChatAndConfirm(
890
+ {
891
+ ...submission,
892
+ submit: true,
893
+ openSidebar: true,
894
+ },
895
+ { timeoutMs: 10_000 },
896
+ );
897
+ if (!delivery.delivered) {
898
+ throw new Error(delivery.reason ?? "Reprompt was not delivered.");
899
+ }
900
+ setDraftMode("reprompt");
901
+ setPendingRepromptId(repromptId);
902
+ setDraftComposerOpen(false);
903
+ toast.success(t("designEditor.nodeRewrite.sent"));
904
+ } catch (error) {
905
+ await callAction("cancel-node-rewrite-request", {
906
+ designId: resourceId,
907
+ fileId: targetId,
908
+ repromptId,
909
+ }).catch(() => {});
910
+ toast.error(
911
+ error instanceof Error && error.message
912
+ ? error.message
913
+ : t("designEditor.nodeRewrite.sendFailed"),
914
+ );
915
+ } finally {
916
+ setAgentSubmitting(false);
917
+ }
918
+ },
919
+ [
920
+ canvas,
921
+ agentSubmitting,
922
+ resourceId,
923
+ sourceType,
924
+ sourceVersionHash,
925
+ t,
926
+ targetId,
927
+ ],
928
+ );
929
+
930
+ const submitSelectionQuestion = useCallback(
931
+ async (pin: ReviewDraftPin) => {
932
+ const instruction = pin.draft.trim();
933
+ const resolved = resolveReviewAnchor(pin.anchor, () => null);
934
+ const nodeId = resolved?.anchor.nodeId;
935
+ const targetSelector =
936
+ typeof pin.metadata.targetSelector === "string"
937
+ ? pin.metadata.targetSelector
938
+ : undefined;
939
+ if (
940
+ !instruction ||
941
+ (!nodeId && !targetSelector) ||
942
+ sourceType !== "inline" ||
943
+ agentSubmitting
944
+ ) {
945
+ return;
946
+ }
947
+ const target: NodeRewriteTarget = {
948
+ ...(nodeId ? { nodeId } : {}),
949
+ ...(targetSelector ? { selector: targetSelector } : {}),
950
+ };
951
+ let element = nodeId ? findNodeElement(canvas!, nodeId) : null;
952
+ if (!element && targetSelector) {
953
+ const iframe = canvas?.querySelector<HTMLIFrameElement>(
954
+ "iframe[data-design-preview-iframe]",
955
+ );
956
+ try {
957
+ element =
958
+ iframe?.contentDocument?.querySelector(targetSelector) ?? null;
959
+ } catch {
960
+ element = null;
961
+ }
962
+ }
963
+ setAgentSubmitting(true);
964
+ try {
965
+ const submission = formatNodeSelectionQuestion({
966
+ designId: resourceId,
967
+ fileId: targetId,
968
+ target,
969
+ instruction,
970
+ subtreeHtml:
971
+ element instanceof HTMLElement || element instanceof SVGElement
972
+ ? element.outerHTML
973
+ : undefined,
974
+ });
975
+ const delivery = await sendToDesignAgentChatAndConfirm(
976
+ {
977
+ ...submission,
978
+ submit: true,
979
+ openSidebar: true,
980
+ },
981
+ { timeoutMs: 10_000 },
982
+ );
983
+ if (!delivery.delivered) {
984
+ throw new Error(delivery.reason ?? "Question was not delivered.");
985
+ }
986
+ cancelDraft();
987
+ } catch (error) {
988
+ toast.error(
989
+ error instanceof Error && error.message
990
+ ? error.message
991
+ : t("designEditor.nodeRewrite.sendFailed"),
992
+ );
993
+ } finally {
994
+ setAgentSubmitting(false);
995
+ }
996
+ },
997
+ [agentSubmitting, cancelDraft, canvas, resourceId, sourceType, t, targetId],
998
+ );
999
+
634
1000
  if (hidden || !canvas) return null;
635
1001
  const rect = canvas.getBoundingClientRect();
636
1002
  void layoutTick;
@@ -639,12 +1005,18 @@ export function ReviewCanvasPins({
639
1005
  (thread) => thread.root.status === "open" && thread.root.anchor,
640
1006
  );
641
1007
  const draftPinPosition = draftPin
642
- ? resolvePinPosition(canvas, draftPin.anchor, frameNodeGeometry)
1008
+ ? getReviewPinPosition(draftPin.anchor)
643
1009
  : null;
1010
+ const pinPlacementEnabled = active && canPost && !pendingRepromptId;
1011
+ const placementHintVisible =
1012
+ pinPlacementEnabled &&
1013
+ !draftComposerOpen &&
1014
+ !activeThreadId &&
1015
+ !repromptDraftRequest;
644
1016
 
645
1017
  return (
646
1018
  <>
647
- {active && canPost ? (
1019
+ {pinPlacementEnabled ? (
648
1020
  <div
649
1021
  data-review-click-plane
650
1022
  className="fixed z-40 cursor-crosshair"
@@ -661,7 +1033,7 @@ export function ReviewCanvasPins({
661
1033
  }}
662
1034
  />
663
1035
  ) : null}
664
- {active && canPost ? (
1036
+ {placementHintVisible ? (
665
1037
  <div className="pointer-events-none fixed left-1/2 top-16 z-[45] flex -translate-x-1/2 items-center gap-2 rounded-full border border-border bg-popover px-3 py-1.5 text-xs shadow-lg">
666
1038
  <IconMessageCircle className="size-3.5 text-primary" />
667
1039
  {t("review.clickToPin")}
@@ -671,11 +1043,7 @@ export function ReviewCanvasPins({
671
1043
  </div>
672
1044
  ) : null}
673
1045
  {openThreads.map((thread, index) => {
674
- const position = resolvePinPosition(
675
- canvas,
676
- thread.root.anchor,
677
- frameNodeGeometry,
678
- );
1046
+ const position = getReviewPinPosition(thread.root.anchor);
679
1047
  if (!position) return null;
680
1048
  return (
681
1049
  <ReviewPin
@@ -754,7 +1122,9 @@ export function ReviewCanvasPins({
754
1122
  canvasRect={rect}
755
1123
  point={draftPinPosition.point}
756
1124
  draft
1125
+ pending={Boolean(pendingRepromptId)}
757
1126
  onClick={() => {
1127
+ if (pendingRepromptId) return;
758
1128
  setActiveThreadId(null);
759
1129
  setReplyDraft("");
760
1130
  setDraftComposerOpen(true);
@@ -776,10 +1146,25 @@ export function ReviewCanvasPins({
776
1146
  );
777
1147
  postDraft({ ...draftPin, resolutionTarget });
778
1148
  }}
1149
+ onSmartSubmit={(mode) => {
1150
+ if (mode === "preview") void submitReprompt(draftPin);
1151
+ else void submitSelectionQuestion(draftPin);
1152
+ }}
779
1153
  resolutionTarget={draftPin.resolutionTarget}
780
- showAgentAction={Boolean(onDispatchCommentToAgent)}
1154
+ showAgentAction={
1155
+ Boolean(onDispatchCommentToAgent) || draftMode === "reprompt"
1156
+ }
1157
+ smartAgentAvailable={
1158
+ sourceType === "inline" &&
1159
+ Boolean(
1160
+ resolveReviewAnchor(draftPin.anchor, () => null)?.anchor
1161
+ .nodeId || draftPin.metadata.targetSelector,
1162
+ )
1163
+ }
1164
+ initialAgentMode={draftMode === "reprompt" ? "preview" : "auto"}
781
1165
  placement={getReviewPopoverPlacement(draftPinPosition.point)}
782
- submitting={createComment.isPending}
1166
+ commentSubmitting={createComment.isPending}
1167
+ agentSubmitting={agentSubmitting}
783
1168
  />
784
1169
  ) : null}
785
1170
  </ReviewPin>
@@ -794,6 +1179,7 @@ function ReviewPin({
794
1179
  canvasRect,
795
1180
  active,
796
1181
  draft = false,
1182
+ pending = false,
797
1183
  onClick,
798
1184
  children,
799
1185
  }: {
@@ -802,6 +1188,7 @@ function ReviewPin({
802
1188
  canvasRect: DOMRect;
803
1189
  active: boolean;
804
1190
  draft?: boolean;
1191
+ pending?: boolean;
805
1192
  onClick: () => void;
806
1193
  children?: ReactNode;
807
1194
  }) {
@@ -831,7 +1218,7 @@ function ReviewPin({
831
1218
  }}
832
1219
  aria-label={t("review.commentNumber", { count: index + 1 })}
833
1220
  >
834
- {index + 1}
1221
+ {pending ? <Spinner className="size-3" /> : index + 1}
835
1222
  </button>
836
1223
  {children}
837
1224
  </div>
@@ -843,21 +1230,94 @@ function DraftComposer({
843
1230
  onChange,
844
1231
  onCancel,
845
1232
  onSubmit,
1233
+ onSmartSubmit,
846
1234
  resolutionTarget,
847
1235
  showAgentAction,
1236
+ smartAgentAvailable,
1237
+ initialAgentMode,
848
1238
  placement,
849
- submitting,
1239
+ commentSubmitting,
1240
+ agentSubmitting,
850
1241
  }: {
851
1242
  value: string;
852
1243
  onChange: (value: string) => void;
853
1244
  onCancel: () => void;
854
1245
  onSubmit: (target: "agent" | "human") => void;
1246
+ onSmartSubmit: (mode: NodeRepromptSendMode) => void;
855
1247
  resolutionTarget: "agent" | "human";
856
1248
  showAgentAction: boolean;
1249
+ smartAgentAvailable: boolean;
1250
+ initialAgentMode: "auto" | NodeRepromptSendMode;
857
1251
  placement: ReviewPopoverPlacement;
858
- submitting: boolean;
1252
+ commentSubmitting: boolean;
1253
+ agentSubmitting: boolean;
859
1254
  }) {
860
1255
  const t = useT();
1256
+ const [modeOverride, setModeOverride] = useState<
1257
+ "auto" | NodeRepromptSendMode
1258
+ >(initialAgentMode);
1259
+ const inferredMode = inferNodeRepromptSendMode(value, {
1260
+ hasEditableTarget: smartAgentAvailable,
1261
+ });
1262
+ const sendMode = modeOverride === "auto" ? inferredMode : modeOverride;
1263
+ const submitting = commentSubmitting || agentSubmitting;
1264
+ const agentLabel =
1265
+ modeOverride === "auto"
1266
+ ? t("review.sendToAgent")
1267
+ : sendMode === "preview"
1268
+ ? t("designEditor.nodeRewrite.modeRegenerate")
1269
+ : t("designEditor.nodeRewrite.modeAsk");
1270
+ const agentAction = smartAgentAvailable ? (
1271
+ <div className="flex w-full min-w-0 @2xs/review:flex-1">
1272
+ <Button
1273
+ type="button"
1274
+ size="sm"
1275
+ variant="outline"
1276
+ className="h-8 min-w-0 flex-1 gap-1.5 rounded-e-none"
1277
+ disabled={submitting || !value.trim()}
1278
+ onClick={() => onSmartSubmit(sendMode)}
1279
+ >
1280
+ {agentSubmitting ? (
1281
+ <Spinner className="size-3.5" />
1282
+ ) : (
1283
+ <IconSend className="size-3.5" />
1284
+ )}
1285
+ <span className="truncate">{agentLabel}</span>
1286
+ </Button>
1287
+ <DropdownMenu>
1288
+ <DropdownMenuTrigger asChild>
1289
+ <Button
1290
+ type="button"
1291
+ size="sm"
1292
+ variant="outline"
1293
+ className="h-8 shrink-0 rounded-s-none border-s-0 px-2"
1294
+ disabled={submitting}
1295
+ aria-label={t("designEditor.nodeRewrite.agentModeOptions")}
1296
+ >
1297
+ <IconChevronDown className="size-3" />
1298
+ </Button>
1299
+ </DropdownMenuTrigger>
1300
+ <DropdownMenuContent align="end" className="w-56">
1301
+ <DropdownMenuRadioGroup
1302
+ value={modeOverride}
1303
+ onValueChange={(nextMode) =>
1304
+ setModeOverride(nextMode as "auto" | NodeRepromptSendMode)
1305
+ }
1306
+ >
1307
+ <DropdownMenuRadioItem value="auto">
1308
+ {t("designEditor.nodeRewrite.modeAuto")}
1309
+ </DropdownMenuRadioItem>
1310
+ <DropdownMenuRadioItem value="ask">
1311
+ {t("designEditor.nodeRewrite.modeAsk")}
1312
+ </DropdownMenuRadioItem>
1313
+ <DropdownMenuRadioItem value="preview">
1314
+ {t("designEditor.nodeRewrite.modeRegenerate")}
1315
+ </DropdownMenuRadioItem>
1316
+ </DropdownMenuRadioGroup>
1317
+ </DropdownMenuContent>
1318
+ </DropdownMenu>
1319
+ </div>
1320
+ ) : undefined;
861
1321
  return (
862
1322
  <div
863
1323
  data-review-popover
@@ -869,17 +1329,23 @@ function DraftComposer({
869
1329
  onClick={(event) => event.stopPropagation()}
870
1330
  >
871
1331
  <div className="flex items-center justify-between px-3 pb-2 pt-3">
872
- <span className="text-sm font-medium">{t("review.newComment")}</span>
873
- <Button
874
- variant="ghost"
875
- size="icon"
876
- className="size-7 text-muted-foreground"
877
- disabled={submitting}
878
- onClick={onCancel}
879
- aria-label={t("designEditor.close")}
880
- >
881
- <IconX className="size-3.5" />
882
- </Button>
1332
+ <span className="text-sm font-medium">
1333
+ {initialAgentMode === "preview"
1334
+ ? t("designEditor.nodeRewrite.composerTitle")
1335
+ : t("review.newComment")}
1336
+ </span>
1337
+ <div className="flex items-center gap-0.5">
1338
+ <Button
1339
+ variant="ghost"
1340
+ size="icon"
1341
+ className="size-7 text-muted-foreground"
1342
+ disabled={submitting}
1343
+ onClick={onCancel}
1344
+ aria-label={t("designEditor.close")}
1345
+ >
1346
+ <IconX className="size-3.5" />
1347
+ </Button>
1348
+ </div>
883
1349
  </div>
884
1350
  <ReviewCommentComposer
885
1351
  className="px-3 pb-3"
@@ -887,13 +1353,28 @@ function DraftComposer({
887
1353
  value={value}
888
1354
  disabled={submitting}
889
1355
  onChange={onChange}
890
- onSubmit={onSubmit}
891
- submittingTarget={submitting ? resolutionTarget : null}
1356
+ onSubmit={(target) => {
1357
+ if (target === "agent" && smartAgentAvailable) {
1358
+ onSmartSubmit(sendMode);
1359
+ return;
1360
+ }
1361
+ onSubmit(target);
1362
+ }}
1363
+ submittingTarget={commentSubmitting ? resolutionTarget : null}
892
1364
  showAgentAction={showAgentAction}
1365
+ agentAction={agentAction}
893
1366
  placeholder={t("review.placeholder")}
894
1367
  commentLabel={t("review.commentMode")}
895
1368
  agentLabel={t("review.sendToAgent")}
1369
+ contextLabel={
1370
+ smartAgentAvailable && (modeOverride !== "auto" || value.trim())
1371
+ ? sendMode === "preview"
1372
+ ? t("designEditor.nodeRewrite.willPreview")
1373
+ : t("designEditor.nodeRewrite.willAsk")
1374
+ : undefined
1375
+ }
896
1376
  submitOnEnter
1377
+ enterSubmitTarget={initialAgentMode === "preview" ? "agent" : "human"}
897
1378
  onEscape={onCancel}
898
1379
  />
899
1380
  </div>