@eventcatalog/core 5.0.0-beta.1 → 5.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +227 -128
  2. package/dist/analytics/analytics.cjs +18 -3
  3. package/dist/analytics/analytics.d.cts +1 -1
  4. package/dist/analytics/analytics.d.ts +1 -1
  5. package/dist/analytics/analytics.js +3 -2
  6. package/dist/analytics/log-build.cjs +71 -35
  7. package/dist/analytics/log-build.js +5 -4
  8. package/dist/analytics/telemetry.cjs +42 -0
  9. package/dist/analytics/telemetry.d.cts +20 -0
  10. package/dist/analytics/telemetry.d.ts +20 -0
  11. package/dist/analytics/telemetry.js +8 -0
  12. package/dist/{chunk-K2ACDO7Y.js → chunk-2LBDRHE4.js} +1 -1
  13. package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
  14. package/dist/chunk-CPW6G62V.js +17 -0
  15. package/dist/{chunk-UDS3SYLZ.js → chunk-CRKJBDN7.js} +56 -33
  16. package/dist/{chunk-Q6SD5SNQ.js → chunk-EM36OL6A.js} +1 -1
  17. package/dist/{chunk-S2QGT7ZF.js → chunk-IONYKKOY.js} +1 -1
  18. package/dist/{chunk-KYJZJLBD.js → chunk-LJMXOEEC.js} +9 -3
  19. package/dist/constants.cjs +1 -1
  20. package/dist/constants.js +1 -1
  21. package/dist/eventcatalog.cjs +204 -124
  22. package/dist/eventcatalog.config.d.cts +59 -1
  23. package/dist/eventcatalog.config.d.ts +59 -1
  24. package/dist/eventcatalog.js +30 -11
  25. package/dist/federation/federate.js +2 -2
  26. package/dist/generate.cjs +1 -1
  27. package/dist/generate.js +3 -3
  28. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  29. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  30. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  31. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  32. package/dist/runtime-dependencies/manifest.json +8 -1
  33. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  34. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  35. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  36. package/dist/runtime-dependencies/yjs.mjs +3 -0
  37. package/dist/utils/cli-logger.cjs +1 -1
  38. package/dist/utils/cli-logger.js +2 -2
  39. package/dist/utils/license-status.cjs +71 -45
  40. package/dist/utils/license-status.d.cts +14 -5
  41. package/dist/utils/license-status.d.ts +14 -5
  42. package/dist/utils/license-status.js +1 -1
  43. package/eventcatalog/astro.config.mjs +5 -0
  44. package/eventcatalog/integrations/storage.mjs +20 -0
  45. package/eventcatalog/integrations/studio-server.mjs +32 -0
  46. package/eventcatalog/src/components/Grids/utils.tsx +0 -23
  47. package/eventcatalog/src/components/Header.astro +3 -3
  48. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
  49. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
  50. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
  51. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
  52. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
  53. package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
  54. package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
  55. package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
  56. package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
  57. package/eventcatalog/src/features/ai/chat-api.ts +3 -3
  58. package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
  59. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
  60. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
  61. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
  62. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +276 -0
  63. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +210 -0
  64. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
  65. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  66. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  67. package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
  68. package/eventcatalog/src/features/server/start.ts +25 -0
  69. package/eventcatalog/src/features/storage/database.ts +88 -0
  70. package/eventcatalog/src/features/storage/migrations.ts +85 -0
  71. package/eventcatalog/src/features/storage/sql/database.sql +5 -0
  72. package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
  73. package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
  74. package/eventcatalog/src/features/storage/sql.ts +28 -0
  75. package/eventcatalog/src/features/studio/agent-choreography.ts +381 -0
  76. package/eventcatalog/src/features/studio/api/canvases-api.ts +170 -0
  77. package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
  78. package/eventcatalog/src/features/studio/api/client.ts +35 -0
  79. package/eventcatalog/src/features/studio/canvas-actions.ts +414 -0
  80. package/eventcatalog/src/features/studio/canvas-doc.ts +414 -0
  81. package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
  82. package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
  83. package/eventcatalog/src/features/studio/catalog.ts +171 -0
  84. package/eventcatalog/src/features/studio/clipboard.ts +97 -0
  85. package/eventcatalog/src/features/studio/components/CanvasControls.tsx +154 -0
  86. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +370 -0
  87. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
  88. package/eventcatalog/src/features/studio/components/Comments.tsx +770 -0
  89. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +281 -0
  90. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
  91. package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
  92. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +81 -0
  93. package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
  94. package/eventcatalog/src/features/studio/components/JoinForm.tsx +34 -0
  95. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +314 -0
  96. package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
  97. package/eventcatalog/src/features/studio/components/Presence.tsx +286 -0
  98. package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +212 -0
  99. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
  100. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1216 -0
  101. package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
  102. package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +255 -0
  103. package/eventcatalog/src/features/studio/components/icons.ts +53 -0
  104. package/eventcatalog/src/features/studio/components/status.ts +53 -0
  105. package/eventcatalog/src/features/studio/edges.ts +56 -0
  106. package/eventcatalog/src/features/studio/flow-state.ts +92 -0
  107. package/eventcatalog/src/features/studio/grouping.ts +226 -0
  108. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
  109. package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
  110. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +347 -0
  111. package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
  112. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +104 -0
  113. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +589 -0
  114. package/eventcatalog/src/features/studio/identity.ts +37 -0
  115. package/eventcatalog/src/features/studio/layout.ts +42 -0
  116. package/eventcatalog/src/features/studio/levels.ts +282 -0
  117. package/eventcatalog/src/features/studio/node-types.ts +205 -0
  118. package/eventcatalog/src/features/studio/pages/[id].astro +68 -0
  119. package/eventcatalog/src/features/studio/pages/index.astro +784 -0
  120. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  121. package/eventcatalog/src/features/studio/placement.ts +197 -0
  122. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +510 -0
  123. package/eventcatalog/src/features/studio/server/runtime.ts +491 -0
  124. package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
  125. package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
  126. package/eventcatalog/src/features/studio/server/storage.ts +90 -0
  127. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  128. package/eventcatalog/src/features/studio/studio.css +273 -0
  129. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  130. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -5
  131. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
  132. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
  133. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
  134. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
  135. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
  136. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
  137. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
  138. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
  139. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
  140. package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
  141. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
  142. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
  143. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
  144. package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
  145. package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
  146. package/eventcatalog/src/styles/tailwind.css +2 -2
  147. package/eventcatalog/src/utils/collections/channels.ts +56 -1
  148. package/eventcatalog/src/utils/collections/flows.ts +50 -1
  149. package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
  150. package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
  151. package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
  152. package/eventcatalog/src/utils/collections/resources.ts +310 -0
  153. package/eventcatalog/src/utils/collections/util.ts +31 -0
  154. package/eventcatalog/src/utils/feature.ts +8 -0
  155. package/eventcatalog/src/utils/git.ts +11 -5
  156. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
  157. package/eventcatalog/src/utils/trial.ts +1 -1
  158. package/package.json +17 -12
  159. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  160. package/eventcatalog/public/studio-bg.png +0 -0
  161. package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
  162. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
  163. package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
  164. package/eventcatalog/src/components/Grids/components.tsx +0 -170
  165. package/eventcatalog/src/components/Grids/message-link.ts +0 -38
  166. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  167. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
  168. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
  169. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
  170. package/eventcatalog/src/pages/studio.astro +0 -316
  171. package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
@@ -127,10 +127,10 @@ The user will ask you some questions about the software architecture catalog, yo
127
127
 
128
128
  At point the referer url (${referrer}) will be the URL of the page the user is on, you should use this to help you answer the question,
129
129
  You may be able to get the resource from the URL
130
- - Example if the url is like /docs|visualiser|architecture/{collection}/{id}/{version}
130
+ - Example if the url is like /docs|visualiser/{collection}/{id}/{version}
131
131
  - (e.g /docs/events/MyEvent/1.0.0) in this case the id is MyEvent and the version is 1.0.0 and collection is events.
132
132
  - (e.g /visualiser/domains/MyDomain/1.0.0) in this case the id is MyDomain and the version is 1.0.0 and collection is domains.
133
- - (e.g /architecture/services/MyService/1.0.0) in this case the id is MyService and the version is 1.0.0 and collection is services.
133
+ - (e.g /docs/services/MyService/1.0.0/resources) in this case the id is MyService and the version is 1.0.0 and collection is services; the user is looking at the resources the service uses.
134
134
 
135
135
  The referer URL is: ${referrer}
136
136
 
@@ -159,7 +159,7 @@ When responding:
159
159
  - The link options are:
160
160
  - If you want to get the documentation for a resource use the /docs/ prefix (e.g /docs/{collection}/{id}/{version})
161
161
  - If you want to let the user know they can visualize a resource use the /visualiser/ prefix (e.g /visualiser/{collection}/{id}/{version})
162
- - If you want to let the user know they can see the architecture of a resource use the /architecture/ prefix (e.g /architecture/{collection}/{id}/{version})
162
+ - If you want to show the user everything a domain, system, service, flow, channel or message (event, command, query) contains, uses or connects to, link its Resources page (e.g /docs/{collection}/{id}/{version}/resources). Only those collections have a Resources page.
163
163
  - If you don't know the version, use the getResource tool to fetch the resource and get the actual version number before creating the link.
164
164
  7. When you return a schema, use code blocks to render the schema to the user too, for example if the schema is in JSON format use \`\`\`json and if the schema is in YAML format use \`\`\`yaml
165
165
  8. IMPORTANT: After answering each question, ALWAYS use the suggestFollowUpQuestions tool to suggest 2-3 relevant follow-up questions the user might want to ask next. These should be contextual to the conversation and help the user explore related topics.
@@ -96,6 +96,14 @@ export function isMcpRoute(pathname: string) {
96
96
  return pathname === '/docs/mcp' || pathname.startsWith('/docs/mcp/');
97
97
  }
98
98
 
99
+ // The Studio API answers in JSON, rather than sending callers to the sign-in page
100
+ const isStudioApiRoute = (pathname: string) => pathname === '/api/studio' || pathname.startsWith('/api/studio/');
101
+ const signInRequired = () =>
102
+ new Response(JSON.stringify({ error: 'Sign in to use this API' }), {
103
+ status: 401,
104
+ headers: { 'Content-Type': 'application/json' },
105
+ });
106
+
99
107
  export const authMiddleware: MiddlewareHandler = async (context, next) => {
100
108
  const { request, redirect, locals } = context;
101
109
  const url = new URL(request.url);
@@ -131,6 +139,7 @@ export const authMiddleware: MiddlewareHandler = async (context, next) => {
131
139
  const session = await getSession(request);
132
140
 
133
141
  if (!session) {
142
+ if (isStudioApiRoute(pathname)) return signInRequired();
134
143
  const callbackUrl = encodeURIComponent(pathname + url.search);
135
144
  return redirect(`/auth/login?callbackUrl=${callbackUrl}`);
136
145
  }
@@ -157,6 +166,7 @@ export const authMiddleware: MiddlewareHandler = async (context, next) => {
157
166
  typedLocals.matchesPattern = matchesPattern;
158
167
  } catch (error) {
159
168
  console.error('Session error:', error);
169
+ if (isStudioApiRoute(pathname)) return signInRequired();
160
170
  const callbackUrl = encodeURIComponent(pathname + url.search);
161
171
  return redirect(`/auth/login?callbackUrl=${callbackUrl}`);
162
172
  }
@@ -13,6 +13,7 @@ import {
13
13
  isFullCatalogAPIEnabled,
14
14
  isDevMode,
15
15
  isSSR,
16
+ isCanvasEnabled,
16
17
  } from '../../utils/feature';
17
18
  import { getCustomPageRoutes, isApiRoute, listCustomPageFiles, resolveCustomPagesPrefix } from '../custom-pages/routes';
18
19
 
@@ -111,7 +112,8 @@ export default function eventCatalogIntegration(): AstroIntegration {
111
112
  }
112
113
 
113
114
  // Handle routes for MCP Server (requires SSR)
114
- if (isEventCatalogMCPEnabled()) {
115
+ // Also for canvases, so agents can join them (the canvas tools are on the MCP server)
116
+ if (isEventCatalogMCPEnabled() || isCanvasEnabled()) {
115
117
  params.injectRoute({
116
118
  pattern: '/docs/mcp/[...path]',
117
119
  entrypoint: path.join(packageDirectory, 'src/features/mcp/mcp-server.ts'),
@@ -173,6 +175,37 @@ export default function eventCatalogIntegration(): AstroIntegration {
173
175
  entrypoint: path.join(packageDirectory, 'src/features/visualizer-layout/reset.ts'),
174
176
  });
175
177
  }
178
+
179
+ // What a server starts before anyone asks for it (the storage configured, migrated; Studio), called by
180
+ // `eventcatalog start` once it listens
181
+ if (isSSR()) {
182
+ params.injectRoute({
183
+ pattern: '/_eventcatalog/start',
184
+ entrypoint: path.join(packageDirectory, 'src/features/server/start.ts'),
185
+ });
186
+ }
187
+
188
+ // Collaborative canvases in EventCatalog Studio (/studio lists them, /studio?new=1 starts one). Their WebSocket
189
+ // server is in integrations/studio-server.mjs.
190
+ if (isCanvasEnabled()) {
191
+ params.injectRoute({
192
+ pattern: '/studio',
193
+ entrypoint: path.join(packageDirectory, 'src/features/studio/pages/index.astro'),
194
+ });
195
+ params.injectRoute({
196
+ pattern: '/studio/[id]',
197
+ entrypoint: path.join(packageDirectory, 'src/features/studio/pages/[id].astro'),
198
+ });
199
+ // The Studio API (canvases as a whole: listing, creating, deleting)
200
+ params.injectRoute({
201
+ pattern: '/api/studio/canvases/[...path]',
202
+ entrypoint: path.join(packageDirectory, 'src/features/studio/api/canvases.ts'),
203
+ });
204
+ params.injectRoute({
205
+ pattern: '/webmcp-relay/[file]',
206
+ entrypoint: path.join(packageDirectory, 'src/features/studio/pages/webmcp-relay.ts'),
207
+ });
208
+ }
176
209
  },
177
210
  'astro:server:setup': ({ server }) => {
178
211
  // When custom page files appear or disappear, refresh the manifest.
@@ -0,0 +1,47 @@
1
+ // Shared by the canvas MCP App (client) and the MCP server: no server-only imports here
2
+ import type { CatalogRelation, CatalogResource } from '../../../studio/catalog-resources';
3
+ import type { CanvasStatus } from '../../../studio/canvas-doc';
4
+
5
+ export const CANVAS_RESOURCE_URI = 'ui://eventcatalog/canvas.html';
6
+ /** Key of the payload in openCanvas results' `_meta` */
7
+ export const CANVAS_META_KEY = 'eventcatalog/canvas';
8
+ /** App-only tool the view loads the canvas from when the host doesn't pass the result's `_meta` */
9
+ export const CANVAS_VIEW_TOOL = 'getCanvasView';
10
+ /** App-only tool the view loads the catalog (for dragging resources in) from */
11
+ export const CANVAS_CATALOG_TOOL = 'getCanvasCatalog';
12
+ /** App-only tool the view syncs through when the chat's sandbox blocks the collaboration WebSocket */
13
+ export const CANVAS_SYNC_TOOL = 'syncCanvas';
14
+
15
+ /** A canvas listed in the view's picker */
16
+ export type CanvasListItem = {
17
+ canvasId: string;
18
+ title?: string;
19
+ status: CanvasStatus;
20
+ people: string[];
21
+ nodeCount: number;
22
+ openComments: number;
23
+ };
24
+
25
+ /**
26
+ * What the view shows: a canvas (it connects to it itself, so people and agents edit it live), or, when opened
27
+ * without one (e.g. from ChatGPT's sidebar), a list of canvases to open or start.
28
+ */
29
+ export type CanvasPayload = CanvasView | CanvasHome;
30
+
31
+ export type CanvasHome = { view: 'home'; canvases: CanvasListItem[] };
32
+
33
+ export type CanvasView = {
34
+ view: 'canvas';
35
+ canvasId: string;
36
+ title?: string;
37
+ /** The collaboration WebSocket, e.g. wss://catalog.example.com/_eventcatalog/studio */
38
+ socketUrl: string;
39
+ /** The canvas in EventCatalog */
40
+ canvasUrl: string;
41
+ /** The EventCatalog MCP server, for "Connect your agent" */
42
+ mcpUrl: string;
43
+ /** What to call the user on the canvas, when the agent knows their name */
44
+ userName?: string;
45
+ };
46
+
47
+ export type CanvasCatalog = { resources: CatalogResource[]; relations: CatalogRelation[] };
@@ -0,0 +1,22 @@
1
+ /*
2
+ * The canvas view's Tailwind stylesheet (with Tailwind's base reset) and EventCatalog's theme variables,
3
+ * built from the classes of the collaborative canvas components it renders.
4
+ */
5
+ @import 'tailwindcss' source(none);
6
+ @import '../../../../styles/theme.css';
7
+ @import '../../../studio/studio.css';
8
+
9
+ @source './*.tsx';
10
+ @source '../../../studio/components/*.{ts,tsx}';
11
+
12
+ body {
13
+ margin: 0;
14
+ background: rgb(var(--ec-page-bg));
15
+ color: rgb(var(--ec-page-text));
16
+ font-family:
17
+ ui-sans-serif,
18
+ system-ui,
19
+ -apple-system,
20
+ 'Segoe UI',
21
+ sans-serif;
22
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * The collaborative canvas as an MCP App: openCanvas shows it in the chat (ChatGPT, Claude...), where it
3
+ * joins the canvas (over the collaboration WebSocket, or through tool calls when the chat's sandbox blocks it).
4
+ * The user edits it there with everyone else on it, people in EventCatalog and agents, live.
5
+ *
6
+ * Opened without a canvas (e.g. from ChatGPT's sidebar or a thread tab, its entrypoints), it lists the canvases
7
+ * to open one or start one. Follows the OpenAI MCP extensions for ChatGPT: entrypoints, display modes, deep
8
+ * links (/canvas/<id>) and model context (background context, and the selection as titled attachments).
9
+ *
10
+ * Built into a single HTML file by scripts/build-mcp-apps.mjs and served as a ui:// resource.
11
+ */
12
+ import { useCallback, useEffect, useMemo, useState } from 'react';
13
+ import { ArrowLeft, ExternalLink, Maximize2, Minimize2, Plus, Users } from 'lucide-react';
14
+ import type { App } from '@modelcontextprotocol/ext-apps';
15
+ import styles from './styles.css?inline';
16
+ import visualiserStyles from '@eventcatalog/visualiser/styles.css?inline';
17
+ import StudioDesigner, { type SelectedNode } from '@features/studio/components/StudioDesigner';
18
+ import { CANVAS_STATUS_LOOK, STATUS } from '@features/studio/components/status';
19
+ import { getToolResultPayload, mountView, useMcpAppView } from '../shared/app-view';
20
+ import {
21
+ CANVAS_CATALOG_TOOL,
22
+ CANVAS_META_KEY,
23
+ CANVAS_SYNC_TOOL,
24
+ CANVAS_VIEW_TOOL,
25
+ type CanvasCatalog,
26
+ type CanvasHome,
27
+ type CanvasPayload,
28
+ type CanvasView,
29
+ } from './shared';
30
+ import type { SyncResponse, ToolSync } from '@features/studio/tool-sync';
31
+
32
+ const isCanvasPayload = (payload?: CanvasPayload): payload is CanvasPayload =>
33
+ payload?.view === 'home' ? Array.isArray(payload.canvases) : Boolean(payload?.canvasId && payload.socketUrl);
34
+
35
+ const VIEW_OPTIONS = {
36
+ name: 'EventCatalog canvas',
37
+ metaKey: CANVAS_META_KEY,
38
+ viewTool: CANVAS_VIEW_TOOL,
39
+ isPayload: isCanvasPayload,
40
+ getViewToolArguments: ({ canvasId }: Record<string, any>) => ({ canvasId }),
41
+ loadErrorMessage: 'The canvas could not be opened',
42
+ };
43
+
44
+ const INLINE_HEIGHT = 640;
45
+ /** Deep links into the app (ChatGPT's `openai/deepLink`), e.g. /canvas/<id> */
46
+ const DEEP_LINK = /^\/canvas(?:es)?\/([0-9a-f-]{36})/i;
47
+
48
+ /** Loads what to show through a tool (the canvas view's own, or openCanvas to start a canvas) */
49
+ const loadPayload = async (app: App, name: string, args: Record<string, unknown>) => {
50
+ const result = await app.callServerTool({ name, arguments: args });
51
+ const payload = getToolResultPayload<CanvasPayload>(result, CANVAS_META_KEY);
52
+ if (result.isError || !isCanvasPayload(payload)) throw new Error('The canvas could not be opened');
53
+ return payload;
54
+ };
55
+
56
+ function CanvasPicker({
57
+ home,
58
+ onOpen,
59
+ onCreate,
60
+ }: {
61
+ home: CanvasHome;
62
+ onOpen: (id: string) => void;
63
+ onCreate: (title: string) => void;
64
+ }) {
65
+ const [title, setTitle] = useState('');
66
+ return (
67
+ <div className="mx-auto max-w-2xl space-y-6 p-6">
68
+ <div className="space-y-1">
69
+ <h1 className="text-lg font-semibold">Canvases</h1>
70
+ <p className="text-sm text-[rgb(var(--ec-page-text-muted))]">
71
+ Work together, live: people in EventCatalog, people in chats and agents, on the same canvas.
72
+ </p>
73
+ </div>
74
+ <form
75
+ className="flex gap-2"
76
+ onSubmit={(event) => {
77
+ event.preventDefault();
78
+ if (title.trim()) onCreate(title.trim());
79
+ }}
80
+ >
81
+ <input
82
+ value={title}
83
+ onChange={(event) => setTitle(event.target.value)}
84
+ placeholder="What are you designing?"
85
+ className="min-w-0 flex-1 rounded-lg border px-3 py-2 text-sm bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]"
86
+ />
87
+ <button
88
+ type="submit"
89
+ disabled={!title.trim()}
90
+ className="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium disabled:opacity-50 bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))] hover:bg-[rgb(var(--ec-button-bg-hover))]"
91
+ >
92
+ <Plus size={16} /> New canvas
93
+ </button>
94
+ </form>
95
+ <div className="space-y-2">
96
+ {home.canvases.length === 0 && <p className="text-sm text-[rgb(var(--ec-page-text-muted))]">No canvases yet.</p>}
97
+ {home.canvases.map((canvas) => (
98
+ <button
99
+ key={canvas.canvasId}
100
+ onClick={() => onOpen(canvas.canvasId)}
101
+ className="flex w-full items-center justify-between gap-3 rounded-xl border px-4 py-3 text-left bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] hover:border-[rgb(var(--ec-accent))]"
102
+ >
103
+ <span className="min-w-0">
104
+ <span className="flex items-center gap-2">
105
+ <span className="truncate text-sm font-semibold">{canvas.title ?? 'Untitled canvas'}</span>
106
+ <span
107
+ className={`shrink-0 rounded-full border px-1.5 py-px text-[10px] font-semibold ${CANVAS_STATUS_LOOK[canvas.status].pill}`}
108
+ >
109
+ {CANVAS_STATUS_LOOK[canvas.status].label}
110
+ </span>
111
+ </span>
112
+ <span className="text-xs text-[rgb(var(--ec-page-text-muted))]">
113
+ {canvas.nodeCount} {canvas.nodeCount === 1 ? 'node' : 'nodes'}
114
+ {canvas.openComments > 0 && ` · ${canvas.openComments} open comments`}
115
+ </span>
116
+ </span>
117
+ {canvas.people.length > 0 && (
118
+ <span
119
+ className={`flex shrink-0 items-center gap-1 text-xs ${STATUS.success.text}`}
120
+ title={canvas.people.join(', ')}
121
+ >
122
+ <Users size={14} /> {canvas.people.length} here now
123
+ </span>
124
+ )}
125
+ </button>
126
+ ))}
127
+ </div>
128
+ </div>
129
+ );
130
+ }
131
+
132
+ function CanvasApp() {
133
+ const { app, payload, setPayload, error, hostContext } = useMcpAppView(VIEW_OPTIONS);
134
+ const [catalog, setCatalog] = useState<CanvasCatalog>({ resources: [], relations: [] });
135
+ const [selection, setSelection] = useState<SelectedNode[]>([]);
136
+ const [loadError, setLoadError] = useState<string | null>(null);
137
+ const canvas = payload?.view === 'canvas' ? payload : undefined;
138
+
139
+ const show = useCallback(
140
+ (load: Promise<CanvasPayload>) =>
141
+ load.then(setPayload).catch((cause) => setLoadError(String((cause as Error)?.message ?? cause))),
142
+ [setPayload]
143
+ );
144
+ const openCanvas = (canvasId?: string) => app && show(loadPayload(app, CANVAS_VIEW_TOOL, canvasId ? { canvasId } : {}));
145
+
146
+ // Deep links (ChatGPT's `openai/deepLink`): /canvas/<id> opens that canvas
147
+ const deepLink = (hostContext as Record<string, { url?: string } | undefined> | undefined)?.['openai/deepLink']?.url;
148
+ useEffect(() => {
149
+ const canvasId = deepLink?.match(DEEP_LINK)?.[1];
150
+ if (app && canvasId && canvasId !== canvas?.canvasId) void show(loadPayload(app, CANVAS_VIEW_TOOL, { canvasId }));
151
+ }, [app, deepLink]);
152
+
153
+ // Chats may not let the view open the collaboration WebSocket (sandbox CSP, local network rules...):
154
+ // then it syncs through the MCP server, the same way the model's tools reach the canvas
155
+ const syncViaTools = useMemo<ToolSync | undefined>(
156
+ () =>
157
+ app
158
+ ? async (request) => {
159
+ const result = await app.callServerTool({ name: CANVAS_SYNC_TOOL, arguments: request });
160
+ if (result.isError || !result.structuredContent) throw new Error('Sync failed');
161
+ return result.structuredContent as SyncResponse;
162
+ }
163
+ : undefined,
164
+ [app]
165
+ );
166
+
167
+ // The catalog (to drag resources in) is loaded separately, to keep tool results small
168
+ useEffect(() => {
169
+ if (!app || !canvas) return;
170
+ app
171
+ .callServerTool({ name: CANVAS_CATALOG_TOOL, arguments: {} })
172
+ .then((result) => {
173
+ const loaded = result.structuredContent as CanvasCatalog | undefined;
174
+ if (loaded?.resources) setCatalog(loaded);
175
+ })
176
+ .catch(() => {});
177
+ }, [app, canvas?.canvasId]);
178
+
179
+ // Model context (replaced on each update): which canvas is open as background context the user doesn't see,
180
+ // and what they've selected as an attachment they can see (and remove), titled with the names
181
+ useEffect(() => {
182
+ if (!app || !payload || !app.getHostCapabilities()?.updateModelContext) return;
183
+ const background = {
184
+ type: 'text' as const,
185
+ text: canvas
186
+ ? `The user has the canvas "${canvas.title ?? 'Untitled'}" open (canvasId ${canvas.canvasId}). Use getCanvas to see what is on it, and the canvas tools to work on it with them.`
187
+ : 'The user is looking at their canvases, to open one or start one.',
188
+ annotations: { audience: ['assistant' as const] },
189
+ };
190
+ const selected = selection.length
191
+ ? [
192
+ {
193
+ type: 'text' as const,
194
+ text: `Selected on the canvas: ${selection
195
+ .map(
196
+ (node) =>
197
+ `${node.name} (${node.type}${node.catalogResource ? `, catalog ${node.catalogResource}` : ''}, nodeId ${node.id})`
198
+ )
199
+ .join('; ')}`,
200
+ _meta: { 'openai/title': selection.map((node) => node.name).join(', ') },
201
+ },
202
+ ]
203
+ : [];
204
+ app.updateModelContext({ content: [background, ...selected] }).catch(() => {});
205
+ }, [app, payload, selection]);
206
+
207
+ if (error || loadError) return <p className="p-4 text-sm">{error ?? loadError}</p>;
208
+ if (!payload) return <p className="p-4 text-sm text-[rgb(var(--ec-page-text-muted))]">Opening the canvas…</p>;
209
+
210
+ const fullscreen = hostContext?.displayMode === 'fullscreen';
211
+ const canToggle = hostContext?.availableDisplayModes?.includes(fullscreen ? 'inline' : 'fullscreen');
212
+ const iconButton =
213
+ 'flex items-center gap-1 rounded-md px-2 py-1 text-xs text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-page-border)/0.5)]';
214
+
215
+ // No join form inside a chat: use the name the agent passed, or name them after the chat they're in
216
+ const hostName = app?.getHostVersion()?.name;
217
+ const userName = (view: CanvasView) =>
218
+ view.userName ?? `${hostName ? hostName.replace(/^\w/, (letter) => letter.toUpperCase()) : 'Chat'} user`;
219
+
220
+ return (
221
+ <div className="flex flex-col" style={{ height: fullscreen ? '100vh' : INLINE_HEIGHT }}>
222
+ <div className="flex items-center justify-between border-b px-3 py-1.5 border-[rgb(var(--ec-page-border))]">
223
+ <span className="flex min-w-0 items-center gap-1">
224
+ {canvas && (
225
+ <button className={iconButton} title="All canvases" onClick={() => void openCanvas()}>
226
+ <ArrowLeft size={14} />
227
+ </button>
228
+ )}
229
+ <span className="truncate text-sm font-semibold">{canvas ? (canvas.title ?? 'Canvas') : 'Canvases'}</span>
230
+ </span>
231
+ <span className="flex items-center gap-1">
232
+ {canvas && (
233
+ <button className={iconButton} onClick={() => app?.openLink({ url: canvas.canvasUrl })}>
234
+ <ExternalLink size={14} />
235
+ Open in EventCatalog
236
+ </button>
237
+ )}
238
+ {canToggle && (
239
+ <button
240
+ className={iconButton}
241
+ title={fullscreen ? 'Exit full screen' : 'Full screen'}
242
+ onClick={() => app?.requestDisplayMode({ mode: fullscreen ? 'inline' : 'fullscreen' })}
243
+ >
244
+ {fullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
245
+ </button>
246
+ )}
247
+ </span>
248
+ </div>
249
+ <div className="min-h-0 flex-1 overflow-y-auto">
250
+ {canvas ? (
251
+ <StudioDesigner
252
+ key={canvas.canvasId}
253
+ canvasId={canvas.canvasId}
254
+ socketUrl={canvas.socketUrl}
255
+ shareUrl={canvas.canvasUrl}
256
+ mcpUrl={canvas.mcpUrl}
257
+ syncViaTools={syncViaTools}
258
+ defaultName={userName(canvas)}
259
+ resources={catalog.resources}
260
+ relations={catalog.relations}
261
+ onSelectionChange={setSelection}
262
+ followChanges
263
+ />
264
+ ) : (
265
+ <CanvasPicker
266
+ home={payload as CanvasHome}
267
+ onOpen={(canvasId) => void openCanvas(canvasId)}
268
+ onCreate={(title) => app && void show(loadPayload(app, 'openCanvas', { title }))}
269
+ />
270
+ )}
271
+ </div>
272
+ </div>
273
+ );
274
+ }
275
+
276
+ mountView(CanvasApp, [visualiserStyles, styles].join('\n'));