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
@@ -125,9 +125,7 @@ function deriveClusterLabel(nodes: CanvasNodeState[]): string {
125
125
  for (const n of nodes) {
126
126
  typeCounts[n.type] = (typeCounts[n.type] ?? 0) + 1;
127
127
  }
128
- const parts = Object.entries(typeCounts).map(([type, count]) =>
129
- count === 1 ? type : `${count} ${type}`,
130
- );
128
+ const parts = Object.entries(typeCounts).map(([type, count]) => (count === 1 ? type : `${count} ${type}`));
131
129
 
132
130
  if (titled) {
133
131
  return `${titled.data.title} + ${parts.join(', ')}`;
@@ -139,22 +137,21 @@ function rectsOverlap(
139
137
  a: { x: number; y: number; width: number; height: number },
140
138
  b: { x: number; y: number; width: number; height: number },
141
139
  ): boolean {
142
- return a.x <= b.x + b.width &&
143
- a.x + a.width >= b.x &&
144
- a.y <= b.y + b.height &&
145
- a.y + a.height >= b.y;
140
+ return a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y;
146
141
  }
147
142
 
148
143
  function summarizeAnnotationForSpatialContext(
149
144
  annotation: CanvasAnnotation,
150
145
  nodes: CanvasNodeState[],
151
146
  ): SpatialAnnotationContext {
152
- const targetNodes = nodes.filter((node) => rectsOverlap(annotation.bounds, {
153
- x: node.position.x,
154
- y: node.position.y,
155
- width: node.size.width,
156
- height: node.size.height,
157
- }));
147
+ const targetNodes = nodes.filter((node) =>
148
+ rectsOverlap(annotation.bounds, {
149
+ x: node.position.x,
150
+ y: node.position.y,
151
+ width: node.size.width,
152
+ height: node.size.height,
153
+ }),
154
+ );
158
155
  const targetNodeTitles = targetNodes.map((node) =>
159
156
  typeof node.data.title === 'string' && node.data.title.length > 0 ? node.data.title : node.id,
160
157
  );
@@ -176,10 +173,7 @@ function summarizeAnnotationForSpatialContext(
176
173
  *
177
174
  * Default threshold: 200px (roughly "visually grouped" on a typical canvas).
178
175
  */
179
- export function detectClusters(
180
- nodes: CanvasNodeState[],
181
- proximityThreshold = 200,
182
- ): SpatialCluster[] {
176
+ export function detectClusters(nodes: CanvasNodeState[], proximityThreshold = 200): SpatialCluster[] {
183
177
  if (nodes.length === 0) return [];
184
178
 
185
179
  // Union-Find for clustering
@@ -312,10 +306,22 @@ export function searchNodes(
312
306
 
313
307
  for (const node of nodes) {
314
308
  const title = ((node.data.title as string) ?? '').toLowerCase();
315
- const content = ((node.data.content as string) ?? (node.data.agentSummary as string) ?? (node.data.contentSummary as string) ?? (node.data.description as string) ?? (node.data.fileContent as string) ?? '').toLowerCase();
309
+ const content = (
310
+ (node.data.content as string) ??
311
+ (node.data.agentSummary as string) ??
312
+ (node.data.contentSummary as string) ??
313
+ (node.data.description as string) ??
314
+ (node.data.fileContent as string) ??
315
+ ''
316
+ ).toLowerCase();
316
317
  const path = ((node.data.path as string) ?? '').toLowerCase();
317
318
  const description = ((node.data.description as string) ?? '').toLowerCase();
318
- const summary = ((node.data.summary as string) ?? (node.data.agentSummary as string) ?? (node.data.contentSummary as string) ?? '').toLowerCase();
319
+ const summary = (
320
+ (node.data.summary as string) ??
321
+ (node.data.agentSummary as string) ??
322
+ (node.data.contentSummary as string) ??
323
+ ''
324
+ ).toLowerCase();
319
325
  const url = ((node.data.url as string) ?? '').toLowerCase();
320
326
 
321
327
  let score = 0;
@@ -333,12 +339,19 @@ export function searchNodes(
333
339
 
334
340
  // Extract a snippet around the first match in content
335
341
  let snippet = '';
336
- const fullContent = (node.data.content as string) ?? (node.data.agentSummary as string) ?? (node.data.contentSummary as string) ?? (node.data.description as string) ?? (node.data.fileContent as string) ?? '';
342
+ const fullContent =
343
+ (node.data.content as string) ??
344
+ (node.data.agentSummary as string) ??
345
+ (node.data.contentSummary as string) ??
346
+ (node.data.description as string) ??
347
+ (node.data.fileContent as string) ??
348
+ '';
337
349
  const matchIdx = fullContent.toLowerCase().indexOf(terms[0]);
338
350
  if (matchIdx >= 0) {
339
351
  const start = Math.max(0, matchIdx - 40);
340
352
  const end = Math.min(fullContent.length, matchIdx + 80);
341
- snippet = (start > 0 ? '...' : '') +
353
+ snippet =
354
+ (start > 0 ? '...' : '') +
342
355
  fullContent.slice(start, end).replace(/\n/g, ' ') +
343
356
  (end < fullContent.length ? '...' : '');
344
357
  } else if (title) {
@@ -383,10 +396,11 @@ export function buildSpatialContext(
383
396
  id: n.id,
384
397
  type: n.type,
385
398
  title: (n.data.title as string) ?? null,
386
- content: summarizeNodeForAgentContext(n, {
387
- defaultTextLength: 320,
388
- webpageTextLength: 640,
389
- }) || null,
399
+ content:
400
+ summarizeNodeForAgentContext(n, {
401
+ defaultTextLength: 320,
402
+ webpageTextLength: 640,
403
+ }) || null,
390
404
  clusterId: nodeToCluster.get(n.id) ?? null,
391
405
  readingOrder: i,
392
406
  }));
@@ -154,8 +154,7 @@ class TraceManager {
154
154
 
155
155
  const startedAt = (node.data.startedAt as number) || Date.now();
156
156
  const durationMs = Math.max(0, Date.now() - startedAt);
157
- const durationText =
158
- durationMs < 1000 ? `${durationMs}ms` : `${(durationMs / 1000).toFixed(1)}s`;
157
+ const durationText = durationMs < 1000 ? `${durationMs}ms` : `${(durationMs / 1000).toFixed(1)}s`;
159
158
 
160
159
  canvasState.updateNode(nodeId, {
161
160
  data: {
@@ -178,10 +177,7 @@ class TraceManager {
178
177
  this.broadcastUpdate();
179
178
  }
180
179
 
181
- onSubagentStarted(payload: {
182
- agentName: string;
183
- agentDisplayName?: string;
184
- }): void {
180
+ onSubagentStarted(payload: { agentName: string; agentDisplayName?: string }): void {
185
181
  if (!this._enabled) return;
186
182
 
187
183
  const id = nextTraceNodeId();
@@ -249,8 +245,7 @@ class TraceManager {
249
245
 
250
246
  const startedAt = (node.data.startedAt as number) || Date.now();
251
247
  const durationMs = payload.durationMs ?? Math.max(0, Date.now() - startedAt);
252
- const durationText =
253
- durationMs < 1000 ? `${durationMs}ms` : `${(durationMs / 1000).toFixed(1)}s`;
248
+ const durationText = durationMs < 1000 ? `${durationMs}ms` : `${(durationMs / 1000).toFixed(1)}s`;
254
249
 
255
250
  canvasState.updateNode(nodeId, {
256
251
  data: {
@@ -169,9 +169,8 @@ function buildWebArtifactSourceContext(
169
169
  }
170
170
 
171
171
  const appPreview = truncatePreviewText(input.appTsx, WEB_ARTIFACT_SOURCE_PREVIEW_MAX_LENGTH);
172
- const cssPreview = typeof input.indexCss === 'string'
173
- ? truncatePreviewText(input.indexCss, WEB_ARTIFACT_CSS_PREVIEW_MAX_LENGTH)
174
- : '';
172
+ const cssPreview =
173
+ typeof input.indexCss === 'string' ? truncatePreviewText(input.indexCss, WEB_ARTIFACT_CSS_PREVIEW_MAX_LENGTH) : '';
175
174
  const allSourceFiles = [...sourceFiles];
176
175
  const storedSourceFiles = allSourceFiles.slice(0, WEB_ARTIFACT_MAX_STORED_SOURCE_FILES);
177
176
  const parts = [
@@ -378,17 +377,12 @@ export function resolveWebArtifactScriptPath(kind: 'init' | 'bundle'): string {
378
377
  if (existsSync(candidate)) return resolve(candidate);
379
378
  }
380
379
 
381
- throw new Error(
382
- `No web-artifact ${kind} script found. Expected one of: ${candidates.join(', ')}`,
383
- );
380
+ throw new Error(`No web-artifact ${kind} script found. Expected one of: ${candidates.join(', ')}`);
384
381
  }
385
382
 
386
383
  function writeProjectFiles(
387
384
  projectPath: string,
388
- input: Pick<
389
- WebArtifactBuildInput,
390
- 'title' | 'appTsx' | 'indexCss' | 'mainTsx' | 'indexHtml' | 'files'
391
- >,
385
+ input: Pick<WebArtifactBuildInput, 'title' | 'appTsx' | 'indexCss' | 'mainTsx' | 'indexHtml' | 'files'>,
392
386
  ): void {
393
387
  const writes = new Map<string, string>();
394
388
  writes.set(join(projectPath, 'src', 'App.tsx'), input.appTsx);
@@ -462,10 +456,7 @@ function summarizeArtifactLog(text: string): WebArtifactLogSummary | undefined {
462
456
  .filter((line) => line.trim().length > 0);
463
457
  if (lines.length === 0) return undefined;
464
458
 
465
- const noisyPatterns = [
466
- /\/dev\/tty/i,
467
- /no such device or address/i,
468
- ];
459
+ const noisyPatterns = [/\/dev\/tty/i, /no such device or address/i];
469
460
  const filteredLines = lines.filter((line) => !noisyPatterns.some((pattern) => pattern.test(line)));
470
461
  const suppressedNoiseCount = lines.length - filteredLines.length;
471
462
  const visibleLines = filteredLines.length > 0 ? filteredLines : lines;
@@ -479,9 +470,7 @@ function summarizeArtifactLog(text: string): WebArtifactLogSummary | undefined {
479
470
  };
480
471
  }
481
472
 
482
- export async function executeWebArtifactBuild(
483
- input: WebArtifactBuildInput,
484
- ): Promise<WebArtifactBuildOutput> {
473
+ export async function executeWebArtifactBuild(input: WebArtifactBuildInput): Promise<WebArtifactBuildOutput> {
485
474
  const workspaceRoot = currentWorkspaceRoot();
486
475
  const artifactsDir = ensureArtifactsDir(workspaceRoot);
487
476
  const slug = slugify(input.title);
@@ -505,9 +494,7 @@ export async function executeWebArtifactBuild(
505
494
  let stdout = '';
506
495
  let stderr = '';
507
496
  const needsInit =
508
- !existsSync(projectPath) ||
509
- !existsSync(join(projectPath, 'package.json')) ||
510
- !existsSync(join(projectPath, 'src'));
497
+ !existsSync(projectPath) || !existsSync(join(projectPath, 'package.json')) || !existsSync(join(projectPath, 'src'));
511
498
 
512
499
  if (needsInit) {
513
500
  const initResult = await runProcess('bash', [initScriptPath, basename(projectPath)], {
@@ -530,10 +517,17 @@ export async function executeWebArtifactBuild(
530
517
  // change install behavior. With deps already validated against
531
518
  // npm-name format and quoted via single-quote escaping, the regular
532
519
  // shell is sufficient and reproducible across machines.
533
- const depResult = await runProcess('bash', ['-c', `if command -v pnpm >/dev/null 2>&1; then pnpm --silent add --ignore-scripts -- ${quotedDeps}; elif command -v bun >/dev/null 2>&1; then bun x pnpm@${pnpmVersion} --silent add --ignore-scripts -- ${quotedDeps}; else npm install --ignore-scripts -- ${quotedDeps}; fi`], {
534
- cwd: projectPath,
535
- timeoutMs,
536
- });
520
+ const depResult = await runProcess(
521
+ 'bash',
522
+ [
523
+ '-c',
524
+ `if command -v pnpm >/dev/null 2>&1; then pnpm --silent add --ignore-scripts -- ${quotedDeps}; elif command -v bun >/dev/null 2>&1; then bun x pnpm@${pnpmVersion} --silent add --ignore-scripts -- ${quotedDeps}; else npm install --ignore-scripts -- ${quotedDeps}; fi`,
525
+ ],
526
+ {
527
+ cwd: projectPath,
528
+ timeoutMs,
529
+ },
530
+ );
537
531
  stdout = [stdout, depResult.stdout].filter(Boolean).join('\n');
538
532
  stderr = [stderr, depResult.stderr].filter(Boolean).join('\n');
539
533
  }
@@ -641,9 +635,11 @@ export function openWebArtifactInCanvas(input: {
641
635
  return { nodeId: id, url };
642
636
  }
643
637
 
644
- export async function buildWebArtifactOnCanvas(input: WebArtifactBuildInput & {
645
- openInCanvas?: boolean;
646
- }): Promise<WebArtifactCanvasBuildResult> {
638
+ export async function buildWebArtifactOnCanvas(
639
+ input: WebArtifactBuildInput & {
640
+ openInCanvas?: boolean;
641
+ },
642
+ ): Promise<WebArtifactCanvasBuildResult> {
647
643
  const startedMs = Date.now();
648
644
  const startedAt = new Date(startedMs).toISOString();
649
645
  const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -72,8 +72,7 @@ function normalizeText(text: string): string {
72
72
  function parseTagAttributes(tag: string): Record<string, string> {
73
73
  const attributes: Record<string, string> = {};
74
74
  const pattern = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g;
75
- let match: RegExpExecArray | null = null;
76
- while ((match = pattern.exec(tag)) !== null) {
75
+ for (const match of tag.matchAll(pattern)) {
77
76
  const [, name, doubleQuoted, singleQuoted, unquoted] = match;
78
77
  attributes[name.toLowerCase()] = doubleQuoted ?? singleQuoted ?? unquoted ?? '';
79
78
  }
@@ -226,11 +225,8 @@ export function summarizeWebpageContent(data: Record<string, unknown>, maxLength
226
225
  const url = typeof data.url === 'string' ? data.url : '';
227
226
  const pageTitle = typeof data.pageTitle === 'string' ? data.pageTitle : '';
228
227
  const description = typeof data.description === 'string' ? data.description : '';
229
- const excerpt = typeof data.excerpt === 'string'
230
- ? data.excerpt
231
- : typeof data.content === 'string'
232
- ? data.content
233
- : '';
228
+ const excerpt =
229
+ typeof data.excerpt === 'string' ? data.excerpt : typeof data.content === 'string' ? data.content : '';
234
230
 
235
231
  if (url) parts.push(`URL: ${url}`);
236
232
  if (pageTitle) parts.push(`Title: ${pageTitle}`);
@@ -267,9 +263,7 @@ export async function fetchWebpageSnapshot(inputUrl: string): Promise<WebpageSna
267
263
  }
268
264
 
269
265
  const pageTitle =
270
- extractMetaContent(body, 'og:title') ??
271
- extractMetaContent(body, 'twitter:title') ??
272
- extractTitle(body);
266
+ extractMetaContent(body, 'og:title') ?? extractMetaContent(body, 'twitter:title') ?? extractTitle(body);
273
267
  const description =
274
268
  extractMetaContent(body, 'description') ??
275
269
  extractMetaContent(body, 'og:description') ??
@@ -300,9 +294,7 @@ export async function fetchWebpageSnapshot(inputUrl: string): Promise<WebpageSna
300
294
  if (error instanceof Error && error.name === 'AbortError') {
301
295
  throw new WebpageFetchError(`Timed out after ${FETCH_TIMEOUT_MS}ms while fetching ${url}.`);
302
296
  }
303
- throw new WebpageFetchError(
304
- error instanceof Error ? error.message : `Failed to fetch ${url}.`,
305
- );
297
+ throw new WebpageFetchError(error instanceof Error ? error.message : `Failed to fetch ${url}.`);
306
298
  } finally {
307
299
  clearTimeout(timeout);
308
300
  }
@@ -56,7 +56,9 @@ const MAX_ROW_WIDTH = 3200;
56
56
  const GROUP_PAD = 40;
57
57
  const GROUP_TITLEBAR_HEIGHT = 32;
58
58
 
59
- function computeGroupBounds(rects: Array<{ position: ArrangePosition; size: ArrangeSize }>): (ArrangePosition & ArrangeSize) | null {
59
+ function computeGroupBounds(
60
+ rects: Array<{ position: ArrangePosition; size: ArrangeSize }>,
61
+ ): (ArrangePosition & ArrangeSize) | null {
60
62
  if (rects.length === 0) return null;
61
63
 
62
64
  let minX = Number.POSITIVE_INFINITY;
@@ -158,7 +160,11 @@ function buildArrangeUnits(allNodes: ArrangeNode[]): {
158
160
  return { units, nodesById, nodeToUnit };
159
161
  }
160
162
 
161
- function buildUnitGraphs(units: ArrangeUnit[], nodeToUnit: Map<string, string>, edges: ArrangeEdge[]): {
163
+ function buildUnitGraphs(
164
+ units: ArrangeUnit[],
165
+ nodeToUnit: Map<string, string>,
166
+ edges: ArrangeEdge[],
167
+ ): {
162
168
  outgoing: Map<string, Set<string>>;
163
169
  undirected: Map<string, Set<string>>;
164
170
  indegree: Map<string, number>;
@@ -277,9 +283,10 @@ function computeGraphComponent(
277
283
  const roots = component
278
284
  .filter((unit) => (indegree.get(unit.id) ?? 0) === 0 && (outdegree.get(unit.id) ?? 0) > 0)
279
285
  .sort((a, b) => a.sortKey.x - b.sortKey.x || a.sortKey.y - b.sortKey.y);
280
- const seedUnits = roots.length > 0
281
- ? roots
282
- : [component.slice().sort((a, b) => a.sortKey.x - b.sortKey.x || a.sortKey.y - b.sortKey.y)[0]];
286
+ const seedUnits =
287
+ roots.length > 0
288
+ ? roots
289
+ : [component.slice().sort((a, b) => a.sortKey.x - b.sortKey.x || a.sortKey.y - b.sortKey.y)[0]];
283
290
 
284
291
  const levels = new Map<string, number>();
285
292
  const queue = seedUnits.map((unit) => unit.id);
@@ -21,12 +21,14 @@ export function sanitizeFrameToken(token: string): string {
21
21
  /** Inline JS (no `<script>` wrapper) that reports content height to the parent. */
22
22
  export function contentHeightReporterSource(frameToken: string): string {
23
23
  const token = JSON.stringify(sanitizeFrameToken(frameToken));
24
- return `(function(){var T=${token};var last=0,timer=null;`
25
- + `function m(){var d=document.documentElement;return Math.max(d?d.scrollHeight:0,document.body?document.body.scrollHeight:0);}`
26
- + `function r(){var h=m();if(Math.abs(h-last)<=4)return;last=h;window.parent.postMessage({source:'pmx-canvas-frame',type:'content-height',token:T,height:h},'*');}`
27
- + `function s(){if(timer)return;timer=setTimeout(function(){timer=null;r();},100);}`
28
- + `if(document.readyState!=='loading')s();window.addEventListener('load',s);`
29
- + `try{new ResizeObserver(s).observe(document.documentElement);}catch(e){}setTimeout(s,60);})();`;
24
+ return (
25
+ `(function(){var T=${token};var last=0,timer=null;` +
26
+ `function m(){var d=document.documentElement;return Math.max(d?d.scrollHeight:0,document.body?document.body.scrollHeight:0);}` +
27
+ `function r(){var h=m();if(Math.abs(h-last)<=4)return;last=h;window.parent.postMessage({source:'pmx-canvas-frame',type:'content-height',token:T,height:h},'*');}` +
28
+ `function s(){if(timer)return;timer=setTimeout(function(){timer=null;r();},100);}` +
29
+ `if(document.readyState!=='loading')s();window.addEventListener('load',s);` +
30
+ `try{new ResizeObserver(s).observe(document.documentElement);}catch(e){}setTimeout(s,60);})();`
31
+ );
30
32
  }
31
33
 
32
34
  /** `<script>`-wrapped reporter for injection into an HTML `<head>` / document. */
@@ -16,9 +16,7 @@ function isCallToolResult(value: unknown): value is CallToolResult {
16
16
  return (
17
17
  isRecord(value) &&
18
18
  Array.isArray(value.content) &&
19
- value.content.every(
20
- (item) => isRecord(item) && typeof item.type === 'string',
21
- )
19
+ value.content.every((item) => isRecord(item) && typeof item.type === 'string')
22
20
  );
23
21
  }
24
22
 
@@ -43,9 +41,7 @@ function serializeExtAppResultValue(value: unknown): string | undefined {
43
41
  }
44
42
  }
45
43
 
46
- export function normalizeExtAppToolResult(
47
- input: NormalizeExtAppToolResultInput,
48
- ): CallToolResult {
44
+ export function normalizeExtAppToolResult(input: NormalizeExtAppToolResultInput): CallToolResult {
49
45
  const isError = input.success === false;
50
46
 
51
47
  if (isCallToolResult(input.result)) {
@@ -69,10 +69,7 @@ export function findOpenCanvasPosition(
69
69
  const rowNodes = existing.filter(
70
70
  (node) => node.position.y <= y + height + gap && node.position.y + node.size.height + gap > y,
71
71
  );
72
- const maxBottom = rowNodes.reduce(
73
- (max, node) => Math.max(max, node.position.y + node.size.height),
74
- y,
75
- );
72
+ const maxBottom = rowNodes.reduce((max, node) => Math.max(max, node.position.y + node.size.height), y);
76
73
  y = maxBottom + gap;
77
74
  }
78
75
 
@@ -1,12 +1,7 @@
1
1
  import type { CanvasEdge, CanvasLayout, CanvasNodeState } from '../client/types.js';
2
2
  import { buildSpatialContext, type SpatialContext } from '../server/spatial-analysis.js';
3
3
 
4
- export type SemanticWatchEventType =
5
- | 'context-pin'
6
- | 'connect'
7
- | 'remove'
8
- | 'group'
9
- | 'move-end';
4
+ export type SemanticWatchEventType = 'context-pin' | 'connect' | 'remove' | 'group' | 'move-end';
10
5
 
11
6
  export const ALL_SEMANTIC_WATCH_EVENT_TYPES: SemanticWatchEventType[] = [
12
7
  'context-pin',
@@ -184,7 +179,10 @@ function buildClusterPeerMap(spatial: SpatialContext): Map<string, string[]> {
184
179
  for (const cluster of spatial.clusters) {
185
180
  const members = [...cluster.nodeIds].sort((a, b) => a.localeCompare(b));
186
181
  for (const nodeId of members) {
187
- map.set(nodeId, members.filter((id) => id !== nodeId));
182
+ map.set(
183
+ nodeId,
184
+ members.filter((id) => id !== nodeId),
185
+ );
188
186
  }
189
187
  }
190
188
  return map;
@@ -208,7 +206,7 @@ function diffSet(prev: Set<string>, next: Set<string>): { added: string[]; remov
208
206
  }
209
207
 
210
208
  function normalizeEventMeta(payload: unknown): EventMeta {
211
- const record = payload && typeof payload === 'object' ? payload as Record<string, unknown> : {};
209
+ const record = payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {};
212
210
  return {
213
211
  timestamp: typeof record.timestamp === 'string' ? record.timestamp : undefined,
214
212
  sessionId: typeof record.sessionId === 'string' ? record.sessionId : undefined,
@@ -311,8 +309,9 @@ export function formatCompactWatchEvent(event: SemanticWatchEvent): string {
311
309
  if (event.updated.length > 0) {
312
310
  parts.push(
313
311
  `updated: ${event.updated
314
- .map((group) =>
315
- `"${group.title ?? group.id}" +${group.addedChildIds.length} -${group.removedChildIds.length} children`,
312
+ .map(
313
+ (group) =>
314
+ `"${group.title ?? group.id}" +${group.addedChildIds.length} -${group.removedChildIds.length} children`,
316
315
  )
317
316
  .join(', ')}`,
318
317
  );
@@ -350,7 +349,7 @@ export class SemanticWatchReducer {
350
349
  }
351
350
 
352
351
  private handleContextPinsChanged(payload: unknown): SemanticWatchEvent[] {
353
- const record = payload && typeof payload === 'object' ? payload as Record<string, unknown> : {};
352
+ const record = payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {};
354
353
  const nodeIds = Array.isArray(record.nodeIds)
355
354
  ? record.nodeIds.filter((id): id is string => typeof id === 'string')
356
355
  : [];
@@ -378,34 +377,36 @@ export class SemanticWatchReducer {
378
377
  return [];
379
378
  }
380
379
 
381
- return [{
382
- type: 'context-pin',
383
- ...meta,
384
- ...previousEventPins,
385
- }];
380
+ return [
381
+ {
382
+ type: 'context-pin',
383
+ ...meta,
384
+ ...previousEventPins,
385
+ },
386
+ ];
386
387
  }
387
388
 
388
389
  private handleLayoutUpdate(payload: unknown): SemanticWatchEvent[] {
389
- const record = payload && typeof payload === 'object' ? payload as Record<string, unknown> : {};
390
- const layout = record.layout && typeof record.layout === 'object'
391
- ? record.layout as CanvasLayout
392
- : null;
390
+ const record = payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {};
391
+ const layout = record.layout && typeof record.layout === 'object' ? (record.layout as CanvasLayout) : null;
393
392
  if (!layout) return [];
394
393
 
395
394
  const meta = normalizeEventMeta(payload);
396
395
  if (!this.currentLayout) {
397
396
  this.currentLayout = layout;
398
- this.previousSpatial = buildSpatialContext(layout.nodes, layout.edges, this.currentPins, layout.annotations ?? []);
397
+ this.previousSpatial = buildSpatialContext(
398
+ layout.nodes,
399
+ layout.edges,
400
+ this.currentPins,
401
+ layout.annotations ?? [],
402
+ );
399
403
  return [];
400
404
  }
401
405
 
402
406
  const prevLayout = this.currentLayout;
403
- const prevSpatial = this.previousSpatial ?? buildSpatialContext(
404
- prevLayout.nodes,
405
- prevLayout.edges,
406
- this.currentPins,
407
- prevLayout.annotations ?? [],
408
- );
407
+ const prevSpatial =
408
+ this.previousSpatial ??
409
+ buildSpatialContext(prevLayout.nodes, prevLayout.edges, this.currentPins, prevLayout.annotations ?? []);
409
410
  const nextSpatial = buildSpatialContext(layout.nodes, layout.edges, this.currentPins, layout.annotations ?? []);
410
411
  const events: SemanticWatchEvent[] = [];
411
412
 
@@ -463,12 +464,16 @@ export class SemanticWatchReducer {
463
464
  nextNodeMap: Map<string, CanvasNodeState>,
464
465
  meta: EventMeta,
465
466
  ): GroupWatchEvent | null {
466
- const prevGroupIds = sortIds(Array.from(prevNodeMap.values())
467
- .filter((node) => node.type === 'group')
468
- .map((node) => node.id));
469
- const nextGroupIds = sortIds(Array.from(nextNodeMap.values())
470
- .filter((node) => node.type === 'group')
471
- .map((node) => node.id));
467
+ const prevGroupIds = sortIds(
468
+ Array.from(prevNodeMap.values())
469
+ .filter((node) => node.type === 'group')
470
+ .map((node) => node.id),
471
+ );
472
+ const nextGroupIds = sortIds(
473
+ Array.from(nextNodeMap.values())
474
+ .filter((node) => node.type === 'group')
475
+ .map((node) => node.id),
476
+ );
472
477
 
473
478
  const created = nextGroupIds
474
479
  .filter((id) => !prevGroupIds.includes(id))
@@ -552,10 +557,7 @@ export class SemanticWatchReducer {
552
557
  }
553
558
  }
554
559
 
555
- const pinIds = new Set<string>([
556
- ...prevNeighborhoodMap.keys(),
557
- ...nextNeighborhoodMap.keys(),
558
- ]);
560
+ const pinIds = new Set<string>([...prevNeighborhoodMap.keys(), ...nextNeighborhoodMap.keys()]);
559
561
  for (const pinId of pinIds) {
560
562
  const oldNeighbors = prevNeighborhoodMap.get(pinId) ?? [];
561
563
  const newNeighbors = nextNeighborhoodMap.get(pinId) ?? [];
@@ -13,8 +13,10 @@
13
13
  export function canOpenNodeAsSurface(type: string, data: Record<string, unknown>): boolean {
14
14
  switch (type) {
15
15
  case 'html':
16
- return (typeof data.html === 'string' && data.html.length > 0)
17
- || (typeof data.content === 'string' && data.content.length > 0);
16
+ return (
17
+ (typeof data.html === 'string' && data.html.length > 0) ||
18
+ (typeof data.content === 'string' && data.content.length > 0)
19
+ );
18
20
  case 'json-render':
19
21
  case 'graph':
20
22
  return true;
@@ -24,8 +26,10 @@ export function canOpenNodeAsSurface(type: string, data: Record<string, unknown>
24
26
  // "Open as site" only produced a broken `-32601` page (report #61). Those apps
25
27
  // open externally through their own app, not PMX. Only a bundled web-artifact
26
28
  // (static) or a real url-backed viewer can open as a standalone site.
27
- return (data.viewerType === 'web-artifact' && typeof data.path === 'string' && data.path.length > 0)
28
- || (typeof data.url === 'string' && data.url.length > 0);
29
+ return (
30
+ (data.viewerType === 'web-artifact' && typeof data.path === 'string' && data.path.length > 0) ||
31
+ (typeof data.url === 'string' && data.url.length > 0)
32
+ );
29
33
  case 'webpage':
30
34
  return typeof data.url === 'string' && data.url.length > 0;
31
35
  default: