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.
- package/.github/extensions/pmx-canvas/extension.mjs +149 -70
- package/CHANGELOG.md +32 -1
- package/Readme.md +40 -13
- package/dist/canvas/global.css +94 -15
- package/dist/canvas/index.js +76 -76
- package/dist/types/cli/daemon.d.ts +5 -5
- package/dist/types/cli/shared.d.ts +3 -1
- 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/shared/delimited-text.d.ts +14 -0
- package/docs/cli.md +14 -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 +35 -14
- package/skills/pmx-canvas/evals/evals.json +171 -0
- package/skills/pmx-canvas/references/full-reference.md +6 -1
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +17 -3
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas/references/tours-and-recording.md +69 -0
- package/skills/pmx-canvas-testing/SKILL.md +7 -2
- package/src/cli/commands/apps.ts +1 -1
- package/src/cli/commands/backup.ts +15 -4
- package/src/cli/commands/boards.ts +16 -3
- package/src/cli/commands/export.ts +9 -2
- package/src/cli/daemon.ts +37 -7
- package/src/cli/shared.ts +11 -2
- package/src/client/App.tsx +0 -1
- package/src/client/canvas/AttentionHistory.tsx +2 -1
- package/src/client/canvas/AttentionToast.tsx +5 -3
- package/src/client/canvas/BoardSwitcher.tsx +3 -3
- package/src/client/canvas/CanvasNode.tsx +17 -9
- package/src/client/canvas/CommandBar.tsx +2 -2
- package/src/client/canvas/ContextMenu.tsx +0 -1
- package/src/client/canvas/ContextPinBar.tsx +3 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -3
- package/src/client/canvas/ExportDialog.tsx +3 -1
- package/src/client/canvas/ExternalWriters.tsx +4 -3
- package/src/client/canvas/FeedbackDialog.tsx +2 -1
- package/src/client/canvas/IntentLayer.tsx +2 -2
- package/src/client/canvas/Presentation.tsx +39 -9
- package/src/client/canvas/SelectionBar.tsx +2 -4
- 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 +2 -1
- package/src/client/canvas/TopBar.tsx +2 -2
- package/src/client/icons.tsx +118 -1
- package/src/client/nodes/AttachmentNode.tsx +27 -30
- 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/PromptNode.tsx +3 -1
- package/src/client/state/attention-bridge.ts +3 -1
- package/src/client/state/sse-bridge.ts +3 -1
- package/src/client/theme/global.css +94 -15
- 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/ops/imports.ts +19 -3
- package/src/server/operations/registry.ts +4 -13
- package/src/server/server.ts +10 -3
- 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
|
|
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(
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
310
|
+
max-width: 560px;
|
|
311
|
+
margin: 12vh auto;
|
|
312
|
+
padding: 24px;
|
|
280
313
|
}
|
|
281
|
-
|
|
314
|
+
.actions {
|
|
282
315
|
display: flex;
|
|
283
|
-
|
|
316
|
+
flex-wrap: wrap;
|
|
284
317
|
gap: 12px;
|
|
285
|
-
|
|
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
|
-
|
|
290
|
-
.
|
|
320
|
+
p { line-height: 1.6; overflow-wrap: anywhere; }
|
|
321
|
+
.workspace {
|
|
291
322
|
color: var(--text-color-muted, #8b949e);
|
|
292
|
-
font-size:
|
|
323
|
+
font-size: 13px;
|
|
293
324
|
}
|
|
294
325
|
.error {
|
|
295
|
-
margin: 0;
|
|
296
326
|
color: var(--true-color-red, #ff7b72);
|
|
297
|
-
|
|
327
|
+
min-height: 24px;
|
|
298
328
|
}
|
|
299
329
|
button {
|
|
300
|
-
border: 1px solid var(--border-color-default, #
|
|
330
|
+
border: 1px solid var(--border-color-default, #6e7681);
|
|
301
331
|
border-radius: 8px;
|
|
302
|
-
background: var(--background-color-
|
|
332
|
+
background: var(--background-color-muted, #21262d);
|
|
303
333
|
color: inherit;
|
|
334
|
+
font: inherit;
|
|
304
335
|
cursor: pointer;
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
321
|
-
<
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
<button type="button" onclick="
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
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(
|
|
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
|
-
|
|
370
|
-
jsonResponse(res, 200,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
400
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
-
## [
|
|
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.
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|