pmx-canvas 0.7.1 → 0.7.3
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/.github/extensions/pmx-canvas/extension.mjs +79 -61
- package/CHANGELOG.md +29 -1
- package/Readme.md +48 -9
- package/dist/canvas/global.css +144 -45
- package/dist/canvas/index.js +90 -90
- package/dist/json-render/index.js +55 -55
- package/dist/types/client/canvas/ChoiceMenu.d.ts +13 -0
- package/dist/types/client/canvas/ExternalWriters.d.ts +3 -3
- package/dist/types/client/icons.d.ts +15 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +0 -12
- package/dist/types/client/nodes/McpAppNode.d.ts +2 -1
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +1 -1
- package/dist/types/json-render/renderer/index.d.ts +2 -1
- package/dist/types/json-render/server.d.ts +2 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +2 -0
- package/dist/types/shared/delimited-text.d.ts +14 -0
- package/docs/ax-host-adapter-contract.md +13 -2
- package/docs/cli.md +9 -2
- package/docs/http-api.md +12 -8
- package/docs/mcp.md +8 -1
- package/docs/sdk.md +2 -2
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +41 -16
- package/skills/pmx-canvas/evals/evals.json +112 -2
- package/skills/pmx-canvas/references/full-reference.md +9 -4
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +17 -3
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +76 -4
- package/skills/pmx-canvas-testing/SKILL.md +49 -0
- package/src/cli/commands/backup.ts +15 -4
- package/src/cli/commands/boards.ts +16 -3
- package/src/client/canvas/AttentionHistory.tsx +2 -1
- package/src/client/canvas/BoardSwitcher.tsx +3 -3
- package/src/client/canvas/CanvasNode.tsx +211 -197
- package/src/client/canvas/CanvasViewport.tsx +6 -0
- package/src/client/canvas/ChoiceMenu.tsx +122 -0
- package/src/client/canvas/CommandBar.tsx +27 -25
- package/src/client/canvas/ContextPinBar.tsx +3 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -3
- package/src/client/canvas/ExternalWriters.tsx +4 -3
- package/src/client/canvas/FeedbackDialog.tsx +12 -9
- package/src/client/canvas/IntentLayer.tsx +2 -2
- package/src/client/canvas/SelectionBar.tsx +2 -2
- package/src/client/canvas/SessionPanel.tsx +6 -4
- package/src/client/canvas/SessionReceipt.tsx +2 -1
- package/src/client/canvas/SnapshotPanel.tsx +3 -2
- package/src/client/canvas/ToolRail.tsx +48 -100
- package/src/client/canvas/TopBar.tsx +2 -2
- package/src/client/icons.tsx +118 -1
- package/src/client/nodes/BoardNode.tsx +2 -1
- package/src/client/nodes/ExtAppFrame.tsx +3 -1
- package/src/client/nodes/FileNode.tsx +4 -78
- package/src/client/nodes/ImageNode.tsx +2 -1
- package/src/client/nodes/MarkdownNode.tsx +2 -1
- package/src/client/nodes/McpAppNode.tsx +16 -0
- package/src/client/nodes/PromptNode.tsx +3 -1
- package/src/client/nodes/use-ax-surface-bridge.ts +4 -2
- package/src/client/nodes/viewer-runtime-state.ts +78 -0
- package/src/client/state/canvas-store.ts +3 -0
- package/src/client/state/intent-bridge.ts +14 -2
- package/src/client/theme/global.css +144 -45
- package/src/json-render/renderer/index.tsx +58 -38
- package/src/json-render/server.ts +8 -1
- package/src/mcp/server.ts +2 -2
- package/src/server/agent-presence.ts +17 -11
- package/src/server/board-export.ts +97 -32
- package/src/server/index.ts +9 -7
- package/src/server/operations/composites.ts +3 -2
- package/src/server/operations/ops/ax-presence.ts +7 -3
- package/src/server/operations/ops/boards.ts +12 -5
- package/src/server/operations/registry.ts +4 -13
- package/src/server/server.ts +13 -4
- package/src/shared/ax-surface-protocol.ts +3 -0
- package/src/shared/delimited-text.ts +75 -0
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
14
|
-
import { Marked, type RendererThis, type Tokens } from 'marked';
|
|
14
|
+
import { Marked, Renderer, type RendererThis, type Tokens } from 'marked';
|
|
15
15
|
import { canvasThemeScheme } from '../shared/themes.js';
|
|
16
|
+
import { delimiterForPath, parseDelimitedText } from '../shared/delimited-text.js';
|
|
16
17
|
import { buildJsonRenderViewerHtml, escapeInlineScriptSource, readJsonRenderBundle } from '../json-render/server.js';
|
|
17
18
|
import type { JsonRenderSpec } from '../json-render/server.js';
|
|
18
19
|
import { getCanvasNodeTitle } from './canvas-serialization.js';
|
|
@@ -89,6 +90,9 @@ function createMarkdown(remoteImages: Set<string>, links: Set<string>): Marked {
|
|
|
89
90
|
html({ text: raw }: Tokens.HTML | Tokens.Tag) {
|
|
90
91
|
return escapeHtml(raw);
|
|
91
92
|
},
|
|
93
|
+
table(this: RendererThis, token: Tokens.Table) {
|
|
94
|
+
return tableScroll(Renderer.prototype.table.call(this, token));
|
|
95
|
+
},
|
|
92
96
|
link(this: RendererThis, { href, tokens }: Tokens.Link) {
|
|
93
97
|
const inner = this.parser.parseInline(tokens);
|
|
94
98
|
if (/^https?:/i.test(href)) links.add(href);
|
|
@@ -136,6 +140,24 @@ function pre(value: string, className = ''): string {
|
|
|
136
140
|
return `<pre class="${className}">${escapeHtml(value)}</pre>`;
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
function tableScroll(html: string): string {
|
|
144
|
+
return `<div class="table-scroll" role="region" aria-label="Table" tabindex="0">${html}</div>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function fileBody(value: string, name: string): string {
|
|
148
|
+
const delimiter = delimiterForPath(name);
|
|
149
|
+
if (delimiter) {
|
|
150
|
+
const table = parseDelimitedText(value, delimiter);
|
|
151
|
+
if (table.header.length > 1) {
|
|
152
|
+
// Match the workbench's bounded table preview without losing the remainder.
|
|
153
|
+
const preview = table.rows.slice(0, 500);
|
|
154
|
+
const html = `<table><thead><tr>${table.header.map((cell) => `<th scope="col">${escapeHtml(cell)}</th>`).join('')}</tr></thead><tbody>${preview.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
|
|
155
|
+
return `${tableScroll(html)}${table.rows.length > preview.length ? `<details><summary>Showing 500 of ${table.rows.length} rows — full text</summary>${pre(value, 'code')}</details>` : ''}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return pre(value, 'code');
|
|
159
|
+
}
|
|
160
|
+
|
|
139
161
|
function rows(entries: Array<[string, unknown]>): string {
|
|
140
162
|
const cells = entries
|
|
141
163
|
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
@@ -258,20 +280,27 @@ async function collect(
|
|
|
258
280
|
break;
|
|
259
281
|
case 'status':
|
|
260
282
|
card.html = rows([
|
|
261
|
-
['Phase', data.phase],
|
|
283
|
+
['Phase', text(data.phase).trim() || text(data.content).trim() || text(data.status).trim() || 'idle'],
|
|
262
284
|
['Message', data.message],
|
|
263
285
|
['Detail', data.detail],
|
|
286
|
+
['Active tool', data.activeTool],
|
|
264
287
|
]);
|
|
265
288
|
break;
|
|
266
289
|
case 'context': {
|
|
267
290
|
const items = Array.isArray(data.cards) ? (data.cards as Array<Record<string, unknown>>) : [];
|
|
268
|
-
card.html =
|
|
269
|
-
|
|
270
|
-
|
|
291
|
+
card.html = `${rows([
|
|
292
|
+
['Tokens', data.currentTokens],
|
|
293
|
+
['Token limit', data.tokenLimit],
|
|
294
|
+
['Messages', data.messagesLength],
|
|
295
|
+
])}${
|
|
296
|
+
items.length
|
|
297
|
+
? `<ul class="list">${items.map((item) => `<li><strong>${escapeHtml(text(item.title) || text(item.label))}</strong><p>${escapeHtml(text(item.summary))}</p></li>`).join('')}</ul>`
|
|
298
|
+
: `<p class="prose">${escapeHtml(text(data.content) || text(data.rendered).replace(/<[^>]+>/g, ' '))}</p>`
|
|
299
|
+
}`;
|
|
271
300
|
break;
|
|
272
301
|
}
|
|
273
302
|
case 'ledger':
|
|
274
|
-
card.html = `${text(data.content) ? pre(text(data.content)) : ''}${rows(
|
|
303
|
+
card.html = `${text(data.content) ? pre(text(data.content).replaceAll('\\n', '\n')) : ''}${rows(
|
|
275
304
|
Object.entries(data).filter(
|
|
276
305
|
([key]) => !['title', 'content', '__type', 'strictSize', 'arrangeLocked'].includes(key),
|
|
277
306
|
),
|
|
@@ -279,10 +308,11 @@ async function collect(
|
|
|
279
308
|
break;
|
|
280
309
|
case 'trace':
|
|
281
310
|
card.html = rows([
|
|
282
|
-
['Tool', data.toolName],
|
|
283
|
-
['Status', data.status],
|
|
311
|
+
['Tool', text(data.toolName) || title],
|
|
312
|
+
['Status', text(data.status) || 'running'],
|
|
284
313
|
['Duration', data.duration],
|
|
285
|
-
['Result', data.resultSummary],
|
|
314
|
+
['Result', text(data.resultSummary) || text(data.content)],
|
|
315
|
+
['Error', data.error],
|
|
286
316
|
]);
|
|
287
317
|
break;
|
|
288
318
|
case 'diff':
|
|
@@ -301,7 +331,7 @@ async function collect(
|
|
|
301
331
|
.join('\n')}</pre>`;
|
|
302
332
|
break;
|
|
303
333
|
case 'prompt':
|
|
304
|
-
card.html =
|
|
334
|
+
card.html = `<div class="prose">${escapeHtml(text(data.text))}</div>`;
|
|
305
335
|
break;
|
|
306
336
|
case 'response':
|
|
307
337
|
card.html = `<div class="md">${await markdown.parse(text(data.content))}</div>`;
|
|
@@ -327,9 +357,9 @@ async function collect(
|
|
|
327
357
|
if (img) manifest.embeddedImages += 1;
|
|
328
358
|
}
|
|
329
359
|
card.html = img
|
|
330
|
-
? `<img class="image" src="${escapeHtml(img)}" alt="${escapeHtml(alt)}" referrerpolicy="no-referrer">${
|
|
331
|
-
text(data.caption) ? `<
|
|
332
|
-
}
|
|
360
|
+
? `<figure class="image-figure"><img class="image" src="${escapeHtml(img)}" alt="${escapeHtml(alt)}" referrerpolicy="no-referrer">${
|
|
361
|
+
text(data.caption) ? `<figcaption class="dim">${escapeHtml(text(data.caption))}</figcaption>` : ''
|
|
362
|
+
}</figure>`
|
|
333
363
|
: `<div class="placeholder">Image not available: ${escapeHtml(src)}</div>`;
|
|
334
364
|
break;
|
|
335
365
|
}
|
|
@@ -337,14 +367,14 @@ async function collect(
|
|
|
337
367
|
const path = text(data.path);
|
|
338
368
|
if (!path) {
|
|
339
369
|
// Inline content the board itself holds, not a file on disk.
|
|
340
|
-
card.html =
|
|
370
|
+
card.html = fileBody(text(data.fileContent) || text(data.content), title);
|
|
341
371
|
break;
|
|
342
372
|
}
|
|
343
373
|
const body = includeFiles ? fileText(node, path) : null;
|
|
344
374
|
manifest.files.push({ nodeId: node.id, path, included: body !== null });
|
|
345
375
|
card.html = `<p class="dim">${escapeHtml(path)}</p>${
|
|
346
376
|
body !== null
|
|
347
|
-
?
|
|
377
|
+
? fileBody(body, path)
|
|
348
378
|
: '<div class="placeholder">File contents not included in this export.</div>'
|
|
349
379
|
}`;
|
|
350
380
|
break;
|
|
@@ -527,7 +557,7 @@ function exportPage(input: {
|
|
|
527
557
|
</div>
|
|
528
558
|
</header>
|
|
529
559
|
<main id="stage" aria-label="Board"><div id="world"><svg id="edges" aria-hidden="true"></svg></div></main>
|
|
530
|
-
<div id="overlay" hidden><div class="sheet" role="dialog" aria-modal="true"><header><strong id="overlay-title"></strong><button type="button" id="overlay-close" aria-label="Close">×</button></header><div id="overlay-body"></div></div></div>
|
|
560
|
+
<div id="overlay" hidden><div class="sheet" role="dialog" aria-modal="true" aria-labelledby="overlay-title"><header><strong id="overlay-title"></strong><button type="button" id="overlay-close" aria-label="Close">×</button></header><div id="overlay-body"></div></div></div>
|
|
531
561
|
<script type="application/json" id="pmx-board">${scriptJson(input.board)}</script>
|
|
532
562
|
<script type="application/json" id="pmx-assets">${scriptJson(input.assets)}</script>
|
|
533
563
|
<script>${EXPORT_VIEWER_JS}</script>
|
|
@@ -540,7 +570,7 @@ const EXPORT_CSS = `
|
|
|
540
570
|
:root{--bg:#0b1016;--panel:#121a23;--line:#263241;--text:#e8edf2;--soft:#9aa7b4;--accent:#4bbcff;--add:#2ea66f;--del:#e25d6e}
|
|
541
571
|
[data-scheme=light]{--bg:#f4f6f8;--panel:#ffffff;--line:#d5dbe1;--text:#17202a;--soft:#5b6875;--accent:#0a6fc2;--add:#1c7a4f;--del:#b3263a}
|
|
542
572
|
*{box-sizing:border-box}
|
|
543
|
-
html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font:
|
|
573
|
+
html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font:15px/1.65 system-ui,-apple-system,Segoe UI,sans-serif}
|
|
544
574
|
.bar{position:fixed;top:0;left:0;right:0;height:48px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 16px;background:var(--panel);border-bottom:1px solid var(--line);z-index:5}
|
|
545
575
|
.bar .name{display:flex;flex-direction:column;min-width:0}
|
|
546
576
|
.bar .name strong,.bar .name span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -548,6 +578,7 @@ html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font:14px/
|
|
|
548
578
|
.tools{display:flex;gap:6px;flex-shrink:0}
|
|
549
579
|
button{font:inherit;color:var(--text);background:none;border:1px solid var(--line);border-radius:7px;padding:4px 10px;cursor:pointer}
|
|
550
580
|
button:hover{border-color:var(--accent)}
|
|
581
|
+
button:focus-visible,a:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--accent);outline-offset:-3px}
|
|
551
582
|
#stage{position:fixed;top:48px;left:0;right:0;bottom:0;overflow:hidden;cursor:grab;touch-action:none}
|
|
552
583
|
#stage.panning{cursor:grabbing}
|
|
553
584
|
#world{position:absolute;left:0;top:0;transform-origin:0 0}
|
|
@@ -556,25 +587,43 @@ button:hover{border-color:var(--accent)}
|
|
|
556
587
|
#edges text{fill:var(--soft);font-size:12px;text-anchor:middle}
|
|
557
588
|
.card{position:absolute;display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden;box-shadow:0 6px 24px rgba(0,0,0,.18)}
|
|
558
589
|
.card.group{background:color-mix(in srgb,var(--accent) 5%,transparent);border-style:dashed;box-shadow:none}
|
|
559
|
-
.card-title{flex-shrink:0;padding:
|
|
590
|
+
.card-title{position:relative;flex-shrink:0;width:100%;text-align:left;padding:12px 42px 12px 18px;font-weight:600;font-size:13px;border:0;border-bottom:1px solid var(--line);border-radius:0;cursor:zoom-in;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;background:color-mix(in srgb,var(--soft) 5%,var(--panel))}
|
|
591
|
+
button.card-title:after{content:'↗';position:absolute;right:16px;color:var(--soft);font-size:16px}
|
|
560
592
|
.card.group .card-title{border-bottom:none;cursor:default;color:var(--soft)}
|
|
561
|
-
.card-body{flex:1;min-height:0;overflow:auto;padding:
|
|
562
|
-
.card
|
|
563
|
-
.
|
|
564
|
-
.md
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
593
|
+
.card-body{flex:1;min-height:0;min-width:0;overflow:auto;padding:20px;overflow-wrap:anywhere;overscroll-behavior:contain;cursor:auto}
|
|
594
|
+
.card-body>:first-child,.md>:first-child{margin-top:0}.card-body>:last-child,.md>:last-child{margin-bottom:0}
|
|
595
|
+
.card iframe,#overlay-body iframe{flex:1;min-height:0;width:100%;height:100%;border:0;background:var(--bg)}
|
|
596
|
+
.md h1,.md h2,.md h3,.md h4,.md h5,.md h6{margin:1.4em 0 .6em;line-height:1.3;font-weight:650;letter-spacing:-.02em}
|
|
597
|
+
.md h1{font-size:26px}.md h2{font-size:21px}.md h3{font-size:18px}.md h4,.md h5,.md h6{font-size:15px}
|
|
598
|
+
.md p{margin:0 0 1em}.md img{max-width:100%;height:auto;border-radius:6px}
|
|
599
|
+
.md ul,.md ol{padding-left:1.6em;margin:0 0 1em}.md li+li{margin-top:.35em}.md li>ul,.md li>ol{margin-top:.4em;margin-bottom:.4em}
|
|
600
|
+
.md input[type=checkbox]{accent-color:var(--accent);margin-right:.5em}
|
|
601
|
+
.md blockquote{margin:1em 0;padding:10px 16px;border-left:3px solid var(--accent);background:color-mix(in srgb,var(--accent) 7%,var(--panel));color:var(--soft)}
|
|
602
|
+
.md blockquote>:last-child{margin-bottom:0}.md hr{border:0;border-top:1px solid var(--line);margin:24px 0}
|
|
603
|
+
.md code,pre{font:13px/1.65 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2}
|
|
604
|
+
.md :not(pre)>code{padding:2px 5px;border:1px solid var(--line);border-radius:4px;background:color-mix(in srgb,var(--soft) 8%,var(--panel))}
|
|
605
|
+
pre{margin:0 0 16px;padding:14px 16px;border:1px solid var(--line);border-radius:8px;background:color-mix(in srgb,var(--soft) 5%,var(--panel));white-space:pre;overflow:auto;overflow-wrap:normal;overscroll-behavior:contain}
|
|
606
|
+
.prose{white-space:pre-wrap}.table-scroll{width:100%;max-width:100%;overflow:auto;margin:16px 0;border:1px solid var(--line);border-radius:8px;overscroll-behavior:contain}
|
|
607
|
+
.table-scroll table{border-collapse:separate;border-spacing:0;width:100%;font-size:13px;line-height:1.55;font-variant-numeric:tabular-nums}
|
|
608
|
+
.table-scroll th,.table-scroll td{padding:10px 14px;min-width:90px;max-width:36ch;border-bottom:1px solid var(--line);border-right:1px solid var(--line);vertical-align:top;overflow-wrap:normal;white-space:pre-wrap}
|
|
609
|
+
.table-scroll th{font-weight:650;background:color-mix(in srgb,var(--soft) 10%,var(--panel))}.table-scroll th:not([align]){text-align:left}
|
|
610
|
+
.table-scroll tr:nth-child(even) td{background:color-mix(in srgb,var(--soft) 4%,var(--panel))}
|
|
611
|
+
.table-scroll tr:last-child td{border-bottom:0}.table-scroll tr>:last-child{border-right:0}
|
|
612
|
+
pre.diff .add{color:var(--add);background:color-mix(in srgb,var(--add) 9%,transparent)}pre.diff .del{color:var(--del);background:color-mix(in srgb,var(--del) 9%,transparent)}pre.diff .hunk{color:var(--accent)}
|
|
613
|
+
a{color:var(--accent);text-underline-offset:3px}
|
|
568
614
|
.dim{color:var(--soft)}
|
|
569
|
-
.kv{border-collapse:collapse;font-size:
|
|
570
|
-
.list{margin:0;padding-
|
|
571
|
-
.image{max-width:100%;
|
|
572
|
-
.placeholder{padding:
|
|
615
|
+
.kv{border-collapse:collapse;width:100%;font-size:14px;table-layout:fixed}.kv th{text-align:left;color:var(--soft);width:30%;font-weight:500}.kv th,.kv td{padding:10px 12px;border-bottom:1px solid var(--line);vertical-align:top;white-space:pre-wrap}.kv tr:last-child>*{border-bottom:0}
|
|
616
|
+
.list{list-style:none;margin:0;padding:0}.list li{padding:12px 0;border-bottom:1px solid var(--line)}.list li:last-child{border-bottom:0}.list p{margin:6px 0 0;color:var(--soft)}
|
|
617
|
+
.image-figure{margin:0;height:100%;display:flex;flex-direction:column;gap:12px}.image{flex:1;min-height:0;max-width:100%;width:100%;object-fit:contain;display:block}.image-figure figcaption{flex-shrink:0;font-size:13px;text-align:center}
|
|
618
|
+
.placeholder{padding:16px;border:1px dashed var(--line);border-radius:8px;color:var(--soft);font-size:14px;line-height:1.6}
|
|
619
|
+
details{margin-top:16px}summary{cursor:pointer;color:var(--soft);margin-bottom:12px}
|
|
573
620
|
#overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;padding:24px;z-index:10}
|
|
574
621
|
#overlay[hidden]{display:none}
|
|
575
622
|
#overlay .sheet{width:min(1100px,100%);height:min(90vh,100%);display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}
|
|
576
623
|
#overlay .sheet>header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;border-bottom:1px solid var(--line)}
|
|
577
|
-
#overlay-body{flex:1;min-height:0;overflow:
|
|
624
|
+
#overlay-body{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column}
|
|
625
|
+
#overlay-body .card-body{padding:28px 32px}#overlay-body .md{max-width:960px;margin:0 auto}
|
|
626
|
+
@media(max-width:600px){#overlay{padding:8px}#overlay .sheet{height:100%}#overlay-body .card-body{padding:18px}.bar{padding:0 10px}.bar .name span{display:none}}
|
|
578
627
|
`;
|
|
579
628
|
|
|
580
629
|
const EXPORT_VIEWER_JS = `(() => {
|
|
@@ -606,12 +655,15 @@ for (const card of ordered) {
|
|
|
606
655
|
const el = document.createElement('section');
|
|
607
656
|
el.className = 'card' + (card.group ? ' group' : '');
|
|
608
657
|
el.dataset.nodeId = card.id;
|
|
658
|
+
el.dataset.nodeType = card.kind;
|
|
609
659
|
Object.assign(el.style, { left: card.x + 'px', top: card.y + 'px', width: card.w + 'px', height: card.h + 'px' });
|
|
610
|
-
const title = document.createElement('div');
|
|
660
|
+
const title = document.createElement(card.group ? 'div' : 'button');
|
|
611
661
|
title.className = 'card-title';
|
|
612
662
|
title.textContent = card.title;
|
|
613
663
|
el.appendChild(title);
|
|
614
664
|
if (!card.group) {
|
|
665
|
+
title.type = 'button';
|
|
666
|
+
title.setAttribute('aria-label', 'Expand ' + card.title);
|
|
615
667
|
el.appendChild(body(card));
|
|
616
668
|
title.addEventListener('click', () => expand(card));
|
|
617
669
|
}
|
|
@@ -653,6 +705,7 @@ const zoomAt = (factor, cx, cy) => {
|
|
|
653
705
|
apply();
|
|
654
706
|
};
|
|
655
707
|
stage.addEventListener('wheel', (e) => {
|
|
708
|
+
if (e.target.closest('.card-body') && !e.ctrlKey && !e.metaKey) return;
|
|
656
709
|
e.preventDefault();
|
|
657
710
|
const r = stage.getBoundingClientRect();
|
|
658
711
|
zoomAt(Math.exp(-e.deltaY * 0.0015), e.clientX - r.left, e.clientY - r.top);
|
|
@@ -673,11 +726,23 @@ stage.addEventListener('pointerup', () => { drag = null; stage.classList.remove(
|
|
|
673
726
|
document.querySelector('[data-zoom=in]').addEventListener('click', () => zoomAt(1.25, stage.clientWidth / 2, stage.clientHeight / 2));
|
|
674
727
|
document.querySelector('[data-zoom=out]').addEventListener('click', () => zoomAt(0.8, stage.clientWidth / 2, stage.clientHeight / 2));
|
|
675
728
|
document.querySelector('[data-zoom=fit]').addEventListener('click', fit);
|
|
676
|
-
|
|
729
|
+
let invoker = null;
|
|
730
|
+
const close = () => {
|
|
731
|
+
if (overlay.hidden) return;
|
|
732
|
+
overlay.hidden = true;
|
|
733
|
+
document.getElementById('overlay-body').replaceChildren();
|
|
734
|
+
stage.inert = false;
|
|
735
|
+
document.querySelector('.bar').inert = false;
|
|
736
|
+
invoker?.focus({ preventScroll: true });
|
|
737
|
+
};
|
|
677
738
|
function expand(card) {
|
|
739
|
+
invoker = document.activeElement;
|
|
678
740
|
document.getElementById('overlay-title').textContent = card.title;
|
|
679
741
|
document.getElementById('overlay-body').replaceChildren(body(card));
|
|
680
742
|
overlay.hidden = false;
|
|
743
|
+
stage.inert = true;
|
|
744
|
+
document.querySelector('.bar').inert = true;
|
|
745
|
+
document.getElementById('overlay-close').focus();
|
|
681
746
|
}
|
|
682
747
|
document.getElementById('overlay-close').addEventListener('click', close);
|
|
683
748
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
package/src/server/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ import type { BackupFile, BackupStatus } from './backup.js';
|
|
|
44
44
|
import type { ExportManifest } from './board-export.js';
|
|
45
45
|
import { onFileNodeChanged } from './file-watcher.js';
|
|
46
46
|
import { findOpenCanvasPosition, computeGroupBounds } from './placement.js';
|
|
47
|
+
import { overlapsAny } from '../shared/placement.js';
|
|
47
48
|
import { searchNodes, buildSpatialContext } from './spatial-analysis.js';
|
|
48
49
|
import { mutationHistory, diffLayouts, formatDiff } from './mutation-history.js';
|
|
49
50
|
import { recomputeCodeGraph, buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
@@ -741,14 +742,19 @@ export class PmxCanvas extends EventEmitter {
|
|
|
741
742
|
} {
|
|
742
743
|
if (canvasState.activeBoardId !== input.boardId) throw new Error('The captured target board is not active.');
|
|
743
744
|
assertInsideFence('node.add', createTarget(input));
|
|
745
|
+
const obstacles = canvasState.getLayout().nodes;
|
|
746
|
+
const requested = { x: input.x ?? 40, y: input.y ?? 80 };
|
|
747
|
+
const position = overlapsAny(requested, 440, 360, obstacles, 48)
|
|
748
|
+
? findOpenCanvasPosition(obstacles, 440, 360, 48)
|
|
749
|
+
: requested;
|
|
750
|
+
assertInsideFence('node.add', createTarget(position));
|
|
744
751
|
const attachment = canvasState.storeAttachment({ ...input, mime: input.mime ?? 'application/octet-stream' });
|
|
745
752
|
if (!attachment) throw new Error('Board not found.');
|
|
746
753
|
const { id: nodeId, node } = addCanvasNode({
|
|
747
754
|
type: 'file',
|
|
748
755
|
title: input.name,
|
|
749
756
|
data: { attachmentId: attachment.id, mime: attachment.mime, size: attachment.size },
|
|
750
|
-
|
|
751
|
-
y: input.y,
|
|
757
|
+
...position,
|
|
752
758
|
defaultWidth: 440,
|
|
753
759
|
defaultHeight: 360,
|
|
754
760
|
fileMode: 'auto',
|
|
@@ -840,11 +846,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
840
846
|
|
|
841
847
|
/** Open a board, or Home with null. */
|
|
842
848
|
async openBoard(id: string | null): Promise<void> {
|
|
843
|
-
await executeOperation(
|
|
844
|
-
'board.open',
|
|
845
|
-
{ id },
|
|
846
|
-
{ source: 'sdk', fromWorkbench: true, humanAuthor: true, suppressAutoGhost: true },
|
|
847
|
-
);
|
|
849
|
+
await executeOperation('board.open', { id }, { source: 'sdk', suppressAutoGhost: true });
|
|
848
850
|
}
|
|
849
851
|
|
|
850
852
|
async deleteBoard(id: string): Promise<void> {
|
|
@@ -151,13 +151,14 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
151
151
|
{
|
|
152
152
|
toolName: 'canvas_board',
|
|
153
153
|
description:
|
|
154
|
-
'Boards in this workspace — the library of the human\'s and your working memory, filed in categories on Home. Your writes always go to the board
|
|
155
|
-
actionSummary: 'list | get | create | create-from | update',
|
|
154
|
+
'Boards in this workspace — the library of the human\'s and your working memory, filed in categories on Home. Your writes always go to the open board (activeBoardId); on Home (no board open) your first write opens the most recent board, or a new one when there are none. Action "list" returns every board (with its category), most recently opened first; "get" reads one (default: the open board); "create" makes a new empty board WITHOUT opening it (optional category); "open" explicitly switches the shared workbench to id (null or omitted opens Home), changing the visible board and subsequent write target. After creating a board, open its returned id and verify activeBoardId before writing; another participant can switch the board later. "update" renames a board, files it under a category (category "" removes it), or designates its README via readmeNodeId. As part of normal board authoring, read the board metadata; if readmeNodeId is null, choose or create a concise markdown introduction explaining its purpose and key context, then designate that node with "update". Preserve an existing README unless the human asks to change it; humans can override the designation manually. Only the human deletes boards.',
|
|
155
|
+
actionSummary: 'list | get | create | create-from | open | update',
|
|
156
156
|
actions: {
|
|
157
157
|
list: 'board.list',
|
|
158
158
|
get: 'board.get',
|
|
159
159
|
create: 'board.create',
|
|
160
160
|
'create-from': 'board.create-from',
|
|
161
|
+
open: 'board.open',
|
|
161
162
|
update: 'board.update',
|
|
162
163
|
},
|
|
163
164
|
},
|
|
@@ -27,13 +27,17 @@ const presenceGetOperation = defineOperation<z.infer<typeof emptySchema>, Record
|
|
|
27
27
|
// One schema (agent-presence.ts) — only the descriptions are added here.
|
|
28
28
|
const presenceSetShape = {
|
|
29
29
|
source: PRESENCE_SET_SHAPE.source.describe('Host label (copilot, codex, mcp, …); defaults to the transport'),
|
|
30
|
-
agentId: PRESENCE_SET_SHAPE.agentId.describe(
|
|
31
|
-
|
|
30
|
+
agentId: PRESENCE_SET_SHAPE.agentId.describe(
|
|
31
|
+
'Stable identity for a genuinely separate agent, not a task name. Omit for the main agent when a host adapter already owns its session.',
|
|
32
|
+
),
|
|
33
|
+
label: PRESENCE_SET_SHAPE.label.describe('Display name for this writer; use detail for the current task'),
|
|
32
34
|
phase: PRESENCE_SET_SHAPE.phase.describe('idle | thinking | tooling | waiting-approval'),
|
|
33
35
|
detail: PRESENCE_SET_SHAPE.detail.describe('Tool / step name shown beside the phase'),
|
|
34
36
|
focusNodeId: PRESENCE_SET_SHAPE.focusNodeId.describe('Node the agent is working on'),
|
|
35
37
|
cursor: PRESENCE_SET_SHAPE.cursor.describe('Agent cursor in world coordinates'),
|
|
36
|
-
attached: PRESENCE_SET_SHAPE.attached.describe(
|
|
38
|
+
attached: PRESENCE_SET_SHAPE.attached.describe(
|
|
39
|
+
'true attaches a session; false detaches it. Read presence first and reuse your host adapter session instead of attaching another. Only detach sessions you own.',
|
|
40
|
+
),
|
|
37
41
|
contextUsage: PRESENCE_SET_SHAPE.contextUsage.describe(
|
|
38
42
|
"The agent's real token window as {used,total} — shown on the board's context meter instead of the pinned-context estimate (null clears it)",
|
|
39
43
|
),
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Many boards (plan 012): board.list / board.get / board.create /
|
|
3
3
|
* board.update / board.open / board.delete.
|
|
4
4
|
*
|
|
5
|
-
* Agents
|
|
6
|
-
* human
|
|
7
|
-
*
|
|
5
|
+
* Agents can explicitly open boards as well as list, read and create them.
|
|
6
|
+
* Deletion requires the human (HUMAN_ONLY_OPS in registry.ts).
|
|
7
|
+
* Every change broadcasts `boards-changed` with the full list.
|
|
8
8
|
*
|
|
9
9
|
* Route-order note: board.open (POST /api/canvas/boards/open) is registered
|
|
10
10
|
* before any `/api/canvas/boards/:id` POST route so ':id' cannot swallow it.
|
|
@@ -171,7 +171,7 @@ const boardCreateOperation = defineOperation<z.infer<typeof createSchema>, Recor
|
|
|
171
171
|
mcp: {
|
|
172
172
|
toolName: 'canvas_create_board',
|
|
173
173
|
description:
|
|
174
|
-
'Create a new empty board
|
|
174
|
+
'Create a new empty board without opening it. Use canvas_board action "open" with the returned board id before writing to it.',
|
|
175
175
|
extraShape: {
|
|
176
176
|
name: z.string().describe('Board name'),
|
|
177
177
|
category: z.string().optional().describe('Folder path on Home, e.g. Engineering/Canvas'),
|
|
@@ -186,7 +186,7 @@ const boardCreateOperation = defineOperation<z.infer<typeof createSchema>, Recor
|
|
|
186
186
|
},
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
-
// ── board.open
|
|
189
|
+
// ── board.open ────────────────────────────────────────────────
|
|
190
190
|
|
|
191
191
|
const openShape = {
|
|
192
192
|
id: z.unknown().optional().describe('Board id to open; null or omitted opens Home'),
|
|
@@ -199,6 +199,13 @@ const boardOpenOperation = defineOperation<z.infer<typeof openSchema>, Record<st
|
|
|
199
199
|
input: openSchema,
|
|
200
200
|
inputShape: openShape,
|
|
201
201
|
http: { method: 'POST', path: '/api/canvas/boards/open' },
|
|
202
|
+
mcp: {
|
|
203
|
+
toolName: 'canvas_open_board',
|
|
204
|
+
description:
|
|
205
|
+
'Open a board in the shared workbench, or Home with null. Changes the visible board and the target of subsequent writes. Verify activeBoardId in the response before authoring.',
|
|
206
|
+
extraShape: { id: z.string().nullable().optional().describe('Board id to open; null or omitted opens Home') },
|
|
207
|
+
formatResult: jsonResult,
|
|
208
|
+
},
|
|
202
209
|
handler: (input, ctx) => {
|
|
203
210
|
const id = typeof input.id === 'string' && input.id.trim() ? input.id.trim() : null;
|
|
204
211
|
if (!openCanvasBoard(id).ok) throw new OperationError(`Board "${id}" not found.`, 404);
|
|
@@ -405,15 +405,9 @@ export async function executeOperation(
|
|
|
405
405
|
);
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
/**
|
|
409
|
-
const HUMAN_ONLY_OPS = new Set(['board.
|
|
410
|
-
const TRUSTED_HUMAN_ONLY_OPS = new Set([
|
|
411
|
-
'board.open',
|
|
412
|
-
'board.delete',
|
|
413
|
-
'import.request',
|
|
414
|
-
'import.cancel',
|
|
415
|
-
'import.commit',
|
|
416
|
-
]);
|
|
408
|
+
/** Board deletion and import review actions require the human in the workbench. */
|
|
409
|
+
const HUMAN_ONLY_OPS = new Set(['board.delete', 'import.request', 'import.cancel', 'import.commit']);
|
|
410
|
+
const TRUSTED_HUMAN_ONLY_OPS = new Set(['board.delete', 'import.request', 'import.cancel', 'import.commit']);
|
|
417
411
|
|
|
418
412
|
/**
|
|
419
413
|
* Writes that need a board beyond the layout mutations: canvas-bound AX items,
|
|
@@ -443,10 +437,7 @@ function needsOpenBoard(op: Operation): boolean {
|
|
|
443
437
|
async function executeOperationInner(name: string, rawInput: unknown, meta: ExecuteOperationMeta): Promise<unknown> {
|
|
444
438
|
const op = getOperation(name);
|
|
445
439
|
if (HUMAN_ONLY_OPS.has(name) && !meta.fromWorkbench) {
|
|
446
|
-
throw new OperationError(
|
|
447
|
-
'Only the human opens or deletes boards. Ask them in the chat to open the board you need.',
|
|
448
|
-
403,
|
|
449
|
-
);
|
|
440
|
+
throw new OperationError('This action requires the human in the workbench.', 403);
|
|
450
441
|
}
|
|
451
442
|
if (TRUSTED_HUMAN_ONLY_OPS.has(name) && !meta.humanAuthor) {
|
|
452
443
|
throw new OperationError('This action requires trusted human authority.', 403);
|
package/src/server/server.ts
CHANGED
|
@@ -61,6 +61,7 @@ import { normalizeExtAppToolResult } from './ext-app-tool-result.js';
|
|
|
61
61
|
import { getMcpAppHostSnapshot } from './mcp-app-host.js';
|
|
62
62
|
import { closeMcpAppSession, closeAllMcpAppSessions } from './mcp-app-runtime.js';
|
|
63
63
|
import { findOpenCanvasPosition } from './placement.js';
|
|
64
|
+
import { overlapsAny } from '../shared/placement.js';
|
|
64
65
|
import { mutationHistory } from './mutation-history.js';
|
|
65
66
|
import { buildAgentContextPreamble } from './agent-context.js';
|
|
66
67
|
import { buildCanvasAxSurfaceSnapshot } from './ax-context.js';
|
|
@@ -1736,10 +1737,12 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
|
1736
1737
|
const html = await buildJsonRenderViewerHtml({
|
|
1737
1738
|
title,
|
|
1738
1739
|
spec,
|
|
1740
|
+
nodeId,
|
|
1741
|
+
stateToken: url.searchParams.get('stateToken') ?? undefined,
|
|
1739
1742
|
...(theme ? { theme } : {}),
|
|
1740
1743
|
...(display ? { display } : {}),
|
|
1741
1744
|
...(devtoolsEnabled ? { devtools: true } : {}),
|
|
1742
|
-
...(axToken ? {
|
|
1745
|
+
...(axToken ? { axToken } : {}),
|
|
1743
1746
|
// Seed the read-side AX state (only for AX-enabled nodes) so specs can bind /ax.
|
|
1744
1747
|
...(axToken && axEnabled ? { axState: buildCanvasAxSurfaceSnapshot() } : {}),
|
|
1745
1748
|
// Content-fit: report natural height (charts render intrinsic) so the node grows.
|
|
@@ -3474,19 +3477,25 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3474
3477
|
canvasState.storeAttachment({ boardId, name, mime, bytes }),
|
|
3475
3478
|
);
|
|
3476
3479
|
if (!attachment) return responseJson({ ok: false, error: 'Board not found.' }, 404);
|
|
3480
|
+
const obstacles = canvasState.getLayout().nodes;
|
|
3481
|
+
const position = overlapsAny({ x, y }, 440, 360, obstacles, 48)
|
|
3482
|
+
? findOpenCanvasPosition(obstacles, 440, 360, 48)
|
|
3483
|
+
: { x, y };
|
|
3477
3484
|
const { node } = withCurrentActor({ actor: 'human', source: 'browser' }, () =>
|
|
3478
3485
|
addCanvasNode({
|
|
3479
3486
|
type: 'file',
|
|
3480
3487
|
title: name,
|
|
3481
3488
|
data: { attachmentId: attachment.id, mime, size: attachment.size },
|
|
3482
|
-
|
|
3483
|
-
y,
|
|
3489
|
+
...position,
|
|
3484
3490
|
defaultWidth: 440,
|
|
3485
3491
|
defaultHeight: 360,
|
|
3486
3492
|
fileMode: 'auto',
|
|
3487
3493
|
}),
|
|
3488
3494
|
);
|
|
3489
|
-
const created = canvasState.createDocumentImport(attachment.id, {
|
|
3495
|
+
const created = canvasState.createDocumentImport(attachment.id, {
|
|
3496
|
+
x: node.position.x,
|
|
3497
|
+
y: node.position.y + 400,
|
|
3498
|
+
});
|
|
3490
3499
|
const documentImport = created;
|
|
3491
3500
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
3492
3501
|
return responseJson({ ok: true, attachment, nodeId: node.id, import: documentImport }, 201);
|
|
@@ -18,6 +18,9 @@ export const AX_SURFACE_ACK_SOURCE = 'pmx-canvas-ax-ack';
|
|
|
18
18
|
/** Parent→surface pushes (theme updates, live AX state) into html/viewer iframes. */
|
|
19
19
|
export const HTML_SURFACE_PUSH_SOURCE = 'pmx-canvas-html-node';
|
|
20
20
|
|
|
21
|
+
/** Tab-local JSON viewer state handoff, independent of AX capabilities. */
|
|
22
|
+
export const JSON_VIEWER_STATE_SOURCE = 'pmx-canvas-json-state';
|
|
23
|
+
|
|
21
24
|
/** Ext-app boot beacon: the iframe's scripts executed (WebKit watchdog liveness). */
|
|
22
25
|
export const EXT_APP_BOOT_BEACON_SOURCE = 'pmx-canvas-ext-app-alive';
|
|
23
26
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface DelimitedTable {
|
|
2
|
+
header: string[];
|
|
3
|
+
/** Rectangular: every row padded (short) or truncated (long) to the header width. */
|
|
4
|
+
rows: string[][];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Column delimiter for a delimited-data path, or null when the file is not one. */
|
|
8
|
+
export function delimiterForPath(path: string): string | null {
|
|
9
|
+
const lower = path.toLowerCase();
|
|
10
|
+
if (lower.endsWith('.csv')) return ',';
|
|
11
|
+
if (lower.endsWith('.tsv')) return '\t';
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* RFC4180-ish delimited-text parser. Quoted fields may contain the delimiter,
|
|
17
|
+
* newlines, and `""` escapes; CRLF, LF and lone CR all end a record; a trailing
|
|
18
|
+
* newline produces no phantom empty row. Ragged records are squared off against
|
|
19
|
+
* the header width instead of throwing.
|
|
20
|
+
*/
|
|
21
|
+
export function parseDelimitedText(text: string, delimiter: string): DelimitedTable {
|
|
22
|
+
const records: string[][] = [];
|
|
23
|
+
let row: string[] = [];
|
|
24
|
+
let field = '';
|
|
25
|
+
let quoted = false;
|
|
26
|
+
// Tracks whether the current record consumed anything — distinguishes a real
|
|
27
|
+
// empty-quoted record from the dangling state after a terminating newline.
|
|
28
|
+
let started = false;
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < text.length; i++) {
|
|
31
|
+
const char = text[i];
|
|
32
|
+
started = true;
|
|
33
|
+
if (quoted) {
|
|
34
|
+
if (char === '"') {
|
|
35
|
+
if (text[i + 1] === '"') {
|
|
36
|
+
field += '"';
|
|
37
|
+
i++;
|
|
38
|
+
} else {
|
|
39
|
+
quoted = false;
|
|
40
|
+
}
|
|
41
|
+
} else if (char === '\r' && text[i + 1] === '\n') {
|
|
42
|
+
field += '\n';
|
|
43
|
+
i++;
|
|
44
|
+
} else {
|
|
45
|
+
field += char;
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (char === '"' && field === '') {
|
|
50
|
+
quoted = true;
|
|
51
|
+
} else if (char === delimiter) {
|
|
52
|
+
row.push(field);
|
|
53
|
+
field = '';
|
|
54
|
+
} else if (char === '\n' || char === '\r') {
|
|
55
|
+
if (char === '\r' && text[i + 1] === '\n') i++;
|
|
56
|
+
row.push(field);
|
|
57
|
+
field = '';
|
|
58
|
+
records.push(row);
|
|
59
|
+
row = [];
|
|
60
|
+
started = false;
|
|
61
|
+
} else {
|
|
62
|
+
field += char;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (started) {
|
|
66
|
+
row.push(field);
|
|
67
|
+
records.push(row);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const header = records.shift() ?? [];
|
|
71
|
+
return {
|
|
72
|
+
header,
|
|
73
|
+
rows: records.map((record) => Array.from({ length: header.length }, (_, i) => record[i] ?? '')),
|
|
74
|
+
};
|
|
75
|
+
}
|