pmx-canvas 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +79 -61
  2. package/CHANGELOG.md +29 -1
  3. package/Readme.md +48 -9
  4. package/dist/canvas/global.css +144 -45
  5. package/dist/canvas/index.js +90 -90
  6. package/dist/json-render/index.js +55 -55
  7. package/dist/types/client/canvas/ChoiceMenu.d.ts +13 -0
  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/client/nodes/McpAppNode.d.ts +2 -1
  13. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -0
  14. package/dist/types/client/state/canvas-store.d.ts +2 -0
  15. package/dist/types/client/state/intent-bridge.d.ts +1 -1
  16. package/dist/types/json-render/renderer/index.d.ts +2 -1
  17. package/dist/types/json-render/server.d.ts +2 -0
  18. package/dist/types/shared/ax-surface-protocol.d.ts +2 -0
  19. package/dist/types/shared/delimited-text.d.ts +14 -0
  20. package/docs/ax-host-adapter-contract.md +13 -2
  21. package/docs/cli.md +9 -2
  22. package/docs/http-api.md +12 -8
  23. package/docs/mcp.md +8 -1
  24. package/docs/sdk.md +2 -2
  25. package/package.json +1 -1
  26. package/skills/pmx-canvas/SKILL.md +41 -16
  27. package/skills/pmx-canvas/evals/evals.json +112 -2
  28. package/skills/pmx-canvas/references/full-reference.md +9 -4
  29. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +17 -3
  30. package/skills/pmx-canvas/references/installing-pmx-canvas.md +76 -4
  31. package/skills/pmx-canvas-testing/SKILL.md +49 -0
  32. package/src/cli/commands/backup.ts +15 -4
  33. package/src/cli/commands/boards.ts +16 -3
  34. package/src/client/canvas/AttentionHistory.tsx +2 -1
  35. package/src/client/canvas/BoardSwitcher.tsx +3 -3
  36. package/src/client/canvas/CanvasNode.tsx +211 -197
  37. package/src/client/canvas/CanvasViewport.tsx +6 -0
  38. package/src/client/canvas/ChoiceMenu.tsx +122 -0
  39. package/src/client/canvas/CommandBar.tsx +27 -25
  40. package/src/client/canvas/ContextPinBar.tsx +3 -1
  41. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -3
  42. package/src/client/canvas/ExternalWriters.tsx +4 -3
  43. package/src/client/canvas/FeedbackDialog.tsx +12 -9
  44. package/src/client/canvas/IntentLayer.tsx +2 -2
  45. package/src/client/canvas/SelectionBar.tsx +2 -2
  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 +48 -100
  50. package/src/client/canvas/TopBar.tsx +2 -2
  51. package/src/client/icons.tsx +118 -1
  52. package/src/client/nodes/BoardNode.tsx +2 -1
  53. package/src/client/nodes/ExtAppFrame.tsx +3 -1
  54. package/src/client/nodes/FileNode.tsx +4 -78
  55. package/src/client/nodes/ImageNode.tsx +2 -1
  56. package/src/client/nodes/MarkdownNode.tsx +2 -1
  57. package/src/client/nodes/McpAppNode.tsx +16 -0
  58. package/src/client/nodes/PromptNode.tsx +3 -1
  59. package/src/client/nodes/use-ax-surface-bridge.ts +4 -2
  60. package/src/client/nodes/viewer-runtime-state.ts +78 -0
  61. package/src/client/state/canvas-store.ts +3 -0
  62. package/src/client/state/intent-bridge.ts +14 -2
  63. package/src/client/theme/global.css +144 -45
  64. package/src/json-render/renderer/index.tsx +58 -38
  65. package/src/json-render/server.ts +8 -1
  66. package/src/mcp/server.ts +2 -2
  67. package/src/server/agent-presence.ts +17 -11
  68. package/src/server/board-export.ts +97 -32
  69. package/src/server/index.ts +9 -7
  70. package/src/server/operations/composites.ts +3 -2
  71. package/src/server/operations/ops/ax-presence.ts +7 -3
  72. package/src/server/operations/ops/boards.ts +12 -5
  73. package/src/server/operations/registry.ts +4 -13
  74. package/src/server/server.ts +13 -4
  75. package/src/shared/ax-surface-protocol.ts +3 -0
  76. 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,35 @@
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.3] - 2026-10-01
7
+
8
+ ### Fixed
9
+
10
+ - JSON form values and dynamic lists survive expanding and closing focus mode without silently saving drafts to the server; authored spec updates still reset runtime state.
11
+ - Interactive surfaces register their action bridge before first paint.
12
+ - Bundled skill quick references match live MCP discovery, including board opening and workboards, and distinguish local JSON runtime state from durable data.
13
+ - Amp setup guidance covers workspace trust approval, portal targeting and native steering verification; testing guidance preserves first-failure evidence and distinguishes host coverage gaps.
14
+ - The main toolbar keeps Markdown, Attach files, Webpage and Group; one file picker replaces separate File, Image and HTML buttons, with supported formats explained in its tooltip.
15
+ - Draw, Text note and Eraser are separate toolbar buttons for quick board feedback.
16
+ - Connection handles are easier to grab at any zoom, and saved connections appear without waiting for the next background update.
17
+ - Feedback and agent selectors use themed menus, and Cancel actions have clearer button styling.
18
+
19
+ ## [0.7.2] - 2026-09-30
20
+
21
+ ### Added
22
+
23
+ - Agents can explicitly open a board before writing through MCP, CLI, HTTP, and SDK; board deletion remains human-only.
24
+
25
+ ### Fixed
26
+
27
+ - Main-agent transport attaches reuse the attached host session, avoiding duplicate Copilot agents while keeping identified subagents separate.
28
+ - 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.
29
+ - Original document attachments now avoid existing cards and each other without moving existing content.
30
+ - Backup and restore commands reject unknown flags instead of silently ignoring them.
31
+ - Amp setup instructions distinguish native MCP configuration from the browser portal.
32
+ - Copilot's fallback panel reconnects to the full workbench and keeps adapter actions on the recovered server.
33
+ - Copilot status omits serialized node context; full context remains available through `get_ax_context`.
34
+ - Attachment, board-menu, card, and panel controls use consistently sized SVG icons instead of small font symbols.
7
35
 
8
36
  ## [0.7.1] - 2026-09-30
9
37
 
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,45 @@ 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.3", "--mcp"],
401
+ "includeTools": ["canvas_*"],
402
+ "env": {
403
+ "PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
404
+ "PMX_CANVAS_PORT": "14313",
405
+ "PMX_CANVAS_URL": "",
406
+ "PMX_CANVAS_AGENT_SOURCE": "amp"
407
+ }
408
+ }
409
+ }
410
+ }
411
+ ```
412
+
413
+ Replace the root and example port with the portal service's actual workspace and local
414
+ listening port, including an orb-assigned port. Configure this inside the orb/runner;
415
+ settings on your laptop are not automatically available there.
416
+ In the PMX product checkout, use `"command": "bun"` and
417
+ `"args": ["run", "src/mcp/server.ts"]` to test the checked-out source instead.
418
+ Run `amp mcp doctor` in that workspace. If it reports `awaiting approval`, the user
419
+ must review the command and run `amp mcp approve canvas` there; the agent must not
420
+ self-approve or bypass workspace trust. Then reload MCP connections and confirm
421
+ `connected`, Canvas tool discovery, and matching `/health.workspace`, version and
422
+ active board before writes. An open portal or standalone MCP handshake does not prove
423
+ native discovery. PMX ships no dedicated Amp lifecycle adapter: an active agent can
424
+ poll steering, but MCP setup alone does not wake an idle thread or activate skills.
425
+ See the bundled [Amp setup and verification guide](skills/pmx-canvas/references/installing-pmx-canvas.md#amp-connect-the-agent-separately-from-the-portal)
426
+ and [Amp MCP configuration](https://ampcode.com/docs/customize/mcp).
427
+
428
+ For the visible workbench, run `pmx-canvas` as an orb service (the orb sets
391
429
  `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:
430
+ thread's portal URL. A consumer-workspace service recipe:
393
431
 
394
432
  ```yaml
395
433
  # .amp/services.yaml — supervised portal service
@@ -403,7 +441,7 @@ services:
403
441
  ```bash
404
442
  # .agents/setup — install the CLI (pin the exact version: a fresh orb running
405
443
  # @latest can silently pick up a newer release than the one you validated)
406
- npm install -g pmx-canvas@0.7.1
444
+ npm install -g pmx-canvas@0.7.3
407
445
  ```
408
446
 
409
447
  The server binds the portal-assigned `$PORT` automatically (gated on the
@@ -419,9 +457,10 @@ be inlined and stay blocked by the portal embed: cross-origin hosted apps (e.g.
419
457
  the hosted Excalidraw MCP app), and PDF file nodes — a PDF node offers an
420
458
  "Open PDF" link there instead of a preview.
421
459
  Debug overrides: `/workbench?transport=poll|sse` and
422
- `?iframe-mode=srcdoc|src`. Once the service is up, `pmx-canvas smoke` verifies
423
- the whole stack (health, versions, MCP handshake, node lifecycle, validation)
424
- in one command.
460
+ `?iframe-mode=srcdoc|src`. After verifying the workspace, `pmx-canvas smoke --port=<actual-port>`
461
+ checks health, versions, a standalone MCP handshake, node lifecycle and validation.
462
+ Use a disposable board: it performs temporary writes and leaves history. This does
463
+ not verify native Amp discovery or browser rendering.
425
464
 
426
465
  ### Install the agent skill (recommended)
427
466
 
@@ -572,9 +572,23 @@ body,
572
572
  box-shadow: 0 4px 24px var(--c-shadow);
573
573
  display: flex;
574
574
  flex-direction: column;
575
- overflow: hidden;
575
+ overflow: visible;
576
576
  transition: box-shadow 0.15s ease;
577
- contain: layout style paint;
577
+ contain: layout style;
578
+ }
579
+
580
+ /* Clip card content, not the connection handles centered on its border. */
581
+ .node-content {
582
+ display: flex;
583
+ flex-direction: column;
584
+ flex: 1;
585
+ min-height: 0;
586
+ border-radius: inherit;
587
+ overflow: hidden;
588
+ contain: paint;
589
+ }
590
+ .group-node > .node-content {
591
+ display: contents;
578
592
  }
579
593
 
580
594
  .canvas-node:hover {
@@ -610,34 +624,42 @@ body,
610
624
  /* ── Connection port handles (drag-to-connect) ───────────── */
611
625
  .node-port {
612
626
  position: absolute;
613
- width: 10px;
614
- height: 10px;
615
- border-radius: 50%;
616
- background: var(--c-accent);
617
- border: 2px solid var(--c-panel);
627
+ /* The visible dot stays compact; its grab area stays usable at every zoom. */
628
+ width: calc(32px * var(--node-port-scale, 1));
629
+ height: calc(32px * var(--node-port-scale, 1));
630
+ display: grid;
631
+ place-items: center;
618
632
  opacity: 0;
619
633
  cursor: crosshair;
620
634
  z-index: 10;
621
- transition: opacity 0.15s, transform 0.15s;
622
- transform: translate(-50%, -50%) scale(0.6);
635
+ touch-action: none;
636
+ transition: opacity 0.15s;
623
637
  pointer-events: auto;
624
638
  }
625
- .canvas-node:hover .node-port {
639
+ .node-port::after {
640
+ content: '';
641
+ width: calc(6px * var(--node-port-scale, 1));
642
+ height: calc(6px * var(--node-port-scale, 1));
643
+ border-radius: 50%;
644
+ background: var(--c-accent);
645
+ border: calc(2px * var(--node-port-scale, 1)) solid var(--c-panel);
646
+ box-shadow: 0 0 0 calc(1px * var(--node-port-scale, 1)) var(--c-accent);
647
+ }
648
+ .canvas-node:hover .node-port,
649
+ .canvas-node.selected .node-port,
650
+ .canvas-node:focus-within .node-port {
626
651
  opacity: 1;
627
- transform: translate(-50%, -50%) scale(1);
628
652
  }
629
- .node-port:hover {
653
+ .node-port:hover::after {
630
654
  background: var(--c-accent-hover);
631
- transform: translate(-50%, -50%) scale(1.3);
632
655
  }
633
- .node-port-top { top: 0; left: 50%; }
634
- .node-port-bottom { bottom: 0; left: 50%; transform: translate(-50%, 50%) scale(0.6); }
635
- .node-port-left { left: 0; top: 50%; }
636
- .node-port-right { right: 0; top: 50%; transform: translate(50%, -50%) scale(0.6); }
637
- .canvas-node:hover .node-port-bottom { transform: translate(-50%, 50%) scale(1); }
638
- .canvas-node:hover .node-port-right { transform: translate(50%, -50%) scale(1); }
639
- .node-port-bottom:hover { transform: translate(-50%, 50%) scale(1.3); }
640
- .node-port-right:hover { transform: translate(50%, -50%) scale(1.3); }
656
+ /* Bias vertical grab areas outward so they do not steal title/footer clicks. */
657
+ .node-port-top { top: 0; left: 50%; transform: translate(-50%, -75%); }
658
+ .node-port-bottom { bottom: 0; left: 50%; transform: translate(-50%, 75%); }
659
+ .node-port-top::after { transform: translateY(calc(8px * var(--node-port-scale, 1))); }
660
+ .node-port-bottom::after { transform: translateY(calc(-8px * var(--node-port-scale, 1))); }
661
+ .node-port-left { left: 0; top: 50%; transform: translate(-50%, -50%); }
662
+ .node-port-right { right: 0; top: 50%; transform: translate(50%, -50%); }
641
663
 
642
664
  .canvas-node .node-titlebar {
643
665
  display: flex;
@@ -760,6 +782,11 @@ body,
760
782
  border-radius: calc(3px * var(--node-chrome-scale, 1));
761
783
  }
762
784
 
785
+ .canvas-node .node-controls button svg {
786
+ width: 1em;
787
+ height: 1em;
788
+ }
789
+
763
790
  .canvas-node .node-controls button:hover {
764
791
  background: var(--c-surface-hover);
765
792
  color: var(--c-text);
@@ -1466,6 +1493,10 @@ body,
1466
1493
  animation: presence-pulse 1.4s ease-in-out infinite;
1467
1494
  }
1468
1495
  .session-collapse {
1496
+ display: inline-flex;
1497
+ align-items: center;
1498
+ justify-content: center;
1499
+ padding: 0;
1469
1500
  width: 26px;
1470
1501
  height: 26px;
1471
1502
  border: 1px solid transparent;
@@ -1682,8 +1713,9 @@ body,
1682
1713
  }
1683
1714
  }
1684
1715
  .session-section-chevron {
1716
+ display: inline-flex;
1717
+ flex-shrink: 0;
1685
1718
  color: var(--c-muted);
1686
- font-size: 13px;
1687
1719
  transition: transform 0.15s ease;
1688
1720
  }
1689
1721
  .session-section-chevron.is-open {
@@ -1966,8 +1998,13 @@ body,
1966
1998
  white-space: nowrap;
1967
1999
  }
1968
2000
  .command-bar-chip-unpin {
1969
- width: 14px;
1970
- height: 14px;
2001
+ display: inline-flex;
2002
+ align-items: center;
2003
+ justify-content: center;
2004
+ flex-shrink: 0;
2005
+ padding: 0;
2006
+ width: 20px;
2007
+ height: 20px;
1971
2008
  border: none;
1972
2009
  border-radius: 50%;
1973
2010
  background: none;
@@ -1986,6 +2023,7 @@ body,
1986
2023
  color: var(--c-dim);
1987
2024
  }
1988
2025
  .command-bar-composer {
2026
+ position: relative;
1989
2027
  display: flex;
1990
2028
  align-items: center;
1991
2029
  gap: 10px;
@@ -1997,24 +2035,72 @@ body,
1997
2035
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
1998
2036
  }
1999
2037
  .command-bar-target {
2000
- appearance: none;
2001
- -webkit-appearance: none;
2002
2038
  max-width: 148px;
2003
- padding: 4px 18px 4px 8px;
2004
- border: 1px solid var(--c-line);
2005
- border-radius: 7px;
2006
- background: color-mix(in srgb, var(--c-panel) 80%, transparent)
2007
- url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M1 1l3 3 3-3' fill='none' stroke='%237a8699' stroke-width='1.4'/%3E%3C/svg%3E")
2008
- no-repeat right 6px center;
2009
- color: var(--c-text-soft);
2010
- font: 12px/1.3 var(--mono);
2011
- cursor: pointer;
2012
2039
  flex-shrink: 0;
2013
2040
  }
2014
- .command-bar-target:hover,
2015
- .command-bar-target:focus-visible {
2016
- border-color: var(--c-accent-25);
2041
+ .choice-menu {
2042
+ position: relative;
2043
+ min-width: 0;
2044
+ }
2045
+ .choice-menu-trigger {
2046
+ display: flex;
2047
+ align-items: center;
2048
+ justify-content: space-between;
2049
+ gap: 10px;
2050
+ width: 100%;
2051
+ min-height: 36px;
2052
+ padding: 8px 10px;
2053
+ border: 1px solid color-mix(in srgb, var(--c-line) 65%, var(--c-muted));
2054
+ border-radius: 8px;
2055
+ background: var(--c-panel-overlay);
2017
2056
  color: var(--c-text);
2057
+ font: 12px/1.35 var(--font);
2058
+ cursor: pointer;
2059
+ }
2060
+ .choice-menu-trigger > span {
2061
+ overflow: hidden;
2062
+ text-overflow: ellipsis;
2063
+ white-space: nowrap;
2064
+ }
2065
+ .choice-menu-trigger svg { flex-shrink: 0; }
2066
+ .choice-menu-trigger:hover,
2067
+ .choice-menu-trigger[aria-expanded='true'] { border-color: var(--c-accent); }
2068
+ .choice-menu-trigger:focus-visible,
2069
+ .choice-menu-options button:focus-visible {
2070
+ outline: 2px solid var(--c-accent);
2071
+ outline-offset: 2px;
2072
+ }
2073
+ .toolbar-menu.choice-menu-options {
2074
+ top: calc(100% + 6px);
2075
+ left: 0;
2076
+ right: auto;
2077
+ min-width: 100%;
2078
+ width: max-content;
2079
+ max-width: min(320px, calc(100vw - 64px));
2080
+ max-height: min(280px, 40vh);
2081
+ overflow-y: auto;
2082
+ z-index: 20;
2083
+ }
2084
+ .toolbar-menu.choice-menu-options.opens-above {
2085
+ top: auto;
2086
+ bottom: calc(100% + 6px);
2087
+ }
2088
+ .command-bar-target.choice-menu {
2089
+ position: static;
2090
+ }
2091
+ .command-bar-target .choice-menu-options {
2092
+ /* Anchor to the composer, not its inset trigger, to clear the session rail. */
2093
+ min-width: min(200px, 100%);
2094
+ max-width: 100%;
2095
+ }
2096
+ .choice-menu-options .toolbar-menu-item {
2097
+ min-height: 36px;
2098
+ white-space: normal;
2099
+ overflow-wrap: anywhere;
2100
+ }
2101
+ .choice-menu-options button:disabled {
2102
+ opacity: 0.5;
2103
+ cursor: not-allowed;
2018
2104
  }
2019
2105
 
2020
2106
  .command-bar-icon {
@@ -4483,13 +4569,24 @@ html.is-node-resizing .ext-app-preview-catcher {
4483
4569
  }
4484
4570
  .text-prompt-cancel,
4485
4571
  .text-prompt-confirm {
4486
- padding: 5px 12px;
4572
+ min-height: 36px;
4573
+ padding: 8px 14px;
4487
4574
  border-radius: 7px;
4488
- border: 1px solid var(--c-line);
4489
- background: none;
4490
- color: var(--c-text-soft);
4575
+ border: 1px solid color-mix(in srgb, var(--c-line) 65%, var(--c-muted));
4576
+ background: var(--c-panel-overlay);
4577
+ color: var(--c-text);
4491
4578
  cursor: pointer;
4492
4579
  font-size: 12px;
4580
+ font-weight: 600;
4581
+ }
4582
+ .text-prompt-cancel:hover {
4583
+ background: var(--c-accent-10);
4584
+ border-color: var(--c-muted);
4585
+ }
4586
+ .text-prompt-cancel:focus-visible,
4587
+ .text-prompt-confirm:focus-visible {
4588
+ outline: 2px solid var(--c-accent);
4589
+ outline-offset: 2px;
4493
4590
  }
4494
4591
  .text-prompt-confirm {
4495
4592
  background: var(--c-accent);
@@ -4509,7 +4606,8 @@ html.is-node-resizing .ext-app-preview-catcher {
4509
4606
  overflow-y: auto;
4510
4607
  color: var(--c-text);
4511
4608
  }
4512
- .feedback-dialog label {
4609
+ .feedback-dialog label,
4610
+ .feedback-field {
4513
4611
  display: flex;
4514
4612
  flex-direction: column;
4515
4613
  gap: 6px;
@@ -6896,7 +6994,7 @@ html.is-node-dragging .intent-line-layer {
6896
6994
  align-items: center;
6897
6995
  gap: 6px;
6898
6996
  max-width: 280px;
6899
- padding: 4px 8px;
6997
+ padding: 6px 8px;
6900
6998
  margin-left: -8px;
6901
6999
  background: none;
6902
7000
  border: 1px solid transparent;
@@ -6914,7 +7012,8 @@ html.is-node-dragging .intent-line-layer {
6914
7012
  white-space: nowrap;
6915
7013
  }
6916
7014
  .board-switcher-caret {
6917
- font-size: 10px;
7015
+ display: inline-flex;
7016
+ flex-shrink: 0;
6918
7017
  color: var(--c-text-soft);
6919
7018
  }
6920
7019
  .board-switcher-menu {