@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
@@ -0,0 +1,638 @@
1
+ import {
2
+ callAction,
3
+ setClientAppState,
4
+ useActionMutation,
5
+ useChangeVersion,
6
+ useT,
7
+ } from "@agent-native/core/client";
8
+ import {
9
+ designRepromptPendingStateKey,
10
+ isNodeRewriteProposal,
11
+ type NodeHtmlPreviewBridgeMessage,
12
+ type NodeRewriteProposal,
13
+ type NodeRewriteTarget,
14
+ } from "@shared/node-rewrite";
15
+ import {
16
+ IconCheck,
17
+ IconChevronLeft,
18
+ IconChevronRight,
19
+ IconRefresh,
20
+ IconX,
21
+ } from "@tabler/icons-react";
22
+ import {
23
+ useCallback,
24
+ useEffect,
25
+ useLayoutEffect,
26
+ useMemo,
27
+ useRef,
28
+ useState,
29
+ } from "react";
30
+ import { createPortal } from "react-dom";
31
+ import { toast } from "sonner";
32
+
33
+ import { Button } from "@/components/ui/button";
34
+ import { Input } from "@/components/ui/input";
35
+ import { Spinner } from "@/components/ui/spinner";
36
+ import {
37
+ Tooltip,
38
+ TooltipContent,
39
+ TooltipProvider,
40
+ TooltipTrigger,
41
+ } from "@/components/ui/tooltip";
42
+ import { sendToDesignAgentChatAndConfirm } from "@/lib/agent-chat";
43
+ import {
44
+ formatNodeRepromptSubmission,
45
+ NODE_REPROMPT_PRESENTED_EVENT,
46
+ NODE_REPROMPT_RESOLVED_EVENT,
47
+ } from "@/lib/node-reprompt";
48
+
49
+ import type { IframeNodeHtmlPreviewAppliedPayload } from "../design/design-canvas/iframe-events";
50
+
51
+ interface NodeRewriteProposalProps {
52
+ designId: string;
53
+ fileId: string;
54
+ canvasSelector: string;
55
+ active?: boolean;
56
+ proposalSnapshot?: NodeRewriteProposal | null;
57
+ }
58
+
59
+ function escapeSelectorValue(value: string): string {
60
+ const escape = globalThis.CSS?.escape;
61
+ return escape ? escape(value) : value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
62
+ }
63
+
64
+ function findProposalElement(
65
+ iframe: HTMLIFrameElement,
66
+ proposalId: string,
67
+ target: NodeRewriteTarget,
68
+ ): Element | null {
69
+ try {
70
+ const document = iframe.contentDocument;
71
+ if (!document) return null;
72
+ const marked = document.querySelector(
73
+ `[data-agent-native-node-rewrite-proposal="${escapeSelectorValue(proposalId)}"]`,
74
+ );
75
+ if (marked) return marked;
76
+ if (target.nodeId) {
77
+ const id = escapeSelectorValue(target.nodeId);
78
+ const byId = document.querySelector(
79
+ `[data-agent-native-node-id="${id}"],[data-code-layer-id="${id}"],[data-layer-id="${id}"],[data-builder-id="${id}"],#${id}`,
80
+ );
81
+ if (byId) return byId;
82
+ }
83
+ return target.selector ? document.querySelector(target.selector) : null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ interface ProposalAnchor {
90
+ centerX: number;
91
+ top: number;
92
+ bottom: number;
93
+ }
94
+
95
+ interface ProposalPopoverSize {
96
+ width: number;
97
+ height: number;
98
+ }
99
+
100
+ const PROPOSAL_POPOVER_GAP = 8;
101
+ const PROPOSAL_POPOVER_MARGIN = 12;
102
+ const DEFAULT_PROPOSAL_POPOVER_SIZE = { width: 320, height: 176 };
103
+
104
+ export function placeNodeRewritePopover(
105
+ anchor: ProposalAnchor,
106
+ popover: ProposalPopoverSize,
107
+ viewport: ProposalPopoverSize,
108
+ ): { left: number; top: number; side: "above" | "below" | "clamped" } {
109
+ const maxLeft = Math.max(
110
+ PROPOSAL_POPOVER_MARGIN,
111
+ viewport.width - popover.width - PROPOSAL_POPOVER_MARGIN,
112
+ );
113
+ const left = Math.min(
114
+ maxLeft,
115
+ Math.max(PROPOSAL_POPOVER_MARGIN, anchor.centerX - popover.width / 2),
116
+ );
117
+ const belowTop = anchor.bottom + PROPOSAL_POPOVER_GAP;
118
+ if (belowTop + popover.height <= viewport.height - PROPOSAL_POPOVER_MARGIN) {
119
+ return { left, top: belowTop, side: "below" };
120
+ }
121
+ const aboveTop = anchor.top - PROPOSAL_POPOVER_GAP - popover.height;
122
+ if (aboveTop >= PROPOSAL_POPOVER_MARGIN) {
123
+ return { left, top: aboveTop, side: "above" };
124
+ }
125
+ return {
126
+ left,
127
+ top: Math.max(
128
+ PROPOSAL_POPOVER_MARGIN,
129
+ Math.min(
130
+ belowTop,
131
+ viewport.height - popover.height - PROPOSAL_POPOVER_MARGIN,
132
+ ),
133
+ ),
134
+ side: "clamped",
135
+ };
136
+ }
137
+
138
+ function proposalAnchor(
139
+ canvas: HTMLElement,
140
+ proposal: NodeRewriteProposal,
141
+ ): ProposalAnchor | null {
142
+ const iframe = canvas.querySelector<HTMLIFrameElement>(
143
+ "iframe[data-design-preview-iframe]",
144
+ );
145
+ if (!iframe) return null;
146
+ const iframeRect = iframe.getBoundingClientRect();
147
+ const element = findProposalElement(
148
+ iframe,
149
+ proposal.proposalId,
150
+ proposal.resolvedTarget,
151
+ );
152
+ if (!element) {
153
+ return {
154
+ centerX: iframeRect.left + iframeRect.width / 2,
155
+ top: iframeRect.top + 16,
156
+ bottom: iframeRect.top + 16,
157
+ };
158
+ }
159
+ const rect = element.getBoundingClientRect();
160
+ const scaleX = iframeRect.width / Math.max(1, iframe.clientWidth);
161
+ const scaleY = iframeRect.height / Math.max(1, iframe.clientHeight);
162
+ return {
163
+ centerX: iframeRect.left + (rect.left + rect.width / 2) * scaleX,
164
+ top: iframeRect.top + rect.top * scaleY,
165
+ bottom: iframeRect.top + (rect.top + rect.height) * scaleY,
166
+ };
167
+ }
168
+
169
+ export function NodeRewriteProposal({
170
+ designId,
171
+ fileId,
172
+ canvasSelector,
173
+ active = true,
174
+ proposalSnapshot,
175
+ }: NodeRewriteProposalProps) {
176
+ const t = useT();
177
+ const appStateVersion = useChangeVersion("app-state");
178
+ const resolveMutation = useActionMutation("resolve-node-rewrite");
179
+ const [proposal, setProposal] = useState<NodeRewriteProposal | null>(null);
180
+ const proposalRef = useRef(proposal);
181
+ proposalRef.current = proposal;
182
+ const [chosenIndex, setChosenIndex] = useState(0);
183
+ const [previewReadyProposalId, setPreviewReadyProposalId] = useState<
184
+ string | null
185
+ >(null);
186
+ const [refinement, setRefinement] = useState("");
187
+ const [refining, setRefining] = useState(false);
188
+ const [layoutTick, setLayoutTick] = useState(0);
189
+ const proposalPopoverRef = useRef<HTMLDivElement | null>(null);
190
+ const [proposalPopoverSize, setProposalPopoverSize] = useState(
191
+ DEFAULT_PROPOSAL_POPOVER_SIZE,
192
+ );
193
+ const getCanvas = useCallback(
194
+ () => document.querySelector<HTMLElement>(canvasSelector), // i18n-ignore DOM query
195
+ [canvasSelector],
196
+ );
197
+
198
+ const postToPreview = useCallback(
199
+ (message: NodeHtmlPreviewBridgeMessage) => {
200
+ const iframe = getCanvas()?.querySelector<HTMLIFrameElement>(
201
+ "iframe[data-design-preview-iframe]",
202
+ );
203
+ if (!iframe?.contentWindow) return false;
204
+ iframe.contentWindow.postMessage(message, "*");
205
+ return true;
206
+ },
207
+ [getCanvas],
208
+ );
209
+
210
+ const restorePreview = useCallback(
211
+ (current: NodeRewriteProposal) => {
212
+ postToPreview({
213
+ type: "node-html-preview",
214
+ proposalId: current.proposalId,
215
+ target: current.resolvedTarget,
216
+ operation: "restore",
217
+ });
218
+ },
219
+ [postToPreview],
220
+ );
221
+
222
+ const syncProposal = useCallback(
223
+ (value: unknown) => {
224
+ if (
225
+ isNodeRewriteProposal(value) &&
226
+ value.designId === designId &&
227
+ value.fileId === fileId
228
+ ) {
229
+ const previous = proposalRef.current;
230
+ if (previous?.proposalId !== value.proposalId) {
231
+ setPreviewReadyProposalId(null);
232
+ }
233
+ if (
234
+ previous?.proposalId === value.proposalId &&
235
+ previous.chosenIndex === value.chosenIndex &&
236
+ previous.variants[value.chosenIndex]?.html ===
237
+ value.variants[value.chosenIndex]?.html
238
+ ) {
239
+ return;
240
+ }
241
+ setProposal(value);
242
+ setChosenIndex(
243
+ Math.min(value.variants.length - 1, Math.max(0, value.chosenIndex)),
244
+ );
245
+ return;
246
+ }
247
+ const previous = proposalRef.current;
248
+ if (previous) {
249
+ restorePreview(previous);
250
+ window.dispatchEvent(
251
+ new CustomEvent(NODE_REPROMPT_RESOLVED_EVENT, {
252
+ detail: { repromptId: previous.repromptId, fileId },
253
+ }),
254
+ );
255
+ }
256
+ setProposal(null);
257
+ },
258
+ [designId, fileId, restorePreview],
259
+ );
260
+
261
+ useEffect(() => {
262
+ if (!active) return;
263
+ if (proposalSnapshot !== undefined) {
264
+ syncProposal(proposalSnapshot);
265
+ }
266
+ }, [active, appStateVersion, proposalSnapshot, syncProposal]);
267
+
268
+ useEffect(() => {
269
+ if (!active || !proposal) return;
270
+ const canvas = getCanvas();
271
+ const iframe = canvas?.querySelector<HTMLIFrameElement>(
272
+ "iframe[data-design-preview-iframe]",
273
+ );
274
+ if (!iframe?.contentWindow) return;
275
+ const onMessage = (event: MessageEvent) => {
276
+ const data = event.data as
277
+ | IframeNodeHtmlPreviewAppliedPayload
278
+ | undefined;
279
+ if (
280
+ event.source !== iframe.contentWindow ||
281
+ data?.type !== "agent-native:node-html-preview-applied" ||
282
+ data.proposalId !== proposal.proposalId
283
+ ) {
284
+ return;
285
+ }
286
+ setPreviewReadyProposalId(proposal.proposalId);
287
+ setLayoutTick((value) => value + 1);
288
+ window.dispatchEvent(
289
+ new CustomEvent(NODE_REPROMPT_PRESENTED_EVENT, {
290
+ detail: { repromptId: proposal.repromptId, fileId },
291
+ }),
292
+ );
293
+ };
294
+ window.addEventListener("message", onMessage);
295
+ return () => window.removeEventListener("message", onMessage);
296
+ }, [active, fileId, getCanvas, proposal]);
297
+
298
+ useEffect(() => {
299
+ if (!active || !proposal) return;
300
+ const variant = proposal.variants[chosenIndex];
301
+ if (!variant) return;
302
+ let cancelled = false;
303
+ let retryFrame: number | null = null;
304
+ let retryCount = 0;
305
+ const sendPreview = () => {
306
+ if (cancelled) return;
307
+ const sent = postToPreview({
308
+ type: "node-html-preview",
309
+ proposalId: proposal.proposalId,
310
+ target: proposal.resolvedTarget,
311
+ html: variant.html,
312
+ operation: "preview",
313
+ });
314
+ if (!sent && retryCount < 120) {
315
+ retryCount += 1;
316
+ retryFrame = window.requestAnimationFrame(sendPreview);
317
+ }
318
+ };
319
+ sendPreview();
320
+ const frame = window.requestAnimationFrame(() =>
321
+ setLayoutTick((value) => value + 1),
322
+ );
323
+ return () => {
324
+ cancelled = true;
325
+ window.cancelAnimationFrame(frame);
326
+ if (retryFrame !== null) window.cancelAnimationFrame(retryFrame);
327
+ };
328
+ }, [active, chosenIndex, postToPreview, proposal]);
329
+
330
+ useEffect(() => {
331
+ if (!active || !proposal) return;
332
+ const canvas = getCanvas();
333
+ if (!canvas) return;
334
+ const bump = () => setLayoutTick((value) => value + 1);
335
+ const observer = new ResizeObserver(bump);
336
+ observer.observe(canvas);
337
+ const iframe = canvas.querySelector<HTMLIFrameElement>(
338
+ "iframe[data-design-preview-iframe]",
339
+ );
340
+ if (iframe) observer.observe(iframe);
341
+ window.addEventListener("resize", bump);
342
+ window.addEventListener("scroll", bump, true);
343
+ return () => {
344
+ observer.disconnect();
345
+ window.removeEventListener("resize", bump);
346
+ window.removeEventListener("scroll", bump, true);
347
+ };
348
+ }, [active, getCanvas, proposal]);
349
+
350
+ useLayoutEffect(() => {
351
+ const element = proposalPopoverRef.current;
352
+ if (!element) return;
353
+ const update = () => {
354
+ const rect = element.getBoundingClientRect();
355
+ if (rect.width <= 0 || rect.height <= 0) return;
356
+ setProposalPopoverSize((current) =>
357
+ current.width === rect.width && current.height === rect.height
358
+ ? current
359
+ : { width: rect.width, height: rect.height },
360
+ );
361
+ };
362
+ update();
363
+ const observer = new ResizeObserver(update);
364
+ observer.observe(element);
365
+ return () => observer.disconnect();
366
+ }, [previewReadyProposalId, proposal?.proposalId]);
367
+
368
+ const chooseVariant = useCallback(
369
+ (index: number) => {
370
+ if (!proposal || index === chosenIndex) return;
371
+ setChosenIndex(index);
372
+ },
373
+ [chosenIndex, proposal],
374
+ );
375
+
376
+ const resolve = useCallback(
377
+ async (resolution: "accept" | "reject") => {
378
+ if (!proposal || refining || resolveMutation.isPending) return;
379
+ if (resolution === "reject") restorePreview(proposal);
380
+ try {
381
+ await resolveMutation.mutateAsync({
382
+ proposalId: proposal.proposalId,
383
+ resolution,
384
+ ...(resolution === "accept" ? { variantIndex: chosenIndex } : {}),
385
+ });
386
+ window.dispatchEvent(
387
+ new CustomEvent(NODE_REPROMPT_RESOLVED_EVENT, {
388
+ detail: { repromptId: proposal.repromptId, fileId },
389
+ }),
390
+ );
391
+ setProposal(null);
392
+ } catch (error) {
393
+ if (resolution === "reject") {
394
+ const variant = proposal.variants[chosenIndex];
395
+ if (variant) {
396
+ postToPreview({
397
+ type: "node-html-preview",
398
+ proposalId: proposal.proposalId,
399
+ target: proposal.resolvedTarget,
400
+ html: variant.html,
401
+ operation: "preview",
402
+ });
403
+ }
404
+ }
405
+ toast.error(
406
+ error instanceof Error && error.message
407
+ ? error.message
408
+ : t("designEditor.nodeRewrite.resolveFailed"),
409
+ );
410
+ }
411
+ },
412
+ [
413
+ chosenIndex,
414
+ fileId,
415
+ postToPreview,
416
+ proposal,
417
+ refining,
418
+ resolveMutation,
419
+ restorePreview,
420
+ t,
421
+ ],
422
+ );
423
+
424
+ const submitRefinement = useCallback(async () => {
425
+ const instruction = refinement.trim();
426
+ if (!proposal || !instruction || refining || resolveMutation.isPending)
427
+ return;
428
+ const repromptId = crypto.randomUUID();
429
+ const pendingKey = designRepromptPendingStateKey(designId, fileId);
430
+ const pending = {
431
+ repromptId,
432
+ designId,
433
+ fileId,
434
+ target: proposal.target,
435
+ baseVersionHash: proposal.baseVersionHash,
436
+ instruction,
437
+ createdAt: new Date().toISOString(),
438
+ priorProposalId: proposal.proposalId,
439
+ priorRepromptId: proposal.repromptId,
440
+ };
441
+ setRefining(true);
442
+ try {
443
+ await setClientAppState(pendingKey, pending);
444
+ const submission = formatNodeRepromptSubmission({
445
+ ...pending,
446
+ priorProposalId: proposal.proposalId,
447
+ subtreeHtml: proposal.variants[chosenIndex]?.html,
448
+ });
449
+ const delivery = await sendToDesignAgentChatAndConfirm(
450
+ {
451
+ ...submission,
452
+ submit: true,
453
+ openSidebar: true,
454
+ },
455
+ { timeoutMs: 10_000 },
456
+ );
457
+ if (!delivery.delivered) {
458
+ throw new Error(delivery.reason ?? "Reprompt was not delivered.");
459
+ }
460
+ setRefinement("");
461
+ toast.success(t("designEditor.nodeRewrite.refinementSent"));
462
+ } catch (error) {
463
+ await callAction("cancel-node-rewrite-request", {
464
+ designId,
465
+ fileId,
466
+ repromptId,
467
+ }).catch(() => {});
468
+ toast.error(
469
+ error instanceof Error && error.message
470
+ ? error.message
471
+ : t("designEditor.nodeRewrite.refinementFailed"),
472
+ );
473
+ } finally {
474
+ setRefining(false);
475
+ }
476
+ }, [
477
+ chosenIndex,
478
+ designId,
479
+ fileId,
480
+ proposal,
481
+ refinement,
482
+ refining,
483
+ resolveMutation.isPending,
484
+ t,
485
+ ]);
486
+
487
+ if (!active || !proposal) {
488
+ return null;
489
+ }
490
+ void layoutTick;
491
+ const canvas = getCanvas();
492
+ const anchor = canvas ? proposalAnchor(canvas, proposal) : null;
493
+ const viewport = { width: window.innerWidth, height: window.innerHeight };
494
+ const position = placeNodeRewritePopover(
495
+ anchor ?? {
496
+ centerX: viewport.width / 2,
497
+ top: viewport.height / 2,
498
+ bottom: viewport.height / 2,
499
+ },
500
+ proposalPopoverSize,
501
+ viewport,
502
+ );
503
+ const busy = resolveMutation.isPending || refining;
504
+
505
+ return createPortal(
506
+ <div
507
+ ref={proposalPopoverRef}
508
+ data-node-rewrite-proposal={proposal.proposalId}
509
+ data-side={position.side}
510
+ className="fixed z-[265] max-h-[calc(100vh-1.5rem)] w-80 max-w-[calc(100vw-1.5rem)] overflow-y-auto rounded-xl border border-border bg-popover p-3 shadow-xl"
511
+ style={{ left: position.left, top: position.top }}
512
+ >
513
+ <div className="flex items-start gap-2">
514
+ <div className="min-w-0 flex-1">
515
+ <p className="text-xs font-semibold text-foreground">
516
+ {t("designEditor.nodeRewrite.previewTitle")}
517
+ </p>
518
+ <p className="mt-0.5 line-clamp-2 text-[11px] leading-4 text-muted-foreground">
519
+ {proposal.variants[chosenIndex]?.summary}
520
+ </p>
521
+ </div>
522
+ {proposal.variants.length > 1 ? (
523
+ <TooltipProvider delayDuration={300}>
524
+ <div
525
+ className="flex shrink-0 items-center gap-0.5 rounded-md border border-border bg-muted/30 p-0.5"
526
+ aria-label={t("designEditor.nodeRewrite.variants")}
527
+ >
528
+ <Tooltip>
529
+ <TooltipTrigger asChild>
530
+ <Button
531
+ type="button"
532
+ size="icon"
533
+ variant="ghost"
534
+ className="size-6"
535
+ disabled={busy || chosenIndex === 0}
536
+ onClick={() => chooseVariant(chosenIndex - 1)}
537
+ aria-label={t("designEditor.nodeRewrite.previousCandidate")}
538
+ >
539
+ <IconChevronLeft className="size-3.5 rtl:rotate-180" />
540
+ </Button>
541
+ </TooltipTrigger>
542
+ <TooltipContent side="top" className="text-xs">
543
+ {t("designEditor.nodeRewrite.previousCandidate")}
544
+ </TooltipContent>
545
+ </Tooltip>
546
+ <span className="min-w-12 text-center text-[11px] font-medium tabular-nums text-muted-foreground">
547
+ {t("designEditor.nodeRewrite.candidatePosition", {
548
+ current: chosenIndex + 1,
549
+ total: proposal.variants.length,
550
+ })}
551
+ </span>
552
+ <Tooltip>
553
+ <TooltipTrigger asChild>
554
+ <Button
555
+ type="button"
556
+ size="icon"
557
+ variant="ghost"
558
+ className="size-6"
559
+ disabled={
560
+ busy || chosenIndex === proposal.variants.length - 1
561
+ }
562
+ onClick={() => chooseVariant(chosenIndex + 1)}
563
+ aria-label={t("designEditor.nodeRewrite.nextCandidate")}
564
+ >
565
+ <IconChevronRight className="size-3.5 rtl:rotate-180" />
566
+ </Button>
567
+ </TooltipTrigger>
568
+ <TooltipContent side="top" className="text-xs">
569
+ {t("designEditor.nodeRewrite.nextCandidate")}
570
+ </TooltipContent>
571
+ </Tooltip>
572
+ </div>
573
+ </TooltipProvider>
574
+ ) : null}
575
+ </div>
576
+
577
+ <div className="mt-3 flex items-center gap-1.5">
578
+ <Input
579
+ value={refinement}
580
+ onChange={(event) => setRefinement(event.currentTarget.value)}
581
+ onKeyDown={(event) => {
582
+ if (event.key === "Enter" && refinement.trim()) {
583
+ event.preventDefault();
584
+ void submitRefinement();
585
+ }
586
+ }}
587
+ disabled={busy}
588
+ placeholder={t("designEditor.nodeRewrite.refinePlaceholder")}
589
+ className="h-8 min-w-0 flex-1 text-xs"
590
+ />
591
+ <Button
592
+ type="button"
593
+ size="icon"
594
+ variant="outline"
595
+ className="size-8 shrink-0"
596
+ disabled={!refinement.trim() || busy}
597
+ onClick={() => void submitRefinement()}
598
+ aria-label={t("designEditor.nodeRewrite.refine")}
599
+ >
600
+ {refining ? (
601
+ <Spinner className="size-3.5" />
602
+ ) : (
603
+ <IconRefresh className="size-3.5" />
604
+ )}
605
+ </Button>
606
+ </div>
607
+
608
+ <div className="mt-2.5 flex items-center justify-end gap-1.5">
609
+ <Button
610
+ type="button"
611
+ size="sm"
612
+ variant="ghost"
613
+ className="h-8 gap-1.5 px-2.5 text-xs"
614
+ disabled={busy}
615
+ onClick={() => void resolve("reject")}
616
+ >
617
+ <IconX className="size-3.5" />
618
+ {t("designEditor.nodeRewrite.reject")}
619
+ </Button>
620
+ <Button
621
+ type="button"
622
+ size="sm"
623
+ className="h-8 gap-1.5 px-3 text-xs"
624
+ disabled={busy}
625
+ onClick={() => void resolve("accept")}
626
+ >
627
+ {resolveMutation.isPending ? (
628
+ <Spinner className="size-3.5" />
629
+ ) : (
630
+ <IconCheck className="size-3.5" />
631
+ )}
632
+ {t("designEditor.nodeRewrite.accept")}
633
+ </Button>
634
+ </div>
635
+ </div>,
636
+ document.body,
637
+ );
638
+ }