pmx-canvas 0.4.5 → 0.4.6
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 +56 -0
- package/Readme.md +21 -4
- package/dist/canvas/global.css +68 -0
- package/dist/canvas/index.js +91 -70
- package/dist/canvas/mermaid-entry.js +3563 -0
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/smoke.d.ts +1 -0
- package/dist/types/client/mermaid-entry.d.ts +11 -0
- package/dist/types/client/nodes/DiffNode.d.ts +20 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +0 -2
- package/dist/types/client/nodes/MermaidNode.d.ts +12 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +10 -1
- package/dist/types/server/canvas-validation.d.ts +34 -1
- package/dist/types/server/html-surface.d.ts +8 -0
- package/dist/types/server/index.d.ts +5 -0
- package/dist/types/server/operations/ops/nodes.d.ts +1 -1
- package/dist/types/server/workboard.d.ts +46 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +7 -0
- package/docs/cli.md +12 -0
- package/docs/http-api.md +12 -0
- package/docs/mcp.md +1 -1
- package/docs/node-types.md +2 -0
- package/docs/screenshot.png +0 -0
- package/package.json +4 -3
- package/skills/pmx-canvas/SKILL.md +56 -19
- package/skills/pmx-canvas-testing/SKILL.md +3 -0
- package/src/cli/agent.ts +2 -0
- package/src/cli/commands/ax.ts +28 -3
- package/src/cli/commands/smoke.ts +212 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/shared.ts +1 -0
- package/src/client/canvas/CanvasViewport.tsx +6 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +10 -0
- package/src/client/canvas/Minimap.tsx +2 -0
- package/src/client/canvas/auto-fit.ts +2 -0
- package/src/client/icons.tsx +4 -0
- package/src/client/mermaid-entry.ts +76 -0
- package/src/client/nodes/DiffNode.tsx +114 -0
- package/src/client/nodes/ExtAppFrame.tsx +259 -84
- package/src/client/nodes/MermaidNode.tsx +81 -0
- package/src/client/state/node-factory.ts +2 -0
- package/src/client/state/sse-bridge.ts +28 -0
- package/src/client/theme/global.css +68 -0
- package/src/client/types.ts +6 -0
- package/src/mcp/server.ts +19 -1
- package/src/server/ax-interaction.ts +6 -0
- package/src/server/canvas-operations.ts +39 -9
- package/src/server/canvas-provenance.ts +2 -0
- package/src/server/canvas-schema.ts +33 -0
- package/src/server/canvas-state.ts +26 -0
- package/src/server/canvas-validation.ts +79 -0
- package/src/server/html-surface.ts +11 -0
- package/src/server/index.ts +25 -6
- package/src/server/operations/composites.ts +3 -2
- package/src/server/operations/ops/batch.ts +8 -0
- package/src/server/operations/ops/json-render.ts +44 -0
- package/src/server/operations/ops/nodes.ts +9 -1
- package/src/server/operations/ops/viewport.ts +8 -1
- package/src/server/server.ts +48 -1
- package/src/server/workboard.ts +159 -0
- package/src/shared/ax-surface-protocol.ts +8 -0
- package/src/shared/surface.ts +2 -0
|
@@ -55,6 +55,8 @@ export const NODE_TYPES = [
|
|
|
55
55
|
'ledger',
|
|
56
56
|
'trace',
|
|
57
57
|
'file',
|
|
58
|
+
'diff',
|
|
59
|
+
'mermaid',
|
|
58
60
|
'image',
|
|
59
61
|
'mcp-app',
|
|
60
62
|
'webpage',
|
|
@@ -74,6 +76,10 @@ export function defaultNodeSize(type: string): { width: number; height: number }
|
|
|
74
76
|
// a useful snippet — the generic default rendered as a content-less bar
|
|
75
77
|
// (0.4.4 portal feedback).
|
|
76
78
|
return { width: 520, height: 360 };
|
|
79
|
+
case 'diff':
|
|
80
|
+
return { width: 640, height: 420 };
|
|
81
|
+
case 'mermaid':
|
|
82
|
+
return { width: 640, height: 460 };
|
|
77
83
|
case 'markdown':
|
|
78
84
|
return MARKDOWN_NODE_DEFAULT_SIZE;
|
|
79
85
|
case 'mcp-app':
|
|
@@ -783,7 +789,7 @@ const nodeAddOperation = defineOperation<z.infer<typeof nodeAddSchema>, NodeAddR
|
|
|
783
789
|
mcp: {
|
|
784
790
|
toolName: 'canvas_add_node',
|
|
785
791
|
description:
|
|
786
|
-
'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, status, context, ledger, trace, file, image, webpage, mcp-app, html, group. Dedicated routes: json-render -> canvas_render {action:"add-json-render"}, graph -> canvas_render {action:"add-graph"}, web-artifact -> canvas_app {action:"build-artifact"}, external apps -> canvas_app {action:"open-mcp-app"}, groups -> canvas_group {action:"create"}. Call canvas_render {action:"describe-schema"} for the full nodeTypeRouting table.',
|
|
792
|
+
'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, status, context, ledger, trace, file, diff, mermaid, image, webpage, mcp-app, html, group. Dedicated routes: json-render -> canvas_render {action:"add-json-render"}, graph -> canvas_render {action:"add-graph"}, web-artifact -> canvas_app {action:"build-artifact"}, external apps -> canvas_app {action:"open-mcp-app"}, groups -> canvas_group {action:"create"}. Call canvas_render {action:"describe-schema"} for the full nodeTypeRouting table.',
|
|
787
793
|
extraShape: {
|
|
788
794
|
type: z
|
|
789
795
|
.enum([
|
|
@@ -793,6 +799,8 @@ const nodeAddOperation = defineOperation<z.infer<typeof nodeAddSchema>, NodeAddR
|
|
|
793
799
|
'ledger',
|
|
794
800
|
'trace',
|
|
795
801
|
'file',
|
|
802
|
+
'diff',
|
|
803
|
+
'mermaid',
|
|
796
804
|
'image',
|
|
797
805
|
'webpage',
|
|
798
806
|
'mcp-app',
|
|
@@ -121,7 +121,14 @@ const focusOperation = defineOperation<z.infer<typeof focusSchema>, Record<strin
|
|
|
121
121
|
if (!node) throw new OperationError(`Node "${nodeId}" not found.`, 404);
|
|
122
122
|
const noPan = body.noPan === true;
|
|
123
123
|
if (!noPan) {
|
|
124
|
-
|
|
124
|
+
// The viewport is a SCREEN-space translate: screen = world * scale + v
|
|
125
|
+
// (CanvasViewport's matrix(scale,0,0,scale,x,y)), the same convention
|
|
126
|
+
// fitCanvasView and the client's focusNode use. So park the node's
|
|
127
|
+
// top-left at a fixed screen-space margin — 64/96 screen px, which
|
|
128
|
+
// clears the 44px floating toolbar at ANY zoom (0.4.5 report Finding Z:
|
|
129
|
+
// the old world-space margin shrank below the toolbar at low scales).
|
|
130
|
+
const scale = canvasState.viewport.scale || 1;
|
|
131
|
+
canvasState.setViewport({ x: 64 - node.position.x * scale, y: 96 - node.position.y * scale });
|
|
125
132
|
} else {
|
|
126
133
|
const maxZ = canvasState.getLayout().nodes.reduce((max, layoutNode) => Math.max(max, layoutNode.zIndex), 0);
|
|
127
134
|
canvasState.updateNode(nodeId, { zIndex: maxZ + 1 });
|
package/src/server/server.ts
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
buildAxStateBridge,
|
|
48
48
|
buildContentHeightReporter,
|
|
49
49
|
buildHtmlSurfaceDocument,
|
|
50
|
+
buildMermaidSurfaceHtml,
|
|
50
51
|
HTML_SURFACE_SANDBOX,
|
|
51
52
|
normalizeSurfaceTheme,
|
|
52
53
|
} from './html-surface.js';
|
|
@@ -1004,6 +1005,7 @@ function canvasSpaHtml(): string {
|
|
|
1004
1005
|
// (see src/client/state/iframe-mode.ts).
|
|
1005
1006
|
process.env.AMP_ORB ? '<script>window.__PMX_AMP_ORB = true;</script>' : ''
|
|
1006
1007
|
}
|
|
1008
|
+
<script>window.__PMX_BOOT_SERVER_VERSION = ${JSON.stringify(serverPackageVersion())};</script>
|
|
1007
1009
|
<script type="module" src="/canvas/index.js?v=${CANVAS_ASSET_VERSION}"></script>
|
|
1008
1010
|
</body>
|
|
1009
1011
|
</html>`;
|
|
@@ -1271,6 +1273,28 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1271
1273
|
|
|
1272
1274
|
const theme = normalizeSurfaceTheme(url.searchParams.get('theme'));
|
|
1273
1275
|
|
|
1276
|
+
if (node.type === 'mermaid') {
|
|
1277
|
+
const source = typeof node.data.content === 'string' ? node.data.content : '';
|
|
1278
|
+
if (!source.trim()) return responseText('Mermaid node has no content', 404);
|
|
1279
|
+
const surfaceTitle = typeof node.data.title === 'string' && node.data.title.trim() ? node.data.title : node.id;
|
|
1280
|
+
// Display-only surface: same theme/height plumbing as html nodes, but no AX
|
|
1281
|
+
// bridge and no presentation mode — the document is our own wrapper around
|
|
1282
|
+
// escaped diagram source, rendered by /canvas/mermaid-entry.js.
|
|
1283
|
+
const doc = buildHtmlSurfaceDocument(buildMermaidSurfaceHtml(source), {
|
|
1284
|
+
theme,
|
|
1285
|
+
title: surfaceTitle,
|
|
1286
|
+
themeToken: url.searchParams.get('themeToken') ?? undefined,
|
|
1287
|
+
// Content-height reporter nonce (lets the node grow to fit the diagram).
|
|
1288
|
+
...(url.searchParams.get('frameToken')
|
|
1289
|
+
? { contentHeightToken: url.searchParams.get('frameToken') as string }
|
|
1290
|
+
: {}),
|
|
1291
|
+
// Inline the theme tokens so srcdoc-rendered surfaces (Amp orb portals)
|
|
1292
|
+
// are styled without depending on the <link> subresource.
|
|
1293
|
+
...(surfaceThemeCssInline() ? { inlineThemeCss: surfaceThemeCssInline() as string } : {}),
|
|
1294
|
+
});
|
|
1295
|
+
return surfaceHtmlResponse(doc, HTML_SURFACE_SANDBOX);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1274
1298
|
if (node.type === 'html') {
|
|
1275
1299
|
const html =
|
|
1276
1300
|
typeof node.data.html === 'string'
|
|
@@ -1449,6 +1473,23 @@ function responseJson(data: unknown, status = 200): Response {
|
|
|
1449
1473
|
});
|
|
1450
1474
|
}
|
|
1451
1475
|
|
|
1476
|
+
// Resolved from the sibling package.json so it stays accurate through bunx,
|
|
1477
|
+
// global npm installs, and repo-local runs — same mechanism as --version in
|
|
1478
|
+
// src/cli/index.ts and the MCP server. Memoized: /health is polled.
|
|
1479
|
+
let cachedPackageVersion: string | null = null;
|
|
1480
|
+
function serverPackageVersion(): string {
|
|
1481
|
+
if (cachedPackageVersion !== null) return cachedPackageVersion;
|
|
1482
|
+
try {
|
|
1483
|
+
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf-8')) as {
|
|
1484
|
+
version?: string;
|
|
1485
|
+
};
|
|
1486
|
+
cachedPackageVersion = pkg.version ?? 'unknown';
|
|
1487
|
+
} catch {
|
|
1488
|
+
cachedPackageVersion = 'unknown';
|
|
1489
|
+
}
|
|
1490
|
+
return cachedPackageVersion;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1452
1493
|
// v0.4.0 (M4): every hand-written error response is a JSON envelope
|
|
1453
1494
|
// ({ ok: false, error }, same status codes) matching the registry dispatcher —
|
|
1454
1495
|
// consumers no longer need to branch on text/plain vs JSON error bodies.
|
|
@@ -1799,6 +1840,9 @@ function buildWorkbenchConnectSnapshot(
|
|
|
1799
1840
|
continuity,
|
|
1800
1841
|
path: primaryWorkbenchPath,
|
|
1801
1842
|
theme: canvasState.theme,
|
|
1843
|
+
// Lets a long-lived client detect that the server was upgraded under it
|
|
1844
|
+
// (stale in-memory SPA — 0.4.5 report Finding W) and reload once.
|
|
1845
|
+
version: serverPackageVersion(),
|
|
1802
1846
|
timestamp: new Date().toISOString(),
|
|
1803
1847
|
},
|
|
1804
1848
|
});
|
|
@@ -3038,11 +3082,14 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3038
3082
|
// when the pid file is stale (e.g. an adapter respawned the server
|
|
3039
3083
|
// outside `serve --daemon` — 0.3.2 report Finding P). `persistence`
|
|
3040
3084
|
// surfaces silent save failures (M4): the canvas keeps serving from
|
|
3041
|
-
// memory, but ok:false means durability is degraded.
|
|
3085
|
+
// memory, but ok:false means durability is degraded. `version` lets
|
|
3086
|
+
// `pmx-canvas smoke` verify the RUNNING server, not just the CLI —
|
|
3087
|
+
// version skew between the two is a known failure mode.
|
|
3042
3088
|
return responseJson({
|
|
3043
3089
|
ok: true,
|
|
3044
3090
|
workspace: activeWorkspaceRoot,
|
|
3045
3091
|
pid: process.pid,
|
|
3092
|
+
version: serverPackageVersion(),
|
|
3046
3093
|
persistence: canvasState.persistenceHealth,
|
|
3047
3094
|
});
|
|
3048
3095
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live AX work-item board, materialized as a json-render node.
|
|
3
|
+
*
|
|
4
|
+
* `buildWorkboardSpec` is the pure spec builder (status columns of work-item
|
|
5
|
+
* cards); `renderWorkboard` is the operation core (create-or-rebuild the
|
|
6
|
+
* single `data.workboard === true` json-render node); `refreshWorkboardNodes`
|
|
7
|
+
* is the live-refresh hook this module registers on `canvasState` so every
|
|
8
|
+
* addWorkItem/updateWorkItem rebuilds the board from the fresh work-item list
|
|
9
|
+
* through the SAME spec/update path the operation uses.
|
|
10
|
+
*
|
|
11
|
+
* The refresh runs inside `withSuppressedRecording` so live board rebuilds do
|
|
12
|
+
* not spam undo/redo history (persistence and SSE emits are unaffected).
|
|
13
|
+
*/
|
|
14
|
+
import type { JsonRenderSpec } from '../json-render/server.js';
|
|
15
|
+
import type { PmxAxWorkItem, PmxAxWorkItemStatus } from './ax-state.js';
|
|
16
|
+
import { buildJsonRenderNodeUpdate, createCanvasJsonRenderNode, emitCanvasLayoutUpdate } from './canvas-operations.js';
|
|
17
|
+
import { canvasState, type CanvasNodeState } from './canvas-state.js';
|
|
18
|
+
|
|
19
|
+
export const WORKBOARD_NODE_TITLE = 'Work Board';
|
|
20
|
+
export const WORKBOARD_NODE_SIZE = { width: 960, height: 560 };
|
|
21
|
+
|
|
22
|
+
const WORKBOARD_STATUS_ORDER: PmxAxWorkItemStatus[] = ['todo', 'in-progress', 'blocked', 'done', 'cancelled'];
|
|
23
|
+
|
|
24
|
+
const WORKBOARD_STATUS_LABELS: Record<PmxAxWorkItemStatus, string> = {
|
|
25
|
+
todo: 'To Do',
|
|
26
|
+
'in-progress': 'In Progress',
|
|
27
|
+
blocked: 'Blocked',
|
|
28
|
+
done: 'Done',
|
|
29
|
+
cancelled: 'Cancelled',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build the workboard json-render spec from a work-item list: one column per
|
|
34
|
+
* status (todo → in-progress → blocked → done → cancelled; empty statuses are
|
|
35
|
+
* omitted), each item a Card with an agentId Badge and detail Text when
|
|
36
|
+
* present. An empty list renders a single muted "No work items" text block.
|
|
37
|
+
*/
|
|
38
|
+
export function buildWorkboardSpec(workItems: PmxAxWorkItem[]): JsonRenderSpec {
|
|
39
|
+
if (workItems.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
root: 'empty',
|
|
42
|
+
elements: {
|
|
43
|
+
empty: { type: 'Text', props: { text: 'No work items', variant: 'muted' }, children: [] },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const elements: Record<string, unknown> = {};
|
|
49
|
+
const columnIds: string[] = [];
|
|
50
|
+
for (const status of WORKBOARD_STATUS_ORDER) {
|
|
51
|
+
const items = workItems.filter((item) => item.status === status);
|
|
52
|
+
if (items.length === 0) continue;
|
|
53
|
+
const columnId = `col-${status}`;
|
|
54
|
+
const headerId = `${columnId}-header`;
|
|
55
|
+
elements[headerId] = {
|
|
56
|
+
type: 'Heading',
|
|
57
|
+
props: { level: 'h4', text: `${WORKBOARD_STATUS_LABELS[status]} (${items.length})` },
|
|
58
|
+
children: [],
|
|
59
|
+
};
|
|
60
|
+
const columnChildren = [headerId];
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
const cardId = `item-${item.id}`;
|
|
63
|
+
const cardChildren: string[] = [];
|
|
64
|
+
if (item.agentId) {
|
|
65
|
+
elements[`${cardId}-agent`] = {
|
|
66
|
+
type: 'Badge',
|
|
67
|
+
props: { text: item.agentId, variant: 'secondary' },
|
|
68
|
+
children: [],
|
|
69
|
+
};
|
|
70
|
+
cardChildren.push(`${cardId}-agent`);
|
|
71
|
+
}
|
|
72
|
+
if (item.detail) {
|
|
73
|
+
elements[`${cardId}-detail`] = {
|
|
74
|
+
type: 'Text',
|
|
75
|
+
props: { text: item.detail, variant: 'muted' },
|
|
76
|
+
children: [],
|
|
77
|
+
};
|
|
78
|
+
cardChildren.push(`${cardId}-detail`);
|
|
79
|
+
}
|
|
80
|
+
elements[cardId] = { type: 'Card', props: { title: item.title }, children: cardChildren };
|
|
81
|
+
columnChildren.push(cardId);
|
|
82
|
+
}
|
|
83
|
+
elements[columnId] = {
|
|
84
|
+
type: 'Stack',
|
|
85
|
+
props: { direction: 'vertical', gap: 'sm' },
|
|
86
|
+
children: columnChildren,
|
|
87
|
+
};
|
|
88
|
+
columnIds.push(columnId);
|
|
89
|
+
}
|
|
90
|
+
elements.board = {
|
|
91
|
+
type: 'Stack',
|
|
92
|
+
props: { direction: 'horizontal', gap: 'md', align: 'start' },
|
|
93
|
+
children: columnIds,
|
|
94
|
+
};
|
|
95
|
+
return { root: 'board', elements };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findWorkboardNodes(): CanvasNodeState[] {
|
|
99
|
+
return canvasState.getLayout().nodes.filter((node) => node.type === 'json-render' && node.data.workboard === true);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Rebuild one workboard node's spec in place (shared by the op and the live refresh). */
|
|
103
|
+
function applyWorkboardSpec(node: CanvasNodeState, spec: JsonRenderSpec): void {
|
|
104
|
+
const title = typeof node.data.title === 'string' && node.data.title ? node.data.title : WORKBOARD_NODE_TITLE;
|
|
105
|
+
const update = buildJsonRenderNodeUpdate(node, { title, spec });
|
|
106
|
+
canvasState.updateNode(node.id, { data: update.data });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Operation core for `render.workboard`: rebuild the existing workboard node
|
|
111
|
+
* in place, or create one (tagged `data.workboard: true`) when none exists.
|
|
112
|
+
*/
|
|
113
|
+
export function renderWorkboard(input: { x?: number; y?: number } = {}): {
|
|
114
|
+
ok: true;
|
|
115
|
+
id: string;
|
|
116
|
+
created: boolean;
|
|
117
|
+
itemCount: number;
|
|
118
|
+
} {
|
|
119
|
+
const workItems = canvasState.getWorkItems();
|
|
120
|
+
const spec = buildWorkboardSpec(workItems);
|
|
121
|
+
const existing = findWorkboardNodes()[0];
|
|
122
|
+
if (existing) {
|
|
123
|
+
applyWorkboardSpec(existing, spec);
|
|
124
|
+
return { ok: true, id: existing.id, created: false, itemCount: workItems.length };
|
|
125
|
+
}
|
|
126
|
+
const created = createCanvasJsonRenderNode({
|
|
127
|
+
title: WORKBOARD_NODE_TITLE,
|
|
128
|
+
spec,
|
|
129
|
+
width: WORKBOARD_NODE_SIZE.width,
|
|
130
|
+
height: WORKBOARD_NODE_SIZE.height,
|
|
131
|
+
...(input.x !== undefined ? { x: input.x } : {}),
|
|
132
|
+
...(input.y !== undefined ? { y: input.y } : {}),
|
|
133
|
+
});
|
|
134
|
+
// Tag the node as THE workboard. Suppressed: the tag is part of the single
|
|
135
|
+
// logical "create workboard" mutation, not a second undo step.
|
|
136
|
+
canvasState.withSuppressedRecording(() => {
|
|
137
|
+
canvasState.updateNode(created.id, { data: { ...created.node.data, workboard: true } });
|
|
138
|
+
});
|
|
139
|
+
return { ok: true, id: created.id, created: true, itemCount: workItems.length };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Live refresh: rebuild every workboard node's spec from the current work-item
|
|
144
|
+
* list. Registered below as the canvasState work-item change listener. Node
|
|
145
|
+
* updates never touch work items, so this cannot recurse.
|
|
146
|
+
*/
|
|
147
|
+
export function refreshWorkboardNodes(): void {
|
|
148
|
+
const nodes = findWorkboardNodes();
|
|
149
|
+
if (nodes.length === 0) return;
|
|
150
|
+
const spec = buildWorkboardSpec(canvasState.getWorkItems());
|
|
151
|
+
canvasState.withSuppressedRecording(() => {
|
|
152
|
+
for (const node of nodes) {
|
|
153
|
+
applyWorkboardSpec(node, spec);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
emitCanvasLayoutUpdate();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
canvasState.setWorkItemsChangedListener(refreshWorkboardNodes);
|
|
@@ -20,3 +20,11 @@ export const HTML_SURFACE_PUSH_SOURCE = 'pmx-canvas-html-node';
|
|
|
20
20
|
|
|
21
21
|
/** Ext-app boot beacon: the iframe's scripts executed (WebKit watchdog liveness). */
|
|
22
22
|
export const EXT_APP_BOOT_BEACON_SOURCE = 'pmx-canvas-ext-app-alive';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parent→ext-app paint probe (Finding N): the parent asks the app document to
|
|
26
|
+
* answer with a double-rAF `paint-tick` beacon. A frame whose rendering
|
|
27
|
+
* pipeline is live answers within a frame or two; silence past the timeout is
|
|
28
|
+
* the paint-fail signal that drives the recovery ladder.
|
|
29
|
+
*/
|
|
30
|
+
export const EXT_APP_PAINT_PROBE_SOURCE = 'pmx-canvas-ext-app-paint-probe';
|
package/src/shared/surface.ts
CHANGED
|
@@ -17,6 +17,8 @@ export function canOpenNodeAsSurface(type: string, data: Record<string, unknown>
|
|
|
17
17
|
(typeof data.html === 'string' && data.html.length > 0) ||
|
|
18
18
|
(typeof data.content === 'string' && data.content.length > 0)
|
|
19
19
|
);
|
|
20
|
+
case 'mermaid':
|
|
21
|
+
return typeof data.content === 'string' && data.content.trim().length > 0;
|
|
20
22
|
case 'json-render':
|
|
21
23
|
case 'graph':
|
|
22
24
|
return true;
|