@eventcatalog/core 5.0.0-beta.11 → 5.0.0-beta.13
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/README.md +1 -1
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +4 -4
- package/dist/catalog-runtime.cjs +1 -1
- package/dist/{chunk-MFEMKQZ6.js → chunk-BO2HHVZ3.js} +1 -1
- package/dist/{chunk-4YFM63DS.js → chunk-JPT3QC5V.js} +1 -1
- package/dist/{chunk-76DAAJLV.js → chunk-PLTLW45Q.js} +5 -5
- package/dist/{chunk-WM4QYMIA.js → chunk-T75DOVFP.js} +1 -1
- package/dist/{chunk-LQZ7IG7B.js → chunk-XA5W6DXX.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +2 -2
- package/dist/eventcatalog.js +14 -14
- package/dist/federation/federate.js +3 -3
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- 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/manifest.json +3 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +42 -3
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
- package/eventcatalog/src/components/NavigationProgress.astro +7 -0
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +3 -3
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +3 -1
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +7 -5
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +132 -70
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +71 -71
- package/eventcatalog/src/features/studio/agent-choreography.ts +13 -10
- package/eventcatalog/src/features/studio/agents.ts +28 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +64 -15
- package/eventcatalog/src/features/studio/api/client.ts +6 -1
- package/eventcatalog/src/features/studio/canvas-actions.ts +160 -34
- package/eventcatalog/src/features/studio/canvas-preview.ts +1 -2
- package/eventcatalog/src/features/studio/catalog.ts +16 -14
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +263 -115
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +15 -14
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +272 -119
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +1 -2
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +28 -44
- package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +101 -26
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +119 -230
- 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 +52 -45
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +277 -141
- package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1286 -411
- 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 +19 -2
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +9 -3
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +2 -2
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +8 -3
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +223 -30
- package/eventcatalog/src/features/studio/identity.ts +23 -1
- package/eventcatalog/src/features/studio/limits.ts +2 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +2 -1
- package/eventcatalog/src/features/studio/pages/index.astro +2 -2
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +8 -23
- package/eventcatalog/src/features/studio/server/runtime.ts +16 -9
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
- package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
- package/package.json +3 -3
- package/eventcatalog/src/features/studio/canvas-doc.ts +0 -414
- package/eventcatalog/src/features/studio/clipboard.ts +0 -97
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +0 -154
- package/eventcatalog/src/features/studio/components/Comments.tsx +0 -770
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +0 -145
- package/eventcatalog/src/features/studio/components/DragGhost.tsx +0 -115
- package/eventcatalog/src/features/studio/components/Picture.tsx +0 -21
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +0 -212
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +0 -255
- package/eventcatalog/src/features/studio/components/icons.ts +0 -53
- package/eventcatalog/src/features/studio/components/status.ts +0 -53
- package/eventcatalog/src/features/studio/edges.ts +0 -56
- package/eventcatalog/src/features/studio/flow-state.ts +0 -92
- package/eventcatalog/src/features/studio/grouping.ts +0 -226
- package/eventcatalog/src/features/studio/layout.ts +0 -42
- package/eventcatalog/src/features/studio/levels.ts +0 -282
- package/eventcatalog/src/features/studio/node-types.ts +0 -205
- package/eventcatalog/src/features/studio/placement.ts +0 -197
- package/eventcatalog/src/features/studio/studio.css +0 -273
- package/dist/{chunk-KHACCTW2.js → chunk-ZEY2XUJN.js} +4 -4
|
@@ -15,8 +15,6 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
addNodes,
|
|
17
17
|
buildNode,
|
|
18
|
-
CANVAS_DELETED_REASON,
|
|
19
|
-
canvasDocumentName,
|
|
20
18
|
connectNodes,
|
|
21
19
|
deleteEdges,
|
|
22
20
|
deleteNodes as deleteNodesInDoc,
|
|
@@ -25,7 +23,9 @@ import {
|
|
|
25
23
|
fitGroupToChildren,
|
|
26
24
|
getCanvasMaps,
|
|
27
25
|
moveNode,
|
|
26
|
+
placeNode,
|
|
28
27
|
changesNodesOrEdges,
|
|
28
|
+
getStructureKey,
|
|
29
29
|
getCanvasStatus,
|
|
30
30
|
readMeta,
|
|
31
31
|
reopenIfDecided,
|
|
@@ -34,16 +34,41 @@ import {
|
|
|
34
34
|
setMeta,
|
|
35
35
|
type Author,
|
|
36
36
|
type CanvasStatus,
|
|
37
|
+
type DiagramLevels,
|
|
38
|
+
type LevelPlace,
|
|
39
|
+
readLevelLayout,
|
|
40
|
+
setLevelPlaces,
|
|
37
41
|
type StatusChange,
|
|
38
|
-
|
|
42
|
+
setNodeParents,
|
|
39
43
|
type RenderedSizes,
|
|
40
44
|
updateNodeData as updateNodeDataInDoc,
|
|
41
|
-
|
|
45
|
+
keepDragged,
|
|
46
|
+
LOCAL_EDGE_KEYS,
|
|
47
|
+
LOCAL_NODE_KEYS,
|
|
48
|
+
patchShared,
|
|
49
|
+
withPositions,
|
|
50
|
+
createSmoother,
|
|
51
|
+
containerFor,
|
|
52
|
+
toRelativePosition,
|
|
53
|
+
type NodePreview,
|
|
54
|
+
type LayoutResult,
|
|
55
|
+
canGoInContainer,
|
|
56
|
+
getNodeDefinition,
|
|
57
|
+
isGroupType,
|
|
58
|
+
createHotspot as createHotspotInDoc,
|
|
59
|
+
updateHotspot as updateHotspotInDoc,
|
|
60
|
+
deleteHotspot as deleteHotspotInDoc,
|
|
61
|
+
readHotspots,
|
|
62
|
+
getLock,
|
|
63
|
+
setLock,
|
|
64
|
+
type Hotspot,
|
|
65
|
+
type Editing,
|
|
66
|
+
addThreads,
|
|
67
|
+
addHotspots,
|
|
68
|
+
type ModelCanvasContent,
|
|
69
|
+
} from '@eventcatalog/visualiser/studio';
|
|
70
|
+
import { CANVAS_DELETED_REASON, canvasDocumentName } from '../document-name';
|
|
42
71
|
import { applyLayout } from '../agent-choreography';
|
|
43
|
-
import { keepDragged, LOCAL_EDGE_KEYS, LOCAL_NODE_KEYS, patchShared, withPositions } from '../flow-state';
|
|
44
|
-
import { findGroupAtPoint, toRelativePosition, type NodePreview } from '../grouping';
|
|
45
|
-
import type { LayoutResult } from '../layout';
|
|
46
|
-
import { canGoInContainer, getNodeDefinition, isGroupType } from '../node-types';
|
|
47
72
|
import { startToolSync, type ToolSync } from '../tool-sync';
|
|
48
73
|
import { createPresenceSender, type PresenceSender } from './presence-sender';
|
|
49
74
|
import { createPresenceStore, type PresenceStore } from './presence-store';
|
|
@@ -67,6 +92,14 @@ const LOCAL_ORIGIN = Symbol('studio-local');
|
|
|
67
92
|
export const BROWSER_AGENT_ORIGIN = Symbol('studio-browser-agent');
|
|
68
93
|
/** Status changes: synced, but not undone (undo takes back edits, not decisions about the canvas) */
|
|
69
94
|
const STATUS_ORIGIN = Symbol('studio-status');
|
|
95
|
+
/** A diagram file opened into a new canvas: what it starts with, synced, not undone (nothing to go back to) */
|
|
96
|
+
const OPEN_ORIGIN = Symbol('studio-open');
|
|
97
|
+
/** Where L1 and L2 put what's new on them, kept so it stays there: synced, not undone (nobody did it) */
|
|
98
|
+
const LEVEL_PLACEMENT_ORIGIN = Symbol('studio-level-placement');
|
|
99
|
+
|
|
100
|
+
const NO_LEVEL_LAYOUTS = { 1: new Map(), 2: new Map() } as const;
|
|
101
|
+
/** What someone changing a note or text shows the others as they change it (see `editing` in their presence) */
|
|
102
|
+
const EDITED_FIELDS = ['text', 'fontSize', 'angle'] as const;
|
|
70
103
|
|
|
71
104
|
export type Status = 'connecting' | 'connected' | 'disconnected';
|
|
72
105
|
export type Transport = 'websocket' | 'tools';
|
|
@@ -90,6 +123,7 @@ export function useStudioFlow({
|
|
|
90
123
|
name,
|
|
91
124
|
color,
|
|
92
125
|
picture,
|
|
126
|
+
agent,
|
|
93
127
|
syncViaTools,
|
|
94
128
|
onLayout,
|
|
95
129
|
onContainerResize,
|
|
@@ -101,6 +135,8 @@ export function useStudioFlow({
|
|
|
101
135
|
color: string;
|
|
102
136
|
/** From your sign-in provider, when you're signed in */
|
|
103
137
|
picture?: string;
|
|
138
|
+
/** You're an AI agent here (e.g. a chat's canvas view, which joins as the chat's agent) */
|
|
139
|
+
agent?: boolean;
|
|
104
140
|
/** Sync through MCP tool calls when the WebSocket can't connect (e.g. inside a chat's sandbox) */
|
|
105
141
|
syncViaTools?: ToolSync;
|
|
106
142
|
/** Someone laid the canvas out (us included): called as the new positions arrive, so they can glide there */
|
|
@@ -121,12 +157,25 @@ export function useStudioFlow({
|
|
|
121
157
|
const [doc, setDoc] = useState<Y.Doc | null>(null);
|
|
122
158
|
const [history, setHistory] = useState({ canUndo: false, canRedo: false });
|
|
123
159
|
const [title, setTitle] = useState<string | undefined>();
|
|
160
|
+
// L1 and L2 of the diagram the canvas was opened from, if it was
|
|
161
|
+
const [diagramLevels, setDiagramLevels] = useState<DiagramLevels | undefined>();
|
|
162
|
+
// What's on the canvas and how it's connected (getStructureKey): worked out when the document's nodes or edges
|
|
163
|
+
// change, which drags don't do (they're in presence until the drop)
|
|
164
|
+
const [structureKey, setStructureKey] = useState('');
|
|
165
|
+
// Where things are on L1 and L2, as people arranged them
|
|
166
|
+
const [levelLayouts, setLevelLayouts] = useState<Record<1 | 2, ReadonlyMap<string, LevelPlace>>>(NO_LEVEL_LAYOUTS);
|
|
124
167
|
const [canvasStatus, setCanvasStatusState] = useState<{ status: CanvasStatus; history: StatusChange[] }>(NEW_CANVAS_STATUS);
|
|
125
168
|
const [transport, setTransport] = useState<Transport>('websocket');
|
|
126
169
|
// Someone deleted the canvas: the server closed it for everyone, and won't open it again
|
|
127
170
|
const [deleted, setDeleted] = useState(false);
|
|
171
|
+
// What's flagged on the canvas (risks, problems, the key parts), on nodes or at places
|
|
172
|
+
const [hotspots, setHotspots] = useState<Hotspot[]>([]);
|
|
173
|
+
// Who locked the canvas (for everyone on it), if it's locked
|
|
174
|
+
const [lockedBy, setLockedBy] = useState<string | null>(null);
|
|
175
|
+
// The server's copy of the canvas has arrived (what's on it is known, e.g. that it's empty)
|
|
176
|
+
const [synced, setSynced] = useState(false);
|
|
128
177
|
const user = useRef<Author>({ name, color });
|
|
129
|
-
user.current = { name, color, ...(picture && { picture }) };
|
|
178
|
+
user.current = { name, color, ...(picture && { picture }), ...(agent && { agent }) };
|
|
130
179
|
const onLayoutRef = useRef(onLayout);
|
|
131
180
|
onLayoutRef.current = onLayout;
|
|
132
181
|
const onContainerResizeRef = useRef(onContainerResize);
|
|
@@ -139,16 +188,41 @@ export function useStudioFlow({
|
|
|
139
188
|
setEdges([]);
|
|
140
189
|
|
|
141
190
|
const doc = new Y.Doc();
|
|
142
|
-
const {
|
|
191
|
+
const {
|
|
192
|
+
nodes: yNodes,
|
|
193
|
+
edges: yEdges,
|
|
194
|
+
threads: yThreads,
|
|
195
|
+
meta: yMeta,
|
|
196
|
+
levelLayouts: yLevelLayouts,
|
|
197
|
+
hotspots: yHotspots,
|
|
198
|
+
} = getCanvasMaps(doc);
|
|
199
|
+
const syncLevelLayouts = (event: Y.YMapEvent<LevelPlace>) => {
|
|
200
|
+
const changed = new Set([...event.keysChanged].map((key) => Number(key.split(':')[0]) as 1 | 2));
|
|
201
|
+
setLevelLayouts((current) => {
|
|
202
|
+
const next = { ...current };
|
|
203
|
+
changed.forEach((level) => (next[level] = readLevelLayout(doc, level)));
|
|
204
|
+
return next;
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
yLevelLayouts.observe(syncLevelLayouts);
|
|
208
|
+
setLevelLayouts(NO_LEVEL_LAYOUTS);
|
|
143
209
|
const syncMeta = (event: Y.YMapEvent<unknown>) => {
|
|
144
210
|
const meta = readMeta(doc);
|
|
145
211
|
setTitle(meta.title);
|
|
212
|
+
if (event.keysChanged.has('diagramLevels')) setDiagramLevels(meta.diagramLevels);
|
|
146
213
|
if (event.keysChanged.has('status') || event.keysChanged.has('statusHistory'))
|
|
147
214
|
setCanvasStatusState({ status: getCanvasStatus(meta), history: meta.statusHistory ?? [] });
|
|
148
215
|
if (event.keysChanged.has('layoutAt')) onLayoutRef.current?.();
|
|
216
|
+
if (event.keysChanged.has('lockedBy')) setLockedBy(getLock(doc));
|
|
149
217
|
};
|
|
150
218
|
yMeta.observe(syncMeta);
|
|
219
|
+
setLockedBy(null);
|
|
220
|
+
const syncHotspots = () => setHotspots(readHotspots(doc));
|
|
221
|
+
yHotspots.observe(syncHotspots);
|
|
222
|
+
setHotspots([]);
|
|
151
223
|
setTitle(undefined);
|
|
224
|
+
setStructureKey('');
|
|
225
|
+
setDiagramLevels(undefined);
|
|
152
226
|
setCanvasStatusState(NEW_CANVAS_STATUS);
|
|
153
227
|
// Changing an accepted (or rejected) canvas here makes it a draft again, for everyone. Changes from others arrive
|
|
154
228
|
// already reopened by whoever made them.
|
|
@@ -169,6 +243,12 @@ export function useStudioFlow({
|
|
|
169
243
|
);
|
|
170
244
|
};
|
|
171
245
|
doc.on('afterTransaction', reopenOnEdit);
|
|
246
|
+
const syncStructure = (transaction: Y.Transaction) => {
|
|
247
|
+
// Drops and resizes here only move and size things (putting something in a container isn't LOCAL_ORIGIN)
|
|
248
|
+
if (transaction.origin !== LOCAL_ORIGIN && changesNodesOrEdges(transaction))
|
|
249
|
+
setStructureKey(getStructureKey(Array.from(yNodes.values()), Array.from(yEdges.values())));
|
|
250
|
+
};
|
|
251
|
+
doc.on('afterTransaction', syncStructure);
|
|
172
252
|
// One presence for whichever connection is used
|
|
173
253
|
const awareness = new Awareness(doc);
|
|
174
254
|
awareness.setLocalStateField('user', user.current);
|
|
@@ -213,6 +293,7 @@ export function useStudioFlow({
|
|
|
213
293
|
// Open isn't connected until the server lets us in (onAuthenticated)
|
|
214
294
|
if (!stopToolSync) setStatus(status === 'connected' && !letIn ? 'connecting' : (status as Status));
|
|
215
295
|
},
|
|
296
|
+
onSynced: ({ state }) => setSynced(state),
|
|
216
297
|
onAuthenticated: () => {
|
|
217
298
|
socketConnected = letIn = true;
|
|
218
299
|
if (!stopToolSync) setStatus('connected');
|
|
@@ -232,6 +313,7 @@ export function useStudioFlow({
|
|
|
232
313
|
});
|
|
233
314
|
setTransport('websocket');
|
|
234
315
|
setDeleted(false);
|
|
316
|
+
setSynced(false);
|
|
235
317
|
|
|
236
318
|
// Back online, or back on the tab: reconnect now rather than when the next retry is due
|
|
237
319
|
const reconnect = () => {
|
|
@@ -249,7 +331,7 @@ export function useStudioFlow({
|
|
|
249
331
|
// A socket that isn't let in within a few seconds (blocked, or never answered) is given up for tool calls
|
|
250
332
|
if (syncViaTools) fallback = setTimeout(() => !socketConnected && syncThroughTools(), SOCKET_TIMEOUT_MS);
|
|
251
333
|
// Undo only takes back your own changes: other people's arrive with the provider as origin, agents' with theirs
|
|
252
|
-
const undoManager = new Y.UndoManager([yNodes, yEdges, yThreads], {
|
|
334
|
+
const undoManager = new Y.UndoManager([yNodes, yEdges, yThreads, yLevelLayouts, yHotspots], {
|
|
253
335
|
captureTimeout: 500,
|
|
254
336
|
trackedOrigins: new Set([null, LOCAL_ORIGIN]),
|
|
255
337
|
});
|
|
@@ -266,6 +348,13 @@ export function useStudioFlow({
|
|
|
266
348
|
// Where others are dragging nodes, from their presence, until they drop them
|
|
267
349
|
let remoteMoves = new Map<string, XYPosition>();
|
|
268
350
|
const settling = new Set<ReturnType<typeof setTimeout>>();
|
|
351
|
+
// Shown gliding, a step a frame: at most one update a frame however many people drag (each presence arriving
|
|
352
|
+
// was an update of its own: ~5 a frame with 7 people dragging), and smooth between their ~30 updates a second.
|
|
353
|
+
// Not what's been dropped since (its drop is in the document, see syncNodes).
|
|
354
|
+
const glide = createSmoother((shown) => {
|
|
355
|
+
const moved = new Map([...shown].filter(([id]) => remoteMoves.has(id)));
|
|
356
|
+
if (moved.size) setNodes((local) => withPositions(local, moved));
|
|
357
|
+
});
|
|
269
358
|
const syncMoves = (_: unknown, origin: unknown) => {
|
|
270
359
|
if (origin === 'local') return;
|
|
271
360
|
const next = new Map<string, XYPosition>();
|
|
@@ -273,10 +362,10 @@ export function useStudioFlow({
|
|
|
273
362
|
if (id === doc.clientID || !state.moving) return;
|
|
274
363
|
Object.entries(state.moving as Record<string, XYPosition>).forEach(([nodeId, position]) => next.set(nodeId, position));
|
|
275
364
|
});
|
|
276
|
-
const moved =
|
|
365
|
+
const moved = [...next].some(([id, { x, y }]) => remoteMoves.get(id)?.x !== x || remoteMoves.get(id)?.y !== y);
|
|
277
366
|
const dropped = [...remoteMoves.keys()].filter((id) => !next.has(id));
|
|
278
367
|
remoteMoves = next;
|
|
279
|
-
if (moved.
|
|
368
|
+
if (moved || dropped.length) glide.set(next);
|
|
280
369
|
if (dropped.length === 0) return;
|
|
281
370
|
// The drop is saved in the document. If it doesn't arrive, back to where the document has it.
|
|
282
371
|
const settle = setTimeout(() => {
|
|
@@ -291,6 +380,40 @@ export function useStudioFlow({
|
|
|
291
380
|
};
|
|
292
381
|
awareness.on('change', syncMoves);
|
|
293
382
|
|
|
383
|
+
// What others are writing on notes and text, and how they're scaling and turning text, as they do it (from their
|
|
384
|
+
// presence): shown on just those nodes, and back to what the document has when they're done (their result is
|
|
385
|
+
// written to the document before their presence is cleared, so it doesn't go back in between)
|
|
386
|
+
let remoteEdits = '';
|
|
387
|
+
let remoteEdited = new Set<string>();
|
|
388
|
+
const syncEdits = (_: unknown, origin: unknown) => {
|
|
389
|
+
if (origin === 'local') return;
|
|
390
|
+
const edits = new Map<string, Record<string, unknown>>();
|
|
391
|
+
awareness.getStates().forEach((state, id) => {
|
|
392
|
+
const editing = state.editing as Editing | null | undefined;
|
|
393
|
+
if (id === doc.clientID || !editing?.id) return;
|
|
394
|
+
const { id: nodeId, ...fields } = editing;
|
|
395
|
+
edits.set(nodeId, fields);
|
|
396
|
+
});
|
|
397
|
+
const key = JSON.stringify([...edits]);
|
|
398
|
+
if (key === remoteEdits) return;
|
|
399
|
+
remoteEdits = key;
|
|
400
|
+
const before = remoteEdited;
|
|
401
|
+
remoteEdited = new Set(edits.keys());
|
|
402
|
+
setNodes((local) => {
|
|
403
|
+
let next: Node[] | undefined;
|
|
404
|
+
local.forEach((node, index) => {
|
|
405
|
+
const edit = edits.get(node.id);
|
|
406
|
+
const savedData = before.has(node.id) ? yNodes.get(node.id)?.data : undefined;
|
|
407
|
+
const changes = edit ?? (savedData && Object.fromEntries(EDITED_FIELDS.map((field) => [field, savedData[field]])));
|
|
408
|
+
if (!changes || Object.entries(changes).every(([field, value]) => node.data[field] === value)) return;
|
|
409
|
+
next ??= [...local];
|
|
410
|
+
next[index] = { ...node, data: { ...node.data, ...changes } };
|
|
411
|
+
});
|
|
412
|
+
return next ?? local;
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
awareness.on('change', syncEdits);
|
|
416
|
+
|
|
294
417
|
// Only what changed is replaced, so React Flow re-renders just those nodes
|
|
295
418
|
const syncNodes = (event: Y.YMapEvent<Node>) => {
|
|
296
419
|
if (event.transaction.origin === LOCAL_ORIGIN) return;
|
|
@@ -326,12 +449,17 @@ export function useStudioFlow({
|
|
|
326
449
|
window.removeEventListener('online', reconnect);
|
|
327
450
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
328
451
|
awareness.off('change', syncMoves);
|
|
452
|
+
awareness.off('change', syncEdits);
|
|
453
|
+
glide.stop();
|
|
329
454
|
sender.destroy();
|
|
330
455
|
stopToolSync?.();
|
|
331
456
|
yNodes.unobserve(syncNodes);
|
|
457
|
+
yLevelLayouts.unobserve(syncLevelLayouts);
|
|
332
458
|
yEdges.unobserve(syncEdges);
|
|
333
459
|
yMeta.unobserve(syncMeta);
|
|
460
|
+
yHotspots.unobserve(syncHotspots);
|
|
334
461
|
doc.off('afterTransaction', reopenOnEdit);
|
|
462
|
+
doc.off('afterTransaction', syncStructure);
|
|
335
463
|
presenceStore.destroy();
|
|
336
464
|
undoManager.destroy();
|
|
337
465
|
provider.destroy();
|
|
@@ -384,10 +512,10 @@ export function useStudioFlow({
|
|
|
384
512
|
dragged.current.forEach((position, id) => moveNode(doc, id, position));
|
|
385
513
|
// Containers as they grew around what was dragged, and what else is in them (moved the other way if they grew up
|
|
386
514
|
// or left). Dragged nodes are where React Flow dropped them, already in their containers' grown coordinates.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
515
|
+
// Only what actually moved or grew is written (the preview has everything in those containers)
|
|
516
|
+
dragPreview.current.forEach(({ position, width, height }, id) =>
|
|
517
|
+
placeNode(doc, id, position, width !== undefined && height !== undefined ? { width, height } : undefined)
|
|
518
|
+
);
|
|
391
519
|
dragPreview.current = new Map();
|
|
392
520
|
dragged.current.clear();
|
|
393
521
|
const shared = sharedRef.current;
|
|
@@ -474,12 +602,18 @@ export function useStudioFlow({
|
|
|
474
602
|
* on the canvas, and returns edges to add with it (e.g. a catalog resource's relationships).
|
|
475
603
|
*/
|
|
476
604
|
const insertNode = useCallback(
|
|
477
|
-
(
|
|
605
|
+
(
|
|
606
|
+
type: string,
|
|
607
|
+
data: Record<string, unknown>,
|
|
608
|
+
center: XYPosition,
|
|
609
|
+
connect?: (id: string, existing: Node[]) => Edge[],
|
|
610
|
+
inside?: string
|
|
611
|
+
) => {
|
|
478
612
|
const built = buildNode(type, data, center);
|
|
479
613
|
withDoc((doc) => {
|
|
480
614
|
const existing = Array.from(getCanvasMaps(doc).nodes.values());
|
|
481
|
-
//
|
|
482
|
-
const group = canGoInContainer(type) ?
|
|
615
|
+
// In the container asked for, else the one it's dropped on
|
|
616
|
+
const group = canGoInContainer(type) ? containerFor(center, existing, inside) : undefined;
|
|
483
617
|
const node = group
|
|
484
618
|
? { ...built, parentId: group.id, position: toRelativePosition(built.position, group.id, existing) }
|
|
485
619
|
: built;
|
|
@@ -491,11 +625,11 @@ export function useStudioFlow({
|
|
|
491
625
|
[]
|
|
492
626
|
);
|
|
493
627
|
|
|
494
|
-
/** Add a node of a
|
|
628
|
+
/** Add a node of a component (its type, or its key in the components list), centred on a canvas position (in a container, if asked) */
|
|
495
629
|
const addNode = useCallback(
|
|
496
|
-
(
|
|
497
|
-
const definition = getNodeDefinition(
|
|
498
|
-
return definition ? insertNode(type, definition.createData(), center) : undefined;
|
|
630
|
+
(component: string, center: XYPosition, inside?: string) => {
|
|
631
|
+
const definition = getNodeDefinition(component);
|
|
632
|
+
return definition ? insertNode(definition.type, definition.createData(), center, undefined, inside) : undefined;
|
|
499
633
|
},
|
|
500
634
|
[insertNode]
|
|
501
635
|
);
|
|
@@ -510,6 +644,12 @@ export function useStudioFlow({
|
|
|
510
644
|
|
|
511
645
|
/** Moves nodes (and resizes containers) from a layout in one change, so it undoes in one go */
|
|
512
646
|
const applyPositions = useCallback((layout: LayoutResult) => withDoc((doc) => applyLayout(doc, layout)), []);
|
|
647
|
+
/** Puts things where they go on L1 or L2: moved there by someone (undone like any edit), or placed by the level */
|
|
648
|
+
const setLevelLayout = useCallback(
|
|
649
|
+
(level: 1 | 2, places: ReadonlyMap<string, LevelPlace>, by: 'person' | 'level' = 'person') =>
|
|
650
|
+
withDoc((doc) => setLevelPlaces(doc, level, places), by === 'level' ? LEVEL_PLACEMENT_ORIGIN : null),
|
|
651
|
+
[]
|
|
652
|
+
);
|
|
513
653
|
|
|
514
654
|
/** Adds ready-made nodes and edges (e.g. a container with what's in it) in one change */
|
|
515
655
|
const insertNodes = useCallback(
|
|
@@ -525,13 +665,12 @@ export function useStudioFlow({
|
|
|
525
665
|
[]
|
|
526
666
|
);
|
|
527
667
|
|
|
528
|
-
/** Puts
|
|
529
|
-
const
|
|
530
|
-
(
|
|
668
|
+
/** Puts nodes in containers (or takes them out) together, e.g. a selection dropped in one, as one change */
|
|
669
|
+
const setParents = useCallback(
|
|
670
|
+
(moves: { id: string; parentId: string | undefined; at?: XYPosition }[]) =>
|
|
531
671
|
withDoc((doc) => {
|
|
532
|
-
// Where it was dropped goes first, so it's moved into (or out of) the container from there
|
|
533
672
|
if (dragged.current.size) saveDrop(doc);
|
|
534
|
-
return
|
|
673
|
+
return setNodeParents(doc, moves, renderedSizes());
|
|
535
674
|
}),
|
|
536
675
|
[]
|
|
537
676
|
);
|
|
@@ -555,6 +694,47 @@ export function useStudioFlow({
|
|
|
555
694
|
sharedRef.current?.sender.send({ viewport });
|
|
556
695
|
}, []);
|
|
557
696
|
|
|
697
|
+
/**
|
|
698
|
+
* The note or text being changed here, as it changes (what's written, a text's size and angle), for the others to
|
|
699
|
+
* see as it happens; `null` when done (after the result is saved), sent straight away
|
|
700
|
+
*/
|
|
701
|
+
const sendEditing = useCallback((editing: Editing | null) => {
|
|
702
|
+
const sender = sharedRef.current?.sender;
|
|
703
|
+
if (!sender) return;
|
|
704
|
+
sender.send({ editing: editing && { ...editing, ...(editing.text !== undefined && { text: editing.text.slice(0, 2000) }) } });
|
|
705
|
+
if (!editing) sender.flush();
|
|
706
|
+
}, []);
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* What a diagram file opened into this (new) canvas starts with: its nodes and connections, comments, hotspots and
|
|
710
|
+
* where things are on L1 and L2, in one change that isn't undone
|
|
711
|
+
*/
|
|
712
|
+
const addContent = useCallback(
|
|
713
|
+
(content: ModelCanvasContent) =>
|
|
714
|
+
withDoc((doc) => {
|
|
715
|
+
addNodes(doc, content.nodes, content.edges);
|
|
716
|
+
if (content.threads?.length) addThreads(doc, content.threads);
|
|
717
|
+
if (content.hotspots?.length) addHotspots(doc, content.hotspots);
|
|
718
|
+
setLevelPlaces(doc, 1, content.levelLayouts[1]);
|
|
719
|
+
setLevelPlaces(doc, 2, content.levelLayouts[2]);
|
|
720
|
+
}, OPEN_ORIGIN),
|
|
721
|
+
[]
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
// ---- Hotspots and the lock ----
|
|
725
|
+
|
|
726
|
+
const createHotspot = useCallback(
|
|
727
|
+
(hotspot: Omit<Hotspot, 'id' | 'createdAt'>) => withDoc((doc) => createHotspotInDoc(doc, hotspot)),
|
|
728
|
+
[]
|
|
729
|
+
);
|
|
730
|
+
const updateHotspot = useCallback(
|
|
731
|
+
(id: string, fields: Parameters<typeof updateHotspotInDoc>[2]) => withDoc((doc) => updateHotspotInDoc(doc, id, fields)),
|
|
732
|
+
[]
|
|
733
|
+
);
|
|
734
|
+
const deleteHotspot = useCallback((id: string) => withDoc((doc) => deleteHotspotInDoc(doc, id)), []);
|
|
735
|
+
/** Locks the canvas for everyone (by a name), or unlocks it (null): looked at, not changed */
|
|
736
|
+
const setLocked = useCallback((by: string | null) => withDoc((doc) => setLock(doc, by), STATUS_ORIGIN), []);
|
|
737
|
+
|
|
558
738
|
return {
|
|
559
739
|
nodes,
|
|
560
740
|
edges,
|
|
@@ -572,14 +752,27 @@ export function useStudioFlow({
|
|
|
572
752
|
updateNodeData,
|
|
573
753
|
deleteNodes,
|
|
574
754
|
applyPositions,
|
|
755
|
+
levelLayouts,
|
|
756
|
+
setLevelLayout,
|
|
575
757
|
insertNodes,
|
|
576
|
-
|
|
758
|
+
setParents,
|
|
577
759
|
title,
|
|
760
|
+
diagramLevels,
|
|
761
|
+
structureKey,
|
|
578
762
|
rename,
|
|
579
763
|
canvasStatus,
|
|
580
764
|
changeStatus,
|
|
581
765
|
transport,
|
|
582
766
|
deleted,
|
|
767
|
+
hotspots,
|
|
768
|
+
createHotspot,
|
|
769
|
+
updateHotspot,
|
|
770
|
+
deleteHotspot,
|
|
771
|
+
lockedBy,
|
|
772
|
+
setLocked,
|
|
773
|
+
sendEditing,
|
|
774
|
+
synced,
|
|
775
|
+
addContent,
|
|
583
776
|
undo,
|
|
584
777
|
redo,
|
|
585
778
|
...history,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Who you are in Studio: the name you give on your first visit, kept in this browser and used on every canvas
|
|
2
|
-
|
|
2
|
+
/** The colours people can go by (as Studio on the web's): on their pointer, avatar and comments */
|
|
3
|
+
export const PERSON_COLORS = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#06b6d4', '#3b82f6', '#8b5cf6', '#ec4899'];
|
|
4
|
+
const COLORS = PERSON_COLORS;
|
|
3
5
|
const NAME_KEY = 'eventcatalog-studio-name';
|
|
6
|
+
const COLOR_KEY = 'eventcatalog-studio-color';
|
|
4
7
|
/** Names people give are at most this long */
|
|
5
8
|
export const MAX_NAME_LENGTH = 60;
|
|
6
9
|
|
|
@@ -35,3 +38,22 @@ export const colorForName = (name: string) => {
|
|
|
35
38
|
for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) | 0;
|
|
36
39
|
return COLORS[Math.abs(hash) % COLORS.length];
|
|
37
40
|
};
|
|
41
|
+
|
|
42
|
+
/** The colour you picked, kept in this browser (else it follows from your name) */
|
|
43
|
+
export const getStoredColor = () => {
|
|
44
|
+
try {
|
|
45
|
+
const color = localStorage.getItem(COLOR_KEY);
|
|
46
|
+
return color && PERSON_COLORS.includes(color) ? color : null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const storeColor = (color: string) => {
|
|
53
|
+
try {
|
|
54
|
+
localStorage.setItem(COLOR_KEY, color);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
// Visualiser styles ship in the page head so ClientRouter navigations keep them (see NodeGraph.astro).
|
|
3
3
|
import '@eventcatalog/visualiser/styles-core.css';
|
|
4
|
-
import '
|
|
4
|
+
import '@eventcatalog/visualiser/studio.css';
|
|
5
|
+
import '@eventcatalog/visualiser/studio-fonts.css';
|
|
5
6
|
import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
|
|
6
7
|
import { buildUrl } from '@utils/url-builder';
|
|
7
8
|
import StudioDesigner from '../components/StudioDesigner';
|
|
@@ -4,9 +4,9 @@ import { ChevronDown, ChevronLeft, ChevronRight, Copy, EllipsisVertical, Pencil,
|
|
|
4
4
|
import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
|
|
5
5
|
import StudioNotice from '../components/StudioNotice.astro';
|
|
6
6
|
import { buildUrl } from '@utils/url-builder';
|
|
7
|
-
import { CANVAS_STATUSES,
|
|
7
|
+
import { CANVAS_STATUSES, CANVAS_STATUS_LOOK, STATUS, type CanvasStatus } from '@eventcatalog/visualiser/studio';
|
|
8
|
+
import { MAX_TITLE_LENGTH } from '../limits';
|
|
8
9
|
import { MAX_NAME_LENGTH } from '../identity';
|
|
9
|
-
import { CANVAS_STATUS_LOOK, STATUS } from '../components/status';
|
|
10
10
|
import { CANVASES_API_PATH } from '../api/canvases-api';
|
|
11
11
|
import { startStudio, type CanvasSummary } from '../server/runtime';
|
|
12
12
|
import { getSignedInUser, isSignedIn } from '../server/sign-in';
|
|
@@ -21,10 +21,10 @@ import {
|
|
|
21
21
|
setCanvasStatus,
|
|
22
22
|
setThreadResolved,
|
|
23
23
|
type Author,
|
|
24
|
-
|
|
24
|
+
getLayoutPositions,
|
|
25
|
+
} from '@eventcatalog/visualiser/studio';
|
|
25
26
|
import { ADD_TO_CANVAS_DESCRIPTION, describeCanvas, edgeSpecsSchema, indexCatalog, nodeSpecsSchema } from '../canvas-actions';
|
|
26
27
|
import { getCatalogResources } from '../catalog-resources';
|
|
27
|
-
import { getLayoutPositions } from '../layout';
|
|
28
28
|
import {
|
|
29
29
|
playAddToCanvas,
|
|
30
30
|
playComment,
|
|
@@ -35,7 +35,8 @@ import {
|
|
|
35
35
|
playUpdateNode,
|
|
36
36
|
type AgentStage,
|
|
37
37
|
} from '../agent-choreography';
|
|
38
|
-
import { STUDIO_SOCKET_PATH, findStudioRuntime
|
|
38
|
+
import { STUDIO_SOCKET_PATH, findStudioRuntime } from './runtime';
|
|
39
|
+
import { getAgent } from '../agents';
|
|
39
40
|
import { addToolIcons } from './tool-icons';
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -46,21 +47,11 @@ import { addToolIcons } from './tool-icons';
|
|
|
46
47
|
|
|
47
48
|
const loadCanvasView = getMcpAppViewLoader('canvas');
|
|
48
49
|
|
|
49
|
-
const AGENT_COLORS: Record<string, string> = { ChatGPT: '#10a37f', Claude: '#d97757' };
|
|
50
|
-
|
|
51
50
|
// The canvases entrypoint's icon (OpenAI MCP extensions: a 20x20, monochrome SVG in currentColor, 1.33px strokes)
|
|
52
51
|
const CANVAS_ICON_SVG =
|
|
53
52
|
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="3.5" width="5.5" height="4.5" rx="1"/><rect x="12" y="3.5" width="5.5" height="4.5" rx="1"/><rect x="12" y="12" width="5.5" height="4.5" rx="1"/><path d="M8 5.75h4M14.75 8v4"/></svg>';
|
|
54
53
|
const CANVAS_ICON = { src: `data:image/svg+xml;base64,${btoa(CANVAS_ICON_SVG)}`, mimeType: 'image/svg+xml', sizes: ['20x20'] };
|
|
55
54
|
|
|
56
|
-
/** Who the agent is, from what it tells us or its client's user agent */
|
|
57
|
-
const getAgent = (agentName: string | undefined, userAgent = ''): AgentIdentity => {
|
|
58
|
-
const name =
|
|
59
|
-
agentName?.trim() ||
|
|
60
|
-
(/openai|chatgpt/i.test(userAgent) ? 'ChatGPT' : /claude|anthropic/i.test(userAgent) ? 'Claude' : 'AI agent');
|
|
61
|
-
return { name, color: AGENT_COLORS[name] ?? '#8b5cf6' };
|
|
62
|
-
};
|
|
63
|
-
|
|
64
55
|
const text = (value: unknown, isError = false) => ({
|
|
65
56
|
content: [{ type: 'text' as const, text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
66
57
|
...(isError && { isError }),
|
|
@@ -151,12 +142,6 @@ export function registerCanvasTools(server: McpServer, { catalogUrl, userAgent }
|
|
|
151
142
|
input: {
|
|
152
143
|
canvasId: canvasId.optional(),
|
|
153
144
|
title: z.string().optional().describe('For a new canvas: what it is for'),
|
|
154
|
-
userName: z
|
|
155
|
-
.string()
|
|
156
|
-
.optional()
|
|
157
|
-
.describe(
|
|
158
|
-
'The name of the person you are working with, if you know it: others on the canvas see them by this name. Leave it out if you do not know it.'
|
|
159
|
-
),
|
|
160
145
|
agentName,
|
|
161
146
|
},
|
|
162
147
|
},
|
|
@@ -170,7 +155,7 @@ export function registerCanvasTools(server: McpServer, { catalogUrl, userAgent }
|
|
|
170
155
|
annotations: writes,
|
|
171
156
|
errorMessage: 'Failed to open the canvas',
|
|
172
157
|
load: async (params) => {
|
|
173
|
-
const { title: newTitle, agentName: name
|
|
158
|
+
const { title: newTitle, agentName: name } = params as { title?: string; agentName?: string };
|
|
174
159
|
|
|
175
160
|
// Opened without a canvas (e.g. from an entrypoint): the canvases to pick from, or start one
|
|
176
161
|
if (!params.canvasId && !newTitle) {
|
|
@@ -207,7 +192,7 @@ export function registerCanvasTools(server: McpServer, { catalogUrl, userAgent }
|
|
|
207
192
|
socketUrl,
|
|
208
193
|
canvasUrl: canvasUrl(id),
|
|
209
194
|
mcpUrl: `${catalogUrl}${buildUrl('/docs/mcp', true)}`,
|
|
210
|
-
|
|
195
|
+
agent: getAgent(name, userAgent),
|
|
211
196
|
};
|
|
212
197
|
return {
|
|
213
198
|
forModel: {
|
|
@@ -418,7 +403,7 @@ export function registerCanvasTools(server: McpServer, { catalogUrl, userAgent }
|
|
|
418
403
|
{
|
|
419
404
|
title: 'Tidy a canvas',
|
|
420
405
|
description:
|
|
421
|
-
"Lays the whole canvas out again with EventCatalog's visualiser layout (left to right, connections routed around nodes); everyone on it sees the nodes glide into place.
|
|
406
|
+
"Lays the whole canvas out again with EventCatalog's visualiser layout (left to right, connections routed around nodes); everyone on it sees the nodes glide into place. Call it once when you finish a round of changes, before you hand back to the user, so the canvas is tidy for everyone, and whenever the user asks to tidy up. Don't call it after every change (addToCanvas already places what you add next to what it connects to while you work), and skip it if the user asked to keep the layout as it is: it moves everything, including what people placed themselves.",
|
|
422
407
|
inputSchema: z.object({ canvasId, agentName }),
|
|
423
408
|
annotations: writes,
|
|
424
409
|
},
|
|
@@ -427,7 +412,7 @@ export function registerCanvasTools(server: McpServer, { catalogUrl, userAgent }
|
|
|
427
412
|
const { nodes, edges } = await runtime.withCanvas(id, readCanvas);
|
|
428
413
|
const positions = await getLayoutPositions(nodes, edges);
|
|
429
414
|
await playLayout(asAgent(id, name).stage, positions);
|
|
430
|
-
return text({ laidOut: positions.size });
|
|
415
|
+
return text({ laidOut: positions.nodes.size });
|
|
431
416
|
}
|
|
432
417
|
);
|
|
433
418
|
|
|
@@ -5,17 +5,17 @@ import { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate, removeAwareness
|
|
|
5
5
|
import type { XYPosition } from '@xyflow/react';
|
|
6
6
|
import type { AgentPresence } from '../agent-choreography';
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
MAX_TITLE_LENGTH,
|
|
10
|
-
canvasDocumentName,
|
|
11
|
-
canvasIdFromDocumentName,
|
|
8
|
+
addCanvasContent,
|
|
12
9
|
getCanvasMaps,
|
|
13
10
|
readMeta,
|
|
14
11
|
setMeta,
|
|
15
12
|
getCanvasStatus,
|
|
13
|
+
type CanvasContent,
|
|
16
14
|
type CanvasStatus,
|
|
17
|
-
} from '
|
|
15
|
+
} from '@eventcatalog/visualiser/studio';
|
|
16
|
+
import { CANVAS_DELETED_REASON, canvasDocumentName, canvasIdFromDocumentName } from '../document-name';
|
|
18
17
|
import { getCanvasPreview, type CanvasPreview } from '../canvas-preview';
|
|
18
|
+
import { MAX_TITLE_LENGTH } from '../limits';
|
|
19
19
|
import { fromBase64, toBase64, type SyncRequest, type SyncResponse } from '../tool-sync';
|
|
20
20
|
import config from '../../../utils/eventcatalog-config/source';
|
|
21
21
|
import { isAuthEnabled } from '../../../utils/feature';
|
|
@@ -31,7 +31,8 @@ const AGENT_PRESENCE_TTL_MS = 25_000;
|
|
|
31
31
|
// Application close codes are 4000-4999
|
|
32
32
|
const CANVAS_DELETED_CLOSE_CODE = 4410;
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
import type { AgentIdentity } from '../agents';
|
|
35
|
+
export type { AgentIdentity };
|
|
35
36
|
export type CanvasSummary = {
|
|
36
37
|
canvasId: string;
|
|
37
38
|
title?: string;
|
|
@@ -299,10 +300,16 @@ export class StudioRuntime {
|
|
|
299
300
|
return true;
|
|
300
301
|
}
|
|
301
302
|
|
|
302
|
-
/**
|
|
303
|
-
|
|
303
|
+
/**
|
|
304
|
+
* Starts a canvas with a title, recording when and by whom (a person, or an agent), and what's on it to begin
|
|
305
|
+
* with, if anything. Returns its id.
|
|
306
|
+
*/
|
|
307
|
+
async createCanvas(started: { title?: string; createdBy?: string }, content?: CanvasContent) {
|
|
304
308
|
const canvasId = crypto.randomUUID();
|
|
305
|
-
await this.withCanvas(canvasId, (doc) =>
|
|
309
|
+
await this.withCanvas(canvasId, (doc) => {
|
|
310
|
+
setStarted(doc, started);
|
|
311
|
+
if (content) addCanvasContent(doc, content);
|
|
312
|
+
});
|
|
306
313
|
return canvasId;
|
|
307
314
|
}
|
|
308
315
|
|