pmx-canvas 0.7.0 → 0.7.1

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.
@@ -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,29 @@ 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) return await resolvePmxServer(ctx, options);
255
+
256
+ const probe = await probeServer(selected.baseUrl, selected.workspaceRoot, {
257
+ timeoutMs: options.timeoutMs ?? 2_000,
258
+ });
259
+ if (probe.ok) return probe;
260
+ return {
261
+ ...probe,
262
+ baseUrl: null,
263
+ error: `The displayed PMX Canvas target is no longer valid: ${probe.error ?? "health check failed"}. Action withheld.`,
264
+ };
265
+ }
266
+
267
+ function rememberPanelTarget(ctx, pmx) {
268
+ if (!ctx?.instanceId || !pmx?.ok || !pmx.baseUrl) return;
269
+ panelTargets.set(ctx.instanceId, {
270
+ baseUrl: pmx.baseUrl,
271
+ workspaceRoot: pmx.health.workspace,
272
+ });
273
+ }
274
+
248
275
  function escapeHtml(value) {
249
276
  return String(value)
250
277
  .replaceAll("&", "&")
@@ -350,6 +377,19 @@ async function readRequestJson(req) {
350
377
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
351
378
  }
352
379
 
380
+ async function verifiedEntryTarget(entry) {
381
+ if (!entry.pmx?.baseUrl) return { ok: false, baseUrl: null, error: "PMX Canvas server is unavailable." };
382
+ const probe = await probeServer(entry.pmx.baseUrl, entry.pmx.health.workspace, {
383
+ timeoutMs: 2_000,
384
+ });
385
+ if (probe.ok) return probe;
386
+ return {
387
+ ...probe,
388
+ baseUrl: null,
389
+ error: `The displayed PMX Canvas target is no longer valid: ${probe.error ?? "health check failed"}. Action withheld.`,
390
+ };
391
+ }
392
+
353
393
  function jsonResponse(res, statusCode, body) {
354
394
  res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
355
395
  res.end(JSON.stringify(body));
@@ -377,13 +417,23 @@ async function startPanelServer(instanceId, ctx, pmx) {
377
417
  return;
378
418
  }
379
419
  if (req.method === "GET" && url.pathname === "/context") {
380
- const context = await getAxContext(entry.pmx?.baseUrl, entry.workspaceRoot, entry.input);
420
+ const target = await verifiedEntryTarget(entry);
421
+ if (!target.ok || !target.baseUrl) {
422
+ jsonResponse(res, 409, target);
423
+ return;
424
+ }
425
+ const context = await getAxContext(target.baseUrl, entry.workspaceRoot, entry.input);
381
426
  jsonResponse(res, 200, context);
382
427
  return;
383
428
  }
384
429
  if (req.method === "POST" && url.pathname === "/focus") {
385
430
  const body = await readRequestJson(req);
386
- const result = await setAxFocus(entry.pmx?.baseUrl, entry.workspaceRoot, entry.input, body.nodeIds);
431
+ const target = await verifiedEntryTarget(entry);
432
+ if (!target.ok || !target.baseUrl) {
433
+ jsonResponse(res, 409, target);
434
+ return;
435
+ }
436
+ const result = await setAxFocus(target.baseUrl, entry.workspaceRoot, entry.input, body.nodeIds);
387
437
  jsonResponse(res, 200, result);
388
438
  return;
389
439
  }
@@ -396,8 +446,9 @@ async function startPanelServer(instanceId, ctx, pmx) {
396
446
  await copilotSession?.send({ prompt: body.prompt });
397
447
  // Explicit user instruction from the panel → mirror onto the AX
398
448
  // timeline as a steering message (fire-and-forget).
399
- if (entry.pmx?.baseUrl) {
400
- void fetchJson(entry.pmx.baseUrl, "/api/canvas/ax/steer", {
449
+ const target = await verifiedEntryTarget(entry);
450
+ if (target.ok && target.baseUrl) {
451
+ void fetchJson(target.baseUrl, "/api/canvas/ax/steer", {
401
452
  method: "POST",
402
453
  headers: { "Content-Type": "application/json" },
403
454
  body: JSON.stringify({ message: body.prompt, source: "copilot" }),
@@ -492,7 +543,7 @@ async function recordInjectedContext(baseUrl, context, injected) {
492
543
  }
493
544
 
494
545
  async function getAxStatus(ctx) {
495
- const resolved = await resolvePmxServer(ctx, { autoStart: false });
546
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
496
547
  if (!resolved.ok || !resolved.baseUrl) return { ok: false, server: resolved };
497
548
  const state = await fetchJson(resolved.baseUrl, "/api/canvas/ax", { timeoutMs: 2_000 });
498
549
  return { ok: true, server: resolved, ax: state };
@@ -517,7 +568,7 @@ async function setAxFocus(baseUrl, workspaceRoot, input = {}, nodeIds = []) {
517
568
  // Resolves the server first so the action returns an actionable error when the
518
569
  // canvas is unavailable, then maps the neutral primitive over plain HTTP.
519
570
  async function postAxRecord(ctx, path, payload) {
520
- const resolved = await resolvePmxServer(ctx, { autoStart: false });
571
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
521
572
  if (!resolved.ok || !resolved.baseUrl) {
522
573
  return { ok: false, error: resolved.error ?? "PMX Canvas server is unavailable." };
523
574
  }
@@ -625,7 +676,7 @@ function reportSteeringDeliveryError(error) {
625
676
  }
626
677
 
627
678
  async function getAxTimeline(ctx, limit) {
628
- const resolved = await resolvePmxServer(ctx, { autoStart: false });
679
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
629
680
  if (!resolved.ok || !resolved.baseUrl) return { ok: false, error: resolved.error };
630
681
  const query = typeof limit === "number" && limit > 0 ? `?limit=${limit}` : "";
631
682
  return await fetchJson(resolved.baseUrl, `/api/canvas/ax/timeline${query}`, { timeoutMs: 2_000 });
@@ -674,7 +725,7 @@ const pmxCanvas = createCanvas({
674
725
  name: "get_ax_context",
675
726
  description: "Return the current PMX Canvas AX pinned and focused context.",
676
727
  handler: async (ctx) => {
677
- const resolved = await resolvePmxServer(ctx, { autoStart: false });
728
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
678
729
  if (!resolved.ok || !resolved.baseUrl) return { ok: false, error: resolved.error };
679
730
  return await getAxContext(resolved.baseUrl, workspaceRootFrom(ctx), ctx.input ?? {});
680
731
  },
@@ -693,7 +744,13 @@ const pmxCanvas = createCanvas({
693
744
  required: ["nodeIds"],
694
745
  additionalProperties: false,
695
746
  },
696
- handler: async (ctx) => await setAxFocus(null, workspaceRootFrom(ctx), ctx.input ?? {}, ctx.input?.nodeIds),
747
+ handler: async (ctx) => {
748
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
749
+ if (!resolved.ok || !resolved.baseUrl) {
750
+ throw new CanvasError("pmx_unavailable", resolved.error ?? "PMX Canvas server is unavailable.");
751
+ }
752
+ return await setAxFocus(resolved.baseUrl, workspaceRootFrom(ctx), ctx.input ?? {}, ctx.input?.nodeIds);
753
+ },
697
754
  },
698
755
  {
699
756
  name: "send_instruction",
@@ -835,7 +892,7 @@ const pmxCanvas = createCanvas({
835
892
  additionalProperties: false,
836
893
  },
837
894
  handler: async (ctx) => {
838
- const resolved = await resolvePmxServer(ctx, { autoStart: false });
895
+ const resolved = await resolvePanelTarget(ctx, { autoStart: false });
839
896
  if (!resolved.ok || !resolved.baseUrl) {
840
897
  return { ok: false, error: resolved.error ?? "PMX Canvas server is unavailable." };
841
898
  }
@@ -863,6 +920,7 @@ const pmxCanvas = createCanvas({
863
920
  },
864
921
  ],
865
922
  open: async (ctx) => {
923
+ panelTargets.delete(ctx.instanceId);
866
924
  let pmx;
867
925
  try {
868
926
  pmx = await resolvePmxServer(ctx);
@@ -877,6 +935,7 @@ const pmxCanvas = createCanvas({
877
935
  }
878
936
 
879
937
  if (pmx.ok && pmx.baseUrl) {
938
+ rememberPanelTarget(ctx, pmx);
880
939
  const fallbackPanel = panelServers.get(ctx.instanceId);
881
940
  if (fallbackPanel) {
882
941
  panelServers.delete(ctx.instanceId);
@@ -898,6 +957,7 @@ const pmxCanvas = createCanvas({
898
957
  panel.entry.workspaceRoot = workspaceRootFrom(ctx);
899
958
  panel.entry.input = ctx.input ?? {};
900
959
  }
960
+ rememberPanelTarget(ctx, pmx);
901
961
  return {
902
962
  title: "PMX Canvas",
903
963
  status: pmx.ok ? "Connected" : "Needs server",
@@ -905,6 +965,7 @@ const pmxCanvas = createCanvas({
905
965
  };
906
966
  },
907
967
  onClose: async (ctx) => {
968
+ panelTargets.delete(ctx.instanceId);
908
969
  const panel = panelServers.get(ctx.instanceId);
909
970
  if (!panel) return;
910
971
  panelServers.delete(ctx.instanceId);
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to `pmx-canvas` are documented here. This project follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.7.1] - 2026-09-30
9
+
10
+ ### Fixed
11
+
12
+ - Imported cards avoid existing content and multi-card imports form a group named after the source file.
13
+ - Asking an agent to import now includes processing consent in one click; review actions have clearer buttons and a separate sticky footer.
14
+ - Presentations support right-drag panning and a visible exit button; export Open and Download actions look like buttons.
15
+ - Board navigation no longer reports removed cards or carries old session receipts onto Home.
16
+ - Refused edits show their reason without a tooltip, keep the reason in Updates, and leave refused deletions visible.
17
+ - Copilot panel actions use the displayed server and refuse writes if its workspace changes.
18
+ - Inline batch JSON works with either spaced or equals-style flags.
19
+ - CLI exports support an independent `--include-derived-text` opt-in and reject unknown flags.
20
+ - Stopping from a different installation preserves live daemon PID files and reports the mismatch.
21
+ - Daemon stop verifies the process on Windows and refuses to stop a live process whose installation cannot be verified.
22
+ - Bundled skills include a retrievable tour reference, conditional browser-host checks, and eval scenarios for the new board and import workflows.
23
+
8
24
  ## [0.7.0] - 2026-09-29
9
25
 
10
26
  ### Highlights
package/Readme.md CHANGED
@@ -83,12 +83,12 @@ the canvas is the union of its [built-in node types](docs/node-types.md) and
83
83
  file reads, web fetch, anything on its toolbelt.
84
84
 
85
85
  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.
86
+ not bundle a converter or automatically launch an agent. Clicking **Ask agent to
87
+ import** also authorizes processing by the connected host's tools/model provider;
88
+ there is no separate consent confirmation. The agent returns Markdown sections
89
+ with references and warnings for review. Multi-card imports are placed together
90
+ in a free area, in a group named after the source file. Remote hosts may require
91
+ manual transfer; OCR, charts, and host format support are not guaranteed.
92
92
 
93
93
  JSON-render panels support bound form state, nested repeats, watchers, and Card
94
94
  header/footer slots. Form edits remain viewer-local until explicitly submitted;
@@ -246,7 +246,9 @@ surfaces (the core never imports a host SDK).
246
246
  session override), wakes Copilot when the board sends a steer, injects
247
247
  pinned/focused AX context on prompt submission, and exposes actions for focus,
248
248
  work items, approval gates, review annotations, the AX timeline, and
249
- host-capability reporting. Install it into any repo with
249
+ host-capability reporting. Actions stay bound to the server shown by their
250
+ panel and are withheld if that server no longer matches the panel workspace.
251
+ Install it into any repo with
250
252
  `pmx-canvas copilot install-extension` (`--dry-run` to preview).
251
253
  - **Codex app** — native through the Codex in-app Browser (opened to
252
254
  `/workbench`) plus the PMX MCP server: agents read `canvas://ax-context` /
@@ -401,7 +403,7 @@ services:
401
403
  ```bash
402
404
  # .agents/setup — install the CLI (pin the exact version: a fresh orb running
403
405
  # @latest can silently pick up a newer release than the one you validated)
404
- npm install -g pmx-canvas@0.7.0
406
+ npm install -g pmx-canvas@0.7.1
405
407
  ```
406
408
 
407
409
  The server binds the portal-assigned `$PORT` automatically (gated on the
@@ -4784,7 +4784,7 @@ html.is-node-resizing .ext-app-preview-catcher {
4784
4784
  position: absolute;
4785
4785
  top: 18px;
4786
4786
  right: 18px;
4787
- max-width: min(260px, calc(100% - 24px));
4787
+ max-width: min(360px, calc(100% - 36px));
4788
4788
  display: inline-flex;
4789
4789
  align-items: center;
4790
4790
  gap: 8px;
@@ -4792,7 +4792,7 @@ html.is-node-resizing .ext-app-preview-catcher {
4792
4792
  background: color-mix(in srgb, var(--c-panel-glass) 96%, transparent);
4793
4793
  backdrop-filter: blur(14px);
4794
4794
  border: 1px solid color-mix(in srgb, var(--c-line) 76%, var(--c-warn) 24%);
4795
- border-radius: 999px;
4795
+ border-radius: 12px;
4796
4796
  box-shadow: 0 8px 22px var(--c-shadow);
4797
4797
  color: var(--c-text);
4798
4798
  font: inherit;
@@ -4831,15 +4831,27 @@ html.is-node-resizing .ext-app-preview-catcher {
4831
4831
  box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 28%, transparent);
4832
4832
  }
4833
4833
 
4834
+ .attention-toast-copy {
4835
+ display: grid;
4836
+ gap: 4px;
4837
+ min-width: 0;
4838
+ text-align: left;
4839
+ overflow-wrap: anywhere;
4840
+ }
4841
+
4842
+ .attention-toast-detail {
4843
+ color: var(--c-text-soft);
4844
+ font-size: 12px;
4845
+ line-height: 1.45;
4846
+ max-height: 9em;
4847
+ overflow: auto;
4848
+ }
4849
+
4834
4850
  .attention-toast-title {
4835
4851
  font-size: 12px;
4836
4852
  font-weight: 600;
4837
4853
  line-height: 1.2;
4838
4854
  letter-spacing: -0.005em;
4839
- white-space: nowrap;
4840
- overflow: hidden;
4841
- text-overflow: ellipsis;
4842
- max-width: 200px;
4843
4855
  }
4844
4856
 
4845
4857
  .attention-history {
@@ -6841,12 +6853,23 @@ html.is-node-dragging .intent-line-layer {
6841
6853
  padding: 4px 12px;
6842
6854
  cursor: pointer;
6843
6855
  }
6844
- .is-presenting > :not(.app-main):not(.presentation-error),
6856
+ .is-presenting > :not(.app-main):not(.presentation-error):not(.presentation-navigation),
6845
6857
  .is-presenting .app-main > :not(.canvas-region),
6846
6858
  .is-presenting .canvas-region > :not(.canvas-viewport) {
6847
6859
  display: none !important;
6848
6860
  }
6849
6861
  .is-presenting .canvas-viewport { pointer-events: none; }
6862
+ .presentation-navigation {
6863
+ position: fixed;
6864
+ inset: 0;
6865
+ z-index: 99;
6866
+ }
6867
+ .presentation-exit {
6868
+ position: absolute;
6869
+ top: 16px;
6870
+ right: 16px;
6871
+ box-shadow: 0 2px 12px var(--c-shadow);
6872
+ }
6850
6873
  .presentation-error {
6851
6874
  position: fixed;
6852
6875
  bottom: 16px;
@@ -7104,10 +7127,17 @@ html.is-node-dragging .intent-line-layer {
7104
7127
  }
7105
7128
  .export-dialog-links {
7106
7129
  display: flex;
7130
+ flex-wrap: wrap;
7107
7131
  gap: 12px;
7132
+ margin-top: 12px;
7108
7133
  }
7109
7134
  .export-dialog-links a {
7110
- color: var(--c-accent);
7135
+ display: inline-flex;
7136
+ align-items: center;
7137
+ justify-content: center;
7138
+ box-sizing: border-box;
7139
+ text-decoration: none;
7140
+ word-break: normal;
7111
7141
  }
7112
7142
 
7113
7143
  /* ── Home categories ── */
@@ -7267,9 +7297,42 @@ html.is-node-dragging .intent-line-layer {
7267
7297
  .attachment-node p { margin: 0; color: var(--c-text-soft); line-height: 1.5; }
7268
7298
  .attachment-node a, .md-import-source a { color: var(--c-accent); }
7269
7299
  .attachment-status { font-weight: 600; }
7270
- .attachment-node .home-board-action { border-color: var(--c-line); color: var(--c-text); min-height: 34px; }
7271
- .attachment-node .home-board-action:hover { background: var(--c-panel); border-color: var(--c-accent); }
7300
+ .attachment-button,
7301
+ .presentation-exit,
7302
+ .export-dialog-links a {
7303
+ min-height: 38px;
7304
+ padding: 8px 14px;
7305
+ border: 1px solid var(--c-text-soft);
7306
+ border-radius: 8px;
7307
+ background: var(--c-panel);
7308
+ color: var(--c-text);
7309
+ font: inherit;
7310
+ font-weight: 600;
7311
+ cursor: pointer;
7312
+ }
7313
+ .attachment-button.is-primary,
7314
+ .export-dialog-links a.is-primary { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
7315
+ .attachment-button:hover:not(:disabled),
7316
+ .presentation-exit:hover,
7317
+ .export-dialog-links a:hover { filter: brightness(1.12); }
7318
+ .attachment-button:focus-visible,
7319
+ .presentation-exit:focus-visible,
7320
+ .export-dialog-links a:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 3px; }
7321
+ .attachment-button:disabled { opacity: 0.5; cursor: default; }
7272
7322
  .attachment-review { width: 100%; border-top: 1px solid var(--c-line); padding-top: 12px; }
7323
+ .attachment-review-actions {
7324
+ position: sticky;
7325
+ bottom: -16px;
7326
+ display: flex;
7327
+ flex-wrap: wrap;
7328
+ justify-content: flex-end;
7329
+ gap: 12px;
7330
+ margin-top: 24px;
7331
+ padding: 16px 0;
7332
+ border-top: 1px solid var(--c-line);
7333
+ background: var(--c-panel);
7334
+ z-index: 1;
7335
+ }
7273
7336
  .attachment-review pre { white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.6 var(--mono); padding: 12px; background: var(--c-bg); border-radius: 8px; }
7274
7337
  .attachment-review h4 { margin-bottom: 6px; }
7275
7338
  .md-import-source { padding: 6px 12px; font-size: 11px; overflow-wrap: anywhere; color: var(--c-text-soft); border-top: 1px solid var(--c-line); }