@eventcatalog/core 5.0.0-beta.3 → 5.0.0-beta.30
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/LICENSE +234 -134
- package/README.md +5 -5
- package/dist/analytics/analytics.cjs +18 -3
- package/dist/analytics/analytics.d.cts +1 -1
- package/dist/analytics/analytics.d.ts +1 -1
- package/dist/analytics/analytics.js +3 -2
- package/dist/analytics/log-build.cjs +160 -77
- package/dist/analytics/log-build.d.cts +2 -2
- package/dist/analytics/log-build.d.ts +2 -2
- package/dist/analytics/log-build.js +7 -5
- package/dist/analytics/studio-usage.cjs +90 -0
- package/dist/analytics/studio-usage.d.cts +5 -0
- package/dist/analytics/studio-usage.d.ts +5 -0
- package/dist/analytics/studio-usage.js +10 -0
- package/dist/analytics/telemetry.cjs +42 -0
- package/dist/analytics/telemetry.d.cts +20 -0
- package/dist/analytics/telemetry.d.ts +20 -0
- package/dist/analytics/telemetry.js +8 -0
- package/dist/catalog-runtime.cjs +1 -1
- package/dist/{chunk-RFIXKATC.js → chunk-4G7FSRPN.js} +35 -42
- package/dist/{chunk-6OC46WQJ.js → chunk-55QETQZ3.js} +66 -35
- package/dist/{chunk-TQLRG35T.js → chunk-AD7CL772.js} +78 -45
- package/dist/chunk-CA6WCQ2T.js +54 -0
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-423S2KHN.js → chunk-FLAZ2JVN.js} +4 -2
- package/dist/{chunk-BH2F5WPN.js → chunk-GQI5UYXZ.js} +1 -1
- package/dist/{chunk-GYIVJXOJ.js → chunk-NA7OTBWW.js} +1 -1
- package/dist/{chunk-MDWB5HLO.js → chunk-NKNOXQ62.js} +1 -1
- package/dist/{chunk-3PKS2IIP.js → chunk-OX6TSVHY.js} +1 -1
- package/dist/{chunk-36RYTYT5.js → chunk-PD7KTSYN.js} +1 -1
- package/dist/chunk-S5GWAUG5.js +29 -0
- package/dist/{chunk-NDJOAITI.js → chunk-T3OROMAU.js} +9 -3
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog-config-file-utils.cjs +34 -41
- package/dist/eventcatalog-config-file-utils.d.cts +56 -58
- package/dist/eventcatalog-config-file-utils.d.ts +56 -58
- package/dist/eventcatalog-config-file-utils.js +1 -1
- package/dist/eventcatalog.cjs +531 -356
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +63 -25
- package/dist/features.cjs +34 -41
- package/dist/features.js +2 -2
- package/dist/federation/federate.cjs +37 -44
- package/dist/federation/federate.js +2 -2
- package/dist/generate.cjs +37 -42
- package/dist/generate.js +4 -4
- package/dist/resolve-catalog-dependencies.cjs +34 -41
- package/dist/resolve-catalog-dependencies.js +2 -2
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
- package/dist/runtime-dependencies/htmlparser2.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +13 -1
- package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
- package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
- package/dist/runtime-dependencies/yjs.mjs +3 -0
- package/dist/utils/child-process-signals.cjs +53 -0
- package/dist/utils/child-process-signals.d.cts +19 -0
- package/dist/utils/child-process-signals.d.ts +19 -0
- package/dist/utils/child-process-signals.js +6 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/dist/utils/license-status.cjs +78 -45
- package/dist/utils/license-status.d.cts +15 -5
- package/dist/utils/license-status.d.ts +15 -5
- package/dist/utils/license-status.js +1 -1
- package/eventcatalog/astro.config.mjs +23 -1
- package/eventcatalog/integrations/config-loader.mjs +56 -58
- package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
- package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
- package/eventcatalog/integrations/storage.mjs +20 -0
- package/eventcatalog/integrations/studio-server.mjs +32 -0
- package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
- package/eventcatalog/src/components/Header.astro +3 -3
- package/eventcatalog/src/components/Homepage/CatalogDashboard.astro +342 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +60 -4
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
- package/eventcatalog/src/components/NavigationProgress.astro +7 -0
- package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
- package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
- package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
- package/eventcatalog/src/components/Search/search-utils.ts +26 -0
- package/eventcatalog/src/components/Seo.astro +5 -1
- package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +53 -48
- package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +183 -178
- package/eventcatalog/src/components/Settings/LicenseSettings.tsx +140 -31
- package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +40 -35
- package/eventcatalog/src/components/Settings/LogoUpload.tsx +1 -1
- package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +9 -8
- package/eventcatalog/src/components/Settings/Row.tsx +7 -4
- package/eventcatalog/src/components/Settings/SettingsShared.tsx +3 -3
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +53 -83
- package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +131 -0
- package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
- package/eventcatalog/src/components/TrialBadge.astro +20 -9
- package/eventcatalog/src/content.config.ts +19 -2
- package/eventcatalog/src/features/auth/error.astro +4 -2
- package/eventcatalog/src/features/auth/login.astro +2 -1
- package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
- package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
- package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +24 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +278 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +272 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +65 -65
- package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
- package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
- package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
- package/eventcatalog/src/features/server/start.ts +25 -0
- package/eventcatalog/src/features/storage/database.ts +88 -0
- package/eventcatalog/src/features/storage/migrations.ts +85 -0
- package/eventcatalog/src/features/storage/sql/database.sql +5 -0
- package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
- package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
- package/eventcatalog/src/features/storage/sql.ts +28 -0
- package/eventcatalog/src/features/studio/agent-choreography.ts +384 -0
- package/eventcatalog/src/features/studio/agents.ts +28 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
- package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
- package/eventcatalog/src/features/studio/api/client.ts +40 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +540 -0
- package/eventcatalog/src/features/studio/canvas-preview.ts +52 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
- package/eventcatalog/src/features/studio/catalog.ts +173 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +518 -0
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +118 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +434 -0
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +143 -0
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
- package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +109 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +206 -0
- package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
- package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +293 -0
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +396 -0
- package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2091 -0
- package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
- package/eventcatalog/src/features/studio/document-name.ts +7 -0
- package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +63 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +353 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +109 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +782 -0
- package/eventcatalog/src/features/studio/identity.ts +59 -0
- package/eventcatalog/src/features/studio/limits.ts +2 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +69 -0
- package/eventcatalog/src/features/studio/pages/index.astro +784 -0
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +495 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +498 -0
- package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
- package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
- package/eventcatalog/src/features/studio/server/storage.ts +90 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
- package/eventcatalog/src/layouts/SettingsLayout.astro +2 -2
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -5
- package/eventcatalog/src/pages/_index.astro +5 -342
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +2 -3
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -1
- package/eventcatalog/src/pages/diagrams/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/diagrams/[id]/[version]/embed.astro +1 -0
- package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/directory/[type]/_index.data.ts +5 -7
- package/eventcatalog/src/pages/directory/[type]/index.astro +3 -1
- package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -7
- package/eventcatalog/src/pages/discover/[type]/index.astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/_[...filename].data.ts +0 -4
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +3 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +2 -3
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_[filename].data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +2 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +1 -0
- package/eventcatalog/src/pages/docs/custom/[...path]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/custom/[...path]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/services/[id]/[docType]/[docId]/index.astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +1 -0
- package/eventcatalog/src/pages/docs/services/[id]/asyncapi/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/changelog/index.astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/graphql/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/services/[id]/spec/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/teams/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/index.astro +4 -2
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +0 -7
- package/eventcatalog/src/pages/schemas/explorer/index.astro +4 -2
- package/eventcatalog/src/pages/settings/license.astro +9 -1
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/_index.data.ts +38 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +18 -41
- package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -1
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +0 -7
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
- package/eventcatalog/src/utils/collections/util.ts +8 -4
- package/eventcatalog/src/utils/feature.ts +8 -0
- package/eventcatalog/src/utils/git.ts +22 -5
- package/eventcatalog/src/utils/homepage-dashboard.ts +143 -0
- package/eventcatalog/src/utils/license.ts +1 -1
- package/eventcatalog/src/utils/link-validation.ts +25 -19
- package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +4 -2
- package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
- package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
- package/eventcatalog/src/utils/page-loaders/hybrid-page.ts +4 -15
- package/eventcatalog/src/utils/page-title.ts +15 -0
- package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
- package/eventcatalog/src/utils/trial.ts +14 -1
- package/package.json +19 -13
- package/dist/runtime-dependencies/shelljs.mjs +0 -3
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/pages/studio.astro +0 -316
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import type { Edge, Node, XYPosition } from '@xyflow/react';
|
|
2
|
+
import type * as Y from 'yjs';
|
|
3
|
+
import {
|
|
4
|
+
addNodes,
|
|
5
|
+
connectNodes,
|
|
6
|
+
createThread,
|
|
7
|
+
deleteEdges,
|
|
8
|
+
deleteNodes,
|
|
9
|
+
fitContainersAround,
|
|
10
|
+
getCanvasMaps,
|
|
11
|
+
moveNode,
|
|
12
|
+
placeNode,
|
|
13
|
+
setEdgeRoutes,
|
|
14
|
+
setMeta,
|
|
15
|
+
type Author,
|
|
16
|
+
type CommentAnchor,
|
|
17
|
+
getAbsolutePosition,
|
|
18
|
+
sizeOf,
|
|
19
|
+
type LayoutResult,
|
|
20
|
+
getNodeName,
|
|
21
|
+
} from '@eventcatalog/visualiser/studio';
|
|
22
|
+
import {
|
|
23
|
+
describeNode,
|
|
24
|
+
nodeCenter,
|
|
25
|
+
planNodes,
|
|
26
|
+
resolveEdgeRefs,
|
|
27
|
+
topLeftFor,
|
|
28
|
+
updateCanvasNode,
|
|
29
|
+
type CatalogIndex,
|
|
30
|
+
type EdgeSpecs,
|
|
31
|
+
type NodeSpecs,
|
|
32
|
+
type NodeUpdate,
|
|
33
|
+
} from './canvas-actions';
|
|
34
|
+
import { getRelatedEdges } from './catalog';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Agents work on a canvas like a person would: their cursor travels to where they're going, they drop one node,
|
|
38
|
+
* drag a connection from it to the next, and so on, rather than everything appearing at once. Used by the MCP
|
|
39
|
+
* tools (on the server) and the WebMCP tools (in the browser), with a stage that makes the changes and shows
|
|
40
|
+
* the agent's presence there.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** What everyone sees of an agent at work */
|
|
44
|
+
export type AgentPresence = {
|
|
45
|
+
pointer: XYPosition | null;
|
|
46
|
+
selection: string[];
|
|
47
|
+
activity?: string;
|
|
48
|
+
/** Where a connection being dragged starts: drawn from there to the pointer */
|
|
49
|
+
connecting?: XYPosition | null;
|
|
50
|
+
/** Nodes being moved, by id, and where they are (relative to their containers), until they're dropped */
|
|
51
|
+
moving?: Record<string, XYPosition> | null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type AgentStage = {
|
|
55
|
+
/** Change the canvas (and read it) */
|
|
56
|
+
change: <T>(fn: (doc: Y.Doc) => T) => Promise<T> | T;
|
|
57
|
+
/** Show the agent on the canvas */
|
|
58
|
+
present: (presence: AgentPresence) => void;
|
|
59
|
+
/** Where the agent's pointer was left last time, if it's been here */
|
|
60
|
+
lastPointer?: XYPosition | null;
|
|
61
|
+
/**
|
|
62
|
+
* Everyone sees the agent's presence, so nodes it moves are shown moving with it and only saved where they
|
|
63
|
+
* end up, like a person's drag. Otherwise each step of a move is saved, so others see it move.
|
|
64
|
+
*/
|
|
65
|
+
sharesMoves?: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const FRAME_MS = 40;
|
|
69
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
|
+
const easeInOut = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
|
|
71
|
+
const distance = (a: XYPosition, b: XYPosition) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
72
|
+
|
|
73
|
+
type Lookup = Map<string, Node>;
|
|
74
|
+
/** Where a node is on the canvas (nodes in containers are positioned relative to them) */
|
|
75
|
+
const positionOf = (node: Node, nodes: Lookup) => getAbsolutePosition(node, nodes);
|
|
76
|
+
/** Where connections leave a node (its right side) and arrive (its left side) */
|
|
77
|
+
const sourceHandle = (node: Node, nodes: Lookup) => {
|
|
78
|
+
const position = positionOf(node, nodes);
|
|
79
|
+
return { x: position.x + sizeOf(node).width, y: position.y + sizeOf(node).height / 2 };
|
|
80
|
+
};
|
|
81
|
+
const targetHandle = (node: Node, nodes: Lookup) => {
|
|
82
|
+
const position = positionOf(node, nodes);
|
|
83
|
+
return { x: position.x, y: position.y + sizeOf(node).height / 2 };
|
|
84
|
+
};
|
|
85
|
+
const nameOf = (node: Node) => getNodeName(node.type, node.data);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The agent's hand: moves its pointer smoothly (in frames, so it glides for everyone) and shows what it's doing.
|
|
89
|
+
* `pace` slows down or speeds up everything (big changes go faster, so a tool call doesn't take too long).
|
|
90
|
+
*/
|
|
91
|
+
export function createAgentHand(stage: AgentStage, { pace = 1 }: { pace?: number } = {}) {
|
|
92
|
+
const presence: AgentPresence = { pointer: stage.lastPointer ?? null, selection: [] };
|
|
93
|
+
const show = () => stage.present({ ...presence });
|
|
94
|
+
|
|
95
|
+
const hand = {
|
|
96
|
+
get pointer() {
|
|
97
|
+
return presence.pointer;
|
|
98
|
+
},
|
|
99
|
+
async pause(ms: number) {
|
|
100
|
+
await sleep(ms * pace);
|
|
101
|
+
},
|
|
102
|
+
say(activity: string) {
|
|
103
|
+
presence.activity = activity;
|
|
104
|
+
show();
|
|
105
|
+
},
|
|
106
|
+
select(ids: string[]) {
|
|
107
|
+
presence.selection = ids;
|
|
108
|
+
show();
|
|
109
|
+
},
|
|
110
|
+
/**
|
|
111
|
+
* Glide the pointer to a point, e.g. dragging a connection from `connecting`, or carrying a node held at
|
|
112
|
+
* `grab` from its position
|
|
113
|
+
*/
|
|
114
|
+
async moveTo(
|
|
115
|
+
target: XYPosition,
|
|
116
|
+
{ connecting, carry }: { connecting?: XYPosition; carry?: { nodeId: string; grab: XYPosition } } = {}
|
|
117
|
+
) {
|
|
118
|
+
// The first time, come in from a little way off rather than appearing on the spot
|
|
119
|
+
const from = presence.pointer ?? { x: target.x - 160, y: target.y - 120 };
|
|
120
|
+
const duration = Math.min(900, Math.max(250, 200 + distance(from, target) * 0.6)) * pace;
|
|
121
|
+
const frames = Math.max(1, Math.round(duration / FRAME_MS));
|
|
122
|
+
presence.connecting = connecting ?? null;
|
|
123
|
+
for (let frame = 1; frame <= frames; frame++) {
|
|
124
|
+
const t = easeInOut(frame / frames);
|
|
125
|
+
presence.pointer = { x: from.x + (target.x - from.x) * t, y: from.y + (target.y - from.y) * t };
|
|
126
|
+
if (carry)
|
|
127
|
+
presence.moving = {
|
|
128
|
+
[carry.nodeId]: { x: presence.pointer.x - carry.grab.x, y: presence.pointer.y - carry.grab.y },
|
|
129
|
+
};
|
|
130
|
+
show();
|
|
131
|
+
await sleep(FRAME_MS);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
/** Let go of a connection being dragged, or nodes being carried (once where they're dropped is saved) */
|
|
135
|
+
release() {
|
|
136
|
+
presence.connecting = null;
|
|
137
|
+
presence.moving = null;
|
|
138
|
+
show();
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
return hand;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Faster for bigger changes, so a tool call stays well within client timeouts */
|
|
145
|
+
const paceFor = (steps: number) => Math.max(0.25, Math.min(1, 10 / Math.max(steps, 1)));
|
|
146
|
+
|
|
147
|
+
const readNodes = (doc: Y.Doc) => new Map<string, Node>(getCanvasMaps(doc).nodes.entries());
|
|
148
|
+
|
|
149
|
+
/** Drag a connection from one node to another, then make it */
|
|
150
|
+
async function playConnection(
|
|
151
|
+
stage: AgentStage,
|
|
152
|
+
hand: ReturnType<typeof createAgentHand>,
|
|
153
|
+
connect: (doc: Y.Doc) => Edge | { error: string },
|
|
154
|
+
sourceId: string,
|
|
155
|
+
targetId: string
|
|
156
|
+
) {
|
|
157
|
+
const nodes = await stage.change(readNodes);
|
|
158
|
+
const source = nodes.get(sourceId);
|
|
159
|
+
const target = nodes.get(targetId);
|
|
160
|
+
if (!source || !target) return stage.change(connect);
|
|
161
|
+
hand.say(`Connecting ${nameOf(source)} to ${nameOf(target)}`);
|
|
162
|
+
const start = sourceHandle(source, nodes);
|
|
163
|
+
await hand.moveTo(start);
|
|
164
|
+
await hand.pause(120);
|
|
165
|
+
await hand.moveTo(targetHandle(target, nodes), { connecting: start });
|
|
166
|
+
const outcome = await stage.change(connect);
|
|
167
|
+
hand.release();
|
|
168
|
+
await hand.pause(180);
|
|
169
|
+
return outcome;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** addToCanvas, played out: each node dropped in turn, connected (to related resources and as asked) as soon as it can be */
|
|
173
|
+
export async function playAddToCanvas(
|
|
174
|
+
stage: AgentStage,
|
|
175
|
+
{ nodes: specs, edges = [] }: { nodes: NodeSpecs; edges?: EdgeSpecs },
|
|
176
|
+
catalog: CatalogIndex
|
|
177
|
+
) {
|
|
178
|
+
const existing = await stage.change((doc) => [...readNodes(doc).values()]);
|
|
179
|
+
const { planned, errors } = planNodes(existing, specs, catalog, edges);
|
|
180
|
+
const pending = resolveEdgeRefs(edges, planned);
|
|
181
|
+
const hand = createAgentHand(stage, { pace: paceFor(planned.length * 2 + pending.length) });
|
|
182
|
+
const connections: string[] = [];
|
|
183
|
+
|
|
184
|
+
for (const { node: plannedNode, canvasPosition, name, resource } of planned) {
|
|
185
|
+
hand.say(`Adding ${name}`);
|
|
186
|
+
const { width, height } = sizeOf(plannedNode);
|
|
187
|
+
await hand.moveTo({ x: canvasPosition.x + width / 2, y: canvasPosition.y + height / 2 });
|
|
188
|
+
await hand.pause(150);
|
|
189
|
+
const { node, related } = await stage.change((doc) => {
|
|
190
|
+
const onCanvas = readNodes(doc);
|
|
191
|
+
// Where it goes on the canvas, relative to its container as it is now (containers move as they grow)
|
|
192
|
+
const parent = plannedNode.parentId ? onCanvas.get(plannedNode.parentId) : undefined;
|
|
193
|
+
const origin = parent ? getAbsolutePosition(parent, onCanvas) : { x: 0, y: 0 };
|
|
194
|
+
const node = { ...plannedNode, position: { x: canvasPosition.x - origin.x, y: canvasPosition.y - origin.y } };
|
|
195
|
+
addNodes(doc, [node]);
|
|
196
|
+
// The containers it's in grow to fit it
|
|
197
|
+
fitContainersAround(doc, node.id);
|
|
198
|
+
const related = resource
|
|
199
|
+
? getRelatedEdges(resource, node.id, [...onCanvas.values()], catalog.relationsByKey, node.type)
|
|
200
|
+
: [];
|
|
201
|
+
return { node, related };
|
|
202
|
+
});
|
|
203
|
+
hand.select([node.id]);
|
|
204
|
+
await hand.pause(300);
|
|
205
|
+
|
|
206
|
+
// Its connections to what's already there: related catalog resources, then the ones asked for
|
|
207
|
+
for (const edge of related) {
|
|
208
|
+
await playConnection(stage, hand, (doc) => (addNodes(doc, [], [edge]), edge), edge.source, edge.target);
|
|
209
|
+
connections.push(edge.id);
|
|
210
|
+
}
|
|
211
|
+
const onCanvas = await stage.change((doc) => new Set(readNodes(doc).keys()));
|
|
212
|
+
for (const edge of pending.filter((edge) => onCanvas.has(edge.from) && onCanvas.has(edge.to))) {
|
|
213
|
+
pending.splice(pending.indexOf(edge), 1);
|
|
214
|
+
const outcome = await playConnection(
|
|
215
|
+
stage,
|
|
216
|
+
hand,
|
|
217
|
+
(doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label),
|
|
218
|
+
edge.from,
|
|
219
|
+
edge.to
|
|
220
|
+
);
|
|
221
|
+
if ('error' in outcome) errors.push(outcome.error);
|
|
222
|
+
else connections.push(outcome.id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Connections to nodes that aren't on the canvas
|
|
227
|
+
for (const edge of pending) {
|
|
228
|
+
const outcome = await stage.change((doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label));
|
|
229
|
+
if ('error' in outcome) errors.push(outcome.error);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
hand.say('Done');
|
|
233
|
+
return { created: planned.map(({ ref, node, name }) => ({ ref, nodeId: node.id, name })), connections, errors };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** connectCanvasNodes, played out: each connection dragged from one node to the other */
|
|
237
|
+
export async function playConnectAll(stage: AgentStage, edges: EdgeSpecs) {
|
|
238
|
+
const hand = createAgentHand(stage, { pace: paceFor(edges.length) });
|
|
239
|
+
const connections: string[] = [];
|
|
240
|
+
const errors: string[] = [];
|
|
241
|
+
for (const edge of edges) {
|
|
242
|
+
const outcome = await playConnection(
|
|
243
|
+
stage,
|
|
244
|
+
hand,
|
|
245
|
+
(doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label),
|
|
246
|
+
edge.from,
|
|
247
|
+
edge.to
|
|
248
|
+
);
|
|
249
|
+
if ('error' in outcome) errors.push(outcome.error);
|
|
250
|
+
else connections.push(outcome.id);
|
|
251
|
+
}
|
|
252
|
+
return { connections, errors };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Moving a node, played out: grab it and drag it there */
|
|
256
|
+
export async function playMoveNode(stage: AgentStage, nodeId: string, to: XYPosition, hand = createAgentHand(stage)) {
|
|
257
|
+
const nodes = await stage.change(readNodes);
|
|
258
|
+
const node = nodes.get(nodeId);
|
|
259
|
+
if (!node) return false;
|
|
260
|
+
// Positions are relative to the node's container, the pointer is on the canvas
|
|
261
|
+
const parent = node.parentId ? nodes.get(node.parentId) : undefined;
|
|
262
|
+
const origin = parent ? positionOf(parent, nodes) : { x: 0, y: 0 };
|
|
263
|
+
const grab = { x: origin.x + sizeOf(node).width / 2, y: origin.y + 24 };
|
|
264
|
+
hand.say(`Moving ${nameOf(node)}`);
|
|
265
|
+
await hand.moveTo({ x: node.position.x + grab.x, y: node.position.y + grab.y });
|
|
266
|
+
hand.select([nodeId]);
|
|
267
|
+
await hand.pause(120);
|
|
268
|
+
if (stage.sharesMoves) {
|
|
269
|
+
await hand.moveTo({ x: to.x + grab.x, y: to.y + grab.y }, { carry: { nodeId, grab } });
|
|
270
|
+
await stage.change((doc) => moveNode(doc, nodeId, to));
|
|
271
|
+
hand.release();
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
const from = node.position;
|
|
275
|
+
const frames = 12;
|
|
276
|
+
for (let frame = 1; frame <= frames; frame++) {
|
|
277
|
+
const t = easeInOut(frame / frames);
|
|
278
|
+
const position = { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
|
|
279
|
+
await stage.change((doc) => moveNode(doc, nodeId, position));
|
|
280
|
+
await hand.moveTo({ x: position.x + grab.x, y: position.y + grab.y });
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Laying the canvas out: everything moves in one change, and everyone's canvas glides the nodes into place
|
|
287
|
+
* (the layout time in the canvas's meta tells them to)
|
|
288
|
+
*/
|
|
289
|
+
export async function playLayout(stage: AgentStage, layout: LayoutResult) {
|
|
290
|
+
const hand = createAgentHand(stage);
|
|
291
|
+
hand.say('Tidying the layout');
|
|
292
|
+
await stage.change((doc) => applyLayout(doc, layout));
|
|
293
|
+
await hand.pause(500);
|
|
294
|
+
hand.say('Done');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Moves nodes, resizes containers and routes connections from a layout, in one change, and marks when it
|
|
299
|
+
* happened. Connections the layout didn't route lose their old route (drawn as a plain step instead).
|
|
300
|
+
*/
|
|
301
|
+
export const applyLayout = (doc: Y.Doc, layout: LayoutResult) =>
|
|
302
|
+
doc.transact(() => {
|
|
303
|
+
layout.nodes.forEach(({ position, size }, id) => placeNode(doc, id, position, size));
|
|
304
|
+
setEdgeRoutes(doc, layout.routes);
|
|
305
|
+
setMeta(doc, { layoutAt: Date.now() });
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
/** Removing things, played out: point at each, then remove it */
|
|
309
|
+
export async function playRemove(stage: AgentStage, nodeIds: string[], connectionIds: string[]) {
|
|
310
|
+
const hand = createAgentHand(stage, { pace: paceFor(nodeIds.length) });
|
|
311
|
+
if (connectionIds.length) await stage.change((doc) => deleteEdges(doc, connectionIds));
|
|
312
|
+
for (const id of nodeIds) {
|
|
313
|
+
const nodes = await stage.change(readNodes);
|
|
314
|
+
const node = nodes.get(id);
|
|
315
|
+
if (!node) continue;
|
|
316
|
+
hand.say(`Removing ${nameOf(node)}`);
|
|
317
|
+
await hand.moveTo(nodeCenter(node, nodes));
|
|
318
|
+
hand.select([id]);
|
|
319
|
+
await hand.pause(250);
|
|
320
|
+
await stage.change((doc) => deleteNodes(doc, [id]));
|
|
321
|
+
hand.select([]);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Commenting, played out: go to where the comment goes, then leave it */
|
|
326
|
+
export async function playComment(
|
|
327
|
+
stage: AgentStage,
|
|
328
|
+
{ text, nodeId, x, y }: { text: string; nodeId?: string; x?: number; y?: number },
|
|
329
|
+
author: Author
|
|
330
|
+
) {
|
|
331
|
+
const nodes = await stage.change(readNodes);
|
|
332
|
+
const node = nodeId ? nodes.get(nodeId) : undefined;
|
|
333
|
+
if (nodeId && !node) return { error: `No node "${nodeId}" on the canvas` };
|
|
334
|
+
const offset = node ? { x: sizeOf(node).width - 8, y: 8 } : undefined;
|
|
335
|
+
const position = node && positionOf(node, nodes);
|
|
336
|
+
const anchor: CommentAnchor =
|
|
337
|
+
node && offset && position
|
|
338
|
+
? { position: { x: position.x + offset.x, y: position.y + offset.y }, nodeId: node.id, offset }
|
|
339
|
+
: { position: { x: x ?? 0, y: y ?? 0 } };
|
|
340
|
+
const hand = createAgentHand(stage);
|
|
341
|
+
hand.say('Commenting');
|
|
342
|
+
await hand.moveTo(anchor.position);
|
|
343
|
+
await hand.pause(400);
|
|
344
|
+
const threadId = await stage.change((doc) => createThread(doc, anchor, text, author));
|
|
345
|
+
return { threadId };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Pointing at a node while changing it (e.g. a rename) */
|
|
349
|
+
export async function playOnNode<T>(stage: AgentStage, nodeId: string, activity: string, change: (doc: Y.Doc) => T) {
|
|
350
|
+
const nodes = await stage.change(readNodes);
|
|
351
|
+
const node = nodes.get(nodeId);
|
|
352
|
+
const hand = createAgentHand(stage);
|
|
353
|
+
if (node) {
|
|
354
|
+
hand.say(activity);
|
|
355
|
+
await hand.moveTo(nodeCenter(node, nodes));
|
|
356
|
+
hand.select([nodeId]);
|
|
357
|
+
await hand.pause(250);
|
|
358
|
+
}
|
|
359
|
+
return stage.change(change);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Updating a node, played out: point at it to rename or re-describe it, then drag it to its new centre (x/y) */
|
|
363
|
+
export async function playUpdateNode(
|
|
364
|
+
stage: AgentStage,
|
|
365
|
+
{ nodeId, name, summary, version, x, y }: NodeUpdate & { x?: number; y?: number }
|
|
366
|
+
) {
|
|
367
|
+
const missing = { error: `No node "${nodeId}" on the canvas` };
|
|
368
|
+
if (name !== undefined || summary !== undefined || version !== undefined) {
|
|
369
|
+
const outcome = await playOnNode(stage, nodeId, 'Editing', (doc) =>
|
|
370
|
+
updateCanvasNode(doc, { nodeId, name, summary, version })
|
|
371
|
+
);
|
|
372
|
+
if ('error' in outcome) return outcome;
|
|
373
|
+
}
|
|
374
|
+
if (x !== undefined || y !== undefined) {
|
|
375
|
+
const nodes = await stage.change(readNodes);
|
|
376
|
+
const node = nodes.get(nodeId);
|
|
377
|
+
if (!node) return missing;
|
|
378
|
+
const center = nodeCenter(node, nodes);
|
|
379
|
+
await playMoveNode(stage, nodeId, topLeftFor(node, { x: x ?? center.x, y: y ?? center.y }, nodes));
|
|
380
|
+
}
|
|
381
|
+
const nodes = await stage.change(readNodes);
|
|
382
|
+
const node = nodes.get(nodeId);
|
|
383
|
+
return node ? describeNode(node, nodes) : missing;
|
|
384
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who an agent is, named after its client when it doesn't say, in its brand's colour: on the server (MCP tools) and
|
|
3
|
+
* in a chat's canvas view (which joins as the chat's agent). Their logos are in the visualiser's ClientLogos.
|
|
4
|
+
*/
|
|
5
|
+
export type AgentIdentity = { name: string; color: string };
|
|
6
|
+
|
|
7
|
+
const AGENT_COLORS: Record<string, string> = {
|
|
8
|
+
ChatGPT: '#10a37f',
|
|
9
|
+
Codex: '#10a37f',
|
|
10
|
+
Claude: '#d97757',
|
|
11
|
+
'Claude Code': '#d97757',
|
|
12
|
+
Cursor: '#111827',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Agents' clients, by what's in their user agent or host name (most specific first) */
|
|
16
|
+
const CLIENTS: [RegExp, string][] = [
|
|
17
|
+
[/claude[-_ ]?code/i, 'Claude Code'],
|
|
18
|
+
[/claude|anthropic/i, 'Claude'],
|
|
19
|
+
[/codex/i, 'Codex'],
|
|
20
|
+
[/openai|chatgpt/i, 'ChatGPT'],
|
|
21
|
+
[/cursor/i, 'Cursor'],
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** Who the agent is, from the name it gives, else its client's user agent (or a chat's host name) */
|
|
25
|
+
export const getAgent = (agentName: string | undefined, client = ''): AgentIdentity => {
|
|
26
|
+
const name = agentName?.trim() || CLIENTS.find(([pattern]) => pattern.test(client))?.[1] || 'AI agent';
|
|
27
|
+
return { name, color: AGENT_COLORS[name] ?? '#8b5cf6' };
|
|
28
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Hono, type Context } from 'hono';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { paginate } from '@features/tools/catalog-tools';
|
|
4
|
+
import { type CanvasContent, type CanvasStatus } from '@eventcatalog/visualiser/studio';
|
|
5
|
+
import { MAX_TITLE_LENGTH } from '../limits';
|
|
6
|
+
import { diagramLevelsSchema, indexCatalog, newCanvasEdgesSchema, nodeSpecsSchema, planCanvas } from '../canvas-actions';
|
|
7
|
+
import { getCatalogResources } from '../catalog-resources';
|
|
8
|
+
import { MAX_NAME_LENGTH } from '../identity';
|
|
9
|
+
import type { CanvasSummary, StudioRuntime } from '../server/runtime';
|
|
10
|
+
import type { SignedInUser } from '../server/sign-in';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The Studio API: canvases as a whole (listing, creating, reading and deleting them). What's on a canvas is changed
|
|
14
|
+
* over the collaboration connection, or by agents through the MCP tools, never here: they're live documents that
|
|
15
|
+
* people and agents edit together.
|
|
16
|
+
*
|
|
17
|
+
* GET /api/studio/canvases Canvases, most recently changed first (?pageSize=, ?cursor= for the next page)
|
|
18
|
+
* (not ones left empty and untitled: GET /:id still finds them)
|
|
19
|
+
* POST /api/studio/canvases Creates one: { "title"?: string, "createdBy"?: string, "nodes"?: [...],
|
|
20
|
+
* "edges"?: [...] }, starting with the nodes and edges given, if any (as the
|
|
21
|
+
* add_to_canvas MCP tool takes them: catalog resources or new components, x/y
|
|
22
|
+
* their centres, and only the connections in edges are made, each drawn
|
|
23
|
+
* along its "route" if given: { "points": [{x, y}, ...], "label"?: {x, y} }),
|
|
24
|
+
* and "levels" (L1 and L2 laid out, e.g. as a diagram shows them, by the
|
|
25
|
+
* nodes' refs), shown as given until what's on the canvas changes
|
|
26
|
+
* GET /api/studio/canvases/:id One canvas
|
|
27
|
+
* POST /api/studio/canvases/:id/copy Copies its design (nodes and connections, not comments or status) to a new
|
|
28
|
+
* draft: { "title"?: string, "createdBy"?: string }
|
|
29
|
+
* DELETE /api/studio/canvases/:id Deletes it for everyone (anyone who has it open is told), for good
|
|
30
|
+
*
|
|
31
|
+
* Errors are `{ "error": "..." }` with the HTTP status. With sign-in on, only signed-in people can use it (401
|
|
32
|
+
* otherwise), and canvases are created by whoever is signed in. Requests that change something (POST, DELETE) from
|
|
33
|
+
* outside a browser send `Content-Type: application/json`: Astro refuses ones with no content type that don't come
|
|
34
|
+
* from the catalog's own pages.
|
|
35
|
+
*/
|
|
36
|
+
export const CANVASES_API_PATH = '/api/studio/canvases';
|
|
37
|
+
|
|
38
|
+
const CANVAS_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
39
|
+
const MAX_PAGE_SIZE = 100;
|
|
40
|
+
|
|
41
|
+
/** A canvas, as the API gives it */
|
|
42
|
+
export type ApiCanvas = {
|
|
43
|
+
id: string;
|
|
44
|
+
title: string | null;
|
|
45
|
+
status: CanvasStatus;
|
|
46
|
+
/** ISO 8601 */
|
|
47
|
+
createdAt: string | null;
|
|
48
|
+
/** Who started it (a person, or an agent) */
|
|
49
|
+
createdBy: string | null;
|
|
50
|
+
/** When anyone (a person, or an agent) last changed it (ISO 8601) */
|
|
51
|
+
updatedAt: string | null;
|
|
52
|
+
nodeCount: number;
|
|
53
|
+
edgeCount: number;
|
|
54
|
+
openComments: number;
|
|
55
|
+
/** Where people open it */
|
|
56
|
+
url: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type CanvasesApiEnv = {
|
|
60
|
+
Bindings: {
|
|
61
|
+
runtime: StudioRuntime;
|
|
62
|
+
/** Who's signed in (sign-in on) */
|
|
63
|
+
signedInAs?: SignedInUser;
|
|
64
|
+
/** A canvas's page */
|
|
65
|
+
canvasUrl: (canvasId: string) => string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const MAX_NODES = 1000;
|
|
70
|
+
const MAX_EDGES = 2000;
|
|
71
|
+
|
|
72
|
+
const NewCanvas = z.object({
|
|
73
|
+
title: z.string().trim().max(MAX_TITLE_LENGTH).optional(),
|
|
74
|
+
// Ignored when someone's signed in: it's them
|
|
75
|
+
createdBy: z.string().trim().max(MAX_NAME_LENGTH).optional(),
|
|
76
|
+
});
|
|
77
|
+
// A new canvas can start with nodes and edges; a copy starts with the original's
|
|
78
|
+
const NewCanvasWithContent = NewCanvas.extend({
|
|
79
|
+
nodes: nodeSpecsSchema.max(MAX_NODES).optional(),
|
|
80
|
+
edges: newCanvasEdgesSchema.max(MAX_EDGES).optional(),
|
|
81
|
+
levels: diagramLevelsSchema.optional(),
|
|
82
|
+
}).refine((canvas) => !canvas.edges?.length || canvas.nodes, {
|
|
83
|
+
message: 'Edges connect the nodes given with them: give nodes too',
|
|
84
|
+
path: ['edges'],
|
|
85
|
+
});
|
|
86
|
+
const ListQuery = z.object({
|
|
87
|
+
cursor: z.string().optional(),
|
|
88
|
+
pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const iso = (time?: number) => (time ? new Date(time).toISOString() : null);
|
|
92
|
+
const toApiCanvas = (canvas: CanvasSummary, canvasUrl: (canvasId: string) => string): ApiCanvas => ({
|
|
93
|
+
id: canvas.canvasId,
|
|
94
|
+
title: canvas.title ?? null,
|
|
95
|
+
status: canvas.status,
|
|
96
|
+
createdAt: iso(canvas.createdAt),
|
|
97
|
+
createdBy: canvas.createdBy ?? null,
|
|
98
|
+
updatedAt: iso(canvas.updatedAt),
|
|
99
|
+
nodeCount: canvas.nodeCount,
|
|
100
|
+
edgeCount: canvas.edgeCount,
|
|
101
|
+
openComments: canvas.openComments,
|
|
102
|
+
url: canvasUrl(canvas.canvasId),
|
|
103
|
+
});
|
|
104
|
+
const describeIssues = (error: z.ZodError) =>
|
|
105
|
+
error.issues.map((issue) => (issue.path.length ? `${issue.path.join('.')}: ${issue.message}` : issue.message)).join('; ');
|
|
106
|
+
|
|
107
|
+
type ApiContext = Context<CanvasesApiEnv>;
|
|
108
|
+
const noCanvas = (c: ApiContext, id: string) => c.json({ error: `No canvas "${id}"` }, 404);
|
|
109
|
+
|
|
110
|
+
type NewCanvasDetails = { title?: string; createdBy?: string };
|
|
111
|
+
|
|
112
|
+
/** The request's JSON body (or {} without one), or the response saying what's wrong with it */
|
|
113
|
+
const readJson = async (c: ApiContext): Promise<unknown> => {
|
|
114
|
+
// JSON only: a page on another site can send a form or plain text here without asking first, but not JSON
|
|
115
|
+
const body = await c.req.text();
|
|
116
|
+
if (body && !c.req.header('content-type')?.toLowerCase().startsWith('application/json')) {
|
|
117
|
+
return c.json({ error: 'Send the canvas as JSON (Content-Type: application/json)' }, 415);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return body ? JSON.parse(body) : {};
|
|
121
|
+
} catch {
|
|
122
|
+
return c.json({ error: 'The body is not valid JSON' }, 400);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const detailsOf = (c: ApiContext, canvas: z.output<typeof NewCanvas>): NewCanvasDetails => ({
|
|
127
|
+
title: canvas.title || undefined,
|
|
128
|
+
// Whoever's signed in, if anyone
|
|
129
|
+
createdBy: c.env.signedInAs?.name ?? (canvas.createdBy || undefined),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/** A copy's details from the request (optional), or the response saying what's wrong with them */
|
|
133
|
+
const readCopy = async (c: ApiContext): Promise<NewCanvasDetails | Response> => {
|
|
134
|
+
const json = await readJson(c);
|
|
135
|
+
if (json instanceof Response) return json;
|
|
136
|
+
const parsed = NewCanvas.safeParse(json);
|
|
137
|
+
if (!parsed.success) return c.json({ error: describeIssues(parsed.error) }, 400);
|
|
138
|
+
return detailsOf(c, parsed.data);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** A new canvas's details and what it starts with (all optional), or the response saying what's wrong with them */
|
|
142
|
+
const readNewCanvas = async (c: ApiContext): Promise<{ details: NewCanvasDetails; content?: CanvasContent } | Response> => {
|
|
143
|
+
const json = await readJson(c);
|
|
144
|
+
if (json instanceof Response) return json;
|
|
145
|
+
const parsed = NewCanvasWithContent.safeParse(json);
|
|
146
|
+
if (!parsed.success) return c.json({ error: describeIssues(parsed.error) }, 400);
|
|
147
|
+
const details = detailsOf(c, parsed.data);
|
|
148
|
+
if (!parsed.data.nodes) return { details };
|
|
149
|
+
const { resources, relations } = await getCatalogResources();
|
|
150
|
+
const { content, errors } = planCanvas(
|
|
151
|
+
parsed.data.nodes,
|
|
152
|
+
parsed.data.edges ?? [],
|
|
153
|
+
indexCatalog(resources, relations),
|
|
154
|
+
parsed.data.levels
|
|
155
|
+
);
|
|
156
|
+
if (errors.length) return c.json({ error: errors.join('; ') }, 400);
|
|
157
|
+
return { details, content };
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** The API, served under `basePath` (the catalog's base URL, then CANVASES_API_PATH) */
|
|
161
|
+
export const createCanvasesApi = (basePath: string) => {
|
|
162
|
+
const app = new Hono<CanvasesApiEnv>({ strict: false }).basePath(basePath);
|
|
163
|
+
|
|
164
|
+
app.get('/', (c) => {
|
|
165
|
+
const query = ListQuery.safeParse(c.req.query());
|
|
166
|
+
if (!query.success) return c.json({ error: describeIssues(query.error) }, 400);
|
|
167
|
+
const { runtime, canvasUrl } = c.env;
|
|
168
|
+
const page = paginate(runtime.listCanvases(), query.data.cursor, query.data.pageSize);
|
|
169
|
+
if ('error' in page) return c.json({ error: page.error }, 400);
|
|
170
|
+
return c.json({
|
|
171
|
+
canvases: page.items.map((canvas) => toApiCanvas(canvas, canvasUrl)),
|
|
172
|
+
totalCount: page.totalCount,
|
|
173
|
+
...(page.nextCursor && { nextCursor: page.nextCursor }),
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/** A canvas just made, as created */
|
|
178
|
+
const created = (c: ApiContext, canvasId: string) => {
|
|
179
|
+
c.header('Location', `${basePath}/${canvasId}`);
|
|
180
|
+
return c.json({ canvas: toApiCanvas(c.env.runtime.getCanvas(canvasId)!, c.env.canvasUrl) }, 201);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
app.post('/', async (c) => {
|
|
184
|
+
const canvas = await readNewCanvas(c);
|
|
185
|
+
if (canvas instanceof Response) return canvas;
|
|
186
|
+
return created(c, await c.env.runtime.createCanvas(canvas.details, canvas.content));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
app.post('/:id/copy', async (c) => {
|
|
190
|
+
const id = c.req.param('id');
|
|
191
|
+
if (!CANVAS_ID.test(id)) return noCanvas(c, id);
|
|
192
|
+
const details = await readCopy(c);
|
|
193
|
+
if (details instanceof Response) return details;
|
|
194
|
+
const copyId = await c.env.runtime.copyCanvas(id, details);
|
|
195
|
+
return copyId ? created(c, copyId) : noCanvas(c, id);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
app.get('/:id', (c) => {
|
|
199
|
+
const id = c.req.param('id');
|
|
200
|
+
const canvas = CANVAS_ID.test(id) ? c.env.runtime.getCanvas(id) : undefined;
|
|
201
|
+
if (!canvas) return noCanvas(c, id);
|
|
202
|
+
return c.json({ canvas: toApiCanvas(canvas, c.env.canvasUrl) });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
app.delete('/:id', (c) => {
|
|
206
|
+
const id = c.req.param('id');
|
|
207
|
+
if (!CANVAS_ID.test(id)) return noCanvas(c, id);
|
|
208
|
+
try {
|
|
209
|
+
if (!c.env.runtime.deleteCanvas(id)) return noCanvas(c, id);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error(`[studio] Could not delete canvas ${id}`, error);
|
|
212
|
+
return c.json({ error: `Could not delete the canvas: ${(error as Error).message}` }, 500);
|
|
213
|
+
}
|
|
214
|
+
return c.body(null, 204);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.notFound((c) => c.json({ error: 'Not found' }, 404));
|
|
218
|
+
return app;
|
|
219
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { buildUrl } from '@utils/url-builder';
|
|
3
|
+
import { startStudio } from '../server/runtime';
|
|
4
|
+
import { getSignedInUser, isSignedIn } from '../server/sign-in';
|
|
5
|
+
import { CANVASES_API_PATH, createCanvasesApi } from './canvases-api';
|
|
6
|
+
|
|
7
|
+
// The Studio API (see canvases-api.ts)
|
|
8
|
+
const api = createCanvasesApi(buildUrl(CANVASES_API_PATH, true));
|
|
9
|
+
|
|
10
|
+
export const ALL: APIRoute = async ({ request, locals, url }) => {
|
|
11
|
+
let runtime: Awaited<ReturnType<typeof startStudio>>;
|
|
12
|
+
try {
|
|
13
|
+
runtime = await startStudio({ isSignedIn });
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// Its storage can't be used (e.g. the database was updated by a newer EventCatalog)
|
|
16
|
+
return Response.json({ error: `Studio isn't available: ${(error as Error).message}` }, { status: 503 });
|
|
17
|
+
}
|
|
18
|
+
return api.fetch(request, {
|
|
19
|
+
runtime,
|
|
20
|
+
signedInAs: getSignedInUser(locals),
|
|
21
|
+
canvasUrl: (canvasId: string) => `${url.origin}${buildUrl(`/studio/${canvasId}`)}`,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const prerender = false;
|