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.
- package/CHANGELOG.md +154 -0
- package/dist/canvas/index.js +44 -44
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +115 -115
- package/dist/types/client/canvas/auto-fit.d.ts +1 -1
- package/dist/types/json-render/catalog.d.ts +326 -310
- package/dist/types/json-render/charts/components.d.ts +18 -0
- package/dist/types/json-render/charts/definitions.d.ts +4 -0
- package/dist/types/json-render/charts/extra-components.d.ts +3 -0
- package/dist/types/json-render/charts/extra-definitions.d.ts +6 -0
- package/dist/types/json-render/server.d.ts +4 -0
- package/dist/types/server/canvas-operations.d.ts +2 -0
- package/dist/types/server/index.d.ts +2 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +9 -0
- package/src/cli/agent.ts +103 -5
- package/src/cli/index.ts +6 -3
- package/src/client/canvas/CanvasNode.tsx +3 -1
- package/src/client/canvas/auto-fit.ts +3 -3
- package/src/json-render/catalog.ts +9 -0
- package/src/json-render/charts/components.tsx +18 -10
- package/src/json-render/charts/definitions.ts +4 -0
- package/src/json-render/charts/extra-components.tsx +23 -16
- package/src/json-render/charts/extra-definitions.ts +6 -0
- package/src/json-render/renderer/index.css +61 -0
- package/src/json-render/renderer/index.tsx +22 -0
- package/src/json-render/server.ts +11 -11
- package/src/mcp/server.ts +10 -0
- package/src/server/canvas-operations.ts +21 -1
- package/src/server/canvas-schema.ts +5 -0
- package/src/server/canvas-validation.ts +9 -2
- package/src/server/diagram-presets.ts +82 -4
- package/src/server/index.ts +7 -1
- 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
|
|
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
|
|
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 {
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
|
package/src/server/server.ts
CHANGED
|
@@ -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, {
|
|
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
|
}
|