pmx-canvas 0.4.4 → 0.4.6

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 (98) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/Readme.md +52 -4
  3. package/dist/canvas/global.css +115 -11
  4. package/dist/canvas/index.js +93 -72
  5. package/dist/canvas/mermaid-entry.js +3563 -0
  6. package/dist/canvas/surface-theme.css +2 -2
  7. package/dist/json-render/index.css +1 -1
  8. package/dist/types/cli/agent.d.ts +1 -0
  9. package/dist/types/cli/commands/skills.d.ts +1 -1
  10. package/dist/types/cli/commands/smoke.d.ts +1 -0
  11. package/dist/types/client/mermaid-entry.d.ts +11 -0
  12. package/dist/types/client/nodes/DiffNode.d.ts +20 -0
  13. package/dist/types/client/nodes/ExtAppFrame.d.ts +0 -2
  14. package/dist/types/client/nodes/MermaidNode.d.ts +12 -0
  15. package/dist/types/client/state/intent-store.d.ts +6 -0
  16. package/dist/types/client/types.d.ts +2 -2
  17. package/dist/types/server/ax-state-manager.d.ts +5 -0
  18. package/dist/types/server/ax-state.d.ts +9 -3
  19. package/dist/types/server/canvas-db.d.ts +3 -2
  20. package/dist/types/server/canvas-provenance.d.ts +1 -1
  21. package/dist/types/server/canvas-state.d.ts +22 -1
  22. package/dist/types/server/canvas-validation.d.ts +34 -1
  23. package/dist/types/server/html-surface.d.ts +8 -0
  24. package/dist/types/server/index.d.ts +5 -0
  25. package/dist/types/server/operations/ops/ax-shared.d.ts +4 -1
  26. package/dist/types/server/operations/ops/nodes.d.ts +1 -1
  27. package/dist/types/server/operations/registry.d.ts +9 -1
  28. package/dist/types/server/workboard.d.ts +46 -0
  29. package/dist/types/shared/ax-intent.d.ts +6 -0
  30. package/dist/types/shared/ax-surface-protocol.d.ts +7 -0
  31. package/docs/cli.md +12 -0
  32. package/docs/environment.md +1 -0
  33. package/docs/http-api.md +12 -0
  34. package/docs/mcp.md +1 -1
  35. package/docs/node-types.md +2 -0
  36. package/docs/screenshot.png +0 -0
  37. package/docs/screenshots/amp-orb-portal.png +0 -0
  38. package/docs/screenshots/claude-code-desktop.png +0 -0
  39. package/docs/screenshots/codex-app.png +0 -0
  40. package/docs/screenshots/github-copilot-app.png +0 -0
  41. package/package.json +4 -3
  42. package/skills/pmx-canvas/SKILL.md +71 -24
  43. package/skills/pmx-canvas-orchestration/SKILL.md +82 -0
  44. package/skills/pmx-canvas-testing/SKILL.md +3 -0
  45. package/src/cli/agent.ts +2 -0
  46. package/src/cli/commands/ax.ts +28 -3
  47. package/src/cli/commands/skills.ts +1 -1
  48. package/src/cli/commands/smoke.ts +212 -0
  49. package/src/cli/index.ts +15 -2
  50. package/src/cli/shared.ts +1 -0
  51. package/src/client/canvas/CanvasNode.tsx +12 -1
  52. package/src/client/canvas/CanvasViewport.tsx +6 -0
  53. package/src/client/canvas/ExpandedNodeOverlay.tsx +10 -0
  54. package/src/client/canvas/IntentLayer.tsx +8 -6
  55. package/src/client/canvas/Minimap.tsx +2 -0
  56. package/src/client/canvas/auto-fit.ts +2 -0
  57. package/src/client/icons.tsx +4 -0
  58. package/src/client/mermaid-entry.ts +76 -0
  59. package/src/client/nodes/DiffNode.tsx +114 -0
  60. package/src/client/nodes/ExtAppFrame.tsx +259 -84
  61. package/src/client/nodes/MermaidNode.tsx +81 -0
  62. package/src/client/state/intent-bridge.ts +12 -3
  63. package/src/client/state/intent-store.ts +47 -2
  64. package/src/client/state/node-factory.ts +2 -0
  65. package/src/client/state/sse-bridge.ts +28 -0
  66. package/src/client/theme/global.css +115 -11
  67. package/src/client/theme/surface-theme.css +2 -2
  68. package/src/client/types.ts +6 -0
  69. package/src/mcp/server.ts +22 -6
  70. package/src/server/ax-interaction.ts +6 -0
  71. package/src/server/ax-state-manager.ts +27 -6
  72. package/src/server/ax-state.ts +36 -3
  73. package/src/server/canvas-db.ts +83 -60
  74. package/src/server/canvas-operations.ts +39 -9
  75. package/src/server/canvas-provenance.ts +2 -0
  76. package/src/server/canvas-schema.ts +33 -0
  77. package/src/server/canvas-state.ts +78 -6
  78. package/src/server/canvas-validation.ts +79 -0
  79. package/src/server/html-surface.ts +11 -0
  80. package/src/server/index.ts +25 -6
  81. package/src/server/intent-registry.ts +2 -0
  82. package/src/server/operations/composites.ts +3 -2
  83. package/src/server/operations/http.ts +5 -1
  84. package/src/server/operations/ops/ax-shared.ts +20 -1
  85. package/src/server/operations/ops/ax-state.ts +4 -7
  86. package/src/server/operations/ops/ax-timeline.ts +26 -3
  87. package/src/server/operations/ops/ax-work.ts +27 -1
  88. package/src/server/operations/ops/batch.ts +11 -1
  89. package/src/server/operations/ops/json-render.ts +44 -0
  90. package/src/server/operations/ops/nodes.ts +24 -2
  91. package/src/server/operations/ops/viewport.ts +8 -1
  92. package/src/server/operations/registry.ts +122 -1
  93. package/src/server/server.ts +58 -3
  94. package/src/server/workboard.ts +159 -0
  95. package/src/shared/ax-intent.ts +6 -0
  96. package/src/shared/ax-surface-protocol.ts +8 -0
  97. package/src/shared/surface.ts +2 -0
  98. package/src/shared/themes.ts +1 -1
@@ -23,8 +23,16 @@ export const hoveredIntentId = signal<string | null>(null);
23
23
 
24
24
  const SETTLE_MS = 480;
25
25
  const DISSOLVE_MS = 320;
26
+ /**
27
+ * Minimum time a ghost stays in its forming state before a settle/dissolve
28
+ * plays. Fast agents (and every server auto-ghost) signal and settle within
29
+ * milliseconds — without this floor, correct ghost usage is imperceptible.
30
+ */
31
+ export const MIN_FORMING_MS = 650;
26
32
 
27
33
  const exitTimers = new Map<string, ReturnType<typeof setTimeout>>();
34
+ const phaseDelayTimers = new Map<string, ReturnType<typeof setTimeout>>();
35
+ const formedAt = new Map<string, number>();
28
36
  let pruneTimer: ReturnType<typeof setInterval> | null = null;
29
37
 
30
38
  function writeIntents(next: Map<string, ClientIntent>): void {
@@ -40,9 +48,19 @@ function clearExitTimer(id: string): void {
40
48
  }
41
49
  }
42
50
 
51
+ function clearPhaseDelay(id: string): void {
52
+ const timer = phaseDelayTimers.get(id);
53
+ if (timer) {
54
+ clearTimeout(timer);
55
+ phaseDelayTimers.delete(id);
56
+ }
57
+ }
58
+
43
59
  /** A live `ax-intent` frame: (re)place the ghost in its forming state. */
44
60
  export function upsertIntent(intent: PmxAxIntent): void {
45
61
  clearExitTimer(intent.id);
62
+ clearPhaseDelay(intent.id);
63
+ if (!intents.value.has(intent.id)) formedAt.set(intent.id, Date.now());
46
64
  const next = new Map(intents.value);
47
65
  next.set(intent.id, { ...intent, phase: 'forming' });
48
66
  writeIntents(next);
@@ -50,6 +68,8 @@ export function upsertIntent(intent: PmxAxIntent): void {
50
68
 
51
69
  export function removeIntent(id: string): void {
52
70
  clearExitTimer(id);
71
+ clearPhaseDelay(id);
72
+ formedAt.delete(id);
53
73
  if (!intents.value.has(id)) return;
54
74
  const next = new Map(intents.value);
55
75
  next.delete(id);
@@ -69,19 +89,44 @@ function setPhase(id: string, phase: IntentPhase, ms: number, settledNodeId?: st
69
89
  );
70
90
  }
71
91
 
92
+ /**
93
+ * Minimum-dwell wrapper: when a settle/dissolve arrives before the ghost has
94
+ * been visible for MIN_FORMING_MS, hold the forming state until the floor
95
+ * elapses, then play the exit phase.
96
+ */
97
+ function setPhaseAfterDwell(id: string, phase: IntentPhase, ms: number, settledNodeId?: string): void {
98
+ clearPhaseDelay(id);
99
+ const elapsed = Date.now() - (formedAt.get(id) ?? 0);
100
+ const delay = Math.max(0, MIN_FORMING_MS - elapsed);
101
+ if (delay === 0) {
102
+ setPhase(id, phase, ms, settledNodeId);
103
+ return;
104
+ }
105
+ phaseDelayTimers.set(
106
+ id,
107
+ setTimeout(() => {
108
+ phaseDelayTimers.delete(id);
109
+ setPhase(id, phase, ms, settledNodeId);
110
+ }, delay),
111
+ );
112
+ }
113
+
72
114
  /** Resolve a ghost into a real node — the settle morph, then removal. */
73
115
  export function settleIntent(id: string, settledNodeId?: string): void {
74
- setPhase(id, 'settling', SETTLE_MS, settledNodeId);
116
+ setPhaseAfterDwell(id, 'settling', SETTLE_MS, settledNodeId);
75
117
  }
76
118
 
77
119
  /** Dissolve a ghost (expired / vetoed / evicted / abandoned), then remove it. */
78
120
  export function dissolveIntent(id: string): void {
79
- setPhase(id, 'dissolving', DISSOLVE_MS);
121
+ setPhaseAfterDwell(id, 'dissolving', DISSOLVE_MS);
80
122
  }
81
123
 
82
124
  export function resetIntents(): void {
83
125
  for (const timer of exitTimers.values()) clearTimeout(timer);
84
126
  exitTimers.clear();
127
+ for (const timer of phaseDelayTimers.values()) clearTimeout(timer);
128
+ phaseDelayTimers.clear();
129
+ formedAt.clear();
85
130
  if (pruneTimer) {
86
131
  clearInterval(pruneTimer);
87
132
  pruneTimer = null;
@@ -34,6 +34,8 @@ export const DEFAULT_POSITIONS: Record<CanvasNodeState['type'], { x: number; y:
34
34
  ledger: { x: 1130, y: 520, w: 320, h: 280 },
35
35
  trace: { x: 40, y: 900, w: 200, h: 56 },
36
36
  file: { x: 380, y: 80, w: 720, h: 600 },
37
+ diff: { x: 380, y: 80, w: 640, h: 420 },
38
+ mermaid: { x: 380, y: 80, w: 640, h: 460 },
37
39
  image: { x: 380, y: 80, w: 720, h: 520 },
38
40
  html: { x: 380, y: 80, w: 720, h: 640 },
39
41
  group: { x: 220, y: 60, w: 840, h: 560 },
@@ -307,6 +307,8 @@ function isCanvasNodeType(value: unknown): value is CanvasNodeState['type'] {
307
307
  value === 'ledger' ||
308
308
  value === 'trace' ||
309
309
  value === 'file' ||
310
+ value === 'diff' ||
311
+ value === 'mermaid' ||
310
312
  value === 'image' ||
311
313
  value === 'html' ||
312
314
  value === 'group'
@@ -402,11 +404,37 @@ function parseCanvasAnnotation(raw: Record<string, unknown>): CanvasAnnotation |
402
404
  }
403
405
 
404
406
  // ── SSE event handlers ───────────────────────────────────────
407
+
408
+ /**
409
+ * Stale-SPA guard (0.4.5 report Finding W): a long-lived host panel keeps its
410
+ * in-memory bundle across a daemon upgrade — SSE reconnects silently and the
411
+ * old client renders none of the new release's behavior. The boot HTML stamps
412
+ * the server version at page-serve time; when a `connected` frame reports a
413
+ * DIFFERENT version, the server was upgraded under this page — reload once to
414
+ * pick up the new bundle. Guarded per version so a broken stamp can't loop.
415
+ */
416
+ const VERSION_RELOAD_KEY = 'pmx-canvas-version-reload';
417
+ function reloadIfServerUpgraded(serverVersion: unknown): void {
418
+ if (typeof serverVersion !== 'string' || !serverVersion || serverVersion === 'unknown') return;
419
+ if (typeof window === 'undefined') return;
420
+ const bootVersion = (window as Window & { __PMX_BOOT_SERVER_VERSION?: unknown }).__PMX_BOOT_SERVER_VERSION;
421
+ if (typeof bootVersion !== 'string' || !bootVersion || bootVersion === 'unknown') return;
422
+ if (serverVersion === bootVersion) return;
423
+ try {
424
+ if (window.sessionStorage.getItem(VERSION_RELOAD_KEY) === serverVersion) return;
425
+ window.sessionStorage.setItem(VERSION_RELOAD_KEY, serverVersion);
426
+ } catch {
427
+ return; // No sessionStorage loop guard available — never risk a reload loop.
428
+ }
429
+ window.location.reload();
430
+ }
431
+
405
432
  function handleConnected(data: Record<string, unknown>): void {
406
433
  sessionId.value = (data.sessionId as string) || '';
407
434
  connectionStatus.value = 'connected';
408
435
  // Reconnect marker for holders of server-minted URLs (Finding S).
409
436
  workbenchConnectionEpoch.value += 1;
437
+ reloadIfServerUpgraded(data.version);
410
438
  // A ?theme= session override (host-default theming) wins over the
411
439
  // server-global theme for THIS client only.
412
440
  if (typeof data.theme === 'string' && !themeOverrideActive()) {
@@ -456,7 +456,7 @@
456
456
  --c-text-soft: #CBD3CA;
457
457
  --c-muted: #9CA49C;
458
458
  --c-dim: #626962;
459
- --c-accent: #F34E3F;
459
+ --c-accent: #DFDFC1;
460
460
  --c-ok: #52C880;
461
461
  --c-warn: #E4C465;
462
462
  --c-warn-alt: #FFB300;
@@ -465,22 +465,22 @@
465
465
  --c-thinking: #BC8FB4;
466
466
  --c-subagent: #6CA0E0;
467
467
  --c-input-bg: #111311;
468
- --c-dot-grid: rgba(243, 78, 63, 0.08);
468
+ --c-dot-grid: rgba(223, 223, 193, 0.08);
469
469
  --c-panel-overlay: rgba(11, 13, 11, 0.6);
470
470
  --c-panel-glass: rgba(23, 26, 23, 0.92);
471
471
  --c-shadow: rgba(0, 0, 0, 0.3);
472
472
  --c-shadow-heavy: rgba(0, 0, 0, 0.5);
473
- --c-glow-accent: rgba(243, 78, 63, 0.15);
473
+ --c-glow-accent: rgba(223, 223, 193, 0.15);
474
474
  --c-surface-hover: rgba(255, 255, 255, 0.06);
475
475
  --c-surface-subtle: rgba(255, 255, 255, 0.03);
476
476
  --c-surface-th: rgba(255, 255, 255, 0.04);
477
- --c-accent-8: rgba(243, 78, 63, 0.08);
478
- --c-accent-10: rgba(243, 78, 63, 0.1);
479
- --c-accent-12: rgba(243, 78, 63, 0.12);
480
- --c-accent-15: rgba(243, 78, 63, 0.15);
481
- --c-accent-25: rgba(243, 78, 63, 0.25);
482
- --c-accent-30: rgba(243, 78, 63, 0.3);
483
- --c-accent-40: rgba(243, 78, 63, 0.4);
477
+ --c-accent-8: rgba(223, 223, 193, 0.08);
478
+ --c-accent-10: rgba(223, 223, 193, 0.1);
479
+ --c-accent-12: rgba(223, 223, 193, 0.12);
480
+ --c-accent-15: rgba(223, 223, 193, 0.15);
481
+ --c-accent-25: rgba(223, 223, 193, 0.25);
482
+ --c-accent-30: rgba(223, 223, 193, 0.3);
483
+ --c-accent-40: rgba(223, 223, 193, 0.4);
484
484
  --c-ok-10: rgba(82, 200, 128, 0.1);
485
485
  --c-ok-12: rgba(82, 200, 128, 0.12);
486
486
  --c-ok-20: rgba(82, 200, 128, 0.2);
@@ -495,7 +495,7 @@
495
495
  --c-danger-12: rgba(214, 118, 112, 0.12);
496
496
  --c-line-30: rgba(39, 42, 39, 0.3);
497
497
  --c-contrast-fg: #0B0D0B;
498
- --c-accent-hover: #F8695B;
498
+ --c-accent-hover: #EDEDDB;
499
499
  --c-warn-hover: #EBD087;
500
500
  --c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
501
501
  --c-annotation: #F1F7F0;
@@ -669,6 +669,29 @@ body,
669
669
  flex-shrink: 0;
670
670
  }
671
671
 
672
+ .canvas-node .node-ax-status {
673
+ font-size: calc(9px * var(--node-chrome-scale, 1));
674
+ padding: calc(1px * var(--node-chrome-scale, 1)) calc(6px * var(--node-chrome-scale, 1));
675
+ border-radius: calc(4px * var(--node-chrome-scale, 1));
676
+ background: var(--c-panel-overlay);
677
+ color: var(--c-muted);
678
+ text-transform: uppercase;
679
+ letter-spacing: 0.04em;
680
+ flex-shrink: 0;
681
+ }
682
+
683
+ .canvas-node .node-ax-status-in-progress {
684
+ color: var(--c-accent);
685
+ }
686
+
687
+ .canvas-node .node-ax-status-done {
688
+ color: var(--c-ok);
689
+ }
690
+
691
+ .canvas-node .node-ax-status-blocked {
692
+ color: var(--c-warn);
693
+ }
694
+
672
695
  .canvas-node .node-controls {
673
696
  display: flex;
674
697
  align-items: center;
@@ -1872,6 +1895,74 @@ html.is-node-resizing .ext-app-preview-catcher {
1872
1895
  }
1873
1896
  }
1874
1897
 
1898
+ /* Diff nodes — unified diff viewer with per-file sections */
1899
+ .diff-node {
1900
+ display: flex;
1901
+ flex-direction: column;
1902
+ gap: 10px;
1903
+ height: 100%;
1904
+ overflow: auto;
1905
+ font-family: var(--mono);
1906
+ font-size: 12px;
1907
+ color: var(--c-text);
1908
+ }
1909
+
1910
+ .diff-node.diff-node-empty {
1911
+ align-items: center;
1912
+ justify-content: center;
1913
+ color: var(--c-muted);
1914
+ }
1915
+
1916
+ .diff-node .diff-file {
1917
+ border: 1px solid var(--c-line);
1918
+ border-radius: 6px;
1919
+ overflow: hidden;
1920
+ flex-shrink: 0;
1921
+ }
1922
+
1923
+ .diff-node .diff-file-header {
1924
+ padding: 4px 8px;
1925
+ background: var(--c-panel-overlay);
1926
+ border-bottom: 1px solid var(--c-line);
1927
+ color: var(--c-text-soft);
1928
+ font-weight: 600;
1929
+ white-space: nowrap;
1930
+ overflow: hidden;
1931
+ text-overflow: ellipsis;
1932
+ }
1933
+
1934
+ .diff-node .diff-lines {
1935
+ margin: 0;
1936
+ padding: 4px 0;
1937
+ overflow-x: auto;
1938
+ font-family: inherit;
1939
+ font-size: inherit;
1940
+ line-height: 1.5;
1941
+ }
1942
+
1943
+ .diff-node .diff-lines > div {
1944
+ padding: 0 8px;
1945
+ white-space: pre;
1946
+ }
1947
+
1948
+ .diff-node .diff-hunk {
1949
+ color: var(--c-muted);
1950
+ }
1951
+
1952
+ .diff-node .diff-line-add {
1953
+ color: var(--c-ok);
1954
+ background: var(--c-ok-10);
1955
+ }
1956
+
1957
+ .diff-node .diff-line-remove {
1958
+ color: var(--c-danger);
1959
+ background: var(--c-danger-12);
1960
+ }
1961
+
1962
+ .diff-node .diff-line-context {
1963
+ color: var(--c-text-soft);
1964
+ }
1965
+
1875
1966
  /* Docked node — compact toolbar-flanking panel */
1876
1967
  .docked-node {
1877
1968
  min-width: 200px;
@@ -3955,6 +4046,19 @@ button.welcome-hint:hover {
3955
4046
  display: contents;
3956
4047
  }
3957
4048
 
4049
+ /* Auto-ghosts (server-synthesized for unlinked agent mutations) read lighter
4050
+ than a deliberate canvas_intent signal: desaturated, dotted, no veto —
4051
+ the opacity drop lives in ghostOpacity() (inline style). */
4052
+ .intent-ghost.is-auto {
4053
+ filter: saturate(0.7);
4054
+ border-style: dotted;
4055
+ }
4056
+
4057
+ .intent-ghost.is-auto .intent-ghost-titlebar,
4058
+ .intent-ghost.is-auto .intent-chip {
4059
+ border-style: dotted;
4060
+ }
4061
+
3958
4062
  .intent-ghost {
3959
4063
  position: absolute;
3960
4064
  pointer-events: none;
@@ -291,7 +291,7 @@
291
291
  --c-text-soft: #CBD3CA;
292
292
  --c-muted: #9CA49C;
293
293
  --c-dim: #626962;
294
- --c-accent: #F34E3F;
294
+ --c-accent: #DFDFC1;
295
295
  --c-ok: #52C880;
296
296
  --c-warn: #E4C465;
297
297
  --c-warn-alt: #FFB300;
@@ -307,7 +307,7 @@
307
307
  --color-text-secondary: #CBD3CA;
308
308
  --color-text-muted: #9CA49C;
309
309
  --color-text-dim: #626962;
310
- --color-accent: #F34E3F;
310
+ --color-accent: #DFDFC1;
311
311
  --color-success: #52C880;
312
312
  --color-warning: #E4C465;
313
313
  --color-danger: #D67670;
@@ -21,6 +21,8 @@ export interface CanvasNodeState {
21
21
  | 'ledger'
22
22
  | 'trace'
23
23
  | 'file'
24
+ | 'diff'
25
+ | 'mermaid'
24
26
  | 'image'
25
27
  | 'html'
26
28
  | 'group';
@@ -77,6 +79,8 @@ export const TYPE_LABELS: Record<CanvasNodeState['type'], string> = {
77
79
  ledger: 'LOG',
78
80
  trace: 'TRACE',
79
81
  file: 'FILE',
82
+ diff: 'DIFF',
83
+ mermaid: 'MERMAID',
80
84
  image: 'IMG',
81
85
  html: 'HTML',
82
86
  group: 'GROUP',
@@ -92,6 +96,8 @@ export const EXPANDABLE_TYPES = new Set<CanvasNodeState['type']>([
92
96
  'context',
93
97
  'ledger',
94
98
  'file',
99
+ 'diff',
100
+ 'mermaid',
95
101
  'image',
96
102
  'html',
97
103
  ]);
package/src/mcp/server.ts CHANGED
@@ -26,7 +26,8 @@ import { fileURLToPath } from 'node:url';
26
26
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
27
27
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
28
28
  import { z } from 'zod';
29
- import { canvasState, describeCanvasSchema } from '../server/index.js';
29
+ import { AX_SOURCES } from '../server/operations/ops/ax-shared.js';
30
+ import { canvasState, describeCanvasSchema, stopCanvasServer } from '../server/index.js';
30
31
  import { AX_INTERACTION_TYPES } from '../server/ax-interaction.js';
31
32
  import { buildPendingAxActivity } from '../server/ax-state.js';
32
33
  import { registerOperationTools, registerCompositeTools } from '../server/operations/index.js';
@@ -253,10 +254,7 @@ export async function startMcpServer(): Promise<void> {
253
254
  .describe('Type-specific payload, e.g. {"title":"..."} for ax.work.create.'),
254
255
  sourceSurface: z.enum(['native-node', 'json-render', 'html-node', 'mcp-app', 'adapter']).optional(),
255
256
  correlationId: z.string().optional(),
256
- source: z
257
- .enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
258
- .optional()
259
- .describe('Optional host/source label. Defaults to mcp.'),
257
+ source: z.enum(AX_SOURCES).optional().describe('Optional host/source label. Defaults to mcp.'),
260
258
  },
261
259
  async ({ type, sourceNodeId, payload, sourceSurface, correlationId, source }) => {
262
260
  try {
@@ -345,7 +343,7 @@ export async function startMcpServer(): Promise<void> {
345
343
  })
346
344
  .optional()
347
345
  .describe('Override or suppress the kind-driven default reactions.'),
348
- source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']).optional(),
346
+ source: z.enum(AX_SOURCES).optional(),
349
347
  },
350
348
  async ({ kind, title, summary, outcome, ref, nodeIds, data, reactions, source }) => {
351
349
  try {
@@ -886,6 +884,24 @@ export async function startMcpServer(): Promise<void> {
886
884
  // Connect via stdio
887
885
  const transport = new StdioServerTransport();
888
886
  await server.connect(transport);
887
+
888
+ // Exit when the stdio channel closes (0.4.5 report Finding Y): without this,
889
+ // a client that completes the official close sequence leaves this process
890
+ // orphaned — the auto-started canvas server and its timers keep the event
891
+ // loop alive forever. stopCanvasServer() checkpoints the SQLite DB before
892
+ // exit and is a no-op when this process only attached to a remote daemon.
893
+ const shutdown = (): void => {
894
+ try {
895
+ stopCanvasServer();
896
+ } catch {
897
+ // Exit regardless — a failed cleanup must not resurrect the orphan.
898
+ }
899
+ process.exit(0);
900
+ };
901
+ server.server.onclose = shutdown;
902
+ process.stdin.on('end', shutdown);
903
+ process.on('SIGINT', shutdown);
904
+ process.on('SIGTERM', shutdown);
889
905
  }
890
906
 
891
907
  // Allow direct execution: bun run src/mcp/server.ts
@@ -102,6 +102,12 @@ export const DEFAULT_NODE_AX_CAPABILITIES: Record<CanvasNodeType, NodeAxCapabili
102
102
  'notify-agent',
103
103
  ),
104
104
  file: caps(true, ['ax.evidence.add', 'ax.review.add', 'ax.focus.set', 'ax.event.record']),
105
+ diff: caps(true, ['ax.evidence.add', 'ax.review.add', 'ax.focus.set', 'ax.event.record']),
106
+ mermaid: caps(
107
+ true,
108
+ ['ax.steer', 'ax.work.create', 'ax.evidence.add', 'ax.command.invoke', 'ax.event.record'],
109
+ 'notify-agent',
110
+ ),
105
111
  'json-render': caps(true, [
106
112
  'ax.work.create',
107
113
  'ax.work.update',
@@ -239,7 +239,13 @@ export class AxStateManager {
239
239
  }
240
240
 
241
241
  addWorkItem(
242
- input: { title: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
242
+ input: {
243
+ title: string;
244
+ status?: PmxAxWorkItemStatus;
245
+ detail?: string | null;
246
+ nodeIds?: string[];
247
+ agentId?: string | null;
248
+ },
243
249
  options: { source?: PmxAxSource } = {},
244
250
  ): PmxAxWorkItem {
245
251
  const oldAxState = this.getAxState();
@@ -267,7 +273,13 @@ export class AxStateManager {
267
273
 
268
274
  updateWorkItem(
269
275
  id: string,
270
- patch: { title?: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
276
+ patch: {
277
+ title?: string;
278
+ status?: PmxAxWorkItemStatus;
279
+ detail?: string | null;
280
+ nodeIds?: string[];
281
+ agentId?: string | null;
282
+ },
271
283
  options: { source?: PmxAxSource } = {},
272
284
  ): PmxAxWorkItem | null {
273
285
  const oldAxState = this.getAxState();
@@ -280,6 +292,7 @@ export class AxStateManager {
280
292
  ...(patch.status !== undefined ? { status: patch.status } : {}),
281
293
  ...(patch.detail !== undefined ? { detail: patch.detail } : {}),
282
294
  ...(patch.nodeIds !== undefined ? { nodeIds: patch.nodeIds.filter((n) => validNodeIds.has(n)) } : {}),
295
+ ...(patch.agentId !== undefined ? { agentId: patch.agentId } : {}),
283
296
  updatedAt: new Date().toISOString(),
284
297
  source: options.source ?? existing.source,
285
298
  };
@@ -703,9 +716,9 @@ export class AxStateManager {
703
716
  nodeIds?: string[];
704
717
  data?: Record<string, unknown> | null;
705
718
  },
706
- options: { source?: PmxAxSource } = {},
719
+ options: { source?: PmxAxSource; agentId?: string | null } = {},
707
720
  ): PmxAxEvent {
708
- const draft = createAxEvent(input, options.source ?? 'api');
721
+ const draft = createAxEvent(input, options.source ?? 'api', options.agentId ?? null);
709
722
  const db = this.deps.getDb();
710
723
  if (db) {
711
724
  try {
@@ -746,8 +759,16 @@ export class AxStateManager {
746
759
  return { ...draft, seq: 0 };
747
760
  }
748
761
 
749
- recordSteeringMessage(message: string, options: { source?: PmxAxSource } = {}): PmxAxSteeringMessage {
750
- const draft = createAxSteeringMessage(message, options.source ?? 'api');
762
+ recordSteeringMessage(
763
+ message: string,
764
+ options: { source?: PmxAxSource; agentId?: string | null; target?: string | null } = {},
765
+ ): PmxAxSteeringMessage {
766
+ const draft = createAxSteeringMessage(
767
+ message,
768
+ options.source ?? 'api',
769
+ options.agentId ?? null,
770
+ options.target ?? null,
771
+ );
751
772
  const db = this.deps.getDb();
752
773
  if (db) {
753
774
  try {
@@ -1,7 +1,7 @@
1
1
  import type { CanvasLayout, CanvasNodeState } from './canvas-state.js';
2
2
  import type { AgentContextNode } from './agent-context.js';
3
3
 
4
- export type PmxAxSource = 'agent' | 'api' | 'browser' | 'cli' | 'codex' | 'copilot' | 'mcp' | 'sdk' | 'system';
4
+ export type PmxAxSource = 'agent' | 'amp' | 'api' | 'browser' | 'cli' | 'codex' | 'copilot' | 'mcp' | 'sdk' | 'system';
5
5
 
6
6
  export interface PmxAxFocusState {
7
7
  nodeIds: string[];
@@ -39,6 +39,7 @@ export interface PmxAxWorkItem {
39
39
  createdAt: string;
40
40
  updatedAt: string;
41
41
  source: PmxAxSource | null;
42
+ agentId: string | null;
42
43
  }
43
44
 
44
45
  export interface PmxAxApprovalGate {
@@ -87,6 +88,7 @@ export interface PmxAxEvent {
87
88
  data: Record<string, unknown> | null;
88
89
  createdAt: string;
89
90
  source: PmxAxSource | null;
91
+ agentId: string | null;
90
92
  }
91
93
 
92
94
  export interface PmxAxEvidence {
@@ -109,6 +111,9 @@ export interface PmxAxSteeringMessage {
109
111
  delivered: boolean;
110
112
  createdAt: string;
111
113
  source: PmxAxSource | null;
114
+ agentId: string | null;
115
+ /** The agentId/consumer this steer is addressed to; null = broadcast to all consumers. */
116
+ target: string | null;
112
117
  }
113
118
 
114
119
  // ── Host/session capability (own table; reported by adapters) ──────
@@ -269,7 +274,18 @@ export function buildPendingAxActivity(state: PmxAxState, consumer?: string): Pe
269
274
  return out;
270
275
  }
271
276
 
272
- const AX_SOURCES = new Set<PmxAxSource>(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']);
277
+ const AX_SOURCES = new Set<PmxAxSource>([
278
+ 'agent',
279
+ 'amp',
280
+ 'api',
281
+ 'browser',
282
+ 'cli',
283
+ 'codex',
284
+ 'copilot',
285
+ 'mcp',
286
+ 'sdk',
287
+ 'system',
288
+ ]);
273
289
 
274
290
  function isRecord(value: unknown): value is Record<string, unknown> {
275
291
  return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -622,6 +638,7 @@ export function normalizeAxWorkItem(input: unknown, validNodeIds?: Set<string>):
622
638
  createdAt,
623
639
  updatedAt: normalizeTimestamp(input.updatedAt) ?? createdAt,
624
640
  source: normalizeSource(input.source),
641
+ agentId: optionalString(input.agentId),
625
642
  };
626
643
  }
627
644
 
@@ -715,6 +732,7 @@ export function normalizeAxEvent(input: unknown): PmxAxEvent | null {
715
732
  data: boundedRecord(input.data),
716
733
  createdAt: normalizeTimestamp(input.createdAt) ?? nowIso(),
717
734
  source: normalizeSource(input.source),
735
+ agentId: optionalString(input.agentId),
718
736
  };
719
737
  }
720
738
 
@@ -745,12 +763,20 @@ export function normalizeAxSteeringMessage(input: unknown): PmxAxSteeringMessage
745
763
  delivered: input.delivered === true,
746
764
  createdAt: normalizeTimestamp(input.createdAt) ?? nowIso(),
747
765
  source: normalizeSource(input.source),
766
+ agentId: optionalString(input.agentId),
767
+ target: optionalString(input.target),
748
768
  };
749
769
  }
750
770
 
751
771
  // ── Factories ──────────────────────────────────────────────────────
752
772
  export function createAxWorkItem(
753
- input: { title: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
773
+ input: {
774
+ title: string;
775
+ status?: PmxAxWorkItemStatus;
776
+ detail?: string | null;
777
+ nodeIds?: string[];
778
+ agentId?: string | null;
779
+ },
754
780
  source: PmxAxSource | null,
755
781
  validNodeIds?: Set<string>,
756
782
  ): PmxAxWorkItem {
@@ -764,6 +790,7 @@ export function createAxWorkItem(
764
790
  createdAt: now,
765
791
  updatedAt: now,
766
792
  source,
793
+ agentId: input.agentId ?? null,
767
794
  };
768
795
  }
769
796
 
@@ -829,6 +856,7 @@ export function createAxEvent(
829
856
  data?: Record<string, unknown> | null;
830
857
  },
831
858
  source: PmxAxSource | null,
859
+ agentId: string | null = null,
832
860
  ): Omit<PmxAxEvent, 'seq'> {
833
861
  return {
834
862
  id: axId('evt'),
@@ -839,6 +867,7 @@ export function createAxEvent(
839
867
  data: input.data ?? null,
840
868
  createdAt: nowIso(),
841
869
  source,
870
+ agentId,
842
871
  };
843
872
  }
844
873
 
@@ -869,6 +898,8 @@ export function createAxEvidence(
869
898
  export function createAxSteeringMessage(
870
899
  message: string,
871
900
  source: PmxAxSource | null,
901
+ agentId: string | null = null,
902
+ target: string | null = null,
872
903
  ): Omit<PmxAxSteeringMessage, 'seq'> {
873
904
  return {
874
905
  id: axId('steer'),
@@ -876,6 +907,8 @@ export function createAxSteeringMessage(
876
907
  delivered: false,
877
908
  createdAt: nowIso(),
878
909
  source,
910
+ agentId,
911
+ target,
879
912
  };
880
913
  }
881
914