@agent-native/core 0.80.10 → 0.81.0

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 (267) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +53 -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 +419 -19
  6. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  7. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  8. package/corpus/core/src/client/chat/index.ts +1 -0
  9. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  10. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  11. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  12. package/corpus/core/src/client/index.ts +1 -0
  13. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  14. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  15. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  16. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  17. package/corpus/core/src/db/client.ts +29 -2
  18. package/corpus/core/src/provider-api/index.ts +29 -0
  19. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  20. package/corpus/core/src/styles/agent-native.css +54 -0
  21. package/corpus/core/src/usage/store.ts +2 -2
  22. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  23. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  24. package/corpus/templates/analytics/app/global.css +8 -0
  25. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  26. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  27. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  29. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  30. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  31. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  36. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  37. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  38. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  39. package/corpus/templates/assets/AGENTS.md +4 -0
  40. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  41. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  42. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  43. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  44. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  45. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  46. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  47. package/corpus/templates/assets/app/global.css +15 -0
  48. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  49. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  50. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  51. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  52. package/corpus/templates/assets/app/routes/library.tsx +233 -52
  53. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  54. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  55. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  56. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  57. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  58. package/corpus/templates/assets/shared/api.ts +4 -0
  59. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  60. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  61. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  64. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  65. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  66. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  67. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  68. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  69. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  70. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  71. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  72. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  73. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  74. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  75. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  76. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  77. package/corpus/templates/design/AGENTS.md +27 -0
  78. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  79. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  80. package/corpus/templates/design/actions/apply-component-prop-edit.ts +76 -25
  81. package/corpus/templates/design/actions/apply-design-state.ts +17 -4
  82. package/corpus/templates/design/actions/apply-motion-edit.ts +76 -115
  83. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  84. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  85. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  86. package/corpus/templates/design/actions/delete-design.ts +20 -0
  87. package/corpus/templates/design/actions/export-zip.ts +23 -8
  88. package/corpus/templates/design/actions/generate-design.ts +53 -1
  89. package/corpus/templates/design/actions/get-component-details.ts +21 -26
  90. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  91. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  92. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  93. package/corpus/templates/design/actions/index-design-tokens.ts +18 -5
  94. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  95. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  96. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  97. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  98. package/corpus/templates/design/actions/list-design-states.ts +19 -1
  99. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  100. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  101. package/corpus/templates/design/actions/navigate.ts +18 -3
  102. package/corpus/templates/design/actions/open-component-source.ts +7 -2
  103. package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
  104. package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
  105. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  106. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  107. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  108. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  109. package/corpus/templates/design/actions/run-design-audit.ts +74 -10
  110. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  111. package/corpus/templates/design/actions/view-screen.ts +1 -1
  112. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  113. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +244 -3197
  114. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  115. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +795 -289
  116. package/corpus/templates/design/app/components/design/EditPanel.tsx +635 -234
  117. package/corpus/templates/design/app/components/design/LayersPanel.tsx +124 -32
  118. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  119. package/corpus/templates/design/app/components/design/MotionDock.tsx +244 -266
  120. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +491 -56
  121. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +55 -67
  122. package/corpus/templates/design/app/components/design/StatesPanel.tsx +38 -63
  123. package/corpus/templates/design/app/components/design/TokensPanel.tsx +264 -8
  124. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  125. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  126. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  127. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  128. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  129. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  130. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  131. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  132. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  133. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  134. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  135. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  136. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  137. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  138. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  139. package/corpus/templates/design/app/global.css +7 -0
  140. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  141. package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
  142. package/corpus/templates/design/app/i18n-data.ts +369 -0
  143. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  144. package/corpus/templates/design/app/pages/DesignEditor.tsx +3024 -616
  145. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  146. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  165. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  171. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  172. package/corpus/templates/design/e2e/helpers.ts +168 -65
  173. package/corpus/templates/design/package.json +3 -1
  174. package/corpus/templates/design/server/db/schema.ts +23 -0
  175. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  176. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  177. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  178. package/corpus/templates/design/server/plugins/db.ts +39 -0
  179. package/corpus/templates/design/server/register-secrets.ts +38 -0
  180. package/corpus/templates/design/shared/board-file.ts +228 -0
  181. package/corpus/templates/design/shared/board-objects.ts +288 -0
  182. package/corpus/templates/design/shared/code-layer.ts +52 -0
  183. package/corpus/templates/design/shared/component-model.ts +35 -0
  184. package/corpus/templates/design/shared/motion-compiler.ts +137 -15
  185. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  186. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  187. package/corpus/templates/design/tsconfig.json +2 -1
  188. package/corpus/templates/slides/AGENTS.md +9 -0
  189. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  190. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  191. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  192. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  193. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  194. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  195. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  196. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  197. package/corpus/templates/slides/package.json +1 -0
  198. package/corpus/templates/slides/vite.config.ts +1 -0
  199. package/dist/agent/production-agent.d.ts.map +1 -1
  200. package/dist/agent/production-agent.js +94 -10
  201. package/dist/agent/production-agent.js.map +1 -1
  202. package/dist/cli/design-connect.d.ts +25 -0
  203. package/dist/cli/design-connect.d.ts.map +1 -1
  204. package/dist/cli/design-connect.js +357 -18
  205. package/dist/cli/design-connect.js.map +1 -1
  206. package/dist/client/AssistantChat.d.ts +14 -0
  207. package/dist/client/AssistantChat.d.ts.map +1 -1
  208. package/dist/client/AssistantChat.js +115 -22
  209. package/dist/client/AssistantChat.js.map +1 -1
  210. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  211. package/dist/client/ConnectBuilderCard.js +1 -0
  212. package/dist/client/ConnectBuilderCard.js.map +1 -1
  213. package/dist/client/chat/index.d.ts +1 -1
  214. package/dist/client/chat/index.d.ts.map +1 -1
  215. package/dist/client/chat/index.js +1 -1
  216. package/dist/client/chat/index.js.map +1 -1
  217. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  218. package/dist/client/chat/run-recovery.js +5 -5
  219. package/dist/client/chat/run-recovery.js.map +1 -1
  220. package/dist/client/chat-view-transition.d.ts +6 -0
  221. package/dist/client/chat-view-transition.d.ts.map +1 -1
  222. package/dist/client/chat-view-transition.js +23 -2
  223. package/dist/client/chat-view-transition.js.map +1 -1
  224. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  225. package/dist/client/composer/PromptComposer.js +3 -2
  226. package/dist/client/composer/PromptComposer.js.map +1 -1
  227. package/dist/client/index.d.ts +1 -1
  228. package/dist/client/index.d.ts.map +1 -1
  229. package/dist/client/index.js +1 -1
  230. package/dist/client/index.js.map +1 -1
  231. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  232. package/dist/client/org/OrgSwitcher.js +11 -4
  233. package/dist/client/org/OrgSwitcher.js.map +1 -1
  234. package/dist/client/sharing/ShareButton.js +3 -3
  235. package/dist/client/sharing/ShareButton.js.map +1 -1
  236. package/dist/client/sse-event-processor.d.ts.map +1 -1
  237. package/dist/client/sse-event-processor.js +10 -0
  238. package/dist/client/sse-event-processor.js.map +1 -1
  239. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  240. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  241. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  242. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  243. package/dist/collab/awareness.d.ts +2 -2
  244. package/dist/collab/awareness.d.ts.map +1 -1
  245. package/dist/collab/routes.d.ts +2 -2
  246. package/dist/db/client.d.ts +1 -0
  247. package/dist/db/client.d.ts.map +1 -1
  248. package/dist/db/client.js +28 -2
  249. package/dist/db/client.js.map +1 -1
  250. package/dist/notifications/routes.d.ts +3 -3
  251. package/dist/observability/routes.d.ts +3 -3
  252. package/dist/progress/routes.d.ts +1 -1
  253. package/dist/provider-api/index.d.ts +5 -4
  254. package/dist/provider-api/index.d.ts.map +1 -1
  255. package/dist/provider-api/index.js +28 -0
  256. package/dist/provider-api/index.js.map +1 -1
  257. package/dist/resources/handlers.d.ts +3 -3
  258. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  259. package/dist/server/core-routes-plugin.d.ts +25 -0
  260. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  261. package/dist/server/core-routes-plugin.js +43 -18
  262. package/dist/server/core-routes-plugin.js.map +1 -1
  263. package/dist/server/transcribe-voice.d.ts +1 -1
  264. package/dist/styles/agent-native.css +54 -0
  265. package/dist/usage/store.js +2 -2
  266. package/dist/usage/store.js.map +1 -1
  267. package/package.json +1 -1
@@ -1,13 +1,11 @@
1
1
  // i18n-raw-literal-disable-file — new Design Studio panel; UI strings are localized when this feature is finalized in the follow-up PR.
2
2
  import {
3
- PromptComposer,
4
3
  agentNativePath,
5
4
  sendToAgentChat,
5
+ useActionQuery,
6
6
  useActionMutation,
7
7
  useChangeVersions,
8
8
  useT,
9
- type PromptComposerProps,
10
- type PromptComposerSubmitOptions,
11
9
  } from "@agent-native/core/client";
12
10
  import { EmbeddedExtension } from "@agent-native/core/client/extensions";
13
11
  import {
@@ -16,31 +14,45 @@ import {
16
14
  } from "@agent-native/core/embedding/react";
17
15
  import type { ShaderDescriptor } from "@shared/shader-presets";
18
16
  import {
17
+ IconAdjustmentsHorizontal,
18
+ IconAssembly,
19
+ IconBrandFigma,
20
+ IconChevronDown,
19
21
  IconExternalLink,
20
22
  IconLock,
21
- IconPalette,
22
23
  IconPhoto,
23
24
  IconPlayerPlay,
24
- IconPlus,
25
25
  IconPuzzle,
26
+ IconSearch,
26
27
  IconSparkles,
27
28
  } from "@tabler/icons-react";
28
29
  import { useQuery, useQueryClient } from "@tanstack/react-query";
29
- import { useCallback, useEffect, useMemo, useState } from "react";
30
+ import {
31
+ useCallback,
32
+ useEffect,
33
+ useMemo,
34
+ useRef,
35
+ useState,
36
+ type FormEvent,
37
+ } from "react";
30
38
  import { toast } from "sonner";
31
39
 
32
40
  import { Button } from "@/components/ui/button";
41
+ import {
42
+ DropdownMenu,
43
+ DropdownMenuContent,
44
+ DropdownMenuItem,
45
+ DropdownMenuTrigger,
46
+ } from "@/components/ui/dropdown-menu";
47
+ import { Input } from "@/components/ui/input";
33
48
  import {
34
49
  Popover,
35
50
  PopoverContent,
36
51
  PopoverTrigger,
37
52
  } from "@/components/ui/popover";
38
53
  import { Skeleton } from "@/components/ui/skeleton";
39
- import {
40
- Tooltip,
41
- TooltipContent,
42
- TooltipTrigger,
43
- } from "@/components/ui/tooltip";
54
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
55
+ import { Textarea } from "@/components/ui/textarea";
44
56
  import { cn } from "@/lib/utils";
45
57
 
46
58
  import { ShaderFillsPanel } from "./inspector/ShaderFillsPanel";
@@ -74,6 +86,8 @@ export interface DesignExtensionSlotContext extends Record<string, unknown> {
74
86
  designTitle: string | null;
75
87
  activeFileId: string | null;
76
88
  activeFilename: string | null;
89
+ activeFileUpdatedAt: string | null;
90
+ activeContent: string;
77
91
  viewMode: "single" | "overview";
78
92
  zoom: number;
79
93
  screens: Array<{
@@ -86,14 +100,23 @@ export interface DesignExtensionSlotContext extends Record<string, unknown> {
86
100
  mode: string;
87
101
  activeTool: string;
88
102
  tweakValues: Record<string, string | number | boolean>;
103
+ onShaderFillPreview?: (descriptor: ShaderDescriptor, css: string) => void;
104
+ onShaderFillPreviewClear?: () => void;
105
+ onShaderFillApplied?: (
106
+ fileId: string,
107
+ content: string,
108
+ updatedAt?: string,
109
+ ) => void;
89
110
  }
90
111
 
91
112
  interface DesignExtensionsPanelProps {
92
113
  context: DesignExtensionSlotContext;
93
114
  className?: string;
115
+ hideAssetLibrary?: boolean;
116
+ title?: string;
94
117
  }
95
118
 
96
- type PromptComposerSubmitHandler = PromptComposerProps["onSubmit"];
119
+ type CreateExtensionSubmitHandler = (text: string) => void;
97
120
 
98
121
  // ─── First-party extension ids ───────────────────────────────────────────────
99
122
 
@@ -103,9 +126,23 @@ type FirstPartyExtId =
103
126
  | "design.token-auditor"
104
127
  | "design.motion-presets";
105
128
 
129
+ type ToolSourceFilter = "all" | "built-in" | "extensions";
130
+ type ToolCategoryFilter = "all" | "shader" | "tokens" | "motion" | "plugins";
131
+
132
+ type FirstPartyRow = {
133
+ id: FirstPartyExtId;
134
+ label: string;
135
+ description: string;
136
+ category: ToolCategoryFilter;
137
+ icon: React.ReactNode;
138
+ badge?: React.ReactNode;
139
+ panel: React.ReactNode;
140
+ };
141
+
106
142
  // ─── Assets picker types (mirrors PromptDialog) ───────────────────────────
107
143
 
108
- const DEFAULT_ASSETS_PICKER_URL = "https://assets.agent-native.com/picker";
144
+ const DEFAULT_ASSETS_PICKER_URL =
145
+ "https://assets.agent-native.com/library?__an_picker=1&mediaType=image&layout=vertical";
109
146
 
110
147
  interface PickedAssetPayload {
111
148
  url?: unknown;
@@ -119,12 +156,30 @@ interface PickedAssetPayload {
119
156
  }
120
157
 
121
158
  function assetsPickerUrl(): string {
122
- return (
159
+ const configured =
123
160
  (typeof import.meta !== "undefined" &&
124
161
  (import.meta as { env?: Record<string, string> }).env
125
162
  ?.VITE_AGENT_NATIVE_ASSETS_PICKER_URL) ||
126
- DEFAULT_ASSETS_PICKER_URL
127
- );
163
+ DEFAULT_ASSETS_PICKER_URL;
164
+ try {
165
+ const base =
166
+ typeof window !== "undefined"
167
+ ? window.location.origin
168
+ : "https://assets.agent-native.com";
169
+ const url = new URL(configured, base);
170
+ if (url.pathname === "/picker") url.pathname = "/library";
171
+ url.searchParams.set("__an_picker", "1");
172
+ url.searchParams.set(
173
+ "mediaType",
174
+ url.searchParams.get("mediaType") || "image",
175
+ );
176
+ url.searchParams.set("layout", "vertical");
177
+ url.searchParams.set("embedded", "1");
178
+ url.searchParams.set("callerAppId", "design");
179
+ return url.toString();
180
+ } catch {
181
+ return configured;
182
+ }
128
183
  }
129
184
 
130
185
  function pickedAssetString(value: unknown): string | null {
@@ -152,7 +207,7 @@ function buildExtensionCreateContext(
152
207
  ? JSON.stringify(context.selectedElement, null, 2)
153
208
  : "No element is currently selected.";
154
209
  return [
155
- `The user is in the Design editor Extensions inspector for design id "${context.designId}"${context.designTitle ? ` (title: "${context.designTitle}")` : ""}.`,
210
+ `The user is in the Design editor Tools panel for design id "${context.designId}"${context.designTitle ? ` (title: "${context.designTitle}")` : ""}.`,
156
211
  context.activeFileId
157
212
  ? `Active screen: "${context.activeFilename ?? context.activeFileId}" (file id: "${context.activeFileId}").`
158
213
  : "There is no active screen yet.",
@@ -160,7 +215,7 @@ function buildExtensionCreateContext(
160
215
  `User request: "${prompt}"`,
161
216
  "",
162
217
  "After create-extension succeeds, call add-extension-slot-target with this slot id, then install-extension with this slot id so the extension appears in the editor panel immediately.",
163
- 'If create-extension opens the standalone extension editor, call navigate with view "editor", the current design id, and inspectorTab "extensions" after install so the user returns to this inline panel.',
218
+ 'If create-extension opens the standalone extension editor, call navigate with view "editor", the current design id, and leftPanel "tools" after install so the user returns to this inline panel.',
164
219
  "",
165
220
  "The extension will receive window.slotContext and onSlotContext updates with the current design selection:",
166
221
  JSON.stringify(
@@ -245,7 +300,6 @@ interface FirstPartyRowProps {
245
300
 
246
301
  function FirstPartyExtRow({
247
302
  label,
248
- description,
249
303
  icon,
250
304
  badge,
251
305
  isOpen,
@@ -253,50 +307,171 @@ function FirstPartyExtRow({
253
307
  children,
254
308
  }: FirstPartyRowProps) {
255
309
  return (
256
- <div className="overflow-hidden rounded border border-border bg-background">
310
+ <div className="overflow-hidden rounded-md">
257
311
  <button
258
312
  type="button"
259
313
  onClick={onToggle}
260
- 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"
314
+ className="group flex w-full cursor-pointer items-center gap-3 rounded-md px-2 py-2.5 text-left transition-colors hover:bg-accent/60 active:bg-accent"
261
315
  >
262
- <span className="flex size-6 shrink-0 items-center justify-center rounded bg-muted text-muted-foreground">
316
+ <span className="flex size-10 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/70 text-muted-foreground transition-colors group-hover:border-border group-hover:bg-muted">
263
317
  {icon}
264
318
  </span>
265
319
  <span className="min-w-0 flex-1">
266
- <span className="block truncate text-[11px] font-medium text-foreground">
320
+ <span className="block truncate text-sm font-medium leading-tight text-foreground">
267
321
  {label}
268
322
  </span>
269
- <span className="block truncate text-[10px] leading-tight text-muted-foreground">
270
- {description}
323
+ <span className="mt-0.5 flex items-center gap-1.5 truncate text-xs leading-none text-muted-foreground">
324
+ <IconPuzzle className="size-3 shrink-0" />
325
+ <span className="truncate">Built-in</span>
271
326
  </span>
272
327
  </span>
273
328
  {badge}
274
329
  </button>
275
- {isOpen && <div className="border-t border-border/60">{children}</div>}
330
+ {isOpen && (
331
+ <div className="mb-2 ml-[3.75rem] overflow-hidden rounded-md border border-border/70 bg-background/70">
332
+ {children}
333
+ </div>
334
+ )}
276
335
  </div>
277
336
  );
278
337
  }
279
338
 
339
+ function ToolFilterMenu<T extends string>({
340
+ label,
341
+ value,
342
+ options,
343
+ onChange,
344
+ }: {
345
+ label: string;
346
+ value: T;
347
+ options: Array<{ value: T; label: string }>;
348
+ onChange: (value: T) => void;
349
+ }) {
350
+ const selected = options.find((option) => option.value === value);
351
+ return (
352
+ <DropdownMenu>
353
+ <DropdownMenuTrigger asChild>
354
+ <Button
355
+ type="button"
356
+ variant="outline"
357
+ size="sm"
358
+ className="h-8 cursor-pointer gap-1.5 rounded-md bg-transparent px-2.5 text-sm font-medium"
359
+ >
360
+ <IconAdjustmentsHorizontal className="size-4 text-muted-foreground" />
361
+ <span>{selected?.label ?? label}</span>
362
+ <IconChevronDown className="size-3.5 text-muted-foreground" />
363
+ </Button>
364
+ </DropdownMenuTrigger>
365
+ <DropdownMenuContent align="start" className="min-w-36">
366
+ {options.map((option) => (
367
+ <DropdownMenuItem
368
+ key={option.value}
369
+ onClick={() => onChange(option.value)}
370
+ >
371
+ {option.label}
372
+ </DropdownMenuItem>
373
+ ))}
374
+ </DropdownMenuContent>
375
+ </DropdownMenu>
376
+ );
377
+ }
378
+
280
379
  // ─── Asset Library panel ──────────────────────────────────────────────────────
281
380
 
282
381
  interface AssetLibraryPanelProps {
283
382
  context: DesignExtensionSlotContext;
284
383
  }
285
384
 
286
- function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
287
- const t = useT();
288
- const [pickerOpen, setPickerOpen] = useState(false);
289
- const [pickerReady, setPickerReady] = useState(false);
385
+ type FigmaLibraryAsset = {
386
+ id: string;
387
+ kind: "component" | "component_set";
388
+ fileKey: string;
389
+ nodeId: string | null;
390
+ componentKey: string | null;
391
+ name: string;
392
+ description: string | null;
393
+ thumbnailUrl: string | null;
394
+ renderUrl: string | null;
395
+ insertUrl: string | null;
396
+ sourceUrl: string | null;
397
+ containingFrame: { name: string | null; nodeId: string | null } | null;
398
+ updatedAt: string | null;
399
+ };
400
+
401
+ type FigmaLibraryResponse = {
402
+ fileKey: string;
403
+ total: number;
404
+ returned: number;
405
+ assets: FigmaLibraryAsset[];
406
+ };
407
+
408
+ type DesignNativeAssetKind =
409
+ | "section-frame"
410
+ | "text-block"
411
+ | "button"
412
+ | "card"
413
+ | "input"
414
+ | "nav-bar"
415
+ | "hero"
416
+ | "feature-grid";
417
+
418
+ type DesignNativeAsset = {
419
+ kind: DesignNativeAssetKind;
420
+ title: string;
421
+ description: string;
422
+ category: "primitive" | "component" | "layout";
423
+ componentName: string;
424
+ };
425
+
426
+ type DesignNativeAssetsResponse = {
427
+ source: "design-native";
428
+ assets: DesignNativeAsset[];
429
+ };
430
+
431
+ const NATIVE_ASSET_CATEGORY_LABELS: Record<
432
+ DesignNativeAsset["category"],
433
+ string
434
+ > = {
435
+ primitive: "Primitive",
436
+ component: "Component",
437
+ layout: "Layout",
438
+ };
439
+
440
+ export function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
441
+ const [assetPickerReady, setAssetPickerReady] = useState(false);
442
+ const [figmaFileInput, setFigmaFileInput] = useState("");
443
+ const [figmaQueryInput, setFigmaQueryInput] = useState("");
444
+ const [figmaRequest, setFigmaRequest] = useState<{
445
+ fileUrl: string;
446
+ query?: string;
447
+ } | null>(null);
448
+ const nativeAssets = useActionQuery<DesignNativeAssetsResponse>(
449
+ "list-design-native-assets",
450
+ );
451
+ const insertNativeAsset = useActionMutation("insert-design-native-asset");
290
452
  const insertAsset = useActionMutation("insert-asset");
291
-
292
- useEffect(() => {
293
- if (pickerOpen) setPickerReady(false);
294
- }, [pickerOpen]);
453
+ const insertFigmaAsset = useActionMutation("insert-figma-library-asset");
454
+ const figmaAssets = useActionQuery<FigmaLibraryResponse>(
455
+ "list-figma-library-assets",
456
+ figmaRequest
457
+ ? {
458
+ fileUrl: figmaRequest.fileUrl,
459
+ query: figmaRequest.query,
460
+ limit: 48,
461
+ renderFormat: "svg",
462
+ }
463
+ : undefined,
464
+ { enabled: Boolean(figmaRequest) },
465
+ );
295
466
 
296
467
  const handleReady = useCallback(
297
468
  (_payload: unknown, _event: MessageEvent, ref: EmbeddedAppRef) => {
298
- setPickerReady(true);
299
- ref.postMessage("configure", {});
469
+ setAssetPickerReady(true);
470
+ ref.postMessage("configure", {
471
+ mediaType: "image",
472
+ layout: "vertical",
473
+ callerAppId: "design",
474
+ });
300
475
  },
301
476
  [],
302
477
  );
@@ -304,7 +479,7 @@ function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
304
479
  const handleMessage = useCallback(
305
480
  (name: string, payload: unknown) => {
306
481
  if (name === "close") {
307
- setPickerOpen(false);
482
+ setAssetPickerReady(false);
308
483
  return;
309
484
  }
310
485
  if (name !== "chooseImage" && name !== "chooseAsset") return;
@@ -341,99 +516,293 @@ function AssetLibraryPanel({ context }: AssetLibraryPanelProps) {
341
516
  },
342
517
  },
343
518
  );
344
-
345
- setPickerOpen(false);
346
519
  },
347
520
  [context.designId, context.activeFileId, insertAsset],
348
521
  );
349
522
 
350
- const handleAskAgent = () => {
351
- sendToAgentChat({
352
- message:
353
- "Open the Assets picker so I can choose an image to insert into the design.",
354
- context: [
355
- `Design id: ${context.designId}`,
356
- context.activeFileId ? `Active file id: ${context.activeFileId}` : null,
357
- context.selectedElement?.selector
358
- ? `Selected element selector: ${context.selectedElement.selector}`
359
- : null,
360
- "Call insert-asset with the chosen URL after the user picks from the Assets picker.",
361
- ]
362
- .filter(Boolean)
363
- .join("\n"),
364
- submit: true,
365
- openSidebar: true,
366
- });
523
+ const handleInsertNativeAsset = (asset: DesignNativeAsset) => {
524
+ insertNativeAsset.mutate(
525
+ {
526
+ kind: asset.kind,
527
+ designId: context.designId || undefined,
528
+ fileId: context.activeFileId || undefined,
529
+ },
530
+ {
531
+ onSuccess: () => {
532
+ toast.success(`${asset.title} inserted into design.`);
533
+ },
534
+ onError: (error) => {
535
+ toast.error(error.message || "Failed to insert native asset.");
536
+ },
537
+ },
538
+ );
539
+ };
540
+
541
+ const handleBrowseFigma = (event: FormEvent<HTMLFormElement>) => {
542
+ event.preventDefault();
543
+ const fileUrl = figmaFileInput.trim();
544
+ if (!fileUrl) return;
545
+ const query = figmaQueryInput.trim();
546
+ setFigmaRequest({ fileUrl, query: query || undefined });
547
+ };
548
+
549
+ const handleInsertFigmaAsset = (asset: FigmaLibraryAsset) => {
550
+ const renderUrl = asset.insertUrl ?? asset.renderUrl ?? asset.thumbnailUrl;
551
+ if (!renderUrl) {
552
+ toast.error("Figma did not return an insertable render URL.");
553
+ return;
554
+ }
555
+ insertFigmaAsset.mutate(
556
+ {
557
+ renderUrl,
558
+ fileKey: asset.fileKey,
559
+ nodeId: asset.nodeId ?? undefined,
560
+ componentKey: asset.componentKey ?? undefined,
561
+ kind: asset.kind,
562
+ name: asset.name,
563
+ description: asset.description ?? undefined,
564
+ sourceUrl: asset.sourceUrl ?? undefined,
565
+ designId: context.designId || undefined,
566
+ fileId: context.activeFileId || undefined,
567
+ },
568
+ {
569
+ onSuccess: () => {
570
+ toast.success("Figma asset inserted into design.");
571
+ },
572
+ onError: (error) => {
573
+ toast.error(error.message || "Failed to insert Figma asset.");
574
+ },
575
+ },
576
+ );
367
577
  };
368
578
 
369
579
  return (
370
- <div className="p-2.5">
371
- <p className="mb-2 text-[10px] leading-snug text-muted-foreground">
372
- Browse and insert generated or uploaded images into the active design
373
- screen. Inserts near the selected element when one is active.
374
- </p>
375
- <div className="flex flex-wrap gap-1.5">
376
- <Button
377
- type="button"
378
- size="sm"
379
- className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
380
- disabled={insertAsset.isPending || !context.activeFileId}
381
- onClick={() => setPickerOpen(true)}
580
+ <div className="flex min-h-0 flex-1 flex-col">
581
+ <Tabs defaultValue="native" className="flex min-h-0 flex-1 flex-col">
582
+ <div className="border-b border-border/60 px-2 py-1.5">
583
+ <TabsList className="grid h-7 w-full grid-cols-3 rounded-md p-0.5">
584
+ <TabsTrigger value="native" className="h-6 gap-1 px-2 text-[11px]">
585
+ <IconAssembly className="size-3" />
586
+ Native
587
+ </TabsTrigger>
588
+ <TabsTrigger value="media" className="h-6 gap-1 px-2 text-[11px]">
589
+ <IconPhoto className="size-3" />
590
+ Media
591
+ </TabsTrigger>
592
+ <TabsTrigger value="figma" className="h-6 gap-1 px-2 text-[11px]">
593
+ <IconBrandFigma className="size-3" />
594
+ Figma
595
+ </TabsTrigger>
596
+ </TabsList>
597
+ </div>
598
+
599
+ <TabsContent
600
+ value="native"
601
+ className="design-inspector-scroll m-0 min-h-0 flex-1 overflow-y-auto p-2.5"
382
602
  >
383
- <IconPhoto className="size-3" />
384
- {insertAsset.isPending ? "Inserting…" : "Browse Assets"}
385
- </Button>
386
- <Button
387
- type="button"
388
- variant="outline"
389
- size="sm"
390
- className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
391
- onClick={handleAskAgent}
603
+ {nativeAssets.isLoading && (
604
+ <div className="space-y-1.5">
605
+ {Array.from({ length: 5 }).map((_, index) => (
606
+ <Skeleton key={index} className="h-16 rounded-md" />
607
+ ))}
608
+ </div>
609
+ )}
610
+
611
+ {nativeAssets.isError && (
612
+ <p className="rounded border border-destructive/30 bg-destructive/5 px-2 py-1.5 text-[10px] leading-snug text-destructive">
613
+ {nativeAssets.error.message}
614
+ </p>
615
+ )}
616
+
617
+ {!nativeAssets.isLoading && nativeAssets.data && (
618
+ <div className="space-y-1.5">
619
+ {nativeAssets.data.assets.map((asset) => (
620
+ <button
621
+ key={asset.kind}
622
+ type="button"
623
+ disabled={
624
+ !context.activeFileId || insertNativeAsset.isPending
625
+ }
626
+ onClick={() => handleInsertNativeAsset(asset)}
627
+ className="group flex w-full cursor-pointer items-start gap-2 rounded-md border border-border bg-background px-2 py-2 text-left transition hover:border-primary/50 hover:bg-accent/30 disabled:cursor-not-allowed disabled:opacity-55"
628
+ >
629
+ <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/50 text-muted-foreground group-hover:text-foreground">
630
+ <IconAssembly className="size-4" />
631
+ </span>
632
+ <span className="min-w-0 flex-1">
633
+ <span className="flex items-center gap-1.5">
634
+ <span className="truncate text-[11px] font-medium leading-tight text-foreground">
635
+ {asset.title}
636
+ </span>
637
+ <span className="shrink-0 rounded border border-border/70 px-1 py-0.5 text-[9px] leading-none text-muted-foreground">
638
+ {NATIVE_ASSET_CATEGORY_LABELS[asset.category]}
639
+ </span>
640
+ </span>
641
+ <span className="mt-1 line-clamp-2 text-[10px] leading-snug text-muted-foreground">
642
+ {asset.description}
643
+ </span>
644
+ </span>
645
+ </button>
646
+ ))}
647
+ </div>
648
+ )}
649
+ </TabsContent>
650
+
651
+ <TabsContent
652
+ value="media"
653
+ className="m-0 flex min-h-0 flex-1 flex-col overflow-hidden"
392
654
  >
393
- <IconSparkles className="size-3" />
394
- {t("designEditor.askAgent") || "Ask agent"}
395
- </Button>
396
- </div>
397
- {!context.activeFileId && (
398
- <p className="mt-1.5 text-[10px] text-amber-600 dark:text-amber-400">
399
- Open a design screen first to insert assets.
400
- </p>
401
- )}
655
+ <div className="relative min-h-[540px] flex-1 overflow-hidden bg-background">
656
+ {!assetPickerReady && (
657
+ <div className="absolute inset-0 z-10 p-2.5">
658
+ <Skeleton className="h-full w-full rounded-none" />
659
+ </div>
660
+ )}
661
+ <EmbeddedApp
662
+ url={assetsPickerUrl()}
663
+ title="Assets picker"
664
+ onLoad={() => setAssetPickerReady(true)}
665
+ onReady={handleReady}
666
+ onMessage={handleMessage}
667
+ className="h-full w-full"
668
+ />
669
+ </div>
670
+ </TabsContent>
402
671
 
403
- {/* Assets picker overlay */}
404
- {pickerOpen && (
405
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
406
- <div
407
- data-assets-picker-overlay
408
- className="relative flex h-[min(86vh,760px)] w-[min(96vw,1040px)] flex-col overflow-hidden rounded-xl border border-border bg-popover shadow-2xl"
409
- >
410
- <div className="flex h-10 shrink-0 items-center border-b border-border px-4">
411
- <span className="flex-1 text-sm font-medium">Assets Library</span>
412
- <button
413
- type="button"
414
- aria-label="Close picker"
415
- onClick={() => setPickerOpen(false)}
416
- className="flex size-7 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
672
+ <TabsContent
673
+ value="figma"
674
+ className="design-inspector-scroll m-0 min-h-0 flex-1 overflow-y-auto p-2.5"
675
+ >
676
+ <form className="space-y-1.5" onSubmit={handleBrowseFigma}>
677
+ <Input
678
+ value={figmaFileInput}
679
+ onChange={(event) => setFigmaFileInput(event.target.value)}
680
+ placeholder="Figma file URL or key"
681
+ className="h-7 text-[11px]"
682
+ />
683
+ <div className="flex gap-1.5">
684
+ <Input
685
+ value={figmaQueryInput}
686
+ onChange={(event) => setFigmaQueryInput(event.target.value)}
687
+ placeholder="Search components"
688
+ className="h-7 min-w-0 flex-1 text-[11px]"
689
+ />
690
+ <Button
691
+ type="submit"
692
+ size="sm"
693
+ className="h-7 cursor-pointer gap-1 px-2 text-[11px]"
694
+ disabled={!figmaFileInput.trim() || figmaAssets.isFetching}
417
695
  >
418
- ×
419
- </button>
696
+ <IconSearch className="size-3" />
697
+ {figmaAssets.isFetching ? "Loading" : "Browse"}
698
+ </Button>
420
699
  </div>
421
- <div className="relative min-h-0 flex-1">
422
- {!pickerReady && (
423
- <div className="absolute inset-0 flex items-center justify-center">
424
- <Skeleton className="h-full w-full rounded-none" />
425
- </div>
426
- )}
427
- <EmbeddedApp
428
- url={assetsPickerUrl()}
429
- title="Assets picker"
430
- onReady={handleReady}
431
- onMessage={handleMessage}
432
- className="h-full w-full"
433
- />
700
+ </form>
701
+
702
+ {figmaAssets.isError && (
703
+ <p className="mt-2 rounded border border-destructive/30 bg-destructive/5 px-2 py-1.5 text-[10px] leading-snug text-destructive">
704
+ {figmaAssets.error.message}
705
+ </p>
706
+ )}
707
+
708
+ {figmaAssets.isFetching && (
709
+ <div className="mt-2 grid grid-cols-2 gap-1.5">
710
+ {Array.from({ length: 4 }).map((_, index) => (
711
+ <Skeleton key={index} className="aspect-square rounded-md" />
712
+ ))}
434
713
  </div>
435
- </div>
436
- </div>
714
+ )}
715
+
716
+ {!figmaAssets.isFetching &&
717
+ figmaAssets.data &&
718
+ figmaAssets.data.assets.length === 0 && (
719
+ <p className="mt-2 rounded border border-border bg-muted/30 px-2 py-1.5 text-[10px] leading-snug text-muted-foreground">
720
+ No components matched this Figma file and search.
721
+ </p>
722
+ )}
723
+
724
+ {!figmaAssets.isFetching &&
725
+ figmaAssets.data &&
726
+ figmaAssets.data.assets.length > 0 && (
727
+ <div className="mt-2 space-y-1.5">
728
+ <div className="flex items-center justify-between gap-2 text-[10px] text-muted-foreground">
729
+ <span>
730
+ {figmaAssets.data.returned} of {figmaAssets.data.total}
731
+ </span>
732
+ <span className="truncate">{figmaAssets.data.fileKey}</span>
733
+ </div>
734
+ <div className="grid grid-cols-2 gap-1.5">
735
+ {figmaAssets.data.assets.map((asset) => {
736
+ const preview = asset.thumbnailUrl ?? asset.renderUrl;
737
+ const canInsert = Boolean(
738
+ context.activeFileId &&
739
+ (asset.insertUrl ??
740
+ asset.renderUrl ??
741
+ asset.thumbnailUrl),
742
+ );
743
+ return (
744
+ <div
745
+ key={asset.id}
746
+ className="overflow-hidden rounded-md border border-border bg-background"
747
+ >
748
+ <button
749
+ type="button"
750
+ className="block aspect-square w-full cursor-pointer bg-muted/30"
751
+ disabled={!canInsert || insertFigmaAsset.isPending}
752
+ onClick={() => handleInsertFigmaAsset(asset)}
753
+ >
754
+ {preview ? (
755
+ <img
756
+ src={preview}
757
+ alt={asset.name}
758
+ className="size-full object-contain p-2"
759
+ loading="lazy"
760
+ />
761
+ ) : (
762
+ <span className="flex size-full items-center justify-center text-muted-foreground">
763
+ <IconAssembly className="size-5" />
764
+ </span>
765
+ )}
766
+ </button>
767
+ <div className="border-t border-border/70 px-1.5 py-1">
768
+ <div className="flex items-center gap-1">
769
+ <span className="min-w-0 flex-1 truncate text-[10px] font-medium leading-tight">
770
+ {asset.name}
771
+ </span>
772
+ {asset.sourceUrl && (
773
+ <a
774
+ href={asset.sourceUrl}
775
+ target="_blank"
776
+ rel="noreferrer"
777
+ className="shrink-0 text-muted-foreground hover:text-foreground"
778
+ aria-label="Open in Figma"
779
+ >
780
+ <IconExternalLink className="size-3" />
781
+ </a>
782
+ )}
783
+ </div>
784
+ <p className="mt-0.5 truncate text-[9px] text-muted-foreground">
785
+ {asset.kind === "component_set"
786
+ ? "Component set"
787
+ : "Component"}
788
+ {asset.containingFrame?.name
789
+ ? ` · ${asset.containingFrame.name}`
790
+ : ""}
791
+ </p>
792
+ </div>
793
+ </div>
794
+ );
795
+ })}
796
+ </div>
797
+ </div>
798
+ )}
799
+ </TabsContent>
800
+ </Tabs>
801
+
802
+ {!context.activeFileId && (
803
+ <p className="border-t border-border/60 px-2.5 py-1.5 text-[10px] text-amber-600 dark:text-amber-400">
804
+ Open a design screen first to insert assets.
805
+ </p>
437
806
  )}
438
807
  </div>
439
808
  );
@@ -445,12 +814,33 @@ interface ShaderFillsExtPanelProps {
445
814
  context: DesignExtensionSlotContext;
446
815
  }
447
816
 
817
+ interface PreviewedShaderFill {
818
+ descriptor: ShaderDescriptor;
819
+ fileId?: string;
820
+ nodeId?: string;
821
+ selector?: string;
822
+ }
823
+
448
824
  function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
449
825
  const [showShaders, setShowShaders] = useState(false);
826
+ const clearPreviewRef = useRef(context.onShaderFillPreviewClear);
827
+ useEffect(() => {
828
+ clearPreviewRef.current = context.onShaderFillPreviewClear;
829
+ }, [context.onShaderFillPreviewClear]);
830
+ useEffect(
831
+ () => () => {
832
+ clearPreviewRef.current?.();
833
+ },
834
+ [],
835
+ );
836
+ const closeShaders = () => {
837
+ context.onShaderFillPreviewClear?.();
838
+ setShowShaders(false);
839
+ };
450
840
  // The most recently previewed descriptor — Apply persists exactly this one,
451
841
  // so the write is intentional (one atomic call) rather than firing on every
452
842
  // slider tweak.
453
- const [previewed, setPreviewed] = useState<ShaderDescriptor | null>(null);
843
+ const [previewed, setPreviewed] = useState<PreviewedShaderFill | null>(null);
454
844
  const applyShaderFill = useActionMutation("apply-shader-fill");
455
845
 
456
846
  // The persisting apply path needs an HTML file plus a target element. Without
@@ -460,6 +850,16 @@ function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
460
850
  const canPersist = Boolean(
461
851
  context.activeFileId && (targetNodeId || targetSelector),
462
852
  );
853
+ const previewMatchesTarget = Boolean(
854
+ previewed &&
855
+ previewed.fileId === context.activeFileId &&
856
+ previewed.nodeId === targetNodeId &&
857
+ previewed.selector === targetSelector,
858
+ );
859
+
860
+ useEffect(() => {
861
+ setPreviewed(null);
862
+ }, [context.activeFileId, targetNodeId, targetSelector]);
463
863
 
464
864
  const persistFill = (descriptor: ShaderDescriptor) => {
465
865
  if (!canPersist) return;
@@ -482,13 +882,41 @@ function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
482
882
  kind: "design-file" as const,
483
883
  designId: context.designId || undefined,
484
884
  fileId: context.activeFileId || undefined,
885
+ revision: context.activeFileUpdatedAt || undefined,
886
+ currentContent: context.activeContent,
485
887
  },
486
888
  },
487
889
  {
488
890
  onSuccess: (res) => {
489
- const r = res as { persisted?: boolean } | undefined;
891
+ const r = res as
892
+ | {
893
+ fileId?: unknown;
894
+ patchedContent?: unknown;
895
+ persisted?: boolean;
896
+ conflict?: boolean;
897
+ error?: unknown;
898
+ note?: unknown;
899
+ updatedAt?: unknown;
900
+ }
901
+ | undefined;
490
902
  if (r?.persisted) {
903
+ if (
904
+ typeof r.fileId === "string" &&
905
+ typeof r.patchedContent === "string"
906
+ ) {
907
+ context.onShaderFillApplied?.(
908
+ r.fileId,
909
+ r.patchedContent,
910
+ typeof r.updatedAt === "string" ? r.updatedAt : undefined,
911
+ );
912
+ }
491
913
  toast.success("Shader fill applied to the selected element.");
914
+ } else if (r?.conflict) {
915
+ toast.error(
916
+ typeof r.error === "string" || typeof r.note === "string"
917
+ ? String(r.error ?? r.note)
918
+ : "This file changed since the shader fill was previewed. Refresh and try again.",
919
+ );
492
920
  } else {
493
921
  toast.message("Shader fill previewed — nothing was written.");
494
922
  }
@@ -535,14 +963,20 @@ function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
535
963
  return (
536
964
  <div className="flex flex-col">
537
965
  <ShaderFillsPanel
538
- onApply={(descriptor, _css) => {
966
+ onApply={(descriptor, css) => {
539
967
  // Preview only: ShaderFillsPanel fires apply-shader (planning/codegen)
540
968
  // for agent context on every tune and the iframe shows the gradient.
541
969
  // We just record the latest descriptor here; the explicit Apply
542
970
  // button below performs the single intentional persist write.
543
- setPreviewed(descriptor);
971
+ setPreviewed({
972
+ descriptor,
973
+ fileId: context.activeFileId || undefined,
974
+ nodeId: targetNodeId,
975
+ selector: targetSelector,
976
+ });
977
+ context.onShaderFillPreview?.(descriptor, css);
544
978
  }}
545
- onBack={() => setShowShaders(false)}
979
+ onBack={closeShaders}
546
980
  applyContext={{
547
981
  designId: context.designId || undefined,
548
982
  fileId: context.activeFileId || undefined,
@@ -558,15 +992,17 @@ function ShaderFillsExtPanel({ context }: ShaderFillsExtPanelProps) {
558
992
  type="button"
559
993
  size="sm"
560
994
  className="h-6 cursor-pointer gap-1 px-2 text-[11px]"
561
- disabled={!previewed || applyShaderFill.isPending}
995
+ disabled={!previewMatchesTarget || applyShaderFill.isPending}
562
996
  onClick={() => {
563
- if (previewed) persistFill(previewed);
997
+ if (previewMatchesTarget && previewed) {
998
+ persistFill(previewed.descriptor);
999
+ }
564
1000
  }}
565
1001
  >
566
1002
  <IconSparkles className="size-3" />
567
1003
  {applyShaderFill.isPending
568
1004
  ? "Applying…"
569
- : previewed
1005
+ : previewMatchesTarget
570
1006
  ? "Apply fill"
571
1007
  : "Pick a preset"}
572
1008
  </Button>
@@ -691,11 +1127,17 @@ function MotionPresetsPanel({ context }: MotionPresetsPanelProps) {
691
1127
  export function DesignExtensionsPanel({
692
1128
  context,
693
1129
  className,
1130
+ hideAssetLibrary = false,
1131
+ title,
694
1132
  }: DesignExtensionsPanelProps) {
695
1133
  const t = useT();
696
1134
  const queryClient = useQueryClient();
697
1135
  const [createOpen, setCreateOpen] = useState(false);
698
1136
  const [installingId, setInstallingId] = useState<string | null>(null);
1137
+ const [searchQuery, setSearchQuery] = useState("");
1138
+ const [sourceFilter, setSourceFilter] = useState<ToolSourceFilter>("all");
1139
+ const [categoryFilter, setCategoryFilter] =
1140
+ useState<ToolCategoryFilter>("all");
699
1141
  const [openFirstParty, setOpenFirstParty] = useState<FirstPartyExtId | null>(
700
1142
  null,
701
1143
  );
@@ -714,12 +1156,7 @@ export function DesignExtensionsPanel({
714
1156
  setOpenFirstParty((prev) => (prev === id ? null : id));
715
1157
  };
716
1158
 
717
- const submitCreatePrompt: PromptComposerSubmitHandler = (
718
- text: string,
719
- _files,
720
- _references,
721
- options: PromptComposerSubmitOptions,
722
- ) => {
1159
+ const submitCreatePrompt: CreateExtensionSubmitHandler = (text: string) => {
723
1160
  const trimmed = text.trim();
724
1161
  if (!trimmed) return;
725
1162
  sendToAgentChat({
@@ -728,9 +1165,6 @@ export function DesignExtensionsPanel({
728
1165
  submit: true,
729
1166
  openSidebar: true,
730
1167
  newTab: true,
731
- model: options.model,
732
- engine: options.engine,
733
- effort: options.effort,
734
1168
  });
735
1169
  setCreateOpen(false);
736
1170
  };
@@ -763,18 +1197,12 @@ export function DesignExtensionsPanel({
763
1197
  };
764
1198
 
765
1199
  // First-party extension row config
766
- const firstPartyRows: Array<{
767
- id: FirstPartyExtId;
768
- label: string;
769
- description: string;
770
- icon: React.ReactNode;
771
- badge?: React.ReactNode;
772
- panel: React.ReactNode;
773
- }> = [
1200
+ const allFirstPartyRows: FirstPartyRow[] = [
774
1201
  {
775
1202
  id: "design.asset-library",
776
1203
  label: "Asset Library",
777
1204
  description: "Browse & insert assets into the active screen",
1205
+ category: "plugins",
778
1206
  icon: <IconPhoto className="size-3.5" />,
779
1207
  panel: <AssetLibraryPanel context={context} />,
780
1208
  },
@@ -782,6 +1210,7 @@ export function DesignExtensionsPanel({
782
1210
  id: "design.shader-fills",
783
1211
  label: "Shader Fills",
784
1212
  description: "GPU shader fill presets — preview & apply",
1213
+ category: "shader",
785
1214
  icon: <IconSparkles className="size-3.5" />,
786
1215
  panel: <ShaderFillsExtPanel context={context} />,
787
1216
  },
@@ -789,39 +1218,62 @@ export function DesignExtensionsPanel({
789
1218
  id: "design.token-auditor",
790
1219
  label: "Token Auditor",
791
1220
  description: "Audit CSS token usage, flag clashes",
792
- icon: <IconPalette className="size-3.5" />,
1221
+ category: "tokens",
1222
+ icon: <IconAssembly className="size-3.5" />,
793
1223
  panel: <TokenAuditorPanel context={context} />,
794
1224
  },
795
1225
  {
796
1226
  id: "design.motion-presets",
797
1227
  label: "Motion Presets",
798
1228
  description: "One-click animation presets for elements",
1229
+ category: "motion",
799
1230
  icon: <IconPlayerPlay className="size-3.5" />,
800
1231
  panel: <MotionPresetsPanel context={context} />,
801
1232
  },
802
1233
  ];
1234
+ const firstPartyRows = allFirstPartyRows.filter(
1235
+ (row) => !hideAssetLibrary || row.id !== "design.asset-library",
1236
+ );
1237
+ const normalizedSearch = searchQuery.trim().toLowerCase();
1238
+ const sourceMatches = (source: ToolSourceFilter) =>
1239
+ sourceFilter === "all" || sourceFilter === source;
1240
+ const categoryMatches = (category: ToolCategoryFilter) =>
1241
+ categoryFilter === "all" || categoryFilter === category;
1242
+ const textMatches = (name: string, description?: string | null) => {
1243
+ if (!normalizedSearch) return true;
1244
+ return `${name} ${description ?? ""}`
1245
+ .toLowerCase()
1246
+ .includes(normalizedSearch);
1247
+ };
1248
+ const visibleFirstPartyRows = firstPartyRows.filter(
1249
+ (row) =>
1250
+ sourceMatches("built-in") &&
1251
+ categoryMatches(row.category) &&
1252
+ textMatches(row.label, row.description),
1253
+ );
1254
+ const visibleInstalls = installs.filter(
1255
+ (install) =>
1256
+ sourceMatches("extensions") &&
1257
+ categoryMatches("plugins") &&
1258
+ textMatches(install.name, install.description),
1259
+ );
1260
+ const visibleInstallable = installable.filter(
1261
+ (extension) =>
1262
+ sourceMatches("extensions") &&
1263
+ categoryMatches("plugins") &&
1264
+ textMatches(extension.name, extension.description),
1265
+ );
1266
+ const hasAnyVisibleTool =
1267
+ visibleFirstPartyRows.length > 0 ||
1268
+ visibleInstalls.length > 0 ||
1269
+ visibleInstallable.length > 0;
803
1270
 
804
1271
  return (
805
1272
  <div className={cn("flex min-h-0 flex-1 flex-col", className)}>
806
- {/* Section header 32 px tall, matches PanelSection / design inspector headers */}
807
- <div className="flex min-h-8 shrink-0 items-center gap-1.5 border-b border-border/60 px-3">
808
- <h3 className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground">
809
- {t("designEditor.extensions")}
1273
+ <div className="flex h-16 shrink-0 items-center gap-3 border-b border-border/60 px-4">
1274
+ <h3 className="min-w-0 flex-1 truncate text-xl font-semibold tracking-tight text-foreground">
1275
+ {title ?? t("designEditor.extensions")}
810
1276
  </h3>
811
- <Tooltip>
812
- <TooltipTrigger asChild>
813
- <a
814
- href="https://www.agent-native.com/docs/extensions"
815
- target="_blank"
816
- rel="noreferrer"
817
- className="inline-flex size-6 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
818
- aria-label={t("designEditor.extensionsDocs")}
819
- >
820
- <IconExternalLink className="size-3.5" />
821
- </a>
822
- </TooltipTrigger>
823
- <TooltipContent>{t("designEditor.extensionsDocs")}</TooltipContent>
824
- </Tooltip>
825
1277
  <CreateExtensionPopover
826
1278
  open={createOpen}
827
1279
  onOpenChange={setCreateOpen}
@@ -829,117 +1281,146 @@ export function DesignExtensionsPanel({
829
1281
  />
830
1282
  </div>
831
1283
 
832
- <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 pb-2 pt-1.5">
833
- {/* ── First-party built-in extensions ──────────────────────────── */}
834
- <div className="mb-3 space-y-1">
835
- <p className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
836
- Built-in
837
- </p>
838
- {firstPartyRows.map((row) => (
839
- <FirstPartyExtRow
840
- key={row.id}
841
- id={row.id}
842
- label={row.label}
843
- description={row.description}
844
- icon={row.icon}
845
- badge={row.badge}
846
- isOpen={openFirstParty === row.id}
847
- onToggle={() => toggleFirstParty(row.id)}
848
- >
849
- {row.panel}
850
- </FirstPartyExtRow>
851
- ))}
1284
+ <div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-4 pt-4">
1285
+ <div className="relative mb-4">
1286
+ <IconSearch className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
1287
+ <Input
1288
+ value={searchQuery}
1289
+ onChange={(event) => setSearchQuery(event.target.value)}
1290
+ placeholder="Search all tools"
1291
+ className="h-9 border-0 bg-muted/70 pl-8 text-sm shadow-none focus-visible:ring-1"
1292
+ />
852
1293
  </div>
853
1294
 
854
- {/* ── Agent help text ───────────────────────────────────────────── */}
855
- <div className="mb-3 flex items-start gap-1.5 rounded-md bg-muted/40 px-2 py-1.5">
856
- <IconSparkles className="mt-0.5 size-3 shrink-0 text-muted-foreground/70" />
857
- <p className="text-[10px] leading-snug text-muted-foreground">
858
- The agent can insert assets, apply shader fills, audit tokens, and
859
- apply motion presets. Ask in the chat sidebar for help.
860
- </p>
1295
+ <div className="mb-5 flex flex-wrap gap-2">
1296
+ <ToolFilterMenu
1297
+ label="Source"
1298
+ value={sourceFilter}
1299
+ onChange={setSourceFilter}
1300
+ options={[
1301
+ { value: "all", label: "Source" },
1302
+ { value: "built-in", label: "Built-in" },
1303
+ { value: "extensions", label: "Extensions" },
1304
+ ]}
1305
+ />
1306
+ <ToolFilterMenu
1307
+ label="Category"
1308
+ value={categoryFilter}
1309
+ onChange={setCategoryFilter}
1310
+ options={[
1311
+ { value: "all", label: "Category" },
1312
+ { value: "shader", label: "Shaders" },
1313
+ { value: "tokens", label: "Tokens" },
1314
+ { value: "motion", label: "Motion" },
1315
+ { value: "plugins", label: "Plugins" },
1316
+ ]}
1317
+ />
861
1318
  </div>
862
1319
 
863
- {/* ── User-installed extensions ──────────────────────────────────── */}
864
1320
  {isLoading ? (
865
- <div className="space-y-1.5">
866
- <Skeleton className="h-24 rounded" />
867
- <Skeleton className="h-32 rounded" />
868
- </div>
869
- ) : installs.length > 0 ? (
870
- <div className="space-y-1.5">
871
- <p className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
872
- Installed
873
- </p>
874
- {installs.map((install) => (
875
- <EmbeddedExtension
876
- key={install.installId}
877
- extensionId={install.extensionId}
878
- slotId={slotId}
879
- context={context}
880
- initialHeight={180}
881
- className="overflow-hidden rounded border border-border bg-background"
882
- />
883
- ))}
1321
+ <div className="space-y-3">
1322
+ <Skeleton className="h-12 rounded-md" />
1323
+ <Skeleton className="h-12 rounded-md" />
1324
+ <Skeleton className="h-12 rounded-md" />
884
1325
  </div>
885
1326
  ) : (
886
- /* Empty state for user extensions — compact */
887
- <div className="flex flex-col items-center gap-2 py-4 text-center">
888
- <div className="flex size-7 items-center justify-center rounded-lg bg-muted/60">
889
- <IconPuzzle className="size-3.5 text-muted-foreground/70" />
890
- </div>
891
- <div>
892
- <p className="text-[11px] font-medium text-foreground">
893
- {t("designEditor.extensionsEmptyTitle")}
894
- </p>
895
- <p className="mt-0.5 text-[10px] leading-snug text-muted-foreground">
896
- {t("designEditor.extensionsEmptyDescription")}
897
- </p>
898
- </div>
899
- <Button
900
- type="button"
901
- size="sm"
902
- className="h-6 cursor-pointer px-2.5 text-[11px]"
903
- onClick={() => setCreateOpen(true)}
904
- >
905
- <IconPlus className="size-3" />
906
- {t("designEditor.addExtension")}
907
- </Button>
908
- </div>
909
- )}
1327
+ <>
1328
+ {visibleInstalls.length > 0 ? (
1329
+ <div className="mb-6">
1330
+ <p className="mb-3 text-sm font-medium text-muted-foreground">
1331
+ Recents
1332
+ </p>
1333
+ <div className="space-y-2">
1334
+ {visibleInstalls.map((install) => (
1335
+ <EmbeddedExtension
1336
+ key={install.installId}
1337
+ extensionId={install.extensionId}
1338
+ slotId={slotId}
1339
+ context={context}
1340
+ initialHeight={180}
1341
+ className="overflow-hidden rounded-md border border-border bg-background"
1342
+ />
1343
+ ))}
1344
+ </div>
1345
+ </div>
1346
+ ) : null}
1347
+
1348
+ {visibleFirstPartyRows.length > 0 ? (
1349
+ <div className="mb-6">
1350
+ <p className="mb-3 text-sm font-medium text-muted-foreground">
1351
+ Suggested
1352
+ </p>
1353
+ <div className="space-y-1">
1354
+ {visibleFirstPartyRows.map((row) => (
1355
+ <FirstPartyExtRow
1356
+ key={row.id}
1357
+ id={row.id}
1358
+ label={row.label}
1359
+ description={row.description}
1360
+ icon={row.icon}
1361
+ badge={row.badge}
1362
+ isOpen={openFirstParty === row.id}
1363
+ onToggle={() => toggleFirstParty(row.id)}
1364
+ >
1365
+ {row.panel}
1366
+ </FirstPartyExtRow>
1367
+ ))}
1368
+ </div>
1369
+ </div>
1370
+ ) : null}
1371
+
1372
+ {visibleInstallable.length > 0 ? (
1373
+ <div className="border-t border-border/60 pt-4">
1374
+ <p className="mb-3 text-sm font-medium text-muted-foreground">
1375
+ Available
1376
+ </p>
1377
+ <div className="space-y-1">
1378
+ {visibleInstallable.map((extension) => (
1379
+ <button
1380
+ key={extension.extensionId}
1381
+ type="button"
1382
+ disabled={installingId === extension.extensionId}
1383
+ onClick={() => installExtension(extension.extensionId)}
1384
+ className="group flex w-full cursor-pointer items-center gap-3 rounded-md px-2 py-2.5 text-left transition-colors hover:bg-accent/60 active:bg-accent disabled:cursor-default disabled:opacity-50"
1385
+ >
1386
+ <span className="flex size-10 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/70 text-muted-foreground transition-colors group-hover:border-border group-hover:bg-muted">
1387
+ <IconPuzzle className="size-4" />
1388
+ </span>
1389
+ <span className="min-w-0 flex-1">
1390
+ <span className="block truncate text-sm font-medium leading-tight text-foreground">
1391
+ {extension.name}
1392
+ </span>
1393
+ <span className="mt-0.5 flex items-center gap-1.5 truncate text-xs leading-none text-muted-foreground">
1394
+ <IconPuzzle className="size-3 shrink-0" />
1395
+ <span className="truncate">
1396
+ {extension.description || "Extension"}
1397
+ </span>
1398
+ </span>
1399
+ </span>
1400
+ <span className="shrink-0 text-xs font-medium text-muted-foreground">
1401
+ {t("designEditor.extensionsInstall")}
1402
+ </span>
1403
+ </button>
1404
+ ))}
1405
+ </div>
1406
+ </div>
1407
+ ) : null}
910
1408
 
911
- {/* ── Available (installable) extensions ────────────────────────── */}
912
- {installable.length > 0 ? (
913
- <div className="mt-3 border-t border-border/60 pt-1.5">
914
- <p className="mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
915
- {t("designEditor.extensionsAvailable")}
916
- </p>
917
- <div className="space-y-px">
918
- {installable.map((extension) => (
919
- <button
920
- key={extension.extensionId}
921
- type="button"
922
- disabled={installingId === extension.extensionId}
923
- onClick={() => installExtension(extension.extensionId)}
924
- className="flex h-6 w-full cursor-pointer items-center gap-1.5 rounded px-2 text-left transition-colors hover:bg-accent active:bg-accent/80 disabled:cursor-default disabled:opacity-50"
925
- >
926
- <IconSparkles className="size-3 shrink-0 text-muted-foreground" />
927
- <span className="min-w-0 flex-1 truncate text-[11px] font-medium text-foreground">
928
- {extension.name}
929
- </span>
930
- {extension.description ? (
931
- <span className="hidden truncate text-[11px] text-muted-foreground sm:block">
932
- {extension.description}
933
- </span>
934
- ) : null}
935
- <span className="shrink-0 text-[11px] text-muted-foreground">
936
- {t("designEditor.extensionsInstall")}
937
- </span>
938
- </button>
939
- ))}
940
- </div>
941
- </div>
942
- ) : null}
1409
+ {!hasAnyVisibleTool ? (
1410
+ <div className="py-12 text-center">
1411
+ <div className="mx-auto mb-3 flex size-9 items-center justify-center rounded-md bg-muted/70">
1412
+ <IconSearch className="size-4 text-muted-foreground" />
1413
+ </div>
1414
+ <p className="text-sm font-medium text-foreground">
1415
+ No tools found
1416
+ </p>
1417
+ <p className="mx-auto mt-1 max-w-52 text-xs leading-5 text-muted-foreground">
1418
+ Try another search or clear the filters.
1419
+ </p>
1420
+ </div>
1421
+ ) : null}
1422
+ </>
1423
+ )}
943
1424
  </div>
944
1425
  </div>
945
1426
  );
@@ -954,40 +1435,65 @@ function CreateExtensionPopover({
954
1435
  }: {
955
1436
  open: boolean;
956
1437
  onOpenChange: (open: boolean) => void;
957
- onSubmit: PromptComposerSubmitHandler;
1438
+ onSubmit: CreateExtensionSubmitHandler;
958
1439
  }) {
959
1440
  const t = useT();
1441
+ const [draft, setDraft] = useState("");
1442
+ const canSubmit = draft.trim().length > 0;
1443
+ const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
1444
+ event.preventDefault();
1445
+ if (!canSubmit) return;
1446
+ onSubmit(draft);
1447
+ setDraft("");
1448
+ };
1449
+
960
1450
  return (
961
1451
  <Popover open={open} onOpenChange={onOpenChange}>
962
- <Tooltip>
963
- <TooltipTrigger asChild>
964
- <PopoverTrigger asChild>
1452
+ <PopoverTrigger asChild>
1453
+ <Button
1454
+ type="button"
1455
+ variant="outline"
1456
+ size="sm"
1457
+ className="h-8 cursor-pointer gap-1.5 rounded-md bg-transparent px-3 text-sm font-medium"
1458
+ aria-label={t("designEditor.addExtension")}
1459
+ >
1460
+ Create
1461
+ <IconChevronDown className="size-3.5 text-muted-foreground" />
1462
+ </Button>
1463
+ </PopoverTrigger>
1464
+ <PopoverContent align="end" sideOffset={8} className="w-80 p-3">
1465
+ <form onSubmit={handleSubmit} className="space-y-3">
1466
+ <p className="px-0.5 text-sm font-semibold text-foreground">
1467
+ {t("designEditor.extensionsPromptTitle")}
1468
+ </p>
1469
+ <Textarea
1470
+ autoFocus
1471
+ value={draft}
1472
+ onChange={(event) => setDraft(event.target.value)}
1473
+ placeholder={t("designEditor.extensionsPlaceholder")}
1474
+ className="min-h-24 resize-none border-border/80 bg-background/80 text-sm shadow-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0"
1475
+ />
1476
+ <div className="flex justify-end gap-2">
965
1477
  <Button
966
1478
  type="button"
967
1479
  variant="ghost"
968
- size="icon"
969
- className="size-6 rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
970
- aria-label={t("designEditor.addExtension")}
1480
+ size="sm"
1481
+ className="h-8 px-3"
1482
+ onClick={() => onOpenChange(false)}
971
1483
  >
972
- <IconPlus className="size-3.5" />
1484
+ Cancel
973
1485
  </Button>
974
- </PopoverTrigger>
975
- </TooltipTrigger>
976
- <TooltipContent>{t("designEditor.addExtension")}</TooltipContent>
977
- </Tooltip>
978
- <PopoverContent align="end" sideOffset={6} className="w-80 p-3">
979
- <p className="px-1 pb-2 text-sm font-semibold text-foreground">
980
- {t("designEditor.extensionsPromptTitle")}
981
- </p>
982
- <PromptComposer
983
- autoFocus
984
- attachmentsEnabled={false}
985
- plusMenuMode="hidden"
986
- layoutVariant="compact"
987
- draftScope="design:editor-extension-create"
988
- placeholder={t("designEditor.extensionsPlaceholder")}
989
- onSubmit={onSubmit}
990
- />
1486
+ <Button
1487
+ type="submit"
1488
+ size="sm"
1489
+ className="h-8 gap-1.5 px-3"
1490
+ disabled={!canSubmit}
1491
+ >
1492
+ <IconSparkles className="size-3.5" />
1493
+ Create
1494
+ </Button>
1495
+ </div>
1496
+ </form>
991
1497
  </PopoverContent>
992
1498
  </Popover>
993
1499
  );