pmx-canvas 0.1.21 → 0.1.22
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 +65 -0
- package/dist/canvas/global.css +23 -40
- package/dist/canvas/index.js +44 -44
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/docs/screenshot.png +0 -0
- package/package.json +1 -1
- package/src/cli/agent.ts +25 -1
- package/src/cli/index.ts +3 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +25 -15
- package/src/client/nodes/HtmlNode.tsx +1 -1
- package/src/client/theme/global.css +23 -40
- package/src/server/canvas-operations.ts +8 -3
- package/src/server/canvas-schema.ts +4 -4
- package/src/server/canvas-serialization.ts +26 -0
- package/src/server/html-primitives.ts +14 -4
- package/src/server/server.ts +10 -5
|
@@ -35,6 +35,7 @@ export declare function getCanvasNodeTitle(node: CanvasNodeState): string | null
|
|
|
35
35
|
export declare function getCanvasNodeContent(node: CanvasNodeState): string | null;
|
|
36
36
|
export declare function serializeCanvasNode(node: CanvasNodeState): SerializedCanvasNode;
|
|
37
37
|
export declare function serializeCanvasNodeForAgent(node: CanvasNodeState): SerializedCanvasNode;
|
|
38
|
+
export declare function serializeCanvasNodeCompact(node: CanvasNodeState): SerializedCanvasNode;
|
|
38
39
|
export declare function serializeCanvasNodeWithBlobSummaries(node: CanvasNodeState): SerializedCanvasNode;
|
|
39
40
|
export declare function serializeCanvasLayout(layout: CanvasLayout): SerializedCanvasLayout;
|
|
40
41
|
export declare function serializeCanvasLayoutForAgent(layout: CanvasLayout): SerializedCanvasLayout;
|
package/docs/screenshot.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
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",
|
package/src/cli/agent.ts
CHANGED
|
@@ -164,7 +164,7 @@ function parseFlags(args: string[]): { positional: string[]; flags: Record<strin
|
|
|
164
164
|
// Boolean-only flags (never take a value argument)
|
|
165
165
|
const BOOL_FLAGS = new Set([
|
|
166
166
|
'help', 'h', 'ids', 'stdin', 'yes', 'list', 'clear', 'set', 'animated', 'dry-run', 'all',
|
|
167
|
-
'no-open-in-canvas', 'lock-arrange', 'unlock-arrange', 'json', 'compact',
|
|
167
|
+
'no-open-in-canvas', 'lock-arrange', 'unlock-arrange', 'json', 'compact',
|
|
168
168
|
'verbose', 'include-logs', 'no-pan', 'schema', 'example', 'examples', 'strict-size', 'scroll-overflow',
|
|
169
169
|
]);
|
|
170
170
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1723,6 +1723,16 @@ cmd('external-app add', 'Create a hosted external app node', [
|
|
|
1723
1723
|
: result);
|
|
1724
1724
|
});
|
|
1725
1725
|
|
|
1726
|
+
cmd('diagram add', 'Create an Excalidraw diagram node', [
|
|
1727
|
+
'pmx-canvas diagram add --title "Architecture"',
|
|
1728
|
+
'pmx-canvas diagram add --title "Architecture" --elements \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'',
|
|
1729
|
+
], async (args) => {
|
|
1730
|
+
const { flags } = parseFlags(args);
|
|
1731
|
+
if (flags.help || flags.h) return showCommandHelp('diagram add');
|
|
1732
|
+
const externalAppAdd = COMMANDS['external-app add'];
|
|
1733
|
+
await externalAppAdd.run([...args, '--kind', 'excalidraw']);
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1726
1736
|
// ── pin ──────────────────────────────────────────────────────
|
|
1727
1737
|
cmd('pin', 'Manage context pins', [
|
|
1728
1738
|
'pmx-canvas pin node1 node2 node3',
|
|
@@ -2431,11 +2441,19 @@ function showCommandHelp(name: string): void {
|
|
|
2431
2441
|
if (name === 'node add') {
|
|
2432
2442
|
console.log('\nSchema help:');
|
|
2433
2443
|
console.log(' pmx-canvas node add --help --type webpage');
|
|
2444
|
+
console.log(' pmx-canvas node add --help --type html');
|
|
2434
2445
|
console.log(' pmx-canvas node add --help --type json-render --component Table');
|
|
2435
2446
|
console.log(' pmx-canvas node add --help --type graph');
|
|
2436
2447
|
console.log(' pmx-canvas html primitive schema --summary');
|
|
2437
2448
|
console.log(' pmx-canvas node add --help --type webpage --json');
|
|
2438
2449
|
console.log(' Use --strict-size to keep explicit width/height fixed and scroll overflowing content.');
|
|
2450
|
+
console.log('\nHTML sidecar flags:');
|
|
2451
|
+
console.log(' --summary <text> Explicit human/agent-readable summary');
|
|
2452
|
+
console.log(' --agent-summary <text> Semantic summary for search, pinned context, and spatial context');
|
|
2453
|
+
console.log(' --description <text> Optional longer semantic description');
|
|
2454
|
+
console.log(' --presentation true Mark raw HTML as an explicit presentation deck');
|
|
2455
|
+
console.log(' --slide-title <text> Add a presentation slide title sidecar');
|
|
2456
|
+
console.log(' --embedded-node-id <id> Link represented/embedded canvas node ID');
|
|
2439
2457
|
}
|
|
2440
2458
|
if (name === 'html primitive add' || name === 'html primitive schema') {
|
|
2441
2459
|
console.log('\nPrimitive flags:');
|
|
@@ -2534,6 +2552,10 @@ function showCommandHelp(name: string): void {
|
|
|
2534
2552
|
console.log(' --initial-file <path> Alias for --elements-file');
|
|
2535
2553
|
console.log(' --timeout-ms <number> Optional downstream MCP timeout for cold starts');
|
|
2536
2554
|
}
|
|
2555
|
+
if (name === 'diagram add') {
|
|
2556
|
+
console.log('\nAlias:');
|
|
2557
|
+
console.log(' Equivalent to: pmx-canvas external-app add --kind excalidraw ...');
|
|
2558
|
+
}
|
|
2537
2559
|
console.log('');
|
|
2538
2560
|
}
|
|
2539
2561
|
|
|
@@ -2561,6 +2583,7 @@ Node commands:
|
|
|
2561
2583
|
pmx-canvas graph add [options] Add a graph node
|
|
2562
2584
|
pmx-canvas html primitive add Add an HTML communication primitive
|
|
2563
2585
|
pmx-canvas html primitive schema List HTML primitive kinds and shapes
|
|
2586
|
+
pmx-canvas diagram add Add an Excalidraw diagram node
|
|
2564
2587
|
|
|
2565
2588
|
Edge commands:
|
|
2566
2589
|
pmx-canvas edge add [options] Add an edge between nodes
|
|
@@ -2636,6 +2659,7 @@ Examples:
|
|
|
2636
2659
|
pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value
|
|
2637
2660
|
pmx-canvas html primitive add --kind choice-grid --data-file ./options.json --title "Options"
|
|
2638
2661
|
pmx-canvas html primitive schema --summary
|
|
2662
|
+
pmx-canvas diagram add --title "Architecture"
|
|
2639
2663
|
pmx-canvas node add --help --type webpage
|
|
2640
2664
|
pmx-canvas node schema --type json-render
|
|
2641
2665
|
pmx-canvas node schema --type json-render --component Table --summary
|
package/src/cli/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
32
32
|
const AGENT_COMMANDS = new Set([
|
|
33
33
|
'node', 'edge', 'json-render', 'search', 'layout', 'status', 'arrange', 'focus',
|
|
34
34
|
'fit', 'screenshot', 'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
|
|
35
|
-
'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'graph', 'html', 'batch', 'validate', 'serve',
|
|
35
|
+
'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'diagram', 'graph', 'html', 'batch', 'validate', 'serve',
|
|
36
36
|
]);
|
|
37
37
|
|
|
38
38
|
const firstArg = args[0] ?? '';
|
|
@@ -506,6 +506,7 @@ Agent CLI (works against running server):
|
|
|
506
506
|
watch [--json] [--events ...] Watch low-token semantic canvas changes
|
|
507
507
|
focus <node-id> Pan to node
|
|
508
508
|
external-app add Add hosted external apps like Excalidraw
|
|
509
|
+
diagram add Add an Excalidraw diagram node
|
|
509
510
|
pin <ids...> | --list | --clear Manage context pins
|
|
510
511
|
undo / redo / history Time travel
|
|
511
512
|
snapshot save|list|restore|diff|delete
|
|
@@ -550,6 +551,7 @@ Examples:
|
|
|
550
551
|
pmx-canvas node schema --type json-render Show running-server schema info
|
|
551
552
|
pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx
|
|
552
553
|
pmx-canvas external-app add --kind excalidraw --title "Diagram"
|
|
554
|
+
pmx-canvas diagram add --title "Diagram"
|
|
553
555
|
pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
|
|
554
556
|
pmx-canvas open Open the workbench in a browser
|
|
555
557
|
pmx-canvas webview status Show WebView automation status
|
|
@@ -93,6 +93,10 @@ function isPresentationNavigationKey(key: string): boolean {
|
|
|
93
93
|
return key === 'ArrowRight' || key === 'PageDown' || key === ' ' || key === 'ArrowLeft' || key === 'PageUp' || key === 'Home' || key === 'End';
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function isPresentationExitButtonTarget(target: EventTarget | null): boolean {
|
|
97
|
+
return target instanceof HTMLElement && Boolean(target.closest('.html-presentation-exit'));
|
|
98
|
+
}
|
|
99
|
+
|
|
96
100
|
export function ExpandedNodeOverlay() {
|
|
97
101
|
const nodeId = expandedNodeId.value;
|
|
98
102
|
const node = nodeId ? nodes.value.get(nodeId) : undefined;
|
|
@@ -100,6 +104,7 @@ export function ExpandedNodeOverlay() {
|
|
|
100
104
|
const [presenting, setPresenting] = useState(false);
|
|
101
105
|
const [presentationExitToken, setPresentationExitToken] = useState('');
|
|
102
106
|
const presentationOverlayRef = useRef<HTMLDivElement>(null);
|
|
107
|
+
const presentationExitButtonRef = useRef<HTMLButtonElement>(null);
|
|
103
108
|
|
|
104
109
|
const handleClose = useCallback(() => {
|
|
105
110
|
setPresenting(false);
|
|
@@ -131,6 +136,13 @@ export function ExpandedNodeOverlay() {
|
|
|
131
136
|
setPresenting(false);
|
|
132
137
|
return;
|
|
133
138
|
}
|
|
139
|
+
if (event.key === 'Tab' && !isPresentationExitButtonTarget(event.target)) {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
event.stopPropagation();
|
|
142
|
+
presentationExitButtonRef.current?.focus();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if ((event.key === ' ' || event.key === 'Enter') && isPresentationExitButtonTarget(event.target)) return;
|
|
134
146
|
if (!isPresentationNavigationKey(event.key)) return;
|
|
135
147
|
event.preventDefault();
|
|
136
148
|
event.stopPropagation();
|
|
@@ -165,7 +177,9 @@ export function ExpandedNodeOverlay() {
|
|
|
165
177
|
useLayoutEffect(() => {
|
|
166
178
|
if (!presenting) return;
|
|
167
179
|
const focusPresentationOverlay = () => {
|
|
168
|
-
presentationOverlayRef.current
|
|
180
|
+
const overlay = presentationOverlayRef.current;
|
|
181
|
+
if (!overlay || overlay.contains(document.activeElement)) return;
|
|
182
|
+
overlay.focus();
|
|
169
183
|
};
|
|
170
184
|
const focusTimers = [0, 50, 150].map((delay) => window.setTimeout(focusPresentationOverlay, delay));
|
|
171
185
|
const handleMessage = (event: MessageEvent) => {
|
|
@@ -354,20 +368,16 @@ export function ExpandedNodeOverlay() {
|
|
|
354
368
|
</div>
|
|
355
369
|
{canPresent && presenting && (
|
|
356
370
|
<div ref={presentationOverlayRef} class="html-presentation-overlay" role="dialog" aria-modal="true" aria-label={`Present ${title}`} tabIndex={-1} onKeyDownCapture={handlePresentationKeyDown}>
|
|
357
|
-
<
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
>
|
|
368
|
-
Exit presentation
|
|
369
|
-
</button>
|
|
370
|
-
</div>
|
|
371
|
+
<button
|
|
372
|
+
ref={presentationExitButtonRef}
|
|
373
|
+
type="button"
|
|
374
|
+
class="html-presentation-exit"
|
|
375
|
+
onClick={handleExitPresentation}
|
|
376
|
+
title="Exit presentation (Esc)"
|
|
377
|
+
aria-label="Exit presentation"
|
|
378
|
+
>
|
|
379
|
+
Exit presentation
|
|
380
|
+
</button>
|
|
371
381
|
<div class="html-presentation-stage">
|
|
372
382
|
<HtmlNode node={node} expanded presentation presentationExitToken={presentationExitToken} />
|
|
373
383
|
</div>
|
|
@@ -255,7 +255,7 @@ export function HtmlNode({
|
|
|
255
255
|
minHeight: presentation ? 0 : expanded ? '70vh' : '300px',
|
|
256
256
|
border: 'none',
|
|
257
257
|
background: 'var(--c-bg)',
|
|
258
|
-
borderRadius: presentation ?
|
|
258
|
+
borderRadius: presentation ? 0 : '6px',
|
|
259
259
|
display: 'block',
|
|
260
260
|
}}
|
|
261
261
|
/>
|
|
@@ -2472,75 +2472,58 @@ body,
|
|
|
2472
2472
|
inset: 0;
|
|
2473
2473
|
z-index: 10050;
|
|
2474
2474
|
display: flex;
|
|
2475
|
-
|
|
2476
|
-
gap: 14px;
|
|
2477
|
-
padding: clamp(12px, 2vw, 28px);
|
|
2475
|
+
padding: 0;
|
|
2478
2476
|
background:
|
|
2479
2477
|
radial-gradient(circle at top left, var(--c-accent-25), transparent 36rem),
|
|
2480
2478
|
rgba(3, 7, 18, 0.96);
|
|
2481
2479
|
color: var(--c-text);
|
|
2482
2480
|
}
|
|
2483
2481
|
|
|
2484
|
-
.html-presentation-toolbar {
|
|
2485
|
-
display: flex;
|
|
2486
|
-
align-items: center;
|
|
2487
|
-
justify-content: space-between;
|
|
2488
|
-
gap: 16px;
|
|
2489
|
-
flex-shrink: 0;
|
|
2490
|
-
padding: 10px 12px;
|
|
2491
|
-
border: 1px solid var(--c-line);
|
|
2492
|
-
border-radius: 16px;
|
|
2493
|
-
background: var(--c-panel-glass);
|
|
2494
|
-
box-shadow: 0 18px 50px var(--c-shadow-heavy);
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
|
-
.html-presentation-kicker {
|
|
2498
|
-
color: var(--c-accent);
|
|
2499
|
-
font-size: 10px;
|
|
2500
|
-
font-weight: 800;
|
|
2501
|
-
letter-spacing: 0.14em;
|
|
2502
|
-
text-transform: uppercase;
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
.html-presentation-title {
|
|
2506
|
-
max-width: min(72vw, 900px);
|
|
2507
|
-
overflow: hidden;
|
|
2508
|
-
color: var(--c-text);
|
|
2509
|
-
font-size: 14px;
|
|
2510
|
-
font-weight: 700;
|
|
2511
|
-
text-overflow: ellipsis;
|
|
2512
|
-
white-space: nowrap;
|
|
2513
|
-
}
|
|
2514
|
-
|
|
2515
2482
|
.html-presentation-exit {
|
|
2516
|
-
|
|
2517
|
-
|
|
2483
|
+
position: fixed;
|
|
2484
|
+
top: 12px;
|
|
2485
|
+
right: 12px;
|
|
2486
|
+
z-index: 1;
|
|
2487
|
+
padding: 10px 14px;
|
|
2518
2488
|
border: 1px solid var(--c-line);
|
|
2519
2489
|
border-radius: 999px;
|
|
2520
|
-
background: var(--c-panel-
|
|
2490
|
+
background: var(--c-panel-glass);
|
|
2491
|
+
box-shadow: 0 18px 50px var(--c-shadow-heavy);
|
|
2521
2492
|
color: var(--c-text-soft);
|
|
2522
2493
|
cursor: pointer;
|
|
2523
2494
|
font: 600 12px/1 var(--font);
|
|
2495
|
+
opacity: 0;
|
|
2496
|
+
pointer-events: none;
|
|
2497
|
+
transform: translateY(-6px);
|
|
2498
|
+
transition: opacity 0.15s ease, transform 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
2524
2499
|
}
|
|
2525
2500
|
|
|
2526
|
-
.html-presentation-exit:hover
|
|
2501
|
+
.html-presentation-exit:hover,
|
|
2502
|
+
.html-presentation-exit:focus-visible {
|
|
2527
2503
|
border-color: var(--c-accent);
|
|
2528
2504
|
color: var(--c-text);
|
|
2529
2505
|
}
|
|
2530
2506
|
|
|
2507
|
+
.html-presentation-exit:focus,
|
|
2508
|
+
.html-presentation-exit:focus-visible {
|
|
2509
|
+
opacity: 1;
|
|
2510
|
+
pointer-events: auto;
|
|
2511
|
+
transform: translateY(0);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2531
2514
|
.html-presentation-stage {
|
|
2532
2515
|
flex: 1;
|
|
2533
2516
|
min-height: 0;
|
|
2534
2517
|
display: flex;
|
|
2535
|
-
border-radius:
|
|
2518
|
+
border-radius: 0;
|
|
2536
2519
|
background: var(--c-bg);
|
|
2537
|
-
box-shadow: 0 24px 90px rgba(0, 0, 0, 0.55);
|
|
2538
2520
|
overflow: hidden;
|
|
2539
2521
|
}
|
|
2540
2522
|
|
|
2541
2523
|
.html-node-frame-presentation {
|
|
2542
2524
|
flex: 1;
|
|
2543
2525
|
min-height: 0;
|
|
2526
|
+
border-radius: 0 !important;
|
|
2544
2527
|
}
|
|
2545
2528
|
|
|
2546
2529
|
/* ── Context pin button on node title bar ────────────────────── */
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { mutationHistory } from './mutation-history.js';
|
|
20
20
|
import { computeGroupBounds, findOpenCanvasPosition } from './placement.js';
|
|
21
21
|
import { searchNodes } from './spatial-analysis.js';
|
|
22
|
-
import { getCanvasNodeTitle,
|
|
22
|
+
import { getCanvasNodeTitle, serializeCanvasNodeCompact, type SerializedCanvasNode } from './canvas-serialization.js';
|
|
23
23
|
import { computeAutoArrange } from '../shared/auto-arrange.js';
|
|
24
24
|
import {
|
|
25
25
|
buildGraphSpec,
|
|
@@ -1490,7 +1490,7 @@ function resolveBatchRefs(value: unknown, refs: Record<string, unknown>): unknow
|
|
|
1490
1490
|
}
|
|
1491
1491
|
|
|
1492
1492
|
function serializeCreatedNode(node: CanvasNodeState): SerializedCanvasNode {
|
|
1493
|
-
return
|
|
1493
|
+
return serializeCanvasNodeCompact(node);
|
|
1494
1494
|
}
|
|
1495
1495
|
|
|
1496
1496
|
export async function executeCanvasBatch(
|
|
@@ -1527,10 +1527,15 @@ export async function executeCanvasBatch(
|
|
|
1527
1527
|
throw new Error('Batch html-primitive creation is not supported yet. Use node.add with type "html" and generated html, or create the primitive through MCP/HTTP/CLI first.');
|
|
1528
1528
|
}
|
|
1529
1529
|
if (type === 'webpage') {
|
|
1530
|
+
const content = typeof args.url === 'string' && args.url.trim().length > 0
|
|
1531
|
+
? args.url
|
|
1532
|
+
: typeof args.content === 'string'
|
|
1533
|
+
? args.content
|
|
1534
|
+
: undefined;
|
|
1530
1535
|
const created = addCanvasNode({
|
|
1531
1536
|
type: 'webpage',
|
|
1532
1537
|
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1533
|
-
...(
|
|
1538
|
+
...(content ? { content } : {}),
|
|
1534
1539
|
...(isPlainRecord(args.data) ? { data: args.data } : {}),
|
|
1535
1540
|
...(typeof args.x === 'number' ? { x: args.x } : {}),
|
|
1536
1541
|
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
@@ -244,11 +244,11 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
244
244
|
fields: [
|
|
245
245
|
{ name: 'html', type: 'string', required: false, description: 'HTML document or fragment rendered in the sandboxed iframe.', aliases: ['content', 'stdin'] },
|
|
246
246
|
{ name: 'summary', type: 'string', required: false, description: 'Explicit agent-readable summary. If omitted, PMX derives one from visible HTML text.' },
|
|
247
|
-
{ name: 'agentSummary', type: 'string', required: false, description: 'Explicit semantic sidecar used by search, pinned context, and spatial context.' },
|
|
248
|
-
{ name: 'embeddedNodeIds', type: 'string[]', required: false, description: 'Canvas node IDs represented or iframe-embedded by this HTML surface.' },
|
|
249
|
-
{ name: 'embeddedUrls', type: 'string[]', required: false, description: 'URLs represented or iframe-embedded by this HTML surface.' },
|
|
247
|
+
{ name: 'agentSummary', type: 'string', required: false, description: 'Explicit semantic sidecar used by search, pinned context, and spatial context.', aliases: ['agent-summary'] },
|
|
248
|
+
{ name: 'embeddedNodeIds', type: 'string[]', required: false, description: 'Canvas node IDs represented or iframe-embedded by this HTML surface.', aliases: ['embedded-node-id'] },
|
|
249
|
+
{ name: 'embeddedUrls', type: 'string[]', required: false, description: 'URLs represented or iframe-embedded by this HTML surface.', aliases: ['embedded-url'] },
|
|
250
250
|
{ name: 'presentation', type: 'boolean', required: false, description: 'Marks this HTML surface as a fullscreen presentation/deck.' },
|
|
251
|
-
{ name: 'slideTitles', type: 'string[]', required: false, description: 'Agent-readable slide titles for presentation HTML.' },
|
|
251
|
+
{ name: 'slideTitles', type: 'string[]', required: false, description: 'Agent-readable slide titles for presentation HTML.', aliases: ['slide-title'] },
|
|
252
252
|
{ name: 'primitive', type: 'HtmlPrimitiveKind', required: false, description: 'Generate HTML from a built-in communication primitive instead of passing raw HTML.', aliases: ['kind'] },
|
|
253
253
|
{ name: 'data', type: 'record<string, unknown>', required: false, description: 'Primitive data when --primitive is used, or arbitrary node metadata.' },
|
|
254
254
|
{ name: 'title', type: 'string', required: false, description: 'Optional node title.' },
|
|
@@ -56,6 +56,13 @@ interface ExternalMcpAppHtmlSummary {
|
|
|
56
56
|
sha256: string;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
interface FileContentSummary {
|
|
60
|
+
omitted: 'file-content';
|
|
61
|
+
bytes: number;
|
|
62
|
+
lineCount: number;
|
|
63
|
+
sha256: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
59
66
|
function pickString(value: unknown): string | null {
|
|
60
67
|
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
61
68
|
}
|
|
@@ -142,6 +149,25 @@ export function serializeCanvasNodeForAgent(node: CanvasNodeState): SerializedCa
|
|
|
142
149
|
};
|
|
143
150
|
}
|
|
144
151
|
|
|
152
|
+
export function serializeCanvasNodeCompact(node: CanvasNodeState): SerializedCanvasNode {
|
|
153
|
+
const serialized = serializeCanvasNode(node);
|
|
154
|
+
if (serialized.type !== 'file' || typeof serialized.data.fileContent !== 'string') return serialized;
|
|
155
|
+
const fileContent = serialized.data.fileContent;
|
|
156
|
+
return {
|
|
157
|
+
...serialized,
|
|
158
|
+
content: serialized.path,
|
|
159
|
+
data: {
|
|
160
|
+
...serialized.data,
|
|
161
|
+
fileContent: {
|
|
162
|
+
omitted: 'file-content',
|
|
163
|
+
bytes: Buffer.byteLength(fileContent, 'utf-8'),
|
|
164
|
+
lineCount: fileContent.split('\n').length,
|
|
165
|
+
sha256: createHash('sha256').update(fileContent).digest('hex'),
|
|
166
|
+
} satisfies FileContentSummary,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
145
171
|
function summarizeBlobValue(value: unknown): unknown {
|
|
146
172
|
if (!canvasState.isBlobReference(value)) return value;
|
|
147
173
|
return {
|
|
@@ -511,14 +511,23 @@ const PRESENTATION_THEMES: Record<PresentationThemeName, PresentationThemeTokens
|
|
|
511
511
|
},
|
|
512
512
|
};
|
|
513
513
|
|
|
514
|
+
function isPresentationThemeName(value: string): value is PresentationThemeName {
|
|
515
|
+
return value === 'canvas' || value === 'midnight' || value === 'paper' || value === 'aurora';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function parsePresentationThemeName(value: string, field = 'theme'): PresentationThemeName {
|
|
519
|
+
if (isPresentationThemeName(value)) return value;
|
|
520
|
+
throw new Error(`Invalid presentation ${field} "${value}". Use canvas, midnight, paper, aurora, or a custom theme object.`);
|
|
521
|
+
}
|
|
522
|
+
|
|
514
523
|
function presentationTheme(data: Record<string, unknown>): PresentationThemeTokens {
|
|
515
524
|
const raw = data.theme ?? data.presentationTheme;
|
|
516
525
|
if (typeof raw === 'string') {
|
|
517
|
-
return PRESENTATION_THEMES[raw
|
|
526
|
+
return PRESENTATION_THEMES[parsePresentationThemeName(raw)];
|
|
518
527
|
}
|
|
519
528
|
if (!isRecord(raw)) return PRESENTATION_THEMES.canvas;
|
|
520
|
-
const baseName = typeof raw.base === 'string'
|
|
521
|
-
? raw.base
|
|
529
|
+
const baseName = typeof raw.base === 'string'
|
|
530
|
+
? parsePresentationThemeName(raw.base, 'theme base')
|
|
522
531
|
: 'canvas';
|
|
523
532
|
const base = PRESENTATION_THEMES[baseName];
|
|
524
533
|
const readColor = (key: string, fallback: string): string => {
|
|
@@ -544,8 +553,9 @@ function presentationTheme(data: Record<string, unknown>): PresentationThemeToke
|
|
|
544
553
|
|
|
545
554
|
function presentationThemeMetadata(data: Record<string, unknown>): string | Record<string, string> | undefined {
|
|
546
555
|
const raw = data.theme ?? data.presentationTheme;
|
|
547
|
-
if (typeof raw === 'string') return raw;
|
|
556
|
+
if (typeof raw === 'string') return parsePresentationThemeName(raw);
|
|
548
557
|
if (!isRecord(raw)) return undefined;
|
|
558
|
+
if (typeof raw.base === 'string') parsePresentationThemeName(raw.base, 'theme base');
|
|
549
559
|
const result: Record<string, string> = {};
|
|
550
560
|
for (const [key, value] of Object.entries(raw)) {
|
|
551
561
|
if (typeof value === 'string') result[key] = value;
|
package/src/server/server.ts
CHANGED
|
@@ -1541,11 +1541,16 @@ function createCanvasHtmlPrimitiveNode(body: Record<string, unknown>): Response
|
|
|
1541
1541
|
return responseJson({ ok: false, error: `Unknown HTML primitive: ${String(rawKind)}.` }, 400);
|
|
1542
1542
|
}
|
|
1543
1543
|
const data = isRecord(body.data) ? body.data : {};
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1544
|
+
let built: ReturnType<typeof buildHtmlPrimitive>;
|
|
1545
|
+
try {
|
|
1546
|
+
built = buildHtmlPrimitive({
|
|
1547
|
+
kind: rawKind,
|
|
1548
|
+
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1549
|
+
data,
|
|
1550
|
+
});
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1553
|
+
}
|
|
1549
1554
|
const geometry = resolveCreateGeometry(body);
|
|
1550
1555
|
const { node } = addCanvasNode({
|
|
1551
1556
|
type: 'html',
|