@agent-native/core 0.114.11 → 0.114.13

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 (109) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/application-state/index.ts +3 -0
  5. package/corpus/core/src/application-state/script-helpers.ts +12 -1
  6. package/corpus/core/src/application-state/store.ts +216 -15
  7. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +89 -1
  8. package/corpus/core/src/db/client.ts +44 -1
  9. package/corpus/templates/clips/changelog/2026-07-21-live-dictation-text-now-appears-above-the-desktop-recording-.md +6 -0
  10. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +14 -3
  11. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +25 -0
  12. package/corpus/templates/clips/desktop/src/styles.css +20 -0
  13. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +3 -4
  14. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +11 -4
  15. package/corpus/templates/design/AGENTS.md +7 -3
  16. package/corpus/templates/design/actions/begin-node-rewrite-request.ts +78 -0
  17. package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +17 -7
  18. package/corpus/templates/design/actions/propose-node-rewrite.ts +26 -18
  19. package/corpus/templates/design/actions/resolve-node-rewrite.ts +67 -19
  20. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +1 -4
  21. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +2 -6
  22. package/corpus/templates/design/shared/node-rewrite.ts +32 -0
  23. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +6 -3
  24. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -18
  25. package/corpus/templates/forms/app/pages/FormsListPage.tsx +9 -5
  26. package/corpus/templates/slides/app/context/DeckContext.tsx +18 -0
  27. package/corpus/templates/slides/changelog/2026-07-21-decks-list-now-refreshes-when-you-switch-organizations.md +6 -0
  28. package/corpus/templates/tasks/app/components/custom-fields/FieldCreateBar.tsx +13 -19
  29. package/corpus/templates/tasks/app/components/custom-fields/FieldListRow.tsx +27 -22
  30. package/corpus/templates/tasks/app/components/custom-fields/FieldsList.tsx +26 -22
  31. package/corpus/templates/tasks/app/components/custom-fields/FieldsListSkeleton.tsx +4 -1
  32. package/corpus/templates/tasks/app/components/custom-fields/FieldsPage.tsx +4 -2
  33. package/corpus/templates/tasks/app/components/custom-fields/editor/FieldEditorSidebar.tsx +12 -3
  34. package/corpus/templates/tasks/app/components/custom-fields/editor/FieldTitleSection.tsx +6 -3
  35. package/corpus/templates/tasks/app/components/custom-fields/editor/config/CurrencyConfigControl.tsx +5 -2
  36. package/corpus/templates/tasks/app/components/custom-fields/editor/config/NumberConfigControl.tsx +5 -2
  37. package/corpus/templates/tasks/app/components/custom-fields/editor/config/PercentConfigControl.tsx +4 -1
  38. package/corpus/templates/tasks/app/components/custom-fields/editor/config/SelectConfigControl.tsx +17 -7
  39. package/corpus/templates/tasks/app/components/custom-fields/editor/config/select-colors.ts +37 -9
  40. package/corpus/templates/tasks/app/components/custom-fields/field-types.ts +23 -0
  41. package/corpus/templates/tasks/app/components/inbox/InboxList.tsx +24 -16
  42. package/corpus/templates/tasks/app/components/inbox/InboxListPage.tsx +5 -3
  43. package/corpus/templates/tasks/app/components/inbox/InboxListRow.tsx +7 -5
  44. package/corpus/templates/tasks/app/components/layout/Header.tsx +10 -10
  45. package/corpus/templates/tasks/app/components/layout/Layout.tsx +11 -7
  46. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +17 -10
  47. package/corpus/templates/tasks/app/components/shared/AddListItemInput.tsx +14 -8
  48. package/corpus/templates/tasks/app/components/shared/AlertDialog.tsx +6 -2
  49. package/corpus/templates/tasks/app/components/shared/BulkDeleteDialog.tsx +13 -6
  50. package/corpus/templates/tasks/app/components/shared/ChipSelect.tsx +12 -5
  51. package/corpus/templates/tasks/app/components/shared/DeleteItemDialog.tsx +6 -6
  52. package/corpus/templates/tasks/app/components/shared/ListViewHeader.tsx +3 -1
  53. package/corpus/templates/tasks/app/components/shared/SidePanel.tsx +5 -2
  54. package/corpus/templates/tasks/app/components/shared/list/ListRowDragHandle.tsx +3 -1
  55. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionBar.tsx +46 -20
  56. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionHeaderToggle.tsx +3 -1
  57. package/corpus/templates/tasks/app/components/shared/selection/ListSelectionToolbar.tsx +10 -6
  58. package/corpus/templates/tasks/app/components/tasks/TaskList.tsx +24 -13
  59. package/corpus/templates/tasks/app/components/tasks/TaskListHeaderRow.tsx +4 -1
  60. package/corpus/templates/tasks/app/components/tasks/TaskListPage.tsx +5 -3
  61. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +15 -6
  62. package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +11 -6
  63. package/corpus/templates/tasks/app/components/tasks/fields/TaskTitleSection.tsx +8 -3
  64. package/corpus/templates/tasks/app/components/tasks/fields/controls/RichTextValueControl.tsx +12 -6
  65. package/corpus/templates/tasks/app/components/tasks/fields/controls/SingleSelectValueControl.tsx +6 -1
  66. package/corpus/templates/tasks/app/i18n/en-US.ts +209 -0
  67. package/corpus/templates/tasks/app/i18n/index.ts +34 -0
  68. package/corpus/templates/tasks/app/root.tsx +34 -2
  69. package/corpus/templates/tasks/app/routes/extensions.$id.tsx +2 -1
  70. package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -1
  71. package/corpus/templates/tasks/app/routes/fields.tsx +2 -1
  72. package/corpus/templates/tasks/app/routes/inbox.tsx +2 -1
  73. package/corpus/templates/tasks/app/routes/tasks.tsx +2 -1
  74. package/corpus/templates/tasks/app/routes/team.tsx +6 -3
  75. package/corpus/templates/tasks/changelog/2026-07-20-tasks-is-now-available-in-10-additional-languages.md +6 -0
  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 +3 -1
  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 +7 -1
  85. package/dist/application-state/store.d.ts.map +1 -1
  86. package/dist/application-state/store.js +143 -17
  87. package/dist/application-state/store.js.map +1 -1
  88. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  89. package/dist/client/extensions/EmbeddedExtension.js +65 -1
  90. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  91. package/dist/collab/awareness.d.ts +2 -2
  92. package/dist/collab/awareness.d.ts.map +1 -1
  93. package/dist/collab/routes.d.ts +1 -1
  94. package/dist/db/client.d.ts +7 -0
  95. package/dist/db/client.d.ts.map +1 -1
  96. package/dist/db/client.js +39 -1
  97. package/dist/db/client.js.map +1 -1
  98. package/dist/notifications/routes.d.ts +1 -1
  99. package/dist/observability/routes.d.ts +3 -3
  100. package/dist/progress/routes.d.ts +1 -1
  101. package/dist/secrets/routes.d.ts +6 -6
  102. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  103. package/dist/server/transcribe-voice.d.ts +1 -1
  104. package/package.json +1 -1
  105. package/src/application-state/index.ts +3 -0
  106. package/src/application-state/script-helpers.ts +12 -1
  107. package/src/application-state/store.ts +216 -15
  108. package/src/client/extensions/EmbeddedExtension.tsx +89 -1
  109. package/src/db/client.ts +44 -1
@@ -4905,13 +4905,33 @@ body[data-clips-route="recording-pill"] #root {
4905
4905
  position: fixed;
4906
4906
  inset: var(--overlay-shadow-gutter);
4907
4907
  display: flex;
4908
+ flex-direction: column;
4908
4909
  align-items: center;
4909
4910
  justify-content: flex-end;
4911
+ gap: 6px;
4910
4912
  padding-bottom: 8px;
4911
4913
  background: transparent;
4912
4914
  pointer-events: none;
4913
4915
  }
4914
4916
 
4917
+ .flow-bar-transcript-preview {
4918
+ max-width: 560px;
4919
+ max-height: 90px;
4920
+ padding: 5px 10px;
4921
+ overflow: hidden;
4922
+ border: 1px solid rgba(255, 255, 255, 0.06);
4923
+ border-radius: 10px;
4924
+ background: rgba(18, 18, 20, 0.92);
4925
+ color: rgba(255, 255, 255, 0.9);
4926
+ font-size: 12px;
4927
+ font-weight: 500;
4928
+ line-height: 18px;
4929
+ text-align: center;
4930
+ word-break: break-word;
4931
+ backdrop-filter: blur(20px);
4932
+ -webkit-backdrop-filter: blur(20px);
4933
+ }
4934
+
4915
4935
  .flow-bar {
4916
4936
  display: flex;
4917
4937
  align-items: center;
@@ -1506,10 +1506,9 @@ pub async fn show_flow_bar(app: AppHandle) -> Result<(), String> {
1506
1506
 
1507
1507
  let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
1508
1508
  let scale = overlay_scale_factor(&app);
1509
- // Compact Wispr-style pill window: just enough transparent canvas for
1510
- // the bottom-centered waveform bar and its shadow gutter.
1511
- let content_w: u32 = (160.0 * scale).round() as u32;
1512
- let content_h: u32 = (56.0 * scale).round() as u32;
1509
+ // Wide + tall enough for a 5-line transcript preview above the pill.
1510
+ let content_w: u32 = (640.0 * scale).round() as u32;
1511
+ let content_h: u32 = (160.0 * scale).round() as u32;
1513
1512
  let bottom_margin: i32 = (14.0 * scale).round() as i32;
1514
1513
  let gutter = overlay_shadow_gutter_physical(&app);
1515
1514
  let w: u32 = content_w + gutter * 2;
@@ -2465,8 +2465,15 @@ impl LiveAudioMixer {
2465
2465
  }
2466
2466
 
2467
2467
  /// Emit mixed sample buffers covering `out_pos..safe_end` in
2468
- /// `MIX_CHUNK_FRAMES` chunks: sum system + mic per frame, clamp, wrap as
2469
- /// LPCM `CMSampleBuffer`s with contiguous PTS.
2468
+ /// `MIX_CHUNK_FRAMES` chunks: average system + mic per frame, clamp, wrap
2469
+ /// as LPCM `CMSampleBuffer`s with contiguous PTS.
2470
+ ///
2471
+ /// Each source is weighted at 0.5 before summing so that two full-scale
2472
+ /// signals (which occurs when a USB audio interface with software monitoring
2473
+ /// routes the mic back through system audio) can never exceed ±1.0 and
2474
+ /// hard-clip. The standard SCK pipeline applies the same 0.5×L + 0.5×R
2475
+ /// pan-downmix for the same reason; loudnorm restores the target loudness
2476
+ /// in post-processing.
2470
2477
  fn drain_ready(
2471
2478
  &mut self,
2472
2479
  flush: bool,
@@ -2488,8 +2495,8 @@ impl LiveAudioMixer {
2488
2495
  let frame = a + f as i64;
2489
2496
  let (sl, sr) = self.system.sample_at(frame);
2490
2497
  let (ml, mr) = self.mic.sample_at(frame);
2491
- interleaved[f * 2] = (sl + ml).clamp(-1.0, 1.0);
2492
- interleaved[f * 2 + 1] = (sr + mr).clamp(-1.0, 1.0);
2498
+ interleaved[f * 2] = (sl * 0.5 + ml * 0.5).clamp(-1.0, 1.0);
2499
+ interleaved[f * 2 + 1] = (sr * 0.5 + mr * 0.5).clamp(-1.0, 1.0);
2493
2500
  }
2494
2501
  emitted.push(self.build_sample_buffer(&interleaved, a)?);
2495
2502
  a = b;
@@ -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
  });
@@ -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:
@@ -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