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.
- package/CHANGELOG.md +108 -0
- package/Readme.md +52 -4
- package/dist/canvas/global.css +115 -11
- package/dist/canvas/index.js +93 -72
- package/dist/canvas/mermaid-entry.js +3563 -0
- package/dist/canvas/surface-theme.css +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/skills.d.ts +1 -1
- package/dist/types/cli/commands/smoke.d.ts +1 -0
- package/dist/types/client/mermaid-entry.d.ts +11 -0
- package/dist/types/client/nodes/DiffNode.d.ts +20 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +0 -2
- package/dist/types/client/nodes/MermaidNode.d.ts +12 -0
- package/dist/types/client/state/intent-store.d.ts +6 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/server/ax-state-manager.d.ts +5 -0
- package/dist/types/server/ax-state.d.ts +9 -3
- package/dist/types/server/canvas-db.d.ts +3 -2
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +22 -1
- package/dist/types/server/canvas-validation.d.ts +34 -1
- package/dist/types/server/html-surface.d.ts +8 -0
- package/dist/types/server/index.d.ts +5 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +4 -1
- package/dist/types/server/operations/ops/nodes.d.ts +1 -1
- package/dist/types/server/operations/registry.d.ts +9 -1
- package/dist/types/server/workboard.d.ts +46 -0
- package/dist/types/shared/ax-intent.d.ts +6 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +7 -0
- package/docs/cli.md +12 -0
- package/docs/environment.md +1 -0
- package/docs/http-api.md +12 -0
- package/docs/mcp.md +1 -1
- package/docs/node-types.md +2 -0
- package/docs/screenshot.png +0 -0
- package/docs/screenshots/amp-orb-portal.png +0 -0
- package/docs/screenshots/claude-code-desktop.png +0 -0
- package/docs/screenshots/codex-app.png +0 -0
- package/docs/screenshots/github-copilot-app.png +0 -0
- package/package.json +4 -3
- package/skills/pmx-canvas/SKILL.md +71 -24
- package/skills/pmx-canvas-orchestration/SKILL.md +82 -0
- package/skills/pmx-canvas-testing/SKILL.md +3 -0
- package/src/cli/agent.ts +2 -0
- package/src/cli/commands/ax.ts +28 -3
- package/src/cli/commands/skills.ts +1 -1
- package/src/cli/commands/smoke.ts +212 -0
- package/src/cli/index.ts +15 -2
- package/src/cli/shared.ts +1 -0
- package/src/client/canvas/CanvasNode.tsx +12 -1
- package/src/client/canvas/CanvasViewport.tsx +6 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +10 -0
- package/src/client/canvas/IntentLayer.tsx +8 -6
- package/src/client/canvas/Minimap.tsx +2 -0
- package/src/client/canvas/auto-fit.ts +2 -0
- package/src/client/icons.tsx +4 -0
- package/src/client/mermaid-entry.ts +76 -0
- package/src/client/nodes/DiffNode.tsx +114 -0
- package/src/client/nodes/ExtAppFrame.tsx +259 -84
- package/src/client/nodes/MermaidNode.tsx +81 -0
- package/src/client/state/intent-bridge.ts +12 -3
- package/src/client/state/intent-store.ts +47 -2
- package/src/client/state/node-factory.ts +2 -0
- package/src/client/state/sse-bridge.ts +28 -0
- package/src/client/theme/global.css +115 -11
- package/src/client/theme/surface-theme.css +2 -2
- package/src/client/types.ts +6 -0
- package/src/mcp/server.ts +22 -6
- package/src/server/ax-interaction.ts +6 -0
- package/src/server/ax-state-manager.ts +27 -6
- package/src/server/ax-state.ts +36 -3
- package/src/server/canvas-db.ts +83 -60
- package/src/server/canvas-operations.ts +39 -9
- package/src/server/canvas-provenance.ts +2 -0
- package/src/server/canvas-schema.ts +33 -0
- package/src/server/canvas-state.ts +78 -6
- package/src/server/canvas-validation.ts +79 -0
- package/src/server/html-surface.ts +11 -0
- package/src/server/index.ts +25 -6
- package/src/server/intent-registry.ts +2 -0
- package/src/server/operations/composites.ts +3 -2
- package/src/server/operations/http.ts +5 -1
- package/src/server/operations/ops/ax-shared.ts +20 -1
- package/src/server/operations/ops/ax-state.ts +4 -7
- package/src/server/operations/ops/ax-timeline.ts +26 -3
- package/src/server/operations/ops/ax-work.ts +27 -1
- package/src/server/operations/ops/batch.ts +11 -1
- package/src/server/operations/ops/json-render.ts +44 -0
- package/src/server/operations/ops/nodes.ts +24 -2
- package/src/server/operations/ops/viewport.ts +8 -1
- package/src/server/operations/registry.ts +122 -1
- package/src/server/server.ts +58 -3
- package/src/server/workboard.ts +159 -0
- package/src/shared/ax-intent.ts +6 -0
- package/src/shared/ax-surface-protocol.ts +8 -0
- package/src/shared/surface.ts +2 -0
- 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
|
-
|
|
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
|
-
|
|
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: #
|
|
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(
|
|
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(
|
|
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(
|
|
478
|
-
--c-accent-10: rgba(
|
|
479
|
-
--c-accent-12: rgba(
|
|
480
|
-
--c-accent-15: rgba(
|
|
481
|
-
--c-accent-25: rgba(
|
|
482
|
-
--c-accent-30: rgba(
|
|
483
|
-
--c-accent-40: rgba(
|
|
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: #
|
|
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: #
|
|
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: #
|
|
310
|
+
--color-accent: #DFDFC1;
|
|
311
311
|
--color-success: #52C880;
|
|
312
312
|
--color-warning: #E4C465;
|
|
313
313
|
--color-danger: #D67670;
|
package/src/client/types.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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: {
|
|
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: {
|
|
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(
|
|
750
|
-
|
|
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 {
|
package/src/server/ax-state.ts
CHANGED
|
@@ -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>([
|
|
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: {
|
|
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
|
|