pmx-canvas 0.2.7 → 0.3.1
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 +148 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +0 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +35 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +5 -2
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +12 -7
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +58 -56
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +40 -121
- package/src/mcp/server.ts +206 -271
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +185 -41
- package/src/server/ax-state.ts +142 -47
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +500 -102
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +155 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +20 -7
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +4 -20
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +321 -218
- package/src/server/intent-registry.ts +3 -6
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +38 -40
- package/src/server/operations/http.ts +5 -5
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +29 -36
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +102 -77
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +300 -111
- package/src/server/operations/ops/query.ts +62 -33
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +121 -1012
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/ax-intent.ts +1 -1
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canvasState } from './canvas-state.js';
|
|
3
3
|
import type { CanvasAnnotation, CanvasLayout, CanvasNodeState, ViewportState } from './canvas-state.js';
|
|
4
|
-
import {
|
|
5
|
-
normalizeCanvasNodeData,
|
|
6
|
-
type CanvasNodeProvenance,
|
|
7
|
-
} from './canvas-provenance.js';
|
|
4
|
+
import { normalizeCanvasNodeData, type CanvasNodeProvenance } from './canvas-provenance.js';
|
|
8
5
|
import { getCanvasNodeKind as getSharedCanvasNodeKind } from '../shared/canvas-node-kind.js';
|
|
9
6
|
import { canOpenNodeAsSurface } from '../shared/surface.js';
|
|
10
7
|
|
|
@@ -79,34 +76,32 @@ export function getCanvasNodeKind(node: CanvasNodeState, data: Record<string, un
|
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
export function getCanvasNodeTitle(node: CanvasNodeState): string | null {
|
|
82
|
-
return pickString(node.data.title)
|
|
83
|
-
?? (node.type === 'webpage' ? pickString(node.data.pageTitle) : null)
|
|
84
|
-
?? null;
|
|
79
|
+
return pickString(node.data.title) ?? (node.type === 'webpage' ? pickString(node.data.pageTitle) : null) ?? null;
|
|
85
80
|
}
|
|
86
81
|
|
|
87
82
|
export function getCanvasNodeContent(node: CanvasNodeState): string | null {
|
|
88
83
|
if (node.type === 'html') {
|
|
89
84
|
const primitive = typeof node.data.htmlPrimitive === 'string' ? node.data.htmlPrimitive : null;
|
|
90
85
|
const description = pickString(node.data.description);
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
??
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
return (
|
|
87
|
+
pickString(node.data.agentSummary) ??
|
|
88
|
+
pickString(node.data.contentSummary) ??
|
|
89
|
+
(primitive ? (description ? `${primitive}: ${description}` : primitive) : null)
|
|
90
|
+
);
|
|
96
91
|
}
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
92
|
+
return (
|
|
93
|
+
pickString(node.data.content) ??
|
|
94
|
+
pickString(node.data.fileContent) ??
|
|
95
|
+
pickString(node.data.text) ??
|
|
96
|
+
(node.type === 'file' ? pickString(node.data.path) : null) ??
|
|
97
|
+
(node.type === 'image' ? pickString(node.data.src) : null) ??
|
|
98
|
+
(node.type === 'webpage' ? pickString(node.data.url) : null) ??
|
|
99
|
+
null
|
|
100
|
+
);
|
|
104
101
|
}
|
|
105
102
|
|
|
106
103
|
export function getCanvasNodeSurfaceUrl(node: CanvasNodeState, data: Record<string, unknown>): string | null {
|
|
107
|
-
return canOpenNodeAsSurface(node.type, data)
|
|
108
|
-
? `/api/canvas/surface/${encodeURIComponent(node.id)}`
|
|
109
|
-
: null;
|
|
104
|
+
return canOpenNodeAsSurface(node.type, data) ? `/api/canvas/surface/${encodeURIComponent(node.id)}` : null;
|
|
110
105
|
}
|
|
111
106
|
|
|
112
107
|
export function serializeCanvasNode(node: CanvasNodeState): SerializedCanvasNode {
|
|
@@ -236,22 +231,21 @@ function rectsOverlap(
|
|
|
236
231
|
a: { x: number; y: number; width: number; height: number },
|
|
237
232
|
b: { x: number; y: number; width: number; height: number },
|
|
238
233
|
): boolean {
|
|
239
|
-
return a.x <= b.x + b.width &&
|
|
240
|
-
a.x + a.width >= b.x &&
|
|
241
|
-
a.y <= b.y + b.height &&
|
|
242
|
-
a.y + a.height >= b.y;
|
|
234
|
+
return a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y;
|
|
243
235
|
}
|
|
244
236
|
|
|
245
237
|
export function summarizeCanvasAnnotationForContext(
|
|
246
238
|
annotation: CanvasAnnotation,
|
|
247
239
|
nodes: CanvasNodeState[],
|
|
248
240
|
): CanvasAnnotationContextSummary {
|
|
249
|
-
const targetNodes = nodes.filter((node) =>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
241
|
+
const targetNodes = nodes.filter((node) =>
|
|
242
|
+
rectsOverlap(annotation.bounds, {
|
|
243
|
+
x: node.position.x,
|
|
244
|
+
y: node.position.y,
|
|
245
|
+
width: node.size.width,
|
|
246
|
+
height: node.size.height,
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
255
249
|
const targetNodeTitles = targetNodes.map((node) => getCanvasNodeTitle(node) ?? node.id);
|
|
256
250
|
return {
|
|
257
251
|
id: annotation.id,
|
|
@@ -284,9 +278,7 @@ export function buildCanvasSummary(): CanvasSummary {
|
|
|
284
278
|
typeCounts[kind] = (typeCounts[kind] ?? 0) + 1;
|
|
285
279
|
}
|
|
286
280
|
|
|
287
|
-
const pinnedTitles = layout.nodes
|
|
288
|
-
.filter((n) => pinnedIds.has(n.id))
|
|
289
|
-
.map((n) => getCanvasNodeTitle(n) ?? n.id);
|
|
281
|
+
const pinnedTitles = layout.nodes.filter((n) => pinnedIds.has(n.id)).map((n) => getCanvasNodeTitle(n) ?? n.id);
|
|
290
282
|
|
|
291
283
|
return {
|
|
292
284
|
totalNodes: layout.nodes.length,
|
|
@@ -11,7 +11,16 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { createHash } from 'node:crypto';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
existsSync,
|
|
16
|
+
readFileSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
unlinkSync,
|
|
23
|
+
} from 'node:fs';
|
|
15
24
|
import { isAbsolute, join, dirname, relative } from 'node:path';
|
|
16
25
|
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
17
26
|
import { normalizeCanvasNodeData } from './canvas-provenance.js';
|
|
@@ -123,9 +132,15 @@ interface LoadFromDiskOptions {
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
export const IMAGE_MIME_MAP: Record<string, string> = {
|
|
126
|
-
png: 'image/png',
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
png: 'image/png',
|
|
136
|
+
jpg: 'image/jpeg',
|
|
137
|
+
jpeg: 'image/jpeg',
|
|
138
|
+
gif: 'image/gif',
|
|
139
|
+
svg: 'image/svg+xml',
|
|
140
|
+
webp: 'image/webp',
|
|
141
|
+
bmp: 'image/bmp',
|
|
142
|
+
ico: 'image/x-icon',
|
|
143
|
+
avif: 'image/avif',
|
|
129
144
|
};
|
|
130
145
|
|
|
131
146
|
export interface CanvasSnapshot {
|
|
@@ -235,7 +250,34 @@ export interface CanvasNodeUpdate {
|
|
|
235
250
|
export type CanvasChangeType = 'pins' | 'nodes' | 'ax' | 'ax-timeline';
|
|
236
251
|
|
|
237
252
|
export interface MutationRecordInfo {
|
|
238
|
-
operationType:
|
|
253
|
+
operationType:
|
|
254
|
+
| 'addNode'
|
|
255
|
+
| 'updateNode'
|
|
256
|
+
| 'removeNode'
|
|
257
|
+
| 'addEdge'
|
|
258
|
+
| 'removeEdge'
|
|
259
|
+
| 'addAnnotation'
|
|
260
|
+
| 'removeAnnotation'
|
|
261
|
+
| 'clear'
|
|
262
|
+
| 'restoreSnapshot'
|
|
263
|
+
| 'setPins'
|
|
264
|
+
| 'setAxFocus'
|
|
265
|
+
| 'addWorkItem'
|
|
266
|
+
| 'updateWorkItem'
|
|
267
|
+
| 'requestApproval'
|
|
268
|
+
| 'resolveApproval'
|
|
269
|
+
| 'addReviewAnnotation'
|
|
270
|
+
| 'updateReviewAnnotation'
|
|
271
|
+
| 'requestElicitation'
|
|
272
|
+
| 'respondElicitation'
|
|
273
|
+
| 'requestMode'
|
|
274
|
+
| 'resolveModeRequest'
|
|
275
|
+
| 'setPolicy'
|
|
276
|
+
| 'arrange'
|
|
277
|
+
| 'batch'
|
|
278
|
+
| 'groupNodes'
|
|
279
|
+
| 'ungroupNodes'
|
|
280
|
+
| 'viewport';
|
|
239
281
|
description: string;
|
|
240
282
|
forward: () => void;
|
|
241
283
|
inverse: () => void;
|
|
@@ -287,13 +329,15 @@ function replaceById<T extends { id: string }>(list: T[], item: T): T[] {
|
|
|
287
329
|
}
|
|
288
330
|
|
|
289
331
|
function isPersistedBlobRef(value: unknown): value is PersistedBlobRef {
|
|
290
|
-
return
|
|
332
|
+
return (
|
|
333
|
+
isRecord(value) &&
|
|
291
334
|
value.__pmxCanvasBlob === 'v1' &&
|
|
292
335
|
typeof value.path === 'string' &&
|
|
293
336
|
typeof value.sha256 === 'string' &&
|
|
294
337
|
value.encoding === 'json+gzip' &&
|
|
295
338
|
typeof value.bytes === 'number' &&
|
|
296
|
-
typeof value.jsonBytes === 'number'
|
|
339
|
+
typeof value.jsonBytes === 'number'
|
|
340
|
+
);
|
|
297
341
|
}
|
|
298
342
|
|
|
299
343
|
class CanvasStateManager {
|
|
@@ -360,7 +404,11 @@ class CanvasStateManager {
|
|
|
360
404
|
/** Run a function with mutation recording suppressed (for undo/redo replay and computed edges). */
|
|
361
405
|
withSuppressedRecording(fn: () => void): void {
|
|
362
406
|
this._suppressRecordingDepth++;
|
|
363
|
-
try {
|
|
407
|
+
try {
|
|
408
|
+
fn();
|
|
409
|
+
} finally {
|
|
410
|
+
this._suppressRecordingDepth--;
|
|
411
|
+
}
|
|
364
412
|
}
|
|
365
413
|
|
|
366
414
|
/** Create a closure that runs with recording suppressed. */
|
|
@@ -388,9 +436,8 @@ class CanvasStateManager {
|
|
|
388
436
|
bounds: { x: number; y: number; width: number; height: number },
|
|
389
437
|
existingGroups?: CanvasPlacementRect[],
|
|
390
438
|
): void {
|
|
391
|
-
const otherGroups =
|
|
392
|
-
(node) => node.id !== groupId && node.type === 'group'
|
|
393
|
-
);
|
|
439
|
+
const otherGroups =
|
|
440
|
+
existingGroups ?? Array.from(this.nodes.values()).filter((node) => node.id !== groupId && node.type === 'group');
|
|
394
441
|
const resolved = resolveGroupCollision(bounds, otherGroups);
|
|
395
442
|
const deltaX = resolved.x - bounds.x;
|
|
396
443
|
const deltaY = resolved.y - bounds.y;
|
|
@@ -472,7 +519,12 @@ class CanvasStateManager {
|
|
|
472
519
|
}
|
|
473
520
|
}
|
|
474
521
|
|
|
475
|
-
private translateGroupChildren(
|
|
522
|
+
private translateGroupChildren(
|
|
523
|
+
groupId: string,
|
|
524
|
+
deltaX: number,
|
|
525
|
+
deltaY: number,
|
|
526
|
+
skipIds: ReadonlySet<string> = new Set(),
|
|
527
|
+
): void {
|
|
476
528
|
if (deltaX === 0 && deltaY === 0) return;
|
|
477
529
|
const snapshot = this.getGroupSnapshot(groupId);
|
|
478
530
|
if (!snapshot) return;
|
|
@@ -509,9 +561,7 @@ class CanvasStateManager {
|
|
|
509
561
|
const snapshot = this.getGroupSnapshot(groupId);
|
|
510
562
|
if (!snapshot || snapshot.children.length === 0) return;
|
|
511
563
|
if (snapshot.group.data.frameMode === 'manual') {
|
|
512
|
-
const sorted = [...snapshot.children].sort(
|
|
513
|
-
(a, b) => a.position.y - b.position.y || a.position.x - b.position.x,
|
|
514
|
-
);
|
|
564
|
+
const sorted = [...snapshot.children].sort((a, b) => a.position.y - b.position.y || a.position.x - b.position.x);
|
|
515
565
|
const left = snapshot.group.position.x + GROUP_PAD;
|
|
516
566
|
const top = snapshot.group.position.y + GROUP_TITLEBAR_HEIGHT + GROUP_PAD;
|
|
517
567
|
const right = snapshot.group.position.x + snapshot.group.size.width - GROUP_PAD;
|
|
@@ -571,6 +621,11 @@ class CanvasStateManager {
|
|
|
571
621
|
private _db: import('bun:sqlite').Database | null = null;
|
|
572
622
|
private _saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
573
623
|
|
|
624
|
+
/** Workspace root backing persistence and workspace-relative path resolution. */
|
|
625
|
+
get workspaceRoot(): string {
|
|
626
|
+
return this._workspaceRoot;
|
|
627
|
+
}
|
|
628
|
+
|
|
574
629
|
/** Set the workspace root to enable auto-persistence. */
|
|
575
630
|
setWorkspaceRoot(workspaceRoot: string): void {
|
|
576
631
|
this.close();
|
|
@@ -590,9 +645,10 @@ class CanvasStateManager {
|
|
|
590
645
|
}
|
|
591
646
|
|
|
592
647
|
// Keep legacy _stateFilePath for JSON migration detection
|
|
593
|
-
this._stateFilePath =
|
|
594
|
-
|
|
595
|
-
|
|
648
|
+
this._stateFilePath =
|
|
649
|
+
stateFileOverride && !stateFileOverride.endsWith('.db')
|
|
650
|
+
? stateFileOverride
|
|
651
|
+
: join(workspaceRoot, PMX_CANVAS_DIR, STATE_FILENAME);
|
|
596
652
|
|
|
597
653
|
try {
|
|
598
654
|
this._db = openCanvasDb(dbPath);
|
|
@@ -744,9 +800,7 @@ class CanvasStateManager {
|
|
|
744
800
|
private externalizePersistedStateBlobs<T extends PersistedCanvasState>(state: T): T {
|
|
745
801
|
return {
|
|
746
802
|
...state,
|
|
747
|
-
nodes: Array.isArray(state.nodes)
|
|
748
|
-
? state.nodes.map((node) => this.externalizeNodeDataBlobs(node))
|
|
749
|
-
: [],
|
|
803
|
+
nodes: Array.isArray(state.nodes) ? state.nodes.map((node) => this.externalizeNodeDataBlobs(node)) : [],
|
|
750
804
|
};
|
|
751
805
|
}
|
|
752
806
|
|
|
@@ -1155,7 +1209,9 @@ class CanvasStateManager {
|
|
|
1155
1209
|
if (!dir || !existsSync(dir)) return [];
|
|
1156
1210
|
|
|
1157
1211
|
try {
|
|
1158
|
-
const files = readdirSync(dir)
|
|
1212
|
+
const files = readdirSync(dir)
|
|
1213
|
+
.filter((f) => f.endsWith('.json'))
|
|
1214
|
+
.sort();
|
|
1159
1215
|
const snapshots: CanvasSnapshot[] = [];
|
|
1160
1216
|
for (const file of files) {
|
|
1161
1217
|
try {
|
|
@@ -1227,7 +1283,9 @@ class CanvasStateManager {
|
|
|
1227
1283
|
viewport: structuredClone(resolved.state.viewport),
|
|
1228
1284
|
nodes: Array.isArray(resolved.state.nodes) ? resolved.state.nodes.map((node) => structuredClone(node)) : [],
|
|
1229
1285
|
edges: Array.isArray(resolved.state.edges) ? resolved.state.edges.map((edge) => structuredClone(edge)) : [],
|
|
1230
|
-
annotations: Array.isArray(resolved.state.annotations)
|
|
1286
|
+
annotations: Array.isArray(resolved.state.annotations)
|
|
1287
|
+
? resolved.state.annotations.map((annotation) => structuredClone(annotation))
|
|
1288
|
+
: [],
|
|
1231
1289
|
contextPins: Array.isArray(resolved.state.contextPins) ? [...resolved.state.contextPins] : [],
|
|
1232
1290
|
ax: resolved.state.ax ? structuredClone(resolved.state.ax) : createEmptyAxState(),
|
|
1233
1291
|
};
|
|
@@ -1268,7 +1326,9 @@ class CanvasStateManager {
|
|
|
1268
1326
|
}
|
|
1269
1327
|
|
|
1270
1328
|
/** Read a snapshot's data without restoring it (for diff). Resolves by ID or name. */
|
|
1271
|
-
getSnapshotData(
|
|
1329
|
+
getSnapshotData(
|
|
1330
|
+
idOrName: string,
|
|
1331
|
+
): { name: string; nodes: CanvasNodeState[]; edges: CanvasEdge[]; annotations: CanvasAnnotation[] } | null {
|
|
1272
1332
|
const resolved = this.readResolvedSnapshot(idOrName);
|
|
1273
1333
|
if (!resolved) return null;
|
|
1274
1334
|
const state = {
|
|
@@ -1281,7 +1341,9 @@ class CanvasStateManager {
|
|
|
1281
1341
|
name: resolved.snapshot.name,
|
|
1282
1342
|
nodes: Array.isArray(state.nodes) ? state.nodes.map((node) => structuredClone(node)) : [],
|
|
1283
1343
|
edges: Array.isArray(state.edges) ? state.edges.map((edge) => structuredClone(edge)) : [],
|
|
1284
|
-
annotations: Array.isArray(state.annotations)
|
|
1344
|
+
annotations: Array.isArray(state.annotations)
|
|
1345
|
+
? state.annotations.map((annotation) => structuredClone(annotation))
|
|
1346
|
+
: [],
|
|
1285
1347
|
};
|
|
1286
1348
|
}
|
|
1287
1349
|
|
|
@@ -1339,9 +1401,10 @@ class CanvasStateManager {
|
|
|
1339
1401
|
// (including undock → dockPosition null) are respected (see normalizeNode).
|
|
1340
1402
|
// Skip during suppressed replay (undo/redo re-add) so a deliberately-undocked
|
|
1341
1403
|
// context node is restored verbatim instead of being snapped back to the dock.
|
|
1342
|
-
const seeded =
|
|
1343
|
-
|
|
1344
|
-
|
|
1404
|
+
const seeded =
|
|
1405
|
+
node.type === 'context' && node.dockPosition == null && this._suppressRecordingDepth === 0
|
|
1406
|
+
? { ...node, dockPosition: 'right' as const, collapsed: true }
|
|
1407
|
+
: node;
|
|
1345
1408
|
const cloned = structuredClone(this.normalizeNode(seeded));
|
|
1346
1409
|
this.nodes.set(node.id, cloned);
|
|
1347
1410
|
this.scheduleSave();
|
|
@@ -1367,11 +1430,7 @@ class CanvasStateManager {
|
|
|
1367
1430
|
if (!existing) return;
|
|
1368
1431
|
const oldSnapshot = structuredClone(existing);
|
|
1369
1432
|
if (existing.type === 'group' && patch.position) {
|
|
1370
|
-
this.translateGroupChildren(
|
|
1371
|
-
id,
|
|
1372
|
-
patch.position.x - existing.position.x,
|
|
1373
|
-
patch.position.y - existing.position.y,
|
|
1374
|
-
);
|
|
1433
|
+
this.translateGroupChildren(id, patch.position.x - existing.position.x, patch.position.y - existing.position.y);
|
|
1375
1434
|
}
|
|
1376
1435
|
const nextNode = this.normalizeNode({ ...existing, ...patch });
|
|
1377
1436
|
this.nodes.set(id, nextNode);
|
|
@@ -1390,7 +1449,11 @@ class CanvasStateManager {
|
|
|
1390
1449
|
operationType: 'updateNode',
|
|
1391
1450
|
description: `Updated node "${(existing.data.title as string) ?? id}"`,
|
|
1392
1451
|
forward: this.suppressed(() => this.updateNode(id, structuredClone(patch))),
|
|
1393
|
-
inverse: this.suppressed(() => {
|
|
1452
|
+
inverse: this.suppressed(() => {
|
|
1453
|
+
this.nodes.set(id, structuredClone(oldSnapshot));
|
|
1454
|
+
this.scheduleSave();
|
|
1455
|
+
this.notifyChange('nodes');
|
|
1456
|
+
}),
|
|
1394
1457
|
});
|
|
1395
1458
|
}
|
|
1396
1459
|
|
|
@@ -1443,7 +1506,8 @@ class CanvasStateManager {
|
|
|
1443
1506
|
// original deletion already recorded the note, so replaying must NOT append a
|
|
1444
1507
|
// duplicate (the timeline is append-only). `revalidateAfterNodeRemoval` above
|
|
1445
1508
|
// still runs unconditionally — only the timeline note is gated.
|
|
1446
|
-
const affected =
|
|
1509
|
+
const affected =
|
|
1510
|
+
orphaned.reanchoredIds.length > 0 || orphaned.removedReviewIds.length > 0 || orphaned.reanchoredFocus;
|
|
1447
1511
|
if (existing && this._suppressRecordingDepth === 0 && affected) {
|
|
1448
1512
|
const title = (existing.data.title as string) ?? id;
|
|
1449
1513
|
const focusNote = orphaned.reanchoredFocus ? ' (focus anchor cleared)' : '';
|
|
@@ -1606,9 +1670,7 @@ class CanvasStateManager {
|
|
|
1606
1670
|
const oldSnapshots = new Map<string, CanvasNodeState>();
|
|
1607
1671
|
const appliedUpdates: CanvasNodeUpdate[] = [];
|
|
1608
1672
|
const explicitPositionUpdateIds = new Set(
|
|
1609
|
-
updates
|
|
1610
|
-
.filter((update) => update.position !== undefined)
|
|
1611
|
-
.map((update) => update.id),
|
|
1673
|
+
updates.filter((update) => update.position !== undefined).map((update) => update.id),
|
|
1612
1674
|
);
|
|
1613
1675
|
|
|
1614
1676
|
for (const update of updates) {
|
|
@@ -1618,16 +1680,10 @@ class CanvasStateManager {
|
|
|
1618
1680
|
continue;
|
|
1619
1681
|
}
|
|
1620
1682
|
const nextPatch: Partial<CanvasNodeState> = {};
|
|
1621
|
-
if (
|
|
1622
|
-
update.position &&
|
|
1623
|
-
(update.position.x !== existing.position.x || update.position.y !== existing.position.y)
|
|
1624
|
-
) {
|
|
1683
|
+
if (update.position && (update.position.x !== existing.position.x || update.position.y !== existing.position.y)) {
|
|
1625
1684
|
nextPatch.position = update.position;
|
|
1626
1685
|
}
|
|
1627
|
-
if (
|
|
1628
|
-
update.size &&
|
|
1629
|
-
(update.size.width !== existing.size.width || update.size.height !== existing.size.height)
|
|
1630
|
-
) {
|
|
1686
|
+
if (update.size && (update.size.width !== existing.size.width || update.size.height !== existing.size.height)) {
|
|
1631
1687
|
nextPatch.size = update.size;
|
|
1632
1688
|
}
|
|
1633
1689
|
if (update.collapsed !== undefined && update.collapsed !== existing.collapsed) {
|
|
@@ -1650,10 +1706,13 @@ class CanvasStateManager {
|
|
|
1650
1706
|
explicitPositionUpdateIds,
|
|
1651
1707
|
);
|
|
1652
1708
|
}
|
|
1653
|
-
this.nodes.set(
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1709
|
+
this.nodes.set(
|
|
1710
|
+
update.id,
|
|
1711
|
+
this.normalizeNode({
|
|
1712
|
+
...existing,
|
|
1713
|
+
...nextPatch,
|
|
1714
|
+
}),
|
|
1715
|
+
);
|
|
1657
1716
|
const parentGroupId = existing.data.parentGroup as string | undefined;
|
|
1658
1717
|
if (parentGroupId) {
|
|
1659
1718
|
touchedParentGroups.add(parentGroupId);
|
|
@@ -1678,7 +1737,10 @@ class CanvasStateManager {
|
|
|
1678
1737
|
operationType: 'batch',
|
|
1679
1738
|
description: formatBatchUpdateDescription(appliedUpdates),
|
|
1680
1739
|
forward: this.suppressed(() => {
|
|
1681
|
-
this.applyUpdates(
|
|
1740
|
+
this.applyUpdates(
|
|
1741
|
+
appliedUpdates.map((update) => structuredClone(update)),
|
|
1742
|
+
options,
|
|
1743
|
+
);
|
|
1682
1744
|
}),
|
|
1683
1745
|
inverse: this.suppressed(() => {
|
|
1684
1746
|
for (const snapshot of inverseSnapshots) {
|
|
@@ -1878,7 +1940,11 @@ class CanvasStateManager {
|
|
|
1878
1940
|
}
|
|
1879
1941
|
|
|
1880
1942
|
/** Invoke a registry-gated PMX command intent — records a timeline event (no execution). */
|
|
1881
|
-
invokeCommand(
|
|
1943
|
+
invokeCommand(
|
|
1944
|
+
name: string,
|
|
1945
|
+
args: Record<string, unknown> | null = null,
|
|
1946
|
+
options: { source?: PmxAxSource } = {},
|
|
1947
|
+
): PmxAxEvent | null {
|
|
1882
1948
|
return this.ax.invokeCommand(name, args, options);
|
|
1883
1949
|
}
|
|
1884
1950
|
|
|
@@ -1900,14 +1966,27 @@ class CanvasStateManager {
|
|
|
1900
1966
|
|
|
1901
1967
|
// ── Timeline (DB-direct; NOT in _axState; NOT history-recorded) ───
|
|
1902
1968
|
recordAxEvent(
|
|
1903
|
-
input: {
|
|
1969
|
+
input: {
|
|
1970
|
+
kind: PmxAxEventKind;
|
|
1971
|
+
summary: string;
|
|
1972
|
+
detail?: string | null;
|
|
1973
|
+
nodeIds?: string[];
|
|
1974
|
+
data?: Record<string, unknown> | null;
|
|
1975
|
+
},
|
|
1904
1976
|
options: { source?: PmxAxSource } = {},
|
|
1905
1977
|
): PmxAxEvent {
|
|
1906
1978
|
return this.ax.recordAxEvent(input, options);
|
|
1907
1979
|
}
|
|
1908
1980
|
|
|
1909
1981
|
addEvidence(
|
|
1910
|
-
input: {
|
|
1982
|
+
input: {
|
|
1983
|
+
kind: PmxAxEvidenceKind;
|
|
1984
|
+
title: string;
|
|
1985
|
+
body?: string | null;
|
|
1986
|
+
ref?: string | null;
|
|
1987
|
+
nodeIds?: string[];
|
|
1988
|
+
data?: Record<string, unknown> | null;
|
|
1989
|
+
},
|
|
1911
1990
|
options: { source?: PmxAxSource } = {},
|
|
1912
1991
|
): PmxAxEvidence {
|
|
1913
1992
|
return this.ax.addEvidence(input, options);
|
|
@@ -1933,11 +2012,23 @@ class CanvasStateManager {
|
|
|
1933
2012
|
reactions?: {
|
|
1934
2013
|
workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
|
|
1935
2014
|
evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
|
|
1936
|
-
review?:
|
|
2015
|
+
review?:
|
|
2016
|
+
| false
|
|
2017
|
+
| {
|
|
2018
|
+
severity?: PmxAxReviewSeverity;
|
|
2019
|
+
kind?: PmxAxReviewKind;
|
|
2020
|
+
anchorType?: PmxAxReviewAnchorType;
|
|
2021
|
+
nodeId?: string | null;
|
|
2022
|
+
};
|
|
1937
2023
|
};
|
|
1938
2024
|
},
|
|
1939
2025
|
options: { source?: PmxAxSource } = {},
|
|
1940
|
-
): {
|
|
2026
|
+
): {
|
|
2027
|
+
event: PmxAxEvent;
|
|
2028
|
+
workItem: PmxAxWorkItem | null;
|
|
2029
|
+
evidence: PmxAxEvidence | null;
|
|
2030
|
+
review: PmxAxReviewAnnotation | null;
|
|
2031
|
+
} {
|
|
1941
2032
|
return this.ax.ingestActivity(input, options);
|
|
1942
2033
|
}
|
|
1943
2034
|
|
|
@@ -1969,7 +2060,12 @@ class CanvasStateManager {
|
|
|
1969
2060
|
return this.ax.getAxTimelineSummary();
|
|
1970
2061
|
}
|
|
1971
2062
|
|
|
1972
|
-
getAxTimeline(q: AxTimelineQuery = {}): {
|
|
2063
|
+
getAxTimeline(q: AxTimelineQuery = {}): {
|
|
2064
|
+
events: PmxAxEvent[];
|
|
2065
|
+
evidence: PmxAxEvidence[];
|
|
2066
|
+
steering: PmxAxSteeringMessage[];
|
|
2067
|
+
summary: PmxAxTimelineSummary;
|
|
2068
|
+
} {
|
|
1973
2069
|
return this.ax.getAxTimeline(q);
|
|
1974
2070
|
}
|
|
1975
2071
|
|
|
@@ -2051,7 +2147,8 @@ class CanvasStateManager {
|
|
|
2051
2147
|
const c = this.nodes.get(id);
|
|
2052
2148
|
if (!c) continue;
|
|
2053
2149
|
const d = { ...c.data };
|
|
2054
|
-
if (oldParent) d.parentGroup = oldParent;
|
|
2150
|
+
if (oldParent) d.parentGroup = oldParent;
|
|
2151
|
+
else delete d.parentGroup;
|
|
2055
2152
|
this.nodes.set(id, { ...c, data: d });
|
|
2056
2153
|
}
|
|
2057
2154
|
this.scheduleSave();
|
|
@@ -15,15 +15,14 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
17
|
const extAppsPackageDir = dirname(require.resolve('@modelcontextprotocol/ext-apps/package.json'));
|
|
18
|
-
const extAppsRuntimeSource = readFileSync(
|
|
19
|
-
join(extAppsPackageDir, 'dist', 'src', 'app-with-deps.js'),
|
|
20
|
-
'utf-8',
|
|
21
|
-
);
|
|
18
|
+
const extAppsRuntimeSource = readFileSync(join(extAppsPackageDir, 'dist', 'src', 'app-with-deps.js'), 'utf-8');
|
|
22
19
|
const appBindingMatch = extAppsRuntimeSource.match(/([A-Za-z_$][\w$]*) as App/);
|
|
23
20
|
const transportBindingMatch = extAppsRuntimeSource.match(/([A-Za-z_$][\w$]*) as PostMessageTransport/);
|
|
24
21
|
|
|
25
22
|
if (!appBindingMatch || !transportBindingMatch) {
|
|
26
|
-
throw new Error(
|
|
23
|
+
throw new Error(
|
|
24
|
+
'Failed to locate App or PostMessageTransport export bindings in @modelcontextprotocol/ext-apps runtime',
|
|
25
|
+
);
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
const extAppsBootstrapSource = `${extAppsRuntimeSource}
|
package/src/server/code-graph.ts
CHANGED
|
@@ -113,7 +113,7 @@ function resolveImportPath(specifier: string, fromFilePath: string): string | nu
|
|
|
113
113
|
// Check if it looks like a relative project path (contains / and no @)
|
|
114
114
|
if (!specifier.includes('/') || specifier.startsWith('@')) return null;
|
|
115
115
|
// For non-relative paths, try workspace-relative resolution
|
|
116
|
-
const wsRoot =
|
|
116
|
+
const wsRoot = canvasState.workspaceRoot;
|
|
117
117
|
return tryResolveWithExtensions(resolve(wsRoot, specifier));
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -123,9 +123,20 @@ function resolveImportPath(specifier: string, fromFilePath: string): string | nu
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const RESOLVE_EXTENSIONS = [
|
|
126
|
-
'',
|
|
127
|
-
'
|
|
128
|
-
'.
|
|
126
|
+
'',
|
|
127
|
+
'.ts',
|
|
128
|
+
'.tsx',
|
|
129
|
+
'.js',
|
|
130
|
+
'.jsx',
|
|
131
|
+
'.mjs',
|
|
132
|
+
'.mts',
|
|
133
|
+
'/index.ts',
|
|
134
|
+
'/index.tsx',
|
|
135
|
+
'/index.js',
|
|
136
|
+
'/index.jsx',
|
|
137
|
+
'.py',
|
|
138
|
+
'.go',
|
|
139
|
+
'.rs',
|
|
129
140
|
];
|
|
130
141
|
|
|
131
142
|
function tryResolveWithExtensions(basePath: string): string | null {
|
|
@@ -179,7 +190,7 @@ function getFileNodes(): CanvasNodeState[] {
|
|
|
179
190
|
*/
|
|
180
191
|
function buildPathIndex(fileNodes?: CanvasNodeState[]): Map<string, string> {
|
|
181
192
|
const index = new Map<string, string>();
|
|
182
|
-
for (const node of
|
|
193
|
+
for (const node of fileNodes ?? getFileNodes()) {
|
|
183
194
|
index.set(node.data.path as string, node.id);
|
|
184
195
|
}
|
|
185
196
|
return index;
|
|
@@ -299,7 +310,7 @@ export function buildCodeGraphSummary(): CodeGraphSummary {
|
|
|
299
310
|
path: relative(process.cwd(), node.data.path as string) || (node.data.path as string),
|
|
300
311
|
title: (node.data.title as string) ?? null,
|
|
301
312
|
imports: importSpecs.get(node.id) ?? [],
|
|
302
|
-
importedBy: [...
|
|
313
|
+
importedBy: [...inc].map((id) => {
|
|
303
314
|
const path = idToPath.get(id);
|
|
304
315
|
return path ? relative(process.cwd(), path) : id;
|
|
305
316
|
}),
|
|
@@ -350,7 +361,9 @@ export function formatCodeGraph(summary: CodeGraphSummary): string {
|
|
|
350
361
|
}
|
|
351
362
|
|
|
352
363
|
if (summary.isolatedFiles.length > 0) {
|
|
353
|
-
lines.push(
|
|
364
|
+
lines.push(
|
|
365
|
+
`Isolated files (${summary.isolatedFiles.length}): ${summary.isolatedFiles.map((f) => f.title ?? f.path).join(', ')}`,
|
|
366
|
+
);
|
|
354
367
|
lines.push('');
|
|
355
368
|
}
|
|
356
369
|
|