pmx-canvas 0.7.3 → 0.8.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/.github/extensions/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
- package/.github/extensions/pmx-canvas/extension.mjs +25 -172
- package/.github/extensions/pmx-canvas/mcp-bootstrap.ts +198 -0
- package/.github/extensions/pmx-canvas/runtime.d.mts +41 -0
- package/.github/extensions/pmx-canvas/runtime.json +3 -0
- package/.github/extensions/pmx-canvas/runtime.mjs +194 -0
- package/.github/extensions/pmx-canvas/start-mcp.mjs +51 -0
- package/.github/extensions/pmx-canvas/tools.json +2560 -0
- package/CHANGELOG.md +47 -0
- package/Readme.md +21 -7
- package/dist/canvas/fonts/OFL.txt +93 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/dist/canvas/global.css +723 -672
- package/dist/canvas/index.js +90 -88
- package/dist/canvas/mcp-app.html +7878 -0
- package/dist/canvas/mermaid-entry.js +1 -1
- package/dist/canvas/surface-theme.css +151 -46
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/commands/record.d.ts +1 -1
- package/dist/types/client/canvas/NodeContextMark.d.ts +24 -0
- package/dist/types/client/canvas/SessionReceipt.d.ts +3 -1
- package/dist/types/client/icons.d.ts +1 -2
- package/dist/types/client/mcp-app/index.d.ts +1 -0
- package/dist/types/client/nodes/ax-node-actions.d.ts +2 -2
- package/dist/types/client/nodes/markdown-sanitize.d.ts +2 -0
- package/dist/types/client/nodes/use-workbench-asset.d.ts +2 -0
- package/dist/types/client/state/attention-store.d.ts +0 -5
- package/dist/types/client/state/context-status-store.d.ts +53 -0
- package/dist/types/client/state/intent-bridge.d.ts +3 -0
- package/dist/types/client/state/near-pin-store.d.ts +16 -0
- package/dist/types/client/state/presence-store.d.ts +5 -0
- package/dist/types/client/state/session-store.d.ts +33 -3
- package/dist/types/client/state/workbench-transport.d.ts +16 -0
- package/dist/types/client/theme/tokens.d.ts +1 -3
- package/dist/types/client/types.d.ts +9 -0
- package/dist/types/json-render/server.d.ts +1 -1
- package/dist/types/mcp/app-server.d.ts +3 -0
- package/dist/types/mcp/canvas-access.d.ts +1 -1
- package/dist/types/mcp/canvas-app.d.ts +4 -0
- package/dist/types/mcp/workbench-app.d.ts +4 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/ax-state.d.ts +1 -1
- package/dist/types/server/canvas-db.d.ts +8 -0
- package/dist/types/server/canvas-geometry.d.ts +25 -0
- package/dist/types/server/canvas-operations.d.ts +1 -11
- package/dist/types/server/canvas-state.d.ts +25 -3
- package/dist/types/server/context-brief.d.ts +8 -1
- package/dist/types/server/context-reads.d.ts +30 -7
- package/dist/types/server/index.d.ts +6 -1
- package/dist/types/server/mcp-app-runtime.d.ts +1 -0
- package/dist/types/server/operations/http.d.ts +2 -0
- package/dist/types/server/operations/invoker.d.ts +4 -2
- package/dist/types/server/operations/ops/nodes.d.ts +0 -6
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/spatial-analysis.d.ts +2 -0
- package/dist/types/shared/agent-context.d.ts +22 -0
- package/dist/types/shared/agent-presence.d.ts +3 -10
- package/dist/types/shared/canvas-app.d.ts +54 -0
- package/dist/types/shared/themes.d.ts +1 -1
- package/docs/RELEASE.md +7 -3
- package/docs/agent-plugin.md +123 -0
- package/docs/ax-state-contract.md +40 -0
- package/docs/cli.md +7 -2
- package/docs/design/AgentContext.dc.html +186 -0
- package/docs/design/Before.dc.html +208 -0
- package/docs/design/BoardPins.dc.html +151 -0
- package/docs/design/Chrome.dc.html +208 -0
- package/docs/design/Context.dc.html +239 -0
- package/docs/design/Flows.dc.html +181 -0
- package/docs/design/Home.dc.html +209 -0
- package/docs/design/HomeGraph.dc.html +211 -0
- package/docs/design/HomeMap.dc.html +117 -0
- package/docs/design/HomeTeam.dc.html +176 -0
- package/docs/design/Light.dc.html +28 -0
- package/docs/design/Main.dc.html +280 -0
- package/docs/design/NearPin.dc.html +260 -0
- package/docs/design/Pane600.dc.html +120 -0
- package/docs/design/Relations.dc.html +297 -0
- package/docs/design/Themes.dc.html +121 -0
- package/docs/design/TokenSystem.dc.html +136 -0
- package/docs/design/Tokens.dc.html +163 -0
- package/docs/design/Types1.dc.html +177 -0
- package/docs/design/Types2.dc.html +226 -0
- package/docs/design/Types3.dc.html +195 -0
- package/docs/design/Uses.dc.html +152 -0
- package/docs/design/Vision.dc.html +116 -0
- package/docs/design/Zoom.dc.html +82 -0
- package/docs/design/canvas.json +274 -0
- package/docs/design.md +214 -0
- package/docs/environment.md +1 -1
- package/docs/http-api.md +80 -15
- package/docs/mcp.md +198 -3
- package/docs/node-types.md +34 -10
- package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
- package/docs/plans/plan-015-design-foundation-wave-0.md +177 -0
- package/docs/plans/plan-016-context-made-visible-wave-1.md +148 -0
- package/docs/product-vision-2026-09.md +47 -7
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +1 -0
- package/package.json +10 -4
- package/plugins/pmx-canvas/LICENSE +21 -0
- package/plugins/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
- package/plugins/pmx-canvas/assets/preview.png +0 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +930 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +3 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.mjs +194 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/steering-delivery.mjs +79 -0
- package/plugins/pmx-canvas/mcp.json +12 -0
- package/plugins/pmx-canvas/plugin.json +24 -0
- package/plugins/pmx-canvas/runtime.json +3 -0
- package/plugins/pmx-canvas/runtime.mjs +194 -0
- package/plugins/pmx-canvas/start-mcp.mjs +51 -0
- package/plugins/pmx-canvas/tools.json +2560 -0
- package/skills/pmx-canvas/SKILL.md +10 -7
- package/skills/pmx-canvas/references/full-reference.md +11 -7
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +20 -8
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas-testing/SKILL.md +5 -4
- package/src/cli/commands/copilot.ts +5 -0
- package/src/cli/commands/view.ts +1 -1
- package/src/cli/index.ts +8 -3
- package/src/client/App.tsx +6 -1
- package/src/client/canvas/AgentPresenceLayer.tsx +3 -3
- package/src/client/canvas/AnnotationLayer.tsx +1 -1
- package/src/client/canvas/CanvasNode.tsx +32 -49
- package/src/client/canvas/CanvasViewport.tsx +0 -2
- package/src/client/canvas/CommandPalette.tsx +3 -2
- package/src/client/canvas/ContextMenu.tsx +11 -0
- package/src/client/canvas/EdgeLayer.tsx +1 -1
- package/src/client/canvas/ExportDialog.tsx +33 -6
- package/src/client/canvas/IntentLayer.tsx +2 -2
- package/src/client/canvas/Minimap.tsx +12 -7
- package/src/client/canvas/NodeContextMark.tsx +156 -0
- package/src/client/canvas/SessionPanel.tsx +4 -7
- package/src/client/canvas/SessionReceipt.tsx +114 -17
- package/src/client/canvas/ShortcutOverlay.tsx +6 -1
- package/src/client/canvas/SnapshotPanel.tsx +11 -2
- package/src/client/canvas/ToolRail.tsx +12 -9
- package/src/client/canvas/TopBar.tsx +16 -11
- package/src/client/icons.tsx +2 -12
- package/src/client/mcp-app/index.tsx +137 -0
- package/src/client/mcp-app/style.css +10 -0
- package/src/client/nodes/AttachmentNode.tsx +8 -1
- package/src/client/nodes/AxStepControls.tsx +1 -1
- package/src/client/nodes/ContextNode.tsx +9 -9
- package/src/client/nodes/ExtAppFrame.tsx +13 -8
- package/src/client/nodes/FileNode.tsx +8 -5
- package/src/client/nodes/ImageNode.tsx +4 -2
- package/src/client/nodes/InlineMarkdownEditor.tsx +2 -1
- package/src/client/nodes/LedgerNode.tsx +2 -2
- package/src/client/nodes/MarkdownNode.tsx +44 -9
- package/src/client/nodes/McpAppNode.tsx +6 -4
- package/src/client/nodes/PromptNode.tsx +5 -17
- package/src/client/nodes/ResponseNode.tsx +3 -15
- package/src/client/nodes/StatusNode.tsx +2 -2
- package/src/client/nodes/StatusSummary.tsx +3 -1
- package/src/client/nodes/TraceNode.tsx +6 -4
- package/src/client/nodes/WebpageNode.tsx +1 -1
- package/src/client/nodes/ax-node-actions.ts +2 -2
- package/src/client/nodes/iframe-document-url.ts +3 -2
- package/src/client/nodes/markdown-sanitize.ts +10 -0
- package/src/client/nodes/surface-url.ts +10 -0
- package/src/client/nodes/use-surface-frame.ts +3 -1
- package/src/client/nodes/use-workbench-asset.ts +26 -0
- package/src/client/state/attention-bridge.ts +0 -8
- package/src/client/state/attention-store.ts +0 -17
- package/src/client/state/canvas-store.ts +4 -2
- package/src/client/state/context-status-store.ts +66 -0
- package/src/client/state/human-store.ts +2 -0
- package/src/client/state/iframe-mode.ts +5 -0
- package/src/client/state/intent-bridge.ts +10 -4
- package/src/client/state/near-pin-store.ts +53 -0
- package/src/client/state/presence-store.ts +5 -10
- package/src/client/state/session-store.ts +106 -9
- package/src/client/state/sse-bridge.ts +26 -7
- package/src/client/state/theme-override.ts +1 -1
- package/src/client/state/workbench-transport.ts +36 -0
- package/src/client/theme/global.css +723 -672
- package/src/client/theme/surface-theme.css +151 -46
- package/src/client/theme/tokens.ts +7 -11
- package/src/client/types.ts +10 -0
- package/src/json-render/server.ts +71 -14
- package/src/mcp/app-server.ts +64 -0
- package/src/mcp/canvas-access.ts +14 -11
- package/src/mcp/canvas-app.ts +318 -0
- package/src/mcp/server.ts +1 -1
- package/src/mcp/workbench-app.ts +211 -0
- package/src/server/agent-presence.ts +45 -6
- package/src/server/ax-context.ts +1 -1
- package/src/server/ax-state.ts +1 -1
- package/src/server/board-export.ts +5 -2
- package/src/server/canvas-db.ts +46 -12
- package/src/server/canvas-geometry.ts +64 -0
- package/src/server/canvas-operations.ts +6 -24
- package/src/server/canvas-state.ts +105 -15
- package/src/server/canvas-validation.ts +19 -4
- package/src/server/context-brief.ts +39 -2
- package/src/server/context-reads.ts +86 -15
- package/src/server/demo-state.json +31 -31
- package/src/server/html-surface.ts +4 -5
- package/src/server/index.ts +19 -3
- package/src/server/mcp-app-runtime.ts +11 -0
- package/src/server/operations/composites.ts +4 -3
- package/src/server/operations/http.ts +4 -4
- package/src/server/operations/invoker.ts +13 -4
- package/src/server/operations/ops/ax-read.ts +1 -1
- package/src/server/operations/ops/ax-timeline.ts +37 -0
- package/src/server/operations/ops/boards.ts +1 -1
- package/src/server/operations/ops/canvas-wire.ts +3 -6
- package/src/server/operations/ops/intent.ts +31 -2
- package/src/server/operations/ops/json-render.ts +1 -1
- package/src/server/operations/ops/nodes.ts +8 -76
- package/src/server/operations/ops/query.ts +8 -2
- package/src/server/operations/ops/snapshots.ts +4 -1
- package/src/server/operations/ops/viewport.ts +29 -5
- package/src/server/operations/registry.ts +38 -4
- package/src/server/scope-fence.ts +55 -10
- package/src/server/server.ts +83 -5
- package/src/server/spatial-analysis.ts +2 -2
- package/src/server/workboard.ts +6 -1
- package/src/{server → shared}/agent-context.ts +2 -2
- package/src/shared/agent-presence.ts +3 -15
- package/src/shared/canvas-app.ts +62 -0
- package/src/shared/themes.ts +5 -5
- package/src/client/canvas/FocusFieldLayer.tsx +0 -116
- package/src/client/canvas/kind-colors.ts +0 -23
package/src/client/icons.tsx
CHANGED
|
@@ -112,14 +112,6 @@ export function IconUndo(p: IconProps): JSX.Element {
|
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
export function IconSparkle(p: IconProps): JSX.Element {
|
|
116
|
-
return (
|
|
117
|
-
<Icon {...p}>
|
|
118
|
-
<path d="m8 1 2 5 5 2-5 2-2 5-2-5-5-2 5-2Z" />
|
|
119
|
-
</Icon>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
115
|
/** Upload into a tray — attach a document. */
|
|
124
116
|
export function IconUpload(p: IconProps): JSX.Element {
|
|
125
117
|
return (
|
|
@@ -326,13 +318,11 @@ export function IconSteer(p: IconProps): JSX.Element {
|
|
|
326
318
|
);
|
|
327
319
|
}
|
|
328
320
|
|
|
329
|
-
/**
|
|
321
|
+
/** Push pin — context pinning (docs/design/Chrome.dc.html). */
|
|
330
322
|
export function IconPin(p: IconProps): JSX.Element {
|
|
331
323
|
return (
|
|
332
324
|
<Icon {...p}>
|
|
333
|
-
<
|
|
334
|
-
<line x1="11" y1="1.5" x2="11" y2="5" />
|
|
335
|
-
<circle cx="11" cy="4.5" r="1.6" fill="currentColor" stroke="none" />
|
|
325
|
+
<path d="M6 2.67h4l-.53 3.66 1.86 2H4.67l1.86-2zM8 8.33v5" />
|
|
336
326
|
</Icon>
|
|
337
327
|
);
|
|
338
328
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import '../uuid-polyfill';
|
|
2
|
+
import { App as McpApp } from '@modelcontextprotocol/ext-apps';
|
|
3
|
+
import { render } from 'preact';
|
|
4
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { App as Workbench } from '../App';
|
|
7
|
+
import { activeBoardId, boardList, boardsLoaded } from '../state/boards-store';
|
|
8
|
+
import { axSurfaceState, contextPinnedNodeIds, nodes, selectedNodeIds } from '../state/canvas-store';
|
|
9
|
+
import { installWorkbenchDownload, installWorkbenchTransport } from '../state/workbench-transport';
|
|
10
|
+
import { resolveIframeMode } from '../state/iframe-mode';
|
|
11
|
+
import { summarizeNodeForAgentContext } from '../../shared/agent-context';
|
|
12
|
+
import { canvasAppContext } from '../../shared/canvas-app';
|
|
13
|
+
|
|
14
|
+
const bridge = new McpApp(
|
|
15
|
+
{ name: 'PMX Canvas', version: '1.0.0' },
|
|
16
|
+
{ availableDisplayModes: ['inline', 'fullscreen'] },
|
|
17
|
+
);
|
|
18
|
+
const responseSchema = z.object({
|
|
19
|
+
status: z.number(),
|
|
20
|
+
body: z.string(),
|
|
21
|
+
contentType: z.string(),
|
|
22
|
+
encoding: z.enum(['text', 'base64']),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Preserve the workbench's fetch/Response contracts, without passing its human
|
|
26
|
+
// headers or exposing the daemon URL to the embedded browser.
|
|
27
|
+
installWorkbenchTransport(async (input, init) => {
|
|
28
|
+
const path = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
29
|
+
const method = (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
|
30
|
+
if (init?.body !== undefined && typeof init.body !== 'string')
|
|
31
|
+
return Response.json({ ok: false, error: 'Document uploads require the local PMX workbench.' }, { status: 403 });
|
|
32
|
+
init?.signal?.throwIfAborted();
|
|
33
|
+
const result = await bridge.callServerTool({
|
|
34
|
+
name: 'pmx_workbench_request',
|
|
35
|
+
arguments: {
|
|
36
|
+
path,
|
|
37
|
+
method,
|
|
38
|
+
...(init?.body === undefined ? {} : { body: init.body }),
|
|
39
|
+
expectedBoardId: activeBoardId.value,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
init?.signal?.throwIfAborted();
|
|
43
|
+
const response = responseSchema.parse(result._meta?.response);
|
|
44
|
+
const body =
|
|
45
|
+
response.encoding === 'base64' ? Uint8Array.from(atob(response.body), (char) => char.charCodeAt(0)) : response.body;
|
|
46
|
+
return new Response(method === 'HEAD' || response.status === 204 ? null : body, {
|
|
47
|
+
status: response.status,
|
|
48
|
+
headers: { 'Content-Type': response.contentType },
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const focusSchema = z.object({ focus: z.array(z.string()) });
|
|
53
|
+
|
|
54
|
+
function selectionContext() {
|
|
55
|
+
const focus = focusSchema.safeParse(axSurfaceState.value);
|
|
56
|
+
return canvasAppContext(
|
|
57
|
+
{
|
|
58
|
+
boardId: activeBoardId.value,
|
|
59
|
+
boardName: boardList.value.find((board) => board.id === activeBoardId.value)?.name ?? 'Home',
|
|
60
|
+
pinnedNodeIds: [...contextPinnedNodeIds.value],
|
|
61
|
+
nodes: [...nodes.value.values()].map((node) => ({
|
|
62
|
+
id: node.id,
|
|
63
|
+
type: node.type,
|
|
64
|
+
title: typeof node.data.title === 'string' ? node.data.title : node.type,
|
|
65
|
+
contentRevision: node.contentRevision ?? 0,
|
|
66
|
+
text: summarizeNodeForAgentContext(node),
|
|
67
|
+
})),
|
|
68
|
+
},
|
|
69
|
+
[...selectedNodeIds.value],
|
|
70
|
+
focus.success ? focus.data.focus : [],
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function HostedWorkbench() {
|
|
75
|
+
const [acknowledged, setAcknowledged] = useState('');
|
|
76
|
+
const [retry, setRetry] = useState(0);
|
|
77
|
+
const queue = useRef(Promise.resolve());
|
|
78
|
+
const context = selectionContext();
|
|
79
|
+
const key = JSON.stringify(context);
|
|
80
|
+
const desired = useRef(key);
|
|
81
|
+
desired.current = key;
|
|
82
|
+
const caps = bridge.getHostCapabilities();
|
|
83
|
+
const ready = boardsLoaded.value;
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!ready || !caps?.updateModelContext || acknowledged === key) return;
|
|
86
|
+
let cancelled = false;
|
|
87
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
88
|
+
queue.current = queue.current
|
|
89
|
+
.then(async () => {
|
|
90
|
+
if (cancelled || desired.current !== key) return;
|
|
91
|
+
setAcknowledged('');
|
|
92
|
+
await bridge.updateModelContext({ structuredContent: context });
|
|
93
|
+
if (cancelled || desired.current !== key) return;
|
|
94
|
+
setAcknowledged(key);
|
|
95
|
+
})
|
|
96
|
+
.catch(() => {
|
|
97
|
+
// Context is background synchronization, not extra workbench chrome.
|
|
98
|
+
// Retry unchanged context too, and only consider a host ACK successful.
|
|
99
|
+
if (!cancelled && desired.current === key) timer = setTimeout(() => setRetry((value) => value + 1), 2000);
|
|
100
|
+
});
|
|
101
|
+
return () => {
|
|
102
|
+
cancelled = true;
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
};
|
|
105
|
+
}, [key, ready, retry]);
|
|
106
|
+
return (
|
|
107
|
+
<div class="pmx-host-app">
|
|
108
|
+
<Workbench />
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Polling is the sole state owner. Launcher tool results never rehydrate the
|
|
114
|
+
// workbench, so delayed initial notifications cannot roll back its live state.
|
|
115
|
+
bridge.ontoolresult = () => {};
|
|
116
|
+
void bridge
|
|
117
|
+
.connect()
|
|
118
|
+
.then(async () => {
|
|
119
|
+
if (!bridge.getHostCapabilities()?.serverTools) throw new Error('This host does not support app tool calls.');
|
|
120
|
+
if (bridge.getHostCapabilities()?.downloadFile) {
|
|
121
|
+
installWorkbenchDownload((file) =>
|
|
122
|
+
bridge.downloadFile({
|
|
123
|
+
contents: [
|
|
124
|
+
{
|
|
125
|
+
type: 'resource',
|
|
126
|
+
resource: { uri: `file:///${encodeURIComponent(file.name)}`, mimeType: file.mimeType, text: file.text },
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
await resolveIframeMode();
|
|
133
|
+
render(<HostedWorkbench />, document.getElementById('app')!);
|
|
134
|
+
})
|
|
135
|
+
.catch((error: unknown) => {
|
|
136
|
+
document.getElementById('app')!.textContent = `Could not connect PMX: ${String(error)}`;
|
|
137
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
.pmx-host-app { height: 100%; display: flex; flex-direction: column; }
|
|
2
|
+
.pmx-host-app > .app-shell { flex: 1; min-height: 0; height: auto; }
|
|
3
|
+
/* Keep context controls clear of the host's floating chat composer. */
|
|
4
|
+
.pmx-host-app .context-pin-bar {
|
|
5
|
+
bottom: 16px;
|
|
6
|
+
left: 16px;
|
|
7
|
+
transform: none;
|
|
8
|
+
}
|
|
9
|
+
/* Guard refusals must remain readable while the full source editor is open. */
|
|
10
|
+
.pmx-host-app .attention-toast.attention-tone-remove { z-index: 100100; }
|
|
@@ -4,10 +4,12 @@ import type { CanvasNodeState } from '../types';
|
|
|
4
4
|
import { activeBoardId } from '../state/boards-store';
|
|
5
5
|
import { requestJson } from '../state/intent-bridge';
|
|
6
6
|
import { expandNode, collapseExpandedNode } from '../state/canvas-store';
|
|
7
|
+
import { useWorkbenchAsset } from './use-workbench-asset';
|
|
7
8
|
|
|
8
9
|
/** Originals stay useful even when the connected host cannot extract them. */
|
|
9
10
|
export function AttachmentNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
10
11
|
const attachmentId = String(node.data.attachmentId);
|
|
12
|
+
const downloadUrl = useWorkbenchAsset(`/api/canvas/attachments/${encodeURIComponent(attachmentId)}/bytes`);
|
|
11
13
|
const boardId = activeBoardId.value;
|
|
12
14
|
const [job, setJob] = useState<DocumentImport | null>(null);
|
|
13
15
|
const [review, setReview] = useState(expanded);
|
|
@@ -68,7 +70,12 @@ export function AttachmentNode({ node, expanded = false }: { node: CanvasNodeSta
|
|
|
68
70
|
? 'Markdown added'
|
|
69
71
|
: 'Original attached'}
|
|
70
72
|
</div>
|
|
71
|
-
<a
|
|
73
|
+
<a
|
|
74
|
+
href={downloadUrl}
|
|
75
|
+
download={String(node.data.title ?? 'attachment')}
|
|
76
|
+
target="_blank"
|
|
77
|
+
rel="noopener noreferrer"
|
|
78
|
+
>
|
|
72
79
|
Download original · {Math.ceil(Number(node.data.size ?? 0) / 1024)} KB
|
|
73
80
|
</a>
|
|
74
81
|
{job?.reason && <p>{job.reason}</p>}
|
|
@@ -298,8 +298,8 @@ export function ContextNode({
|
|
|
298
298
|
style={{
|
|
299
299
|
padding: '4px 8px',
|
|
300
300
|
fontSize: '10px',
|
|
301
|
-
background: 'var(--c-accent
|
|
302
|
-
border: '1px solid var(--c-accent
|
|
301
|
+
background: 'color-mix(in srgb, var(--c-accent) 12%, transparent)',
|
|
302
|
+
border: '1px solid color-mix(in srgb, var(--c-accent) 25%, transparent)',
|
|
303
303
|
borderRadius: '4px',
|
|
304
304
|
color: 'var(--c-text-soft)',
|
|
305
305
|
cursor: 'pointer',
|
|
@@ -363,7 +363,7 @@ export function ContextNode({
|
|
|
363
363
|
style={{
|
|
364
364
|
fontSize: '9px',
|
|
365
365
|
padding: '1px 4px',
|
|
366
|
-
background: 'var(--c-accent
|
|
366
|
+
background: 'color-mix(in srgb, var(--c-accent) 10%, transparent)',
|
|
367
367
|
color: 'var(--c-accent)',
|
|
368
368
|
borderRadius: '3px',
|
|
369
369
|
display: 'inline-block',
|
|
@@ -402,8 +402,8 @@ export function ContextNode({
|
|
|
402
402
|
style={{
|
|
403
403
|
fontSize: '9px',
|
|
404
404
|
padding: '1px 4px',
|
|
405
|
-
background: 'var(--c-
|
|
406
|
-
color: 'var(--c-
|
|
405
|
+
background: 'color-mix(in srgb, var(--c-accent) 12%, transparent)',
|
|
406
|
+
color: 'var(--c-accent)',
|
|
407
407
|
borderRadius: '3px',
|
|
408
408
|
display: 'inline-block',
|
|
409
409
|
}}
|
|
@@ -420,8 +420,8 @@ export function ContextNode({
|
|
|
420
420
|
style={{
|
|
421
421
|
padding: '4px 8px',
|
|
422
422
|
fontSize: '10px',
|
|
423
|
-
background: 'var(--c-accent
|
|
424
|
-
border: '1px solid var(--c-accent
|
|
423
|
+
background: 'color-mix(in srgb, var(--c-accent) 12%, transparent)',
|
|
424
|
+
border: '1px solid color-mix(in srgb, var(--c-accent) 25%, transparent)',
|
|
425
425
|
borderRadius: '4px',
|
|
426
426
|
color: 'var(--c-text-soft)',
|
|
427
427
|
cursor: 'pointer',
|
|
@@ -518,8 +518,8 @@ export function ContextNode({
|
|
|
518
518
|
style={{
|
|
519
519
|
padding: '4px 8px',
|
|
520
520
|
fontSize: '10px',
|
|
521
|
-
background: 'var(--c-accent
|
|
522
|
-
border: '1px solid var(--c-accent
|
|
521
|
+
background: 'color-mix(in srgb, var(--c-accent) 12%, transparent)',
|
|
522
|
+
border: '1px solid color-mix(in srgb, var(--c-accent) 25%, transparent)',
|
|
523
523
|
borderRadius: '4px',
|
|
524
524
|
color: 'var(--c-text-soft)',
|
|
525
525
|
cursor: 'pointer',
|
|
@@ -3,6 +3,7 @@ import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
|
3
3
|
import { AppBridge, PostMessageTransport, buildAllowAttribute } from '@modelcontextprotocol/ext-apps/app-bridge';
|
|
4
4
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
5
5
|
import { IconClose } from '../icons';
|
|
6
|
+
import { workbenchFetch } from '../state/workbench-transport';
|
|
6
7
|
import {
|
|
7
8
|
AX_SURFACE_ACK_SOURCE,
|
|
8
9
|
AX_SURFACE_EMIT_SOURCE,
|
|
@@ -41,7 +42,7 @@ interface ExtAppHostDimensionsTarget {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
async function postJson<T>(url: string, body: Record<string, unknown>): Promise<T> {
|
|
44
|
-
const response = await
|
|
45
|
+
const response = await workbenchFetch(url, {
|
|
45
46
|
method: 'POST',
|
|
46
47
|
headers: { 'Content-Type': 'application/json' },
|
|
47
48
|
body: JSON.stringify(body),
|
|
@@ -150,7 +151,7 @@ function queueExtAppRecoveryReport(entry: { t: number; nodeId: string; event: st
|
|
|
150
151
|
extAppReportTimer = null;
|
|
151
152
|
const entries = extAppReportQueue.splice(0);
|
|
152
153
|
if (entries.length === 0) return;
|
|
153
|
-
void
|
|
154
|
+
void workbenchFetch('/api/canvas/debug/ext-app-recovery', {
|
|
154
155
|
method: 'POST',
|
|
155
156
|
headers: { 'Content-Type': 'application/json' },
|
|
156
157
|
body: JSON.stringify({ entries }),
|
|
@@ -1383,10 +1384,14 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
1383
1384
|
padding: '6px 10px',
|
|
1384
1385
|
fontSize: '11px',
|
|
1385
1386
|
background:
|
|
1386
|
-
sessionStatus === 'error' && !sessionErrorWithContent
|
|
1387
|
+
sessionStatus === 'error' && !sessionErrorWithContent
|
|
1388
|
+
? 'color-mix(in srgb, var(--c-danger) 12%, transparent)'
|
|
1389
|
+
: 'color-mix(in srgb, var(--c-warn) 10%, transparent)',
|
|
1387
1390
|
color: sessionStatus === 'error' && !sessionErrorWithContent ? 'var(--c-danger)' : 'var(--c-warn)',
|
|
1388
1391
|
borderBottom: `1px solid ${
|
|
1389
|
-
sessionStatus === 'error' && !sessionErrorWithContent
|
|
1392
|
+
sessionStatus === 'error' && !sessionErrorWithContent
|
|
1393
|
+
? 'color-mix(in srgb, var(--c-danger) 12%, transparent)'
|
|
1394
|
+
: 'color-mix(in srgb, var(--c-warn) 15%, transparent)'
|
|
1390
1395
|
}`,
|
|
1391
1396
|
}}
|
|
1392
1397
|
>
|
|
@@ -1398,9 +1403,9 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
1398
1403
|
style={{
|
|
1399
1404
|
padding: '6px 10px',
|
|
1400
1405
|
fontSize: '11px',
|
|
1401
|
-
background: 'var(--c-danger
|
|
1406
|
+
background: 'color-mix(in srgb, var(--c-danger) 12%, transparent)',
|
|
1402
1407
|
color: 'var(--c-danger)',
|
|
1403
|
-
borderBottom: '1px solid var(--c-danger
|
|
1408
|
+
borderBottom: '1px solid color-mix(in srgb, var(--c-danger) 12%, transparent)',
|
|
1404
1409
|
display: 'flex',
|
|
1405
1410
|
alignItems: 'center',
|
|
1406
1411
|
gap: '6px',
|
|
@@ -1417,7 +1422,7 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
1417
1422
|
}}
|
|
1418
1423
|
style={{
|
|
1419
1424
|
background: 'var(--c-surface-hover)',
|
|
1420
|
-
border: '1px solid var(--c-danger
|
|
1425
|
+
border: '1px solid color-mix(in srgb, var(--c-danger) 12%, transparent)',
|
|
1421
1426
|
borderRadius: '3px',
|
|
1422
1427
|
color: 'var(--c-danger)',
|
|
1423
1428
|
cursor: 'pointer',
|
|
@@ -1512,7 +1517,7 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
1512
1517
|
}}
|
|
1513
1518
|
style={{
|
|
1514
1519
|
background: 'var(--c-surface-hover)',
|
|
1515
|
-
border: '1px solid var(--c-warn
|
|
1520
|
+
border: '1px solid color-mix(in srgb, var(--c-warn) 15%, transparent)',
|
|
1516
1521
|
borderRadius: '4px',
|
|
1517
1522
|
color: 'var(--c-warn)',
|
|
1518
1523
|
cursor: 'pointer',
|
|
@@ -7,6 +7,7 @@ import { fetchFile, updateNodeFromClient } from '../state/intent-bridge';
|
|
|
7
7
|
import type { CanvasNodeState } from '../types';
|
|
8
8
|
import { runNodeAxInteraction } from './ax-node-actions';
|
|
9
9
|
import { AttachmentNode } from './AttachmentNode';
|
|
10
|
+
import { useWorkbenchAsset } from './use-workbench-asset';
|
|
10
11
|
|
|
11
12
|
/** Guess a language label from a file extension for display. */
|
|
12
13
|
function langFromPath(path: string): string {
|
|
@@ -180,9 +181,11 @@ function TextFileNode({ node, expanded = false }: { node: CanvasNodeState; expan
|
|
|
180
181
|
const parsed = parseDelimitedText(content, delimiter);
|
|
181
182
|
return parsed.header.length >= 2 ? parsed : null;
|
|
182
183
|
}, [content, delimiter, servesBytes]);
|
|
183
|
-
const bytesUrl =
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
const bytesUrl = useWorkbenchAsset(
|
|
185
|
+
servesBytes
|
|
186
|
+
? `/api/canvas/file-bytes?nodeId=${encodeURIComponent(node.id)}${bytesVersion ? `&v=${bytesVersion}` : ''}`
|
|
187
|
+
: '',
|
|
188
|
+
);
|
|
186
189
|
// Amp orb portals block src-URL child iframes and force srcdoc, which cannot
|
|
187
190
|
// inline a PDF — offer the raw URL instead of a dead frame.
|
|
188
191
|
const pdfFrameBlocked = iframeMode.value === 'srcdoc';
|
|
@@ -204,7 +207,7 @@ function TextFileNode({ node, expanded = false }: { node: CanvasNodeState; expan
|
|
|
204
207
|
display: 'flex',
|
|
205
208
|
flexDirection: 'column',
|
|
206
209
|
height: '100%',
|
|
207
|
-
fontFamily: 'var(--
|
|
210
|
+
fontFamily: 'var(--font-code)',
|
|
208
211
|
fontSize: expanded ? '13px' : '11px',
|
|
209
212
|
}}
|
|
210
213
|
>
|
|
@@ -223,7 +226,7 @@ function TextFileNode({ node, expanded = false }: { node: CanvasNodeState; expan
|
|
|
223
226
|
style={{
|
|
224
227
|
fontSize: '9px',
|
|
225
228
|
padding: '1px 5px',
|
|
226
|
-
background: 'var(--c-accent
|
|
229
|
+
background: 'color-mix(in srgb, var(--c-accent) 12%, transparent)',
|
|
227
230
|
color: 'var(--c-accent)',
|
|
228
231
|
borderRadius: '3px',
|
|
229
232
|
fontWeight: 600,
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
|
2
2
|
import { IconRefresh } from '../icons';
|
|
3
3
|
import type { CanvasNodeState } from '../types';
|
|
4
4
|
import { getImageNodeWarnings } from './image-warnings';
|
|
5
|
+
import { useWorkbenchAsset } from './use-workbench-asset';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Image node renderer.
|
|
@@ -15,10 +16,11 @@ export function ImageNode({ node, expanded = false }: { node: CanvasNodeState; e
|
|
|
15
16
|
const warnings = getImageNodeWarnings(node);
|
|
16
17
|
|
|
17
18
|
// Determine the image source URL
|
|
18
|
-
const imageSrc =
|
|
19
|
+
const imageSrc = useWorkbenchAsset(
|
|
19
20
|
src.startsWith('data:') || src.startsWith('http://') || src.startsWith('https://')
|
|
20
21
|
? src
|
|
21
|
-
: `/api/canvas/image/${node.id}
|
|
22
|
+
: `/api/canvas/image/${node.id}`,
|
|
23
|
+
);
|
|
22
24
|
|
|
23
25
|
const [loaded, setLoaded] = useState(false);
|
|
24
26
|
const [error, setError] = useState(false);
|
|
@@ -3,6 +3,7 @@ import TurndownService from 'turndown';
|
|
|
3
3
|
import { useCallback, useEffect, useRef } from 'preact/hooks';
|
|
4
4
|
import { InlineFormatBar } from './InlineFormatBar';
|
|
5
5
|
import { promptAndInsertLink } from './inline-editor-commands';
|
|
6
|
+
import { sanitizeMarkdownHtml } from './markdown-sanitize';
|
|
6
7
|
|
|
7
8
|
let _turndown: TurndownService | null = null;
|
|
8
9
|
function getTurndown(): TurndownService {
|
|
@@ -45,7 +46,7 @@ export function InlineMarkdownEditor({
|
|
|
45
46
|
useEffect(() => {
|
|
46
47
|
const el = rootRef.current;
|
|
47
48
|
if (!el) return;
|
|
48
|
-
el.innerHTML = initialHtmlRef.current;
|
|
49
|
+
el.innerHTML = sanitizeMarkdownHtml(initialHtmlRef.current);
|
|
49
50
|
// GFM task checkboxes are live in the editor too: marked emits them
|
|
50
51
|
// `disabled`, which also swallows clicks in the expanded view.
|
|
51
52
|
for (const box of el.querySelectorAll('input[type="checkbox"][disabled]')) {
|
|
@@ -34,7 +34,7 @@ export function LedgerNode({ node }: { node: CanvasNodeState }) {
|
|
|
34
34
|
padding: '3px 0',
|
|
35
35
|
borderBottom: i < lines.length - 1 ? '1px solid rgba(45,55,90,0.3)' : 'none',
|
|
36
36
|
color: 'var(--c-text)',
|
|
37
|
-
fontFamily: 'var(--
|
|
37
|
+
fontFamily: 'var(--font-code)',
|
|
38
38
|
fontSize: '11px',
|
|
39
39
|
whiteSpace: 'pre-wrap',
|
|
40
40
|
wordBreak: 'break-word',
|
|
@@ -62,7 +62,7 @@ export function LedgerNode({ node }: { node: CanvasNodeState }) {
|
|
|
62
62
|
<span
|
|
63
63
|
style={{
|
|
64
64
|
color: 'var(--c-text)',
|
|
65
|
-
fontFamily: 'var(--
|
|
65
|
+
fontFamily: 'var(--font-code)',
|
|
66
66
|
fontSize: '11px',
|
|
67
67
|
textAlign: 'right',
|
|
68
68
|
wordBreak: 'break-word',
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { JSX } from 'preact';
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
3
3
|
import { IconArrowLeft } from '../icons';
|
|
4
|
-
import { expandNode, updateNodeData } from '../state/canvas-store';
|
|
4
|
+
import { expandNode, nodes, updateNodeData } from '../state/canvas-store';
|
|
5
5
|
import { fetchFile, renderMarkdown, saveFile, updateNodeFromClient } from '../state/intent-bridge';
|
|
6
6
|
import type { CanvasNodeState } from '../types';
|
|
7
7
|
import { activeBoard, setBoardReadme } from '../state/boards-store';
|
|
8
8
|
import { MdFormatBar } from './MdFormatBar';
|
|
9
9
|
import { handleFormatShortcut, handleTab } from './md-format';
|
|
10
10
|
import { InlineMarkdownEditor } from './InlineMarkdownEditor';
|
|
11
|
+
import { isHostedWorkbench } from '../state/workbench-transport';
|
|
12
|
+
import { activeBoardId } from '../state/boards-store';
|
|
13
|
+
import { sanitizeMarkdownHtml } from './markdown-sanitize';
|
|
11
14
|
|
|
12
15
|
function RenderedMarkdown({
|
|
13
16
|
html,
|
|
@@ -37,7 +40,7 @@ function RenderedMarkdown({
|
|
|
37
40
|
container.replaceChildren();
|
|
38
41
|
if (!html) return;
|
|
39
42
|
const template = document.createElement('template');
|
|
40
|
-
template.innerHTML = html;
|
|
43
|
+
template.innerHTML = sanitizeMarkdownHtml(html);
|
|
41
44
|
container.append(template.content.cloneNode(true));
|
|
42
45
|
if (onTaskToggleRef.current) {
|
|
43
46
|
for (const box of container.querySelectorAll('input[type="checkbox"][disabled]')) {
|
|
@@ -98,9 +101,14 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
|
|
|
98
101
|
// debounced save without capturing stale closures.
|
|
99
102
|
const latestMdRef = useRef<string>('');
|
|
100
103
|
const persistFnRef = useRef<((md: string) => Promise<void>) | null>(null);
|
|
104
|
+
const dirtyRef = useRef(false);
|
|
105
|
+
const editBase = useRef({ boardId: activeBoardId.value, revision: node.contentRevision ?? 0 });
|
|
106
|
+
const saveQueue = useRef(Promise.resolve());
|
|
101
107
|
const reviewActive = node.data.reviewActive as boolean | undefined;
|
|
102
108
|
|
|
103
109
|
useEffect(() => {
|
|
110
|
+
if (isHostedWorkbench() && dirtyRef.current) return;
|
|
111
|
+
editBase.current = { boardId: activeBoardId.value, revision: node.contentRevision ?? 0 };
|
|
104
112
|
let cancelled = false;
|
|
105
113
|
(async () => {
|
|
106
114
|
let raw: string;
|
|
@@ -116,7 +124,7 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
|
|
|
116
124
|
}
|
|
117
125
|
setContent(raw);
|
|
118
126
|
const html = await renderMarkdown(raw);
|
|
119
|
-
if (cancelled) return;
|
|
127
|
+
if (cancelled || (isHostedWorkbench() && dirtyRef.current)) return;
|
|
120
128
|
setRendered(html);
|
|
121
129
|
setLoaded(true);
|
|
122
130
|
updateNodeData(node.id, { content: raw, rendered: html });
|
|
@@ -128,6 +136,8 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
|
|
|
128
136
|
|
|
129
137
|
const handleInput = useCallback(async (e: Event) => {
|
|
130
138
|
const value = (e.target as HTMLTextAreaElement).value;
|
|
139
|
+
dirtyRef.current = true;
|
|
140
|
+
latestMdRef.current = value;
|
|
131
141
|
setContent(value);
|
|
132
142
|
setDirty(true);
|
|
133
143
|
const html = await renderMarkdown(value);
|
|
@@ -136,6 +146,32 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
|
|
|
136
146
|
|
|
137
147
|
const persistContent = useCallback(
|
|
138
148
|
async (newContent: string) => {
|
|
149
|
+
if (isHostedWorkbench()) {
|
|
150
|
+
// Serialize autosaves against the captured source revision, not whatever
|
|
151
|
+
// newer revision a background poll has delivered while typing.
|
|
152
|
+
saveQueue.current = saveQueue.current.then(async () => {
|
|
153
|
+
setSaving(true);
|
|
154
|
+
const result = await updateNodeFromClient(node.id, {
|
|
155
|
+
content: newContent,
|
|
156
|
+
expectedBoardId: editBase.current.boardId,
|
|
157
|
+
expectedContentRevision: editBase.current.revision,
|
|
158
|
+
});
|
|
159
|
+
setSaving(false);
|
|
160
|
+
if (!result.ok) return; // Refusal toast is shown by the request helper; keep the draft.
|
|
161
|
+
editBase.current.revision = result.contentRevision ?? editBase.current.revision;
|
|
162
|
+
const current = nodes.value.get(node.id);
|
|
163
|
+
if (current)
|
|
164
|
+
nodes.value = new Map(nodes.value).set(node.id, { ...current, contentRevision: editBase.current.revision });
|
|
165
|
+
if (latestMdRef.current !== newContent) return;
|
|
166
|
+
dirtyRef.current = false;
|
|
167
|
+
setDirty(false);
|
|
168
|
+
const html = await renderMarkdown(newContent);
|
|
169
|
+
setRendered(html);
|
|
170
|
+
updateNodeData(node.id, { content: newContent, rendered: html });
|
|
171
|
+
});
|
|
172
|
+
await saveQueue.current;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
139
175
|
if (!path) {
|
|
140
176
|
const html = await renderMarkdown(newContent);
|
|
141
177
|
setRendered(html);
|
|
@@ -191,6 +227,7 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
|
|
|
191
227
|
// don't hit the backend on every letter.
|
|
192
228
|
const handleInlineChange = useCallback(
|
|
193
229
|
(md: string) => {
|
|
230
|
+
dirtyRef.current = true;
|
|
194
231
|
setContent(md);
|
|
195
232
|
setDirty(true);
|
|
196
233
|
latestMdRef.current = md;
|
|
@@ -208,6 +245,7 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
|
|
|
208
245
|
// write twice with slightly different content.
|
|
209
246
|
const handleInlineSave = useCallback(
|
|
210
247
|
(md: string) => {
|
|
248
|
+
dirtyRef.current = true;
|
|
211
249
|
setContent(md);
|
|
212
250
|
latestMdRef.current = md;
|
|
213
251
|
if (persistTimerRef.current !== null) {
|
|
@@ -236,9 +274,9 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
|
|
|
236
274
|
style={{
|
|
237
275
|
padding: '4px 8px',
|
|
238
276
|
fontSize: '10px',
|
|
239
|
-
background: 'var(--c-ok
|
|
277
|
+
background: 'color-mix(in srgb, var(--c-ok) 10%, transparent)',
|
|
240
278
|
color: 'var(--c-ok)',
|
|
241
|
-
borderBottom: '1px solid var(--c-ok
|
|
279
|
+
borderBottom: '1px solid color-mix(in srgb, var(--c-ok) 20%, transparent)',
|
|
242
280
|
textTransform: 'uppercase',
|
|
243
281
|
letterSpacing: '0.05em',
|
|
244
282
|
fontWeight: 600,
|
|
@@ -274,9 +312,6 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
|
|
|
274
312
|
<textarea ref={textareaRef} value={content} onInput={handleInput} spellcheck={false} />
|
|
275
313
|
<MdFormatBar textareaRef={textareaRef} />
|
|
276
314
|
</div>
|
|
277
|
-
{/* D5/H4: Trust boundary — rendered HTML comes from server-side marked()
|
|
278
|
-
on the user's own markdown files, served only on 127.0.0.1. No DOMPurify
|
|
279
|
-
needed for this localhost-only rendering of user-owned content. */}
|
|
280
315
|
<RenderedMarkdown html={rendered} className="md-preview" />
|
|
281
316
|
</div>
|
|
282
317
|
</div>
|
|
@@ -322,7 +357,7 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
|
|
|
322
357
|
style={{
|
|
323
358
|
padding: '4px 10px',
|
|
324
359
|
fontSize: '11px',
|
|
325
|
-
background: dirty ? 'var(--c-accent
|
|
360
|
+
background: dirty ? 'color-mix(in srgb, var(--c-accent) 25%, transparent)' : 'var(--c-input-bg)',
|
|
326
361
|
border: `1px solid ${dirty ? 'var(--c-accent)' : 'var(--c-line)'}`,
|
|
327
362
|
borderRadius: '6px',
|
|
328
363
|
color: dirty ? 'var(--c-text)' : 'var(--c-dim)',
|
|
@@ -11,6 +11,7 @@ import { useAxSurfaceBridge } from './use-ax-surface-bridge';
|
|
|
11
11
|
import { useIframeContentHeight } from './use-iframe-content-height';
|
|
12
12
|
import { useSurfaceFrame } from './use-surface-frame';
|
|
13
13
|
import { restoreViewerRuntimeState } from './viewer-runtime-state';
|
|
14
|
+
import { isHostedWorkbench } from '../state/workbench-transport';
|
|
14
15
|
|
|
15
16
|
function withViewerParams(
|
|
16
17
|
url: string,
|
|
@@ -24,7 +25,8 @@ function withViewerParams(
|
|
|
24
25
|
): string {
|
|
25
26
|
if (!url) return url;
|
|
26
27
|
try {
|
|
27
|
-
const
|
|
28
|
+
const hosted = isHostedWorkbench() && url.startsWith('/');
|
|
29
|
+
const resolved = new URL(url, hosted ? 'https://pmx.invalid' : window.location.origin);
|
|
28
30
|
// The json-render viewer + artifacts understand dark/light — collapse named
|
|
29
31
|
// themes (sepia → light, midnight/arctic/ember/forest → dark) to a scheme.
|
|
30
32
|
resolved.searchParams.set('theme', canvasThemeScheme(canvasTheme.value));
|
|
@@ -41,7 +43,7 @@ function withViewerParams(
|
|
|
41
43
|
if (frameToken) resolved.searchParams.set('frameToken', frameToken);
|
|
42
44
|
if (fitContent) resolved.searchParams.set('fit', 'content');
|
|
43
45
|
if (stateToken) resolved.searchParams.set('stateToken', stateToken);
|
|
44
|
-
return resolved.toString();
|
|
46
|
+
return hosted ? `${resolved.pathname}${resolved.search}` : resolved.toString();
|
|
45
47
|
} catch {
|
|
46
48
|
return url;
|
|
47
49
|
}
|
|
@@ -283,9 +285,9 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
283
285
|
style={{
|
|
284
286
|
padding: '4px 8px',
|
|
285
287
|
fontSize: '10px',
|
|
286
|
-
background: 'var(--c-warn
|
|
288
|
+
background: 'color-mix(in srgb, var(--c-warn) 10%, transparent)',
|
|
287
289
|
color: 'var(--c-warn)',
|
|
288
|
-
borderBottom: '1px solid var(--c-warn
|
|
290
|
+
borderBottom: '1px solid color-mix(in srgb, var(--c-warn) 15%, transparent)',
|
|
289
291
|
}}
|
|
290
292
|
>
|
|
291
293
|
Unverified domain
|