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.
Files changed (161) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/canvas/index.js +65 -65
  3. package/dist/types/cli/agent.d.ts +9 -1
  4. package/dist/types/cli/daemon.d.ts +74 -0
  5. package/dist/types/cli/watch.d.ts +2 -2
  6. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  7. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  8. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  9. package/dist/types/client/nodes/ExtAppFrame.d.ts +9 -1
  10. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  11. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  12. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  13. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  14. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  15. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  16. package/dist/types/server/canvas-schema.d.ts +1 -1
  17. package/dist/types/server/html-primitives.d.ts +1 -1
  18. package/dist/types/server/index.d.ts +4 -4
  19. package/dist/types/server/operations/index.d.ts +1 -1
  20. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  21. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  22. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  23. package/dist/types/server/server.d.ts +8 -0
  24. package/docs/cli.md +10 -1
  25. package/docs/http-api.md +28 -0
  26. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  27. package/docs/screenshot.png +0 -0
  28. package/docs/tech-debt-assessment-2026-07.md +2 -2
  29. package/package.json +5 -2
  30. package/skills/pmx-canvas/SKILL.md +15 -10
  31. package/skills/pmx-canvas/references/full-reference.md +17 -3
  32. package/src/cli/agent.ts +1951 -1571
  33. package/src/cli/daemon.ts +460 -0
  34. package/src/cli/index.ts +80 -323
  35. package/src/cli/watch.ts +2 -10
  36. package/src/client/App.tsx +48 -46
  37. package/src/client/canvas/AttentionHistory.tsx +11 -1
  38. package/src/client/canvas/CanvasNode.tsx +41 -29
  39. package/src/client/canvas/CanvasViewport.tsx +101 -66
  40. package/src/client/canvas/CommandPalette.tsx +61 -27
  41. package/src/client/canvas/ContextMenu.tsx +13 -20
  42. package/src/client/canvas/ContextPinBar.tsx +1 -5
  43. package/src/client/canvas/ContextPinHud.tsx +1 -6
  44. package/src/client/canvas/DockedNode.tsx +4 -4
  45. package/src/client/canvas/EdgeLayer.tsx +37 -36
  46. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  47. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  48. package/src/client/canvas/IntentLayer.tsx +31 -14
  49. package/src/client/canvas/Minimap.tsx +11 -16
  50. package/src/client/canvas/SelectionBar.tsx +4 -11
  51. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  52. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  53. package/src/client/canvas/auto-fit.ts +15 -14
  54. package/src/client/canvas/snap-guides.ts +12 -12
  55. package/src/client/canvas/use-node-resize.ts +1 -5
  56. package/src/client/canvas/use-pan-zoom.ts +25 -26
  57. package/src/client/ext-app/bridge.ts +3 -12
  58. package/src/client/icons.tsx +63 -20
  59. package/src/client/nodes/ContextNode.tsx +14 -25
  60. package/src/client/nodes/ExtAppFrame.tsx +194 -80
  61. package/src/client/nodes/FileNode.tsx +74 -62
  62. package/src/client/nodes/GroupNode.tsx +4 -6
  63. package/src/client/nodes/HtmlNode.tsx +76 -46
  64. package/src/client/nodes/ImageNode.tsx +18 -27
  65. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  66. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  67. package/src/client/nodes/LedgerNode.tsx +10 -4
  68. package/src/client/nodes/MarkdownNode.tsx +3 -10
  69. package/src/client/nodes/McpAppNode.tsx +26 -22
  70. package/src/client/nodes/MdFormatBar.tsx +10 -7
  71. package/src/client/nodes/PromptNode.tsx +23 -51
  72. package/src/client/nodes/ResponseNode.tsx +3 -13
  73. package/src/client/nodes/StatusNode.tsx +5 -9
  74. package/src/client/nodes/StatusSummary.tsx +2 -8
  75. package/src/client/nodes/WebpageNode.tsx +20 -14
  76. package/src/client/nodes/iframe-document-url.ts +25 -16
  77. package/src/client/nodes/image-warnings.ts +1 -7
  78. package/src/client/nodes/md-format.ts +20 -5
  79. package/src/client/state/attention-bridge.ts +4 -9
  80. package/src/client/state/attention-store.ts +1 -7
  81. package/src/client/state/canvas-store.ts +52 -36
  82. package/src/client/state/intent-bridge.ts +176 -112
  83. package/src/client/state/intent-store.ts +4 -1
  84. package/src/client/state/sse-bridge.ts +53 -70
  85. package/src/json-render/catalog.ts +12 -16
  86. package/src/json-render/charts/components.tsx +16 -20
  87. package/src/json-render/charts/extra-components.tsx +8 -16
  88. package/src/json-render/charts/extra-definitions.ts +1 -2
  89. package/src/json-render/charts/tufte-components.tsx +37 -20
  90. package/src/json-render/charts/tufte-definitions.ts +8 -2
  91. package/src/json-render/renderer/index.tsx +42 -22
  92. package/src/json-render/schema.ts +6 -3
  93. package/src/json-render/server.ts +33 -39
  94. package/src/mcp/canvas-access.ts +35 -21
  95. package/src/mcp/server.ts +132 -70
  96. package/src/server/agent-context.ts +63 -36
  97. package/src/server/ax-context.ts +7 -5
  98. package/src/server/ax-interaction.ts +176 -43
  99. package/src/server/ax-state-manager.ts +182 -39
  100. package/src/server/ax-state.ts +142 -47
  101. package/src/server/canvas-db.ts +213 -95
  102. package/src/server/canvas-operations.ts +180 -123
  103. package/src/server/canvas-provenance.ts +1 -4
  104. package/src/server/canvas-schema.ts +454 -73
  105. package/src/server/canvas-serialization.ts +27 -35
  106. package/src/server/canvas-state.ts +150 -58
  107. package/src/server/chart-template.ts +4 -5
  108. package/src/server/code-graph.ts +19 -6
  109. package/src/server/diagram-presets.ts +28 -29
  110. package/src/server/ext-app-lookup.ts +3 -12
  111. package/src/server/html-node-summary.ts +19 -10
  112. package/src/server/html-primitives.ts +326 -97
  113. package/src/server/html-surface.ts +6 -9
  114. package/src/server/image-source.ts +6 -4
  115. package/src/server/index.ts +320 -217
  116. package/src/server/intent-registry.ts +2 -5
  117. package/src/server/mcp-app-candidate.ts +5 -10
  118. package/src/server/mcp-app-host.ts +14 -38
  119. package/src/server/mcp-app-runtime.ts +12 -20
  120. package/src/server/mutation-history.ts +15 -5
  121. package/src/server/operations/composites.ts +1 -3
  122. package/src/server/operations/http.ts +2 -3
  123. package/src/server/operations/index.ts +7 -1
  124. package/src/server/operations/invoker.ts +4 -3
  125. package/src/server/operations/mcp.ts +22 -30
  126. package/src/server/operations/ops/annotation.ts +122 -10
  127. package/src/server/operations/ops/app.ts +98 -73
  128. package/src/server/operations/ops/ax-await.ts +17 -10
  129. package/src/server/operations/ops/ax-read.ts +347 -0
  130. package/src/server/operations/ops/ax-shared.ts +2 -7
  131. package/src/server/operations/ops/ax-state.ts +32 -14
  132. package/src/server/operations/ops/ax-timeline.ts +32 -19
  133. package/src/server/operations/ops/ax-work.ts +54 -37
  134. package/src/server/operations/ops/batch.ts +41 -15
  135. package/src/server/operations/ops/canvas-wire.ts +91 -0
  136. package/src/server/operations/ops/edges.ts +37 -25
  137. package/src/server/operations/ops/ext-app.ts +346 -0
  138. package/src/server/operations/ops/groups.ts +49 -20
  139. package/src/server/operations/ops/intent.ts +18 -12
  140. package/src/server/operations/ops/json-render.ts +239 -98
  141. package/src/server/operations/ops/nodes.ts +298 -109
  142. package/src/server/operations/ops/query.ts +46 -28
  143. package/src/server/operations/ops/snapshots.ts +35 -26
  144. package/src/server/operations/ops/validate.ts +2 -1
  145. package/src/server/operations/ops/viewport.ts +60 -16
  146. package/src/server/operations/ops/webview.ts +44 -18
  147. package/src/server/operations/registry.ts +2 -3
  148. package/src/server/operations/types.ts +7 -5
  149. package/src/server/operations/webview-runner.ts +1 -3
  150. package/src/server/placement.ts +8 -18
  151. package/src/server/server.ts +122 -1028
  152. package/src/server/spatial-analysis.ts +39 -25
  153. package/src/server/trace-manager.ts +3 -8
  154. package/src/server/web-artifacts.ts +23 -27
  155. package/src/server/webpage-node.ts +5 -13
  156. package/src/shared/auto-arrange.ts +12 -5
  157. package/src/shared/content-height-reporter.ts +8 -6
  158. package/src/shared/ext-app-tool-result.ts +2 -6
  159. package/src/shared/placement.ts +1 -4
  160. package/src/shared/semantic-attention.ts +39 -37
  161. 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
- }: { node: CanvasNodeState; expanded?: boolean; presentation?: boolean; presentationExitToken?: string; autoFocus?: boolean }) {
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 = typeof node.data.html === 'string'
29
- ? node.data.html
30
- : typeof node.data.content === 'string'
31
- ? node.data.content
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
- () => (html
42
- ? nodeSurfaceUrl(node.id, { theme, themeToken, present: presentation, presentToken: presentationExitToken, v, axToken, frameToken })
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; token?: string; nodeId?: string; correlationId?: 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
- source: 'pmx-canvas-ax-ack',
80
- token: axToken,
81
- ...(data.correlationId ? { correlationId: data.correlationId } : {}),
82
- interaction: { type: interactionType },
83
- result: res,
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
- source: 'pmx-canvas-html-node',
94
- type: 'theme-update',
95
- token: themeToken,
96
- theme,
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
- source: 'pmx-canvas-html-node',
112
- type: 'ax-update',
113
- token: axToken,
114
- state: axStateValue,
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
- source: 'pmx-canvas-html-node',
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: 'ax-update',
135
- token: axToken,
136
- state: axSurfaceState.value,
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 = src.startsWith('data:') || src.startsWith('http://') || src.startsWith('https://')
21
- ? src
22
- : `/api/canvas/image/${node.id}`;
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((e: PointerEvent) => {
58
- if (zoom <= 1) return;
59
- e.stopPropagation();
60
- dragging.current = true;
61
- lastPos.current = { x: e.clientX, y: e.clientY };
62
- (e.target as HTMLElement).setPointerCapture(e.pointerId);
63
- }, [zoom]);
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
  );
@@ -120,4 +120,3 @@ export function InlineMarkdownEditor({
120
120
  </>
121
121
  );
122
122
  }
123
-
@@ -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 style={{ color: 'var(--c-text)', fontFamily: 'var(--mono)', fontSize: '11px', textAlign: 'right', wordBreak: 'break-word' }}>
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; token?: string; nodeId?: string; correlationId?: 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
- source: 'pmx-canvas-ax-ack',
112
- token: axToken,
113
- ...(data.correlationId ? { correlationId: data.correlationId } : {}),
114
- interaction: { type: interactionType },
115
- result: res,
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
- source: 'pmx-canvas-html-node',
130
- type: 'ax-update',
131
- token: axToken,
132
- state: axStateValue,
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((action: FormatAction) => {
66
- const ta = textareaRef.current;
67
- if (ta) {
68
- action.action(ta);
69
- ta.focus();
70
- }
71
- }, [textareaRef]);
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 () => { cancelled = true; };
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"><span class="status-dot" />You</div>
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"><span class={`status-dot${turn.status === 'streaming' ? ' pulsing' : ''}`} />PMX</div>
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' : ''}