@eventcatalog/core 5.0.0-beta.5 → 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.
- 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/{chunk-43AEF3JE.js → chunk-56CD2NH6.js} +1 -1
- package/dist/{chunk-YYMBYOQR.js → chunk-DE3QHVJP.js} +1 -1
- package/dist/{chunk-XQXMSQA5.js → chunk-L2IRFOE7.js} +1 -1
- package/dist/{chunk-SHHO3JJI.js → chunk-QBP3BBSV.js} +1 -1
- package/dist/{chunk-EBDTVZDW.js → chunk-XMPFHLK4.js} +4 -4
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +11 -11
- package/dist/federation/federate.js +4 -4
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +8 -0
- package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
- package/dist/runtime-dependencies/yjs.mjs +3 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/astro.config.mjs +2 -0
- package/eventcatalog/integrations/studio-server.mjs +17 -0
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +20 -1
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +39 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +269 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +209 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +2 -2
- package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
- package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
- package/eventcatalog/src/features/mcp/mcp-server.ts +17 -4
- package/eventcatalog/src/features/studio/agent-choreography.ts +378 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +287 -0
- package/eventcatalog/src/features/studio/canvas-doc.ts +280 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +152 -0
- package/eventcatalog/src/features/studio/catalog.ts +171 -0
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +153 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +332 -0
- package/eventcatalog/src/features/studio/components/Comments.tsx +455 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +217 -0
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +58 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +218 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +284 -0
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +75 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +760 -0
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +79 -0
- package/eventcatalog/src/features/studio/components/icons.ts +48 -0
- package/eventcatalog/src/features/studio/components/status.ts +22 -0
- package/eventcatalog/src/features/studio/edges.ts +54 -0
- package/eventcatalog/src/features/studio/flow-state.ts +92 -0
- package/eventcatalog/src/features/studio/grouping.ts +133 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +115 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +337 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +47 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +99 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +423 -0
- package/eventcatalog/src/features/studio/layout.ts +39 -0
- package/eventcatalog/src/features/studio/levels.ts +216 -0
- package/eventcatalog/src/features/studio/node-types.ts +173 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +38 -0
- package/eventcatalog/src/features/studio/pages/index.astro +8 -0
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
- package/eventcatalog/src/features/studio/placement.ts +197 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +475 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +215 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/studio.css +89 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +9 -2
- package/eventcatalog/src/utils/feature.ts +4 -0
- package/package.json +9 -2
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/pages/studio.astro +0 -316
- package/dist/{chunk-ZEY2XUJN.js → chunk-AYNZXUB5.js} +7 -7
|
@@ -84,7 +84,8 @@ export function useMcpAppView<Payload>(options: McpAppViewOptions<Payload>) {
|
|
|
84
84
|
setHostContext(context);
|
|
85
85
|
}, [app]);
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
// setPayload: views that switch what they show themselves (e.g. picking a canvas) without a new tool result
|
|
88
|
+
return { app, payload, setPayload, error: connectionError?.message ?? error, hostContext };
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
/** Adds the view's styles and renders it */
|
|
@@ -12,7 +12,7 @@ const mcpAppViews = import.meta.glob<string>('./apps/generated/*.html', { query:
|
|
|
12
12
|
export const getMcpAppViewLoader = (name: string) => mcpAppViews[`./apps/generated/${name}.html`];
|
|
13
13
|
|
|
14
14
|
/** Tool `_meta` telling MCP hosts that support MCP Apps to show the tool's result in a view */
|
|
15
|
-
const appViewMeta = (resourceUri: string, visibility?: Array<'model' | 'app'>) => ({
|
|
15
|
+
export const appViewMeta = (resourceUri: string, visibility?: Array<'model' | 'app'>) => ({
|
|
16
16
|
ui: { resourceUri, ...(visibility && { visibility }) },
|
|
17
17
|
[RESOURCE_URI_META_KEY]: resourceUri,
|
|
18
18
|
});
|
|
@@ -38,6 +38,8 @@ type ToolDefinition<Shape extends z.ZodRawShape> = {
|
|
|
38
38
|
description: string;
|
|
39
39
|
/** The tool's arguments, as a Zod shape */
|
|
40
40
|
input: Shape;
|
|
41
|
+
/** More `_meta` for the tool, e.g. host extensions such as ChatGPT's `openai/ui` entrypoints */
|
|
42
|
+
meta?: Record<string, unknown>;
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
export type McpAppDefinition<Shape extends z.ZodRawShape, ViewShape extends z.ZodRawShape, Payload> = {
|
|
@@ -47,7 +49,8 @@ export type McpAppDefinition<Shape extends z.ZodRawShape, ViewShape extends z.Zo
|
|
|
47
49
|
name: string;
|
|
48
50
|
uri: string;
|
|
49
51
|
description: string;
|
|
50
|
-
|
|
52
|
+
/** Origins the view may load from (resourceDomains) and connect to with fetch or WebSockets (connectDomains) */
|
|
53
|
+
csp?: { resourceDomains?: string[]; connectDomains?: string[] };
|
|
51
54
|
/** How hosts open the view: ChatGPT reads these before loading it, so it can open straight in that mode */
|
|
52
55
|
displayModes?: { preferred: McpUiDisplayMode; available: McpUiDisplayMode[] };
|
|
53
56
|
};
|
|
@@ -110,7 +113,7 @@ export function registerMcpApp<Shape extends z.ZodRawShape, ViewShape extends z.
|
|
|
110
113
|
description: app.tool.description,
|
|
111
114
|
inputSchema: toolInput,
|
|
112
115
|
annotations: app.annotations,
|
|
113
|
-
_meta: loadView ? appViewMeta(app.resource.uri) : undefined,
|
|
116
|
+
_meta: loadView ? { ...appViewMeta(app.resource.uri), ...app.tool.meta } : undefined,
|
|
114
117
|
},
|
|
115
118
|
showInView
|
|
116
119
|
);
|
|
@@ -22,6 +22,9 @@ import { RESOURCE_VIEWS, VIEWER_META_KEY, VIEWER_RESOURCE_URI, VIEWER_VIEW_TOOL,
|
|
|
22
22
|
import { toSchemaViewerSchemas } from './schema-viewer';
|
|
23
23
|
import { getMcpAppViewLoader, mcpAppNote, registerMcpApp } from './mcp-apps';
|
|
24
24
|
import { buildUrl } from '@utils/url-builder';
|
|
25
|
+
import { isCanvasEnabled } from '@utils/feature';
|
|
26
|
+
import { CANVAS_TOOL_NAMES, registerCanvasTools } from '@features/studio/server/canvas-mcp';
|
|
27
|
+
import { findStudioRuntime } from '@features/studio/server/runtime';
|
|
25
28
|
|
|
26
29
|
const loadViewerView = getMcpAppViewLoader('viewer');
|
|
27
30
|
|
|
@@ -36,6 +39,8 @@ const SHOW_RESOURCE_DESCRIPTION = [
|
|
|
36
39
|
type McpServerOptions = {
|
|
37
40
|
/** Where this EventCatalog is served, for links from MCP App views back to it */
|
|
38
41
|
catalogUrl: string;
|
|
42
|
+
/** The MCP client's user agent, to name agents that join a canvas */
|
|
43
|
+
userAgent?: string;
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
const catalogDirectory = process.env.PROJECT_DIR || process.cwd();
|
|
@@ -115,7 +120,7 @@ function getServerInstructions(scope?: McpScope) {
|
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
// Create MCP Server with tools that access Astro collections
|
|
118
|
-
function createMcpServer(scope: McpScope | undefined, { catalogUrl }: McpServerOptions) {
|
|
123
|
+
function createMcpServer(scope: McpScope | undefined, { catalogUrl, userAgent }: McpServerOptions) {
|
|
119
124
|
const server = new McpServer(
|
|
120
125
|
{
|
|
121
126
|
name: scope ? `EventCatalog MCP Server — ${scope.name} ${scope.ref.kind}` : 'EventCatalog MCP Server',
|
|
@@ -831,6 +836,9 @@ function createMcpServer(scope: McpScope | undefined, { catalogUrl }: McpServerO
|
|
|
831
836
|
);
|
|
832
837
|
}
|
|
833
838
|
|
|
839
|
+
// Collaborative canvases (when the collaboration server runs in this process)
|
|
840
|
+
if (!scope && isCanvasEnabled()) registerCanvasTools(server, { catalogUrl, userAgent });
|
|
841
|
+
|
|
834
842
|
return server;
|
|
835
843
|
}
|
|
836
844
|
|
|
@@ -964,7 +972,9 @@ const handleGetRequest = async (c: Context, kind?: McpScopeKind) => {
|
|
|
964
972
|
version: MCP_SERVER_VERSION,
|
|
965
973
|
status: 'running',
|
|
966
974
|
...(scope && { scope: scope.ref }),
|
|
967
|
-
tools: scope
|
|
975
|
+
tools: scope
|
|
976
|
+
? getScopedBuiltInTools(scope)
|
|
977
|
+
: [...globalBuiltInTools, ...(isCanvasEnabled() && findStudioRuntime() ? CANVAS_TOOL_NAMES : []), ...extendedToolNames],
|
|
968
978
|
extendedTools: !scope && extendedToolNames.length > 0 ? extendedToolNames : undefined,
|
|
969
979
|
resources: getMcpResourceUris(scope),
|
|
970
980
|
});
|
|
@@ -983,7 +993,10 @@ const handleMcpRequest = async (c: Context, kind?: McpScopeKind) => {
|
|
|
983
993
|
}
|
|
984
994
|
|
|
985
995
|
const scope = await resolveRequestScope(c, kind);
|
|
986
|
-
const server = createMcpServer(scope, {
|
|
996
|
+
const server = createMcpServer(scope, {
|
|
997
|
+
catalogUrl: new URL(c.req.url).origin,
|
|
998
|
+
userAgent: c.req.header('user-agent'),
|
|
999
|
+
});
|
|
987
1000
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
988
1001
|
sessionIdGenerator: undefined,
|
|
989
1002
|
});
|
|
@@ -1023,7 +1036,7 @@ app.post('/', (c: Context) => handleMcpRequest(c));
|
|
|
1023
1036
|
|
|
1024
1037
|
// Astro API route handler - delegates all requests to Hono
|
|
1025
1038
|
// Note: SSR checks are handled at build time by the integration
|
|
1026
|
-
// This route is only injected when isEventCatalogMCPEnabled() returns true
|
|
1039
|
+
// This route is only injected when isEventCatalogMCPEnabled() or isCanvasEnabled() returns true
|
|
1027
1040
|
export const ALL: APIRoute = async ({ request }) => {
|
|
1028
1041
|
return app.fetch(request);
|
|
1029
1042
|
};
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import type { Edge, Node, XYPosition } from '@xyflow/react';
|
|
2
|
+
import type * as Y from 'yjs';
|
|
3
|
+
import {
|
|
4
|
+
addNodes,
|
|
5
|
+
connectNodes,
|
|
6
|
+
createThread,
|
|
7
|
+
deleteEdges,
|
|
8
|
+
deleteNodes,
|
|
9
|
+
fitGroupToChildren,
|
|
10
|
+
getCanvasMaps,
|
|
11
|
+
moveNode,
|
|
12
|
+
resizeNode,
|
|
13
|
+
setMeta,
|
|
14
|
+
type Author,
|
|
15
|
+
type CommentAnchor,
|
|
16
|
+
} from './canvas-doc';
|
|
17
|
+
import {
|
|
18
|
+
describeNode,
|
|
19
|
+
nodeCenter,
|
|
20
|
+
planNodes,
|
|
21
|
+
resolveEdgeRefs,
|
|
22
|
+
topLeftFor,
|
|
23
|
+
updateCanvasNode,
|
|
24
|
+
type CatalogIndex,
|
|
25
|
+
type EdgeSpecs,
|
|
26
|
+
type NodeSpecs,
|
|
27
|
+
} from './canvas-actions';
|
|
28
|
+
import { getRelatedEdges } from './catalog';
|
|
29
|
+
import { getAbsolutePosition, sizeOf } from './grouping';
|
|
30
|
+
import type { LayoutResult } from './layout';
|
|
31
|
+
import { getNodeName } from './node-types';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Agents work on a canvas like a person would: their cursor travels to where they're going, they drop one node,
|
|
35
|
+
* drag a connection from it to the next, and so on, rather than everything appearing at once. Used by the MCP
|
|
36
|
+
* tools (on the server) and the WebMCP tools (in the browser), with a stage that makes the changes and shows
|
|
37
|
+
* the agent's presence there.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** What everyone sees of an agent at work */
|
|
41
|
+
export type AgentPresence = {
|
|
42
|
+
pointer: XYPosition | null;
|
|
43
|
+
selection: string[];
|
|
44
|
+
activity?: string;
|
|
45
|
+
/** Where a connection being dragged starts: drawn from there to the pointer */
|
|
46
|
+
connecting?: XYPosition | null;
|
|
47
|
+
/** Nodes being moved, by id, and where they are (relative to their containers), until they're dropped */
|
|
48
|
+
moving?: Record<string, XYPosition> | null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type AgentStage = {
|
|
52
|
+
/** Change the canvas (and read it) */
|
|
53
|
+
change: <T>(fn: (doc: Y.Doc) => T) => Promise<T> | T;
|
|
54
|
+
/** Show the agent on the canvas */
|
|
55
|
+
present: (presence: AgentPresence) => void;
|
|
56
|
+
/** Where the agent's pointer was left last time, if it's been here */
|
|
57
|
+
lastPointer?: XYPosition | null;
|
|
58
|
+
/**
|
|
59
|
+
* Everyone sees the agent's presence, so nodes it moves are shown moving with it and only saved where they
|
|
60
|
+
* end up, like a person's drag. Otherwise each step of a move is saved, so others see it move.
|
|
61
|
+
*/
|
|
62
|
+
sharesMoves?: boolean;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const FRAME_MS = 40;
|
|
66
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
67
|
+
const easeInOut = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
|
|
68
|
+
const distance = (a: XYPosition, b: XYPosition) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
69
|
+
|
|
70
|
+
type Lookup = Map<string, Node>;
|
|
71
|
+
/** Where a node is on the canvas (nodes in containers are positioned relative to them) */
|
|
72
|
+
const positionOf = (node: Node, nodes: Lookup) => getAbsolutePosition(node, nodes);
|
|
73
|
+
/** Where connections leave a node (its right side) and arrive (its left side) */
|
|
74
|
+
const sourceHandle = (node: Node, nodes: Lookup) => {
|
|
75
|
+
const position = positionOf(node, nodes);
|
|
76
|
+
return { x: position.x + sizeOf(node).width, y: position.y + sizeOf(node).height / 2 };
|
|
77
|
+
};
|
|
78
|
+
const targetHandle = (node: Node, nodes: Lookup) => {
|
|
79
|
+
const position = positionOf(node, nodes);
|
|
80
|
+
return { x: position.x, y: position.y + sizeOf(node).height / 2 };
|
|
81
|
+
};
|
|
82
|
+
const nameOf = (node: Node) => getNodeName(node.type, node.data);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The agent's hand: moves its pointer smoothly (in frames, so it glides for everyone) and shows what it's doing.
|
|
86
|
+
* `pace` slows down or speeds up everything (big changes go faster, so a tool call doesn't take too long).
|
|
87
|
+
*/
|
|
88
|
+
export function createAgentHand(stage: AgentStage, { pace = 1 }: { pace?: number } = {}) {
|
|
89
|
+
const presence: AgentPresence = { pointer: stage.lastPointer ?? null, selection: [] };
|
|
90
|
+
const show = () => stage.present({ ...presence });
|
|
91
|
+
|
|
92
|
+
const hand = {
|
|
93
|
+
get pointer() {
|
|
94
|
+
return presence.pointer;
|
|
95
|
+
},
|
|
96
|
+
async pause(ms: number) {
|
|
97
|
+
await sleep(ms * pace);
|
|
98
|
+
},
|
|
99
|
+
say(activity: string) {
|
|
100
|
+
presence.activity = activity;
|
|
101
|
+
show();
|
|
102
|
+
},
|
|
103
|
+
select(ids: string[]) {
|
|
104
|
+
presence.selection = ids;
|
|
105
|
+
show();
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* Glide the pointer to a point, e.g. dragging a connection from `connecting`, or carrying a node held at
|
|
109
|
+
* `grab` from its position
|
|
110
|
+
*/
|
|
111
|
+
async moveTo(
|
|
112
|
+
target: XYPosition,
|
|
113
|
+
{ connecting, carry }: { connecting?: XYPosition; carry?: { nodeId: string; grab: XYPosition } } = {}
|
|
114
|
+
) {
|
|
115
|
+
// The first time, come in from a little way off rather than appearing on the spot
|
|
116
|
+
const from = presence.pointer ?? { x: target.x - 160, y: target.y - 120 };
|
|
117
|
+
const duration = Math.min(900, Math.max(250, 200 + distance(from, target) * 0.6)) * pace;
|
|
118
|
+
const frames = Math.max(1, Math.round(duration / FRAME_MS));
|
|
119
|
+
presence.connecting = connecting ?? null;
|
|
120
|
+
for (let frame = 1; frame <= frames; frame++) {
|
|
121
|
+
const t = easeInOut(frame / frames);
|
|
122
|
+
presence.pointer = { x: from.x + (target.x - from.x) * t, y: from.y + (target.y - from.y) * t };
|
|
123
|
+
if (carry)
|
|
124
|
+
presence.moving = {
|
|
125
|
+
[carry.nodeId]: { x: presence.pointer.x - carry.grab.x, y: presence.pointer.y - carry.grab.y },
|
|
126
|
+
};
|
|
127
|
+
show();
|
|
128
|
+
await sleep(FRAME_MS);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
/** Let go of a connection being dragged, or nodes being carried (once where they're dropped is saved) */
|
|
132
|
+
release() {
|
|
133
|
+
presence.connecting = null;
|
|
134
|
+
presence.moving = null;
|
|
135
|
+
show();
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
return hand;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Faster for bigger changes, so a tool call stays well within client timeouts */
|
|
142
|
+
const paceFor = (steps: number) => Math.max(0.25, Math.min(1, 10 / Math.max(steps, 1)));
|
|
143
|
+
|
|
144
|
+
const readNodes = (doc: Y.Doc) => new Map<string, Node>(getCanvasMaps(doc).nodes.entries());
|
|
145
|
+
|
|
146
|
+
/** Drag a connection from one node to another, then make it */
|
|
147
|
+
async function playConnection(
|
|
148
|
+
stage: AgentStage,
|
|
149
|
+
hand: ReturnType<typeof createAgentHand>,
|
|
150
|
+
connect: (doc: Y.Doc) => Edge | { error: string },
|
|
151
|
+
sourceId: string,
|
|
152
|
+
targetId: string
|
|
153
|
+
) {
|
|
154
|
+
const nodes = await stage.change(readNodes);
|
|
155
|
+
const source = nodes.get(sourceId);
|
|
156
|
+
const target = nodes.get(targetId);
|
|
157
|
+
if (!source || !target) return stage.change(connect);
|
|
158
|
+
hand.say(`Connecting ${nameOf(source)} to ${nameOf(target)}`);
|
|
159
|
+
const start = sourceHandle(source, nodes);
|
|
160
|
+
await hand.moveTo(start);
|
|
161
|
+
await hand.pause(120);
|
|
162
|
+
await hand.moveTo(targetHandle(target, nodes), { connecting: start });
|
|
163
|
+
const outcome = await stage.change(connect);
|
|
164
|
+
hand.release();
|
|
165
|
+
await hand.pause(180);
|
|
166
|
+
return outcome;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** addToCanvas, played out: each node dropped in turn, connected (to related resources and as asked) as soon as it can be */
|
|
170
|
+
export async function playAddToCanvas(
|
|
171
|
+
stage: AgentStage,
|
|
172
|
+
{ nodes: specs, edges = [] }: { nodes: NodeSpecs; edges?: EdgeSpecs },
|
|
173
|
+
catalog: CatalogIndex
|
|
174
|
+
) {
|
|
175
|
+
const existing = await stage.change((doc) => [...readNodes(doc).values()]);
|
|
176
|
+
const { planned, errors } = planNodes(existing, specs, catalog, edges);
|
|
177
|
+
const pending = resolveEdgeRefs(edges, planned);
|
|
178
|
+
const hand = createAgentHand(stage, { pace: paceFor(planned.length * 2 + pending.length) });
|
|
179
|
+
const connections: string[] = [];
|
|
180
|
+
|
|
181
|
+
for (const { node: plannedNode, canvasPosition, name, resource } of planned) {
|
|
182
|
+
hand.say(`Adding ${name}`);
|
|
183
|
+
const { width, height } = sizeOf(plannedNode);
|
|
184
|
+
await hand.moveTo({ x: canvasPosition.x + width / 2, y: canvasPosition.y + height / 2 });
|
|
185
|
+
await hand.pause(150);
|
|
186
|
+
const { node, related } = await stage.change((doc) => {
|
|
187
|
+
const onCanvas = readNodes(doc);
|
|
188
|
+
// Where it goes on the canvas, relative to its container as it is now (containers move as they grow)
|
|
189
|
+
const parent = plannedNode.parentId ? onCanvas.get(plannedNode.parentId) : undefined;
|
|
190
|
+
const origin = parent ? getAbsolutePosition(parent, onCanvas) : { x: 0, y: 0 };
|
|
191
|
+
const node = { ...plannedNode, position: { x: canvasPosition.x - origin.x, y: canvasPosition.y - origin.y } };
|
|
192
|
+
addNodes(doc, [node]);
|
|
193
|
+
// The containers it's in grow to fit it
|
|
194
|
+
for (let id = node.parentId; id; id = readNodes(doc).get(id)?.parentId) fitGroupToChildren(doc, id);
|
|
195
|
+
const related = resource
|
|
196
|
+
? getRelatedEdges(resource, node.id, [...onCanvas.values()], catalog.relationsByKey, node.type)
|
|
197
|
+
: [];
|
|
198
|
+
return { node, related };
|
|
199
|
+
});
|
|
200
|
+
hand.select([node.id]);
|
|
201
|
+
await hand.pause(300);
|
|
202
|
+
|
|
203
|
+
// Its connections to what's already there: related catalog resources, then the ones asked for
|
|
204
|
+
for (const edge of related) {
|
|
205
|
+
await playConnection(stage, hand, (doc) => (addNodes(doc, [], [edge]), edge), edge.source, edge.target);
|
|
206
|
+
connections.push(edge.id);
|
|
207
|
+
}
|
|
208
|
+
const onCanvas = await stage.change((doc) => new Set(readNodes(doc).keys()));
|
|
209
|
+
for (const edge of pending.filter((edge) => onCanvas.has(edge.from) && onCanvas.has(edge.to))) {
|
|
210
|
+
pending.splice(pending.indexOf(edge), 1);
|
|
211
|
+
const outcome = await playConnection(
|
|
212
|
+
stage,
|
|
213
|
+
hand,
|
|
214
|
+
(doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label),
|
|
215
|
+
edge.from,
|
|
216
|
+
edge.to
|
|
217
|
+
);
|
|
218
|
+
if ('error' in outcome) errors.push(outcome.error);
|
|
219
|
+
else connections.push(outcome.id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Connections to nodes that aren't on the canvas
|
|
224
|
+
for (const edge of pending) {
|
|
225
|
+
const outcome = await stage.change((doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label));
|
|
226
|
+
if ('error' in outcome) errors.push(outcome.error);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
hand.say('Done');
|
|
230
|
+
return { created: planned.map(({ ref, node, name }) => ({ ref, nodeId: node.id, name })), connections, errors };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** connectCanvasNodes, played out: each connection dragged from one node to the other */
|
|
234
|
+
export async function playConnectAll(stage: AgentStage, edges: EdgeSpecs) {
|
|
235
|
+
const hand = createAgentHand(stage, { pace: paceFor(edges.length) });
|
|
236
|
+
const connections: string[] = [];
|
|
237
|
+
const errors: string[] = [];
|
|
238
|
+
for (const edge of edges) {
|
|
239
|
+
const outcome = await playConnection(
|
|
240
|
+
stage,
|
|
241
|
+
hand,
|
|
242
|
+
(doc) => connectNodes(doc, { source: edge.from, target: edge.to }, edge.label),
|
|
243
|
+
edge.from,
|
|
244
|
+
edge.to
|
|
245
|
+
);
|
|
246
|
+
if ('error' in outcome) errors.push(outcome.error);
|
|
247
|
+
else connections.push(outcome.id);
|
|
248
|
+
}
|
|
249
|
+
return { connections, errors };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Moving a node, played out: grab it and drag it there */
|
|
253
|
+
export async function playMoveNode(stage: AgentStage, nodeId: string, to: XYPosition, hand = createAgentHand(stage)) {
|
|
254
|
+
const nodes = await stage.change(readNodes);
|
|
255
|
+
const node = nodes.get(nodeId);
|
|
256
|
+
if (!node) return false;
|
|
257
|
+
// Positions are relative to the node's container, the pointer is on the canvas
|
|
258
|
+
const parent = node.parentId ? nodes.get(node.parentId) : undefined;
|
|
259
|
+
const origin = parent ? positionOf(parent, nodes) : { x: 0, y: 0 };
|
|
260
|
+
const grab = { x: origin.x + sizeOf(node).width / 2, y: origin.y + 24 };
|
|
261
|
+
hand.say(`Moving ${nameOf(node)}`);
|
|
262
|
+
await hand.moveTo({ x: node.position.x + grab.x, y: node.position.y + grab.y });
|
|
263
|
+
hand.select([nodeId]);
|
|
264
|
+
await hand.pause(120);
|
|
265
|
+
if (stage.sharesMoves) {
|
|
266
|
+
await hand.moveTo({ x: to.x + grab.x, y: to.y + grab.y }, { carry: { nodeId, grab } });
|
|
267
|
+
await stage.change((doc) => moveNode(doc, nodeId, to));
|
|
268
|
+
hand.release();
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
const from = node.position;
|
|
272
|
+
const frames = 12;
|
|
273
|
+
for (let frame = 1; frame <= frames; frame++) {
|
|
274
|
+
const t = easeInOut(frame / frames);
|
|
275
|
+
const position = { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
|
|
276
|
+
await stage.change((doc) => moveNode(doc, nodeId, position));
|
|
277
|
+
await hand.moveTo({ x: position.x + grab.x, y: position.y + grab.y });
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Laying the canvas out: everything moves in one change, and everyone's canvas glides the nodes into place
|
|
284
|
+
* (the layout time in the canvas's meta tells them to)
|
|
285
|
+
*/
|
|
286
|
+
export async function playLayout(stage: AgentStage, layout: LayoutResult) {
|
|
287
|
+
const hand = createAgentHand(stage);
|
|
288
|
+
hand.say('Tidying the layout');
|
|
289
|
+
await stage.change((doc) => applyLayout(doc, layout));
|
|
290
|
+
await hand.pause(500);
|
|
291
|
+
hand.say('Done');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Moves nodes and resizes containers from a layout, in one change, and marks when it happened */
|
|
295
|
+
export const applyLayout = (doc: Y.Doc, layout: LayoutResult) =>
|
|
296
|
+
doc.transact(() => {
|
|
297
|
+
layout.forEach(({ position, size }, id) => {
|
|
298
|
+
moveNode(doc, id, position);
|
|
299
|
+
if (size) resizeNode(doc, id, size);
|
|
300
|
+
});
|
|
301
|
+
setMeta(doc, { layoutAt: Date.now() });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
/** Removing things, played out: point at each, then remove it */
|
|
305
|
+
export async function playRemove(stage: AgentStage, nodeIds: string[], connectionIds: string[]) {
|
|
306
|
+
const hand = createAgentHand(stage, { pace: paceFor(nodeIds.length) });
|
|
307
|
+
if (connectionIds.length) await stage.change((doc) => deleteEdges(doc, connectionIds));
|
|
308
|
+
for (const id of nodeIds) {
|
|
309
|
+
const nodes = await stage.change(readNodes);
|
|
310
|
+
const node = nodes.get(id);
|
|
311
|
+
if (!node) continue;
|
|
312
|
+
hand.say(`Removing ${nameOf(node)}`);
|
|
313
|
+
await hand.moveTo(nodeCenter(node, nodes));
|
|
314
|
+
hand.select([id]);
|
|
315
|
+
await hand.pause(250);
|
|
316
|
+
await stage.change((doc) => deleteNodes(doc, [id]));
|
|
317
|
+
hand.select([]);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Commenting, played out: go to where the comment goes, then leave it */
|
|
322
|
+
export async function playComment(
|
|
323
|
+
stage: AgentStage,
|
|
324
|
+
{ text, nodeId, x, y }: { text: string; nodeId?: string; x?: number; y?: number },
|
|
325
|
+
author: Author
|
|
326
|
+
) {
|
|
327
|
+
const nodes = await stage.change(readNodes);
|
|
328
|
+
const node = nodeId ? nodes.get(nodeId) : undefined;
|
|
329
|
+
if (nodeId && !node) return { error: `No node "${nodeId}" on the canvas` };
|
|
330
|
+
const offset = node ? { x: sizeOf(node).width - 8, y: 8 } : undefined;
|
|
331
|
+
const position = node && positionOf(node, nodes);
|
|
332
|
+
const anchor: CommentAnchor =
|
|
333
|
+
node && offset && position
|
|
334
|
+
? { position: { x: position.x + offset.x, y: position.y + offset.y }, nodeId: node.id, offset }
|
|
335
|
+
: { position: { x: x ?? 0, y: y ?? 0 } };
|
|
336
|
+
const hand = createAgentHand(stage);
|
|
337
|
+
hand.say('Commenting');
|
|
338
|
+
await hand.moveTo(anchor.position);
|
|
339
|
+
await hand.pause(400);
|
|
340
|
+
const threadId = await stage.change((doc) => createThread(doc, anchor, text, author));
|
|
341
|
+
return { threadId };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Pointing at a node while changing it (e.g. a rename) */
|
|
345
|
+
export async function playOnNode<T>(stage: AgentStage, nodeId: string, activity: string, change: (doc: Y.Doc) => T) {
|
|
346
|
+
const nodes = await stage.change(readNodes);
|
|
347
|
+
const node = nodes.get(nodeId);
|
|
348
|
+
const hand = createAgentHand(stage);
|
|
349
|
+
if (node) {
|
|
350
|
+
hand.say(activity);
|
|
351
|
+
await hand.moveTo(nodeCenter(node, nodes));
|
|
352
|
+
hand.select([nodeId]);
|
|
353
|
+
await hand.pause(250);
|
|
354
|
+
}
|
|
355
|
+
return stage.change(change);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Updating a node, played out: point at it to rename or re-describe it, then drag it to its new centre (x/y) */
|
|
359
|
+
export async function playUpdateNode(
|
|
360
|
+
stage: AgentStage,
|
|
361
|
+
{ nodeId, name, summary, x, y }: { nodeId: string; name?: string; summary?: string; x?: number; y?: number }
|
|
362
|
+
) {
|
|
363
|
+
const missing = { error: `No node "${nodeId}" on the canvas` };
|
|
364
|
+
if (name !== undefined || summary !== undefined) {
|
|
365
|
+
const outcome = await playOnNode(stage, nodeId, 'Editing', (doc) => updateCanvasNode(doc, { nodeId, name, summary }));
|
|
366
|
+
if ('error' in outcome) return outcome;
|
|
367
|
+
}
|
|
368
|
+
if (x !== undefined || y !== undefined) {
|
|
369
|
+
const nodes = await stage.change(readNodes);
|
|
370
|
+
const node = nodes.get(nodeId);
|
|
371
|
+
if (!node) return missing;
|
|
372
|
+
const center = nodeCenter(node, nodes);
|
|
373
|
+
await playMoveNode(stage, nodeId, topLeftFor(node, { x: x ?? center.x, y: y ?? center.y }, nodes));
|
|
374
|
+
}
|
|
375
|
+
const nodes = await stage.change(readNodes);
|
|
376
|
+
const node = nodes.get(nodeId);
|
|
377
|
+
return node ? describeNode(node, nodes) : missing;
|
|
378
|
+
}
|