pmx-canvas 0.1.9 → 0.1.11

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 (34) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/dist/canvas/index.js +44 -44
  3. package/dist/json-render/index.css +1 -1
  4. package/dist/json-render/index.js +115 -115
  5. package/dist/types/client/canvas/auto-fit.d.ts +1 -1
  6. package/dist/types/json-render/catalog.d.ts +326 -310
  7. package/dist/types/json-render/charts/components.d.ts +18 -0
  8. package/dist/types/json-render/charts/definitions.d.ts +4 -0
  9. package/dist/types/json-render/charts/extra-components.d.ts +3 -0
  10. package/dist/types/json-render/charts/extra-definitions.d.ts +6 -0
  11. package/dist/types/json-render/server.d.ts +4 -0
  12. package/dist/types/server/canvas-operations.d.ts +2 -0
  13. package/dist/types/server/index.d.ts +2 -0
  14. package/package.json +1 -1
  15. package/skills/pmx-canvas/SKILL.md +9 -0
  16. package/src/cli/agent.ts +103 -5
  17. package/src/cli/index.ts +6 -3
  18. package/src/client/canvas/CanvasNode.tsx +3 -1
  19. package/src/client/canvas/auto-fit.ts +3 -3
  20. package/src/json-render/catalog.ts +9 -0
  21. package/src/json-render/charts/components.tsx +18 -10
  22. package/src/json-render/charts/definitions.ts +4 -0
  23. package/src/json-render/charts/extra-components.tsx +23 -16
  24. package/src/json-render/charts/extra-definitions.ts +6 -0
  25. package/src/json-render/renderer/index.css +61 -0
  26. package/src/json-render/renderer/index.tsx +22 -0
  27. package/src/json-render/server.ts +11 -11
  28. package/src/mcp/server.ts +10 -0
  29. package/src/server/canvas-operations.ts +21 -1
  30. package/src/server/canvas-schema.ts +5 -0
  31. package/src/server/canvas-validation.ts +9 -2
  32. package/src/server/diagram-presets.ts +82 -4
  33. package/src/server/index.ts +7 -1
  34. package/src/server/server.ts +33 -2
@@ -115,6 +115,84 @@ function elementHasCameraUpdate(elements: Array<Record<string, unknown>>): boole
115
115
  return elements.some((element) => element.type === 'cameraUpdate');
116
116
  }
117
117
 
118
+ function hasRenderableExcalidrawElement(elements: Array<Record<string, unknown>>): boolean {
119
+ return elements.some((element) => {
120
+ if (element.isDeleted === true) return false;
121
+ if (element.type === 'cameraUpdate' || element.type === 'restoreCheckpoint' || element.type === 'delete') return false;
122
+ if (typeof element.type !== 'string' || element.type.length === 0) return false;
123
+ if (element.type === 'text') {
124
+ return typeof element.text === 'string' && element.text.trim().length > 0;
125
+ }
126
+ return finiteNumber(element.x) !== null && finiteNumber(element.y) !== null;
127
+ });
128
+ }
129
+
130
+ function normalizeExcalidrawBoundText(elements: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
131
+ const elementsById = new Map<string, Record<string, unknown>>();
132
+ for (const element of elements) {
133
+ if (typeof element.id === 'string') elementsById.set(element.id, element);
134
+ }
135
+
136
+ let changed = false;
137
+ const boundElementIdsByContainer = new Map<string, Set<string>>();
138
+ const labelByContainer = new Map<string, Record<string, unknown>>();
139
+ const textIdsConvertedToLabels = new Set<string>();
140
+
141
+ for (const element of elements) {
142
+ if (element.type !== 'text' || typeof element.id !== 'string' || typeof element.containerId !== 'string') continue;
143
+ const container = elementsById.get(element.containerId);
144
+ if (!container) continue;
145
+ const ids = boundElementIdsByContainer.get(element.containerId) ?? new Set<string>();
146
+ ids.add(element.id);
147
+ boundElementIdsByContainer.set(element.containerId, ids);
148
+ const text = typeof element.text === 'string' ? element.text.trim() : '';
149
+ if (!isRecord(container.label) && text.length > 0) {
150
+ labelByContainer.set(element.containerId, {
151
+ text,
152
+ ...(typeof element.fontSize === 'number' && Number.isFinite(element.fontSize) ? { fontSize: element.fontSize } : {}),
153
+ });
154
+ textIdsConvertedToLabels.add(element.id);
155
+ }
156
+ }
157
+
158
+ const normalized = elements.flatMap<Record<string, unknown>>((element) => {
159
+ if (typeof element.id === 'string' && textIdsConvertedToLabels.has(element.id)) {
160
+ changed = true;
161
+ return [];
162
+ }
163
+ if (typeof element.id !== 'string') return element;
164
+ const boundTextIds = boundElementIdsByContainer.get(element.id);
165
+ const label = labelByContainer.get(element.id);
166
+ if ((!boundTextIds || boundTextIds.size === 0) && !label) return element;
167
+
168
+ const existing = Array.isArray(element.boundElements)
169
+ ? element.boundElements.filter(isRecord)
170
+ : [];
171
+ const remainingExisting = existing.filter((boundElement) => {
172
+ return !(boundElement.type === 'text' && typeof boundElement.id === 'string' && textIdsConvertedToLabels.has(boundElement.id));
173
+ });
174
+ const existingTextIds = new Set(
175
+ remainingExisting
176
+ .filter((boundElement) => boundElement.type === 'text' && typeof boundElement.id === 'string')
177
+ .map((boundElement) => boundElement.id as string),
178
+ );
179
+ const missing = [...(boundTextIds ?? [])]
180
+ .filter((id) => !textIdsConvertedToLabels.has(id) && !existingTextIds.has(id));
181
+ if (missing.length === 0 && !label && remainingExisting.length === existing.length) return element;
182
+
183
+ changed = true;
184
+ return {
185
+ ...element,
186
+ ...(label ? { label } : {}),
187
+ ...(remainingExisting.length > 0 || missing.length > 0
188
+ ? { boundElements: [...remainingExisting, ...missing.map((id) => ({ type: 'text', id }))] }
189
+ : {}),
190
+ };
191
+ });
192
+
193
+ return changed ? normalized : elements;
194
+ }
195
+
118
196
  function resolveExcalidrawCameraSize(width: number, height: number): { width: number; height: number } {
119
197
  const requiredWidth = Math.max(EXCALIDRAW_MIN_CAMERA_WIDTH, width);
120
198
  const requiredHeight = Math.max(EXCALIDRAW_MIN_CAMERA_HEIGHT, height);
@@ -205,19 +283,19 @@ function withInferredCameraUpdate(
205
283
 
206
284
  export function normalizeExcalidrawElements(elements: unknown): string {
207
285
  const parsed = parseExcalidrawElements(elements);
208
- return JSON.stringify(parsed.length > 0 ? parsed : DEFAULT_EXCALIDRAW_ELEMENTS);
286
+ return JSON.stringify(hasRenderableExcalidrawElement(parsed) ? parsed : DEFAULT_EXCALIDRAW_ELEMENTS);
209
287
  }
210
288
 
211
289
  export function normalizeExcalidrawElementsForToolInput(elements: unknown): string {
212
290
  const parsed = parseExcalidrawElements(elements);
213
- const seeded = parsed.length > 0 ? parsed : [...DEFAULT_EXCALIDRAW_ELEMENTS];
214
- return JSON.stringify(withInferredCameraUpdate(seeded));
291
+ const seeded = hasRenderableExcalidrawElement(parsed) ? parsed : [...DEFAULT_EXCALIDRAW_ELEMENTS];
292
+ return JSON.stringify(withInferredCameraUpdate(normalizeExcalidrawBoundText(seeded)));
215
293
  }
216
294
 
217
295
  export function normalizeExcalidrawCheckpointDataForToolInput(data: unknown): string | null {
218
296
  const elements = parseExcalidrawCheckpointElements(data);
219
297
 
220
- return elements ? JSON.stringify(withInferredCameraUpdate(elements)) : null;
298
+ return elements ? JSON.stringify(withInferredCameraUpdate(normalizeExcalidrawBoundText(elements))) : null;
221
299
  }
222
300
 
223
301
  export function buildExcalidrawRestoreCheckpointToolInput(checkpointId: string, data?: unknown): string {
@@ -162,6 +162,7 @@ export class PmxCanvas extends EventEmitter {
162
162
  y?: number;
163
163
  width?: number;
164
164
  height?: number;
165
+ strictSize?: boolean;
165
166
  }): string {
166
167
  if (input.type === 'webpage') {
167
168
  throw new Error('Use addWebpageNode for webpage nodes so page content is fetched and cached on the server.');
@@ -171,6 +172,7 @@ export class PmxCanvas extends EventEmitter {
171
172
  defaultWidth: 360,
172
173
  defaultHeight: 200,
173
174
  fileMode: 'path',
175
+ ...(input.strictSize ? { strictSize: true } : {}),
174
176
  });
175
177
 
176
178
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
@@ -191,6 +193,7 @@ export class PmxCanvas extends EventEmitter {
191
193
  y?: number;
192
194
  width?: number;
193
195
  height?: number;
196
+ strictSize?: boolean;
194
197
  }): Promise<{ ok: boolean; id: string; error?: string; fetch: { ok: boolean; error?: string } }> {
195
198
  const { id } = addCanvasNode({
196
199
  type: 'webpage',
@@ -200,6 +203,7 @@ export class PmxCanvas extends EventEmitter {
200
203
  ...(typeof input.y === 'number' ? { y: input.y } : {}),
201
204
  ...(typeof input.width === 'number' ? { width: input.width } : {}),
202
205
  ...(typeof input.height === 'number' ? { height: input.height } : {}),
206
+ ...(input.strictSize ? { strictSize: true } : {}),
203
207
  defaultWidth: 520,
204
208
  defaultHeight: 420,
205
209
  });
@@ -238,7 +242,8 @@ export class PmxCanvas extends EventEmitter {
238
242
  patch.data !== undefined ||
239
243
  patch.title !== undefined ||
240
244
  patch.content !== undefined ||
241
- typeof patch.arrangeLocked === 'boolean'
245
+ typeof patch.arrangeLocked === 'boolean' ||
246
+ typeof patch.strictSize === 'boolean'
242
247
  ) {
243
248
  resolvedPatch.data = {
244
249
  ...existing.data,
@@ -246,6 +251,7 @@ export class PmxCanvas extends EventEmitter {
246
251
  ...(typeof patch.title === 'string' ? { title: patch.title } : {}),
247
252
  ...(typeof patch.content === 'string' ? { content: patch.content } : {}),
248
253
  ...(typeof patch.arrangeLocked === 'boolean' ? { arrangeLocked: patch.arrangeLocked } : {}),
254
+ ...(typeof patch.strictSize === 'boolean' ? { strictSize: patch.strictSize } : {}),
249
255
  };
250
256
  }
251
257
 
@@ -828,6 +828,24 @@ function findOnlyPendingCanvasExtAppNodeId(serverName: string, toolName: string)
828
828
  return matchId;
829
829
  }
830
830
 
831
+ function extAppEventGeometryPatch(
832
+ node: CanvasNodeState,
833
+ payload: PrimaryWorkbenchEventPayload,
834
+ ): Partial<Pick<CanvasNodeState, 'position' | 'size'>> {
835
+ const x = typeof payload.x === 'number' ? payload.x : undefined;
836
+ const y = typeof payload.y === 'number' ? payload.y : undefined;
837
+ const width = typeof payload.width === 'number' ? payload.width : undefined;
838
+ const height = typeof payload.height === 'number' ? payload.height : undefined;
839
+ return {
840
+ ...(x !== undefined || y !== undefined
841
+ ? { position: { x: x ?? node.position.x, y: y ?? node.position.y } }
842
+ : {}),
843
+ ...(width !== undefined || height !== undefined
844
+ ? { size: { width: width ?? node.size.width, height: height ?? node.size.height } }
845
+ : {}),
846
+ };
847
+ }
848
+
831
849
  function toSseFrame(event: string, payload: PrimaryWorkbenchEventPayload): Uint8Array {
832
850
  const id = nextWorkbenchEventId++;
833
851
  const lines = [`id: ${id}`, `event: ${event}`, `data: ${JSON.stringify(payload)}`, ''];
@@ -1250,6 +1268,7 @@ async function createCanvasWebpageNode(body: Record<string, unknown>): Promise<R
1250
1268
  ...(typeof body.title === 'string' ? { title: body.title } : {}),
1251
1269
  content: normalizedUrl,
1252
1270
  ...(extraData ? { data: extraData } : {}),
1271
+ ...(body.strictSize === true ? { strictSize: true } : {}),
1253
1272
  ...geometry,
1254
1273
  ...(geometry.width === undefined ? { width: WEBPAGE_NODE_DEFAULT_SIZE.width } : {}),
1255
1274
  ...(geometry.height === undefined ? { height: WEBPAGE_NODE_DEFAULT_SIZE.height } : {}),
@@ -1312,6 +1331,7 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
1312
1331
  ...(typeof body.title === 'string' ? { title: body.title } : {}),
1313
1332
  ...(typeof content === 'string' ? { content } : {}),
1314
1333
  ...(extraData ? { data: extraData } : {}),
1334
+ ...(body.strictSize === true ? { strictSize: true } : {}),
1315
1335
  ...geometry,
1316
1336
  defaultWidth: 360,
1317
1337
  defaultHeight: 200,
@@ -1475,7 +1495,7 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
1475
1495
  } catch (error) {
1476
1496
  return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
1477
1497
  }
1478
- } else if (body.title !== undefined || body.content !== undefined || body.data || typeof body.arrangeLocked === 'boolean') {
1498
+ } else if (body.title !== undefined || body.content !== undefined || body.data || typeof body.arrangeLocked === 'boolean' || typeof body.strictSize === 'boolean') {
1479
1499
  const data = { ...existing.data };
1480
1500
  if (body.title !== undefined) {
1481
1501
  data.title = String(body.title);
@@ -1485,6 +1505,7 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
1485
1505
  }
1486
1506
  if (body.content !== undefined) data.content = String(body.content);
1487
1507
  if (typeof body.arrangeLocked === 'boolean') data.arrangeLocked = body.arrangeLocked;
1508
+ if (typeof body.strictSize === 'boolean') data.strictSize = body.strictSize;
1488
1509
  // Merge extra data fields (for status, context, ledger, trace nodes)
1489
1510
  if (body.data && typeof body.data === 'object' && !Array.isArray(body.data)) {
1490
1511
  Object.assign(data, body.data as Record<string, unknown>);
@@ -1720,6 +1741,7 @@ async function handleCanvasAddJsonRender(req: Request): Promise<Response> {
1720
1741
  const result = createCanvasJsonRenderNode({
1721
1742
  ...(title ? { title } : {}),
1722
1743
  spec: rawSpec,
1744
+ ...(body.strictSize === true ? { strictSize: true } : {}),
1723
1745
  ...geometry,
1724
1746
  });
1725
1747
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
@@ -1756,6 +1778,8 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
1756
1778
  const y = pickFiniteNumber(body, 'y') ?? (position ? pickFiniteNumber(position, 'y') : undefined);
1757
1779
  const width = pickPositiveNumber(body, 'width') ?? (size ? pickPositiveNumber(size, 'width') : undefined);
1758
1780
  const nodeHeight = pickPositiveNumber(body, 'nodeHeight') ?? (size ? pickPositiveNumber(size, 'height') : undefined);
1781
+ const showLegend = typeof body.showLegend === 'boolean' ? body.showLegend : undefined;
1782
+ const showLabels = typeof body.showLabels === 'boolean' ? body.showLabels : undefined;
1759
1783
  const result = createCanvasGraphNode({
1760
1784
  title,
1761
1785
  graphType,
@@ -1775,6 +1799,9 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
1775
1799
  ...(typeof body.barColor === 'string' ? { barColor: body.barColor } : {}),
1776
1800
  ...(typeof body.lineColor === 'string' ? { lineColor: body.lineColor } : {}),
1777
1801
  ...(typeof body.height === 'number' ? { height: body.height } : {}),
1802
+ ...(showLegend !== undefined ? { showLegend } : {}),
1803
+ ...(showLabels !== undefined ? { showLabels } : {}),
1804
+ ...(body.strictSize === true ? { strictSize: true } : {}),
1778
1805
  ...(x !== undefined ? { x } : {}),
1779
1806
  ...(y !== undefined ? { y } : {}),
1780
1807
  ...(width !== undefined ? { width } : {}),
@@ -3336,7 +3363,10 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
3336
3363
  if (previousSessionId && nextSessionId && previousSessionId !== nextSessionId) {
3337
3364
  closeMcpAppSession(previousSessionId);
3338
3365
  }
3339
- canvasState.updateNode(id, { data: { ...existing.data, ...dataPatch } });
3366
+ canvasState.updateNode(id, {
3367
+ data: { ...existing.data, ...dataPatch },
3368
+ ...extAppEventGeometryPatch(existing, payload),
3369
+ });
3340
3370
  } else {
3341
3371
  const reusableNodeId =
3342
3372
  typeof payload.serverName === 'string' &&
@@ -3355,6 +3385,7 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
3355
3385
  }
3356
3386
  canvasState.updateNode(reusableNodeId, {
3357
3387
  data: { ...reusableNode.data, ...dataPatch },
3388
+ ...extAppEventGeometryPatch(reusableNode, payload),
3358
3389
  });
3359
3390
  return;
3360
3391
  }