pmx-canvas 0.1.16 → 0.1.18
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 +110 -0
- package/Readme.md +14 -7
- package/dist/canvas/global.css +25 -0
- package/dist/canvas/index.js +72 -72
- package/dist/types/client/canvas/AnnotationLayer.d.ts +4 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +4 -1
- package/dist/types/client/canvas/use-pan-zoom.d.ts +2 -1
- package/dist/types/client/icons.d.ts +4 -0
- package/dist/types/client/state/canvas-store.d.ts +16 -1
- package/dist/types/client/types.d.ts +20 -0
- package/dist/types/mcp/canvas-access.d.ts +1 -0
- package/dist/types/server/canvas-serialization.d.ts +25 -1
- package/dist/types/server/canvas-state.d.ts +27 -1
- package/dist/types/server/index.d.ts +7 -2
- package/dist/types/server/mutation-history.d.ts +1 -1
- package/dist/types/server/spatial-analysis.d.ts +11 -2
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +19 -0
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +145 -0
- package/src/cli/agent.ts +6 -0
- package/src/client/App.tsx +60 -3
- package/src/client/canvas/AnnotationLayer.tsx +28 -0
- package/src/client/canvas/CanvasViewport.tsx +169 -10
- package/src/client/canvas/ContextPinBar.tsx +2 -1
- package/src/client/canvas/use-pan-zoom.ts +10 -5
- package/src/client/icons.tsx +22 -0
- package/src/client/state/canvas-store.ts +52 -2
- package/src/client/state/sse-bridge.ts +35 -1
- package/src/client/theme/global.css +25 -0
- package/src/client/types.ts +17 -0
- package/src/mcp/canvas-access.ts +10 -0
- package/src/mcp/server.ts +43 -6
- package/src/server/canvas-schema.ts +25 -0
- package/src/server/canvas-serialization.ts +117 -1
- package/src/server/canvas-state.ts +74 -2
- package/src/server/diagram-presets.ts +54 -19
- package/src/server/index.ts +20 -3
- package/src/server/mutation-history.ts +2 -0
- package/src/server/server.ts +77 -2
- package/src/server/spatial-analysis.ts +46 -1
- package/src/shared/semantic-attention.ts +4 -2
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { CanvasNodeState } from '../types';
|
|
2
|
+
type AnnotationTool = 'pen' | 'eraser' | null;
|
|
2
3
|
interface CanvasViewportProps {
|
|
3
4
|
onNodeContextMenu?: (e: MouseEvent, nodeId: string) => void;
|
|
4
5
|
onCanvasContextMenu?: (e: MouseEvent, canvasX: number, canvasY: number) => void;
|
|
6
|
+
annotationMode?: boolean;
|
|
7
|
+
annotationTool?: AnnotationTool;
|
|
5
8
|
}
|
|
6
9
|
export declare function getRenderableWorldNodes(allNodes: Iterable<CanvasNodeState>, focusedNodeId: string | null): CanvasNodeState[];
|
|
7
|
-
export declare function CanvasViewport({ onNodeContextMenu, onCanvasContextMenu }: CanvasViewportProps): import("preact/src").JSX.Element;
|
|
10
|
+
export declare function CanvasViewport({ onNodeContextMenu, onCanvasContextMenu, annotationMode, annotationTool }: CanvasViewportProps): import("preact/src").JSX.Element;
|
|
8
11
|
export {};
|
|
@@ -4,6 +4,7 @@ interface PanZoomOptions {
|
|
|
4
4
|
viewport: Signal<ViewportState>;
|
|
5
5
|
onViewportChange: (v: ViewportState) => void;
|
|
6
6
|
onViewportCommit: (v: ViewportState) => void;
|
|
7
|
+
disabled?: boolean;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* Hook that wires up pan/zoom interactions on a container element.
|
|
@@ -12,5 +13,5 @@ interface PanZoomOptions {
|
|
|
12
13
|
* - Pointer drag on background: pan
|
|
13
14
|
* - Pinch (touch): zoom
|
|
14
15
|
*/
|
|
15
|
-
export declare function usePanZoom({ viewport, onViewportChange, onViewportCommit }: PanZoomOptions): import("preact/src").RefObject<HTMLDivElement>;
|
|
16
|
+
export declare function usePanZoom({ viewport, onViewportChange, onViewportCommit, disabled }: PanZoomOptions): import("preact/src").RefObject<HTMLDivElement>;
|
|
16
17
|
export {};
|
|
@@ -19,6 +19,10 @@ export declare function IconMinimap(p: IconProps): JSX.Element;
|
|
|
19
19
|
export declare function IconSun(p: IconProps): JSX.Element;
|
|
20
20
|
/** Crescent moon */
|
|
21
21
|
export declare function IconMoon(p: IconProps): JSX.Element;
|
|
22
|
+
/** Pen stroke — canvas annotation mode */
|
|
23
|
+
export declare function IconPen(p: IconProps): JSX.Element;
|
|
24
|
+
/** Eraser — remove canvas annotations */
|
|
25
|
+
export declare function IconEraser(p: IconProps): JSX.Element;
|
|
22
26
|
/** Camera — snapshots */
|
|
23
27
|
export declare function IconSnapshot(p: IconProps): JSX.Element;
|
|
24
28
|
/** Bullseye — trace toggle */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type CanvasEdge, type CanvasLayout, type CanvasNodeState, type ConnectionStatus, type ViewportState } from '../types';
|
|
1
|
+
import { type CanvasAnnotation, type CanvasEdge, type CanvasLayout, type CanvasNodeState, type ConnectionStatus, type ViewportState } from '../types';
|
|
2
2
|
export declare const viewport: import("@preact/signals-core").Signal<ViewportState>;
|
|
3
3
|
export declare const nodes: import("@preact/signals-core").Signal<Map<string, CanvasNodeState>>;
|
|
4
4
|
export declare const edges: import("@preact/signals-core").Signal<Map<string, CanvasEdge>>;
|
|
5
|
+
export declare const annotations: import("@preact/signals-core").Signal<Map<string, CanvasAnnotation>>;
|
|
5
6
|
export declare const activeNodeId: import("@preact/signals-core").Signal<string | null>;
|
|
6
7
|
export declare const connectionStatus: import("@preact/signals-core").Signal<ConnectionStatus>;
|
|
7
8
|
export declare const sessionId: import("@preact/signals-core").Signal<string>;
|
|
@@ -41,6 +42,19 @@ export declare function removeNode(id: string): void;
|
|
|
41
42
|
export declare function addEdge(edge: CanvasEdge): void;
|
|
42
43
|
export declare function removeEdge(id: string): void;
|
|
43
44
|
export declare function removeEdgesForNode(nodeId: string): void;
|
|
45
|
+
export declare function addAnnotation(annotation: CanvasAnnotation): void;
|
|
46
|
+
export declare function removeAnnotation(id: string): void;
|
|
47
|
+
export declare function createAnnotationFromClient(input: {
|
|
48
|
+
points: CanvasAnnotation['points'];
|
|
49
|
+
color: string;
|
|
50
|
+
width: number;
|
|
51
|
+
label?: string;
|
|
52
|
+
}): Promise<{
|
|
53
|
+
ok: boolean;
|
|
54
|
+
}>;
|
|
55
|
+
export declare function removeAnnotationFromClient(id: string): Promise<{
|
|
56
|
+
ok: boolean;
|
|
57
|
+
}>;
|
|
44
58
|
export declare function resizeNode(id: string, size: {
|
|
45
59
|
width: number;
|
|
46
60
|
height: number;
|
|
@@ -56,6 +70,7 @@ export declare function replaceViewport(next: ViewportState): void;
|
|
|
56
70
|
export declare function commitViewport(next: ViewportState): void;
|
|
57
71
|
export declare function applyServerCanvasLayout(layout: Pick<CanvasLayout, 'nodes' | 'edges'> & {
|
|
58
72
|
viewport?: ViewportState;
|
|
73
|
+
annotations?: CanvasAnnotation[];
|
|
59
74
|
}, options?: {
|
|
60
75
|
applyViewport?: boolean;
|
|
61
76
|
}): void;
|
|
@@ -29,6 +29,25 @@ export interface CanvasEdge {
|
|
|
29
29
|
style?: 'solid' | 'dashed' | 'dotted';
|
|
30
30
|
animated?: boolean;
|
|
31
31
|
}
|
|
32
|
+
export interface CanvasAnnotationPoint {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
}
|
|
36
|
+
export interface CanvasAnnotation {
|
|
37
|
+
id: string;
|
|
38
|
+
type: 'freehand';
|
|
39
|
+
points: CanvasAnnotationPoint[];
|
|
40
|
+
bounds: {
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
};
|
|
46
|
+
color: string;
|
|
47
|
+
width: number;
|
|
48
|
+
label?: string;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
}
|
|
32
51
|
export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected';
|
|
33
52
|
export declare const TYPE_LABELS: Record<CanvasNodeState['type'], string>;
|
|
34
53
|
/** Node types that support the full-viewport expand/focus overlay. */
|
|
@@ -40,4 +59,5 @@ export interface CanvasLayout {
|
|
|
40
59
|
viewport: ViewportState;
|
|
41
60
|
nodes: CanvasNodeState[];
|
|
42
61
|
edges: CanvasEdge[];
|
|
62
|
+
annotations?: CanvasAnnotation[];
|
|
43
63
|
}
|
|
@@ -54,6 +54,7 @@ export interface CanvasAccess {
|
|
|
54
54
|
buildWebArtifact(input: WebArtifactInput): Promise<WebArtifactResult>;
|
|
55
55
|
updateNode(id: string, patch: UpdateNodePatch): Promise<void>;
|
|
56
56
|
removeNode(id: string): Promise<void>;
|
|
57
|
+
removeAnnotation(id: string): Promise<boolean>;
|
|
57
58
|
addEdge(input: AddEdgeInput): Promise<string>;
|
|
58
59
|
removeEdge(id: string): Promise<void>;
|
|
59
60
|
createGroup(input: CreateGroupInput): Promise<string>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CanvasLayout, CanvasNodeState, ViewportState } from './canvas-state.js';
|
|
1
|
+
import type { CanvasAnnotation, CanvasLayout, CanvasNodeState, ViewportState } from './canvas-state.js';
|
|
2
2
|
import { type CanvasNodeProvenance } from './canvas-provenance.js';
|
|
3
3
|
export interface SerializedCanvasNode extends CanvasNodeState {
|
|
4
4
|
kind: string;
|
|
@@ -11,16 +11,40 @@ export interface SerializedCanvasNode extends CanvasNodeState {
|
|
|
11
11
|
export interface SerializedCanvasLayout extends Omit<CanvasLayout, 'nodes'> {
|
|
12
12
|
nodes: SerializedCanvasNode[];
|
|
13
13
|
}
|
|
14
|
+
export interface CanvasAnnotationSummary {
|
|
15
|
+
id: string;
|
|
16
|
+
type: CanvasAnnotation['type'];
|
|
17
|
+
bounds: CanvasAnnotation['bounds'];
|
|
18
|
+
color: string;
|
|
19
|
+
width: number;
|
|
20
|
+
pointCount: number;
|
|
21
|
+
label: string | null;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
}
|
|
24
|
+
export interface CanvasAnnotationContextSummary {
|
|
25
|
+
id: string;
|
|
26
|
+
label: string | null;
|
|
27
|
+
bounds: CanvasAnnotation['bounds'];
|
|
28
|
+
targetNodeIds: string[];
|
|
29
|
+
targetNodeTitles: string[];
|
|
30
|
+
target: string;
|
|
31
|
+
}
|
|
14
32
|
export declare function getCanvasNodeKind(node: CanvasNodeState, data: Record<string, unknown>): string;
|
|
15
33
|
export declare function getCanvasNodeTitle(node: CanvasNodeState): string | null;
|
|
16
34
|
export declare function getCanvasNodeContent(node: CanvasNodeState): string | null;
|
|
17
35
|
export declare function serializeCanvasNode(node: CanvasNodeState): SerializedCanvasNode;
|
|
36
|
+
export declare function serializeCanvasNodeForAgent(node: CanvasNodeState): SerializedCanvasNode;
|
|
18
37
|
export declare function serializeCanvasNodeWithBlobSummaries(node: CanvasNodeState): SerializedCanvasNode;
|
|
19
38
|
export declare function serializeCanvasLayout(layout: CanvasLayout): SerializedCanvasLayout;
|
|
39
|
+
export declare function serializeCanvasLayoutForAgent(layout: CanvasLayout): SerializedCanvasLayout;
|
|
20
40
|
export declare function serializeCanvasLayoutWithBlobSummaries(layout: CanvasLayout): SerializedCanvasLayout;
|
|
41
|
+
export declare function summarizeCanvasAnnotation(annotation: CanvasAnnotation): CanvasAnnotationSummary;
|
|
42
|
+
export declare function summarizeCanvasAnnotationForContext(annotation: CanvasAnnotation, nodes: CanvasNodeState[]): CanvasAnnotationContextSummary;
|
|
21
43
|
export interface CanvasSummary {
|
|
22
44
|
totalNodes: number;
|
|
23
45
|
totalEdges: number;
|
|
46
|
+
totalAnnotations: number;
|
|
47
|
+
annotations: CanvasAnnotationContextSummary[];
|
|
24
48
|
nodesByType: Record<string, number>;
|
|
25
49
|
pinnedCount: number;
|
|
26
50
|
pinnedTitles: string[];
|
|
@@ -22,6 +22,7 @@ interface PersistedCanvasState {
|
|
|
22
22
|
viewport: ViewportState;
|
|
23
23
|
nodes: CanvasNodeState[];
|
|
24
24
|
edges: CanvasEdge[];
|
|
25
|
+
annotations?: CanvasAnnotation[];
|
|
25
26
|
contextPins: string[];
|
|
26
27
|
}
|
|
27
28
|
interface LoadFromDiskOptions {
|
|
@@ -81,10 +82,30 @@ export interface CanvasEdge {
|
|
|
81
82
|
style?: 'solid' | 'dashed' | 'dotted';
|
|
82
83
|
animated?: boolean;
|
|
83
84
|
}
|
|
85
|
+
export interface CanvasAnnotationPoint {
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
}
|
|
89
|
+
export interface CanvasAnnotation {
|
|
90
|
+
id: string;
|
|
91
|
+
type: 'freehand';
|
|
92
|
+
points: CanvasAnnotationPoint[];
|
|
93
|
+
bounds: {
|
|
94
|
+
x: number;
|
|
95
|
+
y: number;
|
|
96
|
+
width: number;
|
|
97
|
+
height: number;
|
|
98
|
+
};
|
|
99
|
+
color: string;
|
|
100
|
+
width: number;
|
|
101
|
+
label?: string;
|
|
102
|
+
createdAt: string;
|
|
103
|
+
}
|
|
84
104
|
export interface CanvasLayout {
|
|
85
105
|
viewport: ViewportState;
|
|
86
106
|
nodes: CanvasNodeState[];
|
|
87
107
|
edges: CanvasEdge[];
|
|
108
|
+
annotations: CanvasAnnotation[];
|
|
88
109
|
}
|
|
89
110
|
export interface CanvasNodeUpdate {
|
|
90
111
|
id: string;
|
|
@@ -101,7 +122,7 @@ export interface CanvasNodeUpdate {
|
|
|
101
122
|
}
|
|
102
123
|
export type CanvasChangeType = 'pins' | 'nodes';
|
|
103
124
|
export interface MutationRecordInfo {
|
|
104
|
-
operationType: 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'clear' | 'restoreSnapshot' | 'setPins' | 'arrange' | 'batch' | 'groupNodes' | 'ungroupNodes' | 'viewport';
|
|
125
|
+
operationType: 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'addAnnotation' | 'removeAnnotation' | 'clear' | 'restoreSnapshot' | 'setPins' | 'arrange' | 'batch' | 'groupNodes' | 'ungroupNodes' | 'viewport';
|
|
105
126
|
description: string;
|
|
106
127
|
forward: () => void;
|
|
107
128
|
inverse: () => void;
|
|
@@ -114,6 +135,7 @@ interface GroupNodesOptions {
|
|
|
114
135
|
declare class CanvasStateManager {
|
|
115
136
|
private nodes;
|
|
116
137
|
private edges;
|
|
138
|
+
private annotations;
|
|
117
139
|
private _viewport;
|
|
118
140
|
private _contextPinnedNodeIds;
|
|
119
141
|
private _workspaceRoot;
|
|
@@ -185,6 +207,7 @@ declare class CanvasStateManager {
|
|
|
185
207
|
name: string;
|
|
186
208
|
nodes: CanvasNodeState[];
|
|
187
209
|
edges: CanvasEdge[];
|
|
210
|
+
annotations: CanvasAnnotation[];
|
|
188
211
|
} | null;
|
|
189
212
|
/** Delete a snapshot. */
|
|
190
213
|
deleteSnapshot(id: string): boolean;
|
|
@@ -200,6 +223,9 @@ declare class CanvasStateManager {
|
|
|
200
223
|
removeEdge(id: string): boolean;
|
|
201
224
|
getEdges(): CanvasEdge[];
|
|
202
225
|
getEdgesForNode(nodeId: string): CanvasEdge[];
|
|
226
|
+
addAnnotation(annotation: CanvasAnnotation): void;
|
|
227
|
+
removeAnnotation(id: string): boolean;
|
|
228
|
+
getAnnotations(): CanvasAnnotation[];
|
|
203
229
|
private removeEdgesForNode;
|
|
204
230
|
getLayout(): CanvasLayout;
|
|
205
231
|
getLayoutForPersistence(): CanvasLayout;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import type { CanvasNodeState, CanvasEdge, CanvasLayout } from './canvas-state.js';
|
|
2
|
+
import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout } from './canvas-state.js';
|
|
3
3
|
import { searchNodes } from './spatial-analysis.js';
|
|
4
4
|
import { diffLayouts } from './mutation-history.js';
|
|
5
5
|
import { fitCanvasView, gcCanvasSnapshots, listCanvasSnapshots } from './canvas-operations.js';
|
|
@@ -69,6 +69,11 @@ export declare class PmxCanvas extends EventEmitter {
|
|
|
69
69
|
style?: CanvasEdge['style'];
|
|
70
70
|
animated?: boolean;
|
|
71
71
|
}): string;
|
|
72
|
+
addAnnotation(input: Omit<CanvasAnnotation, 'id' | 'createdAt'> & {
|
|
73
|
+
id?: string;
|
|
74
|
+
createdAt?: string;
|
|
75
|
+
}): string;
|
|
76
|
+
removeAnnotation(id: string): boolean;
|
|
72
77
|
removeEdge(id: string): void;
|
|
73
78
|
/**
|
|
74
79
|
* Create a group node and optionally add child nodes to it.
|
|
@@ -263,7 +268,7 @@ export type { CanvasNodeState, CanvasEdge, CanvasLayout, ViewportState } from '.
|
|
|
263
268
|
export type { CanvasAutomationWebViewOptions, CanvasAutomationWebViewStatus, PrimaryWorkbenchCanvasPromptRequest, PrimaryWorkbenchIntent, } from './server.js';
|
|
264
269
|
export { emitPrimaryWorkbenchEvent, consumePrimaryWorkbenchIntents, setPrimaryWorkbenchAutoOpenEnabled, setPrimaryWorkbenchCanvasPromptHandler, startCanvasServer, stopCanvasServer, getCanvasServerPort, openUrlInExternalBrowser, getCanvasAutomationWebViewStatus, startCanvasAutomationWebView, stopCanvasAutomationWebView, evaluateCanvasAutomationWebView, resizeCanvasAutomationWebView, screenshotCanvasAutomationWebView, } from './server.js';
|
|
265
270
|
export { canvasState } from './canvas-state.js';
|
|
266
|
-
export type { CanvasSnapshot, CanvasSnapshotGcResult, CanvasSnapshotListOptions } from './canvas-state.js';
|
|
271
|
+
export type { CanvasAnnotation, CanvasSnapshot, CanvasSnapshotGcResult, CanvasSnapshotListOptions } from './canvas-state.js';
|
|
267
272
|
export { findOpenCanvasPosition } from './placement.js';
|
|
268
273
|
export { searchNodes, buildSpatialContext, detectClusters, findNeighborhoods } from './spatial-analysis.js';
|
|
269
274
|
export type { SpatialCluster, SpatialContext, SpatialNeighbor, NodeSpatialInfo } from './spatial-analysis.js';
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - _replaying flag prevents undo/redo from recording new entries
|
|
13
13
|
*/
|
|
14
14
|
import type { CanvasNodeState, CanvasEdge } from './canvas-state.js';
|
|
15
|
-
export type MutationOp = 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'clear' | 'arrange' | 'restoreSnapshot' | 'setPins' | 'batch' | 'viewport' | 'groupNodes' | 'ungroupNodes';
|
|
15
|
+
export type MutationOp = 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'addAnnotation' | 'removeAnnotation' | 'clear' | 'arrange' | 'restoreSnapshot' | 'setPins' | 'batch' | 'viewport' | 'groupNodes' | 'ungroupNodes';
|
|
16
16
|
export interface MutationEntry {
|
|
17
17
|
id: string;
|
|
18
18
|
timestamp: string;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* actually real for agents. Instead of raw x/y coordinates, agents get
|
|
10
10
|
* semantic clusters, ordered context, and implicit human intent.
|
|
11
11
|
*/
|
|
12
|
-
import type { CanvasNodeState, CanvasEdge } from './canvas-state.js';
|
|
12
|
+
import type { CanvasAnnotation, CanvasNodeState, CanvasEdge } from './canvas-state.js';
|
|
13
13
|
export interface SpatialCluster {
|
|
14
14
|
/** Auto-generated cluster ID */
|
|
15
15
|
id: string;
|
|
@@ -45,6 +45,14 @@ export interface NodeSpatialInfo {
|
|
|
45
45
|
/** Reading order index (top-left to bottom-right) */
|
|
46
46
|
readingOrder: number;
|
|
47
47
|
}
|
|
48
|
+
export interface SpatialAnnotationContext {
|
|
49
|
+
id: string;
|
|
50
|
+
label: string | null;
|
|
51
|
+
bounds: CanvasAnnotation['bounds'];
|
|
52
|
+
targetNodeIds: string[];
|
|
53
|
+
targetNodeTitles: string[];
|
|
54
|
+
target: string;
|
|
55
|
+
}
|
|
48
56
|
export interface SpatialContext {
|
|
49
57
|
/** Total nodes on canvas */
|
|
50
58
|
totalNodes: number;
|
|
@@ -58,6 +66,7 @@ export interface SpatialContext {
|
|
|
58
66
|
pinnedNodeTitle: string | null;
|
|
59
67
|
neighbors: SpatialNeighbor[];
|
|
60
68
|
}[];
|
|
69
|
+
annotations: SpatialAnnotationContext[];
|
|
61
70
|
}
|
|
62
71
|
/**
|
|
63
72
|
* Detect proximity clusters using single-linkage clustering.
|
|
@@ -84,4 +93,4 @@ export declare function searchNodes(nodes: CanvasNodeState[], query: string): {
|
|
|
84
93
|
/**
|
|
85
94
|
* Build the complete spatial context for the canvas.
|
|
86
95
|
*/
|
|
87
|
-
export declare function buildSpatialContext(nodes: CanvasNodeState[], _edges: CanvasEdge[], pinnedIds: Set<string
|
|
96
|
+
export declare function buildSpatialContext(nodes: CanvasNodeState[], _edges: CanvasEdge[], pinnedIds: Set<string>, annotations?: CanvasAnnotation[]): SpatialContext;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
|
@@ -302,6 +302,10 @@ If a node type is rejected by `canvas_add_node`, call `canvas_describe_schema` a
|
|
|
302
302
|
- `id` (required): node to remove
|
|
303
303
|
- Clean up nodes that are no longer relevant
|
|
304
304
|
|
|
305
|
+
**`canvas_remove_annotation`** — Remove a human-drawn annotation
|
|
306
|
+
- `id` (required): annotation to remove
|
|
307
|
+
- Use when context gives you the annotation ID; use WebView first if you need to identify a mark by shape or location.
|
|
308
|
+
|
|
305
309
|
**`canvas_get_node`** — Get a single node's full data
|
|
306
310
|
- `id` (required): node to retrieve
|
|
307
311
|
|
|
@@ -564,6 +568,11 @@ tools below operate on the live canvas state.
|
|
|
564
568
|
Useful workbench selectors:
|
|
565
569
|
- Nodes: `.canvas-node`, `.canvas-node.active`, `.canvas-node.context-pinned`, `.canvas-node.group-node`
|
|
566
570
|
- Node internals: `.node-title`, `.node-titlebar`, `.node-body`, `.node-type-badge`, `.node-controls`
|
|
571
|
+
- Annotations: `.annotation-layer path` renders human-drawn freehand ink. Use WebView
|
|
572
|
+
to inspect or screenshot annotation shapes; MCP/context resources only expose compact
|
|
573
|
+
annotation target summaries, not the raw visual shape. Humans can remove marks with
|
|
574
|
+
the eraser toolbar button; agents can remove a known annotation ID with
|
|
575
|
+
`canvas_remove_annotation`.
|
|
567
576
|
- Canvas chrome: `.hud-layer`, `.canvas-toolbar`, `.connection-dot`, `.canvas-bootstrap-card`
|
|
568
577
|
- Nodes do not expose stable `data-node-id` attributes. Use `canvas_get_layout`, `canvas_search`, or MCP resource data for exact node IDs.
|
|
569
578
|
|
|
@@ -610,6 +619,11 @@ canvas_webview_stop();
|
|
|
610
619
|
geometric `graph` node would feel too rigid
|
|
611
620
|
- Prefer labeled shapes (`"label": { "text": "..." }` on rectangle/ellipse/diamond) over
|
|
612
621
|
separate text elements — fewer tokens and auto-centered
|
|
622
|
+
- Do not use separate `text` elements with `containerId`/`boundElements` to place centered text
|
|
623
|
+
inside shapes. The hosted SVG preview does not auto-position those; PMX normalizes imported
|
|
624
|
+
canonical bound text back into shape labels for hosted app calls.
|
|
625
|
+
- For detailed sizing, camera, and label-fit rules, read `references/excalidraw-diagram-authoring.md`
|
|
626
|
+
before creating dense diagrams.
|
|
613
627
|
- Prefer the pastel fill palette in the Excalidraw `read_me` (light blue/green/orange/...) for
|
|
614
628
|
a consistent look across diagrams
|
|
615
629
|
|
|
@@ -691,6 +705,11 @@ The `canvas://spatial-context` resource reveals how the human has organized info
|
|
|
691
705
|
This implies sequence or priority.
|
|
692
706
|
- **Pinned neighborhoods** — For each pinned node, nearby unpinned nodes are listed. These
|
|
693
707
|
are the human's implicit context — things they consider related to what they pinned.
|
|
708
|
+
- **Annotations** — Human-drawn markup is summarized by target/bounds only, e.g. an
|
|
709
|
+
annotation over a node or empty canvas region. Use WebView (`canvas_webview_start` +
|
|
710
|
+
`canvas_evaluate`/`canvas_screenshot`) when you need to see whether the mark is an
|
|
711
|
+
arrow, line, circle, or other drawn shape. Remove known annotations with
|
|
712
|
+
`canvas_remove_annotation`; otherwise use WebView to identify the mark first.
|
|
694
713
|
- **Board density matters** — On a dense board, spatial context can still read like one large
|
|
695
714
|
gallery unless groups and spacing separate the major regions clearly.
|
|
696
715
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Excalidraw Diagram Authoring
|
|
2
|
+
|
|
3
|
+
Use this guide when creating diagrams through PMX Canvas with `canvas_add_diagram` or
|
|
4
|
+
`pmx-canvas external-app add --kind excalidraw`.
|
|
5
|
+
|
|
6
|
+
## Why Text Can Still Drift
|
|
7
|
+
|
|
8
|
+
PMX normalizes canonical Excalidraw bound text (`containerId` / `boundElements`) into the hosted
|
|
9
|
+
app's supported shape-level `label` format before calling Excalidraw. That fixes the payload
|
|
10
|
+
format mismatch, but it does not fix poor diagram geometry.
|
|
11
|
+
|
|
12
|
+
Text can still appear clipped or misplaced when:
|
|
13
|
+
|
|
14
|
+
- A label is too long for its shape.
|
|
15
|
+
- A diamond or ellipse is too small for the label's usable center area.
|
|
16
|
+
- The `cameraUpdate` viewport is too tight or not 4:3.
|
|
17
|
+
- Title, footer, or notes are placed near the camera edge.
|
|
18
|
+
- The caller bypasses PMX and sends raw elements directly to Excalidraw MCP.
|
|
19
|
+
|
|
20
|
+
## Text Format Rules
|
|
21
|
+
|
|
22
|
+
For text inside a rectangle, ellipse, or diamond, use shape-level `label`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"type": "rectangle",
|
|
27
|
+
"id": "step-a",
|
|
28
|
+
"x": 100,
|
|
29
|
+
"y": 100,
|
|
30
|
+
"width": 260,
|
|
31
|
+
"height": 90,
|
|
32
|
+
"label": { "text": "Step A", "fontSize": 18 }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Do not create separate centered text elements for shape labels:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"type": "text",
|
|
41
|
+
"containerId": "step-a",
|
|
42
|
+
"text": "Step A"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Standalone text is fine for titles, notes, captions, and free-floating annotations. For standalone
|
|
47
|
+
text, `x` is the left edge; `textAlign` does not center it on a point.
|
|
48
|
+
|
|
49
|
+
## Label Length Rules
|
|
50
|
+
|
|
51
|
+
- Use 1-4 words inside shapes.
|
|
52
|
+
- Put detailed explanations in nearby standalone text annotations.
|
|
53
|
+
- Prefer `Bound Text` over `Pattern B: containerId+boundElements only`.
|
|
54
|
+
- If a label needs more than 4 words, either widen the shape or split the idea into a label plus an annotation.
|
|
55
|
+
|
|
56
|
+
## Shape Sizing Rules
|
|
57
|
+
|
|
58
|
+
- Minimum labeled rectangle or ellipse: `180x80`.
|
|
59
|
+
- For 3-5 word labels: `240x90` or larger.
|
|
60
|
+
- For long labels: `320+` width or use an external annotation.
|
|
61
|
+
- Diamonds need more room than rectangles because the usable center area is smaller.
|
|
62
|
+
- Leave at least `30px` gap between shapes and labels/arrows.
|
|
63
|
+
|
|
64
|
+
## Camera Rules
|
|
65
|
+
|
|
66
|
+
Always start with a `cameraUpdate` as the first element.
|
|
67
|
+
|
|
68
|
+
Use 4:3 camera sizes only:
|
|
69
|
+
|
|
70
|
+
- `400x300`
|
|
71
|
+
- `600x450`
|
|
72
|
+
- `800x600`
|
|
73
|
+
- `1200x900`
|
|
74
|
+
- `1600x1200`
|
|
75
|
+
|
|
76
|
+
Camera bounds must include the full diagram plus padding. Leave at least `80px` padding around all
|
|
77
|
+
visible content.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "type": "cameraUpdate", "x": 20, "y": 0, "width": 1200, "height": 900 }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If a title, footer, or rightmost label is clipped, the camera is wrong even if the elements are valid.
|
|
86
|
+
|
|
87
|
+
## Good Pattern
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
[
|
|
91
|
+
{ "type": "cameraUpdate", "x": 20, "y": 0, "width": 1200, "height": 900 },
|
|
92
|
+
{
|
|
93
|
+
"type": "rectangle",
|
|
94
|
+
"id": "a",
|
|
95
|
+
"x": 120,
|
|
96
|
+
"y": 160,
|
|
97
|
+
"width": 260,
|
|
98
|
+
"height": 90,
|
|
99
|
+
"backgroundColor": "#a5d8ff",
|
|
100
|
+
"fillStyle": "solid",
|
|
101
|
+
"label": { "text": "Short Label", "fontSize": 18 }
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"type": "rectangle",
|
|
105
|
+
"id": "b",
|
|
106
|
+
"x": 520,
|
|
107
|
+
"y": 160,
|
|
108
|
+
"width": 280,
|
|
109
|
+
"height": 90,
|
|
110
|
+
"backgroundColor": "#b2f2bb",
|
|
111
|
+
"fillStyle": "solid",
|
|
112
|
+
"label": { "text": "Next Step", "fontSize": 18 }
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"type": "arrow",
|
|
116
|
+
"id": "a-to-b",
|
|
117
|
+
"x": 390,
|
|
118
|
+
"y": 205,
|
|
119
|
+
"width": 110,
|
|
120
|
+
"height": 0,
|
|
121
|
+
"points": [[0, 0], [110, 0]],
|
|
122
|
+
"endArrowhead": "arrow",
|
|
123
|
+
"label": { "text": "then", "fontSize": 14 }
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"type": "text",
|
|
127
|
+
"id": "note",
|
|
128
|
+
"x": 120,
|
|
129
|
+
"y": 290,
|
|
130
|
+
"text": "Longer explanation goes here, outside the shape.",
|
|
131
|
+
"fontSize": 16
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Preflight Checklist
|
|
137
|
+
|
|
138
|
+
- Shape text uses `label`, not separate `text` elements.
|
|
139
|
+
- Shape labels are short enough to fit.
|
|
140
|
+
- Long explanations are outside shapes.
|
|
141
|
+
- The first element is a 4:3 `cameraUpdate`.
|
|
142
|
+
- Camera has at least `80px` padding around all visible content.
|
|
143
|
+
- Titles and footers are not near the camera edge.
|
|
144
|
+
- Arrows have explicit `points` and enough space for labels.
|
|
145
|
+
- Calls go through PMX (`canvas_add_diagram` or `external-app add --kind excalidraw`) unless you manually apply these rules to raw Excalidraw MCP input.
|
package/src/cli/agent.ts
CHANGED
|
@@ -1027,6 +1027,7 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1027
1027
|
'pmx-canvas node add --type status --title "Build" --content "passing"',
|
|
1028
1028
|
'pmx-canvas node add --type file --content "src/index.ts"',
|
|
1029
1029
|
'pmx-canvas node add --type webpage --url "https://example.com/docs"',
|
|
1030
|
+
'pmx-canvas node add --type html --title "Widget" --content "<main>Hello</main>"',
|
|
1030
1031
|
'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
|
|
1031
1032
|
'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
|
|
1032
1033
|
'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
@@ -1069,6 +1070,9 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1069
1070
|
body.url = webpageUrl;
|
|
1070
1071
|
} else if (type === 'image' && imagePath && !flags.content) {
|
|
1071
1072
|
body.content = imagePath;
|
|
1073
|
+
} else if (type === 'html') {
|
|
1074
|
+
const html = getStringFlag(flags, 'html') ?? getStringFlag(flags, 'content');
|
|
1075
|
+
if (html !== undefined) body.html = html;
|
|
1072
1076
|
} else if (flags.content) {
|
|
1073
1077
|
body.content = flags.content;
|
|
1074
1078
|
}
|
|
@@ -1090,6 +1094,8 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1090
1094
|
if (flags.stdin) {
|
|
1091
1095
|
if (type === 'webpage') {
|
|
1092
1096
|
body.url = await readStdin();
|
|
1097
|
+
} else if (type === 'html') {
|
|
1098
|
+
body.html = await readStdin();
|
|
1093
1099
|
} else {
|
|
1094
1100
|
body.content = await readStdin();
|
|
1095
1101
|
}
|