pmx-canvas 0.1.16 → 0.1.17
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 +65 -0
- package/Readme.md +2 -2
- 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 +23 -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 +17 -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 +35 -4
- package/src/server/canvas-schema.ts +25 -0
- package/src/server/canvas-serialization.ts +69 -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,6 +11,24 @@ 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;
|
|
@@ -18,9 +36,13 @@ export declare function serializeCanvasNode(node: CanvasNodeState): SerializedCa
|
|
|
18
36
|
export declare function serializeCanvasNodeWithBlobSummaries(node: CanvasNodeState): SerializedCanvasNode;
|
|
19
37
|
export declare function serializeCanvasLayout(layout: CanvasLayout): SerializedCanvasLayout;
|
|
20
38
|
export declare function serializeCanvasLayoutWithBlobSummaries(layout: CanvasLayout): SerializedCanvasLayout;
|
|
39
|
+
export declare function summarizeCanvasAnnotation(annotation: CanvasAnnotation): CanvasAnnotationSummary;
|
|
40
|
+
export declare function summarizeCanvasAnnotationForContext(annotation: CanvasAnnotation, nodes: CanvasNodeState[]): CanvasAnnotationContextSummary;
|
|
21
41
|
export interface CanvasSummary {
|
|
22
42
|
totalNodes: number;
|
|
23
43
|
totalEdges: number;
|
|
44
|
+
totalAnnotations: number;
|
|
45
|
+
annotations: CanvasAnnotationContextSummary[];
|
|
24
46
|
nodesByType: Record<string, number>;
|
|
25
47
|
pinnedCount: number;
|
|
26
48
|
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.17",
|
|
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,9 @@ 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.
|
|
613
625
|
- Prefer the pastel fill palette in the Excalidraw `read_me` (light blue/green/orange/...) for
|
|
614
626
|
a consistent look across diagrams
|
|
615
627
|
|
|
@@ -691,6 +703,11 @@ The `canvas://spatial-context` resource reveals how the human has organized info
|
|
|
691
703
|
This implies sequence or priority.
|
|
692
704
|
- **Pinned neighborhoods** — For each pinned node, nearby unpinned nodes are listed. These
|
|
693
705
|
are the human's implicit context — things they consider related to what they pinned.
|
|
706
|
+
- **Annotations** — Human-drawn markup is summarized by target/bounds only, e.g. an
|
|
707
|
+
annotation over a node or empty canvas region. Use WebView (`canvas_webview_start` +
|
|
708
|
+
`canvas_evaluate`/`canvas_screenshot`) when you need to see whether the mark is an
|
|
709
|
+
arrow, line, circle, or other drawn shape. Remove known annotations with
|
|
710
|
+
`canvas_remove_annotation`; otherwise use WebView to identify the mark first.
|
|
694
711
|
- **Board density matters** — On a dense board, spatial context can still read like one large
|
|
695
712
|
gallery unless groups and spacing separate the major regions clearly.
|
|
696
713
|
|
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
|
}
|
package/src/client/App.tsx
CHANGED
|
@@ -41,10 +41,12 @@ import { connectSSE } from './state/sse-bridge';
|
|
|
41
41
|
import {
|
|
42
42
|
IconArrange,
|
|
43
43
|
IconClearTrace,
|
|
44
|
+
IconEraser,
|
|
44
45
|
IconFitAll,
|
|
45
46
|
IconLogo,
|
|
46
47
|
IconMinimap,
|
|
47
48
|
IconMoon,
|
|
49
|
+
IconPen,
|
|
48
50
|
IconResetView,
|
|
49
51
|
IconSearch,
|
|
50
52
|
IconShortcuts,
|
|
@@ -71,6 +73,8 @@ function sendIntent(type: string, payload: Record<string, unknown> = {}): void {
|
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
type AnnotationTool = 'pen' | 'eraser' | null;
|
|
77
|
+
|
|
74
78
|
function ToolbarHint({
|
|
75
79
|
label,
|
|
76
80
|
detail,
|
|
@@ -108,6 +112,9 @@ function Toolbar({
|
|
|
108
112
|
snapshotBtnRef,
|
|
109
113
|
onOpenPalette,
|
|
110
114
|
onOpenShortcuts,
|
|
115
|
+
annotationTool,
|
|
116
|
+
onToggleAnnotationMode,
|
|
117
|
+
onToggleAnnotationEraser,
|
|
111
118
|
}: {
|
|
112
119
|
minimapVisible: boolean;
|
|
113
120
|
onToggleMinimap: () => void;
|
|
@@ -116,6 +123,9 @@ function Toolbar({
|
|
|
116
123
|
snapshotBtnRef: { current: HTMLButtonElement | null };
|
|
117
124
|
onOpenPalette: () => void;
|
|
118
125
|
onOpenShortcuts: () => void;
|
|
126
|
+
annotationTool: AnnotationTool;
|
|
127
|
+
onToggleAnnotationMode: () => void;
|
|
128
|
+
onToggleAnnotationEraser: () => void;
|
|
119
129
|
}) {
|
|
120
130
|
const status = connectionStatus.value;
|
|
121
131
|
const hasSynced = hasInitialServerLayout.value;
|
|
@@ -278,6 +288,37 @@ function Toolbar({
|
|
|
278
288
|
|
|
279
289
|
<div class="separator" />
|
|
280
290
|
|
|
291
|
+
<ToolbarHint
|
|
292
|
+
label={annotationTool === 'pen' ? 'Stop annotating' : 'Annotate canvas'}
|
|
293
|
+
detail="Draw directly on the canvas for human-visible markup"
|
|
294
|
+
>
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
onClick={onToggleAnnotationMode}
|
|
298
|
+
aria-label={annotationTool === 'pen' ? 'Stop annotating' : 'Annotate canvas'}
|
|
299
|
+
aria-pressed={annotationTool === 'pen'}
|
|
300
|
+
style={{ color: annotationTool === 'pen' ? 'var(--c-accent)' : undefined }}
|
|
301
|
+
>
|
|
302
|
+
<IconPen />
|
|
303
|
+
</button>
|
|
304
|
+
</ToolbarHint>
|
|
305
|
+
<ToolbarHint
|
|
306
|
+
label={annotationTool === 'eraser' ? 'Stop erasing' : 'Erase annotations'}
|
|
307
|
+
detail="Click a drawn annotation to remove it"
|
|
308
|
+
>
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
onClick={onToggleAnnotationEraser}
|
|
312
|
+
aria-label={annotationTool === 'eraser' ? 'Stop erasing' : 'Erase annotations'}
|
|
313
|
+
aria-pressed={annotationTool === 'eraser'}
|
|
314
|
+
style={{ color: annotationTool === 'eraser' ? 'var(--c-accent)' : undefined }}
|
|
315
|
+
>
|
|
316
|
+
<IconEraser />
|
|
317
|
+
</button>
|
|
318
|
+
</ToolbarHint>
|
|
319
|
+
|
|
320
|
+
<div class="separator" />
|
|
321
|
+
|
|
281
322
|
<ToolbarHint label="Search nodes and actions" shortcut={`${MOD_KEY}+K`}>
|
|
282
323
|
<button
|
|
283
324
|
type="button"
|
|
@@ -341,6 +382,7 @@ export function App() {
|
|
|
341
382
|
const [snapshotOpen, setSnapshotOpen] = useState(false);
|
|
342
383
|
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
343
384
|
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
|
385
|
+
const [annotationTool, setAnnotationTool] = useState<AnnotationTool>(null);
|
|
344
386
|
const snapshotBtnRef = useRef<HTMLButtonElement>(null);
|
|
345
387
|
const { menu, openNodeMenu, openCanvasMenu, closeMenu } = useContextMenu();
|
|
346
388
|
const hasInitialLayout = hasInitialServerLayout.value;
|
|
@@ -348,14 +390,18 @@ export function App() {
|
|
|
348
390
|
const handleToggleMinimap = useCallback(() => setMinimapVisible((v) => !v), []);
|
|
349
391
|
const handleToggleSnapshot = useCallback(() => setSnapshotOpen((v) => !v), []);
|
|
350
392
|
const handleCloseSnapshot = useCallback(() => setSnapshotOpen(false), []);
|
|
393
|
+
const handleToggleAnnotationMode = useCallback(() => setAnnotationTool((tool) => tool === 'pen' ? null : 'pen'), []);
|
|
394
|
+
const handleToggleAnnotationEraser = useCallback(() => setAnnotationTool((tool) => tool === 'eraser' ? null : 'eraser'), []);
|
|
351
395
|
|
|
352
396
|
const handleMinimapNavigate = useCallback((x: number, y: number) => {
|
|
353
397
|
animateViewport({ x, y, scale: viewport.value.scale }, 200);
|
|
354
398
|
}, []);
|
|
355
399
|
|
|
356
400
|
useEffect(() => {
|
|
357
|
-
|
|
401
|
+
return connectSSE();
|
|
402
|
+
}, []);
|
|
358
403
|
|
|
404
|
+
useEffect(() => {
|
|
359
405
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
360
406
|
const mod = e.metaKey || e.ctrlKey;
|
|
361
407
|
|
|
@@ -366,6 +412,13 @@ export function App() {
|
|
|
366
412
|
return;
|
|
367
413
|
}
|
|
368
414
|
|
|
415
|
+
// Esc exits annotation tools before handling overlays or selection.
|
|
416
|
+
if (e.key === 'Escape' && annotationTool) {
|
|
417
|
+
e.preventDefault();
|
|
418
|
+
setAnnotationTool(null);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
369
422
|
// Esc always collapses expanded node first (even from inside inputs)
|
|
370
423
|
if (e.key === 'Escape' && expandedNodeId.value && !pendingExpandedNodeCloseId.value) {
|
|
371
424
|
e.preventDefault();
|
|
@@ -428,10 +481,9 @@ export function App() {
|
|
|
428
481
|
|
|
429
482
|
document.addEventListener('keydown', handleKeyDown);
|
|
430
483
|
return () => {
|
|
431
|
-
disconnect();
|
|
432
484
|
document.removeEventListener('keydown', handleKeyDown);
|
|
433
485
|
};
|
|
434
|
-
}, [closeMenu, paletteOpen, shortcutsOpen]);
|
|
486
|
+
}, [annotationTool, closeMenu, paletteOpen, shortcutsOpen]);
|
|
435
487
|
|
|
436
488
|
useEffect(() => {
|
|
437
489
|
if (!hasInitialLayout) return;
|
|
@@ -465,6 +517,9 @@ export function App() {
|
|
|
465
517
|
snapshotBtnRef={snapshotBtnRef}
|
|
466
518
|
onOpenPalette={() => setPaletteOpen(true)}
|
|
467
519
|
onOpenShortcuts={() => setShortcutsOpen((v) => !v)}
|
|
520
|
+
annotationTool={annotationTool}
|
|
521
|
+
onToggleAnnotationMode={handleToggleAnnotationMode}
|
|
522
|
+
onToggleAnnotationEraser={handleToggleAnnotationEraser}
|
|
468
523
|
/>
|
|
469
524
|
<div class="hud-right">
|
|
470
525
|
{dockedRight.map((n) => (
|
|
@@ -477,6 +532,8 @@ export function App() {
|
|
|
477
532
|
<CanvasViewport
|
|
478
533
|
onNodeContextMenu={openNodeMenu}
|
|
479
534
|
onCanvasContextMenu={openCanvasMenu}
|
|
535
|
+
annotationMode={annotationTool !== null}
|
|
536
|
+
annotationTool={annotationTool}
|
|
480
537
|
/>
|
|
481
538
|
{hasInitialLayout && allNodes.filter((n) => !n.dockPosition).length === 0 && (
|
|
482
539
|
<WelcomeCard onOpenPalette={() => setPaletteOpen(true)} />
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CanvasAnnotation, CanvasAnnotationPoint } from '../types';
|
|
2
|
+
|
|
3
|
+
function pointsToPath(points: CanvasAnnotationPoint[]): string {
|
|
4
|
+
const [first, ...rest] = points;
|
|
5
|
+
if (!first) return '';
|
|
6
|
+
return rest.reduce((path, point) => `${path} L ${point.x} ${point.y}`, `M ${first.x} ${first.y}`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function AnnotationLayer({ annotations }: { annotations: CanvasAnnotation[] }) {
|
|
10
|
+
if (annotations.length === 0) return null;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<svg class="annotation-layer" aria-hidden="true">
|
|
14
|
+
{annotations.map((annotation) => (
|
|
15
|
+
<path
|
|
16
|
+
key={annotation.id}
|
|
17
|
+
d={pointsToPath(annotation.points)}
|
|
18
|
+
fill="none"
|
|
19
|
+
stroke={annotation.color === 'currentColor' ? 'var(--c-annotation)' : annotation.color}
|
|
20
|
+
stroke-width={annotation.width}
|
|
21
|
+
stroke-linecap="round"
|
|
22
|
+
stroke-linejoin="round"
|
|
23
|
+
opacity="0.9"
|
|
24
|
+
/>
|
|
25
|
+
))}
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
}
|