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.
Files changed (87) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/Readme.md +13 -4
  3. package/dist/canvas/index.js +71 -71
  4. package/dist/json-render/index.js +47 -47
  5. package/dist/types/cli/agent.d.ts +13 -8
  6. package/dist/types/cli/commands/apps.d.ts +1 -0
  7. package/dist/types/cli/commands/ax.d.ts +1 -0
  8. package/dist/types/cli/commands/copilot.d.ts +1 -0
  9. package/dist/types/cli/commands/edges.d.ts +1 -0
  10. package/dist/types/cli/commands/groups.d.ts +1 -0
  11. package/dist/types/cli/commands/history.d.ts +1 -0
  12. package/dist/types/cli/commands/nodes.d.ts +1 -0
  13. package/dist/types/cli/commands/pins.d.ts +1 -0
  14. package/dist/types/cli/commands/query.d.ts +1 -0
  15. package/dist/types/cli/commands/view.d.ts +1 -0
  16. package/dist/types/cli/commands/webview.d.ts +1 -0
  17. package/dist/types/cli/shared.d.ts +73 -0
  18. package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -1
  19. package/dist/types/client/index.d.ts +1 -1
  20. package/dist/types/client/nodes/ExtAppFrame.d.ts +10 -0
  21. package/dist/types/client/nodes/HtmlNode.d.ts +1 -1
  22. package/dist/types/client/nodes/McpAppNode.d.ts +1 -1
  23. package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +20 -0
  24. package/dist/types/client/state/node-factory.d.ts +29 -0
  25. package/dist/types/client/uuid-polyfill.d.ts +0 -0
  26. package/dist/types/server/canvas-db.d.ts +0 -1
  27. package/dist/types/server/canvas-operations.d.ts +2 -0
  28. package/dist/types/server/canvas-state.d.ts +14 -15
  29. package/dist/types/server/diagram-presets.d.ts +8 -0
  30. package/dist/types/server/operations/composites.d.ts +6 -30
  31. package/dist/types/server/server.d.ts +1 -0
  32. package/dist/types/shared/ax-surface-protocol.d.ts +18 -0
  33. package/docs/bun-webview-integration.md +5 -0
  34. package/docs/cli.md +5 -0
  35. package/docs/environment.md +48 -0
  36. package/docs/http-api.md +25 -1
  37. package/docs/mcp.md +21 -30
  38. package/docs/node-types.md +5 -0
  39. package/docs/plans/plan-009-tech-debt-backlog.md +34 -15
  40. package/docs/screenshot.png +0 -0
  41. package/docs/tech-debt-assessment-2026-06.md +5 -5
  42. package/package.json +13 -10
  43. package/skills/pmx-canvas/SKILL.md +16 -6
  44. package/skills/pmx-canvas/evals/evals.json +7 -7
  45. package/skills/pmx-canvas/references/full-reference.md +25 -16
  46. package/src/cli/agent.ts +22 -3512
  47. package/src/cli/commands/apps.ts +165 -0
  48. package/src/cli/commands/ax.ts +604 -0
  49. package/src/cli/commands/copilot.ts +43 -0
  50. package/src/cli/commands/edges.ts +73 -0
  51. package/src/cli/commands/groups.ts +81 -0
  52. package/src/cli/commands/history.ts +220 -0
  53. package/src/cli/commands/nodes.ts +984 -0
  54. package/src/cli/commands/pins.ts +42 -0
  55. package/src/cli/commands/query.ts +223 -0
  56. package/src/cli/commands/view.ts +186 -0
  57. package/src/cli/commands/webview.ts +227 -0
  58. package/src/cli/index.ts +8 -2
  59. package/src/cli/shared.ts +861 -0
  60. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -2
  61. package/src/client/index.tsx +2 -0
  62. package/src/client/nodes/ExtAppFrame.tsx +70 -51
  63. package/src/client/nodes/HtmlNode.tsx +8 -51
  64. package/src/client/nodes/McpAppNode.tsx +12 -47
  65. package/src/client/nodes/use-ax-surface-bridge.ts +71 -0
  66. package/src/client/state/node-factory.ts +72 -0
  67. package/src/client/state/sse-bridge.ts +209 -86
  68. package/src/client/uuid-polyfill.ts +14 -0
  69. package/src/json-render/renderer/index.tsx +3 -2
  70. package/src/mcp/server.ts +0 -47
  71. package/src/server/canvas-db.ts +0 -8
  72. package/src/server/canvas-operations.ts +8 -0
  73. package/src/server/canvas-schema.ts +2 -8
  74. package/src/server/canvas-state.ts +51 -151
  75. package/src/server/diagram-presets.ts +15 -1
  76. package/src/server/html-surface.ts +11 -6
  77. package/src/server/index.ts +3 -9
  78. package/src/server/operations/composites.ts +25 -34
  79. package/src/server/operations/invoker.ts +10 -3
  80. package/src/server/operations/mcp.ts +51 -1
  81. package/src/server/operations/ops/app.ts +5 -0
  82. package/src/server/operations/ops/snapshots.ts +6 -7
  83. package/src/server/operations/registry.ts +4 -1
  84. package/src/server/server.ts +172 -74
  85. package/src/shared/ax-surface-protocol.ts +22 -0
  86. package/dist/types/client/canvas/ContextPinHud.d.ts +0 -1
  87. 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
- 'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). 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.',
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
- 'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Save the current canvas state as a named snapshot. Snapshots persist to disk and can be restored later.',
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
- 'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Delete old saved canvas snapshots, keeping the newest N snapshots. Use dryRun=true to preview deletions.',
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
- 'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Compare the current canvas state against a saved snapshot. Shows added/removed/modified nodes and edges. Pass either a snapshot name or ID.',
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
- 'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Restore the canvas to a previously saved snapshot. Use canvas_snapshot to save first. Pass either the snapshot ID or name to restore.',
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 emit suppression (mirrors canvasState._suppressRecordingDepth).
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
@@ -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 new Response(text, {
1376
- status,
1377
- headers: {
1378
- 'Content-Type': 'text/plain; charset=utf-8',
1379
- 'Cache-Control': 'no-store',
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
- function handleWorkbenchEvents(req: Request): Response {
1695
- const reqUrl = new URL(req.url);
1696
- const requestedSessionId = reqUrl.searchParams.get('session')?.trim() ?? '';
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
- controller.enqueue(
1711
- toSseFrame('connected', {
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
- const preferredPort = options.port ?? Number(process.env.PMX_WEB_CANVAS_PORT ?? DEFAULT_PORT);
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
- return responseJson({ ok: true, workspace: activeWorkspaceRoot, pid: process.pid });
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
- }