@agent-native/core 0.80.7 → 0.80.8

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 (55) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AssistantChat.tsx +70 -27
  5. package/corpus/core/src/client/analytics.ts +3 -1
  6. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +69 -16
  7. package/corpus/templates/content/changelog/2026-06-29-read-only-pages-no-longer-try-to-autosave-or-open-editor-onl.md +6 -0
  8. package/corpus/templates/design/AGENTS.md +4 -2
  9. package/corpus/templates/design/actions/edit-design.ts +6 -3
  10. package/corpus/templates/design/actions/generate-screens.ts +5 -3
  11. package/corpus/templates/design/actions/update-design.ts +47 -10
  12. package/corpus/templates/design/actions/update-file.ts +33 -8
  13. package/corpus/templates/design/actions/view-screen.ts +14 -0
  14. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +75 -17
  15. package/corpus/templates/design/app/components/design/EditPanel.tsx +121 -22
  16. package/corpus/templates/design/app/components/design/LayersPanel.tsx +51 -8
  17. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +148 -33
  18. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +11 -11
  19. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +27 -13
  20. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +3 -37
  21. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +18 -2
  22. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +15 -3
  23. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +4 -2
  24. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +18 -4
  25. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +10 -4
  26. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +448 -259
  27. package/corpus/templates/design/app/hooks/use-variant-flow.ts +176 -92
  28. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  29. package/corpus/templates/design/app/i18n-data.ts +10 -0
  30. package/corpus/templates/design/app/pages/DesignEditor.tsx +220 -40
  31. package/corpus/templates/design/changelog/2026-06-29-agent-edits-to-a-design-are-no-longer-occasionally-dropped-o.md +6 -0
  32. package/corpus/templates/design/changelog/2026-06-29-box-shadow-and-gradient-colors-keep-named-css-colors-like-re.md +6 -0
  33. package/corpus/templates/design/changelog/2026-06-29-cut-cmd-ctrl-x-now-removes-the-selected-element-to-the-clipb.md +6 -0
  34. package/corpus/templates/design/changelog/2026-06-29-deleting-with-multiple-layers-selected-in-the-layers-panel-n.md +6 -0
  35. package/corpus/templates/design/changelog/2026-06-29-duplicating-a-screen-now-gives-the-copy-its-own-layer-ids-so.md +6 -0
  36. package/corpus/templates/design/changelog/2026-06-29-hiding-and-re-showing-a-gradient-fill-now-preserves-each-sto.md +6 -0
  37. package/corpus/templates/design/changelog/2026-06-29-picking-a-design-direction-now-submits-to-the-agent-right-aw.md +6 -0
  38. package/corpus/templates/design/changelog/2026-06-29-style-changes-that-set-several-properties-at-once-fixed-size.md +6 -0
  39. package/corpus/templates/design/changelog/2026-06-29-the-editor-no-longer-crashes-when-a-screen-ends-up-with-dupl.md +6 -0
  40. package/corpus/templates/design/e2e/README.md +1 -0
  41. package/corpus/templates/design/e2e/global-setup.ts +13 -1
  42. package/corpus/templates/design/playwright.config.ts +10 -1
  43. package/corpus/templates/design/shared/color-utils.ts +159 -0
  44. package/corpus/templates/design/shared/generation-session.ts +1 -0
  45. package/dist/client/AssistantChat.d.ts.map +1 -1
  46. package/dist/client/AssistantChat.js +54 -24
  47. package/dist/client/AssistantChat.js.map +1 -1
  48. package/dist/client/analytics.d.ts.map +1 -1
  49. package/dist/client/analytics.js +2 -1
  50. package/dist/client/analytics.js.map +1 -1
  51. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  52. package/dist/notifications/routes.d.ts +2 -2
  53. package/dist/observability/routes.d.ts +8 -8
  54. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  55. package/package.json +1 -1
@@ -5,7 +5,9 @@ import {
5
5
  isEmbedAuthActive,
6
6
  } from "@agent-native/core/client";
7
7
  import { useQuery, useQueryClient } from "@tanstack/react-query";
8
- import { useEffect, useCallback, useState } from "react";
8
+ import { useEffect, useCallback, useRef, useState } from "react";
9
+
10
+ import { toast } from "@/hooks/use-toast";
9
11
 
10
12
  export const DESIGN_VARIANT_PICKED_EVENT = "agent-native-design-variant-picked";
11
13
 
@@ -100,6 +102,22 @@ export function useVariantFlow(designId: string | undefined) {
100
102
  const [standalonePick, setStandalonePick] = useState<StandalonePick | null>(
101
103
  null,
102
104
  );
105
+ // Re-entry latch so a double-click on "Use this direction" can't fire two
106
+ // generate-design writes + two pick messages.
107
+ const pickingRef = useRef(false);
108
+ // Set once a pick commits; keeps the 2s poll from re-showing the grid in the
109
+ // window before the server DELETE lands. Cleared once the server state is gone
110
+ // so a later, genuinely new variant set can still appear.
111
+ const pickedRef = useRef(false);
112
+
113
+ // Tracks the designId of a consumed variant set so stale in-flight poll
114
+ // responses with the same designId cannot re-open the grid, even if the
115
+ // server DELETE is delayed or the user picks while a poll is in-flight.
116
+ const consumedDesignIdRef = useRef<string | null>(null);
117
+ // Bounded suppression window (epoch ms). After it elapses we stop suppressing
118
+ // so a failed DELETE or a fast follow-up present-design-variants for the same
119
+ // design can't keep the picker hidden indefinitely.
120
+ const consumedUntilRef = useRef(0);
103
121
 
104
122
  const { data } = useQuery({
105
123
  queryKey: ["design-variants"],
@@ -121,24 +139,68 @@ export function useVariantFlow(designId: string | undefined) {
121
139
  });
122
140
 
123
141
  useEffect(() => {
124
- if (
125
- data?.variants &&
126
- data.variants.length > 0 &&
127
- data.designId === designId
142
+ const hasVariants = Boolean(
143
+ data?.variants && data.variants.length > 0 && data.designId === designId,
144
+ );
145
+ // Suppression after a pick covers two things: the brief window where a stale
146
+ // in-flight poll could re-open the just-picked grid, and the gap until the
147
+ // server DELETE lands. It is bounded in time so a failed DELETE (server state
148
+ // never clears) or a fast follow-up variant set for the same design can't
149
+ // keep the picker hidden forever.
150
+ const windowElapsed = Date.now() >= consumedUntilRef.current;
151
+ if ((consumedDesignIdRef.current || pickedRef.current) && windowElapsed) {
152
+ consumedDesignIdRef.current = null;
153
+ pickedRef.current = false;
154
+ } else if (
155
+ data?.designId &&
156
+ data.designId === consumedDesignIdRef.current
128
157
  ) {
129
- setState(data);
130
- } else {
158
+ // Re-arm early once the server confirms the consumed state is gone.
159
+ if (!hasVariants) consumedDesignIdRef.current = null;
131
160
  setState(null);
161
+ return;
162
+ } else if (pickedRef.current) {
163
+ if (!hasVariants) pickedRef.current = false;
164
+ setState(null);
165
+ return;
132
166
  }
167
+ setState(hasVariants && data ? data : null);
133
168
  }, [data, designId]);
134
169
 
135
- const clear = useCallback(() => {
136
- setState(null);
137
- qc.setQueryData(["design-variants"], null);
138
- fetch(agentNativePath("/_agent-native/application-state/design-variants"), {
139
- method: "DELETE",
140
- }).catch(() => {});
141
- }, [qc]);
170
+ const clear = useCallback(
171
+ async (consumedId?: string) => {
172
+ setState(null);
173
+ // Mark the designId as consumed before issuing the DELETE so any
174
+ // in-flight or subsequent polls with this designId are suppressed
175
+ // even if the network request is delayed.
176
+ if (consumedId) {
177
+ consumedDesignIdRef.current = consumedId;
178
+ consumedUntilRef.current = Date.now() + 6000;
179
+ }
180
+ qc.setQueryData(["design-variants"], null);
181
+ try {
182
+ const res = await fetch(
183
+ agentNativePath("/_agent-native/application-state/design-variants"),
184
+ { method: "DELETE" },
185
+ );
186
+ if (!res.ok) {
187
+ // DELETE failed — the consumed marker keeps the grid hidden client-side,
188
+ // but log so the issue is visible rather than silently swallowed.
189
+ console.warn(
190
+ "[use-variant-flow] Failed to clear design-variants state:",
191
+ res.status,
192
+ );
193
+ }
194
+ } catch (err) {
195
+ // Network error — same: log but don't re-show grid (consumed marker holds).
196
+ console.warn(
197
+ "[use-variant-flow] Error clearing design-variants state:",
198
+ err,
199
+ );
200
+ }
201
+ },
202
+ [qc],
203
+ );
142
204
 
143
205
  const dismissStandalonePick = useCallback(() => setStandalonePick(null), []);
144
206
 
@@ -147,95 +209,117 @@ export function useVariantFlow(designId: string | undefined) {
147
209
  if (!state || !designId) return;
148
210
  const chosen = state.variants.find((v) => v.id === variantId);
149
211
  if (!chosen) return;
150
-
151
- // Persist the chosen variant as the design's primary file via the
152
- // agent's own action endpoint so every host lands on the same design.
153
- let persisted = false;
212
+ if (pickingRef.current) return;
213
+ pickingRef.current = true;
214
+ pickedRef.current = true;
215
+ // Start the bounded suppression window so the just-picked grid stays hidden
216
+ // through the DELETE round-trip + a poll cycle, but not indefinitely.
217
+ consumedUntilRef.current = Date.now() + 6000;
154
218
  try {
155
- await callAction(
156
- "generate-design" as any,
157
- {
158
- designId,
159
- prompt: `User picked variant "${chosen.label}"`,
160
- files: [
161
- {
162
- filename: "index.html",
163
- content: chosen.content,
164
- fileType: "html",
165
- },
166
- ],
167
- } as any,
168
- );
169
- await Promise.all([
170
- qc.invalidateQueries({
171
- queryKey: ["action", "get-design", { id: designId }],
172
- }),
173
- qc.invalidateQueries({ queryKey: ["action", "get-design"] }),
174
- qc.invalidateQueries({ queryKey: ["action", "list-designs"] }),
175
- ]);
176
- persisted = true;
177
- if (typeof window !== "undefined") {
178
- window.dispatchEvent(
179
- new CustomEvent(DESIGN_VARIANT_PICKED_EVENT, {
180
- detail: { designId, content: chosen.content },
181
- }),
219
+ // Persist the chosen variant as the design's primary file via the
220
+ // agent's own action endpoint so every host lands on the same design.
221
+ let persisted = false;
222
+ try {
223
+ await callAction(
224
+ "generate-design" as any,
225
+ {
226
+ designId,
227
+ prompt: `User picked variant "${chosen.label}"`,
228
+ files: [
229
+ {
230
+ filename: "index.html",
231
+ content: chosen.content,
232
+ fileType: "html",
233
+ },
234
+ ],
235
+ } as any,
182
236
  );
237
+ await Promise.all([
238
+ qc.invalidateQueries({
239
+ queryKey: ["action", "get-design", { id: designId }],
240
+ }),
241
+ qc.invalidateQueries({ queryKey: ["action", "get-design"] }),
242
+ qc.invalidateQueries({ queryKey: ["action", "list-designs"] }),
243
+ ]);
244
+ persisted = true;
245
+ if (typeof window !== "undefined") {
246
+ window.dispatchEvent(
247
+ new CustomEvent(DESIGN_VARIANT_PICKED_EVENT, {
248
+ detail: { designId, content: chosen.content },
249
+ }),
250
+ );
251
+ }
252
+ } catch {
253
+ // Network error: show a visible error and keep the grid open so
254
+ // the user can retry rather than silently losing their choice.
255
+ toast({
256
+ title: "Couldn't save your pick",
257
+ description:
258
+ "Saving didn't finish. Try again or refresh and re-pick.",
259
+ variant: "destructive",
260
+ });
183
261
  }
184
- } catch {
185
- // Network error: report the choice below so the agent still records it;
186
- // the grid still clears so the user isn't stuck.
187
- }
188
262
 
189
- const refineHint = persisted
190
- ? `Its content has been saved as index.html. Continue refining from there if the user asks.`
191
- : `Saving the chosen variant did not complete. Ask the user whether to retry before refining it.`;
192
- const guardHint = persisted
193
- ? `Do not show further variants unless the user explicitly asks for "more options" or "alternatives".`
194
- : `Do not claim the design file was updated until generate-design succeeds.`;
263
+ const refineHint = persisted
264
+ ? `Its content has been saved as index.html. Continue refining from there if the user asks.`
265
+ : `Saving the chosen variant did not complete. Ask the user whether to retry before refining it.`;
266
+ const guardHint = persisted
267
+ ? `Do not show further variants unless the user explicitly asks for "more options" or "alternatives".`
268
+ : `Do not claim the design file was updated until generate-design succeeds.`;
195
269
 
196
- if (isEmbedAuthActive()) {
197
- // Embedded MCP host (ChatGPT / Claude): the pick rides the host chat
198
- // bridge straight into the conversation.
199
- sendToAgentChat({
200
- message: `I picked "${chosen.label}".`,
201
- context: [
202
- `The user chose variant "${chosen.label}" (id: ${chosen.id}) for design ${designId} inside the embedded Design app.`,
203
- refineHint,
204
- guardHint,
205
- ].join("\n"),
206
- submit: true,
207
- openSidebar: false,
208
- });
209
- } else if (isLinkOnlyHandoff()) {
210
- // Link-only host (CLI / Codex / Claude Code): no chat bridge — show a
211
- // copyable summary the user pastes back into their coding agent. The
212
- // card owns the clipboard write so its "Copied" state stays truthful.
213
- setStandalonePick({
214
- heading: "Direction selected",
215
- label: chosen.label,
216
- text: variantHandoffText(designId, chosen, persisted),
217
- });
218
- } else {
219
- // First-party app: post the pick to its own agent sidebar composer.
220
- sendToAgentChat({
221
- message: `I picked "${chosen.label}".`,
222
- context: [
223
- `The user chose variant "${chosen.label}" (id: ${chosen.id}) for design ${designId}.`,
224
- refineHint,
225
- guardHint,
226
- ].join("\n"),
227
- submit: false,
228
- });
229
- }
270
+ if (isEmbedAuthActive()) {
271
+ // Embedded MCP host (ChatGPT / Claude): the pick rides the host chat
272
+ // bridge straight into the conversation.
273
+ sendToAgentChat({
274
+ message: `I picked "${chosen.label}".`,
275
+ context: [
276
+ `The user chose variant "${chosen.label}" (id: ${chosen.id}) for design ${designId} inside the embedded Design app.`,
277
+ refineHint,
278
+ guardHint,
279
+ ].join("\n"),
280
+ submit: true,
281
+ openSidebar: false,
282
+ });
283
+ } else if (isLinkOnlyHandoff()) {
284
+ // Link-only host (CLI / Codex / Claude Code): no chat bridge — show a
285
+ // copyable summary the user pastes back into their coding agent. The
286
+ // card owns the clipboard write so its "Copied" state stays truthful.
287
+ setStandalonePick({
288
+ heading: "Direction selected",
289
+ label: chosen.label,
290
+ text: variantHandoffText(designId, chosen, persisted),
291
+ });
292
+ } else {
293
+ // First-party app: post the pick to its own agent sidebar composer.
294
+ sendToAgentChat({
295
+ message: `I picked "${chosen.label}".`,
296
+ context: [
297
+ `The user chose variant "${chosen.label}" (id: ${chosen.id}) for design ${designId}.`,
298
+ refineHint,
299
+ guardHint,
300
+ ].join("\n"),
301
+ submit: true,
302
+ });
303
+ }
230
304
 
231
- clear();
305
+ // Only clear the grid when the variant was successfully persisted.
306
+ // If saving failed, keep the grid open so the user can retry.
307
+ if (persisted) {
308
+ await clear(designId);
309
+ } else {
310
+ // Re-arm the picking latch so the user can try again.
311
+ pickedRef.current = false;
312
+ }
313
+ } finally {
314
+ pickingRef.current = false;
315
+ }
232
316
  },
233
317
  [state, designId, qc, clear],
234
318
  );
235
319
 
236
320
  const dismiss = useCallback(() => {
237
321
  const embedded = isEmbedAuthActive();
238
- clear();
322
+ void clear(designId);
239
323
  if (isLinkOnlyHandoff() && !isEmbedAuthActive()) {
240
324
  // No chat bridge to relay the dismissal — give the user a copyable note
241
325
  // so their coding agent doesn't wait on a pick that isn't coming.
@@ -252,7 +336,7 @@ export function useVariantFlow(designId: string | undefined) {
252
336
  submit: embedded,
253
337
  ...(embedded ? { openSidebar: false } : {}),
254
338
  });
255
- }, [clear]);
339
+ }, [clear, designId]);
256
340
 
257
341
  return { state, useVariant, dismiss, standalonePick, dismissStandalonePick };
258
342
  }
@@ -409,6 +409,7 @@ const messages = {
409
409
  propsPasted: "屬性已貼上",
410
410
  primitiveInsertFailed: "無法將該圖層新增到畫面",
411
411
  layerMoveFailed: "無法移動該圖層",
412
+ duplicateElementFailed: "無法複製該元素",
412
413
  saveCopyError: "無法儲存這個設計的副本",
413
414
  },
414
415
  },
@@ -421,6 +421,7 @@ const enUS = {
421
421
  propsPasted: "Properties pasted",
422
422
  primitiveInsertFailed: "Could not add that layer to the screen",
423
423
  layerMoveFailed: "Could not move that layer",
424
+ duplicateElementFailed: "Could not duplicate that element",
424
425
  saveCopyError: "Could not save a copy of this design",
425
426
  },
426
427
  },
@@ -8104,6 +8105,7 @@ const designCanvasFeatureOverrides = {
8104
8105
  propsPasted: "属性已粘贴",
8105
8106
  primitiveInsertFailed: "无法将该图层添加到画面",
8106
8107
  layerMoveFailed: "无法移动该图层",
8108
+ duplicateElementFailed: "无法复制该元素",
8107
8109
  },
8108
8110
  },
8109
8111
  editPanel: {
@@ -8162,6 +8164,7 @@ const designCanvasFeatureOverrides = {
8162
8164
  propsPasted: "Propiedades pegadas",
8163
8165
  primitiveInsertFailed: "No se pudo añadir esa capa a la pantalla",
8164
8166
  layerMoveFailed: "No se pudo mover esa capa",
8167
+ duplicateElementFailed: "No se pudo duplicar ese elemento",
8165
8168
  },
8166
8169
  },
8167
8170
  editPanel: {
@@ -8220,6 +8223,7 @@ const designCanvasFeatureOverrides = {
8220
8223
  propsPasted: "Propriétés collées",
8221
8224
  primitiveInsertFailed: "Impossible d’ajouter ce calque à l’écran",
8222
8225
  layerMoveFailed: "Impossible de déplacer ce calque",
8226
+ duplicateElementFailed: "Impossible de dupliquer cet élément",
8223
8227
  },
8224
8228
  },
8225
8229
  editPanel: {
@@ -8279,6 +8283,7 @@ const designCanvasFeatureOverrides = {
8279
8283
  primitiveInsertFailed:
8280
8284
  "Diese Ebene konnte nicht zur Ansicht hinzugefügt werden",
8281
8285
  layerMoveFailed: "Diese Ebene konnte nicht verschoben werden",
8286
+ duplicateElementFailed: "Dieses Element konnte nicht dupliziert werden",
8282
8287
  },
8283
8288
  },
8284
8289
  editPanel: {
@@ -8337,6 +8342,7 @@ const designCanvasFeatureOverrides = {
8337
8342
  propsPasted: "プロパティを貼り付けました",
8338
8343
  primitiveInsertFailed: "そのレイヤーを画面に追加できませんでした",
8339
8344
  layerMoveFailed: "そのレイヤーを移動できませんでした",
8345
+ duplicateElementFailed: "その要素を複製できませんでした",
8340
8346
  },
8341
8347
  },
8342
8348
  editPanel: {
@@ -8395,6 +8401,7 @@ const designCanvasFeatureOverrides = {
8395
8401
  propsPasted: "속성이 붙여넣어짐",
8396
8402
  primitiveInsertFailed: "해당 레이어를 화면에 추가할 수 없습니다",
8397
8403
  layerMoveFailed: "해당 레이어를 이동할 수 없습니다",
8404
+ duplicateElementFailed: "해당 요소를 복제할 수 없습니다",
8398
8405
  },
8399
8406
  },
8400
8407
  editPanel: {
@@ -8453,6 +8460,7 @@ const designCanvasFeatureOverrides = {
8453
8460
  propsPasted: "Propriedades coladas",
8454
8461
  primitiveInsertFailed: "Não foi possível adicionar essa camada à tela",
8455
8462
  layerMoveFailed: "Não foi possível mover essa camada",
8463
+ duplicateElementFailed: "Não foi possível duplicar esse elemento",
8456
8464
  },
8457
8465
  },
8458
8466
  editPanel: {
@@ -8511,6 +8519,7 @@ const designCanvasFeatureOverrides = {
8511
8519
  propsPasted: "गुण चिपकाए गए",
8512
8520
  primitiveInsertFailed: "उस परत को स्क्रीन में नहीं जोड़ा जा सका",
8513
8521
  layerMoveFailed: "उस परत को स्थानांतरित नहीं किया जा सका",
8522
+ duplicateElementFailed: "उस तत्व की प्रतिलिपि नहीं बनाई जा सकी",
8514
8523
  },
8515
8524
  },
8516
8525
  editPanel: {
@@ -8569,6 +8578,7 @@ const designCanvasFeatureOverrides = {
8569
8578
  propsPasted: "تم لصق الخصائص",
8570
8579
  primitiveInsertFailed: "تعذرت إضافة تلك الطبقة إلى الشاشة",
8571
8580
  layerMoveFailed: "تعذر نقل تلك الطبقة",
8581
+ duplicateElementFailed: "تعذّر تكرار هذا العنصر",
8572
8582
  },
8573
8583
  },
8574
8584
  editPanel: {