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.
- package/CHANGELOG.md +38 -0
- package/dist/canvas/index.js +2 -2
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/docs/http-api.md +28 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
- package/docs/tech-debt-assessment-2026-07.md +2 -2
- package/package.json +5 -2
- package/skills/pmx-canvas/SKILL.md +3 -1
- package/skills/pmx-canvas/references/full-reference.md +10 -3
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +35 -21
- package/src/mcp/server.ts +132 -70
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +182 -39
- package/src/server/ax-state.ts +142 -47
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +454 -73
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +150 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +19 -6
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +3 -12
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +320 -217
- package/src/server/intent-registry.ts +2 -5
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +1 -3
- package/src/server/operations/http.ts +2 -3
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +22 -30
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +98 -73
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +298 -109
- package/src/server/operations/ops/query.ts +46 -28
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +108 -1027
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- 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) =>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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 = (
|
|
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 = (
|
|
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 =
|
|
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 =
|
|
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:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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 =
|
|
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(
|
|
534
|
-
|
|
535
|
-
|
|
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(
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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)) {
|
package/src/shared/placement.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
315
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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(
|
|
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 =
|
|
404
|
-
|
|
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(
|
|
467
|
-
.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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) ?? [];
|
package/src/shared/surface.ts
CHANGED
|
@@ -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 (
|
|
17
|
-
|
|
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 (
|
|
28
|
-
|
|
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:
|