@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,257 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ compareAndSetAppState,
4
+ listAppState,
5
+ readAppState,
6
+ } from "@agent-native/core/application-state";
7
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
8
+ import { and, eq } from "drizzle-orm";
9
+ import { z } from "zod";
10
+
11
+ import { getDb, schema } from "../server/db/index.js";
12
+ import {
13
+ readLiveSourceFile,
14
+ writeInlineSourceFile,
15
+ type SourceWorkspaceFile,
16
+ } from "../server/source-workspace.js";
17
+ import type { CodeLayerSource } from "../shared/code-layer.js";
18
+ import {
19
+ DESIGN_REPROMPT_PROPOSAL_STATE_PREFIX,
20
+ designRepromptPendingStateKey,
21
+ designRepromptProposalStateKey,
22
+ isPendingDesignReprompt,
23
+ spliceNodeRewriteVariant,
24
+ type NodeHtmlPreviewBridgeMessage,
25
+ type NodeRewriteProposal,
26
+ } from "../shared/node-rewrite.js";
27
+ import { designSourceTypeFromData } from "../shared/source-mode.js";
28
+
29
+ const targetSchema = z
30
+ .object({
31
+ nodeId: z.string().min(1).optional(),
32
+ selector: z.string().min(1).optional(),
33
+ })
34
+ .superRefine((target, ctx) => {
35
+ if (!target.nodeId && !target.selector) {
36
+ ctx.addIssue({
37
+ code: z.ZodIssueCode.custom,
38
+ path: ["nodeId"],
39
+ message: "target.nodeId or target.selector is required",
40
+ });
41
+ }
42
+ });
43
+
44
+ const proposalSchema = z.object({
45
+ proposalId: z.string().min(1),
46
+ repromptId: z.string().min(1),
47
+ designId: z.string().min(1),
48
+ fileId: z.string().min(1),
49
+ filename: z.string().min(1),
50
+ target: targetSchema,
51
+ resolvedTarget: z.object({
52
+ nodeId: z.string().min(1),
53
+ selector: z.string().min(1),
54
+ }),
55
+ baseVersionHash: z.string().min(1),
56
+ variants: z
57
+ .array(
58
+ z.object({
59
+ html: z.string().min(1).max(250_000),
60
+ summary: z.string().min(1).max(240),
61
+ }),
62
+ )
63
+ .min(1)
64
+ .max(3),
65
+ chosenIndex: z.number().int().min(0),
66
+ createdAt: z.string(),
67
+ });
68
+
69
+ interface EditableDesignFile extends SourceWorkspaceFile {
70
+ designData: string | null;
71
+ }
72
+
73
+ async function resolveProposal(proposalId: string): Promise<{
74
+ key: string;
75
+ proposal: NodeRewriteProposal;
76
+ }> {
77
+ const candidates = await listAppState(DESIGN_REPROMPT_PROPOSAL_STATE_PREFIX);
78
+ const matches = candidates.filter(
79
+ ({ value }) => value.proposalId === proposalId,
80
+ );
81
+ if (matches.length !== 1) {
82
+ throw new Error(
83
+ matches.length === 0
84
+ ? "Node rewrite proposal not found or already resolved."
85
+ : "Multiple node rewrite proposals matched this id.",
86
+ );
87
+ }
88
+ const match = matches[0]!;
89
+ const proposal = proposalSchema.parse(match.value) as NodeRewriteProposal;
90
+ const expectedKey = designRepromptProposalStateKey(
91
+ proposal.designId,
92
+ proposal.fileId,
93
+ proposal.repromptId,
94
+ );
95
+ if (match.key !== expectedKey) {
96
+ throw new Error("Node rewrite proposal state is scoped to the wrong file.");
97
+ }
98
+ return { key: match.key, proposal };
99
+ }
100
+
101
+ async function resolveEditableDesignFile(
102
+ proposal: NodeRewriteProposal,
103
+ ): Promise<EditableDesignFile> {
104
+ const db = getDb();
105
+ const [file] = await db
106
+ .select({
107
+ id: schema.designFiles.id,
108
+ designId: schema.designFiles.designId,
109
+ filename: schema.designFiles.filename,
110
+ fileType: schema.designFiles.fileType,
111
+ content: schema.designFiles.content,
112
+ createdAt: schema.designFiles.createdAt,
113
+ updatedAt: schema.designFiles.updatedAt,
114
+ designData: schema.designs.data,
115
+ })
116
+ .from(schema.designFiles)
117
+ .innerJoin(
118
+ schema.designs,
119
+ eq(schema.designFiles.designId, schema.designs.id),
120
+ )
121
+ .where(
122
+ and(
123
+ accessFilter(schema.designs, schema.designShares),
124
+ eq(schema.designFiles.id, proposal.fileId),
125
+ eq(schema.designFiles.designId, proposal.designId),
126
+ ),
127
+ )
128
+ .limit(1);
129
+
130
+ if (!file) throw new Error("Design HTML file not found.");
131
+ if (file.filename !== proposal.filename || file.fileType !== "html") {
132
+ throw new Error("Node rewrite proposal no longer matches its HTML file.");
133
+ }
134
+ if (designSourceTypeFromData(file.designData) !== "inline") {
135
+ throw new Error("Node rewrites only support inline design screens.");
136
+ }
137
+ await assertAccess("design", proposal.designId, "editor");
138
+ return file;
139
+ }
140
+
141
+ async function clearProposalState(
142
+ proposalKey: string,
143
+ proposal: NodeRewriteProposal,
144
+ pending: Record<string, unknown>,
145
+ ): Promise<void> {
146
+ const pendingKey = designRepromptPendingStateKey(
147
+ proposal.designId,
148
+ proposal.fileId,
149
+ );
150
+ const [proposalCleared] = await Promise.all([
151
+ compareAndSetAppState(
152
+ proposalKey,
153
+ proposal as unknown as Record<string, unknown>,
154
+ null,
155
+ ),
156
+ compareAndSetAppState(pendingKey, pending, null),
157
+ ]);
158
+ if (!proposalCleared) {
159
+ throw new Error("Node rewrite proposal changed while it was resolving.");
160
+ }
161
+ }
162
+
163
+ export default defineAction({
164
+ agentTool: false,
165
+ description:
166
+ "Accept or reject a pending scoped Design node rewrite. Accept performs one " +
167
+ "version-checked inline/Yjs content transaction; reject changes no design content. " +
168
+ "Both resolutions clear the proposal and its matching paired pending reprompt.",
169
+ schema: z.object({
170
+ proposalId: z.string().min(1),
171
+ resolution: z.enum(["accept", "reject"]),
172
+ variantIndex: z.number().int().min(0).optional(),
173
+ }),
174
+ run: async ({ proposalId, resolution, variantIndex }) => {
175
+ const { key: proposalKey, proposal } = await resolveProposal(proposalId);
176
+ const pendingKey = designRepromptPendingStateKey(
177
+ proposal.designId,
178
+ proposal.fileId,
179
+ );
180
+ const pending = await readAppState(pendingKey);
181
+ if (
182
+ !isPendingDesignReprompt(pending) ||
183
+ pending.repromptId !== proposal.repromptId
184
+ ) {
185
+ throw new Error(
186
+ "A newer regeneration request replaced this proposal. Review the latest candidates instead.",
187
+ );
188
+ }
189
+ const restoreMessage: NodeHtmlPreviewBridgeMessage = {
190
+ type: "node-html-preview",
191
+ proposalId,
192
+ target: proposal.resolvedTarget,
193
+ operation: "restore",
194
+ };
195
+
196
+ if (resolution === "reject") {
197
+ await assertAccess("design", proposal.designId, "editor");
198
+ await clearProposalState(proposalKey, proposal, pending);
199
+ return {
200
+ proposalId,
201
+ repromptId: proposal.repromptId,
202
+ resolution,
203
+ changed: false,
204
+ bridgeMessages: [restoreMessage],
205
+ };
206
+ }
207
+
208
+ const file = await resolveEditableDesignFile(proposal);
209
+ const chosenIndex = variantIndex ?? proposal.chosenIndex;
210
+ const variant = proposal.variants[chosenIndex];
211
+ if (!variant) {
212
+ throw new Error(
213
+ `Variant index ${chosenIndex} is out of range for this proposal.`,
214
+ );
215
+ }
216
+
217
+ const live = await readLiveSourceFile(file);
218
+ if (live.versionHash !== proposal.baseVersionHash) {
219
+ throw new Error(
220
+ "Screen changed since proposal — regenerate before accepting.",
221
+ );
222
+ }
223
+ const source: CodeLayerSource = {
224
+ kind: "design-file",
225
+ sourceType: "inline",
226
+ designId: file.designId,
227
+ fileId: file.id,
228
+ filename: file.filename,
229
+ };
230
+ const rewrite = spliceNodeRewriteVariant({
231
+ content: live.content,
232
+ target: proposal.resolvedTarget,
233
+ variant,
234
+ source,
235
+ fileType: file.fileType,
236
+ });
237
+ const write = await writeInlineSourceFile({
238
+ designId: file.designId,
239
+ file,
240
+ content: rewrite.content,
241
+ expectedVersionHash: proposal.baseVersionHash,
242
+ });
243
+ await clearProposalState(proposalKey, proposal, pending);
244
+
245
+ return {
246
+ proposalId,
247
+ repromptId: proposal.repromptId,
248
+ resolution,
249
+ variantIndex: chosenIndex,
250
+ summary: variant.summary,
251
+ changed: write.changed,
252
+ designId: file.designId,
253
+ fileId: file.id,
254
+ versionHash: write.versionHash,
255
+ };
256
+ },
257
+ });
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { defineAction } from "@agent-native/core";
11
11
  import {
12
+ listAppState,
12
13
  readAppState,
13
14
  readAppStateForCurrentTab,
14
15
  } from "@agent-native/core/application-state";
@@ -30,6 +31,14 @@ import { getDb, schema } from "../server/db/index.js";
30
31
  import { parseCanvasFrameGeometryById } from "../shared/canvas-frames.js";
31
32
  import { getDesignTemplatePreset } from "../shared/design-template-presets.js";
32
33
  import { designGenerationSessionKey } from "../shared/generation-session.js";
34
+ import {
35
+ DESIGN_REPROMPT_PENDING_STATE_PREFIX,
36
+ DESIGN_REPROMPT_PROPOSAL_STATE_PREFIX,
37
+ designRepromptPendingStateKey,
38
+ designRepromptProposalStateKey,
39
+ isNodeRewriteProposal,
40
+ isPendingDesignReprompt,
41
+ } from "../shared/node-rewrite.js";
33
42
 
34
43
  interface ReviewThreadSummary {
35
44
  openCount: number;
@@ -309,14 +318,101 @@ export default defineAction({
309
318
  data = {};
310
319
  }
311
320
  }
321
+ const activeScreen = resolveActiveScreen(
322
+ files,
323
+ navigation,
324
+ designSelection,
325
+ );
312
326
  screen.design = {
313
327
  id: designId,
314
328
  title: (access.resource as { title?: unknown }).title ?? null,
315
329
  screens: files,
316
- activeScreen: resolveActiveScreen(files, navigation, designSelection),
330
+ activeScreen,
317
331
  activeCodeFile: resolveActiveCodeFile(files, designSelection),
318
332
  canvasFrames: parseCanvasFrameGeometryById(data.canvasFrames),
319
333
  };
334
+ const proposalPrefix = `${DESIGN_REPROMPT_PROPOSAL_STATE_PREFIX}${designId}:`;
335
+ const pendingPrefix = `${DESIGN_REPROMPT_PENDING_STATE_PREFIX}${designId}:`;
336
+ const [proposalEntries, pendingEntries] = await Promise.all([
337
+ listAppState(proposalPrefix),
338
+ listAppState(pendingPrefix),
339
+ ]);
340
+ const filesById = new Map(files.map((file) => [file.id, file]));
341
+ const pendingByFileId = new Map(
342
+ pendingEntries.flatMap(({ key, value }) => {
343
+ if (
344
+ !isPendingDesignReprompt(value) ||
345
+ value.designId !== designId ||
346
+ !filesById.has(value.fileId) ||
347
+ key !== designRepromptPendingStateKey(designId, value.fileId)
348
+ ) {
349
+ return [];
350
+ }
351
+ return [[value.fileId, value] as const];
352
+ }),
353
+ );
354
+ const proposalsByReprompt = new Map(
355
+ proposalEntries.flatMap(({ key, value }) => {
356
+ if (
357
+ !isNodeRewriteProposal(value) ||
358
+ value.designId !== designId ||
359
+ !filesById.has(value.fileId) ||
360
+ key !==
361
+ designRepromptProposalStateKey(
362
+ designId,
363
+ value.fileId,
364
+ value.repromptId,
365
+ )
366
+ ) {
367
+ return [];
368
+ }
369
+ return [[`${value.fileId}:${value.repromptId}`, value] as const];
370
+ }),
371
+ );
372
+ const proposalsByFileId = new Map(
373
+ [...pendingByFileId.entries()].flatMap(([fileId, pending]) => {
374
+ const current = proposalsByReprompt.get(
375
+ `${fileId}:${pending.repromptId}`,
376
+ );
377
+ const prior = pending.priorRepromptId
378
+ ? proposalsByReprompt.get(`${fileId}:${pending.priorRepromptId}`)
379
+ : undefined;
380
+ const proposal =
381
+ current ??
382
+ (prior?.proposalId === pending.priorProposalId
383
+ ? prior
384
+ : undefined);
385
+ return proposal ? [[fileId, proposal] as const] : [];
386
+ }),
387
+ );
388
+ const pendingCandidateReviews = [...proposalsByFileId.values()].map(
389
+ (proposal) => {
390
+ const file = filesById.get(proposal.fileId)!;
391
+ return {
392
+ proposalId: proposal.proposalId,
393
+ fileId: proposal.fileId,
394
+ filename: proposal.filename || file.filename,
395
+ candidateCount: proposal.variants.length,
396
+ chosenIndex: proposal.chosenIndex,
397
+ target: proposal.target,
398
+ createdAt: proposal.createdAt,
399
+ };
400
+ },
401
+ );
402
+ if (pendingCandidateReviews.length > 0) {
403
+ (screen.design as Record<string, unknown>).pendingCandidateReviews =
404
+ pendingCandidateReviews;
405
+ }
406
+ if (activeScreen?.id) {
407
+ const pendingReprompt = pendingByFileId.get(activeScreen.id);
408
+ const proposal = proposalsByFileId.get(activeScreen.id);
409
+ if (pendingReprompt || proposal) {
410
+ (screen.design as Record<string, unknown>).reprompt = {
411
+ pending: pendingReprompt ?? null,
412
+ proposal: proposal ?? null,
413
+ };
414
+ }
415
+ }
320
416
  const reviewContext = ctx as ReviewResourceContext | undefined;
321
417
  const reviewScope = {
322
418
  userEmail: reviewContext?.userEmail ?? null,
@@ -357,7 +453,7 @@ export default defineAction({
357
453
  redactReviewIdentity
358
454
  ? redactPublicReviewStatusIdentity(reviewStatus)
359
455
  : reviewStatus,
360
- resolveActiveScreen(files, navigation, designSelection)?.id,
456
+ activeScreen?.id,
361
457
  reviewSummary,
362
458
  );
363
459
  }
@@ -92,6 +92,7 @@ export type CanvasContextMenuAction =
92
92
  | "add-auto-layout"
93
93
  | "suggest-auto-layout"
94
94
  | "create-component"
95
+ | "reprompt"
95
96
  | "go-to-main-component"
96
97
  | "swap-instance"
97
98
  | "detach-instance"
@@ -135,6 +136,7 @@ export type CanvasContextMenuActionHandler = (
135
136
 
136
137
  export interface CanvasContextMenuLabels {
137
138
  selectLayer: string;
139
+ reprompt: string;
138
140
  pasteHere: string;
139
141
  selectAll: string;
140
142
  zoomToFit: string;
@@ -231,6 +233,10 @@ export interface CanvasContextMenuProps {
231
233
  selectedCount?: number;
232
234
  layerCandidates?: readonly CanvasLayerHitCandidate[];
233
235
  onSelectLayer?: (candidate: CanvasLayerHitCandidate) => void;
236
+ onRepromptLayer?: (
237
+ candidate: CanvasLayerHitCandidate,
238
+ details: CanvasContextMenuActionDetails,
239
+ ) => void;
234
240
  hasClipboard?: boolean;
235
241
  hasPropsClipboard?: boolean;
236
242
  hasAnimationClipboard?: boolean;
@@ -261,6 +267,7 @@ export interface CanvasContextMenuProps {
261
267
  canAddAutoLayout?: boolean;
262
268
  canSuggestAutoLayout?: boolean;
263
269
  canCreateComponent?: boolean;
270
+ canReprompt?: boolean;
264
271
  // Whether the current selection IS a component instance — gates the
265
272
  // whole Go to main component / Swap instance / Detach instance cluster on
266
273
  // (rather than showing them permanently disabled for non-instance
@@ -330,6 +337,7 @@ export interface CanvasContextMenuProps {
330
337
  onAddAutoLayout?: CanvasContextMenuActionHandler;
331
338
  onSuggestAutoLayout?: CanvasContextMenuActionHandler;
332
339
  onCreateComponent?: CanvasContextMenuActionHandler;
340
+ onReprompt?: CanvasContextMenuActionHandler;
333
341
  onGoToMainComponent?: CanvasContextMenuActionHandler;
334
342
  onSwapInstance?: CanvasContextMenuActionHandler;
335
343
  onDetachInstance?: CanvasContextMenuActionHandler;
@@ -361,6 +369,7 @@ export interface CanvasContextMenuProps {
361
369
 
362
370
  const DEFAULT_LABELS: CanvasContextMenuLabels = {
363
371
  selectLayer: "Select layer",
372
+ reprompt: "Regenerate…",
364
373
  pasteHere: "Paste here",
365
374
  selectAll: "Select all",
366
375
  zoomToFit: "Zoom to fit",
@@ -481,6 +490,7 @@ export const CanvasContextMenu = forwardRef<
481
490
  selectedCount = 0,
482
491
  layerCandidates = [],
483
492
  onSelectLayer,
493
+ onRepromptLayer,
484
494
  hasClipboard = false,
485
495
  hasPropsClipboard = false,
486
496
  hasAnimationClipboard = false,
@@ -500,6 +510,7 @@ export const CanvasContextMenu = forwardRef<
500
510
  canAddAutoLayout = selectedCount > 0,
501
511
  canSuggestAutoLayout = false,
502
512
  canCreateComponent = selectedCount > 0,
513
+ canReprompt = selectedCount > 0 || layerCandidates.length > 0,
503
514
  isComponentInstance = false,
504
515
  canGoToMainComponent = isComponentInstance,
505
516
  canSwapInstance = isComponentInstance,
@@ -540,6 +551,7 @@ export const CanvasContextMenu = forwardRef<
540
551
  onAddAutoLayout,
541
552
  onSuggestAutoLayout,
542
553
  onCreateComponent,
554
+ onReprompt,
543
555
  onGoToMainComponent,
544
556
  onSwapInstance,
545
557
  onDetachInstance,
@@ -624,6 +636,7 @@ export const CanvasContextMenu = forwardRef<
624
636
  "add-auto-layout": onAddAutoLayout,
625
637
  "suggest-auto-layout": onSuggestAutoLayout,
626
638
  "create-component": onCreateComponent,
639
+ reprompt: onReprompt,
627
640
  "go-to-main-component": onGoToMainComponent,
628
641
  "swap-instance": onSwapInstance,
629
642
  "detach-instance": onDetachInstance,
@@ -654,6 +667,7 @@ export const CanvasContextMenu = forwardRef<
654
667
  onCopyAsSvg,
655
668
  onCopyProps,
656
669
  onCreateComponent,
670
+ onReprompt,
657
671
  onDetachInstance,
658
672
  onFlipHorizontal,
659
673
  onFlipVertical,
@@ -772,6 +786,62 @@ export const CanvasContextMenu = forwardRef<
772
786
  <CanvasMenuSeparator />
773
787
  </>
774
788
  ) : null}
789
+ {canReprompt && (onReprompt || onRepromptLayer) ? (
790
+ <>
791
+ <ContextMenuGroup>
792
+ {layerCandidates.length > 1 && onRepromptLayer ? (
793
+ <ContextMenuSub>
794
+ <ContextMenuSubTrigger className={MENU_SUB_TRIGGER_CLASS}>
795
+ {labels.reprompt}
796
+ </ContextMenuSubTrigger>
797
+ <ContextMenuSubContent
798
+ className={cn(MENU_CONTENT_CLASS, "w-56")}
799
+ >
800
+ {layerCandidates.map((candidate) => (
801
+ <CanvasLayerCandidateItem
802
+ key={`reprompt:${candidate.key}`}
803
+ candidate={candidate}
804
+ onSelect={(event) => {
805
+ onRepromptLayer(candidate, {
806
+ action: "reprompt",
807
+ point,
808
+ selectedCount,
809
+ originalEvent: event,
810
+ });
811
+ handleOpenChange(false);
812
+ }}
813
+ />
814
+ ))}
815
+ </ContextMenuSubContent>
816
+ </ContextMenuSub>
817
+ ) : (
818
+ <CanvasMenuItem
819
+ hidden={isHiddenAction("reprompt")}
820
+ disabled={
821
+ !canReprompt ||
822
+ (!onReprompt &&
823
+ !(onRepromptLayer && layerCandidates.length === 1))
824
+ }
825
+ label={labels.reprompt}
826
+ onSelect={(event) => {
827
+ const candidate = layerCandidates[0];
828
+ if (!onReprompt && onRepromptLayer && candidate) {
829
+ onRepromptLayer(candidate, {
830
+ action: "reprompt",
831
+ point,
832
+ selectedCount,
833
+ originalEvent: event,
834
+ });
835
+ return;
836
+ }
837
+ runAction("reprompt", event);
838
+ }}
839
+ />
840
+ )}
841
+ </ContextMenuGroup>
842
+ <CanvasMenuSeparator />
843
+ </>
844
+ ) : null}
775
845
  {hasSelection ? (
776
846
  <>
777
847
  {/* LIVE-VERIFIED Figma "with selection" canvas menu. */}
@@ -1083,7 +1153,7 @@ function CanvasLayerCandidateItem({
1083
1153
  onSelect,
1084
1154
  }: {
1085
1155
  candidate: CanvasLayerHitCandidate;
1086
- onSelect: () => void;
1156
+ onSelect: (event: Event) => void;
1087
1157
  }) {
1088
1158
  const tag = candidate.info.tagName.toLowerCase();
1089
1159
  const Icon = candidate.info.componentName
@@ -24,6 +24,7 @@ import {
24
24
  type PenPath,
25
25
  type PenPoint,
26
26
  } from "@shared/pen-path";
27
+ import { sourceContentHash } from "@shared/source-workspace";
27
28
  import { IconPlugConnectedX, IconRefresh } from "@tabler/icons-react";
28
29
  import {
29
30
  useCallback,
@@ -43,6 +44,7 @@ import { Button } from "@/components/ui/button";
43
44
  import {
44
45
  DrawOverlay as SharedDrawOverlay,
45
46
  ReviewCanvasPins,
47
+ type RepromptDraftRequest,
46
48
  type ReviewFocusRequest,
47
49
  } from "@/components/visual-editor";
48
50
  import { sendToDesignAgentChatAndConfirm } from "@/lib/agent-chat";
@@ -556,6 +558,11 @@ interface DesignCanvasProps {
556
558
  previewFrameId?: string;
557
559
  /** Human-readable label for comment-pin prompts. */
558
560
  commentContextLabel?: string;
561
+ /** Opens an ephemeral, pre-anchored regenerate composer for this screen. */
562
+ repromptDraftRequest?: RepromptDraftRequest | null;
563
+ onRepromptDraftConsumed?: (nonce: number) => void;
564
+ /** Marks the one base canvas used for pending rewrite previews. */
565
+ nodeRewriteCanvasTarget?: boolean;
559
566
  /**
560
567
  * Called when a link inside the prototype points to another screen (a
561
568
  * relative href or `data-screen`). Lets the editor switch the active screen
@@ -1053,6 +1060,9 @@ export function DesignCanvas({
1053
1060
  commentContextId,
1054
1061
  screenId,
1055
1062
  previewFrameId,
1063
+ repromptDraftRequest,
1064
+ onRepromptDraftConsumed,
1065
+ nodeRewriteCanvasTarget = false,
1056
1066
  onPrototypeNavigate,
1057
1067
  motionTracks,
1058
1068
  motionDefaultEase,
@@ -4027,6 +4037,9 @@ export function DesignCanvas({
4027
4037
  const iframeElement = (
4028
4038
  <div
4029
4039
  data-review-canvas-id={reviewCanvasId}
4040
+ data-node-rewrite-canvas-target={
4041
+ nodeRewriteCanvasTarget ? "true" : undefined
4042
+ }
4030
4043
  className="design-canvas-iframe-wrapper relative inline-block ring-1 ring-border/60 shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_8px_24px_-12px_rgba(0,0,0,0.45)]"
4031
4044
  onDragEnter={handleWrapperDragEnter}
4032
4045
  onDragOver={handleWrapperDragOver}
@@ -4548,6 +4561,12 @@ export function DesignCanvas({
4548
4561
  onDispatchCommentToAgent={onDispatchCommentToAgent}
4549
4562
  onSendThreadToAgent={onSendThreadToAgent}
4550
4563
  sendingThreadId={reviewSendingThreadId}
4564
+ sourceType={
4565
+ sourceType ?? (externalPreviewUrl ? "localhost" : "inline")
4566
+ }
4567
+ sourceVersionHash={sourceContentHash(content)}
4568
+ repromptDraftRequest={repromptDraftRequest}
4569
+ onRepromptDraftConsumed={onRepromptDraftConsumed}
4551
4570
  />
4552
4571
  ) : null}
4553
4572
  </div>
@@ -398,6 +398,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
398
398
  hiddenScreenIds = EMPTY_SCREEN_IDS,
399
399
  lockedScreenIds = EMPTY_SCREEN_IDS,
400
400
  fullViewScreenIds,
401
+ pendingReviewScreenIds = EMPTY_SCREEN_IDS,
402
+ onReviewPendingScreen,
401
403
  interactMode = false,
402
404
  activeScreenHasHoveredChild = false,
403
405
  hoveredChildScreenId,
@@ -525,6 +527,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
525
527
  () => new Set(lockedScreenIds),
526
528
  [lockedScreenIds],
527
529
  );
530
+ const pendingReviewScreenIdSet = useMemo(
531
+ () => new Set(pendingReviewScreenIds),
532
+ [pendingReviewScreenIds],
533
+ );
528
534
  const renderedScreens = useMemo(
529
535
  () => screens.filter((screen) => !hiddenScreenIdSet.has(screen.id)),
530
536
  [hiddenScreenIdSet, screens],
@@ -7609,6 +7615,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7609
7615
  !isBreakpointSelectionTarget(screen)
7610
7616
  }
7611
7617
  showFullView={fullViewIdSet.has(screen.id)}
7618
+ pendingReview={pendingReviewScreenIdSet.has(screen.id)}
7619
+ onReviewPendingScreen={onReviewPendingScreen}
7612
7620
  interactMode={interactMode}
7613
7621
  isDirectlyHovered={screen.id === directlyHoveredScreenId}
7614
7622
  isFileDragOver={
@@ -8898,6 +8906,8 @@ interface ScreenProps {
8898
8906
  isSelected: boolean;
8899
8907
  isTopScreen: boolean;
8900
8908
  showFullView: boolean;
8909
+ pendingReview: boolean;
8910
+ onReviewPendingScreen?: (screenId: string) => void;
8901
8911
  interactMode: boolean;
8902
8912
  isDirectlyHovered: boolean;
8903
8913
  /** True while a native OS file drag is hovering this frame (Figma parity §1). */
@@ -8966,6 +8976,8 @@ const Screen = memo(function Screen({
8966
8976
  isSelected,
8967
8977
  isTopScreen,
8968
8978
  showFullView,
8979
+ pendingReview,
8980
+ onReviewPendingScreen,
8969
8981
  interactMode,
8970
8982
  isDirectlyHovered,
8971
8983
  isFileDragOver,
@@ -9174,6 +9186,27 @@ const Screen = memo(function Screen({
9174
9186
  >
9175
9187
  {display}
9176
9188
  </span>
9189
+ {pendingReview && onReviewPendingScreen ? (
9190
+ <button
9191
+ type="button"
9192
+ data-node-rewrite-review-badge
9193
+ className="flex h-5 shrink-0 items-center gap-1 rounded-full border border-border bg-background/95 px-1.5 !text-[9px] font-medium text-foreground shadow-sm hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
9194
+ title={t("designEditor.nodeRewrite.reviewCandidate")}
9195
+ aria-label={t("designEditor.nodeRewrite.reviewCandidate")}
9196
+ onClick={(event) => {
9197
+ event.preventDefault();
9198
+ event.stopPropagation();
9199
+ onReviewPendingScreen(screen.id);
9200
+ }}
9201
+ onMouseDown={(event) => {
9202
+ event.preventDefault();
9203
+ event.stopPropagation();
9204
+ }}
9205
+ >
9206
+ <span className="size-1.5 rounded-full bg-primary" />
9207
+ {t("designEditor.nodeRewrite.reviewCandidate")}
9208
+ </button>
9209
+ ) : null}
9177
9210
  {metadata.source === "fusion" ? (
9178
9211
  <span
9179
9212
  data-frame-source-badge="fusion"