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

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 (66) 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 +1 -0
  9. package/corpus/templates/design/actions/add-localhost-screens.ts +54 -22
  10. package/corpus/templates/design/actions/apply-visual-edit.ts +1 -0
  11. package/corpus/templates/design/actions/create-design-from-template.ts +50 -35
  12. package/corpus/templates/design/actions/create-design-version.ts +21 -0
  13. package/corpus/templates/design/actions/create-file.ts +35 -33
  14. package/corpus/templates/design/actions/delete-design.ts +29 -28
  15. package/corpus/templates/design/actions/delete-file.ts +154 -23
  16. package/corpus/templates/design/actions/generate-design.ts +28 -16
  17. package/corpus/templates/design/actions/get-component-details.ts +187 -33
  18. package/corpus/templates/design/actions/get-design-version.ts +9 -1
  19. package/corpus/templates/design/actions/go-to-main-component.ts +37 -35
  20. package/corpus/templates/design/actions/index-components.ts +241 -126
  21. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +12 -9
  22. package/corpus/templates/design/actions/present-design-variants.ts +68 -72
  23. package/corpus/templates/design/actions/rename-component.ts +294 -0
  24. package/corpus/templates/design/actions/rename-screen.ts +10 -10
  25. package/corpus/templates/design/actions/update-file.ts +327 -330
  26. package/corpus/templates/design/actions/update-screen-source.ts +2 -43
  27. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1 -0
  28. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +8 -1
  29. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +55 -33
  30. package/corpus/templates/design/app/components/design/code-workbench/workspace/create-providers.ts +2 -0
  31. package/corpus/templates/design/app/components/design/code-workbench/workspace/inline-provider.ts +4 -2
  32. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +5 -1
  33. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +57 -9
  34. package/corpus/templates/design/app/components/design/types.ts +1 -0
  35. package/corpus/templates/design/app/hooks/use-design-versions.ts +3 -2
  36. package/corpus/templates/design/app/i18n-breakpoints.ts +0 -110
  37. package/corpus/templates/design/app/i18n-data.ts +0 -10
  38. package/corpus/templates/design/app/pages/DesignEditor.tsx +250 -107
  39. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +2 -0
  40. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +69 -47
  41. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +8 -1
  42. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +107 -2
  43. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +26 -1
  44. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +63 -2
  45. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +74 -22
  46. package/corpus/templates/design/app/pages/design-editor/source-publication.ts +14 -2
  47. package/corpus/templates/design/changelog/2026-09-16-design-components-retain-linked-identities-when-edited-and-d.md +6 -0
  48. package/corpus/templates/design/changelog/2026-09-17-screen-deletion-is-immediate-and-undoable-with-history-that-.md +6 -0
  49. package/corpus/templates/design/e2e/fixtures/design-component-react/index.html +98 -0
  50. package/corpus/templates/design/e2e/fixtures/design-component-react/inline-component.html +65 -0
  51. package/corpus/templates/design/server/lib/design-data-mutation.ts +9 -5
  52. package/corpus/templates/design/server/lib/design-snapshot.ts +28 -16
  53. package/corpus/templates/design/server/lib/design-versions.ts +208 -130
  54. package/corpus/templates/design/server/lib/fusion-screens.ts +46 -20
  55. package/corpus/templates/design/server/lib/import-design-files.ts +5 -1
  56. package/corpus/templates/design/server/lib/localhost-connection.ts +54 -0
  57. package/corpus/templates/design/server/source-workspace.ts +412 -356
  58. package/corpus/templates/design/shared/component-library.ts +36 -6
  59. package/corpus/templates/design/shared/component-model.ts +8 -2
  60. package/dist/automation/index.d.ts +1 -1
  61. package/dist/client/chat/agentkit-protocol.js +148 -34
  62. package/dist/collab/struct-routes.d.ts +1 -1
  63. package/dist/progress/routes.d.ts +1 -1
  64. package/dist/server/core-routes-plugin.d.ts +1 -1
  65. package/package.json +4 -4
  66. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +0 -66
@@ -8,6 +8,7 @@ import { z } from "zod";
8
8
  import { getDb, schema } from "../server/db/index.js";
9
9
  import { mutateDesignData } from "../server/lib/design-data-mutation.js";
10
10
  import { snapshotDesignBeforeAgentEdit } from "../server/lib/design-versions.js";
11
+ import { withDesignSourceMutationTransaction } from "../server/source-workspace.js";
11
12
  import {
12
13
  mergeCanvasFramePlacements,
13
14
  nextFreeCanvasRowY,
@@ -55,26 +56,6 @@ export default defineAction({
55
56
  await assertAccess("design", designId, "editor");
56
57
  await snapshotDesignBeforeAgentEdit(designId, context);
57
58
 
58
- const db = getDb();
59
-
60
- // Guard against duplicate (designId, filename) — edit-design uses .limit(1)
61
- // which is non-deterministic when multiple rows match the same key.
62
- const [existing] = await db
63
- .select({ id: schema.designFiles.id })
64
- .from(schema.designFiles)
65
- .where(
66
- and(
67
- eq(schema.designFiles.designId, designId),
68
- eq(schema.designFiles.filename, filename),
69
- ),
70
- )
71
- .limit(1);
72
- if (existing) {
73
- throw new Error(
74
- `File "${filename}" already exists in design ${designId} — use edit-design to modify it`,
75
- );
76
- }
77
-
78
59
  const id = nanoid();
79
60
  const now = new Date().toISOString();
80
61
 
@@ -92,24 +73,45 @@ export default defineAction({
92
73
  filename,
93
74
  });
94
75
 
95
- await db.insert(schema.designFiles).values({
96
- id,
97
- designId,
98
- filename,
99
- fileType: fileType ?? "html",
100
- content: annotatedContent,
101
- createdAt: now,
102
- updatedAt: now,
76
+ await withDesignSourceMutationTransaction(designId, async (tx) => {
77
+ // Guard against duplicate (designId, filename) inside the same
78
+ // transaction as the insert; the unique index remains the final guard.
79
+ const [existing] = await tx
80
+ .select({ id: schema.designFiles.id })
81
+ .from(schema.designFiles)
82
+ .where(
83
+ and(
84
+ eq(schema.designFiles.designId, designId),
85
+ eq(schema.designFiles.filename, filename),
86
+ ),
87
+ )
88
+ .limit(1);
89
+ if (existing) {
90
+ throw new Error(
91
+ `File "${filename}" already exists in design ${designId} — use edit-design to modify it`,
92
+ );
93
+ }
94
+
95
+ await tx.insert(schema.designFiles).values({
96
+ id,
97
+ designId,
98
+ filename,
99
+ fileType: fileType ?? "html",
100
+ content: annotatedContent,
101
+ createdAt: now,
102
+ updatedAt: now,
103
+ });
104
+
105
+ await tx
106
+ .update(schema.designs)
107
+ .set({ updatedAt: now })
108
+ .where(eq(schema.designs.id, designId));
103
109
  });
104
110
 
105
111
  // Seed collab state for the new file
106
112
  await seedFromText(id, annotatedContent);
107
113
 
108
- // Update the design's updatedAt timestamp
109
- await db
110
- .update(schema.designs)
111
- .set({ updatedAt: now })
112
- .where(eq(schema.designs.id, designId));
114
+ const db = getDb();
113
115
 
114
116
  const resolvedFileType = fileType ?? "html";
115
117
  const renderable =
@@ -3,7 +3,8 @@ import { assertAccess } from "@agent-native/core/sharing";
3
3
  import { eq } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
- import { getDb, schema } from "../server/db/index.js";
6
+ import { schema } from "../server/db/index.js";
7
+ import { withDesignSourceMutationTransaction } from "../server/source-workspace.js";
7
8
 
8
9
  export default defineAction({
9
10
  description:
@@ -14,40 +15,40 @@ export default defineAction({
14
15
  run: async ({ id }) => {
15
16
  await assertAccess("design", id, "admin");
16
17
 
17
- const db = getDb();
18
+ await withDesignSourceMutationTransaction(id, async (tx) => {
19
+ await tx
20
+ .delete(schema.designShares)
21
+ .where(eq(schema.designShares.resourceId, id));
18
22
 
19
- await db
20
- .delete(schema.designShares)
21
- .where(eq(schema.designShares.resourceId, id));
23
+ await tx
24
+ .delete(schema.componentIndex)
25
+ .where(eq(schema.componentIndex.designId, id));
22
26
 
23
- await db
24
- .delete(schema.componentIndex)
25
- .where(eq(schema.componentIndex.designId, id));
27
+ await tx
28
+ .delete(schema.motionTimeline)
29
+ .where(eq(schema.motionTimeline.designId, id));
26
30
 
27
- await db
28
- .delete(schema.motionTimeline)
29
- .where(eq(schema.motionTimeline.designId, id));
31
+ await tx
32
+ .delete(schema.designState)
33
+ .where(eq(schema.designState.designId, id));
30
34
 
31
- await db
32
- .delete(schema.designState)
33
- .where(eq(schema.designState.designId, id));
35
+ await tx
36
+ .delete(schema.designReviewSnapshot)
37
+ .where(eq(schema.designReviewSnapshot.designId, id));
34
38
 
35
- await db
36
- .delete(schema.designReviewSnapshot)
37
- .where(eq(schema.designReviewSnapshot.designId, id));
39
+ // Delete associated files first
40
+ await tx
41
+ .delete(schema.designFiles)
42
+ .where(eq(schema.designFiles.designId, id));
38
43
 
39
- // Delete associated files first
40
- await db
41
- .delete(schema.designFiles)
42
- .where(eq(schema.designFiles.designId, id));
44
+ // Delete associated versions
45
+ await tx
46
+ .delete(schema.designVersions)
47
+ .where(eq(schema.designVersions.designId, id));
43
48
 
44
- // Delete associated versions
45
- await db
46
- .delete(schema.designVersions)
47
- .where(eq(schema.designVersions.designId, id));
48
-
49
- // Delete the design itself
50
- await db.delete(schema.designs).where(eq(schema.designs.id, id));
49
+ // Delete the design itself
50
+ await tx.delete(schema.designs).where(eq(schema.designs.id, id));
51
+ });
51
52
 
52
53
  return { id, deleted: true };
53
54
  },
@@ -1,11 +1,19 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
2
  import { accessFilter, assertAccess } from "@agent-native/core/sharing";
3
- import { and, eq } from "drizzle-orm";
3
+ import { and, eq, sql } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
- import { mutateDesignData } from "../server/lib/design-data-mutation.js";
8
- import { snapshotDesignBeforeAgentEdit } from "../server/lib/design-versions.js";
7
+ import {
8
+ snapshotDesignBeforeAgentEditInVersionLock,
9
+ withDesignVersionLock,
10
+ } from "../server/lib/design-versions.js";
11
+ import {
12
+ affectedRowCount,
13
+ designSourceMutationLockKey,
14
+ lockDesignFilesTable,
15
+ } from "../server/source-workspace.js";
16
+ import { isOverviewScreenFile } from "../shared/design-files.js";
9
17
  import { countLockedLayers } from "../shared/locked-layers.js";
10
18
 
11
19
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -61,6 +69,27 @@ function pruneDeletedFileMetadata(
61
69
  };
62
70
  }
63
71
 
72
+ function nextUpdatedAt(current: string | null, now: Date): string {
73
+ const currentMs = current ? Date.parse(current) : Number.NaN;
74
+ return new Date(
75
+ Math.max(now.getTime(), Number.isFinite(currentMs) ? currentMs + 1 : 0),
76
+ ).toISOString();
77
+ }
78
+
79
+ function parseDesignData(
80
+ designId: string,
81
+ serialized: string | null,
82
+ ): Record<string, unknown> {
83
+ if (serialized === null) return {};
84
+ try {
85
+ const parsed: unknown = JSON.parse(serialized);
86
+ if (isRecord(parsed)) return parsed;
87
+ } catch {
88
+ throw new Error(`Design "${designId}" has invalid data JSON.`);
89
+ }
90
+ throw new Error(`Design "${designId}" has invalid data JSON.`);
91
+ }
92
+
64
93
  export default defineAction({
65
94
  description:
66
95
  "Delete a file from a design project. Idempotent: if the file is already gone, returns deleted=false so cleanup retries can continue. Validates ownership via the parent design's access when the file exists.",
@@ -73,8 +102,14 @@ export default defineAction({
73
102
  .describe(
74
103
  "Delete the screen even though it holds locked layers. Only set this when the user explicitly asked for that screen to go.",
75
104
  ),
105
+ historyCheckpointId: z
106
+ .string()
107
+ .optional()
108
+ .describe(
109
+ "Existing frontend editor checkpoint to reuse for one grouped screen deletion.",
110
+ ),
76
111
  }),
77
- run: async ({ id, allowLockedLayers }, context) => {
112
+ run: async ({ id, allowLockedLayers, historyCheckpointId }, context) => {
78
113
  const db = getDb();
79
114
 
80
115
  // Look up the file to get its designId for access check
@@ -82,6 +117,8 @@ export default defineAction({
82
117
  .select({
83
118
  id: schema.designFiles.id,
84
119
  designId: schema.designFiles.designId,
120
+ filename: schema.designFiles.filename,
121
+ fileType: schema.designFiles.fileType,
85
122
  content: schema.designFiles.content,
86
123
  })
87
124
  .from(schema.designFiles)
@@ -100,7 +137,6 @@ export default defineAction({
100
137
  if (!file) return { id, deleted: false, alreadyMissing: true };
101
138
 
102
139
  await assertAccess("design", file.designId, "editor");
103
- await snapshotDesignBeforeAgentEdit(file.designId, context);
104
140
  // Locks exist to stop an agent destroying template branding in passing.
105
141
  // A person deleting their own screen has already decided, and every
106
142
  // template-backed screen carries locked layers — without this opt-in they
@@ -111,25 +147,120 @@ export default defineAction({
111
147
  );
112
148
  }
113
149
 
114
- const pruneMetadata = () =>
115
- mutateDesignData({
116
- designId: file.designId,
117
- mutate: (current, { updatedAt }) => ({
118
- ...pruneDeletedFileMetadata(current, id),
119
- updatedAt,
120
- }),
121
- isApplied: (current) =>
122
- JSON.stringify(pruneDeletedFileMetadata(current, id)) ===
123
- JSON.stringify(current),
124
- });
150
+ if (historyCheckpointId !== undefined) {
151
+ if (context?.caller !== "frontend") {
152
+ throw new Error(
153
+ "A reusable editor history checkpoint is only valid for frontend deletes.",
154
+ );
155
+ }
156
+ const [checkpoint] = await db
157
+ .select({
158
+ id: schema.designVersions.id,
159
+ designId: schema.designVersions.designId,
160
+ })
161
+ .from(schema.designVersions)
162
+ .where(
163
+ and(
164
+ eq(schema.designVersions.id, historyCheckpointId),
165
+ eq(schema.designVersions.designId, file.designId),
166
+ ),
167
+ )
168
+ .limit(1);
169
+ if (!checkpoint) {
170
+ throw new Error(
171
+ "The editor history checkpoint is no longer available.",
172
+ );
173
+ }
174
+ }
175
+
176
+ // Restore locks the same design and updates file rows before designs.data.
177
+ // Keep the checkpoint and mutation under the same table/version boundary so
178
+ // history cannot capture a state that interleaves with the delete.
179
+ const deleted = await withDesignVersionLock(file.designId, async () => {
180
+ return db.transaction(async (tx) => {
181
+ await tx.execute(
182
+ sql`SELECT pg_advisory_xact_lock(hashtextextended(${designSourceMutationLockKey(file.designId)}, 0::bigint))`,
183
+ );
184
+ await lockDesignFilesTable(tx);
185
+ const currentFiles = await tx
186
+ .select({
187
+ id: schema.designFiles.id,
188
+ filename: schema.designFiles.filename,
189
+ fileType: schema.designFiles.fileType,
190
+ })
191
+ .from(schema.designFiles)
192
+ .where(eq(schema.designFiles.designId, file.designId))
193
+ .for("update");
194
+ const currentFile = currentFiles.find(
195
+ (candidate) => candidate.id === id,
196
+ );
197
+ if (!currentFile) return false;
198
+ if (
199
+ isOverviewScreenFile(currentFile) &&
200
+ currentFiles.filter(isOverviewScreenFile).length <= 1
201
+ ) {
202
+ throw new Error(
203
+ "A design must keep at least one user screen. Delete another screen first.",
204
+ );
205
+ }
206
+ if (historyCheckpointId === undefined) {
207
+ await snapshotDesignBeforeAgentEditInVersionLock(
208
+ file.designId,
209
+ context,
210
+ tx,
211
+ );
212
+ }
125
213
 
126
- // Prune before deletion so a retry never loses the parent design id, then
127
- // prune once more after deletion to close the small window where another
128
- // editor could have refreshed metadata for the still-existing file.
129
- await pruneMetadata();
130
- await db.delete(schema.designFiles).where(eq(schema.designFiles.id, id));
131
- await pruneMetadata();
214
+ const deleteResult = await tx
215
+ .delete(schema.designFiles)
216
+ .where(
217
+ and(
218
+ eq(schema.designFiles.id, id),
219
+ eq(schema.designFiles.designId, file.designId),
220
+ ),
221
+ );
222
+ const affected = affectedRowCount(deleteResult);
223
+ if (affected === 0) return false;
224
+ if (affected === undefined)
225
+ throw new Error("Could not verify that the design file was deleted.");
226
+ if (affected !== 1)
227
+ throw new Error("Unexpected design file delete result.");
228
+
229
+ const [design] = await tx
230
+ .select({
231
+ data: schema.designs.data,
232
+ updatedAt: schema.designs.updatedAt,
233
+ })
234
+ .from(schema.designs)
235
+ .where(eq(schema.designs.id, file.designId))
236
+ .for("update");
237
+ if (!design) throw new Error(`Design "${file.designId}" not found.`);
238
+
239
+ const updatedAt = nextUpdatedAt(design.updatedAt, new Date());
240
+ const data = pruneDeletedFileMetadata(
241
+ parseDesignData(file.designId, design.data),
242
+ id,
243
+ );
244
+ data.updatedAt = updatedAt;
245
+ const designUpdateResult = await tx
246
+ .update(schema.designs)
247
+ .set({ data: JSON.stringify(data), updatedAt })
248
+ .where(eq(schema.designs.id, file.designId));
249
+ const designAffected = affectedRowCount(designUpdateResult);
250
+ if (designAffected === undefined) {
251
+ throw new Error(
252
+ "Could not verify that the design metadata was updated.",
253
+ );
254
+ }
255
+ if (designAffected !== 1) {
256
+ throw new Error("Unexpected design metadata update result.");
257
+ }
258
+ return true;
259
+ });
260
+ });
132
261
 
133
- return { id, deleted: true };
262
+ return deleted
263
+ ? { id, deleted: true }
264
+ : { id, deleted: false, alreadyMissing: true };
134
265
  },
135
266
  });
@@ -34,6 +34,7 @@ import { snapshotDesignBeforeAgentEdit } from "../server/lib/design-versions.js"
34
34
  import {
35
35
  readLiveSourceFile,
36
36
  SourceWorkspaceEditConflictError,
37
+ withDesignSourceMutationTransaction,
37
38
  writeInlineSourceFile,
38
39
  type SourceWorkspaceFile,
39
40
  } from "../server/source-workspace.js";
@@ -910,10 +911,19 @@ const generateDesignAction = defineAction({
910
911
  // html -> jsx), matching the original update behavior.
911
912
  const nextFileType = file.fileType ?? "html";
912
913
  if (nextFileType !== (existing.fileType ?? "html")) {
913
- await db
914
- .update(schema.designFiles)
915
- .set({ fileType: nextFileType, updatedAt: now })
916
- .where(eq(schema.designFiles.id, existing.id));
914
+ await withDesignSourceMutationTransaction(
915
+ existing.designId,
916
+ (tx) =>
917
+ tx
918
+ .update(schema.designFiles)
919
+ .set({ fileType: nextFileType, updatedAt: now })
920
+ .where(
921
+ and(
922
+ eq(schema.designFiles.id, existing.id),
923
+ eq(schema.designFiles.designId, existing.designId),
924
+ ),
925
+ ),
926
+ );
917
927
  }
918
928
  } finally {
919
929
  agentLeaveDocument(existing.id);
@@ -956,18 +966,20 @@ const generateDesignAction = defineAction({
956
966
  } else {
957
967
  // Create new file
958
968
  const fileId = nanoid();
959
- await db.insert(schema.designFiles).values({
960
- id: fileId,
961
- designId,
962
- filename: file.filename,
963
- fileType: file.fileType ?? "html",
964
- content: file.content,
965
- contentOperationSource: null,
966
- contentOperationRevision: null,
967
- contentOperationResultHash: null,
968
- createdAt: now,
969
- updatedAt: now,
970
- });
969
+ await withDesignSourceMutationTransaction(designId, (tx) =>
970
+ tx.insert(schema.designFiles).values({
971
+ id: fileId,
972
+ designId,
973
+ filename: file.filename,
974
+ fileType: file.fileType ?? "html",
975
+ content: file.content,
976
+ contentOperationSource: null,
977
+ contentOperationRevision: null,
978
+ contentOperationResultHash: null,
979
+ createdAt: now,
980
+ updatedAt: now,
981
+ }),
982
+ );
971
983
 
972
984
  // Publish agent presence for the new file before seeding.
973
985
  agentEnterDocument(fileId);