pmx-canvas 0.1.20 → 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 +85 -0
- package/dist/canvas/global.css +88 -0
- package/dist/canvas/index.js +87 -53
- package/dist/types/client/nodes/HtmlNode.d.ts +12 -1
- package/dist/types/client/types.d.ts +1 -1
- package/dist/types/server/html-node-summary.d.ts +2 -0
- package/dist/types/server/html-primitives.d.ts +9 -1
- package/dist/types/server/index.d.ts +8 -1
- package/docs/http-api.md +1 -1
- package/docs/mcp.md +4 -0
- package/docs/node-types.md +27 -5
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +1 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +10 -4
- package/skills/pmx-canvas/references/html-primitives.md +132 -0
- package/src/cli/agent.ts +9 -0
- package/src/cli/index.ts +1 -1
- package/src/client/App.tsx +1 -1
- package/src/client/canvas/CommandPalette.tsx +1 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +105 -2
- package/src/client/canvas/auto-fit.ts +5 -1
- package/src/client/nodes/HtmlNode.tsx +125 -13
- package/src/client/state/sse-bridge.ts +1 -1
- package/src/client/theme/global.css +88 -0
- package/src/mcp/canvas-access.ts +31 -1
- package/src/mcp/server.ts +17 -3
- package/src/server/agent-context.ts +23 -1
- package/src/server/canvas-operations.ts +10 -2
- package/src/server/canvas-provenance.ts +8 -6
- package/src/server/canvas-schema.ts +11 -0
- package/src/server/canvas-serialization.ts +10 -5
- package/src/server/html-node-summary.ts +141 -0
- package/src/server/html-primitives.ts +318 -8
- package/src/server/index.ts +22 -3
- package/src/server/server.ts +17 -4
- package/src/server/spatial-analysis.ts +4 -2
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { CanvasNodeState } from '../types';
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function createHtmlNodeSrcDocForTest(userHtml: string, options: {
|
|
3
|
+
theme: string;
|
|
4
|
+
themeCss: string;
|
|
5
|
+
themeToken?: string;
|
|
6
|
+
presentation?: boolean;
|
|
7
|
+
presentationExitToken?: string;
|
|
8
|
+
}): string;
|
|
9
|
+
export declare function shouldShowPresentationControls(node: CanvasNodeState): boolean;
|
|
10
|
+
export declare function HtmlNode({ node, expanded, presentation, presentationExitToken, autoFocus, }: {
|
|
3
11
|
node: CanvasNodeState;
|
|
4
12
|
expanded?: boolean;
|
|
13
|
+
presentation?: boolean;
|
|
14
|
+
presentationExitToken?: string;
|
|
15
|
+
autoFocus?: boolean;
|
|
5
16
|
}): import("preact/src").JSX.Element;
|
|
@@ -52,7 +52,7 @@ export interface CanvasAnnotation {
|
|
|
52
52
|
export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected';
|
|
53
53
|
export declare const TYPE_LABELS: Record<CanvasNodeState['type'], string>;
|
|
54
54
|
/** Node types that support the full-viewport expand/focus overlay. */
|
|
55
|
-
export declare const EXPANDABLE_TYPES: Set<"markdown" | "mcp-app" | "webpage" | "json-render" | "graph" | "prompt" | "response" | "status" | "context" | "ledger" | "trace" | "file" | "image" | "
|
|
55
|
+
export declare const EXPANDABLE_TYPES: Set<"html" | "markdown" | "mcp-app" | "webpage" | "json-render" | "graph" | "prompt" | "response" | "status" | "context" | "ledger" | "trace" | "file" | "image" | "group">;
|
|
56
56
|
export declare const EXCALIDRAW_SERVER_NAME = "Excalidraw";
|
|
57
57
|
export declare const EXCALIDRAW_CREATE_VIEW_TOOL = "create_view";
|
|
58
58
|
export declare function isExcalidrawNode(node: CanvasNodeState): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const HTML_PRIMITIVE_KINDS: readonly ["choice-grid", "plan-timeline", "review-sheet", "pr-writeup", "system-map", "code-walkthrough", "design-sheet", "component-gallery", "interaction-prototype", "flowchart", "deck", "illustration-set", "explainer", "status-report", "incident-report", "triage-board", "config-editor", "prompt-tuner"];
|
|
1
|
+
export declare const HTML_PRIMITIVE_KINDS: readonly ["choice-grid", "plan-timeline", "review-sheet", "pr-writeup", "system-map", "code-walkthrough", "design-sheet", "component-gallery", "interaction-prototype", "flowchart", "deck", "presentation", "illustration-set", "explainer", "status-report", "incident-report", "triage-board", "config-editor", "prompt-tuner"];
|
|
2
2
|
export type HtmlPrimitiveKind = typeof HTML_PRIMITIVE_KINDS[number];
|
|
3
3
|
export interface HtmlPrimitiveDescriptor {
|
|
4
4
|
kind: HtmlPrimitiveKind;
|
|
@@ -28,7 +28,15 @@ export interface HtmlPrimitiveBuildResult {
|
|
|
28
28
|
};
|
|
29
29
|
data: Record<string, unknown>;
|
|
30
30
|
}
|
|
31
|
+
export interface HtmlPrimitiveSemanticMetadata {
|
|
32
|
+
presentation?: true;
|
|
33
|
+
slideCount?: number;
|
|
34
|
+
slideTitles?: string[];
|
|
35
|
+
speakerNotes?: string[];
|
|
36
|
+
presentationTheme?: string | Record<string, string>;
|
|
37
|
+
}
|
|
31
38
|
export declare function isHtmlPrimitiveKind(value: string): value is HtmlPrimitiveKind;
|
|
32
39
|
export declare function getHtmlPrimitiveDescriptor(kind: HtmlPrimitiveKind): HtmlPrimitiveDescriptor;
|
|
33
40
|
export declare function listHtmlPrimitiveDescriptors(): HtmlPrimitiveDescriptor[];
|
|
41
|
+
export declare function getHtmlPrimitiveSemanticMetadata(data: Record<string, unknown>): HtmlPrimitiveSemanticMetadata;
|
|
34
42
|
export declare function buildHtmlPrimitive(input: HtmlPrimitiveInput): HtmlPrimitiveBuildResult;
|
|
@@ -244,6 +244,13 @@ export declare class PmxCanvas extends EventEmitter {
|
|
|
244
244
|
addHtmlNode(input: {
|
|
245
245
|
html: string;
|
|
246
246
|
title?: string;
|
|
247
|
+
summary?: string;
|
|
248
|
+
agentSummary?: string;
|
|
249
|
+
description?: string;
|
|
250
|
+
presentation?: boolean;
|
|
251
|
+
slideTitles?: string[];
|
|
252
|
+
embeddedNodeIds?: string[];
|
|
253
|
+
embeddedUrls?: string[];
|
|
247
254
|
x?: number;
|
|
248
255
|
y?: number;
|
|
249
256
|
width?: number;
|
|
@@ -292,7 +299,7 @@ export type { SpatialCluster, SpatialContext, SpatialNeighbor, NodeSpatialInfo }
|
|
|
292
299
|
export { mutationHistory, diffLayouts, formatDiff } from './mutation-history.js';
|
|
293
300
|
export { recomputeCodeGraph, buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
294
301
|
export { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
295
|
-
export { buildHtmlPrimitive, isHtmlPrimitiveKind, listHtmlPrimitiveDescriptors } from './html-primitives.js';
|
|
302
|
+
export { buildHtmlPrimitive, getHtmlPrimitiveSemanticMetadata, isHtmlPrimitiveKind, listHtmlPrimitiveDescriptors } from './html-primitives.js';
|
|
296
303
|
export { buildWebArtifactOnCanvas, executeWebArtifactBuild, openWebArtifactInCanvas, resolveWebArtifactScriptPath, resolveWorkspacePath, } from './web-artifacts.js';
|
|
297
304
|
export { buildGraphSpec, buildJsonRenderViewerHtml, createJsonRenderNodeData, GRAPH_NODE_SIZE, JSON_RENDER_NODE_SIZE, normalizeAndValidateJsonRenderSpec, } from '../json-render/server.js';
|
|
298
305
|
export type { CodeGraphSummary, CodeGraphEdge } from './code-graph.js';
|
package/docs/http-api.md
CHANGED
|
@@ -40,7 +40,7 @@ curl -X POST http://localhost:4313/api/canvas/node \
|
|
|
40
40
|
# Add an html node (sandboxed iframe)
|
|
41
41
|
curl -X POST http://localhost:4313/api/canvas/node \
|
|
42
42
|
-H "Content-Type: application/json" \
|
|
43
|
-
-d '{"type":"html","title":"Chart","html":"<canvas id=\"c\"></canvas><script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script><script>/* ... */</script>"}'
|
|
43
|
+
-d '{"type":"html","title":"Chart","summary":"Cost projection chart for the Q2 plan.","html":"<canvas id=\"c\"></canvas><script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script><script>/* ... */</script>"}'
|
|
44
44
|
|
|
45
45
|
# Add a generated HTML primitive as a sandboxed html node
|
|
46
46
|
curl -X POST http://localhost:4313/api/canvas/node \
|
package/docs/mcp.md
CHANGED
|
@@ -37,6 +37,10 @@ The canvas auto-starts on first tool call.
|
|
|
37
37
|
| `canvas_add_json_render_node` | Create a native json-render node from a validated spec |
|
|
38
38
|
| `canvas_add_graph_node` | Create a native graph node (line, bar, pie, area, scatter, radar, stacked-bar, composed) |
|
|
39
39
|
| `canvas_build_web_artifact` | Build a bundled HTML artifact and open it on the canvas |
|
|
40
|
+
|
|
41
|
+
`canvas_add_html_node` accepts optional `summary`, `agentSummary`, `embeddedNodeIds`, and
|
|
42
|
+
`embeddedUrls`. PMX also derives a bounded text summary from visible HTML, so rich HTML nodes stay
|
|
43
|
+
searchable and readable in pinned/spatial context.
|
|
40
44
|
| `canvas_update_node` | Update content, position, size, collapsed state |
|
|
41
45
|
| `canvas_remove_node` | Remove a node and its edges |
|
|
42
46
|
| `canvas_get_layout` | Get full canvas state |
|
package/docs/node-types.md
CHANGED
|
@@ -143,7 +143,7 @@ component catalog before building a spec.
|
|
|
143
143
|
|
|
144
144
|
## HTML nodes
|
|
145
145
|
|
|
146
|
-
`html` nodes render a self-contained HTML/JS document in a sandboxed iframe.
|
|
146
|
+
`html` nodes render a normal self-contained HTML/JS document in a sandboxed iframe.
|
|
147
147
|
They sit between `json-render` (no custom JS) and `web-artifact` (full bundled
|
|
148
148
|
React app) — perfect for Chart.js, D3, custom widgets, and any HTML you can
|
|
149
149
|
write or paste.
|
|
@@ -152,7 +152,8 @@ The sandbox runs with `allow-scripts` only — no same-origin access, no
|
|
|
152
152
|
top-level navigation, no form submission. Inline `<script>` and CDN
|
|
153
153
|
`<script src>` both work. The canvas auto-injects its theme tokens
|
|
154
154
|
(`--c-*` and `--color-*` aliases) into the iframe `<head>` so artifacts can
|
|
155
|
-
match the active theme.
|
|
155
|
+
match the active theme. Theme updates are posted into sandboxed HTML iframes,
|
|
156
|
+
so theme-aware HTML can follow dark/light switches without reopening the node.
|
|
156
157
|
|
|
157
158
|
```ts
|
|
158
159
|
canvas_add_html_node({
|
|
@@ -164,6 +165,17 @@ canvas_add_html_node({
|
|
|
164
165
|
A fragment without `<html>`/`<head>` is wrapped in a full document
|
|
165
166
|
automatically. Default size is 720×640.
|
|
166
167
|
|
|
168
|
+
Presentation mode is opt-in. Raw HTML nodes do not show the browser `Present`
|
|
169
|
+
button unless callers set `presentation: true`; prefer the `presentation`
|
|
170
|
+
primitive when the user explicitly asks for a PowerPoint-like deck, pitch,
|
|
171
|
+
briefing, workshop walkthrough, or fullscreen story.
|
|
172
|
+
|
|
173
|
+
HTML nodes also store an agent-readable semantic sidecar. Callers can pass
|
|
174
|
+
`summary`, `agentSummary`, `embeddedNodeIds`, or `embeddedUrls`; when no summary
|
|
175
|
+
is provided, PMX derives `data.contentSummary` from visible HTML text and stores
|
|
176
|
+
`data.agentSummary` for search, pinned context, and spatial context. Scripts and
|
|
177
|
+
styles are ignored during extraction.
|
|
178
|
+
|
|
167
179
|
### HTML primitives
|
|
168
180
|
|
|
169
181
|
`html-primitive` is a virtual schema type that creates a normal sandboxed
|
|
@@ -171,8 +183,8 @@ automatically. Default size is 720×640.
|
|
|
171
183
|
answer would be easier to review as an option grid, implementation timeline,
|
|
172
184
|
review sheet, PR writeup, code walkthrough, system map, design sheet,
|
|
173
185
|
component gallery, interaction prototype, flowchart, SVG illustration set,
|
|
174
|
-
explainer, status report, incident report, triage board, config
|
|
175
|
-
prompt tuner.
|
|
186
|
+
presentation, explainer, status report, incident report, triage board, config
|
|
187
|
+
editor, or prompt tuner.
|
|
176
188
|
|
|
177
189
|
```ts
|
|
178
190
|
canvas_add_html_primitive({
|
|
@@ -189,7 +201,17 @@ canvas_add_html_primitive({
|
|
|
189
201
|
HTTP callers may post either `{ "type": "html-primitive", "kind": "choice-grid", "data": ... }`
|
|
190
202
|
or `{ "type": "html", "primitive": "choice-grid", "data": ... }`. The stored
|
|
191
203
|
node remains `type: "html"` with `data.htmlPrimitive`, `data.primitiveData`, and
|
|
192
|
-
the generated `data.html` payload.
|
|
204
|
+
the generated `data.html` payload. Generated primitives also get an
|
|
205
|
+
agent-readable summary sidecar.
|
|
206
|
+
|
|
207
|
+
Only presentation-marked HTML nodes expose a `Present` button in the browser.
|
|
208
|
+
Use the `presentation` primitive for PowerPoint-like decks; it persists
|
|
209
|
+
`presentation`, `slideCount`, `slideTitles`, optional `speakerNotes`, and
|
|
210
|
+
optional `presentationTheme` metadata while the iframe handles
|
|
211
|
+
Arrow/Space/Page Up/Page Down slide navigation. Presentation data supports
|
|
212
|
+
`theme: "canvas" | "midnight" | "paper" | "aurora"` or a custom color object with
|
|
213
|
+
`bg`, `panel`, `surface`, `border`, `text`, `textSecondary`, `textMuted`,
|
|
214
|
+
`accent`, and `colorScheme`.
|
|
193
215
|
|
|
194
216
|
## Web artifacts
|
|
195
217
|
|
package/docs/screenshot.png
CHANGED
|
Binary file
|
package/docs/sdk.md
CHANGED
|
@@ -29,6 +29,7 @@ canvas.createGroup({ title: 'Build Pipeline', childIds: [n1, n2] });
|
|
|
29
29
|
// Self-contained HTML in a sandboxed iframe
|
|
30
30
|
canvas.addHtmlNode({
|
|
31
31
|
title: 'Cost projection',
|
|
32
|
+
summary: 'Cost projection chart for the Q2 plan.',
|
|
32
33
|
html: '<canvas id="c"></canvas><script src="https://cdn.jsdelivr.net/npm/chart.js"></script><script>/* ... */</script>',
|
|
33
34
|
});
|
|
34
35
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
|
@@ -50,6 +50,7 @@ tree:
|
|
|
50
50
|
2. Decide which canvas node type best matches each output:
|
|
51
51
|
- Long-form / narrative results → `markdown`
|
|
52
52
|
- Structured records, tables, dashboards → `json-render` or `graph`
|
|
53
|
+
- Rich reusable communication artifacts → `html-primitive`
|
|
53
54
|
- Interactive tool surfaces with their own UI → `mcp-app` (open with
|
|
54
55
|
`canvas_open_mcp_app`)
|
|
55
56
|
- Local source files → `file` (live-watched)
|
|
@@ -653,18 +654,23 @@ server's `ui://` resource as an iframe node on the canvas
|
|
|
653
654
|
|
|
654
655
|
### HTML Nodes (Sandboxed iframe)
|
|
655
656
|
|
|
656
|
-
**`canvas_add_html_node`** — Add a self-contained HTML document rendered in a sandboxed iframe
|
|
657
|
+
**`canvas_add_html_node`** — Add a normal self-contained HTML document rendered in a sandboxed iframe
|
|
657
658
|
- Required: `html` (full document or fragment; inline `<script>` and CDN `<script src="...">` are allowed)
|
|
658
|
-
- Optional: `title`, `x`, `y`, `width` (default 720), `height` (default 640), `strictSize`
|
|
659
|
+
- Optional: `title`, `summary`, `agentSummary`, `presentation`, `slideTitles`, `embeddedNodeIds`, `embeddedUrls`, `x`, `y`, `width` (default 720), `height` (default 640), `strictSize`
|
|
659
660
|
- Iframe sandbox is `allow-scripts` only — no same-origin access, no top-navigation, no forms
|
|
660
|
-
- Canvas theme tokens are auto-injected as CSS custom properties (both `--c-*` and common `--color-*` aliases such as `--color-text-primary`, `--color-bg`, `--color-accent`)
|
|
661
|
+
- Canvas theme tokens are auto-injected as CSS custom properties (both `--c-*` and common `--color-*` aliases such as `--color-text-primary`, `--color-bg`, `--color-accent`) and updated live when the canvas theme changes
|
|
661
662
|
- Use for moderate-complexity visualizations and interactive widgets that need real JS but do not warrant a full React build (Chart.js demos, D3 sketches, custom HTML report views)
|
|
663
|
+
- Normal HTML is the default. Only pass `presentation: true` when the user explicitly asks for a deck/fullscreen presentation; otherwise do not mark raw HTML as presentable.
|
|
664
|
+
- Only presentation-marked HTML nodes expose a browser `Present` button. Use it when the HTML is a deck, briefing, or fullscreen review surface; the PMX shell owns the fullscreen overlay and exits via `Esc` or `Exit presentation`.
|
|
665
|
+
- PMX stores a semantic sidecar (`agentSummary`, `contentSummary`, embedded references) so HTML nodes remain understandable in search, pinned context, and spatial context
|
|
662
666
|
|
|
663
667
|
**`canvas_add_html_primitive`** — Generate a reusable HTML communication primitive as a sandboxed `html` node
|
|
664
668
|
- Required: `kind`; run `canvas_describe_schema` and read `htmlPrimitives` for the current catalog
|
|
665
669
|
- Optional: `title`, `data`, `x`, `y`, `width`, `height`, `strictSize`
|
|
666
670
|
- Use when markdown would be too dense and a structured visual artifact is clearer: tradeoff grids, implementation plans, PR reviews, module maps, design sheets, explainers, reports, and lightweight human-editable boards/editors
|
|
671
|
+
- When the human asks for a PowerPoint-like output, pitch deck, briefing, or presentation, use `kind: "presentation"` unless a bespoke raw HTML deck is required. Include `slides` with short titles, one idea per slide, optional `metrics`, `note` fields for speaker notes, and optional `theme: "canvas" | "midnight" | "paper" | "aurora"` or a custom theme object.
|
|
667
672
|
- Read `htmlPrimitives` from `canvas_describe_schema` for the data shape and examples before constructing a payload
|
|
673
|
+
- For payload patterns, export loops, and the primitive catalog, read `references/html-primitives.md` before creating dense or editable artifacts
|
|
668
674
|
|
|
669
675
|
### Choosing the Right Visual Tier
|
|
670
676
|
|
|
@@ -673,7 +679,7 @@ When the output is more than markdown, pick the lightest tier that fits:
|
|
|
673
679
|
| Tier | Tool | Build cost | When to pick it |
|
|
674
680
|
|------|------|------------|-----------------|
|
|
675
681
|
| Declarative UI | `canvas_add_json_render_node` / `canvas_add_graph_node` | None | Schema-driven dashboards, forms, charts; agent-friendly to read back via `canvas_get_node` |
|
|
676
|
-
| Generated HTML primitive | `canvas_add_html_primitive` | None | Reusable communication artifacts such as choices, plans, reviews, maps, reports, decks, and lightweight editors |
|
|
682
|
+
| Generated HTML primitive | `canvas_add_html_primitive` | None | Reusable communication artifacts such as choices, plans, reviews, maps, reports, presentations/decks, and lightweight editors |
|
|
677
683
|
| Sandboxed HTML+JS | `canvas_add_html_node` | None | Self-contained HTML with inline JS or CDN scripts; one-off visualizations or report views |
|
|
678
684
|
| Hosted MCP app | `canvas_open_mcp_app` / `canvas_add_diagram` | None | Interactive editors backed by an external MCP server (e.g. Excalidraw) |
|
|
679
685
|
| Bundled React app | `canvas_build_web_artifact` | Heavy (npm install + bundle) | Multi-component UIs needing React state, routing, shadcn/ui, or Tailwind class composition |
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# HTML Primitive Authoring
|
|
2
|
+
|
|
3
|
+
Use this guide when creating reusable sandboxed HTML communication primitives through
|
|
4
|
+
`canvas_add_html_primitive` or `pmx-canvas html primitive add`.
|
|
5
|
+
|
|
6
|
+
## What They Are
|
|
7
|
+
|
|
8
|
+
HTML primitives are generated `html` nodes. PMX Canvas stores the generated HTML in the normal
|
|
9
|
+
sandboxed iframe node, plus metadata describing the primitive kind and source data.
|
|
10
|
+
|
|
11
|
+
- Use them when markdown becomes too dense but a full React web artifact would be too heavy.
|
|
12
|
+
- Prefer them for communication artifacts: choices, plans, reviews, maps, explainers, reports,
|
|
13
|
+
lightweight editors, and handoff boards.
|
|
14
|
+
- Keep using `canvas_add_html_node` for bespoke one-off HTML and JS.
|
|
15
|
+
- Keep using `canvas_build_web_artifact` for multi-component apps, routing, React state, or shadcn UI.
|
|
16
|
+
|
|
17
|
+
The design language is inspired by high-density HTML communication patterns: strong hierarchy,
|
|
18
|
+
metric cards, sticky context panels, annotated snippets, inline SVG figures, and copy/export actions.
|
|
19
|
+
Do not copy upstream demo HTML verbatim; use the PMX primitive catalog and data shapes instead.
|
|
20
|
+
|
|
21
|
+
## Current Catalog
|
|
22
|
+
|
|
23
|
+
| Kind | Best use |
|
|
24
|
+
|------|----------|
|
|
25
|
+
| `choice-grid` | Side-by-side options with pros, cons, tradeoffs, and evidence |
|
|
26
|
+
| `plan-timeline` | Sequenced implementation plans with risks, flow, and checkpoints |
|
|
27
|
+
| `review-sheet` | PR or code review findings with severity, file, line, and diff context |
|
|
28
|
+
| `pr-writeup` | Reviewer-ready PR narrative with motivation, file tour, tests, and rollout |
|
|
29
|
+
| `system-map` | Architecture/module maps with entry points and relationships |
|
|
30
|
+
| `code-walkthrough` | Guided source-path explanations with ordered steps and snippets |
|
|
31
|
+
| `design-sheet` | Visual directions, palettes, tokens, type samples, and rationale |
|
|
32
|
+
| `component-gallery` | Component variants, states, sizes, and accessibility notes |
|
|
33
|
+
| `interaction-prototype` | Throwaway interaction or motion studies with live controls |
|
|
34
|
+
| `flowchart` | Process, journey, pipeline, and failure-path diagrams |
|
|
35
|
+
| `deck` | Compact arrow-key narrative decks with speaker notes |
|
|
36
|
+
| `presentation` | Fullscreen-ready PowerPoint-like slide decks for briefings and pitches |
|
|
37
|
+
| `illustration-set` | Inline SVG figure sheets with captions and SVG copy/export |
|
|
38
|
+
| `explainer` | Feature or algorithm explainers with TLDR, steps, FAQ, and glossary |
|
|
39
|
+
| `status-report` | Skimmable project health with metrics, shipped/slipped lists, and next actions |
|
|
40
|
+
| `incident-report` | Incident summaries with impact, timeline, root cause, logs, and action items |
|
|
41
|
+
| `triage-board` | Human-reorderable Now/Next/Later/Cut boards with markdown export |
|
|
42
|
+
| `config-editor` | Feature flag or config editors with dependency warnings and diff export |
|
|
43
|
+
| `prompt-tuner` | Prompt/template editors with variable previews and copy export |
|
|
44
|
+
|
|
45
|
+
## Request Shape
|
|
46
|
+
|
|
47
|
+
Use the running server as the source of truth before constructing data payloads:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pmx-canvas html primitive schema --summary
|
|
51
|
+
pmx-canvas html primitive schema --kind explainer
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
MCP shape:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"kind": "explainer",
|
|
59
|
+
"title": "HTML Primitives",
|
|
60
|
+
"data": {
|
|
61
|
+
"summary": "Reusable generated HTML nodes for rich agent-to-human communication.",
|
|
62
|
+
"steps": [
|
|
63
|
+
{ "title": "Pick a primitive", "detail": "Match the work product to the catalog kind." }
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"x": 120,
|
|
67
|
+
"y": 80,
|
|
68
|
+
"width": 980,
|
|
69
|
+
"height": 760
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
CLI shape:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pmx-canvas html primitive add \
|
|
77
|
+
--kind choice-grid \
|
|
78
|
+
--title "Implementation Options" \
|
|
79
|
+
--data-json '{"items":[{"title":"Small patch","summary":"Least disruption","tradeoff":"Limited future flexibility","pros":["Fast"],"cons":["May need follow-up"]}]}'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Important details:
|
|
83
|
+
|
|
84
|
+
- `kind` is required.
|
|
85
|
+
- `title`, `data`, `x`, `y`, `width`, `height`, and `strictSize` are optional.
|
|
86
|
+
- The persisted node type is `html`, not a separate durable `html-primitive` node type.
|
|
87
|
+
- `html-primitive` is accepted by schema validation and convenience creation paths as a virtual type.
|
|
88
|
+
- Batch `node.add` does not create HTML primitives; use the dedicated primitive tool first or batch the generated `html` node.
|
|
89
|
+
|
|
90
|
+
## Payload Rules
|
|
91
|
+
|
|
92
|
+
- Keep titles short and scannable; put detail in structured fields.
|
|
93
|
+
- Prefer arrays of small records over one large prose blob.
|
|
94
|
+
- Include file paths, line numbers, test commands, and exact statuses when the primitive is review or engineering focused.
|
|
95
|
+
- Use `summary`, `why`, `risks`, `next`, `tests`, and `reviewFocus` fields when the chosen primitive supports them.
|
|
96
|
+
- For `presentation`, use `slides: [{ title, kicker?, body?, bullets?, metrics?, note? }]`; keep one idea per slide, use `metrics` for big numbers, and put speaker notes in `note`.
|
|
97
|
+
- Only choose `presentation` when the user explicitly asks for a PowerPoint-like deck, pitch, briefing, workshop walkthrough, or fullscreen story. Otherwise create a normal `html` node or a non-presentation primitive.
|
|
98
|
+
- Presentation data supports `theme: "canvas" | "midnight" | "paper" | "aurora"` or a custom color object with `bg`, `panel`, `surface`, `border`, `text`, `textSecondary`, `textMuted`, `accent`, and `colorScheme`.
|
|
99
|
+
- For visual primitives, provide colors as hex/rgb/hsl values only; unsafe color strings are discarded.
|
|
100
|
+
- For editor primitives, seed realistic initial columns, flags, controls, variables, or template text so the human can interact immediately.
|
|
101
|
+
|
|
102
|
+
## Human Feedback Loop
|
|
103
|
+
|
|
104
|
+
Interactive/editor primitives are useful when the human should modify state and return it to the
|
|
105
|
+
agent.
|
|
106
|
+
|
|
107
|
+
- `triage-board` lets the human reorder and rebucket items, then copy markdown.
|
|
108
|
+
- `config-editor` lets the human toggle flags and copy a diff-like export.
|
|
109
|
+
- `prompt-tuner` lets the human edit prompts, preview sample substitutions, and copy the current prompt state.
|
|
110
|
+
- `interaction-prototype` exposes live controls and copyable config for implementation tuning.
|
|
111
|
+
- Tell the human which copy/export button to use, then ask them to paste the result back if you need to act on edits.
|
|
112
|
+
|
|
113
|
+
## Sandbox And Persistence
|
|
114
|
+
|
|
115
|
+
- Primitive output runs inside the existing HTML iframe sandbox with scripts allowed but no same-origin access.
|
|
116
|
+
- Generated HTML receives PMX Canvas theme CSS variables so it adapts to the current canvas theme, and sandboxed iframes receive live theme updates when the canvas theme changes.
|
|
117
|
+
- Generated and raw HTML nodes store an agent-readable summary sidecar (`agentSummary`, `contentSummary`, and embedded references) for search, pinned context, and spatial context.
|
|
118
|
+
- Only presentation-marked HTML nodes have a browser `Present` button. Use it for `presentation` nodes so the human can review the deck fullscreen, navigate slides with arrow keys/Space/Page Up/Page Down, and exit with `Esc` or `Exit presentation`.
|
|
119
|
+
- Presentation primitives also persist `presentation`, `slideCount`, `slideTitles`, and optional `speakerNotes` metadata so agents can understand the deck without parsing the iframe HTML.
|
|
120
|
+
- Primitive data is persisted in canvas state; do not include secrets, credentials, private tokens, or unnecessary personal data.
|
|
121
|
+
- Treat primitives as communication surfaces, not authoritative application state. If the result must be machine-roundtrippable, keep the original data in the node metadata or another structured node as well.
|
|
122
|
+
|
|
123
|
+
## Picking The Right Primitive
|
|
124
|
+
|
|
125
|
+
- Pick `choice-grid` when a decision has competing options.
|
|
126
|
+
- Pick `plan-timeline` when sequence, dependency, and risk matter.
|
|
127
|
+
- Pick `review-sheet` or `pr-writeup` for code review and pull-request communication.
|
|
128
|
+
- Pick `system-map`, `code-walkthrough`, or `flowchart` for architecture and source explanations.
|
|
129
|
+
- Pick `design-sheet`, `component-gallery`, `interaction-prototype`, or `illustration-set` for visual/product work.
|
|
130
|
+
- Pick `presentation` when the human asks for a PowerPoint-like deck, pitch, briefing, workshop walkthrough, or fullscreen story.
|
|
131
|
+
- Pick `explainer`, `deck`, `status-report`, or `incident-report` for narrative reporting.
|
|
132
|
+
- Pick `triage-board`, `config-editor`, or `prompt-tuner` when the human needs to edit or return structured feedback.
|
package/src/cli/agent.ts
CHANGED
|
@@ -1160,6 +1160,15 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1160
1160
|
} else if (type === 'html') {
|
|
1161
1161
|
const html = getStringFlag(flags, 'html') ?? getStringFlag(flags, 'content');
|
|
1162
1162
|
if (html !== undefined) body.html = html;
|
|
1163
|
+
const summary = getStringFlag(flags, 'summary');
|
|
1164
|
+
const agentSummary = getStringFlag(flags, 'agent-summary', 'agentSummary');
|
|
1165
|
+
const description = getStringFlag(flags, 'description');
|
|
1166
|
+
if (summary !== undefined) body.summary = summary;
|
|
1167
|
+
if (agentSummary !== undefined) body.agentSummary = agentSummary;
|
|
1168
|
+
if (description !== undefined) body.description = description;
|
|
1169
|
+
if (optionalBooleanFlag(flags, 'presentation', 'Use --presentation true or --presentation false') === true) body.presentation = true;
|
|
1170
|
+
if (typeof flags['slide-title'] === 'string') body.slideTitles = [flags['slide-title']];
|
|
1171
|
+
if (typeof flags['embedded-node-id'] === 'string') body.embeddedNodeIds = [flags['embedded-node-id']];
|
|
1163
1172
|
} else if (flags.content) {
|
|
1164
1173
|
body.content = flags.content;
|
|
1165
1174
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
32
32
|
const AGENT_COMMANDS = new Set([
|
|
33
33
|
'node', 'edge', 'json-render', 'search', 'layout', 'status', 'arrange', 'focus',
|
|
34
34
|
'fit', 'screenshot', 'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
|
|
35
|
-
'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'graph', 'batch', 'validate', 'serve',
|
|
35
|
+
'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'graph', 'html', 'batch', 'validate', 'serve',
|
|
36
36
|
]);
|
|
37
37
|
|
|
38
38
|
const firstArg = args[0] ?? '';
|
package/src/client/App.tsx
CHANGED
|
@@ -237,9 +237,9 @@ function Toolbar({
|
|
|
237
237
|
type="button"
|
|
238
238
|
onClick={() => {
|
|
239
239
|
const next = canvasTheme.value === 'dark' ? 'light' : 'dark';
|
|
240
|
-
canvasTheme.value = next;
|
|
241
240
|
document.documentElement.setAttribute('data-theme', next);
|
|
242
241
|
invalidateTokenCache();
|
|
242
|
+
canvasTheme.value = next;
|
|
243
243
|
}}
|
|
244
244
|
aria-label={`Switch to ${canvasTheme.value === 'dark' ? 'light' : 'dark'} theme`}
|
|
245
245
|
>
|
|
@@ -145,9 +145,9 @@ export function CommandPalette({
|
|
|
145
145
|
badge: 'THEME',
|
|
146
146
|
action: () => {
|
|
147
147
|
const next = canvasTheme.value === 'dark' ? 'light' : 'dark';
|
|
148
|
-
canvasTheme.value = next;
|
|
149
148
|
document.documentElement.setAttribute('data-theme', next);
|
|
150
149
|
invalidateTokenCache();
|
|
150
|
+
canvasTheme.value = next;
|
|
151
151
|
onClose();
|
|
152
152
|
},
|
|
153
153
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useState } from 'preact/hooks';
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { ContextNode } from '../nodes/ContextNode';
|
|
3
3
|
import { FileNode } from '../nodes/FileNode';
|
|
4
4
|
import { LedgerNode } from '../nodes/LedgerNode';
|
|
@@ -7,7 +7,7 @@ import { McpAppNode } from '../nodes/McpAppNode';
|
|
|
7
7
|
import { StatusNode } from '../nodes/StatusNode';
|
|
8
8
|
import { ImageNode } from '../nodes/ImageNode';
|
|
9
9
|
import { WebpageNode } from '../nodes/WebpageNode';
|
|
10
|
-
import { HtmlNode } from '../nodes/HtmlNode';
|
|
10
|
+
import { HtmlNode, shouldShowPresentationControls } from '../nodes/HtmlNode';
|
|
11
11
|
import { PromptNode } from '../nodes/PromptNode';
|
|
12
12
|
import { ResponseNode } from '../nodes/ResponseNode';
|
|
13
13
|
import { TraceNode } from '../nodes/TraceNode';
|
|
@@ -81,15 +81,62 @@ function wordCount(text: string): number {
|
|
|
81
81
|
return text.split(/\s+/).filter(Boolean).length;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function isPresentationExitMessage(value: unknown, token: string): boolean {
|
|
85
|
+
return value !== null &&
|
|
86
|
+
typeof value === 'object' &&
|
|
87
|
+
(value as { source?: unknown }).source === 'pmx-canvas-html-node' &&
|
|
88
|
+
(value as { type?: unknown }).type === 'presentation-exit' &&
|
|
89
|
+
(value as { token?: unknown }).token === token;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isPresentationNavigationKey(key: string): boolean {
|
|
93
|
+
return key === 'ArrowRight' || key === 'PageDown' || key === ' ' || key === 'ArrowLeft' || key === 'PageUp' || key === 'Home' || key === 'End';
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
export function ExpandedNodeOverlay() {
|
|
85
97
|
const nodeId = expandedNodeId.value;
|
|
86
98
|
const node = nodeId ? nodes.value.get(nodeId) : undefined;
|
|
87
99
|
const [copied, setCopied] = useState(false);
|
|
100
|
+
const [presenting, setPresenting] = useState(false);
|
|
101
|
+
const [presentationExitToken, setPresentationExitToken] = useState('');
|
|
102
|
+
const presentationOverlayRef = useRef<HTMLDivElement>(null);
|
|
88
103
|
|
|
89
104
|
const handleClose = useCallback(() => {
|
|
105
|
+
setPresenting(false);
|
|
90
106
|
collapseExpandedNode();
|
|
91
107
|
}, []);
|
|
92
108
|
|
|
109
|
+
const handlePresent = useCallback(() => {
|
|
110
|
+
setPresentationExitToken(`presentation-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
|
|
111
|
+
setPresenting(true);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
const postPresentationMessage = useCallback((message: Record<string, unknown>) => {
|
|
115
|
+
const frame = document.querySelector<HTMLIFrameElement>('.html-presentation-overlay iframe.html-node-frame-presentation');
|
|
116
|
+
frame?.contentWindow?.postMessage({
|
|
117
|
+
source: 'pmx-canvas-html-node',
|
|
118
|
+
token: presentationExitToken,
|
|
119
|
+
...message,
|
|
120
|
+
}, '*');
|
|
121
|
+
}, [presentationExitToken]);
|
|
122
|
+
|
|
123
|
+
const handleExitPresentation = useCallback(() => {
|
|
124
|
+
setPresenting(false);
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
const handlePresentationKeyDown = useCallback((event: KeyboardEvent) => {
|
|
128
|
+
if (event.key === 'Escape') {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
event.stopPropagation();
|
|
131
|
+
setPresenting(false);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!isPresentationNavigationKey(event.key)) return;
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
event.stopPropagation();
|
|
137
|
+
postPresentationMessage({ type: 'presentation-key', key: event.key });
|
|
138
|
+
}, [postPresentationMessage]);
|
|
139
|
+
|
|
93
140
|
const handleBackdropPointerDown = useCallback((e: PointerEvent) => {
|
|
94
141
|
if ((e.target as HTMLElement).classList.contains('expanded-overlay-backdrop')) {
|
|
95
142
|
collapseExpandedNode();
|
|
@@ -111,6 +158,29 @@ export function ExpandedNodeOverlay() {
|
|
|
111
158
|
toggleContextPin(nodeId);
|
|
112
159
|
}, [nodeId]);
|
|
113
160
|
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
setPresenting(false);
|
|
163
|
+
}, [nodeId]);
|
|
164
|
+
|
|
165
|
+
useLayoutEffect(() => {
|
|
166
|
+
if (!presenting) return;
|
|
167
|
+
const focusPresentationOverlay = () => {
|
|
168
|
+
presentationOverlayRef.current?.focus();
|
|
169
|
+
};
|
|
170
|
+
const focusTimers = [0, 50, 150].map((delay) => window.setTimeout(focusPresentationOverlay, delay));
|
|
171
|
+
const handleMessage = (event: MessageEvent) => {
|
|
172
|
+
if (!isPresentationExitMessage(event.data, presentationExitToken)) return;
|
|
173
|
+
setPresenting(false);
|
|
174
|
+
};
|
|
175
|
+
document.addEventListener('keydown', handlePresentationKeyDown, true);
|
|
176
|
+
window.addEventListener('message', handleMessage);
|
|
177
|
+
return () => {
|
|
178
|
+
focusTimers.forEach((timer) => window.clearTimeout(timer));
|
|
179
|
+
document.removeEventListener('keydown', handlePresentationKeyDown, true);
|
|
180
|
+
window.removeEventListener('message', handleMessage);
|
|
181
|
+
};
|
|
182
|
+
}, [handlePresentationKeyDown, presentationExitToken, presenting]);
|
|
183
|
+
|
|
114
184
|
if (!node) return null;
|
|
115
185
|
|
|
116
186
|
const title =
|
|
@@ -123,6 +193,7 @@ export function ExpandedNodeOverlay() {
|
|
|
123
193
|
const hasText = textContent.length > 0;
|
|
124
194
|
const pendingClose = pendingExpandedNodeCloseId.value === nodeId;
|
|
125
195
|
const isEmbeddedViewer = node.type === 'mcp-app' || node.type === 'webpage' || node.type === 'json-render' || node.type === 'graph';
|
|
196
|
+
const canPresent = shouldShowPresentationControls(node);
|
|
126
197
|
|
|
127
198
|
return (
|
|
128
199
|
<div
|
|
@@ -218,6 +289,17 @@ export function ExpandedNodeOverlay() {
|
|
|
218
289
|
</button>
|
|
219
290
|
)}
|
|
220
291
|
|
|
292
|
+
{canPresent && (
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
class="expanded-action-btn expanded-action-primary"
|
|
296
|
+
onClick={handlePresent}
|
|
297
|
+
title="Present this HTML node fullscreen"
|
|
298
|
+
>
|
|
299
|
+
Present
|
|
300
|
+
</button>
|
|
301
|
+
)}
|
|
302
|
+
|
|
221
303
|
{/* Word count */}
|
|
222
304
|
{words > 0 && (
|
|
223
305
|
<span class="expanded-meta">
|
|
@@ -270,6 +352,27 @@ export function ExpandedNodeOverlay() {
|
|
|
270
352
|
</div>
|
|
271
353
|
) : renderContent(node, true)}
|
|
272
354
|
</div>
|
|
355
|
+
{canPresent && presenting && (
|
|
356
|
+
<div ref={presentationOverlayRef} class="html-presentation-overlay" role="dialog" aria-modal="true" aria-label={`Present ${title}`} tabIndex={-1} onKeyDownCapture={handlePresentationKeyDown}>
|
|
357
|
+
<div class="html-presentation-toolbar">
|
|
358
|
+
<div>
|
|
359
|
+
<div class="html-presentation-kicker">HTML presentation</div>
|
|
360
|
+
<div class="html-presentation-title">{title}</div>
|
|
361
|
+
</div>
|
|
362
|
+
<button
|
|
363
|
+
type="button"
|
|
364
|
+
class="html-presentation-exit"
|
|
365
|
+
onClick={handleExitPresentation}
|
|
366
|
+
title="Exit presentation (Esc)"
|
|
367
|
+
>
|
|
368
|
+
Exit presentation
|
|
369
|
+
</button>
|
|
370
|
+
</div>
|
|
371
|
+
<div class="html-presentation-stage">
|
|
372
|
+
<HtmlNode node={node} expanded presentation presentationExitToken={presentationExitToken} />
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
273
376
|
</div>
|
|
274
377
|
</div>
|
|
275
378
|
);
|
|
@@ -11,8 +11,12 @@ function hasExplicitStructuredFrame(node: CanvasNodeState): boolean {
|
|
|
11
11
|
return node.type === 'graph' || node.type === 'json-render';
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function isPresentationHtmlNode(node: CanvasNodeState): boolean {
|
|
15
|
+
return node.type === 'html' && node.data.presentation === true;
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
export function shouldAutoFitNode(node: CanvasNodeState): boolean {
|
|
15
|
-
return !node.collapsed && !node.dockPosition && node.data.strictSize !== true && node.type !== 'group' && !isExtAppNode(node) && !hasExplicitStructuredFrame(node);
|
|
19
|
+
return !node.collapsed && !node.dockPosition && node.data.strictSize !== true && node.type !== 'group' && !isExtAppNode(node) && !hasExplicitStructuredFrame(node) && !isPresentationHtmlNode(node);
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export function computeAutoFitHeight(node: CanvasNodeState, contentHeight: number): number | null {
|