pmx-canvas 0.1.25 → 0.1.27

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 (63) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +191 -0
  2. package/CHANGELOG.md +116 -0
  3. package/Readme.md +74 -27
  4. package/dist/canvas/index.js +82 -82
  5. package/dist/json-render/index.css +1 -1
  6. package/dist/json-render/index.js +944 -164
  7. package/dist/types/json-render/catalog.d.ts +195 -20
  8. package/dist/types/json-render/charts/components.d.ts +7 -0
  9. package/dist/types/json-render/charts/definitions.d.ts +13 -1
  10. package/dist/types/json-render/charts/tufte-components.d.ts +65 -0
  11. package/dist/types/json-render/charts/tufte-definitions.d.ts +164 -0
  12. package/dist/types/json-render/directives.d.ts +23 -0
  13. package/dist/types/json-render/renderer/index.d.ts +1 -0
  14. package/dist/types/json-render/server.d.ts +32 -1
  15. package/dist/types/mcp/canvas-access.d.ts +62 -0
  16. package/dist/types/server/ax-state.d.ts +170 -0
  17. package/dist/types/server/canvas-db.d.ts +17 -1
  18. package/dist/types/server/canvas-operations.d.ts +45 -0
  19. package/dist/types/server/canvas-schema.d.ts +5 -1
  20. package/dist/types/server/canvas-state.d.ts +95 -4
  21. package/dist/types/server/index.d.ts +118 -2
  22. package/dist/types/server/mutation-history.d.ts +1 -1
  23. package/docs/cli.md +42 -0
  24. package/docs/http-api.md +64 -0
  25. package/docs/mcp.md +23 -5
  26. package/docs/node-types.md +1 -1
  27. package/docs/screenshots/codex-app.png +0 -0
  28. package/docs/screenshots/github-copilot-app.png +0 -0
  29. package/docs/sdk.md +19 -1
  30. package/package.json +10 -7
  31. package/skills/control-session-orchestrator/SKILL.md +359 -0
  32. package/skills/control-session-orchestrator/evals/evals.json +75 -0
  33. package/skills/data-analysis/SKILL.md +6 -0
  34. package/skills/pmx-canvas/SKILL.md +63 -4
  35. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +6 -0
  36. package/skills/tufte-viz/SKILL.md +157 -0
  37. package/skills/tufte-viz/references/analytical-design.md +217 -0
  38. package/skills/tufte-viz/references/tufte-principles.md +147 -0
  39. package/src/cli/agent.ts +280 -2
  40. package/src/cli/index.ts +2 -1
  41. package/src/client/nodes/ExtAppFrame.tsx +23 -1
  42. package/src/client/nodes/McpAppNode.tsx +6 -2
  43. package/src/json-render/catalog.ts +22 -1
  44. package/src/json-render/charts/components.tsx +97 -10
  45. package/src/json-render/charts/definitions.ts +19 -2
  46. package/src/json-render/charts/extra-components.tsx +5 -4
  47. package/src/json-render/charts/tufte-components.tsx +383 -0
  48. package/src/json-render/charts/tufte-definitions.ts +128 -0
  49. package/src/json-render/directives.ts +29 -0
  50. package/src/json-render/renderer/index.css +101 -0
  51. package/src/json-render/renderer/index.tsx +33 -0
  52. package/src/json-render/server.ts +257 -5
  53. package/src/mcp/canvas-access.ts +261 -0
  54. package/src/mcp/server.ts +500 -7
  55. package/src/server/ax-context.ts +8 -3
  56. package/src/server/ax-state.ts +447 -0
  57. package/src/server/canvas-db.ts +184 -1
  58. package/src/server/canvas-operations.ts +107 -0
  59. package/src/server/canvas-schema.ts +26 -3
  60. package/src/server/canvas-state.ts +349 -2
  61. package/src/server/index.ts +250 -2
  62. package/src/server/mutation-history.ts +6 -0
  63. package/src/server/server.ts +428 -2
package/src/mcp/server.ts CHANGED
@@ -100,7 +100,7 @@ function sleep(ms: number): Promise<void> {
100
100
  return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
101
101
  }
102
102
 
103
- function sendCanvasResourceNotifications(type: 'nodes' | 'pins' | 'ax' = 'nodes'): void {
103
+ function sendCanvasResourceNotifications(type: 'nodes' | 'pins' | 'ax' | 'ax-timeline' = 'nodes'): void {
104
104
  const server = resourceNotificationServer;
105
105
  if (!server) return;
106
106
  try {
@@ -110,6 +110,12 @@ function sendCanvasResourceNotifications(type: 'nodes' | 'pins' | 'ax' = 'nodes'
110
110
  if (type === 'pins' || type === 'ax') {
111
111
  server.server.sendResourceUpdated({ uri: 'canvas://ax' });
112
112
  server.server.sendResourceUpdated({ uri: 'canvas://ax-context' });
113
+ server.server.sendResourceUpdated({ uri: 'canvas://ax-timeline' });
114
+ server.server.sendResourceUpdated({ uri: 'canvas://ax-work' });
115
+ }
116
+ if (type === 'ax-timeline') {
117
+ server.server.sendResourceUpdated({ uri: 'canvas://ax-timeline' });
118
+ server.server.sendResourceUpdated({ uri: 'canvas://ax-context' });
113
119
  }
114
120
  server.server.sendResourceUpdated({ uri: 'canvas://layout' });
115
121
  server.server.sendResourceUpdated({ uri: 'canvas://summary' });
@@ -126,7 +132,13 @@ function handleRemoteSseFrame(frame: string): void {
126
132
  const event = eventLine?.slice('event: '.length).trim() ?? '';
127
133
  if (!event || event === 'connected' || event === 'ping') return;
128
134
  sendCanvasResourceNotifications(
129
- event === 'context-pins-changed' ? 'pins' : event === 'ax-state-changed' ? 'ax' : 'nodes',
135
+ event === 'context-pins-changed'
136
+ ? 'pins'
137
+ : event === 'ax-state-changed'
138
+ ? 'ax'
139
+ : event === 'ax-event-created'
140
+ ? 'ax-timeline'
141
+ : 'nodes',
130
142
  );
131
143
  }
132
144
 
@@ -363,6 +375,10 @@ export async function startMcpServer(): Promise<void> {
363
375
  width: z.number().optional().describe('Width in pixels (default: 720)'),
364
376
  height: z.number().optional().describe('Height in pixels (default: 600)'),
365
377
  strictSize: z.boolean().optional().describe('Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting'),
378
+ children: z.array(z.string()).optional().describe('Group-only alias for childIds. Node IDs to include in a generic group node.'),
379
+ childIds: z.array(z.string()).optional().describe('Group-only field. Node IDs to include in a generic group node. Prefer canvas_create_group for groups.'),
380
+ childLayout: z.enum(['grid', 'column', 'flow']).optional().describe('Group-only optional layout for grouped children.'),
381
+ color: z.string().optional().describe('Group-only frame accent color.'),
366
382
  toolName: z.string().optional().describe('Trace node tool or operation label'),
367
383
  category: z.string().optional().describe('Trace node category: mcp, file, subagent, or other'),
368
384
  status: z.string().optional().describe('Trace node status: running, success, or failed'),
@@ -411,7 +427,7 @@ export async function startMcpServer(): Promise<void> {
411
427
  'canvas_add_html_node',
412
428
  'Add a normal html node: a self-contained HTML document (with optional inline <script> and CDN <script src="...">) rendered inside a sandboxed iframe (sandbox="allow-scripts"). This is the default HTML surface for reports, widgets, and bespoke visualizations. Presentation mode is opt-in: only pass presentation:true when the user explicitly asks for a deck/fullscreen presentation, or use canvas_add_html_primitive with kind="presentation". The iframe inherits live canvas theme tokens via injected CSS custom properties (both --c-* and common --color-* aliases) so authored HTML using var(--color-text-secondary), var(--color-bg), etc. renders cohesively. No same-origin access; no top-navigation; no forms. For declarative-only views with zero JS, prefer canvas_add_json_render_node. For React + shadcn + routing or multi-component apps, use canvas_build_web_artifact.',
413
429
  {
414
- html: z.string().describe('HTML document or fragment. Full <html>...</html> documents are passed through with theme styles injected into <head>; bare fragments are wrapped in a minimal document. Inline <script> and remote CDN <script src="..."> are allowed.'),
430
+ html: z.string().describe('HTML document or fragment. Full <html>...</html> documents are passed through with theme styles injected into <head>; bare fragments are wrapped in a minimal document. Inline <script> and remote CDN <script src="..."> are allowed. If this is a bare path to an existing local .html/.htm file, the file contents are read and used as the HTML.'),
415
431
  title: z.string().optional().describe('Node title shown in the canvas titlebar.'),
416
432
  summary: z.string().optional().describe('Agent-readable semantic summary for this HTML node. If omitted, PMX derives one from visible HTML text.'),
417
433
  agentSummary: z.string().optional().describe('Explicit agent-readable summary. Alias for summary with higher priority when both are provided.'),
@@ -621,7 +637,7 @@ export async function startMcpServer(): Promise<void> {
621
637
  yKey: z.string().optional().describe('Y-axis key for line/bar graphs'),
622
638
  zKey: z.string().optional().describe('Optional bubble-size key for scatter charts'),
623
639
  nameKey: z.string().optional().describe('Slice name key for pie graphs'),
624
- valueKey: z.string().optional().describe('Slice value key for pie graphs'),
640
+ valueKey: z.string().optional().describe('Value key for pie slices, sparkline, dot-plot, and the bullet measure'),
625
641
  axisKey: z.string().optional().describe('Category key for radar charts'),
626
642
  metrics: z.array(z.string()).optional().describe('Series keys to plot as radar polygons'),
627
643
  series: z.array(z.string()).optional().describe('Series keys for stacked-bar segments'),
@@ -629,8 +645,23 @@ export async function startMcpServer(): Promise<void> {
629
645
  lineKey: z.string().optional().describe('Line series key for composed charts'),
630
646
  aggregate: z.enum(['sum', 'count', 'avg']).optional().describe('Optional aggregation for repeated keys'),
631
647
  color: z.string().optional().describe('Optional graph color'),
648
+ colorBy: z.enum(['series', 'category', 'value', 'none']).optional().describe("Bar charts only: how bars are colored (default 'series')"),
649
+ highlight: z.union([z.number(), z.enum(['max', 'min'])]).nullable().optional().describe("Bar charts only, colorBy='series': which bar gets the accent"),
632
650
  barColor: z.string().optional().describe('Optional bar color for composed charts'),
633
651
  lineColor: z.string().optional().describe('Optional line color for composed charts'),
652
+ labelKey: z.string().optional().describe('Category label key for dot-plot / bullet / slopegraph rows'),
653
+ targetKey: z.string().optional().describe('Per-row target value key for bullet charts'),
654
+ rangesKey: z.string().optional().describe('Per-row qualitative band thresholds key (number[]) for bullet charts'),
655
+ beforeKey: z.string().optional().describe('Left-column value key for slopegraph'),
656
+ afterKey: z.string().optional().describe('Right-column value key for slopegraph'),
657
+ beforeLabel: z.string().optional().describe('Header label for the slopegraph left column'),
658
+ afterLabel: z.string().optional().describe('Header label for the slopegraph right column'),
659
+ sort: z.enum(['asc', 'desc', 'none']).optional().describe('Row sort order for dot-plot (defaults to desc)'),
660
+ fill: z.boolean().optional().describe('Sparkline: draw a light area fill under the line'),
661
+ showEndDot: z.boolean().optional().describe('Sparkline: draw a dot at the last point (default true)'),
662
+ showMinMax: z.boolean().optional().describe('Sparkline: mark the min and max points'),
663
+ showValue: z.boolean().optional().describe('Sparkline: print the last value inline'),
664
+ colorByDirection: z.boolean().optional().describe('Slopegraph: accent rising lines and mute falling ones (default off)'),
634
665
  height: z.number().optional().describe('Optional graph content height'),
635
666
  },
636
667
  async (input) => {
@@ -667,8 +698,23 @@ export async function startMcpServer(): Promise<void> {
667
698
  ...(typeof input.lineKey === 'string' ? { lineKey: input.lineKey } : {}),
668
699
  ...(typeof input.aggregate === 'string' ? { aggregate: input.aggregate } : {}),
669
700
  ...(typeof input.color === 'string' ? { color: input.color } : {}),
701
+ ...(typeof input.colorBy === 'string' ? { colorBy: input.colorBy } : {}),
702
+ ...(input.highlight !== undefined ? { highlight: input.highlight } : {}),
670
703
  ...(typeof input.barColor === 'string' ? { barColor: input.barColor } : {}),
671
704
  ...(typeof input.lineColor === 'string' ? { lineColor: input.lineColor } : {}),
705
+ ...(typeof input.labelKey === 'string' ? { labelKey: input.labelKey } : {}),
706
+ ...(typeof input.targetKey === 'string' ? { targetKey: input.targetKey } : {}),
707
+ ...(typeof input.rangesKey === 'string' ? { rangesKey: input.rangesKey } : {}),
708
+ ...(typeof input.beforeKey === 'string' ? { beforeKey: input.beforeKey } : {}),
709
+ ...(typeof input.afterKey === 'string' ? { afterKey: input.afterKey } : {}),
710
+ ...(typeof input.beforeLabel === 'string' ? { beforeLabel: input.beforeLabel } : {}),
711
+ ...(typeof input.afterLabel === 'string' ? { afterLabel: input.afterLabel } : {}),
712
+ ...(typeof input.sort === 'string' ? { sort: input.sort } : {}),
713
+ ...(typeof input.fill === 'boolean' ? { fill: input.fill } : {}),
714
+ ...(typeof input.showEndDot === 'boolean' ? { showEndDot: input.showEndDot } : {}),
715
+ ...(typeof input.showMinMax === 'boolean' ? { showMinMax: input.showMinMax } : {}),
716
+ ...(typeof input.showValue === 'boolean' ? { showValue: input.showValue } : {}),
717
+ ...(typeof input.colorByDirection === 'boolean' ? { colorByDirection: input.colorByDirection } : {}),
672
718
  ...(typeof input.height === 'number' ? { height: input.height } : {}),
673
719
  },
674
720
  });
@@ -827,19 +873,74 @@ export async function startMcpServer(): Promise<void> {
827
873
  },
828
874
  );
829
875
 
876
+ // ── canvas_stream_json_render_node ────────────────────────
877
+ server.tool(
878
+ 'canvas_stream_json_render_node',
879
+ 'Progressively build a json-render node by streaming SpecStream patches, so a panel fills in live as you generate it. Omit nodeId on the first call to create a new streaming node (returns its id); pass that same nodeId on later calls to append more patches; set done=true on the final call. Each call updates the live node. Patches are JSON-Patch operations, e.g. {"op":"add","path":"/elements/card","value":{"type":"Card","props":{"title":"Live"},"children":[]}}, {"op":"replace","path":"/root","value":"card"}, {"op":"add","path":"/elements/card/children/-","value":"row1"}. Build the spec incrementally: set /root, add container elements, then append children. The server accumulates the spec (it is the source of truth); partial specs render what they can.',
880
+ {
881
+ nodeId: z.string().optional().describe('Existing streaming node id to append to; omit to create a new streaming node'),
882
+ title: z.string().optional().describe('Title when creating a new streaming node'),
883
+ patches: z
884
+ .array(z.union([z.string(), z.record(z.string(), z.unknown())]))
885
+ .optional()
886
+ .describe('SpecStream patches to apply this call: JSON-Patch objects ({op,path,value}) or raw JSONL patch lines'),
887
+ done: z.boolean().optional().describe('Set true on the final call to mark the stream complete'),
888
+ x: z.number().optional().describe('Optional X position (new node)'),
889
+ y: z.number().optional().describe('Optional Y position (new node)'),
890
+ width: z.number().optional().describe('Optional node width (new node)'),
891
+ nodeHeight: z.number().optional().describe('Optional node height (new node)'),
892
+ strictSize: z.boolean().optional().describe('Keep explicit node size fixed and scroll overflowing content (new node)'),
893
+ },
894
+ async (input) => {
895
+ const c = await ensureCanvas();
896
+ try {
897
+ const result = await c.streamJsonRenderNode({
898
+ ...(typeof input.nodeId === 'string' ? { nodeId: input.nodeId } : {}),
899
+ ...(typeof input.title === 'string' ? { title: input.title } : {}),
900
+ ...(Array.isArray(input.patches) ? { patches: input.patches } : {}),
901
+ ...(input.done === true ? { done: true } : {}),
902
+ ...(typeof input.x === 'number' ? { x: input.x } : {}),
903
+ ...(typeof input.y === 'number' ? { y: input.y } : {}),
904
+ ...(typeof input.width === 'number' ? { width: input.width } : {}),
905
+ ...(typeof input.nodeHeight === 'number' ? { height: input.nodeHeight } : {}),
906
+ ...(input.strictSize === true ? { strictSize: true } : {}),
907
+ });
908
+ return {
909
+ content: [{
910
+ type: 'text',
911
+ text: JSON.stringify({
912
+ ...(await createdNodePayload(c, result.id)),
913
+ url: result.url,
914
+ applied: result.applied,
915
+ skipped: result.skipped,
916
+ specVersion: result.specVersion,
917
+ elementCount: result.elementCount,
918
+ streamStatus: result.streamStatus,
919
+ }, null, 2),
920
+ }],
921
+ };
922
+ } catch (error) {
923
+ return {
924
+ content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
925
+ isError: true,
926
+ };
927
+ }
928
+ },
929
+ );
930
+
830
931
  // ── canvas_add_graph_node ─────────────────────────────────
831
932
  server.tool(
832
933
  'canvas_add_graph_node',
833
- 'Create a native graph node backed by the json-render chart catalog. Supports line, bar, pie, area, scatter, radar, stacked-bar, and composed (bar+line) graphs rendered directly inside PMX Canvas.',
934
+ 'Create a native graph node backed by the json-render chart catalog. Supports line, bar, pie, area, scatter, radar, stacked-bar, composed (bar+line), sparkline, dot-plot (Cleveland), bullet (Few KPI vs target), and slopegraph (paired before/after) graphs rendered directly inside PMX Canvas.',
834
935
  {
835
936
  title: z.string().optional().describe('Optional node title'),
836
- graphType: z.string().describe('Graph type: line, bar, pie, area, scatter, radar, stacked-bar (or "stack"), composed (or "combo")'),
937
+ graphType: z.string().describe('Graph type: line, bar, pie, area, scatter, radar, stacked-bar (or "stack"), composed (or "combo"), sparkline, dot-plot (or "dot"), bullet, slopegraph (or "slope")'),
837
938
  data: z.array(z.record(z.string(), z.unknown())).describe('Array of chart data objects'),
838
939
  xKey: z.string().optional().describe('X-axis key (line/bar/area/scatter/stacked/composed)'),
839
940
  yKey: z.string().optional().describe('Y-axis key (line/bar/area/scatter); falls back to barKey for composed'),
840
941
  zKey: z.string().optional().describe('Optional bubble-size key for scatter charts'),
841
942
  nameKey: z.string().optional().describe('Name key for pie graphs'),
842
- valueKey: z.string().optional().describe('Value key for pie graphs'),
943
+ valueKey: z.string().optional().describe('Value key for pie slices, sparkline, dot-plot, and the bullet measure'),
843
944
  axisKey: z.string().optional().describe('Category key for radar charts'),
844
945
  metrics: z.array(z.string()).optional().describe('Series keys to plot as radar polygons (defaults to non-axis numeric columns)'),
845
946
  series: z.array(z.string()).optional().describe('Series keys for stacked-bar segments (defaults to non-x numeric columns)'),
@@ -847,8 +948,30 @@ export async function startMcpServer(): Promise<void> {
847
948
  lineKey: z.string().optional().describe('Line series key for composed charts'),
848
949
  aggregate: z.enum(['sum', 'count', 'avg']).optional().describe('Optional aggregation for repeated x-axis values (line/bar/area/stacked)'),
849
950
  color: z.string().optional().describe('Optional series color (line/bar/area/scatter)'),
951
+ colorBy: z
952
+ .enum(['series', 'category', 'value', 'none'])
953
+ .optional()
954
+ .describe("Bar charts only: how bars are colored. 'series' (default) = single accent with one highlighted bar; 'category' = rotate palette per bar; 'value' = shade by magnitude; 'none' = flat. Prefer 'series' — color should encode data, not decorate."),
955
+ highlight: z
956
+ .union([z.number(), z.enum(['max', 'min'])])
957
+ .nullable()
958
+ .optional()
959
+ .describe("Bar charts only, for colorBy='series': which bar gets the accent — 'max' (default), 'min', a 0-based index, or null for no emphasis."),
850
960
  barColor: z.string().optional().describe('Optional bar color for composed charts'),
851
961
  lineColor: z.string().optional().describe('Optional line color for composed charts'),
962
+ labelKey: z.string().optional().describe('Category label key for dot-plot / bullet / slopegraph rows'),
963
+ targetKey: z.string().optional().describe('Per-row target value key for bullet charts'),
964
+ rangesKey: z.string().optional().describe('Per-row qualitative band thresholds key (number[]) for bullet charts'),
965
+ beforeKey: z.string().optional().describe('Left-column value key for slopegraph'),
966
+ afterKey: z.string().optional().describe('Right-column value key for slopegraph'),
967
+ beforeLabel: z.string().optional().describe('Header label for the slopegraph left column'),
968
+ afterLabel: z.string().optional().describe('Header label for the slopegraph right column'),
969
+ sort: z.enum(['asc', 'desc', 'none']).optional().describe('Row sort order for dot-plot (defaults to desc)'),
970
+ fill: z.boolean().optional().describe('Sparkline: draw a light area fill under the line'),
971
+ showEndDot: z.boolean().optional().describe('Sparkline: draw a dot at the last point (default true)'),
972
+ showMinMax: z.boolean().optional().describe('Sparkline: mark the min and max points'),
973
+ showValue: z.boolean().optional().describe('Sparkline: print the last value inline'),
974
+ colorByDirection: z.boolean().optional().describe('Slopegraph: accent rising lines and mute falling ones (default off — lines use one neutral ink)'),
852
975
  height: z.number().optional().describe('Optional chart content height'),
853
976
  showLegend: z.boolean().optional().describe('Show chart legend when supported; pass false for compact node layouts'),
854
977
  showLabels: z.boolean().optional().describe('Show direct labels when supported, such as pie slice labels (defaults to true)'),
@@ -877,8 +1000,23 @@ export async function startMcpServer(): Promise<void> {
877
1000
  ...(typeof input.lineKey === 'string' ? { lineKey: input.lineKey } : {}),
878
1001
  ...(typeof input.aggregate === 'string' ? { aggregate: input.aggregate } : {}),
879
1002
  ...(typeof input.color === 'string' ? { color: input.color } : {}),
1003
+ ...(typeof input.colorBy === 'string' ? { colorBy: input.colorBy } : {}),
1004
+ ...(input.highlight !== undefined ? { highlight: input.highlight } : {}),
880
1005
  ...(typeof input.barColor === 'string' ? { barColor: input.barColor } : {}),
881
1006
  ...(typeof input.lineColor === 'string' ? { lineColor: input.lineColor } : {}),
1007
+ ...(typeof input.labelKey === 'string' ? { labelKey: input.labelKey } : {}),
1008
+ ...(typeof input.targetKey === 'string' ? { targetKey: input.targetKey } : {}),
1009
+ ...(typeof input.rangesKey === 'string' ? { rangesKey: input.rangesKey } : {}),
1010
+ ...(typeof input.beforeKey === 'string' ? { beforeKey: input.beforeKey } : {}),
1011
+ ...(typeof input.afterKey === 'string' ? { afterKey: input.afterKey } : {}),
1012
+ ...(typeof input.beforeLabel === 'string' ? { beforeLabel: input.beforeLabel } : {}),
1013
+ ...(typeof input.afterLabel === 'string' ? { afterLabel: input.afterLabel } : {}),
1014
+ ...(typeof input.sort === 'string' ? { sort: input.sort } : {}),
1015
+ ...(typeof input.fill === 'boolean' ? { fill: input.fill } : {}),
1016
+ ...(typeof input.showEndDot === 'boolean' ? { showEndDot: input.showEndDot } : {}),
1017
+ ...(typeof input.showMinMax === 'boolean' ? { showMinMax: input.showMinMax } : {}),
1018
+ ...(typeof input.showValue === 'boolean' ? { showValue: input.showValue } : {}),
1019
+ ...(typeof input.colorByDirection === 'boolean' ? { colorByDirection: input.colorByDirection } : {}),
882
1020
  ...(typeof input.height === 'number' ? { height: input.height } : {}),
883
1021
  ...(typeof input.showLegend === 'boolean' ? { showLegend: input.showLegend } : {}),
884
1022
  ...(typeof input.showLabels === 'boolean' ? { showLabels: input.showLabels } : {}),
@@ -1144,6 +1282,7 @@ export async function startMcpServer(): Promise<void> {
1144
1282
  async ({ includeContext }) => {
1145
1283
  const c = await ensureCanvas();
1146
1284
  const state = await c.getAxState();
1285
+ const host = await c.getHostCapability();
1147
1286
  const context = includeContext === false ? undefined : await c.getAxContext();
1148
1287
  return {
1149
1288
  content: [
@@ -1152,6 +1291,7 @@ export async function startMcpServer(): Promise<void> {
1152
1291
  text: JSON.stringify({
1153
1292
  ok: true,
1154
1293
  state,
1294
+ host,
1155
1295
  ...(context ? { context } : {}),
1156
1296
  }),
1157
1297
  },
@@ -1183,6 +1323,312 @@ export async function startMcpServer(): Promise<void> {
1183
1323
  },
1184
1324
  );
1185
1325
 
1326
+ server.tool(
1327
+ 'canvas_record_ax_event',
1328
+ 'Record a normalized AX timeline event (prompt/assistant-message/tool-start/tool-result/failure/approval/steering). Timeline events persist for diagnostics and continuity but are not restored by snapshots.',
1329
+ {
1330
+ kind: z.enum(['prompt', 'assistant-message', 'tool-start', 'tool-result', 'failure', 'approval', 'steering'])
1331
+ .describe('Normalized event kind.'),
1332
+ summary: z.string().describe('Short human-readable summary of the event.'),
1333
+ detail: z.string().optional().describe('Optional longer detail or payload text.'),
1334
+ nodeIds: z.array(z.string()).optional().describe('Optional node IDs this event relates to.'),
1335
+ data: z.record(z.string(), z.unknown()).optional().describe('Optional structured data payload.'),
1336
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
1337
+ .optional()
1338
+ .describe('Optional host/source label. Defaults to mcp.'),
1339
+ },
1340
+ async ({ kind, summary, detail, nodeIds, data, source }) => {
1341
+ const c = await ensureCanvas();
1342
+ const event = await c.recordAxEvent(
1343
+ {
1344
+ kind,
1345
+ summary,
1346
+ ...(typeof detail === 'string' ? { detail } : {}),
1347
+ ...(Array.isArray(nodeIds) ? { nodeIds } : {}),
1348
+ ...(data ? { data } : {}),
1349
+ },
1350
+ { source: source ?? 'mcp' },
1351
+ );
1352
+ return {
1353
+ content: [
1354
+ {
1355
+ type: 'text',
1356
+ text: JSON.stringify({ ok: true, event }),
1357
+ },
1358
+ ],
1359
+ };
1360
+ },
1361
+ );
1362
+
1363
+ server.tool(
1364
+ 'canvas_send_steering',
1365
+ 'Record a steering message: a user instruction from the surface to the active agent session. Persisted on the AX timeline and exposed via canvas://ax-timeline.',
1366
+ {
1367
+ message: z.string().describe('The steering instruction to deliver to the active agent session.'),
1368
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
1369
+ .optional()
1370
+ .describe('Optional host/source label. Defaults to mcp.'),
1371
+ },
1372
+ async ({ message, source }) => {
1373
+ const c = await ensureCanvas();
1374
+ const steering = await c.sendSteering(message, { source: source ?? 'mcp' });
1375
+ return {
1376
+ content: [
1377
+ {
1378
+ type: 'text',
1379
+ text: JSON.stringify({ ok: true, steering }),
1380
+ },
1381
+ ],
1382
+ };
1383
+ },
1384
+ );
1385
+
1386
+ server.tool(
1387
+ 'canvas_get_ax_timeline',
1388
+ 'Read the bounded AX timeline: recent agent-events, evidence, and steering messages plus counts. Use this for diagnostics and session continuity.',
1389
+ {
1390
+ limit: z.number().optional().describe('Max rows per timeline table (default 50, max 200).'),
1391
+ },
1392
+ async ({ limit }) => {
1393
+ const c = await ensureCanvas();
1394
+ const timeline = await c.getAxTimeline(
1395
+ typeof limit === 'number' && limit > 0 ? { limit } : undefined,
1396
+ );
1397
+ return {
1398
+ content: [
1399
+ {
1400
+ type: 'text',
1401
+ text: JSON.stringify({ ok: true, ...timeline }),
1402
+ },
1403
+ ],
1404
+ };
1405
+ },
1406
+ );
1407
+
1408
+ server.tool(
1409
+ 'canvas_add_work_item',
1410
+ 'Add a canvas-bound AX work item: a visible task/plan/status tied to nodes and agent work. Work items participate in snapshots and are exposed via canvas://ax-work.',
1411
+ {
1412
+ title: z.string().describe('Short title of the work item.'),
1413
+ status: z.enum(['todo', 'in-progress', 'blocked', 'done', 'cancelled'])
1414
+ .optional()
1415
+ .describe('Work item status. Defaults to todo.'),
1416
+ detail: z.string().optional().describe('Optional longer description.'),
1417
+ nodeIds: z.array(z.string()).optional().describe('Optional node IDs this work item is tied to.'),
1418
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
1419
+ .optional()
1420
+ .describe('Optional host/source label. Defaults to mcp.'),
1421
+ },
1422
+ async ({ title, status, detail, nodeIds, source }) => {
1423
+ const c = await ensureCanvas();
1424
+ const workItem = await c.addWorkItem(
1425
+ {
1426
+ title,
1427
+ ...(status ? { status } : {}),
1428
+ ...(typeof detail === 'string' ? { detail } : {}),
1429
+ ...(Array.isArray(nodeIds) ? { nodeIds } : {}),
1430
+ },
1431
+ { source: source ?? 'mcp' },
1432
+ );
1433
+ return {
1434
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, workItem }) }],
1435
+ };
1436
+ },
1437
+ );
1438
+
1439
+ server.tool(
1440
+ 'canvas_update_work_item',
1441
+ 'Update a canvas-bound AX work item by ID (title/status/detail/nodeIds). Returns null if the work item does not exist.',
1442
+ {
1443
+ id: z.string().describe('Work item ID to update.'),
1444
+ title: z.string().optional().describe('New title.'),
1445
+ status: z.enum(['todo', 'in-progress', 'blocked', 'done', 'cancelled'])
1446
+ .optional()
1447
+ .describe('New status.'),
1448
+ detail: z.string().optional().describe('New detail text.'),
1449
+ nodeIds: z.array(z.string()).optional().describe('Replacement node IDs.'),
1450
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
1451
+ .optional()
1452
+ .describe('Optional host/source label. Defaults to mcp.'),
1453
+ },
1454
+ async ({ id, title, status, detail, nodeIds, source }) => {
1455
+ const c = await ensureCanvas();
1456
+ const workItem = await c.updateWorkItem(
1457
+ id,
1458
+ {
1459
+ ...(typeof title === 'string' ? { title } : {}),
1460
+ ...(status ? { status } : {}),
1461
+ ...(typeof detail === 'string' ? { detail } : {}),
1462
+ ...(Array.isArray(nodeIds) ? { nodeIds } : {}),
1463
+ },
1464
+ { source: source ?? 'mcp' },
1465
+ );
1466
+ return {
1467
+ content: [{ type: 'text', text: JSON.stringify({ ok: workItem !== null, workItem }) }],
1468
+ };
1469
+ },
1470
+ );
1471
+
1472
+ server.tool(
1473
+ 'canvas_request_approval',
1474
+ 'Request human approval before a high-impact AX action: creates a pending approval gate tied to nodes. Canvas-bound and snapshotted; exposed via canvas://ax-work.',
1475
+ {
1476
+ title: z.string().describe('Short title of what needs approval.'),
1477
+ detail: z.string().optional().describe('Optional explanation of the action and its impact.'),
1478
+ action: z.string().optional().describe('Optional machine-readable action identifier the approval gates.'),
1479
+ nodeIds: z.array(z.string()).optional().describe('Optional node IDs this approval relates to.'),
1480
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
1481
+ .optional()
1482
+ .describe('Optional host/source label. Defaults to mcp.'),
1483
+ },
1484
+ async ({ title, detail, action, nodeIds, source }) => {
1485
+ const c = await ensureCanvas();
1486
+ const approvalGate = await c.requestApproval(
1487
+ {
1488
+ title,
1489
+ ...(typeof detail === 'string' ? { detail } : {}),
1490
+ ...(typeof action === 'string' ? { action } : {}),
1491
+ ...(Array.isArray(nodeIds) ? { nodeIds } : {}),
1492
+ },
1493
+ { source: source ?? 'mcp' },
1494
+ );
1495
+ return {
1496
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, approvalGate }) }],
1497
+ };
1498
+ },
1499
+ );
1500
+
1501
+ server.tool(
1502
+ 'canvas_resolve_approval',
1503
+ 'Resolve a pending approval gate by ID with approved or rejected. Returns null if the gate does not exist or is already resolved.',
1504
+ {
1505
+ id: z.string().describe('Approval gate ID to resolve.'),
1506
+ decision: z.enum(['approved', 'rejected']).describe('Approval decision.'),
1507
+ resolution: z.string().optional().describe('Optional human-readable resolution note.'),
1508
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
1509
+ .optional()
1510
+ .describe('Optional host/source label. Defaults to mcp.'),
1511
+ },
1512
+ async ({ id, decision, resolution, source }) => {
1513
+ const c = await ensureCanvas();
1514
+ const approvalGate = await c.resolveApproval(id, decision, {
1515
+ ...(typeof resolution === 'string' ? { resolution } : {}),
1516
+ source: source ?? 'mcp',
1517
+ });
1518
+ return {
1519
+ content: [{ type: 'text', text: JSON.stringify({ ok: approvalGate !== null, approvalGate }) }],
1520
+ };
1521
+ },
1522
+ );
1523
+
1524
+ server.tool(
1525
+ 'canvas_add_evidence',
1526
+ 'Record an AX evidence item (logs/tool-result/screenshot/file/diff/test-output) on the timeline. Evidence persists for diagnostics and continuity but is not restored by snapshots; exposed via canvas://ax-timeline.',
1527
+ {
1528
+ kind: z.enum(['logs', 'tool-result', 'screenshot', 'file', 'diff', 'test-output'])
1529
+ .describe('Evidence kind.'),
1530
+ title: z.string().describe('Short human-readable title for the evidence.'),
1531
+ body: z.string().optional().describe('Optional inline body/content.'),
1532
+ ref: z.string().optional().describe('Optional reference (path, URL, or external locator).'),
1533
+ nodeIds: z.array(z.string()).optional().describe('Optional node IDs this evidence relates to.'),
1534
+ data: z.record(z.string(), z.unknown()).optional().describe('Optional structured data payload.'),
1535
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
1536
+ .optional()
1537
+ .describe('Optional host/source label. Defaults to mcp.'),
1538
+ },
1539
+ async ({ kind, title, body, ref, nodeIds, data, source }) => {
1540
+ const c = await ensureCanvas();
1541
+ const evidence = await c.addEvidence(
1542
+ {
1543
+ kind,
1544
+ title,
1545
+ ...(typeof body === 'string' ? { body } : {}),
1546
+ ...(typeof ref === 'string' ? { ref } : {}),
1547
+ ...(Array.isArray(nodeIds) ? { nodeIds } : {}),
1548
+ ...(data ? { data } : {}),
1549
+ },
1550
+ { source: source ?? 'mcp' },
1551
+ );
1552
+ return {
1553
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, evidence }) }],
1554
+ };
1555
+ },
1556
+ );
1557
+
1558
+ server.tool(
1559
+ 'canvas_add_review_annotation',
1560
+ 'Add a canvas-bound review annotation: a comment or finding anchored to a node, file, or region. Review annotations participate in snapshots and are exposed via canvas://ax-work.',
1561
+ {
1562
+ body: z.string().describe('Annotation body text.'),
1563
+ kind: z.enum(['comment', 'finding']).optional().describe('Annotation kind. Default comment.'),
1564
+ severity: z.enum(['info', 'warning', 'error']).optional().describe('Severity. Default info.'),
1565
+ anchorType: z.enum(['node', 'file', 'region']).optional().describe('Anchor type. Default node.'),
1566
+ nodeId: z.string().optional().describe('Node ID when anchorType is node.'),
1567
+ file: z.string().optional().describe('File path when anchorType is file.'),
1568
+ region: z.object({
1569
+ line: z.number().optional(),
1570
+ endLine: z.number().optional(),
1571
+ label: z.string().optional(),
1572
+ }).optional().describe('Region descriptor when anchorType is region.'),
1573
+ author: z.string().optional().describe('Optional author label.'),
1574
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
1575
+ .optional()
1576
+ .describe('Optional host/source label. Defaults to mcp.'),
1577
+ },
1578
+ async ({ body, kind, severity, anchorType, nodeId, file, region, author, source }) => {
1579
+ const c = await ensureCanvas();
1580
+ const reviewAnnotation = await c.addReviewAnnotation(
1581
+ {
1582
+ body,
1583
+ ...(kind ? { kind } : {}),
1584
+ ...(severity ? { severity } : {}),
1585
+ ...(anchorType ? { anchorType } : {}),
1586
+ ...(typeof nodeId === 'string' ? { nodeId } : {}),
1587
+ ...(typeof file === 'string' ? { file } : {}),
1588
+ ...(region ? { region } : {}),
1589
+ ...(typeof author === 'string' ? { author } : {}),
1590
+ },
1591
+ { source: source ?? 'mcp' },
1592
+ );
1593
+ if (!reviewAnnotation) {
1594
+ return {
1595
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'node-anchored review annotation requires a nodeId that exists on the canvas.' }) }],
1596
+ isError: true,
1597
+ };
1598
+ }
1599
+ return {
1600
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, reviewAnnotation }) }],
1601
+ };
1602
+ },
1603
+ );
1604
+
1605
+ server.tool(
1606
+ 'canvas_report_host_capability',
1607
+ 'Report host/session capability from an adapter: what the host can do (canvas/hooks/tools/sessionMessaging/permissions/files/uiPrompts). Stored for diagnostics; core does not depend on a host.',
1608
+ {
1609
+ host: z.string().optional().describe('Host identifier (e.g. copilot, codex).'),
1610
+ canvas: z.boolean().optional(),
1611
+ hooks: z.boolean().optional(),
1612
+ tools: z.boolean().optional(),
1613
+ sessionMessaging: z.boolean().optional(),
1614
+ permissions: z.boolean().optional(),
1615
+ files: z.boolean().optional(),
1616
+ uiPrompts: z.boolean().optional(),
1617
+ raw: z.record(z.string(), z.unknown()).optional().describe('Optional raw capability payload for diagnostics.'),
1618
+ source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
1619
+ .optional()
1620
+ .describe('Optional host/source label. Defaults to mcp.'),
1621
+ },
1622
+ async (input) => {
1623
+ const c = await ensureCanvas();
1624
+ const { source, ...capability } = input;
1625
+ const host = await c.reportHostCapability(capability, { source: source ?? 'mcp' });
1626
+ return {
1627
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, host }) }],
1628
+ };
1629
+ },
1630
+ );
1631
+
1186
1632
  server.tool(
1187
1633
  'canvas_fit_view',
1188
1634
  'Fit the canvas viewport to all nodes or a selected subset. Useful before screenshots and whole-board review.',
@@ -1606,6 +2052,53 @@ export async function startMcpServer(): Promise<void> {
1606
2052
  },
1607
2053
  );
1608
2054
 
2055
+ server.resource(
2056
+ 'ax-timeline',
2057
+ 'canvas://ax-timeline',
2058
+ {
2059
+ description:
2060
+ 'Bounded PMX AX timeline: recent agent-events, evidence, and steering messages with counts. Persisted for diagnostics and continuity; not restored by snapshots.',
2061
+ mimeType: 'application/json',
2062
+ },
2063
+ async () => {
2064
+ const c = await ensureCanvas();
2065
+ const timeline = await c.getAxTimeline();
2066
+ return {
2067
+ contents: [
2068
+ {
2069
+ uri: 'canvas://ax-timeline',
2070
+ mimeType: 'application/json',
2071
+ text: JSON.stringify(timeline, null, 2),
2072
+ },
2073
+ ],
2074
+ };
2075
+ },
2076
+ );
2077
+
2078
+ server.resource(
2079
+ 'ax-work',
2080
+ 'canvas://ax-work',
2081
+ {
2082
+ description:
2083
+ 'Canvas-bound PMX AX work state: work items, approval gates, and review annotations. Participates in snapshots and restore.',
2084
+ mimeType: 'application/json',
2085
+ },
2086
+ async () => {
2087
+ const c = await ensureCanvas();
2088
+ const [workItems, approvalGates] = await Promise.all([c.listWorkItems(), c.listApprovalGates()]);
2089
+ const state = await c.getAxState();
2090
+ return {
2091
+ contents: [
2092
+ {
2093
+ uri: 'canvas://ax-work',
2094
+ mimeType: 'application/json',
2095
+ text: JSON.stringify({ workItems, approvalGates, reviewAnnotations: state.reviewAnnotations }, null, 2),
2096
+ },
2097
+ ],
2098
+ };
2099
+ },
2100
+ );
2101
+
1609
2102
  server.resource(
1610
2103
  'canvas-layout',
1611
2104
  'canvas://layout',
@@ -25,14 +25,19 @@ export function buildCanvasAxPinnedContext(): PmxAxPinnedContext {
25
25
 
26
26
  export function buildCanvasAxContext(): PmxAxContext {
27
27
  const layout = canvasState.getLayout();
28
- const focus = canvasState.getAxFocus();
29
- const focusNodes = focus.nodeIds
28
+ const ax = canvasState.getAxState();
29
+ const focusNodes = ax.focus.nodeIds
30
30
  .map((id) => canvasState.getNode(id))
31
31
  .filter((node): node is CanvasNodeState => node !== undefined);
32
32
  return buildAxContext({
33
33
  layout,
34
34
  pinned: buildCanvasAxPinnedContext(),
35
- focus,
35
+ focus: ax.focus,
36
36
  focusNodes: serializeNodes(focusNodes),
37
+ workItems: ax.workItems,
38
+ approvalGates: ax.approvalGates,
39
+ reviewAnnotations: ax.reviewAnnotations,
40
+ timeline: canvasState.getAxTimelineSummary(),
41
+ host: canvasState.getHostCapability(),
37
42
  });
38
43
  }