pmx-canvas 0.9.3 → 0.11.0
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/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/tools.json +9418 -2016
- package/CHANGELOG.md +54 -1
- package/dist/canvas/chunks/abnfDiagram-N423BO3Z-p8dtp6va.js +1 -0
- package/dist/canvas/chunks/architecture-TIHT7OUA-9tffvpsv.js +1 -0
- package/dist/canvas/chunks/architectureDiagram-T3A2C74G-5jhmxrzf.js +36 -0
- package/dist/canvas/chunks/blockDiagram-VBNYF7ZC-pw1fa9yb.js +132 -0
- package/dist/canvas/chunks/c4Diagram-5PPSVZJV-x07nq0tt.js +10 -0
- package/dist/canvas/chunks/classDiagram-JCYQIIEL-nc6g5x1r.js +1 -0
- package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-0qnbpwvh.js +1 -0
- package/dist/canvas/chunks/cose-bilkent-JH36ORCC-nm39npq0.js +1 -0
- package/dist/canvas/chunks/cynefin-VYW2F7L2-ys8p46ts.js +1 -0
- package/dist/canvas/chunks/cynefinDiagram-MW4NZA55-vdxrwahz.js +62 -0
- package/dist/canvas/chunks/dagre-VZM6K2ZE-bqdm7wpd.js +4 -0
- package/dist/canvas/chunks/diagram-7IWD3JNH-gs5gpr93.js +30 -0
- package/dist/canvas/chunks/diagram-B4RE2ZJO-cgwjg47s.js +3 -0
- package/dist/canvas/chunks/diagram-LBJQPF4R-w3vahpav.js +24 -0
- package/dist/canvas/chunks/diagram-Q27KOJAE-42837z6h.js +24 -0
- package/dist/canvas/chunks/diagram-UB23O5K3-4fz83q59.js +41 -0
- package/dist/canvas/chunks/ebnfDiagram-BXEA7PRR-zphtgegj.js +1 -0
- package/dist/canvas/chunks/erDiagram-JOGREHBK-zvbbdxp4.js +85 -0
- package/dist/canvas/chunks/eventmodeling-45OFAUF4-v9q6dn4c.js +1 -0
- package/dist/canvas/chunks/flowDiagram-UKHOOZJN-yrr5z8tc.js +1 -0
- package/dist/canvas/chunks/ganttDiagram-PKOTCBZU-282jzycm.js +292 -0
- package/dist/canvas/chunks/gitGraph-TEB2WS4Q-kdwb8m0a.js +1 -0
- package/dist/canvas/chunks/gitGraphDiagram-DS77QQ5N-vqqy6p1b.js +106 -0
- package/dist/canvas/chunks/index-0rk4gp1g.js +2 -0
- package/dist/canvas/chunks/index-194m3hc3.js +2 -0
- package/dist/canvas/chunks/index-1r72hdzb.js +301 -0
- package/dist/canvas/chunks/index-353m7b0p.js +2 -0
- package/dist/canvas/chunks/index-3cy1bzwh.js +2 -0
- package/dist/canvas/chunks/index-5tv8v1hk.js +1 -0
- package/dist/canvas/chunks/index-6ceya556.js +332 -0
- package/dist/canvas/chunks/index-6qmsnxwy.js +4 -0
- package/dist/canvas/chunks/index-d3b3gqe9.js +2 -0
- package/dist/canvas/chunks/index-fjtdggjn.js +2 -0
- package/dist/canvas/chunks/index-g3pwnqeb.js +2 -0
- package/dist/canvas/chunks/index-gf4e0et4.js +2 -0
- package/dist/canvas/chunks/index-hj99nz1f.js +1 -0
- package/dist/canvas/chunks/index-nprm96xp.js +16 -0
- package/dist/canvas/chunks/index-qj8fxwsf.js +2 -0
- package/dist/canvas/chunks/index-r2fyc9w1.js +89 -0
- package/dist/canvas/chunks/index-rft2hcz5.js +2 -0
- package/dist/canvas/chunks/index-rh7nv68n.js +2 -0
- package/dist/canvas/chunks/index-s4j678gj.js +2 -0
- package/dist/canvas/chunks/index-tb1wf525.js +179 -0
- package/dist/canvas/chunks/index-vv56h019.js +2 -0
- package/dist/canvas/chunks/index-w2mra17t.js +207 -0
- package/dist/canvas/chunks/index-wgv7en21.js +232 -0
- package/dist/canvas/chunks/index-yw8h9vha.js +157 -0
- package/dist/canvas/chunks/info-DKCQHKI2-wpzs6sz3.js +1 -0
- package/dist/canvas/chunks/infoDiagram-6WML65LV-v1yvggqj.js +2 -0
- package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-5hnq1a9p.js +70 -0
- package/dist/canvas/chunks/journeyDiagram-NVQOT4AX-zjcffveh.js +139 -0
- package/dist/canvas/chunks/kanban-definition-27J2QSJJ-dakqtfp1.js +89 -0
- package/dist/canvas/chunks/katex-dmg8k4zm.js +257 -0
- package/dist/canvas/chunks/mermaid.core-j2geswrd.js +12 -0
- package/dist/canvas/chunks/mindmap-definition-FAOFIHXS-142xhr3k.js +96 -0
- package/dist/canvas/chunks/packet-7NZHBO7P-xn439f0s.js +1 -0
- package/dist/canvas/chunks/pegDiagram-VL7TDLO6-5wvk7vsd.js +1 -0
- package/dist/canvas/chunks/pie-RZYD4A2V-0kf16rwh.js +1 -0
- package/dist/canvas/chunks/pieDiagram-7S7Q4E2Y-ztdhwycc.js +39 -0
- package/dist/canvas/chunks/quadrantDiagram-CIZ2JOQS-c5p9gmg6.js +7 -0
- package/dist/canvas/chunks/radar-I7S5WNFK-0g2pz36t.js +1 -0
- package/dist/canvas/chunks/railroad-3IZDKUUU-fcaran6j.js +1 -0
- package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-wgdya41q.js +1 -0
- package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-gy07a4ka.js +1 -0
- package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-ewk6wtkh.js +1 -0
- package/dist/canvas/chunks/railroadDiagram-AXF67PYL-xkmqkjwf.js +1 -0
- package/dist/canvas/chunks/requirementDiagram-LRYGKXZP-9wm2dyaw.js +84 -0
- package/dist/canvas/chunks/sankeyDiagram-W5VNT64P-2w19etgb.js +40 -0
- package/dist/canvas/chunks/sequenceDiagram-SI44F4Z6-0jn0vwmz.js +162 -0
- package/dist/canvas/chunks/sizeCapture-X5ZJPWSS-x75epwdq.js +1 -0
- package/dist/canvas/chunks/stateDiagram-OKZ733FA-schjpy6d.js +1 -0
- package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-k8sfp2e7.js +1 -0
- package/dist/canvas/chunks/swimlanes-SLNWSIFB-gkw4xce3.js +1 -0
- package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-nqysw67j.js +8 -0
- package/dist/canvas/chunks/timeline-definition-Z64GVDOM-x7nkcy01.js +120 -0
- package/dist/canvas/chunks/treeView-QDETBFTQ-cesw6gp0.js +1 -0
- package/dist/canvas/chunks/treemap-6X3UGDF4-zdv4knp9.js +1 -0
- package/dist/canvas/chunks/vennDiagram-T6HMQDX7-x8dqn9yf.js +34 -0
- package/dist/canvas/chunks/wardley-OPB4EBWU-44pdddez.js +1 -0
- package/dist/canvas/chunks/wardleyDiagram-T6FBY63Y-t1j52t9r.js +78 -0
- package/dist/canvas/chunks/xychartDiagram-ELKLHX3M-38t16pme.js +7 -0
- package/dist/canvas/global.css +389 -229
- package/dist/canvas/index.js +141 -211
- package/dist/canvas/mcp-app.html +4205 -554
- package/dist/canvas/mermaid-entry.js +338 -338
- package/dist/canvas/surface-theme.css +6 -6
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +49 -49
- package/dist/types/client/canvas/BarHint.d.ts +5 -3
- package/dist/types/client/canvas/CanvasNode.d.ts +0 -11
- package/dist/types/client/canvas/EdgeLayer.d.ts +1 -2
- package/dist/types/client/canvas/auto-fit.d.ts +4 -5
- package/dist/types/client/mermaid-entry.d.ts +1 -2
- package/dist/types/client/mermaid-render.d.ts +1 -0
- package/dist/types/client/nodes/BoardNode.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +7 -90
- package/dist/types/client/nodes/McpAppNode.d.ts +1 -25
- package/dist/types/client/nodes/MermaidNode.d.ts +1 -8
- package/dist/types/client/nodes/ViewerFrame.d.ts +29 -0
- package/dist/types/client/nodes/ViewerHost.d.ts +6 -0
- package/dist/types/client/nodes/iframe-document-url.d.ts +3 -9
- package/dist/types/client/nodes/primitive-copy.d.ts +5 -0
- package/dist/types/client/nodes/use-iframe-content-height.d.ts +1 -1
- package/dist/types/client/nodes/use-surface-frame.d.ts +4 -6
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +2 -1
- package/dist/types/client/state/canvas-store.d.ts +3 -1
- package/dist/types/client/state/intent-bridge.d.ts +1 -0
- package/dist/types/client/state/presentation.d.ts +17 -6
- package/dist/types/json-render/server.d.ts +2 -1
- package/dist/types/mcp/canvas-access.d.ts +1 -12
- package/dist/types/mcp/resource-subscriptions.d.ts +7 -0
- package/dist/types/mcp/server.d.ts +2 -0
- package/dist/types/server/ax-wait.d.ts +1 -0
- package/dist/types/server/canvas-operations.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/index.d.ts +11 -6
- package/dist/types/server/operations/http.d.ts +10 -0
- package/dist/types/server/operations/index.d.ts +2 -2
- package/dist/types/server/operations/invoker.d.ts +4 -3
- package/dist/types/server/operations/mcp-contracts.d.ts +20 -0
- package/dist/types/server/operations/mcp.d.ts +2 -0
- package/dist/types/server/operations/ops/batch.d.ts +1 -0
- package/dist/types/server/operations/ops/boards.d.ts +0 -1
- package/dist/types/server/operations/registry.d.ts +2 -2
- package/dist/types/server/operations/types.d.ts +12 -2
- package/dist/types/server/server.d.ts +2 -0
- package/dist/types/server/tour-control.d.ts +13 -3
- package/dist/types/server/webpage-node.d.ts +1 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +0 -9
- package/dist/types/shared/tour.d.ts +8 -0
- package/docs/api-reference.md +2 -2
- package/docs/cli.md +1 -1
- package/docs/design/Chrome.dc.html +32 -6
- package/docs/design/Types1.dc.html +3 -2
- package/docs/design/Types2.dc.html +1 -1
- package/docs/design/Types3.dc.html +1 -1
- package/docs/design/Zoom.dc.html +37 -60
- package/docs/design/canvas.json +1 -1
- package/docs/design.md +169 -4
- package/docs/http-api.md +82 -1
- package/docs/mcp.md +101 -21
- package/docs/node-types.md +38 -3
- package/docs/plans/plan-009-tech-debt-backlog.md +2 -2
- package/docs/plans/plan-019-post-0.9-roadmap.md +402 -13
- package/docs/product-vision-2026-09.md +9 -0
- package/docs/sdk.md +5 -2
- package/package.json +2 -2
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +1 -1
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/tools.json +9418 -2016
- package/skills/pmx-canvas/SKILL.md +19 -21
- package/skills/pmx-canvas/references/full-reference.md +44 -6
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +7 -7
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +3 -5
- package/src/client/canvas/BarHint.tsx +6 -3
- package/src/client/canvas/CanvasNode.tsx +34 -47
- package/src/client/canvas/CanvasViewport.tsx +6 -7
- package/src/client/canvas/ContextMenu.tsx +63 -3
- package/src/client/canvas/EdgeLayer.tsx +365 -169
- package/src/client/canvas/ExpandedNodeOverlay.tsx +1 -2
- package/src/client/canvas/Presentation.tsx +53 -3
- package/src/client/canvas/ToolRail.tsx +2 -0
- package/src/client/canvas/TopBar.tsx +24 -9
- package/src/client/canvas/auto-fit.ts +9 -11
- package/src/client/canvas/canvas-area.ts +1 -1
- package/src/client/canvas/use-focus-trap.ts +6 -1
- package/src/client/mermaid-entry.ts +6 -20
- package/src/client/mermaid-render.ts +39 -0
- package/src/client/nodes/AxStepControls.tsx +45 -69
- package/src/client/nodes/BoardNode.tsx +65 -9
- package/src/client/nodes/ContextNode.tsx +88 -396
- package/src/client/nodes/ExtAppFrame.tsx +283 -1462
- package/src/client/nodes/FileNode.tsx +92 -81
- package/src/client/nodes/HtmlNode.tsx +8 -36
- package/src/client/nodes/LedgerNode.tsx +31 -17
- package/src/client/nodes/MarkdownNode.tsx +8 -17
- package/src/client/nodes/McpAppNode.tsx +48 -154
- package/src/client/nodes/MermaidNode.tsx +80 -69
- package/src/client/nodes/PromptNode.tsx +12 -14
- package/src/client/nodes/ResponseNode.tsx +8 -6
- package/src/client/nodes/StatusNode.tsx +22 -7
- package/src/client/nodes/StatusSummary.tsx +5 -3
- package/src/client/nodes/TraceNode.tsx +8 -8
- package/src/client/nodes/ViewerFrame.tsx +262 -0
- package/src/client/nodes/ViewerHost.tsx +41 -0
- package/src/client/nodes/WebpageNode.tsx +22 -24
- package/src/client/nodes/iframe-document-url.ts +20 -27
- package/src/client/nodes/primitive-copy.ts +97 -0
- package/src/client/nodes/use-iframe-content-height.ts +34 -10
- package/src/client/nodes/use-surface-frame.ts +22 -11
- package/src/client/nodes/viewer-runtime-state.ts +41 -35
- package/src/client/state/canvas-store.ts +18 -12
- package/src/client/state/intent-bridge.ts +1 -0
- package/src/client/state/presentation.ts +135 -15
- package/src/client/state/sse-bridge.ts +18 -19
- package/src/client/theme/global.css +389 -229
- package/src/client/theme/surface-theme.css +6 -6
- package/src/json-render/charts/tufte-components.tsx +44 -2
- package/src/json-render/renderer/index.css +62 -2
- package/src/json-render/renderer/index.tsx +31 -7
- package/src/json-render/server.ts +3 -2
- package/src/mcp/canvas-access.ts +6 -47
- package/src/mcp/resource-subscriptions.ts +181 -0
- package/src/mcp/server.ts +185 -219
- package/src/server/ax-wait.ts +21 -6
- package/src/server/board-export.ts +35 -24
- package/src/server/canvas-geometry.ts +1 -0
- package/src/server/canvas-operations.ts +6 -2
- package/src/server/canvas-state.ts +5 -0
- package/src/server/demo-state.json +106 -100
- package/src/server/html-primitives.ts +3 -9
- package/src/server/html-surface.ts +8 -0
- package/src/server/index.ts +30 -12
- package/src/server/operations/http.ts +69 -21
- package/src/server/operations/index.ts +8 -1
- package/src/server/operations/invoker.ts +53 -5
- package/src/server/operations/mcp-contracts.ts +354 -0
- package/src/server/operations/mcp.ts +153 -18
- package/src/server/operations/ops/ax-await.ts +18 -10
- package/src/server/operations/ops/ax-read.ts +2 -0
- package/src/server/operations/ops/ax-timeline.ts +2 -2
- package/src/server/operations/ops/batch.ts +27 -8
- package/src/server/operations/ops/boards.ts +1 -3
- package/src/server/operations/ops/nodes.ts +18 -3
- package/src/server/operations/ops/viewport.ts +11 -9
- package/src/server/operations/registry.ts +24 -4
- package/src/server/operations/types.ts +16 -2
- package/src/server/server.ts +17 -59
- package/src/server/tour-control.ts +66 -6
- package/src/server/webpage-node.ts +9 -4
- package/src/shared/ax-surface-protocol.ts +0 -11
- package/src/shared/content-height-reporter.ts +4 -3
- package/src/shared/tour.ts +8 -0
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
/* ── Core palette (dark · ink + cyan) ────────────────────── */
|
|
22
22
|
--c-bg: #081524;
|
|
23
23
|
--c-panel: #0f1d31;
|
|
24
|
-
--c-panel-soft: #
|
|
24
|
+
--c-panel-soft: #13243b;
|
|
25
25
|
--c-line: #1b2c44;
|
|
26
26
|
--c-text: #e6eef7;
|
|
27
|
-
--c-text-soft: #
|
|
27
|
+
--c-text-soft: #b9c6d6;
|
|
28
28
|
--c-muted: #8ea3bd;
|
|
29
|
-
--c-dim: #
|
|
29
|
+
--c-dim: #6b7d94;
|
|
30
30
|
--c-accent: #4BBCFF;
|
|
31
31
|
--c-ok: #2fd07f;
|
|
32
32
|
--c-warn: #f4c542;
|
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
/* Common aliases authored HTML might use. */
|
|
47
47
|
--color-bg: #081524;
|
|
48
48
|
--color-panel: #0f1d31;
|
|
49
|
-
--color-surface: #
|
|
49
|
+
--color-surface: #13243b;
|
|
50
50
|
--color-border: #1b2c44;
|
|
51
51
|
--color-text: #e6eef7;
|
|
52
52
|
--color-text-primary: #e6eef7;
|
|
53
|
-
--color-text-secondary: #
|
|
53
|
+
--color-text-secondary: #b9c6d6;
|
|
54
54
|
--color-text-muted: #8ea3bd;
|
|
55
|
-
--color-text-dim: #
|
|
55
|
+
--color-text-dim: #6b7d94;
|
|
56
56
|
--color-accent: #4BBCFF;
|
|
57
57
|
--color-success: #2fd07f;
|
|
58
58
|
--color-warning: #f4c542;
|
|
@@ -336,6 +336,18 @@ interface SlopegraphProps {
|
|
|
336
336
|
height?: number | null;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/** Separate close labels without moving the data points they describe. */
|
|
340
|
+
function slopeLabelPositions(values: number[], top: number, bottom: number): number[] {
|
|
341
|
+
const order = values.map((y, index) => ({ y, index })).sort((a, b) => a.y - b.y);
|
|
342
|
+
const gap = Math.min(14, (bottom - top) / Math.max(1, order.length - 1));
|
|
343
|
+
for (let i = 1; i < order.length; i++) order[i].y = Math.max(order[i].y, order[i - 1].y + gap);
|
|
344
|
+
if (order.length) order[order.length - 1].y = Math.min(order[order.length - 1].y, bottom);
|
|
345
|
+
for (let i = order.length - 2; i >= 0; i--) order[i].y = Math.min(order[i].y, order[i + 1].y - gap);
|
|
346
|
+
const result = [...values];
|
|
347
|
+
for (const { y, index } of order) result[index] = y;
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
|
|
339
351
|
function ChartSlopegraph({ props }: BaseComponentProps<SlopegraphProps>) {
|
|
340
352
|
const stroke = props.color ?? ACCENT;
|
|
341
353
|
const rows = (props.data ?? []).map((row) => ({
|
|
@@ -359,6 +371,16 @@ function ChartSlopegraph({ props }: BaseComponentProps<SlopegraphProps>) {
|
|
|
359
371
|
// fills without pushing a scrollbar onto the iframe document.
|
|
360
372
|
const plotH = chartPlotHeight(height, Boolean(props.title));
|
|
361
373
|
const scaleY = (v: number) => topPad + (1 - (v - min) / (max - min)) * (plotH - topPad - botPad);
|
|
374
|
+
const beforeLabels = slopeLabelPositions(
|
|
375
|
+
rows.map((row) => scaleY(row.before)),
|
|
376
|
+
topPad,
|
|
377
|
+
plotH - botPad,
|
|
378
|
+
);
|
|
379
|
+
const afterLabels = slopeLabelPositions(
|
|
380
|
+
rows.map((row) => scaleY(row.after)),
|
|
381
|
+
topPad,
|
|
382
|
+
plotH - botPad,
|
|
383
|
+
);
|
|
362
384
|
|
|
363
385
|
return (
|
|
364
386
|
<div ref={frameRef} className="pmx-chart pmx-chart--slopegraph" style={{ height }}>
|
|
@@ -390,10 +412,30 @@ function ChartSlopegraph({ props }: BaseComponentProps<SlopegraphProps>) {
|
|
|
390
412
|
<line x1={leftX} y1={y1} x2={rightX} y2={y2} stroke={lineColor} strokeWidth={1.5} />
|
|
391
413
|
<circle cx={leftX} cy={y1} r={2.5} fill={lineColor} />
|
|
392
414
|
<circle cx={rightX} cy={y2} r={2.5} fill={lineColor} />
|
|
393
|
-
|
|
415
|
+
{beforeLabels[i] !== y1 && (
|
|
416
|
+
<line x1={leftX} y1={y1} x2={leftX - 5} y2={beforeLabels[i]} stroke={lineColor} />
|
|
417
|
+
)}
|
|
418
|
+
{afterLabels[i] !== y2 && (
|
|
419
|
+
<line x1={rightX} y1={y2} x2={rightX + 5} y2={afterLabels[i]} stroke={lineColor} />
|
|
420
|
+
)}
|
|
421
|
+
<text
|
|
422
|
+
x={leftX - 8}
|
|
423
|
+
y={beforeLabels[i]}
|
|
424
|
+
textAnchor="end"
|
|
425
|
+
dominantBaseline="central"
|
|
426
|
+
fontSize={11}
|
|
427
|
+
fill={INK}
|
|
428
|
+
>
|
|
394
429
|
{`${row.label} ${row.before}`}
|
|
395
430
|
</text>
|
|
396
|
-
<text
|
|
431
|
+
<text
|
|
432
|
+
x={rightX + 8}
|
|
433
|
+
y={afterLabels[i]}
|
|
434
|
+
textAnchor="start"
|
|
435
|
+
dominantBaseline="central"
|
|
436
|
+
fontSize={11}
|
|
437
|
+
fill={INK}
|
|
438
|
+
>
|
|
397
439
|
{`${row.after} ${row.label}`}
|
|
398
440
|
</text>
|
|
399
441
|
</g>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
@source "../../../../node_modules/@json-render/shadcn/dist/index.js";
|
|
5
5
|
|
|
6
|
-
@custom-variant dark (&:is(
|
|
6
|
+
@custom-variant dark (&:is(.dark *));
|
|
7
7
|
|
|
8
8
|
@theme {
|
|
9
9
|
--font-sans: "IBM Plex Sans", system-ui, sans-serif;
|
|
@@ -99,6 +99,66 @@
|
|
|
99
99
|
--chart-6: #ffffff;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/* Named themes share the canvas surface, including nested cards and charts.
|
|
103
|
+
Explicit dark/light remain available for authored design experiments. */
|
|
104
|
+
:root[data-canvas-theme] {
|
|
105
|
+
--radius: 6px;
|
|
106
|
+
--text-xs: 11px;
|
|
107
|
+
--text-sm: 12px;
|
|
108
|
+
--text-base: 13px;
|
|
109
|
+
--text-lg: 15px;
|
|
110
|
+
--text-xl: 20px;
|
|
111
|
+
--background: var(--c-panel);
|
|
112
|
+
--foreground: var(--c-text);
|
|
113
|
+
--card: var(--c-panel-soft);
|
|
114
|
+
--card-foreground: var(--c-text);
|
|
115
|
+
--popover: var(--c-panel);
|
|
116
|
+
--popover-foreground: var(--c-text);
|
|
117
|
+
--primary: var(--c-accent);
|
|
118
|
+
--primary-foreground: var(--c-on-accent, var(--c-on-pin));
|
|
119
|
+
--secondary: var(--c-panel-soft);
|
|
120
|
+
--secondary-foreground: var(--c-text);
|
|
121
|
+
--muted: var(--c-panel-soft);
|
|
122
|
+
--muted-foreground: var(--c-muted);
|
|
123
|
+
--accent: var(--c-panel-soft);
|
|
124
|
+
--accent-foreground: var(--c-text);
|
|
125
|
+
--destructive: var(--c-danger);
|
|
126
|
+
--border: var(--c-line);
|
|
127
|
+
--input: var(--c-line);
|
|
128
|
+
--ring: var(--c-accent);
|
|
129
|
+
--chart-1: var(--c-accent);
|
|
130
|
+
--chart-2: var(--c-rel-supports);
|
|
131
|
+
--chart-3: var(--c-rel-contradicts);
|
|
132
|
+
--chart-4: var(--c-rel-cites);
|
|
133
|
+
--chart-5: var(--c-rel-informs);
|
|
134
|
+
--chart-6: var(--c-muted);
|
|
135
|
+
font-size: 16px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* surface-theme's unlayered body rule otherwise wins over Tailwind's base layer. */
|
|
139
|
+
body {
|
|
140
|
+
background: var(--background);
|
|
141
|
+
color: var(--foreground);
|
|
142
|
+
padding: 0;
|
|
143
|
+
font-family: var(--font-sans);
|
|
144
|
+
font-size: 13px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.pmx-viewer-root { width: 100%; min-width: 0; }
|
|
148
|
+
[data-embedded] .pmx-viewer-root:not(:has(> [data-slot="card"])) { padding: 12px 14px; }
|
|
149
|
+
[data-embedded] .pmx-viewer-root > [data-slot="card"] {
|
|
150
|
+
width: 100%;
|
|
151
|
+
max-width: none;
|
|
152
|
+
border: 0;
|
|
153
|
+
border-radius: 0;
|
|
154
|
+
box-shadow: none;
|
|
155
|
+
background: transparent;
|
|
156
|
+
padding: 12px 14px;
|
|
157
|
+
}
|
|
158
|
+
[data-embedded][data-duplicate-title] .pmx-viewer-root > [data-slot="card"] > [data-slot="card-header"] > [data-slot="card-title"] { display: none; }
|
|
159
|
+
[data-embedded][data-duplicate-title] .pmx-viewer-root > [data-slot="card"] > [data-slot="card-header"]:not(:has([data-slot="card-description"])) { display: none; }
|
|
160
|
+
[data-embedded] .pmx-viewer-root > [data-slot="card"] > [data-slot="card-content"] { padding: 0; }
|
|
161
|
+
|
|
102
162
|
@theme inline {
|
|
103
163
|
--radius-sm: calc(var(--radius) - 4px);
|
|
104
164
|
--radius-md: calc(var(--radius) - 2px);
|
|
@@ -159,7 +219,7 @@ button {
|
|
|
159
219
|
border: 1px solid transparent;
|
|
160
220
|
border-radius: 9999px;
|
|
161
221
|
padding: 0.125rem 0.5rem;
|
|
162
|
-
font-size: 0.75rem;
|
|
222
|
+
font-size: max(11px, 0.75rem);
|
|
163
223
|
font-weight: 500;
|
|
164
224
|
line-height: 1.25rem;
|
|
165
225
|
}
|
|
@@ -23,6 +23,7 @@ import { extraChartComponents } from '../charts/extra-components';
|
|
|
23
23
|
import { tufteChartComponents } from '../charts/tufte-components';
|
|
24
24
|
import { pmxCanvasDirectives } from '../directives';
|
|
25
25
|
import { JsonRenderDevtools } from '@json-render/devtools-react';
|
|
26
|
+
import { canvasThemeScheme, isCanvasTheme } from '../../shared/themes.js';
|
|
26
27
|
|
|
27
28
|
type BadgeVariant =
|
|
28
29
|
| 'default'
|
|
@@ -167,10 +168,12 @@ function syncPreferredTheme(): void {
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
function applyTheme(theme: unknown): void {
|
|
170
|
-
if (theme !== 'dark' && theme !== 'light' && theme
|
|
171
|
+
if (theme !== 'dark' && theme !== 'light' && !isCanvasTheme(theme)) return;
|
|
171
172
|
document.documentElement.setAttribute('data-theme', theme);
|
|
172
|
-
document.documentElement.
|
|
173
|
-
|
|
173
|
+
document.documentElement.toggleAttribute('data-canvas-theme', isCanvasTheme(theme));
|
|
174
|
+
const scheme = theme === 'light' ? 'light' : canvasThemeScheme(theme);
|
|
175
|
+
document.documentElement.classList.toggle('dark', scheme === 'dark');
|
|
176
|
+
document.documentElement.style.colorScheme = scheme;
|
|
174
177
|
}
|
|
175
178
|
|
|
176
179
|
function App({ store }: { store: StateStore }) {
|
|
@@ -235,11 +238,15 @@ function App({ store }: { store: StateStore }) {
|
|
|
235
238
|
padding: 0,
|
|
236
239
|
boxSizing: 'border-box' as const,
|
|
237
240
|
}
|
|
238
|
-
: { minHeight: '100vh', padding:
|
|
241
|
+
: { minHeight: '100vh', padding: 0, boxSizing: 'border-box' as const };
|
|
239
242
|
return (
|
|
240
|
-
<div
|
|
243
|
+
<div
|
|
244
|
+
style={containerStyle}
|
|
245
|
+
data-embedded={!isSite ? '' : undefined}
|
|
246
|
+
data-duplicate-title={spec.elements[spec.root]?.props?.title === document.title ? '' : undefined}
|
|
247
|
+
>
|
|
241
248
|
<JSONUIProvider registry={registry} store={store} directives={pmxCanvasDirectives} handlers={buildAxHandlers()}>
|
|
242
|
-
<div style={isSite ? { flex: 1, minHeight: 0 } : undefined}>
|
|
249
|
+
<div className="pmx-viewer-root" style={isSite ? { flex: 1, minHeight: 0 } : undefined}>
|
|
243
250
|
<Renderer spec={spec} registry={registry} loading={false} />
|
|
244
251
|
</div>
|
|
245
252
|
{window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ ? <JsonRenderDevtools position="right" /> : null}
|
|
@@ -282,8 +289,25 @@ if (root) {
|
|
|
282
289
|
// Standalone viewers have no state token and render immediately.
|
|
283
290
|
window.addEventListener('message', (event: MessageEvent) => {
|
|
284
291
|
if (event.source !== window.parent) return;
|
|
285
|
-
const m = event.data as {
|
|
292
|
+
const m = event.data as {
|
|
293
|
+
source?: string;
|
|
294
|
+
type?: string;
|
|
295
|
+
token?: string;
|
|
296
|
+
state?: unknown;
|
|
297
|
+
theme?: unknown;
|
|
298
|
+
title?: unknown;
|
|
299
|
+
} | null;
|
|
286
300
|
if (!m) return;
|
|
301
|
+
if (m.source === HTML_SURFACE_PUSH_SOURCE && m.type === 'theme-update' && stateToken && m.token === stateToken) {
|
|
302
|
+
applyTheme(m.theme);
|
|
303
|
+
if (typeof m.title === 'string') {
|
|
304
|
+
document.title = m.title;
|
|
305
|
+
const spec = window.__PMX_CANVAS_JSON_RENDER_SPEC__;
|
|
306
|
+
document
|
|
307
|
+
.querySelector('[data-embedded]')
|
|
308
|
+
?.toggleAttribute('data-duplicate-title', spec?.elements[spec.root]?.props?.title === m.title);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
287
311
|
if (m.source === JSON_VIEWER_STATE_SOURCE && m.type === 'restore' && stateToken && m.token === stateToken) {
|
|
288
312
|
if (m.state === null || (typeof m.state === 'object' && !Array.isArray(m.state)))
|
|
289
313
|
mount(m.state as Record<string, unknown> | null);
|
|
@@ -6,6 +6,7 @@ import { applySpecPatch, parseSpecStreamLine, type Spec, type SpecStreamLine } f
|
|
|
6
6
|
import { allComponentDefinitions, catalog, validateShadcnElementProps, type JsonRenderIssue } from './catalog.js';
|
|
7
7
|
import { findUnknownDirectiveKey, isDynamicPropValue } from './directives.js';
|
|
8
8
|
import { contentHeightReporterTag } from '../shared/content-height-reporter.js';
|
|
9
|
+
import type { CanvasThemeName } from '../shared/themes.js';
|
|
9
10
|
|
|
10
11
|
export interface JsonRenderSpec {
|
|
11
12
|
root: string;
|
|
@@ -1030,7 +1031,7 @@ export async function readJsonRenderBundle(): Promise<{ js: string; css: string
|
|
|
1030
1031
|
export async function buildJsonRenderViewerHtml(options: {
|
|
1031
1032
|
title: string;
|
|
1032
1033
|
spec: JsonRenderSpec;
|
|
1033
|
-
theme?:
|
|
1034
|
+
theme?: CanvasThemeName | 'dark' | 'light';
|
|
1034
1035
|
display?: 'expanded' | 'site';
|
|
1035
1036
|
devtools?: boolean;
|
|
1036
1037
|
nodeId?: string;
|
|
@@ -1076,7 +1077,7 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
1076
1077
|
const heightReporter = options.frameToken ? contentHeightReporterTag(options.frameToken) : '';
|
|
1077
1078
|
return buildAppHtml({
|
|
1078
1079
|
title: options.title,
|
|
1079
|
-
css: cssBundle
|
|
1080
|
+
css: `${readFileSync(join(import.meta.dir, '../client/theme/surface-theme.css'), 'utf-8')}\n${cssBundle}`,
|
|
1080
1081
|
js: escapeInlineScriptSource(boot),
|
|
1081
1082
|
head: `<meta name="color-scheme" content="light dark" />${heightReporter}`,
|
|
1082
1083
|
});
|
package/src/mcp/canvas-access.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { realpathSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, dirname, resolve } from 'node:path';
|
|
4
4
|
import { createCanvas, canvasState, type CanvasLayout, type CanvasNodeState, type PmxCanvas } from '../server/index.js';
|
|
5
|
-
import type { PmxAxSource } from '../server/ax-state.js';
|
|
6
5
|
import type { ContextReadInput } from '../server/context-reads.js';
|
|
7
6
|
import { HttpOperationInvoker, LocalOperationInvoker, type OperationInvoker } from '../server/operations/index.js';
|
|
8
7
|
|
|
@@ -14,13 +13,9 @@ import { HttpOperationInvoker, LocalOperationInvoker, type OperationInvoker } fr
|
|
|
14
13
|
// not CanvasAccess. The public SDK PmxCanvas methods are unchanged.
|
|
15
14
|
type AxStateResult = ReturnType<PmxCanvas['getAxState']>;
|
|
16
15
|
type AxContextResult = ReturnType<PmxCanvas['getAxContext']>;
|
|
17
|
-
type SubmitAxInteractionInput = Parameters<PmxCanvas['submitAxInteraction']>[0];
|
|
18
|
-
type SubmitAxInteractionResult = ReturnType<PmxCanvas['submitAxInteraction']>;
|
|
19
16
|
type GetPendingSteeringResult = ReturnType<PmxCanvas['getPendingSteering']>;
|
|
20
17
|
type ListElicitationsResult = ReturnType<PmxCanvas['listElicitations']>;
|
|
21
18
|
type ListModeRequestsResult = ReturnType<PmxCanvas['listModeRequests']>;
|
|
22
|
-
type IngestActivityInput = Parameters<PmxCanvas['ingestActivity']>[0];
|
|
23
|
-
type IngestActivityResult = ReturnType<PmxCanvas['ingestActivity']>;
|
|
24
19
|
type GetPolicyResult = ReturnType<PmxCanvas['getPolicy']>;
|
|
25
20
|
type GetAxTimelineQuery = Parameters<PmxCanvas['getAxTimeline']>[0];
|
|
26
21
|
type GetAxTimelineResult = ReturnType<PmxCanvas['getAxTimeline']>;
|
|
@@ -56,14 +51,9 @@ export interface CanvasAccess {
|
|
|
56
51
|
listWorkItems(): Promise<ListWorkItemsResult>;
|
|
57
52
|
listApprovalGates(): Promise<ListApprovalGatesResult>;
|
|
58
53
|
listReviewAnnotations(): Promise<ListReviewAnnotationsResult>;
|
|
59
|
-
submitAxInteraction(
|
|
60
|
-
input: SubmitAxInteractionInput,
|
|
61
|
-
options?: { source?: PmxAxSource },
|
|
62
|
-
): Promise<SubmitAxInteractionResult>;
|
|
63
54
|
getPendingSteering(options?: { consumer?: string; limit?: number }): Promise<GetPendingSteeringResult>;
|
|
64
55
|
listElicitations(): Promise<ListElicitationsResult>;
|
|
65
56
|
listModeRequests(): Promise<ListModeRequestsResult>;
|
|
66
|
-
ingestActivity(input: IngestActivityInput, options?: { source?: PmxAxSource }): Promise<IngestActivityResult>;
|
|
67
57
|
getPolicy(): Promise<GetPolicyResult>;
|
|
68
58
|
getHistory(): Promise<HistoryResult>;
|
|
69
59
|
getPinnedNodeIds(board?: string): Promise<string[]>;
|
|
@@ -75,7 +65,7 @@ export interface CanvasAccess {
|
|
|
75
65
|
// canvas_screenshot (still hand-written — binary payload) is the only webview
|
|
76
66
|
// tool left on CanvasAccess; it needs the status + screenshot accessors.
|
|
77
67
|
getAutomationWebViewStatus(): Promise<AutomationWebViewStatus>;
|
|
78
|
-
screenshotAutomationWebView(options?: AutomationScreenshotOptions): Promise<Uint8Array>;
|
|
68
|
+
screenshotAutomationWebView(options?: AutomationScreenshotOptions, signal?: AbortSignal): Promise<Uint8Array>;
|
|
79
69
|
}
|
|
80
70
|
|
|
81
71
|
class LocalCanvasAccess implements CanvasAccess {
|
|
@@ -123,13 +113,6 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
123
113
|
return this.canvas.getAxTimeline(query);
|
|
124
114
|
}
|
|
125
115
|
|
|
126
|
-
async submitAxInteraction(
|
|
127
|
-
input: SubmitAxInteractionInput,
|
|
128
|
-
options?: { source?: PmxAxSource },
|
|
129
|
-
): Promise<SubmitAxInteractionResult> {
|
|
130
|
-
return this.canvas.submitAxInteraction(input, { source: options?.source ?? 'mcp' });
|
|
131
|
-
}
|
|
132
|
-
|
|
133
116
|
async getPendingSteering(options?: { consumer?: string; limit?: number }): Promise<GetPendingSteeringResult> {
|
|
134
117
|
return this.canvas.getPendingSteering(options);
|
|
135
118
|
}
|
|
@@ -142,10 +125,6 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
142
125
|
return this.canvas.listModeRequests();
|
|
143
126
|
}
|
|
144
127
|
|
|
145
|
-
async ingestActivity(input: IngestActivityInput, options?: { source?: PmxAxSource }): Promise<IngestActivityResult> {
|
|
146
|
-
return this.canvas.ingestActivity(input, { source: options?.source ?? 'mcp' });
|
|
147
|
-
}
|
|
148
|
-
|
|
149
128
|
async getPolicy(): Promise<GetPolicyResult> {
|
|
150
129
|
return this.canvas.getPolicy();
|
|
151
130
|
}
|
|
@@ -299,23 +278,6 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
299
278
|
return await this.requestJson<GetAxTimelineResult>('GET', `/api/canvas/ax/timeline${qs}`);
|
|
300
279
|
}
|
|
301
280
|
|
|
302
|
-
async submitAxInteraction(
|
|
303
|
-
input: SubmitAxInteractionInput,
|
|
304
|
-
options?: { source?: PmxAxSource },
|
|
305
|
-
): Promise<SubmitAxInteractionResult> {
|
|
306
|
-
// The interaction endpoint returns its structured outcome (ok/code/error) in
|
|
307
|
-
// the body for both accepted and rejected interactions, so read the body
|
|
308
|
-
// regardless of HTTP status rather than throwing on a denial.
|
|
309
|
-
const response = await fetch(`${this.remoteBaseUrl}/api/canvas/ax/interaction`, {
|
|
310
|
-
method: 'POST',
|
|
311
|
-
headers: { 'Content-Type': 'application/json' },
|
|
312
|
-
body: JSON.stringify({ ...input, source: options?.source ?? 'mcp' }),
|
|
313
|
-
});
|
|
314
|
-
const body = await response.json().catch(() => null);
|
|
315
|
-
if (body && typeof body === 'object') return body as SubmitAxInteractionResult;
|
|
316
|
-
throw new Error(`Remote canvas interaction failed with HTTP ${response.status}`);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
281
|
async getPendingSteering(options?: { consumer?: string; limit?: number }): Promise<GetPendingSteeringResult> {
|
|
320
282
|
const params = new URLSearchParams();
|
|
321
283
|
if (options?.consumer) params.set('consumer', options.consumer);
|
|
@@ -338,13 +300,6 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
338
300
|
return r.modeRequests ?? [];
|
|
339
301
|
}
|
|
340
302
|
|
|
341
|
-
async ingestActivity(input: IngestActivityInput, options?: { source?: PmxAxSource }): Promise<IngestActivityResult> {
|
|
342
|
-
return await this.requestJson<IngestActivityResult>('POST', '/api/canvas/ax/activity', {
|
|
343
|
-
...input,
|
|
344
|
-
source: options?.source ?? 'mcp',
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
303
|
async getPolicy(): Promise<GetPolicyResult> {
|
|
349
304
|
const r = await this.requestJson<{ policy?: GetPolicyResult }>('GET', '/api/canvas/ax/policy');
|
|
350
305
|
if (!r.policy) throw new Error('Remote canvas did not return a policy.');
|
|
@@ -404,11 +359,15 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
404
359
|
return await this.requestJson<AutomationWebViewStatus>('GET', '/api/workbench/webview');
|
|
405
360
|
}
|
|
406
361
|
|
|
407
|
-
async screenshotAutomationWebView(
|
|
362
|
+
async screenshotAutomationWebView(
|
|
363
|
+
options: AutomationScreenshotOptions = {},
|
|
364
|
+
signal?: AbortSignal,
|
|
365
|
+
): Promise<Uint8Array> {
|
|
408
366
|
const response = await fetch(`${this.remoteBaseUrl}/api/workbench/webview/screenshot`, {
|
|
409
367
|
method: 'POST',
|
|
410
368
|
headers: { 'Content-Type': 'application/json' },
|
|
411
369
|
body: JSON.stringify(options),
|
|
370
|
+
signal,
|
|
412
371
|
});
|
|
413
372
|
if (!response.ok) {
|
|
414
373
|
const text = await response.text();
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import {
|
|
4
|
+
ErrorCode,
|
|
5
|
+
McpError,
|
|
6
|
+
SubscribeRequestSchema,
|
|
7
|
+
UnsubscribeRequestSchema,
|
|
8
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { canvasState } from '../server/canvas-state.js';
|
|
10
|
+
import { onWorkbenchEvent } from '../server/server.js';
|
|
11
|
+
import type { CanvasAccess } from './canvas-access.js';
|
|
12
|
+
import { ContextUriTemplate } from './context-uri-template.js';
|
|
13
|
+
|
|
14
|
+
const liveResources = new Set(
|
|
15
|
+
[
|
|
16
|
+
'layout',
|
|
17
|
+
'boards',
|
|
18
|
+
'summary',
|
|
19
|
+
'context',
|
|
20
|
+
'graph',
|
|
21
|
+
'spatial-context',
|
|
22
|
+
'history',
|
|
23
|
+
'code-graph',
|
|
24
|
+
'pinned-context',
|
|
25
|
+
'ax',
|
|
26
|
+
'ax-context',
|
|
27
|
+
'ax-timeline',
|
|
28
|
+
'ax-work',
|
|
29
|
+
'ax-pending-steering',
|
|
30
|
+
'ax-delivery',
|
|
31
|
+
].map((name) => `canvas://${name}`),
|
|
32
|
+
);
|
|
33
|
+
const contextTemplate = new ContextUriTemplate();
|
|
34
|
+
const graphTemplate = new ContextUriTemplate('graph', ['focus', 'depth']);
|
|
35
|
+
|
|
36
|
+
// A context-status-changed read receipt must never trigger another read forever.
|
|
37
|
+
const resourceEvents = new Set([
|
|
38
|
+
'context-pins-changed',
|
|
39
|
+
'boards-changed',
|
|
40
|
+
'knowledge-changed',
|
|
41
|
+
'ax-state-changed',
|
|
42
|
+
'ax-event-created',
|
|
43
|
+
'canvas-layout-update',
|
|
44
|
+
'canvas-viewport-update',
|
|
45
|
+
'canvas-camera-move',
|
|
46
|
+
'canvas-tour-step',
|
|
47
|
+
'canvas-tour-exit',
|
|
48
|
+
// These events synchronize node data inside server.ts.
|
|
49
|
+
'workbench-open',
|
|
50
|
+
'ext-app-open',
|
|
51
|
+
'ext-app-update',
|
|
52
|
+
'ext-app-result',
|
|
53
|
+
'context-cards',
|
|
54
|
+
'context-usage',
|
|
55
|
+
'aux-open',
|
|
56
|
+
'aux-close',
|
|
57
|
+
'canvas-status',
|
|
58
|
+
'execution-phase',
|
|
59
|
+
'canvas-response-start',
|
|
60
|
+
'canvas-response-delta',
|
|
61
|
+
'canvas-response-complete',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function isLiveResource(uri: string): boolean {
|
|
65
|
+
return liveResources.has(uri) || contextTemplate.match(uri) !== null || graphTemplate.match(uri) !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One subscription set and event source per MCP connection. Notifications are not reads. */
|
|
69
|
+
export function registerResourceSubscriptions(server: McpServer, getCanvas: () => Promise<CanvasAccess>) {
|
|
70
|
+
const subscriptions = new Set<string>();
|
|
71
|
+
let disposeSource: (() => void) | undefined;
|
|
72
|
+
let source: string | null | undefined;
|
|
73
|
+
let closed = false;
|
|
74
|
+
|
|
75
|
+
function notify(): void {
|
|
76
|
+
// Broad invalidation is intentional: context projections span pins, nodes and AX.
|
|
77
|
+
for (const uri of subscriptions) {
|
|
78
|
+
void server.server.sendResourceUpdated({ uri }).catch((error: unknown) => {
|
|
79
|
+
if (!closed) console.error('[mcp] resource notification failed:', error);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function watchRemote(baseUrl: string, signal: AbortSignal): Promise<void> {
|
|
85
|
+
while (!signal.aborted) {
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetch(`${baseUrl}/api/workbench/events`, { signal });
|
|
88
|
+
if (!response.ok || !response.body) throw new Error(`Canvas event stream: HTTP ${response.status}`);
|
|
89
|
+
const reader = response.body.getReader();
|
|
90
|
+
const decoder = new TextDecoder();
|
|
91
|
+
let buffer = '';
|
|
92
|
+
try {
|
|
93
|
+
// Reconnecting may have missed a change. Invalidate subscribed URIs only.
|
|
94
|
+
notify();
|
|
95
|
+
while (!signal.aborted) {
|
|
96
|
+
const { done, value } = await reader.read();
|
|
97
|
+
if (done) break;
|
|
98
|
+
buffer += decoder.decode(value, { stream: true });
|
|
99
|
+
const frames = buffer.split('\n\n');
|
|
100
|
+
buffer = frames.pop() ?? '';
|
|
101
|
+
for (const frame of frames) {
|
|
102
|
+
const event = frame
|
|
103
|
+
.split('\n')
|
|
104
|
+
.find((line) => line.startsWith('event:'))
|
|
105
|
+
?.slice(6)
|
|
106
|
+
.trim();
|
|
107
|
+
if (event && resourceEvents.has(event)) notify();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
await reader.cancel().catch(() => {});
|
|
112
|
+
reader.releaseLock();
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (signal.aborted) return;
|
|
116
|
+
console.error('[mcp] canvas event stream disconnected:', error);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
await delay(1_000, undefined, { signal });
|
|
120
|
+
} catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function attach(canvas: CanvasAccess): void {
|
|
127
|
+
if (closed || subscriptions.size === 0 || source === canvas.remoteBaseUrl) return;
|
|
128
|
+
disposeSource?.();
|
|
129
|
+
source = canvas.remoteBaseUrl;
|
|
130
|
+
if (source) {
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
disposeSource = () => controller.abort();
|
|
133
|
+
void watchRemote(source, controller.signal);
|
|
134
|
+
} else {
|
|
135
|
+
const disposeState = canvasState.onChange(notify);
|
|
136
|
+
// Library-only writes do not change the active board's state.
|
|
137
|
+
const disposeEvents = onWorkbenchEvent((event) => {
|
|
138
|
+
if (resourceEvents.has(event)) notify();
|
|
139
|
+
});
|
|
140
|
+
disposeSource = () => {
|
|
141
|
+
disposeState();
|
|
142
|
+
disposeEvents();
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function dispose(): void {
|
|
148
|
+
closed = true;
|
|
149
|
+
subscriptions.clear();
|
|
150
|
+
disposeSource?.();
|
|
151
|
+
disposeSource = undefined;
|
|
152
|
+
source = undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
server.server.registerCapabilities({ resources: { subscribe: true } });
|
|
156
|
+
server.server.setRequestHandler(SubscribeRequestSchema, async ({ params }, extra) => {
|
|
157
|
+
if (!isLiveResource(params.uri))
|
|
158
|
+
throw new McpError(ErrorCode.InvalidParams, `Resource does not support subscriptions: ${params.uri}`);
|
|
159
|
+
const canvas = await getCanvas();
|
|
160
|
+
extra.signal.throwIfAborted();
|
|
161
|
+
if (closed) throw new McpError(ErrorCode.InternalError, 'Connection closed');
|
|
162
|
+
subscriptions.add(params.uri);
|
|
163
|
+
attach(canvas);
|
|
164
|
+
return {};
|
|
165
|
+
});
|
|
166
|
+
server.server.setRequestHandler(UnsubscribeRequestSchema, ({ params }) => {
|
|
167
|
+
subscriptions.delete(params.uri);
|
|
168
|
+
if (subscriptions.size === 0) {
|
|
169
|
+
disposeSource?.();
|
|
170
|
+
disposeSource = undefined;
|
|
171
|
+
source = undefined;
|
|
172
|
+
}
|
|
173
|
+
return {};
|
|
174
|
+
});
|
|
175
|
+
const onclose = server.server.onclose;
|
|
176
|
+
server.server.onclose = () => {
|
|
177
|
+
dispose();
|
|
178
|
+
onclose?.();
|
|
179
|
+
};
|
|
180
|
+
return { attach, dispose };
|
|
181
|
+
}
|