@agent-native/core 0.105.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 (194) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +39 -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/AssistantChat.tsx +4 -0
  8. package/corpus/core/src/client/MultiTabAssistantChat.tsx +5 -1
  9. package/corpus/core/src/client/composer/PromptComposer.tsx +8 -0
  10. package/corpus/core/src/client/composer/TiptapComposer.tsx +38 -1
  11. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +107 -32
  12. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +33 -0
  13. package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
  14. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +10 -3
  15. package/corpus/core/src/client/use-chat-models.ts +10 -2
  16. package/corpus/core/src/connections/catalog.ts +107 -6
  17. package/corpus/core/src/localization/default-messages.ts +2 -0
  18. package/corpus/core/src/mcp/actions/call-mcp-tool.ts +18 -0
  19. package/corpus/core/src/mcp/actions/list-mcp-tools.ts +18 -0
  20. package/corpus/core/src/mcp-client/app-api.ts +136 -0
  21. package/corpus/core/src/mcp-client/index.ts +21 -0
  22. package/corpus/core/src/mcp-client/oauth-client.ts +462 -0
  23. package/corpus/core/src/mcp-client/oauth-routes.ts +307 -0
  24. package/corpus/core/src/mcp-client/remote-store.ts +93 -118
  25. package/corpus/core/src/mcp-client/remote-url.ts +97 -0
  26. package/corpus/core/src/mcp-client/routes.ts +11 -0
  27. package/corpus/core/src/oauth-tokens/store.ts +10 -4
  28. package/corpus/core/src/provider-api/index.ts +690 -55
  29. package/corpus/core/src/secrets/register-framework-secrets.ts +30 -0
  30. package/corpus/core/src/server/action-discovery.ts +2 -0
  31. package/corpus/core/src/server/core-routes-plugin.ts +9 -1
  32. package/corpus/core/src/server/workspace-provider-oauth.ts +448 -61
  33. package/corpus/core/src/templates/default/AGENTS.md +9 -7
  34. package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
  35. package/corpus/templates/analytics/app/lib/data-source-status.ts +2 -0
  36. package/corpus/templates/analytics/app/pages/DataSources.tsx +61 -0
  37. package/corpus/templates/analytics/server/handlers/jira.ts +7 -0
  38. package/corpus/templates/analytics/server/lib/jira.ts +14 -42
  39. package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +11 -0
  40. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +22 -0
  41. package/corpus/templates/design/AGENTS.md +16 -0
  42. package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +54 -0
  43. package/corpus/templates/design/actions/propose-node-rewrite.ts +290 -0
  44. package/corpus/templates/design/actions/resolve-node-rewrite.ts +257 -0
  45. package/corpus/templates/design/actions/view-screen.ts +98 -2
  46. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +71 -1
  47. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +19 -0
  48. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -0
  49. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +194 -0
  50. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +21 -4
  51. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +18 -0
  52. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  53. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +638 -0
  54. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +535 -54
  55. package/corpus/templates/design/app/components/visual-editor/index.ts +5 -1
  56. package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +21 -3
  57. package/corpus/templates/design/app/i18n/ar-SA.ts +35 -0
  58. package/corpus/templates/design/app/i18n/de-DE.ts +35 -0
  59. package/corpus/templates/design/app/i18n/es-ES.ts +35 -0
  60. package/corpus/templates/design/app/i18n/fr-FR.ts +35 -0
  61. package/corpus/templates/design/app/i18n/hi-IN.ts +35 -0
  62. package/corpus/templates/design/app/i18n/ja-JP.ts +35 -0
  63. package/corpus/templates/design/app/i18n/ko-KR.ts +35 -0
  64. package/corpus/templates/design/app/i18n/pt-BR.ts +35 -0
  65. package/corpus/templates/design/app/i18n/zh-CN.ts +35 -0
  66. package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
  67. package/corpus/templates/design/app/i18n-data.ts +32 -0
  68. package/corpus/templates/design/app/lib/node-reprompt.ts +110 -0
  69. package/corpus/templates/design/changelog/2026-07-16-design-editing-and-review-are-now-more-predictable.md +6 -0
  70. package/corpus/templates/design/server/lib/reprompt-action-guard.ts +107 -0
  71. package/corpus/templates/design/server/plugins/agent-chat.ts +9 -1
  72. package/corpus/templates/design/shared/code-layer.ts +15 -0
  73. package/corpus/templates/design/shared/node-rewrite.ts +213 -0
  74. package/corpus/templates/design/shared/review-anchor.ts +26 -3
  75. package/corpus/templates/dispatch/app/routes/integrations.tsx +57 -4
  76. package/dist/application-state/index.d.ts +2 -2
  77. package/dist/application-state/index.d.ts.map +1 -1
  78. package/dist/application-state/index.js +2 -2
  79. package/dist/application-state/index.js.map +1 -1
  80. package/dist/application-state/script-helpers.d.ts +1 -0
  81. package/dist/application-state/script-helpers.d.ts.map +1 -1
  82. package/dist/application-state/script-helpers.js +7 -1
  83. package/dist/application-state/script-helpers.js.map +1 -1
  84. package/dist/application-state/store.d.ts +1 -0
  85. package/dist/application-state/store.d.ts.map +1 -1
  86. package/dist/application-state/store.js +28 -0
  87. package/dist/application-state/store.js.map +1 -1
  88. package/dist/client/AssistantChat.d.ts +2 -0
  89. package/dist/client/AssistantChat.d.ts.map +1 -1
  90. package/dist/client/AssistantChat.js +2 -2
  91. package/dist/client/AssistantChat.js.map +1 -1
  92. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  93. package/dist/client/MultiTabAssistantChat.js +5 -2
  94. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  95. package/dist/client/composer/PromptComposer.d.ts +2 -0
  96. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  97. package/dist/client/composer/PromptComposer.js +5 -2
  98. package/dist/client/composer/PromptComposer.js.map +1 -1
  99. package/dist/client/composer/TiptapComposer.d.ts +4 -1
  100. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  101. package/dist/client/composer/TiptapComposer.js +13 -5
  102. package/dist/client/composer/TiptapComposer.js.map +1 -1
  103. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  104. package/dist/client/resources/McpIntegrationDialog.js +58 -14
  105. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  106. package/dist/client/resources/mcp-integration-catalog.d.ts +9 -0
  107. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  108. package/dist/client/resources/mcp-integration-catalog.js +13 -0
  109. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  110. package/dist/client/resources/use-mcp-servers.d.ts +1 -0
  111. package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
  112. package/dist/client/resources/use-mcp-servers.js.map +1 -1
  113. package/dist/client/review/ReviewCommentComposer.d.ts +4 -1
  114. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  115. package/dist/client/review/ReviewCommentComposer.js +3 -3
  116. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  117. package/dist/client/use-chat-models.d.ts +1 -0
  118. package/dist/client/use-chat-models.d.ts.map +1 -1
  119. package/dist/client/use-chat-models.js +8 -2
  120. package/dist/client/use-chat-models.js.map +1 -1
  121. package/dist/collab/struct-routes.d.ts +1 -1
  122. package/dist/connections/catalog.d.ts +78 -7
  123. package/dist/connections/catalog.d.ts.map +1 -1
  124. package/dist/connections/catalog.js +97 -6
  125. package/dist/connections/catalog.js.map +1 -1
  126. package/dist/localization/default-messages.d.ts +2 -0
  127. package/dist/localization/default-messages.d.ts.map +1 -1
  128. package/dist/localization/default-messages.js +2 -0
  129. package/dist/localization/default-messages.js.map +1 -1
  130. package/dist/mcp/actions/call-mcp-tool.d.ts +7 -0
  131. package/dist/mcp/actions/call-mcp-tool.d.ts.map +1 -0
  132. package/dist/mcp/actions/call-mcp-tool.js +15 -0
  133. package/dist/mcp/actions/call-mcp-tool.js.map +1 -0
  134. package/dist/mcp/actions/list-mcp-tools.d.ts +5 -0
  135. package/dist/mcp/actions/list-mcp-tools.d.ts.map +1 -0
  136. package/dist/mcp/actions/list-mcp-tools.js +16 -0
  137. package/dist/mcp/actions/list-mcp-tools.js.map +1 -0
  138. package/dist/mcp-client/app-api.d.ts +34 -0
  139. package/dist/mcp-client/app-api.d.ts.map +1 -0
  140. package/dist/mcp-client/app-api.js +94 -0
  141. package/dist/mcp-client/app-api.js.map +1 -0
  142. package/dist/mcp-client/index.d.ts +3 -1
  143. package/dist/mcp-client/index.d.ts.map +1 -1
  144. package/dist/mcp-client/index.js +3 -1
  145. package/dist/mcp-client/index.js.map +1 -1
  146. package/dist/mcp-client/oauth-client.d.ts +113 -0
  147. package/dist/mcp-client/oauth-client.d.ts.map +1 -0
  148. package/dist/mcp-client/oauth-client.js +316 -0
  149. package/dist/mcp-client/oauth-client.js.map +1 -0
  150. package/dist/mcp-client/oauth-routes.d.ts +25 -0
  151. package/dist/mcp-client/oauth-routes.d.ts.map +1 -0
  152. package/dist/mcp-client/oauth-routes.js +218 -0
  153. package/dist/mcp-client/oauth-routes.js.map +1 -0
  154. package/dist/mcp-client/remote-store.d.ts +21 -6
  155. package/dist/mcp-client/remote-store.d.ts.map +1 -1
  156. package/dist/mcp-client/remote-store.js +72 -124
  157. package/dist/mcp-client/remote-store.js.map +1 -1
  158. package/dist/mcp-client/remote-url.d.ts +8 -0
  159. package/dist/mcp-client/remote-url.d.ts.map +1 -0
  160. package/dist/mcp-client/remote-url.js +103 -0
  161. package/dist/mcp-client/remote-url.js.map +1 -0
  162. package/dist/mcp-client/routes.d.ts +1 -0
  163. package/dist/mcp-client/routes.d.ts.map +1 -1
  164. package/dist/mcp-client/routes.js +9 -0
  165. package/dist/mcp-client/routes.js.map +1 -1
  166. package/dist/notifications/routes.d.ts +2 -2
  167. package/dist/oauth-tokens/store.d.ts +2 -2
  168. package/dist/oauth-tokens/store.d.ts.map +1 -1
  169. package/dist/oauth-tokens/store.js +10 -6
  170. package/dist/oauth-tokens/store.js.map +1 -1
  171. package/dist/observability/routes.d.ts +5 -5
  172. package/dist/provider-api/index.d.ts +14 -0
  173. package/dist/provider-api/index.d.ts.map +1 -1
  174. package/dist/provider-api/index.js +499 -61
  175. package/dist/provider-api/index.js.map +1 -1
  176. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  177. package/dist/secrets/register-framework-secrets.js +28 -0
  178. package/dist/secrets/register-framework-secrets.js.map +1 -1
  179. package/dist/secrets/routes.d.ts +9 -9
  180. package/dist/server/action-discovery.d.ts.map +1 -1
  181. package/dist/server/action-discovery.js +2 -0
  182. package/dist/server/action-discovery.js.map +1 -1
  183. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  184. package/dist/server/core-routes-plugin.js +9 -1
  185. package/dist/server/core-routes-plugin.js.map +1 -1
  186. package/dist/server/workspace-provider-oauth.d.ts +10 -3
  187. package/dist/server/workspace-provider-oauth.d.ts.map +1 -1
  188. package/dist/server/workspace-provider-oauth.js +325 -43
  189. package/dist/server/workspace-provider-oauth.js.map +1 -1
  190. package/dist/templates/default/AGENTS.md +9 -7
  191. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
  192. package/package.json +2 -2
  193. package/src/templates/default/AGENTS.md +9 -7
  194. package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
@@ -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
+ }