pmx-canvas 0.3.0 → 0.3.1

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 (156) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/canvas/index.js +2 -2
  3. package/dist/types/cli/daemon.d.ts +74 -0
  4. package/dist/types/cli/watch.d.ts +2 -2
  5. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  6. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  7. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  8. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  9. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  10. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  11. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  12. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  13. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  14. package/dist/types/server/canvas-schema.d.ts +1 -1
  15. package/dist/types/server/html-primitives.d.ts +1 -1
  16. package/dist/types/server/index.d.ts +4 -4
  17. package/dist/types/server/operations/index.d.ts +1 -1
  18. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  19. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  20. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  21. package/docs/http-api.md +28 -0
  22. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  23. package/docs/tech-debt-assessment-2026-07.md +2 -2
  24. package/package.json +5 -2
  25. package/skills/pmx-canvas/SKILL.md +3 -1
  26. package/skills/pmx-canvas/references/full-reference.md +10 -3
  27. package/src/cli/agent.ts +1861 -1548
  28. package/src/cli/daemon.ts +460 -0
  29. package/src/cli/index.ts +63 -326
  30. package/src/cli/watch.ts +2 -10
  31. package/src/client/App.tsx +48 -46
  32. package/src/client/canvas/AttentionHistory.tsx +11 -1
  33. package/src/client/canvas/CanvasNode.tsx +41 -29
  34. package/src/client/canvas/CanvasViewport.tsx +101 -66
  35. package/src/client/canvas/CommandPalette.tsx +61 -27
  36. package/src/client/canvas/ContextMenu.tsx +13 -20
  37. package/src/client/canvas/ContextPinBar.tsx +1 -5
  38. package/src/client/canvas/ContextPinHud.tsx +1 -6
  39. package/src/client/canvas/DockedNode.tsx +4 -4
  40. package/src/client/canvas/EdgeLayer.tsx +37 -36
  41. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  42. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  43. package/src/client/canvas/IntentLayer.tsx +31 -14
  44. package/src/client/canvas/Minimap.tsx +11 -16
  45. package/src/client/canvas/SelectionBar.tsx +4 -11
  46. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  47. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  48. package/src/client/canvas/auto-fit.ts +15 -14
  49. package/src/client/canvas/snap-guides.ts +12 -12
  50. package/src/client/canvas/use-node-resize.ts +1 -5
  51. package/src/client/canvas/use-pan-zoom.ts +25 -26
  52. package/src/client/ext-app/bridge.ts +3 -12
  53. package/src/client/icons.tsx +63 -20
  54. package/src/client/nodes/ContextNode.tsx +14 -25
  55. package/src/client/nodes/ExtAppFrame.tsx +60 -39
  56. package/src/client/nodes/FileNode.tsx +74 -62
  57. package/src/client/nodes/GroupNode.tsx +4 -6
  58. package/src/client/nodes/HtmlNode.tsx +76 -46
  59. package/src/client/nodes/ImageNode.tsx +18 -27
  60. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  61. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  62. package/src/client/nodes/LedgerNode.tsx +10 -4
  63. package/src/client/nodes/MarkdownNode.tsx +3 -10
  64. package/src/client/nodes/McpAppNode.tsx +26 -22
  65. package/src/client/nodes/MdFormatBar.tsx +10 -7
  66. package/src/client/nodes/PromptNode.tsx +23 -51
  67. package/src/client/nodes/ResponseNode.tsx +3 -13
  68. package/src/client/nodes/StatusNode.tsx +5 -9
  69. package/src/client/nodes/StatusSummary.tsx +2 -8
  70. package/src/client/nodes/WebpageNode.tsx +20 -14
  71. package/src/client/nodes/iframe-document-url.ts +25 -16
  72. package/src/client/nodes/image-warnings.ts +1 -7
  73. package/src/client/nodes/md-format.ts +20 -5
  74. package/src/client/state/attention-bridge.ts +4 -9
  75. package/src/client/state/attention-store.ts +1 -7
  76. package/src/client/state/canvas-store.ts +52 -36
  77. package/src/client/state/intent-bridge.ts +176 -112
  78. package/src/client/state/intent-store.ts +4 -1
  79. package/src/client/state/sse-bridge.ts +53 -70
  80. package/src/json-render/catalog.ts +12 -16
  81. package/src/json-render/charts/components.tsx +16 -20
  82. package/src/json-render/charts/extra-components.tsx +8 -16
  83. package/src/json-render/charts/extra-definitions.ts +1 -2
  84. package/src/json-render/charts/tufte-components.tsx +37 -20
  85. package/src/json-render/charts/tufte-definitions.ts +8 -2
  86. package/src/json-render/renderer/index.tsx +42 -22
  87. package/src/json-render/schema.ts +6 -3
  88. package/src/json-render/server.ts +33 -39
  89. package/src/mcp/canvas-access.ts +35 -21
  90. package/src/mcp/server.ts +132 -70
  91. package/src/server/agent-context.ts +63 -36
  92. package/src/server/ax-context.ts +7 -5
  93. package/src/server/ax-interaction.ts +176 -43
  94. package/src/server/ax-state-manager.ts +182 -39
  95. package/src/server/ax-state.ts +142 -47
  96. package/src/server/canvas-db.ts +213 -95
  97. package/src/server/canvas-operations.ts +177 -120
  98. package/src/server/canvas-provenance.ts +1 -4
  99. package/src/server/canvas-schema.ts +454 -73
  100. package/src/server/canvas-serialization.ts +27 -35
  101. package/src/server/canvas-state.ts +150 -58
  102. package/src/server/chart-template.ts +4 -5
  103. package/src/server/code-graph.ts +19 -6
  104. package/src/server/diagram-presets.ts +28 -29
  105. package/src/server/ext-app-lookup.ts +3 -12
  106. package/src/server/html-node-summary.ts +19 -10
  107. package/src/server/html-primitives.ts +326 -97
  108. package/src/server/html-surface.ts +6 -9
  109. package/src/server/image-source.ts +6 -4
  110. package/src/server/index.ts +320 -217
  111. package/src/server/intent-registry.ts +2 -5
  112. package/src/server/mcp-app-candidate.ts +5 -10
  113. package/src/server/mcp-app-host.ts +14 -38
  114. package/src/server/mcp-app-runtime.ts +12 -20
  115. package/src/server/mutation-history.ts +15 -5
  116. package/src/server/operations/composites.ts +1 -3
  117. package/src/server/operations/http.ts +2 -3
  118. package/src/server/operations/index.ts +7 -1
  119. package/src/server/operations/invoker.ts +4 -3
  120. package/src/server/operations/mcp.ts +22 -30
  121. package/src/server/operations/ops/annotation.ts +122 -10
  122. package/src/server/operations/ops/app.ts +98 -73
  123. package/src/server/operations/ops/ax-await.ts +17 -10
  124. package/src/server/operations/ops/ax-read.ts +347 -0
  125. package/src/server/operations/ops/ax-shared.ts +2 -7
  126. package/src/server/operations/ops/ax-state.ts +32 -14
  127. package/src/server/operations/ops/ax-timeline.ts +32 -19
  128. package/src/server/operations/ops/ax-work.ts +54 -37
  129. package/src/server/operations/ops/batch.ts +39 -14
  130. package/src/server/operations/ops/canvas-wire.ts +91 -0
  131. package/src/server/operations/ops/edges.ts +37 -25
  132. package/src/server/operations/ops/ext-app.ts +346 -0
  133. package/src/server/operations/ops/groups.ts +49 -20
  134. package/src/server/operations/ops/intent.ts +18 -12
  135. package/src/server/operations/ops/json-render.ts +239 -98
  136. package/src/server/operations/ops/nodes.ts +298 -109
  137. package/src/server/operations/ops/query.ts +46 -28
  138. package/src/server/operations/ops/snapshots.ts +35 -26
  139. package/src/server/operations/ops/validate.ts +2 -1
  140. package/src/server/operations/ops/viewport.ts +60 -16
  141. package/src/server/operations/ops/webview.ts +44 -18
  142. package/src/server/operations/registry.ts +2 -3
  143. package/src/server/operations/types.ts +7 -5
  144. package/src/server/operations/webview-runner.ts +1 -3
  145. package/src/server/placement.ts +8 -18
  146. package/src/server/server.ts +108 -1027
  147. package/src/server/spatial-analysis.ts +39 -25
  148. package/src/server/trace-manager.ts +3 -8
  149. package/src/server/web-artifacts.ts +23 -27
  150. package/src/server/webpage-node.ts +5 -13
  151. package/src/shared/auto-arrange.ts +12 -5
  152. package/src/shared/content-height-reporter.ts +8 -6
  153. package/src/shared/ext-app-tool-result.ts +2 -6
  154. package/src/shared/placement.ts +1 -4
  155. package/src/shared/semantic-attention.ts +39 -37
  156. package/src/shared/surface.ts +8 -4
@@ -106,10 +106,7 @@ function entryFromContextPin(event: ContextPinWatchEvent): AttentionEntry | null
106
106
  'context',
107
107
  'Context updated',
108
108
  detail,
109
- [
110
- ...event.added.map((node) => node.id),
111
- ...event.removed.map((node) => node.id),
112
- ],
109
+ [...event.added.map((node) => node.id), ...event.removed.map((node) => node.id)],
113
110
  event.timestamp ? Date.parse(event.timestamp) || Date.now() : Date.now(),
114
111
  );
115
112
  }
@@ -148,10 +145,7 @@ function entryFromRemove(event: RemoveWatchEvent): AttentionEntry | null {
148
145
  'remove',
149
146
  'Items removed',
150
147
  parts.join(' · '),
151
- [
152
- ...event.nodes.map((node) => node.id),
153
- ...event.edges.flatMap((edge) => [edge.fromId, edge.toId]),
154
- ],
148
+ [...event.nodes.map((node) => node.id), ...event.edges.flatMap((edge) => [edge.fromId, edge.toId])],
155
149
  event.timestamp ? Date.parse(event.timestamp) || Date.now() : Date.now(),
156
150
  );
157
151
  }
@@ -318,7 +312,8 @@ export function resetAttentionBridge(): void {
318
312
  }
319
313
 
320
314
  export function syncAttentionFromSse(message: SseMessage): void {
321
- const entries = reducer.handleMessage(message)
315
+ const entries = reducer
316
+ .handleMessage(message)
322
317
  .map((event) => entryFromEvent(event))
323
318
  .filter((entry): entry is AttentionEntry => entry !== null);
324
319
 
@@ -1,13 +1,7 @@
1
1
  import { signal } from '@preact/signals';
2
2
  import type { SemanticAttentionRegion } from '../../shared/semantic-attention.js';
3
3
 
4
- export type AttentionTone =
5
- | 'context'
6
- | 'relationship'
7
- | 'group'
8
- | 'cluster'
9
- | 'neighborhood'
10
- | 'remove';
4
+ export type AttentionTone = 'context' | 'relationship' | 'group' | 'cluster' | 'neighborhood' | 'remove';
11
5
 
12
6
  export interface AttentionEntry {
13
7
  id: string;
@@ -1,5 +1,13 @@
1
1
  import { batch, computed, signal } from '@preact/signals';
2
- import { isExcalidrawNode, type CanvasAnnotation, type CanvasEdge, type CanvasLayout, type CanvasNodeState, type ConnectionStatus, type ViewportState } from '../types';
2
+ import {
3
+ isExcalidrawNode,
4
+ type CanvasAnnotation,
5
+ type CanvasEdge,
6
+ type CanvasLayout,
7
+ type CanvasNodeState,
8
+ type ConnectionStatus,
9
+ type ViewportState,
10
+ } from '../types';
3
11
  import { computeAutoArrange } from '../../shared/auto-arrange';
4
12
  import { pushCanvasUpdate, updateViewportFromClient } from './intent-bridge';
5
13
 
@@ -29,7 +37,7 @@ export const axSurfaceState = signal<unknown>(null);
29
37
  export const expandedNodeId = signal<string | null>(null);
30
38
  export const pendingExpandedNodeCloseId = signal<string | null>(null);
31
39
  let expandedCloseTimer: ReturnType<typeof setTimeout> | null = null;
32
- let pendingCloseInitialCheckpointAt: unknown = undefined;
40
+ let pendingCloseInitialCheckpointAt: unknown;
33
41
  const EXCALIDRAW_CLOSE_POLL_MS = 100;
34
42
  const EXCALIDRAW_CLOSE_MAX_WAIT_MS = 2500;
35
43
 
@@ -54,10 +62,7 @@ export const selectedNodeIds = signal<Set<string>>(new Set());
54
62
  // ── Context pins (persistent context for agent queries) ──────
55
63
  export const contextPinnedNodeIds = signal<Set<string>>(new Set());
56
64
 
57
- export function getNeighborNodeIds(
58
- nodeId: string | null,
59
- edgeMap: Map<string, CanvasEdge>,
60
- ): Set<string> {
65
+ export function getNeighborNodeIds(nodeId: string | null, edgeMap: Map<string, CanvasEdge>): Set<string> {
61
66
  if (!nodeId) return new Set();
62
67
 
63
68
  const neighborIds = new Set<string>();
@@ -392,10 +397,7 @@ export function commitViewport(next: ViewportState): void {
392
397
  commitViewportWithOptions(next);
393
398
  }
394
399
 
395
- function commitViewportWithOptions(
396
- next: ViewportState,
397
- options: { recordHistory?: boolean } = {},
398
- ): void {
400
+ function commitViewportWithOptions(next: ViewportState, options: { recordHistory?: boolean } = {}): void {
399
401
  viewport.value = next;
400
402
  persistLayout(options);
401
403
  void updateViewportFromClient(next, options);
@@ -412,9 +414,7 @@ export function applyServerCanvasLayout(
412
414
  if (node.zIndex >= nextMaxZ) nextMaxZ = node.zIndex + 1;
413
415
  }
414
416
 
415
- const edgeSource = layout.edges.filter(
416
- (edge) => nextNodes.has(edge.from) && nextNodes.has(edge.to),
417
- );
417
+ const edgeSource = layout.edges.filter((edge) => nextNodes.has(edge.from) && nextNodes.has(edge.to));
418
418
  const nextEdges = new Map<string, CanvasEdge>();
419
419
  for (const edge of edgeSource) {
420
420
  nextEdges.set(edge.id, edge);
@@ -424,8 +424,7 @@ export function applyServerCanvasLayout(
424
424
  nextAnnotations.set(annotation.id, annotation);
425
425
  }
426
426
 
427
- const nextActiveNodeId =
428
- activeNodeId.value !== null && nextNodes.has(activeNodeId.value) ? activeNodeId.value : null;
427
+ const nextActiveNodeId = activeNodeId.value !== null && nextNodes.has(activeNodeId.value) ? activeNodeId.value : null;
429
428
  const nextExpandedNodeId =
430
429
  expandedNodeId.value !== null && nextNodes.has(expandedNodeId.value) ? expandedNodeId.value : null;
431
430
  const nextSelectedNodeIds = filterNodeIdSet(selectedNodeIds.value, nextNodes);
@@ -618,11 +617,15 @@ export function focusNode(id: string, options: { recordHistory?: boolean } = {})
618
617
  const v = viewport.value;
619
618
  const cx = node.position.x + node.size.width / 2;
620
619
  const cy = node.position.y + node.size.height / 2;
621
- animateViewport({
622
- x: window.innerWidth / 2 - cx * v.scale,
623
- y: window.innerHeight / 2 - cy * v.scale,
624
- scale: v.scale,
625
- }, 300, options);
620
+ animateViewport(
621
+ {
622
+ x: window.innerWidth / 2 - cx * v.scale,
623
+ y: window.innerHeight / 2 - cy * v.scale,
624
+ scale: v.scale,
625
+ },
626
+ 300,
627
+ options,
628
+ );
626
629
  bringToFront(id);
627
630
  }
628
631
 
@@ -669,10 +672,18 @@ export function walkGraph(direction: 'up' | 'down' | 'left' | 'right'): void {
669
672
  // Dot product with direction vector, normalized by distance
670
673
  let dot: number;
671
674
  switch (direction) {
672
- case 'up': dot = -dy; break;
673
- case 'down': dot = dy; break;
674
- case 'left': dot = -dx; break;
675
- case 'right': dot = dx; break;
675
+ case 'up':
676
+ dot = -dy;
677
+ break;
678
+ case 'down':
679
+ dot = dy;
680
+ break;
681
+ case 'left':
682
+ dot = -dx;
683
+ break;
684
+ case 'right':
685
+ dot = dx;
686
+ break;
676
687
  }
677
688
 
678
689
  // Only consider nodes that are at least somewhat in the right direction
@@ -727,10 +738,7 @@ export function collapseExpandedNode(): void {
727
738
  const pollForSave = () => {
728
739
  const latestNode = nodes.value.get(closingNodeId);
729
740
  const latestCheckpointAt = (latestNode?.data.appCheckpoint as { updatedAt?: unknown } | undefined)?.updatedAt;
730
- if (
731
- latestCheckpointAt !== undefined &&
732
- latestCheckpointAt !== pendingCloseInitialCheckpointAt
733
- ) {
741
+ if (latestCheckpointAt !== undefined && latestCheckpointAt !== pendingCloseInitialCheckpointAt) {
734
742
  finishExpandedNodeClose(closingNodeId);
735
743
  return;
736
744
  }
@@ -762,10 +770,14 @@ export function autoArrange(): void {
762
770
  updateNode(id, { position });
763
771
  }
764
772
  for (const [groupId, bounds] of result.groupBounds.entries()) {
765
- updateNodeWithOptions(groupId, {
766
- position: { x: bounds.x, y: bounds.y },
767
- size: { width: bounds.width, height: bounds.height },
768
- }, { skipGroupChildTranslation: true });
773
+ updateNodeWithOptions(
774
+ groupId,
775
+ {
776
+ position: { x: bounds.x, y: bounds.y },
777
+ size: { width: bounds.width, height: bounds.height },
778
+ },
779
+ { skipGroupChildTranslation: true },
780
+ );
769
781
  }
770
782
  });
771
783
  persistLayout();
@@ -780,10 +792,14 @@ export function forceDirectedArrange(): void {
780
792
  updateNode(id, { position });
781
793
  }
782
794
  for (const [groupId, bounds] of result.groupBounds.entries()) {
783
- updateNodeWithOptions(groupId, {
784
- position: { x: bounds.x, y: bounds.y },
785
- size: { width: bounds.width, height: bounds.height },
786
- }, { skipGroupChildTranslation: true });
795
+ updateNodeWithOptions(
796
+ groupId,
797
+ {
798
+ position: { x: bounds.x, y: bounds.y },
799
+ size: { width: bounds.width, height: bounds.height },
800
+ },
801
+ { skipGroupChildTranslation: true },
802
+ );
787
803
  }
788
804
  });
789
805
  persistLayout();
@@ -2,12 +2,7 @@ function logRequestError(action: string, error: unknown): void {
2
2
  console.error(`[intent-bridge] ${action} failed`, error);
3
3
  }
4
4
 
5
- async function requestJson<T>(
6
- action: string,
7
- url: string,
8
- fallback: T,
9
- init?: RequestInit,
10
- ): Promise<T> {
5
+ async function requestJson<T>(action: string, url: string, fallback: T, init?: RequestInit): Promise<T> {
11
6
  try {
12
7
  const res = await fetch(url, init);
13
8
  return (await res.json()) as T;
@@ -17,11 +12,7 @@ async function requestJson<T>(
17
12
  }
18
13
  }
19
14
 
20
- async function requestOk(
21
- action: string,
22
- url: string,
23
- init?: RequestInit,
24
- ): Promise<{ ok: boolean }> {
15
+ async function requestOk(action: string, url: string, init?: RequestInit): Promise<{ ok: boolean }> {
25
16
  try {
26
17
  const res = await fetch(url, init);
27
18
  return { ok: res.ok };
@@ -31,11 +22,7 @@ async function requestOk(
31
22
  }
32
23
  }
33
24
 
34
- async function requestBestEffort(
35
- action: string,
36
- url: string,
37
- init?: RequestInit,
38
- ): Promise<void> {
25
+ async function requestBestEffort(action: string, url: string, init?: RequestInit): Promise<void> {
39
26
  try {
40
27
  await fetch(url, init);
41
28
  } catch (error) {
@@ -44,15 +31,17 @@ async function requestBestEffort(
44
31
  }
45
32
 
46
33
  /** Dispatch user intents from the canvas to the server (for TUI consumption). */
47
- export async function sendIntent(
48
- type: string,
49
- payload: Record<string, unknown> = {},
50
- ): Promise<{ ok: boolean }> {
51
- return requestJson('sendIntent', '/api/workbench/intent', { ok: false }, {
52
- method: 'POST',
53
- headers: { 'Content-Type': 'application/json' },
54
- body: JSON.stringify({ type, payload }),
55
- });
34
+ export async function sendIntent(type: string, payload: Record<string, unknown> = {}): Promise<{ ok: boolean }> {
35
+ return requestJson(
36
+ 'sendIntent',
37
+ '/api/workbench/intent',
38
+ { ok: false },
39
+ {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ type, payload }),
43
+ },
44
+ );
56
45
  }
57
46
 
58
47
  /**
@@ -88,11 +77,16 @@ export async function vetoGhostIntent(intent: {
88
77
 
89
78
  /** Fetch rendered markdown HTML from the server. */
90
79
  export async function renderMarkdown(markdown: string): Promise<string> {
91
- const data = await requestJson<{ html?: string }>('renderMarkdown', '/api/render', {}, {
92
- method: 'POST',
93
- headers: { 'Content-Type': 'application/json' },
94
- body: JSON.stringify({ markdown }),
95
- });
80
+ const data = await requestJson<{ html?: string }>(
81
+ 'renderMarkdown',
82
+ '/api/render',
83
+ {},
84
+ {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({ markdown }),
88
+ },
89
+ );
96
90
  return data.html ?? '';
97
91
  }
98
92
 
@@ -105,15 +99,17 @@ export async function fetchFile(path: string): Promise<{
105
99
  }
106
100
 
107
101
  /** Save file content to the server. */
108
- export async function saveFile(
109
- path: string,
110
- content: string,
111
- ): Promise<{ ok: boolean; updatedAt?: string }> {
112
- return requestJson('saveFile', '/api/file/save', { ok: false }, {
113
- method: 'POST',
114
- headers: { 'Content-Type': 'application/json' },
115
- body: JSON.stringify({ path, content }),
116
- });
102
+ export async function saveFile(path: string, content: string): Promise<{ ok: boolean; updatedAt?: string }> {
103
+ return requestJson(
104
+ 'saveFile',
105
+ '/api/file/save',
106
+ { ok: false },
107
+ {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ path, content }),
111
+ },
112
+ );
117
113
  }
118
114
 
119
115
  /** Fetch current workbench state. */
@@ -136,11 +132,16 @@ export async function fetchCanvasState(): Promise<Record<string, unknown>> {
136
132
  }
137
133
 
138
134
  export async function saveCanvasTheme(theme: string): Promise<{ ok: boolean; theme?: string }> {
139
- return requestJson('saveCanvasTheme', '/api/canvas/theme', { ok: false }, {
140
- method: 'POST',
141
- headers: { 'Content-Type': 'application/json' },
142
- body: JSON.stringify({ theme }),
143
- });
135
+ return requestJson(
136
+ 'saveCanvasTheme',
137
+ '/api/canvas/theme',
138
+ { ok: false },
139
+ {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify({ theme }),
143
+ },
144
+ );
144
145
  }
145
146
 
146
147
  /** Fetch available slash commands for prompt completion. */
@@ -157,17 +158,22 @@ export async function submitCanvasPrompt(
157
158
  threadNodeId?: string,
158
159
  ): Promise<{ ok: boolean; nodeId?: string; error?: string }> {
159
160
  if (!text.trim()) return { ok: false, error: 'Prompt text is required' };
160
- return requestJson('submitCanvasPrompt', '/api/canvas/prompt', { ok: false, error: 'Network error' }, {
161
- method: 'POST',
162
- headers: { 'Content-Type': 'application/json' },
163
- body: JSON.stringify({
164
- text,
165
- ...(position ? { position } : {}),
166
- ...(parentNodeId ? { parentNodeId } : {}),
167
- ...(contextNodeIds && contextNodeIds.length > 0 ? { contextNodeIds } : {}),
168
- ...(threadNodeId ? { threadNodeId } : {}),
169
- }),
170
- });
161
+ return requestJson(
162
+ 'submitCanvasPrompt',
163
+ '/api/canvas/prompt',
164
+ { ok: false, error: 'Network error' },
165
+ {
166
+ method: 'POST',
167
+ headers: { 'Content-Type': 'application/json' },
168
+ body: JSON.stringify({
169
+ text,
170
+ ...(position ? { position } : {}),
171
+ ...(parentNodeId ? { parentNodeId } : {}),
172
+ ...(contextNodeIds && contextNodeIds.length > 0 ? { contextNodeIds } : {}),
173
+ ...(threadNodeId ? { threadNodeId } : {}),
174
+ }),
175
+ },
176
+ );
171
177
  }
172
178
 
173
179
  /** Submit a reply into an existing prompt thread. */
@@ -203,11 +209,16 @@ export async function createEdgeFromClient(
203
209
  type: string,
204
210
  label?: string,
205
211
  ): Promise<{ ok: boolean; id?: string }> {
206
- return requestJson('createEdgeFromClient', '/api/canvas/edge', { ok: false }, {
207
- method: 'POST',
208
- headers: { 'Content-Type': 'application/json' },
209
- body: JSON.stringify({ from, to, type, label }),
210
- });
212
+ return requestJson(
213
+ 'createEdgeFromClient',
214
+ '/api/canvas/edge',
215
+ { ok: false },
216
+ {
217
+ method: 'POST',
218
+ headers: { 'Content-Type': 'application/json' },
219
+ body: JSON.stringify({ from, to, type, label }),
220
+ },
221
+ );
211
222
  }
212
223
 
213
224
  /** Create a canvas node via the server. Returns the new node ID. */
@@ -220,11 +231,16 @@ export async function createNodeFromClient(opts: {
220
231
  width?: number;
221
232
  height?: number;
222
233
  }): Promise<{ ok: boolean; id?: string }> {
223
- return requestJson('createNodeFromClient', '/api/canvas/node', { ok: false }, {
224
- method: 'POST',
225
- headers: { 'Content-Type': 'application/json' },
226
- body: JSON.stringify(opts),
227
- });
234
+ return requestJson(
235
+ 'createNodeFromClient',
236
+ '/api/canvas/node',
237
+ { ok: false },
238
+ {
239
+ method: 'POST',
240
+ headers: { 'Content-Type': 'application/json' },
241
+ body: JSON.stringify(opts),
242
+ },
243
+ );
228
244
  }
229
245
 
230
246
  /** Update a canvas node via the server. */
@@ -241,11 +257,16 @@ export async function updateNodeFromClient(
241
257
  data?: Record<string, unknown>;
242
258
  },
243
259
  ): Promise<{ ok: boolean; id?: string }> {
244
- return requestJson('updateNodeFromClient', `/api/canvas/node/${encodeURIComponent(id)}`, { ok: false }, {
245
- method: 'PATCH',
246
- headers: { 'Content-Type': 'application/json' },
247
- body: JSON.stringify(patch),
248
- });
260
+ return requestJson(
261
+ 'updateNodeFromClient',
262
+ `/api/canvas/node/${encodeURIComponent(id)}`,
263
+ { ok: false },
264
+ {
265
+ method: 'PATCH',
266
+ headers: { 'Content-Type': 'application/json' },
267
+ body: JSON.stringify(patch),
268
+ },
269
+ );
249
270
  }
250
271
 
251
272
  /** Refresh a webpage node from its persisted URL on the server. */
@@ -253,18 +274,28 @@ export async function refreshWebpageNodeFromClient(
253
274
  id: string,
254
275
  url?: string,
255
276
  ): Promise<{ ok: boolean; id?: string; error?: string }> {
256
- return requestJson('refreshWebpageNodeFromClient', `/api/canvas/node/${encodeURIComponent(id)}/refresh`, { ok: false }, {
257
- method: 'POST',
258
- headers: { 'Content-Type': 'application/json' },
259
- body: JSON.stringify(url ? { url } : {}),
260
- });
277
+ return requestJson(
278
+ 'refreshWebpageNodeFromClient',
279
+ `/api/canvas/node/${encodeURIComponent(id)}/refresh`,
280
+ { ok: false },
281
+ {
282
+ method: 'POST',
283
+ headers: { 'Content-Type': 'application/json' },
284
+ body: JSON.stringify(url ? { url } : {}),
285
+ },
286
+ );
261
287
  }
262
288
 
263
289
  /** Remove a canvas node via the server. */
264
290
  export async function removeNodeFromClient(id: string): Promise<{ ok: boolean; removed?: string }> {
265
- return requestJson('removeNodeFromClient', `/api/canvas/node/${encodeURIComponent(id)}`, { ok: false }, {
266
- method: 'DELETE',
267
- });
291
+ return requestJson(
292
+ 'removeNodeFromClient',
293
+ `/api/canvas/node/${encodeURIComponent(id)}`,
294
+ { ok: false },
295
+ {
296
+ method: 'DELETE',
297
+ },
298
+ );
268
299
  }
269
300
 
270
301
  // ── PMX-AX node interactions ──────────────────────────────────
@@ -292,7 +323,10 @@ export async function fetchAxSurfaceState(): Promise<unknown> {
292
323
  }
293
324
 
294
325
  /** Ask the server to open a node's surface in the system browser. */
295
- export async function openNodeInSystemBrowserRequest(nodeId: string, url?: string): Promise<{ ok: boolean; opened: boolean }> {
326
+ export async function openNodeInSystemBrowserRequest(
327
+ nodeId: string,
328
+ url?: string,
329
+ ): Promise<{ ok: boolean; opened: boolean }> {
296
330
  return requestJson<{ ok: boolean; opened: boolean }>(
297
331
  'openNodeInSystemBrowserRequest',
298
332
  '/api/canvas/open-external',
@@ -324,14 +358,19 @@ export async function updateViewportFromClient(
324
358
  viewport: { x: number; y: number; scale: number },
325
359
  options: { recordHistory?: boolean } = {},
326
360
  ): Promise<{ ok: boolean }> {
327
- return requestJson('updateViewportFromClient', '/api/canvas/viewport', { ok: false }, {
328
- method: 'POST',
329
- headers: { 'Content-Type': 'application/json' },
330
- body: JSON.stringify({
331
- ...viewport,
332
- ...(options.recordHistory === false ? { recordHistory: false } : {}),
333
- }),
334
- });
361
+ return requestJson(
362
+ 'updateViewportFromClient',
363
+ '/api/canvas/viewport',
364
+ { ok: false },
365
+ {
366
+ method: 'POST',
367
+ headers: { 'Content-Type': 'application/json' },
368
+ body: JSON.stringify({
369
+ ...viewport,
370
+ ...(options.recordHistory === false ? { recordHistory: false } : {}),
371
+ }),
372
+ },
373
+ );
335
374
  }
336
375
 
337
376
  // ── Group API ─────────────────────────────────────────────────
@@ -346,29 +385,44 @@ export async function createGroupFromClient(opts: {
346
385
  width?: number;
347
386
  height?: number;
348
387
  }): Promise<{ ok: boolean; id?: string }> {
349
- return requestJson('createGroupFromClient', '/api/canvas/group', { ok: false }, {
350
- method: 'POST',
351
- headers: { 'Content-Type': 'application/json' },
352
- body: JSON.stringify(opts),
353
- });
388
+ return requestJson(
389
+ 'createGroupFromClient',
390
+ '/api/canvas/group',
391
+ { ok: false },
392
+ {
393
+ method: 'POST',
394
+ headers: { 'Content-Type': 'application/json' },
395
+ body: JSON.stringify(opts),
396
+ },
397
+ );
354
398
  }
355
399
 
356
400
  /** Add nodes to an existing group. */
357
401
  export async function addToGroupFromClient(groupId: string, childIds: string[]): Promise<{ ok: boolean }> {
358
- return requestJson('addToGroupFromClient', '/api/canvas/group/add', { ok: false }, {
359
- method: 'POST',
360
- headers: { 'Content-Type': 'application/json' },
361
- body: JSON.stringify({ groupId, childIds }),
362
- });
402
+ return requestJson(
403
+ 'addToGroupFromClient',
404
+ '/api/canvas/group/add',
405
+ { ok: false },
406
+ {
407
+ method: 'POST',
408
+ headers: { 'Content-Type': 'application/json' },
409
+ body: JSON.stringify({ groupId, childIds }),
410
+ },
411
+ );
363
412
  }
364
413
 
365
414
  /** Ungroup all children from a group. */
366
415
  export async function ungroupFromClient(groupId: string): Promise<{ ok: boolean }> {
367
- return requestJson('ungroupFromClient', '/api/canvas/group/ungroup', { ok: false }, {
368
- method: 'POST',
369
- headers: { 'Content-Type': 'application/json' },
370
- body: JSON.stringify({ groupId }),
371
- });
416
+ return requestJson(
417
+ 'ungroupFromClient',
418
+ '/api/canvas/group/ungroup',
419
+ { ok: false },
420
+ {
421
+ method: 'POST',
422
+ headers: { 'Content-Type': 'application/json' },
423
+ body: JSON.stringify({ groupId }),
424
+ },
425
+ );
372
426
  }
373
427
 
374
428
  // ── Snapshot API ──────────────────────────────────────────────
@@ -386,11 +440,16 @@ export async function listSnapshots(): Promise<CanvasSnapshotInfo[]> {
386
440
  }
387
441
 
388
442
  export async function saveSnapshot(name: string): Promise<{ ok: boolean; snapshot?: CanvasSnapshotInfo }> {
389
- return requestJson('saveSnapshot', '/api/canvas/snapshots', { ok: false }, {
390
- method: 'POST',
391
- headers: { 'Content-Type': 'application/json' },
392
- body: JSON.stringify({ name }),
393
- });
443
+ return requestJson(
444
+ 'saveSnapshot',
445
+ '/api/canvas/snapshots',
446
+ { ok: false },
447
+ {
448
+ method: 'POST',
449
+ headers: { 'Content-Type': 'application/json' },
450
+ body: JSON.stringify({ name }),
451
+ },
452
+ );
394
453
  }
395
454
 
396
455
  export async function restoreSnapshot(id: string): Promise<{ ok: boolean }> {
@@ -403,9 +462,14 @@ export async function deleteSnapshot(id: string): Promise<{ ok: boolean }> {
403
462
 
404
463
  /** Remove a canvas edge via the server. */
405
464
  export async function removeEdgeFromClient(edgeId: string): Promise<{ ok: boolean }> {
406
- return requestJson('removeEdgeFromClient', '/api/canvas/edge', { ok: false }, {
407
- method: 'DELETE',
408
- headers: { 'Content-Type': 'application/json' },
409
- body: JSON.stringify({ edge_id: edgeId }),
410
- });
465
+ return requestJson(
466
+ 'removeEdgeFromClient',
467
+ '/api/canvas/edge',
468
+ { ok: false },
469
+ {
470
+ method: 'DELETE',
471
+ headers: { 'Content-Type': 'application/json' },
472
+ body: JSON.stringify({ edge_id: edgeId }),
473
+ },
474
+ );
411
475
  }
@@ -63,7 +63,10 @@ function setPhase(id: string, phase: IntentPhase, ms: number, settledNodeId?: st
63
63
  next.set(id, { ...current, phase, ...(settledNodeId ? { settledNodeId } : {}) });
64
64
  writeIntents(next);
65
65
  clearExitTimer(id);
66
- exitTimers.set(id, setTimeout(() => removeIntent(id), ms));
66
+ exitTimers.set(
67
+ id,
68
+ setTimeout(() => removeIntent(id), ms),
69
+ );
67
70
  }
68
71
 
69
72
  /** Resolve a ghost into a real node — the settle morph, then removal. */