@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,1222 +1,14 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
2
  import {
3
- IconEraser,
4
- IconArrowBackUp,
5
- IconArrowForwardUp,
6
- IconLoader2,
7
- IconSend,
8
- IconCursorText,
9
- IconX,
10
- } from "@tabler/icons-react";
11
- import {
12
- useState,
13
- useRef,
14
- useCallback,
15
- useEffect,
16
- useLayoutEffect,
17
- type ReactNode,
18
- } from "react";
19
- import { createPortal } from "react-dom";
20
- import { toast } from "sonner";
21
-
22
- import { Button } from "@/components/ui/button";
23
- import { Input } from "@/components/ui/input";
24
- import {
25
- Tooltip,
26
- TooltipContent,
27
- TooltipTrigger,
28
- } from "@/components/ui/tooltip";
29
- import { cn } from "@/lib/utils";
30
-
31
- export interface DrawAnnotation {
32
- id: string;
33
- type: "path" | "text";
34
- /** SVG path data for "path" type */
35
- pathData?: string;
36
- /** Text content for "text" type */
37
- text?: string;
38
- /** Annotation color (hex) */
39
- color: string;
40
- /** Stroke width for paths, font weight reference for text */
41
- lineWidth: number;
42
- /** Position relative to the canvas (text annotations only) */
43
- position: { x: number; y: number };
44
- /**
45
- * Creation timestamp (Date.now()) — used for unified undo ordering across
46
- * strokes and text annotations. Optional so existing callers of DrawAnnotation
47
- * don't need to supply it.
48
- */
49
- createdAt?: number;
50
- }
51
-
52
- interface DrawOverlayProps {
53
- /** Whether the overlay is currently visible (toggled from the toolbar) */
54
- visible: boolean;
55
- /** When false, canvas clicks pass through to sibling tools while the toolbar stays usable. */
56
- canvasInteractive?: boolean;
57
- /** Extra queued annotations owned by sibling tools, such as comment pins. */
58
- queuedAnnotationCount?: number;
59
- /**
60
- * Current zoom level (percentage, e.g. 100 = 100%). Used to convert
61
- * getBoundingClientRect() visual-space coordinates to layout-space so that
62
- * strokes and text labels stay anchored to their painted position across
63
- * zoom changes.
64
- */
65
- zoom?: number;
66
- /** Called when the user submits the queued strokes/text to the agent */
67
- onSend: (
68
- annotations: DrawAnnotation[],
69
- instruction: string,
70
- canvasSize: { width: number; height: number },
71
- ) => void;
72
- /** Called when the user cancels / closes the draw mode */
73
- onClose: () => void;
74
- /**
75
- * True while the caller is capturing/compositing/uploading the annotated
76
- * screenshot for a just-submitted drawing (see the design app's
77
- * design-canvas/annotation-snapshot.ts). Disables Send and swaps its icon
78
- * for a spinner so a slow capture can't be triggered twice from the same
79
- * drawing. The entire overlay becomes inert while true so edits made after
80
- * the submitted snapshot cannot be erased when that submission is later
81
- * confirmed and the caller clears the batch.
82
- */
83
- sending?: boolean;
84
- /**
85
- * Bump this (e.g. a counter) exactly when the caller wants to discard the
86
- * current strokes/text annotations — after `onClose` or a confirmed Send.
87
- * Strokes/text are intentionally NOT cleared merely because `visible`
88
- * turns false: `visible` can toggle off for reasons that have nothing to
89
- * do with the user wanting to discard their work (switching tools, views,
90
- * or side panels), and doing so silently would destroy annotations the
91
- * user never got a chance to send. Only an in-progress (uncommitted)
92
- * gesture is reset on hide; a pending text label is committed so its typed
93
- * content survives.
94
- */
95
- clearSignal?: number;
96
- /**
97
- * Identity of the canvas this annotation batch belongs to. When the host
98
- * reuses one DrawOverlay instance for a different screen, the old batch is
99
- * cleared with a warning instead of being silently reinterpreted against
100
- * the new screen's pixels and submitted with the wrong screenshot.
101
- */
102
- scopeKey?: string;
103
- /**
104
- * Keep the canvas DOM/bitmap alive while hidden. Use for the one active
105
- * editor overlay whose work must survive mode/view toggles; leave false for
106
- * large preview grids so every dormant screen does not allocate a canvas
107
- * and ResizeObserver.
108
- */
109
- retainSurfaceWhenHidden?: boolean;
110
- }
111
-
112
- const PRESET_COLORS = [
113
- { color: "#ef4444", label: "Red" },
114
- { color: "#3b82f6", label: "Blue" },
115
- { color: "#22c55e", label: "Green" },
116
- { color: "#eab308", label: "Yellow" },
117
- ];
118
-
119
- const LINE_WIDTHS = [
120
- { value: 2, label: "Thin" },
121
- { value: 4, label: "Medium" },
122
- { value: 8, label: "Thick" },
123
- ];
124
-
125
- /**
126
- * A point stored as fractions (0..1) of the canvas visual rect.
127
- * This makes coordinates zoom- and resize-stable: multiply by the current
128
- * rect width/height to get visual pixels for canvas drawing, or multiply by
129
- * rect/scale to get layout-space for CSS positioning and pathData output.
130
- */
131
- interface Point {
132
- x: number;
133
- y: number;
134
- }
135
-
136
- interface Stroke {
137
- id: string;
138
- /** Points stored as fractions (0..1) of the canvas visual rect. */
139
- points: Point[];
140
- color: string;
141
- lineWidth: number;
142
- /** Creation timestamp for unified undo ordering. */
143
- createdAt: number;
144
- }
145
-
146
- interface PendingTextInput {
147
- /** Fractional x (0..1) of the visual rect. */
148
- xFrac: number;
149
- /** Fractional y (0..1) of the visual rect. */
150
- yFrac: number;
151
- value: string;
152
- /**
153
- * Monotonic generation id. A click that opens a new pending text box while
154
- * an earlier one is still focused fires pointerdown (which replaces the
155
- * ref synchronously) before the outgoing box's native `blur` event runs.
156
- * Without this, that trailing blur reads the *new* (empty) pending input
157
- * out of the ref and wipes it via `commitTextAnnotation`'s unconditional
158
- * `setPendingTextInput(null)` before the user can type anything. Callers
159
- * pass the generation captured at their own render so a stale blur/Enter/
160
- * Escape from a since-replaced box is a no-op instead of clobbering the
161
- * newer one.
162
- */
163
- generation: number;
164
- }
165
-
166
- /**
167
- * Keep long pen/stylus gestures bounded before they become agent prompt data.
168
- * Once this limit is reached, older samples are progressively decimated while
169
- * preserving the first, latest, and future points. At normal pointer rates the
170
- * sub-pixel filter below is the only sampling users will notice.
171
- */
172
- const MAX_STROKE_POINTS = 2048;
173
- const MIN_POINT_DISTANCE_PX = 0.35;
174
-
175
- function pointFromClient(
176
- clientX: number,
177
- clientY: number,
178
- rect: DOMRect,
179
- ): Point | null {
180
- if (rect.width <= 0 || rect.height <= 0) return null;
181
- return {
182
- x: Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)),
183
- y: Math.max(0, Math.min(1, (clientY - rect.top) / rect.height)),
184
- };
185
- }
186
-
187
- function appendStrokePoint(
188
- points: Point[],
189
- point: Point,
190
- rect: DOMRect,
191
- force = false,
192
- ): Point[] {
193
- const last = points[points.length - 1];
194
- if (last) {
195
- const dx = (point.x - last.x) * rect.width;
196
- const dy = (point.y - last.y) * rect.height;
197
- const distanceSquared = dx * dx + dy * dy;
198
- if (
199
- distanceSquared === 0 ||
200
- (!force && distanceSquared < MIN_POINT_DISTANCE_PX ** 2)
201
- ) {
202
- return points;
203
- }
204
- }
205
-
206
- let next = points;
207
- if (next.length >= MAX_STROKE_POINTS) {
208
- // Preserve the full trajectory instead of dropping the tail of a long
209
- // gesture. Repeated compaction gradually lowers only the oldest sampling
210
- // density, which is visually preferable to a path that suddenly stops.
211
- const compacted: Point[] = [next[0]];
212
- for (let index = 2; index < next.length - 1; index += 2) {
213
- compacted.push(next[index]);
214
- }
215
- compacted.push(next[next.length - 1]);
216
- next = compacted;
217
- }
218
-
219
- next.push(point);
220
- return next;
221
- }
222
-
223
- /**
224
- * Draw-to-prompt overlay for the slide canvas.
225
- *
226
- * Mirrors claude.ai/design's "Draw mode": the user sketches on the canvas,
227
- * adds a one-line text instruction, hits Send, and the strokes + instruction
228
- * are forwarded to the agent. The agent receives the path geometry along with
229
- * the canvas size so it can interpret position semantically (e.g. "move the
230
- * title here").
231
- *
232
- * This component is canvas-agnostic — it overlays absolutely-positioned over
233
- * its parent, so the parent (a slide editor or design canvas) only needs to
234
- * be `position: relative`.
235
- *
236
- * Coordinate model
237
- * ----------------
238
- * All stroke points and text positions are stored as fractions (0..1) of the
239
- * canvas's visual rect (getBoundingClientRect). This makes them stable across
240
- * zoom and resize. When rendering:
241
- * - Canvas strokes: multiply by rect.width / rect.height (visual pixels).
242
- * - CSS text labels: multiply by rect.width/scale / rect.height/scale
243
- * (layout pixels inside the scaled wrapper).
244
- * - pathData in send(): layout pixels = fraction * rect.width / scale.
245
- */
246
- export function DrawOverlay({
247
- visible,
248
- canvasInteractive = true,
249
- queuedAnnotationCount = 0,
250
- zoom = 100,
251
- onSend,
252
- onClose,
253
- sending = false,
254
- clearSignal,
255
- scopeKey,
256
- retainSurfaceWhenHidden = false,
257
- }: DrawOverlayProps) {
258
- const t = useT();
259
- const containerRef = useRef<HTMLDivElement>(null);
260
- const canvasRef = useRef<HTMLCanvasElement>(null);
261
- const [color, setColor] = useState(PRESET_COLORS[0].color);
262
- const [lineWidth, setLineWidth] = useState(LINE_WIDTHS[1].value);
263
- const [strokes, setStrokes] = useState<Stroke[]>([]);
264
- const strokesRef = useRef<Stroke[]>([]);
265
- const [textAnnotations, setTextAnnotations] = useState<DrawAnnotation[]>([]);
266
- const textAnnotationsRef = useRef<DrawAnnotation[]>([]);
267
- // Unified redo stack. Each entry is either a Stroke or a DrawAnnotation so
268
- // undo/redo work in creation order across both types.
269
- const [redoStack, setRedoStack] = useState<Array<Stroke | DrawAnnotation>>(
270
- [],
271
- );
272
- const [currentStroke, setCurrentStroke] = useState<Point[] | null>(null);
273
- // Pointer events can arrive down -> move -> up before React renders once.
274
- // This ref is the authoritative in-progress gesture; state is only a
275
- // frame-throttled snapshot used to redraw the canvas.
276
- const currentStrokeRef = useRef<Point[] | null>(null);
277
- const currentStrokeFrameRef = useRef<number | null>(null);
278
- const activePointerIdRef = useRef<number | null>(null);
279
- const activeStrokeStyleRef = useRef({ color, lineWidth });
280
- const lastCreatedAtRef = useRef(0);
281
- const [textMode, setTextMode] = useState(false);
282
- const [textInput, setTextInput] = useState<PendingTextInput | null>(null);
283
- const textInputStateRef = useRef<PendingTextInput | null>(null);
284
- const textInputGenerationRef = useRef(0);
285
- const textInputRef = useRef<HTMLInputElement>(null);
286
- // Escape cancels the pending text annotation, but unmounting the input also
287
- // fires its blur handler, which would commit the very annotation the user
288
- // just cancelled. This flag lets the blur handler skip that commit.
289
- const cancelingTextRef = useRef(false);
290
- const [instruction, setInstruction] = useState("");
291
- const instructionRef = useRef("");
292
- const drawing = useRef(false);
293
- const canvasSizeRef = useRef({ w: 0, h: 0 });
294
- // Resize tick: bumped by ResizeObserver so the redraw effect re-runs when
295
- // the canvas element's CSS size changes (e.g. device frame switch).
296
- const [resizeTick, setResizeTick] = useState(0);
297
-
298
- const scale = Math.max(zoom / 100, 0.01);
299
-
300
- // A host can still unmount the active editor entirely (route/design close,
301
- // or focused canvas teardown) rather than merely hiding this component.
302
- // Losing unsent work in that teardown must never be silent. Normal
303
- // overview/focused visibility changes retain their active surface and do
304
- // not hit this cleanup; only a real component unmount does.
305
- useEffect(() => {
306
- return () => {
307
- // A true unmount never runs the `!visible` effect above (the prop
308
- // never transitions — the component is simply gone), so a still-open
309
- // pending text box never gets its usual commit-on-hide chance. Count
310
- // it here too, or a label the user was mid-typing would vanish
311
- // without even counting toward the warning.
312
- const pendingText = textInputStateRef.current?.value.trim();
313
- const discardedCount =
314
- strokesRef.current.length +
315
- textAnnotationsRef.current.length +
316
- (pendingText ? 1 : 0) +
317
- (instructionRef.current.trim() ? 1 : 0);
318
- if (discardedCount > 0) {
319
- toast(
320
- t("visualEditor.annotationsDiscardedOnViewChange", {
321
- count: discardedCount,
322
- }),
323
- );
324
- }
325
- };
326
- // eslint-disable-next-line react-hooks/exhaustive-deps -- must only run
327
- // its cleanup on actual unmount, not on every `t`/locale change.
328
- }, []);
329
-
330
- const cancelScheduledStrokeFrame = useCallback(() => {
331
- if (currentStrokeFrameRef.current === null) return;
332
- window.cancelAnimationFrame(currentStrokeFrameRef.current);
333
- currentStrokeFrameRef.current = null;
334
- }, []);
335
-
336
- const scheduleCurrentStrokeRedraw = useCallback(() => {
337
- if (currentStrokeFrameRef.current !== null) return;
338
- currentStrokeFrameRef.current = window.requestAnimationFrame(() => {
339
- currentStrokeFrameRef.current = null;
340
- const points = currentStrokeRef.current;
341
- setCurrentStroke(points ? [...points] : null);
342
- });
343
- }, []);
344
-
345
- const resetActiveStroke = useCallback(() => {
346
- drawing.current = false;
347
- activePointerIdRef.current = null;
348
- currentStrokeRef.current = null;
349
- cancelScheduledStrokeFrame();
350
- setCurrentStroke(null);
351
- }, [cancelScheduledStrokeFrame]);
352
-
353
- const nextCreatedAt = useCallback(() => {
354
- const next = Math.max(Date.now(), lastCreatedAtRef.current + 1);
355
- lastCreatedAtRef.current = next;
356
- return next;
357
- }, []);
358
-
359
- const setPendingTextInput = useCallback(
360
- (
361
- update:
362
- | PendingTextInput
363
- | null
364
- | ((current: PendingTextInput | null) => PendingTextInput | null),
365
- ) => {
366
- const next =
367
- typeof update === "function"
368
- ? update(textInputStateRef.current)
369
- : update;
370
- textInputStateRef.current = next;
371
- setTextInput(next);
372
- },
373
- [],
374
- );
375
-
376
- const clearAnnotationState = useCallback(() => {
377
- resetActiveStroke();
378
- strokesRef.current = [];
379
- textAnnotationsRef.current = [];
380
- setStrokes([]);
381
- setTextAnnotations([]);
382
- setRedoStack([]);
383
- setPendingTextInput(null);
384
- setTextMode(false);
385
- instructionRef.current = "";
386
- setInstruction("");
387
- cancelingTextRef.current = false;
388
- lastCreatedAtRef.current = 0;
389
- textInputGenerationRef.current = 0;
390
- }, [resetActiveStroke, setPendingTextInput]);
391
-
392
- // Hiding ends whatever the user was mid-gesture on, but must NOT discard
393
- // already-committed strokes/text: `visible` can turn false for reasons
394
- // that have nothing to do with the user wanting to abandon their
395
- // annotations (switching tools, views, or side panels). Only reset the
396
- // transient in-flight state here — the committed `strokes`/
397
- // `textAnnotations` arrays are cleared exclusively by `clearSignal` below.
398
- useEffect(() => {
399
- if (!visible) {
400
- resetActiveStroke();
401
- setTextMode(false);
402
- cancelingTextRef.current = false;
403
- }
404
- }, [resetActiveStroke, visible]);
405
-
406
- // Comment-pin mode can temporarily leave the toolbar visible while making
407
- // the drawing surface inert. Never retain a half-finished gesture across
408
- // that tool switch.
409
- useEffect(() => {
410
- if (!canvasInteractive) resetActiveStroke();
411
- }, [canvasInteractive, resetActiveStroke]);
412
-
413
- useEffect(
414
- () => () => {
415
- cancelScheduledStrokeFrame();
416
- },
417
- [cancelScheduledStrokeFrame],
418
- );
419
-
420
- const shouldFocusTextInput = textInput !== null;
421
- useEffect(() => {
422
- if (!visible || !shouldFocusTextInput) return;
423
- const id = window.requestAnimationFrame(() => {
424
- textInputRef.current?.focus();
425
- });
426
- return () => window.cancelAnimationFrame(id);
427
- }, [shouldFocusTextInput, visible]);
428
-
429
- // Retained editor surfaces keep their canvas mounted while hidden, so a
430
- // hidden-first host installs its observer and the bitmap does not flash
431
- // blank when the user re-enters annotate mode. Non-retained preview-grid
432
- // overlays attach only while visible to avoid idle canvas/observer cost.
433
- useEffect(() => {
434
- if (!visible && !retainSurfaceWhenHidden) return;
435
- const canvas = canvasRef.current;
436
- if (!canvas) return;
437
- if (typeof ResizeObserver === "undefined") return;
438
- const ro = new ResizeObserver(() => setResizeTick((t) => t + 1));
439
- ro.observe(canvas);
440
- return () => ro.disconnect();
441
- }, [retainSurfaceWhenHidden, visible]);
442
-
443
- // Redraw before paint whenever strokes change, the canvas is resized, or a
444
- // preserved batch becomes visible again. The visible dependency is
445
- // intentional even though the DOM stays mounted: browsers may discard a
446
- // hidden canvas backing store under memory pressure, and re-entry must
447
- // never expose a blank frame before the next pointer event.
448
- useLayoutEffect(() => {
449
- if (!visible) return;
450
- const canvas = canvasRef.current;
451
- if (!canvas) return;
452
- const ctx = canvas.getContext("2d");
453
- if (!ctx) return;
454
-
455
- const rect = canvas.getBoundingClientRect();
456
- const dpr = window.devicePixelRatio;
457
- const newW = rect.width * dpr;
458
- const newH = rect.height * dpr;
459
- if (newW !== canvasSizeRef.current.w || newH !== canvasSizeRef.current.h) {
460
- canvas.width = newW;
461
- canvas.height = newH;
462
- canvasSizeRef.current = { w: newW, h: newH };
463
- ctx.scale(dpr, dpr);
464
- }
465
-
466
- ctx.clearRect(0, 0, rect.width, rect.height);
467
-
468
- // Points are stored as fractions; multiply by current rect to get visual px.
469
- for (const stroke of strokes) {
470
- drawStroke(
471
- ctx,
472
- stroke.points.map((p) => ({
473
- x: p.x * rect.width,
474
- y: p.y * rect.height,
475
- })),
476
- stroke.color,
477
- stroke.lineWidth,
478
- );
479
- }
480
-
481
- if (currentStroke && currentStroke.length > 0) {
482
- const activeStyle = activeStrokeStyleRef.current;
483
- drawStroke(
484
- ctx,
485
- currentStroke.map((p) => ({
486
- x: p.x * rect.width,
487
- y: p.y * rect.height,
488
- })),
489
- activeStyle.color,
490
- activeStyle.lineWidth,
491
- );
492
- }
493
- // `zoom` is a dependency because it scales the canvas via a CSS transform,
494
- // which changes getBoundingClientRect() without firing ResizeObserver — the
495
- // effect must re-run on zoom change to redraw the fraction-based strokes at
496
- // the new visual size.
497
- }, [strokes, currentStroke, color, lineWidth, resizeTick, visible, zoom]);
498
-
499
- /**
500
- * Commits the pending text annotation (if any) and clears the pending
501
- * input. `expectedGeneration`, when passed, guards against a stale
502
- * blur/Enter/Escape belonging to a pending text box that has since been
503
- * replaced or already cleared — see `PendingTextInput.generation`. Called
504
- * with no argument to force-commit whatever is currently pending (e.g.
505
- * before opening a new text box, or right before Send).
506
- */
507
- const commitTextAnnotation = useCallback(
508
- (expectedGeneration?: number) => {
509
- const pendingText = textInputStateRef.current;
510
- if (
511
- !pendingText ||
512
- (expectedGeneration !== undefined &&
513
- pendingText.generation !== expectedGeneration)
514
- ) {
515
- return;
516
- }
517
- // Clear the authoritative pending value first. Enter, blur, and Send can
518
- // all occur in the same browser turn; any later handler becomes a no-op
519
- // instead of duplicating the label.
520
- setPendingTextInput(null);
521
- if (!pendingText.value.trim()) return;
522
-
523
- const ann: DrawAnnotation = {
524
- id: crypto.randomUUID(),
525
- type: "text",
526
- text: pendingText.value.trim(),
527
- // Store fractional position so the label stays anchored across zoom changes.
528
- position: { x: pendingText.xFrac, y: pendingText.yFrac },
529
- color,
530
- lineWidth,
531
- createdAt: nextCreatedAt(),
532
- };
533
- const nextTexts = [...textAnnotationsRef.current, ann];
534
- textAnnotationsRef.current = nextTexts;
535
- setTextAnnotations(nextTexts);
536
- // A new text annotation also clears the redo stack.
537
- setRedoStack([]);
538
- },
539
- [color, lineWidth, nextCreatedAt, setPendingTextInput],
540
- );
541
-
542
- // Hiding removes the pending-text input from interaction. Commit whatever
543
- // was typed explicitly so a still-open label survives an unrelated exit
544
- // instead of relying on incidental native blur ordering.
545
- useEffect(() => {
546
- if (!visible) commitTextAnnotation();
547
- }, [visible, commitTextAnnotation]);
3
+ DrawOverlay as ToolkitDrawOverlay,
4
+ type DrawOverlayProps as ToolkitDrawOverlayProps,
5
+ } from "@agent-native/toolkit/canvas-annotations";
548
6
 
549
- // The explicit-discard path: the caller bumps `clearSignal` exactly when
550
- // the user deliberately closes the overlay (X/Escape) or a Send is
551
- // confirmed delivered. A scope change below is the only other reset path,
552
- // guarding against submitting one screen's geometry against another.
553
- const lastClearSignalRef = useRef(clearSignal);
554
- useLayoutEffect(() => {
555
- if (clearSignal === lastClearSignalRef.current) return;
556
- lastClearSignalRef.current = clearSignal;
557
- clearAnnotationState();
558
- }, [clearAnnotationState, clearSignal]);
7
+ export type { DrawAnnotation } from "@agent-native/toolkit/canvas-annotations";
559
8
 
560
- const lastScopeKeyRef = useRef(scopeKey);
561
- useLayoutEffect(() => {
562
- if (scopeKey === lastScopeKeyRef.current) return;
563
- lastScopeKeyRef.current = scopeKey;
564
- const pendingText = textInputStateRef.current?.value.trim();
565
- const discardedCount =
566
- strokesRef.current.length +
567
- textAnnotationsRef.current.length +
568
- (pendingText ? 1 : 0) +
569
- (instructionRef.current.trim() ? 1 : 0);
570
- if (discardedCount > 0) {
571
- toast(
572
- t("visualEditor.annotationsDiscardedOnViewChange", {
573
- count: discardedCount,
574
- }),
575
- );
576
- }
577
- clearAnnotationState();
578
- }, [clearAnnotationState, scopeKey, t]);
579
-
580
- const handlePointerDown = useCallback(
581
- (e: React.PointerEvent) => {
582
- const rect = canvasRef.current?.getBoundingClientRect();
583
- if (!rect || rect.width <= 0 || rect.height <= 0) return;
584
- if (!canvasInteractive || sending || drawing.current) return;
585
- if (e.pointerType === "mouse" && e.button !== 0) return;
586
-
587
- if (textMode) {
588
- e.preventDefault();
589
- cancelingTextRef.current = false;
590
- // Commit whatever text box was still open (using the current ref,
591
- // which still holds the outgoing box at this point) before replacing
592
- // it, so clicking a new spot never silently drops the previous label.
593
- commitTextAnnotation();
594
- textInputGenerationRef.current += 1;
595
- setPendingTextInput({
596
- xFrac: (e.clientX - rect.left) / rect.width,
597
- yFrac: (e.clientY - rect.top) / rect.height,
598
- value: "",
599
- generation: textInputGenerationRef.current,
600
- });
601
- return;
602
- }
603
-
604
- const point = pointFromClient(e.clientX, e.clientY, rect);
605
- if (!point) return;
606
- e.preventDefault();
607
- drawing.current = true;
608
- activePointerIdRef.current = e.pointerId;
609
- activeStrokeStyleRef.current = { color, lineWidth };
610
- currentStrokeRef.current = [point];
611
- setCurrentStroke([point]);
612
- try {
613
- e.currentTarget.setPointerCapture(e.pointerId);
614
- } catch {
615
- // Pointer capture may fail if the browser already cancelled the
616
- // gesture. The pointer-id guard still prevents cross-pointer mixing.
617
- }
618
- },
619
- [
620
- canvasInteractive,
621
- color,
622
- commitTextAnnotation,
623
- lineWidth,
624
- sending,
625
- setPendingTextInput,
626
- textMode,
627
- ],
628
- );
629
-
630
- const handlePointerMove = useCallback(
631
- (e: React.PointerEvent) => {
632
- if (!drawing.current || textMode) return;
633
- if (activePointerIdRef.current !== e.pointerId) return;
634
- const rect = canvasRef.current?.getBoundingClientRect();
635
- if (!rect || !currentStrokeRef.current) return;
636
-
637
- const nativeEvent = e.nativeEvent;
638
- const coalesced = nativeEvent.getCoalescedEvents?.() ?? [];
639
- const samples = [...coalesced, nativeEvent];
640
- let points = currentStrokeRef.current;
641
- let changed = false;
642
- for (const sample of samples) {
643
- const point = pointFromClient(sample.clientX, sample.clientY, rect);
644
- if (!point) continue;
645
- const previousLength = points.length;
646
- const next = appendStrokePoint(points, point, rect);
647
- if (next !== points || next.length !== previousLength) changed = true;
648
- points = next;
649
- }
650
- currentStrokeRef.current = points;
651
- if (changed) scheduleCurrentStrokeRedraw();
652
- },
653
- [scheduleCurrentStrokeRedraw, textMode],
654
- );
655
-
656
- const handlePointerUp = useCallback(
657
- (e: React.PointerEvent<HTMLCanvasElement>) => {
658
- if (!drawing.current || activePointerIdRef.current !== e.pointerId)
659
- return;
660
-
661
- e.preventDefault();
662
- const rect = canvasRef.current?.getBoundingClientRect();
663
- let points = currentStrokeRef.current;
664
- if (rect && points) {
665
- const coalesced = e.nativeEvent.getCoalescedEvents?.() ?? [];
666
- for (const sample of [...coalesced, e.nativeEvent]) {
667
- const point = pointFromClient(sample.clientX, sample.clientY, rect);
668
- if (point) points = appendStrokePoint(points, point, rect, true);
669
- }
670
- currentStrokeRef.current = points;
671
- }
672
-
673
- drawing.current = false;
674
- activePointerIdRef.current = null;
675
- cancelScheduledStrokeFrame();
676
- if (points && points.length > 1) {
677
- const activeStyle = activeStrokeStyleRef.current;
678
- const stroke: Stroke = {
679
- id: crypto.randomUUID(),
680
- points: [...points],
681
- color: activeStyle.color,
682
- lineWidth: activeStyle.lineWidth,
683
- createdAt: nextCreatedAt(),
684
- };
685
- const nextStrokes = [...strokesRef.current, stroke];
686
- strokesRef.current = nextStrokes;
687
- setStrokes(nextStrokes);
688
- // A new stroke clears the redo stack (standard editor convention).
689
- setRedoStack([]);
690
- }
691
- currentStrokeRef.current = null;
692
- setCurrentStroke(null);
693
-
694
- try {
695
- if (e.currentTarget.hasPointerCapture(e.pointerId)) {
696
- e.currentTarget.releasePointerCapture(e.pointerId);
697
- }
698
- } catch {
699
- // The browser can release capture before React handles pointerup.
700
- }
701
- },
702
- [cancelScheduledStrokeFrame, nextCreatedAt],
703
- );
704
-
705
- const handlePointerCancel = useCallback(
706
- (e: React.PointerEvent<HTMLCanvasElement>) => {
707
- if (activePointerIdRef.current !== e.pointerId) return;
708
- resetActiveStroke();
709
- try {
710
- if (e.currentTarget.hasPointerCapture(e.pointerId)) {
711
- e.currentTarget.releasePointerCapture(e.pointerId);
712
- }
713
- } catch {
714
- // Pointer cancellation often releases capture before this event.
715
- }
716
- },
717
- [resetActiveStroke],
718
- );
719
-
720
- const handleLostPointerCapture = useCallback(
721
- (e: React.PointerEvent<HTMLCanvasElement>) => {
722
- if (drawing.current && activePointerIdRef.current === e.pointerId) {
723
- resetActiveStroke();
724
- }
725
- },
726
- [resetActiveStroke],
727
- );
728
-
729
- /**
730
- * Undo removes the most recently created annotation (stroke or text) in
731
- * creation order and pushes it onto the unified redo stack.
732
- */
733
- const undo = () => {
734
- if (sending) return;
735
- const currentStrokes = strokesRef.current;
736
- const currentTexts = textAnnotationsRef.current;
737
- const lastStroke =
738
- currentStrokes.length > 0
739
- ? currentStrokes[currentStrokes.length - 1]
740
- : null;
741
- const lastText =
742
- currentTexts.length > 0 ? currentTexts[currentTexts.length - 1] : null;
743
-
744
- if (!lastStroke && !lastText) return;
745
-
746
- // Remove whichever was created most recently.
747
- const strokeTime = lastStroke?.createdAt ?? -Infinity;
748
- const textTime = lastText?.createdAt ?? 0;
749
-
750
- if (lastStroke && strokeTime >= textTime) {
751
- const nextStrokes = currentStrokes.slice(0, -1);
752
- strokesRef.current = nextStrokes;
753
- setStrokes(nextStrokes);
754
- setRedoStack((stack) => [...stack, lastStroke]);
755
- } else if (lastText) {
756
- const nextTexts = currentTexts.slice(0, -1);
757
- textAnnotationsRef.current = nextTexts;
758
- setTextAnnotations(nextTexts);
759
- setRedoStack((stack) => [...stack, lastText]);
760
- }
761
- };
762
-
763
- const redo = () => {
764
- if (sending) return;
765
- setRedoStack((stack) => {
766
- if (stack.length === 0) return stack;
767
- const top = stack[stack.length - 1];
768
- const remaining = stack.slice(0, -1);
769
- if ("points" in top) {
770
- // It's a Stroke
771
- const nextStrokes = [...strokesRef.current, top as Stroke];
772
- strokesRef.current = nextStrokes;
773
- setStrokes(nextStrokes);
774
- } else {
775
- // It's a DrawAnnotation (text)
776
- const nextTexts = [
777
- ...textAnnotationsRef.current,
778
- top as DrawAnnotation,
779
- ];
780
- textAnnotationsRef.current = nextTexts;
781
- setTextAnnotations(nextTexts);
782
- }
783
- return remaining;
784
- });
785
- };
786
-
787
- const clear = () => {
788
- if (sending) return;
789
- if (strokes.length === 0 && textAnnotations.length === 0) return;
790
- const prevStrokes = strokesRef.current;
791
- const prevTexts = textAnnotationsRef.current;
792
- const prevRedo = redoStack;
793
- strokesRef.current = [];
794
- textAnnotationsRef.current = [];
795
- setStrokes([]);
796
- setTextAnnotations([]);
797
- setRedoStack([]);
798
- toast(t("visualEditor.clearedAllAnnotations"), {
799
- action: {
800
- label: t("visualEditor.undo"),
801
- onClick: () => {
802
- // Merge snapshot with any strokes/texts drawn during the toast window
803
- // so new work is not discarded; also restore the pre-clear redo stack.
804
- const restoredStrokes = [...prevStrokes, ...strokesRef.current];
805
- const restoredTexts = [...prevTexts, ...textAnnotationsRef.current];
806
- strokesRef.current = restoredStrokes;
807
- textAnnotationsRef.current = restoredTexts;
808
- setStrokes(restoredStrokes);
809
- setTextAnnotations(restoredTexts);
810
- setRedoStack(prevRedo);
811
- },
812
- },
813
- duration: 6000,
814
- });
815
- };
816
-
817
- const send = () => {
818
- if (sending) return;
819
- const canvas = canvasRef.current;
820
- if (!canvas) return;
821
- // A focused text label normally commits on blur before the Send click, but
822
- // keyboard/programmatic activation does not guarantee that ordering.
823
- commitTextAnnotation();
824
- const rect = canvas.getBoundingClientRect();
825
- // Layout-space dimensions = visual rect / scale factor.
826
- const layoutW = rect.width / scale;
827
- const layoutH = rect.height / scale;
828
-
829
- const pathAnnotations: DrawAnnotation[] = strokesRef.current.map((s) => ({
830
- id: s.id,
831
- type: "path",
832
- // Convert fractional points to layout-space absolute pixels for the agent.
833
- pathData: s.points
834
- .map(
835
- (p, i) =>
836
- `${i === 0 ? "M" : "L"}${(p.x * layoutW).toFixed(1)},${(p.y * layoutH).toFixed(1)}`,
837
- )
838
- .join(" "),
839
- color: s.color,
840
- lineWidth: s.lineWidth,
841
- position: { x: 0, y: 0 },
842
- }));
843
-
844
- // Convert fractional text positions to layout-space absolute pixels.
845
- const layoutTextAnnotations: DrawAnnotation[] =
846
- textAnnotationsRef.current.map((a) => ({
847
- ...a,
848
- position: {
849
- x: a.position.x * layoutW,
850
- y: a.position.y * layoutH,
851
- },
852
- }));
853
-
854
- const all = [...pathAnnotations, ...layoutTextAnnotations];
855
- if (all.length === 0 && !instruction.trim() && queuedAnnotationCount === 0)
856
- return;
857
-
858
- onSend(all, instruction.trim(), {
859
- width: layoutW,
860
- height: layoutH,
861
- });
862
- };
863
-
864
- if (!visible && !retainSurfaceWhenHidden) return null;
865
-
866
- const hasContent =
867
- strokes.length > 0 ||
868
- textAnnotations.length > 0 ||
869
- !!textInput?.value.trim() ||
870
- instruction.trim() ||
871
- queuedAnnotationCount > 0;
872
-
873
- const canUndo = strokes.length > 0 || textAnnotations.length > 0;
874
- const canRedo = redoStack.length > 0;
875
-
876
- const toolbar = (
877
- <div
878
- data-draw-toolbar
879
- className="pointer-events-auto fixed bottom-20 left-1/2 z-[110] flex max-w-[calc(100vw-1rem)] -translate-x-1/2 flex-wrap items-center justify-center gap-2 rounded-xl border border-border bg-popover px-3 py-2 shadow-2xl"
880
- >
881
- {/* Color picker */}
882
- <div className="flex gap-1">
883
- {PRESET_COLORS.map((preset) => (
884
- <Tooltip key={preset.color}>
885
- <TooltipTrigger asChild>
886
- <button
887
- type="button"
888
- aria-label={preset.label}
889
- data-testid={`draw-color-${preset.label.toLowerCase()}`}
890
- onClick={() => {
891
- if (!sending) setColor(preset.color);
892
- }}
893
- disabled={sending}
894
- className={cn(
895
- "h-5 w-5 cursor-pointer rounded-full",
896
- color === preset.color
897
- ? "ring-2 ring-foreground ring-offset-1 ring-offset-popover"
898
- : "ring-1 ring-border",
899
- )}
900
- style={{ backgroundColor: preset.color }}
901
- />
902
- </TooltipTrigger>
903
- <TooltipContent>{preset.label}</TooltipContent>
904
- </Tooltip>
905
- ))}
906
- </div>
907
-
908
- <div className="mx-1 h-4 w-px bg-border" />
909
-
910
- {/* Line widths */}
911
- <div className="flex gap-1">
912
- {LINE_WIDTHS.map((lw) => (
913
- <Tooltip key={lw.value}>
914
- <TooltipTrigger asChild>
915
- <button
916
- type="button"
917
- aria-label={lw.label}
918
- data-testid={`draw-line-width-${lw.label.toLowerCase()}`}
919
- onClick={() => {
920
- if (!sending) setLineWidth(lw.value);
921
- }}
922
- disabled={sending}
923
- className={cn(
924
- "flex h-6 w-6 cursor-pointer items-center justify-center rounded",
925
- lineWidth === lw.value
926
- ? "bg-accent text-foreground"
927
- : "text-muted-foreground hover:text-foreground",
928
- )}
929
- >
930
- <div
931
- className="rounded-full bg-current"
932
- style={{ width: lw.value + 2, height: lw.value + 2 }}
933
- />
934
- </button>
935
- </TooltipTrigger>
936
- <TooltipContent>{lw.label}</TooltipContent>
937
- </Tooltip>
938
- ))}
939
- </div>
940
-
941
- <div className="mx-1 h-4 w-px bg-border" />
942
-
943
- {/* Text mode */}
944
- <Tooltip>
945
- <TooltipTrigger asChild>
946
- <button
947
- type="button"
948
- aria-label={t("visualEditor.typeAnywhereOnCanvas")}
949
- data-testid="draw-text-mode"
950
- onClick={() => {
951
- if (!sending) setTextMode(!textMode);
952
- }}
953
- disabled={sending}
954
- className={cn(
955
- "flex h-6 w-6 cursor-pointer items-center justify-center rounded",
956
- textMode
957
- ? "bg-accent text-foreground"
958
- : "text-muted-foreground hover:text-foreground",
959
- )}
960
- >
961
- <IconCursorText className="h-3.5 w-3.5" />
962
- </button>
963
- </TooltipTrigger>
964
- <TooltipContent>
965
- {t("visualEditor.typeAnywhereOnCanvas")}
966
- </TooltipContent>
967
- </Tooltip>
968
-
969
- {/* Undo last annotation (stroke or text) */}
970
- <Tooltip>
971
- <TooltipTrigger asChild>
972
- <button
973
- type="button"
974
- aria-label={t("visualEditor.undoStroke")}
975
- data-testid="draw-undo"
976
- onClick={undo}
977
- disabled={!canUndo || sending}
978
- className="flex h-6 w-6 cursor-pointer items-center justify-center rounded text-muted-foreground hover:text-foreground disabled:cursor-default disabled:opacity-30"
979
- >
980
- <IconArrowBackUp className="h-3.5 w-3.5" />
981
- </button>
982
- </TooltipTrigger>
983
- <TooltipContent>{t("visualEditor.undoStroke")}</TooltipContent>
984
- </Tooltip>
985
-
986
- {/* Redo */}
987
- <Tooltip>
988
- <TooltipTrigger asChild>
989
- <button
990
- type="button"
991
- aria-label={t("visualEditor.redoStroke")}
992
- data-testid="draw-redo"
993
- onClick={redo}
994
- disabled={!canRedo || sending}
995
- className="flex h-6 w-6 cursor-pointer items-center justify-center rounded text-muted-foreground hover:text-foreground disabled:cursor-default disabled:opacity-30"
996
- >
997
- <IconArrowForwardUp className="h-3.5 w-3.5" />
998
- </button>
999
- </TooltipTrigger>
1000
- <TooltipContent>{t("visualEditor.redoStroke")}</TooltipContent>
1001
- </Tooltip>
1002
-
1003
- {/* Clear all */}
1004
- <Tooltip>
1005
- <TooltipTrigger asChild>
1006
- <button
1007
- type="button"
1008
- aria-label={t("visualEditor.clearAll")}
1009
- data-testid="draw-clear-all"
1010
- onClick={clear}
1011
- disabled={
1012
- sending || (strokes.length === 0 && textAnnotations.length === 0)
1013
- }
1014
- className="flex h-6 w-6 cursor-pointer items-center justify-center rounded text-muted-foreground hover:text-foreground disabled:cursor-default disabled:opacity-30"
1015
- >
1016
- <IconEraser className="h-3.5 w-3.5" />
1017
- </button>
1018
- </TooltipTrigger>
1019
- <TooltipContent>{t("visualEditor.clearAll")}</TooltipContent>
1020
- </Tooltip>
1021
-
1022
- <div className="mx-1 h-4 w-px bg-border" />
1023
-
1024
- {/* Instruction input */}
1025
- <Input
1026
- value={instruction}
1027
- onChange={(e) => {
1028
- if (sending) return;
1029
- instructionRef.current = e.target.value;
1030
- setInstruction(e.target.value);
1031
- }}
1032
- onKeyDown={(e) => {
1033
- if (e.key === "Enter" && hasContent && !sending) send();
1034
- if (e.key === "Escape") onClose();
1035
- }}
1036
- placeholder={t("visualEditor.tellAgentWhatToDo")}
1037
- disabled={sending}
1038
- className="h-7 w-56 border-border bg-background text-xs"
1039
- />
1040
-
1041
- {/* Send */}
1042
- <Button
1043
- size="sm"
1044
- data-testid="draw-send"
1045
- className="h-7 gap-1 px-3 !text-[11px] cursor-pointer"
1046
- onClick={send}
1047
- disabled={!hasContent || sending}
1048
- >
1049
- {sending ? (
1050
- <IconLoader2 className="h-3 w-3 animate-spin" />
1051
- ) : (
1052
- <IconSend className="h-3 w-3" />
1053
- )}
1054
- {sending ? t("visualEditor.sendingDrawing") : t("visualEditor.send")}
1055
- </Button>
1056
-
1057
- {/* Close */}
1058
- <Tooltip>
1059
- <TooltipTrigger asChild>
1060
- <button
1061
- type="button"
1062
- aria-label={t("visualEditor.exitDrawMode")}
1063
- data-testid="draw-exit"
1064
- onClick={onClose}
1065
- disabled={sending}
1066
- className="flex h-6 w-6 cursor-pointer items-center justify-center rounded text-muted-foreground hover:text-foreground"
1067
- >
1068
- <IconX className="h-3.5 w-3.5" />
1069
- </button>
1070
- </TooltipTrigger>
1071
- <TooltipContent>{t("visualEditor.exitDrawMode")}</TooltipContent>
1072
- </Tooltip>
1073
- </div>
1074
- );
1075
-
1076
- return (
1077
- <div
1078
- ref={containerRef}
1079
- data-draw-overlay
1080
- aria-hidden={!visible}
1081
- className={cn(
1082
- "absolute inset-0 z-[100]",
1083
- visible ? "visible" : "invisible pointer-events-none",
1084
- canvasInteractive && !sending
1085
- ? "pointer-events-auto"
1086
- : "pointer-events-none",
1087
- )}
1088
- >
1089
- {/* Drawing canvas */}
1090
- <canvas
1091
- ref={canvasRef}
1092
- data-draw-canvas
1093
- className={cn(
1094
- "absolute inset-0 h-full w-full touch-none",
1095
- textMode ? "cursor-text" : "cursor-crosshair",
1096
- (!canvasInteractive || sending) && "pointer-events-none",
1097
- )}
1098
- onPointerDown={handlePointerDown}
1099
- onPointerMove={handlePointerMove}
1100
- onPointerUp={handlePointerUp}
1101
- onPointerCancel={handlePointerCancel}
1102
- onLostPointerCapture={handleLostPointerCapture}
1103
- />
1104
-
1105
- {/* Rendered text annotations.
1106
- Positions are stored as fractions (0..1) of the visual rect. Convert
1107
- to layout-space pixels (fraction * visualDim / scale) so the label
1108
- sits at the right layout position inside the scaled wrapper, where
1109
- CSS left/top are interpreted in pre-scale coordinates. */}
1110
- {textAnnotations.map((ann) => {
1111
- const canvas = canvasRef.current;
1112
- const rect = canvas?.getBoundingClientRect();
1113
- const layoutX = rect
1114
- ? (ann.position.x * rect.width) / scale
1115
- : ann.position.x;
1116
- const layoutY = rect
1117
- ? (ann.position.y * rect.height) / scale
1118
- : ann.position.y;
1119
- return (
1120
- <div
1121
- key={ann.id}
1122
- className="absolute pointer-events-none select-none whitespace-nowrap font-semibold"
1123
- style={{
1124
- left: layoutX,
1125
- top: layoutY,
1126
- color: ann.color,
1127
- fontSize: 14 + ann.lineWidth,
1128
- }}
1129
- >
1130
- {ann.text}
1131
- </div>
1132
- );
1133
- })}
1134
-
1135
- {/* Pending text input — positioned at the click point in layout space. */}
1136
- {textInput &&
1137
- (() => {
1138
- const canvas = canvasRef.current;
1139
- const rect = canvas?.getBoundingClientRect();
1140
- const layoutX = rect
1141
- ? (textInput.xFrac * rect.width) / scale
1142
- : textInput.xFrac;
1143
- const layoutY = rect
1144
- ? (textInput.yFrac * rect.height) / scale
1145
- : textInput.yFrac;
1146
- return (
1147
- <div
1148
- className="pointer-events-auto absolute z-40"
1149
- style={{ left: layoutX, top: layoutY }}
1150
- >
1151
- <Input
1152
- ref={textInputRef}
1153
- value={textInput.value}
1154
- onChange={(e) =>
1155
- !sending &&
1156
- setPendingTextInput((prev) =>
1157
- prev ? { ...prev, value: e.target.value } : null,
1158
- )
1159
- }
1160
- onBlur={() => {
1161
- // Escape unmounts the input, which fires this blur — skip
1162
- // the commit for a cancelled annotation.
1163
- if (cancelingTextRef.current) {
1164
- cancelingTextRef.current = false;
1165
- return;
1166
- }
1167
- // Pass this render's generation so a trailing blur that
1168
- // arrives after pointerdown has already opened a *newer*
1169
- // text box (see PendingTextInput.generation) is a no-op
1170
- // instead of wiping the box the user just started typing.
1171
- commitTextAnnotation(textInput.generation);
1172
- }}
1173
- onKeyDown={(e) => {
1174
- if (e.key === "Enter") {
1175
- e.preventDefault();
1176
- // Committing unmounts the focused input, which can emit a
1177
- // blur event in the same turn. Skip that second commit.
1178
- cancelingTextRef.current = true;
1179
- commitTextAnnotation(textInput.generation);
1180
- }
1181
- if (e.key === "Escape") {
1182
- cancelingTextRef.current = true;
1183
- setPendingTextInput(null);
1184
- }
1185
- }}
1186
- className="h-7 w-48 border-primary bg-background text-sm"
1187
- disabled={sending}
1188
- autoFocus
1189
- placeholder={t("visualEditor.typeAnnotationFancy")}
1190
- />
1191
- </div>
1192
- );
1193
- })()}
1194
-
1195
- {visible ? <DrawToolbarPortal>{toolbar}</DrawToolbarPortal> : null}
1196
- </div>
1197
- );
1198
- }
1199
-
1200
- function DrawToolbarPortal({ children }: { children: ReactNode }) {
1201
- if (typeof document === "undefined") return <>{children}</>;
1202
- return createPortal(children, document.body);
1203
- }
9
+ export type DrawOverlayProps = Omit<ToolkitDrawOverlayProps, "translate">;
1204
10
 
1205
- function drawStroke(
1206
- ctx: CanvasRenderingContext2D,
1207
- points: Point[],
1208
- color: string,
1209
- lineWidth: number,
1210
- ) {
1211
- if (points.length < 2) return;
1212
- ctx.beginPath();
1213
- ctx.strokeStyle = color;
1214
- ctx.lineWidth = lineWidth;
1215
- ctx.lineCap = "round";
1216
- ctx.lineJoin = "round";
1217
- ctx.moveTo(points[0].x, points[0].y);
1218
- for (let i = 1; i < points.length; i++) {
1219
- ctx.lineTo(points[i].x, points[i].y);
1220
- }
1221
- ctx.stroke();
11
+ export function DrawOverlay(props: DrawOverlayProps) {
12
+ const translate = useT();
13
+ return <ToolkitDrawOverlay {...props} translate={translate} />;
1222
14
  }