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
@@ -22,10 +22,10 @@ import {
22
22
  restoreLayout,
23
23
  sessionId,
24
24
  traceEnabled,
25
- updateNode,
26
25
  updateNodeData,
27
26
  } from './canvas-store';
28
27
  import { fetchAxSurfaceState } from './intent-bridge';
28
+ import { DEFAULT_POSITIONS, makeNodeState } from './node-factory';
29
29
  import { invalidateTokenCache } from '../theme/tokens';
30
30
  import { resetAttentionBridge, syncAttentionFromSse } from './attention-bridge';
31
31
  import { dissolveIntent, resetIntents, settleIntent, upsertIntent } from './intent-store';
@@ -36,10 +36,36 @@ let savedLayout: Map<string, Partial<CanvasNodeState>> | null = null;
36
36
  let reconnectAttempts = 0;
37
37
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
38
38
 
39
+ // ── Proxy-safe polling transport ──────────────────────────────
40
+ // Some proxies (e.g. the Amp orb portal) buffer streaming responses and only
41
+ // flush them on close, so the SSE stream delivers NOTHING — the connection
42
+ // "opens" (headers pass through) but no event ever arrives. Fallback: if the
43
+ // stream produces no event at all within the watchdog window, switch to
44
+ // polling GET /api/workbench/poll, which returns buffered events (or a full
45
+ // connect snapshot) as a normal response every couple of seconds. The same
46
+ // EVENT_HANDLERS map serves both transports. `?transport=poll` forces polling
47
+ // from the start; `?transport=sse` disables the fallback.
48
+ const SSE_FIRST_EVENT_WATCHDOG_MS = 3000;
49
+ const POLL_INTERVAL_MS = 2000;
50
+ const POLL_ERROR_INTERVAL_MS = 5000;
51
+ let pollingMode = false;
52
+ let pollTimer: ReturnType<typeof setTimeout> | null = null;
53
+ let pollSeq: number | null = null;
54
+ let pollGeneration = 0;
55
+ let sseFirstEventWatchdog: ReturnType<typeof setTimeout> | null = null;
56
+
57
+ function forcedTransport(): 'poll' | 'sse' | null {
58
+ if (typeof location === 'undefined') return null;
59
+ const value = new URLSearchParams(location.search).get('transport');
60
+ return value === 'poll' || value === 'sse' ? value : null;
61
+ }
62
+
39
63
  // Maps responseNodeId → thread prompt node ID so response deltas/completions
40
64
  // are routed into the thread's turns array instead of creating separate nodes.
41
- // Entries are added on response-start and removed on response-complete.
42
- // Not cleaned on SSE reconnect — orphaned entries are benign (small, bounded by active streams).
65
+ // Entries are added on response-start and removed on response-complete. Streams
66
+ // still in flight when the connection drops would otherwise leak entries, so
67
+ // connectSSE() — the shared entry path for both transports — clears the map
68
+ // alongside the rest of the per-connection state.
43
69
  const responseToThreadMap = new Map<string, string>();
44
70
 
45
71
  // ── Helpers ───────────────────────────────────────────────────
@@ -71,44 +97,15 @@ function applyLayoutOverrides(node: CanvasNodeState): CanvasNodeState {
71
97
  };
72
98
  }
73
99
 
74
- // ── Default positions by type ─────────────────────────────────
75
- const DEFAULT_POSITIONS: Record<CanvasNodeState['type'], { x: number; y: number; w: number; h: number }> &
76
- Record<'prompt' | 'response', { x: number; y: number; w: number; h: number }> = {
77
- status: { x: 40, y: 80, w: 300, h: 120 },
78
- markdown: { x: 380, y: 80, w: 720, h: 600 },
79
- context: { x: 1130, y: 80, w: 320, h: 400 },
80
- 'mcp-app': { x: 380, y: 720, w: 960, h: 600 },
81
- webpage: { x: 380, y: 80, w: 520, h: 420 },
82
- 'json-render': { x: 380, y: 720, w: 840, h: 620 },
83
- graph: { x: 380, y: 720, w: 760, h: 520 },
84
- ledger: { x: 1130, y: 520, w: 320, h: 280 },
85
- trace: { x: 40, y: 900, w: 200, h: 56 },
86
- file: { x: 380, y: 80, w: 720, h: 600 },
87
- image: { x: 380, y: 80, w: 720, h: 520 },
88
- html: { x: 380, y: 80, w: 720, h: 640 },
89
- group: { x: 220, y: 60, w: 840, h: 560 },
90
- prompt: { x: 380, y: 1260, w: 520, h: 400 },
91
- response: { x: 380, y: 1480, w: 720, h: 400 },
92
- };
93
-
100
+ // Default geometry lives in the shared node factory (node-factory.ts); this
101
+ // wrapper just layers the connection-local saved-layout overrides on top.
94
102
  function makeNode(
95
103
  id: string,
96
104
  type: CanvasNodeState['type'],
97
105
  data: Record<string, unknown>,
98
106
  dockPosition: 'left' | 'right' | null = null,
99
107
  ): CanvasNodeState {
100
- const pos = DEFAULT_POSITIONS[type];
101
- return applyLayoutOverrides({
102
- id,
103
- type,
104
- position: { x: pos.x, y: pos.y },
105
- size: { width: pos.w, height: pos.h },
106
- zIndex: type === 'status' ? 0 : 1,
107
- collapsed: false,
108
- pinned: false,
109
- dockPosition,
110
- data,
111
- });
108
+ return applyLayoutOverrides(makeNodeState(id, type, data, { dockPosition }));
112
109
  }
113
110
 
114
111
  function getMarkdownPlacement(): { x: number; y: number } {
@@ -205,26 +202,20 @@ function ensureExtAppNode(data: Record<string, unknown>): void {
205
202
  customX === undefined || customY === undefined
206
203
  ? findOpenCanvasPosition([...nodes.value.values()], width, height)
207
204
  : null;
208
- const node = applyLayoutOverrides({
209
- id,
210
- type: 'mcp-app' as const,
211
- position: {
212
- x: customX ?? autoPos?.x ?? pos.x,
213
- y: customY ?? autoPos?.y ?? pos.y,
214
- },
215
- size: {
216
- width,
217
- height,
218
- },
219
- zIndex: 1,
220
- collapsed: false,
221
- pinned: false,
222
- dockPosition: null,
223
- data: {
224
- mode: 'ext-app',
225
- ...data,
226
- },
227
- });
205
+ const node = applyLayoutOverrides(
206
+ makeNodeState(
207
+ id,
208
+ 'mcp-app',
209
+ { mode: 'ext-app', ...data },
210
+ {
211
+ position: {
212
+ x: customX ?? autoPos?.x ?? pos.x,
213
+ y: customY ?? autoPos?.y ?? pos.y,
214
+ },
215
+ size: { width, height },
216
+ },
217
+ ),
218
+ );
228
219
  addNode(node);
229
220
  if (!node.dockPosition) {
230
221
  focusNode(id, { recordHistory: false });
@@ -637,24 +628,21 @@ function handleCanvasPromptCreated(data: Record<string, unknown>): void {
637
628
  if (!nodes.value.has(nodeId)) {
638
629
  const pos = position ?? DEFAULT_POSITIONS.prompt;
639
630
  addNode(
640
- applyLayoutOverrides({
641
- id: nodeId,
642
- type: 'prompt' as const,
643
- position: { x: pos.x, y: pos.y },
644
- size: { width: DEFAULT_POSITIONS.prompt.w, height: 400 },
645
- zIndex: 1,
646
- collapsed: false,
647
- pinned: false,
648
- dockPosition: null,
649
- data: {
650
- text,
651
- turns: text ? [{ role: 'user', text, status: 'pending' }] : [],
652
- threadStatus: text ? 'pending' : 'draft',
653
- status: text ? 'pending' : 'draft',
654
- parentNodeId,
655
- contextNodeIds,
656
- },
657
- }),
631
+ applyLayoutOverrides(
632
+ makeNodeState(
633
+ nodeId,
634
+ 'prompt',
635
+ {
636
+ text,
637
+ turns: text ? [{ role: 'user', text, status: 'pending' }] : [],
638
+ threadStatus: text ? 'pending' : 'draft',
639
+ status: text ? 'pending' : 'draft',
640
+ parentNodeId,
641
+ contextNodeIds,
642
+ },
643
+ { position: { x: pos.x, y: pos.y } },
644
+ ),
645
+ ),
658
646
  );
659
647
  focusNode(nodeId);
660
648
  }
@@ -706,17 +694,14 @@ function handleCanvasResponseStart(data: Record<string, unknown>): void {
706
694
 
707
695
  if (!nodes.value.has(responseNodeId)) {
708
696
  addNode(
709
- applyLayoutOverrides({
710
- id: responseNodeId,
711
- type: 'response' as const,
712
- position: pos,
713
- size: { width: DEFAULT_POSITIONS.response.w, height: DEFAULT_POSITIONS.response.h },
714
- zIndex: 1,
715
- collapsed: false,
716
- pinned: false,
717
- dockPosition: null,
718
- data: { content: '', status: 'streaming', promptNodeId },
719
- }),
697
+ applyLayoutOverrides(
698
+ makeNodeState(
699
+ responseNodeId,
700
+ 'response',
701
+ { content: '', status: 'streaming', promptNodeId },
702
+ { position: pos },
703
+ ),
704
+ ),
720
705
  );
721
706
  }
722
707
 
@@ -991,17 +976,124 @@ export const EVENT_HANDLERS: Record<string, (data: Record<string, unknown>) => v
991
976
  'ax-intent-clear': handleAxIntentClear,
992
977
  };
993
978
 
979
+ /** Dispatch one event (from either transport) through the shared handler map. */
980
+ function dispatchWorkbenchEvent(event: string, payload: unknown): void {
981
+ const handler = EVENT_HANDLERS[event as keyof typeof EVENT_HANDLERS];
982
+ if (!handler) return;
983
+ // The map's handlers each declare their own payload type; over the wire the
984
+ // payload is untyped JSON either way (SSE previously passed JSON.parse output
985
+ // straight in). The double cast states that honestly without `any`.
986
+ const invoke = handler as unknown as (value: unknown) => void;
987
+ try {
988
+ invoke(payload);
989
+ } catch (err) {
990
+ console.warn(`[sse-bridge] Handler for "${event}" failed:`, err);
991
+ }
992
+ }
993
+
994
+ /**
995
+ * Polling transport loop. Each response is short-lived, so it flushes through
996
+ * any buffering proxy. Snapshot responses (fresh client / server restart /
997
+ * ring eviction) carry the same event list an SSE connect sends, so replaying
998
+ * them through the handlers matches existing reconnect semantics.
999
+ */
1000
+ function startPollingTransport(): () => void {
1001
+ pollingMode = true;
1002
+ pollGeneration += 1;
1003
+ const generation = pollGeneration;
1004
+ connectionStatus.value = 'connecting';
1005
+ // Server-run identity from the last poll response. The event seq counter
1006
+ // restarts at 0 on every server boot, so a cursor from a previous run can
1007
+ // look "usable" to a new run and silently skip its earlier events.
1008
+ let lastPollSessionId: string | null = null;
1009
+ let dispatchedOnce = false;
1010
+
1011
+ const pollOnce = async (): Promise<void> => {
1012
+ if (generation !== pollGeneration) return;
1013
+ try {
1014
+ const params = new URLSearchParams();
1015
+ if (pollSeq !== null) params.set('since', String(pollSeq));
1016
+ const sid = sessionId.value;
1017
+ if (sid) params.set('session', sid);
1018
+ const query = params.toString();
1019
+ const response = await fetch(`/api/workbench/poll${query ? `?${query}` : ''}`);
1020
+ if (generation !== pollGeneration) return;
1021
+ if (!response.ok) throw new Error(`poll failed with HTTP ${response.status}`);
1022
+ const body = (await response.json()) as {
1023
+ ok: boolean;
1024
+ seq: number;
1025
+ snapshot: boolean;
1026
+ sessionId?: string;
1027
+ events: Array<{ event: string; payload: unknown }>;
1028
+ };
1029
+ if (generation !== pollGeneration) return;
1030
+ if (!body.ok) throw new Error('poll returned ok: false');
1031
+ const priorSession = lastPollSessionId;
1032
+ lastPollSessionId = body.sessionId ?? null;
1033
+ if (priorSession !== null && body.sessionId && priorSession !== body.sessionId && !body.snapshot) {
1034
+ // The server identity changed under our cursor (restart): a non-snapshot
1035
+ // batch may have silently skipped the new run's earlier events. Discard
1036
+ // it, drop the cursor, and re-poll immediately for a full snapshot.
1037
+ pollSeq = null;
1038
+ pollTimer = setTimeout(() => void pollOnce(), 0);
1039
+ return;
1040
+ }
1041
+ if (body.snapshot && dispatchedOnce) {
1042
+ // A mid-polling snapshot (ring eviction or server restart) replaces
1043
+ // client state wholesale — run the same per-connection resets an SSE
1044
+ // reconnect performs so in-flight stream routes and transient state
1045
+ // from the previous window can't leak (H5 orphan class).
1046
+ savedLayout = restoreLayout();
1047
+ ensureStatusNode();
1048
+ hasInitialServerLayout.value = false;
1049
+ resetAttentionBridge();
1050
+ resetIntents();
1051
+ responseToThreadMap.clear();
1052
+ }
1053
+ for (const entry of body.events) dispatchWorkbenchEvent(entry.event, entry.payload);
1054
+ dispatchedOnce = true;
1055
+ pollSeq = body.seq;
1056
+ connectionStatus.value = 'connected';
1057
+ pollTimer = setTimeout(() => void pollOnce(), POLL_INTERVAL_MS);
1058
+ } catch (err) {
1059
+ if (generation !== pollGeneration) return;
1060
+ console.warn('[sse-bridge] poll transport error:', err);
1061
+ connectionStatus.value = 'disconnected';
1062
+ pollTimer = setTimeout(() => void pollOnce(), POLL_ERROR_INTERVAL_MS);
1063
+ }
1064
+ };
1065
+ void pollOnce();
1066
+
1067
+ return () => {
1068
+ pollGeneration += 1;
1069
+ pollingMode = false;
1070
+ pollSeq = null;
1071
+ if (pollTimer) {
1072
+ clearTimeout(pollTimer);
1073
+ pollTimer = null;
1074
+ }
1075
+ };
1076
+ }
1077
+
994
1078
  export function connectSSE(): () => void {
995
1079
  savedLayout = restoreLayout();
996
1080
  ensureStatusNode();
997
1081
  hasInitialServerLayout.value = false;
998
1082
  resetAttentionBridge();
999
1083
  resetIntents();
1084
+ // Response→thread routes belong to the dropped connection's in-flight
1085
+ // streams; clear them so orphans can't accumulate across reconnect cycles.
1086
+ responseToThreadMap.clear();
1000
1087
  if (reconnectTimer) {
1001
1088
  clearTimeout(reconnectTimer);
1002
1089
  reconnectTimer = null;
1003
1090
  }
1004
1091
 
1092
+ const transport = forcedTransport();
1093
+ if (transport === 'poll' || pollingMode) {
1094
+ return startPollingTransport();
1095
+ }
1096
+
1005
1097
  const sid = sessionId.value;
1006
1098
  const url = sid ? `/api/workbench/events?session=${sid}` : '/api/workbench/events';
1007
1099
  connectionStatus.value = 'connecting';
@@ -1009,10 +1101,28 @@ export function connectSSE(): () => void {
1009
1101
  const source = new EventSource(url);
1010
1102
  eventSource = source;
1011
1103
 
1012
- for (const [event, handler] of Object.entries(EVENT_HANDLERS)) {
1104
+ // Proxy detection: a buffered stream "opens" (headers pass through) but never
1105
+ // delivers a single event — not even the immediate `connected` frame. Key the
1106
+ // watchdog on the FIRST EVENT, not onopen.
1107
+ let gotFirstEvent = false;
1108
+ if (sseFirstEventWatchdog) clearTimeout(sseFirstEventWatchdog);
1109
+ sseFirstEventWatchdog = null;
1110
+ if (transport !== 'sse') {
1111
+ sseFirstEventWatchdog = setTimeout(() => {
1112
+ sseFirstEventWatchdog = null;
1113
+ if (gotFirstEvent || eventSource !== source) return;
1114
+ console.warn('[sse-bridge] No SSE event within watchdog window — switching to polling transport.');
1115
+ source.close();
1116
+ eventSource = null;
1117
+ startPollingTransport();
1118
+ }, SSE_FIRST_EVENT_WATCHDOG_MS);
1119
+ }
1120
+
1121
+ for (const [event] of Object.entries(EVENT_HANDLERS)) {
1013
1122
  source.addEventListener(event, (e) => {
1123
+ gotFirstEvent = true;
1014
1124
  try {
1015
- handler(JSON.parse((e as MessageEvent).data));
1125
+ dispatchWorkbenchEvent(event, JSON.parse((e as MessageEvent).data));
1016
1126
  } catch (err) {
1017
1127
  // H5: Surface malformed SSE data during debugging instead of silently swallowing
1018
1128
  console.warn(`[sse-bridge] Failed to parse "${event}" event:`, err);
@@ -1043,6 +1153,19 @@ export function connectSSE(): () => void {
1043
1153
  clearTimeout(reconnectTimer);
1044
1154
  reconnectTimer = null;
1045
1155
  }
1156
+ if (sseFirstEventWatchdog) {
1157
+ clearTimeout(sseFirstEventWatchdog);
1158
+ sseFirstEventWatchdog = null;
1159
+ }
1160
+ if (pollingMode) {
1161
+ pollGeneration += 1;
1162
+ pollingMode = false;
1163
+ pollSeq = null;
1164
+ if (pollTimer) {
1165
+ clearTimeout(pollTimer);
1166
+ pollTimer = null;
1167
+ }
1168
+ }
1046
1169
  source.close();
1047
1170
  eventSource = null;
1048
1171
  };
@@ -0,0 +1,14 @@
1
+ // crypto.randomUUID is only defined in SECURE CONTEXTS. Embedded hosts that
2
+ // serve the canvas over plain http inside an iframe (portal/proxy setups) get
3
+ // `crypto` without `randomUUID`, which would throw on every per-mount nonce
4
+ // (AX tokens, frame tokens, session ids). Polyfill with the standard v4
5
+ // fallback; a strict no-op wherever the native implementation exists. These
6
+ // ids are nonces/correlation ids, not cryptographic secrets, so Math.random
7
+ // quality is acceptable for the fallback path.
8
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
9
+ (crypto as { randomUUID?: () => string }).randomUUID = () =>
10
+ 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
11
+ const r = (Math.random() * 16) | 0;
12
+ return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
13
+ });
14
+ }
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { Spec } from '@json-render/core';
11
+ import { AX_SURFACE_EMIT_SOURCE, HTML_SURFACE_PUSH_SOURCE } from '../../shared/ax-surface-protocol.js';
11
12
  import { useEffect } from 'react';
12
13
  import { createRoot } from 'react-dom/client';
13
14
  import { defineRegistry, JSONUIProvider, Renderer, useStateBinding } from '@json-render/react';
@@ -101,7 +102,7 @@ function AxStateSync() {
101
102
  if (!token) return undefined;
102
103
  function onMessage(event: MessageEvent) {
103
104
  const m = event.data as { source?: string; type?: string; token?: string; state?: unknown } | null;
104
- if (!m || m.source !== 'pmx-canvas-html-node' || m.type !== 'ax-update' || m.token !== token) return;
105
+ if (!m || m.source !== HTML_SURFACE_PUSH_SOURCE || m.type !== 'ax-update' || m.token !== token) return;
105
106
  window.__PMX_CANVAS_AX_STATE__ = m.state;
106
107
  setAx(m.state);
107
108
  }
@@ -141,7 +142,7 @@ function buildAxHandlers(): Record<string, (params: Record<string, unknown>) =>
141
142
  handlers[type] = (params: Record<string, unknown>) => {
142
143
  window.parent.postMessage(
143
144
  {
144
- source: 'pmx-canvas-ax',
145
+ source: AX_SURFACE_EMIT_SOURCE,
145
146
  token,
146
147
  nodeId,
147
148
  interaction: { type, payload: params && typeof params === 'object' ? params : {} },
package/src/mcp/server.ts CHANGED
@@ -175,53 +175,6 @@ function encodeBase64(bytes: Uint8Array): string {
175
175
  return Buffer.from(bytes).toString('base64');
176
176
  }
177
177
 
178
- function wantsFullPayload(input: { full?: boolean; verbose?: boolean; includeData?: boolean } = {}): boolean {
179
- return input.full === true || input.verbose === true || input.includeData === true;
180
- }
181
-
182
- function compactNodePayload(node: Awaited<ReturnType<CanvasAccess['getNode']>>): Record<string, unknown> | null {
183
- if (!node) return null;
184
- const serialized = serializeCanvasNode(node);
185
- return {
186
- id: serialized.id,
187
- type: serialized.type,
188
- kind: serialized.kind,
189
- title: serialized.title,
190
- content: serialized.content,
191
- position: serialized.position,
192
- size: serialized.size,
193
- pinned: serialized.pinned,
194
- collapsed: serialized.collapsed,
195
- dockPosition: serialized.dockPosition,
196
- provenance: serialized.provenance,
197
- };
198
- }
199
-
200
- function compactLayoutPayload(
201
- layout: Awaited<ReturnType<CanvasAccess['getLayout']>>,
202
- pinnedIds: string[],
203
- ): Record<string, unknown> {
204
- return {
205
- summary: buildSummaryFromLayout(layout, pinnedIds),
206
- viewport: layout.viewport,
207
- annotations: (layout.annotations ?? []).map((annotation) =>
208
- summarizeCanvasAnnotationForContext(annotation, layout.nodes),
209
- ),
210
- nodes: layout.nodes
211
- .map((node) => compactNodePayload(node))
212
- .filter((node): node is Record<string, unknown> => node !== null),
213
- edges: layout.edges.map((edge) => ({
214
- id: edge.id,
215
- from: edge.from,
216
- to: edge.to,
217
- type: edge.type,
218
- ...(edge.label ? { label: edge.label } : {}),
219
- ...(edge.style ? { style: edge.style } : {}),
220
- ...(edge.animated !== undefined ? { animated: edge.animated } : {}),
221
- })),
222
- };
223
- }
224
-
225
178
  function agentSafeFullLayoutPayload(layout: Awaited<ReturnType<CanvasAccess['getLayout']>>): Record<string, unknown> {
226
179
  return {
227
180
  ...serializeCanvasLayoutForAgent(layout),
@@ -823,14 +823,6 @@ export function readBlobFromDB(db: Database, sha256: string): string | null {
823
823
  return gunzipSync(row.data).toString('utf-8');
824
824
  }
825
825
 
826
- export function hasBlobInDB(db: Database, sha256: string): boolean {
827
- interface CountRow {
828
- c: number;
829
- }
830
- const row = db.query<CountRow, [string]>('SELECT COUNT(*) as c FROM blobs WHERE sha256 = ?').get(sha256);
831
- return (row?.c ?? 0) > 0;
832
- }
833
-
834
826
  // ── AX Timeline Persistence (NOT snapshotted; bounded by retention) ──
835
827
 
836
828
  function safeParseJson(raw: string | null | undefined): unknown {
@@ -856,6 +856,14 @@ export function scheduleCodeGraphRecompute(onComplete?: () => void): void {
856
856
  }, 300);
857
857
  }
858
858
 
859
+ /** Clear a pending recompute so the timer cannot outlive its server (start/stop cycles, tests). */
860
+ export function cancelCodeGraphRecompute(): void {
861
+ if (codeGraphTimer) {
862
+ clearTimeout(codeGraphTimer);
863
+ codeGraphTimer = null;
864
+ }
865
+ }
866
+
859
867
  /**
860
868
  * Resolve an html-node `html` field that may be a path to a local .html/.htm file.
861
869
  *
@@ -937,7 +937,7 @@ export function describeCanvasSchema(): {
937
937
  htmlPrimitives: listHtmlPrimitiveDescriptors(),
938
938
  mcp: {
939
939
  tools: [
940
- // 15 composites
940
+ // 16 composites (canvas_snapshot folded the legacy snapshot standalones in v0.4.0)
941
941
  'canvas_node',
942
942
  'canvas_render',
943
943
  'canvas_edge',
@@ -947,6 +947,7 @@ export function describeCanvasSchema(): {
947
947
  'canvas_query',
948
948
  'canvas_webview',
949
949
  'canvas_app',
950
+ 'canvas_snapshot',
950
951
  'canvas_ax_state',
951
952
  'canvas_ax_work',
952
953
  'canvas_ax_gate',
@@ -960,13 +961,6 @@ export function describeCanvasSchema(): {
960
961
  'canvas_ax_interaction',
961
962
  'canvas_ingest_activity',
962
963
  'canvas_screenshot',
963
- // 6 snapshot tools (deprecated; fold into canvas_snapshot composite in v0.4)
964
- 'canvas_snapshot',
965
- 'canvas_list_snapshots',
966
- 'canvas_restore',
967
- 'canvas_delete_snapshot',
968
- 'canvas_gc_snapshots',
969
- 'canvas_diff',
970
964
  ],
971
965
  resources: ['canvas://schema'],
972
966
  nodeTypeRouting: buildMcpNodeTypeRouting(nodeTypes),