pmx-canvas 0.1.13 → 0.1.15
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 +163 -0
- package/Readme.md +108 -1058
- package/dist/canvas/global.css +141 -0
- package/dist/canvas/index.js +137 -87
- package/dist/json-render/index.css +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -3
- package/dist/types/client/nodes/HtmlNode.d.ts +5 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +2 -1
- package/dist/types/client/state/canvas-store.d.ts +5 -1
- package/dist/types/client/state/intent-bridge.d.ts +3 -1
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +7 -1
- package/dist/types/server/agent-context.d.ts +1 -0
- package/dist/types/server/canvas-operations.d.ts +12 -2
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-serialization.d.ts +3 -0
- package/dist/types/server/canvas-state.d.ts +51 -4
- package/dist/types/server/demo.d.ts +5 -0
- package/dist/types/server/diagram-presets.d.ts +4 -0
- package/dist/types/server/index.d.ts +21 -3
- package/dist/types/server/mcp-app-runtime.d.ts +1 -0
- package/dist/types/server/web-artifacts.d.ts +18 -0
- package/dist/types/shared/canvas-node-kind.d.ts +5 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +43 -0
- package/skills/pmx-canvas-testing/SKILL.md +17 -0
- package/src/cli/agent.ts +66 -5
- package/src/cli/index.ts +2 -23
- package/src/client/canvas/AttentionHistory.tsx +14 -1
- package/src/client/canvas/CanvasNode.tsx +1 -1
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/DockedNode.tsx +110 -12
- package/src/client/canvas/ExpandedNodeOverlay.tsx +8 -3
- package/src/client/canvas/Minimap.tsx +1 -0
- package/src/client/icons.tsx +1 -0
- package/src/client/nodes/ExtAppFrame.tsx +10 -35
- package/src/client/nodes/HtmlNode.tsx +151 -0
- package/src/client/nodes/McpAppNode.tsx +2 -2
- package/src/client/state/canvas-store.ts +24 -2
- package/src/client/state/intent-bridge.ts +4 -3
- package/src/client/state/sse-bridge.ts +2 -0
- package/src/client/theme/global.css +141 -0
- package/src/client/types.ts +3 -0
- package/src/mcp/canvas-access.ts +34 -7
- package/src/mcp/server.ts +199 -26
- package/src/server/agent-context.ts +50 -3
- package/src/server/canvas-operations.ts +55 -3
- package/src/server/canvas-provenance.ts +2 -1
- package/src/server/canvas-serialization.ts +38 -13
- package/src/server/canvas-state.ts +305 -34
- package/src/server/demo.ts +792 -0
- package/src/server/diagram-presets.ts +45 -25
- package/src/server/index.ts +64 -7
- package/src/server/mcp-app-runtime.ts +15 -5
- package/src/server/server.ts +169 -63
- package/src/server/web-artifacts.ts +116 -3
- package/src/shared/canvas-node-kind.ts +14 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useMemo } from 'preact/hooks';
|
|
2
|
+
import { getCanvasTokens } from '../theme/tokens';
|
|
3
|
+
import type { CanvasNodeState } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Strip characters that could break out of a CSS custom-property value context
|
|
7
|
+
* before interpolating into a `<style>` block. The expected token shape is a
|
|
8
|
+
* CSS color (`#abc`, `rgb(...)`) or font-family list, neither of which needs
|
|
9
|
+
* `<`, `>`, `{`, `}`, `;`, or backticks. Defense-in-depth against a future
|
|
10
|
+
* scenario where theme tokens become runtime-editable.
|
|
11
|
+
*/
|
|
12
|
+
function sanitizeCssTokenValue(value: string): string {
|
|
13
|
+
return value.replace(/[<>{};`\\]/g, '').trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build a `<style>` block that exposes canvas theme tokens to the iframe under
|
|
18
|
+
* both the canonical `--c-*` names and common `--color-*` aliases. Also sets
|
|
19
|
+
* sensible body defaults (font, bg, color) so authored HTML inherits the look.
|
|
20
|
+
*/
|
|
21
|
+
function buildThemeStyleBlock(): string {
|
|
22
|
+
const raw = getCanvasTokens();
|
|
23
|
+
const t = {
|
|
24
|
+
bg: sanitizeCssTokenValue(raw.bg),
|
|
25
|
+
panel: sanitizeCssTokenValue(raw.panel),
|
|
26
|
+
panelSoft: sanitizeCssTokenValue(raw.panelSoft),
|
|
27
|
+
line: sanitizeCssTokenValue(raw.line),
|
|
28
|
+
text: sanitizeCssTokenValue(raw.text),
|
|
29
|
+
textSoft: sanitizeCssTokenValue(raw.textSoft),
|
|
30
|
+
muted: sanitizeCssTokenValue(raw.muted),
|
|
31
|
+
dim: sanitizeCssTokenValue(raw.dim),
|
|
32
|
+
accent: sanitizeCssTokenValue(raw.accent),
|
|
33
|
+
ok: sanitizeCssTokenValue(raw.ok),
|
|
34
|
+
warn: sanitizeCssTokenValue(raw.warn),
|
|
35
|
+
warnAlt: sanitizeCssTokenValue(raw.warnAlt),
|
|
36
|
+
danger: sanitizeCssTokenValue(raw.danger),
|
|
37
|
+
purple: sanitizeCssTokenValue(raw.purple),
|
|
38
|
+
font: sanitizeCssTokenValue(raw.font),
|
|
39
|
+
mono: sanitizeCssTokenValue(raw.mono),
|
|
40
|
+
};
|
|
41
|
+
return `
|
|
42
|
+
:root {
|
|
43
|
+
--c-bg: ${t.bg};
|
|
44
|
+
--c-panel: ${t.panel};
|
|
45
|
+
--c-panel-soft: ${t.panelSoft};
|
|
46
|
+
--c-line: ${t.line};
|
|
47
|
+
--c-text: ${t.text};
|
|
48
|
+
--c-text-soft: ${t.textSoft};
|
|
49
|
+
--c-muted: ${t.muted};
|
|
50
|
+
--c-dim: ${t.dim};
|
|
51
|
+
--c-accent: ${t.accent};
|
|
52
|
+
--c-ok: ${t.ok};
|
|
53
|
+
--c-warn: ${t.warn};
|
|
54
|
+
--c-warn-alt: ${t.warnAlt};
|
|
55
|
+
--c-danger: ${t.danger};
|
|
56
|
+
--c-purple: ${t.purple};
|
|
57
|
+
|
|
58
|
+
/* Common aliases authored HTML might use. */
|
|
59
|
+
--color-bg: ${t.bg};
|
|
60
|
+
--color-panel: ${t.panel};
|
|
61
|
+
--color-surface: ${t.panelSoft};
|
|
62
|
+
--color-border: ${t.line};
|
|
63
|
+
--color-text: ${t.text};
|
|
64
|
+
--color-text-primary: ${t.text};
|
|
65
|
+
--color-text-secondary: ${t.textSoft};
|
|
66
|
+
--color-text-muted: ${t.muted};
|
|
67
|
+
--color-text-dim: ${t.dim};
|
|
68
|
+
--color-accent: ${t.accent};
|
|
69
|
+
--color-success: ${t.ok};
|
|
70
|
+
--color-warning: ${t.warn};
|
|
71
|
+
--color-danger: ${t.danger};
|
|
72
|
+
|
|
73
|
+
--font: ${t.font};
|
|
74
|
+
--font-sans: ${t.font};
|
|
75
|
+
--font-mono: ${t.mono};
|
|
76
|
+
|
|
77
|
+
color-scheme: dark light;
|
|
78
|
+
}
|
|
79
|
+
html, body {
|
|
80
|
+
margin: 0;
|
|
81
|
+
padding: 0;
|
|
82
|
+
background: ${t.bg};
|
|
83
|
+
color: ${t.text};
|
|
84
|
+
font-family: ${t.font || 'system-ui, sans-serif'};
|
|
85
|
+
font-size: 14px;
|
|
86
|
+
line-height: 1.5;
|
|
87
|
+
}
|
|
88
|
+
body { padding: 16px; box-sizing: border-box; }
|
|
89
|
+
a { color: ${t.accent}; }
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Inject the theme style block into the user-supplied HTML. If the document has
|
|
95
|
+
* a `<head>`, inject at the top of head; otherwise wrap the content in a full
|
|
96
|
+
* document. Returns a complete HTML string suitable for `srcdoc`.
|
|
97
|
+
*/
|
|
98
|
+
function buildSrcDoc(userHtml: string): string {
|
|
99
|
+
const styleBlock = `<style data-pmx-canvas-theme>${buildThemeStyleBlock()}</style>`;
|
|
100
|
+
const trimmed = userHtml.trim();
|
|
101
|
+
const isFullDoc = /<html[\s>]/i.test(trimmed);
|
|
102
|
+
if (isFullDoc) {
|
|
103
|
+
if (/<head[\s>]/i.test(trimmed)) {
|
|
104
|
+
return trimmed.replace(/<head([^>]*)>/i, `<head$1>${styleBlock}`);
|
|
105
|
+
}
|
|
106
|
+
// Has <html> but no <head> — inject one.
|
|
107
|
+
return trimmed.replace(/<html([^>]*)>/i, `<html$1><head>${styleBlock}</head>`);
|
|
108
|
+
}
|
|
109
|
+
// Fragment — wrap in full document.
|
|
110
|
+
return `<!doctype html><html><head><meta charset="utf-8">${styleBlock}</head><body>${userHtml}</body></html>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function HtmlNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
114
|
+
const html = typeof node.data.html === 'string'
|
|
115
|
+
? node.data.html
|
|
116
|
+
: typeof node.data.content === 'string'
|
|
117
|
+
? node.data.content
|
|
118
|
+
: '';
|
|
119
|
+
const srcDoc = useMemo(() => (html ? buildSrcDoc(html) : ''), [html]);
|
|
120
|
+
|
|
121
|
+
if (!html) {
|
|
122
|
+
return (
|
|
123
|
+
<div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>
|
|
124
|
+
No HTML content set
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// SECURITY: sandbox is intentionally `allow-scripts` ONLY. Do NOT add
|
|
130
|
+
// `allow-same-origin` (would grant the iframe access to parent localStorage
|
|
131
|
+
// and credentialed requests to the canvas origin), `allow-top-navigation`
|
|
132
|
+
// (would let scripts redirect the parent window), or `allow-forms` (would
|
|
133
|
+
// let the iframe POST back to the host). The whole html-node tier assumes
|
|
134
|
+
// arbitrary author code runs inside this exact sandbox.
|
|
135
|
+
return (
|
|
136
|
+
<iframe
|
|
137
|
+
title={typeof node.data.title === 'string' ? node.data.title : 'HTML node'}
|
|
138
|
+
sandbox="allow-scripts"
|
|
139
|
+
srcdoc={srcDoc}
|
|
140
|
+
style={{
|
|
141
|
+
width: '100%',
|
|
142
|
+
height: '100%',
|
|
143
|
+
minHeight: expanded ? '70vh' : '300px',
|
|
144
|
+
border: 'none',
|
|
145
|
+
background: 'var(--c-bg)',
|
|
146
|
+
borderRadius: '6px',
|
|
147
|
+
display: 'block',
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -13,9 +13,9 @@ function withTheme(url: string): string {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function McpAppNode({ node }: { node: CanvasNodeState }) {
|
|
16
|
+
export function McpAppNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
17
17
|
if (node.data.mode === 'ext-app') {
|
|
18
|
-
return <ExtAppFrame node={node} />;
|
|
18
|
+
return <ExtAppFrame node={node} expanded={expanded} />;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const url = withTheme((node.data.url as string) || '');
|
|
@@ -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;
|
|
@@ -411,7 +433,7 @@ export function cancelViewportAnimation(): void {
|
|
|
411
433
|
// ── Persistence ───────────────────────────────────────────────
|
|
412
434
|
const STORAGE_KEY = 'pmx-canvas-layout';
|
|
413
435
|
|
|
414
|
-
export function persistLayout(): void {
|
|
436
|
+
export function persistLayout(options: { recordHistory?: boolean } = {}): void {
|
|
415
437
|
try {
|
|
416
438
|
const allNodes = Array.from(nodes.value.values());
|
|
417
439
|
const nodeUpdates = allNodes.map((n) => ({
|
|
@@ -444,7 +466,7 @@ export function persistLayout(): void {
|
|
|
444
466
|
contextPinnedNodeIds: Array.from(contextPinnedNodeIds.value),
|
|
445
467
|
};
|
|
446
468
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(layout));
|
|
447
|
-
void pushCanvasUpdate(nodeUpdates);
|
|
469
|
+
void pushCanvasUpdate(nodeUpdates, options);
|
|
448
470
|
} catch (error) {
|
|
449
471
|
logCanvasStoreError('persistLayout', error);
|
|
450
472
|
}
|
|
@@ -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
|
|
|
@@ -286,7 +287,7 @@ export interface CanvasSnapshotInfo {
|
|
|
286
287
|
}
|
|
287
288
|
|
|
288
289
|
export async function listSnapshots(): Promise<CanvasSnapshotInfo[]> {
|
|
289
|
-
return requestJson<CanvasSnapshotInfo[]>('listSnapshots', '/api/canvas/snapshots', []);
|
|
290
|
+
return requestJson<CanvasSnapshotInfo[]>('listSnapshots', '/api/canvas/snapshots?all=true', []);
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
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 },
|
|
@@ -529,6 +530,7 @@ function handleExtAppOpen(data: Record<string, unknown>): void {
|
|
|
529
530
|
if (typeof data.toolCallId !== 'string' || !data.toolCallId) return;
|
|
530
531
|
ensureExtAppNode({
|
|
531
532
|
toolCallId: data.toolCallId,
|
|
533
|
+
...(typeof data.nodeId === 'string' && data.nodeId.length > 0 ? { nodeId: data.nodeId } : {}),
|
|
532
534
|
title: data.title,
|
|
533
535
|
html: data.html,
|
|
534
536
|
toolInput: data.toolInput,
|
|
@@ -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;
|
package/src/client/types.ts
CHANGED
|
@@ -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';
|
package/src/mcp/canvas-access.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|