pmx-canvas 0.7.0 → 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 (72) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +149 -70
  2. package/CHANGELOG.md +32 -1
  3. package/Readme.md +40 -13
  4. package/dist/canvas/global.css +94 -15
  5. package/dist/canvas/index.js +76 -76
  6. package/dist/types/cli/daemon.d.ts +5 -5
  7. package/dist/types/cli/shared.d.ts +3 -1
  8. package/dist/types/client/canvas/ExternalWriters.d.ts +3 -3
  9. package/dist/types/client/icons.d.ts +15 -0
  10. package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -1
  11. package/dist/types/client/nodes/FileNode.d.ts +0 -12
  12. package/dist/types/shared/delimited-text.d.ts +14 -0
  13. package/docs/cli.md +14 -2
  14. package/docs/http-api.md +12 -8
  15. package/docs/mcp.md +8 -1
  16. package/docs/sdk.md +2 -2
  17. package/package.json +1 -1
  18. package/skills/pmx-canvas/SKILL.md +35 -14
  19. package/skills/pmx-canvas/evals/evals.json +171 -0
  20. package/skills/pmx-canvas/references/full-reference.md +6 -1
  21. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +17 -3
  22. package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
  23. package/skills/pmx-canvas/references/tours-and-recording.md +69 -0
  24. package/skills/pmx-canvas-testing/SKILL.md +7 -2
  25. package/src/cli/commands/apps.ts +1 -1
  26. package/src/cli/commands/backup.ts +15 -4
  27. package/src/cli/commands/boards.ts +16 -3
  28. package/src/cli/commands/export.ts +9 -2
  29. package/src/cli/daemon.ts +37 -7
  30. package/src/cli/shared.ts +11 -2
  31. package/src/client/App.tsx +0 -1
  32. package/src/client/canvas/AttentionHistory.tsx +2 -1
  33. package/src/client/canvas/AttentionToast.tsx +5 -3
  34. package/src/client/canvas/BoardSwitcher.tsx +3 -3
  35. package/src/client/canvas/CanvasNode.tsx +17 -9
  36. package/src/client/canvas/CommandBar.tsx +2 -2
  37. package/src/client/canvas/ContextMenu.tsx +0 -1
  38. package/src/client/canvas/ContextPinBar.tsx +3 -1
  39. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -3
  40. package/src/client/canvas/ExportDialog.tsx +3 -1
  41. package/src/client/canvas/ExternalWriters.tsx +4 -3
  42. package/src/client/canvas/FeedbackDialog.tsx +2 -1
  43. package/src/client/canvas/IntentLayer.tsx +2 -2
  44. package/src/client/canvas/Presentation.tsx +39 -9
  45. package/src/client/canvas/SelectionBar.tsx +2 -4
  46. package/src/client/canvas/SessionPanel.tsx +6 -4
  47. package/src/client/canvas/SessionReceipt.tsx +2 -1
  48. package/src/client/canvas/SnapshotPanel.tsx +3 -2
  49. package/src/client/canvas/ToolRail.tsx +2 -1
  50. package/src/client/canvas/TopBar.tsx +2 -2
  51. package/src/client/icons.tsx +118 -1
  52. package/src/client/nodes/AttachmentNode.tsx +27 -30
  53. package/src/client/nodes/BoardNode.tsx +2 -1
  54. package/src/client/nodes/ExtAppFrame.tsx +3 -1
  55. package/src/client/nodes/FileNode.tsx +4 -78
  56. package/src/client/nodes/ImageNode.tsx +2 -1
  57. package/src/client/nodes/MarkdownNode.tsx +2 -1
  58. package/src/client/nodes/PromptNode.tsx +3 -1
  59. package/src/client/state/attention-bridge.ts +3 -1
  60. package/src/client/state/sse-bridge.ts +3 -1
  61. package/src/client/theme/global.css +94 -15
  62. package/src/mcp/server.ts +2 -2
  63. package/src/server/agent-presence.ts +17 -11
  64. package/src/server/board-export.ts +97 -32
  65. package/src/server/index.ts +9 -7
  66. package/src/server/operations/composites.ts +3 -2
  67. package/src/server/operations/ops/ax-presence.ts +7 -3
  68. package/src/server/operations/ops/boards.ts +12 -5
  69. package/src/server/operations/ops/imports.ts +19 -3
  70. package/src/server/operations/registry.ts +4 -13
  71. package/src/server/server.ts +10 -3
  72. package/src/shared/delimited-text.ts +75 -0
@@ -23,6 +23,10 @@ let managedWorkspaceRoot = null;
23
23
  let managedPort = null;
24
24
  const managedLogs = [];
25
25
  const panelServers = new Map();
26
+ // Canvas actions are invoked separately from `open`, and their context can
27
+ // omit the canvas input. Keep the server selected for each visible instance so
28
+ // an action cannot fall back to the process/default port behind the panel.
29
+ const panelTargets = new Map();
26
30
 
27
31
  function normalizeBaseUrl(value) {
28
32
  if (typeof value !== "string" || value.trim() === "") return null;
@@ -227,7 +231,7 @@ async function resolvePmxServer(ctxOrInput, options = {}) {
227
231
  const allowWorkspaceMismatch = input?.allowWorkspaceMismatch === true;
228
232
  for (const candidate of candidateBaseUrls(input)) {
229
233
  const probe = await probeServer(candidate.baseUrl, workspaceRoot, {
230
- allowWorkspaceMismatch: candidate.explicit || allowWorkspaceMismatch,
234
+ allowWorkspaceMismatch,
231
235
  });
232
236
  if (probe.ok) return probe;
233
237
  }
@@ -245,6 +249,41 @@ async function resolvePmxServer(ctxOrInput, options = {}) {
245
249
  return await startManagedServer(workspaceRoot, input);
246
250
  }
247
251
 
252
+ async function resolvePanelTarget(ctx, options = {}) {
253
+ const selected = panelTargets.get(ctx?.instanceId);
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
+ }
267
+
268
+ const probe = await probeServer(selected.baseUrl, selected.workspaceRoot, {
269
+ timeoutMs: options.timeoutMs ?? 2_000,
270
+ });
271
+ if (probe.ok) return probe;
272
+ return {
273
+ ...probe,
274
+ baseUrl: null,
275
+ error: `The displayed PMX Canvas target is no longer valid: ${probe.error ?? "health check failed"}. Action withheld.`,
276
+ };
277
+ }
278
+
279
+ function rememberPanelTarget(ctx, pmx) {
280
+ if (!ctx?.instanceId || !pmx?.ok || !pmx.baseUrl) return;
281
+ panelTargets.set(ctx.instanceId, {
282
+ baseUrl: pmx.baseUrl,
283
+ workspaceRoot: pmx.health.workspace,
284
+ });
285
+ }
286
+
248
287
  function escapeHtml(value) {
249
288
  return String(value)
250
289
  .replaceAll("&", "&")
@@ -253,15 +292,9 @@ function escapeHtml(value) {
253
292
  .replaceAll('"', """);
254
293
  }
255
294
 
256
- function renderShell(instanceId, entry) {
257
- const status = entry.pmx?.ok ? "Connected" : "Not connected";
258
- // theme=light is a client-local session default matching the Copilot app's
259
- // light chrome — it never changes the server-global theme other clients
260
- // see, and the in-panel theme picker still overrides it.
261
- const frameSrc = entry.pmx?.baseUrl ? `${entry.pmx.baseUrl}/workbench?theme=light` : "about:blank";
262
- const error = entry.pmx?.error ? `<p class="error">${escapeHtml(entry.pmx.error)}</p>` : "";
295
+ function renderShell(entry) {
263
296
  return `<!doctype html>
264
- <html>
297
+ <html lang="en">
265
298
  <head>
266
299
  <meta charset="utf-8" />
267
300
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -274,70 +307,83 @@ function renderShell(instanceId, entry) {
274
307
  font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
275
308
  }
276
309
  .shell {
277
- display: grid;
278
- grid-template-rows: auto 1fr;
279
- height: 100vh;
310
+ max-width: 560px;
311
+ margin: 12vh auto;
312
+ padding: 24px;
280
313
  }
281
- header {
314
+ .actions {
282
315
  display: flex;
283
- align-items: center;
316
+ flex-wrap: wrap;
284
317
  gap: 12px;
285
- padding: 10px 12px;
286
- border-bottom: 1px solid var(--border-color-default, #30363d);
287
- background: var(--background-color-muted, rgba(22, 27, 34, 0.95));
318
+ margin-top: 24px;
288
319
  }
289
- strong { font-weight: var(--font-weight-semibold, 600); }
290
- .status {
320
+ p { line-height: 1.6; overflow-wrap: anywhere; }
321
+ .workspace {
291
322
  color: var(--text-color-muted, #8b949e);
292
- font-size: var(--text-body-small, 12px);
323
+ font-size: 13px;
293
324
  }
294
325
  .error {
295
- margin: 0;
296
326
  color: var(--true-color-red, #ff7b72);
297
- font-size: var(--text-body-small, 12px);
327
+ min-height: 24px;
298
328
  }
299
329
  button {
300
- border: 1px solid var(--border-color-default, #30363d);
330
+ border: 1px solid var(--border-color-default, #6e7681);
301
331
  border-radius: 8px;
302
- background: var(--background-color-default, #0d1117);
332
+ background: var(--background-color-muted, #21262d);
303
333
  color: inherit;
334
+ font: inherit;
304
335
  cursor: pointer;
305
- padding: 6px 10px;
336
+ min-height: 40px;
337
+ padding: 8px 16px;
306
338
  }
307
- button:hover {
339
+ button:hover, button:focus-visible {
308
340
  border-color: var(--color-focus-outline, #2f81f7);
309
341
  }
310
- iframe {
311
- width: 100%;
312
- height: 100%;
313
- border: 0;
314
- background: #0d1117;
315
- }
316
- .spacer { flex: 1; }
342
+ button:disabled { opacity: 0.6; cursor: wait; }
317
343
  </style>
318
344
  </head>
319
345
  <body>
320
- <div class="shell">
321
- <header>
322
- <strong>PMX Canvas</strong>
323
- <span class="status">${escapeHtml(status)}${entry.pmx?.baseUrl ? ` · ${escapeHtml(entry.pmx.baseUrl)}` : ""}</span>
324
- ${error}
325
- <span class="spacer"></span>
326
- <button type="button" onclick="refreshContext()">Refresh AX</button>
327
- <button type="button" onclick="startServer()">Start server</button>
328
- </header>
329
- <iframe title="PMX Canvas workbench" src="${escapeHtml(frameSrc)}"></iframe>
330
- </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>
331
358
  <script>
332
- async function startServer() {
333
- await fetch('/start', { method: 'POST' });
334
- window.location.reload();
335
- }
336
- async function refreshContext() {
337
- const response = await fetch('/context');
338
- const context = await response.json();
339
- 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);
340
385
  }
386
+ connect(false);
341
387
  </script>
342
388
  </body>
343
389
  </html>`;
@@ -350,6 +396,19 @@ async function readRequestJson(req) {
350
396
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
351
397
  }
352
398
 
399
+ async function verifiedEntryTarget(entry) {
400
+ if (!entry.pmx?.ok || !entry.pmx.baseUrl) return { ok: false, baseUrl: null, error: "PMX Canvas server is unavailable." };
401
+ const probe = await probeServer(entry.pmx.baseUrl, entry.pmx.health.workspace, {
402
+ timeoutMs: 2_000,
403
+ });
404
+ if (probe.ok) return probe;
405
+ return {
406
+ ...probe,
407
+ baseUrl: null,
408
+ error: `The displayed PMX Canvas target is no longer valid: ${probe.error ?? "health check failed"}. Action withheld.`,
409
+ };
410
+ }
411
+
353
412
  function jsonResponse(res, statusCode, body) {
354
413
  res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
355
414
  res.end(JSON.stringify(body));
@@ -361,29 +420,38 @@ async function startPanelServer(instanceId, ctx, pmx) {
361
420
  try {
362
421
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
363
422
  if (req.method === "GET" && url.pathname === "/") {
364
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
365
- 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));
366
425
  return;
367
426
  }
368
427
  if (req.method === "POST" && url.pathname === "/start") {
369
- entry.pmx = await resolvePmxServer({ input: entry.input, session: { workingDirectory: entry.workspaceRoot } });
370
- jsonResponse(res, 200, entry.pmx);
428
+ const latest = await resolvePanelTarget({ instanceId }, { autoStart: true });
429
+ jsonResponse(res, 200, latest);
371
430
  return;
372
431
  }
373
432
  if (req.method === "GET" && url.pathname === "/status") {
374
- const latest = await resolvePmxServer({ input: entry.input, session: { workingDirectory: entry.workspaceRoot } }, { autoStart: false });
375
- entry.pmx = latest.ok ? latest : entry.pmx;
433
+ const latest = await resolvePanelTarget({ instanceId }, { autoStart: false });
376
434
  jsonResponse(res, 200, latest);
377
435
  return;
378
436
  }
379
437
  if (req.method === "GET" && url.pathname === "/context") {
380
- const context = await getAxContext(entry.pmx?.baseUrl, entry.workspaceRoot, entry.input);
438
+ const target = await verifiedEntryTarget(entry);
439
+ if (!target.ok || !target.baseUrl) {
440
+ jsonResponse(res, 409, target);
441
+ return;
442
+ }
443
+ const context = await getAxContext(target.baseUrl, entry.workspaceRoot, entry.input);
381
444
  jsonResponse(res, 200, context);
382
445
  return;
383
446
  }
384
447
  if (req.method === "POST" && url.pathname === "/focus") {
385
448
  const body = await readRequestJson(req);
386
- const result = await setAxFocus(entry.pmx?.baseUrl, entry.workspaceRoot, entry.input, body.nodeIds);
449
+ const target = await verifiedEntryTarget(entry);
450
+ if (!target.ok || !target.baseUrl) {
451
+ jsonResponse(res, 409, target);
452
+ return;
453
+ }
454
+ const result = await setAxFocus(target.baseUrl, entry.workspaceRoot, entry.input, body.nodeIds);
387
455
  jsonResponse(res, 200, result);
388
456
  return;
389
457
  }
@@ -396,8 +464,9 @@ async function startPanelServer(instanceId, ctx, pmx) {
396
464
  await copilotSession?.send({ prompt: body.prompt });
397
465
  // Explicit user instruction from the panel → mirror onto the AX
398
466
  // timeline as a steering message (fire-and-forget).
399
- if (entry.pmx?.baseUrl) {
400
- void fetchJson(entry.pmx.baseUrl, "/api/canvas/ax/steer", {
467
+ const target = await verifiedEntryTarget(entry);
468
+ if (target.ok && target.baseUrl) {
469
+ void fetchJson(target.baseUrl, "/api/canvas/ax/steer", {
401
470
  method: "POST",
402
471
  headers: { "Content-Type": "application/json" },
403
472
  body: JSON.stringify({ message: body.prompt, source: "copilot" }),
@@ -492,9 +561,9 @@ async function recordInjectedContext(baseUrl, context, injected) {
492
561
  }
493
562
 
494
563
  async function getAxStatus(ctx) {
495
- const resolved = await resolvePmxServer(ctx, { autoStart: false });
564
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
496
565
  if (!resolved.ok || !resolved.baseUrl) return { ok: false, server: resolved };
497
- 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 });
498
567
  return { ok: true, server: resolved, ax: state };
499
568
  }
500
569
 
@@ -517,7 +586,7 @@ async function setAxFocus(baseUrl, workspaceRoot, input = {}, nodeIds = []) {
517
586
  // Resolves the server first so the action returns an actionable error when the
518
587
  // canvas is unavailable, then maps the neutral primitive over plain HTTP.
519
588
  async function postAxRecord(ctx, path, payload) {
520
- const resolved = await resolvePmxServer(ctx, { autoStart: false });
589
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
521
590
  if (!resolved.ok || !resolved.baseUrl) {
522
591
  return { ok: false, error: resolved.error ?? "PMX Canvas server is unavailable." };
523
592
  }
@@ -625,7 +694,7 @@ function reportSteeringDeliveryError(error) {
625
694
  }
626
695
 
627
696
  async function getAxTimeline(ctx, limit) {
628
- const resolved = await resolvePmxServer(ctx, { autoStart: false });
697
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
629
698
  if (!resolved.ok || !resolved.baseUrl) return { ok: false, error: resolved.error };
630
699
  const query = typeof limit === "number" && limit > 0 ? `?limit=${limit}` : "";
631
700
  return await fetchJson(resolved.baseUrl, `/api/canvas/ax/timeline${query}`, { timeoutMs: 2_000 });
@@ -667,14 +736,14 @@ const pmxCanvas = createCanvas({
667
736
  actions: [
668
737
  {
669
738
  name: "status",
670
- 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.",
671
740
  handler: async (ctx) => await getAxStatus(ctx),
672
741
  },
673
742
  {
674
743
  name: "get_ax_context",
675
744
  description: "Return the current PMX Canvas AX pinned and focused context.",
676
745
  handler: async (ctx) => {
677
- const resolved = await resolvePmxServer(ctx, { autoStart: false });
746
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
678
747
  if (!resolved.ok || !resolved.baseUrl) return { ok: false, error: resolved.error };
679
748
  return await getAxContext(resolved.baseUrl, workspaceRootFrom(ctx), ctx.input ?? {});
680
749
  },
@@ -693,7 +762,13 @@ const pmxCanvas = createCanvas({
693
762
  required: ["nodeIds"],
694
763
  additionalProperties: false,
695
764
  },
696
- handler: async (ctx) => await setAxFocus(null, workspaceRootFrom(ctx), ctx.input ?? {}, ctx.input?.nodeIds),
765
+ handler: async (ctx) => {
766
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
767
+ if (!resolved.ok || !resolved.baseUrl) {
768
+ throw new CanvasError("pmx_unavailable", resolved.error ?? "PMX Canvas server is unavailable.");
769
+ }
770
+ return await setAxFocus(resolved.baseUrl, workspaceRootFrom(ctx), ctx.input ?? {}, ctx.input?.nodeIds);
771
+ },
697
772
  },
698
773
  {
699
774
  name: "send_instruction",
@@ -835,7 +910,7 @@ const pmxCanvas = createCanvas({
835
910
  additionalProperties: false,
836
911
  },
837
912
  handler: async (ctx) => {
838
- const resolved = await resolvePmxServer(ctx, { autoStart: false });
913
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
839
914
  if (!resolved.ok || !resolved.baseUrl) {
840
915
  return { ok: false, error: resolved.error ?? "PMX Canvas server is unavailable." };
841
916
  }
@@ -863,6 +938,7 @@ const pmxCanvas = createCanvas({
863
938
  },
864
939
  ],
865
940
  open: async (ctx) => {
941
+ panelTargets.delete(ctx.instanceId);
866
942
  let pmx;
867
943
  try {
868
944
  pmx = await resolvePmxServer(ctx);
@@ -877,6 +953,7 @@ const pmxCanvas = createCanvas({
877
953
  }
878
954
 
879
955
  if (pmx.ok && pmx.baseUrl) {
956
+ rememberPanelTarget(ctx, pmx);
880
957
  const fallbackPanel = panelServers.get(ctx.instanceId);
881
958
  if (fallbackPanel) {
882
959
  panelServers.delete(ctx.instanceId);
@@ -898,6 +975,7 @@ const pmxCanvas = createCanvas({
898
975
  panel.entry.workspaceRoot = workspaceRootFrom(ctx);
899
976
  panel.entry.input = ctx.input ?? {};
900
977
  }
978
+ rememberPanelTarget(ctx, pmx);
901
979
  return {
902
980
  title: "PMX Canvas",
903
981
  status: pmx.ok ? "Connected" : "Needs server",
@@ -905,6 +983,7 @@ const pmxCanvas = createCanvas({
905
983
  };
906
984
  },
907
985
  onClose: async (ctx) => {
986
+ panelTargets.delete(ctx.instanceId);
908
987
  const panel = panelServers.get(ctx.instanceId);
909
988
  if (!panel) return;
910
989
  panelServers.delete(ctx.instanceId);
package/CHANGELOG.md CHANGED
@@ -3,7 +3,38 @@
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.
22
+
23
+ ## [0.7.1] - 2026-09-30
24
+
25
+ ### Fixed
26
+
27
+ - Imported cards avoid existing content and multi-card imports form a group named after the source file.
28
+ - Asking an agent to import now includes processing consent in one click; review actions have clearer buttons and a separate sticky footer.
29
+ - Presentations support right-drag panning and a visible exit button; export Open and Download actions look like buttons.
30
+ - Board navigation no longer reports removed cards or carries old session receipts onto Home.
31
+ - Refused edits show their reason without a tooltip, keep the reason in Updates, and leave refused deletions visible.
32
+ - Copilot panel actions use the displayed server and refuse writes if its workspace changes.
33
+ - Inline batch JSON works with either spaced or equals-style flags.
34
+ - CLI exports support an independent `--include-derived-text` opt-in and reject unknown flags.
35
+ - Stopping from a different installation preserves live daemon PID files and reports the mismatch.
36
+ - Daemon stop verifies the process on Windows and refuses to stop a live process whose installation cannot be verified.
37
+ - Bundled skills include a retrievable tour reference, conditional browser-host checks, and eval scenarios for the new board and import workflows.
7
38
 
8
39
  ## [0.7.0] - 2026-09-29
9
40
 
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
 
@@ -83,12 +85,12 @@ the canvas is the union of its [built-in node types](docs/node-types.md) and
83
85
  file reads, web fetch, anything on its toolbelt.
84
86
 
85
87
  Dropping a PDF or Office document attaches the original (up to 20 MiB). It does
86
- not bundle a converter or automatically launch an agent. After you explicitly
87
- request agent-assisted import and consent to processing by the connected host's
88
- tools/model provider, the agent fetches the controlled bytes, returns Markdown
89
- sections with references and warnings, and you review before adding them to the
90
- board. Remote hosts may require manual transfer; OCR, charts, and host format
91
- support are not guaranteed.
88
+ not bundle a converter or automatically launch an agent. Clicking **Ask agent to
89
+ import** also authorizes processing by the connected host's tools/model provider;
90
+ there is no separate consent confirmation. The agent returns Markdown sections
91
+ with references and warnings for review. Multi-card imports are placed together
92
+ in a free area, in a group named after the source file. Remote hosts may require
93
+ manual transfer; OCR, charts, and host format support are not guaranteed.
92
94
 
93
95
  JSON-render panels support bound form state, nested repeats, watchers, and Card
94
96
  header/footer slots. Form edits remain viewer-local until explicitly submitted;
@@ -246,7 +248,9 @@ surfaces (the core never imports a host SDK).
246
248
  session override), wakes Copilot when the board sends a steer, injects
247
249
  pinned/focused AX context on prompt submission, and exposes actions for focus,
248
250
  work items, approval gates, review annotations, the AX timeline, and
249
- host-capability reporting. Install it into any repo with
251
+ host-capability reporting. Actions stay bound to the server shown by their
252
+ panel and are withheld if that server no longer matches the panel workspace.
253
+ Install it into any repo with
250
254
  `pmx-canvas copilot install-extension` (`--dry-run` to preview).
251
255
  - **Codex app** — native through the Codex in-app Browser (opened to
252
256
  `/workbench`) plus the PMX MCP server: agents read `canvas://ax-context` /
@@ -385,9 +389,32 @@ the full workflow and live-test checklist.
385
389
 
386
390
  ### Use inside AMPCode orbs and the Amp portal
387
391
 
388
- 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
389
416
  `AMP_ORB` in the environment automatically) and open the workbench through the
390
- 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:
391
418
 
392
419
  ```yaml
393
420
  # .amp/services.yaml — supervised portal service
@@ -401,7 +428,7 @@ services:
401
428
  ```bash
402
429
  # .agents/setup — install the CLI (pin the exact version: a fresh orb running
403
430
  # @latest can silently pick up a newer release than the one you validated)
404
- npm install -g pmx-canvas@0.7.0
431
+ npm install -g pmx-canvas@0.7.2
405
432
  ```
406
433
 
407
434
  The server binds the portal-assigned `$PORT` automatically (gated on the