pmx-canvas 0.1.16 → 0.1.17
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/CHANGELOG.md +65 -0
- package/Readme.md +2 -2
- package/dist/canvas/global.css +25 -0
- package/dist/canvas/index.js +72 -72
- package/dist/types/client/canvas/AnnotationLayer.d.ts +4 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +4 -1
- package/dist/types/client/canvas/use-pan-zoom.d.ts +2 -1
- package/dist/types/client/icons.d.ts +4 -0
- package/dist/types/client/state/canvas-store.d.ts +16 -1
- package/dist/types/client/types.d.ts +20 -0
- package/dist/types/mcp/canvas-access.d.ts +1 -0
- package/dist/types/server/canvas-serialization.d.ts +23 -1
- package/dist/types/server/canvas-state.d.ts +27 -1
- package/dist/types/server/index.d.ts +7 -2
- package/dist/types/server/mutation-history.d.ts +1 -1
- package/dist/types/server/spatial-analysis.d.ts +11 -2
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +17 -0
- package/src/cli/agent.ts +6 -0
- package/src/client/App.tsx +60 -3
- package/src/client/canvas/AnnotationLayer.tsx +28 -0
- package/src/client/canvas/CanvasViewport.tsx +169 -10
- package/src/client/canvas/ContextPinBar.tsx +2 -1
- package/src/client/canvas/use-pan-zoom.ts +10 -5
- package/src/client/icons.tsx +22 -0
- package/src/client/state/canvas-store.ts +52 -2
- package/src/client/state/sse-bridge.ts +35 -1
- package/src/client/theme/global.css +25 -0
- package/src/client/types.ts +17 -0
- package/src/mcp/canvas-access.ts +10 -0
- package/src/mcp/server.ts +35 -4
- package/src/server/canvas-schema.ts +25 -0
- package/src/server/canvas-serialization.ts +69 -1
- package/src/server/canvas-state.ts +74 -2
- package/src/server/diagram-presets.ts +54 -19
- package/src/server/index.ts +20 -3
- package/src/server/mutation-history.ts +2 -0
- package/src/server/server.ts +77 -2
- package/src/server/spatial-analysis.ts +46 -1
- package/src/shared/semantic-attention.ts +4 -2
package/src/server/server.ts
CHANGED
|
@@ -46,7 +46,7 @@ import type {
|
|
|
46
46
|
ListResourceTemplatesResult,
|
|
47
47
|
ListToolsResult,
|
|
48
48
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
49
|
-
import { type CanvasEdge, type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
|
|
49
|
+
import { type CanvasAnnotation, type CanvasEdge, type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
|
|
50
50
|
import { findCanvasExtAppNodeId as findCanvasExtAppNodeIdShared } from './ext-app-lookup.js';
|
|
51
51
|
import { normalizeExtAppToolResult } from './ext-app-tool-result.js';
|
|
52
52
|
import { getMcpAppHostSnapshot } from './mcp-app-host.js';
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
serializeCanvasLayoutWithBlobSummaries,
|
|
72
72
|
serializeCanvasNode,
|
|
73
73
|
serializeCanvasNodeWithBlobSummaries,
|
|
74
|
+
summarizeCanvasAnnotation,
|
|
74
75
|
} from './canvas-serialization.js';
|
|
75
76
|
import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
76
77
|
import { buildAgentContextPreamble, serializeNodeForAgentContext } from './agent-context.js';
|
|
@@ -1227,6 +1228,72 @@ async function handleCanvasViewport(req: Request): Promise<Response> {
|
|
|
1227
1228
|
return responseJson({ ok: true });
|
|
1228
1229
|
}
|
|
1229
1230
|
|
|
1231
|
+
function annotationBounds(points: CanvasAnnotation['points']): CanvasAnnotation['bounds'] {
|
|
1232
|
+
const xs = points.map((point) => point.x);
|
|
1233
|
+
const ys = points.map((point) => point.y);
|
|
1234
|
+
const minX = Math.min(...xs);
|
|
1235
|
+
const minY = Math.min(...ys);
|
|
1236
|
+
const maxX = Math.max(...xs);
|
|
1237
|
+
const maxY = Math.max(...ys);
|
|
1238
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function parseAnnotationPoints(value: unknown): CanvasAnnotation['points'] {
|
|
1242
|
+
if (!Array.isArray(value)) return [];
|
|
1243
|
+
return value
|
|
1244
|
+
.map((point) => {
|
|
1245
|
+
if (!point || typeof point !== 'object' || Array.isArray(point)) return null;
|
|
1246
|
+
const record = point as Record<string, unknown>;
|
|
1247
|
+
if (typeof record.x !== 'number' || typeof record.y !== 'number') return null;
|
|
1248
|
+
if (!Number.isFinite(record.x) || !Number.isFinite(record.y)) return null;
|
|
1249
|
+
return { x: record.x, y: record.y };
|
|
1250
|
+
})
|
|
1251
|
+
.filter((point): point is CanvasAnnotation['points'][number] => point !== null);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
async function handleCanvasAddAnnotation(req: Request): Promise<Response> {
|
|
1255
|
+
const body = await readJson(req);
|
|
1256
|
+
const points = parseAnnotationPoints(body.points);
|
|
1257
|
+
if (points.length < 2) {
|
|
1258
|
+
return responseJson({ ok: false, error: 'Annotation requires at least two valid points.' }, 400);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const width = typeof body.width === 'number' && Number.isFinite(body.width)
|
|
1262
|
+
? Math.min(24, Math.max(1, body.width))
|
|
1263
|
+
: 4;
|
|
1264
|
+
const color = typeof body.color === 'string' && (body.color === 'currentColor' || /^#[0-9a-fA-F]{6}$/.test(body.color))
|
|
1265
|
+
? body.color
|
|
1266
|
+
: 'currentColor';
|
|
1267
|
+
const label = typeof body.label === 'string' && body.label.trim().length > 0
|
|
1268
|
+
? body.label.trim().slice(0, 160)
|
|
1269
|
+
: undefined;
|
|
1270
|
+
const id = typeof body.id === 'string' && body.id.trim().length > 0
|
|
1271
|
+
? body.id.trim().slice(0, 120)
|
|
1272
|
+
: `ann-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1273
|
+
const annotation: CanvasAnnotation = {
|
|
1274
|
+
id,
|
|
1275
|
+
type: 'freehand',
|
|
1276
|
+
points,
|
|
1277
|
+
bounds: annotationBounds(points),
|
|
1278
|
+
color,
|
|
1279
|
+
width,
|
|
1280
|
+
...(label ? { label } : {}),
|
|
1281
|
+
createdAt: new Date().toISOString(),
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
canvasState.addAnnotation(annotation);
|
|
1285
|
+
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1286
|
+
return responseJson({ ok: true, annotation: summarizeCanvasAnnotation(annotation) });
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function handleCanvasRemoveAnnotation(id: string): Response {
|
|
1290
|
+
const decodedId = decodeURIComponent(id);
|
|
1291
|
+
const removed = canvasState.removeAnnotation(decodedId);
|
|
1292
|
+
if (!removed) return responseJson({ ok: false, error: `Annotation "${decodedId}" not found.` }, 404);
|
|
1293
|
+
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1294
|
+
return responseJson({ ok: true, removed: decodedId });
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1230
1297
|
// ── Serve image file for image nodes ─────────────────────────
|
|
1231
1298
|
async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
1232
1299
|
const nodeId = pathname.replace('/api/canvas/image/', '');
|
|
@@ -4002,6 +4069,14 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4002
4069
|
return handleCanvasViewport(req);
|
|
4003
4070
|
}
|
|
4004
4071
|
|
|
4072
|
+
if (url.pathname === '/api/canvas/annotation' && req.method === 'POST') {
|
|
4073
|
+
return handleCanvasAddAnnotation(req);
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
if (url.pathname.startsWith('/api/canvas/annotation/') && req.method === 'DELETE') {
|
|
4077
|
+
return handleCanvasRemoveAnnotation(url.pathname.slice('/api/canvas/annotation/'.length));
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4005
4080
|
if (url.pathname === '/api/canvas/node' && req.method === 'POST') {
|
|
4006
4081
|
return handleCanvasAddNode(req);
|
|
4007
4082
|
}
|
|
@@ -4117,7 +4192,7 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4117
4192
|
// Spatial context API
|
|
4118
4193
|
if (url.pathname === '/api/canvas/spatial-context' && req.method === 'GET') {
|
|
4119
4194
|
const layout = canvasState.getLayout();
|
|
4120
|
-
const spatial = buildSpatialContext(layout.nodes, layout.edges, canvasState.contextPinnedNodeIds);
|
|
4195
|
+
const spatial = buildSpatialContext(layout.nodes, layout.edges, canvasState.contextPinnedNodeIds, layout.annotations);
|
|
4121
4196
|
return responseJson(spatial);
|
|
4122
4197
|
}
|
|
4123
4198
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* semantic clusters, ordered context, and implicit human intent.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { CanvasNodeState, CanvasEdge } from './canvas-state.js';
|
|
13
|
+
import type { CanvasAnnotation, CanvasNodeState, CanvasEdge } from './canvas-state.js';
|
|
14
14
|
import { summarizeNodeForAgentContext } from './agent-context.js';
|
|
15
15
|
|
|
16
16
|
// ── Types ────────────────────────────────────────────────────────────
|
|
@@ -45,6 +45,15 @@ export interface NodeSpatialInfo {
|
|
|
45
45
|
readingOrder: number;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export interface SpatialAnnotationContext {
|
|
49
|
+
id: string;
|
|
50
|
+
label: string | null;
|
|
51
|
+
bounds: CanvasAnnotation['bounds'];
|
|
52
|
+
targetNodeIds: string[];
|
|
53
|
+
targetNodeTitles: string[];
|
|
54
|
+
target: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
export interface SpatialContext {
|
|
49
58
|
/** Total nodes on canvas */
|
|
50
59
|
totalNodes: number;
|
|
@@ -58,6 +67,7 @@ export interface SpatialContext {
|
|
|
58
67
|
pinnedNodeTitle: string | null;
|
|
59
68
|
neighbors: SpatialNeighbor[];
|
|
60
69
|
}[];
|
|
70
|
+
annotations: SpatialAnnotationContext[];
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
@@ -125,6 +135,39 @@ function deriveClusterLabel(nodes: CanvasNodeState[]): string {
|
|
|
125
135
|
return parts.join(', ');
|
|
126
136
|
}
|
|
127
137
|
|
|
138
|
+
function rectsOverlap(
|
|
139
|
+
a: { x: number; y: number; width: number; height: number },
|
|
140
|
+
b: { x: number; y: number; width: number; height: number },
|
|
141
|
+
): boolean {
|
|
142
|
+
return a.x <= b.x + b.width &&
|
|
143
|
+
a.x + a.width >= b.x &&
|
|
144
|
+
a.y <= b.y + b.height &&
|
|
145
|
+
a.y + a.height >= b.y;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function summarizeAnnotationForSpatialContext(
|
|
149
|
+
annotation: CanvasAnnotation,
|
|
150
|
+
nodes: CanvasNodeState[],
|
|
151
|
+
): SpatialAnnotationContext {
|
|
152
|
+
const targetNodes = nodes.filter((node) => rectsOverlap(annotation.bounds, {
|
|
153
|
+
x: node.position.x,
|
|
154
|
+
y: node.position.y,
|
|
155
|
+
width: node.size.width,
|
|
156
|
+
height: node.size.height,
|
|
157
|
+
}));
|
|
158
|
+
const targetNodeTitles = targetNodes.map((node) =>
|
|
159
|
+
typeof node.data.title === 'string' && node.data.title.length > 0 ? node.data.title : node.id,
|
|
160
|
+
);
|
|
161
|
+
return {
|
|
162
|
+
id: annotation.id,
|
|
163
|
+
label: annotation.label ?? null,
|
|
164
|
+
bounds: annotation.bounds,
|
|
165
|
+
targetNodeIds: targetNodes.map((node) => node.id),
|
|
166
|
+
targetNodeTitles,
|
|
167
|
+
target: targetNodeTitles.length > 0 ? targetNodeTitles.join(', ') : 'empty canvas region',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
128
171
|
// ── Core Analysis ────────────────────────────────────────────────────
|
|
129
172
|
|
|
130
173
|
/**
|
|
@@ -320,6 +363,7 @@ export function buildSpatialContext(
|
|
|
320
363
|
nodes: CanvasNodeState[],
|
|
321
364
|
_edges: CanvasEdge[],
|
|
322
365
|
pinnedIds: Set<string>,
|
|
366
|
+
annotations: CanvasAnnotation[] = [],
|
|
323
367
|
): SpatialContext {
|
|
324
368
|
const clusters = detectClusters(nodes);
|
|
325
369
|
|
|
@@ -352,5 +396,6 @@ export function buildSpatialContext(
|
|
|
352
396
|
clusters,
|
|
353
397
|
nodesInReadingOrder,
|
|
354
398
|
pinnedNeighborhoods,
|
|
399
|
+
annotations: annotations.map((annotation) => summarizeAnnotationForSpatialContext(annotation, nodes)),
|
|
355
400
|
};
|
|
356
401
|
}
|
|
@@ -371,6 +371,7 @@ export class SemanticWatchReducer {
|
|
|
371
371
|
this.currentLayout.nodes,
|
|
372
372
|
this.currentLayout.edges,
|
|
373
373
|
this.currentPins,
|
|
374
|
+
this.currentLayout.annotations ?? [],
|
|
374
375
|
);
|
|
375
376
|
|
|
376
377
|
if (previousEventPins.added.length === 0 && previousEventPins.removed.length === 0) {
|
|
@@ -394,7 +395,7 @@ export class SemanticWatchReducer {
|
|
|
394
395
|
const meta = normalizeEventMeta(payload);
|
|
395
396
|
if (!this.currentLayout) {
|
|
396
397
|
this.currentLayout = layout;
|
|
397
|
-
this.previousSpatial = buildSpatialContext(layout.nodes, layout.edges, this.currentPins);
|
|
398
|
+
this.previousSpatial = buildSpatialContext(layout.nodes, layout.edges, this.currentPins, layout.annotations ?? []);
|
|
398
399
|
return [];
|
|
399
400
|
}
|
|
400
401
|
|
|
@@ -403,8 +404,9 @@ export class SemanticWatchReducer {
|
|
|
403
404
|
prevLayout.nodes,
|
|
404
405
|
prevLayout.edges,
|
|
405
406
|
this.currentPins,
|
|
407
|
+
prevLayout.annotations ?? [],
|
|
406
408
|
);
|
|
407
|
-
const nextSpatial = buildSpatialContext(layout.nodes, layout.edges, this.currentPins);
|
|
409
|
+
const nextSpatial = buildSpatialContext(layout.nodes, layout.edges, this.currentPins, layout.annotations ?? []);
|
|
408
410
|
const events: SemanticWatchEvent[] = [];
|
|
409
411
|
|
|
410
412
|
const prevNodeMap = toNodeMap(prevLayout.nodes);
|