pmx-canvas 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/Readme.md +13 -4
- package/dist/canvas/index.js +71 -71
- package/dist/json-render/index.js +47 -47
- package/dist/types/cli/agent.d.ts +13 -8
- package/dist/types/cli/commands/apps.d.ts +1 -0
- package/dist/types/cli/commands/ax.d.ts +1 -0
- package/dist/types/cli/commands/copilot.d.ts +1 -0
- package/dist/types/cli/commands/edges.d.ts +1 -0
- package/dist/types/cli/commands/groups.d.ts +1 -0
- package/dist/types/cli/commands/history.d.ts +1 -0
- package/dist/types/cli/commands/nodes.d.ts +1 -0
- package/dist/types/cli/commands/pins.d.ts +1 -0
- package/dist/types/cli/commands/query.d.ts +1 -0
- package/dist/types/cli/commands/view.d.ts +1 -0
- package/dist/types/cli/commands/webview.d.ts +1 -0
- package/dist/types/cli/shared.d.ts +73 -0
- package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -1
- package/dist/types/client/index.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +10 -0
- package/dist/types/client/nodes/HtmlNode.d.ts +1 -1
- package/dist/types/client/nodes/McpAppNode.d.ts +1 -1
- package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +20 -0
- package/dist/types/client/state/node-factory.d.ts +29 -0
- package/dist/types/client/uuid-polyfill.d.ts +0 -0
- package/dist/types/server/canvas-db.d.ts +0 -1
- package/dist/types/server/canvas-operations.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +14 -15
- package/dist/types/server/diagram-presets.d.ts +8 -0
- package/dist/types/server/operations/composites.d.ts +6 -30
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +18 -0
- package/docs/bun-webview-integration.md +5 -0
- package/docs/cli.md +5 -0
- package/docs/environment.md +48 -0
- package/docs/http-api.md +25 -1
- package/docs/mcp.md +21 -30
- package/docs/node-types.md +5 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +34 -15
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +5 -5
- package/package.json +13 -10
- package/skills/pmx-canvas/SKILL.md +16 -6
- package/skills/pmx-canvas/evals/evals.json +7 -7
- package/skills/pmx-canvas/references/full-reference.md +25 -16
- package/src/cli/agent.ts +22 -3512
- package/src/cli/commands/apps.ts +165 -0
- package/src/cli/commands/ax.ts +604 -0
- package/src/cli/commands/copilot.ts +43 -0
- package/src/cli/commands/edges.ts +73 -0
- package/src/cli/commands/groups.ts +81 -0
- package/src/cli/commands/history.ts +220 -0
- package/src/cli/commands/nodes.ts +984 -0
- package/src/cli/commands/pins.ts +42 -0
- package/src/cli/commands/query.ts +223 -0
- package/src/cli/commands/view.ts +186 -0
- package/src/cli/commands/webview.ts +227 -0
- package/src/cli/index.ts +8 -2
- package/src/cli/shared.ts +861 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -2
- package/src/client/index.tsx +2 -0
- package/src/client/nodes/ExtAppFrame.tsx +70 -51
- package/src/client/nodes/HtmlNode.tsx +8 -51
- package/src/client/nodes/McpAppNode.tsx +12 -47
- package/src/client/nodes/use-ax-surface-bridge.ts +71 -0
- package/src/client/state/node-factory.ts +72 -0
- package/src/client/state/sse-bridge.ts +209 -86
- package/src/client/uuid-polyfill.ts +14 -0
- package/src/json-render/renderer/index.tsx +3 -2
- package/src/mcp/server.ts +0 -47
- package/src/server/canvas-db.ts +0 -8
- package/src/server/canvas-operations.ts +8 -0
- package/src/server/canvas-schema.ts +2 -8
- package/src/server/canvas-state.ts +51 -151
- package/src/server/diagram-presets.ts +15 -1
- package/src/server/html-surface.ts +11 -6
- package/src/server/index.ts +3 -9
- package/src/server/operations/composites.ts +25 -34
- package/src/server/operations/invoker.ts +10 -3
- package/src/server/operations/mcp.ts +51 -1
- package/src/server/operations/ops/app.ts +5 -0
- package/src/server/operations/ops/snapshots.ts +6 -7
- package/src/server/operations/registry.ts +4 -1
- package/src/server/server.ts +172 -74
- package/src/shared/ax-surface-protocol.ts +22 -0
- package/dist/types/client/canvas/ContextPinHud.d.ts +0 -1
- package/src/client/canvas/ContextPinHud.tsx +0 -17
|
@@ -70,7 +70,7 @@ const snapshotListOperation = defineOperation<z.infer<typeof snapshotListSchema>
|
|
|
70
70
|
mcp: {
|
|
71
71
|
toolName: 'canvas_list_snapshots',
|
|
72
72
|
description:
|
|
73
|
-
'
|
|
73
|
+
'List saved canvas snapshots with IDs, names, timestamps, and node/edge counts. Defaults to the 20 newest snapshots; pass all=true to return every snapshot.',
|
|
74
74
|
extraShape: {
|
|
75
75
|
limit: z.number().optional().describe('Maximum snapshots to return (default: 20)'),
|
|
76
76
|
query: z.string().optional().describe('Optional case-insensitive ID/name filter'),
|
|
@@ -117,7 +117,7 @@ const snapshotSaveOperation = defineOperation<z.infer<typeof snapshotSaveSchema>
|
|
|
117
117
|
mcp: {
|
|
118
118
|
toolName: 'canvas_snapshot',
|
|
119
119
|
description:
|
|
120
|
-
'
|
|
120
|
+
'Save the current canvas state as a named snapshot. Snapshots persist to disk and can be restored later.',
|
|
121
121
|
extraShape: {
|
|
122
122
|
name: z.string().describe('Name for this snapshot (e.g., "before refactor", "investigation v2")'),
|
|
123
123
|
},
|
|
@@ -155,7 +155,7 @@ const snapshotGcOperation = defineOperation<z.infer<typeof snapshotGcSchema>, Re
|
|
|
155
155
|
mcp: {
|
|
156
156
|
toolName: 'canvas_gc_snapshots',
|
|
157
157
|
description:
|
|
158
|
-
'
|
|
158
|
+
'Delete old saved canvas snapshots, keeping the newest N snapshots. Use dryRun=true to preview deletions.',
|
|
159
159
|
extraShape: {
|
|
160
160
|
keep: z.number().optional().describe('Number of newest snapshots to keep (default: 20)'),
|
|
161
161
|
dryRun: z.boolean().optional().describe('Preview deletions without removing snapshot files'),
|
|
@@ -206,7 +206,7 @@ const snapshotDiffOperation = defineOperation<z.infer<typeof snapshotDiffSchema>
|
|
|
206
206
|
mcp: {
|
|
207
207
|
toolName: 'canvas_diff',
|
|
208
208
|
description:
|
|
209
|
-
'
|
|
209
|
+
'Compare the current canvas state against a saved snapshot. Shows added/removed/modified nodes and edges. Pass either a snapshot name or ID.',
|
|
210
210
|
extraShape: {
|
|
211
211
|
snapshot: z.string().describe('Snapshot name or ID to compare against'),
|
|
212
212
|
},
|
|
@@ -291,7 +291,7 @@ const snapshotRestoreOperation = defineOperation<z.infer<typeof snapshotRestoreS
|
|
|
291
291
|
mcp: {
|
|
292
292
|
toolName: 'canvas_restore',
|
|
293
293
|
description:
|
|
294
|
-
'
|
|
294
|
+
'Restore the canvas to a previously saved snapshot. Use canvas_snapshot to save first. Pass either the snapshot ID or name to restore.',
|
|
295
295
|
extraShape: {
|
|
296
296
|
id: z.string().describe('Snapshot ID or name to restore (from canvas_snapshot or snapshot list)'),
|
|
297
297
|
},
|
|
@@ -346,8 +346,7 @@ const snapshotDeleteOperation = defineOperation<z.infer<typeof snapshotDeleteSch
|
|
|
346
346
|
},
|
|
347
347
|
mcp: {
|
|
348
348
|
toolName: 'canvas_delete_snapshot',
|
|
349
|
-
description:
|
|
350
|
-
'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Delete a saved snapshot by ID.',
|
|
349
|
+
description: 'Delete a saved snapshot by ID.',
|
|
351
350
|
extraShape: {
|
|
352
351
|
id: z.string().describe('Snapshot ID to delete'),
|
|
353
352
|
},
|
|
@@ -39,7 +39,10 @@ export function setOperationEventEmitter(emitter: OperationEventEmitter | null):
|
|
|
39
39
|
operationEventEmitter = emitter;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// Depth-counted
|
|
42
|
+
// Depth-counted EMIT suppression (same pattern as, but deliberately distinct
|
|
43
|
+
// state from, canvasState's recording suppression: batch suppresses SSE emits
|
|
44
|
+
// while sub-ops still record history; undo/redo suppresses recording while
|
|
45
|
+
// still emitting SSE — merging the two counters would break one or the other).
|
|
43
46
|
// While > 0, emitOperationEvent is a no-op so a meta-op (canvas.batch) can run
|
|
44
47
|
// many sub-ops without producing per-entry SSE frames, then emit ONE final
|
|
45
48
|
// layout frame itself. Both the `mutates` auto-emit and `ctx.emit` route through
|
package/src/server/server.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* - GET /api/canvas/json-render/view?nodeId=... -> local json-render viewer
|
|
25
25
|
* - POST /api/canvas/web-artifact -> build bundled HTML artifact + optional canvas node
|
|
26
26
|
* - GET /api/workbench/events -> SSE event stream
|
|
27
|
+
* - GET /api/workbench/poll -> proxy-safe polling transport (same events, JSON)
|
|
27
28
|
* - GET /api/workbench/state -> workbench state snapshot
|
|
28
29
|
* - POST /api/workbench/intent -> workbench intents
|
|
29
30
|
* - GET /api/workbench/webview -> Bun.WebView automation status
|
|
@@ -61,6 +62,7 @@ import { resolveNodeAxCapabilities } from './ax-interaction.js';
|
|
|
61
62
|
import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
|
|
62
63
|
import { validateLocalImageFile } from './image-source.js';
|
|
63
64
|
import {
|
|
65
|
+
cancelCodeGraphRecompute,
|
|
64
66
|
primeCanvasRuntimeBackends,
|
|
65
67
|
setCanvasLayoutUpdateEmitter,
|
|
66
68
|
syncCanvasRuntimeBackends,
|
|
@@ -709,7 +711,24 @@ function toSseFrame(event: string, payload: PrimaryWorkbenchEventPayload): Uint8
|
|
|
709
711
|
return textEncoder.encode(`${lines.join('\n')}\n`);
|
|
710
712
|
}
|
|
711
713
|
|
|
714
|
+
// ── Workbench event ring (proxy-safe polling transport) ─────────
|
|
715
|
+
// Buffering proxies (e.g. the Amp orb portal) hold a long-lived SSE response
|
|
716
|
+
// open and never flush it to the browser, so an EventSource client behind one
|
|
717
|
+
// receives nothing and the canvas never boots. Every broadcast event is also
|
|
718
|
+
// recorded here with a monotonic seq; GET /api/workbench/poll replays them as
|
|
719
|
+
// a normal, immediately-closed JSON response that any proxy delivers. Bounded:
|
|
720
|
+
// a client whose `since` fell off the ring gets a fresh connect snapshot,
|
|
721
|
+
// which fully rebuilds client state (same events an SSE connect sends).
|
|
722
|
+
const WORKBENCH_EVENT_RING_MAX = 500;
|
|
723
|
+
let workbenchEventSeq = 0;
|
|
724
|
+
const workbenchEventRing: Array<{ seq: number; event: string; payload: PrimaryWorkbenchEventPayload }> = [];
|
|
725
|
+
|
|
712
726
|
function broadcastWorkbenchEvent(event: string, payload: PrimaryWorkbenchEventPayload): void {
|
|
727
|
+
workbenchEventSeq += 1;
|
|
728
|
+
workbenchEventRing.push({ seq: workbenchEventSeq, event, payload });
|
|
729
|
+
if (workbenchEventRing.length > WORKBENCH_EVENT_RING_MAX) {
|
|
730
|
+
workbenchEventRing.splice(0, workbenchEventRing.length - WORKBENCH_EVENT_RING_MAX);
|
|
731
|
+
}
|
|
713
732
|
const frame = toSseFrame(event, payload);
|
|
714
733
|
for (const [subscriberId, controller] of workbenchSubscribers.entries()) {
|
|
715
734
|
try {
|
|
@@ -1371,14 +1390,19 @@ function responseJson(data: unknown, status = 200): Response {
|
|
|
1371
1390
|
});
|
|
1372
1391
|
}
|
|
1373
1392
|
|
|
1393
|
+
// v0.4.0 (M4): every hand-written error response is a JSON envelope
|
|
1394
|
+
// ({ ok: false, error }, same status codes) matching the registry dispatcher —
|
|
1395
|
+
// consumers no longer need to branch on text/plain vs JSON error bodies.
|
|
1374
1396
|
function responseText(text: string, status = 400): Response {
|
|
1375
|
-
return
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1397
|
+
return Response.json(
|
|
1398
|
+
{ ok: false, error: text },
|
|
1399
|
+
{
|
|
1400
|
+
status,
|
|
1401
|
+
headers: {
|
|
1402
|
+
'Cache-Control': 'no-store',
|
|
1403
|
+
},
|
|
1380
1404
|
},
|
|
1381
|
-
|
|
1405
|
+
);
|
|
1382
1406
|
}
|
|
1383
1407
|
|
|
1384
1408
|
function handleArtifactView(url: URL): Response {
|
|
@@ -1691,15 +1715,134 @@ async function handleWorkbenchIntent(req: Request): Promise<Response> {
|
|
|
1691
1715
|
return responseJson({ ok: true, intent });
|
|
1692
1716
|
}
|
|
1693
1717
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1718
|
+
/**
|
|
1719
|
+
* The connect snapshot: the ordered event list that fully rebuilds a client's
|
|
1720
|
+
* state. Single source for BOTH transports — the SSE stream enqueues these
|
|
1721
|
+
* frames on connect, and /api/workbench/poll returns them as JSON when a
|
|
1722
|
+
* poller has no usable `since` (fresh client, server restart, or ring
|
|
1723
|
+
* eviction). Adding a new connect-time event here reaches both paths.
|
|
1724
|
+
*/
|
|
1725
|
+
function buildWorkbenchConnectSnapshot(
|
|
1726
|
+
requestedSessionId: string,
|
|
1727
|
+
): Array<{ event: string; payload: Record<string, unknown> }> {
|
|
1697
1728
|
const continuity =
|
|
1698
1729
|
requestedSessionId.length === 0
|
|
1699
1730
|
? 'fresh'
|
|
1700
1731
|
: requestedSessionId === primaryWorkbenchSessionId
|
|
1701
1732
|
? 'resumed'
|
|
1702
1733
|
: 'rotated';
|
|
1734
|
+
const events: Array<{ event: string; payload: Record<string, unknown> }> = [];
|
|
1735
|
+
events.push({
|
|
1736
|
+
event: 'connected',
|
|
1737
|
+
payload: {
|
|
1738
|
+
sessionId: primaryWorkbenchSessionId,
|
|
1739
|
+
requestedSessionId: requestedSessionId || null,
|
|
1740
|
+
continuity,
|
|
1741
|
+
path: primaryWorkbenchPath,
|
|
1742
|
+
theme: canvasState.theme,
|
|
1743
|
+
timestamp: new Date().toISOString(),
|
|
1744
|
+
},
|
|
1745
|
+
});
|
|
1746
|
+
if (primaryWorkbenchPath) {
|
|
1747
|
+
events.push({
|
|
1748
|
+
event: 'workbench-open',
|
|
1749
|
+
payload: {
|
|
1750
|
+
sessionId: primaryWorkbenchSessionId,
|
|
1751
|
+
path: primaryWorkbenchPath,
|
|
1752
|
+
title: basename(primaryWorkbenchPath),
|
|
1753
|
+
source: 'snapshot',
|
|
1754
|
+
updatedAt: new Date().toISOString(),
|
|
1755
|
+
},
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
if (lastWorkbenchContextCardsEnvelope) {
|
|
1759
|
+
events.push({
|
|
1760
|
+
event: 'context-cards',
|
|
1761
|
+
payload: {
|
|
1762
|
+
...lastWorkbenchContextCardsEnvelope,
|
|
1763
|
+
sessionId: primaryWorkbenchSessionId,
|
|
1764
|
+
timestamp: new Date().toISOString(),
|
|
1765
|
+
},
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
events.push({
|
|
1769
|
+
event: 'mcp-app-host-snapshot',
|
|
1770
|
+
payload: {
|
|
1771
|
+
reason: 'connect-snapshot',
|
|
1772
|
+
...getMcpAppHostSnapshot(),
|
|
1773
|
+
timestamp: new Date().toISOString(),
|
|
1774
|
+
},
|
|
1775
|
+
});
|
|
1776
|
+
events.push({
|
|
1777
|
+
event: 'canvas-layout-update',
|
|
1778
|
+
payload: {
|
|
1779
|
+
layout: canvasState.getLayout(),
|
|
1780
|
+
sessionId: primaryWorkbenchSessionId,
|
|
1781
|
+
timestamp: new Date().toISOString(),
|
|
1782
|
+
},
|
|
1783
|
+
});
|
|
1784
|
+
events.push({
|
|
1785
|
+
event: 'context-pins-changed',
|
|
1786
|
+
payload: {
|
|
1787
|
+
count: canvasState.contextPinnedNodeIds.size,
|
|
1788
|
+
nodeIds: Array.from(canvasState.contextPinnedNodeIds),
|
|
1789
|
+
sessionId: primaryWorkbenchSessionId,
|
|
1790
|
+
timestamp: new Date().toISOString(),
|
|
1791
|
+
},
|
|
1792
|
+
});
|
|
1793
|
+
for (const intent of intentRegistry.list()) {
|
|
1794
|
+
events.push({
|
|
1795
|
+
event: 'ax-intent',
|
|
1796
|
+
payload: {
|
|
1797
|
+
intent,
|
|
1798
|
+
reason: 'connect-snapshot',
|
|
1799
|
+
sessionId: primaryWorkbenchSessionId,
|
|
1800
|
+
timestamp: new Date().toISOString(),
|
|
1801
|
+
},
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
return events;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/**
|
|
1808
|
+
* Proxy-safe transport (Amp orb portals and any proxy that buffers streaming
|
|
1809
|
+
* responses): returns the buffered events after `since` — or the full connect
|
|
1810
|
+
* snapshot when `since` is absent, from a previous server run, or already
|
|
1811
|
+
* evicted from the ring — as a normal JSON response that closes immediately.
|
|
1812
|
+
* The client polls every couple of seconds; each response flushes through any
|
|
1813
|
+
* proxy. `seq` is the cursor for the next poll.
|
|
1814
|
+
*/
|
|
1815
|
+
function handleWorkbenchPoll(url: URL): Response {
|
|
1816
|
+
rotatePrimaryWorkbenchSessionIfNeeded();
|
|
1817
|
+
const requestedSessionId = url.searchParams.get('session')?.trim() ?? '';
|
|
1818
|
+
const sinceRaw = url.searchParams.get('since');
|
|
1819
|
+
const since = sinceRaw === null ? Number.NaN : Number(sinceRaw);
|
|
1820
|
+
const ringHeadSeq =
|
|
1821
|
+
workbenchEventRing.length > 0 ? (workbenchEventRing[0] as { seq: number }).seq : workbenchEventSeq + 1;
|
|
1822
|
+
const usableSince = Number.isInteger(since) && since >= 0 && since <= workbenchEventSeq && since + 1 >= ringHeadSeq;
|
|
1823
|
+
if (!usableSince) {
|
|
1824
|
+
return responseJson({
|
|
1825
|
+
ok: true,
|
|
1826
|
+
transport: 'poll',
|
|
1827
|
+
snapshot: true,
|
|
1828
|
+
seq: workbenchEventSeq,
|
|
1829
|
+
sessionId: primaryWorkbenchSessionId,
|
|
1830
|
+
events: buildWorkbenchConnectSnapshot(requestedSessionId),
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
return responseJson({
|
|
1834
|
+
ok: true,
|
|
1835
|
+
transport: 'poll',
|
|
1836
|
+
snapshot: false,
|
|
1837
|
+
seq: workbenchEventSeq,
|
|
1838
|
+
sessionId: primaryWorkbenchSessionId,
|
|
1839
|
+
events: workbenchEventRing.filter((entry) => entry.seq > since),
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function handleWorkbenchEvents(req: Request): Response {
|
|
1844
|
+
const reqUrl = new URL(req.url);
|
|
1845
|
+
const requestedSessionId = reqUrl.searchParams.get('session')?.trim() ?? '';
|
|
1703
1846
|
const subscriberId = nextWorkbenchSubscriberId++;
|
|
1704
1847
|
let pingTimer: ReturnType<typeof setInterval> | null = null;
|
|
1705
1848
|
|
|
@@ -1707,68 +1850,8 @@ function handleWorkbenchEvents(req: Request): Response {
|
|
|
1707
1850
|
start(controller) {
|
|
1708
1851
|
workbenchSubscribers.set(subscriberId, controller);
|
|
1709
1852
|
syncCanvasBrowserOpenedFromSubscribers();
|
|
1710
|
-
|
|
1711
|
-
toSseFrame(
|
|
1712
|
-
sessionId: primaryWorkbenchSessionId,
|
|
1713
|
-
requestedSessionId: requestedSessionId || null,
|
|
1714
|
-
continuity,
|
|
1715
|
-
path: primaryWorkbenchPath,
|
|
1716
|
-
theme: canvasState.theme,
|
|
1717
|
-
timestamp: new Date().toISOString(),
|
|
1718
|
-
}),
|
|
1719
|
-
);
|
|
1720
|
-
if (primaryWorkbenchPath) {
|
|
1721
|
-
controller.enqueue(
|
|
1722
|
-
toSseFrame('workbench-open', {
|
|
1723
|
-
sessionId: primaryWorkbenchSessionId,
|
|
1724
|
-
path: primaryWorkbenchPath,
|
|
1725
|
-
title: basename(primaryWorkbenchPath),
|
|
1726
|
-
source: 'snapshot',
|
|
1727
|
-
updatedAt: new Date().toISOString(),
|
|
1728
|
-
}),
|
|
1729
|
-
);
|
|
1730
|
-
}
|
|
1731
|
-
if (lastWorkbenchContextCardsEnvelope) {
|
|
1732
|
-
controller.enqueue(
|
|
1733
|
-
toSseFrame('context-cards', {
|
|
1734
|
-
...lastWorkbenchContextCardsEnvelope,
|
|
1735
|
-
sessionId: primaryWorkbenchSessionId,
|
|
1736
|
-
timestamp: new Date().toISOString(),
|
|
1737
|
-
}),
|
|
1738
|
-
);
|
|
1739
|
-
}
|
|
1740
|
-
controller.enqueue(
|
|
1741
|
-
toSseFrame('mcp-app-host-snapshot', {
|
|
1742
|
-
reason: 'connect-snapshot',
|
|
1743
|
-
...getMcpAppHostSnapshot(),
|
|
1744
|
-
timestamp: new Date().toISOString(),
|
|
1745
|
-
}),
|
|
1746
|
-
);
|
|
1747
|
-
const layout = canvasState.getLayout();
|
|
1748
|
-
controller.enqueue(
|
|
1749
|
-
toSseFrame('canvas-layout-update', {
|
|
1750
|
-
layout,
|
|
1751
|
-
sessionId: primaryWorkbenchSessionId,
|
|
1752
|
-
timestamp: new Date().toISOString(),
|
|
1753
|
-
}),
|
|
1754
|
-
);
|
|
1755
|
-
controller.enqueue(
|
|
1756
|
-
toSseFrame('context-pins-changed', {
|
|
1757
|
-
count: canvasState.contextPinnedNodeIds.size,
|
|
1758
|
-
nodeIds: Array.from(canvasState.contextPinnedNodeIds),
|
|
1759
|
-
sessionId: primaryWorkbenchSessionId,
|
|
1760
|
-
timestamp: new Date().toISOString(),
|
|
1761
|
-
}),
|
|
1762
|
-
);
|
|
1763
|
-
for (const intent of intentRegistry.list()) {
|
|
1764
|
-
controller.enqueue(
|
|
1765
|
-
toSseFrame('ax-intent', {
|
|
1766
|
-
intent,
|
|
1767
|
-
reason: 'connect-snapshot',
|
|
1768
|
-
sessionId: primaryWorkbenchSessionId,
|
|
1769
|
-
timestamp: new Date().toISOString(),
|
|
1770
|
-
}),
|
|
1771
|
-
);
|
|
1853
|
+
for (const { event, payload } of buildWorkbenchConnectSnapshot(requestedSessionId)) {
|
|
1854
|
+
controller.enqueue(toSseFrame(event, payload));
|
|
1772
1855
|
}
|
|
1773
1856
|
pingTimer = setInterval(() => {
|
|
1774
1857
|
try {
|
|
@@ -2854,7 +2937,10 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
2854
2937
|
|
|
2855
2938
|
rotatePrimaryWorkbenchSessionIfNeeded();
|
|
2856
2939
|
|
|
2857
|
-
|
|
2940
|
+
// M11: PMX_WEB_CANVAS_PORT (server-specific) wins, then PMX_CANVAS_PORT — so
|
|
2941
|
+
// setting only the client var no longer splits the client and server ports.
|
|
2942
|
+
const preferredPort =
|
|
2943
|
+
options.port ?? Number(process.env.PMX_WEB_CANVAS_PORT ?? process.env.PMX_CANVAS_PORT ?? DEFAULT_PORT);
|
|
2858
2944
|
const portCandidates =
|
|
2859
2945
|
options.port === 0
|
|
2860
2946
|
? [0]
|
|
@@ -2883,8 +2969,15 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
2883
2969
|
if (url.pathname === '/health') {
|
|
2884
2970
|
// `pid` lets `serve status` report the ACTUAL serving process even
|
|
2885
2971
|
// when the pid file is stale (e.g. an adapter respawned the server
|
|
2886
|
-
// outside `serve --daemon` — 0.3.2 report Finding P).
|
|
2887
|
-
|
|
2972
|
+
// outside `serve --daemon` — 0.3.2 report Finding P). `persistence`
|
|
2973
|
+
// surfaces silent save failures (M4): the canvas keeps serving from
|
|
2974
|
+
// memory, but ok:false means durability is degraded.
|
|
2975
|
+
return responseJson({
|
|
2976
|
+
ok: true,
|
|
2977
|
+
workspace: activeWorkspaceRoot,
|
|
2978
|
+
pid: process.pid,
|
|
2979
|
+
persistence: canvasState.persistenceHealth,
|
|
2980
|
+
});
|
|
2888
2981
|
}
|
|
2889
2982
|
|
|
2890
2983
|
if (url.pathname === '/favicon.ico' || url.pathname === '/favicon.svg') {
|
|
@@ -2951,6 +3044,10 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
2951
3044
|
return handleWorkbenchEvents(req);
|
|
2952
3045
|
}
|
|
2953
3046
|
|
|
3047
|
+
if (url.pathname === '/api/workbench/poll' && req.method === 'GET') {
|
|
3048
|
+
return handleWorkbenchPoll(url);
|
|
3049
|
+
}
|
|
3050
|
+
|
|
2954
3051
|
if (url.pathname === '/api/workbench/intent' && req.method === 'POST') {
|
|
2955
3052
|
return handleWorkbenchIntent(req);
|
|
2956
3053
|
}
|
|
@@ -3015,6 +3112,7 @@ export function stopCanvasServer(): void {
|
|
|
3015
3112
|
intentRegistry.reset();
|
|
3016
3113
|
canvasState.close();
|
|
3017
3114
|
closeAllMcpAppSessions();
|
|
3115
|
+
cancelCodeGraphRecompute();
|
|
3018
3116
|
setCanvasLayoutUpdateEmitter(null);
|
|
3019
3117
|
void closeCanvasAutomationWebViewInternal().catch((error) => {
|
|
3020
3118
|
logWorkbenchWarning('stopCanvasServer closeCanvasAutomationWebViewInternal', error);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-protocol source tags for sandboxed-surface ↔ canvas postMessage traffic
|
|
3
|
+
* (plan-009 M2): one module owns the strings that used to be spread across the
|
|
4
|
+
* client node components, the server-side surface script generators, the
|
|
5
|
+
* ext-app script builders, and the json-render viewer. These are the trust
|
|
6
|
+
* boundary between opaque-origin iframes and the canvas — a typo'd tag fails
|
|
7
|
+
* SILENTLY (the listener just ignores the message), so single-siting them is a
|
|
8
|
+
* correctness measure, not cosmetics. The values are a persisted wire contract
|
|
9
|
+
* with already-generated surface documents: never change them.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A sandboxed surface emitting an AX interaction to the parent canvas. */
|
|
13
|
+
export const AX_SURFACE_EMIT_SOURCE = 'pmx-canvas-ax';
|
|
14
|
+
|
|
15
|
+
/** The parent canvas acking an AX interaction back to the emitting surface (#55). */
|
|
16
|
+
export const AX_SURFACE_ACK_SOURCE = 'pmx-canvas-ax-ack';
|
|
17
|
+
|
|
18
|
+
/** Parent→surface pushes (theme updates, live AX state) into html/viewer iframes. */
|
|
19
|
+
export const HTML_SURFACE_PUSH_SOURCE = 'pmx-canvas-html-node';
|
|
20
|
+
|
|
21
|
+
/** Ext-app boot beacon: the iframe's scripts executed (WebKit watchdog liveness). */
|
|
22
|
+
export const EXT_APP_BOOT_BEACON_SOURCE = 'pmx-canvas-ext-app-alive';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function ContextPinHud(): import("preact/src").JSX.Element | null;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { clearContextPins, contextPinnedNodeIds } from '../state/canvas-store';
|
|
2
|
-
|
|
3
|
-
export function ContextPinHud() {
|
|
4
|
-
const count = contextPinnedNodeIds.value.size;
|
|
5
|
-
if (count === 0) return null;
|
|
6
|
-
|
|
7
|
-
return (
|
|
8
|
-
<div class="context-pin-hud">
|
|
9
|
-
<span class="context-pin-hud-label">
|
|
10
|
-
{'\u2726'} {count} in context
|
|
11
|
-
</span>
|
|
12
|
-
<button type="button" class="context-pin-hud-clear" onClick={clearContextPins} title="Clear all context pins">
|
|
13
|
-
×
|
|
14
|
-
</button>
|
|
15
|
-
</div>
|
|
16
|
-
);
|
|
17
|
-
}
|