pmx-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/Readme.md +865 -0
  4. package/dist/canvas/global.css +3173 -0
  5. package/dist/canvas/index.js +183 -0
  6. package/dist/json-render/index.css +2 -0
  7. package/dist/json-render/index.js +389 -0
  8. package/dist/types/cli/agent.d.ts +13 -0
  9. package/dist/types/cli/index.d.ts +2 -0
  10. package/dist/types/cli/watch.d.ts +5 -0
  11. package/dist/types/client/App.d.ts +1 -0
  12. package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
  13. package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
  14. package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
  15. package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
  16. package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
  17. package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
  18. package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
  19. package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
  20. package/dist/types/client/canvas/DockedNode.d.ts +4 -0
  21. package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
  22. package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
  23. package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
  24. package/dist/types/client/canvas/Minimap.d.ts +23 -0
  25. package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
  26. package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
  27. package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
  28. package/dist/types/client/canvas/snap-guides.d.ts +23 -0
  29. package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
  30. package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
  31. package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
  32. package/dist/types/client/ext-app/bridge.d.ts +161 -0
  33. package/dist/types/client/icons.d.ts +70 -0
  34. package/dist/types/client/index.d.ts +1 -0
  35. package/dist/types/client/nodes/ContextNode.d.ts +34 -0
  36. package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
  37. package/dist/types/client/nodes/FileNode.d.ts +5 -0
  38. package/dist/types/client/nodes/GroupNode.d.ts +6 -0
  39. package/dist/types/client/nodes/ImageNode.d.ts +10 -0
  40. package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
  41. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
  42. package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
  43. package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
  44. package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
  45. package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
  46. package/dist/types/client/nodes/PromptNode.d.ts +5 -0
  47. package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
  48. package/dist/types/client/nodes/StatusNode.d.ts +4 -0
  49. package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
  50. package/dist/types/client/nodes/TraceNode.d.ts +4 -0
  51. package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
  52. package/dist/types/client/nodes/image-warnings.d.ts +6 -0
  53. package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
  54. package/dist/types/client/nodes/md-format.d.ts +25 -0
  55. package/dist/types/client/state/attention-bridge.d.ts +3 -0
  56. package/dist/types/client/state/attention-store.d.ts +25 -0
  57. package/dist/types/client/state/canvas-store.d.ts +74 -0
  58. package/dist/types/client/state/intent-bridge.d.ts +158 -0
  59. package/dist/types/client/state/sse-bridge.d.ts +5 -0
  60. package/dist/types/client/theme/tokens.d.ts +27 -0
  61. package/dist/types/client/types.d.ts +40 -0
  62. package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
  63. package/dist/types/client/utils/placement.d.ts +1 -0
  64. package/dist/types/client/utils/platform.d.ts +2 -0
  65. package/dist/types/json-render/catalog.d.ts +815 -0
  66. package/dist/types/json-render/charts/components.d.ts +54 -0
  67. package/dist/types/json-render/charts/definitions.d.ts +103 -0
  68. package/dist/types/json-render/charts/extra-components.d.ts +58 -0
  69. package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
  70. package/dist/types/json-render/renderer/index.d.ts +16 -0
  71. package/dist/types/json-render/schema.d.ts +46 -0
  72. package/dist/types/json-render/server.d.ts +55 -0
  73. package/dist/types/mcp/server.d.ts +22 -0
  74. package/dist/types/server/agent-context.d.ts +21 -0
  75. package/dist/types/server/artifact-paths.d.ts +3 -0
  76. package/dist/types/server/canvas-operations.d.ts +154 -0
  77. package/dist/types/server/canvas-provenance.d.ts +13 -0
  78. package/dist/types/server/canvas-schema.d.ts +49 -0
  79. package/dist/types/server/canvas-serialization.d.ts +25 -0
  80. package/dist/types/server/canvas-state.d.ts +174 -0
  81. package/dist/types/server/canvas-validation.d.ts +33 -0
  82. package/dist/types/server/chart-template.d.ts +29 -0
  83. package/dist/types/server/code-graph.d.ts +67 -0
  84. package/dist/types/server/context-cards.d.ts +24 -0
  85. package/dist/types/server/diagram-presets.d.ts +28 -0
  86. package/dist/types/server/ext-app-call-registry.d.ts +16 -0
  87. package/dist/types/server/ext-app-tool-result.d.ts +1 -0
  88. package/dist/types/server/file-watcher.d.ts +16 -0
  89. package/dist/types/server/index.d.ts +243 -0
  90. package/dist/types/server/mcp-app-candidate.d.ts +25 -0
  91. package/dist/types/server/mcp-app-host.d.ts +65 -0
  92. package/dist/types/server/mcp-app-runtime.d.ts +47 -0
  93. package/dist/types/server/mutation-history.d.ts +105 -0
  94. package/dist/types/server/placement.d.ts +37 -0
  95. package/dist/types/server/server.d.ts +103 -0
  96. package/dist/types/server/spatial-analysis.d.ts +87 -0
  97. package/dist/types/server/trace-manager.d.ts +48 -0
  98. package/dist/types/server/web-artifacts.d.ts +50 -0
  99. package/dist/types/server/webpage-node.d.ts +25 -0
  100. package/dist/types/shared/auto-arrange.d.ts +29 -0
  101. package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
  102. package/dist/types/shared/placement.d.ts +26 -0
  103. package/dist/types/shared/semantic-attention.d.ts +97 -0
  104. package/package.json +109 -0
  105. package/skills/data-analysis/SKILL.md +324 -0
  106. package/skills/doc-coauthoring/SKILL.md +375 -0
  107. package/skills/frontend-design/SKILL.md +45 -0
  108. package/skills/json-render-codegen/SKILL.md +112 -0
  109. package/skills/json-render-core/SKILL.md +265 -0
  110. package/skills/json-render-ink/SKILL.md +273 -0
  111. package/skills/json-render-mcp/SKILL.md +132 -0
  112. package/skills/json-render-react/SKILL.md +264 -0
  113. package/skills/json-render-shadcn/SKILL.md +159 -0
  114. package/skills/playwright-cli/SKILL.md +67 -0
  115. package/skills/pmx-canvas/SKILL.md +668 -0
  116. package/skills/pmx-canvas/evals/evals.json +186 -0
  117. package/skills/pmx-canvas-testing/SKILL.md +78 -0
  118. package/skills/published-consumer-e2e/SKILL.md +43 -0
  119. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
  120. package/skills/web-artifacts-builder/SKILL.md +80 -0
  121. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
  122. package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
  123. package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  124. package/skills/web-design-guidelines/SKILL.md +39 -0
  125. package/src/cli/agent.ts +2144 -0
  126. package/src/cli/index.ts +622 -0
  127. package/src/cli/watch.ts +88 -0
  128. package/src/client/App.tsx +507 -0
  129. package/src/client/canvas/AttentionHistory.tsx +81 -0
  130. package/src/client/canvas/AttentionToast.tsx +19 -0
  131. package/src/client/canvas/CanvasNode.tsx +363 -0
  132. package/src/client/canvas/CanvasViewport.tsx +590 -0
  133. package/src/client/canvas/CommandPalette.tsx +302 -0
  134. package/src/client/canvas/ContextMenu.tsx +601 -0
  135. package/src/client/canvas/ContextPinBar.tsx +25 -0
  136. package/src/client/canvas/ContextPinHud.tsx +22 -0
  137. package/src/client/canvas/DockedNode.tsx +66 -0
  138. package/src/client/canvas/EdgeLayer.tsx +280 -0
  139. package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
  140. package/src/client/canvas/FocusFieldLayer.tsx +107 -0
  141. package/src/client/canvas/Minimap.tsx +301 -0
  142. package/src/client/canvas/SelectionBar.tsx +69 -0
  143. package/src/client/canvas/ShortcutOverlay.tsx +69 -0
  144. package/src/client/canvas/SnapshotPanel.tsx +236 -0
  145. package/src/client/canvas/snap-guides.ts +170 -0
  146. package/src/client/canvas/use-node-drag.ts +51 -0
  147. package/src/client/canvas/use-node-resize.ts +59 -0
  148. package/src/client/canvas/use-pan-zoom.ts +191 -0
  149. package/src/client/ext-app/bridge.ts +542 -0
  150. package/src/client/icons.tsx +424 -0
  151. package/src/client/index.tsx +7 -0
  152. package/src/client/nodes/ContextNode.tsx +412 -0
  153. package/src/client/nodes/ExtAppFrame.tsx +509 -0
  154. package/src/client/nodes/FileNode.tsx +256 -0
  155. package/src/client/nodes/GroupNode.tsx +39 -0
  156. package/src/client/nodes/ImageNode.tsx +160 -0
  157. package/src/client/nodes/InlineFormatBar.tsx +169 -0
  158. package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
  159. package/src/client/nodes/LedgerNode.tsx +37 -0
  160. package/src/client/nodes/MarkdownNode.tsx +359 -0
  161. package/src/client/nodes/McpAppNode.tsx +85 -0
  162. package/src/client/nodes/MdFormatBar.tsx +109 -0
  163. package/src/client/nodes/PromptNode.tsx +597 -0
  164. package/src/client/nodes/ResponseNode.tsx +153 -0
  165. package/src/client/nodes/StatusNode.tsx +84 -0
  166. package/src/client/nodes/StatusSummary.tsx +38 -0
  167. package/src/client/nodes/TraceNode.tsx +120 -0
  168. package/src/client/nodes/WebpageNode.tsx +288 -0
  169. package/src/client/nodes/image-warnings.ts +95 -0
  170. package/src/client/nodes/inline-editor-commands.ts +37 -0
  171. package/src/client/nodes/md-format.ts +206 -0
  172. package/src/client/state/attention-bridge.ts +328 -0
  173. package/src/client/state/attention-store.ts +73 -0
  174. package/src/client/state/canvas-store.ts +631 -0
  175. package/src/client/state/intent-bridge.ts +315 -0
  176. package/src/client/state/sse-bridge.ts +965 -0
  177. package/src/client/theme/global.css +3173 -0
  178. package/src/client/theme/tokens.ts +72 -0
  179. package/src/client/types-shims.d.ts +5 -0
  180. package/src/client/types.ts +81 -0
  181. package/src/client/utils/ext-app-tool-result.ts +4 -0
  182. package/src/client/utils/placement.ts +4 -0
  183. package/src/client/utils/platform.ts +2 -0
  184. package/src/json-render/catalog.ts +256 -0
  185. package/src/json-render/charts/components.tsx +198 -0
  186. package/src/json-render/charts/definitions.ts +81 -0
  187. package/src/json-render/charts/extra-components.tsx +267 -0
  188. package/src/json-render/charts/extra-definitions.ts +145 -0
  189. package/src/json-render/renderer/index.css +174 -0
  190. package/src/json-render/renderer/index.tsx +86 -0
  191. package/src/json-render/schema.ts +62 -0
  192. package/src/json-render/server.ts +597 -0
  193. package/src/mcp/server.ts +1377 -0
  194. package/src/server/agent-context.ts +242 -0
  195. package/src/server/artifact-paths.ts +17 -0
  196. package/src/server/canvas-operations.ts +1279 -0
  197. package/src/server/canvas-provenance.ts +243 -0
  198. package/src/server/canvas-schema.ts +432 -0
  199. package/src/server/canvas-serialization.ts +95 -0
  200. package/src/server/canvas-state.ts +1134 -0
  201. package/src/server/canvas-validation.ts +114 -0
  202. package/src/server/chart-template.ts +449 -0
  203. package/src/server/code-graph.ts +370 -0
  204. package/src/server/context-cards.ts +31 -0
  205. package/src/server/diagram-presets.ts +71 -0
  206. package/src/server/ext-app-call-registry.ts +77 -0
  207. package/src/server/ext-app-tool-result.ts +4 -0
  208. package/src/server/file-watcher.ts +121 -0
  209. package/src/server/index.ts +647 -0
  210. package/src/server/mcp-app-candidate.ts +174 -0
  211. package/src/server/mcp-app-host.ts +814 -0
  212. package/src/server/mcp-app-runtime.ts +459 -0
  213. package/src/server/mutation-history.ts +350 -0
  214. package/src/server/placement.ts +125 -0
  215. package/src/server/server.ts +3846 -0
  216. package/src/server/spatial-analysis.ts +356 -0
  217. package/src/server/trace-manager.ts +333 -0
  218. package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
  219. package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
  220. package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
  221. package/src/server/web-artifacts.ts +442 -0
  222. package/src/server/webpage-node.ts +328 -0
  223. package/src/shared/auto-arrange.ts +439 -0
  224. package/src/shared/ext-app-tool-result.ts +76 -0
  225. package/src/shared/placement.ts +81 -0
  226. package/src/shared/semantic-attention.ts +598 -0
@@ -0,0 +1,243 @@
1
+ import { pathToFileURL } from 'node:url';
2
+
3
+ export type CanvasNodeType =
4
+ | 'markdown'
5
+ | 'mcp-app'
6
+ | 'webpage'
7
+ | 'json-render'
8
+ | 'graph'
9
+ | 'prompt'
10
+ | 'response'
11
+ | 'status'
12
+ | 'context'
13
+ | 'ledger'
14
+ | 'trace'
15
+ | 'file'
16
+ | 'image'
17
+ | 'group';
18
+
19
+ export type CanvasNodeProvenanceSourceKind =
20
+ | 'workspace-file'
21
+ | 'webpage-url'
22
+ | 'mcp-tool'
23
+ | 'artifact-file'
24
+ | 'image-url';
25
+
26
+ export type CanvasNodeRefreshStrategy =
27
+ | 'file-watch'
28
+ | 'file-read-write'
29
+ | 'image-reload'
30
+ | 'webpage-refresh'
31
+ | 'mcp-app-rehydrate'
32
+ | 'artifact-reopen';
33
+
34
+ export interface CanvasNodeProvenance {
35
+ sourceKind: CanvasNodeProvenanceSourceKind;
36
+ sourceUri: string;
37
+ refreshStrategy: CanvasNodeRefreshStrategy;
38
+ snapshotContent: boolean;
39
+ syncedAt?: string;
40
+ details?: Record<string, unknown>;
41
+ }
42
+
43
+ function isRecord(value: unknown): value is Record<string, unknown> {
44
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
45
+ }
46
+
47
+ function pickString(value: unknown): string | null {
48
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
49
+ }
50
+
51
+ function toFileUri(path: string): string {
52
+ try {
53
+ return pathToFileURL(path).toString();
54
+ } catch {
55
+ return `file://${path}`;
56
+ }
57
+ }
58
+
59
+ function buildMcpToolUri(serverName: string, toolName: string): string {
60
+ return `mcp-tool://${encodeURIComponent(serverName)}/${encodeURIComponent(toolName)}`;
61
+ }
62
+
63
+ function normalizeExistingProvenance(value: unknown): CanvasNodeProvenance | null {
64
+ if (!isRecord(value)) return null;
65
+
66
+ const sourceKind = pickString(value.sourceKind);
67
+ const sourceUri = pickString(value.sourceUri);
68
+ const refreshStrategy = pickString(value.refreshStrategy);
69
+ if (!sourceKind || !sourceUri || !refreshStrategy) return null;
70
+
71
+ const normalized: CanvasNodeProvenance = {
72
+ sourceKind: sourceKind as CanvasNodeProvenanceSourceKind,
73
+ sourceUri,
74
+ refreshStrategy: refreshStrategy as CanvasNodeRefreshStrategy,
75
+ snapshotContent: value.snapshotContent !== false,
76
+ };
77
+
78
+ const syncedAt = pickString(value.syncedAt);
79
+ if (syncedAt) normalized.syncedAt = syncedAt;
80
+ if (isRecord(value.details) && Object.keys(value.details).length > 0) {
81
+ normalized.details = { ...value.details };
82
+ }
83
+
84
+ return normalized;
85
+ }
86
+
87
+ function mergeProvenance(
88
+ existing: CanvasNodeProvenance | null,
89
+ inferred: CanvasNodeProvenance | null,
90
+ ): CanvasNodeProvenance | null {
91
+ if (!inferred) return existing;
92
+ if (!existing) return inferred;
93
+
94
+ const mergedDetails = {
95
+ ...(existing.details ?? {}),
96
+ ...(inferred.details ?? {}),
97
+ };
98
+
99
+ return {
100
+ ...existing,
101
+ ...inferred,
102
+ ...(Object.keys(mergedDetails).length > 0 ? { details: mergedDetails } : {}),
103
+ };
104
+ }
105
+
106
+ function inferFileProvenance(
107
+ nodeType: 'markdown' | 'file',
108
+ data: Record<string, unknown>,
109
+ ): CanvasNodeProvenance | null {
110
+ const path = pickString(data.path);
111
+ if (!path) return null;
112
+
113
+ const syncedAt = pickString(data.updatedAt) ?? pickString(data.savedAt);
114
+ return {
115
+ sourceKind: 'workspace-file',
116
+ sourceUri: toFileUri(path),
117
+ refreshStrategy: nodeType === 'file' ? 'file-watch' : 'file-read-write',
118
+ snapshotContent: true,
119
+ ...(syncedAt ? { syncedAt } : {}),
120
+ details: {
121
+ path,
122
+ nodeType,
123
+ },
124
+ };
125
+ }
126
+
127
+ function inferImageProvenance(data: Record<string, unknown>): CanvasNodeProvenance | null {
128
+ const path = pickString(data.path);
129
+ if (path) {
130
+ return {
131
+ sourceKind: 'workspace-file',
132
+ sourceUri: toFileUri(path),
133
+ refreshStrategy: 'image-reload',
134
+ snapshotContent: true,
135
+ details: { path, nodeType: 'image' },
136
+ };
137
+ }
138
+
139
+ const src = pickString(data.src);
140
+ if (!src || !/^https?:\/\//i.test(src)) return null;
141
+ return {
142
+ sourceKind: 'image-url',
143
+ sourceUri: src,
144
+ refreshStrategy: 'image-reload',
145
+ snapshotContent: true,
146
+ details: { url: src, nodeType: 'image' },
147
+ };
148
+ }
149
+
150
+ function inferWebpageProvenance(data: Record<string, unknown>): CanvasNodeProvenance | null {
151
+ const url = pickString(data.url);
152
+ if (!url) return null;
153
+
154
+ const details: Record<string, unknown> = { url, nodeType: 'webpage' };
155
+ const pageTitle = pickString(data.pageTitle);
156
+ if (pageTitle) details.pageTitle = pageTitle;
157
+
158
+ const syncedAt = pickString(data.fetchedAt);
159
+ return {
160
+ sourceKind: 'webpage-url',
161
+ sourceUri: url,
162
+ refreshStrategy: 'webpage-refresh',
163
+ snapshotContent: true,
164
+ ...(syncedAt ? { syncedAt } : {}),
165
+ details,
166
+ };
167
+ }
168
+
169
+ function inferMcpAppProvenance(data: Record<string, unknown>): CanvasNodeProvenance | null {
170
+ const path = pickString(data.path);
171
+ const url = pickString(data.url);
172
+ if (path && url?.startsWith('/artifact?')) {
173
+ return {
174
+ sourceKind: 'artifact-file',
175
+ sourceUri: toFileUri(path),
176
+ refreshStrategy: 'artifact-reopen',
177
+ snapshotContent: true,
178
+ details: {
179
+ path,
180
+ url,
181
+ nodeType: 'mcp-app',
182
+ },
183
+ };
184
+ }
185
+
186
+ const serverName = pickString(data.serverName) ?? pickString(data.sourceServer);
187
+ const toolName = pickString(data.toolName) ?? pickString(data.sourceTool);
188
+ if (!serverName && !toolName) return null;
189
+
190
+ const normalizedServer = serverName ?? 'unknown-server';
191
+ const normalizedTool = toolName ?? 'unknown-tool';
192
+ const details: Record<string, unknown> = {
193
+ serverName: normalizedServer,
194
+ toolName: normalizedTool,
195
+ nodeType: 'mcp-app',
196
+ };
197
+
198
+ const resourceUri = pickString(data.resourceUri);
199
+ if (resourceUri) details.resourceUri = resourceUri;
200
+ const transportType = isRecord(data.transportConfig) ? pickString(data.transportConfig.type) : null;
201
+ if (transportType) details.transportType = transportType;
202
+ if (isRecord(data.toolInput)) details.toolInput = data.toolInput;
203
+
204
+ return {
205
+ sourceKind: 'mcp-tool',
206
+ sourceUri: buildMcpToolUri(normalizedServer, normalizedTool),
207
+ refreshStrategy: 'mcp-app-rehydrate',
208
+ snapshotContent: true,
209
+ details,
210
+ };
211
+ }
212
+
213
+ export function inferCanvasNodeProvenance(
214
+ nodeType: CanvasNodeType,
215
+ data: Record<string, unknown>,
216
+ ): CanvasNodeProvenance | null {
217
+ if (nodeType === 'file' || nodeType === 'markdown') {
218
+ return inferFileProvenance(nodeType, data);
219
+ }
220
+ if (nodeType === 'image') return inferImageProvenance(data);
221
+ if (nodeType === 'webpage') return inferWebpageProvenance(data);
222
+ if (nodeType === 'mcp-app') return inferMcpAppProvenance(data);
223
+ return null;
224
+ }
225
+
226
+ export function normalizeCanvasNodeData<T extends Record<string, unknown>>(
227
+ nodeType: CanvasNodeType,
228
+ data: T,
229
+ ): T {
230
+ const existing = normalizeExistingProvenance(data.provenance);
231
+ const inferred = inferCanvasNodeProvenance(nodeType, data);
232
+ const provenance = mergeProvenance(existing, inferred);
233
+
234
+ if (provenance) {
235
+ return { ...data, provenance } as T;
236
+ }
237
+ if ('provenance' in data) {
238
+ const nextData = { ...data };
239
+ delete nextData.provenance;
240
+ return nextData as T;
241
+ }
242
+ return data;
243
+ }
@@ -0,0 +1,432 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { describeJsonRenderCatalog, type JsonRenderComponentDescriptor } from '../json-render/catalog.js';
4
+ import {
5
+ buildGraphSpec,
6
+ normalizeAndValidateJsonRenderSpec,
7
+ normalizeGraphType,
8
+ type GraphNodeInput,
9
+ type JsonRenderSpec,
10
+ } from '../json-render/server.js';
11
+
12
+ export interface CanvasCreateField {
13
+ name: string;
14
+ type: string;
15
+ required: boolean;
16
+ description: string;
17
+ aliases?: string[];
18
+ }
19
+
20
+ export interface CanvasCreateTypeSchema {
21
+ type: string;
22
+ kind: 'node' | 'virtual-node';
23
+ description: string;
24
+ endpoint: string;
25
+ fields: CanvasCreateField[];
26
+ example: Record<string, unknown>;
27
+ notes?: string[];
28
+ }
29
+
30
+ export interface StructuredValidationResult {
31
+ ok: true;
32
+ type: 'json-render' | 'graph';
33
+ normalizedSpec: JsonRenderSpec;
34
+ summary: Record<string, unknown>;
35
+ }
36
+
37
+ const CANONICAL_GRAPH_TYPES = [
38
+ 'line',
39
+ 'bar',
40
+ 'pie',
41
+ 'area',
42
+ 'scatter',
43
+ 'radar',
44
+ 'stacked-bar',
45
+ 'composed',
46
+ ] as const;
47
+
48
+ type CanvasGraphType = typeof CANONICAL_GRAPH_TYPES[number];
49
+
50
+ function readPackageVersion(): string | null {
51
+ try {
52
+ const raw = readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf-8');
53
+ const parsed = JSON.parse(raw) as { version?: unknown };
54
+ return typeof parsed.version === 'string' ? parsed.version : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
61
+ {
62
+ type: 'markdown',
63
+ kind: 'node',
64
+ description: 'Freeform markdown note.',
65
+ endpoint: '/api/canvas/node',
66
+ fields: [
67
+ { name: 'title', type: 'string', required: false, description: 'Optional node title.' },
68
+ { name: 'content', type: 'string', required: false, description: 'Markdown body.' },
69
+ { name: 'x', type: 'number', required: false, description: 'Optional X position.' },
70
+ { name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
71
+ { name: 'width', type: 'number', required: false, description: 'Optional node width.' },
72
+ { name: 'height', type: 'number', required: false, description: 'Optional node height.' },
73
+ ],
74
+ example: {
75
+ type: 'markdown',
76
+ title: 'Design Doc',
77
+ content: '# Overview\n\nCapture the working plan here.',
78
+ },
79
+ notes: [
80
+ 'When a markdown node is path-backed (`data.path`), the server persists `data.provenance` so snapshots keep a refreshable file source.',
81
+ ],
82
+ },
83
+ {
84
+ type: 'status',
85
+ kind: 'node',
86
+ description: 'Compact status indicator.',
87
+ endpoint: '/api/canvas/node',
88
+ fields: [
89
+ { name: 'title', type: 'string', required: false, description: 'Status label.' },
90
+ { name: 'content', type: 'string', required: false, description: 'Rendered status text.' },
91
+ ],
92
+ example: {
93
+ type: 'status',
94
+ title: 'Build',
95
+ content: 'passing',
96
+ },
97
+ },
98
+ {
99
+ type: 'context',
100
+ kind: 'node',
101
+ description: 'Agent context card container.',
102
+ endpoint: '/api/canvas/node',
103
+ fields: [
104
+ { name: 'title', type: 'string', required: false, description: 'Optional title override.' },
105
+ { name: 'content', type: 'string', required: false, description: 'Optional context body.' },
106
+ ],
107
+ example: {
108
+ type: 'context',
109
+ title: 'Pinned Context',
110
+ content: 'Human-curated context summary.',
111
+ },
112
+ },
113
+ {
114
+ type: 'ledger',
115
+ kind: 'node',
116
+ description: 'Structured ledger/log node.',
117
+ endpoint: '/api/canvas/node',
118
+ fields: [
119
+ { name: 'title', type: 'string', required: false, description: 'Optional title.' },
120
+ { name: 'content', type: 'string', required: false, description: 'Ledger body text.' },
121
+ ],
122
+ example: {
123
+ type: 'ledger',
124
+ title: 'Decision Log',
125
+ content: '- Chose SSE for realtime sync',
126
+ },
127
+ },
128
+ {
129
+ type: 'trace',
130
+ kind: 'node',
131
+ description: 'Execution trace viewer.',
132
+ endpoint: '/api/canvas/node',
133
+ fields: [
134
+ { name: 'title', type: 'string', required: false, description: 'Optional title.' },
135
+ { name: 'content', type: 'string', required: false, description: 'Trace summary.' },
136
+ ],
137
+ example: {
138
+ type: 'trace',
139
+ title: 'Execution Trace',
140
+ content: 'Canvas actions and tool events.',
141
+ },
142
+ },
143
+ {
144
+ type: 'file',
145
+ kind: 'node',
146
+ description: 'Workspace file viewer.',
147
+ endpoint: '/api/canvas/node',
148
+ fields: [
149
+ { name: 'content', type: 'string', required: true, description: 'Workspace-relative or absolute file path.' },
150
+ { name: 'title', type: 'string', required: false, description: 'Optional title override.' },
151
+ ],
152
+ example: {
153
+ type: 'file',
154
+ content: 'src/server/server.ts',
155
+ },
156
+ notes: [
157
+ 'Path-backed file nodes automatically persist `data.provenance` with a file URI and file-watch refresh strategy.',
158
+ ],
159
+ },
160
+ {
161
+ type: 'image',
162
+ kind: 'node',
163
+ description: 'Image node backed by a path, URL, or data URI.',
164
+ endpoint: '/api/canvas/node',
165
+ fields: [
166
+ { name: 'content', type: 'string', required: true, description: 'Image path, URL, or data URI.' },
167
+ { name: 'title', type: 'string', required: false, description: 'Optional title override.' },
168
+ { name: 'data.warning', type: 'string | { title?: string; detail: string }', required: false, description: 'Optional agent-supplied warning shown above the image.' },
169
+ { name: 'data.warnings', type: 'Array<string | { title?: string; detail: string }>', required: false, description: 'Optional list of agent-supplied image warnings.' },
170
+ { name: 'data.validationStatus', type: '"passed" | "failed" | "invalid"', required: false, description: 'Optional agent validation result for evidence-style images.' },
171
+ { name: 'data.validationMessage', type: 'string', required: false, description: 'Optional detail shown when validation fails.' },
172
+ ],
173
+ example: {
174
+ type: 'image',
175
+ content: 'artifacts/architecture.png',
176
+ data: {
177
+ validationStatus: 'failed',
178
+ validationMessage: 'Captured login page instead of the intended dashboard.',
179
+ },
180
+ },
181
+ notes: [
182
+ 'File-backed and HTTP(S)-backed images automatically persist `data.provenance` so agents can tell whether the node came from disk or a remote URL.',
183
+ ],
184
+ },
185
+ {
186
+ type: 'webpage',
187
+ kind: 'node',
188
+ description: 'Persisted webpage snapshot with server-side fetch and refresh.',
189
+ endpoint: '/api/canvas/node',
190
+ fields: [
191
+ { name: 'url', type: 'string', required: true, description: 'HTTP(S) URL to fetch and cache.', aliases: ['content'] },
192
+ { name: 'title', type: 'string', required: false, description: 'Optional title override.' },
193
+ { name: 'x', type: 'number', required: false, description: 'Optional X position.' },
194
+ { name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
195
+ { name: 'width', type: 'number', required: false, description: 'Optional node width.' },
196
+ { name: 'height', type: 'number', required: false, description: 'Optional node height.' },
197
+ ],
198
+ example: {
199
+ type: 'webpage',
200
+ title: 'PMX Canvas README',
201
+ url: 'https://example.com/docs',
202
+ },
203
+ notes: [
204
+ '`url` is the canonical field. `content` is still accepted for backward compatibility.',
205
+ 'Webpage nodes persist `data.provenance` with the source URL and refresh strategy so reopened snapshots can be re-fetched.',
206
+ ],
207
+ },
208
+ {
209
+ type: 'mcp-app',
210
+ kind: 'node',
211
+ description: 'Hosted iframe/app node.',
212
+ endpoint: '/api/canvas/node',
213
+ fields: [
214
+ { name: 'title', type: 'string', required: false, description: 'App title.' },
215
+ { name: 'content', type: 'string', required: false, description: 'Optional inline content.' },
216
+ ],
217
+ example: {
218
+ type: 'mcp-app',
219
+ title: 'Embedded App',
220
+ content: 'Use the dedicated tool for artifact builds when possible.',
221
+ },
222
+ notes: [
223
+ 'Tool-backed MCP app nodes and hosted artifact nodes persist `data.provenance` when the server can infer a reopen or rehydrate path.',
224
+ ],
225
+ },
226
+ {
227
+ type: 'group',
228
+ kind: 'node',
229
+ description: 'Canvas group frame.',
230
+ endpoint: '/api/canvas/group',
231
+ fields: [
232
+ { name: 'title', type: 'string', required: false, description: 'Group title.' },
233
+ { name: 'childIds', type: 'string[]', required: false, description: 'Initial child node IDs.' },
234
+ { name: 'childLayout', type: '"grid" | "column" | "flow"', required: false, description: 'Optional layout for grouped children.' },
235
+ ],
236
+ example: {
237
+ title: 'API Layer',
238
+ childIds: ['node-a', 'node-b'],
239
+ childLayout: 'column',
240
+ },
241
+ },
242
+ {
243
+ type: 'json-render',
244
+ kind: 'virtual-node',
245
+ description: 'Native structured UI panel rendered from a validated json-render spec.',
246
+ endpoint: '/api/canvas/json-render',
247
+ fields: [
248
+ { name: 'title', type: 'string', required: true, description: 'Rendered node title.' },
249
+ { name: 'spec', type: 'JsonRenderSpec', required: true, description: 'Complete json-render spec.' },
250
+ { name: 'x', type: 'number', required: false, description: 'Optional X position.' },
251
+ { name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
252
+ { name: 'width', type: 'number', required: false, description: 'Optional node width.' },
253
+ { name: 'height', type: 'number', required: false, description: 'Optional node height.' },
254
+ ],
255
+ example: {
256
+ title: 'Ops Dashboard',
257
+ spec: {
258
+ root: 'card',
259
+ elements: {
260
+ card: {
261
+ type: 'Card',
262
+ props: { title: 'Ops Dashboard' },
263
+ children: ['copy'],
264
+ },
265
+ copy: {
266
+ type: 'Text',
267
+ props: { text: 'Live service summary' },
268
+ children: [],
269
+ },
270
+ },
271
+ },
272
+ },
273
+ },
274
+ {
275
+ type: 'graph',
276
+ kind: 'virtual-node',
277
+ description: 'Native chart node backed by the json-render chart catalog.',
278
+ endpoint: '/api/canvas/graph',
279
+ fields: [
280
+ {
281
+ name: 'graphType',
282
+ type: '"line" | "bar" | "pie" | "area" | "scatter" | "radar" | "stacked-bar" | "composed"',
283
+ required: true,
284
+ description: 'Chart type. Aliases like "stack" and "combo" are normalized server-side.',
285
+ },
286
+ { name: 'data', type: 'Record<string, unknown>[]', required: true, description: 'Chart dataset.' },
287
+ { name: 'title', type: 'string', required: false, description: 'Optional graph title.' },
288
+ { name: 'xKey', type: 'string', required: false, description: 'X-axis/category key for line, bar, area, scatter, stacked-bar, and composed charts.' },
289
+ { name: 'yKey', type: 'string', required: false, description: 'Y-axis value key for line, bar, area, and scatter charts. Also used as a fallback bar key for composed charts.' },
290
+ { name: 'zKey', type: 'string', required: false, description: 'Optional bubble-size key for scatter charts.' },
291
+ { name: 'nameKey', type: 'string', required: false, description: 'Slice name key for pie graphs.' },
292
+ { name: 'valueKey', type: 'string', required: false, description: 'Slice value key for pie graphs.' },
293
+ { name: 'axisKey', type: 'string', required: false, description: 'Category key for radar charts.' },
294
+ { name: 'metrics', type: 'string[]', required: false, description: 'Series keys to plot as radar polygons. Defaults to non-axis numeric columns.' },
295
+ { name: 'series', type: 'string[]', required: false, description: 'Series keys for stacked-bar segments. Defaults to non-x numeric columns.' },
296
+ { name: 'barKey', type: 'string', required: false, description: 'Bar series key for composed charts.' },
297
+ { name: 'lineKey', type: 'string', required: false, description: 'Line series key for composed charts.' },
298
+ { name: 'aggregate', type: '"sum" | "count" | "avg"', required: false, description: 'Optional aggregation for repeated x-axis values in line, bar, area, and stacked-bar charts.' },
299
+ { name: 'color', type: 'string', required: false, description: 'Optional series color for line, bar, area, and scatter charts.' },
300
+ { name: 'barColor', type: 'string', required: false, description: 'Optional bar color for composed charts.' },
301
+ { name: 'lineColor', type: 'string', required: false, description: 'Optional line color for composed charts.' },
302
+ { name: 'height', type: 'number', required: false, description: 'Optional chart content height.' },
303
+ { name: 'width', type: 'number', required: false, description: 'Optional node width.' },
304
+ { name: 'nodeHeight', type: 'number', required: false, description: 'Optional node height.' },
305
+ ],
306
+ example: {
307
+ title: 'Deploy Trend',
308
+ graphType: 'line',
309
+ data: [
310
+ { day: 'Mon', value: 3 },
311
+ { day: 'Tue', value: 5 },
312
+ { day: 'Wed', value: 4 },
313
+ ],
314
+ xKey: 'day',
315
+ yKey: 'value',
316
+ },
317
+ notes: [
318
+ 'Canonical graph types are line, bar, pie, area, scatter, radar, stacked-bar, and composed.',
319
+ 'Server-side validation normalizes aliases like "stack" -> "stacked-bar" and "combo" -> "composed".',
320
+ ],
321
+ },
322
+ {
323
+ type: 'web-artifact',
324
+ kind: 'virtual-node',
325
+ description: 'Bundled single-file HTML artifact that can open as an embedded canvas node.',
326
+ endpoint: '/api/canvas/web-artifact',
327
+ fields: [
328
+ { name: 'title', type: 'string', required: true, description: 'Artifact title used for default paths.' },
329
+ { name: 'appTsx', type: 'string', required: true, description: 'Contents for src/App.tsx.', aliases: ['stdin', 'app-file', 'app-tsx'] },
330
+ { name: 'indexCss', type: 'string', required: false, description: 'Optional src/index.css contents.', aliases: ['index-css-file', 'index-css'] },
331
+ { name: 'mainTsx', type: 'string', required: false, description: 'Optional src/main.tsx contents.', aliases: ['main-file', 'main-tsx'] },
332
+ { name: 'indexHtml', type: 'string', required: false, description: 'Optional index.html contents.', aliases: ['index-html-file', 'index-html'] },
333
+ { name: 'projectPath', type: 'string', required: false, description: 'Optional project directory.' },
334
+ { name: 'outputPath', type: 'string', required: false, description: 'Optional output HTML path.' },
335
+ { name: 'openInCanvas', type: 'boolean', required: false, description: 'Open the built artifact on the canvas (default true).' },
336
+ { name: 'includeLogs', type: 'boolean', required: false, description: 'Include raw build stdout/stderr in the response (default false).' },
337
+ ],
338
+ example: {
339
+ title: 'Dashboard Artifact',
340
+ appTsx: 'export default function App() { return <main>Artifact</main>; }',
341
+ indexCss: 'body { background: #123456; color: white; }',
342
+ },
343
+ },
344
+ ];
345
+
346
+ function clone<T>(value: T): T {
347
+ return JSON.parse(JSON.stringify(value)) as T;
348
+ }
349
+
350
+ export function describeCanvasSchema(): {
351
+ ok: true;
352
+ source: 'running-server';
353
+ version: string | null;
354
+ nodeTypes: CanvasCreateTypeSchema[];
355
+ jsonRender: {
356
+ rootShape: Record<string, string>;
357
+ components: JsonRenderComponentDescriptor[];
358
+ };
359
+ graph: {
360
+ graphTypes: CanvasGraphType[];
361
+ };
362
+ mcp: {
363
+ tools: string[];
364
+ resources: string[];
365
+ };
366
+ } {
367
+ return {
368
+ ok: true,
369
+ source: 'running-server',
370
+ version: readPackageVersion(),
371
+ nodeTypes: clone(CANVAS_CREATE_TYPES),
372
+ jsonRender: {
373
+ rootShape: {
374
+ root: 'string',
375
+ elements: 'record<string, { type, props, children, visible? }>',
376
+ state: 'record<string, unknown> | optional',
377
+ },
378
+ components: clone(describeJsonRenderCatalog()),
379
+ },
380
+ graph: {
381
+ graphTypes: [...CANONICAL_GRAPH_TYPES],
382
+ },
383
+ mcp: {
384
+ tools: [
385
+ 'canvas_add_node',
386
+ 'canvas_add_json_render_node',
387
+ 'canvas_add_graph_node',
388
+ 'canvas_build_web_artifact',
389
+ 'canvas_describe_schema',
390
+ 'canvas_validate_spec',
391
+ ],
392
+ resources: ['canvas://schema'],
393
+ },
394
+ };
395
+ }
396
+
397
+ export function validateStructuredCanvasPayload(input: {
398
+ type: 'json-render' | 'graph';
399
+ spec?: unknown;
400
+ graph?: GraphNodeInput;
401
+ }): StructuredValidationResult {
402
+ if (input.type === 'json-render') {
403
+ const normalizedSpec = normalizeAndValidateJsonRenderSpec(input.spec);
404
+ return {
405
+ ok: true,
406
+ type: 'json-render',
407
+ normalizedSpec,
408
+ summary: {
409
+ root: normalizedSpec.root,
410
+ elementCount: Object.keys(normalizedSpec.elements).length,
411
+ stateKeys: Object.keys(normalizedSpec.state ?? {}).length,
412
+ },
413
+ };
414
+ }
415
+
416
+ if (!input.graph) {
417
+ throw new Error('Graph validation requires a graph payload.');
418
+ }
419
+
420
+ const normalizedSpec = buildGraphSpec(input.graph);
421
+ return {
422
+ ok: true,
423
+ type: 'graph',
424
+ normalizedSpec,
425
+ summary: {
426
+ graphType: normalizeGraphType(input.graph.graphType),
427
+ dataPoints: input.graph.data.length,
428
+ root: normalizedSpec.root,
429
+ elementCount: Object.keys(normalizedSpec.elements).length,
430
+ },
431
+ };
432
+ }