pmx-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/Readme.md +865 -0
  4. package/dist/canvas/global.css +3173 -0
  5. package/dist/canvas/index.js +183 -0
  6. package/dist/json-render/index.css +2 -0
  7. package/dist/json-render/index.js +389 -0
  8. package/dist/types/cli/agent.d.ts +13 -0
  9. package/dist/types/cli/index.d.ts +2 -0
  10. package/dist/types/cli/watch.d.ts +5 -0
  11. package/dist/types/client/App.d.ts +1 -0
  12. package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
  13. package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
  14. package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
  15. package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
  16. package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
  17. package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
  18. package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
  19. package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
  20. package/dist/types/client/canvas/DockedNode.d.ts +4 -0
  21. package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
  22. package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
  23. package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
  24. package/dist/types/client/canvas/Minimap.d.ts +23 -0
  25. package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
  26. package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
  27. package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
  28. package/dist/types/client/canvas/snap-guides.d.ts +23 -0
  29. package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
  30. package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
  31. package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
  32. package/dist/types/client/ext-app/bridge.d.ts +161 -0
  33. package/dist/types/client/icons.d.ts +70 -0
  34. package/dist/types/client/index.d.ts +1 -0
  35. package/dist/types/client/nodes/ContextNode.d.ts +34 -0
  36. package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
  37. package/dist/types/client/nodes/FileNode.d.ts +5 -0
  38. package/dist/types/client/nodes/GroupNode.d.ts +6 -0
  39. package/dist/types/client/nodes/ImageNode.d.ts +10 -0
  40. package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
  41. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
  42. package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
  43. package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
  44. package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
  45. package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
  46. package/dist/types/client/nodes/PromptNode.d.ts +5 -0
  47. package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
  48. package/dist/types/client/nodes/StatusNode.d.ts +4 -0
  49. package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
  50. package/dist/types/client/nodes/TraceNode.d.ts +4 -0
  51. package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
  52. package/dist/types/client/nodes/image-warnings.d.ts +6 -0
  53. package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
  54. package/dist/types/client/nodes/md-format.d.ts +25 -0
  55. package/dist/types/client/state/attention-bridge.d.ts +3 -0
  56. package/dist/types/client/state/attention-store.d.ts +25 -0
  57. package/dist/types/client/state/canvas-store.d.ts +74 -0
  58. package/dist/types/client/state/intent-bridge.d.ts +158 -0
  59. package/dist/types/client/state/sse-bridge.d.ts +5 -0
  60. package/dist/types/client/theme/tokens.d.ts +27 -0
  61. package/dist/types/client/types.d.ts +40 -0
  62. package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
  63. package/dist/types/client/utils/placement.d.ts +1 -0
  64. package/dist/types/client/utils/platform.d.ts +2 -0
  65. package/dist/types/json-render/catalog.d.ts +815 -0
  66. package/dist/types/json-render/charts/components.d.ts +54 -0
  67. package/dist/types/json-render/charts/definitions.d.ts +103 -0
  68. package/dist/types/json-render/charts/extra-components.d.ts +58 -0
  69. package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
  70. package/dist/types/json-render/renderer/index.d.ts +16 -0
  71. package/dist/types/json-render/schema.d.ts +46 -0
  72. package/dist/types/json-render/server.d.ts +55 -0
  73. package/dist/types/mcp/server.d.ts +22 -0
  74. package/dist/types/server/agent-context.d.ts +21 -0
  75. package/dist/types/server/artifact-paths.d.ts +3 -0
  76. package/dist/types/server/canvas-operations.d.ts +154 -0
  77. package/dist/types/server/canvas-provenance.d.ts +13 -0
  78. package/dist/types/server/canvas-schema.d.ts +49 -0
  79. package/dist/types/server/canvas-serialization.d.ts +25 -0
  80. package/dist/types/server/canvas-state.d.ts +174 -0
  81. package/dist/types/server/canvas-validation.d.ts +33 -0
  82. package/dist/types/server/chart-template.d.ts +29 -0
  83. package/dist/types/server/code-graph.d.ts +67 -0
  84. package/dist/types/server/context-cards.d.ts +24 -0
  85. package/dist/types/server/diagram-presets.d.ts +28 -0
  86. package/dist/types/server/ext-app-call-registry.d.ts +16 -0
  87. package/dist/types/server/ext-app-tool-result.d.ts +1 -0
  88. package/dist/types/server/file-watcher.d.ts +16 -0
  89. package/dist/types/server/index.d.ts +243 -0
  90. package/dist/types/server/mcp-app-candidate.d.ts +25 -0
  91. package/dist/types/server/mcp-app-host.d.ts +65 -0
  92. package/dist/types/server/mcp-app-runtime.d.ts +47 -0
  93. package/dist/types/server/mutation-history.d.ts +105 -0
  94. package/dist/types/server/placement.d.ts +37 -0
  95. package/dist/types/server/server.d.ts +103 -0
  96. package/dist/types/server/spatial-analysis.d.ts +87 -0
  97. package/dist/types/server/trace-manager.d.ts +48 -0
  98. package/dist/types/server/web-artifacts.d.ts +50 -0
  99. package/dist/types/server/webpage-node.d.ts +25 -0
  100. package/dist/types/shared/auto-arrange.d.ts +29 -0
  101. package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
  102. package/dist/types/shared/placement.d.ts +26 -0
  103. package/dist/types/shared/semantic-attention.d.ts +97 -0
  104. package/package.json +109 -0
  105. package/skills/data-analysis/SKILL.md +324 -0
  106. package/skills/doc-coauthoring/SKILL.md +375 -0
  107. package/skills/frontend-design/SKILL.md +45 -0
  108. package/skills/json-render-codegen/SKILL.md +112 -0
  109. package/skills/json-render-core/SKILL.md +265 -0
  110. package/skills/json-render-ink/SKILL.md +273 -0
  111. package/skills/json-render-mcp/SKILL.md +132 -0
  112. package/skills/json-render-react/SKILL.md +264 -0
  113. package/skills/json-render-shadcn/SKILL.md +159 -0
  114. package/skills/playwright-cli/SKILL.md +67 -0
  115. package/skills/pmx-canvas/SKILL.md +668 -0
  116. package/skills/pmx-canvas/evals/evals.json +186 -0
  117. package/skills/pmx-canvas-testing/SKILL.md +78 -0
  118. package/skills/published-consumer-e2e/SKILL.md +43 -0
  119. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
  120. package/skills/web-artifacts-builder/SKILL.md +80 -0
  121. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
  122. package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
  123. package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  124. package/skills/web-design-guidelines/SKILL.md +39 -0
  125. package/src/cli/agent.ts +2144 -0
  126. package/src/cli/index.ts +622 -0
  127. package/src/cli/watch.ts +88 -0
  128. package/src/client/App.tsx +507 -0
  129. package/src/client/canvas/AttentionHistory.tsx +81 -0
  130. package/src/client/canvas/AttentionToast.tsx +19 -0
  131. package/src/client/canvas/CanvasNode.tsx +363 -0
  132. package/src/client/canvas/CanvasViewport.tsx +590 -0
  133. package/src/client/canvas/CommandPalette.tsx +302 -0
  134. package/src/client/canvas/ContextMenu.tsx +601 -0
  135. package/src/client/canvas/ContextPinBar.tsx +25 -0
  136. package/src/client/canvas/ContextPinHud.tsx +22 -0
  137. package/src/client/canvas/DockedNode.tsx +66 -0
  138. package/src/client/canvas/EdgeLayer.tsx +280 -0
  139. package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
  140. package/src/client/canvas/FocusFieldLayer.tsx +107 -0
  141. package/src/client/canvas/Minimap.tsx +301 -0
  142. package/src/client/canvas/SelectionBar.tsx +69 -0
  143. package/src/client/canvas/ShortcutOverlay.tsx +69 -0
  144. package/src/client/canvas/SnapshotPanel.tsx +236 -0
  145. package/src/client/canvas/snap-guides.ts +170 -0
  146. package/src/client/canvas/use-node-drag.ts +51 -0
  147. package/src/client/canvas/use-node-resize.ts +59 -0
  148. package/src/client/canvas/use-pan-zoom.ts +191 -0
  149. package/src/client/ext-app/bridge.ts +542 -0
  150. package/src/client/icons.tsx +424 -0
  151. package/src/client/index.tsx +7 -0
  152. package/src/client/nodes/ContextNode.tsx +412 -0
  153. package/src/client/nodes/ExtAppFrame.tsx +509 -0
  154. package/src/client/nodes/FileNode.tsx +256 -0
  155. package/src/client/nodes/GroupNode.tsx +39 -0
  156. package/src/client/nodes/ImageNode.tsx +160 -0
  157. package/src/client/nodes/InlineFormatBar.tsx +169 -0
  158. package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
  159. package/src/client/nodes/LedgerNode.tsx +37 -0
  160. package/src/client/nodes/MarkdownNode.tsx +359 -0
  161. package/src/client/nodes/McpAppNode.tsx +85 -0
  162. package/src/client/nodes/MdFormatBar.tsx +109 -0
  163. package/src/client/nodes/PromptNode.tsx +597 -0
  164. package/src/client/nodes/ResponseNode.tsx +153 -0
  165. package/src/client/nodes/StatusNode.tsx +84 -0
  166. package/src/client/nodes/StatusSummary.tsx +38 -0
  167. package/src/client/nodes/TraceNode.tsx +120 -0
  168. package/src/client/nodes/WebpageNode.tsx +288 -0
  169. package/src/client/nodes/image-warnings.ts +95 -0
  170. package/src/client/nodes/inline-editor-commands.ts +37 -0
  171. package/src/client/nodes/md-format.ts +206 -0
  172. package/src/client/state/attention-bridge.ts +328 -0
  173. package/src/client/state/attention-store.ts +73 -0
  174. package/src/client/state/canvas-store.ts +631 -0
  175. package/src/client/state/intent-bridge.ts +315 -0
  176. package/src/client/state/sse-bridge.ts +965 -0
  177. package/src/client/theme/global.css +3173 -0
  178. package/src/client/theme/tokens.ts +72 -0
  179. package/src/client/types-shims.d.ts +5 -0
  180. package/src/client/types.ts +81 -0
  181. package/src/client/utils/ext-app-tool-result.ts +4 -0
  182. package/src/client/utils/placement.ts +4 -0
  183. package/src/client/utils/platform.ts +2 -0
  184. package/src/json-render/catalog.ts +256 -0
  185. package/src/json-render/charts/components.tsx +198 -0
  186. package/src/json-render/charts/definitions.ts +81 -0
  187. package/src/json-render/charts/extra-components.tsx +267 -0
  188. package/src/json-render/charts/extra-definitions.ts +145 -0
  189. package/src/json-render/renderer/index.css +174 -0
  190. package/src/json-render/renderer/index.tsx +86 -0
  191. package/src/json-render/schema.ts +62 -0
  192. package/src/json-render/server.ts +597 -0
  193. package/src/mcp/server.ts +1377 -0
  194. package/src/server/agent-context.ts +242 -0
  195. package/src/server/artifact-paths.ts +17 -0
  196. package/src/server/canvas-operations.ts +1279 -0
  197. package/src/server/canvas-provenance.ts +243 -0
  198. package/src/server/canvas-schema.ts +432 -0
  199. package/src/server/canvas-serialization.ts +95 -0
  200. package/src/server/canvas-state.ts +1134 -0
  201. package/src/server/canvas-validation.ts +114 -0
  202. package/src/server/chart-template.ts +449 -0
  203. package/src/server/code-graph.ts +370 -0
  204. package/src/server/context-cards.ts +31 -0
  205. package/src/server/diagram-presets.ts +71 -0
  206. package/src/server/ext-app-call-registry.ts +77 -0
  207. package/src/server/ext-app-tool-result.ts +4 -0
  208. package/src/server/file-watcher.ts +121 -0
  209. package/src/server/index.ts +647 -0
  210. package/src/server/mcp-app-candidate.ts +174 -0
  211. package/src/server/mcp-app-host.ts +814 -0
  212. package/src/server/mcp-app-runtime.ts +459 -0
  213. package/src/server/mutation-history.ts +350 -0
  214. package/src/server/placement.ts +125 -0
  215. package/src/server/server.ts +3846 -0
  216. package/src/server/spatial-analysis.ts +356 -0
  217. package/src/server/trace-manager.ts +333 -0
  218. package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
  219. package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
  220. package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
  221. package/src/server/web-artifacts.ts +442 -0
  222. package/src/server/webpage-node.ts +328 -0
  223. package/src/shared/auto-arrange.ts +439 -0
  224. package/src/shared/ext-app-tool-result.ts +76 -0
  225. package/src/shared/placement.ts +81 -0
  226. package/src/shared/semantic-attention.ts +598 -0
@@ -0,0 +1,153 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
2
+ import { renderMarkdown, submitCanvasPrompt } from '../state/intent-bridge';
3
+ import type { CanvasNodeState } from '../types';
4
+
5
+ /** Strip dangerous HTML from rendered markdown to prevent XSS. */
6
+ function sanitizeHtml(html: string): string {
7
+ return html
8
+ .replace(/<script[\s>][\s\S]*?<\/script>/gi, '')
9
+ .replace(/<iframe[\s>][\s\S]*?<\/iframe>/gi, '')
10
+ .replace(/<object[\s>][\s\S]*?<\/object>/gi, '')
11
+ .replace(/<embed[\s>][\s\S]*?(?:\/>|<\/embed>)/gi, '')
12
+ .replace(/<link[\s>][\s\S]*?(?:\/>|<\/link>)/gi, '')
13
+ .replace(/\bon\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
14
+ .replace(/href\s*=\s*"javascript:[^"]*"/gi, 'href="#"')
15
+ .replace(/href\s*=\s*'javascript:[^']*'/gi, "href='#'");
16
+ }
17
+
18
+ function RenderedMarkdown({ html }: { html: string }) {
19
+ const containerRef = useRef<HTMLDivElement>(null);
20
+
21
+ useEffect(() => {
22
+ const container = containerRef.current;
23
+ if (!container) return;
24
+ container.replaceChildren();
25
+ if (!html) return;
26
+
27
+ const template = document.createElement('template');
28
+ template.innerHTML = html;
29
+ container.append(template.content.cloneNode(true));
30
+ }, [html]);
31
+
32
+ return <div ref={containerRef} />;
33
+ }
34
+
35
+ export function ResponseNode({
36
+ node,
37
+ expanded = false,
38
+ }: { node: CanvasNodeState; expanded?: boolean }) {
39
+ const content = (node.data.content as string) || '';
40
+ const status = (node.data.status as string) || 'streaming';
41
+ const [rendered, setRendered] = useState('');
42
+
43
+ const isStreaming = status === 'streaming';
44
+ const isComplete = status === 'complete';
45
+
46
+ // Re-render markdown when content changes
47
+ useEffect(() => {
48
+ if (!content) {
49
+ setRendered('');
50
+ return;
51
+ }
52
+ let cancelled = false;
53
+ renderMarkdown(content).then((html) => {
54
+ if (!cancelled) setRendered(sanitizeHtml(html));
55
+ });
56
+ return () => {
57
+ cancelled = true;
58
+ };
59
+ }, [content]);
60
+
61
+ const handleReply = useCallback(() => {
62
+ submitCanvasPrompt(
63
+ '',
64
+ { x: node.position.x, y: node.position.y + node.size.height + 24 },
65
+ node.id,
66
+ );
67
+ }, [node]);
68
+
69
+ return (
70
+ <div
71
+ class="response-node-inner"
72
+ style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
73
+ >
74
+ {/* Streaming indicator */}
75
+ {isStreaming && (
76
+ <div
77
+ class="response-streaming-bar"
78
+ style={{
79
+ height: '2px',
80
+ background: 'linear-gradient(90deg, transparent, var(--c-accent), transparent)',
81
+ animation: 'response-stream-pulse 1.5s ease-in-out infinite',
82
+ borderRadius: '1px',
83
+ marginBottom: '4px',
84
+ flexShrink: 0,
85
+ }}
86
+ />
87
+ )}
88
+
89
+ {/* Rendered markdown content */}
90
+ <div
91
+ class={expanded ? 'md-reader' : undefined}
92
+ style={{
93
+ flex: 1,
94
+ overflow: 'auto',
95
+ minHeight: 0,
96
+ opacity: content ? 1 : 0.4,
97
+ transition: 'opacity 0.2s ease',
98
+ }}
99
+ >
100
+ {rendered ? (
101
+ <div class={expanded ? 'md-reader-content' : undefined}>
102
+ <RenderedMarkdown html={rendered} />
103
+ </div>
104
+ ) : (
105
+ <div style={{ color: 'var(--c-muted)', fontStyle: 'italic', fontSize: '13px' }}>
106
+ {isStreaming ? 'Waiting for response…' : 'Empty response'}
107
+ </div>
108
+ )}
109
+ </div>
110
+
111
+ {/* Footer */}
112
+ <div
113
+ style={{
114
+ display: 'flex',
115
+ justifyContent: 'space-between',
116
+ alignItems: 'center',
117
+ paddingTop: '6px',
118
+ borderTop: '1px solid var(--c-line)',
119
+ marginTop: '4px',
120
+ flexShrink: 0,
121
+ }}
122
+ >
123
+ <span
124
+ style={{
125
+ fontSize: '10px',
126
+ textTransform: 'uppercase',
127
+ fontWeight: 600,
128
+ color: isStreaming ? 'var(--c-accent)' : isComplete ? 'var(--c-ok)' : 'var(--c-muted)',
129
+ }}
130
+ >
131
+ {isStreaming ? 'Streaming…' : isComplete ? 'Complete' : status}
132
+ </span>
133
+ {isComplete && (
134
+ <button
135
+ type="button"
136
+ onClick={handleReply}
137
+ style={{
138
+ padding: '3px 10px',
139
+ fontSize: '11px',
140
+ background: 'rgba(70,182,255,0.1)',
141
+ border: '1px solid rgba(70,182,255,0.3)',
142
+ borderRadius: 'var(--radius-sm)',
143
+ color: 'var(--c-accent)',
144
+ cursor: 'pointer',
145
+ }}
146
+ >
147
+ Reply
148
+ </button>
149
+ )}
150
+ </div>
151
+ </div>
152
+ );
153
+ }
@@ -0,0 +1,84 @@
1
+ import { PHASE_COLORS } from '../theme/tokens';
2
+ import type { CanvasNodeState } from '../types';
3
+
4
+ export function StatusNode({ node }: { node: CanvasNodeState }) {
5
+ const phase = (node.data.phase as string) || 'idle';
6
+ const detail = (node.data.detail as string) || '';
7
+ const message = (node.data.message as string) || '';
8
+ const level = (node.data.level as string) || 'ok';
9
+ const activeTool = node.data.activeTool as string | null;
10
+ const subagent = node.data.subagent as { state: string; name: string } | undefined;
11
+
12
+ const phaseColor = PHASE_COLORS[phase] ?? 'var(--c-muted)';
13
+ const isActive = phase !== 'idle';
14
+
15
+ return (
16
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', fontSize: '12px' }}>
17
+ {/* Phase indicator */}
18
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
19
+ <div
20
+ style={{
21
+ width: '8px',
22
+ height: '8px',
23
+ borderRadius: '50%',
24
+ background: phaseColor,
25
+ boxShadow: isActive ? `0 0 8px ${phaseColor}` : 'none',
26
+ animation: isActive ? 'pulse 1.5s infinite' : 'none',
27
+ flexShrink: 0,
28
+ }}
29
+ />
30
+ <span
31
+ style={{
32
+ fontWeight: 600,
33
+ color: phaseColor,
34
+ textTransform: 'uppercase',
35
+ letterSpacing: '0.05em',
36
+ }}
37
+ >
38
+ {phase}
39
+ </span>
40
+ {detail && (
41
+ <span
42
+ style={{
43
+ color: 'var(--c-muted)',
44
+ overflow: 'hidden',
45
+ textOverflow: 'ellipsis',
46
+ whiteSpace: 'nowrap',
47
+ }}
48
+ >
49
+ {detail}
50
+ </span>
51
+ )}
52
+ </div>
53
+
54
+ {/* Active tool */}
55
+ {activeTool && (
56
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--c-warn)' }}>
57
+ <span style={{ fontSize: '10px' }}>âš™</span>
58
+ <span style={{ fontFamily: 'var(--mono)', fontSize: '11px' }}>{activeTool}</span>
59
+ </div>
60
+ )}
61
+
62
+ {/* Sub-agent */}
63
+ {subagent && subagent.state !== 'completed' && (
64
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--c-subagent)' }}>
65
+ <span style={{ fontSize: '10px' }}>â ‰</span>
66
+ <span>{subagent.name}</span>
67
+ <span style={{ color: 'var(--c-muted)', fontSize: '10px' }}>({subagent.state})</span>
68
+ </div>
69
+ )}
70
+
71
+ {/* Status message */}
72
+ {message && (
73
+ <div
74
+ style={{
75
+ color: level === 'warn' ? 'var(--c-warn)' : level === 'error' ? 'var(--c-danger)' : 'var(--c-muted)',
76
+ lineHeight: 1.4,
77
+ }}
78
+ >
79
+ {message}
80
+ </div>
81
+ )}
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,38 @@
1
+ import { PHASE_COLORS } from '../theme/tokens';
2
+ import type { CanvasNodeState } from '../types';
3
+
4
+ export function StatusSummary({ node }: { node: CanvasNodeState }) {
5
+ const phase = (node.data.phase as string) || 'idle';
6
+ const activeTool = node.data.activeTool as string | null;
7
+ const subagent = node.data.subagent as { state: string; name: string } | undefined;
8
+ const phaseColor = PHASE_COLORS[phase] ?? 'var(--c-muted)';
9
+ const isActive = phase !== 'idle';
10
+
11
+ return (
12
+ <span style={{ display: 'flex', alignItems: 'center', gap: '6px', flex: 1, minWidth: 0 }}>
13
+ <span
14
+ style={{
15
+ width: '6px',
16
+ height: '6px',
17
+ borderRadius: '50%',
18
+ background: phaseColor,
19
+ animation: isActive ? 'pulse 1.5s infinite' : 'none',
20
+ flexShrink: 0,
21
+ }}
22
+ />
23
+ <span
24
+ style={{ color: phaseColor, fontSize: '10px', textTransform: 'uppercase', fontWeight: 600 }}
25
+ >
26
+ {phase}
27
+ </span>
28
+ {activeTool && (
29
+ <span style={{ color: 'var(--c-warn)', fontSize: '10px', fontFamily: 'var(--mono)' }}>
30
+ âš™ {activeTool}
31
+ </span>
32
+ )}
33
+ {subagent && subagent.state !== 'completed' && (
34
+ <span style={{ color: 'var(--c-subagent)', fontSize: '10px' }}>â ‰ {subagent.name}</span>
35
+ )}
36
+ </span>
37
+ );
38
+ }
@@ -0,0 +1,120 @@
1
+ import type { CanvasNodeState } from '../types';
2
+
3
+ const CATEGORY_COLORS: Record<string, string> = {
4
+ mcp: 'var(--c-accent)',
5
+ file: 'var(--c-warn)',
6
+ subagent: 'var(--c-purple)',
7
+ other: 'var(--c-muted)',
8
+ };
9
+
10
+ const STATUS_ICONS: Record<string, string> = {
11
+ running: 'â ‹',
12
+ success: 'âś“',
13
+ failed: 'âś•',
14
+ };
15
+
16
+ const STATUS_COLORS: Record<string, string> = {
17
+ running: 'var(--c-accent)',
18
+ success: 'var(--c-ok)',
19
+ failed: 'var(--c-danger)',
20
+ };
21
+
22
+ export function TraceNode({ node }: { node: CanvasNodeState }) {
23
+ const toolName = (node.data.toolName as string) || 'unknown';
24
+ const category = (node.data.category as string) || 'other';
25
+ const status = (node.data.status as string) || 'running';
26
+ const duration = (node.data.duration as string) || '';
27
+ const resultSummary = (node.data.resultSummary as string) || '';
28
+ const error = (node.data.error as string) || '';
29
+
30
+ const catColor = CATEGORY_COLORS[category] ?? CATEGORY_COLORS.other;
31
+ const statusIcon = STATUS_ICONS[status] ?? 'â—Ś';
32
+ const statusColor = STATUS_COLORS[status] ?? 'var(--c-muted)';
33
+ const isRunning = status === 'running';
34
+
35
+ // Truncate summary to ~30 chars
36
+ const summary = error
37
+ ? error.slice(0, 30)
38
+ : resultSummary.length > 30
39
+ ? `${resultSummary.slice(0, 28)}…`
40
+ : resultSummary;
41
+
42
+ return (
43
+ <div
44
+ style={{
45
+ display: 'flex',
46
+ alignItems: 'center',
47
+ gap: '8px',
48
+ padding: '0 12px',
49
+ height: '100%',
50
+ overflow: 'hidden',
51
+ }}
52
+ >
53
+ {/* Status icon */}
54
+ <span
55
+ style={{
56
+ fontSize: '14px',
57
+ color: statusColor,
58
+ flexShrink: 0,
59
+ animation: isRunning ? 'pulse 1.5s infinite' : 'none',
60
+ }}
61
+ >
62
+ {statusIcon}
63
+ </span>
64
+
65
+ {/* Tool name + summary */}
66
+ <div
67
+ style={{
68
+ flex: 1,
69
+ minWidth: 0,
70
+ overflow: 'hidden',
71
+ }}
72
+ >
73
+ <div
74
+ style={{
75
+ fontFamily: 'var(--mono)',
76
+ fontSize: '11px',
77
+ fontWeight: 600,
78
+ color: catColor,
79
+ whiteSpace: 'nowrap',
80
+ overflow: 'hidden',
81
+ textOverflow: 'ellipsis',
82
+ }}
83
+ >
84
+ {toolName}
85
+ </div>
86
+ {summary && (
87
+ <div
88
+ style={{
89
+ fontSize: '10px',
90
+ color: error ? 'var(--c-danger)' : 'var(--c-muted)',
91
+ whiteSpace: 'nowrap',
92
+ overflow: 'hidden',
93
+ textOverflow: 'ellipsis',
94
+ lineHeight: 1.3,
95
+ }}
96
+ >
97
+ {summary}
98
+ </div>
99
+ )}
100
+ </div>
101
+
102
+ {/* Duration badge */}
103
+ {duration && (
104
+ <span
105
+ style={{
106
+ fontSize: '9px',
107
+ padding: '1px 5px',
108
+ borderRadius: '3px',
109
+ background: 'rgba(255,255,255,0.06)',
110
+ color: 'var(--c-muted)',
111
+ flexShrink: 0,
112
+ whiteSpace: 'nowrap',
113
+ }}
114
+ >
115
+ {duration}
116
+ </span>
117
+ )}
118
+ </div>
119
+ );
120
+ }
@@ -0,0 +1,288 @@
1
+ import { useCallback, useEffect, useState } from 'preact/hooks';
2
+ import { refreshWebpageNodeFromClient } from '../state/intent-bridge';
3
+ import type { CanvasNodeState } from '../types';
4
+
5
+ function formatHost(url: string): string {
6
+ try {
7
+ return new URL(url).host;
8
+ } catch {
9
+ return url;
10
+ }
11
+ }
12
+
13
+ function formatFetchedAt(value: string | undefined): string | null {
14
+ if (!value) return null;
15
+ const date = new Date(value);
16
+ return Number.isNaN(date.getTime()) ? null : date.toLocaleString();
17
+ }
18
+
19
+ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
20
+ const url = typeof node.data.url === 'string' ? node.data.url : '';
21
+ const pageTitle = typeof node.data.pageTitle === 'string' ? node.data.pageTitle : '';
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
+ : '';
28
+ const status = typeof node.data.status === 'string' ? node.data.status : 'idle';
29
+ const error = typeof node.data.error === 'string' ? node.data.error : '';
30
+ const fetchedAt = formatFetchedAt(typeof node.data.fetchedAt === 'string' ? node.data.fetchedAt : undefined);
31
+ const statusCode = typeof node.data.statusCode === 'number' ? node.data.statusCode : null;
32
+ const imageUrl = typeof node.data.imageUrl === 'string' ? node.data.imageUrl : '';
33
+ const frameBlocked = node.data.frameBlocked === true;
34
+ const frameBlockedReason = typeof node.data.frameBlockedReason === 'string' ? node.data.frameBlockedReason : '';
35
+ const [refreshing, setRefreshing] = useState(false);
36
+ const [showEmbed, setShowEmbed] = useState(expanded);
37
+
38
+ useEffect(() => {
39
+ if (expanded) {
40
+ setShowEmbed(true);
41
+ }
42
+ }, [expanded]);
43
+
44
+ const handleRefresh = useCallback(async () => {
45
+ if (!url || refreshing) return;
46
+ setRefreshing(true);
47
+ try {
48
+ await refreshWebpageNodeFromClient(node.id);
49
+ } finally {
50
+ setRefreshing(false);
51
+ }
52
+ }, [node.id, refreshing, url]);
53
+
54
+ if (!url) {
55
+ return <div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>No webpage URL set</div>;
56
+ }
57
+
58
+ const statusTone =
59
+ status === 'ready'
60
+ ? 'var(--c-ok)'
61
+ : status === 'error'
62
+ ? 'var(--c-danger)'
63
+ : 'var(--c-warn)';
64
+
65
+ return (
66
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', height: '100%' }}>
67
+ <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: '12px' }}>
68
+ <div style={{ minWidth: 0, flex: 1 }}>
69
+ <div style={{ fontSize: '12px', color: 'var(--c-muted)' }}>{formatHost(url)}</div>
70
+ <div style={{ fontSize: expanded ? '18px' : '15px', fontWeight: 700, color: 'var(--c-text)' }}>
71
+ {pageTitle || (node.data.title as string) || url}
72
+ </div>
73
+ <a
74
+ href={url}
75
+ target="_blank"
76
+ rel="noopener noreferrer"
77
+ style={{ color: 'var(--c-accent)', fontSize: '12px', wordBreak: 'break-all', textDecoration: 'none' }}
78
+ >
79
+ {url}
80
+ </a>
81
+ </div>
82
+ <div
83
+ style={{
84
+ display: 'inline-flex',
85
+ alignItems: 'center',
86
+ gap: '6px',
87
+ padding: '4px 8px',
88
+ borderRadius: '999px',
89
+ background: 'rgba(255,255,255,0.04)',
90
+ color: statusTone,
91
+ fontSize: '11px',
92
+ textTransform: 'uppercase',
93
+ letterSpacing: '0.04em',
94
+ flexShrink: 0,
95
+ }}
96
+ >
97
+ <span style={{ width: '7px', height: '7px', borderRadius: '999px', background: statusTone }} />
98
+ {status}
99
+ </div>
100
+ </div>
101
+
102
+ {(description || imageUrl) && (
103
+ <div
104
+ style={{
105
+ display: 'grid',
106
+ gridTemplateColumns: imageUrl && expanded ? '160px 1fr' : '1fr',
107
+ gap: '12px',
108
+ alignItems: 'start',
109
+ }}
110
+ >
111
+ {imageUrl && expanded && (
112
+ <img
113
+ src={imageUrl}
114
+ alt={pageTitle || 'Webpage preview image'}
115
+ style={{
116
+ width: '160px',
117
+ height: '96px',
118
+ objectFit: 'cover',
119
+ borderRadius: '10px',
120
+ border: '1px solid var(--c-line)',
121
+ background: 'var(--c-panel-soft)',
122
+ }}
123
+ />
124
+ )}
125
+ {description && (
126
+ <p style={{ margin: 0, color: 'var(--c-text-soft)', lineHeight: 1.5, fontSize: expanded ? '14px' : '12px' }}>
127
+ {description}
128
+ </p>
129
+ )}
130
+ </div>
131
+ )}
132
+
133
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
134
+ <button
135
+ type="button"
136
+ onClick={handleRefresh}
137
+ disabled={refreshing}
138
+ style={{
139
+ border: '1px solid var(--c-line)',
140
+ background: 'var(--c-panel-soft)',
141
+ color: 'var(--c-text)',
142
+ borderRadius: '8px',
143
+ padding: '6px 10px',
144
+ cursor: refreshing ? 'progress' : 'pointer',
145
+ fontSize: '12px',
146
+ }}
147
+ >
148
+ {refreshing || status === 'fetching' ? 'Refreshing…' : 'Refresh'}
149
+ </button>
150
+ {expanded && !frameBlocked && (
151
+ <button
152
+ type="button"
153
+ onClick={() => setShowEmbed((current) => !current)}
154
+ style={{
155
+ border: '1px solid var(--c-line)',
156
+ background: 'var(--c-panel-soft)',
157
+ color: 'var(--c-text)',
158
+ borderRadius: '8px',
159
+ padding: '6px 10px',
160
+ cursor: 'pointer',
161
+ fontSize: '12px',
162
+ }}
163
+ >
164
+ {showEmbed ? 'Hide live preview' : 'Show live preview'}
165
+ </button>
166
+ )}
167
+ <button
168
+ type="button"
169
+ onClick={() => window.open(url, '_blank', 'noopener')}
170
+ style={{
171
+ border: '1px solid var(--c-line)',
172
+ background: 'transparent',
173
+ color: 'var(--c-accent)',
174
+ borderRadius: '8px',
175
+ padding: '6px 10px',
176
+ cursor: 'pointer',
177
+ fontSize: '12px',
178
+ }}
179
+ >
180
+ Open in browser
181
+ </button>
182
+ </div>
183
+
184
+ {expanded && frameBlocked && (
185
+ <div
186
+ style={{
187
+ border: '1px solid var(--c-line)',
188
+ borderRadius: '12px',
189
+ overflow: 'hidden',
190
+ background: 'var(--c-panel-soft)',
191
+ }}
192
+ >
193
+ <div
194
+ style={{
195
+ padding: '8px 12px',
196
+ fontSize: '11px',
197
+ color: 'var(--c-muted)',
198
+ borderBottom: '1px solid var(--c-line)',
199
+ background: 'rgba(255,255,255,0.03)',
200
+ }}
201
+ >
202
+ Live preview unavailable. This site refuses embedding, so PMX Canvas cannot show it inline.
203
+ </div>
204
+ <div
205
+ style={{
206
+ padding: '20px',
207
+ color: 'var(--c-text-soft)',
208
+ lineHeight: 1.6,
209
+ fontSize: '13px',
210
+ }}
211
+ >
212
+ {frameBlockedReason || 'The remote site blocks iframe embedding.'}
213
+ </div>
214
+ </div>
215
+ )}
216
+
217
+ {expanded && showEmbed && !frameBlocked && (
218
+ <div
219
+ style={{
220
+ border: '1px solid var(--c-line)',
221
+ borderRadius: '12px',
222
+ overflow: 'hidden',
223
+ background: 'var(--c-panel-soft)',
224
+ }}
225
+ >
226
+ <div
227
+ style={{
228
+ padding: '8px 12px',
229
+ fontSize: '11px',
230
+ color: 'var(--c-muted)',
231
+ borderBottom: '1px solid var(--c-line)',
232
+ background: 'rgba(255,255,255,0.03)',
233
+ }}
234
+ >
235
+ Live preview (best effort). If this stays blank, the site likely blocks framing. The cached text snapshot below still works.
236
+ </div>
237
+ <iframe
238
+ class="webpage-node-iframe"
239
+ title={pageTitle || (node.data.title as string) || url}
240
+ src={url}
241
+ loading="lazy"
242
+ referrerPolicy="no-referrer"
243
+ sandbox="allow-downloads allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-scripts"
244
+ style={{
245
+ display: 'block',
246
+ width: '100%',
247
+ height: expanded ? '320px' : '180px',
248
+ border: 'none',
249
+ background: '#fff',
250
+ }}
251
+ />
252
+ </div>
253
+ )}
254
+
255
+ {(fetchedAt || statusCode !== null) && (
256
+ <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', color: 'var(--c-muted)', fontSize: '11px' }}>
257
+ {fetchedAt && <span>Fetched {fetchedAt}</span>}
258
+ {statusCode !== null && <span>HTTP {statusCode}</span>}
259
+ {frameBlocked && <span>Preview blocked by site</span>}
260
+ </div>
261
+ )}
262
+
263
+ {error && <div style={{ color: 'var(--c-danger)', fontSize: '12px', lineHeight: 1.5 }}>{error}</div>}
264
+
265
+ <div
266
+ style={{
267
+ flex: 1,
268
+ minHeight: 0,
269
+ overflow: 'auto',
270
+ border: '1px solid var(--c-line)',
271
+ borderRadius: '10px',
272
+ background: 'var(--c-panel-soft)',
273
+ padding: expanded ? '14px' : '10px',
274
+ }}
275
+ >
276
+ {excerpt ? (
277
+ <div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.55, color: 'var(--c-text)', fontSize: expanded ? '14px' : '12px' }}>
278
+ {excerpt}
279
+ </div>
280
+ ) : (
281
+ <div style={{ color: 'var(--c-dim)', fontStyle: 'italic' }}>
282
+ {status === 'error' ? 'No cached page text available.' : 'Waiting for page text...'}
283
+ </div>
284
+ )}
285
+ </div>
286
+ </div>
287
+ );
288
+ }