pmx-canvas 0.1.10 → 0.1.12
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 +131 -0
- package/dist/canvas/index.js +30 -30
- package/dist/json-render/index.js +115 -115
- package/dist/types/json-render/catalog.d.ts +10 -0
- 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/mcp/canvas-access.d.ts +87 -0
- package/dist/types/server/canvas-operations.d.ts +2 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/server.d.ts +1 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +9 -0
- package/src/cli/agent.ts +78 -2
- package/src/cli/index.ts +6 -3
- package/src/client/canvas/CanvasNode.tsx +3 -1
- package/src/client/canvas/auto-fit.ts +1 -1
- 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/server.ts +11 -0
- package/src/mcp/canvas-access.ts +651 -0
- package/src/mcp/server.ts +170 -95
- package/src/server/canvas-operations.ts +21 -1
- package/src/server/canvas-schema.ts +5 -0
- package/src/server/diagram-presets.ts +44 -12
- package/src/server/index.ts +9 -3
- package/src/server/server.ts +37 -3
|
@@ -67,6 +67,7 @@ export interface CanvasStructuredNodeUpdateInput extends Omit<CanvasGraphNodeUpd
|
|
|
67
67
|
content?: unknown;
|
|
68
68
|
data?: unknown;
|
|
69
69
|
arrangeLocked?: unknown;
|
|
70
|
+
strictSize?: boolean;
|
|
70
71
|
chartHeight?: unknown;
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -82,6 +83,7 @@ interface CanvasAddNodeInput {
|
|
|
82
83
|
defaultWidth?: number;
|
|
83
84
|
defaultHeight?: number;
|
|
84
85
|
fileMode?: 'path' | 'inline' | 'auto';
|
|
86
|
+
strictSize?: boolean;
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
interface CanvasCreateGroupInput {
|
|
@@ -164,6 +166,8 @@ function hasGraphUpdateFields(input: Record<string, unknown>): boolean {
|
|
|
164
166
|
input.zKey !== undefined ||
|
|
165
167
|
input.nameKey !== undefined ||
|
|
166
168
|
input.valueKey !== undefined ||
|
|
169
|
+
input.showLegend !== undefined ||
|
|
170
|
+
input.showLabels !== undefined ||
|
|
167
171
|
input.axisKey !== undefined ||
|
|
168
172
|
input.metrics !== undefined ||
|
|
169
173
|
input.series !== undefined ||
|
|
@@ -200,6 +204,7 @@ function mergeNodeDataFields(
|
|
|
200
204
|
...base,
|
|
201
205
|
...(isRecord(input.data) ? input.data : {}),
|
|
202
206
|
...(typeof input.arrangeLocked === 'boolean' ? { arrangeLocked: input.arrangeLocked } : {}),
|
|
207
|
+
...(typeof input.strictSize === 'boolean' ? { strictSize: input.strictSize } : {}),
|
|
203
208
|
};
|
|
204
209
|
}
|
|
205
210
|
|
|
@@ -251,6 +256,8 @@ function graphConfigToInput(config: Record<string, unknown>, fallbackTitle: stri
|
|
|
251
256
|
...(pickString(config, 'zKey') ? { zKey: pickString(config, 'zKey') } : {}),
|
|
252
257
|
...(pickString(config, 'nameKey') ? { nameKey: pickString(config, 'nameKey') } : {}),
|
|
253
258
|
...(pickString(config, 'valueKey') ? { valueKey: pickString(config, 'valueKey') } : {}),
|
|
259
|
+
...(typeof config.showLegend === 'boolean' ? { showLegend: config.showLegend } : {}),
|
|
260
|
+
...(typeof config.showLabels === 'boolean' ? { showLabels: config.showLabels } : {}),
|
|
254
261
|
...(pickString(config, 'axisKey') ? { axisKey: pickString(config, 'axisKey') } : {}),
|
|
255
262
|
...(pickStringArray(config, 'metrics') ? { metrics: pickStringArray(config, 'metrics') } : {}),
|
|
256
263
|
...(pickStringArray(config, 'series') ? { series: pickStringArray(config, 'series') } : {}),
|
|
@@ -276,6 +283,12 @@ function mergeGraphInput(source: Record<string, unknown>, fallback: GraphNodeInp
|
|
|
276
283
|
...((pickString(source, 'zKey') ?? fallback?.zKey) ? { zKey: pickString(source, 'zKey') ?? fallback?.zKey } : {}),
|
|
277
284
|
...((pickString(source, 'nameKey') ?? fallback?.nameKey) ? { nameKey: pickString(source, 'nameKey') ?? fallback?.nameKey } : {}),
|
|
278
285
|
...((pickString(source, 'valueKey') ?? fallback?.valueKey) ? { valueKey: pickString(source, 'valueKey') ?? fallback?.valueKey } : {}),
|
|
286
|
+
...(typeof source.showLegend === 'boolean' || typeof fallback?.showLegend === 'boolean'
|
|
287
|
+
? { showLegend: typeof source.showLegend === 'boolean' ? source.showLegend : fallback?.showLegend }
|
|
288
|
+
: {}),
|
|
289
|
+
...(typeof source.showLabels === 'boolean' || typeof fallback?.showLabels === 'boolean'
|
|
290
|
+
? { showLabels: typeof source.showLabels === 'boolean' ? source.showLabels : fallback?.showLabels }
|
|
291
|
+
: {}),
|
|
279
292
|
...((pickString(source, 'axisKey') ?? fallback?.axisKey) ? { axisKey: pickString(source, 'axisKey') ?? fallback?.axisKey } : {}),
|
|
280
293
|
...((pickStringArray(source, 'metrics') ?? fallback?.metrics) ? { metrics: pickStringArray(source, 'metrics') ?? fallback?.metrics } : {}),
|
|
281
294
|
...((pickStringArray(source, 'series') ?? fallback?.series) ? { series: pickStringArray(source, 'series') ?? fallback?.series } : {}),
|
|
@@ -752,6 +765,7 @@ function buildNodeData(input: CanvasAddNodeInput): Record<string, unknown> {
|
|
|
752
765
|
...(input.data ?? {}),
|
|
753
766
|
...(input.title ? { title: input.title } : {}),
|
|
754
767
|
...(input.content ? { content: input.content } : {}),
|
|
768
|
+
...(input.strictSize ? { strictSize: true } : {}),
|
|
755
769
|
};
|
|
756
770
|
}
|
|
757
771
|
|
|
@@ -1271,6 +1285,7 @@ export function createCanvasJsonRenderNode(
|
|
|
1271
1285
|
dockPosition: null,
|
|
1272
1286
|
data: createJsonRenderNodeData(id, input.title?.trim() || inferJsonRenderNodeTitle(spec), spec, {
|
|
1273
1287
|
viewerType: 'json-render',
|
|
1288
|
+
...(input.strictSize ? { strictSize: true } : {}),
|
|
1274
1289
|
}),
|
|
1275
1290
|
};
|
|
1276
1291
|
|
|
@@ -1302,6 +1317,7 @@ export function createCanvasGraphNode(
|
|
|
1302
1317
|
data: createJsonRenderNodeData(id, title, spec, {
|
|
1303
1318
|
viewerType: 'graph',
|
|
1304
1319
|
graphConfig: buildGraphConfig(input),
|
|
1320
|
+
...(input.strictSize ? { strictSize: true } : {}),
|
|
1305
1321
|
}),
|
|
1306
1322
|
};
|
|
1307
1323
|
|
|
@@ -1418,6 +1434,7 @@ export async function executeCanvasBatch(
|
|
|
1418
1434
|
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1419
1435
|
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1420
1436
|
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1437
|
+
...(args.strictSize === true ? { strictSize: true } : {}),
|
|
1421
1438
|
defaultWidth: 520,
|
|
1422
1439
|
defaultHeight: 420,
|
|
1423
1440
|
});
|
|
@@ -1441,6 +1458,7 @@ export async function executeCanvasBatch(
|
|
|
1441
1458
|
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1442
1459
|
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1443
1460
|
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1461
|
+
...(args.strictSize === true ? { strictSize: true } : {}),
|
|
1444
1462
|
defaultWidth: 360,
|
|
1445
1463
|
defaultHeight: 200,
|
|
1446
1464
|
fileMode: 'auto',
|
|
@@ -1471,12 +1489,13 @@ export async function executeCanvasBatch(
|
|
|
1471
1489
|
if (args.dockPosition === null || args.dockPosition === 'left' || args.dockPosition === 'right') {
|
|
1472
1490
|
patch.dockPosition = args.dockPosition;
|
|
1473
1491
|
}
|
|
1474
|
-
if (typeof args.title === 'string' || typeof args.content === 'string' || typeof args.arrangeLocked === 'boolean' || isPlainRecord(args.data)) {
|
|
1492
|
+
if (typeof args.title === 'string' || typeof args.content === 'string' || typeof args.arrangeLocked === 'boolean' || typeof args.strictSize === 'boolean' || isPlainRecord(args.data)) {
|
|
1475
1493
|
patch.data = {
|
|
1476
1494
|
...node.data,
|
|
1477
1495
|
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1478
1496
|
...(typeof args.content === 'string' ? { content: args.content } : {}),
|
|
1479
1497
|
...(typeof args.arrangeLocked === 'boolean' ? { arrangeLocked: args.arrangeLocked } : {}),
|
|
1498
|
+
...(typeof args.strictSize === 'boolean' ? { strictSize: args.strictSize } : {}),
|
|
1480
1499
|
...(isPlainRecord(args.data) ? args.data : {}),
|
|
1481
1500
|
};
|
|
1482
1501
|
}
|
|
@@ -1511,6 +1530,7 @@ export async function executeCanvasBatch(
|
|
|
1511
1530
|
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1512
1531
|
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1513
1532
|
...(typeof args.nodeHeight === 'number' ? { heightPx: args.nodeHeight } : {}),
|
|
1533
|
+
...(args.strictSize === true ? { strictSize: true } : {}),
|
|
1514
1534
|
});
|
|
1515
1535
|
result = {
|
|
1516
1536
|
ok: true,
|
|
@@ -72,6 +72,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
72
72
|
{ name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
|
|
73
73
|
{ name: 'width', type: 'number', required: false, description: 'Optional node width.' },
|
|
74
74
|
{ name: 'height', type: 'number', required: false, description: 'Optional node height.' },
|
|
75
|
+
{ name: 'strictSize', type: 'boolean', required: false, description: 'Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting.', aliases: ['strict-size', 'scroll-overflow'] },
|
|
75
76
|
],
|
|
76
77
|
example: {
|
|
77
78
|
type: 'markdown',
|
|
@@ -203,6 +204,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
203
204
|
{ name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
|
|
204
205
|
{ name: 'width', type: 'number', required: false, description: 'Optional node width.' },
|
|
205
206
|
{ name: 'height', type: 'number', required: false, description: 'Optional node height.' },
|
|
207
|
+
{ name: 'strictSize', type: 'boolean', required: false, description: 'Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting.', aliases: ['strict-size', 'scroll-overflow'] },
|
|
206
208
|
],
|
|
207
209
|
example: {
|
|
208
210
|
type: 'webpage',
|
|
@@ -337,8 +339,11 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
337
339
|
{ name: 'barColor', type: 'string', required: false, description: 'Optional bar color for composed charts.', aliases: ['bar-color'] },
|
|
338
340
|
{ name: 'lineColor', type: 'string', required: false, description: 'Optional line color for composed charts.', aliases: ['line-color'] },
|
|
339
341
|
{ name: 'height', type: 'number', required: false, description: 'Optional chart content height.', aliases: ['chart-height'] },
|
|
342
|
+
{ name: 'showLegend', type: 'boolean', required: false, description: 'Show chart legend when supported; pass false for compact node layouts.', aliases: ['show-legend'] },
|
|
343
|
+
{ name: 'showLabels', type: 'boolean', required: false, description: 'Show direct labels when supported, such as pie slice labels; defaults to true.', aliases: ['show-labels'] },
|
|
340
344
|
{ name: 'width', type: 'number', required: false, description: 'Optional node width.' },
|
|
341
345
|
{ name: 'nodeHeight', type: 'number', required: false, description: 'Optional node height (canvas frame). Distinct from `height`, which sets only the chart content height inside the node.', aliases: ['node-height'] },
|
|
346
|
+
{ name: 'strictSize', type: 'boolean', required: false, description: 'Keep explicit node size fixed and scroll overflowing content instead of browser auto-fitting.', aliases: ['strict-size', 'scroll-overflow'] },
|
|
342
347
|
],
|
|
343
348
|
example: {
|
|
344
349
|
title: 'Deploy Trend',
|
|
@@ -115,6 +115,18 @@ 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
|
+
|
|
118
130
|
function normalizeExcalidrawBoundText(elements: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
|
119
131
|
const elementsById = new Map<string, Record<string, unknown>>();
|
|
120
132
|
for (const element of elements) {
|
|
@@ -123,38 +135,58 @@ function normalizeExcalidrawBoundText(elements: Array<Record<string, unknown>>):
|
|
|
123
135
|
|
|
124
136
|
let changed = false;
|
|
125
137
|
const boundElementIdsByContainer = new Map<string, Set<string>>();
|
|
138
|
+
const labelByContainer = new Map<string, Record<string, unknown>>();
|
|
139
|
+
const textIdsConvertedToLabels = new Set<string>();
|
|
126
140
|
|
|
127
141
|
for (const element of elements) {
|
|
128
142
|
if (element.type !== 'text' || typeof element.id !== 'string' || typeof element.containerId !== 'string') continue;
|
|
129
|
-
|
|
143
|
+
const container = elementsById.get(element.containerId);
|
|
144
|
+
if (!container) continue;
|
|
130
145
|
const ids = boundElementIdsByContainer.get(element.containerId) ?? new Set<string>();
|
|
131
146
|
ids.add(element.id);
|
|
132
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
|
+
}
|
|
133
156
|
}
|
|
134
157
|
|
|
135
|
-
const normalized = elements.
|
|
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
|
+
}
|
|
136
163
|
if (typeof element.id !== 'string') return element;
|
|
137
164
|
const boundTextIds = boundElementIdsByContainer.get(element.id);
|
|
138
|
-
|
|
165
|
+
const label = labelByContainer.get(element.id);
|
|
166
|
+
if ((!boundTextIds || boundTextIds.size === 0) && !label) return element;
|
|
139
167
|
|
|
140
168
|
const existing = Array.isArray(element.boundElements)
|
|
141
169
|
? element.boundElements.filter(isRecord)
|
|
142
170
|
: [];
|
|
171
|
+
const remainingExisting = existing.filter((boundElement) => {
|
|
172
|
+
return !(boundElement.type === 'text' && typeof boundElement.id === 'string' && textIdsConvertedToLabels.has(boundElement.id));
|
|
173
|
+
});
|
|
143
174
|
const existingTextIds = new Set(
|
|
144
|
-
|
|
175
|
+
remainingExisting
|
|
145
176
|
.filter((boundElement) => boundElement.type === 'text' && typeof boundElement.id === 'string')
|
|
146
177
|
.map((boundElement) => boundElement.id as string),
|
|
147
178
|
);
|
|
148
|
-
const missing = [...boundTextIds
|
|
149
|
-
|
|
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;
|
|
150
182
|
|
|
151
183
|
changed = true;
|
|
152
184
|
return {
|
|
153
185
|
...element,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
...missing.map((id) => ({ type: 'text', id }))
|
|
157
|
-
|
|
186
|
+
...(label ? { label } : {}),
|
|
187
|
+
...(remainingExisting.length > 0 || missing.length > 0
|
|
188
|
+
? { boundElements: [...remainingExisting, ...missing.map((id) => ({ type: 'text', id }))] }
|
|
189
|
+
: {}),
|
|
158
190
|
};
|
|
159
191
|
});
|
|
160
192
|
|
|
@@ -251,12 +283,12 @@ function withInferredCameraUpdate(
|
|
|
251
283
|
|
|
252
284
|
export function normalizeExcalidrawElements(elements: unknown): string {
|
|
253
285
|
const parsed = parseExcalidrawElements(elements);
|
|
254
|
-
return JSON.stringify(parsed
|
|
286
|
+
return JSON.stringify(hasRenderableExcalidrawElement(parsed) ? parsed : DEFAULT_EXCALIDRAW_ELEMENTS);
|
|
255
287
|
}
|
|
256
288
|
|
|
257
289
|
export function normalizeExcalidrawElementsForToolInput(elements: unknown): string {
|
|
258
290
|
const parsed = parseExcalidrawElements(elements);
|
|
259
|
-
const seeded = parsed
|
|
291
|
+
const seeded = hasRenderableExcalidrawElement(parsed) ? parsed : [...DEFAULT_EXCALIDRAW_ELEMENTS];
|
|
260
292
|
return JSON.stringify(withInferredCameraUpdate(normalizeExcalidrawBoundText(seeded)));
|
|
261
293
|
}
|
|
262
294
|
|
package/src/server/index.ts
CHANGED
|
@@ -97,7 +97,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
97
97
|
open?: boolean;
|
|
98
98
|
automationWebView?: boolean | CanvasAutomationWebViewOptions;
|
|
99
99
|
}): Promise<void> {
|
|
100
|
-
const base = startCanvasServer({ port: this._port });
|
|
100
|
+
const base = startCanvasServer({ port: this._port, allowPortFallback: false });
|
|
101
101
|
if (!base) {
|
|
102
102
|
throw new Error(`Failed to start canvas server on port ${this._port}`);
|
|
103
103
|
}
|
|
@@ -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
|
|
|
@@ -599,7 +605,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
599
605
|
async startAutomationWebView(
|
|
600
606
|
options: CanvasAutomationWebViewOptions = {},
|
|
601
607
|
): Promise<CanvasAutomationWebViewStatus> {
|
|
602
|
-
const base = this._server ?? startCanvasServer({ port: this._port });
|
|
608
|
+
const base = this._server ?? startCanvasServer({ port: this._port, allowPortFallback: false });
|
|
603
609
|
if (!base) {
|
|
604
610
|
throw new Error(`Failed to start canvas server on port ${this._port}`);
|
|
605
611
|
}
|
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
|
}
|
|
@@ -3696,6 +3727,7 @@ export interface CanvasServerOptions {
|
|
|
3696
3727
|
port?: number;
|
|
3697
3728
|
workspaceRoot?: string;
|
|
3698
3729
|
autoOpenBrowser?: boolean;
|
|
3730
|
+
allowPortFallback?: boolean;
|
|
3699
3731
|
}
|
|
3700
3732
|
|
|
3701
3733
|
export function startCanvasServer(options: CanvasServerOptions = {}): string | null {
|
|
@@ -3733,7 +3765,9 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3733
3765
|
rotatePrimaryWorkbenchSessionIfNeeded();
|
|
3734
3766
|
|
|
3735
3767
|
const preferredPort = options.port ?? Number(process.env.PMX_WEB_CANVAS_PORT ?? DEFAULT_PORT);
|
|
3736
|
-
const portCandidates =
|
|
3768
|
+
const portCandidates = options.allowPortFallback === false
|
|
3769
|
+
? [preferredPort > 0 ? Math.floor(preferredPort) : DEFAULT_PORT]
|
|
3770
|
+
: buildPortCandidates(preferredPort);
|
|
3737
3771
|
|
|
3738
3772
|
for (const portCandidate of portCandidates) {
|
|
3739
3773
|
try {
|