@agent-native/core 0.84.10 → 0.84.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 (140) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +48 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +30 -0
  5. package/corpus/core/src/agent/production-agent.ts +34 -6
  6. package/corpus/core/src/agent/run-manager.ts +45 -2
  7. package/corpus/core/src/agent/run-store.ts +29 -6
  8. package/corpus/core/src/chat-threads/store.ts +2 -0
  9. package/corpus/core/src/cli/skills.ts +10 -6
  10. package/corpus/core/src/client/AssistantChat.tsx +40 -9
  11. package/corpus/core/src/client/agent-chat-adapter.ts +23 -2
  12. package/corpus/core/src/client/chat/run-recovery.tsx +52 -44
  13. package/corpus/core/src/client/chat/tool-call-display.tsx +7 -2
  14. package/corpus/core/src/client/require-session.tsx +20 -1
  15. package/corpus/core/src/client/sharing/ShareButton.tsx +9 -3
  16. package/corpus/core/src/collab/storage.ts +40 -5
  17. package/corpus/core/src/server/agent-chat-plugin.ts +1 -0
  18. package/corpus/core/src/server/auth.ts +6 -0
  19. package/corpus/core/src/shared/streaming-text-smoothing.ts +10 -5
  20. package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +7 -6
  21. package/corpus/templates/assets/AGENTS.md +15 -0
  22. package/corpus/templates/assets/actions/_helpers.ts +3 -1
  23. package/corpus/templates/assets/actions/create-generation-preset.ts +12 -1
  24. package/corpus/templates/assets/actions/generate-asset.ts +5 -1
  25. package/corpus/templates/assets/actions/generate-image-batch.ts +6 -1
  26. package/corpus/templates/assets/actions/generate-image.ts +18 -11
  27. package/corpus/templates/assets/actions/open-asset-picker.ts +4 -2
  28. package/corpus/templates/assets/actions/update-generation-preset.ts +16 -3
  29. package/corpus/templates/assets/app/i18n/zh-TW.ts +3 -0
  30. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  31. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +42 -7
  32. package/corpus/templates/assets/app/routes/library.tsx +4 -2
  33. package/corpus/templates/assets/changelog/2026-07-01-generation-presets-can-now-composite-your-brand-s-canonical-.md +6 -0
  34. package/corpus/templates/assets/changelog/2026-07-01-tagging-a-preset-now-briefs-the-agent-on-that-preset-s-aesth.md +6 -0
  35. package/corpus/templates/assets/server/lib/preset-chat-context.ts +118 -0
  36. package/corpus/templates/assets/server/plugins/agent-chat.ts +5 -0
  37. package/corpus/templates/assets/shared/api.ts +1 -0
  38. package/corpus/templates/clips/app/components/library/recording-card.tsx +14 -4
  39. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +10 -12
  40. package/corpus/templates/clips/app/hooks/use-library.ts +7 -4
  41. package/corpus/templates/clips/app/lib/recording-status.ts +32 -0
  42. package/corpus/templates/clips/changelog/2026-07-01-dictation-keeps-listening-through-natural-pauses-and-gives-c.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-01-redoing-a-paused-desktop-recording-no-longer-leaves-the-recording-controls-disabled.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-01-stalled-clip-uploads-now-update-in-the-library-and-surface-as-failed.md +6 -0
  45. package/corpus/templates/clips/desktop/src/lib/recorder.ts +36 -18
  46. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +99 -27
  47. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +4 -43
  48. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +3 -0
  49. package/corpus/templates/clips/desktop/src/styles.css +5 -48
  50. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +4 -5
  51. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +15 -6
  52. package/corpus/templates/design/AGENTS.md +8 -4
  53. package/corpus/templates/design/actions/create-design.ts +1 -0
  54. package/corpus/templates/design/actions/duplicate-design.ts +3 -1
  55. package/corpus/templates/design/actions/edit-design.ts +43 -13
  56. package/corpus/templates/design/actions/export-html.ts +4 -2
  57. package/corpus/templates/design/actions/export-pdf.ts +3 -1
  58. package/corpus/templates/design/actions/export-svg.ts +4 -2
  59. package/corpus/templates/design/actions/export-zip.ts +10 -3
  60. package/corpus/templates/design/actions/generate-design.ts +5 -1
  61. package/corpus/templates/design/actions/present-design-variants.ts +17 -12
  62. package/corpus/templates/design/actions/update-file.ts +68 -17
  63. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +3 -2
  64. package/corpus/templates/design/app/components/design/EditPanel.tsx +35 -3
  65. package/corpus/templates/design/app/components/design/MotionDock.tsx +46 -11
  66. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +80 -30
  67. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +60 -10
  68. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +2 -0
  69. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +4 -0
  70. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  71. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +2 -2
  72. package/corpus/templates/design/app/i18n-data.ts +1 -1
  73. package/corpus/templates/design/app/pages/DesignEditor.tsx +493 -109
  74. package/corpus/templates/design/app/pages/VisualEdit.tsx +4 -16
  75. package/corpus/templates/design/changelog/2026-07-01-org-members-can-see-design-projects-created-in-their-workspace.md +6 -0
  76. package/corpus/templates/design/e2e/helpers.ts +2 -2
  77. package/corpus/templates/design/server/plugins/db.ts +9 -0
  78. package/dist/agent/durable-background.d.ts +3 -0
  79. package/dist/agent/durable-background.d.ts.map +1 -1
  80. package/dist/agent/durable-background.js +19 -0
  81. package/dist/agent/durable-background.js.map +1 -1
  82. package/dist/agent/production-agent.d.ts +1 -0
  83. package/dist/agent/production-agent.d.ts.map +1 -1
  84. package/dist/agent/production-agent.js +29 -6
  85. package/dist/agent/production-agent.js.map +1 -1
  86. package/dist/agent/run-manager.d.ts.map +1 -1
  87. package/dist/agent/run-manager.js +40 -2
  88. package/dist/agent/run-manager.js.map +1 -1
  89. package/dist/agent/run-store.d.ts.map +1 -1
  90. package/dist/agent/run-store.js +28 -6
  91. package/dist/agent/run-store.js.map +1 -1
  92. package/dist/chat-threads/store.d.ts +1 -0
  93. package/dist/chat-threads/store.d.ts.map +1 -1
  94. package/dist/chat-threads/store.js +2 -0
  95. package/dist/chat-threads/store.js.map +1 -1
  96. package/dist/cli/skills.d.ts.map +1 -1
  97. package/dist/cli/skills.js +10 -6
  98. package/dist/cli/skills.js.map +1 -1
  99. package/dist/client/AssistantChat.d.ts +10 -0
  100. package/dist/client/AssistantChat.d.ts.map +1 -1
  101. package/dist/client/AssistantChat.js +26 -8
  102. package/dist/client/AssistantChat.js.map +1 -1
  103. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  104. package/dist/client/agent-chat-adapter.js +21 -2
  105. package/dist/client/agent-chat-adapter.js.map +1 -1
  106. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  107. package/dist/client/chat/run-recovery.js +15 -10
  108. package/dist/client/chat/run-recovery.js.map +1 -1
  109. package/dist/client/chat/tool-call-display.d.ts +1 -0
  110. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  111. package/dist/client/chat/tool-call-display.js +3 -2
  112. package/dist/client/chat/tool-call-display.js.map +1 -1
  113. package/dist/client/require-session.d.ts +14 -0
  114. package/dist/client/require-session.d.ts.map +1 -1
  115. package/dist/client/require-session.js +19 -1
  116. package/dist/client/require-session.js.map +1 -1
  117. package/dist/client/sharing/ShareButton.d.ts +4 -2
  118. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  119. package/dist/client/sharing/ShareButton.js +3 -1
  120. package/dist/client/sharing/ShareButton.js.map +1 -1
  121. package/dist/collab/routes.d.ts +2 -2
  122. package/dist/collab/storage.d.ts.map +1 -1
  123. package/dist/collab/storage.js +31 -5
  124. package/dist/collab/storage.js.map +1 -1
  125. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  126. package/dist/notifications/routes.d.ts +2 -2
  127. package/dist/observability/routes.d.ts +5 -5
  128. package/dist/progress/routes.d.ts +1 -1
  129. package/dist/resources/handlers.d.ts +3 -3
  130. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  131. package/dist/server/agent-chat-plugin.js +1 -1
  132. package/dist/server/agent-chat-plugin.js.map +1 -1
  133. package/dist/server/auth.d.ts.map +1 -1
  134. package/dist/server/auth.js +7 -0
  135. package/dist/server/auth.js.map +1 -1
  136. package/dist/server/transcribe-voice.d.ts +1 -1
  137. package/dist/shared/streaming-text-smoothing.d.ts.map +1 -1
  138. package/dist/shared/streaming-text-smoothing.js +8 -5
  139. package/dist/shared/streaming-text-smoothing.js.map +1 -1
  140. package/package.json +1 -1
@@ -139,20 +139,31 @@ export default defineAction({
139
139
  "`replace` adds the wrapper around the original text. For broad copy-only " +
140
140
  'changes such as translating all visible text, use `mode: "replace-file"` ' +
141
141
  "with `replacementContent`: the complete updated file content copied from " +
142
- "the snapshot with only the requested copy changed. Use `generate-design` " +
143
- "instead only for brand-new files or large structural rewrites.",
142
+ "the snapshot with only the requested copy changed. After a variant pick " +
143
+ "or any other selected-screen follow-up, pass the exact `fileId` from " +
144
+ '`get-design-snapshot` and use `mode: "replace-file"` when replacing ' +
145
+ "the representative placeholder with the full chosen direction. Use " +
146
+ "`generate-design` instead only for brand-new files.",
144
147
  schema: z
145
148
  .object({
146
149
  designId: z.string().describe("Design project ID"),
150
+ fileId: z
151
+ .string()
152
+ .optional()
153
+ .describe(
154
+ "Optional exact design file ID to edit. Use this after a variant pick or selected-screen snapshot; when provided, it wins over filename.",
155
+ ),
147
156
  filename: z
148
157
  .string()
149
- .default("index.html")
150
- .describe("File to edit (e.g. 'index.html')"),
158
+ .optional()
159
+ .describe(
160
+ "File to edit (e.g. 'index.html'). Defaults to index.html only when fileId is omitted.",
161
+ ),
151
162
  mode: z
152
163
  .enum(["search-replace", "replace-file"])
153
164
  .optional()
154
165
  .describe(
155
- "Defaults to search-replace. Use replace-file for broad copy-only edits like translating the whole page after reading get-design-snapshot.",
166
+ "Defaults to search-replace. Use replace-file for selected variant expansion or broad copy-only edits after reading get-design-snapshot.",
156
167
  ),
157
168
  edits: editBlocksSchema
158
169
  .optional()
@@ -164,7 +175,7 @@ export default defineAction({
164
175
  .min(1)
165
176
  .optional()
166
177
  .describe(
167
- "Complete updated file content. Use only with mode=replace-file for broad copy-only changes; preserve all HTML structure, CSS, scripts, and tweaks from get-design-snapshot.",
178
+ "Complete updated file content. Use only with mode=replace-file for selected variant expansion or broad copy-only changes; preserve all HTML structure, CSS, scripts, and tweaks from get-design-snapshot.",
168
179
  ),
169
180
  })
170
181
  .superRefine((value, ctx) => {
@@ -201,16 +212,31 @@ export default defineAction({
201
212
  });
202
213
  }
203
214
  }),
204
- run: async ({ designId, filename, edits, mode, replacementContent }) => {
215
+ run: async ({
216
+ designId,
217
+ fileId,
218
+ filename,
219
+ edits,
220
+ mode,
221
+ replacementContent,
222
+ }) => {
205
223
  await assertAccess("design", designId, "editor");
206
224
 
207
225
  const db = getDb();
208
226
  const now = new Date().toISOString();
227
+ const requestedFileId = fileId?.trim();
228
+ const targetFilename = requestedFileId
229
+ ? undefined
230
+ : filename?.trim() || "index.html";
231
+ const targetCondition = requestedFileId
232
+ ? eq(schema.designFiles.id, requestedFileId)
233
+ : eq(schema.designFiles.filename, targetFilename!);
209
234
 
210
- // Resolve the target file (access-scoped) by design + filename.
235
+ // Resolve the target file (access-scoped) by design + fileId or filename.
211
236
  const [file] = await db
212
237
  .select({
213
238
  id: schema.designFiles.id,
239
+ filename: schema.designFiles.filename,
214
240
  content: schema.designFiles.content,
215
241
  })
216
242
  .from(schema.designFiles)
@@ -221,14 +247,18 @@ export default defineAction({
221
247
  .where(
222
248
  and(
223
249
  eq(schema.designFiles.designId, designId),
224
- eq(schema.designFiles.filename, filename),
250
+ targetCondition,
225
251
  accessFilter(schema.designs, schema.designShares),
226
252
  ),
227
253
  )
228
254
  .limit(1);
229
255
 
230
256
  if (!file) {
231
- throw new Error(`File "${filename}" not found in design ${designId}`);
257
+ throw new Error(
258
+ requestedFileId
259
+ ? `File id "${requestedFileId}" not found in design ${designId}`
260
+ : `File "${targetFilename}" not found in design ${designId}`,
261
+ );
232
262
  }
233
263
 
234
264
  // Prefer live collab content so we edit in-flight changes, not a stale
@@ -265,13 +295,13 @@ export default defineAction({
265
295
  selection: firstSearch
266
296
  ? `[data-edit-target="${firstSearch.slice(0, 40)}"]`
267
297
  : null,
268
- editingFile: filename,
298
+ editingFile: file.filename,
269
299
  designId,
270
300
  });
271
301
  } else {
272
302
  agentUpdateSelection(file.id, {
273
303
  selection: null,
274
- editingFile: filename,
304
+ editingFile: file.filename,
275
305
  designId,
276
306
  });
277
307
  }
@@ -301,7 +331,7 @@ export default defineAction({
301
331
 
302
332
  return {
303
333
  designId,
304
- filename,
334
+ filename: file.filename,
305
335
  fileId: file.id,
306
336
  mode: resolvedMode,
307
337
  editsApplied: applied,
@@ -9,6 +9,7 @@ import {
9
9
  exportFilename,
10
10
  trySaveExportFile,
11
11
  } from "../server/lib/design-export.js";
12
+ import { isBoardFile } from "../shared/board-file.js";
12
13
  import "../server/db/index.js"; // ensure registerShareableResource runs
13
14
 
14
15
  export default defineAction({
@@ -31,12 +32,13 @@ export default defineAction({
31
32
  .select()
32
33
  .from(schema.designFiles)
33
34
  .where(eq(schema.designFiles.designId, id));
35
+ const exportFiles = files.filter((file) => !isBoardFile(file.filename));
34
36
 
35
- const html = buildStandaloneHtml({ title: row.title, files });
37
+ const html = buildStandaloneHtml({ title: row.title, files: exportFiles });
36
38
 
37
39
  const filename = exportFilename(row.title, "html");
38
40
  const saveResult = await trySaveExportFile(filename, html);
39
41
 
40
- return { html, filename, ...saveResult, fileCount: files.length };
42
+ return { html, filename, ...saveResult, fileCount: exportFiles.length };
41
43
  },
42
44
  });
@@ -4,6 +4,7 @@ import { eq } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import { isBoardFile } from "../shared/board-file.js";
7
8
  import "../server/db/index.js"; // ensure registerShareableResource runs
8
9
 
9
10
  export default defineAction({
@@ -27,6 +28,7 @@ export default defineAction({
27
28
  .select()
28
29
  .from(schema.designFiles)
29
30
  .where(eq(schema.designFiles.designId, id));
31
+ const exportFiles = files.filter((file) => !isBoardFile(file.filename));
30
32
 
31
33
  return {
32
34
  id: row.id,
@@ -34,7 +36,7 @@ export default defineAction({
34
36
  description: row.description,
35
37
  projectType: row.projectType,
36
38
  data: row.data ?? null,
37
- files: files.map((f) => ({
39
+ files: exportFiles.map((f) => ({
38
40
  id: f.id,
39
41
  filename: f.filename,
40
42
  fileType: f.fileType,
@@ -10,6 +10,7 @@ import {
10
10
  exportFilename,
11
11
  trySaveExportFile,
12
12
  } from "../server/lib/design-export.js";
13
+ import { isBoardFile } from "../shared/board-file.js";
13
14
  import "../server/db/index.js"; // ensure registerShareableResource runs
14
15
 
15
16
  export default defineAction({
@@ -45,8 +46,9 @@ export default defineAction({
45
46
  .select()
46
47
  .from(schema.designFiles)
47
48
  .where(eq(schema.designFiles.designId, id));
49
+ const exportFiles = files.filter((file) => !isBoardFile(file.filename));
48
50
 
49
- const html = buildStandaloneHtml({ title: row.title, files });
51
+ const html = buildStandaloneHtml({ title: row.title, files: exportFiles });
50
52
  const svg = buildSvgForeignObject({
51
53
  html,
52
54
  width,
@@ -56,6 +58,6 @@ export default defineAction({
56
58
  const filename = exportFilename(row.title, "svg");
57
59
  const saveResult = await trySaveExportFile(filename, svg);
58
60
 
59
- return { svg, filename, ...saveResult, fileCount: files.length };
61
+ return { svg, filename, ...saveResult, fileCount: exportFiles.length };
60
62
  },
61
63
  });
@@ -8,6 +8,7 @@ import {
8
8
  exportFilename,
9
9
  trySaveExportFile,
10
10
  } from "../server/lib/design-export.js";
11
+ import { isBoardFile } from "../shared/board-file.js";
11
12
  import "../server/db/index.js"; // ensure registerShareableResource runs
12
13
 
13
14
  const METADATA_ARCHIVE_DIR = "agent-native-metadata";
@@ -40,6 +41,7 @@ export default defineAction({
40
41
  .select()
41
42
  .from(schema.designFiles)
42
43
  .where(eq(schema.designFiles.designId, id));
44
+ const exportFiles = files.filter((file) => !isBoardFile(file.filename));
43
45
 
44
46
  // Dynamic import JSZip
45
47
  const JSZip = (await import("jszip")).default;
@@ -57,14 +59,14 @@ export default defineAction({
57
59
  "",
58
60
  "## Files",
59
61
  "",
60
- ...files.map((f) => `- ${f.filename} (${f.fileType})`),
62
+ ...exportFiles.map((f) => `- ${f.filename} (${f.fileType})`),
61
63
  ].join("\n");
62
64
 
63
65
  zip.file(`${METADATA_ARCHIVE_DIR}/README.md`, readme);
64
66
 
65
67
  // Preserve design-relative paths so exported HTML keeps working with
66
68
  // sibling CSS/assets. Strip traversal segments defensively for legacy rows.
67
- for (const [index, file] of files.entries()) {
69
+ for (const [index, file] of exportFiles.entries()) {
68
70
  const filename = safeArchivePath(
69
71
  file.filename,
70
72
  `design-file-${index + 1}.txt`,
@@ -84,6 +86,11 @@ export default defineAction({
84
86
  const filename = exportFilename(row.title, "zip");
85
87
  const saveResult = await trySaveExportFile(filename, zipBuffer);
86
88
 
87
- return { zipBase64, filename, ...saveResult, fileCount: files.length };
89
+ return {
90
+ zipBase64,
91
+ filename,
92
+ ...saveResult,
93
+ fileCount: exportFiles.length,
94
+ };
88
95
  },
89
96
  });
@@ -81,7 +81,8 @@ const generateDesignAgentParameters = {
81
81
  type: "string",
82
82
  description:
83
83
  "JSON array of files to save. Pass one compact, complete, renderable index.html first, e.g. " +
84
- '[{"filename":"index.html","fileType":"html","content":"<!doctype html>..."}].',
84
+ '[{"filename":"index.html","fileType":"html","content":"<!doctype html>..."}]. ' +
85
+ "Do not use generate-design to replace a selected variant screen after a variant pick; snapshot that fileId and use edit-design instead.",
85
86
  },
86
87
  designSystemId: {
87
88
  type: ["string", "null"],
@@ -117,6 +118,9 @@ const generateDesignAction = defineAction({
117
118
  "version then refine individual files with `edit-design` (search/replace) rather " +
118
119
  "than resending a big multi-file payload — a single oversized payload can get cut " +
119
120
  "off mid-stream and stall the turn. " +
121
+ "Do not use this action to replace a selected variant screen after a " +
122
+ "variant pick; call `get-design-snapshot` for the selected `fileId` and " +
123
+ "`edit-design` that same `fileId` instead. " +
120
124
  "Do not report a design as ready until this action succeeds. " +
121
125
  "When adding multiple screens or states, pass canvasFrames with filenames " +
122
126
  "and x/y/width/height so the new screens appear placed on the overview canvas.",
@@ -41,16 +41,19 @@ const FALLBACK_INSTRUCTIONS =
41
41
  "overview board. The chat shows one button per screen. Ask the user to pick " +
42
42
  "a screen by name if the inline buttons are not available; after they pick, " +
43
43
  "delete the other variant screens, call get-design-snapshot with fileId for " +
44
- "the kept screen, and continue from that one file in a bounded pass.";
44
+ "the kept screen, then call edit-design on that same fileId in a bounded pass. " +
45
+ 'Use mode "replace-file" when expanding the representative placeholder. ' +
46
+ "Do not call generate-design after a variant pick.";
45
47
 
46
48
  const VARIANT_PICK_SUBMIT_MESSAGE =
47
49
  "Use this design direction. Delete the other variant screens, call " +
48
50
  "get-design-snapshot with the selected option's fileId (or filename if " +
49
- "needed) for the kept file, then continue from that current HTML in a " +
50
- "bounded single-file pass. Prefer edit-design for refinements; if " +
51
- "generate-design is needed, update only the kept file compactly. Do not " +
52
- "generate a full multi-screen rewrite or resend a huge payload. Stop after " +
53
- "the first successful save.";
51
+ "needed) for the kept file, then call edit-design with that same fileId in " +
52
+ 'a bounded single-file pass. Use mode "replace-file" when replacing the ' +
53
+ "representative placeholder with the full chosen direction, or search/replace " +
54
+ "for smaller refinements. Do not call generate-design after a variant pick, " +
55
+ "do not create index.html, and do not resend a huge payload. Stop after the " +
56
+ "first successful edit-design save.";
54
57
 
55
58
  const variantSchema = z.object({
56
59
  id: z.string().min(1).describe("Stable variant id, e.g. 'minimal-focus'"),
@@ -421,9 +424,11 @@ export default defineAction({
421
424
  "exploration before follow-up refinement. After the user's choice, keep " +
422
425
  "the chosen screen, delete the other generated variant screens, and " +
423
426
  "call get-design-snapshot with fileId for the kept screen before " +
424
- "continuing from that single file in a bounded pass. Prefer edit-design " +
425
- "for refinements; if generate-design is needed, update only the kept file " +
426
- "compactly and stop after the first successful save. For complex apps, " +
427
+ "calling edit-design on that same fileId in a bounded pass. Use " +
428
+ '`mode: "replace-file"` when expanding the representative placeholder ' +
429
+ "into the full chosen direction. Do not call generate-design after a " +
430
+ "variant pick. Stop after the first successful edit-design save. For " +
431
+ "complex apps, " +
427
432
  "make each variant a " +
428
433
  "compact representative screen; pass concise labels/descriptions/features " +
429
434
  "and omit content when full HTML would be too large. Design will render " +
@@ -577,7 +582,7 @@ export default defineAction({
577
582
  await writeAppStateForCurrentTab("guided-questions", {
578
583
  title: prompt ?? "Pick a direction",
579
584
  description:
580
- "All options are on the board. Choose one to keep; I will delete the others, read only the kept screen, and continue from that single file in a bounded pass.",
585
+ "All options are on the board. Choose one to keep; I will delete the others, read only the kept screen, and edit that same file in a bounded pass.",
581
586
  submitLabel: "Use selected direction",
582
587
  submitMessage: VARIANT_PICK_SUBMIT_MESSAGE,
583
588
  skipLabel: "Show another set",
@@ -605,7 +610,7 @@ export default defineAction({
605
610
  value:
606
611
  `Keep "${screen.label}" (${screen.filename}, file id ${screen.id}) ` +
607
612
  `from variant set ${variantSetId}. Delete the other variant screens: ${otherScreens}. ` +
608
- `Then call get-design-snapshot with designId ${designId} and fileId ${screen.id} (filename ${screen.filename}) and continue from that current HTML in a bounded single-file pass. Prefer edit-design for refinements; if generate-design is needed, update only this kept file compactly. Do not generate a full multi-screen rewrite or resend a huge payload. Stop after the first successful save.`,
613
+ `Then call get-design-snapshot with designId ${designId} and fileId ${screen.id} (filename ${screen.filename}), then call edit-design with fileId ${screen.id} on that same kept file in a bounded single-file pass. Use mode "replace-file" when replacing the representative placeholder with the full chosen direction, or search/replace for smaller refinements. Do not call generate-design after this variant pick, do not create index.html, and do not resend a huge payload. Stop after the first successful edit-design save.`,
609
614
  };
610
615
  }),
611
616
  },
@@ -623,7 +628,7 @@ export default defineAction({
623
628
  embed: true,
624
629
  fallbackInstructions: FALLBACK_INSTRUCTIONS,
625
630
  nextRequiredAction:
626
- "Wait for the user to pick a screen in chat. Then delete the unchosen variant screens with delete-file, call get-design-snapshot with fileId for the chosen screen, and continue from that single file in a bounded pass. Prefer edit-design; if generate-design is needed, update only the kept file compactly and stop after the first successful save.",
631
+ 'Wait for the user to pick a screen in chat. Then delete the unchosen variant screens with delete-file, call get-design-snapshot with fileId for the chosen screen, and call edit-design with that same fileId in a bounded pass. Use mode "replace-file" when expanding the placeholder into the full chosen direction. Do not call generate-design after a variant pick. Stop after the first successful edit-design save.',
627
632
  };
628
633
  },
629
634
  link: ({ result }) => {
@@ -4,12 +4,24 @@ import {
4
4
  applyText,
5
5
  seedFromText,
6
6
  } from "@agent-native/core/collab";
7
+ import { isPostgres } from "@agent-native/core/db";
7
8
  import { accessFilter, assertAccess } from "@agent-native/core/sharing";
8
- import { and, eq } from "drizzle-orm";
9
+ import { and, eq, sql } from "drizzle-orm";
9
10
  import { z } from "zod";
10
11
 
11
12
  import { getDb, schema } from "../server/db/index.js";
12
13
 
14
+ function rowsAffected(result: unknown): number | undefined {
15
+ const candidate = result as {
16
+ rowsAffected?: unknown;
17
+ rowCount?: unknown;
18
+ changes?: unknown;
19
+ } | null;
20
+ const value =
21
+ candidate?.rowsAffected ?? candidate?.rowCount ?? candidate?.changes;
22
+ return typeof value === "number" ? value : undefined;
23
+ }
24
+
13
25
  export default defineAction({
14
26
  description:
15
27
  "Update an existing file in a design project. " +
@@ -70,13 +82,19 @@ export default defineAction({
70
82
 
71
83
  await assertAccess("design", file.designId, "editor");
72
84
 
73
- // Reject a rename that would collide with an existing filename in the same
74
- // design. The collision check and the write run in one transaction so they
75
- // can't be interleaved by a concurrent rename. (A DB-level UNIQUE index on
76
- // (designId, filename) would be the strongest guarantee but is a non-additive
77
- // schema change on existing data, so it's deferred.)
78
- await db.transaction(async (tx) => {
79
- if (filename !== undefined) {
85
+ const updates: Record<string, unknown> = { updatedAt: now };
86
+ if (content !== undefined) updates.content = content;
87
+ if (filename !== undefined) updates.filename = filename;
88
+ if (fileType !== undefined) updates.fileType = fileType;
89
+
90
+ if (filename !== undefined && isPostgres()) {
91
+ await db.transaction(async (tx) => {
92
+ // Postgres evaluates concurrent NOT EXISTS updates under MVCC, so a
93
+ // guarded UPDATE alone can still race. Serialize design-file renames in
94
+ // this rare path without using SQLite's fragile async savepoint wrapper.
95
+ await (
96
+ tx as unknown as { execute: (query: unknown) => Promise<unknown> }
97
+ ).execute(sql`LOCK TABLE design_files IN SHARE ROW EXCLUSIVE MODE`);
80
98
  const [collision] = await tx
81
99
  .select({ id: schema.designFiles.id })
82
100
  .from(schema.designFiles)
@@ -92,18 +110,51 @@ export default defineAction({
92
110
  `File "${filename}" already exists in design ${file.designId}`,
93
111
  );
94
112
  }
95
- }
113
+ await tx
114
+ .update(schema.designFiles)
115
+ .set(updates)
116
+ .where(eq(schema.designFiles.id, id));
117
+ });
118
+ } else {
119
+ // Reject colliding SQLite renames as part of the write. SQLite's local
120
+ // async transaction wrapper can fail under concurrent editor/collab writes,
121
+ // so keep this to one guarded UPDATE instead of a SELECT-then-UPDATE window.
122
+ const updateWhere =
123
+ filename === undefined
124
+ ? eq(schema.designFiles.id, id)
125
+ : and(
126
+ eq(schema.designFiles.id, id),
127
+ sql`NOT EXISTS (
128
+ SELECT 1 FROM design_files AS sibling
129
+ WHERE sibling.design_id = ${file.designId}
130
+ AND sibling.filename = ${filename}
131
+ AND sibling.id <> ${id}
132
+ )`,
133
+ );
96
134
 
97
- const updates: Record<string, unknown> = { updatedAt: now };
98
- if (content !== undefined) updates.content = content;
99
- if (filename !== undefined) updates.filename = filename;
100
- if (fileType !== undefined) updates.fileType = fileType;
101
-
102
- await tx
135
+ const updateResult = await db
103
136
  .update(schema.designFiles)
104
137
  .set(updates)
105
- .where(eq(schema.designFiles.id, id));
106
- });
138
+ .where(updateWhere);
139
+
140
+ if (filename !== undefined && rowsAffected(updateResult) === 0) {
141
+ const [collision] = await db
142
+ .select({ id: schema.designFiles.id })
143
+ .from(schema.designFiles)
144
+ .where(
145
+ and(
146
+ eq(schema.designFiles.designId, file.designId),
147
+ eq(schema.designFiles.filename, filename),
148
+ ),
149
+ )
150
+ .limit(1);
151
+ if (collision && collision.id !== id) {
152
+ throw new Error(
153
+ `File "${filename}" already exists in design ${file.designId}`,
154
+ );
155
+ }
156
+ }
157
+ }
107
158
 
108
159
  // Push content through the collab layer so live editors see the change
109
160
  if (content !== undefined && syncCollab) {
@@ -1811,14 +1811,15 @@ export function DesignCanvas({
1811
1811
  : "allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"
1812
1812
  }
1813
1813
  data-design-preview-iframe
1814
- data-screen-iframe-id={screenId ?? undefined}
1814
+ data-screen-iframe-id={
1815
+ boardSurface ? undefined : (screenId ?? undefined)
1816
+ }
1815
1817
  data-design-source-type={
1816
1818
  sourceType ??
1817
1819
  (externalPreviewUrl
1818
1820
  ? "localhost" // inferred — content is a URL
1819
1821
  : "inline")
1820
1822
  }
1821
- allowTransparency={transparentBackground || undefined}
1822
1823
  className="block h-full w-full border-0 bg-transparent"
1823
1824
  style={{
1824
1825
  background: iframeBackgroundColor,
@@ -138,7 +138,7 @@ import type { StatesPanelProps } from "./StatesPanel";
138
138
  import { TweaksPanelContent } from "./TweaksPanel";
139
139
  import type { ElementInfo } from "./types";
140
140
 
141
- export type InspectorTab = "design" | "tweaks";
141
+ export type InspectorTab = "design" | "tweaks" | "extensions";
142
142
 
143
143
  const MIXED_VALUE = "Mixed";
144
144
 
@@ -216,6 +216,7 @@ interface EditPanelProps {
216
216
  tweakValues?: Record<string, string | number | boolean>;
217
217
  onTweakChange?: (id: string, value: string | number | boolean) => void;
218
218
  onRequestTweaks?: (anchor: HTMLElement) => void;
219
+ extensionsPanel?: ReactNode;
219
220
  onStyleChange: (property: string, value: string) => void;
220
221
  onStylesChange?: (styles: Record<string, string>) => void;
221
222
  onExport?: (settings: ExportSettingsValue[]) => void;
@@ -283,6 +284,8 @@ interface EditPanelProps {
283
284
  defaultComponentName?: string;
284
285
  /** Code-inspection data for the "Inspect code" popover. */
285
286
  inspectCode?: InspectCodeData;
287
+ /** Optional compact AI edit controls for selected/local source elements. */
288
+ aiActions?: ReactNode;
286
289
  }
287
290
 
288
291
  /**
@@ -2960,10 +2963,12 @@ function InspectorTabsHeader({
2960
2963
  activeTab,
2961
2964
  onActiveTabChange,
2962
2965
  trailing,
2966
+ showExtensions,
2963
2967
  }: {
2964
2968
  activeTab: InspectorTab;
2965
2969
  onActiveTabChange: (tab: InspectorTab) => void;
2966
2970
  trailing?: ReactNode;
2971
+ showExtensions?: boolean;
2967
2972
  }) {
2968
2973
  const t = useT();
2969
2974
 
@@ -2986,6 +2991,14 @@ function InspectorTabsHeader({
2986
2991
  >
2987
2992
  {t("designEditor.tweaks")}
2988
2993
  </TabsTrigger>
2994
+ {showExtensions ? (
2995
+ <TabsTrigger
2996
+ value="extensions"
2997
+ className="h-6 rounded-md px-1.5 !text-[11px] font-semibold text-muted-foreground shadow-none transition-colors hover:text-foreground data-[state=active]:bg-[var(--design-editor-panel-raised-bg)] data-[state=active]:text-foreground data-[state=active]:shadow-none"
2998
+ >
2999
+ {"Extensions" /* i18n-ignore design inspector tab */}
3000
+ </TabsTrigger>
3001
+ ) : null}
2989
3002
  </TabsList>
2990
3003
  </Tabs>
2991
3004
  {trailing ? <div className="shrink-0">{trailing}</div> : null}
@@ -3282,7 +3295,7 @@ function CornerRadiusControl({
3282
3295
  value={radius}
3283
3296
  onChange={commitRadius}
3284
3297
  min={0}
3285
- precision={1}
3298
+ precision={0}
3286
3299
  />
3287
3300
  <Tooltip>
3288
3301
  <TooltipTrigger asChild>
@@ -3308,6 +3321,7 @@ function CornerRadiusControl({
3308
3321
  <>
3309
3322
  <AppearanceScrubField
3310
3323
  label={t("editPanel.labels.topLeft")}
3324
+ ariaLabel="Top left"
3311
3325
  icon={IconRadiusTopLeft}
3312
3326
  value={corners.topLeft}
3313
3327
  onChange={(value) =>
@@ -3321,6 +3335,7 @@ function CornerRadiusControl({
3321
3335
  />
3322
3336
  <AppearanceScrubField
3323
3337
  label={t("editPanel.labels.topRight")}
3338
+ ariaLabel="Top right"
3324
3339
  icon={IconRadiusTopRight}
3325
3340
  value={corners.topRight}
3326
3341
  onChange={(value) =>
@@ -3335,6 +3350,7 @@ function CornerRadiusControl({
3335
3350
  <span aria-hidden="true" />
3336
3351
  <AppearanceScrubField
3337
3352
  label={t("editPanel.labels.bottomLeft")}
3353
+ ariaLabel="Bottom left"
3338
3354
  icon={IconRadiusBottomLeft}
3339
3355
  value={corners.bottomLeft}
3340
3356
  onChange={(value) =>
@@ -3348,6 +3364,7 @@ function CornerRadiusControl({
3348
3364
  />
3349
3365
  <AppearanceScrubField
3350
3366
  label={t("editPanel.labels.bottomRight")}
3367
+ ariaLabel="Bottom right"
3351
3368
  icon={IconRadiusBottomRight}
3352
3369
  value={corners.bottomRight}
3353
3370
  onChange={(value) =>
@@ -3368,6 +3385,7 @@ function CornerRadiusControl({
3368
3385
 
3369
3386
  function AppearanceScrubField({
3370
3387
  label,
3388
+ ariaLabel,
3371
3389
  icon,
3372
3390
  value,
3373
3391
  onChange,
@@ -3379,6 +3397,7 @@ function AppearanceScrubField({
3379
3397
  precision,
3380
3398
  }: {
3381
3399
  label: string;
3400
+ ariaLabel?: string;
3382
3401
  icon: (props: { className?: string }) => ReactNode;
3383
3402
  value: number;
3384
3403
  onChange: (value: number) => void;
@@ -3392,7 +3411,7 @@ function AppearanceScrubField({
3392
3411
  return (
3393
3412
  <ScrubInput
3394
3413
  label={label}
3395
- ariaLabel={label}
3414
+ ariaLabel={ariaLabel ?? label}
3396
3415
  icon={icon}
3397
3416
  value={value}
3398
3417
  onChange={onChange}
@@ -6842,6 +6861,7 @@ export function EditPanel({
6842
6861
  tweakValues = {},
6843
6862
  onTweakChange,
6844
6863
  onRequestTweaks,
6864
+ extensionsPanel,
6845
6865
  onStyleChange,
6846
6866
  onStylesChange,
6847
6867
  onExport,
@@ -6858,6 +6878,7 @@ export function EditPanel({
6858
6878
  selectedElementAlreadyComponent = false,
6859
6879
  defaultComponentName = "Component",
6860
6880
  inspectCode,
6881
+ aiActions,
6861
6882
  }: EditPanelProps) {
6862
6883
  const t = useT();
6863
6884
  const [createComponentOpen, setCreateComponentOpen] = useState(false);
@@ -6950,6 +6971,7 @@ export function EditPanel({
6950
6971
  activeTab={activeTab}
6951
6972
  onActiveTabChange={handleActiveTabChange}
6952
6973
  trailing={headerTrailing}
6974
+ showExtensions={!!extensionsPanel}
6953
6975
  />
6954
6976
 
6955
6977
  {activeTab === "design" ? (
@@ -7058,6 +7080,12 @@ export function EditPanel({
7058
7080
  />
7059
7081
  )}
7060
7082
 
7083
+ {aiActions ? (
7084
+ <div className="border-b border-[var(--design-editor-control-border)] px-2 py-1.5">
7085
+ {aiActions}
7086
+ </div>
7087
+ ) : null}
7088
+
7061
7089
  {!inspectorElement && (
7062
7090
  <PageProperties
7063
7091
  styles={pageStyles}
@@ -7228,6 +7256,10 @@ export function EditPanel({
7228
7256
  />
7229
7257
  </div>
7230
7258
  </div>
7259
+ ) : activeTab === "extensions" && extensionsPanel ? (
7260
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
7261
+ {extensionsPanel}
7262
+ </div>
7231
7263
  ) : null}
7232
7264
  </div>
7233
7265
  );