@agent-native/core 0.80.9 → 0.80.11

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 (148) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/cli/design-connect.ts +28 -2
  6. package/corpus/core/src/cli/skills.ts +45 -24
  7. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  8. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  9. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  10. package/corpus/templates/assets/AGENTS.md +4 -0
  11. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  12. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  13. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  14. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  15. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  16. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  17. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  18. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  19. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  20. package/corpus/templates/assets/app/routes/library.tsx +145 -38
  21. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  22. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  23. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  24. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  25. package/corpus/templates/assets/shared/api.ts +4 -0
  26. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  27. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  28. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
  29. package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
  30. package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
  31. package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
  32. package/corpus/templates/design/actions/apply-component-prop-edit.ts +441 -0
  33. package/corpus/templates/design/actions/apply-design-state.ts +213 -0
  34. package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
  35. package/corpus/templates/design/actions/apply-motion-edit.ts +413 -0
  36. package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
  37. package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
  38. package/corpus/templates/design/actions/capture-design-state.ts +224 -0
  39. package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
  40. package/corpus/templates/design/actions/create-component.ts +447 -0
  41. package/corpus/templates/design/actions/create-design-branch.ts +263 -0
  42. package/corpus/templates/design/actions/create-design-state.ts +162 -0
  43. package/corpus/templates/design/actions/delete-design-state.ts +55 -0
  44. package/corpus/templates/design/actions/delete-design.ts +20 -0
  45. package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
  46. package/corpus/templates/design/actions/get-component-details.ts +251 -0
  47. package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
  48. package/corpus/templates/design/actions/get-design-review.ts +314 -0
  49. package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
  50. package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
  51. package/corpus/templates/design/actions/index-components.ts +258 -0
  52. package/corpus/templates/design/actions/index-design-tokens.ts +277 -0
  53. package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
  54. package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
  55. package/corpus/templates/design/actions/list-design-states.ts +90 -0
  56. package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
  57. package/corpus/templates/design/actions/open-component-source.ts +242 -0
  58. package/corpus/templates/design/actions/preview-component-prop-edit.ts +260 -0
  59. package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
  60. package/corpus/templates/design/actions/preview-shader-fill.ts +187 -0
  61. package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
  62. package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
  63. package/corpus/templates/design/actions/run-design-audit.ts +436 -0
  64. package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
  65. package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
  66. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1550 -82
  67. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +649 -43
  68. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
  69. package/corpus/templates/design/app/components/design/EditPanel.tsx +1788 -52
  70. package/corpus/templates/design/app/components/design/LayersPanel.tsx +114 -33
  71. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
  72. package/corpus/templates/design/app/components/design/MotionDock.tsx +1071 -0
  73. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1053 -99
  74. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +766 -0
  75. package/corpus/templates/design/app/components/design/StatesPanel.tsx +581 -0
  76. package/corpus/templates/design/app/components/design/TokensPanel.tsx +578 -0
  77. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
  78. package/corpus/templates/design/app/components/design/index.ts +16 -0
  79. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
  80. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
  81. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
  82. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
  83. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  84. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
  85. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
  86. package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
  87. package/corpus/templates/design/app/i18n-data.ts +280 -0
  88. package/corpus/templates/design/app/pages/DesignEditor.tsx +3087 -490
  89. package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
  90. package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
  91. package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
  92. package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
  93. package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
  94. package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
  95. package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
  96. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
  97. package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
  98. package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
  99. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  100. package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
  101. package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
  102. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
  103. package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
  104. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  105. package/corpus/templates/design/e2e/helpers.ts +10 -4
  106. package/corpus/templates/design/server/db/schema.ts +123 -0
  107. package/corpus/templates/design/server/plugins/db.ts +90 -0
  108. package/corpus/templates/design/shared/builder-app.ts +297 -0
  109. package/corpus/templates/design/shared/capability-resolver.ts +123 -0
  110. package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
  111. package/corpus/templates/design/shared/code-layer.ts +1016 -71
  112. package/corpus/templates/design/shared/component-model.ts +274 -0
  113. package/corpus/templates/design/shared/design-review.ts +275 -0
  114. package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
  115. package/corpus/templates/design/shared/design-state.ts +112 -0
  116. package/corpus/templates/design/shared/design-surface-index.ts +258 -0
  117. package/corpus/templates/design/shared/motion-compiler.ts +349 -0
  118. package/corpus/templates/design/shared/motion-timeline.ts +193 -0
  119. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  120. package/corpus/templates/design/shared/responsive-classes.ts +452 -0
  121. package/corpus/templates/design/shared/shader-fill.ts +323 -0
  122. package/corpus/templates/design/shared/source-mode.ts +245 -0
  123. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
  124. package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
  125. package/dist/agent/production-agent.d.ts.map +1 -1
  126. package/dist/agent/production-agent.js +94 -10
  127. package/dist/agent/production-agent.js.map +1 -1
  128. package/dist/cli/design-connect.d.ts.map +1 -1
  129. package/dist/cli/design-connect.js +28 -2
  130. package/dist/cli/design-connect.js.map +1 -1
  131. package/dist/cli/skills.d.ts.map +1 -1
  132. package/dist/cli/skills.js +37 -21
  133. package/dist/cli/skills.js.map +1 -1
  134. package/dist/client/AssistantChat.d.ts +14 -0
  135. package/dist/client/AssistantChat.d.ts.map +1 -1
  136. package/dist/client/AssistantChat.js +115 -22
  137. package/dist/client/AssistantChat.js.map +1 -1
  138. package/dist/client/sse-event-processor.d.ts.map +1 -1
  139. package/dist/client/sse-event-processor.js +10 -0
  140. package/dist/client/sse-event-processor.js.map +1 -1
  141. package/dist/collab/routes.d.ts +1 -1
  142. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  143. package/dist/notifications/routes.d.ts +2 -2
  144. package/dist/observability/routes.d.ts +8 -8
  145. package/dist/resources/handlers.d.ts +2 -2
  146. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  147. package/dist/server/transcribe-voice.d.ts +1 -1
  148. package/package.json +1 -1
@@ -1,21 +1,32 @@
1
+ // i18n-raw-literal-disable-file — new Design Studio panel; UI strings are localized when this feature is finalized in the follow-up PR.
1
2
  import {
2
3
  PromptComposer,
3
4
  agentNativePath,
4
5
  sendToAgentChat,
6
+ useActionMutation,
5
7
  useChangeVersions,
6
8
  useT,
7
9
  type PromptComposerProps,
8
10
  type PromptComposerSubmitOptions,
9
11
  } from "@agent-native/core/client";
10
12
  import { EmbeddedExtension } from "@agent-native/core/client/extensions";
13
+ import {
14
+ EmbeddedApp,
15
+ type EmbeddedAppRef,
16
+ } from "@agent-native/core/embedding/react";
17
+ import type { ShaderDescriptor } from "@shared/shader-presets";
11
18
  import {
12
19
  IconExternalLink,
20
+ IconLock,
21
+ IconPalette,
22
+ IconPhoto,
23
+ IconPlayerPlay,
13
24
  IconPlus,
14
25
  IconPuzzle,
15
26
  IconSparkles,
16
27
  } from "@tabler/icons-react";
17
28
  import { useQuery, useQueryClient } from "@tanstack/react-query";
18
- import { useMemo, useState } from "react";
29
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
19
30
  import { toast } from "sonner";
20
31
 
21
32
  import { Button } from "@/components/ui/button";
@@ -32,10 +43,13 @@ import {
32
43
  } from "@/components/ui/tooltip";
33
44
  import { cn } from "@/lib/utils";
34
45
 
46
+ import { ShaderFillsPanel } from "./inspector/ShaderFillsPanel";
35
47
  import type { ElementInfo } from "./types";
36
48
 
37
49
  export const DESIGN_EDITOR_EXTENSION_SLOT_ID = "design.editor.inspector";
38
50
 
51
+ // ─── Types ──────────────────────────────────────────────────────────────────
52
+
39
53
  interface SlotInstall {
40
54
  installId: string;
41
55
  extensionId: string;
@@ -72,6 +86,8 @@ export interface DesignExtensionSlotContext extends Record<string, unknown> {
72
86
  mode: string;
73
87
  activeTool: string;
74
88
  tweakValues: Record<string, string | number | boolean>;
89
+ onShaderFillPreview?: (descriptor: ShaderDescriptor, css: string) => void;
90
+ onShaderFillPreviewClear?: () => void;
75
91
  }
76
92
 
77
93
  interface DesignExtensionsPanelProps {
@@ -81,6 +97,55 @@ interface DesignExtensionsPanelProps {
81
97
 
82
98
  type PromptComposerSubmitHandler = PromptComposerProps["onSubmit"];
83
99
 
100
+ // ─── First-party extension ids ───────────────────────────────────────────────
101
+
102
+ type FirstPartyExtId =
103
+ | "design.asset-library"
104
+ | "design.shader-fills"
105
+ | "design.token-auditor"
106
+ | "design.motion-presets";
107
+
108
+ // ─── Assets picker types (mirrors PromptDialog) ───────────────────────────
109
+
110
+ const DEFAULT_ASSETS_PICKER_URL = "https://assets.agent-native.com/picker";
111
+
112
+ interface PickedAssetPayload {
113
+ url?: unknown;
114
+ previewUrl?: unknown;
115
+ downloadUrl?: unknown;
116
+ embedUrl?: unknown;
117
+ altText?: unknown;
118
+ title?: unknown;
119
+ assetId?: unknown;
120
+ mimeType?: unknown;
121
+ }
122
+
123
+ function assetsPickerUrl(): string {
124
+ return (
125
+ (typeof import.meta !== "undefined" &&
126
+ (import.meta as { env?: Record<string, string> }).env
127
+ ?.VITE_AGENT_NATIVE_ASSETS_PICKER_URL) ||
128
+ DEFAULT_ASSETS_PICKER_URL
129
+ );
130
+ }
131
+
132
+ function pickedAssetString(value: unknown): string | null {
133
+ return typeof value === "string" && value.trim() ? value.trim() : null;
134
+ }
135
+
136
+ function pickedAssetImageSource(payload: unknown): string | null {
137
+ if (!payload || typeof payload !== "object") return null;
138
+ const p = payload as PickedAssetPayload;
139
+ return (
140
+ pickedAssetString(p.url) ??
141
+ pickedAssetString(p.previewUrl) ??
142
+ pickedAssetString(p.downloadUrl) ??
143
+ pickedAssetString(p.embedUrl)
144
+ );
145
+ }
146
+
147
+ // ─── Build extension create context ──────────────────────────────────────────
148
+
84
149
  function buildExtensionCreateContext(
85
150
  prompt: string,
86
151
  context: DesignExtensionSlotContext,
@@ -131,6 +196,8 @@ function buildExtensionCreateContext(
131
196
  ].join("\n");
132
197
  }
133
198
 
199
+ // ─── Hooks ───────────────────────────────────────────────────────────────────
200
+
134
201
  function useSlotInstalls(slotId: string) {
135
202
  const versions = useChangeVersions(["action"]);
136
203
  return useQuery<SlotInstall[]>({
@@ -165,6 +232,479 @@ function useAvailableExtensions(slotId: string) {
165
232
  });
166
233
  }
167
234
 
235
+ // ─── First-party extension rows ───────────────────────────────────────────────
236
+
237
+ interface FirstPartyRowProps {
238
+ id: FirstPartyExtId;
239
+ label: string;
240
+ description: string;
241
+ icon: React.ReactNode;
242
+ badge?: React.ReactNode;
243
+ isOpen: boolean;
244
+ onToggle: () => void;
245
+ children: React.ReactNode;
246
+ }
247
+
248
+ function FirstPartyExtRow({
249
+ label,
250
+ description,
251
+ icon,
252
+ badge,
253
+ isOpen,
254
+ onToggle,
255
+ children,
256
+ }: FirstPartyRowProps) {
257
+ return (
258
+ <div className="overflow-hidden rounded border border-border bg-background">
259
+ <button
260
+ type="button"
261
+ onClick={onToggle}
262
+ className="flex w-full cursor-pointer items-center gap-2 px-2.5 py-2 text-left transition-colors hover:bg-accent/50 active:bg-accent"
263
+ >
264
+ <span className="flex size-6 shrink-0 items-center justify-center rounded bg-muted text-muted-foreground">
265
+ {icon}
266
+ </span>
267
+ <span className="min-w-0 flex-1">
268
+ <span className="block truncate text-[11px] font-medium text-foreground">
269
+ {label}
270
+ </span>
271
+ <span className="block truncate text-[10px] leading-tight text-muted-foreground">
272
+ {description}
273
+ </span>
274
+ </span>
275
+ {badge}
276
+ </button>
277
+ {isOpen && <div className="border-t border-border/60">{children}</div>}
278
+ </div>
279
+ );
280
+ }
281
+
282
+ // ─── Asset Library panel ──────────────────────────────────────────────────────
283
+
284
+ interface AssetLibraryPanelProps {
285
+ context: DesignExtensionSlotContext;
286
+ }
287
+
288
+ function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
289
+ const t = useT();
290
+ const [pickerOpen, setPickerOpen] = useState(false);
291
+ const [pickerReady, setPickerReady] = useState(false);
292
+ const insertAsset = useActionMutation("insert-asset");
293
+
294
+ useEffect(() => {
295
+ if (pickerOpen) setPickerReady(false);
296
+ }, [pickerOpen]);
297
+
298
+ const handleReady = useCallback(
299
+ (_payload: unknown, _event: MessageEvent, ref: EmbeddedAppRef) => {
300
+ setPickerReady(true);
301
+ ref.postMessage("configure", {});
302
+ },
303
+ [],
304
+ );
305
+
306
+ const handleMessage = useCallback(
307
+ (name: string, payload: unknown) => {
308
+ if (name === "close") {
309
+ setPickerOpen(false);
310
+ return;
311
+ }
312
+ if (name !== "chooseImage" && name !== "chooseAsset") return;
313
+
314
+ const url = pickedAssetImageSource(payload);
315
+ if (!url) {
316
+ toast.error("No image URL returned from Assets picker.");
317
+ return;
318
+ }
319
+
320
+ const p = payload as PickedAssetPayload;
321
+ const title = pickedAssetString(p.title) ?? undefined;
322
+ const altText = pickedAssetString(p.altText) ?? undefined;
323
+ const assetId = pickedAssetString(p.assetId) ?? undefined;
324
+ const mimeType = pickedAssetString(p.mimeType) ?? "";
325
+ const mediaType = mimeType.startsWith("video/") ? "video" : "image";
326
+
327
+ insertAsset.mutate(
328
+ {
329
+ assetUrl: url,
330
+ assetId,
331
+ title,
332
+ altText,
333
+ mediaType: mediaType as "image" | "video",
334
+ designId: context.designId || undefined,
335
+ fileId: context.activeFileId || undefined,
336
+ },
337
+ {
338
+ onSuccess: () => {
339
+ toast.success("Asset inserted into design.");
340
+ },
341
+ onError: () => {
342
+ toast.error("Failed to insert asset.");
343
+ },
344
+ },
345
+ );
346
+
347
+ setPickerOpen(false);
348
+ },
349
+ [context.designId, context.activeFileId, insertAsset],
350
+ );
351
+
352
+ const handleAskAgent = () => {
353
+ sendToAgentChat({
354
+ message:
355
+ "Open the Assets picker so I can choose an image to insert into the design.",
356
+ context: [
357
+ `Design id: ${context.designId}`,
358
+ context.activeFileId ? `Active file id: ${context.activeFileId}` : null,
359
+ context.selectedElement?.selector
360
+ ? `Selected element selector: ${context.selectedElement.selector}`
361
+ : null,
362
+ "Call insert-asset with the chosen URL after the user picks from the Assets picker.",
363
+ ]
364
+ .filter(Boolean)
365
+ .join("\n"),
366
+ submit: true,
367
+ openSidebar: true,
368
+ });
369
+ };
370
+
371
+ return (
372
+ <div className="p-2.5">
373
+ <p className="mb-2 text-[10px] leading-snug text-muted-foreground">
374
+ Browse and insert generated or uploaded images into the active design
375
+ screen. Inserts near the selected element when one is active.
376
+ </p>
377
+ <div className="flex flex-wrap gap-1.5">
378
+ <Button
379
+ type="button"
380
+ size="sm"
381
+ className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
382
+ disabled={insertAsset.isPending || !context.activeFileId}
383
+ onClick={() => setPickerOpen(true)}
384
+ >
385
+ <IconPhoto className="size-3" />
386
+ {insertAsset.isPending ? "Inserting…" : "Browse Assets"}
387
+ </Button>
388
+ <Button
389
+ type="button"
390
+ variant="outline"
391
+ size="sm"
392
+ className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
393
+ onClick={handleAskAgent}
394
+ >
395
+ <IconSparkles className="size-3" />
396
+ {t("designEditor.askAgent") || "Ask agent"}
397
+ </Button>
398
+ </div>
399
+ {!context.activeFileId && (
400
+ <p className="mt-1.5 text-[10px] text-amber-600 dark:text-amber-400">
401
+ Open a design screen first to insert assets.
402
+ </p>
403
+ )}
404
+
405
+ {/* Assets picker overlay */}
406
+ {pickerOpen && (
407
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
408
+ <div
409
+ data-assets-picker-overlay
410
+ className="relative flex h-[min(86vh,760px)] w-[min(96vw,1040px)] flex-col overflow-hidden rounded-xl border border-border bg-popover shadow-2xl"
411
+ >
412
+ <div className="flex h-10 shrink-0 items-center border-b border-border px-4">
413
+ <span className="flex-1 text-sm font-medium">Assets Library</span>
414
+ <button
415
+ type="button"
416
+ aria-label="Close picker"
417
+ onClick={() => setPickerOpen(false)}
418
+ className="flex size-7 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
419
+ >
420
+ ×
421
+ </button>
422
+ </div>
423
+ <div className="relative min-h-0 flex-1">
424
+ {!pickerReady && (
425
+ <div className="absolute inset-0 flex items-center justify-center">
426
+ <Skeleton className="h-full w-full rounded-none" />
427
+ </div>
428
+ )}
429
+ <EmbeddedApp
430
+ url={assetsPickerUrl()}
431
+ title="Assets picker"
432
+ onReady={handleReady}
433
+ onMessage={handleMessage}
434
+ className="h-full w-full"
435
+ />
436
+ </div>
437
+ </div>
438
+ </div>
439
+ )}
440
+ </div>
441
+ );
442
+ }
443
+
444
+ // ─── Shader Fills panel ───────────────────────────────────────────────────────
445
+
446
+ interface ShaderFillsExtPanelProps {
447
+ context: DesignExtensionSlotContext;
448
+ }
449
+
450
+ function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
451
+ const [showShaders, setShowShaders] = useState(false);
452
+ const clearPreviewRef = useRef(context.onShaderFillPreviewClear);
453
+ useEffect(() => {
454
+ clearPreviewRef.current = context.onShaderFillPreviewClear;
455
+ }, [context.onShaderFillPreviewClear]);
456
+ useEffect(
457
+ () => () => {
458
+ clearPreviewRef.current?.();
459
+ },
460
+ [],
461
+ );
462
+ const closeShaders = () => {
463
+ context.onShaderFillPreviewClear?.();
464
+ setShowShaders(false);
465
+ };
466
+ // The most recently previewed descriptor — Apply persists exactly this one,
467
+ // so the write is intentional (one atomic call) rather than firing on every
468
+ // slider tweak.
469
+ const [previewed, setPreviewed] = useState<ShaderDescriptor | null>(null);
470
+ const applyShaderFill = useActionMutation("apply-shader-fill");
471
+
472
+ // The persisting apply path needs an HTML file plus a target element. Without
473
+ // a selected element we can still preview, but we cannot write a fill.
474
+ const targetNodeId = context.selectedElement?.sourceId ?? undefined;
475
+ const targetSelector = context.selectedElement?.selector ?? undefined;
476
+ const canPersist = Boolean(
477
+ context.activeFileId && (targetNodeId || targetSelector),
478
+ );
479
+
480
+ const persistFill = (descriptor: ShaderDescriptor) => {
481
+ if (!canPersist) return;
482
+ applyShaderFill.mutate(
483
+ {
484
+ descriptor: {
485
+ preset: descriptor.preset,
486
+ params: descriptor.params,
487
+ colors: descriptor.colors,
488
+ speed: descriptor.speed,
489
+ frame: descriptor.frame,
490
+ fit: descriptor.fit,
491
+ scale: descriptor.scale,
492
+ rotation: descriptor.rotation,
493
+ offsetX: descriptor.offsetX,
494
+ offsetY: descriptor.offsetY,
495
+ },
496
+ target: { nodeId: targetNodeId, selector: targetSelector },
497
+ source: {
498
+ kind: "design-file" as const,
499
+ designId: context.designId || undefined,
500
+ fileId: context.activeFileId || undefined,
501
+ },
502
+ },
503
+ {
504
+ onSuccess: (res) => {
505
+ const r = res as { persisted?: boolean } | undefined;
506
+ if (r?.persisted) {
507
+ toast.success("Shader fill applied to the selected element.");
508
+ } else {
509
+ toast.message("Shader fill previewed — nothing was written.");
510
+ }
511
+ },
512
+ onError: () => {
513
+ toast.error("Failed to apply shader fill.");
514
+ },
515
+ },
516
+ );
517
+ };
518
+
519
+ if (!showShaders) {
520
+ return (
521
+ <div className="p-2.5">
522
+ <p className="mb-2 text-[10px] leading-snug text-muted-foreground">
523
+ GPU shader fill presets — MeshGradient, GrainGradient, Voronoi,
524
+ Metaballs, Warp, GodRays, Dithering, PaperTexture. Preview as a CSS
525
+ gradient, then apply it to the selected element as a CSS background.
526
+ </p>
527
+ {!canPersist && (
528
+ <div className="mb-2 flex items-start gap-1.5 rounded-md border border-amber-500/30 bg-amber-500/5 px-2 py-1.5">
529
+ <IconLock className="mt-0.5 size-3 shrink-0 text-amber-600 dark:text-amber-400" />
530
+ <p className="text-[10px] leading-snug text-amber-700 dark:text-amber-300">
531
+ Select an element on the canvas to apply a fill. Without a
532
+ selection you can still browse and preview presets.
533
+ </p>
534
+ </div>
535
+ )}
536
+ <div className="flex flex-wrap gap-1.5">
537
+ <Button
538
+ type="button"
539
+ size="sm"
540
+ className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
541
+ onClick={() => setShowShaders(true)}
542
+ >
543
+ <IconSparkles className="size-3" />
544
+ Browse Shaders
545
+ </Button>
546
+ </div>
547
+ </div>
548
+ );
549
+ }
550
+
551
+ return (
552
+ <div className="flex flex-col">
553
+ <ShaderFillsPanel
554
+ onApply={(descriptor, css) => {
555
+ // Preview only: ShaderFillsPanel fires apply-shader (planning/codegen)
556
+ // for agent context on every tune and the iframe shows the gradient.
557
+ // We just record the latest descriptor here; the explicit Apply
558
+ // button below performs the single intentional persist write.
559
+ setPreviewed(descriptor);
560
+ context.onShaderFillPreview?.(descriptor, css);
561
+ }}
562
+ onBack={closeShaders}
563
+ applyContext={{
564
+ designId: context.designId || undefined,
565
+ fileId: context.activeFileId || undefined,
566
+ nodeId: targetNodeId,
567
+ selector: targetSelector,
568
+ }}
569
+ />
570
+
571
+ {/* ── Apply bar: persists the previewed fill onto the selected element ── */}
572
+ <div className="flex items-center gap-2 border-t border-border/60 px-3 py-2">
573
+ {canPersist ? (
574
+ <Button
575
+ type="button"
576
+ size="sm"
577
+ className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
578
+ disabled={!previewed || applyShaderFill.isPending}
579
+ onClick={() => {
580
+ if (previewed) persistFill(previewed);
581
+ }}
582
+ >
583
+ <IconSparkles className="size-3" />
584
+ {applyShaderFill.isPending
585
+ ? "Applying…"
586
+ : previewed
587
+ ? "Apply fill"
588
+ : "Pick a preset"}
589
+ </Button>
590
+ ) : (
591
+ <p className="flex items-start gap-1.5 text-[10px] leading-snug text-amber-700 dark:text-amber-300">
592
+ <IconLock className="mt-0.5 size-3 shrink-0 text-amber-600 dark:text-amber-400" />
593
+ Select an element on the canvas to apply this fill.
594
+ </p>
595
+ )}
596
+ </div>
597
+ </div>
598
+ );
599
+ }
600
+
601
+ // ─── Token Auditor panel ─────────────────────────────────────────────────────
602
+
603
+ interface TokenAuditorPanelProps {
604
+ context: DesignExtensionSlotContext;
605
+ }
606
+
607
+ function TokenAuditorPanel({ context }: TokenAuditorPanelProps) {
608
+ const t = useT();
609
+
610
+ const handleAskAgent = () => {
611
+ sendToAgentChat({
612
+ message:
613
+ "Run a token audit on the active design: index CSS custom properties, surface any hard-coded colours that should be tokens, flag clashes, and suggest fixes.",
614
+ context: [
615
+ `Design id: ${context.designId}`,
616
+ context.activeFileId ? `Active file id: ${context.activeFileId}` : null,
617
+ "Call index-design-tokens to parse CSS vars, then preview-design-token-edit for suggested changes.",
618
+ ]
619
+ .filter(Boolean)
620
+ .join("\n"),
621
+ submit: true,
622
+ openSidebar: true,
623
+ });
624
+ };
625
+
626
+ return (
627
+ <div className="p-2.5">
628
+ <p className="mb-2 text-[10px] leading-snug text-muted-foreground">
629
+ Index CSS custom property usage across the active design, surface
630
+ hard-coded colours that should be tokens, and flag clashes.
631
+ </p>
632
+ <div className="mb-1.5 text-[10px] text-muted-foreground">
633
+ Token reads and tweaks are available for all source types. Source
634
+ write-back (globals.css / tailwind.config) is gated on bridge hardening.
635
+ </div>
636
+ <div className="flex flex-wrap gap-1.5">
637
+ <Button
638
+ type="button"
639
+ size="sm"
640
+ className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
641
+ onClick={handleAskAgent}
642
+ >
643
+ <IconSparkles className="size-3" />
644
+ {t("designEditor.askAgent") || "Ask agent"}
645
+ </Button>
646
+ </div>
647
+ </div>
648
+ );
649
+ }
650
+
651
+ // ─── Motion Presets panel ─────────────────────────────────────────────────────
652
+
653
+ interface MotionPresetsPanelProps {
654
+ context: DesignExtensionSlotContext;
655
+ }
656
+
657
+ function MotionPresetsPanel({ context }: MotionPresetsPanelProps) {
658
+ const t = useT();
659
+
660
+ const handleAskAgent = () => {
661
+ sendToAgentChat({
662
+ message:
663
+ "Apply a motion preset to the selected element. Suggest fade-in, slide-up, pulse, bounce, or spin, then call apply-motion-edit to write the keyframes atomically.",
664
+ context: [
665
+ `Design id: ${context.designId}`,
666
+ context.activeFileId ? `Active file id: ${context.activeFileId}` : null,
667
+ context.selectedElement
668
+ ? `Selected element: ${JSON.stringify({ selector: context.selectedElement.selector, sourceId: context.selectedElement.sourceId }, null, 2)}`
669
+ : "No element selected — ask the user to click an element first.",
670
+ "Call get-motion-timeline to check for an existing timeline, then preview-motion-frame first, then apply-motion-edit for the atomic write.",
671
+ ]
672
+ .filter(Boolean)
673
+ .join("\n"),
674
+ submit: true,
675
+ openSidebar: true,
676
+ });
677
+ };
678
+
679
+ return (
680
+ <div className="p-2.5">
681
+ <p className="mb-2 text-[10px] leading-snug text-muted-foreground">
682
+ One-click animation presets — fade-in, slide-up, pulse, bounce, spin —
683
+ applied to the selected element via the motion timeline.
684
+ </p>
685
+ {!context.selectedElement && (
686
+ <p className="mb-1.5 text-[10px] text-muted-foreground">
687
+ Select an element on the canvas first, then ask the agent to apply a
688
+ motion preset.
689
+ </p>
690
+ )}
691
+ <div className="flex flex-wrap gap-1.5">
692
+ <Button
693
+ type="button"
694
+ size="sm"
695
+ className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
696
+ onClick={handleAskAgent}
697
+ >
698
+ <IconSparkles className="size-3" />
699
+ {t("designEditor.askAgent") || "Ask agent"}
700
+ </Button>
701
+ </div>
702
+ </div>
703
+ );
704
+ }
705
+
706
+ // ─── Main panel ──────────────────────────────────────────────────────────────
707
+
168
708
  export function DesignExtensionsPanel({
169
709
  context,
170
710
  className,
@@ -173,6 +713,9 @@ export function DesignExtensionsPanel({
173
713
  const queryClient = useQueryClient();
174
714
  const [createOpen, setCreateOpen] = useState(false);
175
715
  const [installingId, setInstallingId] = useState<string | null>(null);
716
+ const [openFirstParty, setOpenFirstParty] = useState<FirstPartyExtId | null>(
717
+ null,
718
+ );
176
719
  const slotId = DESIGN_EDITOR_EXTENSION_SLOT_ID;
177
720
  const { data: installs = [], isLoading } = useSlotInstalls(slotId);
178
721
  const { data: available = [] } = useAvailableExtensions(slotId);
@@ -184,6 +727,10 @@ export function DesignExtensionsPanel({
184
727
  (extension) => !installedIds.has(extension.extensionId),
185
728
  );
186
729
 
730
+ const toggleFirstParty = (id: FirstPartyExtId) => {
731
+ setOpenFirstParty((prev) => (prev === id ? null : id));
732
+ };
733
+
187
734
  const submitCreatePrompt: PromptComposerSubmitHandler = (
188
735
  text: string,
189
736
  _files,
@@ -232,6 +779,45 @@ export function DesignExtensionsPanel({
232
779
  }
233
780
  };
234
781
 
782
+ // First-party extension row config
783
+ const firstPartyRows: Array<{
784
+ id: FirstPartyExtId;
785
+ label: string;
786
+ description: string;
787
+ icon: React.ReactNode;
788
+ badge?: React.ReactNode;
789
+ panel: React.ReactNode;
790
+ }> = [
791
+ {
792
+ id: "design.asset-library",
793
+ label: "Asset Library",
794
+ description: "Browse & insert assets into the active screen",
795
+ icon: <IconPhoto className="size-3.5" />,
796
+ panel: <AssetLibraryPanel context={context} />,
797
+ },
798
+ {
799
+ id: "design.shader-fills",
800
+ label: "Shader Fills",
801
+ description: "GPU shader fill presets — preview & apply",
802
+ icon: <IconSparkles className="size-3.5" />,
803
+ panel: <ShaderFillsExtPanel context={context} />,
804
+ },
805
+ {
806
+ id: "design.token-auditor",
807
+ label: "Token Auditor",
808
+ description: "Audit CSS token usage, flag clashes",
809
+ icon: <IconPalette className="size-3.5" />,
810
+ panel: <TokenAuditorPanel context={context} />,
811
+ },
812
+ {
813
+ id: "design.motion-presets",
814
+ label: "Motion Presets",
815
+ description: "One-click animation presets for elements",
816
+ icon: <IconPlayerPlay className="size-3.5" />,
817
+ panel: <MotionPresetsPanel context={context} />,
818
+ },
819
+ ];
820
+
235
821
  return (
236
822
  <div className={cn("flex min-h-0 flex-1 flex-col", className)}>
237
823
  {/* Section header — 32 px tall, matches PanelSection / design inspector headers */}
@@ -261,54 +847,47 @@ export function DesignExtensionsPanel({
261
847
  </div>
262
848
 
263
849
  <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 pb-2 pt-1.5">
850
+ {/* ── First-party built-in extensions ──────────────────────────── */}
851
+ <div className="mb-3 space-y-1">
852
+ <p className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
853
+ Built-in
854
+ </p>
855
+ {firstPartyRows.map((row) => (
856
+ <FirstPartyExtRow
857
+ key={row.id}
858
+ id={row.id}
859
+ label={row.label}
860
+ description={row.description}
861
+ icon={row.icon}
862
+ badge={row.badge}
863
+ isOpen={openFirstParty === row.id}
864
+ onToggle={() => toggleFirstParty(row.id)}
865
+ >
866
+ {row.panel}
867
+ </FirstPartyExtRow>
868
+ ))}
869
+ </div>
870
+
871
+ {/* ── Agent help text ───────────────────────────────────────────── */}
872
+ <div className="mb-3 flex items-start gap-1.5 rounded-md bg-muted/40 px-2 py-1.5">
873
+ <IconSparkles className="mt-0.5 size-3 shrink-0 text-muted-foreground/70" />
874
+ <p className="text-[10px] leading-snug text-muted-foreground">
875
+ The agent can insert assets, apply shader fills, audit tokens, and
876
+ apply motion presets. Ask in the chat sidebar for help.
877
+ </p>
878
+ </div>
879
+
880
+ {/* ── User-installed extensions ──────────────────────────────────── */}
264
881
  {isLoading ? (
265
882
  <div className="space-y-1.5">
266
883
  <Skeleton className="h-24 rounded" />
267
884
  <Skeleton className="h-32 rounded" />
268
885
  </div>
269
- ) : installs.length === 0 ? (
270
- /* Empty state — compact, matches TweaksPanel empty style */
271
- <div className="flex flex-col items-center gap-2 py-6 text-center">
272
- <div className="flex size-8 items-center justify-center rounded-lg bg-muted/60">
273
- <IconPuzzle className="size-4 text-muted-foreground/70" />
274
- </div>
275
- <div>
276
- <p className="text-[11px] font-medium text-foreground">
277
- {t("designEditor.extensionsEmptyTitle")}
278
- </p>
279
- <p className="mt-0.5 text-[11px] leading-snug text-muted-foreground">
280
- {t("designEditor.extensionsEmptyDescription")}
281
- </p>
282
- </div>
283
- <div className="flex flex-wrap items-center justify-center gap-1.5">
284
- <Button
285
- type="button"
286
- size="sm"
287
- className="h-6 cursor-pointer px-2.5 text-[11px]"
288
- onClick={() => setCreateOpen(true)}
289
- >
290
- <IconPlus className="size-3" />
291
- {t("designEditor.addExtension")}
292
- </Button>
293
- <Button
294
- asChild
295
- type="button"
296
- variant="outline"
297
- size="sm"
298
- className="h-6 cursor-pointer px-2.5 text-[11px]"
299
- >
300
- <a
301
- href="https://www.agent-native.com/docs/extensions"
302
- target="_blank"
303
- rel="noreferrer"
304
- >
305
- {t("designEditor.extensionsDocs")}
306
- </a>
307
- </Button>
308
- </div>
309
- </div>
310
- ) : (
886
+ ) : installs.length > 0 ? (
311
887
  <div className="space-y-1.5">
888
+ <p className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
889
+ Installed
890
+ </p>
312
891
  {installs.map((install) => (
313
892
  <EmbeddedExtension
314
893
  key={install.installId}
@@ -320,8 +899,33 @@ export function DesignExtensionsPanel({
320
899
  />
321
900
  ))}
322
901
  </div>
902
+ ) : (
903
+ /* Empty state for user extensions — compact */
904
+ <div className="flex flex-col items-center gap-2 py-4 text-center">
905
+ <div className="flex size-7 items-center justify-center rounded-lg bg-muted/60">
906
+ <IconPuzzle className="size-3.5 text-muted-foreground/70" />
907
+ </div>
908
+ <div>
909
+ <p className="text-[11px] font-medium text-foreground">
910
+ {t("designEditor.extensionsEmptyTitle")}
911
+ </p>
912
+ <p className="mt-0.5 text-[10px] leading-snug text-muted-foreground">
913
+ {t("designEditor.extensionsEmptyDescription")}
914
+ </p>
915
+ </div>
916
+ <Button
917
+ type="button"
918
+ size="sm"
919
+ className="h-6 cursor-pointer px-2.5 text-[11px]"
920
+ onClick={() => setCreateOpen(true)}
921
+ >
922
+ <IconPlus className="size-3" />
923
+ {t("designEditor.addExtension")}
924
+ </Button>
925
+ </div>
323
926
  )}
324
927
 
928
+ {/* ── Available (installable) extensions ────────────────────────── */}
325
929
  {installable.length > 0 ? (
326
930
  <div className="mt-3 border-t border-border/60 pt-1.5">
327
931
  <p className="mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
@@ -358,6 +962,8 @@ export function DesignExtensionsPanel({
358
962
  );
359
963
  }
360
964
 
965
+ // ─── Create extension popover ─────────────────────────────────────────────────
966
+
361
967
  function CreateExtensionPopover({
362
968
  open,
363
969
  onOpenChange,