pmx-canvas 0.1.22 → 0.1.24
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/.github/extensions/pmx-canvas/extension.mjs +591 -0
- package/CHANGELOG.md +140 -0
- package/Readme.md +40 -8
- package/dist/canvas/global.css +36 -3
- package/dist/canvas/index.js +54 -54
- package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -0
- package/dist/types/client/nodes/iframe-document-url.d.ts +8 -0
- package/dist/types/client/state/intent-bridge.d.ts +4 -0
- package/dist/types/client/types.d.ts +1 -0
- package/dist/types/json-render/catalog.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +9 -0
- package/dist/types/server/ax-context.d.ts +3 -0
- package/dist/types/server/ax-state.d.ts +43 -0
- package/dist/types/server/canvas-db.d.ts +38 -0
- package/dist/types/server/canvas-state.d.ts +36 -16
- package/dist/types/server/index.d.ts +6 -0
- package/dist/types/server/mutation-history.d.ts +1 -1
- package/docs/cli.md +13 -0
- package/docs/http-api.md +24 -0
- package/docs/mcp.md +20 -2
- package/docs/plans/plan-004-pmx-ax-primitives.md +463 -0
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +5 -0
- package/package.json +3 -2
- package/skills/pmx-canvas/SKILL.md +22 -4
- package/skills/pmx-canvas/references/codex-app-adapter.md +107 -0
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +111 -0
- package/src/cli/agent.ts +34 -0
- package/src/cli/index.ts +2 -1
- package/src/client/App.tsx +2 -0
- package/src/client/canvas/CanvasNode.tsx +7 -0
- package/src/client/canvas/CommandPalette.tsx +2 -1
- package/src/client/canvas/use-node-drag.ts +29 -7
- package/src/client/canvas/use-node-resize.ts +27 -7
- package/src/client/nodes/ExtAppFrame.tsx +51 -10
- package/src/client/nodes/HtmlNode.tsx +5 -2
- package/src/client/nodes/iframe-document-url.ts +58 -0
- package/src/client/state/intent-bridge.ts +8 -0
- package/src/client/state/sse-bridge.ts +2 -2
- package/src/client/theme/global.css +36 -3
- package/src/client/types.ts +1 -0
- package/src/mcp/canvas-access.ts +38 -0
- package/src/mcp/server.ts +113 -4
- package/src/server/ax-context.ts +38 -0
- package/src/server/ax-state.ts +130 -0
- package/src/server/canvas-db.ts +745 -0
- package/src/server/canvas-operations.ts +80 -1
- package/src/server/canvas-schema.ts +3 -3
- package/src/server/canvas-state.ts +390 -50
- package/src/server/canvas-validation.ts +6 -0
- package/src/server/index.ts +18 -0
- package/src/server/mutation-history.ts +1 -0
- package/src/server/server.ts +197 -11
|
@@ -1032,7 +1032,7 @@ export function removeCanvasNode(id: string): {
|
|
|
1032
1032
|
}
|
|
1033
1033
|
|
|
1034
1034
|
function isArrangeLocked(node: CanvasNodeState): boolean {
|
|
1035
|
-
return node.pinned || node.data.arrangeLocked === true;
|
|
1035
|
+
return node.pinned || node.dockPosition !== null || node.data.arrangeLocked === true;
|
|
1036
1036
|
}
|
|
1037
1037
|
|
|
1038
1038
|
function collectArrangeExcludedNodeIds(nodes: CanvasNodeState[]): Set<string> {
|
|
@@ -1099,6 +1099,82 @@ function collectGridArrangeExcludedNodeIds(nodes: CanvasNodeState[]): Set<string
|
|
|
1099
1099
|
return excluded;
|
|
1100
1100
|
}
|
|
1101
1101
|
|
|
1102
|
+
interface ArrangeObstacleRect {
|
|
1103
|
+
id: string;
|
|
1104
|
+
position: { x: number; y: number };
|
|
1105
|
+
size: { width: number; height: number };
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const GRID_OBSTACLE_GAP_Y = 72;
|
|
1109
|
+
|
|
1110
|
+
function rectsOverlap(a: ArrangeObstacleRect, b: ArrangeObstacleRect): boolean {
|
|
1111
|
+
return (
|
|
1112
|
+
a.position.x < b.position.x + b.size.width &&
|
|
1113
|
+
a.position.x + a.size.width > b.position.x &&
|
|
1114
|
+
a.position.y < b.position.y + b.size.height &&
|
|
1115
|
+
a.position.y + a.size.height > b.position.y
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function collectGridArrangeObstacles(nodes: CanvasNodeState[], excludedIds: Set<string>): ArrangeObstacleRect[] {
|
|
1120
|
+
return nodes
|
|
1121
|
+
.filter((node) => excludedIds.has(node.id) && node.dockPosition === null)
|
|
1122
|
+
.map((node) => ({
|
|
1123
|
+
id: node.id,
|
|
1124
|
+
position: { ...node.position },
|
|
1125
|
+
size: { ...node.size },
|
|
1126
|
+
}));
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function buildUpdatedArrangeRect(
|
|
1130
|
+
update: CanvasNodeUpdate,
|
|
1131
|
+
nodesById: Map<string, CanvasNodeState>,
|
|
1132
|
+
): ArrangeObstacleRect | null {
|
|
1133
|
+
const node = nodesById.get(update.id);
|
|
1134
|
+
if (!node) return null;
|
|
1135
|
+
return {
|
|
1136
|
+
id: update.id,
|
|
1137
|
+
position: update.position ? { ...update.position } : { ...node.position },
|
|
1138
|
+
size: update.size ? { ...update.size } : { ...node.size },
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function shiftGridUpdatesBelowObstacles(
|
|
1143
|
+
updates: CanvasNodeUpdate[],
|
|
1144
|
+
nodes: CanvasNodeState[],
|
|
1145
|
+
obstacles: ArrangeObstacleRect[],
|
|
1146
|
+
): CanvasNodeUpdate[] {
|
|
1147
|
+
if (updates.length === 0 || obstacles.length === 0) return updates;
|
|
1148
|
+
|
|
1149
|
+
// Grid arrange only sees movable nodes, so preserved locked/docked-group frames
|
|
1150
|
+
// need a separate obstacle pass before applying the planned positions.
|
|
1151
|
+
const nodesById = new Map(nodes.map((node) => [node.id, node]));
|
|
1152
|
+
let shifted = updates;
|
|
1153
|
+
|
|
1154
|
+
for (let attempt = 0; attempt <= obstacles.length; attempt++) {
|
|
1155
|
+
const plannedRects = shifted
|
|
1156
|
+
.map((update) => buildUpdatedArrangeRect(update, nodesById))
|
|
1157
|
+
.filter((rect): rect is ArrangeObstacleRect => rect !== null);
|
|
1158
|
+
if (plannedRects.length === 0) return shifted;
|
|
1159
|
+
|
|
1160
|
+
const blockers = obstacles.filter((obstacle) =>
|
|
1161
|
+
plannedRects.some((rect) => rect.id !== obstacle.id && rectsOverlap(rect, obstacle)),
|
|
1162
|
+
);
|
|
1163
|
+
if (blockers.length === 0) return shifted;
|
|
1164
|
+
|
|
1165
|
+
const minPlannedY = Math.min(...plannedRects.map((rect) => rect.position.y));
|
|
1166
|
+
const blockerBottom = Math.max(...blockers.map((rect) => rect.position.y + rect.size.height));
|
|
1167
|
+
const deltaY = blockerBottom + GRID_OBSTACLE_GAP_Y - minPlannedY;
|
|
1168
|
+
if (deltaY <= 0) return shifted;
|
|
1169
|
+
|
|
1170
|
+
shifted = shifted.map((update) => update.position
|
|
1171
|
+
? { ...update, position: { x: update.position.x, y: update.position.y + deltaY } }
|
|
1172
|
+
: update);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
return shifted;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1102
1178
|
export function arrangeCanvasNodes(layout: CanvasArrangeMode): { arranged: number; layout: CanvasArrangeMode } {
|
|
1103
1179
|
const nodes = canvasState.getLayout().nodes;
|
|
1104
1180
|
const excludedIds = layout === 'grid'
|
|
@@ -1135,6 +1211,9 @@ export function arrangeCanvasNodes(layout: CanvasArrangeMode): { arranged: numbe
|
|
|
1135
1211
|
size: { width: bounds.width, height: bounds.height },
|
|
1136
1212
|
});
|
|
1137
1213
|
}
|
|
1214
|
+
const obstacles = collectGridArrangeObstacles(nodes, excludedIds);
|
|
1215
|
+
const shiftedUpdates = shiftGridUpdatesBelowObstacles(updates, nodes, obstacles);
|
|
1216
|
+
updates.splice(0, updates.length, ...shiftedUpdates);
|
|
1138
1217
|
}
|
|
1139
1218
|
|
|
1140
1219
|
canvasState.withSuppressedRecording(() => {
|
|
@@ -245,10 +245,10 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
245
245
|
{ name: 'html', type: 'string', required: false, description: 'HTML document or fragment rendered in the sandboxed iframe.', aliases: ['content', 'stdin'] },
|
|
246
246
|
{ name: 'summary', type: 'string', required: false, description: 'Explicit agent-readable summary. If omitted, PMX derives one from visible HTML text.' },
|
|
247
247
|
{ name: 'agentSummary', type: 'string', required: false, description: 'Explicit semantic sidecar used by search, pinned context, and spatial context.', aliases: ['agent-summary'] },
|
|
248
|
-
{ name: 'embeddedNodeIds', type: 'string[]', required: false, description: 'Canvas node IDs represented or iframe-embedded by this HTML surface.', aliases: ['embedded-node-id'] },
|
|
249
|
-
{ name: 'embeddedUrls', type: 'string[]', required: false, description: 'URLs represented or iframe-embedded by this HTML surface.', aliases: ['embedded-url'] },
|
|
248
|
+
{ name: 'embeddedNodeIds', type: 'string[]', required: false, description: 'Canvas node IDs represented or iframe-embedded by this HTML surface.', aliases: ['embedded-node-id', 'embedded-node-ids'] },
|
|
249
|
+
{ name: 'embeddedUrls', type: 'string[]', required: false, description: 'URLs represented or iframe-embedded by this HTML surface.', aliases: ['embedded-url', 'embedded-urls'] },
|
|
250
250
|
{ name: 'presentation', type: 'boolean', required: false, description: 'Marks this HTML surface as a fullscreen presentation/deck.' },
|
|
251
|
-
{ name: 'slideTitles', type: 'string[]', required: false, description: 'Agent-readable slide titles for presentation HTML.', aliases: ['slide-title'] },
|
|
251
|
+
{ name: 'slideTitles', type: 'string[]', required: false, description: 'Agent-readable slide titles for presentation HTML.', aliases: ['slide-title', 'slide-titles'] },
|
|
252
252
|
{ name: 'primitive', type: 'HtmlPrimitiveKind', required: false, description: 'Generate HTML from a built-in communication primitive instead of passing raw HTML.', aliases: ['kind'] },
|
|
253
253
|
{ name: 'data', type: 'record<string, unknown>', required: false, description: 'Primitive data when --primitive is used, or arbitrary node metadata.' },
|
|
254
254
|
{ name: 'title', type: 'string', required: false, description: 'Optional node title.' },
|