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
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 !==
|
|
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:
|
|
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),
|
package/src/server/canvas-db.ts
CHANGED
|
@@ -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
|
-
//
|
|
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),
|