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,509 @@
1
+ import type { CallToolResult, ListToolsResult, RequestId, Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3
+ import { AppBridge, PostMessageTransport, buildAllowAttribute } from '@modelcontextprotocol/ext-apps/app-bridge';
4
+ import { useEffect, useRef, useState } from 'preact/hooks';
5
+ import {
6
+ canvasTheme,
7
+ collapseExpandedNode,
8
+ expandNode,
9
+ expandedNodeId,
10
+ } from '../state/canvas-store';
11
+ import type { CanvasNodeState } from '../types';
12
+
13
+ type McpUiTheme = 'light' | 'dark';
14
+
15
+ type IframeLoadTarget = Pick<
16
+ HTMLIFrameElement,
17
+ 'addEventListener' | 'removeEventListener' | 'contentDocument'
18
+ >;
19
+
20
+ type ExtAppBridgeNotifications = Pick<AppBridge, 'sendToolInput' | 'sendToolResult'>;
21
+ type DisplayMode = 'inline' | 'fullscreen' | 'pip';
22
+
23
+ async function postJson<T>(url: string, body: Record<string, unknown>): Promise<T> {
24
+ const response = await fetch(url, {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify(body),
28
+ });
29
+ const json = await response.json() as {
30
+ ok: boolean;
31
+ result?: T;
32
+ error?: string;
33
+ };
34
+ if (!json.ok) throw new Error(json.error ?? `Request failed: ${url}`);
35
+ return json.result as T;
36
+ }
37
+
38
+ export function waitForExtAppFrameLoad(target: IframeLoadTarget): Promise<void> {
39
+ const readyState = target.contentDocument?.readyState;
40
+ if (readyState === 'interactive' || readyState === 'complete') {
41
+ return Promise.resolve();
42
+ }
43
+
44
+ return new Promise<void>((resolve) => {
45
+ const onLoad = () => {
46
+ target.removeEventListener('load', onLoad);
47
+ resolve();
48
+ };
49
+ target.addEventListener('load', onLoad, { once: true });
50
+ });
51
+ }
52
+
53
+ export function getExtAppBridgeInitKey(node: CanvasNodeState, retryKey: number): string {
54
+ const html = typeof node.data.html === 'string' ? node.data.html : '';
55
+ const serverName = typeof node.data.serverName === 'string' ? node.data.serverName : '';
56
+ const appSessionId = typeof node.data.appSessionId === 'string' ? node.data.appSessionId : '';
57
+ const sessionStatus = typeof node.data.sessionStatus === 'string' ? node.data.sessionStatus : '';
58
+ return `${node.id}:${retryKey}:${node.size.height}:${serverName}:${appSessionId}:${sessionStatus}:${html}`;
59
+ }
60
+
61
+ export function resolveExtAppDisplayModeRequest(
62
+ requestedMode: DisplayMode,
63
+ isExpanded: boolean,
64
+ ): { nextMode: DisplayMode; shouldExpand: boolean; shouldCollapse: boolean } {
65
+ if (requestedMode === 'fullscreen') {
66
+ return {
67
+ nextMode: 'fullscreen',
68
+ shouldExpand: !isExpanded,
69
+ shouldCollapse: false,
70
+ };
71
+ }
72
+
73
+ if (requestedMode === 'inline') {
74
+ return {
75
+ nextMode: 'inline',
76
+ shouldExpand: false,
77
+ shouldCollapse: isExpanded,
78
+ };
79
+ }
80
+
81
+ return {
82
+ nextMode: requestedMode,
83
+ shouldExpand: false,
84
+ shouldCollapse: false,
85
+ };
86
+ }
87
+
88
+ export async function sendExtAppBootstrapState(
89
+ bridge: ExtAppBridgeNotifications,
90
+ toolInput: Record<string, unknown>,
91
+ toolResult: CallToolResult | undefined,
92
+ ): Promise<void> {
93
+ await bridge.sendToolInput({ arguments: toolInput });
94
+ if (toolResult) {
95
+ await bridge.sendToolResult(toolResult);
96
+ }
97
+ }
98
+
99
+ export function ExtAppFrame({ node }: { node: CanvasNodeState }) {
100
+ const iframeRef = useRef<HTMLIFrameElement>(null);
101
+ const bridgeRef = useRef<AppBridge | null>(null);
102
+ const transportRef = useRef<PostMessageTransport | null>(null);
103
+ const latestToolInputRef = useRef<Record<string, unknown>>({});
104
+ const latestToolResultRef = useRef<CallToolResult | undefined>(undefined);
105
+ const toolResultSentRef = useRef(false);
106
+ const toolResultSendingRef = useRef<Promise<void> | null>(null);
107
+ const bridgeReadyRef = useRef(false);
108
+ const themeUnsubRef = useRef<(() => void) | null>(null);
109
+ const [status, setStatus] = useState<'loading' | 'ready' | 'done'>('loading');
110
+ const [error, setError] = useState<string | null>(null);
111
+ const [retryKey, setRetryKey] = useState(0);
112
+
113
+ const html = node.data.html as string | null;
114
+ const serverName = node.data.serverName as string | undefined;
115
+ const appSessionId = node.data.appSessionId as string | undefined;
116
+ const toolInput = (node.data.toolInput as Record<string, unknown> | undefined) ?? {};
117
+ const toolResult = node.data.toolResult as CallToolResult | undefined;
118
+ const toolName = (node.data.toolName as string) ?? 'ext-app';
119
+ const toolDefinition = node.data.toolDefinition as Tool | undefined;
120
+ const rawToolCallId = node.data.toolCallId;
121
+ const toolCallId: RequestId | undefined =
122
+ typeof rawToolCallId === 'string' || typeof rawToolCallId === 'number' ? rawToolCallId : undefined;
123
+ const resourceMeta = node.data.resourceMeta as { permissions?: Record<string, unknown> } | undefined;
124
+ const sessionStatus = node.data.sessionStatus as string | undefined;
125
+ const sessionError = node.data.sessionError as string | undefined;
126
+ const maxHeight = node.size.height;
127
+ const nodeId = node.id;
128
+ const frameKey = `${node.id}:${retryKey}`;
129
+ const bridgeInitKey = getExtAppBridgeInitKey(node, retryKey);
130
+ const toMcpTheme = (theme: string): McpUiTheme => (theme === 'light' ? 'light' : 'dark');
131
+ const isExpanded = expandedNodeId.value === nodeId;
132
+
133
+ latestToolInputRef.current = toolInput;
134
+ latestToolResultRef.current = toolResult;
135
+
136
+ const sessionUnavailableMessage =
137
+ sessionStatus === 'error'
138
+ ? (sessionError ?? 'Saved app session is unavailable. Reopen the app to restore interactivity.')
139
+ : 'Reconnecting saved app session...';
140
+
141
+ const flushToolResult = (bridge: AppBridge | null): Promise<void> | null => {
142
+ const pendingToolResult = latestToolResultRef.current;
143
+ if (!bridge || !bridgeReadyRef.current || !pendingToolResult || toolResultSentRef.current) {
144
+ return null;
145
+ }
146
+ if (toolResultSendingRef.current) return toolResultSendingRef.current;
147
+ const sendPromise = bridge
148
+ .sendToolResult(pendingToolResult)
149
+ .then(() => {
150
+ toolResultSentRef.current = true;
151
+ setStatus('done');
152
+ })
153
+ .catch((err) => {
154
+ const msg = err instanceof Error ? err.message : String(err);
155
+ setError(`Tool result delivery failed: ${msg}`);
156
+ throw err;
157
+ })
158
+ .finally(() => {
159
+ toolResultSendingRef.current = null;
160
+ });
161
+ toolResultSendingRef.current = sendPromise;
162
+ return sendPromise;
163
+ };
164
+
165
+ // Initialize bridge when iframe loads and HTML is available
166
+ useEffect(() => {
167
+ if (!html) return; // Wait for HTML to arrive
168
+ const iframe = iframeRef.current;
169
+ if (!iframe) return;
170
+ let disposed = false;
171
+ let fallbackTimer: ReturnType<typeof setTimeout> | null = null;
172
+ toolResultSentRef.current = false;
173
+ toolResultSendingRef.current = null;
174
+ bridgeReadyRef.current = false;
175
+
176
+ const clearFallbackTimer = (): void => {
177
+ if (!fallbackTimer) return;
178
+ clearTimeout(fallbackTimer);
179
+ fallbackTimer = null;
180
+ };
181
+
182
+ const init = async () => {
183
+ let contentWindow = iframe.contentWindow;
184
+ if (!contentWindow) {
185
+ await waitForExtAppFrameLoad(iframe);
186
+ if (disposed) return;
187
+ contentWindow = iframe.contentWindow;
188
+ }
189
+ if (!contentWindow) {
190
+ throw new Error('Ext-app iframe window is unavailable');
191
+ }
192
+
193
+ const bridge = new AppBridge(
194
+ null,
195
+ { name: 'PMX Canvas', version: '1.0.0' },
196
+ {
197
+ openLinks: {},
198
+ serverTools: { listChanged: false },
199
+ serverResources: { listChanged: false },
200
+ logging: {},
201
+ updateModelContext: { text: {}, structuredContent: {} },
202
+ },
203
+ {
204
+ hostContext: {
205
+ theme: toMcpTheme(canvasTheme.value),
206
+ platform: 'web',
207
+ containerDimensions: { maxHeight },
208
+ displayMode: isExpanded ? 'fullscreen' : 'inline',
209
+ locale: navigator.language,
210
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
211
+ ...(toolDefinition ? {
212
+ toolInfo: {
213
+ id: toolCallId,
214
+ tool: toolDefinition,
215
+ },
216
+ } : {}),
217
+ },
218
+ },
219
+ );
220
+
221
+ // Register handlers BEFORE connect
222
+ bridge.onsizechange = async ({ height }) => {
223
+ if (height && iframe) iframe.style.height = `${height}px`;
224
+ return {};
225
+ };
226
+
227
+ bridge.onopenlink = async ({ url }) => {
228
+ window.open(url, '_blank', 'noopener');
229
+ return {};
230
+ };
231
+
232
+ // Handle native fullscreen requests from the widget (e.g. Excalidraw expand button)
233
+ bridge.onrequestdisplaymode = async ({ mode }) => {
234
+ const { nextMode, shouldExpand, shouldCollapse } = resolveExtAppDisplayModeRequest(mode, isExpanded);
235
+ if (shouldExpand) {
236
+ expandNode(nodeId);
237
+ } else if (shouldCollapse) {
238
+ collapseExpandedNode();
239
+ }
240
+ return { mode: nextMode };
241
+ };
242
+
243
+ // Proxy callServerTool back to PMX server
244
+ bridge.oncalltool = async (params) => {
245
+ if (!appSessionId) {
246
+ throw new Error(sessionUnavailableMessage);
247
+ }
248
+ try {
249
+ const result = await postJson<CallToolResult>('/api/ext-app/call-tool', {
250
+ sessionId: appSessionId,
251
+ nodeId,
252
+ serverName,
253
+ toolName: params.name,
254
+ arguments: params.arguments ?? {},
255
+ });
256
+ setError(null);
257
+ return result;
258
+ } catch (err) {
259
+ const msg = err instanceof Error ? err.message : String(err);
260
+ setError(`Tool call failed: ${msg}`);
261
+ throw err;
262
+ }
263
+ };
264
+
265
+ bridge.setRequestHandler(ListToolsRequestSchema, async () => {
266
+ if (!appSessionId) {
267
+ return { tools: [] } satisfies ListToolsResult;
268
+ }
269
+ return postJson<ListToolsResult>('/api/ext-app/list-tools', { sessionId: appSessionId });
270
+ });
271
+
272
+ bridge.onlistresources = async () =>
273
+ appSessionId ? postJson('/api/ext-app/list-resources', { sessionId: appSessionId }) : { resources: [] };
274
+
275
+ bridge.onlistresourcetemplates = async () =>
276
+ appSessionId
277
+ ? postJson('/api/ext-app/list-resource-templates', { sessionId: appSessionId })
278
+ : { resourceTemplates: [] };
279
+
280
+ bridge.onreadresource = async (params) => {
281
+ if (!appSessionId) {
282
+ throw new Error(sessionUnavailableMessage);
283
+ }
284
+ return postJson('/api/ext-app/read-resource', {
285
+ sessionId: appSessionId,
286
+ uri: params.uri,
287
+ });
288
+ };
289
+
290
+ bridge.onlistprompts = async () =>
291
+ appSessionId ? postJson('/api/ext-app/list-prompts', { sessionId: appSessionId }) : { prompts: [] };
292
+
293
+ bridge.onupdatemodelcontext = async (params) => {
294
+ if (!appSessionId) return {};
295
+ await postJson('/api/ext-app/model-context', {
296
+ nodeId,
297
+ ...(Array.isArray(params.content) ? { content: params.content } : {}),
298
+ ...(params.structuredContent && typeof params.structuredContent === 'object'
299
+ ? { structuredContent: params.structuredContent }
300
+ : {}),
301
+ });
302
+ return {};
303
+ };
304
+
305
+ const transport = new PostMessageTransport(contentWindow, contentWindow);
306
+
307
+ bridge.oninitialized = () => {
308
+ if (disposed) return;
309
+ clearFallbackTimer();
310
+ bridgeReadyRef.current = true;
311
+ setStatus('ready');
312
+ setError(null);
313
+ void sendExtAppBootstrapState(bridge, latestToolInputRef.current, undefined)
314
+ .then(() => flushToolResult(bridge))
315
+ .catch((err) => {
316
+ const msg = err instanceof Error ? err.message : String(err);
317
+ setError(`Bridge bootstrap failed: ${msg}`);
318
+ });
319
+ };
320
+
321
+ // Fallback bootstrap for widgets whose initialized notification arrives late
322
+ // or never fires. This keeps standards-based apps usable even when the host
323
+ // handshake timing differs across SDK versions.
324
+ fallbackTimer = setTimeout(() => {
325
+ if (disposed || bridgeReadyRef.current) return;
326
+ void sendExtAppBootstrapState(bridge, latestToolInputRef.current, latestToolResultRef.current)
327
+ .then(() => {
328
+ toolResultSentRef.current = Boolean(latestToolResultRef.current);
329
+ setStatus(latestToolResultRef.current ? 'done' : 'ready');
330
+ setError(null);
331
+ })
332
+ .catch((err) => {
333
+ const msg = err instanceof Error ? err.message : String(err);
334
+ setError(`Bridge bootstrap fallback failed: ${msg}`);
335
+ });
336
+ }, 1200);
337
+
338
+ await bridge.connect(transport);
339
+ if (disposed) {
340
+ clearFallbackTimer();
341
+ await transport.close();
342
+ return;
343
+ }
344
+ bridgeRef.current = bridge;
345
+ transportRef.current = transport;
346
+
347
+ // Propagate theme changes to ext-app iframe
348
+ let firstFire = true;
349
+ themeUnsubRef.current = canvasTheme.subscribe((newTheme) => {
350
+ if (firstFire) { firstFire = false; return; }
351
+ if (disposed) return;
352
+ bridge.setHostContext?.({
353
+ theme: toMcpTheme(newTheme),
354
+ platform: 'web',
355
+ containerDimensions: { maxHeight },
356
+ displayMode: 'inline',
357
+ locale: navigator.language,
358
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
359
+ });
360
+ });
361
+
362
+ void flushToolResult(bridge);
363
+ };
364
+
365
+ init().catch((err) => {
366
+ clearFallbackTimer();
367
+ console.error('[ext-app] Bridge init failed:', err);
368
+ setError(err?.message ?? 'Bridge initialization failed');
369
+ });
370
+
371
+ return () => {
372
+ disposed = true;
373
+ clearFallbackTimer();
374
+ bridgeReadyRef.current = false;
375
+ toolResultSendingRef.current = null;
376
+ themeUnsubRef.current?.();
377
+ themeUnsubRef.current = null;
378
+ bridgeRef.current = null;
379
+ if (transportRef.current) {
380
+ transportRef.current.close().catch((closeError) => {
381
+ console.error('[ext-app] transport close failed:', closeError);
382
+ });
383
+ transportRef.current = null;
384
+ }
385
+ };
386
+ }, [bridgeInitKey]);
387
+
388
+ // Forward tool result when it arrives after bridge is ready
389
+ useEffect(() => {
390
+ if (toolResult && bridgeRef.current && (status === 'ready' || status === 'done')) {
391
+ void flushToolResult(bridgeRef.current);
392
+ }
393
+ }, [toolResult, status]);
394
+
395
+ // Loading state — HTML not yet fetched
396
+ if (!html) {
397
+ return (
398
+ <div
399
+ style={{
400
+ height: '100%',
401
+ display: 'flex',
402
+ alignItems: 'center',
403
+ justifyContent: 'center',
404
+ color: 'var(--c-muted)',
405
+ fontSize: '13px',
406
+ flexDirection: 'column',
407
+ gap: '8px',
408
+ }}
409
+ >
410
+ <div style={{ opacity: 0.6 }}>Loading {toolName} viewer...</div>
411
+ <div
412
+ style={{
413
+ width: '24px',
414
+ height: '24px',
415
+ border: '2px solid var(--c-line)',
416
+ borderTopColor: 'var(--c-muted)',
417
+ borderRadius: '50%',
418
+ animation: 'spin 1s linear infinite',
419
+ }}
420
+ />
421
+ <style>{'@keyframes spin { to { transform: rotate(360deg); } }'}</style>
422
+ </div>
423
+ );
424
+ }
425
+
426
+ return (
427
+ <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
428
+ {sessionStatus && sessionStatus !== 'ready' && (
429
+ <div
430
+ style={{
431
+ padding: '6px 10px',
432
+ fontSize: '11px',
433
+ background: sessionStatus === 'error' ? 'var(--c-danger-12)' : 'var(--c-warn-10)',
434
+ color: sessionStatus === 'error' ? 'var(--c-danger)' : 'var(--c-warn)',
435
+ borderBottom: `1px solid ${sessionStatus === 'error' ? 'var(--c-danger-12)' : 'var(--c-warn-15)'}`,
436
+ }}
437
+ >
438
+ {sessionUnavailableMessage}
439
+ </div>
440
+ )}
441
+ {error && (
442
+ <div
443
+ style={{
444
+ padding: '6px 10px',
445
+ fontSize: '11px',
446
+ background: 'var(--c-danger-12)',
447
+ color: 'var(--c-danger)',
448
+ borderBottom: '1px solid var(--c-danger-12)',
449
+ display: 'flex',
450
+ alignItems: 'center',
451
+ gap: '6px',
452
+ }}
453
+ >
454
+ <span>⚠</span>
455
+ <span style={{ flex: 1 }}>{error}</span>
456
+ <button
457
+ type="button"
458
+ onClick={() => {
459
+ setError(null);
460
+ setStatus('loading');
461
+ setRetryKey((k) => k + 1);
462
+ }}
463
+ style={{
464
+ background: 'var(--c-surface-hover)',
465
+ border: '1px solid var(--c-danger-12)',
466
+ borderRadius: '3px',
467
+ color: 'var(--c-danger)',
468
+ cursor: 'pointer',
469
+ fontSize: '10px',
470
+ padding: '1px 6px',
471
+ }}
472
+ >
473
+ Retry
474
+ </button>
475
+ <button
476
+ type="button"
477
+ onClick={() => setError(null)}
478
+ style={{
479
+ background: 'none',
480
+ border: 'none',
481
+ color: 'var(--c-danger)',
482
+ cursor: 'pointer',
483
+ fontSize: '13px',
484
+ padding: '0 2px',
485
+ }}
486
+ >
487
+ ×
488
+ </button>
489
+ </div>
490
+ )}
491
+ {status === 'loading' && (
492
+ <div style={{ padding: '8px', fontSize: '11px', color: 'var(--c-muted)' }}>
493
+ Connecting to ext-app viewer...
494
+ </div>
495
+ )}
496
+ {/* allow-scripts only (no allow-same-origin) — srcdoc gets opaque origin,
497
+ cannot access host cookies/storage/DOM. Communication via postMessage only. */}
498
+ <iframe
499
+ key={frameKey}
500
+ ref={iframeRef}
501
+ srcdoc={html}
502
+ sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox"
503
+ allow={buildAllowAttribute(resourceMeta?.permissions)}
504
+ style={{ flex: 1, border: 'none', background: 'var(--c-panel)' }}
505
+ title={`Ext App: ${toolName}`}
506
+ />
507
+ </div>
508
+ );
509
+ }