pmx-canvas 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +79 -61
  2. package/CHANGELOG.md +16 -1
  3. package/Readme.md +31 -6
  4. package/dist/canvas/global.css +21 -5
  5. package/dist/canvas/index.js +73 -73
  6. package/dist/types/client/canvas/ExternalWriters.d.ts +3 -3
  7. package/dist/types/client/icons.d.ts +15 -0
  8. package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -1
  9. package/dist/types/client/nodes/FileNode.d.ts +0 -12
  10. package/dist/types/shared/delimited-text.d.ts +14 -0
  11. package/docs/cli.md +9 -2
  12. package/docs/http-api.md +12 -8
  13. package/docs/mcp.md +8 -1
  14. package/docs/sdk.md +2 -2
  15. package/package.json +1 -1
  16. package/skills/pmx-canvas/SKILL.md +19 -6
  17. package/skills/pmx-canvas/evals/evals.json +43 -2
  18. package/skills/pmx-canvas/references/full-reference.md +6 -1
  19. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +17 -3
  20. package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
  21. package/src/cli/commands/backup.ts +15 -4
  22. package/src/cli/commands/boards.ts +16 -3
  23. package/src/client/canvas/AttentionHistory.tsx +2 -1
  24. package/src/client/canvas/BoardSwitcher.tsx +3 -3
  25. package/src/client/canvas/CanvasNode.tsx +17 -8
  26. package/src/client/canvas/CommandBar.tsx +2 -2
  27. package/src/client/canvas/ContextPinBar.tsx +3 -1
  28. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -3
  29. package/src/client/canvas/ExternalWriters.tsx +4 -3
  30. package/src/client/canvas/FeedbackDialog.tsx +2 -1
  31. package/src/client/canvas/IntentLayer.tsx +2 -2
  32. package/src/client/canvas/SelectionBar.tsx +2 -2
  33. package/src/client/canvas/SessionPanel.tsx +6 -4
  34. package/src/client/canvas/SessionReceipt.tsx +2 -1
  35. package/src/client/canvas/SnapshotPanel.tsx +3 -2
  36. package/src/client/canvas/ToolRail.tsx +2 -1
  37. package/src/client/canvas/TopBar.tsx +2 -2
  38. package/src/client/icons.tsx +118 -1
  39. package/src/client/nodes/BoardNode.tsx +2 -1
  40. package/src/client/nodes/ExtAppFrame.tsx +3 -1
  41. package/src/client/nodes/FileNode.tsx +4 -78
  42. package/src/client/nodes/ImageNode.tsx +2 -1
  43. package/src/client/nodes/MarkdownNode.tsx +2 -1
  44. package/src/client/nodes/PromptNode.tsx +3 -1
  45. package/src/client/theme/global.css +21 -5
  46. package/src/mcp/server.ts +2 -2
  47. package/src/server/agent-presence.ts +17 -11
  48. package/src/server/board-export.ts +97 -32
  49. package/src/server/index.ts +9 -7
  50. package/src/server/operations/composites.ts +3 -2
  51. package/src/server/operations/ops/ax-presence.ts +7 -3
  52. package/src/server/operations/ops/boards.ts +12 -5
  53. package/src/server/operations/registry.ts +4 -13
  54. package/src/server/server.ts +10 -3
  55. package/src/shared/delimited-text.ts +75 -0
@@ -251,7 +251,19 @@ async function resolvePmxServer(ctxOrInput, options = {}) {
251
251
 
252
252
  async function resolvePanelTarget(ctx, options = {}) {
253
253
  const selected = panelTargets.get(ctx?.instanceId);
254
- if (!selected) return await resolvePmxServer(ctx, options);
254
+ if (!selected) {
255
+ const panel = panelServers.get(ctx?.instanceId);
256
+ if (!panel) return await resolvePmxServer(ctx, options);
257
+ // A fallback panel still owns its original target. Action inputs may
258
+ // contain neither that URL nor its workspace, so never rediscover from them.
259
+ const resolved = await resolvePmxServer({
260
+ input: { ...panel.entry.input, ...(options.autoStart === true ? { autoStart: true } : {}) },
261
+ session: { workingDirectory: panel.entry.workspaceRoot },
262
+ }, options);
263
+ panel.entry.pmx = resolved;
264
+ rememberPanelTarget(ctx, resolved);
265
+ return resolved;
266
+ }
255
267
 
256
268
  const probe = await probeServer(selected.baseUrl, selected.workspaceRoot, {
257
269
  timeoutMs: options.timeoutMs ?? 2_000,
@@ -280,15 +292,9 @@ function escapeHtml(value) {
280
292
  .replaceAll('"', """);
281
293
  }
282
294
 
283
- function renderShell(instanceId, entry) {
284
- const status = entry.pmx?.ok ? "Connected" : "Not connected";
285
- // theme=light is a client-local session default matching the Copilot app's
286
- // light chrome — it never changes the server-global theme other clients
287
- // see, and the in-panel theme picker still overrides it.
288
- const frameSrc = entry.pmx?.baseUrl ? `${entry.pmx.baseUrl}/workbench?theme=light` : "about:blank";
289
- const error = entry.pmx?.error ? `<p class="error">${escapeHtml(entry.pmx.error)}</p>` : "";
295
+ function renderShell(entry) {
290
296
  return `<!doctype html>
291
- <html>
297
+ <html lang="en">
292
298
  <head>
293
299
  <meta charset="utf-8" />
294
300
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -301,70 +307,83 @@ function renderShell(instanceId, entry) {
301
307
  font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
302
308
  }
303
309
  .shell {
304
- display: grid;
305
- grid-template-rows: auto 1fr;
306
- height: 100vh;
310
+ max-width: 560px;
311
+ margin: 12vh auto;
312
+ padding: 24px;
307
313
  }
308
- header {
314
+ .actions {
309
315
  display: flex;
310
- align-items: center;
316
+ flex-wrap: wrap;
311
317
  gap: 12px;
312
- padding: 10px 12px;
313
- border-bottom: 1px solid var(--border-color-default, #30363d);
314
- background: var(--background-color-muted, rgba(22, 27, 34, 0.95));
318
+ margin-top: 24px;
315
319
  }
316
- strong { font-weight: var(--font-weight-semibold, 600); }
317
- .status {
320
+ p { line-height: 1.6; overflow-wrap: anywhere; }
321
+ .workspace {
318
322
  color: var(--text-color-muted, #8b949e);
319
- font-size: var(--text-body-small, 12px);
323
+ font-size: 13px;
320
324
  }
321
325
  .error {
322
- margin: 0;
323
326
  color: var(--true-color-red, #ff7b72);
324
- font-size: var(--text-body-small, 12px);
327
+ min-height: 24px;
325
328
  }
326
329
  button {
327
- border: 1px solid var(--border-color-default, #30363d);
330
+ border: 1px solid var(--border-color-default, #6e7681);
328
331
  border-radius: 8px;
329
- background: var(--background-color-default, #0d1117);
332
+ background: var(--background-color-muted, #21262d);
330
333
  color: inherit;
334
+ font: inherit;
331
335
  cursor: pointer;
332
- padding: 6px 10px;
336
+ min-height: 40px;
337
+ padding: 8px 16px;
333
338
  }
334
- button:hover {
339
+ button:hover, button:focus-visible {
335
340
  border-color: var(--color-focus-outline, #2f81f7);
336
341
  }
337
- iframe {
338
- width: 100%;
339
- height: 100%;
340
- border: 0;
341
- background: #0d1117;
342
- }
343
- .spacer { flex: 1; }
342
+ button:disabled { opacity: 0.6; cursor: wait; }
344
343
  </style>
345
344
  </head>
346
345
  <body>
347
- <div class="shell">
348
- <header>
349
- <strong>PMX Canvas</strong>
350
- <span class="status">${escapeHtml(status)}${entry.pmx?.baseUrl ? ` · ${escapeHtml(entry.pmx.baseUrl)}` : ""}</span>
351
- ${error}
352
- <span class="spacer"></span>
353
- <button type="button" onclick="refreshContext()">Refresh AX</button>
354
- <button type="button" onclick="startServer()">Start server</button>
355
- </header>
356
- <iframe title="PMX Canvas workbench" src="${escapeHtml(frameSrc)}"></iframe>
357
- </div>
346
+ <main class="shell">
347
+ <h1>Connect to PMX Canvas</h1>
348
+ <p>The workbench is not connected yet. Home and recent boards will appear when the server is available.</p>
349
+ <p class="workspace">Workspace: ${escapeHtml(entry.workspaceRoot)}</p>
350
+ <p class="workspace">Server: ${escapeHtml(candidateBaseUrls(entry.input)[0].baseUrl)}</p>
351
+ <p id="connection-status" class="error" role="status">${escapeHtml(entry.pmx?.error ?? "Checking connection…")}</p>
352
+ <div class="actions">
353
+ <button type="button" onclick="connect(false)">Check connection</button>
354
+ <button type="button" onclick="connect(true)">Start server</button>
355
+ </div>
356
+ <p class="workspace">This page checks for the server automatically. Start server launches a local server for this workspace.</p>
357
+ </main>
358
358
  <script>
359
- async function startServer() {
360
- await fetch('/start', { method: 'POST' });
361
- window.location.reload();
362
- }
363
- async function refreshContext() {
364
- const response = await fetch('/context');
365
- const context = await response.json();
366
- window.parent.postMessage({ type: 'pmx-canvas-ax-context', instanceId: ${JSON.stringify(instanceId)}, context }, '*');
359
+ let busy = false;
360
+ let timer;
361
+ async function connect(start) {
362
+ if (busy) return;
363
+ busy = true;
364
+ clearTimeout(timer);
365
+ const buttons = document.querySelectorAll('button');
366
+ buttons.forEach(button => button.disabled = true);
367
+ const status = document.getElementById('connection-status');
368
+ status.textContent = start ? 'Starting server…' : 'Checking connection…';
369
+ try {
370
+ const response = await fetch(start ? '/start' : '/status', { method: start ? 'POST' : 'GET', cache: 'no-store' });
371
+ const target = await response.json();
372
+ if (response.ok && target.ok && target.baseUrl) {
373
+ // Load the real workbench at its own origin, including its token,
374
+ // Home, boards, and relative API paths. The shell is not a proxy.
375
+ window.location.replace(target.baseUrl + '/workbench?theme=light');
376
+ return;
377
+ }
378
+ status.textContent = target.error || 'No matching PMX Canvas server is running.';
379
+ } catch (error) {
380
+ status.textContent = 'Connection check failed. Reopen the canvas if the adapter has stopped.';
381
+ }
382
+ busy = false;
383
+ buttons.forEach(button => button.disabled = false);
384
+ timer = setTimeout(() => connect(false), 2000);
367
385
  }
386
+ connect(false);
368
387
  </script>
369
388
  </body>
370
389
  </html>`;
@@ -378,7 +397,7 @@ async function readRequestJson(req) {
378
397
  }
379
398
 
380
399
  async function verifiedEntryTarget(entry) {
381
- if (!entry.pmx?.baseUrl) return { ok: false, baseUrl: null, error: "PMX Canvas server is unavailable." };
400
+ if (!entry.pmx?.ok || !entry.pmx.baseUrl) return { ok: false, baseUrl: null, error: "PMX Canvas server is unavailable." };
382
401
  const probe = await probeServer(entry.pmx.baseUrl, entry.pmx.health.workspace, {
383
402
  timeoutMs: 2_000,
384
403
  });
@@ -401,18 +420,17 @@ async function startPanelServer(instanceId, ctx, pmx) {
401
420
  try {
402
421
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
403
422
  if (req.method === "GET" && url.pathname === "/") {
404
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
405
- res.end(renderShell(instanceId, entry));
423
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
424
+ res.end(renderShell(entry));
406
425
  return;
407
426
  }
408
427
  if (req.method === "POST" && url.pathname === "/start") {
409
- entry.pmx = await resolvePmxServer({ input: entry.input, session: { workingDirectory: entry.workspaceRoot } });
410
- jsonResponse(res, 200, entry.pmx);
428
+ const latest = await resolvePanelTarget({ instanceId }, { autoStart: true });
429
+ jsonResponse(res, 200, latest);
411
430
  return;
412
431
  }
413
432
  if (req.method === "GET" && url.pathname === "/status") {
414
- const latest = await resolvePmxServer({ input: entry.input, session: { workingDirectory: entry.workspaceRoot } }, { autoStart: false });
415
- entry.pmx = latest.ok ? latest : entry.pmx;
433
+ const latest = await resolvePanelTarget({ instanceId }, { autoStart: false });
416
434
  jsonResponse(res, 200, latest);
417
435
  return;
418
436
  }
@@ -545,7 +563,7 @@ async function recordInjectedContext(baseUrl, context, injected) {
545
563
  async function getAxStatus(ctx) {
546
564
  const resolved = await resolvePanelTarget(ctx, { autoStart: false });
547
565
  if (!resolved.ok || !resolved.baseUrl) return { ok: false, server: resolved };
548
- const state = await fetchJson(resolved.baseUrl, "/api/canvas/ax", { timeoutMs: 2_000 });
566
+ const state = await fetchJson(resolved.baseUrl, "/api/canvas/ax?includeContext=false", { timeoutMs: 2_000 });
549
567
  return { ok: true, server: resolved, ax: state };
550
568
  }
551
569
 
@@ -718,7 +736,7 @@ const pmxCanvas = createCanvas({
718
736
  actions: [
719
737
  {
720
738
  name: "status",
721
- description: "Return PMX Canvas server and AX state status for this workspace.",
739
+ description: "Return PMX Canvas server and AX state without serialized node context. Use get_ax_context for full pinned/focused context.",
722
740
  handler: async (ctx) => await getAxStatus(ctx),
723
741
  },
724
742
  {
package/CHANGELOG.md CHANGED
@@ -3,7 +3,22 @@
3
3
  All notable changes to `pmx-canvas` are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
- ## [Unreleased]
6
+ ## [0.7.2] - 2026-09-30
7
+
8
+ ### Added
9
+
10
+ - Agents can explicitly open a board before writing through MCP, CLI, HTTP, and SDK; board deletion remains human-only.
11
+
12
+ ### Fixed
13
+
14
+ - Main-agent transport attaches reuse the attached host session, avoiding duplicate Copilot agents while keeping identified subagents separate.
15
+ - HTML exports have clearer Markdown typography and tables, readable CSV/TSV previews, complete status and context text, and scrollable cards with keyboard-accessible reading views.
16
+ - Original document attachments now avoid existing cards and each other without moving existing content.
17
+ - Backup and restore commands reject unknown flags instead of silently ignoring them.
18
+ - Amp setup instructions distinguish native MCP configuration from the browser portal.
19
+ - Copilot's fallback panel reconnects to the full workbench and keeps adapter actions on the recovered server.
20
+ - Copilot status omits serialized node context; full context remains available through `get_ax_context`.
21
+ - Attachment, board-menu, card, and panel controls use consistently sized SVG icons instead of small font symbols.
7
22
 
8
23
  ## [0.7.1] - 2026-09-30
9
24
 
package/Readme.md CHANGED
@@ -44,9 +44,11 @@ communication — proximity means relatedness, pinning means *focus here*.
44
44
  The database holds many named boards. **Home is the library view, not a board**:
45
45
  it lists recent boards and nested category paths (up to eight 60-character
46
46
  segments). Empty folders are not independent objects; they exist only while a
47
- board is filed beneath them. Only a human opens/switches or deletes a board.
48
- Agents may list, read, create, rename, categorize, search, and create an inactive
49
- board from selected reusable cards without disturbing the board in view. Home
47
+ board is filed beneath them. Agents can explicitly open/switch boards; deletion
48
+ remains human-only. Creating a board or a copy from reusable cards leaves it
49
+ inactive. Agents open its returned ID before writing; this also switches the
50
+ shared workbench the human sees. Agents may list, read, rename, categorize, and
51
+ search boards without switching. Home
50
52
  provides a copy-preview dialog; an open board's command palette offers a
51
53
  board-link picker. Neither depends on native browser prompts.
52
54
 
@@ -387,9 +389,32 @@ the full workflow and live-test checklist.
387
389
 
388
390
  ### Use inside AMPCode orbs and the Amp portal
389
391
 
390
- Amp needs no adapter either. Run `pmx-canvas` as an orb service (the orb sets
392
+ The portal and the agent's MCP connection are separate. For native Canvas tools,
393
+ merge this into the workspace's `.amp/settings.json` (not `.mcp.json`):
394
+
395
+ ```json
396
+ {
397
+ "amp.mcpServers": {
398
+ "canvas": {
399
+ "command": "bunx",
400
+ "args": ["pmx-canvas@0.7.2", "--mcp"]
401
+ }
402
+ }
403
+ }
404
+ ```
405
+
406
+ In the PMX product checkout, use `"command": "bun"` and
407
+ `"args": ["run", "src/mcp/server.ts"]` to test the checked-out source instead.
408
+ Reload MCP connections and complete any host trust prompt. Confirm Canvas tools
409
+ are discoverable and the target `/health.workspace` and version match before
410
+ mutating a board. If discovery is empty, inspect Amp's MCP connection status;
411
+ an open portal or a successful standalone MCP handshake does not prove a native
412
+ connection, skill activation, or steering delivery. See
413
+ [Amp MCP configuration](https://ampcode.com/docs/customize/mcp).
414
+
415
+ For the visible workbench, run `pmx-canvas` as an orb service (the orb sets
391
416
  `AMP_ORB` in the environment automatically) and open the workbench through the
392
- thread's portal URL. The zero-config recipe for the repo the orb runs in:
417
+ thread's portal URL. A consumer-workspace service recipe:
393
418
 
394
419
  ```yaml
395
420
  # .amp/services.yaml — supervised portal service
@@ -403,7 +428,7 @@ services:
403
428
  ```bash
404
429
  # .agents/setup — install the CLI (pin the exact version: a fresh orb running
405
430
  # @latest can silently pick up a newer release than the one you validated)
406
- npm install -g pmx-canvas@0.7.1
431
+ npm install -g pmx-canvas@0.7.2
407
432
  ```
408
433
 
409
434
  The server binds the portal-assigned `$PORT` automatically (gated on the
@@ -760,6 +760,11 @@ body,
760
760
  border-radius: calc(3px * var(--node-chrome-scale, 1));
761
761
  }
762
762
 
763
+ .canvas-node .node-controls button svg {
764
+ width: 1em;
765
+ height: 1em;
766
+ }
767
+
763
768
  .canvas-node .node-controls button:hover {
764
769
  background: var(--c-surface-hover);
765
770
  color: var(--c-text);
@@ -1466,6 +1471,10 @@ body,
1466
1471
  animation: presence-pulse 1.4s ease-in-out infinite;
1467
1472
  }
1468
1473
  .session-collapse {
1474
+ display: inline-flex;
1475
+ align-items: center;
1476
+ justify-content: center;
1477
+ padding: 0;
1469
1478
  width: 26px;
1470
1479
  height: 26px;
1471
1480
  border: 1px solid transparent;
@@ -1682,8 +1691,9 @@ body,
1682
1691
  }
1683
1692
  }
1684
1693
  .session-section-chevron {
1694
+ display: inline-flex;
1695
+ flex-shrink: 0;
1685
1696
  color: var(--c-muted);
1686
- font-size: 13px;
1687
1697
  transition: transform 0.15s ease;
1688
1698
  }
1689
1699
  .session-section-chevron.is-open {
@@ -1966,8 +1976,13 @@ body,
1966
1976
  white-space: nowrap;
1967
1977
  }
1968
1978
  .command-bar-chip-unpin {
1969
- width: 14px;
1970
- height: 14px;
1979
+ display: inline-flex;
1980
+ align-items: center;
1981
+ justify-content: center;
1982
+ flex-shrink: 0;
1983
+ padding: 0;
1984
+ width: 20px;
1985
+ height: 20px;
1971
1986
  border: none;
1972
1987
  border-radius: 50%;
1973
1988
  background: none;
@@ -6896,7 +6911,7 @@ html.is-node-dragging .intent-line-layer {
6896
6911
  align-items: center;
6897
6912
  gap: 6px;
6898
6913
  max-width: 280px;
6899
- padding: 4px 8px;
6914
+ padding: 6px 8px;
6900
6915
  margin-left: -8px;
6901
6916
  background: none;
6902
6917
  border: 1px solid transparent;
@@ -6914,7 +6929,8 @@ html.is-node-dragging .intent-line-layer {
6914
6929
  white-space: nowrap;
6915
6930
  }
6916
6931
  .board-switcher-caret {
6917
- font-size: 10px;
6932
+ display: inline-flex;
6933
+ flex-shrink: 0;
6918
6934
  color: var(--c-text-soft);
6919
6935
  }
6920
6936
  .board-switcher-menu {