@agent-native/core 0.182.1 → 0.182.2-nightly-20260917225811

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 (119) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/root.tsx +59 -10
  3. package/corpus/templates/clips/changelog/2026-09-17-desktop-popover-closes-when-clicking-away.md +6 -0
  4. package/corpus/templates/clips/changelog/2026-09-17-self-hosted-extension-sign-in.md +6 -0
  5. package/corpus/templates/clips/chrome-extension/src/background.ts +45 -25
  6. package/corpus/templates/clips/chrome-extension/src/content-script.ts +43 -0
  7. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +55 -6
  8. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +107 -19
  9. package/corpus/templates/design/.generated/bridge/hit-test.generated.ts +1 -0
  10. package/corpus/templates/design/actions/add-localhost-screens.ts +54 -22
  11. package/corpus/templates/design/actions/apply-component-prop-edit.ts +174 -14
  12. package/corpus/templates/design/actions/apply-visual-edit.ts +3 -1
  13. package/corpus/templates/design/actions/create-component.ts +173 -12
  14. package/corpus/templates/design/actions/create-design-from-template.ts +50 -35
  15. package/corpus/templates/design/actions/create-design-version.ts +21 -0
  16. package/corpus/templates/design/actions/create-file.ts +35 -33
  17. package/corpus/templates/design/actions/delete-design.ts +29 -28
  18. package/corpus/templates/design/actions/delete-file.ts +154 -23
  19. package/corpus/templates/design/actions/generate-design.ts +28 -16
  20. package/corpus/templates/design/actions/get-component-details.ts +272 -33
  21. package/corpus/templates/design/actions/get-design-version.ts +9 -1
  22. package/corpus/templates/design/actions/go-to-main-component.ts +37 -35
  23. package/corpus/templates/design/actions/index-components.ts +241 -126
  24. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +12 -9
  25. package/corpus/templates/design/actions/present-design-variants.ts +68 -72
  26. package/corpus/templates/design/actions/rename-component.ts +294 -0
  27. package/corpus/templates/design/actions/rename-screen.ts +10 -10
  28. package/corpus/templates/design/actions/update-file.ts +327 -330
  29. package/corpus/templates/design/actions/update-screen-source.ts +2 -43
  30. package/corpus/templates/design/app/components/design/EditPanel.tsx +17 -1
  31. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +228 -24
  32. package/corpus/templates/design/app/components/design/ResponsiveInteractBar.tsx +17 -1
  33. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +151 -32
  34. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +4 -0
  35. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +1 -1
  36. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +8 -1
  37. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +55 -33
  38. package/corpus/templates/design/app/components/design/code-workbench/workspace/create-providers.ts +2 -0
  39. package/corpus/templates/design/app/components/design/code-workbench/workspace/inline-provider.ts +4 -2
  40. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +273 -60
  41. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +9 -2
  42. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +57 -9
  43. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +13 -0
  44. package/corpus/templates/design/app/components/design/multi-screen/types.ts +14 -0
  45. package/corpus/templates/design/app/components/design/types.ts +9 -1
  46. package/corpus/templates/design/app/hooks/use-design-versions.ts +3 -2
  47. package/corpus/templates/design/app/i18n-breakpoints.ts +0 -110
  48. package/corpus/templates/design/app/i18n-data.ts +0 -10
  49. package/corpus/templates/design/app/pages/DesignEditor.tsx +669 -161
  50. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +20 -0
  51. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +2 -0
  52. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +21 -3
  53. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +2 -0
  54. package/corpus/templates/design/app/pages/design-editor/commands/create-component.ts +44 -3
  55. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +69 -47
  56. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +157 -52
  57. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +76 -1
  58. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +152 -44
  59. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +131 -2
  60. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +5 -1
  61. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +26 -1
  62. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +1 -0
  63. package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +58 -8
  64. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +63 -2
  65. package/corpus/templates/design/app/pages/design-editor/history.ts +3 -0
  66. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +74 -22
  67. package/corpus/templates/design/app/pages/design-editor/source-publication.ts +14 -2
  68. package/corpus/templates/design/changelog/2026-09-16-design-components-retain-linked-identities-when-edited-and-d.md +6 -0
  69. package/corpus/templates/design/changelog/2026-09-16-fix-design-nested-layer-dragging-and-screen-duplication-pari.md +6 -0
  70. package/corpus/templates/design/changelog/2026-09-17-design-drag-and-drop-keeps-duplicates-cross-screen-moves-and.md +6 -0
  71. package/corpus/templates/design/changelog/2026-09-17-design-preserves-reusable-component-identity-and-props-acros.md +6 -0
  72. package/corpus/templates/design/changelog/2026-09-17-open-vector-paths-no-longer-show-fills-and-interact-dimensio.md +6 -0
  73. package/corpus/templates/design/changelog/2026-09-17-screen-deletion-is-immediate-and-undoable-with-history-that-.md +6 -0
  74. package/corpus/templates/design/e2e/fixtures/design-component-react/index.html +98 -0
  75. package/corpus/templates/design/e2e/fixtures/design-component-react/inline-component.html +65 -0
  76. package/corpus/templates/design/e2e/helpers.ts +6 -1
  77. package/corpus/templates/design/server/lib/design-data-mutation.ts +9 -5
  78. package/corpus/templates/design/server/lib/design-snapshot.ts +28 -16
  79. package/corpus/templates/design/server/lib/design-versions.ts +208 -130
  80. package/corpus/templates/design/server/lib/fusion-screens.ts +46 -20
  81. package/corpus/templates/design/server/lib/import-design-files.ts +5 -1
  82. package/corpus/templates/design/server/lib/localhost-connection.ts +54 -0
  83. package/corpus/templates/design/server/source-workspace.ts +412 -356
  84. package/corpus/templates/design/shared/component-library.ts +36 -6
  85. package/corpus/templates/design/shared/component-model.ts +12 -2
  86. package/corpus/templates/design/shared/local-jsx-visual-edit.ts +231 -5
  87. package/corpus/templates/design/shared/source-mode.ts +22 -0
  88. package/dist/automation/index.d.ts +1 -1
  89. package/dist/client/AssistantChat.d.ts +4 -1
  90. package/dist/client/AssistantChat.js +26 -3
  91. package/dist/client/chat/agentkit-protocol.js +148 -34
  92. package/dist/client/chat/message-components.js +7 -5
  93. package/dist/client/chat/tool-call-display.js +4 -3
  94. package/dist/client/conversation/AgentConversation.js +4 -2
  95. package/dist/client/tool-display.d.ts +9 -0
  96. package/dist/client/tool-display.js +15 -0
  97. package/dist/collab/awareness.d.ts +2 -2
  98. package/dist/collab/routes.d.ts +1 -1
  99. package/dist/localization/core-messages/ar-SA.js +2 -0
  100. package/dist/localization/core-messages/de-DE.js +2 -0
  101. package/dist/localization/core-messages/en-US.d.ts +2 -0
  102. package/dist/localization/core-messages/en-US.js +2 -0
  103. package/dist/localization/core-messages/es-ES.js +2 -0
  104. package/dist/localization/core-messages/fr-FR.js +2 -0
  105. package/dist/localization/core-messages/hi-IN.js +2 -0
  106. package/dist/localization/core-messages/ja-JP.js +2 -0
  107. package/dist/localization/core-messages/ko-KR.js +2 -0
  108. package/dist/localization/core-messages/pt-BR.js +2 -0
  109. package/dist/localization/core-messages/zh-CN.js +2 -0
  110. package/dist/localization/core-messages/zh-TW.js +2 -0
  111. package/dist/localization/core-messages.d.ts +2 -0
  112. package/dist/notifications/routes.d.ts +3 -3
  113. package/dist/observability/routes.d.ts +3 -3
  114. package/dist/resources/handlers.d.ts +1 -1
  115. package/dist/secrets/routes.d.ts +3 -3
  116. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  117. package/dist/server/core-routes-plugin.d.ts +1 -1
  118. package/package.json +3 -3
  119. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +0 -66
@@ -16,7 +16,12 @@
16
16
  */
17
17
 
18
18
  import { defineAction } from "@agent-native/core/action";
19
- import { getText, hasCollabState } from "@agent-native/core/collab";
19
+ import {
20
+ applyTextToYDoc,
21
+ CollabBaseVersionConflictError,
22
+ withPreparedYDocMutation,
23
+ } from "@agent-native/core/collab";
24
+ import { getDbExec } from "@agent-native/core/db";
20
25
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
21
26
  import { accessFilter, assertAccess } from "@agent-native/core/sharing";
22
27
  import { and, eq } from "drizzle-orm";
@@ -24,10 +29,16 @@ import { z } from "zod";
24
29
 
25
30
  import { getDb, schema } from "../server/db/index.js";
26
31
  import "../server/db/index.js"; // ensure registerShareableResource runs
32
+ import {
33
+ SourceWorkspaceEditConflictError,
34
+ designSourceMutationLockKey,
35
+ withSourceFileWriteLock,
36
+ } from "../server/source-workspace.js";
27
37
  import { resolveSourceCapabilities } from "../shared/capability-resolver.js";
28
38
  import { buildCodeLayerProjection } from "../shared/code-layer.js";
29
39
  import type { CodeLayerSource } from "../shared/code-layer.js";
30
40
  import {
41
+ componentIndexId,
31
42
  detectInstances,
32
43
  buildDefinitions,
33
44
  type ComponentDefinition,
@@ -36,29 +47,6 @@ import {
36
47
  import { hasCapability } from "../shared/design-source-capabilities.js";
37
48
  import { designSourceTypeFromData } from "../shared/source-mode.js";
38
49
 
39
- // ─── Helpers ──────────────────────────────────────────────────────────────────
40
-
41
- async function liveContent(
42
- fileId: string,
43
- storedContent: string,
44
- ): Promise<string> {
45
- try {
46
- if (await hasCollabState(fileId)) {
47
- const live = await getText(fileId, "content");
48
- if (typeof live === "string") return live;
49
- }
50
- } catch {
51
- // Collab reads are best-effort; SQL content is the fallback.
52
- }
53
- return storedContent;
54
- }
55
-
56
- function componentIndexId(designId: string, name: string): string {
57
- // Stable deterministic id — same component name on the same design always
58
- // maps to the same row so upsert is safe.
59
- return `ci_${designId}_${name.toLowerCase().replace(/[^a-z0-9]/g, "_")}`;
60
- }
61
-
62
50
  // ─── Action ───────────────────────────────────────────────────────────────────
63
51
 
64
52
  export default defineAction({
@@ -118,7 +106,9 @@ export default defineAction({
118
106
  };
119
107
  }
120
108
 
121
- // ── Fetch the design file ────────────────────────────────────────────────
109
+ // Resolve the lock target without holding a database lock. The source-file
110
+ // lock must be acquired before the transaction so index and write paths
111
+ // enter Yjs and SQL in the same order.
122
112
  const conditions = [
123
113
  accessFilter(schema.designs, schema.designShares),
124
114
  eq(schema.designFiles.designId, designId),
@@ -126,14 +116,8 @@ export default defineAction({
126
116
  ? eq(schema.designFiles.id, fileId)
127
117
  : eq(schema.designFiles.filename, "index.html"),
128
118
  ];
129
-
130
- const [file] = await db
131
- .select({
132
- id: schema.designFiles.id,
133
- designId: schema.designFiles.designId,
134
- filename: schema.designFiles.filename,
135
- content: schema.designFiles.content,
136
- })
119
+ const [candidate] = await db
120
+ .select({ id: schema.designFiles.id })
137
121
  .from(schema.designFiles)
138
122
  .innerJoin(
139
123
  schema.designs,
@@ -141,102 +125,233 @@ export default defineAction({
141
125
  )
142
126
  .where(and(...conditions))
143
127
  .limit(1);
128
+ if (!candidate) throw new Error("Design HTML file not found.");
129
+
130
+ let preparedCallbackEntered = false;
131
+ let result;
132
+ try {
133
+ result = await withSourceFileWriteLock(candidate.id, () =>
134
+ // Holding the core document lock across the SQL snapshot prevents a
135
+ // local Yjs writer from changing the document between the row locks and
136
+ // the component projection. Cross-process writers are stopped by the
137
+ // transaction-scoped _collab_docs row lock below.
138
+ withPreparedYDocMutation(candidate.id, undefined, async (lease) => {
139
+ preparedCallbackEntered = true;
140
+ const [file] = await db
141
+ .select({
142
+ id: schema.designFiles.id,
143
+ designId: schema.designFiles.designId,
144
+ filename: schema.designFiles.filename,
145
+ content: schema.designFiles.content,
146
+ })
147
+ .from(schema.designFiles)
148
+ .innerJoin(
149
+ schema.designs,
150
+ eq(schema.designFiles.designId, schema.designs.id),
151
+ )
152
+ .where(and(...conditions))
153
+ .limit(1);
154
+
155
+ if (!file) throw new Error("Design HTML file not found.");
156
+
157
+ const transaction = getDbExec().transaction;
158
+ if (!transaction) {
159
+ throw new SourceWorkspaceEditConflictError(
160
+ "Could not verify a source file's live version. Re-read the design and retry.",
161
+ );
162
+ }
163
+
164
+ return transaction(async (tx) => {
165
+ await tx.execute({
166
+ sql: "SELECT pg_advisory_xact_lock(hashtextextended(?, 0::bigint))",
167
+ args: [designSourceMutationLockKey(designId)],
168
+ });
169
+
170
+ // Lock only the selected source row and the live collab row. The
171
+ // latter makes the following text_snapshot a stable cross-process
172
+ // view instead of a best-effort cache read.
173
+ const lockedFileResult = await tx.execute({
174
+ sql: 'SELECT id, design_id AS "designId", filename, content FROM design_files WHERE id = ? AND design_id = ? FOR UPDATE',
175
+ args: [file.id, designId],
176
+ });
177
+ const lockedFileRow = lockedFileResult.rows[0] as
178
+ | {
179
+ id?: unknown;
180
+ designId?: unknown;
181
+ filename?: unknown;
182
+ content?: unknown;
183
+ }
184
+ | undefined;
185
+
186
+ if (
187
+ !lockedFileRow ||
188
+ typeof lockedFileRow.id !== "string" ||
189
+ typeof lockedFileRow.designId !== "string" ||
190
+ typeof lockedFileRow.filename !== "string" ||
191
+ (lockedFileRow.content !== null &&
192
+ typeof lockedFileRow.content !== "string") ||
193
+ lockedFileRow.content !== file.content
194
+ ) {
195
+ throw new SourceWorkspaceEditConflictError(
196
+ "The source file changed while its live version was being read. Re-read the design and retry.",
197
+ );
198
+ }
199
+ const lockedFile = {
200
+ id: lockedFileRow.id,
201
+ designId: lockedFileRow.designId,
202
+ filename: lockedFileRow.filename,
203
+ content: lockedFileRow.content as string | null,
204
+ };
205
+
206
+ let collabResult: { rows: any[] };
207
+ try {
208
+ collabResult = (await tx.execute({
209
+ sql: "SELECT yjs_state, text_snapshot FROM _collab_docs WHERE doc_id = ? FOR SHARE",
210
+ args: [file.id],
211
+ })) as { rows: any[] };
212
+ } catch {
213
+ throw new SourceWorkspaceEditConflictError(
214
+ "Could not verify a source file's live version. Re-read the design and retry.",
215
+ );
216
+ }
217
+ const collabRow = collabResult.rows[0] as
218
+ | { yjs_state?: unknown; text_snapshot?: unknown }
219
+ | undefined;
220
+ let html = (lockedFile.content as string | null) ?? "";
221
+ let needsCollabSeed = !collabRow;
222
+ if (collabRow) {
223
+ if (
224
+ typeof collabRow.yjs_state !== "string" ||
225
+ typeof collabRow.text_snapshot !== "string"
226
+ ) {
227
+ throw new SourceWorkspaceEditConflictError(
228
+ "Could not verify a source file's live version. Re-read the design and retry.",
229
+ );
230
+ }
231
+ if (collabRow.yjs_state.length > 0) {
232
+ html = collabRow.text_snapshot;
233
+ } else {
234
+ needsCollabSeed = true;
235
+ }
236
+ }
237
+
238
+ // A missing or empty row is lazy collab state, not permission to
239
+ // index an unprotected SQL fallback. Seed the prepared Y.Doc in
240
+ // this transaction; if another writer wins the insert/CAS, the
241
+ // lease raises a typed conflict and the index write rolls back.
242
+ if (needsCollabSeed) {
243
+ applyTextToYDoc(lease.doc, "content", html, "agent");
244
+ try {
245
+ await lease.persist(tx, html);
246
+ } catch (error) {
247
+ if (error instanceof CollabBaseVersionConflictError) {
248
+ throw new SourceWorkspaceEditConflictError(
249
+ "The source file changed while its live version was being initialized. Re-read the design and retry.",
250
+ );
251
+ }
252
+ throw error;
253
+ }
254
+ }
255
+
256
+ const codeLayerSource: CodeLayerSource = {
257
+ kind: "design-file",
258
+ designId: lockedFile.designId,
259
+ fileId: lockedFile.id,
260
+ filename: lockedFile.filename,
261
+ };
262
+ const projection = buildCodeLayerProjection(html, {
263
+ source: codeLayerSource,
264
+ });
265
+ const instances = detectInstances(projection.nodes);
266
+ const definitions = buildDefinitions(instances);
267
+
268
+ // ── Persist discovered components ────────────────────────────────────
269
+ // Write each distinct component name into component_index so that
270
+ // get-component-details can resolve metadata by node id.
271
+ const now = new Date().toISOString();
272
+ const selectorByNodeId = new Map(
273
+ instances.map((instance) => [instance.nodeId, instance.selector]),
274
+ );
275
+ // Derive the owner from the request user, falling back to the design's
276
+ // owner. Never stamp an empty-string owner (an unowned row): require a real
277
+ // owner before writing a new component_index row.
278
+ const designOwner = (access.resource as { ownerEmail?: unknown })
279
+ .ownerEmail;
280
+ const ownerEmail =
281
+ getRequestUserEmail() ??
282
+ (typeof designOwner === "string" && designOwner
283
+ ? designOwner
284
+ : null);
285
+ if (!ownerEmail) throw new Error("no authenticated user");
286
+
287
+ for (const def of definitions) {
288
+ const id = componentIndexId(designId, def.name);
289
+ await tx.execute({
290
+ sql: `INSERT INTO component_index
291
+ (id, design_id, name, runtime_selectors, owner_email, created_at, updated_at)
292
+ VALUES (?, ?, ?, ?, ?, ?, ?)
293
+ ON CONFLICT (id) DO UPDATE SET
294
+ runtime_selectors = excluded.runtime_selectors,
295
+ updated_at = excluded.updated_at`,
296
+ args: [
297
+ id,
298
+ designId,
299
+ def.name,
300
+ JSON.stringify(
301
+ def.instanceNodeIds
302
+ .map((nodeId) => selectorByNodeId.get(nodeId))
303
+ .filter((selector): selector is string =>
304
+ Boolean(selector),
305
+ ),
306
+ ),
307
+ ownerEmail,
308
+ now,
309
+ now,
310
+ ],
311
+ });
312
+ }
313
+
314
+ // Annotate instances with their component_index id.
315
+ const indexMap = new Map(
316
+ definitions.map((def) => [
317
+ def.name,
318
+ componentIndexId(designId, def.name),
319
+ ]),
320
+ );
321
+ const annotatedInstances = instances.map((inst) => ({
322
+ ...inst,
323
+ componentIndexId: indexMap.get(inst.name),
324
+ }));
144
325
 
145
- if (!file) throw new Error("Design HTML file not found.");
146
-
147
- const html = await liveContent(file.id, file.content ?? "");
148
-
149
- // ── Projection + detection ───────────────────────────────────────────────
150
- const codeLayerSource: CodeLayerSource = {
151
- kind: "design-file",
152
- designId: file.designId,
153
- fileId: file.id,
154
- filename: file.filename,
155
- };
156
-
157
- const projection = buildCodeLayerProjection(html, {
158
- source: codeLayerSource,
159
- });
160
-
161
- const instances = detectInstances(projection.nodes);
162
- const definitions = buildDefinitions(instances);
163
-
164
- // ── Persist discovered components ────────────────────────────────────────
165
- // Write each distinct component name into component_index so that
166
- // get-component-details can resolve metadata by node id.
167
- const now = new Date().toISOString();
168
- // Derive the owner from the request user, falling back to the design's
169
- // owner. Never stamp an empty-string owner (an unowned row): require a real
170
- // owner before writing a new component_index row.
171
- const designOwner = (access.resource as { ownerEmail?: unknown })
172
- .ownerEmail;
173
- const ownerEmail =
174
- getRequestUserEmail() ??
175
- (typeof designOwner === "string" && designOwner ? designOwner : null);
176
- if (!ownerEmail) throw new Error("no authenticated user");
177
-
178
- for (const def of definitions) {
179
- const id = componentIndexId(designId, def.name);
180
- const existing = await db
181
- .select({ id: schema.componentIndex.id })
182
- .from(schema.componentIndex)
183
- .where(eq(schema.componentIndex.id, id))
184
- .limit(1);
185
-
186
- if (existing.length === 0) {
187
- await db.insert(schema.componentIndex).values({
188
- id,
189
- designId,
190
- name: def.name,
191
- runtimeSelectors: JSON.stringify(
192
- def.instanceNodeIds.map(
193
- (nodeId) => `[data-agent-native-node-id="${nodeId}"]`,
194
- ),
195
- ),
196
- ownerEmail,
197
- createdAt: now,
198
- updatedAt: now,
199
- });
200
- } else {
201
- await db
202
- .update(schema.componentIndex)
203
- .set({
204
- runtimeSelectors: JSON.stringify(
205
- def.instanceNodeIds.map(
206
- (nodeId) => `[data-agent-native-node-id="${nodeId}"]`,
207
- ),
208
- ),
209
- updatedAt: now,
210
- })
211
- .where(eq(schema.componentIndex.id, id));
326
+ return {
327
+ designId,
328
+ sourceType,
329
+ ctaRequired: false,
330
+ hasFullIndex,
331
+ components: definitions,
332
+ instances: annotatedInstances,
333
+ totalComponents: definitions.length,
334
+ totalInstances: instances.length,
335
+ note:
336
+ sourceType === "inline"
337
+ ? "Showing annotated Alpine components from data-agent-native-component attributes. Connect Builder (free tier available) for full TS prop types and cva variants."
338
+ : undefined,
339
+ };
340
+ });
341
+ }),
342
+ );
343
+ } catch (error) {
344
+ if (
345
+ !preparedCallbackEntered &&
346
+ !(error instanceof SourceWorkspaceEditConflictError)
347
+ ) {
348
+ throw new SourceWorkspaceEditConflictError(
349
+ "Could not verify a source file's live version. Re-read the design and retry.",
350
+ );
212
351
  }
352
+ throw error;
213
353
  }
214
354
 
215
- // Annotate instances with their component_index id.
216
- const indexMap = new Map(
217
- definitions.map((def) => [
218
- def.name,
219
- componentIndexId(designId, def.name),
220
- ]),
221
- );
222
- const annotatedInstances = instances.map((inst) => ({
223
- ...inst,
224
- componentIndexId: indexMap.get(inst.name),
225
- }));
226
-
227
- return {
228
- designId,
229
- sourceType,
230
- ctaRequired: false,
231
- hasFullIndex,
232
- components: definitions,
233
- instances: annotatedInstances,
234
- totalComponents: definitions.length,
235
- totalInstances: instances.length,
236
- note:
237
- sourceType === "inline"
238
- ? "Showing annotated Alpine components from data-agent-native-component attributes. Connect Builder (free tier available) for full TS prop types and cva variants."
239
- : undefined,
240
- };
355
+ return result;
241
356
  },
242
357
  });
@@ -42,6 +42,7 @@ import { mutateDesignData } from "../server/lib/design-data-mutation.js";
42
42
  import { snapshotDesignBeforeAgentEdit } from "../server/lib/design-versions.js";
43
43
  import {
44
44
  readLiveSourceFile,
45
+ withDesignSourceMutationTransaction,
45
46
  writeInlineSourceFile,
46
47
  } from "../server/source-workspace.js";
47
48
  import {
@@ -226,15 +227,17 @@ export default defineAction({
226
227
  });
227
228
  } else {
228
229
  try {
229
- await db.insert(schema.designFiles).values({
230
- id: boardFileId,
231
- designId,
232
- filename: BOARD_FILENAME,
233
- fileType: "html",
234
- content: boardHtml,
235
- createdAt: now,
236
- updatedAt: now,
237
- });
230
+ await withDesignSourceMutationTransaction(designId, (tx) =>
231
+ tx.insert(schema.designFiles).values({
232
+ id: boardFileId,
233
+ designId,
234
+ filename: BOARD_FILENAME,
235
+ fileType: "html",
236
+ content: boardHtml,
237
+ createdAt: now,
238
+ updatedAt: now,
239
+ }),
240
+ );
238
241
  } catch (error) {
239
242
  const [concurrentBoardFile] = await db
240
243
  .select({ id: schema.designFiles.id })
@@ -15,12 +15,12 @@ import "../server/db/index.js"; // ensure registerShareableResource runs
15
15
  import { getDb, schema } from "../server/db/index.js";
16
16
  import { mutateDesignData } from "../server/lib/design-data-mutation.js";
17
17
  import { snapshotDesignBeforeAgentEdit } from "../server/lib/design-versions.js";
18
+ import { withDesignSourceMutationTransaction } from "../server/source-workspace.js";
18
19
  import {
19
20
  mergeCanvasFramePlacements,
20
21
  nextFreeCanvasRowY,
21
22
  type CanvasFramePlacement,
22
23
  } from "../shared/canvas-frames.js";
23
- import { isUniqueConstraintViolation } from "../shared/db-conflict.js";
24
24
  import { getOverviewScreenFileIds } from "../shared/design-files.js";
25
25
  import { assertDesignHtmlWellFormed } from "../shared/html-integrity.js";
26
26
  import { widthToPrefix } from "../shared/responsive-classes.js";
@@ -205,8 +205,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
205
205
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
206
206
  }
207
207
 
208
- const MAX_FILENAME_INSERT_ATTEMPTS = 5;
209
-
210
208
  /**
211
209
  * True only when `rawSet` proves none of its screens has ever been removed by
212
210
  * delete-file.ts. This action stamps every variant set it creates with
@@ -496,9 +494,16 @@ async function deleteVariantSetsByIds(
496
494
  });
497
495
 
498
496
  if (removedFileIds.length > 0) {
499
- await db
500
- .delete(schema.designFiles)
501
- .where(inArray(schema.designFiles.id, removedFileIds));
497
+ await withDesignSourceMutationTransaction(designId, async (tx) => {
498
+ await tx
499
+ .delete(schema.designFiles)
500
+ .where(
501
+ and(
502
+ eq(schema.designFiles.designId, designId),
503
+ inArray(schema.designFiles.id, removedFileIds),
504
+ ),
505
+ );
506
+ });
502
507
 
503
508
  // Closes the small window between the metadata prune above and the
504
509
  // physical row delete: mirrors delete-file.ts's before/delete/after
@@ -992,46 +997,45 @@ export default defineAction({
992
997
 
993
998
  const db = getDb();
994
999
  const now = new Date().toISOString();
995
- const existingFiles = await db
996
- .select()
997
- .from(schema.designFiles)
998
- .where(eq(schema.designFiles.designId, designId));
999
- const usedFilenames = new Set(existingFiles.map((file) => file.filename));
1000
- const screenFileIds = getOverviewScreenFileIds(existingFiles);
1001
1000
  const variantSetId = nanoid();
1002
- const screens: VariantScreen[] = [];
1003
-
1004
- for (let index = 0; index < variants.length; index += 1) {
1005
- const variant = variants[index]!;
1006
- const label = variant.label.trim() || optionName(index);
1007
- const slug = slugify(label, `option-${index + 1}`);
1008
- let filename = uniqueFilename(`variant-${slug}.html`, usedFilenames);
1009
- let fileId = nanoid();
1010
- const providedContent = variant.content?.trim();
1011
- const initialSize = inferVariantSize(variant, prompt);
1012
- const rawContent =
1013
- providedContent ||
1014
- fallbackVariantContent(variant, index, prompt, initialSize);
1015
- const { width, height } = providedContent
1016
- ? inferVariantSize({ ...variant, content: rawContent })
1017
- : initialSize;
1018
- // Stamp missing data-agent-native-node-id attributes before persisting
1019
- // so each variant screen is fully addressable by id-keyed editor
1020
- // operations as soon as it lands on the overview board.
1021
- const content = annotateScreenHtmlForPersist(rawContent, "html");
1022
-
1023
- // `usedFilenames` is a snapshot taken once at the top of run(), so a
1024
- // concurrent present-design-variants call (an agent retry after a
1025
- // timeout is the common trigger) can independently compute the same
1026
- // (designId, filename) pair and win the insert first. The
1027
- // `design_files_design_filename_unique_idx` unique index (see
1028
- // server/plugins/db.ts) turns the loser's insert into a constraint
1029
- // error instead of a silently duplicated screen; recover by refreshing
1030
- // the real filename list from the DB, picking a fresh unique name, and
1031
- // retrying — bounded so a persistent non-race failure still surfaces.
1032
- for (let attempt = 0; ; attempt += 1) {
1033
- try {
1034
- await db.insert(schema.designFiles).values({
1001
+ let screenFileIds: string[] = [];
1002
+ const screenContents = new Map<string, string>();
1003
+ const screens = await withDesignSourceMutationTransaction(
1004
+ designId,
1005
+ async (tx) => {
1006
+ const existingFiles = await tx
1007
+ .select()
1008
+ .from(schema.designFiles)
1009
+ .where(eq(schema.designFiles.designId, designId));
1010
+ const usedFilenames = new Set(
1011
+ existingFiles.map((file) => file.filename),
1012
+ );
1013
+ screenFileIds = getOverviewScreenFileIds(existingFiles);
1014
+ const createdScreens: VariantScreen[] = [];
1015
+
1016
+ for (let index = 0; index < variants.length; index += 1) {
1017
+ const variant = variants[index]!;
1018
+ const label = variant.label.trim() || optionName(index);
1019
+ const slug = slugify(label, `option-${index + 1}`);
1020
+ const filename = uniqueFilename(
1021
+ `variant-${slug}.html`,
1022
+ usedFilenames,
1023
+ );
1024
+ const fileId = nanoid();
1025
+ const providedContent = variant.content?.trim();
1026
+ const initialSize = inferVariantSize(variant, prompt);
1027
+ const rawContent =
1028
+ providedContent ||
1029
+ fallbackVariantContent(variant, index, prompt, initialSize);
1030
+ const { width, height } = providedContent
1031
+ ? inferVariantSize({ ...variant, content: rawContent })
1032
+ : initialSize;
1033
+ // Stamp missing data-agent-native-node-id attributes before
1034
+ // persisting so each variant screen is fully addressable by
1035
+ // id-keyed editor operations as soon as it lands on the board.
1036
+ const content = annotateScreenHtmlForPersist(rawContent, "html");
1037
+
1038
+ await tx.insert(schema.designFiles).values({
1035
1039
  id: fileId,
1036
1040
  designId,
1037
1041
  filename,
@@ -1040,34 +1044,26 @@ export default defineAction({
1040
1044
  createdAt: now,
1041
1045
  updatedAt: now,
1042
1046
  });
1043
- break;
1044
- } catch (err) {
1045
- if (
1046
- !isUniqueConstraintViolation(err) ||
1047
- attempt >= MAX_FILENAME_INSERT_ATTEMPTS
1048
- ) {
1049
- throw err;
1050
- }
1051
- const freshFiles = await db
1052
- .select({ filename: schema.designFiles.filename })
1053
- .from(schema.designFiles)
1054
- .where(eq(schema.designFiles.designId, designId));
1055
- for (const file of freshFiles) usedFilenames.add(file.filename);
1056
- filename = uniqueFilename(`variant-${slug}.html`, usedFilenames);
1057
- fileId = nanoid();
1047
+ usedFilenames.add(filename);
1048
+ screenContents.set(fileId, content);
1049
+
1050
+ createdScreens.push({
1051
+ id: fileId,
1052
+ variantId: variant.id,
1053
+ label,
1054
+ filename,
1055
+ width,
1056
+ height,
1057
+ });
1058
1058
  }
1059
- }
1060
- await seedFromText(fileId, content);
1061
-
1062
- screens.push({
1063
- id: fileId,
1064
- variantId: variant.id,
1065
- label,
1066
- filename,
1067
- width,
1068
- height,
1069
- });
1070
- }
1059
+ return createdScreens;
1060
+ },
1061
+ );
1062
+ await Promise.all(
1063
+ screens.map((screen) =>
1064
+ seedFromText(screen.id, screenContents.get(screen.id)!),
1065
+ ),
1066
+ );
1071
1067
 
1072
1068
  // Presenting options should not silently reconfigure the design. When it
1073
1069
  // does, the overview paints an extra preview beside EVERY primary frame