@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
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 9518
34
+ - template files: 9525
@@ -212,6 +212,16 @@ function ClipsExtensionAuthBridge() {
212
212
  const targetExtensionId = extensionId;
213
213
 
214
214
  let cancelled = false;
215
+ let removeBridgeListener: (() => void) | null = null;
216
+
217
+ const completeExtensionSignIn = () => {
218
+ if (cancelled) return;
219
+ const cleaned = new URL(window.location.href);
220
+ cleaned.searchParams.delete("clipsExtensionAuth");
221
+ cleaned.searchParams.delete("clipsExtensionId");
222
+ window.history.replaceState(window.history.state, "", cleaned);
223
+ setShowAuthSuccess(true);
224
+ };
215
225
 
216
226
  async function sendSessionToExtension() {
217
227
  const runtime = (
@@ -219,7 +229,6 @@ function ClipsExtensionAuthBridge() {
219
229
  chrome?: { runtime?: ExternalChromeRuntime };
220
230
  }
221
231
  ).chrome?.runtime;
222
- if (!runtime?.sendMessage) return;
223
232
 
224
233
  const response = await fetch(appPath("/_agent-native/auth/session"), {
225
234
  credentials: "include",
@@ -233,21 +242,60 @@ function ClipsExtensionAuthBridge() {
233
242
  return;
234
243
  }
235
244
 
245
+ const message = {
246
+ source: "clips-auth-bridge",
247
+ kind: "session",
248
+ token: session.token,
249
+ email: session.email,
250
+ clipsBaseUrl: window.location.origin,
251
+ } as const;
252
+ const sendViaPageBridge = () => {
253
+ const onMessage = (event: MessageEvent) => {
254
+ if (
255
+ event.source !== window ||
256
+ event.origin !== window.location.origin
257
+ ) {
258
+ return;
259
+ }
260
+ const data = event.data as
261
+ | { source?: unknown; kind?: unknown; ok?: unknown }
262
+ | undefined;
263
+ if (
264
+ data?.source !== "clips-auth-bridge" ||
265
+ data.kind !== "session-result"
266
+ ) {
267
+ return;
268
+ }
269
+ removeBridgeListener?.();
270
+ removeBridgeListener = null;
271
+ if (data.ok === true) completeExtensionSignIn();
272
+ };
273
+ removeBridgeListener = () =>
274
+ window.removeEventListener("message", onMessage);
275
+ window.addEventListener("message", onMessage);
276
+ window.postMessage(message, window.location.origin);
277
+ };
278
+
279
+ if (!runtime?.sendMessage) {
280
+ sendViaPageBridge();
281
+ return;
282
+ }
283
+
236
284
  runtime.sendMessage(
237
285
  targetExtensionId,
238
286
  {
239
287
  type: "CLIPS_AUTH_SESSION",
240
- token: session.token,
241
- email: session.email,
242
- clipsBaseUrl: window.location.origin,
288
+ token: message.token,
289
+ email: message.email,
290
+ clipsBaseUrl: message.clipsBaseUrl,
243
291
  },
244
292
  (extensionResponse) => {
245
- if (cancelled || runtime.lastError || !extensionResponse?.ok) return;
246
- const cleaned = new URL(window.location.href);
247
- cleaned.searchParams.delete("clipsExtensionAuth");
248
- cleaned.searchParams.delete("clipsExtensionId");
249
- window.history.replaceState(window.history.state, "", cleaned);
250
- setShowAuthSuccess(true);
293
+ if (cancelled) return;
294
+ if (!runtime.lastError && extensionResponse?.ok) {
295
+ completeExtensionSignIn();
296
+ return;
297
+ }
298
+ sendViaPageBridge();
251
299
  },
252
300
  );
253
301
  }
@@ -255,6 +303,7 @@ function ClipsExtensionAuthBridge() {
255
303
  void sendSessionToExtension();
256
304
  return () => {
257
305
  cancelled = true;
306
+ removeBridgeListener?.();
258
307
  };
259
308
  }, [location.search]);
260
309
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-17
4
+ ---
5
+
6
+ The desktop popover now closes when you click away from it.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-17
4
+ ---
5
+
6
+ Self-hosted Clips extensions now finish sign-in reliably.
@@ -77,6 +77,13 @@ type PopupStartMessage = {
77
77
  settings?: Partial<ExtensionSettings>;
78
78
  };
79
79
 
80
+ type AuthSessionMessage = {
81
+ type: "CLIPS_AUTH_SESSION";
82
+ token?: string;
83
+ email?: string;
84
+ clipsBaseUrl?: string;
85
+ };
86
+
80
87
  type ExternalMessage =
81
88
  | {
82
89
  type: "CLIPS_CAPTURE_START";
@@ -93,12 +100,7 @@ type ExternalMessage =
93
100
  type: "CLIPS_CAPTURE_CANCEL";
94
101
  sessionId?: string;
95
102
  }
96
- | {
97
- type: "CLIPS_AUTH_SESSION";
98
- token?: string;
99
- email?: string;
100
- clipsBaseUrl?: string;
101
- };
103
+ | AuthSessionMessage;
102
104
 
103
105
  type ChromeTab = {
104
106
  id?: number;
@@ -2237,6 +2239,35 @@ async function handlePopupSignIn(message: {
2237
2239
  return { ok: true };
2238
2240
  }
2239
2241
 
2242
+ async function handleAuthSession(
2243
+ message: AuthSessionMessage,
2244
+ senderUrl?: string,
2245
+ ) {
2246
+ const settings = await readSettings();
2247
+ const clipsBaseUrl = normalizeBaseUrl(
2248
+ message.clipsBaseUrl ?? settings.clipsBaseUrl,
2249
+ );
2250
+ const senderOrigin = originOf(senderUrl);
2251
+ if (
2252
+ !senderOrigin ||
2253
+ senderOrigin !== originOf(clipsBaseUrl) ||
2254
+ senderOrigin !== originOf(settings.clipsBaseUrl)
2255
+ ) {
2256
+ return { ok: false, error: "Auth message came from the wrong origin." };
2257
+ }
2258
+ if (typeof message.token !== "string" || !message.token.trim()) {
2259
+ return { ok: false, error: "Missing auth token." };
2260
+ }
2261
+ await storageSet({ ...settings, clipsBaseUrl });
2262
+ await saveAuthSession({
2263
+ token: message.token,
2264
+ email: typeof message.email === "string" ? message.email : undefined,
2265
+ clipsBaseUrl,
2266
+ savedAt: nowIso(),
2267
+ });
2268
+ return { ok: true };
2269
+ }
2270
+
2240
2271
  function summarize(snapshot: {
2241
2272
  endedAt: string;
2242
2273
  consoleLogs: ConsoleLog[];
@@ -2734,25 +2765,7 @@ async function handleExternalMessage(
2734
2765
  }
2735
2766
 
2736
2767
  if (message.type === "CLIPS_AUTH_SESSION") {
2737
- const settings = await readSettings();
2738
- const clipsBaseUrl = normalizeBaseUrl(
2739
- message.clipsBaseUrl ?? settings.clipsBaseUrl,
2740
- );
2741
- const senderOrigin = originOf(sender?.url);
2742
- if (!senderOrigin || senderOrigin !== originOf(clipsBaseUrl)) {
2743
- return { ok: false, error: "Auth message came from the wrong origin." };
2744
- }
2745
- if (typeof message.token !== "string" || !message.token.trim()) {
2746
- return { ok: false, error: "Missing auth token." };
2747
- }
2748
- await storageSet({ ...settings, clipsBaseUrl });
2749
- await saveAuthSession({
2750
- token: message.token,
2751
- email: typeof message.email === "string" ? message.email : undefined,
2752
- clipsBaseUrl,
2753
- savedAt: nowIso(),
2754
- });
2755
- return { ok: true };
2768
+ return handleAuthSession(message, sender?.url);
2756
2769
  }
2757
2770
 
2758
2771
  if (message.type === "CLIPS_CAPTURE_START") {
@@ -2858,6 +2871,13 @@ async function dispatchRuntimeMessage(
2858
2871
  ): Promise<unknown> {
2859
2872
  const type = (message as { type?: unknown }).type;
2860
2873
 
2874
+ if (type === "CLIPS_AUTH_SESSION") {
2875
+ return handleAuthSession(
2876
+ message as AuthSessionMessage,
2877
+ sender?.tab?.url ?? sender?.url,
2878
+ );
2879
+ }
2880
+
2861
2881
  if (type === "CLIPS_DIAGNOSTIC_INTERACTION") {
2862
2882
  const tabId = sender?.tab?.id;
2863
2883
  const sessionId =
@@ -141,6 +141,49 @@
141
141
  }
142
142
  sendDiagnosticNavigation(data.url);
143
143
  });
144
+
145
+ window.addEventListener("message", (event) => {
146
+ if (event.source !== window || event.origin !== window.location.origin) {
147
+ return;
148
+ }
149
+ const data = event.data as
150
+ | {
151
+ source?: unknown;
152
+ kind?: unknown;
153
+ token?: unknown;
154
+ email?: unknown;
155
+ clipsBaseUrl?: unknown;
156
+ }
157
+ | undefined;
158
+ if (
159
+ data?.source !== "clips-auth-bridge" ||
160
+ data.kind !== "session" ||
161
+ typeof data.token !== "string" ||
162
+ typeof data.clipsBaseUrl !== "string"
163
+ ) {
164
+ return;
165
+ }
166
+ chrome.runtime.sendMessage(
167
+ {
168
+ type: "CLIPS_AUTH_SESSION",
169
+ token: data.token,
170
+ ...(typeof data.email === "string" ? { email: data.email } : {}),
171
+ clipsBaseUrl: data.clipsBaseUrl,
172
+ },
173
+ (response?: { ok?: boolean; error?: string }) => {
174
+ void chrome.runtime.lastError;
175
+ window.postMessage(
176
+ {
177
+ source: "clips-auth-bridge",
178
+ kind: "session-result",
179
+ ok: response?.ok === true,
180
+ ...(response?.error ? { error: response.error } : {}),
181
+ },
182
+ window.location.origin,
183
+ );
184
+ },
185
+ );
186
+ });
144
187
  window.postMessage(
145
188
  { source: "clips-diagnostic-history", kind: "request-token" },
146
189
  "*",
@@ -43,6 +43,8 @@ mod util;
43
43
  mod whisper_model;
44
44
  mod whisper_speech;
45
45
 
46
+ use std::time::Duration;
47
+
46
48
  use tauri::{Emitter, Manager};
47
49
 
48
50
  use clips::{position_popover, toggle_popover};
@@ -62,6 +64,12 @@ use util::{
62
64
  // should replace with their real icon.
63
65
  pub(crate) const TRAY_PNG: &[u8] = include_bytes!("../icons/tray.png");
64
66
 
67
+ const POPOVER_BLUR_GUARD: Duration = Duration::from_millis(1500);
68
+
69
+ fn popover_blur_delay(elapsed: Duration) -> Option<Duration> {
70
+ (elapsed < POPOVER_BLUR_GUARD).then(|| POPOVER_BLUR_GUARD - elapsed)
71
+ }
72
+
65
73
  fn present_popover(app: &tauri::AppHandle) {
66
74
  if let Some(window) = app.get_webview_window("popover") {
67
75
  set_capture_excluded(&window);
@@ -480,7 +488,7 @@ pub fn run() {
480
488
  }
481
489
 
482
490
  // Hide the popover on blur so it feels like a real menu-bar popover.
483
- // The 250ms guard is the important bit — during the tray-click
491
+ // The 1.5s guard is the important bit — during the tray-click
484
492
  // itself macOS briefly steals focus from the popover, which would
485
493
  // fire Focused(false) and hide the window we literally just showed.
486
494
  if let Some(window) = app.get_webview_window("popover") {
@@ -511,11 +519,37 @@ pub fn run() {
511
519
  let shown_at = app_handle
512
520
  .try_state::<PopoverShownAt>()
513
521
  .and_then(|s| s.0.lock().ok().and_then(|g| *g));
514
- let elapsed_ms = shown_at
515
- .map(|t| t.elapsed().as_millis())
516
- .unwrap_or(u128::MAX);
517
- dlog!("[clips-tray] popover blur, elapsed_ms={}", elapsed_ms);
518
- if elapsed_ms >= 1500 {
522
+ let elapsed = shown_at.map(|t| t.elapsed()).unwrap_or(Duration::MAX);
523
+ dlog!(
524
+ "[clips-tray] popover blur, elapsed_ms={}",
525
+ elapsed.as_millis()
526
+ );
527
+ if let Some(delay) = popover_blur_delay(elapsed) {
528
+ let Some(shown_at) = shown_at else {
529
+ return;
530
+ };
531
+ let delayed_handle = handle.clone();
532
+ let delayed_app_handle = app_handle.clone();
533
+ tauri::async_runtime::spawn(async move {
534
+ tokio::time::sleep(delay).await;
535
+ let still_current = delayed_app_handle
536
+ .try_state::<PopoverShownAt>()
537
+ .and_then(|state| {
538
+ state.0.lock().ok().map(|guard| *guard == Some(shown_at))
539
+ })
540
+ .unwrap_or(false);
541
+ if !still_current
542
+ || is_recording_active(&delayed_app_handle)
543
+ || !config::auto_hide_popover_enabled(&delayed_app_handle)
544
+ || delayed_handle.is_focused().unwrap_or(true)
545
+ {
546
+ return;
547
+ }
548
+ let _ = delayed_handle.hide();
549
+ clips::close_bubble_if_idle(&delayed_app_handle);
550
+ let _ = delayed_app_handle.emit("clips:popover-visible", false);
551
+ });
552
+ } else {
519
553
  let _ = handle.hide();
520
554
  clips::close_bubble_if_idle(&app_handle);
521
555
  let _ = app_handle.emit("clips:popover-visible", false);
@@ -623,3 +657,18 @@ pub fn run() {
623
657
  }
624
658
  });
625
659
  }
660
+
661
+ #[cfg(test)]
662
+ mod tests {
663
+ use super::{popover_blur_delay, POPOVER_BLUR_GUARD};
664
+ use std::time::Duration;
665
+
666
+ #[test]
667
+ fn retries_blur_until_guard_expires() {
668
+ assert_eq!(
669
+ popover_blur_delay(Duration::from_millis(400)),
670
+ Some(Duration::from_millis(1100))
671
+ );
672
+ assert_eq!(popover_blur_delay(POPOVER_BLUR_GUARD), None);
673
+ }
674
+ }
@@ -3320,6 +3320,7 @@ export const editorChromeBridgeScript: string = `"use strict";
3320
3320
  alignItems: parentStyles.alignItems,
3321
3321
  justifyContent: parentStyles.justifyContent,
3322
3322
  gap: parentStyles.gap,
3323
+ gridAutoFlow: parentStyles.gridAutoFlow,
3323
3324
  gridTemplateColumns: parentStyles.gridTemplateColumns,
3324
3325
  gridTemplateRows: parentStyles.gridTemplateRows,
3325
3326
  position: parentStyles.position
@@ -17,6 +17,7 @@ import {
17
17
  } from "../server/lib/design-data-mutation.js";
18
18
  import { snapshotDesignBeforeAgentEdit } from "../server/lib/design-versions.js";
19
19
  import { resolveLocalhostConnectionScope } from "../server/lib/localhost-connection.js";
20
+ import { withDesignSourceMutationTransaction } from "../server/source-workspace.js";
20
21
  import {
21
22
  mergeCanvasFramePlacements,
22
23
  parseCanvasFrameGeometryById,
@@ -938,7 +939,7 @@ export default defineAction({
938
939
  )
939
940
  ? preferredExisting
940
941
  : undefined;
941
- const existing =
942
+ let existing =
942
943
  matchingPreferredExisting ??
943
944
  routeCandidates.find((candidate) => {
944
945
  const frame = existingCanvasFrames[candidate.id];
@@ -997,27 +998,51 @@ export default defineAction({
997
998
  seenRouteRequestKeys.add(routeRequestKey);
998
999
 
999
1000
  if (existing) {
1000
- await db
1001
- .update(schema.designFiles)
1002
- .set({ content: url, fileType: "html", updatedAt: now })
1003
- .where(eq(schema.designFiles.id, existing.id));
1004
- if (await hasCollabState(existing.id)) {
1005
- await applyText(existing.id, url, "content", "agent");
1001
+ const updated = await withDesignSourceMutationTransaction(
1002
+ designId,
1003
+ async (tx) => {
1004
+ const [current] = await tx
1005
+ .select({ id: schema.designFiles.id })
1006
+ .from(schema.designFiles)
1007
+ .where(
1008
+ and(
1009
+ eq(schema.designFiles.id, existing!.id),
1010
+ eq(schema.designFiles.designId, designId),
1011
+ ),
1012
+ )
1013
+ .limit(1);
1014
+ if (!current) return false;
1015
+ await tx
1016
+ .update(schema.designFiles)
1017
+ .set({ content: url, fileType: "html", updatedAt: now })
1018
+ .where(eq(schema.designFiles.id, existing!.id));
1019
+ return true;
1020
+ },
1021
+ );
1022
+ if (updated) {
1023
+ if (await hasCollabState(existing.id)) {
1024
+ await applyText(existing.id, url, "content", "agent");
1025
+ } else {
1026
+ await seedFromText(existing.id, url);
1027
+ }
1006
1028
  } else {
1007
- await seedFromText(existing.id, url);
1029
+ existing = undefined;
1008
1030
  }
1009
- } else {
1031
+ }
1032
+ if (!existing) {
1010
1033
  for (let attempt = 0; ; attempt += 1) {
1011
1034
  try {
1012
- await db.insert(schema.designFiles).values({
1013
- id: fileId,
1014
- designId,
1015
- filename,
1016
- fileType: "html",
1017
- content: url,
1018
- createdAt: now,
1019
- updatedAt: now,
1020
- });
1035
+ await withDesignSourceMutationTransaction(designId, (tx) =>
1036
+ tx.insert(schema.designFiles).values({
1037
+ id: fileId,
1038
+ designId,
1039
+ filename,
1040
+ fileType: "html",
1041
+ content: url,
1042
+ createdAt: now,
1043
+ updatedAt: now,
1044
+ }),
1045
+ );
1021
1046
  await seedFromText(fileId, url);
1022
1047
  break;
1023
1048
  } catch (err) {
@@ -1048,10 +1073,17 @@ export default defineAction({
1048
1073
  routeUrlsMatch(winner.content, url)
1049
1074
  ) {
1050
1075
  fileId = winner.id;
1051
- await db
1052
- .update(schema.designFiles)
1053
- .set({ content: url, fileType: "html", updatedAt: now })
1054
- .where(eq(schema.designFiles.id, winner.id));
1076
+ await withDesignSourceMutationTransaction(designId, (tx) =>
1077
+ tx
1078
+ .update(schema.designFiles)
1079
+ .set({ content: url, fileType: "html", updatedAt: now })
1080
+ .where(
1081
+ and(
1082
+ eq(schema.designFiles.id, winner.id),
1083
+ eq(schema.designFiles.designId, designId),
1084
+ ),
1085
+ ),
1086
+ );
1055
1087
  if (await hasCollabState(winner.id)) {
1056
1088
  await applyText(winner.id, url, "content", "agent");
1057
1089
  } else {
@@ -1458,6 +1458,7 @@ export default defineAction({
1458
1458
 
1459
1459
  const workspace = await resolveSourceWorkspace(file.designId, {
1460
1460
  includeContent: true,
1461
+ includeBoard: true,
1461
1462
  });
1462
1463
  if (workspace.sourceType !== "inline") {
1463
1464
  return noWrite(
@@ -17,6 +17,7 @@ import {
17
17
  remapTemplateFileIds,
18
18
  templateFileDimensions,
19
19
  } from "../server/lib/design-template-data.js";
20
+ import { withDesignSourceMutationTransaction } from "../server/source-workspace.js";
20
21
  import { BOARD_FILENAME } from "../shared/board-file.js";
21
22
  import { getDesignTemplatePreset } from "../shared/design-template-presets.js";
22
23
  import { countLockedLayersAcrossFiles } from "../shared/locked-layers.js";
@@ -24,6 +25,10 @@ import { annotateScreenHtmlForPersist } from "../shared/screen-annotation.js";
24
25
  import { sourceContentHash } from "../shared/source-workspace.js";
25
26
  import getDesignSystem from "./get-design-system.js";
26
27
 
28
+ type DesignTransaction = Parameters<
29
+ Parameters<ReturnType<typeof getDb>["transaction"]>[0]
30
+ >[0];
31
+
27
32
  interface TemplateFile {
28
33
  id: string;
29
34
  filename: string;
@@ -168,39 +173,6 @@ export default defineAction({
168
173
  let targetExistingData: Record<string, unknown> = {};
169
174
  if (targetDesignId) {
170
175
  await assertAccess("design", targetDesignId, "editor");
171
- // The editor creates the board row on mount, so a design with nothing
172
- // drawn in it already has one file. Screens are what count as content.
173
- const [existingDesign] = await db
174
- .select({ data: schema.designs.data })
175
- .from(schema.designs)
176
- .where(eq(schema.designs.id, targetDesignId))
177
- .limit(1);
178
- if (typeof existingDesign?.data === "string") {
179
- try {
180
- const parsed = JSON.parse(existingDesign.data);
181
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
182
- targetExistingData = parsed as Record<string, unknown>;
183
- }
184
- } catch {
185
- // coercion-ok: unreadable prior data is replaced wholesale below,
186
- // which is the same outcome as the create path.
187
- }
188
- }
189
- const [existing] = await db
190
- .select({ id: schema.designFiles.id })
191
- .from(schema.designFiles)
192
- .where(
193
- and(
194
- eq(schema.designFiles.designId, targetDesignId),
195
- ne(schema.designFiles.filename, BOARD_FILENAME),
196
- ),
197
- )
198
- .limit(1);
199
- if (existing) {
200
- throw new Error(
201
- "Target design already has files. Templates only fill an empty design.",
202
- );
203
- }
204
176
  }
205
177
  const designId = targetDesignId ?? nanoid();
206
178
  const now = new Date().toISOString();
@@ -244,8 +216,45 @@ export default defineAction({
244
216
  content: annotateScreenHtmlForPersist(file.content, file.fileType),
245
217
  }));
246
218
 
247
- await db.transaction(async (tx) => {
219
+ const persist = async (tx: DesignTransaction) => {
248
220
  if (targetDesignId) {
221
+ // The editor creates the board row on mount, so a design with nothing
222
+ // drawn in it already has one file. Screens are what count as content.
223
+ const [existingDesign] = await tx
224
+ .select({ data: schema.designs.data })
225
+ .from(schema.designs)
226
+ .where(eq(schema.designs.id, targetDesignId))
227
+ .limit(1);
228
+ if (typeof existingDesign?.data === "string") {
229
+ try {
230
+ const parsed = JSON.parse(existingDesign.data);
231
+ if (
232
+ parsed &&
233
+ typeof parsed === "object" &&
234
+ !Array.isArray(parsed)
235
+ ) {
236
+ targetExistingData = parsed as Record<string, unknown>;
237
+ }
238
+ } catch {
239
+ // coercion-ok: unreadable prior data is replaced wholesale below,
240
+ // which is the same outcome as the create path.
241
+ }
242
+ }
243
+ const [existing] = await tx
244
+ .select({ id: schema.designFiles.id })
245
+ .from(schema.designFiles)
246
+ .where(
247
+ and(
248
+ eq(schema.designFiles.designId, targetDesignId),
249
+ ne(schema.designFiles.filename, BOARD_FILENAME),
250
+ ),
251
+ )
252
+ .limit(1);
253
+ if (existing) {
254
+ throw new Error(
255
+ "Target design already has files. Templates only fill an empty design.",
256
+ );
257
+ }
249
258
  await tx
250
259
  .update(schema.designs)
251
260
  .set({
@@ -285,7 +294,13 @@ export default defineAction({
285
294
  updatedAt: now,
286
295
  })),
287
296
  );
288
- });
297
+ };
298
+
299
+ if (targetDesignId) {
300
+ await withDesignSourceMutationTransaction(targetDesignId, persist);
301
+ } else {
302
+ await db.transaction(persist);
303
+ }
289
304
 
290
305
  return {
291
306
  id: designId,
@@ -0,0 +1,21 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { z } from "zod";
3
+
4
+ import { createDesignVersionSnapshot } from "../server/lib/design-versions.js";
5
+
6
+ export default defineAction({
7
+ description:
8
+ "Create one frontend editor history checkpoint before a grouped design mutation.",
9
+ agentTool: false,
10
+ mcpTool: false,
11
+ schema: z.object({
12
+ designId: z.string().describe("Design project ID"),
13
+ }),
14
+ run: async ({ designId }) => {
15
+ const version = await createDesignVersionSnapshot(designId, {
16
+ label: "Before editor screen delete",
17
+ chatContext: { surface: "editor", actionName: "delete-file" },
18
+ });
19
+ return version;
20
+ },
21
+ });