@agent-native/core 0.131.5 → 0.131.7
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.
- package/corpus/README.md +3 -3
- package/corpus/core/CHANGELOG.md +70 -0
- package/corpus/core/docs/content/agent-native-toolkit.mdx +13 -6
- package/corpus/core/docs/content/cloneable-saas.mdx +5 -5
- package/corpus/core/docs/content/creating-templates.mdx +9 -0
- package/corpus/core/docs/content/deployment.mdx +18 -12
- package/corpus/core/docs/content/doctor.mdx +8 -1
- package/corpus/core/docs/content/drop-in-agent.mdx +1 -1
- package/corpus/core/docs/content/extensions.mdx +18 -18
- package/corpus/core/docs/content/faq.mdx +4 -4
- package/corpus/core/docs/content/key-concepts.mdx +30 -21
- package/corpus/core/docs/content/security.mdx +2 -2
- package/corpus/core/docs/content/template-brain-developers.mdx +4 -1
- package/corpus/core/docs/content/template-mail-developers.mdx +4 -1
- package/corpus/core/docs/content/what-is-agent-native.mdx +6 -6
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +449 -66
- package/corpus/core/src/a2a/correlation.ts +31 -0
- package/corpus/core/src/a2a/invoke.ts +4 -2
- package/corpus/core/src/a2a/types.ts +4 -0
- package/corpus/core/src/action.ts +3 -0
- package/corpus/core/src/agent/action-continuation-guidance.ts +2 -1
- package/corpus/core/src/agent/engine/registry.ts +98 -6
- package/corpus/core/src/agent/production-agent.ts +151 -18
- package/corpus/core/src/agent/run-loop-with-resume.ts +190 -3
- package/corpus/core/src/agent/run-manager.ts +117 -41
- package/corpus/core/src/agent/thread-debug-history.ts +86 -0
- package/corpus/core/src/agent/types.ts +3 -1
- package/corpus/core/src/cli/create.ts +11 -1
- package/corpus/core/src/cli/templates-meta.ts +2 -2
- package/corpus/core/src/client/agent-chat-adapter.ts +91 -13
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +11 -9
- package/corpus/core/src/client/sse-event-processor.ts +17 -3
- package/corpus/core/src/db/client.ts +57 -17
- package/corpus/core/src/guards/no-env-credentials.ts +2 -0
- package/corpus/core/src/integrations/adapters/slack.ts +8 -3
- package/corpus/core/src/localization/default-messages.ts +44 -0
- package/corpus/core/src/observability/traces.ts +78 -5
- package/corpus/core/src/scripts/call-agent.ts +294 -11
- package/corpus/core/src/scripts/describe-workspace-apps.ts +2 -2
- package/corpus/core/src/secrets/crypto.ts +156 -35
- package/corpus/core/src/secrets/storage.ts +64 -26
- package/corpus/core/src/server/agent-capabilities.ts +1 -1
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +113 -7
- package/corpus/core/src/server/agent-chat/context-tools.ts +2 -0
- package/corpus/core/src/server/agent-chat/plugin-options.ts +34 -1
- package/corpus/core/src/server/agent-chat/prompt-resources.ts +1 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +91 -59
- package/corpus/core/src/templates/default/.agents/skills/secrets/SKILL.md +19 -7
- package/corpus/core/src/templates/headless/.agents/skills/secrets/SKILL.md +19 -7
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +18 -7
- package/corpus/core/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +15 -14
- package/corpus/core/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +19 -7
- package/corpus/core/src/vite/client.ts +1 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-ops/SKILL.md +5 -4
- package/corpus/templates/analytics/.agents/skills/gong/SKILL.md +6 -4
- package/corpus/templates/analytics/AGENTS.md +7 -4
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +4 -3
- package/corpus/templates/analytics/changelog/2026-07-30-delegated-analytics-questions-now-return-a-useful-current-me.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-30-scheduled-dashboard-emails-complete-every-panel-without-pool.md +6 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +28 -1
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +18 -11
- package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +4 -4
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -5
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +6 -2
- package/corpus/templates/analytics/server/lib/dashboard-panel-source-resolver.ts +11 -4
- package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +49 -31
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +16 -8
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +9 -3
- package/corpus/templates/analytics/server/lib/production-serverless-runtime.ts +11 -0
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +10 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +3 -2
- package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +2 -1
- package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +2 -1
- package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +1 -12
- package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +54 -6
- package/corpus/templates/brain/.agents/skills/brain/SKILL.md +20 -9
- package/corpus/templates/brain/.agents/skills/ingestion-and-connectors/SKILL.md +33 -1
- package/corpus/templates/brain/AGENTS.md +3 -1
- package/corpus/templates/brain/README.md +4 -1
- package/corpus/templates/brain/actions/_schemas.ts +30 -8
- package/corpus/templates/brain/actions/ask-brain.ts +124 -14
- package/corpus/templates/brain/actions/create-source.ts +18 -2
- package/corpus/templates/brain/actions/enqueue-captures-distillation.ts +8 -123
- package/corpus/templates/brain/actions/enqueue-distillation.ts +6 -127
- package/corpus/templates/brain/actions/import-capture.ts +17 -5
- package/corpus/templates/brain/actions/import-transcript.ts +17 -5
- package/corpus/templates/brain/actions/update-source.ts +25 -5
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/brain/changelog/2026-07-30-blessed-resources-can-feed-cited-company-answers.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-30-the-left-sidebar-no-longer-shows-a-blank-organization-placeh.md +6 -0
- package/corpus/templates/brain/server/lib/brain.ts +49 -19
- package/corpus/templates/brain/server/lib/distillation-queue.ts +147 -0
- package/corpus/templates/brain/server/lib/source-policy.ts +264 -0
- package/corpus/templates/brain/server/plugins/agent-chat.ts +3 -2
- package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +116 -19
- package/corpus/templates/clips/app/components/meetings/google-oauth-identity-notice.tsx +51 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +3 -1
- package/corpus/templates/clips/changelog/2026-07-30-google-calendar-warning.md +6 -0
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +72 -8
- package/corpus/templates/content/changelog/2026-07-30-the-slash-command-hint-now-stays-on-only-the-active-empty-li.md +6 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +506 -11
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +148 -17
- package/corpus/templates/design/app/components/design/canvas-interactions/design-canvas-interactions.ts +357 -0
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +7 -466
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +15 -311
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +19 -997
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +8 -1216
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +31 -38
- package/corpus/templates/design/package.json +0 -1
- package/corpus/templates/dispatch/AGENTS.md +8 -0
- package/corpus/templates/dispatch/DEVELOPING.md +29 -0
- package/corpus/templates/dispatch/actions/view-screen.ts +34 -2
- package/corpus/templates/dispatch/app/i18n-data.ts +457 -0
- package/corpus/templates/dispatch/changelog/2026-07-30-delegated-dispatch-work-continues-reliably-in-the-background.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-30-find-failed-runs-across-apps.md +6 -0
- package/corpus/templates/dispatch/netlify.toml +3 -0
- package/corpus/templates/plan/actions/get-visual-plan.ts +10 -6
- package/corpus/templates/plan/actions/patch-visual-plan-source.ts +117 -1
- package/corpus/templates/plan/actions/update-visual-plan.ts +56 -14
- package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -1
- package/corpus/templates/plan/changelog/2026-07-30-plan-agents-recover-with-targeted-writes.md +6 -0
- package/corpus/templates/plan/shared/plan-content.ts +35 -0
- package/corpus/templates/slides/.agents/skills/analytics-data-for-decks/SKILL.md +8 -12
- package/corpus/templates/slides/AGENTS.md +4 -4
- package/corpus/templates/slides/actions/create-deck.ts +1 -1
- package/corpus/templates/slides/actions/duplicate-deck.ts +19 -4
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -10
- package/corpus/templates/slides/app/components/deck/MermaidRenderer.tsx +12 -1
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +30 -13
- package/corpus/templates/slides/app/components/editor/DeckEditorSkeleton.tsx +35 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +63 -56
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +21 -76
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1110 -292
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +53 -0
- package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +235 -18
- package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +4 -1
- package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +11 -0
- package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +129 -0
- package/corpus/templates/slides/app/components/editor/selection-overlay-measurement.ts +70 -0
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +519 -65
- package/corpus/templates/slides/app/components/visual-editor/CanvasCommentPins.tsx +36 -426
- package/corpus/templates/slides/app/components/visual-editor/DrawOverlay.tsx +19 -458
- package/corpus/templates/slides/app/context/DeckContext.tsx +179 -72
- package/corpus/templates/slides/app/global.css +105 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +8 -0
- package/corpus/templates/slides/app/lib/deck-editor-loading.ts +26 -0
- package/corpus/templates/slides/app/lib/mermaid-blocks.ts +27 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +71 -38
- package/corpus/templates/slides/app/pages/Index.tsx +98 -41
- package/corpus/templates/slides/changelog/2026-07-30-deck-loading-no-longer-flashes-an-unavailable-message.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-duplicating-a-deck-from-the-deck-list-now-opens-the-copy-imm.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-layout-overflow-warning-is-readable-and-dismissible.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-new-decks-now-start-with-an-empty-slide-list-when-no-slides-.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-shared-canvas-annotations.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-slide-objects-can-be-moved-as-a-group-copied-and-pasted-with.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-slide-text-editing-and-object-controls-now-match-google-slides.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-style-panel-now-scrolls-with-speaker-notes-open-shows-the-sl.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-top-toolbar-controls-now-use-consistent-sizing-with-undo-and.md +6 -0
- package/corpus/templates/slides/netlify.toml +3 -1
- package/corpus/templates/slides/package.json +0 -1
- package/corpus/templates/slides/server/lib/chat-attachments.ts +18 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -0
- package/corpus/templates/slides/vite.config.ts +0 -1
- package/corpus/toolkit/CHANGELOG.md +13 -0
- package/corpus/toolkit/README.md +19 -0
- package/corpus/toolkit/agent-native.eject.json +38 -0
- package/corpus/toolkit/package.json +29 -1
- package/corpus/toolkit/src/canvas-annotations/CanvasCommentPins.tsx +861 -0
- package/corpus/toolkit/src/canvas-annotations/DrawOverlay.tsx +1233 -0
- package/corpus/toolkit/src/canvas-annotations/index.ts +15 -0
- package/corpus/toolkit/src/canvas-annotations/types.ts +14 -0
- package/corpus/toolkit/src/canvas-interactions/canvas-interactions.ts +933 -0
- package/corpus/toolkit/src/canvas-interactions/index.ts +67 -0
- package/corpus/toolkit/src/design-tweaks/scrub-input-utils.ts +311 -0
- package/corpus/toolkit/src/design-tweaks/scrub-input.tsx +491 -0
- package/corpus/toolkit/src/design-tweaks/visual-style-controls.tsx +26 -171
- package/corpus/toolkit/src/index.ts +1 -0
- package/dist/a2a/client.d.ts +23 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +309 -44
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/correlation.d.ts +1 -0
- package/dist/a2a/correlation.d.ts.map +1 -1
- package/dist/a2a/correlation.js +27 -0
- package/dist/a2a/correlation.js.map +1 -1
- package/dist/a2a/invoke.d.ts.map +1 -1
- package/dist/a2a/invoke.js +4 -2
- package/dist/a2a/invoke.js.map +1 -1
- package/dist/a2a/types.d.ts +4 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/action.d.ts +3 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/action-continuation-guidance.d.ts.map +1 -1
- package/dist/agent/action-continuation-guidance.js +2 -1
- package/dist/agent/action-continuation-guidance.js.map +1 -1
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +82 -8
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/agent/production-agent.d.ts +40 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +113 -15
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +21 -0
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +163 -4
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/agent/run-manager.d.ts +16 -9
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +93 -42
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/thread-debug-history.d.ts +10 -0
- package/dist/agent/thread-debug-history.d.ts.map +1 -0
- package/dist/agent/thread-debug-history.js +68 -0
- package/dist/agent/thread-debug-history.js.map +1 -0
- package/dist/agent/types.d.ts +3 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +6 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/templates-meta.js +2 -2
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +76 -13
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +1 -1
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +12 -3
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +48 -17
- package/dist/db/client.js.map +1 -1
- package/dist/guards/no-env-credentials.d.ts.map +1 -1
- package/dist/guards/no-env-credentials.js +2 -0
- package/dist/guards/no-env-credentials.js.map +1 -1
- package/dist/integrations/adapters/slack.js +7 -3
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/localization/default-messages.d.ts +43 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +43 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/observability/traces.d.ts +3 -1
- package/dist/observability/traces.d.ts.map +1 -1
- package/dist/observability/traces.js +73 -4
- package/dist/observability/traces.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +237 -12
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/scripts/describe-workspace-apps.js +2 -2
- package/dist/scripts/describe-workspace-apps.js.map +1 -1
- package/dist/secrets/crypto.d.ts +36 -21
- package/dist/secrets/crypto.d.ts.map +1 -1
- package/dist/secrets/crypto.js +134 -33
- package/dist/secrets/crypto.js.map +1 -1
- package/dist/secrets/storage.d.ts +6 -6
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +43 -26
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/agent-capabilities.js +1 -1
- package/dist/server/agent-capabilities.js.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.d.ts +17 -3
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +94 -3
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-chat/context-tools.d.ts.map +1 -1
- package/dist/server/agent-chat/context-tools.js +2 -0
- package/dist/server/agent-chat/context-tools.js.map +1 -1
- package/dist/server/agent-chat/plugin-options.d.ts +23 -0
- package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
- package/dist/server/agent-chat/plugin-options.js +4 -1
- package/dist/server/agent-chat/plugin-options.js.map +1 -1
- package/dist/server/agent-chat/prompt-resources.js +1 -1
- package/dist/server/agent-chat/prompt-resources.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +2 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +79 -56
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/templates/default/.agents/skills/secrets/SKILL.md +19 -7
- package/dist/templates/headless/.agents/skills/secrets/SKILL.md +19 -7
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +18 -7
- package/dist/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +15 -14
- package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +19 -7
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +1 -0
- package/dist/vite/client.js.map +1 -1
- package/docs/content/agent-native-toolkit.mdx +13 -6
- package/docs/content/cloneable-saas.mdx +5 -5
- package/docs/content/creating-templates.mdx +9 -0
- package/docs/content/deployment.mdx +18 -12
- package/docs/content/doctor.mdx +8 -1
- package/docs/content/drop-in-agent.mdx +1 -1
- package/docs/content/extensions.mdx +18 -18
- package/docs/content/faq.mdx +4 -4
- package/docs/content/key-concepts.mdx +30 -21
- package/docs/content/security.mdx +2 -2
- package/docs/content/template-brain-developers.mdx +4 -1
- package/docs/content/template-mail-developers.mdx +4 -1
- package/docs/content/what-is-agent-native.mdx +6 -6
- package/package.json +3 -3
- package/src/a2a/client.ts +449 -66
- package/src/a2a/correlation.ts +31 -0
- package/src/a2a/invoke.ts +4 -2
- package/src/a2a/types.ts +4 -0
- package/src/action.ts +3 -0
- package/src/agent/action-continuation-guidance.ts +2 -1
- package/src/agent/engine/registry.ts +98 -6
- package/src/agent/production-agent.ts +151 -18
- package/src/agent/run-loop-with-resume.ts +190 -3
- package/src/agent/run-manager.ts +117 -41
- package/src/agent/thread-debug-history.ts +86 -0
- package/src/agent/types.ts +3 -1
- package/src/cli/create.ts +11 -1
- package/src/cli/templates-meta.ts +2 -2
- package/src/client/agent-chat-adapter.ts +91 -13
- package/src/client/resources/McpIntegrationDialog.tsx +11 -9
- package/src/client/sse-event-processor.ts +17 -3
- package/src/db/client.ts +57 -17
- package/src/guards/no-env-credentials.ts +2 -0
- package/src/integrations/adapters/slack.ts +8 -3
- package/src/localization/default-messages.ts +44 -0
- package/src/observability/traces.ts +78 -5
- package/src/scripts/call-agent.ts +294 -11
- package/src/scripts/describe-workspace-apps.ts +2 -2
- package/src/secrets/crypto.ts +156 -35
- package/src/secrets/storage.ts +64 -26
- package/src/server/agent-capabilities.ts +1 -1
- package/src/server/agent-chat/action-filters-a2a.ts +113 -7
- package/src/server/agent-chat/context-tools.ts +2 -0
- package/src/server/agent-chat/plugin-options.ts +34 -1
- package/src/server/agent-chat/prompt-resources.ts +1 -1
- package/src/server/agent-chat-plugin.ts +91 -59
- package/src/templates/default/.agents/skills/secrets/SKILL.md +19 -7
- package/src/templates/headless/.agents/skills/secrets/SKILL.md +19 -7
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +18 -7
- package/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +15 -14
- package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +19 -7
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
}
|