@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,284 @@
1
+ import { Fragment, memo, useEffect, useLayoutEffect, useRef, useSyncExternalStore, type MutableRefObject } from 'react';
2
+ import { useInternalNode, useStore, useStoreApi, ViewportPortal, type XYPosition } from '@xyflow/react';
3
+ import { Bot, Eye, EyeOff } from 'lucide-react';
4
+ import { sizeOf } from '../grouping';
5
+ import { usePeople, type Peer, type PresenceStore } from '../hooks/presence-store';
6
+ import { useFollowCamera } from '../hooks/use-follow-camera';
7
+
8
+ /**
9
+ * Other people on the canvas: their pointers, selections and the connections they're dragging. Drawn in the
10
+ * canvas's own coordinates (React Flow's viewport portal), so panning and zooming move them without
11
+ * re-rendering, and counter-scaled so they stay the same size on screen. Each person's pointer and marks follow
12
+ * them on their own, so one pointer moving doesn't re-render anyone else's.
13
+ */
14
+ export const RemotePresence = memo(function RemotePresence({
15
+ store,
16
+ clientId,
17
+ }: {
18
+ store: PresenceStore | null;
19
+ clientId: number | null;
20
+ }) {
21
+ const people = usePeople(store);
22
+ const zoom = useStore((state) => state.transform[2]);
23
+ const scale = 1 / zoom;
24
+ const others = store ? people.filter((peer) => peer.clientId !== clientId) : [];
25
+
26
+ return (
27
+ <ViewportPortal>
28
+ {/* Above nodes (selected ones are raised to 1000) */}
29
+ <div className="pointer-events-none absolute left-0 top-0" style={{ zIndex: 2000 }}>
30
+ {others.map((peer) => (
31
+ <Fragment key={peer.clientId}>
32
+ <PeerMarks store={store!} clientId={peer.clientId} scale={scale} />
33
+ {peer.pointer && <PeerPointer store={store!} peer={peer} scale={scale} />}
34
+ </Fragment>
35
+ ))}
36
+ </div>
37
+ </ViewportPortal>
38
+ );
39
+ });
40
+
41
+ const findPeer = (store: PresenceStore, clientId: number) => store.getPeers().find((peer) => peer.clientId === clientId);
42
+
43
+ /** What a peer's marks show: re-rendered only when it changes */
44
+ const marksKey = (peer: Peer | undefined) =>
45
+ peer
46
+ ? [
47
+ peer.selection.join(','),
48
+ peer.connecting && peer.pointer ? [peer.connecting.x, peer.connecting.y, peer.pointer.x, peer.pointer.y].join(',') : '',
49
+ ].join('|')
50
+ : '';
51
+
52
+ /** Someone's selection, and the connection they're dragging (a dashed line from where it starts to their pointer) */
53
+ const PeerMarks = memo(function PeerMarks({ store, clientId, scale }: { store: PresenceStore; clientId: number; scale: number }) {
54
+ useSyncExternalStore(store.subscribe, () => marksKey(findPeer(store, clientId)));
55
+ const peer = findPeer(store, clientId);
56
+ if (!peer) return null;
57
+ return (
58
+ <>
59
+ {peer.selection.map((id) => (
60
+ <SelectionOutline key={id} nodeId={id} name={peer.name} color={peer.color} scale={scale} />
61
+ ))}
62
+ {peer.pointer && peer.connecting && (
63
+ <svg className="absolute left-0 top-0 overflow-visible" width={1} height={1}>
64
+ <line
65
+ x1={peer.connecting.x}
66
+ y1={peer.connecting.y}
67
+ x2={peer.pointer.x}
68
+ y2={peer.pointer.y}
69
+ stroke={peer.color}
70
+ strokeWidth={2 * scale}
71
+ strokeDasharray={`${6 * scale} ${4 * scale}`}
72
+ />
73
+ <circle cx={peer.connecting.x} cy={peer.connecting.y} r={4 * scale} fill={peer.color} />
74
+ </svg>
75
+ )}
76
+ </>
77
+ );
78
+ });
79
+
80
+ /** Longest a pointer takes to glide to where it was last seen (updates come about every 33 ms) */
81
+ const MAX_GLIDE_MS = 120;
82
+
83
+ /**
84
+ * Someone's pointer, gliding from where it was to each new position over the time since the last one (so a
85
+ * steady stream of updates looks like steady movement). Moved with a transform outside React: no re-renders.
86
+ */
87
+ const PeerPointer = memo(function PeerPointer({ store, peer, scale }: { store: PresenceStore; peer: Peer; scale: number }) {
88
+ const ref = useRef<HTMLDivElement>(null);
89
+ const shown = useRef<XYPosition | null>(null);
90
+ const scaleRef = useRef(scale);
91
+ scaleRef.current = scale;
92
+
93
+ const draw = () => {
94
+ if (ref.current && shown.current)
95
+ ref.current.style.transform = `translate(${shown.current.x}px, ${shown.current.y}px) scale(${scaleRef.current})`;
96
+ };
97
+
98
+ useLayoutEffect(draw, [scale]);
99
+
100
+ useLayoutEffect(() => {
101
+ let target: XYPosition | null = null;
102
+ let from: XYPosition = { x: 0, y: 0 };
103
+ let start = 0;
104
+ let duration = 0;
105
+ let lastSeen = 0;
106
+ let frame = 0;
107
+
108
+ const step = (now: number) => {
109
+ const t = Math.min(1, (now - start) / duration);
110
+ if (target) shown.current = { x: from.x + (target.x - from.x) * t, y: from.y + (target.y - from.y) * t };
111
+ draw();
112
+ frame = t < 1 ? requestAnimationFrame(step) : 0;
113
+ };
114
+
115
+ const follow = () => {
116
+ const pointer = findPeer(store, peer.clientId)?.pointer;
117
+ if (!pointer || (target && pointer.x === target.x && pointer.y === target.y)) return;
118
+ const now = performance.now();
119
+ target = pointer;
120
+ if (!shown.current) {
121
+ shown.current = pointer;
122
+ lastSeen = now;
123
+ return draw();
124
+ }
125
+ from = shown.current;
126
+ start = now;
127
+ duration = Math.min(MAX_GLIDE_MS, Math.max(16, now - lastSeen));
128
+ lastSeen = now;
129
+ if (!frame) frame = requestAnimationFrame(step);
130
+ };
131
+
132
+ follow();
133
+ const unsubscribe = store.subscribe(follow);
134
+ return () => {
135
+ unsubscribe();
136
+ cancelAnimationFrame(frame);
137
+ };
138
+ }, [store, peer.clientId]);
139
+
140
+ return (
141
+ <div ref={ref} className="absolute left-0 top-0 will-change-transform" style={{ transformOrigin: '0 0' }}>
142
+ {/* The pointer's outline is a light border on any background */}
143
+ <svg width="18" height="18" viewBox="0 0 24 24" fill={peer.color} stroke="white" strokeWidth="1.5">
144
+ <path d="M4 2l16 9-7 2-3 7z" />
145
+ </svg>
146
+ <span
147
+ className="absolute left-4 top-4 flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-[11px] font-medium text-white"
148
+ style={{ backgroundColor: peer.color }}
149
+ >
150
+ {peer.agent && <Bot size={12} />}
151
+ {peer.name}
152
+ {peer.activity && <span className="font-normal opacity-90">· {peer.activity}</span>}
153
+ </span>
154
+ </div>
155
+ );
156
+ });
157
+
158
+ /** Someone's selection around a node: follows that node only, so other nodes moving don't re-render it */
159
+ const SelectionOutline = memo(function SelectionOutline({
160
+ nodeId,
161
+ name,
162
+ color,
163
+ scale,
164
+ }: {
165
+ nodeId: string;
166
+ name: string;
167
+ color: string;
168
+ scale: number;
169
+ }) {
170
+ const node = useInternalNode(nodeId);
171
+ if (!node) return null;
172
+ const { width, height } = sizeOf(node);
173
+ const { x, y } = node.internals.positionAbsolute;
174
+ return (
175
+ <div
176
+ className="absolute"
177
+ style={{
178
+ left: x - 4 * scale,
179
+ top: y - 4 * scale,
180
+ width: width + 8 * scale,
181
+ height: height + 8 * scale,
182
+ border: `${2 * scale}px solid ${color}`,
183
+ borderRadius: 8 * scale,
184
+ }}
185
+ >
186
+ <span
187
+ className="absolute right-0 top-0 whitespace-nowrap rounded px-1.5 text-[10px] leading-4 text-white"
188
+ style={{ backgroundColor: color, transform: `translateY(-100%) scale(${scale})`, transformOrigin: 'bottom right' }}
189
+ >
190
+ {name}
191
+ </span>
192
+ </div>
193
+ );
194
+ });
195
+
196
+ /**
197
+ * Shares what we're looking at, so others can jump to it: the canvas position in the middle of the screen,
198
+ * and the zoom. Follows React Flow's store (people's moves and code's, e.g. fit view) without rendering.
199
+ */
200
+ export const ViewportSharer = memo(function ViewportSharer({
201
+ onChange,
202
+ }: {
203
+ onChange: (viewport: { x: number; y: number; zoom: number }) => void;
204
+ }) {
205
+ const store = useStoreApi();
206
+ useEffect(() => {
207
+ let timer: ReturnType<typeof setTimeout> | undefined;
208
+ const share = () => {
209
+ const {
210
+ transform: [x, y, zoom],
211
+ width,
212
+ height,
213
+ } = store.getState();
214
+ onChange({ x: (width / 2 - x) / zoom, y: (height / 2 - y) / zoom, zoom });
215
+ };
216
+ const unsubscribe = store.subscribe((state, previous) => {
217
+ if (state.transform === previous.transform && state.width === previous.width && state.height === previous.height) return;
218
+ clearTimeout(timer);
219
+ timer = setTimeout(share, 150);
220
+ });
221
+ share();
222
+ return () => {
223
+ unsubscribe();
224
+ clearTimeout(timer);
225
+ };
226
+ }, [store, onChange]);
227
+ return null;
228
+ });
229
+
230
+ /**
231
+ * Agents at work: who's working (with a button to follow them or stop), and the camera following them while
232
+ * they work (on by default inside a chat). `pause` is set for whoever moves the camera themselves.
233
+ */
234
+ export const AgentActivity = memo(function AgentActivity({
235
+ store,
236
+ clientId,
237
+ enabled,
238
+ following,
239
+ onToggleFollowing,
240
+ pauseRef,
241
+ }: {
242
+ store: PresenceStore | null;
243
+ clientId: number | null;
244
+ /** Off in the read only levels */
245
+ enabled: boolean;
246
+ following: boolean;
247
+ onToggleFollowing: () => void;
248
+ pauseRef: MutableRefObject<() => void>;
249
+ }) {
250
+ const people = usePeople(store);
251
+ const working = people.filter((peer) => peer.clientId !== clientId && peer.agent && peer.pointer);
252
+ const camera = useFollowCamera({
253
+ enabled: following && enabled,
254
+ agentsWorking: working.length > 0,
255
+ getAgents: () => (store?.getPeers() ?? []).filter((peer: Peer) => peer.clientId !== clientId && peer.agent && peer.pointer),
256
+ });
257
+ pauseRef.current = camera.pause;
258
+
259
+ if (!enabled || working.length === 0) return null;
260
+ return (
261
+ <div className="absolute left-1/2 top-16 z-10 flex -translate-x-1/2 items-center gap-2 rounded-full border px-3 py-1 text-xs shadow-sm bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))]">
262
+ <span className="flex -space-x-1">
263
+ {working.map((agent) => (
264
+ <span
265
+ key={agent.clientId}
266
+ className="h-2.5 w-2.5 rounded-full ring-2 ring-[rgb(var(--ec-card-bg))]"
267
+ style={{ backgroundColor: agent.color }}
268
+ />
269
+ ))}
270
+ </span>
271
+ <span>
272
+ {following ? 'Following' : ''} <span className="font-semibold">{working.map((agent) => agent.name).join(', ')}</span>
273
+ {following ? '' : ` ${working.length === 1 ? 'is' : 'are'} working`}
274
+ </span>
275
+ <button
276
+ onClick={onToggleFollowing}
277
+ className="flex items-center gap-1 rounded-full px-2 py-0.5 font-medium text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))]"
278
+ >
279
+ {following ? <EyeOff size={12} /> : <Eye size={12} />}
280
+ {following ? 'Stop' : 'Follow'}
281
+ </button>
282
+ </div>
283
+ );
284
+ });
@@ -0,0 +1,75 @@
1
+ import { memo } from 'react';
2
+ import type { Node } from '@xyflow/react';
3
+ import { ExternalLink, Trash2 } from 'lucide-react';
4
+ import { getCatalogLink } from '../catalog';
5
+ import { getNodeLabel, getResource, updateResource } from '../node-types';
6
+ import type { UpdateNodeData } from './canvas-nodes';
7
+ import { STATUS } from './status';
8
+
9
+ const inputClass =
10
+ 'w-full rounded-md border px-2 py-1.5 text-sm bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]';
11
+
12
+ /** The selected node's name and summary (read only for catalog resources: those are changed in the catalog) */
13
+ export default memo(function PropertiesPanel({
14
+ node,
15
+ onChange,
16
+ onDelete,
17
+ }: {
18
+ /** What's shown of the node (not its position, so dragging it doesn't re-render the panel) */
19
+ node: Pick<Node, 'id' | 'type' | 'data'>;
20
+ onChange: UpdateNodeData;
21
+ onDelete: (ids: string[]) => void;
22
+ }) {
23
+ const resource = getResource(node.type, node.data);
24
+ const link = getCatalogLink(node);
25
+ const setField = (field: string, value: string) =>
26
+ onChange(node.id, (data) => updateResource(node.type, data, { [field]: value }));
27
+
28
+ return (
29
+ <aside className="w-72 shrink-0 space-y-4 overflow-y-auto border-l p-4 bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))]">
30
+ <h2 className="text-sm font-semibold">{getNodeLabel(node.type)}</h2>
31
+ {link && (
32
+ <div className="space-y-2 rounded-md border p-3 text-xs border-[rgb(var(--ec-page-border))]">
33
+ <p className="text-[rgb(var(--ec-page-text-muted))]">
34
+ Linked to <span className="font-medium text-[rgb(var(--ec-page-text))]">{String(resource.name)}</span> v{link.version}{' '}
35
+ in the catalog. Edit it in the catalog to change its name or summary.
36
+ </p>
37
+ <a
38
+ href={link.url}
39
+ target="_blank"
40
+ rel="noreferrer"
41
+ className="flex items-center gap-1 text-[rgb(var(--ec-accent))] hover:underline"
42
+ >
43
+ View in catalog <ExternalLink size={12} />
44
+ </a>
45
+ </div>
46
+ )}
47
+ <label className="block space-y-1 text-xs text-[rgb(var(--ec-page-text-muted))]">
48
+ Name
49
+ <input
50
+ value={String(resource.name ?? '')}
51
+ readOnly={!!link}
52
+ onChange={(e) => setField('name', e.target.value)}
53
+ className={inputClass}
54
+ />
55
+ </label>
56
+ <label className="block space-y-1 text-xs text-[rgb(var(--ec-page-text-muted))]">
57
+ Summary
58
+ <textarea
59
+ rows={4}
60
+ value={String(resource.summary ?? '')}
61
+ readOnly={!!link}
62
+ onChange={(e) => setField('summary', e.target.value)}
63
+ className={inputClass}
64
+ />
65
+ </label>
66
+ <button
67
+ onClick={() => onDelete([node.id])}
68
+ className={`flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs ${STATUS.danger.text} ${STATUS.danger.hover}`}
69
+ >
70
+ <Trash2 size={14} />
71
+ Delete
72
+ </button>
73
+ </aside>
74
+ );
75
+ });