pmx-canvas 0.1.22 → 0.1.24
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/extension.mjs +591 -0
- package/CHANGELOG.md +140 -0
- package/Readme.md +40 -8
- package/dist/canvas/global.css +36 -3
- package/dist/canvas/index.js +54 -54
- package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -0
- package/dist/types/client/nodes/iframe-document-url.d.ts +8 -0
- package/dist/types/client/state/intent-bridge.d.ts +4 -0
- package/dist/types/client/types.d.ts +1 -0
- package/dist/types/json-render/catalog.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +9 -0
- package/dist/types/server/ax-context.d.ts +3 -0
- package/dist/types/server/ax-state.d.ts +43 -0
- package/dist/types/server/canvas-db.d.ts +38 -0
- package/dist/types/server/canvas-state.d.ts +36 -16
- package/dist/types/server/index.d.ts +6 -0
- package/dist/types/server/mutation-history.d.ts +1 -1
- package/docs/cli.md +13 -0
- package/docs/http-api.md +24 -0
- package/docs/mcp.md +20 -2
- package/docs/plans/plan-004-pmx-ax-primitives.md +463 -0
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +5 -0
- package/package.json +3 -2
- package/skills/pmx-canvas/SKILL.md +22 -4
- package/skills/pmx-canvas/references/codex-app-adapter.md +107 -0
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +111 -0
- package/src/cli/agent.ts +34 -0
- package/src/cli/index.ts +2 -1
- package/src/client/App.tsx +2 -0
- package/src/client/canvas/CanvasNode.tsx +7 -0
- package/src/client/canvas/CommandPalette.tsx +2 -1
- package/src/client/canvas/use-node-drag.ts +29 -7
- package/src/client/canvas/use-node-resize.ts +27 -7
- package/src/client/nodes/ExtAppFrame.tsx +51 -10
- package/src/client/nodes/HtmlNode.tsx +5 -2
- package/src/client/nodes/iframe-document-url.ts +58 -0
- package/src/client/state/intent-bridge.ts +8 -0
- package/src/client/state/sse-bridge.ts +2 -2
- package/src/client/theme/global.css +36 -3
- package/src/client/types.ts +1 -0
- package/src/mcp/canvas-access.ts +38 -0
- package/src/mcp/server.ts +113 -4
- package/src/server/ax-context.ts +38 -0
- package/src/server/ax-state.ts +130 -0
- package/src/server/canvas-db.ts +745 -0
- package/src/server/canvas-operations.ts +80 -1
- package/src/server/canvas-schema.ts +3 -3
- package/src/server/canvas-state.ts +390 -50
- package/src/server/canvas-validation.ts +6 -0
- package/src/server/index.ts +18 -0
- package/src/server/mutation-history.ts +1 -0
- package/src/server/server.ts +197 -11
|
@@ -40,6 +40,10 @@ function overlaps(a: CanvasNodeState, b: CanvasNodeState): boolean {
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function participatesInCanvasCollisionValidation(node: CanvasNodeState): boolean {
|
|
44
|
+
return node.dockPosition === null;
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
function fullyContains(group: CanvasNodeState, child: CanvasNodeState): boolean {
|
|
44
48
|
return (
|
|
45
49
|
child.position.x >= group.position.x &&
|
|
@@ -81,8 +85,10 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
|
|
|
81
85
|
|
|
82
86
|
for (let i = 0; i < layout.nodes.length; i++) {
|
|
83
87
|
const a = layout.nodes[i]!;
|
|
88
|
+
if (!participatesInCanvasCollisionValidation(a)) continue;
|
|
84
89
|
for (let j = i + 1; j < layout.nodes.length; j++) {
|
|
85
90
|
const b = layout.nodes[j]!;
|
|
91
|
+
if (!participatesInCanvasCollisionValidation(b)) continue;
|
|
86
92
|
if (!overlaps(a, b)) continue;
|
|
87
93
|
|
|
88
94
|
if (isGroupChildPair(a, b)) {
|
package/src/server/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { canvasState, IMAGE_MIME_MAP } from './canvas-state.js';
|
|
3
3
|
import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout, ViewportState } from './canvas-state.js';
|
|
4
|
+
import { buildCanvasAxContext } from './ax-context.js';
|
|
5
|
+
import type { PmxAxContext, PmxAxFocusState, PmxAxSource, PmxAxState } from './ax-state.js';
|
|
4
6
|
import { findCanvasExtAppNodeId } from './ext-app-lookup.js';
|
|
5
7
|
import { onFileNodeChanged } from './file-watcher.js';
|
|
6
8
|
import { findOpenCanvasPosition, computeGroupBounds } from './placement.js';
|
|
@@ -402,7 +404,9 @@ export class PmxCanvas extends EventEmitter {
|
|
|
402
404
|
y: node.position.y - 100,
|
|
403
405
|
});
|
|
404
406
|
}
|
|
407
|
+
const focus = canvasState.setAxFocus([id], { source: 'sdk', recordHistory: false });
|
|
405
408
|
emitPrimaryWorkbenchEvent('canvas-focus-node', { nodeId: id, noPan });
|
|
409
|
+
emitPrimaryWorkbenchEvent('ax-state-changed', { focus });
|
|
406
410
|
if (!noPan) {
|
|
407
411
|
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
408
412
|
}
|
|
@@ -410,6 +414,20 @@ export class PmxCanvas extends EventEmitter {
|
|
|
410
414
|
return { focused: id, panned: !noPan };
|
|
411
415
|
}
|
|
412
416
|
|
|
417
|
+
getAxState(): PmxAxState {
|
|
418
|
+
return canvasState.getAxState();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
getAxContext(): PmxAxContext {
|
|
422
|
+
return buildCanvasAxContext();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
setAxFocus(nodeIds: string[], options?: { source?: PmxAxSource }): PmxAxFocusState {
|
|
426
|
+
const focus = canvasState.setAxFocus(nodeIds, { source: options?.source ?? 'sdk' });
|
|
427
|
+
emitPrimaryWorkbenchEvent('ax-state-changed', { focus });
|
|
428
|
+
return focus;
|
|
429
|
+
}
|
|
430
|
+
|
|
413
431
|
fitView(options?: {
|
|
414
432
|
width?: number;
|
|
415
433
|
height?: number;
|
package/src/server/server.ts
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { spawnSync } from 'node:child_process';
|
|
38
|
+
import { randomUUID } from 'node:crypto';
|
|
38
39
|
import { existsSync, readFileSync, statSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
39
40
|
import { readFile } from 'node:fs/promises';
|
|
40
41
|
import { basename, extname, join, relative, resolve } from 'node:path';
|
|
@@ -75,6 +76,9 @@ import {
|
|
|
75
76
|
} from './canvas-serialization.js';
|
|
76
77
|
import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
77
78
|
import { buildAgentContextPreamble, serializeNodeForAgentContext } from './agent-context.js';
|
|
79
|
+
import { buildCanvasAxContext } from './ax-context.js';
|
|
80
|
+
import type { PmxAxSource } from './ax-state.js';
|
|
81
|
+
import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
|
|
78
82
|
import { validateLocalImageFile } from './image-source.js';
|
|
79
83
|
import {
|
|
80
84
|
addCanvasNode,
|
|
@@ -145,9 +149,7 @@ let nextWorkbenchSubscriberId = 1;
|
|
|
145
149
|
const workbenchSubscribers = new Map<number, ReadableStreamDefaultController<Uint8Array>>();
|
|
146
150
|
const textEncoder = new TextEncoder();
|
|
147
151
|
let primaryWorkbenchAutoOpenEnabled = true;
|
|
148
|
-
const
|
|
149
|
-
? process.env.PMX_CANVAS_THEME!
|
|
150
|
-
: 'dark');
|
|
152
|
+
const initialCanvasThemeSetting = normalizeCanvasTheme(process.env.PMX_CANVAS_THEME);
|
|
151
153
|
let lastWorkbenchContextCardsEnvelope: Record<string, unknown> | null = null;
|
|
152
154
|
|
|
153
155
|
function normalizeGraphViewerSpec(
|
|
@@ -1038,6 +1040,24 @@ function normalizeMarkdownExternalUrls(markdown: string): string {
|
|
|
1038
1040
|
|
|
1039
1041
|
// ── Canvas SPA HTML ────────────────────────────────────────────
|
|
1040
1042
|
|
|
1043
|
+
const CANVAS_ASSET_VERSION = Date.now().toString(36);
|
|
1044
|
+
const MAX_FRAME_DOCUMENTS = 128;
|
|
1045
|
+
const MAX_FRAME_DOCUMENT_BYTES = 5 * 1024 * 1024;
|
|
1046
|
+
const DEFAULT_FRAME_DOCUMENT_SANDBOX = 'allow-scripts';
|
|
1047
|
+
const SAFE_FRAME_DOCUMENT_SANDBOX_TOKENS = new Set([
|
|
1048
|
+
'allow-downloads',
|
|
1049
|
+
'allow-forms',
|
|
1050
|
+
'allow-modals',
|
|
1051
|
+
'allow-orientation-lock',
|
|
1052
|
+
'allow-pointer-lock',
|
|
1053
|
+
'allow-popups',
|
|
1054
|
+
'allow-popups-to-escape-sandbox',
|
|
1055
|
+
'allow-presentation',
|
|
1056
|
+
'allow-scripts',
|
|
1057
|
+
'allow-storage-access-by-user-activation',
|
|
1058
|
+
]);
|
|
1059
|
+
const frameDocuments = new Map<string, { html: string; sandbox: string }>();
|
|
1060
|
+
|
|
1041
1061
|
function canvasSpaHtml(): string {
|
|
1042
1062
|
return `<!doctype html>
|
|
1043
1063
|
<html lang="en">
|
|
@@ -1106,7 +1126,7 @@ function canvasSpaHtml(): string {
|
|
|
1106
1126
|
color: #eef4ff;
|
|
1107
1127
|
}
|
|
1108
1128
|
</style>
|
|
1109
|
-
<link rel="stylesheet" href="/canvas/global.css" />
|
|
1129
|
+
<link rel="stylesheet" href="/canvas/global.css?v=${CANVAS_ASSET_VERSION}" />
|
|
1110
1130
|
</head>
|
|
1111
1131
|
<body>
|
|
1112
1132
|
<div id="canvasBootstrap">
|
|
@@ -1143,7 +1163,7 @@ function canvasSpaHtml(): string {
|
|
|
1143
1163
|
}, 4000);
|
|
1144
1164
|
})();
|
|
1145
1165
|
</script>
|
|
1146
|
-
<script type="module" src="/canvas/index.js"></script>
|
|
1166
|
+
<script type="module" src="/canvas/index.js?v=${CANVAS_ASSET_VERSION}"></script>
|
|
1147
1167
|
</body>
|
|
1148
1168
|
</html>`;
|
|
1149
1169
|
}
|
|
@@ -1229,6 +1249,61 @@ function serveCanvasFavicon(): Response {
|
|
|
1229
1249
|
|
|
1230
1250
|
// ── Canvas REST handlers ──────────────────────────────────────
|
|
1231
1251
|
|
|
1252
|
+
function normalizeFrameDocumentSandbox(value: unknown): string | null {
|
|
1253
|
+
if (value === undefined || value === null) return DEFAULT_FRAME_DOCUMENT_SANDBOX;
|
|
1254
|
+
if (typeof value !== 'string') return null;
|
|
1255
|
+
const tokens = value.trim().split(/\s+/).filter(Boolean);
|
|
1256
|
+
if (tokens.length === 0) return DEFAULT_FRAME_DOCUMENT_SANDBOX;
|
|
1257
|
+
const uniqueTokens: string[] = [];
|
|
1258
|
+
for (const token of tokens) {
|
|
1259
|
+
if (!SAFE_FRAME_DOCUMENT_SANDBOX_TOKENS.has(token)) return null;
|
|
1260
|
+
if (!uniqueTokens.includes(token)) uniqueTokens.push(token);
|
|
1261
|
+
}
|
|
1262
|
+
return uniqueTokens.join(' ');
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function addFrameDocument(html: string, sandbox: string): string {
|
|
1266
|
+
const id = randomUUID();
|
|
1267
|
+
frameDocuments.set(id, { html, sandbox });
|
|
1268
|
+
while (frameDocuments.size > MAX_FRAME_DOCUMENTS) {
|
|
1269
|
+
const firstKey = frameDocuments.keys().next().value;
|
|
1270
|
+
if (typeof firstKey !== 'string') break;
|
|
1271
|
+
frameDocuments.delete(firstKey);
|
|
1272
|
+
}
|
|
1273
|
+
return `/api/canvas/frame-documents/${id}`;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
async function handleCreateFrameDocument(req: Request): Promise<Response> {
|
|
1277
|
+
const body = await readJson(req);
|
|
1278
|
+
const html = body.html;
|
|
1279
|
+
if (typeof html !== 'string' || !html) {
|
|
1280
|
+
return responseJson({ ok: false, error: 'Frame document requires non-empty html.' }, 400);
|
|
1281
|
+
}
|
|
1282
|
+
if (new TextEncoder().encode(html).byteLength > MAX_FRAME_DOCUMENT_BYTES) {
|
|
1283
|
+
return responseJson({ ok: false, error: 'Frame document is too large.' }, 413);
|
|
1284
|
+
}
|
|
1285
|
+
const sandbox = normalizeFrameDocumentSandbox(body.sandbox);
|
|
1286
|
+
if (!sandbox) {
|
|
1287
|
+
return responseJson({ ok: false, error: 'Frame document sandbox contains unsupported tokens.' }, 400);
|
|
1288
|
+
}
|
|
1289
|
+
return responseJson({ ok: true, url: addFrameDocument(html, sandbox) });
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function handleFrameDocument(pathname: string): Response {
|
|
1293
|
+
const id = decodeURIComponent(pathname.slice('/api/canvas/frame-documents/'.length));
|
|
1294
|
+
const document = frameDocuments.get(id);
|
|
1295
|
+
if (!document) return responseText('Frame document not found.', 404);
|
|
1296
|
+
return new Response(document.html, {
|
|
1297
|
+
headers: {
|
|
1298
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
1299
|
+
'Cache-Control': 'no-store',
|
|
1300
|
+
'Content-Security-Policy': `sandbox ${document.sandbox}`,
|
|
1301
|
+
'Referrer-Policy': 'no-referrer',
|
|
1302
|
+
'X-Content-Type-Options': 'nosniff',
|
|
1303
|
+
},
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1232
1307
|
async function handleCanvasUpdate(req: Request): Promise<Response> {
|
|
1233
1308
|
const body = await readJson(req);
|
|
1234
1309
|
const updates = Array.isArray(body.updates) ? body.updates : [];
|
|
@@ -1436,7 +1511,8 @@ async function createCanvasWebpageNode(body: Record<string, unknown>): Promise<R
|
|
|
1436
1511
|
|
|
1437
1512
|
async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
1438
1513
|
const body = await readJson(req);
|
|
1439
|
-
const
|
|
1514
|
+
const queryType = new URL(req.url).searchParams.get('type');
|
|
1515
|
+
const type = typeof body.type === 'string' ? body.type : queryType || 'markdown';
|
|
1440
1516
|
|
|
1441
1517
|
if (!VALID_NODE_TYPES.has(type)) {
|
|
1442
1518
|
if (type === 'json-render') {
|
|
@@ -1816,10 +1892,16 @@ async function handleCanvasFocus(req: Request): Promise<Response> {
|
|
|
1816
1892
|
const maxZ = canvasState.getLayout().nodes.reduce((max, layoutNode) => Math.max(max, layoutNode.zIndex), 0);
|
|
1817
1893
|
canvasState.updateNode(nodeId, { zIndex: maxZ + 1 });
|
|
1818
1894
|
}
|
|
1895
|
+
const focus = canvasState.setAxFocus([nodeId], { source: 'api', recordHistory: false });
|
|
1896
|
+
broadcastWorkbenchEvent('ax-state-changed', {
|
|
1897
|
+
focus,
|
|
1898
|
+
sessionId: primaryWorkbenchSessionId,
|
|
1899
|
+
timestamp: new Date().toISOString(),
|
|
1900
|
+
});
|
|
1819
1901
|
emitPrimaryWorkbenchEvent('canvas-focus-node', { nodeId, noPan });
|
|
1820
1902
|
if (!noPan) emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
1821
1903
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1822
|
-
return responseJson({ ok: true, focused: nodeId, panned: !noPan });
|
|
1904
|
+
return responseJson({ ok: true, focused: nodeId, panned: !noPan, axFocus: focus });
|
|
1823
1905
|
}
|
|
1824
1906
|
|
|
1825
1907
|
async function handleCanvasFit(req: Request): Promise<Response> {
|
|
@@ -2104,6 +2186,18 @@ function handleCanvasValidate(): Response {
|
|
|
2104
2186
|
return responseJson(validateCanvasLayout(canvasState.getLayout()));
|
|
2105
2187
|
}
|
|
2106
2188
|
|
|
2189
|
+
async function handleCanvasThemeUpdate(req: Request): Promise<Response> {
|
|
2190
|
+
const body = await readJson(req);
|
|
2191
|
+
const theme = normalizeCanvasTheme(body.theme, canvasState.theme);
|
|
2192
|
+
const next = canvasState.setTheme(theme);
|
|
2193
|
+
broadcastWorkbenchEvent('theme-changed', {
|
|
2194
|
+
theme: next,
|
|
2195
|
+
sessionId: primaryWorkbenchSessionId,
|
|
2196
|
+
timestamp: new Date().toISOString(),
|
|
2197
|
+
});
|
|
2198
|
+
return responseJson({ ok: true, theme: next });
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2107
2201
|
async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
2108
2202
|
const nodeId = url.searchParams.get('nodeId') ?? '';
|
|
2109
2203
|
if (!nodeId) return responseText('Missing nodeId', 400);
|
|
@@ -2972,7 +3066,7 @@ function handleWorkbenchEvents(req: Request): Response {
|
|
|
2972
3066
|
requestedSessionId: requestedSessionId || null,
|
|
2973
3067
|
continuity,
|
|
2974
3068
|
path: primaryWorkbenchPath,
|
|
2975
|
-
theme:
|
|
3069
|
+
theme: canvasState.theme,
|
|
2976
3070
|
timestamp: new Date().toISOString(),
|
|
2977
3071
|
}),
|
|
2978
3072
|
);
|
|
@@ -3390,6 +3484,63 @@ function handleGetPinnedContext(): Response {
|
|
|
3390
3484
|
return responseJson({ preamble, nodeIds: pinnedIds, count: pinnedIds.length, nodes });
|
|
3391
3485
|
}
|
|
3392
3486
|
|
|
3487
|
+
function normalizeAxNodeIds(value: unknown): string[] {
|
|
3488
|
+
if (!Array.isArray(value)) return [];
|
|
3489
|
+
return value.filter((id): id is string => typeof id === 'string');
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
function normalizeAxSource(value: unknown, fallback: PmxAxSource): PmxAxSource {
|
|
3493
|
+
return value === 'agent' ||
|
|
3494
|
+
value === 'api' ||
|
|
3495
|
+
value === 'browser' ||
|
|
3496
|
+
value === 'cli' ||
|
|
3497
|
+
value === 'codex' ||
|
|
3498
|
+
value === 'copilot' ||
|
|
3499
|
+
value === 'mcp' ||
|
|
3500
|
+
value === 'sdk' ||
|
|
3501
|
+
value === 'system'
|
|
3502
|
+
? value
|
|
3503
|
+
: fallback;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
function handleGetAxState(): Response {
|
|
3507
|
+
return responseJson({ ok: true, state: canvasState.getAxState() });
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
function handleGetAxContext(): Response {
|
|
3511
|
+
return responseJson(buildCanvasAxContext());
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
async function handleAxFocusUpdate(req: Request): Promise<Response> {
|
|
3515
|
+
const body = await readJson(req);
|
|
3516
|
+
const nodeIds = normalizeAxNodeIds(body.nodeIds);
|
|
3517
|
+
const source = normalizeAxSource(body.source, 'api');
|
|
3518
|
+
const focus = canvasState.setAxFocus(nodeIds, { source });
|
|
3519
|
+
broadcastWorkbenchEvent('ax-state-changed', {
|
|
3520
|
+
focus,
|
|
3521
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3522
|
+
timestamp: new Date().toISOString(),
|
|
3523
|
+
});
|
|
3524
|
+
return responseJson({ ok: true, focus });
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
async function handleAxStatePatch(req: Request): Promise<Response> {
|
|
3528
|
+
const body = await readJson(req);
|
|
3529
|
+
if (!body.focus || typeof body.focus !== 'object' || Array.isArray(body.focus)) {
|
|
3530
|
+
return responseJson({ ok: false, error: 'PATCH /api/canvas/ax currently requires a focus object.' }, 400);
|
|
3531
|
+
}
|
|
3532
|
+
const focusInput = body.focus as Record<string, unknown>;
|
|
3533
|
+
const focus = canvasState.setAxFocus(normalizeAxNodeIds(focusInput.nodeIds), {
|
|
3534
|
+
source: normalizeAxSource(focusInput.source, 'api'),
|
|
3535
|
+
});
|
|
3536
|
+
broadcastWorkbenchEvent('ax-state-changed', {
|
|
3537
|
+
focus,
|
|
3538
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3539
|
+
timestamp: new Date().toISOString(),
|
|
3540
|
+
});
|
|
3541
|
+
return responseJson({ ok: true, state: canvasState.getAxState() });
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3393
3544
|
// ── Port resolution ───────────────────────────────────────────
|
|
3394
3545
|
|
|
3395
3546
|
function buildPortCandidates(preferredPort: number): number[] {
|
|
@@ -4074,12 +4225,13 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4074
4225
|
|
|
4075
4226
|
// ── Canvas persistence: set workspace root and load saved state ──
|
|
4076
4227
|
canvasState.setWorkspaceRoot(activeWorkspaceRoot);
|
|
4228
|
+
canvasState.setTheme(initialCanvasThemeSetting as CanvasTheme);
|
|
4077
4229
|
const loaded = canvasState.loadFromDisk({ clearExisting: true });
|
|
4078
4230
|
setCanvasLayoutUpdateEmitter(() => {
|
|
4079
4231
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
4080
4232
|
});
|
|
4081
4233
|
if (loaded) {
|
|
4082
|
-
console.log(' Canvas state restored from .pmx-canvas/
|
|
4234
|
+
console.log(' Canvas state restored from .pmx-canvas/canvas.db');
|
|
4083
4235
|
primeCanvasRuntimeBackends({ forceRehydrateExtApps: true });
|
|
4084
4236
|
void syncCanvasRuntimeBackends({ forceRehydrateExtApps: true, alreadyPrimed: true }).finally(() => {
|
|
4085
4237
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
@@ -4089,7 +4241,9 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4089
4241
|
rotatePrimaryWorkbenchSessionIfNeeded();
|
|
4090
4242
|
|
|
4091
4243
|
const preferredPort = options.port ?? Number(process.env.PMX_WEB_CANVAS_PORT ?? DEFAULT_PORT);
|
|
4092
|
-
const portCandidates = options.
|
|
4244
|
+
const portCandidates = options.port === 0
|
|
4245
|
+
? [0]
|
|
4246
|
+
: options.allowPortFallback === false
|
|
4093
4247
|
? [preferredPort > 0 ? Math.floor(preferredPort) : DEFAULT_PORT]
|
|
4094
4248
|
: buildPortCandidates(preferredPort);
|
|
4095
4249
|
|
|
@@ -4118,6 +4272,14 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4118
4272
|
return handleJsonRenderView(url);
|
|
4119
4273
|
}
|
|
4120
4274
|
|
|
4275
|
+
if (url.pathname === '/api/canvas/frame-documents' && req.method === 'POST') {
|
|
4276
|
+
return handleCreateFrameDocument(req);
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
if (url.pathname.startsWith('/api/canvas/frame-documents/') && req.method === 'GET') {
|
|
4280
|
+
return handleFrameDocument(url.pathname);
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4121
4283
|
if (url.pathname === '/' || url.pathname === '/workbench' || url.pathname === '/artifact') {
|
|
4122
4284
|
return new Response(canvasSpaHtml(), {
|
|
4123
4285
|
headers: {
|
|
@@ -4191,6 +4353,14 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4191
4353
|
return responseJson(buildCanvasSummary());
|
|
4192
4354
|
}
|
|
4193
4355
|
|
|
4356
|
+
if (url.pathname === '/api/canvas/theme' && req.method === 'GET') {
|
|
4357
|
+
return responseJson({ ok: true, theme: canvasState.theme });
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
if (url.pathname === '/api/canvas/theme' && req.method === 'POST') {
|
|
4361
|
+
return handleCanvasThemeUpdate(req);
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4194
4364
|
if (url.pathname === '/api/canvas/update' && req.method === 'POST') {
|
|
4195
4365
|
return handleCanvasUpdate(req);
|
|
4196
4366
|
}
|
|
@@ -4333,6 +4503,22 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4333
4503
|
return handleGetPinnedContext();
|
|
4334
4504
|
}
|
|
4335
4505
|
|
|
4506
|
+
if (url.pathname === '/api/canvas/ax' && req.method === 'GET') {
|
|
4507
|
+
return handleGetAxState();
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
if (url.pathname === '/api/canvas/ax' && req.method === 'PATCH') {
|
|
4511
|
+
return handleAxStatePatch(req);
|
|
4512
|
+
}
|
|
4513
|
+
|
|
4514
|
+
if (url.pathname === '/api/canvas/ax/context' && req.method === 'GET') {
|
|
4515
|
+
return handleGetAxContext();
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
if (url.pathname === '/api/canvas/ax/focus' && req.method === 'POST') {
|
|
4519
|
+
return handleAxFocusUpdate(req);
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4336
4522
|
// Spatial context API
|
|
4337
4523
|
if (url.pathname === '/api/canvas/spatial-context' && req.method === 'GET') {
|
|
4338
4524
|
const layout = canvasState.getLayout();
|
|
@@ -4482,7 +4668,7 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4482
4668
|
}
|
|
4483
4669
|
|
|
4484
4670
|
export function stopCanvasServer(): void {
|
|
4485
|
-
canvasState.
|
|
4671
|
+
canvasState.close();
|
|
4486
4672
|
closeAllMcpAppSessions();
|
|
4487
4673
|
setCanvasLayoutUpdateEmitter(null);
|
|
4488
4674
|
void closeCanvasAutomationWebViewInternal().catch((error) => {
|