pmx-canvas 0.4.5 → 0.4.6
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 +56 -0
- package/Readme.md +21 -4
- package/dist/canvas/global.css +68 -0
- package/dist/canvas/index.js +91 -70
- package/dist/canvas/mermaid-entry.js +3563 -0
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/smoke.d.ts +1 -0
- package/dist/types/client/mermaid-entry.d.ts +11 -0
- package/dist/types/client/nodes/DiffNode.d.ts +20 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +0 -2
- package/dist/types/client/nodes/MermaidNode.d.ts +12 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +10 -1
- package/dist/types/server/canvas-validation.d.ts +34 -1
- package/dist/types/server/html-surface.d.ts +8 -0
- package/dist/types/server/index.d.ts +5 -0
- package/dist/types/server/operations/ops/nodes.d.ts +1 -1
- package/dist/types/server/workboard.d.ts +46 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +7 -0
- package/docs/cli.md +12 -0
- package/docs/http-api.md +12 -0
- package/docs/mcp.md +1 -1
- package/docs/node-types.md +2 -0
- package/docs/screenshot.png +0 -0
- package/package.json +4 -3
- package/skills/pmx-canvas/SKILL.md +56 -19
- package/skills/pmx-canvas-testing/SKILL.md +3 -0
- package/src/cli/agent.ts +2 -0
- package/src/cli/commands/ax.ts +28 -3
- package/src/cli/commands/smoke.ts +212 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/shared.ts +1 -0
- package/src/client/canvas/CanvasViewport.tsx +6 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +10 -0
- package/src/client/canvas/Minimap.tsx +2 -0
- package/src/client/canvas/auto-fit.ts +2 -0
- package/src/client/icons.tsx +4 -0
- package/src/client/mermaid-entry.ts +76 -0
- package/src/client/nodes/DiffNode.tsx +114 -0
- package/src/client/nodes/ExtAppFrame.tsx +259 -84
- package/src/client/nodes/MermaidNode.tsx +81 -0
- package/src/client/state/node-factory.ts +2 -0
- package/src/client/state/sse-bridge.ts +28 -0
- package/src/client/theme/global.css +68 -0
- package/src/client/types.ts +6 -0
- package/src/mcp/server.ts +19 -1
- package/src/server/ax-interaction.ts +6 -0
- package/src/server/canvas-operations.ts +39 -9
- package/src/server/canvas-provenance.ts +2 -0
- package/src/server/canvas-schema.ts +33 -0
- package/src/server/canvas-state.ts +26 -0
- package/src/server/canvas-validation.ts +79 -0
- package/src/server/html-surface.ts +11 -0
- package/src/server/index.ts +25 -6
- package/src/server/operations/composites.ts +3 -2
- package/src/server/operations/ops/batch.ts +8 -0
- package/src/server/operations/ops/json-render.ts +44 -0
- package/src/server/operations/ops/nodes.ts +9 -1
- package/src/server/operations/ops/viewport.ts +8 -1
- package/src/server/server.ts +48 -1
- package/src/server/workboard.ts +159 -0
- package/src/shared/ax-surface-protocol.ts +8 -0
- package/src/shared/surface.ts +2 -0
package/src/cli/index.ts
CHANGED
package/src/cli/shared.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { ContextNode } from '../nodes/ContextNode';
|
|
3
|
+
import { DiffNode } from '../nodes/DiffNode';
|
|
3
4
|
import { FileNode } from '../nodes/FileNode';
|
|
4
5
|
import { LedgerNode } from '../nodes/LedgerNode';
|
|
5
6
|
import { MarkdownNode } from '../nodes/MarkdownNode';
|
|
7
|
+
import { MermaidNode } from '../nodes/MermaidNode';
|
|
6
8
|
import { McpAppNode } from '../nodes/McpAppNode';
|
|
7
9
|
import { StatusNode } from '../nodes/StatusNode';
|
|
8
10
|
import { ImageNode } from '../nodes/ImageNode';
|
|
@@ -64,6 +66,10 @@ function renderNodeContent(node: CanvasNodeState) {
|
|
|
64
66
|
return <TraceNode node={node} />;
|
|
65
67
|
case 'file':
|
|
66
68
|
return <FileNode node={node} />;
|
|
69
|
+
case 'diff':
|
|
70
|
+
return <DiffNode node={node} />;
|
|
71
|
+
case 'mermaid':
|
|
72
|
+
return <MermaidNode node={node} />;
|
|
67
73
|
case 'image':
|
|
68
74
|
return <ImageNode node={node} />;
|
|
69
75
|
case 'html':
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { HTML_SURFACE_PUSH_SOURCE } from '../../shared/ax-surface-protocol.js';
|
|
2
2
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
3
3
|
import { ContextNode } from '../nodes/ContextNode';
|
|
4
|
+
import { DiffNode } from '../nodes/DiffNode';
|
|
4
5
|
import { FileNode } from '../nodes/FileNode';
|
|
5
6
|
import { LedgerNode } from '../nodes/LedgerNode';
|
|
6
7
|
import { MarkdownNode } from '../nodes/MarkdownNode';
|
|
8
|
+
import { MermaidNode } from '../nodes/MermaidNode';
|
|
7
9
|
import { McpAppNode } from '../nodes/McpAppNode';
|
|
8
10
|
import { StatusNode } from '../nodes/StatusNode';
|
|
9
11
|
import { ImageNode } from '../nodes/ImageNode';
|
|
@@ -50,6 +52,10 @@ function renderContent(node: CanvasNodeState, expanded: boolean) {
|
|
|
50
52
|
return <TraceNode node={node} />;
|
|
51
53
|
case 'file':
|
|
52
54
|
return <FileNode node={node} expanded={expanded} />;
|
|
55
|
+
case 'diff':
|
|
56
|
+
return <DiffNode node={node} />;
|
|
57
|
+
case 'mermaid':
|
|
58
|
+
return <MermaidNode node={node} expanded={expanded} />;
|
|
53
59
|
case 'image':
|
|
54
60
|
return <ImageNode node={node} expanded={expanded} />;
|
|
55
61
|
case 'html':
|
|
@@ -64,6 +70,10 @@ function getNodeTextContent(node: CanvasNodeState): string {
|
|
|
64
70
|
switch (node.type) {
|
|
65
71
|
case 'markdown':
|
|
66
72
|
return (node.data.content as string) || '';
|
|
73
|
+
case 'diff':
|
|
74
|
+
return (node.data.content as string) || '';
|
|
75
|
+
case 'mermaid':
|
|
76
|
+
return (node.data.content as string) || '';
|
|
67
77
|
case 'file':
|
|
68
78
|
return (node.data.fileContent as string) || '';
|
|
69
79
|
case 'webpage':
|
|
@@ -21,6 +21,7 @@ export const AUTO_FIT_BODY_PADDING = 24;
|
|
|
21
21
|
function isIframeNode(node: CanvasNodeState): boolean {
|
|
22
22
|
return (
|
|
23
23
|
node.type === 'html' ||
|
|
24
|
+
node.type === 'mermaid' ||
|
|
24
25
|
node.type === 'json-render' ||
|
|
25
26
|
node.type === 'graph' ||
|
|
26
27
|
node.type === 'mcp-app' ||
|
|
@@ -33,6 +34,7 @@ function isIframeNode(node: CanvasNodeState): boolean {
|
|
|
33
34
|
* viewers (unbounded/scrolling content that must not drive node height). */
|
|
34
35
|
function isContentFitSurface(node: CanvasNodeState): boolean {
|
|
35
36
|
if (node.type === 'html') return node.data.presentation !== true;
|
|
37
|
+
if (node.type === 'mermaid') return true;
|
|
36
38
|
if (node.type === 'json-render' || node.type === 'graph') return true;
|
|
37
39
|
if (node.type === 'mcp-app') return node.data.viewerType === 'web-artifact';
|
|
38
40
|
return false;
|
package/src/client/icons.tsx
CHANGED
|
@@ -473,6 +473,10 @@ export function getNodeIcon(type: string): (p: IconProps) => JSX.Element {
|
|
|
473
473
|
return IconNodeResponse;
|
|
474
474
|
case 'file':
|
|
475
475
|
return IconNodeFile;
|
|
476
|
+
case 'diff':
|
|
477
|
+
return IconNodeFile;
|
|
478
|
+
case 'mermaid':
|
|
479
|
+
return IconNodeGraph;
|
|
476
480
|
case 'image':
|
|
477
481
|
return IconNodeImage;
|
|
478
482
|
case 'webpage':
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface-side renderer for `mermaid` nodes — the entry point of the separately
|
|
3
|
+
* built dist/canvas/mermaid-entry.js bundle (see package.json build:client).
|
|
4
|
+
* The server (handleNodeSurface) serves a themed surface document containing the
|
|
5
|
+
* HTML-escaped diagram source in `<pre class="mermaid-source">`; this script
|
|
6
|
+
* reads it back via textContent, renders it to SVG, and re-renders when the
|
|
7
|
+
* parent canvas live-switches the theme (the theme bridge toggles the document's
|
|
8
|
+
* `data-theme` attribute). Keeping mermaid out of the main SPA bundle keeps the
|
|
9
|
+
* canvas boot lean — only mermaid-node surfaces pay for the library.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import mermaid from 'mermaid';
|
|
13
|
+
import { canvasThemeScheme } from '../shared/themes.js';
|
|
14
|
+
|
|
15
|
+
function documentScheme(): 'dark' | 'light' {
|
|
16
|
+
return canvasThemeScheme(document.documentElement.getAttribute('data-theme'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function showError(container: HTMLElement, message: string): void {
|
|
20
|
+
container.innerHTML = '';
|
|
21
|
+
const block = document.createElement('pre');
|
|
22
|
+
block.className = 'mermaid-error';
|
|
23
|
+
block.style.cssText =
|
|
24
|
+
'margin:12px;padding:12px;border-radius:6px;color:var(--c-dim,#8a93a6);' +
|
|
25
|
+
'font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;';
|
|
26
|
+
block.textContent = message;
|
|
27
|
+
container.appendChild(block);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let renderSeq = 0;
|
|
31
|
+
|
|
32
|
+
async function renderDiagram(source: string, container: HTMLElement): Promise<void> {
|
|
33
|
+
const scheme = documentScheme();
|
|
34
|
+
mermaid.initialize({
|
|
35
|
+
startOnLoad: false,
|
|
36
|
+
theme: scheme === 'dark' ? 'dark' : 'default',
|
|
37
|
+
securityLevel: 'strict',
|
|
38
|
+
});
|
|
39
|
+
renderSeq += 1;
|
|
40
|
+
try {
|
|
41
|
+
const { svg } = await mermaid.render(`pmx-mermaid-${renderSeq}`, source);
|
|
42
|
+
container.innerHTML = svg;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Never leave a blank/black frame: show the parse/render error instead.
|
|
45
|
+
showError(container, error instanceof Error ? error.message : String(error));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function boot(): void {
|
|
50
|
+
const sourceEl = document.querySelector('.mermaid-source');
|
|
51
|
+
const source = sourceEl?.textContent ?? '';
|
|
52
|
+
const container = document.createElement('div');
|
|
53
|
+
container.className = 'mermaid-diagram';
|
|
54
|
+
document.body.appendChild(container);
|
|
55
|
+
if (!source.trim()) {
|
|
56
|
+
showError(container, 'Empty mermaid diagram');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
void renderDiagram(source, container);
|
|
60
|
+
// Live theme switching: the parent's theme bridge toggles data-theme on the
|
|
61
|
+
// root element; re-render so the diagram palette follows the canvas theme.
|
|
62
|
+
let lastScheme = documentScheme();
|
|
63
|
+
const observer = new MutationObserver(() => {
|
|
64
|
+
const scheme = documentScheme();
|
|
65
|
+
if (scheme === lastScheme) return;
|
|
66
|
+
lastScheme = scheme;
|
|
67
|
+
void renderDiagram(source, container);
|
|
68
|
+
});
|
|
69
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (document.readyState === 'loading') {
|
|
73
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
74
|
+
} else {
|
|
75
|
+
boot();
|
|
76
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useMemo } from 'preact/hooks';
|
|
2
|
+
import type { CanvasNodeState } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface ParsedDiffLine {
|
|
5
|
+
kind: 'add' | 'remove' | 'context' | 'hunk';
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ParsedDiffSection {
|
|
10
|
+
/** Display path derived from the +++/--- headers or the `diff --git` line; null for headerless diffs. */
|
|
11
|
+
path: string | null;
|
|
12
|
+
lines: ParsedDiffLine[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function stripDiffPathPrefix(path: string): string {
|
|
16
|
+
return path.replace(/^[ab]\//, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Derive a display path from a `--- old` / `+++ new` header pair. */
|
|
20
|
+
function headerPairPath(oldPath: string, newPath: string): string | null {
|
|
21
|
+
if (newPath && newPath !== '/dev/null') return stripDiffPathPrefix(newPath);
|
|
22
|
+
if (oldPath && oldPath !== '/dev/null') return stripDiffPathPrefix(oldPath);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Split unified diff text into file sections, hunk headers, and classified
|
|
28
|
+
* lines. Recognizes `diff --git` boundaries and `---`/`+++` header pairs
|
|
29
|
+
* (never misclassifying them as remove/add); diffs without any file headers
|
|
30
|
+
* parse as a single anonymous section.
|
|
31
|
+
*/
|
|
32
|
+
export function parseUnifiedDiff(text: string): ParsedDiffSection[] {
|
|
33
|
+
if (!text.trim()) return [];
|
|
34
|
+
const rawLines = text.split('\n');
|
|
35
|
+
// Drop the trailing empty string produced by a terminating newline.
|
|
36
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === '') rawLines.pop();
|
|
37
|
+
|
|
38
|
+
const sections: ParsedDiffSection[] = [];
|
|
39
|
+
let current: ParsedDiffSection | null = null;
|
|
40
|
+
let currentHasHunk = false;
|
|
41
|
+
|
|
42
|
+
const startSection = (path: string | null): ParsedDiffSection => {
|
|
43
|
+
const section: ParsedDiffSection = { path, lines: [] };
|
|
44
|
+
currentHasHunk = false;
|
|
45
|
+
sections.push(section);
|
|
46
|
+
return section;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
50
|
+
const line = rawLines[i] ?? '';
|
|
51
|
+
|
|
52
|
+
const gitHeader = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
53
|
+
if (gitHeader) {
|
|
54
|
+
current = startSection(gitHeader[2] ?? null);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const next = rawLines[i + 1] ?? '';
|
|
59
|
+
if (line.startsWith('--- ') && next.startsWith('+++ ')) {
|
|
60
|
+
const path = headerPairPath(line.slice(4).trim(), next.slice(4).trim());
|
|
61
|
+
if (current && !currentHasHunk) {
|
|
62
|
+
// Header pair inside a `diff --git` preamble refines that section.
|
|
63
|
+
if (!current.path) current.path = path;
|
|
64
|
+
} else {
|
|
65
|
+
current = startSection(path);
|
|
66
|
+
}
|
|
67
|
+
i++; // Consume the `+++` line too — neither header is an add/remove line.
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!current) current = startSection(null);
|
|
72
|
+
const section = current;
|
|
73
|
+
|
|
74
|
+
if (line.startsWith('@@')) {
|
|
75
|
+
currentHasHunk = true;
|
|
76
|
+
section.lines.push({ kind: 'hunk', text: line });
|
|
77
|
+
} else if (line.startsWith('+')) {
|
|
78
|
+
section.lines.push({ kind: 'add', text: line });
|
|
79
|
+
} else if (line.startsWith('-')) {
|
|
80
|
+
section.lines.push({ kind: 'remove', text: line });
|
|
81
|
+
} else {
|
|
82
|
+
// Includes `` and git meta lines (index, mode).
|
|
83
|
+
section.lines.push({ kind: 'context', text: line });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return sections;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function DiffNode({ node }: { node: CanvasNodeState }) {
|
|
91
|
+
const content = typeof node.data.content === 'string' ? node.data.content : '';
|
|
92
|
+
const sections = useMemo(() => parseUnifiedDiff(content), [content]);
|
|
93
|
+
|
|
94
|
+
if (sections.length === 0) {
|
|
95
|
+
return <div class="diff-node diff-node-empty">Empty diff</div>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div class="diff-node">
|
|
100
|
+
{sections.map((section, sectionIndex) => (
|
|
101
|
+
<div class="diff-file" key={`${section.path ?? 'anonymous'}-${sectionIndex}`}>
|
|
102
|
+
{section.path && <div class="diff-file-header">{section.path}</div>}
|
|
103
|
+
<pre class="diff-lines">
|
|
104
|
+
{section.lines.map((line, lineIndex) => (
|
|
105
|
+
<div key={lineIndex} class={line.kind === 'hunk' ? 'diff-hunk' : `diff-line-${line.kind}`}>
|
|
106
|
+
{line.text || ' '}
|
|
107
|
+
</div>
|
|
108
|
+
))}
|
|
109
|
+
</pre>
|
|
110
|
+
</div>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|