pmx-canvas 0.4.5 → 0.4.7

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 (109) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/Readme.md +76 -36
  3. package/dist/canvas/global.css +169 -3
  4. package/dist/canvas/index.js +112 -87
  5. package/dist/canvas/mermaid-entry.js +3563 -0
  6. package/dist/json-render/index.css +1 -1
  7. package/dist/types/cli/agent.d.ts +1 -0
  8. package/dist/types/cli/commands/smoke.d.ts +1 -0
  9. package/dist/types/cli/shared.d.ts +13 -1
  10. package/dist/types/client/canvas/CanvasNode.d.ts +11 -0
  11. package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
  12. package/dist/types/client/mermaid-entry.d.ts +11 -0
  13. package/dist/types/client/nodes/AxStepControls.d.ts +4 -0
  14. package/dist/types/client/nodes/DiffNode.d.ts +20 -0
  15. package/dist/types/client/nodes/ExtAppFrame.d.ts +0 -2
  16. package/dist/types/client/nodes/FileNode.d.ts +14 -0
  17. package/dist/types/client/nodes/MermaidNode.d.ts +12 -0
  18. package/dist/types/client/state/canvas-store.d.ts +10 -0
  19. package/dist/types/client/state/intent-bridge.d.ts +9 -0
  20. package/dist/types/client/types.d.ts +2 -2
  21. package/dist/types/server/ax-flow-loop.d.ts +5 -0
  22. package/dist/types/server/ax-interaction.d.ts +21 -2
  23. package/dist/types/server/canvas-operations.d.ts +5 -0
  24. package/dist/types/server/canvas-provenance.d.ts +1 -1
  25. package/dist/types/server/canvas-state.d.ts +18 -1
  26. package/dist/types/server/canvas-validation.d.ts +28 -1
  27. package/dist/types/server/file-content.d.ts +46 -0
  28. package/dist/types/server/html-primitives.d.ts +13 -1
  29. package/dist/types/server/html-surface.d.ts +8 -0
  30. package/dist/types/server/index.d.ts +5 -0
  31. package/dist/types/server/operations/ops/ax-work.d.ts +1 -0
  32. package/dist/types/server/operations/ops/nodes.d.ts +1 -1
  33. package/dist/types/server/server.d.ts +1 -0
  34. package/dist/types/server/web-artifacts.d.ts +14 -0
  35. package/dist/types/server/workboard.d.ts +46 -0
  36. package/dist/types/shared/ax-flow.d.ts +51 -0
  37. package/dist/types/shared/ax-surface-protocol.d.ts +7 -0
  38. package/dist/types/shared/node-sizes.d.ts +33 -0
  39. package/docs/cli.md +31 -2
  40. package/docs/environment.md +1 -0
  41. package/docs/http-api.md +29 -0
  42. package/docs/mcp.md +13 -1
  43. package/docs/node-types.md +47 -1
  44. package/docs/screenshot.png +0 -0
  45. package/package.json +5 -4
  46. package/skills/pmx-canvas/SKILL.md +123 -25
  47. package/skills/pmx-canvas/evals/evals.json +54 -3
  48. package/skills/pmx-canvas/references/full-reference.md +11 -6
  49. package/skills/pmx-canvas-testing/SKILL.md +3 -0
  50. package/src/cli/agent.ts +2 -0
  51. package/src/cli/commands/ax.ts +28 -3
  52. package/src/cli/commands/history.ts +1 -1
  53. package/src/cli/commands/nodes.ts +2 -2
  54. package/src/cli/commands/query.ts +1 -1
  55. package/src/cli/commands/smoke.ts +212 -0
  56. package/src/cli/commands/view.ts +1 -1
  57. package/src/cli/index.ts +7 -0
  58. package/src/cli/shared.ts +35 -5
  59. package/src/client/App.tsx +28 -15
  60. package/src/client/canvas/CanvasNode.tsx +54 -5
  61. package/src/client/canvas/CanvasViewport.tsx +6 -0
  62. package/src/client/canvas/EdgeLayer.tsx +44 -24
  63. package/src/client/canvas/ExpandedNodeOverlay.tsx +21 -0
  64. package/src/client/canvas/Minimap.tsx +2 -0
  65. package/src/client/canvas/auto-fit.ts +12 -1
  66. package/src/client/canvas/use-pan-zoom.ts +16 -1
  67. package/src/client/icons.tsx +4 -0
  68. package/src/client/mermaid-entry.ts +76 -0
  69. package/src/client/nodes/AxStepControls.tsx +226 -0
  70. package/src/client/nodes/DiffNode.tsx +114 -0
  71. package/src/client/nodes/ExtAppFrame.tsx +259 -84
  72. package/src/client/nodes/FileNode.tsx +249 -51
  73. package/src/client/nodes/MermaidNode.tsx +81 -0
  74. package/src/client/state/canvas-store.ts +19 -0
  75. package/src/client/state/intent-bridge.ts +32 -0
  76. package/src/client/state/node-factory.ts +2 -0
  77. package/src/client/state/sse-bridge.ts +32 -1
  78. package/src/client/theme/global.css +169 -3
  79. package/src/client/types.ts +6 -0
  80. package/src/mcp/server.ts +19 -1
  81. package/src/server/ax-flow-loop.ts +114 -0
  82. package/src/server/ax-interaction.ts +305 -4
  83. package/src/server/canvas-operations.ts +83 -26
  84. package/src/server/canvas-provenance.ts +2 -0
  85. package/src/server/canvas-schema.ts +33 -0
  86. package/src/server/canvas-state.ts +46 -10
  87. package/src/server/canvas-validation.ts +88 -0
  88. package/src/server/demo-state.json +4605 -523
  89. package/src/server/demo.ts +24 -0
  90. package/src/server/file-content.ts +152 -0
  91. package/src/server/file-watcher.ts +7 -10
  92. package/src/server/html-primitives.ts +636 -0
  93. package/src/server/html-surface.ts +11 -0
  94. package/src/server/index.ts +25 -6
  95. package/src/server/operations/composites.ts +3 -2
  96. package/src/server/operations/ops/ax-work.ts +5 -0
  97. package/src/server/operations/ops/batch.ts +8 -0
  98. package/src/server/operations/ops/json-render.ts +44 -0
  99. package/src/server/operations/ops/nodes.ts +24 -2
  100. package/src/server/operations/ops/viewport.ts +15 -2
  101. package/src/server/server.ts +154 -6
  102. package/src/server/web-artifacts.ts +109 -0
  103. package/src/server/workboard.ts +159 -0
  104. package/src/shared/ax-flow.ts +102 -0
  105. package/src/shared/ax-surface-protocol.ts +8 -0
  106. package/src/shared/node-sizes.ts +52 -0
  107. package/src/shared/surface.ts +2 -0
  108. package/dist/types/server/chart-template.d.ts +0 -29
  109. package/src/server/chart-template.ts +0 -448
@@ -643,6 +643,10 @@ body,
643
643
  overflow: hidden;
644
644
  text-overflow: ellipsis;
645
645
  flex: 1;
646
+ /* The name is the one thing that must survive a cramped title bar. Everything
647
+ else in the bar is flex-shrink: 0, so without a floor the title absorbed all
648
+ the pressure and collapsed to an ellipsis on narrow nodes. */
649
+ min-width: 9ch;
646
650
  min-width: 0;
647
651
  }
648
652
 
@@ -670,6 +674,13 @@ body,
670
674
  }
671
675
 
672
676
  .canvas-node .node-ax-status {
677
+ /* A long status ("in-progress") must not push the node's NAME out of the bar;
678
+ the pill is a glance-level cue, the name is the identity. */
679
+ max-width: 11ch;
680
+ overflow: hidden;
681
+ text-overflow: ellipsis;
682
+ white-space: nowrap;
683
+ flex-shrink: 1;
673
684
  font-size: calc(9px * var(--node-chrome-scale, 1));
674
685
  padding: calc(1px * var(--node-chrome-scale, 1)) calc(6px * var(--node-chrome-scale, 1));
675
686
  border-radius: calc(4px * var(--node-chrome-scale, 1));
@@ -1846,10 +1857,11 @@ html.is-node-resizing .ext-app-preview-catcher {
1846
1857
  border-radius: 999px;
1847
1858
  }
1848
1859
 
1849
- /* Edge labels */
1860
+ /* Edge labels — --edge-chrome-scale is set on the edge <svg> so labels keep a
1861
+ constant screen size while zoomed out (see edgeChromeScale in EdgeLayer). */
1850
1862
  .edge-label {
1851
1863
  font-family: var(--font);
1852
- font-size: 11px;
1864
+ font-size: calc(11px * var(--edge-chrome-scale, 1));
1853
1865
  pointer-events: none;
1854
1866
  user-select: none;
1855
1867
  }
@@ -1857,7 +1869,7 @@ html.is-node-resizing .ext-app-preview-catcher {
1857
1869
  .edge-label-bg {
1858
1870
  fill: var(--c-panel-glass);
1859
1871
  stroke: var(--c-line);
1860
- stroke-width: 0.5;
1872
+ stroke-width: calc(0.5 * var(--edge-chrome-scale, 1));
1861
1873
  }
1862
1874
 
1863
1875
  /* Trace nodes — compact pills for agent tool/subagent visualization */
@@ -1895,6 +1907,160 @@ html.is-node-resizing .ext-app-preview-catcher {
1895
1907
  }
1896
1908
  }
1897
1909
 
1910
+ /* Diff nodes — unified diff viewer with per-file sections */
1911
+ .diff-node {
1912
+ display: flex;
1913
+ flex-direction: column;
1914
+ gap: 10px;
1915
+ height: 100%;
1916
+ overflow: auto;
1917
+ font-family: var(--mono);
1918
+ font-size: 12px;
1919
+ color: var(--c-text);
1920
+ }
1921
+
1922
+ .diff-node.diff-node-empty {
1923
+ align-items: center;
1924
+ justify-content: center;
1925
+ color: var(--c-muted);
1926
+ }
1927
+
1928
+ .diff-node .diff-file {
1929
+ border: 1px solid var(--c-line);
1930
+ border-radius: 6px;
1931
+ overflow: hidden;
1932
+ flex-shrink: 0;
1933
+ }
1934
+
1935
+ .diff-node .diff-file-header {
1936
+ padding: 4px 8px;
1937
+ background: var(--c-panel-overlay);
1938
+ border-bottom: 1px solid var(--c-line);
1939
+ color: var(--c-text-soft);
1940
+ font-weight: 600;
1941
+ white-space: nowrap;
1942
+ overflow: hidden;
1943
+ text-overflow: ellipsis;
1944
+ }
1945
+
1946
+ .diff-node .diff-lines {
1947
+ margin: 0;
1948
+ padding: 4px 0;
1949
+ overflow-x: auto;
1950
+ font-family: inherit;
1951
+ font-size: inherit;
1952
+ line-height: 1.5;
1953
+ }
1954
+
1955
+ .diff-node .diff-lines > div {
1956
+ padding: 0 8px;
1957
+ white-space: pre;
1958
+ }
1959
+
1960
+ .diff-node .diff-hunk {
1961
+ color: var(--c-muted);
1962
+ }
1963
+
1964
+ .diff-node .diff-line-add {
1965
+ color: var(--c-ok);
1966
+ background: var(--c-ok-10);
1967
+ }
1968
+
1969
+ .diff-node .diff-line-remove {
1970
+ color: var(--c-danger);
1971
+ background: var(--c-danger-12);
1972
+ }
1973
+
1974
+ .diff-node .diff-line-context {
1975
+ color: var(--c-text-soft);
1976
+ }
1977
+
1978
+ /* File nodes — delimited-data tables and non-text placeholders */
1979
+ .file-table-shell {
1980
+ display: flex;
1981
+ flex-direction: column;
1982
+ height: 100%;
1983
+ min-height: 0;
1984
+ }
1985
+
1986
+ .file-node-notice {
1987
+ flex-shrink: 0;
1988
+ padding: 4px 10px;
1989
+ color: var(--c-muted);
1990
+ font-size: 10px;
1991
+ }
1992
+
1993
+ /* Own scroll port so a wide CSV scrolls inside the node instead of blowing out
1994
+ the layout — and so the sticky header row has something to stick to. */
1995
+ .file-table-wrap {
1996
+ flex: 1;
1997
+ min-height: 0;
1998
+ overflow: auto;
1999
+ }
2000
+
2001
+ /* The shell/wrap prefix is load-bearing: these rules must out-specify the markdown
2002
+ table styles (`.canvas-node .node-body table` / `th` / `td`), whose 0.5em
2003
+ margin and translucent header background break the sticky header. Borders are
2004
+ separate, not collapsed — collapsed borders belong to the table element, so
2005
+ the header leaks the rows scrolling beneath it. */
2006
+ .file-table-shell .file-table-wrap .file-table {
2007
+ margin: 0;
2008
+ border-collapse: separate;
2009
+ border-spacing: 0;
2010
+ width: max-content;
2011
+ min-width: 100%;
2012
+ font-family: var(--mono);
2013
+ color: var(--c-text);
2014
+ }
2015
+
2016
+ .file-table-shell .file-table-wrap .file-table th,
2017
+ .file-table-shell .file-table-wrap .file-table td {
2018
+ max-width: 320px;
2019
+ padding: 3px 8px;
2020
+ border: none;
2021
+ border-right: 1px solid var(--c-line);
2022
+ border-bottom: 1px solid var(--c-line);
2023
+ text-align: left;
2024
+ white-space: nowrap;
2025
+ overflow: hidden;
2026
+ text-overflow: ellipsis;
2027
+ }
2028
+
2029
+ .file-table-shell .file-table-wrap .file-table th {
2030
+ position: sticky;
2031
+ top: 0;
2032
+ z-index: 1;
2033
+ /* Opaque in every theme — rows must not bleed through while scrolling. */
2034
+ background: var(--c-panel);
2035
+ color: var(--c-text-soft);
2036
+ font-weight: 600;
2037
+ }
2038
+
2039
+ .file-table-shell .file-table-wrap .file-table tbody tr:nth-child(even) {
2040
+ background: var(--c-surface-subtle);
2041
+ }
2042
+
2043
+ .file-node-placeholder {
2044
+ display: flex;
2045
+ flex-direction: column;
2046
+ align-items: center;
2047
+ justify-content: center;
2048
+ gap: 4px;
2049
+ height: 100%;
2050
+ padding: 16px;
2051
+ color: var(--c-muted);
2052
+ text-align: center;
2053
+ }
2054
+
2055
+ .file-node-placeholder-name {
2056
+ color: var(--c-text-soft);
2057
+ word-break: break-all;
2058
+ }
2059
+
2060
+ .file-node-link {
2061
+ color: var(--c-accent);
2062
+ }
2063
+
1898
2064
  /* Docked node — compact toolbar-flanking panel */
1899
2065
  .docked-node {
1900
2066
  min-width: 200px;
@@ -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
@@ -27,7 +27,7 @@ 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
29
  import { AX_SOURCES } from '../server/operations/ops/ax-shared.js';
30
- import { canvasState, describeCanvasSchema } from '../server/index.js';
30
+ import { canvasState, describeCanvasSchema, stopCanvasServer } from '../server/index.js';
31
31
  import { AX_INTERACTION_TYPES } from '../server/ax-interaction.js';
32
32
  import { buildPendingAxActivity } from '../server/ax-state.js';
33
33
  import { registerOperationTools, registerCompositeTools } from '../server/operations/index.js';
@@ -884,6 +884,24 @@ export async function startMcpServer(): Promise<void> {
884
884
  // Connect via stdio
885
885
  const transport = new StdioServerTransport();
886
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);
887
905
  }
888
906
 
889
907
  // Allow direct execution: bun run src/mcp/server.ts
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Server-side bounded loop for materialized AX flows.
3
+ *
4
+ * A flow's loop lives on the CANVAS, not in a panel: the anchor step node carries
5
+ * `data.axFlow.loop` (see shared/ax-flow.ts), so it survives a browser refresh, an
6
+ * iframe reload, and a server restart — where a panel-driven loop dies with its
7
+ * document. The driver is `canvasState`'s work-item change hook: whenever a step's
8
+ * work item reaches `done`, the next step is opened; when the LAST step finishes,
9
+ * the next run starts — until the run cap is reached or a human presses Stop.
10
+ *
11
+ * Bounds (non-negotiable, enforced here rather than trusted from node data):
12
+ * - advances only while `loop.running` is true
13
+ * - stops at `maxRuns`, itself clamped to AX_FLOW_LOOP_HARD_CAP (20)
14
+ * - a `blocked`/`cancelled` step halts advancement (the loop waits, it does not
15
+ * skip); a step whose work item no longer exists stops the loop outright
16
+ * - re-entrancy: every write this module makes fires the same work-item change
17
+ * hook, so `advancing` drops the nested call. State is recomputed from scratch
18
+ * on each pass, so dropping nested calls loses nothing.
19
+ */
20
+ import { AX_FLOW_LOOP_HARD_CAP, readAxFlow, type AxFlowLoopState, type AxFlowStamp } from '../shared/ax-flow.js';
21
+ import type { PmxAxWorkItem } from './ax-state.js';
22
+ import { emitCanvasLayoutUpdate } from './canvas-operations.js';
23
+ import { canvasState, type CanvasNodeState } from './canvas-state.js';
24
+ import { refreshWorkboardNodes } from './workboard.js';
25
+
26
+ /** Re-entrancy guard: true while an advance pass is writing. */
27
+ let advancing = false;
28
+
29
+ /** Persist a new loop state on the anchor node (durable: this is what Stop writes). */
30
+ function writeLoopState(node: CanvasNodeState, flow: AxFlowStamp, loop: AxFlowLoopState): void {
31
+ const current = canvasState.getNode(node.id);
32
+ if (!current) return;
33
+ canvasState.updateNode(node.id, { data: { ...current.data, axFlow: { ...flow, loop } } });
34
+ }
35
+
36
+ /**
37
+ * Advance one flow by at most one step (or one run boundary). Returns true when
38
+ * anything changed, so the caller knows whether to broadcast a layout update.
39
+ */
40
+ function advanceFlow(node: CanvasNodeState, flow: AxFlowStamp, byId: Map<string, PmxAxWorkItem>): boolean {
41
+ const items = flow.steps.map((step) => byId.get(step.workItemId));
42
+ if (items.some((item) => !item)) {
43
+ // The flow was partially dismantled (work items cleared/deleted). Stop rather
44
+ // than keep a loop armed against state it can no longer drive.
45
+ writeLoopState(node, flow, { ...flow.loop, running: false });
46
+ return true;
47
+ }
48
+ const statuses = items as PmxAxWorkItem[];
49
+ // A blocked/cancelled step is a deliberate halt — hold position, stay armed.
50
+ if (statuses.some((item) => item.status === 'blocked' || item.status === 'cancelled')) return false;
51
+
52
+ const openIndex = statuses.findIndex((item) => item.status !== 'done');
53
+ if (openIndex >= 0) {
54
+ // Every earlier step is done by construction. Open this one exactly once:
55
+ // an already in-progress step is the loop's own previous advance.
56
+ if (statuses[openIndex].status !== 'todo') return false;
57
+ canvasState.updateWorkItem(statuses[openIndex].id, { status: 'in-progress' }, { source: 'system' });
58
+ return true;
59
+ }
60
+
61
+ // Every step is done — one full run just completed.
62
+ const maxRuns = Math.min(flow.loop.maxRuns, AX_FLOW_LOOP_HARD_CAP);
63
+ const run = flow.loop.run + 1;
64
+ if (run >= maxRuns) {
65
+ // The cap is reached WITH this run recorded: no run `maxRuns + 1` is opened.
66
+ writeLoopState(node, flow, { running: false, run, maxRuns });
67
+ return true;
68
+ }
69
+ // Write the counter before re-opening the steps, so a crash mid-pass can never
70
+ // replay a run for free.
71
+ writeLoopState(node, flow, { running: true, run, maxRuns });
72
+ for (const step of flow.steps) {
73
+ canvasState.updateWorkItem(step.workItemId, { status: 'todo' }, { source: 'system' });
74
+ }
75
+ canvasState.updateWorkItem(flow.steps[0].workItemId, { status: 'in-progress' }, { source: 'system' });
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * Advance every running flow loop on the canvas. Safe to call on any work-item
81
+ * change: flows that are not running, not ready, or already advanced are no-ops.
82
+ */
83
+ export function advanceAxFlowLoops(): void {
84
+ if (advancing) return;
85
+ advancing = true;
86
+ try {
87
+ const anchors = canvasState
88
+ .getLayout()
89
+ .nodes.map((node) => ({ node, flow: readAxFlow(node.data) }))
90
+ .filter((entry): entry is { node: CanvasNodeState; flow: AxFlowStamp } => entry.flow?.loop.running === true);
91
+ if (anchors.length === 0) return;
92
+ const byId = new Map(canvasState.getWorkItems().map((item) => [item.id, item]));
93
+ let changed = false;
94
+ // Suppressed: loop bookkeeping (status chips, the run counter) is automatic
95
+ // and must not fill undo history — same contract as the live workboard.
96
+ canvasState.withSuppressedRecording(() => {
97
+ for (const { node, flow } of anchors) {
98
+ if (advanceFlow(node, flow, byId)) changed = true;
99
+ }
100
+ });
101
+ if (changed) emitCanvasLayoutUpdate();
102
+ } finally {
103
+ advancing = false;
104
+ }
105
+ }
106
+
107
+ // canvasState exposes ONE work-item listener slot, so this module owns the
108
+ // composition: the live workboard refresh first (unchanged behaviour), then the
109
+ // flow advance. Importing workboard also guarantees ordering — its own
110
+ // registration runs while this module is still evaluating, so this one wins.
111
+ canvasState.setWorkItemsChangedListener(() => {
112
+ refreshWorkboardNodes();
113
+ advanceAxFlowLoops();
114
+ });