pmx-canvas 0.7.3 → 0.8.2
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 +64 -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 +726 -674
- package/dist/canvas/index.js +90 -88
- package/dist/canvas/mcp-app.html +7879 -0
- package/dist/canvas/mermaid-entry.js +1 -1
- package/dist/canvas/surface-theme.css +151 -46
- package/dist/json-render/index.css +81 -1
- package/dist/json-render/index.js +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/HtmlNode.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 +211 -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 +217 -0
- package/docs/environment.md +1 -1
- package/docs/http-api.md +86 -16
- package/docs/mcp.md +202 -4
- 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 +29 -9
- package/skills/pmx-canvas/evals/evals.json +62 -0
- package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
- 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/nodes.ts +3 -2
- package/src/cli/commands/view.ts +1 -1
- package/src/cli/index.ts +10 -4
- 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/HtmlNode.tsx +12 -2
- 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 +726 -674
- 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/renderer/index.css +6 -1
- package/src/json-render/renderer/index.tsx +0 -1
- 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 +7 -25
- 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 +87 -15
- package/src/server/demo-state.json +31 -31
- package/src/server/html-surface.ts +4 -5
- package/src/server/index.ts +41 -5
- 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/mcp.ts +6 -0
- 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 +6 -2
- 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 +41 -5
- 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
|
@@ -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,
|
|
@@ -12,6 +12,14 @@ export function shouldShowPresentationControls(node: CanvasNodeState): boolean {
|
|
|
12
12
|
return node.type === 'html' && node.data.presentation === true;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export function htmlAxCapabilitySignature(value: unknown): string {
|
|
16
|
+
const capabilities = value as { enabled?: unknown; allowed?: unknown } | undefined;
|
|
17
|
+
if (capabilities?.enabled !== true) return 'disabled';
|
|
18
|
+
if (!Array.isArray(capabilities.allowed)) return 'enabled:default';
|
|
19
|
+
const allowed = [...new Set(capabilities.allowed.filter((item): item is string => typeof item === 'string'))].sort();
|
|
20
|
+
return allowed.length > 0 ? `enabled:${allowed.join(',')}` : 'disabled';
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
export function HtmlNode({
|
|
16
24
|
node,
|
|
17
25
|
expanded = false,
|
|
@@ -27,10 +35,12 @@ export function HtmlNode({
|
|
|
27
35
|
}) {
|
|
28
36
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
29
37
|
const theme = canvasTheme.value;
|
|
38
|
+
const axCapabilitySignature = htmlAxCapabilitySignature(node.data.axCapabilities);
|
|
30
39
|
// Stable per-mount nonce that authorizes parent → iframe theme-update messages.
|
|
31
40
|
const themeToken = useMemo(() => `theme-${crypto.randomUUID()}`, []);
|
|
32
|
-
//
|
|
33
|
-
|
|
41
|
+
// Rotate the nonce when capabilities change: both the URL and injected srcdoc
|
|
42
|
+
// change, rebootstrapping the bridge and rejecting emits from the old document.
|
|
43
|
+
const axToken = useMemo(() => `ax-${crypto.randomUUID()}`, [axCapabilitySignature]);
|
|
34
44
|
// Per-mount nonce for the content-height reporter (node grows to fit content).
|
|
35
45
|
const frameToken = useMemo(() => `frame-${crypto.randomUUID()}`, []);
|
|
36
46
|
const html =
|
|
@@ -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
|
|
@@ -3,6 +3,7 @@ import { IconClose } from '../icons';
|
|
|
3
3
|
import { updateNodeData } from '../state/canvas-store';
|
|
4
4
|
import { fetchSlashCommands, renderMarkdown, submitCanvasPrompt, submitThreadReply } from '../state/intent-bridge';
|
|
5
5
|
import type { CanvasNodeState } from '../types';
|
|
6
|
+
import { sanitizeMarkdownHtml } from './markdown-sanitize';
|
|
6
7
|
|
|
7
8
|
// Cached slash commands — fetched once on first use.
|
|
8
9
|
let cachedCommands: Array<{ name: string; description: string }> | null = null;
|
|
@@ -27,19 +28,6 @@ interface ThreadTurn {
|
|
|
27
28
|
status?: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/** Strip dangerous HTML from rendered markdown to prevent XSS. */
|
|
31
|
-
function sanitizeHtml(html: string): string {
|
|
32
|
-
return html
|
|
33
|
-
.replace(/<script[\s>][\s\S]*?<\/script>/gi, '')
|
|
34
|
-
.replace(/<iframe[\s>][\s\S]*?<\/iframe>/gi, '')
|
|
35
|
-
.replace(/<object[\s>][\s\S]*?<\/object>/gi, '')
|
|
36
|
-
.replace(/<embed[\s>][\s\S]*?(?:\/>|<\/embed>)/gi, '')
|
|
37
|
-
.replace(/<link[\s>][\s\S]*?(?:\/>|<\/link>)/gi, '')
|
|
38
|
-
.replace(/\bon\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
|
|
39
|
-
.replace(/href\s*=\s*"javascript:[^"]*"/gi, 'href="#"')
|
|
40
|
-
.replace(/href\s*=\s*'javascript:[^']*'/gi, "href='#'");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
31
|
function RenderedMarkdown({ html }: { html: string }) {
|
|
44
32
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
45
33
|
|
|
@@ -50,7 +38,7 @@ function RenderedMarkdown({ html }: { html: string }) {
|
|
|
50
38
|
if (!html) return;
|
|
51
39
|
|
|
52
40
|
const template = document.createElement('template');
|
|
53
|
-
template.innerHTML = html;
|
|
41
|
+
template.innerHTML = sanitizeMarkdownHtml(html);
|
|
54
42
|
container.append(template.content.cloneNode(true));
|
|
55
43
|
}, [html]);
|
|
56
44
|
|
|
@@ -181,7 +169,7 @@ export function PromptNode({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
181
169
|
Promise.all(
|
|
182
170
|
assistantTurns.map(async ({ turn, index }) => {
|
|
183
171
|
const html = await renderMarkdown(turn.text);
|
|
184
|
-
return { index, html:
|
|
172
|
+
return { index, html: sanitizeMarkdownHtml(html) };
|
|
185
173
|
}),
|
|
186
174
|
).then((results) => {
|
|
187
175
|
if (cancelled) return;
|
|
@@ -328,7 +316,7 @@ export function PromptNode({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
328
316
|
border: '1px solid var(--c-line)',
|
|
329
317
|
borderRadius: 'var(--radius-sm)',
|
|
330
318
|
color: 'var(--c-text)',
|
|
331
|
-
fontFamily: 'var(--font)',
|
|
319
|
+
fontFamily: 'var(--font-ui)',
|
|
332
320
|
fontSize: '13px',
|
|
333
321
|
lineHeight: '1.5',
|
|
334
322
|
padding: '8px 10px',
|
|
@@ -529,7 +517,7 @@ export function PromptNode({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
529
517
|
border: '1px solid var(--c-line)',
|
|
530
518
|
borderRadius: 'var(--radius-sm)',
|
|
531
519
|
color: 'var(--c-text)',
|
|
532
|
-
fontFamily: 'var(--font)',
|
|
520
|
+
fontFamily: 'var(--font-ui)',
|
|
533
521
|
fontSize: '12px',
|
|
534
522
|
lineHeight: '1.5',
|
|
535
523
|
padding: '6px 8px',
|
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { renderMarkdown, submitCanvasPrompt } from '../state/intent-bridge';
|
|
3
3
|
import type { CanvasNodeState } from '../types';
|
|
4
|
-
|
|
5
|
-
/** Strip dangerous HTML from rendered markdown to prevent XSS. */
|
|
6
|
-
function sanitizeHtml(html: string): string {
|
|
7
|
-
return html
|
|
8
|
-
.replace(/<script[\s>][\s\S]*?<\/script>/gi, '')
|
|
9
|
-
.replace(/<iframe[\s>][\s\S]*?<\/iframe>/gi, '')
|
|
10
|
-
.replace(/<object[\s>][\s\S]*?<\/object>/gi, '')
|
|
11
|
-
.replace(/<embed[\s>][\s\S]*?(?:\/>|<\/embed>)/gi, '')
|
|
12
|
-
.replace(/<link[\s>][\s\S]*?(?:\/>|<\/link>)/gi, '')
|
|
13
|
-
.replace(/\bon\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
|
|
14
|
-
.replace(/href\s*=\s*"javascript:[^"]*"/gi, 'href="#"')
|
|
15
|
-
.replace(/href\s*=\s*'javascript:[^']*'/gi, "href='#'");
|
|
16
|
-
}
|
|
4
|
+
import { sanitizeMarkdownHtml } from './markdown-sanitize';
|
|
17
5
|
|
|
18
6
|
function RenderedMarkdown({ html }: { html: string }) {
|
|
19
7
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -25,7 +13,7 @@ function RenderedMarkdown({ html }: { html: string }) {
|
|
|
25
13
|
if (!html) return;
|
|
26
14
|
|
|
27
15
|
const template = document.createElement('template');
|
|
28
|
-
template.innerHTML = html;
|
|
16
|
+
template.innerHTML = sanitizeMarkdownHtml(html);
|
|
29
17
|
container.append(template.content.cloneNode(true));
|
|
30
18
|
}, [html]);
|
|
31
19
|
|
|
@@ -48,7 +36,7 @@ export function ResponseNode({ node, expanded = false }: { node: CanvasNodeState
|
|
|
48
36
|
}
|
|
49
37
|
let cancelled = false;
|
|
50
38
|
renderMarkdown(content).then((html) => {
|
|
51
|
-
if (!cancelled) setRendered(
|
|
39
|
+
if (!cancelled) setRendered(sanitizeMarkdownHtml(html));
|
|
52
40
|
});
|
|
53
41
|
return () => {
|
|
54
42
|
cancelled = true;
|
|
@@ -64,9 +64,9 @@ export function StatusNode({ node }: { node: CanvasNodeState }) {
|
|
|
64
64
|
|
|
65
65
|
{/* Active tool */}
|
|
66
66
|
{activeTool && (
|
|
67
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--c-
|
|
67
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--c-agent)' }}>
|
|
68
68
|
<span style={{ fontSize: '10px' }}>⚙</span>
|
|
69
|
-
<span style={{ fontFamily: 'var(--
|
|
69
|
+
<span style={{ fontFamily: 'var(--font-code)', fontSize: '11px' }}>{activeTool}</span>
|
|
70
70
|
</div>
|
|
71
71
|
)}
|
|
72
72
|
|
|
@@ -23,7 +23,9 @@ export function StatusSummary({ node }: { node: CanvasNodeState }) {
|
|
|
23
23
|
/>
|
|
24
24
|
<span style={{ color: phaseColor, fontSize: '10px', textTransform: 'uppercase', fontWeight: 600 }}>{phase}</span>
|
|
25
25
|
{activeTool && (
|
|
26
|
-
<span style={{ color: 'var(--c-
|
|
26
|
+
<span style={{ color: 'var(--c-agent)', fontSize: '10px', fontFamily: 'var(--font-code)' }}>
|
|
27
|
+
⚙ {activeTool}
|
|
28
|
+
</span>
|
|
27
29
|
)}
|
|
28
30
|
{subagent && subagent.state !== 'completed' && (
|
|
29
31
|
<span style={{ color: 'var(--c-subagent)', fontSize: '10px' }}>⠉ {subagent.name}</span>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { CanvasNodeState } from '../types';
|
|
2
2
|
import { buildTraceDisplayModel } from './trace-model';
|
|
3
3
|
|
|
4
|
+
// Categories carry no colour (docs/design.md, "Kinds carry no colour"); only a
|
|
5
|
+
// subagent's work takes its meaning colour.
|
|
4
6
|
const CATEGORY_COLORS: Record<string, string> = {
|
|
5
|
-
mcp: 'var(--c-
|
|
6
|
-
file: 'var(--c-
|
|
7
|
-
subagent: 'var(--c-
|
|
7
|
+
mcp: 'var(--c-muted)',
|
|
8
|
+
file: 'var(--c-muted)',
|
|
9
|
+
subagent: 'var(--c-subagent)',
|
|
8
10
|
other: 'var(--c-muted)',
|
|
9
11
|
};
|
|
10
12
|
|
|
@@ -68,7 +70,7 @@ export function TraceNode({ node }: { node: CanvasNodeState }) {
|
|
|
68
70
|
>
|
|
69
71
|
<div
|
|
70
72
|
style={{
|
|
71
|
-
fontFamily: 'var(--
|
|
73
|
+
fontFamily: 'var(--font-code)',
|
|
72
74
|
fontSize: '11px',
|
|
73
75
|
fontWeight: 600,
|
|
74
76
|
color: catColor,
|
|
@@ -56,7 +56,7 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
56
56
|
return <div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>No webpage URL set</div>;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const statusTone = status === 'ready' ? 'var(--c-ok)' : status === 'error' ? 'var(--c-danger)' : 'var(--c-
|
|
59
|
+
const statusTone = status === 'ready' ? 'var(--c-ok)' : status === 'error' ? 'var(--c-danger)' : 'var(--c-muted)';
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
62
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', height: '100%' }}>
|
|
@@ -27,8 +27,8 @@ export async function runNodeAxInteraction(
|
|
|
27
27
|
export const axNodeActionButtonStyle = {
|
|
28
28
|
padding: '3px 8px',
|
|
29
29
|
fontSize: '10px',
|
|
30
|
-
background: 'var(--c-accent
|
|
31
|
-
border: '1px solid var(--c-accent
|
|
30
|
+
background: 'color-mix(in srgb, var(--c-accent) 12%, transparent)',
|
|
31
|
+
border: '1px solid color-mix(in srgb, var(--c-accent) 25%, transparent)',
|
|
32
32
|
borderRadius: '4px',
|
|
33
33
|
color: 'var(--c-text-soft)',
|
|
34
34
|
cursor: 'pointer',
|
|
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
|
2
2
|
import { workbenchConnectionEpoch } from '../state/canvas-store';
|
|
3
3
|
import { iframeMode } from '../state/iframe-mode';
|
|
4
4
|
import { surfaceContentHash } from './surface-url';
|
|
5
|
+
import { workbenchFetch } from '../state/workbench-transport';
|
|
5
6
|
|
|
6
7
|
interface FrameDocumentCreateResponse {
|
|
7
8
|
ok: boolean;
|
|
@@ -21,7 +22,7 @@ function isFrameDocumentCreateResponse(value: unknown): value is FrameDocumentCr
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export async function createIframeDocumentUrl(html: string, sandbox: string): Promise<string> {
|
|
24
|
-
const response = await
|
|
25
|
+
const response = await workbenchFetch('/api/canvas/frame-documents', {
|
|
25
26
|
method: 'POST',
|
|
26
27
|
headers: { 'Content-Type': 'application/json' },
|
|
27
28
|
body: JSON.stringify({ html, sandbox }),
|
|
@@ -83,7 +84,7 @@ export function useIframeDocument(
|
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
let cancelled = false;
|
|
86
|
-
void
|
|
87
|
+
void workbenchFetch(src, { method: 'HEAD' })
|
|
87
88
|
.then((res) => {
|
|
88
89
|
if (cancelled) return;
|
|
89
90
|
if (res.ok) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
|
|
3
|
+
/** Sanitize rendered Markdown before it enters the privileged workbench DOM. */
|
|
4
|
+
export function sanitizeMarkdownHtml(html: string): string {
|
|
5
|
+
return DOMPurify.sanitize(html, {
|
|
6
|
+
// Markdown does not need these active document surfaces. Keep regular
|
|
7
|
+
// inputs so marked's GFM task checkboxes remain interactive.
|
|
8
|
+
FORBID_TAGS: ['form', 'svg', 'math'],
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -2,6 +2,8 @@ import { canvasTheme } from '../state/canvas-store';
|
|
|
2
2
|
import { openNodeInSystemBrowserRequest } from '../state/intent-bridge';
|
|
3
3
|
import { canOpenNodeAsSurface } from '../../shared/surface.js';
|
|
4
4
|
import type { CanvasNodeState } from '../types';
|
|
5
|
+
import { isHostedWorkbench } from '../state/workbench-transport';
|
|
6
|
+
import { showToast } from '../state/attention-bridge';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Stable content hash (djb2) used to cache-bust the surface iframe `src` when a
|
|
@@ -52,6 +54,14 @@ export function canOpenAsSite(node: CanvasNodeState): boolean {
|
|
|
52
54
|
* and headless/disabled-browser environments.
|
|
53
55
|
*/
|
|
54
56
|
export async function openNodeAsSite(node: CanvasNodeState): Promise<void> {
|
|
57
|
+
if (isHostedWorkbench()) {
|
|
58
|
+
showToast(
|
|
59
|
+
'context',
|
|
60
|
+
'Open in focus mode',
|
|
61
|
+
'This surface has no public URL. Use Expand to view it here, or export the board to download it.',
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
55
65
|
const url = nodeSurfaceUrl(node.id);
|
|
56
66
|
const res = await openNodeInSystemBrowserRequest(node.id, url);
|
|
57
67
|
if (!res.opened) window.open(url, '_blank', 'noopener');
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useEffect, useState } from 'preact/hooks';
|
|
2
2
|
import { iframeMode } from '../state/iframe-mode';
|
|
3
|
+
import { isHostedWorkbench, workbenchFetch } from '../state/workbench-transport';
|
|
3
4
|
|
|
4
5
|
function isSameOriginUrl(url: string): boolean {
|
|
6
|
+
if (isHostedWorkbench()) return url.startsWith('/') && !url.startsWith('//');
|
|
5
7
|
try {
|
|
6
8
|
return new URL(url, window.location.origin).origin === window.location.origin;
|
|
7
9
|
} catch {
|
|
@@ -29,7 +31,7 @@ export function useSurfaceFrame(url: string): { src?: string; srcdoc?: string }
|
|
|
29
31
|
let cancelled = false;
|
|
30
32
|
// Ask the server to inline bundle assets (the mermaid renderer): hosts
|
|
31
33
|
// that force srcdoc mode block sub-frame subresource requests too.
|
|
32
|
-
void
|
|
34
|
+
void workbenchFetch(`${url}${url.includes('?') ? '&' : '?'}inline-assets=1`, { credentials: 'same-origin' })
|
|
33
35
|
.then((res) => (res.ok ? res.text() : Promise.reject(new Error(`HTTP ${res.status}`))))
|
|
34
36
|
.then((html) => {
|
|
35
37
|
if (!cancelled) setDoc({ url, html });
|