pmx-canvas 0.1.14 → 0.1.16

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 (56) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/Readme.md +108 -1058
  3. package/dist/canvas/global.css +141 -0
  4. package/dist/canvas/index.js +124 -74
  5. package/dist/json-render/index.css +1 -1
  6. package/dist/types/client/nodes/ContextNode.d.ts +11 -2
  7. package/dist/types/client/nodes/HtmlNode.d.ts +5 -0
  8. package/dist/types/client/nodes/StatusNode.d.ts +1 -0
  9. package/dist/types/client/state/canvas-store.d.ts +11 -3
  10. package/dist/types/client/state/intent-bridge.d.ts +5 -1
  11. package/dist/types/client/types.d.ts +2 -2
  12. package/dist/types/json-render/catalog.d.ts +1 -1
  13. package/dist/types/mcp/canvas-access.d.ts +7 -1
  14. package/dist/types/server/agent-context.d.ts +1 -0
  15. package/dist/types/server/canvas-operations.d.ts +4 -2
  16. package/dist/types/server/canvas-provenance.d.ts +1 -1
  17. package/dist/types/server/canvas-serialization.d.ts +3 -0
  18. package/dist/types/server/canvas-state.d.ts +51 -4
  19. package/dist/types/server/demo.d.ts +5 -0
  20. package/dist/types/server/index.d.ts +13 -3
  21. package/dist/types/server/web-artifacts.d.ts +18 -0
  22. package/dist/types/shared/canvas-node-kind.d.ts +5 -0
  23. package/package.json +1 -1
  24. package/skills/pmx-canvas/SKILL.md +43 -0
  25. package/skills/pmx-canvas-testing/SKILL.md +17 -0
  26. package/src/cli/agent.ts +52 -5
  27. package/src/cli/index.ts +2 -23
  28. package/src/client/canvas/AttentionHistory.tsx +14 -1
  29. package/src/client/canvas/CanvasNode.tsx +1 -1
  30. package/src/client/canvas/CanvasViewport.tsx +3 -0
  31. package/src/client/canvas/ContextPinBar.tsx +2 -1
  32. package/src/client/canvas/DockedNode.tsx +112 -13
  33. package/src/client/canvas/ExpandedNodeOverlay.tsx +5 -0
  34. package/src/client/canvas/Minimap.tsx +1 -0
  35. package/src/client/icons.tsx +1 -0
  36. package/src/client/nodes/ContextNode.tsx +128 -6
  37. package/src/client/nodes/HtmlNode.tsx +151 -0
  38. package/src/client/nodes/StatusNode.tsx +16 -1
  39. package/src/client/nodes/StatusSummary.tsx +2 -1
  40. package/src/client/state/canvas-store.ts +37 -7
  41. package/src/client/state/intent-bridge.ts +9 -4
  42. package/src/client/state/sse-bridge.ts +2 -1
  43. package/src/client/theme/global.css +141 -0
  44. package/src/client/types.ts +3 -0
  45. package/src/mcp/canvas-access.ts +34 -7
  46. package/src/mcp/server.ts +178 -25
  47. package/src/server/agent-context.ts +50 -3
  48. package/src/server/canvas-operations.ts +20 -3
  49. package/src/server/canvas-provenance.ts +2 -1
  50. package/src/server/canvas-serialization.ts +38 -13
  51. package/src/server/canvas-state.ts +305 -34
  52. package/src/server/demo.ts +792 -0
  53. package/src/server/index.ts +33 -3
  54. package/src/server/server.ts +98 -14
  55. package/src/server/web-artifacts.ts +116 -3
  56. package/src/shared/canvas-node-kind.ts +14 -0
@@ -277,6 +277,28 @@ export function toggleCollapsed(id: string): void {
277
277
  updateNode(id, { collapsed: !existing.collapsed });
278
278
  }
279
279
 
280
+ // Collapse every docked context node. Used to enforce mutual exclusion between
281
+ // the Context side panel and the Updates side panel (they share the same
282
+ // right-edge anchor and would otherwise visually collide).
283
+ export function collapseDockedContextNodes(): void {
284
+ for (const node of nodes.value.values()) {
285
+ if (node.type === 'context' && node.dockPosition === 'right' && !node.collapsed) {
286
+ updateNode(node.id, { collapsed: true });
287
+ }
288
+ }
289
+ }
290
+
291
+ // True iff at least one docked context node is currently expanded. Used by the
292
+ // Updates pill to hide itself while the Context panel is open.
293
+ export const hasOpenDockedContextPanel = computed(() => {
294
+ for (const node of nodes.value.values()) {
295
+ if (node.type === 'context' && node.dockPosition === 'right' && !node.collapsed) {
296
+ return true;
297
+ }
298
+ }
299
+ return false;
300
+ });
301
+
280
302
  export function dockNode(id: string, position: 'left' | 'right'): void {
281
303
  const existing = nodes.value.get(id);
282
304
  if (!existing) return;
@@ -308,9 +330,16 @@ export function replaceViewport(next: ViewportState): void {
308
330
  }
309
331
 
310
332
  export function commitViewport(next: ViewportState): void {
333
+ commitViewportWithOptions(next);
334
+ }
335
+
336
+ function commitViewportWithOptions(
337
+ next: ViewportState,
338
+ options: { recordHistory?: boolean } = {},
339
+ ): void {
311
340
  viewport.value = next;
312
- persistLayout();
313
- void updateViewportFromClient(next);
341
+ persistLayout(options);
342
+ void updateViewportFromClient(next, options);
314
343
  }
315
344
 
316
345
  export function applyServerCanvasLayout(
@@ -372,6 +401,7 @@ function easeOutCubic(t: number): number {
372
401
  export function animateViewport(
373
402
  target: ViewportState,
374
403
  duration = 300,
404
+ options: { recordHistory?: boolean } = {},
375
405
  ): void {
376
406
  if (animationId !== null) cancelAnimationFrame(animationId);
377
407
 
@@ -393,7 +423,7 @@ export function animateViewport(
393
423
  animationId = requestAnimationFrame(tick);
394
424
  } else {
395
425
  animationId = null;
396
- commitViewport(target);
426
+ commitViewportWithOptions(target, options);
397
427
  }
398
428
  }
399
429
 
@@ -411,7 +441,7 @@ export function cancelViewportAnimation(): void {
411
441
  // ── Persistence ───────────────────────────────────────────────
412
442
  const STORAGE_KEY = 'pmx-canvas-layout';
413
443
 
414
- export function persistLayout(): void {
444
+ export function persistLayout(options: { recordHistory?: boolean } = {}): void {
415
445
  try {
416
446
  const allNodes = Array.from(nodes.value.values());
417
447
  const nodeUpdates = allNodes.map((n) => ({
@@ -444,7 +474,7 @@ export function persistLayout(): void {
444
474
  contextPinnedNodeIds: Array.from(contextPinnedNodeIds.value),
445
475
  };
446
476
  localStorage.setItem(STORAGE_KEY, JSON.stringify(layout));
447
- void pushCanvasUpdate(nodeUpdates);
477
+ void pushCanvasUpdate(nodeUpdates, options);
448
478
  } catch (error) {
449
479
  logCanvasStoreError('persistLayout', error);
450
480
  }
@@ -518,7 +548,7 @@ export function fitAll(containerW: number, containerH: number): void {
518
548
  }
519
549
 
520
550
  // ── Focus node ────────────────────────────────────────────────
521
- export function focusNode(id: string): void {
551
+ export function focusNode(id: string, options: { recordHistory?: boolean } = {}): void {
522
552
  const node = nodes.value.get(id);
523
553
  if (!node) return;
524
554
  const v = viewport.value;
@@ -528,7 +558,7 @@ export function focusNode(id: string): void {
528
558
  x: window.innerWidth / 2 - cx * v.scale,
529
559
  y: window.innerHeight / 2 - cy * v.scale,
530
560
  scale: v.scale,
531
- });
561
+ }, 300, options);
532
562
  bringToFront(id);
533
563
  }
534
564
 
@@ -101,7 +101,7 @@ export async function openWorkbenchFile(path: string): Promise<{ ok: boolean }>
101
101
 
102
102
  /** Fetch canvas state from server. */
103
103
  export async function fetchCanvasState(): Promise<Record<string, unknown>> {
104
- return requestJson('fetchCanvasState', '/api/canvas/state', {});
104
+ return requestJson('fetchCanvasState', '/api/canvas/state?includeBlobs=true', {});
105
105
  }
106
106
 
107
107
  /** Fetch available slash commands for prompt completion. */
@@ -148,11 +148,12 @@ export async function pushCanvasUpdate(
148
148
  collapsed?: boolean;
149
149
  dockPosition?: 'left' | 'right' | null;
150
150
  }>,
151
+ options: { recordHistory?: boolean } = {},
151
152
  ): Promise<void> {
152
153
  await requestBestEffort('pushCanvasUpdate', '/api/canvas/update', {
153
154
  method: 'POST',
154
155
  headers: { 'Content-Type': 'application/json' },
155
- body: JSON.stringify({ updates }),
156
+ body: JSON.stringify({ updates, ...(options.recordHistory === false ? { recordHistory: false } : {}) }),
156
157
  });
157
158
  }
158
159
 
@@ -230,11 +231,15 @@ export async function removeNodeFromClient(id: string): Promise<{ ok: boolean; r
230
231
  /** Commit the current viewport to the authoritative server state. */
231
232
  export async function updateViewportFromClient(
232
233
  viewport: { x: number; y: number; scale: number },
234
+ options: { recordHistory?: boolean } = {},
233
235
  ): Promise<{ ok: boolean }> {
234
236
  return requestJson('updateViewportFromClient', '/api/canvas/viewport', { ok: false }, {
235
237
  method: 'POST',
236
238
  headers: { 'Content-Type': 'application/json' },
237
- body: JSON.stringify(viewport),
239
+ body: JSON.stringify({
240
+ ...viewport,
241
+ ...(options.recordHistory === false ? { recordHistory: false } : {}),
242
+ }),
238
243
  });
239
244
  }
240
245
 
@@ -286,7 +291,7 @@ export interface CanvasSnapshotInfo {
286
291
  }
287
292
 
288
293
  export async function listSnapshots(): Promise<CanvasSnapshotInfo[]> {
289
- return requestJson<CanvasSnapshotInfo[]>('listSnapshots', '/api/canvas/snapshots', []);
294
+ return requestJson<CanvasSnapshotInfo[]>('listSnapshots', '/api/canvas/snapshots?all=true', []);
290
295
  }
291
296
 
292
297
  export async function saveSnapshot(name: string): Promise<{ ok: boolean; snapshot?: CanvasSnapshotInfo }> {
@@ -83,6 +83,7 @@ const DEFAULT_POSITIONS: Record<
83
83
  trace: { x: 40, y: 900, w: 200, h: 56 },
84
84
  file: { x: 380, y: 80, w: 720, h: 600 },
85
85
  image: { x: 380, y: 80, w: 720, h: 520 },
86
+ html: { x: 380, y: 80, w: 720, h: 640 },
86
87
  group: { x: 220, y: 60, w: 840, h: 560 },
87
88
  prompt: { x: 380, y: 1260, w: 520, h: 400 },
88
89
  response: { x: 380, y: 1480, w: 720, h: 400 },
@@ -228,7 +229,7 @@ function ensureExtAppNode(data: Record<string, unknown>): void {
228
229
  });
229
230
  addNode(node);
230
231
  if (!node.dockPosition) {
231
- focusNode(id);
232
+ focusNode(id, { recordHistory: false });
232
233
  }
233
234
  }
234
235
 
@@ -1844,6 +1844,147 @@ body,
1844
1844
  max-width: 200px;
1845
1845
  }
1846
1846
 
1847
+ /* Context dock — collapsed pill mirrors Updates pill, sits above it */
1848
+ .context-dock-tab {
1849
+ position: fixed;
1850
+ top: 92px;
1851
+ right: 0;
1852
+ display: flex;
1853
+ align-items: center;
1854
+ gap: 8px;
1855
+ padding: 8px 12px 8px 14px;
1856
+ background: color-mix(in srgb, var(--c-panel-glass) 96%, transparent);
1857
+ backdrop-filter: blur(16px);
1858
+ border: 1px solid color-mix(in srgb, var(--c-line) 82%, var(--c-accent) 18%);
1859
+ border-right: 0;
1860
+ border-radius: 14px 0 0 14px;
1861
+ box-shadow: 0 12px 36px var(--c-shadow);
1862
+ color: var(--c-text);
1863
+ cursor: pointer;
1864
+ font: inherit;
1865
+ font-size: 11px;
1866
+ font-weight: 600;
1867
+ letter-spacing: 0.08em;
1868
+ text-transform: uppercase;
1869
+ z-index: 60;
1870
+ }
1871
+
1872
+ .context-dock-tab:hover {
1873
+ border-color: color-mix(in srgb, var(--c-accent) 40%, var(--c-line) 60%);
1874
+ color: var(--c-accent);
1875
+ }
1876
+
1877
+ .context-dock-tab svg {
1878
+ display: block;
1879
+ color: var(--c-accent);
1880
+ flex-shrink: 0;
1881
+ }
1882
+
1883
+ .context-dock-tab-label {
1884
+ white-space: nowrap;
1885
+ }
1886
+
1887
+ .context-dock-tab-badge {
1888
+ min-width: 18px;
1889
+ height: 18px;
1890
+ padding: 0 5px;
1891
+ display: inline-flex;
1892
+ align-items: center;
1893
+ justify-content: center;
1894
+ border-radius: 9px;
1895
+ background: var(--c-accent);
1896
+ color: var(--c-contrast-fg);
1897
+ font-size: 10px;
1898
+ font-weight: 700;
1899
+ letter-spacing: 0;
1900
+ text-transform: none;
1901
+ }
1902
+
1903
+ /* Context dock — expanded panel anchored top-right edge.
1904
+ Mutually exclusive with the Updates panel (see DockedNode.tsx and
1905
+ AttentionHistory.tsx) — opening one collapses the other, so they can both
1906
+ share the same right: 18px anchor without overlapping. */
1907
+ .context-dock-panel {
1908
+ position: fixed;
1909
+ top: 92px;
1910
+ right: 18px;
1911
+ width: min(360px, calc(100vw - 24px));
1912
+ max-height: calc(100vh - 110px);
1913
+ display: flex;
1914
+ flex-direction: column;
1915
+ gap: 10px;
1916
+ padding: 14px;
1917
+ background: color-mix(in srgb, var(--c-panel-glass) 96%, transparent);
1918
+ backdrop-filter: blur(16px);
1919
+ border: 1px solid color-mix(in srgb, var(--c-line) 82%, var(--c-accent) 18%);
1920
+ border-radius: 18px;
1921
+ box-shadow: 0 14px 36px var(--c-shadow), 0 0 0 1px color-mix(in srgb, var(--c-accent) 8%, transparent);
1922
+ z-index: 10001;
1923
+ overflow: hidden;
1924
+ }
1925
+
1926
+ .context-dock-header {
1927
+ display: flex;
1928
+ align-items: flex-start;
1929
+ justify-content: space-between;
1930
+ gap: 8px;
1931
+ padding: 2px 2px 4px;
1932
+ flex-shrink: 0;
1933
+ }
1934
+
1935
+ .context-dock-header-text {
1936
+ display: flex;
1937
+ flex-direction: column;
1938
+ gap: 2px;
1939
+ min-width: 0;
1940
+ }
1941
+
1942
+ .context-dock-title {
1943
+ font-size: 12px;
1944
+ font-weight: 700;
1945
+ letter-spacing: 0.08em;
1946
+ text-transform: uppercase;
1947
+ color: var(--c-text);
1948
+ }
1949
+
1950
+ .context-dock-subtitle {
1951
+ font-size: 11px;
1952
+ color: var(--c-dim);
1953
+ }
1954
+
1955
+ .context-dock-controls {
1956
+ display: flex;
1957
+ gap: 4px;
1958
+ flex-shrink: 0;
1959
+ }
1960
+
1961
+ .context-dock-icon-button {
1962
+ width: 22px;
1963
+ height: 22px;
1964
+ border: 0;
1965
+ border-radius: 6px;
1966
+ background: transparent;
1967
+ color: var(--c-dim);
1968
+ font-size: 16px;
1969
+ line-height: 1;
1970
+ cursor: pointer;
1971
+ display: grid;
1972
+ place-items: center;
1973
+ padding: 0;
1974
+ }
1975
+
1976
+ .context-dock-icon-button:hover {
1977
+ color: var(--c-text);
1978
+ background: var(--c-surface-hover);
1979
+ }
1980
+
1981
+ .context-dock-body {
1982
+ flex: 1 1 auto;
1983
+ min-height: 0;
1984
+ overflow-y: auto;
1985
+ padding-right: 2px;
1986
+ }
1987
+
1847
1988
  .attention-history {
1848
1989
  position: fixed;
1849
1990
  top: 146px;
@@ -20,6 +20,7 @@ export interface CanvasNodeState {
20
20
  | 'trace'
21
21
  | 'file'
22
22
  | 'image'
23
+ | 'html'
23
24
  | 'group';
24
25
  position: { x: number; y: number };
25
26
  size: { width: number; height: number };
@@ -58,6 +59,7 @@ export const TYPE_LABELS: Record<CanvasNodeState['type'], string> = {
58
59
  trace: 'TRACE',
59
60
  file: 'FILE',
60
61
  image: 'IMG',
62
+ html: 'HTML',
61
63
  group: 'GROUP',
62
64
  };
63
65
 
@@ -72,6 +74,7 @@ export const EXPANDABLE_TYPES = new Set<CanvasNodeState['type']>([
72
74
  'ledger',
73
75
  'file',
74
76
  'image',
77
+ 'html',
75
78
  ]);
76
79
 
77
80
  export const EXCALIDRAW_SERVER_NAME = 'Excalidraw';
@@ -18,6 +18,7 @@ type OpenMcpAppResult = Awaited<ReturnType<PmxCanvas['openMcpApp']>>;
18
18
  type AddDiagramInput = Parameters<PmxCanvas['addDiagram']>[0];
19
19
  type AddJsonRenderNodeInput = Parameters<PmxCanvas['addJsonRenderNode']>[0];
20
20
  type AddJsonRenderNodeResult = ReturnType<PmxCanvas['addJsonRenderNode']>;
21
+ type AddHtmlNodeInput = Parameters<PmxCanvas['addHtmlNode']>[0];
21
22
  type AddGraphNodeInput = Parameters<PmxCanvas['addGraphNode']>[0];
22
23
  type AddGraphNodeResult = ReturnType<PmxCanvas['addGraphNode']>;
23
24
  type UpdateNodePatch = Parameters<PmxCanvas['updateNode']>[1];
@@ -34,8 +35,11 @@ type HistoryResult = ReturnType<PmxCanvas['getHistory']>;
34
35
  type SetContextPinsResult = ReturnType<PmxCanvas['setContextPins']>;
35
36
  type RunBatchInput = Parameters<PmxCanvas['runBatch']>[0];
36
37
  type RunBatchResult = Awaited<ReturnType<PmxCanvas['runBatch']>>;
38
+ type SnapshotListOptions = Parameters<PmxCanvas['listSnapshots']>[0];
37
39
  type SnapshotList = ReturnType<PmxCanvas['listSnapshots']>;
38
40
  type DeleteSnapshotResult = ReturnType<PmxCanvas['deleteSnapshot']>;
41
+ type GcSnapshotsOptions = Parameters<PmxCanvas['gcSnapshots']>[0];
42
+ type GcSnapshotsResult = ReturnType<PmxCanvas['gcSnapshots']>;
39
43
  type DiffSnapshotResult = ReturnType<PmxCanvas['diffSnapshot']>;
40
44
  type CodeGraphResult = ReturnType<PmxCanvas['getCodeGraph']>;
41
45
  type ValidationResult = ReturnType<PmxCanvas['validate']>;
@@ -97,6 +101,7 @@ export interface CanvasAccess {
97
101
  openMcpApp(input: OpenMcpAppInput): Promise<OpenMcpAppResult>;
98
102
  addDiagram(input: AddDiagramInput): Promise<OpenMcpAppResult>;
99
103
  addJsonRenderNode(input: AddJsonRenderNodeInput): Promise<AddJsonRenderNodeResult>;
104
+ addHtmlNode(input: AddHtmlNodeInput): Promise<string>;
100
105
  addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult>;
101
106
  buildWebArtifact(input: WebArtifactInput): Promise<WebArtifactResult>;
102
107
  updateNode(id: string, patch: UpdateNodePatch): Promise<void>;
@@ -117,10 +122,11 @@ export interface CanvasAccess {
117
122
  setContextPins(nodeIds: string[], mode?: 'set' | 'add' | 'remove'): Promise<SetContextPinsResult>;
118
123
  getPinnedNodeIds(): Promise<string[]>;
119
124
  runBatch(operations: RunBatchInput): Promise<RunBatchResult>;
120
- listSnapshots(): Promise<SnapshotList>;
125
+ listSnapshots(options?: SnapshotListOptions): Promise<SnapshotList>;
121
126
  saveSnapshot(name: string): Promise<CanvasSnapshot | null>;
122
127
  restoreSnapshot(id: string): Promise<{ ok: boolean }>;
123
128
  deleteSnapshot(id: string): Promise<DeleteSnapshotResult>;
129
+ gcSnapshots(options?: GcSnapshotsOptions): Promise<GcSnapshotsResult>;
124
130
  diffSnapshot(idOrName: string): Promise<DiffSnapshotResult>;
125
131
  getCodeGraph(): Promise<CodeGraphResult>;
126
132
  validate(): Promise<ValidationResult>;
@@ -177,6 +183,10 @@ class LocalCanvasAccess implements CanvasAccess {
177
183
  return this.canvas.addJsonRenderNode(input);
178
184
  }
179
185
 
186
+ async addHtmlNode(input: AddHtmlNodeInput): Promise<string> {
187
+ return this.canvas.addHtmlNode(input);
188
+ }
189
+
180
190
  async addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult> {
181
191
  return this.canvas.addGraphNode(input);
182
192
  }
@@ -257,8 +267,8 @@ class LocalCanvasAccess implements CanvasAccess {
257
267
  return await this.canvas.runBatch(operations);
258
268
  }
259
269
 
260
- async listSnapshots(): Promise<SnapshotList> {
261
- return this.canvas.listSnapshots();
270
+ async listSnapshots(options?: SnapshotListOptions): Promise<SnapshotList> {
271
+ return this.canvas.listSnapshots(options);
262
272
  }
263
273
 
264
274
  async saveSnapshot(name: string): Promise<CanvasSnapshot | null> {
@@ -273,6 +283,10 @@ class LocalCanvasAccess implements CanvasAccess {
273
283
  return this.canvas.deleteSnapshot(id);
274
284
  }
275
285
 
286
+ async gcSnapshots(options?: GcSnapshotsOptions): Promise<GcSnapshotsResult> {
287
+ return this.canvas.gcSnapshots(options);
288
+ }
289
+
276
290
  async diffSnapshot(idOrName: string): Promise<DiffSnapshotResult> {
277
291
  return this.canvas.diffSnapshot(idOrName);
278
292
  }
@@ -359,11 +373,11 @@ class RemoteCanvasAccess implements CanvasAccess {
359
373
  }
360
374
 
361
375
  async getLayout(): Promise<CanvasLayout> {
362
- return await this.requestJson<CanvasLayout>('GET', '/api/canvas/state');
376
+ return await this.requestJson<CanvasLayout>('GET', '/api/canvas/state?includeBlobs=true');
363
377
  }
364
378
 
365
379
  async getNode(id: string): Promise<CanvasNodeState | undefined> {
366
- const response = await fetch(`${this.remoteBaseUrl}/api/canvas/node/${encodeURIComponent(id)}`);
380
+ const response = await fetch(`${this.remoteBaseUrl}/api/canvas/node/${encodeURIComponent(id)}?includeBlobs=true`);
367
381
  if (response.status === 404) return undefined;
368
382
  const text = await response.text();
369
383
  let parsed: unknown = undefined;
@@ -415,6 +429,10 @@ class RemoteCanvasAccess implements CanvasAccess {
415
429
  return { id, url: response.url, spec: response.spec };
416
430
  }
417
431
 
432
+ async addHtmlNode(input: AddHtmlNodeInput): Promise<string> {
433
+ return await this.requestNodeId('POST', '/api/canvas/node', { type: 'html', ...input });
434
+ }
435
+
418
436
  async addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult> {
419
437
  const response = await this.requestJson<GraphNodeResponse>('POST', '/api/canvas/graph', {
420
438
  ...input,
@@ -527,8 +545,13 @@ class RemoteCanvasAccess implements CanvasAccess {
527
545
  return await this.requestJson<RunBatchResult>('POST', '/api/canvas/batch', { operations });
528
546
  }
529
547
 
530
- async listSnapshots(): Promise<SnapshotList> {
531
- return await this.requestJson<SnapshotList>('GET', '/api/canvas/snapshots');
548
+ async listSnapshots(options?: SnapshotListOptions): Promise<SnapshotList> {
549
+ const params = new URLSearchParams();
550
+ if (typeof options?.limit === 'number') params.set('limit', String(options.limit));
551
+ if (options?.query) params.set('q', options.query);
552
+ if (options?.all) params.set('all', 'true');
553
+ const query = params.size > 0 ? `?${params.toString()}` : '';
554
+ return await this.requestJson<SnapshotList>('GET', `/api/canvas/snapshots${query}`);
532
555
  }
533
556
 
534
557
  async saveSnapshot(name: string): Promise<CanvasSnapshot | null> {
@@ -544,6 +567,10 @@ class RemoteCanvasAccess implements CanvasAccess {
544
567
  return await this.requestJson<DeleteSnapshotResult>('DELETE', `/api/canvas/snapshots/${encodeURIComponent(id)}`);
545
568
  }
546
569
 
570
+ async gcSnapshots(options?: GcSnapshotsOptions): Promise<GcSnapshotsResult> {
571
+ return await this.requestJson<GcSnapshotsResult>('POST', '/api/canvas/snapshots/gc', options ?? {});
572
+ }
573
+
547
574
  async diffSnapshot(idOrName: string): Promise<DiffSnapshotResult> {
548
575
  return await this.requestJson<DiffSnapshotResult>('GET', `/api/canvas/snapshots/${encodeURIComponent(idOrName)}/diff`);
549
576
  }