pmx-canvas 0.1.17 → 0.1.19
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 +99 -0
- package/Readme.md +12 -5
- package/dist/types/server/canvas-serialization.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/docs/RELEASE.md +153 -0
- package/docs/bun-webview-integration.md +296 -0
- package/docs/cli.md +140 -0
- package/docs/evals/e2e-cli-coverage.md +61 -0
- package/docs/http-api.md +191 -0
- package/docs/mcp.md +135 -0
- package/docs/node-types.md +244 -0
- package/docs/plans/.gitkeep +0 -0
- package/docs/plans/plan-001-semantic-watch-mvp.md +335 -0
- package/docs/plans/plan-002-human-attention-layer-design-spec.md +679 -0
- package/docs/plans/plan-003-human-attention-layer-implementation-plan.md +572 -0
- package/docs/reactive-canvas-proposal.md +578 -0
- package/docs/release-review-0.1.0.md +38 -0
- package/docs/screenshot.png +0 -0
- package/docs/screenshots/demo-workbench-dark.png +0 -0
- package/docs/screenshots/demo-workbench-light.png +0 -0
- package/docs/screenshots/welcome-dark.png +0 -0
- package/docs/screenshots/welcome-light.png +0 -0
- package/docs/sdk.md +92 -0
- package/package.json +2 -1
- package/skills/pmx-canvas/SKILL.md +2 -0
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +145 -0
- package/src/cli/agent.ts +17 -0
- package/src/mcp/canvas-access.ts +2 -0
- package/src/mcp/server.ts +12 -4
- package/src/server/canvas-serialization.ts +48 -0
- package/src/server/canvas-state.ts +18 -5
- package/src/server/server.ts +2 -0
package/docs/sdk.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# JavaScript/TypeScript SDK (Bun runtime)
|
|
2
|
+
|
|
3
|
+
The published SDK entrypoint is Bun-first. Node.js consumers should use the
|
|
4
|
+
[CLI](cli.md), [MCP server](mcp.md), or [HTTP API](http-api.md) instead.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
bun add pmx-canvas
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Quick example
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createCanvas } from 'pmx-canvas';
|
|
14
|
+
|
|
15
|
+
const canvas = createCanvas({ port: 4313 });
|
|
16
|
+
await canvas.start({ open: true });
|
|
17
|
+
|
|
18
|
+
// Add nodes
|
|
19
|
+
const n1 = canvas.addNode({ type: 'markdown', title: 'Plan', content: '# Step 1\nDo the thing.' });
|
|
20
|
+
const n2 = canvas.addNode({ type: 'status', title: 'Build', content: 'passing' });
|
|
21
|
+
const n3 = canvas.addNode({ type: 'file', content: 'src/index.ts' });
|
|
22
|
+
|
|
23
|
+
// Connect them
|
|
24
|
+
canvas.addEdge({ from: n1, to: n2, type: 'flow' });
|
|
25
|
+
|
|
26
|
+
// Group related nodes
|
|
27
|
+
canvas.createGroup({ title: 'Build Pipeline', childIds: [n1, n2] });
|
|
28
|
+
|
|
29
|
+
// Self-contained HTML in a sandboxed iframe
|
|
30
|
+
canvas.addHtmlNode({
|
|
31
|
+
title: 'Cost projection',
|
|
32
|
+
html: '<canvas id="c"></canvas><script src="https://cdn.jsdelivr.net/npm/chart.js"></script><script>/* ... */</script>',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Hand-drawn diagram via the Excalidraw MCP-app preset
|
|
36
|
+
await canvas.addDiagram({
|
|
37
|
+
elements: [
|
|
38
|
+
{ type: 'rectangle', id: 'r1', x: 80, y: 80, width: 160, height: 60,
|
|
39
|
+
roundness: { type: 3 }, backgroundColor: '#a5d8ff', fillStyle: 'solid',
|
|
40
|
+
label: { text: 'Agent' } },
|
|
41
|
+
],
|
|
42
|
+
title: 'Quick sketch',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Batch-build a graph and group around it
|
|
46
|
+
await canvas.runBatch([
|
|
47
|
+
{
|
|
48
|
+
op: 'graph.add',
|
|
49
|
+
assign: 'graph',
|
|
50
|
+
args: {
|
|
51
|
+
title: 'Major wins',
|
|
52
|
+
graphType: 'bar',
|
|
53
|
+
data: [
|
|
54
|
+
{ label: 'Docs', value: 5 },
|
|
55
|
+
{ label: 'Tests', value: 8 },
|
|
56
|
+
],
|
|
57
|
+
xKey: 'label',
|
|
58
|
+
yKey: 'value',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
op: 'group.create',
|
|
63
|
+
args: {
|
|
64
|
+
title: 'Quarterly graphs',
|
|
65
|
+
childIds: ['$graph.id'],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
// Arrange and inspect
|
|
71
|
+
canvas.arrange('grid');
|
|
72
|
+
console.log(canvas.validate());
|
|
73
|
+
console.log(canvas.getLayout());
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## WebView automation
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const webview = await canvas.startAutomationWebView({ backend: 'chrome', width: 1280, height: 800 });
|
|
80
|
+
console.log(webview.active);
|
|
81
|
+
console.log(await canvas.evaluateAutomationWebView('document.title'));
|
|
82
|
+
await canvas.resizeAutomationWebView(1440, 900);
|
|
83
|
+
const screenshot = await canvas.screenshotAutomationWebView({ format: 'png' });
|
|
84
|
+
console.log(screenshot.byteLength);
|
|
85
|
+
await canvas.stopAutomationWebView();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## See also
|
|
89
|
+
|
|
90
|
+
- [Node types](node-types.md) — what each node type is for
|
|
91
|
+
- [HTTP API](http-api.md) — the same operations from any language
|
|
92
|
+
- [MCP reference](mcp.md) — the agent-facing surface
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"src/",
|
|
20
20
|
"skills/",
|
|
21
|
+
"docs/",
|
|
21
22
|
"dist/canvas/",
|
|
22
23
|
"dist/json-render/",
|
|
23
24
|
"dist/types/",
|
|
@@ -622,6 +622,8 @@ canvas_webview_stop();
|
|
|
622
622
|
- Do not use separate `text` elements with `containerId`/`boundElements` to place centered text
|
|
623
623
|
inside shapes. The hosted SVG preview does not auto-position those; PMX normalizes imported
|
|
624
624
|
canonical bound text back into shape labels for hosted app calls.
|
|
625
|
+
- For detailed sizing, camera, and label-fit rules, read `references/excalidraw-diagram-authoring.md`
|
|
626
|
+
before creating dense diagrams.
|
|
625
627
|
- Prefer the pastel fill palette in the Excalidraw `read_me` (light blue/green/orange/...) for
|
|
626
628
|
a consistent look across diagrams
|
|
627
629
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Excalidraw Diagram Authoring
|
|
2
|
+
|
|
3
|
+
Use this guide when creating diagrams through PMX Canvas with `canvas_add_diagram` or
|
|
4
|
+
`pmx-canvas external-app add --kind excalidraw`.
|
|
5
|
+
|
|
6
|
+
## Why Text Can Still Drift
|
|
7
|
+
|
|
8
|
+
PMX normalizes canonical Excalidraw bound text (`containerId` / `boundElements`) into the hosted
|
|
9
|
+
app's supported shape-level `label` format before calling Excalidraw. That fixes the payload
|
|
10
|
+
format mismatch, but it does not fix poor diagram geometry.
|
|
11
|
+
|
|
12
|
+
Text can still appear clipped or misplaced when:
|
|
13
|
+
|
|
14
|
+
- A label is too long for its shape.
|
|
15
|
+
- A diamond or ellipse is too small for the label's usable center area.
|
|
16
|
+
- The `cameraUpdate` viewport is too tight or not 4:3.
|
|
17
|
+
- Title, footer, or notes are placed near the camera edge.
|
|
18
|
+
- The caller bypasses PMX and sends raw elements directly to Excalidraw MCP.
|
|
19
|
+
|
|
20
|
+
## Text Format Rules
|
|
21
|
+
|
|
22
|
+
For text inside a rectangle, ellipse, or diamond, use shape-level `label`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"type": "rectangle",
|
|
27
|
+
"id": "step-a",
|
|
28
|
+
"x": 100,
|
|
29
|
+
"y": 100,
|
|
30
|
+
"width": 260,
|
|
31
|
+
"height": 90,
|
|
32
|
+
"label": { "text": "Step A", "fontSize": 18 }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Do not create separate centered text elements for shape labels:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"type": "text",
|
|
41
|
+
"containerId": "step-a",
|
|
42
|
+
"text": "Step A"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Standalone text is fine for titles, notes, captions, and free-floating annotations. For standalone
|
|
47
|
+
text, `x` is the left edge; `textAlign` does not center it on a point.
|
|
48
|
+
|
|
49
|
+
## Label Length Rules
|
|
50
|
+
|
|
51
|
+
- Use 1-4 words inside shapes.
|
|
52
|
+
- Put detailed explanations in nearby standalone text annotations.
|
|
53
|
+
- Prefer `Bound Text` over `Pattern B: containerId+boundElements only`.
|
|
54
|
+
- If a label needs more than 4 words, either widen the shape or split the idea into a label plus an annotation.
|
|
55
|
+
|
|
56
|
+
## Shape Sizing Rules
|
|
57
|
+
|
|
58
|
+
- Minimum labeled rectangle or ellipse: `180x80`.
|
|
59
|
+
- For 3-5 word labels: `240x90` or larger.
|
|
60
|
+
- For long labels: `320+` width or use an external annotation.
|
|
61
|
+
- Diamonds need more room than rectangles because the usable center area is smaller.
|
|
62
|
+
- Leave at least `30px` gap between shapes and labels/arrows.
|
|
63
|
+
|
|
64
|
+
## Camera Rules
|
|
65
|
+
|
|
66
|
+
Always start with a `cameraUpdate` as the first element.
|
|
67
|
+
|
|
68
|
+
Use 4:3 camera sizes only:
|
|
69
|
+
|
|
70
|
+
- `400x300`
|
|
71
|
+
- `600x450`
|
|
72
|
+
- `800x600`
|
|
73
|
+
- `1200x900`
|
|
74
|
+
- `1600x1200`
|
|
75
|
+
|
|
76
|
+
Camera bounds must include the full diagram plus padding. Leave at least `80px` padding around all
|
|
77
|
+
visible content.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "type": "cameraUpdate", "x": 20, "y": 0, "width": 1200, "height": 900 }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If a title, footer, or rightmost label is clipped, the camera is wrong even if the elements are valid.
|
|
86
|
+
|
|
87
|
+
## Good Pattern
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
[
|
|
91
|
+
{ "type": "cameraUpdate", "x": 20, "y": 0, "width": 1200, "height": 900 },
|
|
92
|
+
{
|
|
93
|
+
"type": "rectangle",
|
|
94
|
+
"id": "a",
|
|
95
|
+
"x": 120,
|
|
96
|
+
"y": 160,
|
|
97
|
+
"width": 260,
|
|
98
|
+
"height": 90,
|
|
99
|
+
"backgroundColor": "#a5d8ff",
|
|
100
|
+
"fillStyle": "solid",
|
|
101
|
+
"label": { "text": "Short Label", "fontSize": 18 }
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"type": "rectangle",
|
|
105
|
+
"id": "b",
|
|
106
|
+
"x": 520,
|
|
107
|
+
"y": 160,
|
|
108
|
+
"width": 280,
|
|
109
|
+
"height": 90,
|
|
110
|
+
"backgroundColor": "#b2f2bb",
|
|
111
|
+
"fillStyle": "solid",
|
|
112
|
+
"label": { "text": "Next Step", "fontSize": 18 }
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"type": "arrow",
|
|
116
|
+
"id": "a-to-b",
|
|
117
|
+
"x": 390,
|
|
118
|
+
"y": 205,
|
|
119
|
+
"width": 110,
|
|
120
|
+
"height": 0,
|
|
121
|
+
"points": [[0, 0], [110, 0]],
|
|
122
|
+
"endArrowhead": "arrow",
|
|
123
|
+
"label": { "text": "then", "fontSize": 14 }
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"type": "text",
|
|
127
|
+
"id": "note",
|
|
128
|
+
"x": 120,
|
|
129
|
+
"y": 290,
|
|
130
|
+
"text": "Longer explanation goes here, outside the shape.",
|
|
131
|
+
"fontSize": 16
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Preflight Checklist
|
|
137
|
+
|
|
138
|
+
- Shape text uses `label`, not separate `text` elements.
|
|
139
|
+
- Shape labels are short enough to fit.
|
|
140
|
+
- Long explanations are outside shapes.
|
|
141
|
+
- The first element is a 4:3 `cameraUpdate`.
|
|
142
|
+
- Camera has at least `80px` padding around all visible content.
|
|
143
|
+
- Titles and footers are not near the camera edge.
|
|
144
|
+
- Arrows have explicit `points` and enough space for labels.
|
|
145
|
+
- Calls go through PMX (`canvas_add_diagram` or `external-app add --kind excalidraw`) unless you manually apply these rules to raw Excalidraw MCP input.
|
package/src/cli/agent.ts
CHANGED
|
@@ -1698,6 +1698,7 @@ cmd('snapshot save', 'Save a named snapshot of the current canvas', [
|
|
|
1698
1698
|
cmd('snapshot list', 'List saved snapshots', [
|
|
1699
1699
|
'pmx-canvas snapshot list',
|
|
1700
1700
|
'pmx-canvas snapshot list --limit 50 --query baseline',
|
|
1701
|
+
'pmx-canvas snapshot list --after 2026-05-01T00:00:00Z --before 2026-05-05T00:00:00Z',
|
|
1701
1702
|
'pmx-canvas snapshot list --all',
|
|
1702
1703
|
], async (args) => {
|
|
1703
1704
|
const { flags } = parseFlags(args);
|
|
@@ -1706,8 +1707,12 @@ cmd('snapshot list', 'List saved snapshots', [
|
|
|
1706
1707
|
const params = new URLSearchParams();
|
|
1707
1708
|
const limit = optionalNumberFlag(flags, 'limit', 'Use a positive integer, e.g. --limit 50');
|
|
1708
1709
|
const query = getStringFlag(flags, 'query', 'q');
|
|
1710
|
+
const before = getStringFlag(flags, 'before');
|
|
1711
|
+
const after = getStringFlag(flags, 'after');
|
|
1709
1712
|
if (limit !== undefined) params.set('limit', String(limit));
|
|
1710
1713
|
if (query) params.set('q', query);
|
|
1714
|
+
if (before) params.set('before', before);
|
|
1715
|
+
if (after) params.set('after', after);
|
|
1711
1716
|
if (flags.all) params.set('all', 'true');
|
|
1712
1717
|
const result = await api('GET', `/api/canvas/snapshots${params.size > 0 ? `?${params.toString()}` : ''}`);
|
|
1713
1718
|
output(result);
|
|
@@ -2328,8 +2333,20 @@ function showCommandHelp(name: string): void {
|
|
|
2328
2333
|
console.log('\nOptions:');
|
|
2329
2334
|
console.log(' --limit <number> Maximum snapshots to return (default 20)');
|
|
2330
2335
|
console.log(' --query <text> Case-insensitive ID/name filter');
|
|
2336
|
+
console.log(' --before <timestamp> Only return snapshots created at or before this ISO timestamp');
|
|
2337
|
+
console.log(' --after <timestamp> Only return snapshots created at or after this ISO timestamp');
|
|
2331
2338
|
console.log(' --all Return all snapshots');
|
|
2332
2339
|
}
|
|
2340
|
+
if (name === 'node update') {
|
|
2341
|
+
console.log('\nTrace fields:');
|
|
2342
|
+
console.log(' --tool-name, --toolName Trace tool or operation label');
|
|
2343
|
+
console.log(' --category <name> Trace category, e.g. mcp, file, subagent, other');
|
|
2344
|
+
console.log(' --status <status> Trace status, e.g. running, success, failed');
|
|
2345
|
+
console.log(' --duration <text> Trace duration badge text');
|
|
2346
|
+
console.log(' --result-summary, --resultSummary <text>');
|
|
2347
|
+
console.log(' Trace result summary');
|
|
2348
|
+
console.log(' --error <text> Trace error message');
|
|
2349
|
+
}
|
|
2333
2350
|
if (name === 'snapshot gc') {
|
|
2334
2351
|
console.log('\nOptions:');
|
|
2335
2352
|
console.log(' --keep <number> Number of newest snapshots to keep (default 20)');
|
package/src/mcp/canvas-access.ts
CHANGED
|
@@ -559,6 +559,8 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
559
559
|
const params = new URLSearchParams();
|
|
560
560
|
if (typeof options?.limit === 'number') params.set('limit', String(options.limit));
|
|
561
561
|
if (options?.query) params.set('q', options.query);
|
|
562
|
+
if (options?.before) params.set('before', options.before);
|
|
563
|
+
if (options?.after) params.set('after', options.after);
|
|
562
564
|
if (options?.all) params.set('all', 'true');
|
|
563
565
|
const query = params.size > 0 ? `?${params.toString()}` : '';
|
|
564
566
|
return await this.requestJson<SnapshotList>('GET', `/api/canvas/snapshots${query}`);
|
package/src/mcp/server.ts
CHANGED
|
@@ -29,7 +29,13 @@ import { createCanvasAccess, refreshCanvasAccess, type CanvasAccess } from './ca
|
|
|
29
29
|
import { serializeNodeForAgentContext } from '../server/agent-context.js';
|
|
30
30
|
import { wrapCanvasAutomationScript } from '../server/server.js';
|
|
31
31
|
import { buildSpatialContext, findNeighborhoods } from '../server/spatial-analysis.js';
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
getCanvasNodeTitle,
|
|
34
|
+
serializeCanvasLayoutForAgent,
|
|
35
|
+
serializeCanvasNode,
|
|
36
|
+
serializeCanvasNodeForAgent,
|
|
37
|
+
summarizeCanvasAnnotationForContext,
|
|
38
|
+
} from '../server/canvas-serialization.js';
|
|
33
39
|
import { listBundledSkills, readBundledSkill } from '../server/bundled-skills.js';
|
|
34
40
|
|
|
35
41
|
let canvas: CanvasAccess | null = null;
|
|
@@ -207,7 +213,7 @@ function compactLayoutPayload(layout: Awaited<ReturnType<CanvasAccess['getLayout
|
|
|
207
213
|
|
|
208
214
|
function agentSafeFullLayoutPayload(layout: Awaited<ReturnType<CanvasAccess['getLayout']>>): Record<string, unknown> {
|
|
209
215
|
return {
|
|
210
|
-
...
|
|
216
|
+
...serializeCanvasLayoutForAgent(layout),
|
|
211
217
|
annotations: (layout.annotations ?? []).map((annotation) => summarizeCanvasAnnotationForContext(annotation, layout.nodes)),
|
|
212
218
|
};
|
|
213
219
|
}
|
|
@@ -240,7 +246,7 @@ async function createdNodePayload(c: CanvasAccess, id: string, options: { full?:
|
|
|
240
246
|
if (!wantsFullPayload(options)) {
|
|
241
247
|
return { ok: true, node: compactNodePayload(node), id };
|
|
242
248
|
}
|
|
243
|
-
const serialized =
|
|
249
|
+
const serialized = serializeCanvasNodeForAgent(node);
|
|
244
250
|
return { ok: true, node: serialized, ...serialized };
|
|
245
251
|
}
|
|
246
252
|
|
|
@@ -324,7 +330,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
324
330
|
isError: true,
|
|
325
331
|
};
|
|
326
332
|
}
|
|
327
|
-
const payload = wantsFullPayload(input) ?
|
|
333
|
+
const payload = wantsFullPayload(input) ? serializeCanvasNodeForAgent(node) : compactNodePayload(node);
|
|
328
334
|
return {
|
|
329
335
|
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
330
336
|
};
|
|
@@ -1753,6 +1759,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1753
1759
|
{
|
|
1754
1760
|
limit: z.number().optional().describe('Maximum snapshots to return (default: 20)'),
|
|
1755
1761
|
query: z.string().optional().describe('Optional case-insensitive ID/name filter'),
|
|
1762
|
+
before: z.string().optional().describe('Only return snapshots created at or before this ISO timestamp'),
|
|
1763
|
+
after: z.string().optional().describe('Only return snapshots created at or after this ISO timestamp'),
|
|
1756
1764
|
all: z.boolean().optional().describe('Return all snapshots instead of the default limit'),
|
|
1757
1765
|
},
|
|
1758
1766
|
async (input) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { canvasState } from './canvas-state.js';
|
|
2
3
|
import type { CanvasAnnotation, CanvasLayout, CanvasNodeState, ViewportState } from './canvas-state.js';
|
|
3
4
|
import {
|
|
@@ -47,6 +48,13 @@ interface BlobSummary {
|
|
|
47
48
|
sha256: string;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
interface ExternalMcpAppHtmlSummary {
|
|
52
|
+
omitted: 'external-mcp-app-html';
|
|
53
|
+
resourceUri: string;
|
|
54
|
+
bytes: number;
|
|
55
|
+
sha256: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
function pickString(value: unknown): string | null {
|
|
51
59
|
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
52
60
|
}
|
|
@@ -90,6 +98,39 @@ export function serializeCanvasNode(node: CanvasNodeState): SerializedCanvasNode
|
|
|
90
98
|
};
|
|
91
99
|
}
|
|
92
100
|
|
|
101
|
+
function summarizeExternalMcpAppHtml(node: SerializedCanvasNode): Record<string, unknown> {
|
|
102
|
+
const html = node.data.html;
|
|
103
|
+
const resourceUri = node.data.resourceUri;
|
|
104
|
+
if (
|
|
105
|
+
node.type !== 'mcp-app' ||
|
|
106
|
+
node.data.mode !== 'ext-app' ||
|
|
107
|
+
typeof html !== 'string' ||
|
|
108
|
+
html.length === 0 ||
|
|
109
|
+
typeof resourceUri !== 'string' ||
|
|
110
|
+
resourceUri.length === 0
|
|
111
|
+
) {
|
|
112
|
+
return node.data;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
...node.data,
|
|
117
|
+
html: {
|
|
118
|
+
omitted: 'external-mcp-app-html',
|
|
119
|
+
resourceUri,
|
|
120
|
+
bytes: Buffer.byteLength(html, 'utf-8'),
|
|
121
|
+
sha256: createHash('sha256').update(html).digest('hex'),
|
|
122
|
+
} satisfies ExternalMcpAppHtmlSummary,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function serializeCanvasNodeForAgent(node: CanvasNodeState): SerializedCanvasNode {
|
|
127
|
+
const serialized = serializeCanvasNode(node);
|
|
128
|
+
return {
|
|
129
|
+
...serialized,
|
|
130
|
+
data: summarizeExternalMcpAppHtml(serialized),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
93
134
|
function summarizeBlobValue(value: unknown): unknown {
|
|
94
135
|
if (!canvasState.isBlobReference(value)) return value;
|
|
95
136
|
return {
|
|
@@ -117,6 +158,13 @@ export function serializeCanvasLayout(layout: CanvasLayout): SerializedCanvasLay
|
|
|
117
158
|
};
|
|
118
159
|
}
|
|
119
160
|
|
|
161
|
+
export function serializeCanvasLayoutForAgent(layout: CanvasLayout): SerializedCanvasLayout {
|
|
162
|
+
return {
|
|
163
|
+
...layout,
|
|
164
|
+
nodes: layout.nodes.map(serializeCanvasNodeForAgent),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
120
168
|
export function serializeCanvasLayoutWithBlobSummaries(layout: CanvasLayout): SerializedCanvasLayout {
|
|
121
169
|
return {
|
|
122
170
|
...layout,
|
|
@@ -32,6 +32,12 @@ function normalizePositiveInteger(value: number | undefined): number | undefined
|
|
|
32
32
|
return Math.floor(value);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function normalizeSnapshotTimestamp(value: string | undefined): string | undefined {
|
|
36
|
+
if (!value) return undefined;
|
|
37
|
+
const parsed = Date.parse(value);
|
|
38
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
export const PMX_CANVAS_DIR = '.pmx-canvas';
|
|
36
42
|
const STATE_FILENAME = 'state.json';
|
|
37
43
|
const SNAPSHOTS_SUBDIR = 'snapshots';
|
|
@@ -89,6 +95,8 @@ export interface CanvasSnapshot {
|
|
|
89
95
|
export interface CanvasSnapshotListOptions {
|
|
90
96
|
limit?: number;
|
|
91
97
|
query?: string;
|
|
98
|
+
before?: string;
|
|
99
|
+
after?: string;
|
|
92
100
|
all?: boolean;
|
|
93
101
|
}
|
|
94
102
|
|
|
@@ -856,11 +864,16 @@ class CanvasStateManager {
|
|
|
856
864
|
}
|
|
857
865
|
}
|
|
858
866
|
const query = options.query?.trim().toLowerCase();
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
)
|
|
863
|
-
|
|
867
|
+
const before = normalizeSnapshotTimestamp(options.before);
|
|
868
|
+
const after = normalizeSnapshotTimestamp(options.after);
|
|
869
|
+
const filtered = snapshots.filter((snapshot) => {
|
|
870
|
+
if (query && !snapshot.id.toLowerCase().includes(query) && !snapshot.name.toLowerCase().includes(query)) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
if (before && snapshot.createdAt > before) return false;
|
|
874
|
+
if (after && snapshot.createdAt < after) return false;
|
|
875
|
+
return true;
|
|
876
|
+
});
|
|
864
877
|
const sorted = filtered.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
865
878
|
const limit = options.all ? undefined : (normalizePositiveInteger(options.limit) ?? 20);
|
|
866
879
|
return limit === undefined ? sorted : sorted.slice(0, limit);
|
package/src/server/server.ts
CHANGED
|
@@ -4145,6 +4145,8 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4145
4145
|
return responseJson(listCanvasSnapshots({
|
|
4146
4146
|
limit: parsePositiveIntegerParam(url.searchParams.get('limit')),
|
|
4147
4147
|
query: url.searchParams.get('q') ?? url.searchParams.get('query') ?? undefined,
|
|
4148
|
+
before: url.searchParams.get('before') ?? undefined,
|
|
4149
|
+
after: url.searchParams.get('after') ?? undefined,
|
|
4148
4150
|
all: url.searchParams.get('all') === 'true',
|
|
4149
4151
|
}));
|
|
4150
4152
|
}
|