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.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/Readme.md +865 -0
- package/dist/canvas/global.css +3173 -0
- package/dist/canvas/index.js +183 -0
- package/dist/json-render/index.css +2 -0
- package/dist/json-render/index.js +389 -0
- package/dist/types/cli/agent.d.ts +13 -0
- package/dist/types/cli/index.d.ts +2 -0
- package/dist/types/cli/watch.d.ts +5 -0
- package/dist/types/client/App.d.ts +1 -0
- package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
- package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
- package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
- package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
- package/dist/types/client/canvas/DockedNode.d.ts +4 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
- package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
- package/dist/types/client/canvas/Minimap.d.ts +23 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
- package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
- package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
- package/dist/types/client/canvas/snap-guides.d.ts +23 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
- package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
- package/dist/types/client/ext-app/bridge.d.ts +161 -0
- package/dist/types/client/icons.d.ts +70 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/nodes/ContextNode.d.ts +34 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
- package/dist/types/client/nodes/FileNode.d.ts +5 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -0
- package/dist/types/client/nodes/ImageNode.d.ts +10 -0
- package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
- package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
- package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
- package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
- package/dist/types/client/nodes/PromptNode.d.ts +5 -0
- package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
- package/dist/types/client/nodes/StatusNode.d.ts +4 -0
- package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
- package/dist/types/client/nodes/TraceNode.d.ts +4 -0
- package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
- package/dist/types/client/nodes/image-warnings.d.ts +6 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
- package/dist/types/client/nodes/md-format.d.ts +25 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/attention-store.d.ts +25 -0
- package/dist/types/client/state/canvas-store.d.ts +74 -0
- package/dist/types/client/state/intent-bridge.d.ts +158 -0
- package/dist/types/client/state/sse-bridge.d.ts +5 -0
- package/dist/types/client/theme/tokens.d.ts +27 -0
- package/dist/types/client/types.d.ts +40 -0
- package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
- package/dist/types/client/utils/placement.d.ts +1 -0
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/json-render/catalog.d.ts +815 -0
- package/dist/types/json-render/charts/components.d.ts +54 -0
- package/dist/types/json-render/charts/definitions.d.ts +103 -0
- package/dist/types/json-render/charts/extra-components.d.ts +58 -0
- package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
- package/dist/types/json-render/renderer/index.d.ts +16 -0
- package/dist/types/json-render/schema.d.ts +46 -0
- package/dist/types/json-render/server.d.ts +55 -0
- package/dist/types/mcp/server.d.ts +22 -0
- package/dist/types/server/agent-context.d.ts +21 -0
- package/dist/types/server/artifact-paths.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +154 -0
- package/dist/types/server/canvas-provenance.d.ts +13 -0
- package/dist/types/server/canvas-schema.d.ts +49 -0
- package/dist/types/server/canvas-serialization.d.ts +25 -0
- package/dist/types/server/canvas-state.d.ts +174 -0
- package/dist/types/server/canvas-validation.d.ts +33 -0
- package/dist/types/server/chart-template.d.ts +29 -0
- package/dist/types/server/code-graph.d.ts +67 -0
- package/dist/types/server/context-cards.d.ts +24 -0
- package/dist/types/server/diagram-presets.d.ts +28 -0
- package/dist/types/server/ext-app-call-registry.d.ts +16 -0
- package/dist/types/server/ext-app-tool-result.d.ts +1 -0
- package/dist/types/server/file-watcher.d.ts +16 -0
- package/dist/types/server/index.d.ts +243 -0
- package/dist/types/server/mcp-app-candidate.d.ts +25 -0
- package/dist/types/server/mcp-app-host.d.ts +65 -0
- package/dist/types/server/mcp-app-runtime.d.ts +47 -0
- package/dist/types/server/mutation-history.d.ts +105 -0
- package/dist/types/server/placement.d.ts +37 -0
- package/dist/types/server/server.d.ts +103 -0
- package/dist/types/server/spatial-analysis.d.ts +87 -0
- package/dist/types/server/trace-manager.d.ts +48 -0
- package/dist/types/server/web-artifacts.d.ts +50 -0
- package/dist/types/server/webpage-node.d.ts +25 -0
- package/dist/types/shared/auto-arrange.d.ts +29 -0
- package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
- package/dist/types/shared/placement.d.ts +26 -0
- package/dist/types/shared/semantic-attention.d.ts +97 -0
- package/package.json +109 -0
- package/skills/data-analysis/SKILL.md +324 -0
- package/skills/doc-coauthoring/SKILL.md +375 -0
- package/skills/frontend-design/SKILL.md +45 -0
- package/skills/json-render-codegen/SKILL.md +112 -0
- package/skills/json-render-core/SKILL.md +265 -0
- package/skills/json-render-ink/SKILL.md +273 -0
- package/skills/json-render-mcp/SKILL.md +132 -0
- package/skills/json-render-react/SKILL.md +264 -0
- package/skills/json-render-shadcn/SKILL.md +159 -0
- package/skills/playwright-cli/SKILL.md +67 -0
- package/skills/pmx-canvas/SKILL.md +668 -0
- package/skills/pmx-canvas/evals/evals.json +186 -0
- package/skills/pmx-canvas-testing/SKILL.md +78 -0
- package/skills/published-consumer-e2e/SKILL.md +43 -0
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
- package/skills/web-artifacts-builder/SKILL.md +80 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
- package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/web-design-guidelines/SKILL.md +39 -0
- package/src/cli/agent.ts +2144 -0
- package/src/cli/index.ts +622 -0
- package/src/cli/watch.ts +88 -0
- package/src/client/App.tsx +507 -0
- package/src/client/canvas/AttentionHistory.tsx +81 -0
- package/src/client/canvas/AttentionToast.tsx +19 -0
- package/src/client/canvas/CanvasNode.tsx +363 -0
- package/src/client/canvas/CanvasViewport.tsx +590 -0
- package/src/client/canvas/CommandPalette.tsx +302 -0
- package/src/client/canvas/ContextMenu.tsx +601 -0
- package/src/client/canvas/ContextPinBar.tsx +25 -0
- package/src/client/canvas/ContextPinHud.tsx +22 -0
- package/src/client/canvas/DockedNode.tsx +66 -0
- package/src/client/canvas/EdgeLayer.tsx +280 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
- package/src/client/canvas/FocusFieldLayer.tsx +107 -0
- package/src/client/canvas/Minimap.tsx +301 -0
- package/src/client/canvas/SelectionBar.tsx +69 -0
- package/src/client/canvas/ShortcutOverlay.tsx +69 -0
- package/src/client/canvas/SnapshotPanel.tsx +236 -0
- package/src/client/canvas/snap-guides.ts +170 -0
- package/src/client/canvas/use-node-drag.ts +51 -0
- package/src/client/canvas/use-node-resize.ts +59 -0
- package/src/client/canvas/use-pan-zoom.ts +191 -0
- package/src/client/ext-app/bridge.ts +542 -0
- package/src/client/icons.tsx +424 -0
- package/src/client/index.tsx +7 -0
- package/src/client/nodes/ContextNode.tsx +412 -0
- package/src/client/nodes/ExtAppFrame.tsx +509 -0
- package/src/client/nodes/FileNode.tsx +256 -0
- package/src/client/nodes/GroupNode.tsx +39 -0
- package/src/client/nodes/ImageNode.tsx +160 -0
- package/src/client/nodes/InlineFormatBar.tsx +169 -0
- package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
- package/src/client/nodes/LedgerNode.tsx +37 -0
- package/src/client/nodes/MarkdownNode.tsx +359 -0
- package/src/client/nodes/McpAppNode.tsx +85 -0
- package/src/client/nodes/MdFormatBar.tsx +109 -0
- package/src/client/nodes/PromptNode.tsx +597 -0
- package/src/client/nodes/ResponseNode.tsx +153 -0
- package/src/client/nodes/StatusNode.tsx +84 -0
- package/src/client/nodes/StatusSummary.tsx +38 -0
- package/src/client/nodes/TraceNode.tsx +120 -0
- package/src/client/nodes/WebpageNode.tsx +288 -0
- package/src/client/nodes/image-warnings.ts +95 -0
- package/src/client/nodes/inline-editor-commands.ts +37 -0
- package/src/client/nodes/md-format.ts +206 -0
- package/src/client/state/attention-bridge.ts +328 -0
- package/src/client/state/attention-store.ts +73 -0
- package/src/client/state/canvas-store.ts +631 -0
- package/src/client/state/intent-bridge.ts +315 -0
- package/src/client/state/sse-bridge.ts +965 -0
- package/src/client/theme/global.css +3173 -0
- package/src/client/theme/tokens.ts +72 -0
- package/src/client/types-shims.d.ts +5 -0
- package/src/client/types.ts +81 -0
- package/src/client/utils/ext-app-tool-result.ts +4 -0
- package/src/client/utils/placement.ts +4 -0
- package/src/client/utils/platform.ts +2 -0
- package/src/json-render/catalog.ts +256 -0
- package/src/json-render/charts/components.tsx +198 -0
- package/src/json-render/charts/definitions.ts +81 -0
- package/src/json-render/charts/extra-components.tsx +267 -0
- package/src/json-render/charts/extra-definitions.ts +145 -0
- package/src/json-render/renderer/index.css +174 -0
- package/src/json-render/renderer/index.tsx +86 -0
- package/src/json-render/schema.ts +62 -0
- package/src/json-render/server.ts +597 -0
- package/src/mcp/server.ts +1377 -0
- package/src/server/agent-context.ts +242 -0
- package/src/server/artifact-paths.ts +17 -0
- package/src/server/canvas-operations.ts +1279 -0
- package/src/server/canvas-provenance.ts +243 -0
- package/src/server/canvas-schema.ts +432 -0
- package/src/server/canvas-serialization.ts +95 -0
- package/src/server/canvas-state.ts +1134 -0
- package/src/server/canvas-validation.ts +114 -0
- package/src/server/chart-template.ts +449 -0
- package/src/server/code-graph.ts +370 -0
- package/src/server/context-cards.ts +31 -0
- package/src/server/diagram-presets.ts +71 -0
- package/src/server/ext-app-call-registry.ts +77 -0
- package/src/server/ext-app-tool-result.ts +4 -0
- package/src/server/file-watcher.ts +121 -0
- package/src/server/index.ts +647 -0
- package/src/server/mcp-app-candidate.ts +174 -0
- package/src/server/mcp-app-host.ts +814 -0
- package/src/server/mcp-app-runtime.ts +459 -0
- package/src/server/mutation-history.ts +350 -0
- package/src/server/placement.ts +125 -0
- package/src/server/server.ts +3846 -0
- package/src/server/spatial-analysis.ts +356 -0
- package/src/server/trace-manager.ts +333 -0
- package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
- package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
- package/src/server/web-artifacts.ts +442 -0
- package/src/server/webpage-node.ts +328 -0
- package/src/shared/auto-arrange.ts +439 -0
- package/src/shared/ext-app-tool-result.ts +76 -0
- package/src/shared/placement.ts +81 -0
- 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
|
+
}
|