pmx-canvas 0.1.26 → 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 +74 -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 +114 -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 +50 -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 +496 -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 +234 -2
  62. package/src/server/mutation-history.ts +6 -0
  63. package/src/server/server.ts +419 -2
@@ -77,7 +77,15 @@ import {
77
77
  import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
78
78
  import { buildAgentContextPreamble, serializeNodeForAgentContext } from './agent-context.js';
79
79
  import { buildCanvasAxContext } from './ax-context.js';
80
- import type { PmxAxSource } from './ax-state.js';
80
+ import { isAxEventKind, isAxEvidenceKind } from './ax-state.js';
81
+ import type {
82
+ PmxAxReviewAnchorType,
83
+ PmxAxReviewKind,
84
+ PmxAxReviewRegion,
85
+ PmxAxReviewSeverity,
86
+ PmxAxReviewStatus,
87
+ PmxAxSource,
88
+ } from './ax-state.js';
81
89
  import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
82
90
  import { validateLocalImageFile } from './image-source.js';
83
91
  import {
@@ -86,12 +94,14 @@ import {
86
94
  MARKDOWN_NODE_DEFAULT_SIZE,
87
95
  MCP_APP_NODE_DEFAULT_SIZE,
88
96
  applyCanvasNodeUpdates,
97
+ appendCanvasJsonRenderStream,
89
98
  buildStructuredNodeUpdate,
90
99
  arrangeCanvasNodes,
91
100
  clearCanvas,
92
101
  createCanvasGraphNode,
93
102
  createCanvasGroup,
94
103
  createCanvasJsonRenderNode,
104
+ createCanvasStreamingJsonRenderNode,
95
105
  deleteCanvasSnapshot,
96
106
  executeCanvasBatch,
97
107
  fitCanvasView,
@@ -101,6 +111,7 @@ import {
101
111
  refreshCanvasWebpageNode,
102
112
  removeCanvasNode,
103
113
  removeCanvasEdge,
114
+ resolveHtmlContent,
104
115
  restoreCanvasSnapshot,
105
116
  saveCanvasSnapshot,
106
117
  scheduleCodeGraphRecompute,
@@ -1656,7 +1667,7 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
1656
1667
  const htmlMergedData = type === 'html'
1657
1668
  ? {
1658
1669
  ...(extraData ?? {}),
1659
- ...(typeof body.html === 'string' ? { html: body.html } : {}),
1670
+ ...(typeof body.html === 'string' ? { html: resolveHtmlContent(body.html) } : {}),
1660
1671
  ...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
1661
1672
  ...(typeof body.agentSummary === 'string' ? { agentSummary: body.agentSummary } : {}),
1662
1673
  ...(typeof body.description === 'string' ? { description: body.description } : {}),
@@ -2214,6 +2225,34 @@ async function handleCanvasAddJsonRender(req: Request): Promise<Response> {
2214
2225
  }
2215
2226
  }
2216
2227
 
2228
+ async function handleJsonRenderStream(req: Request): Promise<Response> {
2229
+ const body = await readJson(req);
2230
+ const patches = Array.isArray(body.patches) ? body.patches : [];
2231
+ const done = body.done === true;
2232
+ const geometry = resolveCreateGeometry(body);
2233
+ try {
2234
+ let nodeId = typeof body.nodeId === 'string' && body.nodeId ? body.nodeId : undefined;
2235
+ let url = '';
2236
+ if (!nodeId) {
2237
+ const created = createCanvasStreamingJsonRenderNode({
2238
+ ...(typeof body.title === 'string' ? { title: body.title } : {}),
2239
+ ...(body.strictSize === true ? { strictSize: true } : {}),
2240
+ ...geometry,
2241
+ });
2242
+ nodeId = created.id;
2243
+ url = created.url;
2244
+ }
2245
+ const result = appendCanvasJsonRenderStream(nodeId, patches, done);
2246
+ if (!result.ok) return responseJson({ ok: false, error: result.error }, 400);
2247
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
2248
+ const node = canvasState.getNode(nodeId);
2249
+ return responseJson({ id: nodeId, url: url || String(node?.data.url ?? ''), ...result });
2250
+ } catch (error) {
2251
+ const message = error instanceof Error ? error.message : String(error);
2252
+ return responseJson({ ok: false, error: message }, 400);
2253
+ }
2254
+ }
2255
+
2217
2256
  async function handleCanvasAddGraph(req: Request): Promise<Response> {
2218
2257
  const body = await readJson(req);
2219
2258
  const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : 'Graph';
@@ -2242,6 +2281,16 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
2242
2281
  const nodeHeight = pickPositiveNumber(body, 'nodeHeight') ?? (size ? pickPositiveNumber(size, 'height') : undefined);
2243
2282
  const showLegend = typeof body.showLegend === 'boolean' ? body.showLegend : undefined;
2244
2283
  const showLabels = typeof body.showLabels === 'boolean' ? body.showLabels : undefined;
2284
+ const colorBy =
2285
+ body.colorBy === 'series' || body.colorBy === 'category' || body.colorBy === 'value' || body.colorBy === 'none'
2286
+ ? body.colorBy
2287
+ : undefined;
2288
+ const highlight =
2289
+ typeof body.highlight === 'number' || body.highlight === 'max' || body.highlight === 'min' || body.highlight === null
2290
+ ? body.highlight
2291
+ : undefined;
2292
+ const sort =
2293
+ body.sort === 'asc' || body.sort === 'desc' || body.sort === 'none' ? body.sort : undefined;
2245
2294
  const result = createCanvasGraphNode({
2246
2295
  title,
2247
2296
  graphType,
@@ -2258,8 +2307,23 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
2258
2307
  ...(typeof body.lineKey === 'string' ? { lineKey: body.lineKey } : {}),
2259
2308
  ...(aggregate ? { aggregate } : {}),
2260
2309
  ...(typeof body.color === 'string' ? { color: body.color } : {}),
2310
+ ...(colorBy ? { colorBy } : {}),
2311
+ ...(highlight !== undefined ? { highlight } : {}),
2261
2312
  ...(typeof body.barColor === 'string' ? { barColor: body.barColor } : {}),
2262
2313
  ...(typeof body.lineColor === 'string' ? { lineColor: body.lineColor } : {}),
2314
+ ...(typeof body.labelKey === 'string' ? { labelKey: body.labelKey } : {}),
2315
+ ...(typeof body.targetKey === 'string' ? { targetKey: body.targetKey } : {}),
2316
+ ...(typeof body.rangesKey === 'string' ? { rangesKey: body.rangesKey } : {}),
2317
+ ...(typeof body.beforeKey === 'string' ? { beforeKey: body.beforeKey } : {}),
2318
+ ...(typeof body.afterKey === 'string' ? { afterKey: body.afterKey } : {}),
2319
+ ...(typeof body.beforeLabel === 'string' ? { beforeLabel: body.beforeLabel } : {}),
2320
+ ...(typeof body.afterLabel === 'string' ? { afterLabel: body.afterLabel } : {}),
2321
+ ...(sort ? { sort } : {}),
2322
+ ...(typeof body.fill === 'boolean' ? { fill: body.fill } : {}),
2323
+ ...(typeof body.showEndDot === 'boolean' ? { showEndDot: body.showEndDot } : {}),
2324
+ ...(typeof body.showMinMax === 'boolean' ? { showMinMax: body.showMinMax } : {}),
2325
+ ...(typeof body.showValue === 'boolean' ? { showValue: body.showValue } : {}),
2326
+ ...(typeof body.colorByDirection === 'boolean' ? { colorByDirection: body.colorByDirection } : {}),
2263
2327
  ...(typeof body.height === 'number' ? { height: body.height } : {}),
2264
2328
  ...(showLegend !== undefined ? { showLegend } : {}),
2265
2329
  ...(showLabels !== undefined ? { showLabels } : {}),
@@ -2334,11 +2398,17 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
2334
2398
  ? themeValue
2335
2399
  : undefined;
2336
2400
  const title = (node.data.title as string) || node.id;
2401
+ // Devtools panel is double-gated: the operator must opt in via the env flag
2402
+ // AND the request must carry ?devtools=1. Off by default in all normal runs.
2403
+ const devtoolsEnabled =
2404
+ process.env.PMX_CANVAS_JSON_RENDER_DEVTOOLS === '1' &&
2405
+ url.searchParams.get('devtools') === '1';
2337
2406
  const html = await buildJsonRenderViewerHtml({
2338
2407
  title,
2339
2408
  spec,
2340
2409
  ...(theme ? { theme } : {}),
2341
2410
  ...(url.searchParams.get('display') === 'expanded' ? { display: 'expanded' as const } : {}),
2411
+ ...(devtoolsEnabled ? { devtools: true } : {}),
2342
2412
  });
2343
2413
  return new Response(html, {
2344
2414
  headers: {
@@ -3653,6 +3723,284 @@ async function handleAxStatePatch(req: Request): Promise<Response> {
3653
3723
  return responseJson({ ok: true, state: canvasState.getAxState() });
3654
3724
  }
3655
3725
 
3726
+ async function handleAxEventAdd(req: Request): Promise<Response> {
3727
+ const body = await readJson(req);
3728
+ if (!isAxEventKind(body.kind) || typeof body.summary !== 'string') {
3729
+ return responseJson({ ok: false, error: 'event requires kind and summary.' }, 400);
3730
+ }
3731
+ const event = canvasState.recordAxEvent(
3732
+ {
3733
+ kind: body.kind,
3734
+ summary: body.summary,
3735
+ detail: typeof body.detail === 'string' ? body.detail : null,
3736
+ nodeIds: normalizeAxNodeIds(body.nodeIds),
3737
+ data: isRecord(body.data) ? body.data : null,
3738
+ },
3739
+ { source: normalizeAxSource(body.source, 'api') },
3740
+ );
3741
+ broadcastWorkbenchEvent('ax-event-created', {
3742
+ event,
3743
+ sessionId: primaryWorkbenchSessionId,
3744
+ timestamp: new Date().toISOString(),
3745
+ });
3746
+ return responseJson({ ok: true, event });
3747
+ }
3748
+
3749
+ async function handleAxSteer(req: Request): Promise<Response> {
3750
+ const body = await readJson(req);
3751
+ if (typeof body.message !== 'string' || !body.message.trim()) {
3752
+ return responseJson({ ok: false, error: 'steer requires a non-empty message.' }, 400);
3753
+ }
3754
+ const steering = canvasState.recordSteeringMessage(body.message, {
3755
+ source: normalizeAxSource(body.source, 'api'),
3756
+ });
3757
+ broadcastWorkbenchEvent('ax-event-created', {
3758
+ steering,
3759
+ sessionId: primaryWorkbenchSessionId,
3760
+ timestamp: new Date().toISOString(),
3761
+ });
3762
+ return responseJson({ ok: true, steering });
3763
+ }
3764
+
3765
+ function handleAxTimelineGet(url: URL): Response {
3766
+ const limit = Number(url.searchParams.get('limit') ?? '');
3767
+ return responseJson({
3768
+ ok: true,
3769
+ ...canvasState.getAxTimeline(Number.isFinite(limit) && limit > 0 ? { limit } : {}),
3770
+ });
3771
+ }
3772
+
3773
+ const AX_WORK_STATUSES = new Set(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
3774
+
3775
+ function normalizeAxWorkItemStatus(value: unknown): 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled' | undefined {
3776
+ return typeof value === 'string' && AX_WORK_STATUSES.has(value)
3777
+ ? value as 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled'
3778
+ : undefined;
3779
+ }
3780
+
3781
+ function handleAxWorkList(): Response {
3782
+ return responseJson({ ok: true, workItems: canvasState.getWorkItems() });
3783
+ }
3784
+
3785
+ async function handleAxWorkAdd(req: Request): Promise<Response> {
3786
+ const body = await readJson(req);
3787
+ if (typeof body.title !== 'string' || !body.title.trim()) {
3788
+ return responseJson({ ok: false, error: 'work item requires a title.' }, 400);
3789
+ }
3790
+ const status = normalizeAxWorkItemStatus(body.status);
3791
+ const workItem = canvasState.addWorkItem(
3792
+ {
3793
+ title: body.title,
3794
+ ...(status ? { status } : {}),
3795
+ ...(typeof body.detail === 'string' ? { detail: body.detail } : {}),
3796
+ ...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
3797
+ },
3798
+ { source: normalizeAxSource(body.source, 'api') },
3799
+ );
3800
+ broadcastWorkbenchEvent('ax-state-changed', {
3801
+ workItem,
3802
+ sessionId: primaryWorkbenchSessionId,
3803
+ timestamp: new Date().toISOString(),
3804
+ });
3805
+ return responseJson({ ok: true, workItem });
3806
+ }
3807
+
3808
+ async function handleAxWorkUpdate(req: Request, id: string): Promise<Response> {
3809
+ const body = await readJson(req);
3810
+ const status = normalizeAxWorkItemStatus(body.status);
3811
+ const workItem = canvasState.updateWorkItem(
3812
+ id,
3813
+ {
3814
+ ...(typeof body.title === 'string' ? { title: body.title } : {}),
3815
+ ...(status ? { status } : {}),
3816
+ ...(typeof body.detail === 'string' || body.detail === null ? { detail: body.detail as string | null } : {}),
3817
+ ...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
3818
+ },
3819
+ { source: normalizeAxSource(body.source, 'api') },
3820
+ );
3821
+ if (!workItem) return responseJson({ ok: false, error: 'work item not found.' }, 404);
3822
+ broadcastWorkbenchEvent('ax-state-changed', {
3823
+ workItem,
3824
+ sessionId: primaryWorkbenchSessionId,
3825
+ timestamp: new Date().toISOString(),
3826
+ });
3827
+ return responseJson({ ok: true, workItem });
3828
+ }
3829
+
3830
+ function handleAxApprovalList(): Response {
3831
+ return responseJson({ ok: true, approvalGates: canvasState.getApprovalGates() });
3832
+ }
3833
+
3834
+ async function handleAxApprovalRequest(req: Request): Promise<Response> {
3835
+ const body = await readJson(req);
3836
+ if (typeof body.title !== 'string' || !body.title.trim()) {
3837
+ return responseJson({ ok: false, error: 'approval request requires a title.' }, 400);
3838
+ }
3839
+ const approvalGate = canvasState.requestApproval(
3840
+ {
3841
+ title: body.title,
3842
+ ...(typeof body.detail === 'string' ? { detail: body.detail } : {}),
3843
+ ...(typeof body.action === 'string' ? { action: body.action } : {}),
3844
+ ...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
3845
+ },
3846
+ { source: normalizeAxSource(body.source, 'api') },
3847
+ );
3848
+ broadcastWorkbenchEvent('ax-state-changed', {
3849
+ approvalGate,
3850
+ sessionId: primaryWorkbenchSessionId,
3851
+ timestamp: new Date().toISOString(),
3852
+ });
3853
+ return responseJson({ ok: true, approvalGate });
3854
+ }
3855
+
3856
+ async function handleAxApprovalResolve(req: Request, id: string): Promise<Response> {
3857
+ const body = await readJson(req);
3858
+ if (body.decision !== 'approved' && body.decision !== 'rejected') {
3859
+ return responseJson({ ok: false, error: 'resolve requires decision approved or rejected.' }, 400);
3860
+ }
3861
+ const approvalGate = canvasState.resolveApproval(
3862
+ id,
3863
+ body.decision,
3864
+ {
3865
+ ...(typeof body.resolution === 'string' ? { resolution: body.resolution } : {}),
3866
+ source: normalizeAxSource(body.source, 'api'),
3867
+ },
3868
+ );
3869
+ if (!approvalGate) return responseJson({ ok: false, error: 'approval gate not found or already resolved.' }, 404);
3870
+ broadcastWorkbenchEvent('ax-state-changed', {
3871
+ approvalGate,
3872
+ sessionId: primaryWorkbenchSessionId,
3873
+ timestamp: new Date().toISOString(),
3874
+ });
3875
+ return responseJson({ ok: true, approvalGate });
3876
+ }
3877
+
3878
+ async function handleAxEvidenceAdd(req: Request): Promise<Response> {
3879
+ const body = await readJson(req);
3880
+ if (!isAxEvidenceKind(body.kind) || typeof body.title !== 'string' || !body.title.trim()) {
3881
+ return responseJson({ ok: false, error: 'evidence requires kind and title.' }, 400);
3882
+ }
3883
+ const evidence = canvasState.addEvidence(
3884
+ {
3885
+ kind: body.kind,
3886
+ title: body.title,
3887
+ body: typeof body.body === 'string' ? body.body : null,
3888
+ ref: typeof body.ref === 'string' ? body.ref : null,
3889
+ nodeIds: normalizeAxNodeIds(body.nodeIds),
3890
+ data: isRecord(body.data) ? body.data : null,
3891
+ },
3892
+ { source: normalizeAxSource(body.source, 'api') },
3893
+ );
3894
+ broadcastWorkbenchEvent('ax-event-created', {
3895
+ evidence,
3896
+ sessionId: primaryWorkbenchSessionId,
3897
+ timestamp: new Date().toISOString(),
3898
+ });
3899
+ return responseJson({ ok: true, evidence });
3900
+ }
3901
+
3902
+ const AX_REVIEW_KINDS = new Set(['comment', 'finding']);
3903
+ const AX_REVIEW_SEVERITIES = new Set(['info', 'warning', 'error']);
3904
+ const AX_REVIEW_STATUSES = new Set(['open', 'resolved', 'dismissed']);
3905
+ const AX_REVIEW_ANCHORS = new Set(['node', 'file', 'region']);
3906
+
3907
+ function normalizeAxReviewKind(value: unknown): PmxAxReviewKind | undefined {
3908
+ return typeof value === 'string' && AX_REVIEW_KINDS.has(value) ? value as PmxAxReviewKind : undefined;
3909
+ }
3910
+ function normalizeAxReviewSeverity(value: unknown): PmxAxReviewSeverity | undefined {
3911
+ return typeof value === 'string' && AX_REVIEW_SEVERITIES.has(value) ? value as PmxAxReviewSeverity : undefined;
3912
+ }
3913
+ function normalizeAxReviewStatus(value: unknown): PmxAxReviewStatus | undefined {
3914
+ return typeof value === 'string' && AX_REVIEW_STATUSES.has(value) ? value as PmxAxReviewStatus : undefined;
3915
+ }
3916
+ function normalizeAxReviewAnchor(value: unknown): PmxAxReviewAnchorType | undefined {
3917
+ return typeof value === 'string' && AX_REVIEW_ANCHORS.has(value) ? value as PmxAxReviewAnchorType : undefined;
3918
+ }
3919
+ function normalizeAxReviewRegion(value: unknown): PmxAxReviewRegion | undefined {
3920
+ if (!isRecord(value)) return undefined;
3921
+ return {
3922
+ ...(typeof value.line === 'number' ? { line: value.line } : {}),
3923
+ ...(typeof value.endLine === 'number' ? { endLine: value.endLine } : {}),
3924
+ ...(typeof value.label === 'string' ? { label: value.label } : {}),
3925
+ };
3926
+ }
3927
+
3928
+ function handleAxReviewList(): Response {
3929
+ return responseJson({ ok: true, reviewAnnotations: canvasState.getReviewAnnotations() });
3930
+ }
3931
+
3932
+ async function handleAxReviewAdd(req: Request): Promise<Response> {
3933
+ const body = await readJson(req);
3934
+ if (typeof body.body !== 'string' || !body.body.trim()) {
3935
+ return responseJson({ ok: false, error: 'review annotation requires a body.' }, 400);
3936
+ }
3937
+ const kind = normalizeAxReviewKind(body.kind);
3938
+ const severity = normalizeAxReviewSeverity(body.severity);
3939
+ const anchorType = normalizeAxReviewAnchor(body.anchorType);
3940
+ const region = normalizeAxReviewRegion(body.region);
3941
+ const reviewAnnotation = canvasState.addReviewAnnotation(
3942
+ {
3943
+ body: body.body,
3944
+ ...(kind ? { kind } : {}),
3945
+ ...(severity ? { severity } : {}),
3946
+ ...(anchorType ? { anchorType } : {}),
3947
+ ...(typeof body.nodeId === 'string' ? { nodeId: body.nodeId } : {}),
3948
+ ...(typeof body.file === 'string' ? { file: body.file } : {}),
3949
+ ...(region ? { region } : {}),
3950
+ ...(typeof body.author === 'string' ? { author: body.author } : {}),
3951
+ },
3952
+ { source: normalizeAxSource(body.source, 'api') },
3953
+ );
3954
+ if (!reviewAnnotation) {
3955
+ return responseJson({ ok: false, error: 'node-anchored review annotation requires a nodeId that exists on the canvas.' }, 400);
3956
+ }
3957
+ broadcastWorkbenchEvent('ax-state-changed', {
3958
+ reviewAnnotation,
3959
+ sessionId: primaryWorkbenchSessionId,
3960
+ timestamp: new Date().toISOString(),
3961
+ });
3962
+ return responseJson({ ok: true, reviewAnnotation });
3963
+ }
3964
+
3965
+ async function handleAxReviewUpdate(req: Request, id: string): Promise<Response> {
3966
+ const body = await readJson(req);
3967
+ const status = normalizeAxReviewStatus(body.status);
3968
+ const severity = normalizeAxReviewSeverity(body.severity);
3969
+ const kind = normalizeAxReviewKind(body.kind);
3970
+ const reviewAnnotation = canvasState.updateReviewAnnotation(
3971
+ id,
3972
+ {
3973
+ ...(typeof body.body === 'string' ? { body: body.body } : {}),
3974
+ ...(status ? { status } : {}),
3975
+ ...(severity ? { severity } : {}),
3976
+ ...(kind ? { kind } : {}),
3977
+ },
3978
+ { source: normalizeAxSource(body.source, 'api') },
3979
+ );
3980
+ if (!reviewAnnotation) return responseJson({ ok: false, error: 'review annotation not found.' }, 404);
3981
+ broadcastWorkbenchEvent('ax-state-changed', {
3982
+ reviewAnnotation,
3983
+ sessionId: primaryWorkbenchSessionId,
3984
+ timestamp: new Date().toISOString(),
3985
+ });
3986
+ return responseJson({ ok: true, reviewAnnotation });
3987
+ }
3988
+
3989
+ function handleAxHostCapabilityGet(): Response {
3990
+ return responseJson({ ok: true, host: canvasState.getHostCapability() });
3991
+ }
3992
+
3993
+ async function handleAxHostCapabilityReport(req: Request): Promise<Response> {
3994
+ const body = await readJson(req);
3995
+ const host = canvasState.setHostCapability(body, { source: normalizeAxSource(body.source, 'api') });
3996
+ broadcastWorkbenchEvent('ax-state-changed', {
3997
+ host,
3998
+ sessionId: primaryWorkbenchSessionId,
3999
+ timestamp: new Date().toISOString(),
4000
+ });
4001
+ return responseJson({ ok: true, host });
4002
+ }
4003
+
3656
4004
  // ── Port resolution ───────────────────────────────────────────
3657
4005
 
3658
4006
  function buildPortCandidates(preferredPort: number): number[] {
@@ -4641,6 +4989,71 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
4641
4989
  return handleAxFocusUpdate(req);
4642
4990
  }
4643
4991
 
4992
+ if (url.pathname === '/api/canvas/ax/event' && req.method === 'POST') {
4993
+ return handleAxEventAdd(req);
4994
+ }
4995
+
4996
+ if (url.pathname === '/api/canvas/ax/steer' && req.method === 'POST') {
4997
+ return handleAxSteer(req);
4998
+ }
4999
+
5000
+ if (url.pathname === '/api/canvas/ax/timeline' && req.method === 'GET') {
5001
+ return handleAxTimelineGet(url);
5002
+ }
5003
+
5004
+ if (url.pathname === '/api/canvas/ax/work' && req.method === 'GET') {
5005
+ return handleAxWorkList();
5006
+ }
5007
+
5008
+ if (url.pathname === '/api/canvas/ax/work' && req.method === 'POST') {
5009
+ return handleAxWorkAdd(req);
5010
+ }
5011
+
5012
+ if (url.pathname.startsWith('/api/canvas/ax/work/') && req.method === 'PATCH') {
5013
+ const workItemId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/work/'.length));
5014
+ return handleAxWorkUpdate(req, workItemId);
5015
+ }
5016
+
5017
+ if (url.pathname === '/api/canvas/ax/approval' && req.method === 'GET') {
5018
+ return handleAxApprovalList();
5019
+ }
5020
+
5021
+ if (url.pathname === '/api/canvas/ax/approval' && req.method === 'POST') {
5022
+ return handleAxApprovalRequest(req);
5023
+ }
5024
+
5025
+ if (url.pathname.startsWith('/api/canvas/ax/approval/') && url.pathname.endsWith('/resolve') && req.method === 'POST') {
5026
+ const approvalId = decodeURIComponent(
5027
+ url.pathname.slice('/api/canvas/ax/approval/'.length, -'/resolve'.length),
5028
+ );
5029
+ return handleAxApprovalResolve(req, approvalId);
5030
+ }
5031
+
5032
+ if (url.pathname === '/api/canvas/ax/evidence' && req.method === 'POST') {
5033
+ return handleAxEvidenceAdd(req);
5034
+ }
5035
+
5036
+ if (url.pathname === '/api/canvas/ax/review' && req.method === 'GET') {
5037
+ return handleAxReviewList();
5038
+ }
5039
+
5040
+ if (url.pathname === '/api/canvas/ax/review' && req.method === 'POST') {
5041
+ return handleAxReviewAdd(req);
5042
+ }
5043
+
5044
+ if (url.pathname.startsWith('/api/canvas/ax/review/') && req.method === 'PATCH') {
5045
+ const reviewId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/review/'.length));
5046
+ return handleAxReviewUpdate(req, reviewId);
5047
+ }
5048
+
5049
+ if (url.pathname === '/api/canvas/ax/host-capability' && req.method === 'GET') {
5050
+ return handleAxHostCapabilityGet();
5051
+ }
5052
+
5053
+ if (url.pathname === '/api/canvas/ax/host-capability' && req.method === 'PUT') {
5054
+ return handleAxHostCapabilityReport(req);
5055
+ }
5056
+
4644
5057
  // Spatial context API
4645
5058
  if (url.pathname === '/api/canvas/spatial-context' && req.method === 'GET') {
4646
5059
  const layout = canvasState.getLayout();
@@ -4703,6 +5116,10 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
4703
5116
  return handleCanvasAddJsonRender(req);
4704
5117
  }
4705
5118
 
5119
+ if (url.pathname === '/api/canvas/json-render/stream' && req.method === 'POST') {
5120
+ return handleJsonRenderStream(req);
5121
+ }
5122
+
4706
5123
  if (url.pathname === '/api/canvas/graph' && req.method === 'POST') {
4707
5124
  return handleCanvasAddGraph(req);
4708
5125
  }