@agent-native/core 0.131.4 → 0.131.6

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 (330) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +69 -0
  3. package/corpus/core/docs/content/agent-native-toolkit.mdx +13 -6
  4. package/corpus/core/docs/content/cloneable-saas.mdx +5 -5
  5. package/corpus/core/docs/content/creating-templates.mdx +9 -0
  6. package/corpus/core/docs/content/deployment.mdx +14 -10
  7. package/corpus/core/docs/content/docs-components.mdx +491 -0
  8. package/corpus/core/docs/content/doctor.mdx +8 -1
  9. package/corpus/core/docs/content/drop-in-agent.mdx +1 -1
  10. package/corpus/core/docs/content/extensions.mdx +18 -18
  11. package/corpus/core/docs/content/faq.mdx +4 -4
  12. package/corpus/core/docs/content/key-concepts.mdx +30 -21
  13. package/corpus/core/docs/content/template-brain-developers.mdx +4 -1
  14. package/corpus/core/docs/content/template-mail-developers.mdx +4 -1
  15. package/corpus/core/docs/content/what-is-agent-native.mdx +6 -6
  16. package/corpus/core/package.json +1 -1
  17. package/corpus/core/src/a2a/client.ts +449 -66
  18. package/corpus/core/src/a2a/correlation.ts +31 -0
  19. package/corpus/core/src/a2a/types.ts +4 -0
  20. package/corpus/core/src/action.ts +3 -0
  21. package/corpus/core/src/agent/engine/registry.ts +45 -5
  22. package/corpus/core/src/agent/production-agent.ts +151 -18
  23. package/corpus/core/src/agent/run-loop-with-resume.ts +190 -3
  24. package/corpus/core/src/agent/run-manager.ts +117 -41
  25. package/corpus/core/src/agent/thread-debug-history.ts +86 -0
  26. package/corpus/core/src/agent/types.ts +3 -1
  27. package/corpus/core/src/cli/create.ts +11 -1
  28. package/corpus/core/src/cli/templates-meta.ts +2 -2
  29. package/corpus/core/src/client/agent-chat-adapter.ts +91 -13
  30. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +11 -9
  31. package/corpus/core/src/client/sse-event-processor.ts +17 -3
  32. package/corpus/core/src/db/client.ts +57 -17
  33. package/corpus/core/src/integrations/adapters/slack.ts +8 -3
  34. package/corpus/core/src/localization/default-messages.ts +44 -0
  35. package/corpus/core/src/observability/traces.ts +78 -5
  36. package/corpus/core/src/scripts/call-agent.ts +290 -9
  37. package/corpus/core/src/scripts/describe-workspace-apps.ts +2 -2
  38. package/corpus/core/src/secrets/crypto.ts +126 -34
  39. package/corpus/core/src/secrets/storage.ts +61 -25
  40. package/corpus/core/src/server/agent-capabilities.ts +1 -1
  41. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +111 -7
  42. package/corpus/core/src/server/agent-chat/context-tools.ts +2 -0
  43. package/corpus/core/src/server/agent-chat/plugin-options.ts +34 -1
  44. package/corpus/core/src/server/agent-chat/prompt-resources.ts +1 -1
  45. package/corpus/core/src/server/agent-chat-plugin.ts +91 -59
  46. package/corpus/core/src/templates/default/.agents/skills/secrets/SKILL.md +14 -7
  47. package/corpus/core/src/templates/headless/.agents/skills/secrets/SKILL.md +14 -7
  48. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +18 -7
  49. package/corpus/core/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +15 -14
  50. package/corpus/core/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -7
  51. package/corpus/core/src/vite/client.ts +1 -0
  52. package/corpus/templates/analytics/.agents/skills/dashboard-ops/SKILL.md +5 -4
  53. package/corpus/templates/analytics/.agents/skills/gong/SKILL.md +6 -4
  54. package/corpus/templates/analytics/AGENTS.md +6 -4
  55. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +4 -3
  56. package/corpus/templates/analytics/changelog/2026-07-30-scheduled-dashboard-emails-complete-every-panel-without-pool.md +6 -0
  57. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +28 -1
  58. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +18 -11
  59. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +4 -4
  60. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -5
  61. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +6 -2
  62. package/corpus/templates/analytics/server/lib/dashboard-panel-source-resolver.ts +11 -4
  63. package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +49 -31
  64. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +16 -8
  65. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +9 -3
  66. package/corpus/templates/analytics/server/lib/production-serverless-runtime.ts +11 -0
  67. package/corpus/templates/analytics/server/plugins/agent-chat.ts +3 -2
  68. package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +2 -1
  69. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +2 -1
  70. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +1 -12
  71. package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +54 -6
  72. package/corpus/templates/brain/.agents/skills/brain/SKILL.md +20 -9
  73. package/corpus/templates/brain/.agents/skills/ingestion-and-connectors/SKILL.md +33 -1
  74. package/corpus/templates/brain/AGENTS.md +3 -1
  75. package/corpus/templates/brain/README.md +4 -1
  76. package/corpus/templates/brain/actions/_schemas.ts +30 -8
  77. package/corpus/templates/brain/actions/ask-brain.ts +124 -14
  78. package/corpus/templates/brain/actions/create-source.ts +18 -2
  79. package/corpus/templates/brain/actions/enqueue-captures-distillation.ts +8 -123
  80. package/corpus/templates/brain/actions/enqueue-distillation.ts +6 -127
  81. package/corpus/templates/brain/actions/import-capture.ts +17 -5
  82. package/corpus/templates/brain/actions/import-transcript.ts +17 -5
  83. package/corpus/templates/brain/actions/update-source.ts +25 -5
  84. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +1 -1
  85. package/corpus/templates/brain/changelog/2026-07-30-blessed-resources-can-feed-cited-company-answers.md +6 -0
  86. package/corpus/templates/brain/changelog/2026-07-30-the-left-sidebar-no-longer-shows-a-blank-organization-placeh.md +6 -0
  87. package/corpus/templates/brain/server/lib/brain.ts +49 -19
  88. package/corpus/templates/brain/server/lib/distillation-queue.ts +147 -0
  89. package/corpus/templates/brain/server/lib/source-policy.ts +264 -0
  90. package/corpus/templates/brain/server/plugins/agent-chat.ts +3 -2
  91. package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +116 -19
  92. package/corpus/templates/clips/app/components/meetings/google-oauth-identity-notice.tsx +51 -0
  93. package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
  94. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +3 -1
  95. package/corpus/templates/clips/changelog/2026-07-30-google-calendar-warning.md +6 -0
  96. package/corpus/templates/content/.agents/skills/content-product-development/SKILL.md +47 -0
  97. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +72 -8
  98. package/corpus/templates/content/changelog/2026-07-30-the-slash-command-hint-now-stays-on-only-the-active-empty-li.md +6 -0
  99. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +506 -11
  100. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +148 -17
  101. package/corpus/templates/design/app/components/design/canvas-interactions/design-canvas-interactions.ts +357 -0
  102. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +7 -466
  103. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +15 -311
  104. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +19 -997
  105. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +8 -1216
  106. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +31 -38
  107. package/corpus/templates/design/package.json +0 -1
  108. package/corpus/templates/dispatch/AGENTS.md +8 -0
  109. package/corpus/templates/dispatch/DEVELOPING.md +29 -0
  110. package/corpus/templates/dispatch/actions/view-screen.ts +34 -2
  111. package/corpus/templates/dispatch/app/i18n-data.ts +457 -0
  112. package/corpus/templates/dispatch/changelog/2026-07-30-delegated-dispatch-work-continues-reliably-in-the-background.md +6 -0
  113. package/corpus/templates/dispatch/changelog/2026-07-30-find-failed-runs-across-apps.md +6 -0
  114. package/corpus/templates/dispatch/netlify.toml +3 -0
  115. package/corpus/templates/slides/.agents/skills/analytics-data-for-decks/SKILL.md +8 -12
  116. package/corpus/templates/slides/AGENTS.md +4 -4
  117. package/corpus/templates/slides/actions/duplicate-deck.ts +19 -4
  118. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -10
  119. package/corpus/templates/slides/app/components/deck/MermaidRenderer.tsx +12 -1
  120. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +30 -13
  121. package/corpus/templates/slides/app/components/editor/DeckEditorSkeleton.tsx +35 -0
  122. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +63 -56
  123. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +21 -76
  124. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1110 -292
  125. package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +53 -0
  126. package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +235 -18
  127. package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +4 -1
  128. package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +11 -0
  129. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +129 -0
  130. package/corpus/templates/slides/app/components/editor/selection-overlay-measurement.ts +70 -0
  131. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +519 -65
  132. package/corpus/templates/slides/app/components/visual-editor/CanvasCommentPins.tsx +36 -426
  133. package/corpus/templates/slides/app/components/visual-editor/DrawOverlay.tsx +19 -458
  134. package/corpus/templates/slides/app/context/DeckContext.tsx +179 -72
  135. package/corpus/templates/slides/app/global.css +105 -0
  136. package/corpus/templates/slides/app/i18n/en-US.ts +8 -0
  137. package/corpus/templates/slides/app/lib/deck-editor-loading.ts +26 -0
  138. package/corpus/templates/slides/app/lib/mermaid-blocks.ts +27 -0
  139. package/corpus/templates/slides/app/pages/DeckEditor.tsx +71 -38
  140. package/corpus/templates/slides/app/pages/Index.tsx +98 -41
  141. package/corpus/templates/slides/changelog/2026-07-30-deck-loading-no-longer-flashes-an-unavailable-message.md +6 -0
  142. package/corpus/templates/slides/changelog/2026-07-30-duplicating-a-deck-from-the-deck-list-now-opens-the-copy-imm.md +6 -0
  143. package/corpus/templates/slides/changelog/2026-07-30-layout-overflow-warning-is-readable-and-dismissible.md +6 -0
  144. package/corpus/templates/slides/changelog/2026-07-30-shared-canvas-annotations.md +6 -0
  145. package/corpus/templates/slides/changelog/2026-07-30-slide-objects-can-be-moved-as-a-group-copied-and-pasted-with.md +6 -0
  146. package/corpus/templates/slides/changelog/2026-07-30-slide-text-editing-and-object-controls-now-match-google-slides.md +6 -0
  147. package/corpus/templates/slides/changelog/2026-07-30-style-panel-now-scrolls-with-speaker-notes-open-shows-the-sl.md +6 -0
  148. package/corpus/templates/slides/changelog/2026-07-30-top-toolbar-controls-now-use-consistent-sizing-with-undo-and.md +6 -0
  149. package/corpus/templates/slides/netlify.toml +3 -1
  150. package/corpus/templates/slides/package.json +0 -1
  151. package/corpus/templates/slides/server/lib/chat-attachments.ts +18 -0
  152. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -0
  153. package/corpus/templates/slides/vite.config.ts +0 -1
  154. package/corpus/toolkit/CHANGELOG.md +13 -0
  155. package/corpus/toolkit/README.md +19 -0
  156. package/corpus/toolkit/agent-native.eject.json +38 -0
  157. package/corpus/toolkit/package.json +29 -1
  158. package/corpus/toolkit/src/canvas-annotations/CanvasCommentPins.tsx +861 -0
  159. package/corpus/toolkit/src/canvas-annotations/DrawOverlay.tsx +1233 -0
  160. package/corpus/toolkit/src/canvas-annotations/index.ts +15 -0
  161. package/corpus/toolkit/src/canvas-annotations/types.ts +14 -0
  162. package/corpus/toolkit/src/canvas-interactions/canvas-interactions.ts +933 -0
  163. package/corpus/toolkit/src/canvas-interactions/index.ts +67 -0
  164. package/corpus/toolkit/src/design-tweaks/scrub-input-utils.ts +311 -0
  165. package/corpus/toolkit/src/design-tweaks/scrub-input.tsx +491 -0
  166. package/corpus/toolkit/src/design-tweaks/visual-style-controls.tsx +26 -171
  167. package/corpus/toolkit/src/index.ts +1 -0
  168. package/dist/a2a/client.d.ts +23 -0
  169. package/dist/a2a/client.d.ts.map +1 -1
  170. package/dist/a2a/client.js +309 -44
  171. package/dist/a2a/client.js.map +1 -1
  172. package/dist/a2a/correlation.d.ts +1 -0
  173. package/dist/a2a/correlation.d.ts.map +1 -1
  174. package/dist/a2a/correlation.js +27 -0
  175. package/dist/a2a/correlation.js.map +1 -1
  176. package/dist/a2a/types.d.ts +4 -0
  177. package/dist/a2a/types.d.ts.map +1 -1
  178. package/dist/a2a/types.js.map +1 -1
  179. package/dist/action.d.ts +3 -0
  180. package/dist/action.d.ts.map +1 -1
  181. package/dist/action.js.map +1 -1
  182. package/dist/agent/engine/registry.d.ts.map +1 -1
  183. package/dist/agent/engine/registry.js +34 -7
  184. package/dist/agent/engine/registry.js.map +1 -1
  185. package/dist/agent/production-agent.d.ts +40 -1
  186. package/dist/agent/production-agent.d.ts.map +1 -1
  187. package/dist/agent/production-agent.js +113 -15
  188. package/dist/agent/production-agent.js.map +1 -1
  189. package/dist/agent/run-loop-with-resume.d.ts +21 -0
  190. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  191. package/dist/agent/run-loop-with-resume.js +163 -4
  192. package/dist/agent/run-loop-with-resume.js.map +1 -1
  193. package/dist/agent/run-manager.d.ts +16 -9
  194. package/dist/agent/run-manager.d.ts.map +1 -1
  195. package/dist/agent/run-manager.js +93 -42
  196. package/dist/agent/run-manager.js.map +1 -1
  197. package/dist/agent/thread-debug-history.d.ts +10 -0
  198. package/dist/agent/thread-debug-history.d.ts.map +1 -0
  199. package/dist/agent/thread-debug-history.js +68 -0
  200. package/dist/agent/thread-debug-history.js.map +1 -0
  201. package/dist/agent/types.d.ts +3 -1
  202. package/dist/agent/types.d.ts.map +1 -1
  203. package/dist/agent/types.js.map +1 -1
  204. package/dist/cli/create.d.ts.map +1 -1
  205. package/dist/cli/create.js +6 -1
  206. package/dist/cli/create.js.map +1 -1
  207. package/dist/cli/templates-meta.js +2 -2
  208. package/dist/cli/templates-meta.js.map +1 -1
  209. package/dist/client/agent-chat-adapter.d.ts +1 -1
  210. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  211. package/dist/client/agent-chat-adapter.js +76 -13
  212. package/dist/client/agent-chat-adapter.js.map +1 -1
  213. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  214. package/dist/client/resources/McpIntegrationDialog.js +1 -1
  215. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  216. package/dist/client/sse-event-processor.d.ts.map +1 -1
  217. package/dist/client/sse-event-processor.js +12 -3
  218. package/dist/client/sse-event-processor.js.map +1 -1
  219. package/dist/collab/awareness.d.ts +2 -2
  220. package/dist/collab/awareness.d.ts.map +1 -1
  221. package/dist/db/client.d.ts.map +1 -1
  222. package/dist/db/client.js +48 -17
  223. package/dist/db/client.js.map +1 -1
  224. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  225. package/dist/integrations/adapters/slack.js +7 -3
  226. package/dist/integrations/adapters/slack.js.map +1 -1
  227. package/dist/localization/default-messages.d.ts +43 -0
  228. package/dist/localization/default-messages.d.ts.map +1 -1
  229. package/dist/localization/default-messages.js +43 -0
  230. package/dist/localization/default-messages.js.map +1 -1
  231. package/dist/notifications/routes.d.ts +2 -2
  232. package/dist/observability/traces.d.ts +3 -1
  233. package/dist/observability/traces.d.ts.map +1 -1
  234. package/dist/observability/traces.js +73 -4
  235. package/dist/observability/traces.js.map +1 -1
  236. package/dist/progress/routes.d.ts +1 -1
  237. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  238. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  239. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  240. package/dist/resources/handlers.d.ts +1 -1
  241. package/dist/scripts/call-agent.d.ts.map +1 -1
  242. package/dist/scripts/call-agent.js +233 -10
  243. package/dist/scripts/call-agent.js.map +1 -1
  244. package/dist/scripts/describe-workspace-apps.js +2 -2
  245. package/dist/scripts/describe-workspace-apps.js.map +1 -1
  246. package/dist/secrets/crypto.d.ts +31 -21
  247. package/dist/secrets/crypto.d.ts.map +1 -1
  248. package/dist/secrets/crypto.js +108 -33
  249. package/dist/secrets/crypto.js.map +1 -1
  250. package/dist/secrets/storage.d.ts +3 -5
  251. package/dist/secrets/storage.d.ts.map +1 -1
  252. package/dist/secrets/storage.js +40 -25
  253. package/dist/secrets/storage.js.map +1 -1
  254. package/dist/server/agent-capabilities.js +1 -1
  255. package/dist/server/agent-capabilities.js.map +1 -1
  256. package/dist/server/agent-chat/action-filters-a2a.d.ts +17 -3
  257. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  258. package/dist/server/agent-chat/action-filters-a2a.js +92 -3
  259. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  260. package/dist/server/agent-chat/context-tools.d.ts.map +1 -1
  261. package/dist/server/agent-chat/context-tools.js +2 -0
  262. package/dist/server/agent-chat/context-tools.js.map +1 -1
  263. package/dist/server/agent-chat/plugin-options.d.ts +23 -0
  264. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  265. package/dist/server/agent-chat/plugin-options.js +4 -1
  266. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  267. package/dist/server/agent-chat/prompt-resources.js +1 -1
  268. package/dist/server/agent-chat/prompt-resources.js.map +1 -1
  269. package/dist/server/agent-chat-plugin.d.ts +2 -1
  270. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  271. package/dist/server/agent-chat-plugin.js +79 -56
  272. package/dist/server/agent-chat-plugin.js.map +1 -1
  273. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  274. package/dist/templates/default/.agents/skills/secrets/SKILL.md +14 -7
  275. package/dist/templates/headless/.agents/skills/secrets/SKILL.md +14 -7
  276. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +18 -7
  277. package/dist/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +15 -14
  278. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -7
  279. package/dist/vite/client.d.ts.map +1 -1
  280. package/dist/vite/client.js +1 -0
  281. package/dist/vite/client.js.map +1 -1
  282. package/docs/content/agent-native-toolkit.mdx +13 -6
  283. package/docs/content/cloneable-saas.mdx +5 -5
  284. package/docs/content/creating-templates.mdx +9 -0
  285. package/docs/content/deployment.mdx +14 -10
  286. package/docs/content/docs-components.mdx +491 -0
  287. package/docs/content/doctor.mdx +8 -1
  288. package/docs/content/drop-in-agent.mdx +1 -1
  289. package/docs/content/extensions.mdx +18 -18
  290. package/docs/content/faq.mdx +4 -4
  291. package/docs/content/key-concepts.mdx +30 -21
  292. package/docs/content/template-brain-developers.mdx +4 -1
  293. package/docs/content/template-mail-developers.mdx +4 -1
  294. package/docs/content/what-is-agent-native.mdx +6 -6
  295. package/package.json +2 -2
  296. package/src/a2a/client.ts +449 -66
  297. package/src/a2a/correlation.ts +31 -0
  298. package/src/a2a/types.ts +4 -0
  299. package/src/action.ts +3 -0
  300. package/src/agent/engine/registry.ts +45 -5
  301. package/src/agent/production-agent.ts +151 -18
  302. package/src/agent/run-loop-with-resume.ts +190 -3
  303. package/src/agent/run-manager.ts +117 -41
  304. package/src/agent/thread-debug-history.ts +86 -0
  305. package/src/agent/types.ts +3 -1
  306. package/src/cli/create.ts +11 -1
  307. package/src/cli/templates-meta.ts +2 -2
  308. package/src/client/agent-chat-adapter.ts +91 -13
  309. package/src/client/resources/McpIntegrationDialog.tsx +11 -9
  310. package/src/client/sse-event-processor.ts +17 -3
  311. package/src/db/client.ts +57 -17
  312. package/src/integrations/adapters/slack.ts +8 -3
  313. package/src/localization/default-messages.ts +44 -0
  314. package/src/observability/traces.ts +78 -5
  315. package/src/scripts/call-agent.ts +290 -9
  316. package/src/scripts/describe-workspace-apps.ts +2 -2
  317. package/src/secrets/crypto.ts +126 -34
  318. package/src/secrets/storage.ts +61 -25
  319. package/src/server/agent-capabilities.ts +1 -1
  320. package/src/server/agent-chat/action-filters-a2a.ts +111 -7
  321. package/src/server/agent-chat/context-tools.ts +2 -0
  322. package/src/server/agent-chat/plugin-options.ts +34 -1
  323. package/src/server/agent-chat/prompt-resources.ts +1 -1
  324. package/src/server/agent-chat-plugin.ts +91 -59
  325. package/src/templates/default/.agents/skills/secrets/SKILL.md +14 -7
  326. package/src/templates/headless/.agents/skills/secrets/SKILL.md +14 -7
  327. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +18 -7
  328. package/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +15 -14
  329. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -7
  330. package/src/vite/client.ts +1 -0
@@ -1,1009 +1,31 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
2
  import {
3
- IconAlertTriangle,
4
- IconBolt,
5
- IconCircleCheck,
6
- IconMessage,
7
- IconMessageCheck,
8
- IconSend,
9
- IconX,
10
- } from "@tabler/icons-react";
11
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ CanvasCommentPins as ToolkitCanvasCommentPins,
4
+ type CanvasCommentPinsProps as ToolkitCanvasCommentPinsProps,
5
+ } from "@agent-native/toolkit/canvas-annotations";
12
6
  import { toast } from "sonner";
13
7
 
14
- import { Button } from "@/components/ui/button";
15
- import { Textarea } from "@/components/ui/textarea";
16
- import {
17
- Tooltip,
18
- TooltipContent,
19
- TooltipTrigger,
20
- } from "@/components/ui/tooltip";
21
- import { sendToDesignAgentChat } from "@/lib/agent-chat";
22
- import { cn } from "@/lib/utils";
23
-
24
- export interface CanvasPin {
25
- id: string;
26
- /** Position as a percentage of the canvas (so it survives resize/zoom) */
27
- xPct: number;
28
- yPct: number;
29
- /** Optional CSS selector of the element under the click, for context */
30
- targetSelector?: string;
31
- /** Best-effort stable canvas/code-layer id when the clicked layer exposes one. */
32
- targetAnchorId?: string;
33
- /** Optional snippet of text content the user clicked near */
34
- targetText?: string;
35
- /** Pending comment text the user is composing */
36
- draft?: string;
37
- /** Held locally until the user batch-applies queued comments. */
38
- queued?: boolean;
39
- /** Submitted state — the marker stays visible as confirmation. */
40
- submitted?: boolean;
41
- }
42
-
43
- interface CanvasCommentPinsProps {
44
- /** Whether the pin tool is active. When true, clicks drop pins. */
45
- active: boolean;
46
- /** In queue mode, pin Send adds to the shared annotation batch. */
47
- submitMode?: "direct" | "queue";
48
- /** Disable / exit the pin mode (called on Escape, after submit, etc.) */
49
- onClose: () => void;
50
- /** Mirrors local pins to a parent that can submit them with other annotations. */
51
- onPinsChange?: (pins: CanvasPin[]) => void;
52
- /** Increment to mark queued pins as submitted by a parent action. */
53
- submitQueuedSignal?: number;
54
- /** Keep the click plane below a sibling draw toolbar in combined annotate mode. */
55
- clickPlaneUnderToolbar?: boolean;
56
- /**
57
- * Selector for the canvas surface (e.g. `.slide-content`). Pins are anchored
58
- * to this element's bounding rect; clicks outside are ignored.
59
- */
60
- canvasSelector: string;
61
- /** Stable identifier for the current view (slide id / design id) — used in
62
- * the agent prompt and as a pin namespace key. */
63
- contextId: string;
64
- /** Human-readable label for the context (slide title, slide index, design
65
- * title) used in the agent prompt. */
66
- contextLabel?: string;
67
- }
68
-
69
- type PinCapabilityStatus =
70
- | "deterministic"
71
- | "needs-agent"
72
- | "conflict"
73
- | "unsupported";
74
-
75
- interface PinStatusMeta {
76
- status: PinCapabilityStatus;
77
- label: string;
78
- shortLabel: string;
79
- detail: string;
80
- markerClassName: string;
81
- badgeClassName: string;
82
- }
83
-
84
- const STATUS_META: Record<PinCapabilityStatus, PinStatusMeta> = {
85
- deterministic: {
86
- status: "deterministic",
87
- label: "",
88
- shortLabel: "",
89
- detail: "",
90
- markerClassName: "bg-emerald-500 text-white ring-emerald-200",
91
- badgeClassName:
92
- "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
93
- },
94
- "needs-agent": {
95
- status: "needs-agent",
96
- label: "",
97
- shortLabel: "",
98
- detail: "",
99
- markerClassName: "bg-amber-400 text-amber-950 ring-amber-200",
100
- badgeClassName:
101
- "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
102
- },
103
- conflict: {
104
- status: "conflict",
105
- label: "",
106
- shortLabel: "",
107
- detail: "",
108
- markerClassName:
109
- "bg-destructive text-destructive-foreground ring-destructive/30",
110
- badgeClassName: "border-destructive/30 bg-destructive/10 text-destructive",
111
- },
112
- unsupported: {
113
- status: "unsupported",
114
- label: "",
115
- shortLabel: "",
116
- detail: "",
117
- markerClassName: "bg-muted text-muted-foreground ring-border",
118
- badgeClassName: "border-border bg-muted text-muted-foreground",
119
- },
120
- };
121
-
122
- function localizePinStatus(
123
- status: PinStatusMeta,
124
- t: (key: string, options?: Record<string, unknown>) => string,
125
- ): PinStatusMeta {
126
- return {
127
- ...status,
128
- label: t(`visualEditor.pinStatus.${status.status}.label`),
129
- shortLabel: t(`visualEditor.pinStatus.${status.status}.shortLabel`),
130
- detail: t(`visualEditor.pinStatus.${status.status}.detail`),
131
- };
132
- }
8
+ import { sendToDesignAgentChatAndConfirm } from "@/lib/agent-chat";
133
9
 
134
- function escapeAttributeValue(value: string): string {
135
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
136
- }
137
-
138
- function escapeCssIdentifier(value: string): string {
139
- if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
140
- return CSS.escape(value);
141
- }
142
- return value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
143
- }
10
+ export type { CanvasPin } from "@agent-native/toolkit/canvas-annotations";
144
11
 
145
- /**
146
- * Best-effort check for whether a pin's captured anchor element still exists
147
- * on the canvas. Only meaningful for parent-DOM canvases with a captured
148
- * `targetSelector` — iframe canvases never capture one (see `dropPinAt`), so
149
- * this naturally no-ops there rather than mislabeling every iframe pin as
150
- * stale. Recomputed on each render (already triggered by scroll/resize/pins
151
- * changes); this deliberately avoids adding a dedicated poll so idle canvas
152
- * perf is unaffected.
153
- */
154
- function pinAnchorStillPresent(canvas: HTMLElement, pin: CanvasPin): boolean {
155
- if (!pin.targetSelector) return true;
156
- try {
157
- return canvas.querySelector(pin.targetSelector) !== null;
158
- } catch {
159
- // A selector that no longer parses shouldn't be reported as "stale" —
160
- // that's a different failure mode than "the element was removed".
161
- return true;
162
- }
163
- }
164
-
165
- function getTargetAnchor(target?: HTMLElement | null): {
166
- targetSelector?: string;
167
- targetAnchorId?: string;
168
- } {
169
- const anchor = target?.closest(
170
- "[data-agent-native-node-id], [data-builder-id], [data-loc], [id]",
171
- );
172
- if (!(anchor instanceof HTMLElement)) return {};
12
+ export type CanvasCommentPinsProps = Omit<
13
+ ToolkitCanvasCommentPinsProps,
14
+ "translate" | "sendToAgent" | "onSendError"
15
+ >;
173
16
 
174
- for (const attribute of [
175
- "data-agent-native-node-id",
176
- "data-builder-id",
177
- "data-loc",
178
- ]) {
179
- const value = anchor.getAttribute(attribute);
180
- if (value) {
181
- return {
182
- targetSelector: `[${attribute}="${escapeAttributeValue(value)}"]`,
183
- targetAnchorId: value,
184
- };
185
- }
186
- }
187
-
188
- if (anchor.id) {
189
- return {
190
- targetSelector: `#${escapeCssIdentifier(anchor.id)}`,
191
- targetAnchorId: anchor.id,
192
- };
193
- }
194
-
195
- return {};
196
- }
197
-
198
- function wordsIncludePair(
199
- text: string,
200
- first: string,
201
- second: string,
202
- ): boolean {
17
+ export function CanvasCommentPins(props: CanvasCommentPinsProps) {
18
+ const translate = useT();
203
19
  return (
204
- new RegExp(`\\b${first}\\b`, "i").test(text) &&
205
- new RegExp(`\\b${second}\\b`, "i").test(text)
206
- );
207
- }
208
-
209
- function hasContradictoryLanguage(text: string): boolean {
210
- return [
211
- ["bigger", "smaller"],
212
- ["larger", "smaller"],
213
- ["hide", "show"],
214
- ["remove", "keep"],
215
- ["delete", "keep"],
216
- ["left", "right"],
217
- ["top", "bottom"],
218
- ["dark", "light"],
219
- ].some(([first, second]) => wordsIncludePair(text, first, second));
220
- }
221
-
222
- function pinsLikelyOverlap(pin: CanvasPin, other: CanvasPin): boolean {
223
- if (pin.id === other.id || other.submitted) return false;
224
- if (pin.targetSelector && pin.targetSelector === other.targetSelector)
225
- return true;
226
- const xDelta = Math.abs(pin.xPct - other.xPct);
227
- const yDelta = Math.abs(pin.yPct - other.yPct);
228
- return xDelta <= 4 && yDelta <= 4;
229
- }
230
-
231
- /**
232
- * Purely visual "same spot" check used to spread overlapping markers apart on
233
- * screen. Unlike `pinsLikelyOverlap` (which only flags *live* conflicts and
234
- * ignores already-submitted pins), this also considers submitted pins so a
235
- * new pin dropped on top of an earlier confirmation marker doesn't render
236
- * fully hidden/unclickable underneath it.
237
- */
238
- function pinsAtSameSpot(pin: CanvasPin, other: CanvasPin): boolean {
239
- if (pin.id === other.id) return false;
240
- const xDelta = Math.abs(pin.xPct - other.xPct);
241
- const yDelta = Math.abs(pin.yPct - other.yPct);
242
- return xDelta <= 3 && yDelta <= 3;
243
- }
244
-
245
- /**
246
- * Deterministic pixel nudge for a pin whose marker would otherwise render
247
- * stacked exactly on top of an earlier pin at (approximately) the same
248
- * canvas position. Pins are placed in a small golden-angle spiral around
249
- * their true position so every marker in a cluster stays visible and
250
- * individually clickable; the stored `xPct`/`yPct` (and the position sent to
251
- * the agent) are unaffected.
252
- */
253
- function pinClusterOffset(
254
- pin: CanvasPin,
255
- indexInPins: number,
256
- pins: CanvasPin[],
257
- ): { dx: number; dy: number } {
258
- let clusterIndex = 0;
259
- for (let i = 0; i < indexInPins; i++) {
260
- if (pinsAtSameSpot(pin, pins[i]!)) clusterIndex += 1;
261
- }
262
- if (clusterIndex === 0) return { dx: 0, dy: 0 };
263
- const angle = clusterIndex * 2.4; // golden angle (radians) for even spread
264
- const radius = 9 + clusterIndex * 5;
265
- return {
266
- dx: Math.round(Math.cos(angle) * radius),
267
- dy: Math.round(Math.sin(angle) * radius),
268
- };
269
- }
270
-
271
- function derivePinStatus(pin: CanvasPin, pins: CanvasPin[]): PinStatusMeta {
272
- const text = (pin.draft || "").toLowerCase();
273
- const anchored = Boolean(pin.targetSelector || pin.targetAnchorId);
274
- const unsupported =
275
- /\b(api|backend|database|server|auth|login|oauth|stripe|payment|email|webhook|permission|role|roles|deploy|domain|routing|route|video|audio|3d|three\.js|pdf|spreadsheet)\b/i.test(
276
- text,
277
- );
278
- if (unsupported) return STATUS_META.unsupported;
279
-
280
- const overlaps = pins.some(
281
- (other) =>
282
- pinsLikelyOverlap(pin, other) &&
283
- (pin.draft || "").trim() &&
284
- (other.draft || "").trim() &&
285
- (pin.draft || "").trim() !== (other.draft || "").trim(),
286
- );
287
- if (overlaps || hasContradictoryLanguage(text)) {
288
- return STATUS_META.conflict;
289
- }
290
-
291
- const directEdit =
292
- /\b(color|font|copy|text|label|spacing|gap|margin|padding|radius|rounded|border|align|center|move|resize|width|height|background|image|button|headline|section|swap|replace)\b/i.test(
293
- text,
294
- );
295
- const vagueInstruction =
296
- /\b(polish|improve|better|nice|clean up|fix it|make it work|decide|surprise me)\b/i.test(
297
- text,
298
- );
299
-
300
- if (anchored && directEdit && !vagueInstruction) {
301
- return STATUS_META.deterministic;
302
- }
303
-
304
- return STATUS_META["needs-agent"];
305
- }
306
-
307
- function PinStatusBadge({ status }: { status: PinStatusMeta }) {
308
- const Icon =
309
- status.status === "deterministic"
310
- ? IconCircleCheck
311
- : status.status === "unsupported" || status.status === "conflict"
312
- ? IconAlertTriangle
313
- : IconBolt;
314
-
315
- return (
316
- <Tooltip>
317
- <TooltipTrigger asChild>
318
- <span
319
- className={cn(
320
- "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium leading-none",
321
- status.badgeClassName,
322
- )}
323
- >
324
- <Icon className="size-3" />
325
- {status.label}
326
- </span>
327
- </TooltipTrigger>
328
- <TooltipContent>{status.detail}</TooltipContent>
329
- </Tooltip>
330
- );
331
- }
332
-
333
- /**
334
- * Click-to-comment pins anchored to the canvas.
335
- *
336
- * Mirrors claude.ai/design's "inline comments" feature — the most-praised
337
- * interaction pattern of that tool. A user clicks anywhere on the canvas to
338
- * drop a pin, types a one-line instruction, and the pin's position + nearby
339
- * element selector + instruction is sent to the agent. Submitted pins stay on
340
- * the canvas as local confirmation; the agent's reply lands in the chat
341
- * sidebar where it can make targeted edits.
342
- *
343
- * Why pins (vs text-anchored comments):
344
- * The existing slide_comments table anchors comments to text selections via
345
- * TipTap. That's good for prose review — but Rochkind also wants to point
346
- * to images, charts, and whitespace. Pins handle those cases without
347
- * requiring a text selection.
348
- */
349
- export function CanvasCommentPins({
350
- active,
351
- submitMode = "direct",
352
- onClose,
353
- onPinsChange,
354
- submitQueuedSignal,
355
- clickPlaneUnderToolbar = false,
356
- canvasSelector,
357
- contextId,
358
- contextLabel,
359
- }: CanvasCommentPinsProps) {
360
- const t = useT();
361
- const [pins, setPins] = useState<CanvasPin[]>([]);
362
- // Mirrors `pins` so the context-change reset effect below can inspect what
363
- // was on the canvas the instant `contextId` changes, without re-running on
364
- // every pin edit (it should only fire once, on the context switch itself).
365
- const pinsRef = useRef<CanvasPin[]>(pins);
366
- pinsRef.current = pins;
367
- const [activePinId, setActivePinId] = useState<string | null>(null);
368
- const containerRef = useRef<HTMLElement | null>(null);
369
- const lastSubmitQueuedSignalRef = useRef(submitQueuedSignal);
370
- const [canvasEl, setCanvasEl] = useState<HTMLElement | null>(null);
371
- // Dummy tick forces a re-render (and a fresh getBoundingClientRect()) whenever
372
- // the canvas scrolls or resizes, keeping pin overlays in sync.
373
- const [, setLayoutTick] = useState(0);
374
-
375
- // Reset pins when the context (slide) changes — they're scoped to one view.
376
- useEffect(() => {
377
- // Queued-but-unsent comment drafts don't survive a context switch (pins
378
- // are intentionally scoped to one view). Losing them is unavoidable here
379
- // since we don't control cross-view navigation, but it must never be
380
- // silent — warn instead of quietly wiping the user's typed drafts.
381
- const discardedDraftCount = pinsRef.current.filter(
382
- (pin) => pin.queued && !pin.submitted && (pin.draft || "").trim(),
383
- ).length;
384
- if (discardedDraftCount > 0) {
385
- toast(
386
- t("visualEditor.queuedCommentsDiscarded", {
387
- count: discardedDraftCount,
388
- }),
389
- );
390
- }
391
- setPins([]);
392
- setActivePinId(null);
393
- setCanvasEl(null);
394
- // eslint-disable-next-line react-hooks/exhaustive-deps -- only fire on an
395
- // actual context (slide/design) switch, not on every pins/t change.
396
- }, [contextId]);
397
-
398
- // Find the canvas container the pins overlay
399
- useEffect(() => {
400
- if (!active) return;
401
- const findCanvas = () => {
402
- const el = document.querySelector(canvasSelector) as HTMLElement | null;
403
- if (el) {
404
- containerRef.current = el;
405
- setCanvasEl(el);
20
+ <ToolkitCanvasCommentPins
21
+ {...props}
22
+ translate={translate}
23
+ sendToAgent={async (input) =>
24
+ (await sendToDesignAgentChatAndConfirm(input)).delivered
406
25
  }
407
- };
408
- findCanvas();
409
- const t = setTimeout(findCanvas, 50);
410
- return () => clearTimeout(t);
411
- }, [active, canvasSelector, contextId]);
412
-
413
- // Re-render when the canvas element moves in the viewport (scroll or resize),
414
- // so getBoundingClientRect() returns fresh coordinates for pin overlays.
415
- useEffect(() => {
416
- const canvas = containerRef.current ?? canvasEl;
417
- if (!canvas) return;
418
-
419
- const bump = () => setLayoutTick((t) => t + 1);
420
-
421
- // ResizeObserver fires when the canvas element's own size changes.
422
- const ro = new ResizeObserver(bump);
423
- ro.observe(canvas);
424
-
425
- // Scroll listeners on every scrollable ancestor and the window cover both
426
- // page-level scrolling and container-level panning. Walk the ancestor
427
- // chain with getComputedStyle instead of a class-name heuristic so
428
- // inline-styled, nested, and multiple scroll containers are all covered.
429
- const isScrollable = (value: string) =>
430
- value === "auto" || value === "scroll" || value === "overlay";
431
- const scrollTargets: Array<HTMLElement | Window> = [];
432
- for (let el: HTMLElement | null = canvas; el; el = el.parentElement) {
433
- const style = getComputedStyle(el);
434
- if (
435
- isScrollable(style.overflow) ||
436
- isScrollable(style.overflowX) ||
437
- isScrollable(style.overflowY)
438
- ) {
439
- scrollTargets.push(el);
440
- }
441
- }
442
- scrollTargets.push(window);
443
- for (const target of scrollTargets) {
444
- target.addEventListener("scroll", bump, { passive: true });
445
- }
446
- window.addEventListener("resize", bump, { passive: true });
447
-
448
- return () => {
449
- ro.disconnect();
450
- for (const target of scrollTargets) {
451
- target.removeEventListener("scroll", bump);
26
+ onSendError={() =>
27
+ toast.error(translate("visualEditor.annotationSendError"))
452
28
  }
453
- window.removeEventListener("resize", bump);
454
- };
455
- }, [canvasEl]);
456
-
457
- const dropPinAt = useCallback(
458
- (clientX: number, clientY: number, target?: HTMLElement | null) => {
459
- const canvas = containerRef.current;
460
- if (!canvas) return;
461
-
462
- const rect = canvas.getBoundingClientRect();
463
- const xPct = ((clientX - rect.left) / rect.width) * 100;
464
- const yPct = ((clientY - rect.top) / rect.height) * 100;
465
- if (xPct < 0 || xPct > 100 || yPct < 0 || yPct > 100) return; // i18n-ignore geometry bounds check, not UI copy
466
-
467
- // Build a best-effort selector for parent-DOM canvases. For iframe
468
- // canvases the transparent overlay captures the click, so target details
469
- // are intentionally omitted but the precise position is preserved.
470
- const { targetSelector, targetAnchorId } = getTargetAnchor(target);
471
- const targetText = target?.textContent?.trim().slice(0, 80) || undefined;
472
-
473
- const newPin: CanvasPin = {
474
- id: crypto.randomUUID(),
475
- xPct,
476
- yPct,
477
- targetSelector,
478
- targetAnchorId,
479
- targetText,
480
- draft: "",
481
- };
482
- setPins((prev) => [...prev, newPin]);
483
- setActivePinId(newPin.id);
484
- },
485
- [],
486
- );
487
-
488
- // Click handler — drops a pin where the user clicks a non-iframe canvas.
489
- // Iframe canvases cannot bubble clicks to the parent, so the rendered
490
- // transparent overlay below handles those reliably.
491
- useEffect(() => {
492
- if (!active) return;
493
- const onClick = (e: MouseEvent) => {
494
- const canvas = containerRef.current;
495
- if (!canvas) return;
496
- const target = e.target as HTMLElement;
497
-
498
- // Ignore clicks on UI chrome (the pin popovers themselves)
499
- if (target.closest("[data-pin-popover]")) return;
500
- if (target.closest("[data-pin-click-overlay]")) return;
501
- if (!canvas.contains(target)) return;
502
- dropPinAt(e.clientX, e.clientY, target);
503
- e.stopPropagation();
504
- e.preventDefault();
505
- };
506
- window.addEventListener("click", onClick, { capture: true });
507
- return () =>
508
- window.removeEventListener("click", onClick, { capture: true });
509
- }, [active, dropPinAt]);
510
-
511
- // Escape closes pin mode
512
- useEffect(() => {
513
- if (!active) return;
514
- const onKey = (e: KeyboardEvent) => {
515
- if (e.key === "Escape") {
516
- onClose();
517
- }
518
- };
519
- window.addEventListener("keydown", onKey);
520
- return () => window.removeEventListener("keydown", onKey);
521
- }, [active, onClose]);
522
-
523
- const updatePin = (id: string, updates: Partial<CanvasPin>) => {
524
- setPins((prev) =>
525
- prev.map((p) => (p.id === id ? { ...p, ...updates } : p)),
526
- );
527
- };
528
-
529
- const removePin = (id: string) => {
530
- setPins((prev) => prev.filter((p) => p.id !== id));
531
- if (activePinId === id) setActivePinId(null);
532
- };
533
-
534
- const queuedPins = useMemo(
535
- () =>
536
- pins.filter(
537
- (pin) => pin.queued && !pin.submitted && (pin.draft || "").trim(),
538
- ),
539
- [pins],
540
- );
541
-
542
- useEffect(() => {
543
- onPinsChange?.(pins);
544
- }, [onPinsChange, pins]);
545
-
546
- useEffect(() => {
547
- if (
548
- submitQueuedSignal === undefined ||
549
- submitQueuedSignal === lastSubmitQueuedSignalRef.current
550
- ) {
551
- return;
552
- }
553
- lastSubmitQueuedSignalRef.current = submitQueuedSignal;
554
- if (queuedPins.length === 0) return;
555
- const submittedIds = new Set(queuedPins.map((pin) => pin.id));
556
- setPins((prev) =>
557
- prev.map((pin) =>
558
- submittedIds.has(pin.id)
559
- ? { ...pin, queued: false, submitted: true }
560
- : pin,
561
- ),
562
- );
563
- setActivePinId(null);
564
- }, [queuedPins, submitQueuedSignal]);
565
-
566
- const submittedCount = pins.filter((pin) => pin.submitted).length;
567
- const statusCounts = useMemo(
568
- () =>
569
- pins.reduce<Record<PinCapabilityStatus, number>>(
570
- (counts, pin) => {
571
- const status = derivePinStatus(pin, pins).status;
572
- counts[status] += 1;
573
- return counts;
574
- },
575
- {
576
- deterministic: 0,
577
- "needs-agent": 0,
578
- conflict: 0,
579
- unsupported: 0,
580
- },
581
- ),
582
- [pins],
583
- );
584
-
585
- const buildPinLines = (pin: CanvasPin, index?: number) => {
586
- const status = localizePinStatus(derivePinStatus(pin, pins), t);
587
- const lines = [
588
- index === undefined
589
- ? `[Comment pin on ${contextLabel || contextId}]`
590
- : `[${index + 1}] Comment pin on ${contextLabel || contextId}`,
591
- `Position: ${pin.xPct.toFixed(1)}% from left, ${pin.yPct.toFixed(1)}% from top`,
592
- `Capability: ${status.label} - ${status.detail}`,
593
- ];
594
- if (pin.targetAnchorId) lines.push(`Anchor id: ${pin.targetAnchorId}`);
595
- if (pin.targetSelector) lines.push(`Element: ${pin.targetSelector}`);
596
- if (pin.targetText) lines.push(`Nearby text: "${pin.targetText}"`);
597
- lines.push("");
598
- lines.push((pin.draft || "").trim());
599
- return lines;
600
- };
601
-
602
- const sendPinsToAgent = (targetPins: CanvasPin[], batch = false) => {
603
- const message = batch
604
- ? [
605
- `[Comment batch on ${contextLabel || contextId}]`,
606
- `Annotations: ${targetPins.length}`,
607
- "",
608
- ...targetPins.flatMap((pin, index) => [
609
- ...buildPinLines(pin, index),
610
- "",
611
- ]),
612
- ].join("\n")
613
- : buildPinLines(targetPins[0]!).join("\n");
614
-
615
- try {
616
- // Use the client chat bridge (not the shared `agentChat.submit`) so the
617
- // request routes correctly when design is embedded in Builder/Frame
618
- // and so the agent sidebar is reliably opened via the `agent-panel:open`
619
- // custom event even if the user has it collapsed.
620
- sendToDesignAgentChat({
621
- message,
622
- submit: true,
623
- openSidebar: true,
624
- });
625
- } catch (err) {
626
- console.error("[CanvasCommentPins] failed to send to agent:", err);
627
- }
628
- };
629
-
630
- const queuePin = (pin: CanvasPin) => {
631
- const text = (pin.draft || "").trim();
632
- if (!text) {
633
- removePin(pin.id);
634
- return;
635
- }
636
- updatePin(pin.id, { draft: text, queued: true });
637
- setActivePinId(null);
638
- };
639
-
640
- const submitPin = (pin: CanvasPin) => {
641
- const text = (pin.draft || "").trim();
642
- if (!text) {
643
- removePin(pin.id);
644
- return;
645
- }
646
- if (submitMode === "queue") {
647
- queuePin(pin);
648
- return;
649
- }
650
- const nextPin = { ...pin, draft: text };
651
- sendPinsToAgent([nextPin]);
652
- updatePin(pin.id, { draft: text, queued: false, submitted: true });
653
- setActivePinId(null);
654
- };
655
-
656
- const submitQueuedPins = () => {
657
- if (queuedPins.length === 0) return;
658
- if (submitMode === "queue") return;
659
- sendPinsToAgent(queuedPins, true);
660
- const submittedIds = new Set(queuedPins.map((pin) => pin.id));
661
- setPins((prev) =>
662
- prev.map((pin) =>
663
- submittedIds.has(pin.id)
664
- ? { ...pin, queued: false, submitted: true }
665
- : pin,
666
- ),
667
- );
668
- setActivePinId(null);
669
- };
670
-
671
- // Refs for drag-detection on the click overlay (Bug 3 fix).
672
- // A pin is only dropped on a completed click, not on any drag gesture.
673
- const overlayDownPos = useRef<{ x: number; y: number } | null>(null);
674
- const overlayDidDrag = useRef(false);
675
-
676
- if (!active && pins.length === 0) return null;
677
-
678
- // Render pins as portaled overlays positioned on top of the canvas
679
- const canvas = containerRef.current ?? canvasEl;
680
- if (!canvas) return null;
681
- const rect = canvas.getBoundingClientRect();
682
- // Clamp summaryLeft so the w-64 (256px) panel, which is shifted left by
683
- // -translate-x-full, never overflows the left viewport edge.
684
- // 264 = 256 (panel width) + 8 (minimum margin from the viewport left edge).
685
- const summaryLeft = Math.max(
686
- 264,
687
- Math.min(rect.right - 8, window.innerWidth - 8),
688
- );
689
- const summaryTop = Math.max(rect.top + 8, 72);
690
-
691
- return (
692
- <>
693
- {/* Parent-side click plane. This is what makes iframe canvases commentable:
694
- clicks inside an iframe never bubble to the parent document. */}
695
- {active && (
696
- <div
697
- data-pin-click-overlay
698
- className={cn(
699
- "fixed cursor-crosshair",
700
- clickPlaneUnderToolbar ? "z-[20]" : "z-[54]",
701
- )}
702
- style={{
703
- left: rect.left,
704
- top: rect.top,
705
- width: rect.width,
706
- height: rect.height,
707
- }}
708
- onPointerDown={(e) => {
709
- overlayDownPos.current = { x: e.clientX, y: e.clientY };
710
- overlayDidDrag.current = false;
711
- e.stopPropagation();
712
- }}
713
- onPointerMove={(e) => {
714
- if (overlayDownPos.current) {
715
- const dx = e.clientX - overlayDownPos.current.x;
716
- const dy = e.clientY - overlayDownPos.current.y;
717
- if (dx * dx + dy * dy > 16) overlayDidDrag.current = true;
718
- }
719
- }}
720
- onClick={(e) => {
721
- e.preventDefault();
722
- e.stopPropagation();
723
- if (!overlayDidDrag.current) {
724
- dropPinAt(e.clientX, e.clientY);
725
- }
726
- overlayDownPos.current = null;
727
- overlayDidDrag.current = false;
728
- }}
729
- />
730
- )}
731
-
732
- {/* Cursor hint banner — only when pin mode is active */}
733
- {active && (
734
- <div
735
- data-pin-mode-banner
736
- className="fixed top-16 left-1/2 -translate-x-1/2 z-50 flex items-center gap-2 rounded-full border border-border bg-popover px-3 py-1.5 shadow-lg pointer-events-none"
737
- >
738
- <IconMessage className="w-3.5 h-3.5 text-[#609FF8]" />
739
- <span className="!text-[11px] text-foreground">
740
- {t("visualEditor.clickToDropCommentPin")}
741
- </span>
742
- <span className="text-[10px] text-muted-foreground ml-1">
743
- {t("visualEditor.escToExit")}
744
- </span>
745
- </div>
746
- )}
747
-
748
- {/* Compact queue/result summary for local annotation batching. */}
749
- {pins.length > 0 && (
750
- <div
751
- data-pin-popover
752
- className="fixed z-[56] w-64 -translate-x-full rounded-lg border border-border bg-popover p-3 shadow-xl"
753
- style={{ left: summaryLeft, top: summaryTop }}
754
- >
755
- <div className="mb-2 flex items-center justify-between gap-2">
756
- <div className="min-w-0">
757
- <p className="truncate text-xs font-semibold text-foreground">
758
- {t("visualEditor.annotationQueue")}
759
- </p>
760
- <p className="text-[10px] text-muted-foreground">
761
- {t("visualEditor.annotationQueueCounts", {
762
- queued: queuedPins.length,
763
- sent: submittedCount,
764
- })}
765
- </p>
766
- </div>
767
- {submitMode === "direct" ? (
768
- <Button
769
- size="sm"
770
- className="h-7 gap-1 px-2 text-[10px]"
771
- onClick={submitQueuedPins}
772
- disabled={queuedPins.length === 0}
773
- >
774
- <IconBolt className="size-3" />
775
- {t("visualEditor.batchApply")}
776
- </Button>
777
- ) : (
778
- <span className="rounded-md bg-muted px-2 py-1 text-[10px] font-medium text-muted-foreground">
779
- {t("visualEditor.queued")}
780
- </span>
781
- )}
782
- </div>
783
- <div className="grid grid-cols-2 gap-1 text-[10px]">
784
- <span className="rounded-md bg-muted/45 px-2 py-1 text-muted-foreground">
785
- {t("visualEditor.pinStatusCount.direct", {
786
- count: statusCounts.deterministic,
787
- })}
788
- </span>
789
- <span className="rounded-md bg-muted/45 px-2 py-1 text-muted-foreground">
790
- {t("visualEditor.pinStatusCount.agent", {
791
- count: statusCounts["needs-agent"],
792
- })}
793
- </span>
794
- <span className="rounded-md bg-muted/45 px-2 py-1 text-muted-foreground">
795
- {t("visualEditor.pinStatusCount.conflict", {
796
- count: statusCounts.conflict,
797
- })}
798
- </span>
799
- <span className="rounded-md bg-muted/45 px-2 py-1 text-muted-foreground">
800
- {t("visualEditor.pinStatusCount.blocked", {
801
- count: statusCounts.unsupported,
802
- })}
803
- </span>
804
- </div>
805
- </div>
806
- )}
807
-
808
- {/* Pin overlays */}
809
- {pins.map((pin, pinIndex) => {
810
- const { dx, dy } = pinClusterOffset(pin, pinIndex, pins);
811
- const left = rect.left + (pin.xPct / 100) * rect.width + dx;
812
- const top = rect.top + (pin.yPct / 100) * rect.height + dy;
813
- const isActive = activePinId === pin.id;
814
- const PinIcon = pin.submitted ? IconMessageCheck : IconMessage;
815
- const status = localizePinStatus(derivePinStatus(pin, pins), t);
816
- // Not submitted yet + claims a specific anchor that's no longer on
817
- // the canvas (e.g. the element was deleted or the design was edited
818
- // elsewhere) — flag it instead of silently pretending the position
819
- // is still meaningful.
820
- const isStaleAnchor =
821
- !pin.submitted && !pinAnchorStillPresent(canvas, pin);
822
- return (
823
- <div
824
- key={pin.id}
825
- data-pin-popover
826
- data-pin-id={pin.id}
827
- className="fixed z-[55]"
828
- style={{ left, top }}
829
- >
830
- {/* Pin marker. The tooltip is suppressed while the composer is
831
- * open: the textarea already shows the same draft text, and a
832
- * shadcn TooltipContent (z-[250], no `pointer-events: none`) was
833
- * intercepting Send-button clicks for pins near the top of the
834
- * canvas where Radix auto-flips the tooltip below the trigger
835
- * and onto the composer. */}
836
- <Tooltip open={isActive ? false : undefined}>
837
- <TooltipTrigger asChild>
838
- <button
839
- onClick={() => {
840
- if (!pin.submitted) setActivePinId(pin.id);
841
- }}
842
- className={cn(
843
- "absolute -mt-1 flex size-7 -translate-x-1/2 -translate-y-full cursor-pointer items-center justify-center rounded-full rounded-bl-none shadow-lg ring-2 transition-transform hover:scale-110",
844
- status.markerClassName,
845
- pin.submitted && "opacity-95",
846
- isStaleAnchor &&
847
- "outline outline-2 outline-dashed outline-offset-2 outline-muted-foreground/70",
848
- )}
849
- >
850
- <PinIcon className="size-3.5" />
851
- </button>
852
- </TooltipTrigger>
853
- <TooltipContent className="pointer-events-none">
854
- {isStaleAnchor
855
- ? t("visualEditor.staleAnchorDetail")
856
- : pin.queued
857
- ? t("visualEditor.queuedStatus", { status: status.label })
858
- : pin.draft ||
859
- (pin.submitted
860
- ? t("visualEditor.commentSentStatus", {
861
- status: status.label,
862
- })
863
- : t("visualEditor.commentStatus", {
864
- status: status.label,
865
- }))}
866
- </TooltipContent>
867
- </Tooltip>
868
- <span
869
- className={cn(
870
- "pointer-events-none absolute left-3 -top-10 rounded-full border px-1.5 py-0.5 text-[9px] font-medium leading-none shadow-sm",
871
- status.badgeClassName,
872
- )}
873
- >
874
- {pin.queued && !pin.submitted
875
- ? t("visualEditor.queued")
876
- : status.shortLabel}
877
- </span>
878
-
879
- {/* Inline composer. z-[260] keeps it above the shadcn floating-UI
880
- * tier (z-[250] — Tooltip, Popover, Dialog overlay, etc.) so a
881
- * stray tooltip that pops over the pin can't swallow Send clicks.
882
- * Composer is flipped horizontally and/or vertically when the pin
883
- * is near a viewport edge so it stays fully on-screen. */}
884
- {isActive &&
885
- !pin.submitted &&
886
- (() => {
887
- const composerW = 288; // w-72
888
- const composerH = 220; // estimated height
889
- const flipX = left + 12 + composerW > window.innerWidth;
890
- const flipY = top + 4 + composerH > window.innerHeight;
891
- return (
892
- <div
893
- data-pin-popover
894
- className="absolute z-[260] w-72 rounded-lg border border-border bg-popover shadow-xl p-2"
895
- style={{
896
- ...(flipX
897
- ? { right: "calc(100% + 4px)", left: "auto" }
898
- : { left: "0.75rem" }),
899
- ...(flipY
900
- ? { bottom: "calc(100% + 4px)", top: "auto" }
901
- : { top: "0.25rem" }),
902
- }}
903
- >
904
- <div className="mb-2 flex items-center justify-between gap-2">
905
- <p className="text-xs font-semibold text-foreground">
906
- {t("visualEditor.editDesign")}
907
- </p>
908
- <PinStatusBadge status={status} />
909
- </div>
910
- <Textarea
911
- autoFocus
912
- value={pin.draft || ""}
913
- onChange={(e) =>
914
- updatePin(pin.id, {
915
- draft: e.target.value,
916
- queued: false,
917
- })
918
- }
919
- onKeyDown={(e) => {
920
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
921
- e.preventDefault();
922
- submitPin(pin);
923
- }
924
- if (e.key === "Escape") {
925
- // Stop propagation so the window Escape listener (which
926
- // calls onClose) does not also fire. Collapse the composer
927
- // back to the pin dot while preserving the draft text —
928
- // the pin and any typed draft survive. A second Escape
929
- // with no composer open exits pin mode, matching Figma.
930
- e.stopPropagation();
931
- e.preventDefault();
932
- setActivePinId(null);
933
- }
934
- }}
935
- placeholder={t("visualEditor.tellAgentWhatToChange")}
936
- className="resize-none text-xs min-h-[60px]"
937
- />
938
- <p className="mt-1 text-[10px] leading-4 text-muted-foreground">
939
- {status.detail}
940
- </p>
941
- {pin.targetAnchorId && (
942
- <div
943
- className={cn(
944
- "mt-1 truncate rounded-md px-2 py-1 text-[10px]",
945
- isStaleAnchor
946
- ? "bg-destructive/10 text-destructive"
947
- : "bg-muted/45 text-muted-foreground",
948
- )}
949
- >
950
- {isStaleAnchor
951
- ? t("visualEditor.staleAnchorDetail")
952
- : t("visualEditor.anchorLabel", {
953
- id: pin.targetAnchorId,
954
- })}
955
- </div>
956
- )}
957
- {pin.targetText && (
958
- <div className="text-[10px] text-muted-foreground mt-1 italic line-clamp-1">
959
- {t("visualEditor.nearText", { text: pin.targetText })}
960
- </div>
961
- )}
962
- <div className="flex items-center justify-between mt-2">
963
- <span className="text-[10px] text-muted-foreground">
964
- {t("visualEditor.submitShortcut", {
965
- mod: /Mac|iPhone|iPad/.test(navigator.userAgent)
966
- ? "⌘"
967
- : "Ctrl",
968
- })}
969
- </span>
970
- <div className="flex gap-1">
971
- <Button
972
- size="sm"
973
- variant="ghost"
974
- className="h-6 px-2 text-[10px] cursor-pointer"
975
- onClick={() => removePin(pin.id)}
976
- >
977
- <IconX className="w-3 h-3" />
978
- </Button>
979
- <Button
980
- size="sm"
981
- variant="outline"
982
- className="h-6 gap-1 px-2 text-[10px] cursor-pointer"
983
- onClick={() => queuePin(pin)}
984
- disabled={!(pin.draft || "").trim()}
985
- >
986
- {t("visualEditor.queue")}
987
- </Button>
988
- {submitMode === "direct" && (
989
- <Button
990
- size="sm"
991
- className="h-6 gap-1 px-2 text-[10px] cursor-pointer"
992
- onClick={() => submitPin(pin)}
993
- disabled={!(pin.draft || "").trim()}
994
- >
995
- <IconSend className="w-3 h-3" />
996
- {t("visualEditor.send")}
997
- </Button>
998
- )}
999
- </div>
1000
- </div>
1001
- </div>
1002
- );
1003
- })()}
1004
- </div>
1005
- );
1006
- })}
1007
- </>
29
+ />
1008
30
  );
1009
31
  }