@eventcatalog/core 5.0.0-beta.4 → 5.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +2 -2
  4. package/dist/analytics/log-build.js +4 -4
  5. package/dist/{chunk-TOMV5YJH.js → chunk-56CD2NH6.js} +1 -1
  6. package/dist/{chunk-M7OVJHPV.js → chunk-DE3QHVJP.js} +1 -1
  7. package/dist/{chunk-Q7FBYUCZ.js → chunk-L2IRFOE7.js} +1 -1
  8. package/dist/{chunk-TQLRG35T.js → chunk-PIYJM642.js} +1 -1
  9. package/dist/{chunk-VA77ENFQ.js → chunk-QBP3BBSV.js} +1 -1
  10. package/dist/{chunk-PL3T5TWF.js → chunk-XMPFHLK4.js} +2 -2
  11. package/dist/constants.cjs +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/eventcatalog.cjs +2 -2
  14. package/dist/eventcatalog.js +6 -6
  15. package/dist/generate.cjs +1 -1
  16. package/dist/generate.js +3 -3
  17. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  18. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  19. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  20. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  21. package/dist/runtime-dependencies/manifest.json +8 -0
  22. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  23. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  24. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  25. package/dist/runtime-dependencies/yjs.mjs +3 -0
  26. package/dist/utils/cli-logger.cjs +1 -1
  27. package/dist/utils/cli-logger.js +2 -2
  28. package/dist/utils/license-status.cjs +1 -1
  29. package/dist/utils/license-status.js +1 -1
  30. package/eventcatalog/astro.config.mjs +2 -0
  31. package/eventcatalog/integrations/studio-server.mjs +17 -0
  32. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +20 -1
  33. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +39 -0
  34. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
  35. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +269 -0
  36. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +209 -0
  37. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +2 -2
  38. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  39. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  40. package/eventcatalog/src/features/mcp/mcp-server.ts +17 -4
  41. package/eventcatalog/src/features/studio/agent-choreography.ts +378 -0
  42. package/eventcatalog/src/features/studio/canvas-actions.ts +287 -0
  43. package/eventcatalog/src/features/studio/canvas-doc.ts +280 -0
  44. package/eventcatalog/src/features/studio/catalog-resources.ts +152 -0
  45. package/eventcatalog/src/features/studio/catalog.ts +171 -0
  46. package/eventcatalog/src/features/studio/components/CanvasControls.tsx +153 -0
  47. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +332 -0
  48. package/eventcatalog/src/features/studio/components/Comments.tsx +455 -0
  49. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +217 -0
  50. package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
  51. package/eventcatalog/src/features/studio/components/JoinForm.tsx +58 -0
  52. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +218 -0
  53. package/eventcatalog/src/features/studio/components/Presence.tsx +284 -0
  54. package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +75 -0
  55. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +760 -0
  56. package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +79 -0
  57. package/eventcatalog/src/features/studio/components/icons.ts +48 -0
  58. package/eventcatalog/src/features/studio/components/status.ts +22 -0
  59. package/eventcatalog/src/features/studio/edges.ts +54 -0
  60. package/eventcatalog/src/features/studio/flow-state.ts +92 -0
  61. package/eventcatalog/src/features/studio/grouping.ts +133 -0
  62. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
  63. package/eventcatalog/src/features/studio/hooks/presence-store.ts +115 -0
  64. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +337 -0
  65. package/eventcatalog/src/features/studio/hooks/use-comments.ts +47 -0
  66. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +99 -0
  67. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +423 -0
  68. package/eventcatalog/src/features/studio/layout.ts +39 -0
  69. package/eventcatalog/src/features/studio/levels.ts +216 -0
  70. package/eventcatalog/src/features/studio/node-types.ts +173 -0
  71. package/eventcatalog/src/features/studio/pages/[id].astro +38 -0
  72. package/eventcatalog/src/features/studio/pages/index.astro +8 -0
  73. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  74. package/eventcatalog/src/features/studio/placement.ts +197 -0
  75. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +475 -0
  76. package/eventcatalog/src/features/studio/server/runtime.ts +215 -0
  77. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  78. package/eventcatalog/src/features/studio/studio.css +89 -0
  79. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  80. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +9 -2
  81. package/eventcatalog/src/utils/feature.ts +4 -0
  82. package/eventcatalog/src/utils/trial.ts +1 -1
  83. package/package.json +9 -2
  84. package/eventcatalog/public/studio-bg.png +0 -0
  85. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  86. package/eventcatalog/src/pages/studio.astro +0 -316
@@ -0,0 +1,269 @@
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 { 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="block truncate text-sm font-semibold">{canvas.title ?? 'Untitled canvas'}</span>
105
+ <span className="text-xs text-[rgb(var(--ec-page-text-muted))]">
106
+ {canvas.nodeCount} {canvas.nodeCount === 1 ? 'node' : 'nodes'}
107
+ {canvas.openComments > 0 && ` · ${canvas.openComments} open comments`}
108
+ </span>
109
+ </span>
110
+ {canvas.people.length > 0 && (
111
+ <span
112
+ className={`flex shrink-0 items-center gap-1 text-xs ${STATUS.success.text}`}
113
+ title={canvas.people.join(', ')}
114
+ >
115
+ <Users size={14} /> {canvas.people.length} here now
116
+ </span>
117
+ )}
118
+ </button>
119
+ ))}
120
+ </div>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ function CanvasApp() {
126
+ const { app, payload, setPayload, error, hostContext } = useMcpAppView(VIEW_OPTIONS);
127
+ const [catalog, setCatalog] = useState<CanvasCatalog>({ resources: [], relations: [] });
128
+ const [selection, setSelection] = useState<SelectedNode[]>([]);
129
+ const [loadError, setLoadError] = useState<string | null>(null);
130
+ const canvas = payload?.view === 'canvas' ? payload : undefined;
131
+
132
+ const show = useCallback(
133
+ (load: Promise<CanvasPayload>) =>
134
+ load.then(setPayload).catch((cause) => setLoadError(String((cause as Error)?.message ?? cause))),
135
+ [setPayload]
136
+ );
137
+ const openCanvas = (canvasId?: string) => app && show(loadPayload(app, CANVAS_VIEW_TOOL, canvasId ? { canvasId } : {}));
138
+
139
+ // Deep links (ChatGPT's `openai/deepLink`): /canvas/<id> opens that canvas
140
+ const deepLink = (hostContext as Record<string, { url?: string } | undefined> | undefined)?.['openai/deepLink']?.url;
141
+ useEffect(() => {
142
+ const canvasId = deepLink?.match(DEEP_LINK)?.[1];
143
+ if (app && canvasId && canvasId !== canvas?.canvasId) void show(loadPayload(app, CANVAS_VIEW_TOOL, { canvasId }));
144
+ }, [app, deepLink]);
145
+
146
+ // Chats may not let the view open the collaboration WebSocket (sandbox CSP, local network rules...):
147
+ // then it syncs through the MCP server, the same way the model's tools reach the canvas
148
+ const syncViaTools = useMemo<ToolSync | undefined>(
149
+ () =>
150
+ app
151
+ ? async (request) => {
152
+ const result = await app.callServerTool({ name: CANVAS_SYNC_TOOL, arguments: request });
153
+ if (result.isError || !result.structuredContent) throw new Error('Sync failed');
154
+ return result.structuredContent as SyncResponse;
155
+ }
156
+ : undefined,
157
+ [app]
158
+ );
159
+
160
+ // The catalog (to drag resources in) is loaded separately, to keep tool results small
161
+ useEffect(() => {
162
+ if (!app || !canvas) return;
163
+ app
164
+ .callServerTool({ name: CANVAS_CATALOG_TOOL, arguments: {} })
165
+ .then((result) => {
166
+ const loaded = result.structuredContent as CanvasCatalog | undefined;
167
+ if (loaded?.resources) setCatalog(loaded);
168
+ })
169
+ .catch(() => {});
170
+ }, [app, canvas?.canvasId]);
171
+
172
+ // Model context (replaced on each update): which canvas is open as background context the user doesn't see,
173
+ // and what they've selected as an attachment they can see (and remove), titled with the names
174
+ useEffect(() => {
175
+ if (!app || !payload || !app.getHostCapabilities()?.updateModelContext) return;
176
+ const background = {
177
+ type: 'text' as const,
178
+ text: canvas
179
+ ? `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.`
180
+ : 'The user is looking at their canvases, to open one or start one.',
181
+ annotations: { audience: ['assistant' as const] },
182
+ };
183
+ const selected = selection.length
184
+ ? [
185
+ {
186
+ type: 'text' as const,
187
+ text: `Selected on the canvas: ${selection
188
+ .map(
189
+ (node) =>
190
+ `${node.name} (${node.type}${node.catalogResource ? `, catalog ${node.catalogResource}` : ''}, nodeId ${node.id})`
191
+ )
192
+ .join('; ')}`,
193
+ _meta: { 'openai/title': selection.map((node) => node.name).join(', ') },
194
+ },
195
+ ]
196
+ : [];
197
+ app.updateModelContext({ content: [background, ...selected] }).catch(() => {});
198
+ }, [app, payload, selection]);
199
+
200
+ if (error || loadError) return <p className="p-4 text-sm">{error ?? loadError}</p>;
201
+ if (!payload) return <p className="p-4 text-sm text-[rgb(var(--ec-page-text-muted))]">Opening the canvas…</p>;
202
+
203
+ const fullscreen = hostContext?.displayMode === 'fullscreen';
204
+ const canToggle = hostContext?.availableDisplayModes?.includes(fullscreen ? 'inline' : 'fullscreen');
205
+ const iconButton =
206
+ '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)]';
207
+
208
+ // No join form inside a chat: use the name the agent passed, or name them after the chat they're in
209
+ const hostName = app?.getHostVersion()?.name;
210
+ const userName = (view: CanvasView) =>
211
+ view.userName ?? `${hostName ? hostName.replace(/^\w/, (letter) => letter.toUpperCase()) : 'Chat'} user`;
212
+
213
+ return (
214
+ <div className="flex flex-col" style={{ height: fullscreen ? '100vh' : INLINE_HEIGHT }}>
215
+ <div className="flex items-center justify-between border-b px-3 py-1.5 border-[rgb(var(--ec-page-border))]">
216
+ <span className="flex min-w-0 items-center gap-1">
217
+ {canvas && (
218
+ <button className={iconButton} title="All canvases" onClick={() => void openCanvas()}>
219
+ <ArrowLeft size={14} />
220
+ </button>
221
+ )}
222
+ <span className="truncate text-sm font-semibold">{canvas ? (canvas.title ?? 'Canvas') : 'Canvases'}</span>
223
+ </span>
224
+ <span className="flex items-center gap-1">
225
+ {canvas && (
226
+ <button className={iconButton} onClick={() => app?.openLink({ url: canvas.canvasUrl })}>
227
+ <ExternalLink size={14} />
228
+ Open in EventCatalog
229
+ </button>
230
+ )}
231
+ {canToggle && (
232
+ <button
233
+ className={iconButton}
234
+ title={fullscreen ? 'Exit full screen' : 'Full screen'}
235
+ onClick={() => app?.requestDisplayMode({ mode: fullscreen ? 'inline' : 'fullscreen' })}
236
+ >
237
+ {fullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
238
+ </button>
239
+ )}
240
+ </span>
241
+ </div>
242
+ <div className="min-h-0 flex-1 overflow-y-auto">
243
+ {canvas ? (
244
+ <StudioDesigner
245
+ key={canvas.canvasId}
246
+ canvasId={canvas.canvasId}
247
+ socketUrl={canvas.socketUrl}
248
+ shareUrl={canvas.canvasUrl}
249
+ mcpUrl={canvas.mcpUrl}
250
+ syncViaTools={syncViaTools}
251
+ defaultName={userName(canvas)}
252
+ resources={catalog.resources}
253
+ relations={catalog.relations}
254
+ onSelectionChange={setSelection}
255
+ followChanges
256
+ />
257
+ ) : (
258
+ <CanvasPicker
259
+ home={payload as CanvasHome}
260
+ onOpen={(canvasId) => void openCanvas(canvasId)}
261
+ onCreate={(title) => app && void show(loadPayload(app, 'openCanvas', { title }))}
262
+ />
263
+ )}
264
+ </div>
265
+ </div>
266
+ );
267
+ }
268
+
269
+ mountView(CanvasApp, [visualiserStyles, styles].join('\n'));