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
|
@@ -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
|
@@ -73,6 +73,15 @@ interface CanvasSchemaResponse {
|
|
|
73
73
|
'line' | 'bar' | 'pie' | 'area' | 'scatter' | 'radar' | 'stacked-bar' | 'composed'
|
|
74
74
|
>;
|
|
75
75
|
};
|
|
76
|
+
htmlPrimitives?: Array<{
|
|
77
|
+
kind: string;
|
|
78
|
+
title: string;
|
|
79
|
+
description: string;
|
|
80
|
+
useWhen: string;
|
|
81
|
+
defaultSize: { width: number; height: number };
|
|
82
|
+
dataShape: string;
|
|
83
|
+
example: Record<string, unknown>;
|
|
84
|
+
}>;
|
|
76
85
|
mcp: {
|
|
77
86
|
tools: string[];
|
|
78
87
|
resources: string[];
|
|
@@ -480,6 +489,14 @@ function parseJsonValue(raw: string, label: string, hint: string): unknown {
|
|
|
480
489
|
}
|
|
481
490
|
}
|
|
482
491
|
|
|
492
|
+
function parseJsonRecord(raw: string, label: string, hint: string): Record<string, unknown> {
|
|
493
|
+
const parsed = parseJsonValue(raw, label, hint);
|
|
494
|
+
if (!isRecord(parsed)) {
|
|
495
|
+
die(`${label} must be a JSON object.`, hint);
|
|
496
|
+
}
|
|
497
|
+
return parsed;
|
|
498
|
+
}
|
|
499
|
+
|
|
483
500
|
async function readTextInput(
|
|
484
501
|
flags: Record<string, string | true>,
|
|
485
502
|
options: {
|
|
@@ -615,6 +632,34 @@ async function buildJsonRenderRequestBody(
|
|
|
615
632
|
return body;
|
|
616
633
|
}
|
|
617
634
|
|
|
635
|
+
async function buildHtmlPrimitiveRequestBody(
|
|
636
|
+
flags: Record<string, string | true>,
|
|
637
|
+
): Promise<Record<string, unknown>> {
|
|
638
|
+
const hint = 'Use: pmx-canvas html primitive add --kind choice-grid --data-file ./primitive.json --title "Options"';
|
|
639
|
+
const kind = getStringFlag(flags, 'kind', 'primitive');
|
|
640
|
+
if (!kind) die('HTML primitives require --kind.', hint);
|
|
641
|
+
const body: Record<string, unknown> = { type: 'html', primitive: kind };
|
|
642
|
+
if (typeof flags.title === 'string') body.title = flags.title;
|
|
643
|
+
const rawData = await readOptionalTextInput(flags, {
|
|
644
|
+
fileFlags: ['data-file'],
|
|
645
|
+
valueFlags: ['data-json', 'data'],
|
|
646
|
+
allowStdin: true,
|
|
647
|
+
label: 'HTML primitive data',
|
|
648
|
+
hint,
|
|
649
|
+
});
|
|
650
|
+
if (rawData !== undefined) {
|
|
651
|
+
body.data = parseJsonRecord(rawData, 'HTML primitive data', hint);
|
|
652
|
+
}
|
|
653
|
+
applyCommonGeometryFlags(body, flags, {
|
|
654
|
+
x: 'Use a finite number, e.g. --x 500',
|
|
655
|
+
y: 'Use a finite number, e.g. --y 300',
|
|
656
|
+
width: 'Use a positive number, e.g. --width 980',
|
|
657
|
+
height: 'Use a positive number, e.g. --height 720',
|
|
658
|
+
});
|
|
659
|
+
applyStrictSizeFlags(body, flags);
|
|
660
|
+
return body;
|
|
661
|
+
}
|
|
662
|
+
|
|
618
663
|
async function buildGraphRequestBody(
|
|
619
664
|
flags: Record<string, string | true>,
|
|
620
665
|
options: { requireData?: boolean; allowStdin?: boolean } = {},
|
|
@@ -967,6 +1012,35 @@ function filterJsonRenderSchemaView(
|
|
|
967
1012
|
return flags.summary ? summarizeJsonRenderComponent(component) : component;
|
|
968
1013
|
}
|
|
969
1014
|
|
|
1015
|
+
function summarizeHtmlPrimitive(primitive: NonNullable<CanvasSchemaResponse['htmlPrimitives']>[number]): Record<string, unknown> {
|
|
1016
|
+
return {
|
|
1017
|
+
kind: primitive.kind,
|
|
1018
|
+
title: primitive.title,
|
|
1019
|
+
description: primitive.description,
|
|
1020
|
+
useWhen: primitive.useWhen,
|
|
1021
|
+
defaultSize: primitive.defaultSize,
|
|
1022
|
+
dataShape: primitive.dataShape,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function filterHtmlPrimitiveSchemaView(
|
|
1027
|
+
schema: CanvasSchemaResponse,
|
|
1028
|
+
flags: Record<string, string | true>,
|
|
1029
|
+
): Record<string, unknown> {
|
|
1030
|
+
const primitives = schema.htmlPrimitives ?? [];
|
|
1031
|
+
const kind = getStringFlag(flags, 'kind', 'primitive');
|
|
1032
|
+
if (!kind) {
|
|
1033
|
+
return {
|
|
1034
|
+
primitives: flags.summary ? primitives.map((entry) => summarizeHtmlPrimitive(entry)) : primitives,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
const primitive = primitives.find((entry) => entry.kind === kind);
|
|
1038
|
+
if (!primitive) {
|
|
1039
|
+
die(`Unknown HTML primitive: ${kind}`, 'Run: pmx-canvas html primitive schema --summary');
|
|
1040
|
+
}
|
|
1041
|
+
return flags.summary ? summarizeHtmlPrimitive(primitive) : primitive;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
970
1044
|
// ── Commands ─────────────────────────────────────────────────
|
|
971
1045
|
|
|
972
1046
|
const COMMANDS: Record<string, { run: (args: string[]) => Promise<void>; help: string; examples: string[] }> = {};
|
|
@@ -1028,6 +1102,7 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1028
1102
|
'pmx-canvas node add --type file --content "src/index.ts"',
|
|
1029
1103
|
'pmx-canvas node add --type webpage --url "https://example.com/docs"',
|
|
1030
1104
|
'pmx-canvas node add --type html --title "Widget" --content "<main>Hello</main>"',
|
|
1105
|
+
'pmx-canvas node add --type html --primitive choice-grid --data-file ./options.json --title "Options"',
|
|
1031
1106
|
'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
|
|
1032
1107
|
'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
|
|
1033
1108
|
'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
@@ -1055,6 +1130,18 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1055
1130
|
return;
|
|
1056
1131
|
}
|
|
1057
1132
|
|
|
1133
|
+
if (type === 'html-primitive') {
|
|
1134
|
+
const result = await api('POST', '/api/canvas/node', await buildHtmlPrimitiveRequestBody(flags));
|
|
1135
|
+
output(result);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (type === 'html' && getStringFlag(flags, 'primitive', 'kind')) {
|
|
1140
|
+
const result = await api('POST', '/api/canvas/node', await buildHtmlPrimitiveRequestBody(flags));
|
|
1141
|
+
output(result);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1058
1145
|
if (type === 'mcp-app') {
|
|
1059
1146
|
die(
|
|
1060
1147
|
'mcp-app nodes require tool-backed app metadata and cannot be created with generic node add.',
|
|
@@ -1073,6 +1160,15 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1073
1160
|
} else if (type === 'html') {
|
|
1074
1161
|
const html = getStringFlag(flags, 'html') ?? getStringFlag(flags, 'content');
|
|
1075
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']];
|
|
1076
1172
|
} else if (flags.content) {
|
|
1077
1173
|
body.content = flags.content;
|
|
1078
1174
|
}
|
|
@@ -1136,6 +1232,28 @@ cmd('json-render', 'Show json-render schema and canonical examples', [
|
|
|
1136
1232
|
output(filterJsonRenderSchemaView(schema.jsonRender, flags));
|
|
1137
1233
|
});
|
|
1138
1234
|
|
|
1235
|
+
cmd('html primitive add', 'Create a reusable sandboxed HTML communication primitive', [
|
|
1236
|
+
'pmx-canvas html primitive add --kind choice-grid --data-file ./options.json --title "Options"',
|
|
1237
|
+
'pmx-canvas html primitive add --kind plan-timeline --data-json \'{"milestones":[{"title":"Ship","detail":"Implement and verify","status":"next"}]}\'',
|
|
1238
|
+
'pmx-canvas html primitive add --kind triage-board --data-file ./tickets.json --strict-size',
|
|
1239
|
+
], async (args) => {
|
|
1240
|
+
const { flags } = parseFlags(args);
|
|
1241
|
+
if (flags.help || flags.h) return showCommandHelp('html primitive add');
|
|
1242
|
+
const result = await api('POST', '/api/canvas/node', await buildHtmlPrimitiveRequestBody(flags));
|
|
1243
|
+
output(result);
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
cmd('html primitive schema', 'Describe reusable HTML communication primitives', [
|
|
1247
|
+
'pmx-canvas html primitive schema --summary',
|
|
1248
|
+
'pmx-canvas html primitive schema --kind choice-grid',
|
|
1249
|
+
'pmx-canvas html primitive schema --kind triage-board --summary',
|
|
1250
|
+
], async (args) => {
|
|
1251
|
+
const { flags } = parseFlags(args);
|
|
1252
|
+
if (flags.help || flags.h) return showCommandHelp('html primitive schema');
|
|
1253
|
+
const schema = await loadCanvasSchema();
|
|
1254
|
+
output(filterHtmlPrimitiveSchemaView(schema, flags));
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1139
1257
|
cmd('graph add', 'Add a graph node to the canvas', [
|
|
1140
1258
|
'pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
1141
1259
|
'pmx-canvas graph add --graphType composed --data \'[{"day":"Mon","visits":10,"conversion":0.4}]\' --xKey day --barKey visits --lineKey conversion',
|
|
@@ -1176,6 +1294,7 @@ cmd('node schema', 'Describe server-supported node create schemas and canonical
|
|
|
1176
1294
|
rootShape: result.jsonRender.rootShape,
|
|
1177
1295
|
},
|
|
1178
1296
|
graph: result.graph,
|
|
1297
|
+
htmlPrimitives: result.htmlPrimitives?.map((entry) => summarizeHtmlPrimitive(entry)) ?? [],
|
|
1179
1298
|
mcp: result.mcp,
|
|
1180
1299
|
});
|
|
1181
1300
|
return;
|
|
@@ -1898,19 +2017,31 @@ cmd('validate', 'Validate the current layout for collisions and missing edge end
|
|
|
1898
2017
|
cmd('validate spec', 'Validate a json-render spec or graph payload without creating a node', [
|
|
1899
2018
|
'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json',
|
|
1900
2019
|
'pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
2020
|
+
'pmx-canvas validate spec --type html-primitive --kind choice-grid --data-file ./options.json',
|
|
1901
2021
|
'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary',
|
|
1902
2022
|
], async (args) => {
|
|
1903
2023
|
const { flags } = parseFlags(args);
|
|
1904
2024
|
if (flags.help || flags.h) return showCommandHelp('validate spec');
|
|
1905
2025
|
|
|
1906
2026
|
const type = getStringFlag(flags, 'type');
|
|
1907
|
-
if (type !== 'json-render' && type !== 'graph') {
|
|
1908
|
-
die('validate spec requires --type json-render or --type
|
|
2027
|
+
if (type !== 'json-render' && type !== 'graph' && type !== 'html-primitive') {
|
|
2028
|
+
die('validate spec requires --type json-render, --type graph, or --type html-primitive.');
|
|
1909
2029
|
}
|
|
1910
2030
|
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2031
|
+
let body: Record<string, unknown>;
|
|
2032
|
+
if (type === 'json-render') {
|
|
2033
|
+
body = { type, spec: (await buildJsonRenderRequestBody({ ...flags, title: String(flags.title ?? 'Validation') })).spec };
|
|
2034
|
+
} else if (type === 'html-primitive') {
|
|
2035
|
+
const primitiveBody = await buildHtmlPrimitiveRequestBody(flags);
|
|
2036
|
+
body = {
|
|
2037
|
+
type,
|
|
2038
|
+
kind: primitiveBody.primitive,
|
|
2039
|
+
...(typeof primitiveBody.title === 'string' ? { title: primitiveBody.title } : {}),
|
|
2040
|
+
...(isRecord(primitiveBody.data) ? { data: primitiveBody.data } : {}),
|
|
2041
|
+
};
|
|
2042
|
+
} else {
|
|
2043
|
+
body = { type, ...(await buildGraphRequestBody(flags)) };
|
|
2044
|
+
}
|
|
1914
2045
|
|
|
1915
2046
|
const result = await api('POST', '/api/canvas/schema/validate', body) as Record<string, unknown>;
|
|
1916
2047
|
if (flags.summary) {
|
|
@@ -2302,9 +2433,17 @@ function showCommandHelp(name: string): void {
|
|
|
2302
2433
|
console.log(' pmx-canvas node add --help --type webpage');
|
|
2303
2434
|
console.log(' pmx-canvas node add --help --type json-render --component Table');
|
|
2304
2435
|
console.log(' pmx-canvas node add --help --type graph');
|
|
2436
|
+
console.log(' pmx-canvas html primitive schema --summary');
|
|
2305
2437
|
console.log(' pmx-canvas node add --help --type webpage --json');
|
|
2306
2438
|
console.log(' Use --strict-size to keep explicit width/height fixed and scroll overflowing content.');
|
|
2307
2439
|
}
|
|
2440
|
+
if (name === 'html primitive add' || name === 'html primitive schema') {
|
|
2441
|
+
console.log('\nPrimitive flags:');
|
|
2442
|
+
console.log(' --kind <name> Run `pmx-canvas html primitive schema --summary` for the full catalog');
|
|
2443
|
+
console.log(' --data-file <path> JSON object payload for the primitive');
|
|
2444
|
+
console.log(' --data-json, --data <json> Inline JSON object payload');
|
|
2445
|
+
console.log(' --stdin Read JSON object payload from stdin');
|
|
2446
|
+
}
|
|
2308
2447
|
if (name === 'json-render') {
|
|
2309
2448
|
console.log('\nOptions:');
|
|
2310
2449
|
console.log(' --schema Show json-render catalog schema (default)');
|
|
@@ -2319,6 +2458,10 @@ function showCommandHelp(name: string): void {
|
|
|
2319
2458
|
console.log(' Use --node-height/--nodeHeight for canvas frame height; use --chart-height for chart content height. --height is kept as a frame-height alias for compatibility.');
|
|
2320
2459
|
console.log(' Pass --show-legend false to hide legends in compact node layouts.');
|
|
2321
2460
|
}
|
|
2461
|
+
if (name === 'validate spec') {
|
|
2462
|
+
console.log('\nHTML primitive flags:');
|
|
2463
|
+
console.log(' --type html-primitive --kind <name> --data-file ./payload.json');
|
|
2464
|
+
}
|
|
2322
2465
|
if (name === 'node schema') {
|
|
2323
2466
|
console.log('\nFilters:');
|
|
2324
2467
|
console.log(' --summary Show compact schema summaries');
|
|
@@ -2328,6 +2471,7 @@ function showCommandHelp(name: string): void {
|
|
|
2328
2471
|
if (name === 'validate spec') {
|
|
2329
2472
|
console.log('\nOutput control:');
|
|
2330
2473
|
console.log(' --summary Return only validation summary metadata');
|
|
2474
|
+
console.log(' For --type html-primitive, pass --kind plus optional --data-file/--data-json.');
|
|
2331
2475
|
}
|
|
2332
2476
|
if (name === 'snapshot list') {
|
|
2333
2477
|
console.log('\nOptions:');
|
|
@@ -2415,6 +2559,8 @@ Node commands:
|
|
|
2415
2559
|
pmx-canvas node remove <id> Remove a node
|
|
2416
2560
|
pmx-canvas json-render Show json-render schema/examples
|
|
2417
2561
|
pmx-canvas graph add [options] Add a graph node
|
|
2562
|
+
pmx-canvas html primitive add Add an HTML communication primitive
|
|
2563
|
+
pmx-canvas html primitive schema List HTML primitive kinds and shapes
|
|
2418
2564
|
|
|
2419
2565
|
Edge commands:
|
|
2420
2566
|
pmx-canvas edge add [options] Add an edge between nodes
|
|
@@ -2488,6 +2634,8 @@ Examples:
|
|
|
2488
2634
|
pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
|
|
2489
2635
|
pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
|
|
2490
2636
|
pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value
|
|
2637
|
+
pmx-canvas html primitive add --kind choice-grid --data-file ./options.json --title "Options"
|
|
2638
|
+
pmx-canvas html primitive schema --summary
|
|
2491
2639
|
pmx-canvas node add --help --type webpage
|
|
2492
2640
|
pmx-canvas node schema --type json-render
|
|
2493
2641
|
pmx-canvas node schema --type json-render --component Table --summary
|
|
@@ -2534,6 +2682,12 @@ export async function runAgentCli(args: string[]): Promise<void> {
|
|
|
2534
2682
|
return;
|
|
2535
2683
|
}
|
|
2536
2684
|
|
|
2685
|
+
const threeWord = `${args[0]} ${args[1] ?? ''} ${args[2] ?? ''}`.trim();
|
|
2686
|
+
if (COMMANDS[threeWord]) {
|
|
2687
|
+
await COMMANDS[threeWord].run(args.slice(3));
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2537
2691
|
// Try two-word command first (e.g., "node add"), then one-word (e.g., "search")
|
|
2538
2692
|
const twoWord = `${args[0]} ${args[1] ?? ''}`.trim();
|
|
2539
2693
|
if (COMMANDS[twoWord]) {
|
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
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
IconShortcuts,
|
|
53
53
|
IconSnapshot,
|
|
54
54
|
IconSun,
|
|
55
|
+
IconTextAnnotation,
|
|
55
56
|
IconTrace,
|
|
56
57
|
IconZoomIn,
|
|
57
58
|
IconZoomOut,
|
|
@@ -73,7 +74,7 @@ function sendIntent(type: string, payload: Record<string, unknown> = {}): void {
|
|
|
73
74
|
});
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
type AnnotationTool = 'pen' | 'eraser' | null;
|
|
77
|
+
type AnnotationTool = 'pen' | 'eraser' | 'text' | null;
|
|
77
78
|
|
|
78
79
|
function ToolbarHint({
|
|
79
80
|
label,
|
|
@@ -115,6 +116,7 @@ function Toolbar({
|
|
|
115
116
|
annotationTool,
|
|
116
117
|
onToggleAnnotationMode,
|
|
117
118
|
onToggleAnnotationEraser,
|
|
119
|
+
onToggleTextAnnotation,
|
|
118
120
|
}: {
|
|
119
121
|
minimapVisible: boolean;
|
|
120
122
|
onToggleMinimap: () => void;
|
|
@@ -126,6 +128,7 @@ function Toolbar({
|
|
|
126
128
|
annotationTool: AnnotationTool;
|
|
127
129
|
onToggleAnnotationMode: () => void;
|
|
128
130
|
onToggleAnnotationEraser: () => void;
|
|
131
|
+
onToggleTextAnnotation: () => void;
|
|
129
132
|
}) {
|
|
130
133
|
const status = connectionStatus.value;
|
|
131
134
|
const hasSynced = hasInitialServerLayout.value;
|
|
@@ -234,9 +237,9 @@ function Toolbar({
|
|
|
234
237
|
type="button"
|
|
235
238
|
onClick={() => {
|
|
236
239
|
const next = canvasTheme.value === 'dark' ? 'light' : 'dark';
|
|
237
|
-
canvasTheme.value = next;
|
|
238
240
|
document.documentElement.setAttribute('data-theme', next);
|
|
239
241
|
invalidateTokenCache();
|
|
242
|
+
canvasTheme.value = next;
|
|
240
243
|
}}
|
|
241
244
|
aria-label={`Switch to ${canvasTheme.value === 'dark' ? 'light' : 'dark'} theme`}
|
|
242
245
|
>
|
|
@@ -316,6 +319,20 @@ function Toolbar({
|
|
|
316
319
|
<IconEraser />
|
|
317
320
|
</button>
|
|
318
321
|
</ToolbarHint>
|
|
322
|
+
<ToolbarHint
|
|
323
|
+
label={annotationTool === 'text' ? 'Stop text annotations' : 'Text annotations'}
|
|
324
|
+
detail="Click anywhere to type an intent note"
|
|
325
|
+
>
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
onClick={onToggleTextAnnotation}
|
|
329
|
+
aria-label={annotationTool === 'text' ? 'Stop text annotations' : 'Text annotations'}
|
|
330
|
+
aria-pressed={annotationTool === 'text'}
|
|
331
|
+
style={{ color: annotationTool === 'text' ? 'var(--c-accent)' : undefined }}
|
|
332
|
+
>
|
|
333
|
+
<IconTextAnnotation />
|
|
334
|
+
</button>
|
|
335
|
+
</ToolbarHint>
|
|
319
336
|
|
|
320
337
|
<div class="separator" />
|
|
321
338
|
|
|
@@ -392,6 +409,7 @@ export function App() {
|
|
|
392
409
|
const handleCloseSnapshot = useCallback(() => setSnapshotOpen(false), []);
|
|
393
410
|
const handleToggleAnnotationMode = useCallback(() => setAnnotationTool((tool) => tool === 'pen' ? null : 'pen'), []);
|
|
394
411
|
const handleToggleAnnotationEraser = useCallback(() => setAnnotationTool((tool) => tool === 'eraser' ? null : 'eraser'), []);
|
|
412
|
+
const handleToggleTextAnnotation = useCallback(() => setAnnotationTool((tool) => tool === 'text' ? null : 'text'), []);
|
|
395
413
|
|
|
396
414
|
const handleMinimapNavigate = useCallback((x: number, y: number) => {
|
|
397
415
|
animateViewport({ x, y, scale: viewport.value.scale }, 200);
|
|
@@ -520,6 +538,7 @@ export function App() {
|
|
|
520
538
|
annotationTool={annotationTool}
|
|
521
539
|
onToggleAnnotationMode={handleToggleAnnotationMode}
|
|
522
540
|
onToggleAnnotationEraser={handleToggleAnnotationEraser}
|
|
541
|
+
onToggleTextAnnotation={handleToggleTextAnnotation}
|
|
523
542
|
/>
|
|
524
543
|
<div class="hud-right">
|
|
525
544
|
{dockedRight.map((n) => (
|
|
@@ -11,18 +11,39 @@ export function AnnotationLayer({ annotations }: { annotations: CanvasAnnotation
|
|
|
11
11
|
|
|
12
12
|
return (
|
|
13
13
|
<svg class="annotation-layer" aria-hidden="true">
|
|
14
|
-
{annotations.map((annotation) =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
{annotations.map((annotation) => {
|
|
15
|
+
const color = annotation.color === 'currentColor' ? 'var(--c-annotation)' : annotation.color;
|
|
16
|
+
if (annotation.type === 'text') {
|
|
17
|
+
const point = annotation.points[0];
|
|
18
|
+
if (!point || !annotation.text) return null;
|
|
19
|
+
return (
|
|
20
|
+
<text
|
|
21
|
+
key={annotation.id}
|
|
22
|
+
x={point.x}
|
|
23
|
+
y={point.y}
|
|
24
|
+
fill={color}
|
|
25
|
+
font-size={annotation.width}
|
|
26
|
+
font-family="var(--font)"
|
|
27
|
+
font-weight="700"
|
|
28
|
+
opacity="0.95"
|
|
29
|
+
>
|
|
30
|
+
{annotation.text}
|
|
31
|
+
</text>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return (
|
|
35
|
+
<path
|
|
36
|
+
key={annotation.id}
|
|
37
|
+
d={pointsToPath(annotation.points)}
|
|
38
|
+
fill="none"
|
|
39
|
+
stroke={color}
|
|
40
|
+
stroke-width={annotation.width}
|
|
41
|
+
stroke-linecap="round"
|
|
42
|
+
stroke-linejoin="round"
|
|
43
|
+
opacity="0.9"
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
26
47
|
</svg>
|
|
27
48
|
);
|
|
28
49
|
}
|