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.
Files changed (53) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +591 -0
  2. package/CHANGELOG.md +140 -0
  3. package/Readme.md +40 -8
  4. package/dist/canvas/global.css +36 -3
  5. package/dist/canvas/index.js +54 -54
  6. package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -0
  7. package/dist/types/client/nodes/iframe-document-url.d.ts +8 -0
  8. package/dist/types/client/state/intent-bridge.d.ts +4 -0
  9. package/dist/types/client/types.d.ts +1 -0
  10. package/dist/types/json-render/catalog.d.ts +1 -1
  11. package/dist/types/mcp/canvas-access.d.ts +9 -0
  12. package/dist/types/server/ax-context.d.ts +3 -0
  13. package/dist/types/server/ax-state.d.ts +43 -0
  14. package/dist/types/server/canvas-db.d.ts +38 -0
  15. package/dist/types/server/canvas-state.d.ts +36 -16
  16. package/dist/types/server/index.d.ts +6 -0
  17. package/dist/types/server/mutation-history.d.ts +1 -1
  18. package/docs/cli.md +13 -0
  19. package/docs/http-api.md +24 -0
  20. package/docs/mcp.md +20 -2
  21. package/docs/plans/plan-004-pmx-ax-primitives.md +463 -0
  22. package/docs/screenshot.png +0 -0
  23. package/docs/sdk.md +5 -0
  24. package/package.json +3 -2
  25. package/skills/pmx-canvas/SKILL.md +22 -4
  26. package/skills/pmx-canvas/references/codex-app-adapter.md +107 -0
  27. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +111 -0
  28. package/src/cli/agent.ts +34 -0
  29. package/src/cli/index.ts +2 -1
  30. package/src/client/App.tsx +2 -0
  31. package/src/client/canvas/CanvasNode.tsx +7 -0
  32. package/src/client/canvas/CommandPalette.tsx +2 -1
  33. package/src/client/canvas/use-node-drag.ts +29 -7
  34. package/src/client/canvas/use-node-resize.ts +27 -7
  35. package/src/client/nodes/ExtAppFrame.tsx +51 -10
  36. package/src/client/nodes/HtmlNode.tsx +5 -2
  37. package/src/client/nodes/iframe-document-url.ts +58 -0
  38. package/src/client/state/intent-bridge.ts +8 -0
  39. package/src/client/state/sse-bridge.ts +2 -2
  40. package/src/client/theme/global.css +36 -3
  41. package/src/client/types.ts +1 -0
  42. package/src/mcp/canvas-access.ts +38 -0
  43. package/src/mcp/server.ts +113 -4
  44. package/src/server/ax-context.ts +38 -0
  45. package/src/server/ax-state.ts +130 -0
  46. package/src/server/canvas-db.ts +745 -0
  47. package/src/server/canvas-operations.ts +80 -1
  48. package/src/server/canvas-schema.ts +3 -3
  49. package/src/server/canvas-state.ts +390 -50
  50. package/src/server/canvas-validation.ts +6 -0
  51. package/src/server/index.ts +18 -0
  52. package/src/server/mutation-history.ts +1 -0
  53. 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.' },