pmx-canvas 0.3.0 → 0.3.1

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 (156) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/canvas/index.js +2 -2
  3. package/dist/types/cli/daemon.d.ts +74 -0
  4. package/dist/types/cli/watch.d.ts +2 -2
  5. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  6. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  7. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  8. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  9. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  10. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  11. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  12. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  13. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  14. package/dist/types/server/canvas-schema.d.ts +1 -1
  15. package/dist/types/server/html-primitives.d.ts +1 -1
  16. package/dist/types/server/index.d.ts +4 -4
  17. package/dist/types/server/operations/index.d.ts +1 -1
  18. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  19. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  20. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  21. package/docs/http-api.md +28 -0
  22. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  23. package/docs/tech-debt-assessment-2026-07.md +2 -2
  24. package/package.json +5 -2
  25. package/skills/pmx-canvas/SKILL.md +3 -1
  26. package/skills/pmx-canvas/references/full-reference.md +10 -3
  27. package/src/cli/agent.ts +1861 -1548
  28. package/src/cli/daemon.ts +460 -0
  29. package/src/cli/index.ts +63 -326
  30. package/src/cli/watch.ts +2 -10
  31. package/src/client/App.tsx +48 -46
  32. package/src/client/canvas/AttentionHistory.tsx +11 -1
  33. package/src/client/canvas/CanvasNode.tsx +41 -29
  34. package/src/client/canvas/CanvasViewport.tsx +101 -66
  35. package/src/client/canvas/CommandPalette.tsx +61 -27
  36. package/src/client/canvas/ContextMenu.tsx +13 -20
  37. package/src/client/canvas/ContextPinBar.tsx +1 -5
  38. package/src/client/canvas/ContextPinHud.tsx +1 -6
  39. package/src/client/canvas/DockedNode.tsx +4 -4
  40. package/src/client/canvas/EdgeLayer.tsx +37 -36
  41. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  42. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  43. package/src/client/canvas/IntentLayer.tsx +31 -14
  44. package/src/client/canvas/Minimap.tsx +11 -16
  45. package/src/client/canvas/SelectionBar.tsx +4 -11
  46. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  47. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  48. package/src/client/canvas/auto-fit.ts +15 -14
  49. package/src/client/canvas/snap-guides.ts +12 -12
  50. package/src/client/canvas/use-node-resize.ts +1 -5
  51. package/src/client/canvas/use-pan-zoom.ts +25 -26
  52. package/src/client/ext-app/bridge.ts +3 -12
  53. package/src/client/icons.tsx +63 -20
  54. package/src/client/nodes/ContextNode.tsx +14 -25
  55. package/src/client/nodes/ExtAppFrame.tsx +60 -39
  56. package/src/client/nodes/FileNode.tsx +74 -62
  57. package/src/client/nodes/GroupNode.tsx +4 -6
  58. package/src/client/nodes/HtmlNode.tsx +76 -46
  59. package/src/client/nodes/ImageNode.tsx +18 -27
  60. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  61. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  62. package/src/client/nodes/LedgerNode.tsx +10 -4
  63. package/src/client/nodes/MarkdownNode.tsx +3 -10
  64. package/src/client/nodes/McpAppNode.tsx +26 -22
  65. package/src/client/nodes/MdFormatBar.tsx +10 -7
  66. package/src/client/nodes/PromptNode.tsx +23 -51
  67. package/src/client/nodes/ResponseNode.tsx +3 -13
  68. package/src/client/nodes/StatusNode.tsx +5 -9
  69. package/src/client/nodes/StatusSummary.tsx +2 -8
  70. package/src/client/nodes/WebpageNode.tsx +20 -14
  71. package/src/client/nodes/iframe-document-url.ts +25 -16
  72. package/src/client/nodes/image-warnings.ts +1 -7
  73. package/src/client/nodes/md-format.ts +20 -5
  74. package/src/client/state/attention-bridge.ts +4 -9
  75. package/src/client/state/attention-store.ts +1 -7
  76. package/src/client/state/canvas-store.ts +52 -36
  77. package/src/client/state/intent-bridge.ts +176 -112
  78. package/src/client/state/intent-store.ts +4 -1
  79. package/src/client/state/sse-bridge.ts +53 -70
  80. package/src/json-render/catalog.ts +12 -16
  81. package/src/json-render/charts/components.tsx +16 -20
  82. package/src/json-render/charts/extra-components.tsx +8 -16
  83. package/src/json-render/charts/extra-definitions.ts +1 -2
  84. package/src/json-render/charts/tufte-components.tsx +37 -20
  85. package/src/json-render/charts/tufte-definitions.ts +8 -2
  86. package/src/json-render/renderer/index.tsx +42 -22
  87. package/src/json-render/schema.ts +6 -3
  88. package/src/json-render/server.ts +33 -39
  89. package/src/mcp/canvas-access.ts +35 -21
  90. package/src/mcp/server.ts +132 -70
  91. package/src/server/agent-context.ts +63 -36
  92. package/src/server/ax-context.ts +7 -5
  93. package/src/server/ax-interaction.ts +176 -43
  94. package/src/server/ax-state-manager.ts +182 -39
  95. package/src/server/ax-state.ts +142 -47
  96. package/src/server/canvas-db.ts +213 -95
  97. package/src/server/canvas-operations.ts +177 -120
  98. package/src/server/canvas-provenance.ts +1 -4
  99. package/src/server/canvas-schema.ts +454 -73
  100. package/src/server/canvas-serialization.ts +27 -35
  101. package/src/server/canvas-state.ts +150 -58
  102. package/src/server/chart-template.ts +4 -5
  103. package/src/server/code-graph.ts +19 -6
  104. package/src/server/diagram-presets.ts +28 -29
  105. package/src/server/ext-app-lookup.ts +3 -12
  106. package/src/server/html-node-summary.ts +19 -10
  107. package/src/server/html-primitives.ts +326 -97
  108. package/src/server/html-surface.ts +6 -9
  109. package/src/server/image-source.ts +6 -4
  110. package/src/server/index.ts +320 -217
  111. package/src/server/intent-registry.ts +2 -5
  112. package/src/server/mcp-app-candidate.ts +5 -10
  113. package/src/server/mcp-app-host.ts +14 -38
  114. package/src/server/mcp-app-runtime.ts +12 -20
  115. package/src/server/mutation-history.ts +15 -5
  116. package/src/server/operations/composites.ts +1 -3
  117. package/src/server/operations/http.ts +2 -3
  118. package/src/server/operations/index.ts +7 -1
  119. package/src/server/operations/invoker.ts +4 -3
  120. package/src/server/operations/mcp.ts +22 -30
  121. package/src/server/operations/ops/annotation.ts +122 -10
  122. package/src/server/operations/ops/app.ts +98 -73
  123. package/src/server/operations/ops/ax-await.ts +17 -10
  124. package/src/server/operations/ops/ax-read.ts +347 -0
  125. package/src/server/operations/ops/ax-shared.ts +2 -7
  126. package/src/server/operations/ops/ax-state.ts +32 -14
  127. package/src/server/operations/ops/ax-timeline.ts +32 -19
  128. package/src/server/operations/ops/ax-work.ts +54 -37
  129. package/src/server/operations/ops/batch.ts +39 -14
  130. package/src/server/operations/ops/canvas-wire.ts +91 -0
  131. package/src/server/operations/ops/edges.ts +37 -25
  132. package/src/server/operations/ops/ext-app.ts +346 -0
  133. package/src/server/operations/ops/groups.ts +49 -20
  134. package/src/server/operations/ops/intent.ts +18 -12
  135. package/src/server/operations/ops/json-render.ts +239 -98
  136. package/src/server/operations/ops/nodes.ts +298 -109
  137. package/src/server/operations/ops/query.ts +46 -28
  138. package/src/server/operations/ops/snapshots.ts +35 -26
  139. package/src/server/operations/ops/validate.ts +2 -1
  140. package/src/server/operations/ops/viewport.ts +60 -16
  141. package/src/server/operations/ops/webview.ts +44 -18
  142. package/src/server/operations/registry.ts +2 -3
  143. package/src/server/operations/types.ts +7 -5
  144. package/src/server/operations/webview-runner.ts +1 -3
  145. package/src/server/placement.ts +8 -18
  146. package/src/server/server.ts +108 -1027
  147. package/src/server/spatial-analysis.ts +39 -25
  148. package/src/server/trace-manager.ts +3 -8
  149. package/src/server/web-artifacts.ts +23 -27
  150. package/src/server/webpage-node.ts +5 -13
  151. package/src/shared/auto-arrange.ts +12 -5
  152. package/src/shared/content-height-reporter.ts +8 -6
  153. package/src/shared/ext-app-tool-result.ts +2 -6
  154. package/src/shared/placement.ts +1 -4
  155. package/src/shared/semantic-attention.ts +39 -37
  156. package/src/shared/surface.ts +8 -4
@@ -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' : ''}
@@ -32,10 +32,7 @@ function RenderedMarkdown({ html }: { html: string }) {
32
32
  return <div ref={containerRef} />;
33
33
  }
34
34
 
35
- export function ResponseNode({
36
- node,
37
- expanded = false,
38
- }: { node: CanvasNodeState; expanded?: boolean }) {
35
+ export function ResponseNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
39
36
  const content = (node.data.content as string) || '';
40
37
  const status = (node.data.status as string) || 'streaming';
41
38
  const [rendered, setRendered] = useState('');
@@ -59,18 +56,11 @@ export function ResponseNode({
59
56
  }, [content]);
60
57
 
61
58
  const handleReply = useCallback(() => {
62
- submitCanvasPrompt(
63
- '',
64
- { x: node.position.x, y: node.position.y + node.size.height + 24 },
65
- node.id,
66
- );
59
+ submitCanvasPrompt('', { x: node.position.x, y: node.position.y + node.size.height + 24 }, node.id);
67
60
  }, [node]);
68
61
 
69
62
  return (
70
- <div
71
- class="response-node-inner"
72
- style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
73
- >
63
+ <div class="response-node-inner" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
74
64
  {/* Streaming indicator */}
75
65
  {isStreaming && (
76
66
  <div
@@ -2,17 +2,13 @@ import { PHASE_COLORS } from '../theme/tokens';
2
2
  import type { CanvasNodeState } from '../types';
3
3
 
4
4
  export function getStatusDisplayPhase(node: CanvasNodeState): string {
5
- const phase = typeof node.data.phase === 'string' && node.data.phase.trim().length > 0
6
- ? node.data.phase.trim()
7
- : '';
5
+ const phase = typeof node.data.phase === 'string' && node.data.phase.trim().length > 0 ? node.data.phase.trim() : '';
8
6
  if (phase) return phase;
9
- const content = typeof node.data.content === 'string' && node.data.content.trim().length > 0
10
- ? node.data.content.trim()
11
- : '';
7
+ const content =
8
+ typeof node.data.content === 'string' && node.data.content.trim().length > 0 ? node.data.content.trim() : '';
12
9
  if (content) return content;
13
- const status = typeof node.data.status === 'string' && node.data.status.trim().length > 0
14
- ? node.data.status.trim()
15
- : '';
10
+ const status =
11
+ typeof node.data.status === 'string' && node.data.status.trim().length > 0 ? node.data.status.trim() : '';
16
12
  return status || 'idle';
17
13
  }
18
14
 
@@ -21,15 +21,9 @@ export function StatusSummary({ node }: { node: CanvasNodeState }) {
21
21
  flexShrink: 0,
22
22
  }}
23
23
  />
24
- <span
25
- style={{ color: phaseColor, fontSize: '10px', textTransform: 'uppercase', fontWeight: 600 }}
26
- >
27
- {phase}
28
- </span>
24
+ <span style={{ color: phaseColor, fontSize: '10px', textTransform: 'uppercase', fontWeight: 600 }}>{phase}</span>
29
25
  {activeTool && (
30
- <span style={{ color: 'var(--c-warn)', fontSize: '10px', fontFamily: 'var(--mono)' }}>
31
- ⚙ {activeTool}
32
- </span>
26
+ <span style={{ color: 'var(--c-warn)', fontSize: '10px', fontFamily: 'var(--mono)' }}>⚙ {activeTool}</span>
33
27
  )}
34
28
  {subagent && subagent.state !== 'completed' && (
35
29
  <span style={{ color: 'var(--c-subagent)', fontSize: '10px' }}>⠉ {subagent.name}</span>
@@ -20,11 +20,12 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
20
20
  const url = typeof node.data.url === 'string' ? node.data.url : '';
21
21
  const pageTitle = typeof node.data.pageTitle === 'string' ? node.data.pageTitle : '';
22
22
  const description = typeof node.data.description === 'string' ? node.data.description : '';
23
- const excerpt = typeof node.data.excerpt === 'string'
24
- ? node.data.excerpt
25
- : typeof node.data.content === 'string'
26
- ? node.data.content
27
- : '';
23
+ const excerpt =
24
+ typeof node.data.excerpt === 'string'
25
+ ? node.data.excerpt
26
+ : typeof node.data.content === 'string'
27
+ ? node.data.content
28
+ : '';
28
29
  const status = typeof node.data.status === 'string' ? node.data.status : 'idle';
29
30
  const error = typeof node.data.error === 'string' ? node.data.error : '';
30
31
  const fetchedAt = formatFetchedAt(typeof node.data.fetchedAt === 'string' ? node.data.fetchedAt : undefined);
@@ -55,12 +56,7 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
55
56
  return <div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>No webpage URL set</div>;
56
57
  }
57
58
 
58
- const statusTone =
59
- status === 'ready'
60
- ? 'var(--c-ok)'
61
- : status === 'error'
62
- ? 'var(--c-danger)'
63
- : 'var(--c-warn)';
59
+ const statusTone = status === 'ready' ? 'var(--c-ok)' : status === 'error' ? 'var(--c-danger)' : 'var(--c-warn)';
64
60
 
65
61
  return (
66
62
  <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', height: '100%' }}>
@@ -123,7 +119,9 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
123
119
  />
124
120
  )}
125
121
  {description && (
126
- <p style={{ margin: 0, color: 'var(--c-text-soft)', lineHeight: 1.5, fontSize: expanded ? '14px' : '12px' }}>
122
+ <p
123
+ style={{ margin: 0, color: 'var(--c-text-soft)', lineHeight: 1.5, fontSize: expanded ? '14px' : '12px' }}
124
+ >
127
125
  {description}
128
126
  </p>
129
127
  )}
@@ -232,7 +230,8 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
232
230
  background: 'rgba(255,255,255,0.03)',
233
231
  }}
234
232
  >
235
- Live preview (best effort). If this stays blank, the site likely blocks framing. The cached text snapshot below still works.
233
+ Live preview (best effort). If this stays blank, the site likely blocks framing. The cached text snapshot
234
+ below still works.
236
235
  </div>
237
236
  <iframe
238
237
  class="webpage-node-iframe"
@@ -274,7 +273,14 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
274
273
  }}
275
274
  >
276
275
  {excerpt ? (
277
- <div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.55, color: 'var(--c-text)', fontSize: expanded ? '14px' : '12px' }}>
276
+ <div
277
+ style={{
278
+ whiteSpace: 'pre-wrap',
279
+ lineHeight: 1.55,
280
+ color: 'var(--c-text)',
281
+ fontSize: expanded ? '14px' : '12px',
282
+ }}
283
+ >
278
284
  {excerpt}
279
285
  </div>
280
286
  ) : (
@@ -7,12 +7,14 @@ interface FrameDocumentCreateResponse {
7
7
  }
8
8
 
9
9
  function isFrameDocumentCreateResponse(value: unknown): value is FrameDocumentCreateResponse {
10
- return Boolean(value)
11
- && typeof value === 'object'
12
- && value !== null
13
- && !Array.isArray(value)
14
- && 'ok' in value
15
- && typeof (value as { ok: unknown }).ok === 'boolean';
10
+ return (
11
+ Boolean(value) &&
12
+ typeof value === 'object' &&
13
+ value !== null &&
14
+ !Array.isArray(value) &&
15
+ 'ok' in value &&
16
+ typeof (value as { ok: unknown }).ok === 'boolean'
17
+ );
16
18
  }
17
19
 
18
20
  export async function createIframeDocumentUrl(html: string, sandbox: string): Promise<string> {
@@ -21,17 +23,21 @@ export async function createIframeDocumentUrl(html: string, sandbox: string): Pr
21
23
  headers: { 'Content-Type': 'application/json' },
22
24
  body: JSON.stringify({ html, sandbox }),
23
25
  });
24
- const json = await response.json() as unknown;
26
+ const json = (await response.json()) as unknown;
25
27
  if (!response.ok || !isFrameDocumentCreateResponse(json) || !json.ok || typeof json.url !== 'string') {
26
- const message = isFrameDocumentCreateResponse(json) && json.error
27
- ? json.error
28
- : `Frame document request failed with HTTP ${response.status}`;
28
+ const message =
29
+ isFrameDocumentCreateResponse(json) && json.error
30
+ ? json.error
31
+ : `Frame document request failed with HTTP ${response.status}`;
29
32
  throw new Error(message);
30
33
  }
31
34
  return json.url;
32
35
  }
33
36
 
34
- export function useIframeDocument(html: string, sandbox: string): { attributes: { src?: string }; ready: boolean; key: string } {
37
+ export function useIframeDocument(
38
+ html: string,
39
+ sandbox: string,
40
+ ): { attributes: { src?: string }; ready: boolean; key: string } {
35
41
  const [src, setSrc] = useState<string | null>(null);
36
42
 
37
43
  useEffect(() => {
@@ -50,9 +56,12 @@ export function useIframeDocument(html: string, sandbox: string): { attributes:
50
56
  };
51
57
  }, [html, sandbox]);
52
58
 
53
- return useMemo(() => ({
54
- attributes: src ? { src } : {},
55
- ready: Boolean(src),
56
- key: src ?? '',
57
- }), [src]);
59
+ return useMemo(
60
+ () => ({
61
+ attributes: src ? { src } : {},
62
+ ready: Boolean(src),
63
+ key: src ?? '',
64
+ }),
65
+ [src],
66
+ );
58
67
  }
@@ -52,13 +52,7 @@ function metadataWarnings(node: CanvasNodeState): ImageNodeWarning[] {
52
52
  }
53
53
 
54
54
  function looksLikeLoginCapture(node: CanvasNodeState): boolean {
55
- const haystack = [
56
- node.data.title,
57
- node.data.caption,
58
- node.data.alt,
59
- node.data.src,
60
- node.data.path,
61
- ]
55
+ const haystack = [node.data.title, node.data.caption, node.data.alt, node.data.src, node.data.path]
62
56
  .map((value) => readTrimmedString(value)?.toLowerCase() ?? '')
63
57
  .join(' ');
64
58
 
@@ -168,11 +168,26 @@ export function getSelectionRect(ta: HTMLTextAreaElement): { top: number; left:
168
168
  const mirror = document.createElement('div');
169
169
  const style = getComputedStyle(ta);
170
170
  const props = [
171
- 'fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing',
172
- 'wordSpacing', 'textIndent', 'whiteSpace', 'wordWrap', 'overflowWrap',
173
- 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
174
- 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
175
- 'boxSizing', 'width',
171
+ 'fontFamily',
172
+ 'fontSize',
173
+ 'fontWeight',
174
+ 'lineHeight',
175
+ 'letterSpacing',
176
+ 'wordSpacing',
177
+ 'textIndent',
178
+ 'whiteSpace',
179
+ 'wordWrap',
180
+ 'overflowWrap',
181
+ 'paddingTop',
182
+ 'paddingRight',
183
+ 'paddingBottom',
184
+ 'paddingLeft',
185
+ 'borderTopWidth',
186
+ 'borderRightWidth',
187
+ 'borderBottomWidth',
188
+ 'borderLeftWidth',
189
+ 'boxSizing',
190
+ 'width',
176
191
  ] as const;
177
192
  for (const p of props) {
178
193
  mirror.style[p] = style[p];