pmx-canvas 0.3.0 → 0.3.2
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 +73 -0
- package/dist/canvas/index.js +65 -65
- package/dist/types/cli/agent.d.ts +9 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +9 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/server/server.d.ts +8 -0
- package/docs/cli.md +10 -1
- package/docs/http-api.md +28 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-07.md +2 -2
- package/package.json +5 -2
- package/skills/pmx-canvas/SKILL.md +15 -10
- package/skills/pmx-canvas/references/full-reference.md +17 -3
- package/src/cli/agent.ts +1951 -1571
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +80 -323
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +194 -80
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +35 -21
- package/src/mcp/server.ts +132 -70
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +182 -39
- package/src/server/ax-state.ts +142 -47
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +180 -123
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +454 -73
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +150 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +19 -6
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +3 -12
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +320 -217
- package/src/server/intent-registry.ts +2 -5
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +1 -3
- package/src/server/operations/http.ts +2 -3
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +22 -30
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +98 -73
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +41 -15
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +298 -109
- package/src/server/operations/ops/query.ts +46 -28
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +122 -1028
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
|
@@ -16,7 +16,13 @@ export function HtmlNode({
|
|
|
16
16
|
presentation = false,
|
|
17
17
|
presentationExitToken,
|
|
18
18
|
autoFocus = false,
|
|
19
|
-
}: {
|
|
19
|
+
}: {
|
|
20
|
+
node: CanvasNodeState;
|
|
21
|
+
expanded?: boolean;
|
|
22
|
+
presentation?: boolean;
|
|
23
|
+
presentationExitToken?: string;
|
|
24
|
+
autoFocus?: boolean;
|
|
25
|
+
}) {
|
|
20
26
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
21
27
|
const theme = canvasTheme.value;
|
|
22
28
|
// Stable per-mount nonce that authorizes parent → iframe theme-update messages.
|
|
@@ -25,11 +31,12 @@ export function HtmlNode({
|
|
|
25
31
|
const axToken = useMemo(() => `ax-${crypto.randomUUID()}`, []);
|
|
26
32
|
// Per-mount nonce for the content-height reporter (node grows to fit content).
|
|
27
33
|
const frameToken = useMemo(() => `frame-${crypto.randomUUID()}`, []);
|
|
28
|
-
const html =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
const html =
|
|
35
|
+
typeof node.data.html === 'string'
|
|
36
|
+
? node.data.html
|
|
37
|
+
: typeof node.data.content === 'string'
|
|
38
|
+
? node.data.content
|
|
39
|
+
: '';
|
|
33
40
|
const v = useMemo(() => surfaceContentHash(html), [html]);
|
|
34
41
|
|
|
35
42
|
// The in-canvas iframe and the "Open as site" tab load the SAME server-rendered
|
|
@@ -38,9 +45,18 @@ export function HtmlNode({
|
|
|
38
45
|
// via postMessage below (no reload), while `v` reloads the frame when the HTML
|
|
39
46
|
// itself changes.
|
|
40
47
|
const surfaceSrc = useMemo(
|
|
41
|
-
() =>
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
() =>
|
|
49
|
+
html
|
|
50
|
+
? nodeSurfaceUrl(node.id, {
|
|
51
|
+
theme,
|
|
52
|
+
themeToken,
|
|
53
|
+
present: presentation,
|
|
54
|
+
presentToken: presentationExitToken,
|
|
55
|
+
v,
|
|
56
|
+
axToken,
|
|
57
|
+
frameToken,
|
|
58
|
+
})
|
|
59
|
+
: '',
|
|
44
60
|
[html, presentation, presentationExitToken, themeToken, v, node.id, axToken, frameToken],
|
|
45
61
|
);
|
|
46
62
|
|
|
@@ -57,7 +73,10 @@ export function HtmlNode({
|
|
|
57
73
|
// nodeId are a second gate, not the only one.
|
|
58
74
|
if (event.source !== iframeRef.current?.contentWindow) return;
|
|
59
75
|
const data = event.data as {
|
|
60
|
-
source?: string;
|
|
76
|
+
source?: string;
|
|
77
|
+
token?: string;
|
|
78
|
+
nodeId?: string;
|
|
79
|
+
correlationId?: string;
|
|
61
80
|
interaction?: { type?: unknown; payload?: unknown };
|
|
62
81
|
} | null;
|
|
63
82
|
if (!data || data.source !== 'pmx-canvas-ax' || data.token !== axToken || data.nodeId !== node.id) return;
|
|
@@ -75,13 +94,16 @@ export function HtmlNode({
|
|
|
75
94
|
if (res.ok) showToast('context', 'AX interaction', interactionType, [node.id]);
|
|
76
95
|
else showToast('remove', 'AX interaction rejected', res.error ?? res.code ?? '', [node.id]);
|
|
77
96
|
// Report #55: ack back to the surface so it can self-confirm (e.g. "queued ✓").
|
|
78
|
-
iframeRef.current?.contentWindow?.postMessage(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
98
|
+
{
|
|
99
|
+
source: 'pmx-canvas-ax-ack',
|
|
100
|
+
token: axToken,
|
|
101
|
+
...(data.correlationId ? { correlationId: data.correlationId } : {}),
|
|
102
|
+
interaction: { type: interactionType },
|
|
103
|
+
result: res,
|
|
104
|
+
},
|
|
105
|
+
'*',
|
|
106
|
+
);
|
|
85
107
|
});
|
|
86
108
|
}
|
|
87
109
|
window.addEventListener('message', onAxMessage);
|
|
@@ -89,12 +111,15 @@ export function HtmlNode({
|
|
|
89
111
|
}, [axToken, node.id]);
|
|
90
112
|
|
|
91
113
|
useEffect(() => {
|
|
92
|
-
iframeRef.current?.contentWindow?.postMessage(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
114
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
115
|
+
{
|
|
116
|
+
source: 'pmx-canvas-html-node',
|
|
117
|
+
type: 'theme-update',
|
|
118
|
+
token: themeToken,
|
|
119
|
+
theme,
|
|
120
|
+
},
|
|
121
|
+
'*',
|
|
122
|
+
);
|
|
98
123
|
if (autoFocus) iframeRef.current?.focus();
|
|
99
124
|
}, [theme, themeToken]);
|
|
100
125
|
|
|
@@ -107,12 +132,15 @@ export function HtmlNode({
|
|
|
107
132
|
const axStateValue = axSurfaceState.value;
|
|
108
133
|
useEffect(() => {
|
|
109
134
|
if (!axEnabled || axStateValue == null) return;
|
|
110
|
-
iframeRef.current?.contentWindow?.postMessage(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
136
|
+
{
|
|
137
|
+
source: 'pmx-canvas-html-node',
|
|
138
|
+
type: 'ax-update',
|
|
139
|
+
token: axToken,
|
|
140
|
+
state: axStateValue,
|
|
141
|
+
},
|
|
142
|
+
'*',
|
|
143
|
+
);
|
|
116
144
|
}, [axEnabled, axStateValue, axToken]);
|
|
117
145
|
|
|
118
146
|
useEffect(() => {
|
|
@@ -122,29 +150,31 @@ export function HtmlNode({
|
|
|
122
150
|
}, [autoFocus, surfaceSrc]);
|
|
123
151
|
|
|
124
152
|
const handleFrameLoad = () => {
|
|
125
|
-
iframeRef.current?.contentWindow?.postMessage(
|
|
126
|
-
|
|
127
|
-
type: 'theme-update',
|
|
128
|
-
token: themeToken,
|
|
129
|
-
theme,
|
|
130
|
-
}, '*');
|
|
131
|
-
if (axEnabled && axSurfaceState.value != null) {
|
|
132
|
-
iframeRef.current?.contentWindow?.postMessage({
|
|
153
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
154
|
+
{
|
|
133
155
|
source: 'pmx-canvas-html-node',
|
|
134
|
-
type: '
|
|
135
|
-
token:
|
|
136
|
-
|
|
137
|
-
},
|
|
156
|
+
type: 'theme-update',
|
|
157
|
+
token: themeToken,
|
|
158
|
+
theme,
|
|
159
|
+
},
|
|
160
|
+
'*',
|
|
161
|
+
);
|
|
162
|
+
if (axEnabled && axSurfaceState.value != null) {
|
|
163
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
164
|
+
{
|
|
165
|
+
source: 'pmx-canvas-html-node',
|
|
166
|
+
type: 'ax-update',
|
|
167
|
+
token: axToken,
|
|
168
|
+
state: axSurfaceState.value,
|
|
169
|
+
},
|
|
170
|
+
'*',
|
|
171
|
+
);
|
|
138
172
|
}
|
|
139
173
|
if (autoFocus) iframeRef.current?.focus();
|
|
140
174
|
};
|
|
141
175
|
|
|
142
176
|
if (!html) {
|
|
143
|
-
return (
|
|
144
|
-
<div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>
|
|
145
|
-
No HTML content set
|
|
146
|
-
</div>
|
|
147
|
-
);
|
|
177
|
+
return <div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>No HTML content set</div>;
|
|
148
178
|
}
|
|
149
179
|
|
|
150
180
|
// SECURITY: sandbox is intentionally `allow-scripts` ONLY. Do NOT add
|
|
@@ -7,19 +7,17 @@ import { getImageNodeWarnings } from './image-warnings';
|
|
|
7
7
|
* Supports: file paths (served via /api/canvas/image/:nodeId), data URIs, and URLs.
|
|
8
8
|
* Features: fit-to-container, zoom in/out within node, pan when zoomed.
|
|
9
9
|
*/
|
|
10
|
-
export function ImageNode({
|
|
11
|
-
node,
|
|
12
|
-
expanded = false,
|
|
13
|
-
}: { node: CanvasNodeState; expanded?: boolean }) {
|
|
10
|
+
export function ImageNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
14
11
|
const src = (node.data.src as string) || '';
|
|
15
12
|
const alt = (node.data.alt as string) || (node.data.title as string) || 'Image';
|
|
16
13
|
const caption = (node.data.caption as string) || '';
|
|
17
14
|
const warnings = getImageNodeWarnings(node);
|
|
18
15
|
|
|
19
16
|
// Determine the image source URL
|
|
20
|
-
const imageSrc =
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
const imageSrc =
|
|
18
|
+
src.startsWith('data:') || src.startsWith('http://') || src.startsWith('https://')
|
|
19
|
+
? src
|
|
20
|
+
: `/api/canvas/image/${node.id}`;
|
|
23
21
|
|
|
24
22
|
const [loaded, setLoaded] = useState(false);
|
|
25
23
|
const [error, setError] = useState(false);
|
|
@@ -54,13 +52,16 @@ export function ImageNode({
|
|
|
54
52
|
setZoom((z) => Math.max(0.25, Math.min(10, z * delta)));
|
|
55
53
|
}, []);
|
|
56
54
|
|
|
57
|
-
const handlePointerDown = useCallback(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
const handlePointerDown = useCallback(
|
|
56
|
+
(e: PointerEvent) => {
|
|
57
|
+
if (zoom <= 1) return;
|
|
58
|
+
e.stopPropagation();
|
|
59
|
+
dragging.current = true;
|
|
60
|
+
lastPos.current = { x: e.clientX, y: e.clientY };
|
|
61
|
+
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
62
|
+
},
|
|
63
|
+
[zoom],
|
|
64
|
+
);
|
|
64
65
|
|
|
65
66
|
const handlePointerMove = useCallback((e: PointerEvent) => {
|
|
66
67
|
if (!dragging.current) return;
|
|
@@ -92,10 +93,7 @@ export function ImageNode({
|
|
|
92
93
|
const zoomPct = Math.round(zoom * 100);
|
|
93
94
|
|
|
94
95
|
return (
|
|
95
|
-
<div
|
|
96
|
-
class={`image-node ${expanded ? 'image-node-expanded' : ''}`}
|
|
97
|
-
ref={containerRef}
|
|
98
|
-
>
|
|
96
|
+
<div class={`image-node ${expanded ? 'image-node-expanded' : ''}`} ref={containerRef}>
|
|
99
97
|
{warnings.length > 0 && (
|
|
100
98
|
<div class="image-node-warning-stack">
|
|
101
99
|
{warnings.map((warning) => (
|
|
@@ -114,9 +112,7 @@ export function ImageNode({
|
|
|
114
112
|
onPointerUp={handlePointerUp}
|
|
115
113
|
style={{ cursor: zoom > 1 ? 'grab' : 'default' }}
|
|
116
114
|
>
|
|
117
|
-
{!loaded && !error &&
|
|
118
|
-
<div class="image-node-loading">Loading…</div>
|
|
119
|
-
)}
|
|
115
|
+
{!loaded && !error && <div class="image-node-loading">Loading…</div>}
|
|
120
116
|
{error && (
|
|
121
117
|
<div class="image-node-error">
|
|
122
118
|
<div class="image-node-error-icon">⚠</div>
|
|
@@ -143,12 +139,7 @@ export function ImageNode({
|
|
|
143
139
|
<span class="image-node-meta">
|
|
144
140
|
{sizeLabel && <span>{sizeLabel}</span>}
|
|
145
141
|
{zoom !== 1 && (
|
|
146
|
-
<button
|
|
147
|
-
type="button"
|
|
148
|
-
class="image-node-zoom-reset"
|
|
149
|
-
onClick={resetView}
|
|
150
|
-
title="Reset zoom"
|
|
151
|
-
>
|
|
142
|
+
<button type="button" class="image-node-zoom-reset" onClick={resetView} title="Reset zoom">
|
|
152
143
|
{zoomPct}% ↺
|
|
153
144
|
</button>
|
|
154
145
|
)}
|
|
@@ -4,13 +4,7 @@ import { promptAndInsertLink, wrapSelectionInCode } from './inline-editor-comman
|
|
|
4
4
|
|
|
5
5
|
const GAP = 8;
|
|
6
6
|
|
|
7
|
-
type ExecCommand =
|
|
8
|
-
| 'bold'
|
|
9
|
-
| 'italic'
|
|
10
|
-
| 'strikeThrough'
|
|
11
|
-
| 'formatBlock'
|
|
12
|
-
| 'insertUnorderedList'
|
|
13
|
-
| 'insertOrderedList';
|
|
7
|
+
type ExecCommand = 'bold' | 'italic' | 'strikeThrough' | 'formatBlock' | 'insertUnorderedList' | 'insertOrderedList';
|
|
14
8
|
|
|
15
9
|
type BlockTag = 'H1' | 'H2' | 'H3' | 'P' | 'BLOCKQUOTE';
|
|
16
10
|
|
|
@@ -54,13 +48,7 @@ function runAction(action: Action): void {
|
|
|
54
48
|
|
|
55
49
|
/** Floating selection toolbar for a contentEditable host. Mounts once; while
|
|
56
50
|
* visible, positions itself above the current selection's viewport rect. */
|
|
57
|
-
export function InlineFormatBar({
|
|
58
|
-
hostRef,
|
|
59
|
-
onChange,
|
|
60
|
-
}: {
|
|
61
|
-
hostRef: RefObject<HTMLElement>;
|
|
62
|
-
onChange: () => void;
|
|
63
|
-
}) {
|
|
51
|
+
export function InlineFormatBar({ hostRef, onChange }: { hostRef: RefObject<HTMLElement>; onChange: () => void }) {
|
|
64
52
|
const [visible, setVisible] = useState(false);
|
|
65
53
|
const [pos, setPos] = useState({ top: 0, left: 0 });
|
|
66
54
|
const [barWidth, setBarWidth] = useState(0);
|
|
@@ -91,10 +79,7 @@ export function InlineFormatBar({
|
|
|
91
79
|
// Use measured width if we have it; fall back to a conservative estimate
|
|
92
80
|
// on the very first show before layout completes.
|
|
93
81
|
const width = barWidth || 420;
|
|
94
|
-
const left = Math.max(
|
|
95
|
-
GAP,
|
|
96
|
-
Math.min(rect.left + rect.width / 2 - width / 2, window.innerWidth - width - GAP),
|
|
97
|
-
);
|
|
82
|
+
const left = Math.max(GAP, Math.min(rect.left + rect.width / 2 - width / 2, window.innerWidth - width - GAP));
|
|
98
83
|
const top = Math.max(GAP, rect.top - GAP - 36);
|
|
99
84
|
setPos({ top, left });
|
|
100
85
|
setVisible(true);
|
|
@@ -160,9 +145,7 @@ export function InlineFormatBar({
|
|
|
160
145
|
{a.icon}
|
|
161
146
|
</button>
|
|
162
147
|
);
|
|
163
|
-
return a.dividerBefore
|
|
164
|
-
? [<span key={`div-${i}`} class="md-inline-format-divider" />, btn]
|
|
165
|
-
: [btn];
|
|
148
|
+
return a.dividerBefore ? [<span key={`div-${i}`} class="md-inline-format-divider" />, btn] : [btn];
|
|
166
149
|
})}
|
|
167
150
|
</div>
|
|
168
151
|
);
|
|
@@ -20,9 +20,7 @@ export function LedgerNode({ node }: { node: CanvasNodeState }) {
|
|
|
20
20
|
const entries = Object.entries(data).filter(([key]) => !HIDDEN_LEDGER_KEYS.has(key));
|
|
21
21
|
|
|
22
22
|
if (lines.length === 0 && entries.length === 0) {
|
|
23
|
-
return (
|
|
24
|
-
<div style={{ color: 'var(--c-dim)', fontSize: '12px', fontStyle: 'italic' }}>No ledger data</div>
|
|
25
|
-
);
|
|
23
|
+
return <div style={{ color: 'var(--c-dim)', fontSize: '12px', fontStyle: 'italic' }}>No ledger data</div>;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
return (
|
|
@@ -61,7 +59,15 @@ export function LedgerNode({ node }: { node: CanvasNodeState }) {
|
|
|
61
59
|
<span style={{ color: 'var(--c-muted)', fontSize: '11px', flexShrink: 0 }}>
|
|
62
60
|
{key.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase())}
|
|
63
61
|
</span>
|
|
64
|
-
<span
|
|
62
|
+
<span
|
|
63
|
+
style={{
|
|
64
|
+
color: 'var(--c-text)',
|
|
65
|
+
fontFamily: 'var(--mono)',
|
|
66
|
+
fontSize: '11px',
|
|
67
|
+
textAlign: 'right',
|
|
68
|
+
wordBreak: 'break-word',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
65
71
|
{typeof value === 'object' ? JSON.stringify(value) : String(value ?? '—')}
|
|
66
72
|
</span>
|
|
67
73
|
</div>
|
|
@@ -31,10 +31,7 @@ function RenderedMarkdown({
|
|
|
31
31
|
return <div ref={containerRef} class={className} style={style} />;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function MarkdownNode({
|
|
35
|
-
node,
|
|
36
|
-
expanded = false,
|
|
37
|
-
}: { node: CanvasNodeState; expanded?: boolean }) {
|
|
34
|
+
export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
38
35
|
const path = node.data.path as string;
|
|
39
36
|
const [content, setContent] = useState('');
|
|
40
37
|
const [rendered, setRendered] = useState('');
|
|
@@ -302,9 +299,7 @@ export function MarkdownNode({
|
|
|
302
299
|
onSave={handleInlineSave}
|
|
303
300
|
/>
|
|
304
301
|
) : (
|
|
305
|
-
<div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '24px' }}>
|
|
306
|
-
Loading…
|
|
307
|
-
</div>
|
|
302
|
+
<div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '24px' }}>Loading…</div>
|
|
308
303
|
)}
|
|
309
304
|
</div>
|
|
310
305
|
<button type="button" class="md-edit-fab" onClick={() => setSourceMode(true)}>
|
|
@@ -323,9 +318,7 @@ export function MarkdownNode({
|
|
|
323
318
|
html={rendered}
|
|
324
319
|
style={{ padding: rendered ? '0' : '12px', color: rendered ? undefined : 'var(--c-dim)' }}
|
|
325
320
|
/>
|
|
326
|
-
{!loaded && (
|
|
327
|
-
<div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>Loading…</div>
|
|
328
|
-
)}
|
|
321
|
+
{!loaded && <div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>Loading…</div>}
|
|
329
322
|
{loaded && !rendered && (
|
|
330
323
|
<div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>Empty node</div>
|
|
331
324
|
)}
|
|
@@ -43,8 +43,7 @@ export function isSameOriginFrameDocumentUrl(url: string, origin = window.locati
|
|
|
43
43
|
try {
|
|
44
44
|
const baseOrigin = new URL(origin).origin;
|
|
45
45
|
const resolved = new URL(url, baseOrigin);
|
|
46
|
-
return resolved.origin === baseOrigin &&
|
|
47
|
-
resolved.pathname.startsWith('/api/canvas/frame-documents/');
|
|
46
|
+
return resolved.origin === baseOrigin && resolved.pathname.startsWith('/api/canvas/frame-documents/');
|
|
48
47
|
} catch {
|
|
49
48
|
return false;
|
|
50
49
|
}
|
|
@@ -89,7 +88,10 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
89
88
|
function onAxMessage(event: MessageEvent) {
|
|
90
89
|
if (event.source !== iframeRef.current?.contentWindow) return;
|
|
91
90
|
const data = event.data as {
|
|
92
|
-
source?: string;
|
|
91
|
+
source?: string;
|
|
92
|
+
token?: string;
|
|
93
|
+
nodeId?: string;
|
|
94
|
+
correlationId?: string;
|
|
93
95
|
interaction?: { type?: unknown; payload?: unknown };
|
|
94
96
|
} | null;
|
|
95
97
|
if (!data || data.source !== 'pmx-canvas-ax' || data.token !== axToken || data.nodeId !== node.id) return;
|
|
@@ -107,13 +109,16 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
107
109
|
if (res.ok) showToast('context', 'AX interaction', interactionType, [node.id]);
|
|
108
110
|
else showToast('remove', 'AX interaction rejected', res.error ?? res.code ?? '', [node.id]);
|
|
109
111
|
// Report #55: ack back to the viewer so the surface can self-confirm.
|
|
110
|
-
iframeRef.current?.contentWindow?.postMessage(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
113
|
+
{
|
|
114
|
+
source: 'pmx-canvas-ax-ack',
|
|
115
|
+
token: axToken,
|
|
116
|
+
...(data.correlationId ? { correlationId: data.correlationId } : {}),
|
|
117
|
+
interaction: { type: interactionType },
|
|
118
|
+
result: res,
|
|
119
|
+
},
|
|
120
|
+
'*',
|
|
121
|
+
);
|
|
117
122
|
});
|
|
118
123
|
}
|
|
119
124
|
window.addEventListener('message', onAxMessage);
|
|
@@ -125,12 +130,15 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
125
130
|
const axStateValue = axSurfaceState.value;
|
|
126
131
|
const pushAxState = () => {
|
|
127
132
|
if (!isAxViewer || !axToken || axStateValue == null) return;
|
|
128
|
-
iframeRef.current?.contentWindow?.postMessage(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
134
|
+
{
|
|
135
|
+
source: 'pmx-canvas-html-node',
|
|
136
|
+
type: 'ax-update',
|
|
137
|
+
token: axToken,
|
|
138
|
+
state: axStateValue,
|
|
139
|
+
},
|
|
140
|
+
'*',
|
|
141
|
+
);
|
|
134
142
|
};
|
|
135
143
|
useEffect(pushAxState, [isAxViewer, axToken, axStateValue]);
|
|
136
144
|
|
|
@@ -156,9 +164,7 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
156
164
|
<span>⚠</span>
|
|
157
165
|
<span>Cannot embed — opened externally</span>
|
|
158
166
|
</div>
|
|
159
|
-
{fallbackReason && (
|
|
160
|
-
<div style={{ color: 'var(--c-muted)', fontSize: '11px' }}>Reason: {fallbackReason}</div>
|
|
161
|
-
)}
|
|
167
|
+
{fallbackReason && <div style={{ color: 'var(--c-muted)', fontSize: '11px' }}>Reason: {fallbackReason}</div>}
|
|
162
168
|
<a
|
|
163
169
|
href={url}
|
|
164
170
|
target="_blank"
|
|
@@ -171,9 +177,7 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
171
177
|
>
|
|
172
178
|
{url}
|
|
173
179
|
</a>
|
|
174
|
-
{sourceServer && (
|
|
175
|
-
<div style={{ color: 'var(--c-dim)', fontSize: '10px' }}>Source: {sourceServer}</div>
|
|
176
|
-
)}
|
|
180
|
+
{sourceServer && <div style={{ color: 'var(--c-dim)', fontSize: '10px' }}>Source: {sourceServer}</div>}
|
|
177
181
|
</div>
|
|
178
182
|
);
|
|
179
183
|
}
|
|
@@ -62,13 +62,16 @@ export function MdFormatBar({ textareaRef }: { textareaRef: { current: HTMLTextA
|
|
|
62
62
|
};
|
|
63
63
|
}, [textareaRef, updatePosition]);
|
|
64
64
|
|
|
65
|
-
const runAction = useCallback(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
const runAction = useCallback(
|
|
66
|
+
(action: FormatAction) => {
|
|
67
|
+
const ta = textareaRef.current;
|
|
68
|
+
if (ta) {
|
|
69
|
+
action.action(ta);
|
|
70
|
+
ta.focus();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[textareaRef],
|
|
74
|
+
);
|
|
72
75
|
|
|
73
76
|
if (!visible) return null;
|
|
74
77
|
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { updateNodeData } from '../state/canvas-store';
|
|
3
|
-
import {
|
|
4
|
-
fetchSlashCommands,
|
|
5
|
-
renderMarkdown,
|
|
6
|
-
submitCanvasPrompt,
|
|
7
|
-
submitThreadReply,
|
|
8
|
-
} from '../state/intent-bridge';
|
|
3
|
+
import { fetchSlashCommands, renderMarkdown, submitCanvasPrompt, submitThreadReply } from '../state/intent-bridge';
|
|
9
4
|
import type { CanvasNodeState } from '../types';
|
|
10
5
|
|
|
11
6
|
// Cached slash commands — fetched once on first use.
|
|
@@ -16,10 +11,7 @@ async function getCommands() {
|
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
/** Find the best matching slash command for a query prefix. */
|
|
19
|
-
function matchSlashCommand(
|
|
20
|
-
query: string,
|
|
21
|
-
commands: Array<{ name: string }>,
|
|
22
|
-
): string | null {
|
|
14
|
+
function matchSlashCommand(query: string, commands: Array<{ name: string }>): string | null {
|
|
23
15
|
if (!query) return null;
|
|
24
16
|
const lower = query.toLowerCase();
|
|
25
17
|
const exact = commands.find((c) => c.name.toLowerCase() === lower);
|
|
@@ -121,15 +113,10 @@ function ErrorBanner({ message, onDismiss }: { message: string | null; onDismiss
|
|
|
121
113
|
);
|
|
122
114
|
}
|
|
123
115
|
|
|
124
|
-
export function PromptNode({
|
|
125
|
-
node,
|
|
126
|
-
expanded = false,
|
|
127
|
-
}: { node: CanvasNodeState; expanded?: boolean }) {
|
|
116
|
+
export function PromptNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
128
117
|
// Backward compat: old canvas states have flat { text, status } data.
|
|
129
118
|
// New threads use { turns[], threadStatus }. Detect format by presence of turns array.
|
|
130
|
-
const turns: ThreadTurn[] = Array.isArray(node.data.turns)
|
|
131
|
-
? (node.data.turns as ThreadTurn[])
|
|
132
|
-
: [];
|
|
119
|
+
const turns: ThreadTurn[] = Array.isArray(node.data.turns) ? (node.data.turns as ThreadTurn[]) : [];
|
|
133
120
|
const isLegacy = turns.length === 0;
|
|
134
121
|
const legacyText = (node.data.text as string) || '';
|
|
135
122
|
const legacyStatus = (node.data.status as string) || 'draft';
|
|
@@ -201,7 +188,9 @@ export function PromptNode({
|
|
|
201
188
|
setRenderedTurns(next);
|
|
202
189
|
});
|
|
203
190
|
|
|
204
|
-
return () => {
|
|
191
|
+
return () => {
|
|
192
|
+
cancelled = true;
|
|
193
|
+
};
|
|
205
194
|
};
|
|
206
195
|
|
|
207
196
|
if (!hasStreaming) {
|
|
@@ -243,9 +232,7 @@ export function PromptNode({
|
|
|
243
232
|
text: trimmed, // keep for backward compat display
|
|
244
233
|
});
|
|
245
234
|
setDraft('');
|
|
246
|
-
const ctxIds = Array.isArray(node.data.contextNodeIds)
|
|
247
|
-
? (node.data.contextNodeIds as string[])
|
|
248
|
-
: undefined;
|
|
235
|
+
const ctxIds = Array.isArray(node.data.contextNodeIds) ? (node.data.contextNodeIds as string[]) : undefined;
|
|
249
236
|
const result = await submitCanvasPrompt(trimmed, node.position, node.id, ctxIds, node.id);
|
|
250
237
|
if (!result.ok) {
|
|
251
238
|
updateNodeData(node.id, { turns: [], threadStatus: 'draft', text: '', status: 'draft' });
|
|
@@ -260,9 +247,7 @@ export function PromptNode({
|
|
|
260
247
|
const savedReply = replyDraft;
|
|
261
248
|
setError(null);
|
|
262
249
|
// Optimistically add user turn
|
|
263
|
-
const currentTurns = Array.isArray(node.data.turns)
|
|
264
|
-
? [...(node.data.turns as ThreadTurn[])]
|
|
265
|
-
: [];
|
|
250
|
+
const currentTurns = Array.isArray(node.data.turns) ? [...(node.data.turns as ThreadTurn[])] : [];
|
|
266
251
|
currentTurns.push({ role: 'user', text: trimmed, status: 'pending' });
|
|
267
252
|
updateNodeData(node.id, { turns: currentTurns, threadStatus: 'pending' });
|
|
268
253
|
setReplyDraft('');
|
|
@@ -319,17 +304,12 @@ export function PromptNode({
|
|
|
319
304
|
[handleReplySubmit, replyDraft],
|
|
320
305
|
);
|
|
321
306
|
|
|
322
|
-
const ctxCount = Array.isArray(node.data.contextNodeIds)
|
|
323
|
-
? (node.data.contextNodeIds as string[]).length
|
|
324
|
-
: 0;
|
|
307
|
+
const ctxCount = Array.isArray(node.data.contextNodeIds) ? (node.data.contextNodeIds as string[]).length : 0;
|
|
325
308
|
|
|
326
309
|
// ── Draft mode (no turns yet): show initial textarea ──
|
|
327
310
|
if (isDraft && turns.length === 0) {
|
|
328
311
|
return (
|
|
329
|
-
<div
|
|
330
|
-
class="prompt-node-inner"
|
|
331
|
-
style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: '8px' }}
|
|
332
|
-
>
|
|
312
|
+
<div class="prompt-node-inner" style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: '8px' }}>
|
|
333
313
|
<ContextBadge count={ctxCount} />
|
|
334
314
|
<ErrorBanner message={error} onDismiss={() => setError(null)} />
|
|
335
315
|
<textarea
|
|
@@ -380,10 +360,7 @@ export function PromptNode({
|
|
|
380
360
|
// ── Legacy flat format (old nodes without turns) ──
|
|
381
361
|
if (isLegacy) {
|
|
382
362
|
return (
|
|
383
|
-
<div
|
|
384
|
-
class="prompt-node-inner"
|
|
385
|
-
style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
|
|
386
|
-
>
|
|
363
|
+
<div class="prompt-node-inner" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
387
364
|
<div
|
|
388
365
|
style={{
|
|
389
366
|
flex: 1,
|
|
@@ -425,10 +402,7 @@ export function PromptNode({
|
|
|
425
402
|
|
|
426
403
|
// ── Thread view: render all turns ──
|
|
427
404
|
return (
|
|
428
|
-
<div
|
|
429
|
-
class="prompt-node-inner"
|
|
430
|
-
style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
|
|
431
|
-
>
|
|
405
|
+
<div class="prompt-node-inner" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
432
406
|
<ContextBadge count={ctxCount} />
|
|
433
407
|
<ErrorBanner message={error} onDismiss={() => setError(null)} />
|
|
434
408
|
|
|
@@ -439,7 +413,10 @@ export function PromptNode({
|
|
|
439
413
|
{i > 0 && <div class="thread-turn-divider" />}
|
|
440
414
|
{turn.role === 'user' ? (
|
|
441
415
|
<div class="thread-turn-user">
|
|
442
|
-
<div class="thread-turn-role"
|
|
416
|
+
<div class="thread-turn-role">
|
|
417
|
+
<span class="status-dot" />
|
|
418
|
+
You
|
|
419
|
+
</div>
|
|
443
420
|
<div
|
|
444
421
|
style={{
|
|
445
422
|
fontSize: expanded ? '15px' : '13px',
|
|
@@ -454,7 +431,10 @@ export function PromptNode({
|
|
|
454
431
|
</div>
|
|
455
432
|
) : (
|
|
456
433
|
<div class="thread-turn-assistant">
|
|
457
|
-
<div class="thread-turn-role"
|
|
434
|
+
<div class="thread-turn-role">
|
|
435
|
+
<span class={`status-dot${turn.status === 'streaming' ? ' pulsing' : ''}`} />
|
|
436
|
+
PMX
|
|
437
|
+
</div>
|
|
458
438
|
{turn.status === 'streaming' && !turn.text && (
|
|
459
439
|
<div
|
|
460
440
|
style={{
|
|
@@ -481,9 +461,7 @@ export function PromptNode({
|
|
|
481
461
|
</>
|
|
482
462
|
) : (
|
|
483
463
|
<div style={{ color: 'var(--c-muted)', fontStyle: 'italic' }}>
|
|
484
|
-
{turn.status === 'streaming'
|
|
485
|
-
? 'Waiting for response…'
|
|
486
|
-
: turn.text || 'Empty response'}
|
|
464
|
+
{turn.status === 'streaming' ? 'Waiting for response…' : turn.text || 'Empty response'}
|
|
487
465
|
</div>
|
|
488
466
|
)}
|
|
489
467
|
</div>
|
|
@@ -525,13 +503,7 @@ export function PromptNode({
|
|
|
525
503
|
: 'var(--c-muted)',
|
|
526
504
|
}}
|
|
527
505
|
>
|
|
528
|
-
{isStreaming
|
|
529
|
-
? 'Streaming…'
|
|
530
|
-
: isPending
|
|
531
|
-
? 'Sending…'
|
|
532
|
-
: isAnswered
|
|
533
|
-
? 'Answered'
|
|
534
|
-
: threadStatus}
|
|
506
|
+
{isStreaming ? 'Streaming…' : isPending ? 'Sending…' : isAnswered ? 'Answered' : threadStatus}
|
|
535
507
|
</span>
|
|
536
508
|
<span style={{ fontSize: '10px', color: 'var(--c-muted)' }}>
|
|
537
509
|
{turns.length} turn{turns.length !== 1 ? 's' : ''}
|