pmx-canvas 0.1.19 → 0.1.21
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 +159 -0
- package/Readme.md +19 -6
- package/dist/canvas/global.css +123 -2
- package/dist/canvas/index.js +103 -68
- package/dist/json-render/index.js +109 -109
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/icons.d.ts +2 -0
- package/dist/types/client/nodes/HtmlNode.d.ts +12 -1
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/types.d.ts +3 -2
- package/dist/types/json-render/charts/components.d.ts +5 -1
- package/dist/types/json-render/renderer/index.d.ts +1 -0
- package/dist/types/json-render/server.d.ts +1 -0
- package/dist/types/mcp/canvas-access.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +4 -0
- package/dist/types/server/canvas-schema.d.ts +19 -3
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +6 -2
- package/dist/types/server/html-node-summary.d.ts +2 -0
- package/dist/types/server/html-primitives.d.ts +42 -0
- package/dist/types/server/index.d.ts +26 -0
- package/docs/cli.md +4 -1
- package/docs/http-api.md +11 -1
- package/docs/mcp.md +10 -4
- package/docs/node-types.md +54 -4
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +12 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +17 -3
- package/skills/pmx-canvas/references/html-primitives.md +132 -0
- package/src/cli/agent.ts +159 -5
- package/src/cli/index.ts +1 -1
- package/src/client/App.tsx +21 -2
- package/src/client/canvas/AnnotationLayer.tsx +33 -12
- package/src/client/canvas/CanvasViewport.tsx +88 -7
- package/src/client/canvas/CommandPalette.tsx +2 -2
- package/src/client/canvas/ContextMenu.tsx +2 -2
- package/src/client/canvas/ExpandedNodeOverlay.tsx +112 -3
- package/src/client/canvas/auto-fit.ts +5 -1
- package/src/client/icons.tsx +13 -0
- package/src/client/nodes/HtmlNode.tsx +125 -13
- package/src/client/nodes/McpAppNode.tsx +12 -4
- package/src/client/state/canvas-store.ts +15 -5
- package/src/client/state/sse-bridge.ts +5 -4
- package/src/client/theme/global.css +123 -2
- package/src/client/types.ts +2 -1
- package/src/json-render/charts/components.tsx +41 -7
- package/src/json-render/charts/extra-components.tsx +13 -12
- package/src/json-render/renderer/index.tsx +1 -0
- package/src/json-render/server.ts +3 -1
- package/src/mcp/canvas-access.ts +54 -1
- package/src/mcp/server.ts +98 -28
- package/src/server/agent-context.ts +39 -0
- package/src/server/canvas-operations.ts +99 -38
- package/src/server/canvas-provenance.ts +8 -6
- package/src/server/canvas-schema.ts +94 -3
- package/src/server/canvas-serialization.ts +16 -4
- package/src/server/canvas-state.ts +9 -4
- package/src/server/demo-state.json +1143 -0
- package/src/server/demo.ts +25 -777
- package/src/server/html-node-summary.ts +141 -0
- package/src/server/html-primitives.ts +1300 -0
- package/src/server/index.ts +63 -3
- package/src/server/server.ts +154 -17
- package/src/server/spatial-analysis.ts +5 -3
package/src/mcp/server.ts
CHANGED
|
@@ -25,6 +25,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
25
25
|
import { isAbsolute, relative, resolve } from 'node:path';
|
|
26
26
|
import { z } from 'zod';
|
|
27
27
|
import { canvasState, describeCanvasSchema, validateStructuredCanvasPayload } from '../server/index.js';
|
|
28
|
+
import { isHtmlPrimitiveKind } from '../server/html-primitives.js';
|
|
29
|
+
import type { HtmlPrimitiveKind } from '../server/html-primitives.js';
|
|
28
30
|
import { createCanvasAccess, refreshCanvasAccess, type CanvasAccess } from './canvas-access.js';
|
|
29
31
|
import { serializeNodeForAgentContext } from '../server/agent-context.js';
|
|
30
32
|
import { wrapCanvasAutomationScript } from '../server/server.js';
|
|
@@ -56,6 +58,8 @@ const jsonRenderSpecSchema = z.union([
|
|
|
56
58
|
}).passthrough(),
|
|
57
59
|
]);
|
|
58
60
|
|
|
61
|
+
const htmlPrimitiveKindSchema = z.string().refine(isHtmlPrimitiveKind, 'Unknown HTML primitive kind');
|
|
62
|
+
|
|
59
63
|
function structuredSchemaDescription(): string {
|
|
60
64
|
const routing = describeCanvasSchema().mcp.nodeTypeRouting;
|
|
61
65
|
return Object.entries(routing)
|
|
@@ -399,10 +403,17 @@ export async function startMcpServer(): Promise<void> {
|
|
|
399
403
|
// ── canvas_add_html_node ────────────────────────────────────────
|
|
400
404
|
server.tool(
|
|
401
405
|
'canvas_add_html_node',
|
|
402
|
-
'Add
|
|
406
|
+
'Add a normal html node: a self-contained HTML document (with optional inline <script> and CDN <script src="...">) rendered inside a sandboxed iframe (sandbox="allow-scripts"). This is the default HTML surface for reports, widgets, and bespoke visualizations. Presentation mode is opt-in: only pass presentation:true when the user explicitly asks for a deck/fullscreen presentation, or use canvas_add_html_primitive with kind="presentation". The iframe inherits live canvas theme tokens via injected CSS custom properties (both --c-* and common --color-* aliases) so authored HTML using var(--color-text-secondary), var(--color-bg), etc. renders cohesively. No same-origin access; no top-navigation; no forms. For declarative-only views with zero JS, prefer canvas_add_json_render_node. For React + shadcn + routing or multi-component apps, use canvas_build_web_artifact.',
|
|
403
407
|
{
|
|
404
408
|
html: z.string().describe('HTML document or fragment. Full <html>...</html> documents are passed through with theme styles injected into <head>; bare fragments are wrapped in a minimal document. Inline <script> and remote CDN <script src="..."> are allowed.'),
|
|
405
409
|
title: z.string().optional().describe('Node title shown in the canvas titlebar.'),
|
|
410
|
+
summary: z.string().optional().describe('Agent-readable semantic summary for this HTML node. If omitted, PMX derives one from visible HTML text.'),
|
|
411
|
+
agentSummary: z.string().optional().describe('Explicit agent-readable summary. Alias for summary with higher priority when both are provided.'),
|
|
412
|
+
description: z.string().optional().describe('Short description included in search and pinned/spatial context.'),
|
|
413
|
+
presentation: z.boolean().optional().describe('Marks this HTML surface as a fullscreen presentation/deck. Omit unless the user explicitly requested presentation mode.'),
|
|
414
|
+
slideTitles: z.array(z.string()).optional().describe('Agent-readable slide titles for presentation HTML.'),
|
|
415
|
+
embeddedNodeIds: z.array(z.string()).optional().describe('Canvas node IDs embedded or represented by this HTML surface.'),
|
|
416
|
+
embeddedUrls: z.array(z.string()).optional().describe('URLs embedded or represented by this HTML surface.'),
|
|
406
417
|
x: z.number().optional().describe('X position (auto-placed if omitted).'),
|
|
407
418
|
y: z.number().optional().describe('Y position (auto-placed if omitted).'),
|
|
408
419
|
width: z.number().optional().describe('Width in pixels (default: 720).'),
|
|
@@ -416,6 +427,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
416
427
|
const id = await c.addHtmlNode({
|
|
417
428
|
html: input.html,
|
|
418
429
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
430
|
+
...(typeof input.summary === 'string' ? { summary: input.summary } : {}),
|
|
431
|
+
...(typeof input.agentSummary === 'string' ? { agentSummary: input.agentSummary } : {}),
|
|
432
|
+
...(typeof input.description === 'string' ? { description: input.description } : {}),
|
|
433
|
+
...(input.presentation === true ? { presentation: true } : {}),
|
|
434
|
+
...(Array.isArray(input.slideTitles) ? { slideTitles: input.slideTitles } : {}),
|
|
435
|
+
...(Array.isArray(input.embeddedNodeIds) ? { embeddedNodeIds: input.embeddedNodeIds } : {}),
|
|
436
|
+
...(Array.isArray(input.embeddedUrls) ? { embeddedUrls: input.embeddedUrls } : {}),
|
|
419
437
|
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
420
438
|
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
421
439
|
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
@@ -428,6 +446,46 @@ export async function startMcpServer(): Promise<void> {
|
|
|
428
446
|
},
|
|
429
447
|
);
|
|
430
448
|
|
|
449
|
+
server.tool(
|
|
450
|
+
'canvas_add_html_primitive',
|
|
451
|
+
'Create a reusable HTML communication primitive as a normal sandboxed html node. Use this instead of long markdown for side-by-side choices, implementation plans, PR review sheets, module maps, design sheets, component galleries, flowcharts, explainers, status reports, and throwaway editors with export/copy paths. Use kind="presentation" only when the user explicitly asks for a PowerPoint-like deck, pitch, briefing, workshop walkthrough, or fullscreen story.',
|
|
452
|
+
{
|
|
453
|
+
kind: htmlPrimitiveKindSchema.describe('Primitive kind. Call canvas_describe_schema and read htmlPrimitives for data shapes and examples.'),
|
|
454
|
+
title: z.string().optional().describe('Node title shown in the canvas titlebar.'),
|
|
455
|
+
data: z.record(z.string(), z.unknown()).optional().describe('Primitive-specific data payload. For kind="presentation", data may include theme:"canvas"|"midnight"|"paper"|"aurora" or a custom color object. See canvas_describe_schema.htmlPrimitives for each shape.'),
|
|
456
|
+
x: z.number().optional().describe('X position (auto-placed if omitted).'),
|
|
457
|
+
y: z.number().optional().describe('Y position (auto-placed if omitted).'),
|
|
458
|
+
width: z.number().optional().describe('Width in pixels (defaults per primitive).'),
|
|
459
|
+
height: z.number().optional().describe('Height in pixels (defaults per primitive).'),
|
|
460
|
+
strictSize: z.boolean().optional().describe('Keep explicit width/height fixed; iframe scrolls overflow internally.'),
|
|
461
|
+
full: z.boolean().optional().describe('Return the full created node payload. Default false returns compact metadata.'),
|
|
462
|
+
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
463
|
+
},
|
|
464
|
+
async (input) => {
|
|
465
|
+
const c = await ensureCanvas();
|
|
466
|
+
const kind = input.kind as HtmlPrimitiveKind;
|
|
467
|
+
const result = await c.addHtmlPrimitive({
|
|
468
|
+
kind,
|
|
469
|
+
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
470
|
+
...(input.data ? { data: input.data } : {}),
|
|
471
|
+
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
472
|
+
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
473
|
+
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
474
|
+
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
475
|
+
...(input.strictSize === true ? { strictSize: true } : {}),
|
|
476
|
+
});
|
|
477
|
+
return {
|
|
478
|
+
content: [{
|
|
479
|
+
type: 'text',
|
|
480
|
+
text: JSON.stringify({
|
|
481
|
+
...(await createdNodePayload(c, result.id, input)),
|
|
482
|
+
primitive: { kind: result.kind, title: result.title, htmlBytes: result.htmlBytes },
|
|
483
|
+
}, null, 2),
|
|
484
|
+
}],
|
|
485
|
+
};
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
|
|
431
489
|
server.tool(
|
|
432
490
|
'canvas_open_mcp_app',
|
|
433
491
|
'Connect to an external MCP server that declares a ui:// app resource, call the specified tool, and open the resulting MCP App inside a canvas mcp-app node. This is a full external-MCP transport call, not the CLI kind shortcut; use canvas_add_diagram for the built-in Excalidraw preset.',
|
|
@@ -543,10 +601,13 @@ export async function startMcpServer(): Promise<void> {
|
|
|
543
601
|
|
|
544
602
|
server.tool(
|
|
545
603
|
'canvas_validate_spec',
|
|
546
|
-
'Validate a json-render spec or
|
|
604
|
+
'Validate a json-render spec, graph payload, or HTML primitive payload without creating a node. Returns normalized metadata the server would accept.',
|
|
547
605
|
{
|
|
548
|
-
type: z.enum(['json-render', 'graph']).describe('Structured payload type to validate'),
|
|
606
|
+
type: z.enum(['json-render', 'graph', 'html-primitive']).describe('Structured payload type to validate'),
|
|
549
607
|
spec: jsonRenderSpecSchema.optional().describe('json-render spec to validate when type="json-render"'),
|
|
608
|
+
kind: htmlPrimitiveKindSchema.optional().describe('HTML primitive kind when type="html-primitive"'),
|
|
609
|
+
primitive: htmlPrimitiveKindSchema.optional().describe('Alias for kind when type="html-primitive"'),
|
|
610
|
+
primitiveData: z.record(z.string(), z.unknown()).optional().describe('HTML primitive data payload when type="html-primitive"'),
|
|
550
611
|
title: z.string().optional().describe('Optional graph title'),
|
|
551
612
|
graphType: z.string().optional().describe('Graph type when type="graph"'),
|
|
552
613
|
data: z.array(z.record(z.string(), z.unknown())).optional().describe('Graph dataset when type="graph"'),
|
|
@@ -573,29 +634,38 @@ export async function startMcpServer(): Promise<void> {
|
|
|
573
634
|
type: 'json-render',
|
|
574
635
|
spec: input.spec,
|
|
575
636
|
})
|
|
576
|
-
:
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
637
|
+
: input.type === 'html-primitive'
|
|
638
|
+
? validateStructuredCanvasPayload({
|
|
639
|
+
type: 'html-primitive',
|
|
640
|
+
primitive: {
|
|
641
|
+
kind: (input.kind ?? input.primitive ?? '') as HtmlPrimitiveKind | '',
|
|
642
|
+
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
643
|
+
...(input.primitiveData ? { data: input.primitiveData } : {}),
|
|
644
|
+
},
|
|
645
|
+
})
|
|
646
|
+
: validateStructuredCanvasPayload({
|
|
647
|
+
type: 'graph',
|
|
648
|
+
graph: {
|
|
649
|
+
title: input.title,
|
|
650
|
+
graphType: input.graphType ?? 'line',
|
|
651
|
+
data: input.data ?? [],
|
|
652
|
+
...(typeof input.xKey === 'string' ? { xKey: input.xKey } : {}),
|
|
653
|
+
...(typeof input.yKey === 'string' ? { yKey: input.yKey } : {}),
|
|
654
|
+
...(typeof input.zKey === 'string' ? { zKey: input.zKey } : {}),
|
|
655
|
+
...(typeof input.nameKey === 'string' ? { nameKey: input.nameKey } : {}),
|
|
656
|
+
...(typeof input.valueKey === 'string' ? { valueKey: input.valueKey } : {}),
|
|
657
|
+
...(typeof input.axisKey === 'string' ? { axisKey: input.axisKey } : {}),
|
|
658
|
+
...(Array.isArray(input.metrics) ? { metrics: input.metrics } : {}),
|
|
659
|
+
...(Array.isArray(input.series) ? { series: input.series } : {}),
|
|
660
|
+
...(typeof input.barKey === 'string' ? { barKey: input.barKey } : {}),
|
|
661
|
+
...(typeof input.lineKey === 'string' ? { lineKey: input.lineKey } : {}),
|
|
662
|
+
...(typeof input.aggregate === 'string' ? { aggregate: input.aggregate } : {}),
|
|
663
|
+
...(typeof input.color === 'string' ? { color: input.color } : {}),
|
|
664
|
+
...(typeof input.barColor === 'string' ? { barColor: input.barColor } : {}),
|
|
665
|
+
...(typeof input.lineColor === 'string' ? { lineColor: input.lineColor } : {}),
|
|
666
|
+
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
667
|
+
},
|
|
668
|
+
});
|
|
599
669
|
|
|
600
670
|
return {
|
|
601
671
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -736,7 +806,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
736
806
|
content: [{
|
|
737
807
|
type: 'text',
|
|
738
808
|
text: JSON.stringify({
|
|
739
|
-
...await createdNodePayload(c, result.id),
|
|
809
|
+
...(await createdNodePayload(c, result.id)),
|
|
740
810
|
url: result.url,
|
|
741
811
|
spec: result.spec,
|
|
742
812
|
}, null, 2),
|
|
@@ -816,7 +886,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
816
886
|
content: [{
|
|
817
887
|
type: 'text',
|
|
818
888
|
text: JSON.stringify({
|
|
819
|
-
...await createdNodePayload(c, result.id),
|
|
889
|
+
...(await createdNodePayload(c, result.id)),
|
|
820
890
|
url: result.url,
|
|
821
891
|
spec: result.spec,
|
|
822
892
|
}, null, 2),
|
|
@@ -155,6 +155,17 @@ function summarizeWebArtifactData(data: Record<string, unknown>, maxLength: numb
|
|
|
155
155
|
return parts.length > 0 ? truncateContextText(parts.join('\n'), maxLength) : 'Web artifact node';
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
function summarizeHtmlPrimitiveData(data: Record<string, unknown>, maxLength: number): string {
|
|
159
|
+
const parts: string[] = [];
|
|
160
|
+
const primitive = typeof data.htmlPrimitive === 'string' ? data.htmlPrimitive : '';
|
|
161
|
+
const description = typeof data.description === 'string' ? data.description : '';
|
|
162
|
+
const primitiveData = data.primitiveData;
|
|
163
|
+
if (primitive) parts.push(`HTML primitive: ${primitive}`);
|
|
164
|
+
if (description) parts.push(description);
|
|
165
|
+
if (primitiveData !== undefined) parts.push(`Data: ${stringifyContextValue(primitiveData, maxLength)}`);
|
|
166
|
+
return truncateContextText(parts.join('\n'), maxLength);
|
|
167
|
+
}
|
|
168
|
+
|
|
158
169
|
function metadataForNode(node: CanvasNodeState): Record<string, unknown> | undefined {
|
|
159
170
|
switch (node.type) {
|
|
160
171
|
case 'webpage': {
|
|
@@ -181,6 +192,18 @@ function metadataForNode(node: CanvasNodeState): Record<string, unknown> | undef
|
|
|
181
192
|
}
|
|
182
193
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
183
194
|
}
|
|
195
|
+
case 'html': {
|
|
196
|
+
const metadata: Record<string, unknown> = {};
|
|
197
|
+
for (const key of ['summary', 'description', 'agentSummary', 'contentSummary', 'htmlPrimitive', 'presentation', 'slideCount', 'slideTitles', 'speakerNotes', 'embeddedNodeIds', 'embeddedUrls']) {
|
|
198
|
+
const value = node.data[key];
|
|
199
|
+
if (Array.isArray(value)) {
|
|
200
|
+
if (value.length > 0) metadata[key] = value;
|
|
201
|
+
} else if (value !== undefined && value !== null && value !== '') {
|
|
202
|
+
metadata[key] = value;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
206
|
+
}
|
|
184
207
|
case 'mcp-app': {
|
|
185
208
|
const metadata: Record<string, unknown> = {};
|
|
186
209
|
for (const key of ['url', 'path', 'mode', 'hostMode', 'viewerType', 'serverName', 'toolName', 'resourceUri', 'sessionStatus', 'projectPath', 'artifactBytes', 'sourceFiles', 'sourceFileCount', 'deps']) {
|
|
@@ -233,6 +256,22 @@ export function summarizeNodeForAgentContext(
|
|
|
233
256
|
if (graphCfg) return truncateContextText(`Graph: ${JSON.stringify(graphCfg)}`, defaultTextLength);
|
|
234
257
|
return stringifyContextValue(node.data.spec ?? {}, defaultTextLength);
|
|
235
258
|
}
|
|
259
|
+
case 'html': {
|
|
260
|
+
if (typeof node.data.agentSummary === 'string') {
|
|
261
|
+
return truncateContextText(node.data.agentSummary, defaultTextLength);
|
|
262
|
+
}
|
|
263
|
+
if (typeof node.data.htmlPrimitive === 'string') {
|
|
264
|
+
return summarizeHtmlPrimitiveData(node.data, defaultTextLength);
|
|
265
|
+
}
|
|
266
|
+
return stringifyContextValue({
|
|
267
|
+
title: node.data.title,
|
|
268
|
+
description: node.data.description,
|
|
269
|
+
summary: node.data.summary,
|
|
270
|
+
contentSummary: node.data.contentSummary,
|
|
271
|
+
embeddedNodeIds: node.data.embeddedNodeIds,
|
|
272
|
+
embeddedUrls: node.data.embeddedUrls,
|
|
273
|
+
}, defaultTextLength);
|
|
274
|
+
}
|
|
236
275
|
case 'prompt':
|
|
237
276
|
case 'response': {
|
|
238
277
|
const text = (node.data.text as string) || (node.data.content as string) || '';
|
|
@@ -20,6 +20,7 @@ import { mutationHistory } from './mutation-history.js';
|
|
|
20
20
|
import { computeGroupBounds, findOpenCanvasPosition } from './placement.js';
|
|
21
21
|
import { searchNodes } from './spatial-analysis.js';
|
|
22
22
|
import { getCanvasNodeTitle, serializeCanvasNode, type SerializedCanvasNode } from './canvas-serialization.js';
|
|
23
|
+
import { computeAutoArrange } from '../shared/auto-arrange.js';
|
|
23
24
|
import {
|
|
24
25
|
buildGraphSpec,
|
|
25
26
|
buildGraphConfig,
|
|
@@ -102,6 +103,8 @@ interface CanvasAddNodeInput {
|
|
|
102
103
|
strictSize?: boolean;
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
export const MARKDOWN_NODE_DEFAULT_SIZE = { width: 520, height: 360 };
|
|
107
|
+
|
|
105
108
|
interface CanvasCreateGroupInput {
|
|
106
109
|
title?: string;
|
|
107
110
|
childIds?: string[];
|
|
@@ -1070,62 +1073,105 @@ function collectArrangeExcludedNodeIds(nodes: CanvasNodeState[]): Set<string> {
|
|
|
1070
1073
|
return excluded;
|
|
1071
1074
|
}
|
|
1072
1075
|
|
|
1076
|
+
function collectGridArrangeExcludedNodeIds(nodes: CanvasNodeState[]): Set<string> {
|
|
1077
|
+
const nodesById = new Map(nodes.map((node) => [node.id, node]));
|
|
1078
|
+
const excluded = new Set<string>();
|
|
1079
|
+
|
|
1080
|
+
for (const node of nodes) {
|
|
1081
|
+
if (isArrangeLocked(node)) excluded.add(node.id);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
for (const node of nodes) {
|
|
1085
|
+
if (node.type !== 'group') continue;
|
|
1086
|
+
const childIds = Array.isArray(node.data.children)
|
|
1087
|
+
? node.data.children.filter((id): id is string => typeof id === 'string')
|
|
1088
|
+
: [];
|
|
1089
|
+
const hasLockedChild = childIds.some((childId) => {
|
|
1090
|
+
const child = nodesById.get(childId);
|
|
1091
|
+
return child ? isArrangeLocked(child) : false;
|
|
1092
|
+
});
|
|
1093
|
+
if (!excluded.has(node.id) && !hasLockedChild) continue;
|
|
1094
|
+
|
|
1095
|
+
excluded.add(node.id);
|
|
1096
|
+
for (const childId of childIds) excluded.add(childId);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
return excluded;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1073
1102
|
export function arrangeCanvasNodes(layout: CanvasArrangeMode): { arranged: number; layout: CanvasArrangeMode } {
|
|
1074
1103
|
const nodes = canvasState.getLayout().nodes;
|
|
1075
|
-
const excludedIds =
|
|
1104
|
+
const excludedIds = layout === 'grid'
|
|
1105
|
+
? collectGridArrangeExcludedNodeIds(nodes)
|
|
1106
|
+
: collectArrangeExcludedNodeIds(nodes);
|
|
1076
1107
|
const movableNodes = nodes.filter((node) => !excludedIds.has(node.id));
|
|
1077
|
-
const gap = 24;
|
|
1078
1108
|
const oldPositions = nodes.map((node) => ({ id: node.id, position: { ...node.position } }));
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1109
|
+
const oldSizes = nodes.map((node) => ({ id: node.id, size: { ...node.size } }));
|
|
1110
|
+
const newSizesById = new Map<string, CanvasNodeUpdate['size']>();
|
|
1111
|
+
const oldSizesById = new Map(oldSizes.map((entry) => [entry.id, entry.size]));
|
|
1112
|
+
const updates: CanvasNodeUpdate[] = [];
|
|
1113
|
+
|
|
1114
|
+
if (layout === 'column' || layout === 'flow') {
|
|
1115
|
+
const gap = 24;
|
|
1116
|
+
let x = 40;
|
|
1117
|
+
let y = 80;
|
|
1118
|
+
for (const node of movableNodes) {
|
|
1119
|
+
updates.push({ id: node.id, position: { x, y } });
|
|
1120
|
+
if (layout === 'column') {
|
|
1085
1121
|
y += node.size.height + gap;
|
|
1086
|
-
}
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
if (layout === 'flow') {
|
|
1091
|
-
let x = 40;
|
|
1092
|
-
for (const node of movableNodes) {
|
|
1093
|
-
canvasState.updateNode(node.id, { position: { x, y: 80 } });
|
|
1122
|
+
} else {
|
|
1094
1123
|
x += node.size.width + gap;
|
|
1095
1124
|
}
|
|
1096
|
-
return;
|
|
1097
1125
|
}
|
|
1098
|
-
|
|
1099
|
-
const
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
let rowY = 80;
|
|
1103
|
-
let rowMaxHeight = 0;
|
|
1104
|
-
for (const node of movableNodes) {
|
|
1105
|
-
const x = 40 + col * (maxNodeWidth + gap);
|
|
1106
|
-
canvasState.updateNode(node.id, { position: { x, y: rowY } });
|
|
1107
|
-
rowMaxHeight = Math.max(rowMaxHeight, node.size.height);
|
|
1108
|
-
col++;
|
|
1109
|
-
if (col >= cols) {
|
|
1110
|
-
col = 0;
|
|
1111
|
-
rowY += rowMaxHeight + gap;
|
|
1112
|
-
rowMaxHeight = 0;
|
|
1113
|
-
}
|
|
1126
|
+
} else {
|
|
1127
|
+
const result = computeAutoArrange(movableNodes, canvasState.getEdges(), 'grid');
|
|
1128
|
+
for (const [id, position] of result.nodePositions.entries()) {
|
|
1129
|
+
updates.push({ id, position });
|
|
1114
1130
|
}
|
|
1131
|
+
for (const [groupId, bounds] of result.groupBounds.entries()) {
|
|
1132
|
+
updates.push({
|
|
1133
|
+
id: groupId,
|
|
1134
|
+
position: { x: bounds.x, y: bounds.y },
|
|
1135
|
+
size: { width: bounds.width, height: bounds.height },
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
canvasState.withSuppressedRecording(() => {
|
|
1141
|
+
canvasState.applyUpdates(updates, layout === 'grid' ? { skipGroupChildTranslation: true } : {});
|
|
1115
1142
|
});
|
|
1116
1143
|
|
|
1117
1144
|
const newPositions = nodes.map((node) => {
|
|
1118
1145
|
const updated = canvasState.getNode(node.id);
|
|
1119
1146
|
return { id: node.id, position: updated ? { ...updated.position } : { ...node.position } };
|
|
1120
1147
|
});
|
|
1148
|
+
for (const node of nodes) {
|
|
1149
|
+
const updated = canvasState.getNode(node.id);
|
|
1150
|
+
const size = updated ? { ...updated.size } : { ...node.size };
|
|
1151
|
+
newSizesById.set(node.id, size);
|
|
1152
|
+
}
|
|
1121
1153
|
mutationHistory.record({
|
|
1122
1154
|
description: `Auto-arranged ${movableNodes.length} nodes (${layout})`,
|
|
1123
1155
|
operationType: 'arrange',
|
|
1124
1156
|
forward: () => canvasState.withSuppressedRecording(() => {
|
|
1125
|
-
|
|
1157
|
+
canvasState.applyUpdates(newPositions.map((position) => {
|
|
1158
|
+
const size = newSizesById.get(position.id);
|
|
1159
|
+
return {
|
|
1160
|
+
id: position.id,
|
|
1161
|
+
position: position.position,
|
|
1162
|
+
...(size ? { size } : {}),
|
|
1163
|
+
};
|
|
1164
|
+
}), layout === 'grid' ? { skipGroupChildTranslation: true } : {});
|
|
1126
1165
|
}),
|
|
1127
1166
|
inverse: () => canvasState.withSuppressedRecording(() => {
|
|
1128
|
-
|
|
1167
|
+
canvasState.applyUpdates(oldPositions.map((position) => {
|
|
1168
|
+
const size = oldSizesById.get(position.id);
|
|
1169
|
+
return {
|
|
1170
|
+
id: position.id,
|
|
1171
|
+
position: position.position,
|
|
1172
|
+
...(size ? { size } : {}),
|
|
1173
|
+
};
|
|
1174
|
+
}), layout === 'grid' ? { skipGroupChildTranslation: true } : {});
|
|
1129
1175
|
}),
|
|
1130
1176
|
});
|
|
1131
1177
|
|
|
@@ -1477,6 +1523,9 @@ export async function executeCanvasBatch(
|
|
|
1477
1523
|
switch (operation.op) {
|
|
1478
1524
|
case 'node.add': {
|
|
1479
1525
|
const type = typeof args.type === 'string' ? args.type : 'markdown';
|
|
1526
|
+
if (type === 'html-primitive') {
|
|
1527
|
+
throw new Error('Batch html-primitive creation is not supported yet. Use node.add with type "html" and generated html, or create the primitive through MCP/HTTP/CLI first.');
|
|
1528
|
+
}
|
|
1480
1529
|
if (type === 'webpage') {
|
|
1481
1530
|
const created = addCanvasNode({
|
|
1482
1531
|
type: 'webpage',
|
|
@@ -1502,18 +1551,30 @@ export async function executeCanvasBatch(
|
|
|
1502
1551
|
...(fetch.ok ? {} : { error: fetch.error }),
|
|
1503
1552
|
};
|
|
1504
1553
|
} else {
|
|
1554
|
+
const data = isPlainRecord(args.data) ? args.data : {};
|
|
1555
|
+
const htmlData = type === 'html'
|
|
1556
|
+
? {
|
|
1557
|
+
...data,
|
|
1558
|
+
...(typeof args.html === 'string' ? { html: args.html } : {}),
|
|
1559
|
+
...(typeof args.summary === 'string' ? { summary: args.summary } : {}),
|
|
1560
|
+
...(typeof args.agentSummary === 'string' ? { agentSummary: args.agentSummary } : {}),
|
|
1561
|
+
...(typeof args.description === 'string' ? { description: args.description } : {}),
|
|
1562
|
+
...(Array.isArray(args.embeddedNodeIds) ? { embeddedNodeIds: args.embeddedNodeIds } : {}),
|
|
1563
|
+
...(Array.isArray(args.embeddedUrls) ? { embeddedUrls: args.embeddedUrls } : {}),
|
|
1564
|
+
}
|
|
1565
|
+
: data;
|
|
1505
1566
|
const created = addCanvasNode({
|
|
1506
1567
|
type: type as CanvasNodeState['type'],
|
|
1507
1568
|
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1508
1569
|
...(typeof args.content === 'string' ? { content: args.content } : {}),
|
|
1509
|
-
...(
|
|
1570
|
+
...(Object.keys(htmlData).length > 0 ? { data: htmlData } : {}),
|
|
1510
1571
|
...(typeof args.x === 'number' ? { x: args.x } : {}),
|
|
1511
1572
|
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1512
1573
|
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1513
1574
|
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1514
1575
|
...(args.strictSize === true ? { strictSize: true } : {}),
|
|
1515
|
-
defaultWidth: 360,
|
|
1516
|
-
defaultHeight: 200,
|
|
1576
|
+
defaultWidth: type === 'html' ? 720 : type === 'markdown' ? MARKDOWN_NODE_DEFAULT_SIZE.width : 360,
|
|
1577
|
+
defaultHeight: type === 'html' ? 640 : type === 'markdown' ? MARKDOWN_NODE_DEFAULT_SIZE.height : 200,
|
|
1517
1578
|
fileMode: 'auto',
|
|
1518
1579
|
});
|
|
1519
1580
|
result = { ok: true, ...serializeCreatedNode(created.node) };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { normalizeHtmlNodeSemanticData } from './html-node-summary.js';
|
|
2
3
|
|
|
3
4
|
export type CanvasNodeType =
|
|
4
5
|
| 'markdown'
|
|
@@ -228,17 +229,18 @@ export function normalizeCanvasNodeData<T extends Record<string, unknown>>(
|
|
|
228
229
|
nodeType: CanvasNodeType,
|
|
229
230
|
data: T,
|
|
230
231
|
): T {
|
|
231
|
-
const
|
|
232
|
-
const
|
|
232
|
+
const semanticData = nodeType === 'html' ? normalizeHtmlNodeSemanticData(data) : data;
|
|
233
|
+
const existing = normalizeExistingProvenance(semanticData.provenance);
|
|
234
|
+
const inferred = inferCanvasNodeProvenance(nodeType, semanticData);
|
|
233
235
|
const provenance = mergeProvenance(existing, inferred);
|
|
234
236
|
|
|
235
237
|
if (provenance) {
|
|
236
|
-
return { ...
|
|
238
|
+
return { ...semanticData, provenance } as T;
|
|
237
239
|
}
|
|
238
|
-
if ('provenance' in
|
|
239
|
-
const nextData = { ...
|
|
240
|
+
if ('provenance' in semanticData) {
|
|
241
|
+
const nextData = { ...semanticData };
|
|
240
242
|
delete nextData.provenance;
|
|
241
243
|
return nextData as T;
|
|
242
244
|
}
|
|
243
|
-
return
|
|
245
|
+
return semanticData;
|
|
244
246
|
}
|
|
@@ -8,6 +8,12 @@ import {
|
|
|
8
8
|
type GraphNodeInput,
|
|
9
9
|
type JsonRenderSpec,
|
|
10
10
|
} from '../json-render/server.js';
|
|
11
|
+
import {
|
|
12
|
+
buildHtmlPrimitive,
|
|
13
|
+
isHtmlPrimitiveKind,
|
|
14
|
+
listHtmlPrimitiveDescriptors,
|
|
15
|
+
type HtmlPrimitiveDescriptor,
|
|
16
|
+
} from './html-primitives.js';
|
|
11
17
|
|
|
12
18
|
export interface CanvasCreateField {
|
|
13
19
|
name: string;
|
|
@@ -30,8 +36,14 @@ export interface CanvasCreateTypeSchema {
|
|
|
30
36
|
|
|
31
37
|
export interface StructuredValidationResult {
|
|
32
38
|
ok: true;
|
|
33
|
-
type: 'json-render' | 'graph';
|
|
34
|
-
normalizedSpec
|
|
39
|
+
type: 'json-render' | 'graph' | 'html-primitive';
|
|
40
|
+
normalizedSpec?: JsonRenderSpec;
|
|
41
|
+
normalizedPrimitive?: {
|
|
42
|
+
kind: string;
|
|
43
|
+
title: string;
|
|
44
|
+
htmlBytes: number;
|
|
45
|
+
defaultSize: { width: number; height: number };
|
|
46
|
+
};
|
|
35
47
|
summary: Record<string, unknown>;
|
|
36
48
|
}
|
|
37
49
|
|
|
@@ -231,6 +243,14 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
231
243
|
mcpTool: 'canvas_add_html_node',
|
|
232
244
|
fields: [
|
|
233
245
|
{ name: 'html', type: 'string', required: false, description: 'HTML document or fragment rendered in the sandboxed iframe.', aliases: ['content', 'stdin'] },
|
|
246
|
+
{ name: 'summary', type: 'string', required: false, description: 'Explicit agent-readable summary. If omitted, PMX derives one from visible HTML text.' },
|
|
247
|
+
{ name: 'agentSummary', type: 'string', required: false, description: 'Explicit semantic sidecar used by search, pinned context, and spatial context.' },
|
|
248
|
+
{ name: 'embeddedNodeIds', type: 'string[]', required: false, description: 'Canvas node IDs represented or iframe-embedded by this HTML surface.' },
|
|
249
|
+
{ name: 'embeddedUrls', type: 'string[]', required: false, description: 'URLs represented or iframe-embedded by this HTML surface.' },
|
|
250
|
+
{ name: 'presentation', type: 'boolean', required: false, description: 'Marks this HTML surface as a fullscreen presentation/deck.' },
|
|
251
|
+
{ name: 'slideTitles', type: 'string[]', required: false, description: 'Agent-readable slide titles for presentation HTML.' },
|
|
252
|
+
{ name: 'primitive', type: 'HtmlPrimitiveKind', required: false, description: 'Generate HTML from a built-in communication primitive instead of passing raw HTML.', aliases: ['kind'] },
|
|
253
|
+
{ name: 'data', type: 'record<string, unknown>', required: false, description: 'Primitive data when --primitive is used, or arbitrary node metadata.' },
|
|
234
254
|
{ name: 'title', type: 'string', required: false, description: 'Optional node title.' },
|
|
235
255
|
{ name: 'x', type: 'number', required: false, description: 'Optional X position.' },
|
|
236
256
|
{ name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
|
|
@@ -245,9 +265,46 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
245
265
|
},
|
|
246
266
|
notes: [
|
|
247
267
|
'The CLI accepts --content as an alias and stores it as data.html so the renderer can load it.',
|
|
268
|
+
'Normal html nodes are the default. Presentation mode is opt-in via presentation:true or the presentation primitive.',
|
|
269
|
+
'HTML nodes persist data.contentSummary and data.agentSummary so agents can understand rich visual HTML without parsing the full iframe payload.',
|
|
270
|
+
'Only presentation-marked HTML nodes expose a browser Present button for fullscreen review; use the presentation primitive for PowerPoint-like decks.',
|
|
271
|
+
'Use `primitive` / `kind` with `data` to create reusable agent communication artifacts such as choice grids, plans, review sheets, explainers, and editors.',
|
|
248
272
|
'HTML runs in a sandboxed iframe without same-origin access to the canvas host.',
|
|
249
273
|
],
|
|
250
274
|
},
|
|
275
|
+
{
|
|
276
|
+
type: 'html-primitive',
|
|
277
|
+
kind: 'virtual-node',
|
|
278
|
+
description: 'Reusable sandboxed HTML communication primitive rendered as an html node.',
|
|
279
|
+
endpoint: '/api/canvas/node',
|
|
280
|
+
mcpTool: 'canvas_add_html_primitive',
|
|
281
|
+
fields: [
|
|
282
|
+
{ name: 'kind', type: 'HtmlPrimitiveKind', required: true, description: 'Primitive kind. See top-level htmlPrimitives for the supported catalog.' },
|
|
283
|
+
{ name: 'data', type: 'record<string, unknown>', required: false, description: 'Primitive-specific JSON object payload.' },
|
|
284
|
+
{ name: 'title', type: 'string', required: false, description: 'Optional node title.' },
|
|
285
|
+
{ name: 'x', type: 'number', required: false, description: 'Optional X position.' },
|
|
286
|
+
{ name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
|
|
287
|
+
{ name: 'width', type: 'number', required: false, description: 'Optional node width; defaults per primitive.' },
|
|
288
|
+
{ name: 'height', type: 'number', required: false, description: 'Optional node height; defaults per primitive.' },
|
|
289
|
+
{ 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'] },
|
|
290
|
+
],
|
|
291
|
+
example: {
|
|
292
|
+
type: 'html-primitive',
|
|
293
|
+
kind: 'choice-grid',
|
|
294
|
+
title: 'Implementation Options',
|
|
295
|
+
data: {
|
|
296
|
+
items: [
|
|
297
|
+
{ title: 'Small patch', summary: 'Least disruption.', pros: ['Fast'], cons: ['Limited flexibility'] },
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
notes: [
|
|
302
|
+
'HTTP callers may POST { type: "html-primitive", kind, data } or { type: "html", primitive: kind, data }; both create a normal html node with primitive metadata.',
|
|
303
|
+
'Use kind "presentation" only when a PowerPoint-like deck is requested; created nodes persist presentation, slideCount, slideTitles, and optional presentationTheme metadata for agents.',
|
|
304
|
+
'Presentation primitive data supports theme: "canvas" | "midnight" | "paper" | "aurora" or a custom color object with bg, panel, surface, border, text, textSecondary, textMuted, accent, and colorScheme.',
|
|
305
|
+
'Interactive editor primitives include copy/export controls so the human can send edited state back to the agent.',
|
|
306
|
+
],
|
|
307
|
+
},
|
|
251
308
|
{
|
|
252
309
|
type: 'mcp-app',
|
|
253
310
|
kind: 'node',
|
|
@@ -449,6 +506,7 @@ export function describeCanvasSchema(): {
|
|
|
449
506
|
graph: {
|
|
450
507
|
graphTypes: CanvasGraphType[];
|
|
451
508
|
};
|
|
509
|
+
htmlPrimitives: HtmlPrimitiveDescriptor[];
|
|
452
510
|
mcp: {
|
|
453
511
|
tools: string[];
|
|
454
512
|
resources: string[];
|
|
@@ -472,9 +530,12 @@ export function describeCanvasSchema(): {
|
|
|
472
530
|
graph: {
|
|
473
531
|
graphTypes: [...CANONICAL_GRAPH_TYPES],
|
|
474
532
|
},
|
|
533
|
+
htmlPrimitives: listHtmlPrimitiveDescriptors(),
|
|
475
534
|
mcp: {
|
|
476
535
|
tools: [
|
|
477
536
|
'canvas_add_node',
|
|
537
|
+
'canvas_add_html_node',
|
|
538
|
+
'canvas_add_html_primitive',
|
|
478
539
|
'canvas_add_json_render_node',
|
|
479
540
|
'canvas_add_graph_node',
|
|
480
541
|
'canvas_build_web_artifact',
|
|
@@ -492,9 +553,10 @@ export function describeCanvasSchema(): {
|
|
|
492
553
|
}
|
|
493
554
|
|
|
494
555
|
export function validateStructuredCanvasPayload(input: {
|
|
495
|
-
type: 'json-render' | 'graph';
|
|
556
|
+
type: 'json-render' | 'graph' | 'html-primitive';
|
|
496
557
|
spec?: unknown;
|
|
497
558
|
graph?: GraphNodeInput;
|
|
559
|
+
primitive?: { kind: string; title?: string; data?: Record<string, unknown> };
|
|
498
560
|
}): StructuredValidationResult {
|
|
499
561
|
if (input.type === 'json-render') {
|
|
500
562
|
const normalizedSpec = normalizeAndValidateJsonRenderSpec(input.spec);
|
|
@@ -510,6 +572,35 @@ export function validateStructuredCanvasPayload(input: {
|
|
|
510
572
|
};
|
|
511
573
|
}
|
|
512
574
|
|
|
575
|
+
if (input.type === 'html-primitive') {
|
|
576
|
+
if (!input.primitive) {
|
|
577
|
+
throw new Error('HTML primitive validation requires a primitive payload.');
|
|
578
|
+
}
|
|
579
|
+
if (!isHtmlPrimitiveKind(input.primitive.kind)) {
|
|
580
|
+
throw new Error(`Unknown HTML primitive: ${input.primitive.kind}`);
|
|
581
|
+
}
|
|
582
|
+
const built = buildHtmlPrimitive({
|
|
583
|
+
kind: input.primitive.kind,
|
|
584
|
+
...(typeof input.primitive.title === 'string' ? { title: input.primitive.title } : {}),
|
|
585
|
+
...(input.primitive.data ? { data: input.primitive.data } : {}),
|
|
586
|
+
});
|
|
587
|
+
return {
|
|
588
|
+
ok: true,
|
|
589
|
+
type: 'html-primitive',
|
|
590
|
+
normalizedPrimitive: {
|
|
591
|
+
kind: built.kind,
|
|
592
|
+
title: built.title,
|
|
593
|
+
htmlBytes: Buffer.byteLength(built.html, 'utf-8'),
|
|
594
|
+
defaultSize: built.defaultSize,
|
|
595
|
+
},
|
|
596
|
+
summary: {
|
|
597
|
+
kind: built.kind,
|
|
598
|
+
title: built.title,
|
|
599
|
+
dataKeys: Object.keys(built.data),
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
513
604
|
if (!input.graph) {
|
|
514
605
|
throw new Error('Graph validation requires a graph payload.');
|
|
515
606
|
}
|