@agent-native/core 0.114.12 → 0.114.15

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 (241) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/docs/content/deployment.mdx +1 -1
  4. package/corpus/core/docs/content/frames.mdx +1 -1
  5. package/corpus/core/docs/content/integrations.mdx +27 -27
  6. package/corpus/core/docs/content/pr-visual-recap.mdx +17 -13
  7. package/corpus/core/docs/content/workspace-management.mdx +1 -1
  8. package/corpus/core/package.json +2 -1
  9. package/corpus/core/src/agent/engine/builder-engine.ts +4 -0
  10. package/corpus/core/src/application-state/index.ts +3 -0
  11. package/corpus/core/src/application-state/script-helpers.ts +12 -1
  12. package/corpus/core/src/application-state/store.ts +216 -15
  13. package/corpus/core/src/client/AgentPanel.tsx +10 -4
  14. package/corpus/core/src/client/ConnectBuilderCard.tsx +5 -1
  15. package/corpus/core/src/client/chat/markdown-renderer.tsx +3 -5
  16. package/corpus/core/src/client/error-format.ts +2 -1
  17. package/corpus/core/src/client/index.ts +1 -0
  18. package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +2 -18
  19. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +43 -32
  20. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +4 -2
  21. package/corpus/core/src/client/settings/BackgroundAgentSection.tsx +5 -1
  22. package/corpus/core/src/client/settings/SettingsPanel.tsx +4 -4
  23. package/corpus/core/src/client/settings/useBuilderStatus.ts +3 -0
  24. package/corpus/core/src/db/client.ts +44 -1
  25. package/corpus/core/src/localization/default-messages.ts +1 -7
  26. package/corpus/core/src/server/builder-browser.ts +4 -0
  27. package/corpus/core/src/server/builder-design-systems.ts +6 -1
  28. package/corpus/core/src/server/fusion-app.ts +5 -1
  29. package/corpus/core/src/shared/builder-link-tracking.ts +41 -0
  30. package/corpus/core/src/shared/index.ts +1 -0
  31. package/corpus/templates/analytics/AGENTS.md +10 -1
  32. package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +10 -2
  33. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +5 -4
  34. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +20 -32
  35. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +62 -0
  36. package/corpus/templates/analytics/changelog/2026-07-21-chart-series-controls-stay-open-while-moving-from-the-legend.md +6 -0
  37. package/corpus/templates/analytics/changelog/2026-07-21-dashboard-email-captures-complete-large-dashboards-in-chunks.md +6 -0
  38. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +4 -4
  39. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -8
  40. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +39 -7
  41. package/corpus/templates/analytics/server/lib/dashboard-report.ts +341 -251
  42. package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +114 -0
  43. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +84 -6
  44. package/corpus/templates/analytics/server/plugins/db.ts +13 -0
  45. package/corpus/templates/clips/app/components/player/video-player.tsx +10 -4
  46. package/corpus/templates/clips/changelog/2026-07-21-choose-which-whisper-model-to-use-in-settings.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-07-21-fixed-organization-recording-visibility-default.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-21-live-dictation-text-now-appears-above-the-desktop-recording-.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-21-meeting-notes-stop-when-calls-end.md +6 -0
  50. package/corpus/templates/clips/desktop/src/app.tsx +66 -75
  51. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +2 -0
  52. package/corpus/templates/clips/desktop/src/hooks/useWhisperSettings.ts +172 -0
  53. package/corpus/templates/clips/desktop/src/lib/meeting-call-app.ts +41 -0
  54. package/corpus/templates/clips/desktop/src/lib/silence-events.ts +1 -0
  55. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +14 -3
  56. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +25 -0
  57. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  58. package/corpus/templates/clips/desktop/src/styles.css +100 -7
  59. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +3 -4
  60. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +32 -0
  61. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  62. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +11 -4
  63. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +268 -20
  64. package/corpus/templates/clips/desktop/src-tauri/src/whisper_model.rs +285 -164
  65. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +18 -7
  66. package/corpus/templates/clips/learnings.defaults.md +1 -1
  67. package/corpus/templates/clips/server/plugins/db.ts +11 -0
  68. package/corpus/templates/clips/shared/builder-credits.ts +1 -1
  69. package/corpus/templates/design/AGENTS.md +7 -3
  70. package/corpus/templates/design/actions/begin-node-rewrite-request.ts +78 -0
  71. package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +17 -7
  72. package/corpus/templates/design/actions/propose-node-rewrite.ts +26 -18
  73. package/corpus/templates/design/actions/resolve-node-rewrite.ts +67 -19
  74. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +9 -1
  75. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +5 -1
  76. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +1 -4
  77. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +2 -6
  78. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +9 -1
  79. package/corpus/templates/design/shared/node-rewrite.ts +32 -0
  80. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +6 -3
  81. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -18
  82. package/corpus/templates/forms/app/pages/FormsListPage.tsx +9 -5
  83. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +9 -1
  84. package/corpus/templates/slides/app/context/DeckContext.tsx +18 -0
  85. package/corpus/templates/slides/changelog/2026-07-21-decks-list-now-refreshes-when-you-switch-organizations.md +6 -0
  86. package/corpus/templates/tasks/app/components/custom-fields/FieldCreateBar.tsx +13 -19
  87. package/corpus/templates/tasks/app/components/custom-fields/FieldListRow.tsx +27 -22
  88. package/corpus/templates/tasks/app/components/custom-fields/FieldsList.tsx +26 -22
  89. package/corpus/templates/tasks/app/components/custom-fields/FieldsListSkeleton.tsx +4 -1
  90. package/corpus/templates/tasks/app/components/custom-fields/FieldsPage.tsx +4 -2
  91. package/corpus/templates/tasks/app/components/custom-fields/editor/FieldEditorSidebar.tsx +12 -3
  92. package/corpus/templates/tasks/app/components/custom-fields/editor/FieldTitleSection.tsx +6 -3
  93. package/corpus/templates/tasks/app/components/custom-fields/editor/config/CurrencyConfigControl.tsx +5 -2
  94. package/corpus/templates/tasks/app/components/custom-fields/editor/config/NumberConfigControl.tsx +5 -2
  95. package/corpus/templates/tasks/app/components/custom-fields/editor/config/PercentConfigControl.tsx +4 -1
  96. package/corpus/templates/tasks/app/components/custom-fields/editor/config/SelectConfigControl.tsx +17 -7
  97. package/corpus/templates/tasks/app/components/custom-fields/editor/config/select-colors.ts +37 -9
  98. package/corpus/templates/tasks/app/components/custom-fields/field-types.ts +23 -0
  99. package/corpus/templates/tasks/app/components/inbox/InboxList.tsx +24 -16
  100. package/corpus/templates/tasks/app/components/inbox/InboxListPage.tsx +5 -3
  101. package/corpus/templates/tasks/app/components/inbox/InboxListRow.tsx +7 -5
  102. package/corpus/templates/tasks/app/components/layout/Header.tsx +10 -10
  103. package/corpus/templates/tasks/app/components/layout/Layout.tsx +11 -7
  104. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +17 -10
  105. package/corpus/templates/tasks/app/components/shared/AddListItemInput.tsx +14 -8
  106. package/corpus/templates/tasks/app/components/shared/AlertDialog.tsx +6 -2
  107. package/corpus/templates/tasks/app/components/shared/BulkDeleteDialog.tsx +13 -6
  108. package/corpus/templates/tasks/app/components/shared/ChipSelect.tsx +12 -5
  109. package/corpus/templates/tasks/app/components/shared/DeleteItemDialog.tsx +6 -6
  110. package/corpus/templates/tasks/app/components/shared/ListViewHeader.tsx +3 -1
  111. package/corpus/templates/tasks/app/components/shared/SidePanel.tsx +5 -2
  112. package/corpus/templates/tasks/app/components/shared/list/ListRowDragHandle.tsx +3 -1
  113. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionBar.tsx +46 -20
  114. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionHeaderToggle.tsx +3 -1
  115. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionToolbar.tsx +10 -6
  116. package/corpus/templates/tasks/app/components/tasks/TaskList.tsx +24 -13
  117. package/corpus/templates/tasks/app/components/tasks/TaskListHeaderRow.tsx +4 -1
  118. package/corpus/templates/tasks/app/components/tasks/TaskListPage.tsx +5 -3
  119. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +15 -6
  120. package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +11 -6
  121. package/corpus/templates/tasks/app/components/tasks/fields/TaskTitleSection.tsx +8 -3
  122. package/corpus/templates/tasks/app/components/tasks/fields/controls/RichTextValueControl.tsx +12 -6
  123. package/corpus/templates/tasks/app/components/tasks/fields/controls/SingleSelectValueControl.tsx +6 -1
  124. package/corpus/templates/tasks/app/i18n/en-US.ts +209 -0
  125. package/corpus/templates/tasks/app/i18n/index.ts +34 -0
  126. package/corpus/templates/tasks/app/root.tsx +34 -2
  127. package/corpus/templates/tasks/app/routes/extensions.$id.tsx +2 -1
  128. package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -1
  129. package/corpus/templates/tasks/app/routes/fields.tsx +2 -1
  130. package/corpus/templates/tasks/app/routes/inbox.tsx +2 -1
  131. package/corpus/templates/tasks/app/routes/tasks.tsx +2 -1
  132. package/corpus/templates/tasks/app/routes/team.tsx +6 -3
  133. package/corpus/templates/tasks/changelog/2026-07-20-tasks-is-now-available-in-10-additional-languages.md +6 -0
  134. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  135. package/dist/agent/engine/builder-engine.js +4 -0
  136. package/dist/agent/engine/builder-engine.js.map +1 -1
  137. package/dist/application-state/index.d.ts +2 -2
  138. package/dist/application-state/index.d.ts.map +1 -1
  139. package/dist/application-state/index.js +2 -2
  140. package/dist/application-state/index.js.map +1 -1
  141. package/dist/application-state/script-helpers.d.ts +3 -1
  142. package/dist/application-state/script-helpers.d.ts.map +1 -1
  143. package/dist/application-state/script-helpers.js +7 -1
  144. package/dist/application-state/script-helpers.js.map +1 -1
  145. package/dist/application-state/store.d.ts +7 -1
  146. package/dist/application-state/store.d.ts.map +1 -1
  147. package/dist/application-state/store.js +143 -17
  148. package/dist/application-state/store.js.map +1 -1
  149. package/dist/client/AgentPanel.d.ts.map +1 -1
  150. package/dist/client/AgentPanel.js +10 -3
  151. package/dist/client/AgentPanel.js.map +1 -1
  152. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  153. package/dist/client/ConnectBuilderCard.js +5 -1
  154. package/dist/client/ConnectBuilderCard.js.map +1 -1
  155. package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
  156. package/dist/client/chat/markdown-renderer.js +3 -2
  157. package/dist/client/chat/markdown-renderer.js.map +1 -1
  158. package/dist/client/error-format.d.ts +1 -1
  159. package/dist/client/error-format.d.ts.map +1 -1
  160. package/dist/client/error-format.js +1 -1
  161. package/dist/client/error-format.js.map +1 -1
  162. package/dist/client/index.d.ts +1 -0
  163. package/dist/client/index.d.ts.map +1 -1
  164. package/dist/client/index.js +1 -0
  165. package/dist/client/index.js.map +1 -1
  166. package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
  167. package/dist/client/integrations/IntegrationsPanel.js +3 -4
  168. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  169. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  170. package/dist/client/resources/McpIntegrationDialog.js +21 -7
  171. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  172. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  173. package/dist/client/resources/mcp-integration-catalog.js +2 -2
  174. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  175. package/dist/client/settings/BackgroundAgentSection.d.ts.map +1 -1
  176. package/dist/client/settings/BackgroundAgentSection.js +5 -1
  177. package/dist/client/settings/BackgroundAgentSection.js.map +1 -1
  178. package/dist/client/settings/SettingsPanel.js +1 -1
  179. package/dist/client/settings/SettingsPanel.js.map +1 -1
  180. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  181. package/dist/client/settings/useBuilderStatus.js +2 -0
  182. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  183. package/dist/collab/routes.d.ts +1 -1
  184. package/dist/db/client.d.ts +7 -0
  185. package/dist/db/client.d.ts.map +1 -1
  186. package/dist/db/client.js +39 -1
  187. package/dist/db/client.js.map +1 -1
  188. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  189. package/dist/localization/default-messages.d.ts +0 -6
  190. package/dist/localization/default-messages.d.ts.map +1 -1
  191. package/dist/localization/default-messages.js +1 -7
  192. package/dist/localization/default-messages.js.map +1 -1
  193. package/dist/notifications/routes.d.ts +2 -2
  194. package/dist/observability/routes.d.ts +3 -3
  195. package/dist/resources/handlers.d.ts +1 -1
  196. package/dist/server/builder-browser.d.ts.map +1 -1
  197. package/dist/server/builder-browser.js +4 -0
  198. package/dist/server/builder-browser.js.map +1 -1
  199. package/dist/server/builder-design-systems.d.ts.map +1 -1
  200. package/dist/server/builder-design-systems.js +6 -1
  201. package/dist/server/builder-design-systems.js.map +1 -1
  202. package/dist/server/fusion-app.d.ts.map +1 -1
  203. package/dist/server/fusion-app.js +2 -1
  204. package/dist/server/fusion-app.js.map +1 -1
  205. package/dist/server/transcribe-voice.d.ts +1 -1
  206. package/dist/shared/builder-link-tracking.d.ts +14 -0
  207. package/dist/shared/builder-link-tracking.d.ts.map +1 -0
  208. package/dist/shared/builder-link-tracking.js +26 -0
  209. package/dist/shared/builder-link-tracking.js.map +1 -0
  210. package/dist/shared/index.d.ts +1 -0
  211. package/dist/shared/index.d.ts.map +1 -1
  212. package/dist/shared/index.js +1 -0
  213. package/dist/shared/index.js.map +1 -1
  214. package/docs/content/deployment.mdx +1 -1
  215. package/docs/content/frames.mdx +1 -1
  216. package/docs/content/integrations.mdx +27 -27
  217. package/docs/content/pr-visual-recap.mdx +17 -13
  218. package/docs/content/workspace-management.mdx +1 -1
  219. package/package.json +3 -2
  220. package/src/agent/engine/builder-engine.ts +4 -0
  221. package/src/application-state/index.ts +3 -0
  222. package/src/application-state/script-helpers.ts +12 -1
  223. package/src/application-state/store.ts +216 -15
  224. package/src/client/AgentPanel.tsx +10 -4
  225. package/src/client/ConnectBuilderCard.tsx +5 -1
  226. package/src/client/chat/markdown-renderer.tsx +3 -5
  227. package/src/client/error-format.ts +2 -1
  228. package/src/client/index.ts +1 -0
  229. package/src/client/integrations/IntegrationsPanel.tsx +2 -18
  230. package/src/client/resources/McpIntegrationDialog.tsx +43 -32
  231. package/src/client/resources/mcp-integration-catalog.ts +4 -2
  232. package/src/client/settings/BackgroundAgentSection.tsx +5 -1
  233. package/src/client/settings/SettingsPanel.tsx +4 -4
  234. package/src/client/settings/useBuilderStatus.ts +3 -0
  235. package/src/db/client.ts +44 -1
  236. package/src/localization/default-messages.ts +1 -7
  237. package/src/server/builder-browser.ts +4 -0
  238. package/src/server/builder-design-systems.ts +6 -1
  239. package/src/server/fusion-app.ts +5 -1
  240. package/src/shared/builder-link-tracking.ts +41 -0
  241. package/src/shared/index.ts +1 -0
@@ -288,11 +288,15 @@ ladder.
288
288
  `view-screen`; not rendered as canvas overlays).
289
289
  - `design-reprompt-pending:<designId>:<fileId>` is the client-captured source
290
290
  selection, instruction, base hash, and authoritative current request id for
291
- a scoped regenerate request.
291
+ a scoped regenerate request. The frontend starts requests through the
292
+ compare-and-set `begin-node-rewrite-request` action and must not overwrite a
293
+ `resolving` acceptance reservation.
292
294
  - `design-reprompt-proposal:<designId>:<fileId>:<repromptId>` is one
293
295
  request-specific preview-only subtree proposal. Candidate payloads have a
294
- 256 KiB aggregate serialized limit. Resolution and cancellation use atomic
295
- compare-and-set cleanup so an older request cannot erase a newer one.
296
+ 256 KiB aggregate serialized limit. Proposal publication, resolution, and
297
+ cancellation use atomic multi-key compare-and-set transitions. Acceptance
298
+ reserves its matching pending request before writing design content, so a
299
+ newer request and an older acceptance cannot both win.
296
300
  `view-screen` lists only proposals paired to the current pending request as
297
301
  `pendingCandidateReviews`.
298
302
  - `show-design-questions` opens focused pre-generation questions in the main
@@ -0,0 +1,78 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ compareAndSetAppState,
4
+ readAppState,
5
+ } from "@agent-native/core/application-state";
6
+ import { assertAccess } from "@agent-native/core/sharing";
7
+ import { z } from "zod";
8
+
9
+ import {
10
+ designRepromptPendingStateKey,
11
+ isNodeRewriteResolutionClaim,
12
+ isPendingDesignReprompt,
13
+ type PendingDesignReprompt,
14
+ } from "../shared/node-rewrite.js";
15
+
16
+ const targetSchema = z
17
+ .object({
18
+ nodeId: z.string().min(1).optional(),
19
+ selector: z.string().min(1).optional(),
20
+ })
21
+ .refine((target) => target.nodeId || target.selector, {
22
+ message: "target.nodeId or target.selector is required",
23
+ });
24
+
25
+ export default defineAction({
26
+ agentTool: false,
27
+ description:
28
+ "Atomically starts or supersedes one pending node rewrite unless its current proposal is being accepted.",
29
+ schema: z.object({
30
+ repromptId: z.string().min(1),
31
+ designId: z.string().min(1),
32
+ fileId: z.string().min(1),
33
+ target: targetSchema,
34
+ baseVersionHash: z.string().min(1),
35
+ instruction: z.string().trim().min(1),
36
+ createdAt: z.string().min(1),
37
+ priorProposalId: z.string().min(1).optional(),
38
+ priorRepromptId: z.string().min(1).optional(),
39
+ }),
40
+ run: async (request) => {
41
+ await assertAccess("design", request.designId, "editor");
42
+ const pendingKey = designRepromptPendingStateKey(
43
+ request.designId,
44
+ request.fileId,
45
+ );
46
+ const current = await readAppState(pendingKey);
47
+ if (isNodeRewriteResolutionClaim(current)) {
48
+ throw new Error(
49
+ "This candidate is currently being accepted. Wait for it to finish before regenerating.",
50
+ );
51
+ }
52
+ if (
53
+ request.priorRepromptId &&
54
+ (!isPendingDesignReprompt(current) ||
55
+ current.repromptId !== request.priorRepromptId)
56
+ ) {
57
+ throw new Error(
58
+ "The candidates changed before refinement started. Review the latest candidates instead.",
59
+ );
60
+ }
61
+
62
+ const pending: PendingDesignReprompt = {
63
+ ...request,
64
+ status: "pending",
65
+ };
66
+ const started = await compareAndSetAppState(
67
+ pendingKey,
68
+ current,
69
+ pending as unknown as Record<string, unknown>,
70
+ );
71
+ if (!started) {
72
+ throw new Error(
73
+ "The regeneration request changed concurrently. Try again with the latest candidates.",
74
+ );
75
+ }
76
+ return { started: true, repromptId: request.repromptId };
77
+ },
78
+ });
@@ -1,6 +1,7 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import {
3
3
  compareAndSetAppState,
4
+ compareAndSetManyAppState,
4
5
  readAppState,
5
6
  } from "@agent-native/core/application-state";
6
7
  import { assertAccess } from "@agent-native/core/sharing";
@@ -39,15 +40,24 @@ export default defineAction({
39
40
  repromptId,
40
41
  );
41
42
  const proposal = await readAppState(proposalKey);
42
- const [pendingCancelled, proposalCancelled] = await Promise.all([
43
- compareAndSetAppState(pendingKey, pending, null),
44
- isNodeRewriteProposal(proposal)
45
- ? compareAndSetAppState(proposalKey, proposal, null)
46
- : Promise.resolve(false),
47
- ]);
43
+ const proposalCancelled = isNodeRewriteProposal(proposal);
44
+ const pendingCancelled = proposalCancelled
45
+ ? await compareAndSetManyAppState([
46
+ {
47
+ key: pendingKey,
48
+ expectedValue: pending,
49
+ nextValue: null,
50
+ },
51
+ {
52
+ key: proposalKey,
53
+ expectedValue: proposal,
54
+ nextValue: null,
55
+ },
56
+ ])
57
+ : await compareAndSetAppState(pendingKey, pending, null);
48
58
  return {
49
59
  cancelled: pendingCancelled,
50
- proposalCancelled,
60
+ proposalCancelled: proposalCancelled && pendingCancelled,
51
61
  superseded: !pendingCancelled,
52
62
  };
53
63
  },
@@ -1,8 +1,8 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import {
3
- compareAndSetAppState,
3
+ compareAndSetManyAppState,
4
4
  readAppState,
5
- writeAppState,
5
+ type AppStateCompareAndSetOperation,
6
6
  } from "@agent-native/core/application-state";
7
7
  import { accessFilter, assertAccess } from "@agent-native/core/sharing";
8
8
  import { and, eq } from "drizzle-orm";
@@ -68,6 +68,7 @@ const variantSchema = z.object({
68
68
  });
69
69
 
70
70
  const pendingRepromptSchema = z.object({
71
+ status: z.literal("pending").optional(),
71
72
  repromptId: z.string().min(1),
72
73
  designId: z.string().min(1),
73
74
  fileId: z.string().min(1),
@@ -241,21 +242,18 @@ export default defineAction({
241
242
  file.id,
242
243
  repromptId,
243
244
  );
244
- await writeAppState(
245
- proposalKey,
246
- proposal as unknown as Record<string, unknown>,
247
- );
248
- const currentPending = await readAppState(pendingKey);
249
- if (currentPending?.repromptId !== repromptId) {
250
- await compareAndSetAppState(
251
- proposalKey,
252
- proposal as unknown as Record<string, unknown>,
253
- null,
254
- );
255
- throw new Error(
256
- "This regeneration was superseded by a newer request before its candidates were published.",
257
- );
258
- }
245
+ const publishOperations: AppStateCompareAndSetOperation[] = [
246
+ {
247
+ key: pendingKey,
248
+ expectedValue: pending as unknown as Record<string, unknown>,
249
+ nextValue: pending as unknown as Record<string, unknown>,
250
+ },
251
+ {
252
+ key: proposalKey,
253
+ expectedValue: null,
254
+ nextValue: proposal as unknown as Record<string, unknown>,
255
+ },
256
+ ];
259
257
  if (pending.priorProposalId && pending.priorRepromptId) {
260
258
  const priorProposalKey = designRepromptProposalStateKey(
261
259
  file.designId,
@@ -264,9 +262,19 @@ export default defineAction({
264
262
  );
265
263
  const priorProposal = await readAppState(priorProposalKey);
266
264
  if (priorProposal?.proposalId === pending.priorProposalId) {
267
- await compareAndSetAppState(priorProposalKey, priorProposal, null);
265
+ publishOperations.push({
266
+ key: priorProposalKey,
267
+ expectedValue: priorProposal,
268
+ nextValue: null,
269
+ });
268
270
  }
269
271
  }
272
+ const published = await compareAndSetManyAppState(publishOperations);
273
+ if (!published) {
274
+ throw new Error(
275
+ "This regeneration was superseded by a newer request before its candidates were published.",
276
+ );
277
+ }
270
278
 
271
279
  const bridgeMessages: NodeHtmlPreviewBridgeMessage[] = [
272
280
  {
@@ -1,11 +1,13 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import {
3
3
  compareAndSetAppState,
4
+ compareAndSetManyAppState,
4
5
  listAppState,
5
6
  readAppState,
6
7
  } from "@agent-native/core/application-state";
7
8
  import { accessFilter, assertAccess } from "@agent-native/core/sharing";
8
9
  import { and, eq } from "drizzle-orm";
10
+ import { nanoid } from "nanoid";
9
11
  import { z } from "zod";
10
12
 
11
13
  import { getDb, schema } from "../server/db/index.js";
@@ -22,6 +24,7 @@ import {
22
24
  isPendingDesignReprompt,
23
25
  spliceNodeRewriteVariant,
24
26
  type NodeHtmlPreviewBridgeMessage,
27
+ type NodeRewriteResolutionClaim,
25
28
  type NodeRewriteProposal,
26
29
  } from "../shared/node-rewrite.js";
27
30
  import { designSourceTypeFromData } from "../shared/source-mode.js";
@@ -142,22 +145,19 @@ async function clearProposalState(
142
145
  proposalKey: string,
143
146
  proposal: NodeRewriteProposal,
144
147
  pending: Record<string, unknown>,
145
- ): Promise<void> {
148
+ ): Promise<boolean> {
146
149
  const pendingKey = designRepromptPendingStateKey(
147
150
  proposal.designId,
148
151
  proposal.fileId,
149
152
  );
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),
153
+ return compareAndSetManyAppState([
154
+ {
155
+ key: proposalKey,
156
+ expectedValue: proposal as unknown as Record<string, unknown>,
157
+ nextValue: null,
158
+ },
159
+ { key: pendingKey, expectedValue: pending, nextValue: null },
157
160
  ]);
158
- if (!proposalCleared) {
159
- throw new Error("Node rewrite proposal changed while it was resolving.");
160
- }
161
161
  }
162
162
 
163
163
  export default defineAction({
@@ -195,7 +195,16 @@ export default defineAction({
195
195
 
196
196
  if (resolution === "reject") {
197
197
  await assertAccess("design", proposal.designId, "editor");
198
- await clearProposalState(proposalKey, proposal, pending);
198
+ const cleanupComplete = await clearProposalState(
199
+ proposalKey,
200
+ proposal,
201
+ pending,
202
+ );
203
+ if (!cleanupComplete) {
204
+ throw new Error(
205
+ "The proposal changed while it was being rejected. Review the latest candidates instead.",
206
+ );
207
+ }
199
208
  return {
200
209
  proposalId,
201
210
  repromptId: proposal.repromptId,
@@ -234,13 +243,51 @@ export default defineAction({
234
243
  source,
235
244
  fileType: file.fileType,
236
245
  });
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);
246
+ const claim: NodeRewriteResolutionClaim = {
247
+ ...pending,
248
+ status: "resolving",
249
+ claimId: `node-rewrite-claim-${nanoid()}`,
250
+ proposalId,
251
+ resolution: "accept",
252
+ };
253
+ const claimValue = claim as unknown as Record<string, unknown>;
254
+ const claimed = await compareAndSetAppState(
255
+ pendingKey,
256
+ pending,
257
+ claimValue,
258
+ );
259
+ if (!claimed) {
260
+ throw new Error(
261
+ "A newer regeneration request replaced this proposal before it could be accepted.",
262
+ );
263
+ }
264
+
265
+ let write;
266
+ try {
267
+ write = await writeInlineSourceFile({
268
+ designId: file.designId,
269
+ file,
270
+ content: rewrite.content,
271
+ expectedVersionHash: proposal.baseVersionHash,
272
+ });
273
+ } catch (error) {
274
+ const released = await compareAndSetAppState(
275
+ pendingKey,
276
+ claimValue,
277
+ pending,
278
+ );
279
+ if (!released) {
280
+ throw new Error(
281
+ "The design write failed and its regeneration reservation could not be restored.",
282
+ );
283
+ }
284
+ throw error;
285
+ }
286
+ const cleanupComplete = await clearProposalState(
287
+ proposalKey,
288
+ proposal,
289
+ claimValue,
290
+ );
244
291
 
245
292
  return {
246
293
  proposalId,
@@ -252,6 +299,7 @@ export default defineAction({
252
299
  designId: file.designId,
253
300
  fileId: file.id,
254
301
  versionHash: write.versionHash,
302
+ cleanupComplete,
255
303
  };
256
304
  },
257
305
  });
@@ -2,6 +2,7 @@ import {
2
2
  useActionMutation,
3
3
  useActionQuery,
4
4
  } from "@agent-native/core/client/hooks";
5
+ import { withBuilderUtmTrackingParams } from "@agent-native/core/shared";
5
6
  import {
6
7
  IconAlertTriangle,
7
8
  IconApps,
@@ -315,7 +316,14 @@ export function FusionAppBanner({
315
316
  </DropdownMenuTrigger>
316
317
  <DropdownMenuContent align="end">
317
318
  <DropdownMenuItem asChild className="cursor-pointer">
318
- <a href={editorUrl} target="_blank" rel="noreferrer">
319
+ <a
320
+ href={withBuilderUtmTrackingParams(editorUrl, {
321
+ campaign: "product",
322
+ content: "fusion_editor",
323
+ })}
324
+ target="_blank"
325
+ rel="noreferrer"
326
+ >
319
327
  <IconExternalLink className="me-2 size-3.5" />
320
328
  {
321
329
  "Open in Builder" /* i18n-ignore fusion app banner, flag-gated feature */
@@ -3,6 +3,7 @@ import {
3
3
  useActionQuery,
4
4
  } from "@agent-native/core/client/hooks";
5
5
  import { useT } from "@agent-native/core/client/i18n";
6
+ import { withBuilderUtmTrackingParams } from "@agent-native/core/shared";
6
7
  import { propNameToDataAttribute } from "@shared/component-model";
7
8
  import {
8
9
  IconArrowRight,
@@ -168,7 +169,10 @@ function MakeItRealCard({
168
169
  `Generating ${migrateResult.branchName ?? "React app"}.`}
169
170
  </p>
170
171
  <a
171
- href={migrateResult.url}
172
+ href={withBuilderUtmTrackingParams(migrateResult.url, {
173
+ campaign: "product",
174
+ content: "design_migration",
175
+ })}
172
176
  target="_blank"
173
177
  rel="noopener noreferrer"
174
178
  className="inline-flex h-6 shrink-0 items-center gap-1 rounded-md px-1.5 text-[10px] font-semibold text-[var(--design-editor-accent-color)] hover:bg-[var(--design-editor-panel-raised-bg)]"
@@ -1,12 +1,10 @@
1
1
  import {
2
2
  callAction,
3
- setClientAppState,
4
3
  useActionMutation,
5
4
  useChangeVersion,
6
5
  } from "@agent-native/core/client/hooks";
7
6
  import { useT } from "@agent-native/core/client/i18n";
8
7
  import {
9
- designRepromptPendingStateKey,
10
8
  isNodeRewriteProposal,
11
9
  type NodeHtmlPreviewBridgeMessage,
12
10
  type NodeRewriteProposal,
@@ -426,7 +424,6 @@ export function NodeRewriteProposal({
426
424
  if (!proposal || !instruction || refining || resolveMutation.isPending)
427
425
  return;
428
426
  const repromptId = crypto.randomUUID();
429
- const pendingKey = designRepromptPendingStateKey(designId, fileId);
430
427
  const pending = {
431
428
  repromptId,
432
429
  designId,
@@ -440,7 +437,7 @@ export function NodeRewriteProposal({
440
437
  };
441
438
  setRefining(true);
442
439
  try {
443
- await setClientAppState(pendingKey, pending);
440
+ await callAction("begin-node-rewrite-request", pending);
444
441
  const submission = formatNodeRepromptSubmission({
445
442
  ...pending,
446
443
  priorProposalId: proposal.proposalId,
@@ -10,10 +10,7 @@ import {
10
10
  type ReviewThread,
11
11
  } from "@agent-native/core/client/review";
12
12
  import type { ReviewComment } from "@agent-native/core/review";
13
- import {
14
- designRepromptPendingStateKey,
15
- type NodeRewriteTarget,
16
- } from "@shared/node-rewrite";
13
+ import type { NodeRewriteTarget } from "@shared/node-rewrite";
17
14
  import {
18
15
  IconArrowUp,
19
16
  IconChevronDown,
@@ -874,10 +871,9 @@ export function ReviewCanvasPins({
874
871
  element = null;
875
872
  }
876
873
  }
877
- const stateKey = designRepromptPendingStateKey(resourceId, targetId);
878
874
  setAgentSubmitting(true);
879
875
  try {
880
- await setClientAppState(stateKey, pending);
876
+ await callAction("begin-node-rewrite-request", pending);
881
877
  const submission = formatNodeRepromptSubmission({
882
878
  ...pending,
883
879
  subtreeHtml:
@@ -2,6 +2,7 @@ import { appApiPath } from "@agent-native/core/client/api-path";
2
2
  import { useActionQuery } from "@agent-native/core/client/hooks";
3
3
  import { useT } from "@agent-native/core/client/i18n";
4
4
  import { openAgentSidebar } from "@agent-native/core/client/navigation";
5
+ import { withBuilderUtmTrackingParams } from "@agent-native/core/shared";
5
6
  import {
6
7
  useSetPageTitle,
7
8
  useSetHeaderActions,
@@ -1128,7 +1129,14 @@ function BuilderIndexPreview({
1128
1129
  <div className="flex items-center gap-2 border-t border-border pt-4">
1129
1130
  {result.builderUrl ? (
1130
1131
  <Button asChild className="cursor-pointer">
1131
- <a href={result.builderUrl} target="_blank" rel="noreferrer">
1132
+ <a
1133
+ href={withBuilderUtmTrackingParams(result.builderUrl, {
1134
+ campaign: "product",
1135
+ content: "design_system_intelligence",
1136
+ })}
1137
+ target="_blank"
1138
+ rel="noreferrer"
1139
+ >
1132
1140
  <IconExternalLink className="size-4" />
1133
1141
  {"Open in Builder" /* i18n-ignore Builder link action */}
1134
1142
  </a>
@@ -15,6 +15,7 @@ export const MAX_NODE_REWRITE_PROPOSAL_BYTES = 256 * 1024;
15
15
  export type NodeRewriteTarget = EditIntentTarget;
16
16
 
17
17
  export interface PendingDesignReprompt {
18
+ status?: "pending";
18
19
  repromptId: string;
19
20
  designId: string;
20
21
  fileId: string;
@@ -26,6 +27,16 @@ export interface PendingDesignReprompt {
26
27
  priorRepromptId?: string;
27
28
  }
28
29
 
30
+ export interface NodeRewriteResolutionClaim extends Omit<
31
+ PendingDesignReprompt,
32
+ "status"
33
+ > {
34
+ status: "resolving";
35
+ claimId: string;
36
+ proposalId: string;
37
+ resolution: "accept";
38
+ }
39
+
29
40
  export interface NodeRewriteVariant {
30
41
  html: string;
31
42
  summary: string;
@@ -69,6 +80,7 @@ export function isPendingDesignReprompt(
69
80
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
70
81
  const pending = value as Partial<PendingDesignReprompt>;
71
82
  return (
83
+ (pending.status === undefined || pending.status === "pending") &&
72
84
  typeof pending.repromptId === "string" &&
73
85
  typeof pending.designId === "string" &&
74
86
  typeof pending.fileId === "string" &&
@@ -79,6 +91,26 @@ export function isPendingDesignReprompt(
79
91
  );
80
92
  }
81
93
 
94
+ export function isNodeRewriteResolutionClaim(
95
+ value: unknown,
96
+ ): value is NodeRewriteResolutionClaim {
97
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
98
+ const claim = value as Partial<NodeRewriteResolutionClaim>;
99
+ return (
100
+ claim.status === "resolving" &&
101
+ typeof claim.claimId === "string" &&
102
+ typeof claim.proposalId === "string" &&
103
+ claim.resolution === "accept" &&
104
+ typeof claim.repromptId === "string" &&
105
+ typeof claim.designId === "string" &&
106
+ typeof claim.fileId === "string" &&
107
+ typeof claim.baseVersionHash === "string" &&
108
+ typeof claim.instruction === "string" &&
109
+ typeof claim.createdAt === "string" &&
110
+ Boolean(claim.target)
111
+ );
112
+ }
113
+
82
114
  export interface NodeHtmlPreviewBridgeMessage {
83
115
  type: "node-html-preview";
84
116
  proposalId: string;
@@ -12,6 +12,7 @@ import { FeedbackButton } from "@agent-native/core/client/ui";
12
12
  import {
13
13
  IconArrowUp,
14
14
  IconPlus,
15
+ IconLoader2,
15
16
  IconMenu2,
16
17
  IconX,
17
18
  IconMessageCircle,
@@ -88,11 +89,9 @@ export function Sidebar() {
88
89
 
89
90
  function handleSkip() {
90
91
  setPopoverOpen(false);
91
- const tempId = crypto.randomUUID().replace(/-/g, "").slice(0, 10);
92
- navigate(`/forms/${tempId}`);
93
92
  createForm.mutate(
94
93
  { title: t("sidebar.untitledForm") },
95
- { onSuccess: (form) => navigate(`/forms/${form.id}`, { replace: true }) },
94
+ { onSuccess: (form) => navigate(`/forms/${form.id}`) },
96
95
  );
97
96
  }
98
97
 
@@ -171,7 +170,11 @@ export function Sidebar() {
171
170
  size="sm"
172
171
  className="min-h-10 px-2 text-xs text-muted-foreground active:scale-[0.96] transition-[background-color,color,transform]"
173
172
  onClick={handleSkip}
173
+ disabled={createForm.isPending}
174
174
  >
175
+ {createForm.isPending && (
176
+ <IconLoader2 className="h-3 w-3 animate-spin" />
177
+ )}
175
178
  {t("sidebar.skipPrompt")}
176
179
  </Button>
177
180
  <span className="text-[11px] text-muted-foreground/70">
@@ -626,20 +626,15 @@ export function FormBuilderPage() {
626
626
  </Tooltip>
627
627
  )}
628
628
 
629
- <Tooltip>
630
- <TooltipTrigger asChild>
631
- <span className="inline-flex">
629
+ {form.status === "published" && (
630
+ <Tooltip>
631
+ <TooltipTrigger asChild>
632
632
  <Button
633
633
  variant="ghost"
634
634
  size="icon"
635
635
  className="h-10 w-10 active:scale-[0.96] motion-reduce:active:scale-100"
636
636
  onClick={copyShareLink}
637
- disabled={form.status !== "published"}
638
- aria-label={
639
- form.status === "published"
640
- ? t("builder.copyPublicFormLink")
641
- : t("builder.publishBeforeCopyPublicFormLink")
642
- }
637
+ aria-label={t("builder.copyPublicFormLink")}
643
638
  >
644
639
  <span className="relative inline-flex h-4 w-4 items-center justify-center">
645
640
  <IconCopy
@@ -660,16 +655,14 @@ export function FormBuilderPage() {
660
655
  />
661
656
  </span>
662
657
  </Button>
663
- </span>
664
- </TooltipTrigger>
665
- <TooltipContent>
666
- {form.status === "published"
667
- ? copied
658
+ </TooltipTrigger>
659
+ <TooltipContent>
660
+ {copied
668
661
  ? t("builder.publicLinkCopied")
669
- : t("builder.copyPublishedPublicLink")
670
- : t("builder.publishBeforeCopyPublicLink")}
671
- </TooltipContent>
672
- </Tooltip>
662
+ : t("builder.copyPublishedPublicLink")}
663
+ </TooltipContent>
664
+ </Tooltip>
665
+ )}
673
666
 
674
667
  <Tooltip>
675
668
  <TooltipTrigger asChild>
@@ -7,6 +7,7 @@ import {
7
7
  import { VisibilityBadge } from "@agent-native/toolkit/sharing";
8
8
  import {
9
9
  IconPlus,
10
+ IconLoader2,
10
11
  IconDots,
11
12
  IconTrash,
12
13
  IconCopy,
@@ -100,11 +101,9 @@ export function FormsListPage() {
100
101
  }, [forms]);
101
102
 
102
103
  function handleCreate() {
103
- const tempId = crypto.randomUUID().replace(/-/g, "").slice(0, 10);
104
- navigate(`/forms/${tempId}`);
105
104
  createForm.mutate(
106
105
  { title: t("forms.untitled") },
107
- { onSuccess: (form) => navigate(`/forms/${form.id}`, { replace: true }) },
106
+ { onSuccess: (form) => navigate(`/forms/${form.id}`) },
108
107
  );
109
108
  }
110
109
 
@@ -114,16 +113,21 @@ export function FormsListPage() {
114
113
  () => (
115
114
  <Button
116
115
  onClick={handleCreate}
116
+ disabled={createForm.isPending}
117
117
  size="sm"
118
118
  className="min-h-10 shrink-0 cursor-pointer active:scale-[0.96] transition-[background-color,box-shadow,transform]"
119
119
  >
120
- <IconPlus className="h-3.5 w-3.5" />
120
+ {createForm.isPending ? (
121
+ <IconLoader2 className="h-3.5 w-3.5 animate-spin" />
122
+ ) : (
123
+ <IconPlus className="h-3.5 w-3.5" />
124
+ )}
121
125
  <span className="hidden sm:inline">{t("forms.newForm")}</span>
122
126
  <span className="sm:hidden">{t("forms.new")}</span>
123
127
  </Button>
124
128
  ),
125
129
  // eslint-disable-next-line react-hooks/exhaustive-deps
126
- [],
130
+ [createForm.isPending],
127
131
  );
128
132
  useSetHeaderActions(headerActions);
129
133