pmx-canvas 0.1.3 → 0.1.4
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 +82 -0
- package/dist/canvas/index.js +25 -25
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +1 -1
- package/dist/types/json-render/charts/components.d.ts +2 -1
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/dist/types/server/ext-app-lookup.d.ts +22 -0
- package/dist/types/server/index.d.ts +2 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +9 -5
- package/src/cli/agent.ts +38 -16
- package/src/client/state/sse-bridge.ts +14 -4
- package/src/json-render/charts/components.tsx +6 -4
- package/src/json-render/charts/extra-components.tsx +5 -5
- package/src/json-render/renderer/index.css +14 -0
- package/src/mcp/server.ts +16 -0
- package/src/server/canvas-operations.ts +8 -0
- package/src/server/canvas-schema.ts +15 -14
- package/src/server/canvas-serialization.ts +19 -1
- package/src/server/ext-app-lookup.ts +49 -0
- package/src/server/index.ts +11 -15
- package/src/server/server.ts +54 -20
- package/src/server/web-artifacts.ts +1 -0
package/src/server/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { canvasState, IMAGE_MIME_MAP } from './canvas-state.js';
|
|
3
3
|
import type { CanvasNodeState, CanvasEdge, CanvasLayout, ViewportState } from './canvas-state.js';
|
|
4
|
+
import { findCanvasExtAppNodeId } from './ext-app-lookup.js';
|
|
4
5
|
import { onFileNodeChanged } from './file-watcher.js';
|
|
5
6
|
import { findOpenCanvasPosition, computeGroupBounds } from './placement.js';
|
|
6
7
|
import { searchNodes, buildSpatialContext } from './spatial-analysis.js';
|
|
@@ -440,18 +441,10 @@ export class PmxCanvas extends EventEmitter {
|
|
|
440
441
|
}
|
|
441
442
|
|
|
442
443
|
private findCanvasExtAppNodeId(toolCallId: string): string | null {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
node.type === 'mcp-app' &&
|
|
448
|
-
node.data.mode === 'ext-app' &&
|
|
449
|
-
node.data.toolCallId === toolCallId
|
|
450
|
-
) {
|
|
451
|
-
return node.id;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
return null;
|
|
444
|
+
return findCanvasExtAppNodeId(toolCallId, {
|
|
445
|
+
getNode: (id) => canvasState.getNode(id),
|
|
446
|
+
listNodes: () => canvasState.getLayout().nodes,
|
|
447
|
+
});
|
|
455
448
|
}
|
|
456
449
|
|
|
457
450
|
describeSchema() {
|
|
@@ -488,20 +481,21 @@ export class PmxCanvas extends EventEmitter {
|
|
|
488
481
|
y?: number;
|
|
489
482
|
width?: number;
|
|
490
483
|
height?: number;
|
|
491
|
-
}): Promise<{ ok: true; nodeId: string | null; toolCallId: string; sessionId: string; resourceUri: string }> {
|
|
484
|
+
}): Promise<{ ok: true; id?: string; nodeId: string | null; toolCallId: string; sessionId: string; resourceUri: string }> {
|
|
492
485
|
const opened = await openExternalMcpApp({
|
|
493
486
|
transport: input.transport,
|
|
494
487
|
toolName: input.toolName,
|
|
495
488
|
...(input.toolArguments ? { toolArguments: input.toolArguments } : {}),
|
|
496
489
|
...(input.serverName ? { serverName: input.serverName } : {}),
|
|
497
490
|
});
|
|
498
|
-
const toolCallId =
|
|
491
|
+
const toolCallId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
499
492
|
const nodeIdSeed = `ext-app-${toolCallId}`;
|
|
500
493
|
const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
|
|
501
494
|
? ensureExcalidrawCheckpointId(opened.toolResult, nodeIdSeed)
|
|
502
495
|
: opened.toolResult;
|
|
503
496
|
emitPrimaryWorkbenchEvent('ext-app-open', {
|
|
504
497
|
toolCallId,
|
|
498
|
+
nodeId: nodeIdSeed,
|
|
505
499
|
title: input.title ?? opened.tool.title ?? opened.tool.name,
|
|
506
500
|
html: opened.html,
|
|
507
501
|
toolInput: opened.toolInput,
|
|
@@ -521,6 +515,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
521
515
|
});
|
|
522
516
|
emitPrimaryWorkbenchEvent('ext-app-result', {
|
|
523
517
|
toolCallId,
|
|
518
|
+
nodeId: nodeIdSeed,
|
|
524
519
|
serverName: opened.serverName,
|
|
525
520
|
toolName: opened.toolName,
|
|
526
521
|
success: toolResult.isError !== true,
|
|
@@ -529,6 +524,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
529
524
|
const nodeId = this.findCanvasExtAppNodeId(toolCallId);
|
|
530
525
|
return {
|
|
531
526
|
ok: true,
|
|
527
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
532
528
|
nodeId,
|
|
533
529
|
toolCallId,
|
|
534
530
|
sessionId: opened.sessionId,
|
|
@@ -538,7 +534,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
538
534
|
|
|
539
535
|
async addDiagram(
|
|
540
536
|
input: DiagramPresetOpenInput,
|
|
541
|
-
): Promise<{ ok: true; nodeId: string | null; toolCallId: string; sessionId: string; resourceUri: string }> {
|
|
537
|
+
): Promise<{ ok: true; id?: string; nodeId: string | null; toolCallId: string; sessionId: string; resourceUri: string }> {
|
|
542
538
|
const built = buildExcalidrawOpenMcpAppInput(input);
|
|
543
539
|
return this.openMcpApp(built);
|
|
544
540
|
}
|
package/src/server/server.ts
CHANGED
|
@@ -46,6 +46,7 @@ import type {
|
|
|
46
46
|
ListToolsResult,
|
|
47
47
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
48
48
|
import { type CanvasEdge, type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
|
|
49
|
+
import { findCanvasExtAppNodeId as findCanvasExtAppNodeIdShared } from './ext-app-lookup.js';
|
|
49
50
|
import { normalizeExtAppToolResult } from './ext-app-tool-result.js';
|
|
50
51
|
import { getMcpAppHostSnapshot } from './mcp-app-host.js';
|
|
51
52
|
import {
|
|
@@ -602,31 +603,42 @@ function getMarkdownPlacement(): { x: number; y: number } {
|
|
|
602
603
|
}
|
|
603
604
|
|
|
604
605
|
function findCanvasExtAppNodeId(toolCallId: string): string | null {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
node.type === 'mcp-app' &&
|
|
610
|
-
node.data.mode === 'ext-app' &&
|
|
611
|
-
node.data.toolCallId === toolCallId
|
|
612
|
-
) {
|
|
613
|
-
return node.id;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
return null;
|
|
606
|
+
return findCanvasExtAppNodeIdShared(toolCallId, {
|
|
607
|
+
getNode: (id) => canvasState.getNode(id),
|
|
608
|
+
listNodes: () => canvasState.getLayout().nodes,
|
|
609
|
+
});
|
|
617
610
|
}
|
|
618
611
|
|
|
619
612
|
function isCheckpointToolName(toolName: string): boolean {
|
|
620
613
|
return toolName === EXCALIDRAW_SAVE_CHECKPOINT_TOOL || toolName === EXCALIDRAW_READ_CHECKPOINT_TOOL;
|
|
621
614
|
}
|
|
622
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Decide whether a fresh `callServerTool` result should *replace* the
|
|
618
|
+
* canvas node's bootstrap-replay `toolResult`.
|
|
619
|
+
*
|
|
620
|
+
* The bootstrap-replay toolResult is what the server re-sends to the
|
|
621
|
+
* widget on reload to restore visible state. We only want to overwrite
|
|
622
|
+
* it when the new result genuinely carries widget state — `isError` or
|
|
623
|
+
* `structuredContent`. A plain-text result (e.g. `read_checkpoint`
|
|
624
|
+
* returning a string status, or any informational message) updates
|
|
625
|
+
* `appModelContext` for the agent's record but should *not* clobber the
|
|
626
|
+
* bootstrap entry, because doing so would replace the widget's restored
|
|
627
|
+
* state with conversational noise on the next reload.
|
|
628
|
+
*
|
|
629
|
+
* This separation is exercised by:
|
|
630
|
+
* - tests/unit/server-api.test.ts "keeps ext-app model context
|
|
631
|
+
* separate from the replayed tool result" (text-only result preserves
|
|
632
|
+
* bootstrap replay)
|
|
633
|
+
* - tests/unit/server-api.test.ts "app-only text tool results update
|
|
634
|
+
* model context without replacing bootstrap replay"
|
|
635
|
+
* - tests/unit/server-api.test.ts "rehydrates Excalidraw checkpoint
|
|
636
|
+
* replay after server restart" (structured-content result becomes
|
|
637
|
+
* the new bootstrap replay)
|
|
638
|
+
*/
|
|
623
639
|
function shouldReplayAppToolResult(toolName: string, result: CallToolResult): boolean {
|
|
624
640
|
void toolName;
|
|
625
|
-
return Boolean(
|
|
626
|
-
result.isError ||
|
|
627
|
-
result.structuredContent ||
|
|
628
|
-
result.content.some((entry) => entry.type !== 'text' || entry.text !== 'ok'),
|
|
629
|
-
);
|
|
641
|
+
return Boolean(result.isError || result.structuredContent);
|
|
630
642
|
}
|
|
631
643
|
|
|
632
644
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -1545,10 +1557,22 @@ async function handleCanvasValidateSpec(req: Request): Promise<Response> {
|
|
|
1545
1557
|
data,
|
|
1546
1558
|
...(typeof body.xKey === 'string' ? { xKey: body.xKey } : {}),
|
|
1547
1559
|
...(typeof body.yKey === 'string' ? { yKey: body.yKey } : {}),
|
|
1560
|
+
...(typeof body.zKey === 'string' ? { zKey: body.zKey } : {}),
|
|
1548
1561
|
...(typeof body.nameKey === 'string' ? { nameKey: body.nameKey } : {}),
|
|
1549
1562
|
...(typeof body.valueKey === 'string' ? { valueKey: body.valueKey } : {}),
|
|
1563
|
+
...(typeof body.axisKey === 'string' ? { axisKey: body.axisKey } : {}),
|
|
1564
|
+
...(Array.isArray(body.metrics)
|
|
1565
|
+
? { metrics: body.metrics.filter((m: unknown): m is string => typeof m === 'string') }
|
|
1566
|
+
: {}),
|
|
1567
|
+
...(Array.isArray(body.series)
|
|
1568
|
+
? { series: body.series.filter((s: unknown): s is string => typeof s === 'string') }
|
|
1569
|
+
: {}),
|
|
1570
|
+
...(typeof body.barKey === 'string' ? { barKey: body.barKey } : {}),
|
|
1571
|
+
...(typeof body.lineKey === 'string' ? { lineKey: body.lineKey } : {}),
|
|
1550
1572
|
...(aggregate ? { aggregate } : {}),
|
|
1551
1573
|
...(typeof body.color === 'string' ? { color: body.color } : {}),
|
|
1574
|
+
...(typeof body.barColor === 'string' ? { barColor: body.barColor } : {}),
|
|
1575
|
+
...(typeof body.lineColor === 'string' ? { lineColor: body.lineColor } : {}),
|
|
1552
1576
|
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
1553
1577
|
},
|
|
1554
1578
|
}));
|
|
@@ -1871,7 +1895,7 @@ function handleRead(pathLike: string): Response {
|
|
|
1871
1895
|
}
|
|
1872
1896
|
|
|
1873
1897
|
function randomExtAppToolCallId(): string {
|
|
1874
|
-
return
|
|
1898
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1875
1899
|
}
|
|
1876
1900
|
|
|
1877
1901
|
function nodeAppSessionId(node: CanvasNodeState | undefined): string | null {
|
|
@@ -1951,7 +1975,7 @@ async function runAndEmitOpenMcpApp(params: RunAndEmitOpenMcpAppParams): Promise
|
|
|
1951
1975
|
});
|
|
1952
1976
|
|
|
1953
1977
|
const toolCallId = randomExtAppToolCallId();
|
|
1954
|
-
const nodeIdSeed = `ext-app-${toolCallId}`;
|
|
1978
|
+
const nodeIdSeed = toolCallId.startsWith('ext-app-') ? toolCallId : `ext-app-${toolCallId}`;
|
|
1955
1979
|
const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
|
|
1956
1980
|
? ensureExcalidrawCheckpointId(opened.toolResult, nodeIdSeed)
|
|
1957
1981
|
: opened.toolResult;
|
|
@@ -1959,6 +1983,7 @@ async function runAndEmitOpenMcpApp(params: RunAndEmitOpenMcpAppParams): Promise
|
|
|
1959
1983
|
|
|
1960
1984
|
emitPrimaryWorkbenchEvent('ext-app-open', {
|
|
1961
1985
|
toolCallId,
|
|
1986
|
+
nodeId: nodeIdSeed,
|
|
1962
1987
|
title: nodeTitle,
|
|
1963
1988
|
html: opened.html,
|
|
1964
1989
|
toolInput: opened.toolInput,
|
|
@@ -1978,6 +2003,7 @@ async function runAndEmitOpenMcpApp(params: RunAndEmitOpenMcpAppParams): Promise
|
|
|
1978
2003
|
});
|
|
1979
2004
|
emitPrimaryWorkbenchEvent('ext-app-result', {
|
|
1980
2005
|
toolCallId,
|
|
2006
|
+
nodeId: nodeIdSeed,
|
|
1981
2007
|
serverName: opened.serverName,
|
|
1982
2008
|
toolName: opened.toolName,
|
|
1983
2009
|
success: toolResult.isError !== true,
|
|
@@ -1987,6 +2013,7 @@ async function runAndEmitOpenMcpApp(params: RunAndEmitOpenMcpAppParams): Promise
|
|
|
1987
2013
|
|
|
1988
2014
|
return responseJson({
|
|
1989
2015
|
ok: true,
|
|
2016
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
1990
2017
|
nodeId,
|
|
1991
2018
|
toolCallId,
|
|
1992
2019
|
sessionId: opened.sessionId,
|
|
@@ -3151,10 +3178,13 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3151
3178
|
} else if (event === 'ext-app-open') {
|
|
3152
3179
|
const toolCallId = payload.toolCallId as string;
|
|
3153
3180
|
if (!toolCallId) return;
|
|
3154
|
-
const id =
|
|
3181
|
+
const id = typeof payload.nodeId === 'string' && payload.nodeId.length > 0
|
|
3182
|
+
? payload.nodeId
|
|
3183
|
+
: toolCallId.startsWith('ext-app-') ? toolCallId : `ext-app-${toolCallId}`;
|
|
3155
3184
|
const dataPatch = {
|
|
3156
3185
|
mode: 'ext-app',
|
|
3157
3186
|
toolCallId,
|
|
3187
|
+
nodeId: id,
|
|
3158
3188
|
...(typeof payload.title === 'string' && payload.title.trim().length > 0
|
|
3159
3189
|
? { title: payload.title.trim() }
|
|
3160
3190
|
: {}),
|
|
@@ -3222,7 +3252,9 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3222
3252
|
} else if (event === 'ext-app-update') {
|
|
3223
3253
|
const toolCallId = payload.toolCallId as string;
|
|
3224
3254
|
if (!toolCallId) return;
|
|
3255
|
+
const payloadNodeId = typeof payload.nodeId === 'string' ? payload.nodeId : '';
|
|
3225
3256
|
const id =
|
|
3257
|
+
(payloadNodeId && canvasState.getNode(payloadNodeId) ? payloadNodeId : null) ||
|
|
3226
3258
|
findCanvasExtAppNodeId(toolCallId) ||
|
|
3227
3259
|
(typeof payload.serverName === 'string' && typeof payload.toolName === 'string'
|
|
3228
3260
|
? findOnlyPendingCanvasExtAppNodeId(payload.serverName, payload.toolName)
|
|
@@ -3235,7 +3267,9 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3235
3267
|
} else if (event === 'ext-app-result') {
|
|
3236
3268
|
const toolCallId = payload.toolCallId as string;
|
|
3237
3269
|
if (!toolCallId) return;
|
|
3270
|
+
const payloadNodeId = typeof payload.nodeId === 'string' ? payload.nodeId : '';
|
|
3238
3271
|
const id =
|
|
3272
|
+
(payloadNodeId && canvasState.getNode(payloadNodeId) ? payloadNodeId : null) ||
|
|
3239
3273
|
findCanvasExtAppNodeId(toolCallId) ||
|
|
3240
3274
|
(typeof payload.serverName === 'string' && typeof payload.toolName === 'string'
|
|
3241
3275
|
? findOnlyPendingCanvasExtAppNodeId(payload.serverName, payload.toolName)
|