pmx-canvas 0.7.3 → 0.8.0

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 (235) hide show
  1. package/.github/extensions/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  2. package/.github/extensions/pmx-canvas/extension.mjs +25 -172
  3. package/.github/extensions/pmx-canvas/mcp-bootstrap.ts +198 -0
  4. package/.github/extensions/pmx-canvas/runtime.d.mts +41 -0
  5. package/.github/extensions/pmx-canvas/runtime.json +3 -0
  6. package/.github/extensions/pmx-canvas/runtime.mjs +194 -0
  7. package/.github/extensions/pmx-canvas/start-mcp.mjs +51 -0
  8. package/.github/extensions/pmx-canvas/tools.json +2560 -0
  9. package/CHANGELOG.md +47 -0
  10. package/Readme.md +21 -7
  11. package/dist/canvas/fonts/OFL.txt +93 -0
  12. package/dist/canvas/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  13. package/dist/canvas/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  14. package/dist/canvas/fonts/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  15. package/dist/canvas/fonts/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  16. package/dist/canvas/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  17. package/dist/canvas/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  18. package/dist/canvas/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  19. package/dist/canvas/fonts/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  20. package/dist/canvas/fonts/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  21. package/dist/canvas/fonts/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  22. package/dist/canvas/global.css +723 -672
  23. package/dist/canvas/index.js +90 -88
  24. package/dist/canvas/mcp-app.html +7878 -0
  25. package/dist/canvas/mermaid-entry.js +1 -1
  26. package/dist/canvas/surface-theme.css +151 -46
  27. package/dist/json-render/index.css +1 -1
  28. package/dist/types/cli/commands/record.d.ts +1 -1
  29. package/dist/types/client/canvas/NodeContextMark.d.ts +24 -0
  30. package/dist/types/client/canvas/SessionReceipt.d.ts +3 -1
  31. package/dist/types/client/icons.d.ts +1 -2
  32. package/dist/types/client/mcp-app/index.d.ts +1 -0
  33. package/dist/types/client/nodes/ax-node-actions.d.ts +2 -2
  34. package/dist/types/client/nodes/markdown-sanitize.d.ts +2 -0
  35. package/dist/types/client/nodes/use-workbench-asset.d.ts +2 -0
  36. package/dist/types/client/state/attention-store.d.ts +0 -5
  37. package/dist/types/client/state/context-status-store.d.ts +53 -0
  38. package/dist/types/client/state/intent-bridge.d.ts +3 -0
  39. package/dist/types/client/state/near-pin-store.d.ts +16 -0
  40. package/dist/types/client/state/presence-store.d.ts +5 -0
  41. package/dist/types/client/state/session-store.d.ts +33 -3
  42. package/dist/types/client/state/workbench-transport.d.ts +16 -0
  43. package/dist/types/client/theme/tokens.d.ts +1 -3
  44. package/dist/types/client/types.d.ts +9 -0
  45. package/dist/types/json-render/server.d.ts +1 -1
  46. package/dist/types/mcp/app-server.d.ts +3 -0
  47. package/dist/types/mcp/canvas-access.d.ts +1 -1
  48. package/dist/types/mcp/canvas-app.d.ts +4 -0
  49. package/dist/types/mcp/workbench-app.d.ts +4 -0
  50. package/dist/types/server/agent-presence.d.ts +1 -1
  51. package/dist/types/server/ax-state.d.ts +1 -1
  52. package/dist/types/server/canvas-db.d.ts +8 -0
  53. package/dist/types/server/canvas-geometry.d.ts +25 -0
  54. package/dist/types/server/canvas-operations.d.ts +1 -11
  55. package/dist/types/server/canvas-state.d.ts +25 -3
  56. package/dist/types/server/context-brief.d.ts +8 -1
  57. package/dist/types/server/context-reads.d.ts +30 -7
  58. package/dist/types/server/index.d.ts +6 -1
  59. package/dist/types/server/mcp-app-runtime.d.ts +1 -0
  60. package/dist/types/server/operations/http.d.ts +2 -0
  61. package/dist/types/server/operations/invoker.d.ts +4 -2
  62. package/dist/types/server/operations/ops/nodes.d.ts +0 -6
  63. package/dist/types/server/server.d.ts +1 -0
  64. package/dist/types/server/spatial-analysis.d.ts +2 -0
  65. package/dist/types/shared/agent-context.d.ts +22 -0
  66. package/dist/types/shared/agent-presence.d.ts +3 -10
  67. package/dist/types/shared/canvas-app.d.ts +54 -0
  68. package/dist/types/shared/themes.d.ts +1 -1
  69. package/docs/RELEASE.md +7 -3
  70. package/docs/agent-plugin.md +123 -0
  71. package/docs/ax-state-contract.md +40 -0
  72. package/docs/cli.md +7 -2
  73. package/docs/design/AgentContext.dc.html +186 -0
  74. package/docs/design/Before.dc.html +208 -0
  75. package/docs/design/BoardPins.dc.html +151 -0
  76. package/docs/design/Chrome.dc.html +208 -0
  77. package/docs/design/Context.dc.html +239 -0
  78. package/docs/design/Flows.dc.html +181 -0
  79. package/docs/design/Home.dc.html +209 -0
  80. package/docs/design/HomeGraph.dc.html +211 -0
  81. package/docs/design/HomeMap.dc.html +117 -0
  82. package/docs/design/HomeTeam.dc.html +176 -0
  83. package/docs/design/Light.dc.html +28 -0
  84. package/docs/design/Main.dc.html +280 -0
  85. package/docs/design/NearPin.dc.html +260 -0
  86. package/docs/design/Pane600.dc.html +120 -0
  87. package/docs/design/Relations.dc.html +297 -0
  88. package/docs/design/Themes.dc.html +121 -0
  89. package/docs/design/TokenSystem.dc.html +136 -0
  90. package/docs/design/Tokens.dc.html +163 -0
  91. package/docs/design/Types1.dc.html +177 -0
  92. package/docs/design/Types2.dc.html +226 -0
  93. package/docs/design/Types3.dc.html +195 -0
  94. package/docs/design/Uses.dc.html +152 -0
  95. package/docs/design/Vision.dc.html +116 -0
  96. package/docs/design/Zoom.dc.html +82 -0
  97. package/docs/design/canvas.json +274 -0
  98. package/docs/design.md +214 -0
  99. package/docs/environment.md +1 -1
  100. package/docs/http-api.md +80 -15
  101. package/docs/mcp.md +198 -3
  102. package/docs/node-types.md +34 -10
  103. package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
  104. package/docs/plans/plan-015-design-foundation-wave-0.md +177 -0
  105. package/docs/plans/plan-016-context-made-visible-wave-1.md +148 -0
  106. package/docs/product-vision-2026-09.md +47 -7
  107. package/docs/screenshot.png +0 -0
  108. package/docs/sdk.md +1 -0
  109. package/package.json +10 -4
  110. package/plugins/pmx-canvas/LICENSE +21 -0
  111. package/plugins/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  112. package/plugins/pmx-canvas/assets/preview.png +0 -0
  113. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +930 -0
  114. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +3 -0
  115. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.mjs +194 -0
  116. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/steering-delivery.mjs +79 -0
  117. package/plugins/pmx-canvas/mcp.json +12 -0
  118. package/plugins/pmx-canvas/plugin.json +24 -0
  119. package/plugins/pmx-canvas/runtime.json +3 -0
  120. package/plugins/pmx-canvas/runtime.mjs +194 -0
  121. package/plugins/pmx-canvas/start-mcp.mjs +51 -0
  122. package/plugins/pmx-canvas/tools.json +2560 -0
  123. package/skills/pmx-canvas/SKILL.md +10 -7
  124. package/skills/pmx-canvas/references/full-reference.md +11 -7
  125. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +20 -8
  126. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  127. package/skills/pmx-canvas-testing/SKILL.md +5 -4
  128. package/src/cli/commands/copilot.ts +5 -0
  129. package/src/cli/commands/view.ts +1 -1
  130. package/src/cli/index.ts +8 -3
  131. package/src/client/App.tsx +6 -1
  132. package/src/client/canvas/AgentPresenceLayer.tsx +3 -3
  133. package/src/client/canvas/AnnotationLayer.tsx +1 -1
  134. package/src/client/canvas/CanvasNode.tsx +32 -49
  135. package/src/client/canvas/CanvasViewport.tsx +0 -2
  136. package/src/client/canvas/CommandPalette.tsx +3 -2
  137. package/src/client/canvas/ContextMenu.tsx +11 -0
  138. package/src/client/canvas/EdgeLayer.tsx +1 -1
  139. package/src/client/canvas/ExportDialog.tsx +33 -6
  140. package/src/client/canvas/IntentLayer.tsx +2 -2
  141. package/src/client/canvas/Minimap.tsx +12 -7
  142. package/src/client/canvas/NodeContextMark.tsx +156 -0
  143. package/src/client/canvas/SessionPanel.tsx +4 -7
  144. package/src/client/canvas/SessionReceipt.tsx +114 -17
  145. package/src/client/canvas/ShortcutOverlay.tsx +6 -1
  146. package/src/client/canvas/SnapshotPanel.tsx +11 -2
  147. package/src/client/canvas/ToolRail.tsx +12 -9
  148. package/src/client/canvas/TopBar.tsx +16 -11
  149. package/src/client/icons.tsx +2 -12
  150. package/src/client/mcp-app/index.tsx +137 -0
  151. package/src/client/mcp-app/style.css +10 -0
  152. package/src/client/nodes/AttachmentNode.tsx +8 -1
  153. package/src/client/nodes/AxStepControls.tsx +1 -1
  154. package/src/client/nodes/ContextNode.tsx +9 -9
  155. package/src/client/nodes/ExtAppFrame.tsx +13 -8
  156. package/src/client/nodes/FileNode.tsx +8 -5
  157. package/src/client/nodes/ImageNode.tsx +4 -2
  158. package/src/client/nodes/InlineMarkdownEditor.tsx +2 -1
  159. package/src/client/nodes/LedgerNode.tsx +2 -2
  160. package/src/client/nodes/MarkdownNode.tsx +44 -9
  161. package/src/client/nodes/McpAppNode.tsx +6 -4
  162. package/src/client/nodes/PromptNode.tsx +5 -17
  163. package/src/client/nodes/ResponseNode.tsx +3 -15
  164. package/src/client/nodes/StatusNode.tsx +2 -2
  165. package/src/client/nodes/StatusSummary.tsx +3 -1
  166. package/src/client/nodes/TraceNode.tsx +6 -4
  167. package/src/client/nodes/WebpageNode.tsx +1 -1
  168. package/src/client/nodes/ax-node-actions.ts +2 -2
  169. package/src/client/nodes/iframe-document-url.ts +3 -2
  170. package/src/client/nodes/markdown-sanitize.ts +10 -0
  171. package/src/client/nodes/surface-url.ts +10 -0
  172. package/src/client/nodes/use-surface-frame.ts +3 -1
  173. package/src/client/nodes/use-workbench-asset.ts +26 -0
  174. package/src/client/state/attention-bridge.ts +0 -8
  175. package/src/client/state/attention-store.ts +0 -17
  176. package/src/client/state/canvas-store.ts +4 -2
  177. package/src/client/state/context-status-store.ts +66 -0
  178. package/src/client/state/human-store.ts +2 -0
  179. package/src/client/state/iframe-mode.ts +5 -0
  180. package/src/client/state/intent-bridge.ts +10 -4
  181. package/src/client/state/near-pin-store.ts +53 -0
  182. package/src/client/state/presence-store.ts +5 -10
  183. package/src/client/state/session-store.ts +106 -9
  184. package/src/client/state/sse-bridge.ts +26 -7
  185. package/src/client/state/theme-override.ts +1 -1
  186. package/src/client/state/workbench-transport.ts +36 -0
  187. package/src/client/theme/global.css +723 -672
  188. package/src/client/theme/surface-theme.css +151 -46
  189. package/src/client/theme/tokens.ts +7 -11
  190. package/src/client/types.ts +10 -0
  191. package/src/json-render/server.ts +71 -14
  192. package/src/mcp/app-server.ts +64 -0
  193. package/src/mcp/canvas-access.ts +14 -11
  194. package/src/mcp/canvas-app.ts +318 -0
  195. package/src/mcp/server.ts +1 -1
  196. package/src/mcp/workbench-app.ts +211 -0
  197. package/src/server/agent-presence.ts +45 -6
  198. package/src/server/ax-context.ts +1 -1
  199. package/src/server/ax-state.ts +1 -1
  200. package/src/server/board-export.ts +5 -2
  201. package/src/server/canvas-db.ts +46 -12
  202. package/src/server/canvas-geometry.ts +64 -0
  203. package/src/server/canvas-operations.ts +6 -24
  204. package/src/server/canvas-state.ts +105 -15
  205. package/src/server/canvas-validation.ts +19 -4
  206. package/src/server/context-brief.ts +39 -2
  207. package/src/server/context-reads.ts +86 -15
  208. package/src/server/demo-state.json +31 -31
  209. package/src/server/html-surface.ts +4 -5
  210. package/src/server/index.ts +19 -3
  211. package/src/server/mcp-app-runtime.ts +11 -0
  212. package/src/server/operations/composites.ts +4 -3
  213. package/src/server/operations/http.ts +4 -4
  214. package/src/server/operations/invoker.ts +13 -4
  215. package/src/server/operations/ops/ax-read.ts +1 -1
  216. package/src/server/operations/ops/ax-timeline.ts +37 -0
  217. package/src/server/operations/ops/boards.ts +1 -1
  218. package/src/server/operations/ops/canvas-wire.ts +3 -6
  219. package/src/server/operations/ops/intent.ts +31 -2
  220. package/src/server/operations/ops/json-render.ts +1 -1
  221. package/src/server/operations/ops/nodes.ts +8 -76
  222. package/src/server/operations/ops/query.ts +8 -2
  223. package/src/server/operations/ops/snapshots.ts +4 -1
  224. package/src/server/operations/ops/viewport.ts +29 -5
  225. package/src/server/operations/registry.ts +38 -4
  226. package/src/server/scope-fence.ts +55 -10
  227. package/src/server/server.ts +83 -5
  228. package/src/server/spatial-analysis.ts +2 -2
  229. package/src/server/workboard.ts +6 -1
  230. package/src/{server → shared}/agent-context.ts +2 -2
  231. package/src/shared/agent-presence.ts +3 -15
  232. package/src/shared/canvas-app.ts +62 -0
  233. package/src/shared/themes.ts +5 -5
  234. package/src/client/canvas/FocusFieldLayer.tsx +0 -116
  235. package/src/client/canvas/kind-colors.ts +0 -23
@@ -1,3 +1,5 @@
1
+ import { isHostedWorkbench } from './workbench-transport';
2
+ import { showToast } from './attention-bridge';
1
3
  import { computed, signal } from '@preact/signals';
2
4
  import type { AxApprovalStatus, AxEventKind, AxWorkItemStatus } from '../../shared/ax-kinds.js';
3
5
  import { HUMAN_STARTED_SESSION_LABEL } from '../../shared/agent-presence.js';
@@ -207,7 +209,7 @@ export function mergeTimeline(
207
209
  // Activity entries are already attributed server-side — the sessionId is
208
210
  // the identity key (hue-stable across chrome), the label its display name.
209
211
  who: write.sessionId ?? null,
210
- ...(top?.actor === 'agent' && newestWrite?.id === write.id ? { undoable: true } : {}),
212
+ ...(!isHostedWorkbench() && top?.actor === 'agent' && newestWrite?.id === write.id ? { undoable: true } : {}),
211
213
  })),
212
214
  ...timeline.events.map((event) => ({
213
215
  id: `event-${event.id}`,
@@ -298,6 +300,10 @@ export async function undoAgentEdit(entry: TimelineEntry): Promise<boolean> {
298
300
 
299
301
  /** Ctrl+Z / Ctrl+Shift+Z: whichever op is top of the shared stack, agent or human. */
300
302
  export async function undoFromKeyboard(redo = false): Promise<boolean> {
303
+ if (isHostedWorkbench()) {
304
+ showToast('remove', 'Undo / redo unavailable', 'Open the local PMX workbench to use shared history.');
305
+ return false;
306
+ }
301
307
  const result = await requestOk(redo ? 'redo' : 'undo', redo ? '/api/canvas/redo' : '/api/canvas/undo', {
302
308
  method: 'POST',
303
309
  });
@@ -371,37 +377,117 @@ export async function endSession(session: { source: string; agentId: string | nu
371
377
  }
372
378
 
373
379
  export interface SessionReceipt {
374
- label: string;
380
+ /** Distinct labels of the sessions on this card (one unless merged). */
381
+ labels: string[];
382
+ /** Endings folded into this card (see applySessionReceipt). */
383
+ sessions: number;
375
384
  endedAt: string;
376
385
  /** Why it ended — the receipt should answer this, not leave the human asking. */
377
386
  endedBy?: 'human' | 'agent' | 'idle-timeout';
378
- /** The session changed nothing on the board (its pre-session snapshot was dropped). */
379
- unchanged?: boolean;
380
387
  /** Cancelled (withdrawn items), rejected (human's explicit no on a gate), and
381
388
  * held (TTL expired unanswered) are three different outcomes — never one
382
389
  * "vetoed" pile. */
383
390
  counts: { items: number; done: number; cancelled: number; rejected: number; held: number };
391
+ /** The pre-session snapshot; null when the board was empty at attach, or on a
392
+ * merged receipt (each session's snapshot is in History). */
384
393
  snapshot: { id: string; name: string } | null;
394
+ /** What the session did with context (docs/design/AgentContext.dc.html receipt). */
395
+ context: SessionContextActivity;
396
+ }
397
+
398
+ export interface ReceiptNode {
399
+ id: string;
400
+ title: string;
401
+ reason?: string;
402
+ }
403
+
404
+ export interface SessionContextActivity {
405
+ read: ReceiptNode[];
406
+ pinned: ReceiptNode[];
407
+ created: ReceiptNode[];
408
+ edited: ReceiptNode[];
409
+ changedSinceRead: ReceiptNode[];
410
+ }
411
+
412
+ function receiptNodes(value: unknown): ReceiptNode[] {
413
+ if (!Array.isArray(value)) return [];
414
+ return value.flatMap((entry) => {
415
+ if (!entry || typeof entry !== 'object') return [];
416
+ const record = entry as Record<string, unknown>;
417
+ if (typeof record.id !== 'string' || typeof record.title !== 'string') return [];
418
+ return [
419
+ { id: record.id, title: record.title, ...(typeof record.reason === 'string' ? { reason: record.reason } : {}) },
420
+ ];
421
+ });
422
+ }
423
+
424
+ /**
425
+ * The agent-activity lens: while on, nodes the last session did not touch
426
+ * (read, pin, create, edit) are dimmed. Null when off.
427
+ */
428
+ export const activityLensNodeIds = signal<Set<string> | null>(null);
429
+
430
+ export function setActivityLens(on: boolean): void {
431
+ const context = sessionReceipt.value?.context;
432
+ activityLensNodeIds.value =
433
+ on && context
434
+ ? new Set([...context.read, ...context.pinned, ...context.created, ...context.edited].map((node) => node.id))
435
+ : null;
385
436
  }
386
437
 
387
438
  /** The last ended session's receipt (design item 2); client-side, cleared on dismiss. */
388
439
  export const sessionReceipt = signal<SessionReceipt | null>(null);
389
440
 
441
+ function unionNodes(a: ReceiptNode[], b: ReceiptNode[]): ReceiptNode[] {
442
+ const seen = new Set(a.map((node) => node.id));
443
+ return [...a, ...b.filter((node) => !seen.has(node.id))];
444
+ }
445
+
446
+ /**
447
+ * Endings that arrive while a receipt is still up fold into it, so a burst of
448
+ * sessions shows one card. A merged card carries no single snapshot — no one
449
+ * snapshot undoes overlapping sessions — and points at History instead.
450
+ */
451
+ function mergeReceipts(open: SessionReceipt, next: SessionReceipt): SessionReceipt {
452
+ return {
453
+ labels: [...new Set([...open.labels, ...next.labels])],
454
+ sessions: open.sessions + 1,
455
+ endedAt: next.endedAt,
456
+ endedBy: open.endedBy === next.endedBy ? next.endedBy : undefined,
457
+ // Board-wide counts: the latest frame is the current board.
458
+ counts: next.counts,
459
+ snapshot: null,
460
+ context: {
461
+ read: unionNodes(open.context.read, next.context.read),
462
+ pinned: unionNodes(open.context.pinned, next.context.pinned),
463
+ created: unionNodes(open.context.created, next.context.created),
464
+ edited: unionNodes(open.context.edited, next.context.edited),
465
+ changedSinceRead: unionNodes(open.context.changedSinceRead, next.context.changedSinceRead),
466
+ },
467
+ };
468
+ }
469
+
470
+ /**
471
+ * The receipt rule: a pop-up only for a top-level session that changed the
472
+ * board or read/pinned context. `unchanged` endings and worker endings
473
+ * (`parentAgentId` set — their orchestrator's receipt covers the board since
474
+ * it attached) stay in the timeline and History only, and never replace or
475
+ * extend an open receipt. Qualifying endings while one is up merge into it.
476
+ */
390
477
  export function applySessionReceipt(data: Record<string, unknown>): void {
391
478
  const counts = data.counts as Partial<SessionReceipt['counts']> | undefined;
392
479
  const snapshot = data.snapshot as SessionReceipt['snapshot'] | undefined;
480
+ const context = data.context as Record<string, unknown> | undefined;
393
481
  if (typeof data.label !== 'string' || typeof data.endedAt !== 'string') return;
394
- // Keep history/timeline intact, but only interrupt for a changed top-level
395
- // session. Quiet endings must not replace an existing useful receipt.
396
482
  if (data.unchanged === true || typeof data.parentAgentId === 'string') return;
397
- sessionReceipt.value = {
398
- label: data.label,
483
+ const next: SessionReceipt = {
484
+ labels: [data.label],
485
+ sessions: 1,
399
486
  endedAt: data.endedAt,
400
487
  endedBy:
401
488
  data.endedBy === 'human' || data.endedBy === 'agent' || data.endedBy === 'idle-timeout'
402
489
  ? data.endedBy
403
490
  : undefined,
404
- unchanged: data.unchanged === true,
405
491
  counts: {
406
492
  items: Number(counts?.items ?? 0) || 0,
407
493
  done: Number(counts?.done ?? 0) || 0,
@@ -411,11 +497,22 @@ export function applySessionReceipt(data: Record<string, unknown>): void {
411
497
  },
412
498
  snapshot:
413
499
  snapshot && typeof snapshot.id === 'string' ? { id: snapshot.id, name: String(snapshot.name ?? '') } : null,
500
+ context: {
501
+ read: receiptNodes(context?.read),
502
+ pinned: receiptNodes(context?.pinned),
503
+ created: receiptNodes(context?.created),
504
+ edited: receiptNodes(context?.edited),
505
+ changedSinceRead: receiptNodes(context?.changedSinceRead),
506
+ },
414
507
  };
508
+ const open = sessionReceipt.value;
509
+ sessionReceipt.value = open ? mergeReceipts(open, next) : next;
510
+ if (open && activityLensNodeIds.value) setActivityLens(true);
415
511
  }
416
512
 
417
513
  export function dismissSessionReceipt(): void {
418
514
  sessionReceipt.value = null;
515
+ activityLensNodeIds.value = null;
419
516
  }
420
517
 
421
518
  export function resetSessionStore(): void {
@@ -1,7 +1,8 @@
1
1
  import { findOpenCanvasPosition } from '../utils/placement.js';
2
2
  import { ownsCamera } from './presentation';
3
+ import { isHostedWorkbench, workbenchFetch } from './workbench-transport';
3
4
  import { normalizeExtAppToolResult } from '../utils/ext-app-tool-result.js';
4
- import type { CanvasAnnotation, CanvasEdge, CanvasNodeState } from '../types';
5
+ import type { CanvasAnnotation, CanvasEdge, CanvasNodeState, NodeActor } from '../types';
5
6
  import {
6
7
  activeNodeId,
7
8
  addEdge,
@@ -30,6 +31,7 @@ import {
30
31
  resetCanvasInteractionState,
31
32
  } from './canvas-store';
32
33
  import { applyBoards, loadBoards, setBoardSwitchHandler } from './boards-store';
34
+ import { applyContextStatus, refreshContextStatus } from './context-status-store';
33
35
  import {
34
36
  fetchAgentPresence,
35
37
  fetchAxSurfaceState,
@@ -75,6 +77,7 @@ let pollGeneration = 0;
75
77
  let sseFirstEventWatchdog: ReturnType<typeof setTimeout> | null = null;
76
78
 
77
79
  export function forcedTransport(): 'poll' | 'sse' | null {
80
+ if (isHostedWorkbench()) return 'poll';
78
81
  if (typeof location === 'undefined' || typeof window === 'undefined') return null;
79
82
  const value = new URLSearchParams(location.search).get('transport');
80
83
  if (value === 'poll' || value === 'sse') return value;
@@ -365,10 +368,19 @@ function parseCanvasNode(raw: Record<string, unknown>): CanvasNodeState | null {
365
368
  zIndex: typeof raw.zIndex === 'number' ? raw.zIndex : 1,
366
369
  collapsed: raw.collapsed === true,
367
370
  pinned: raw.pinned === true,
371
+ contentRevision: typeof raw.contentRevision === 'number' ? raw.contentRevision : 0,
372
+ ...(isActor(raw.createdBy) ? { createdBy: raw.createdBy } : {}),
373
+ ...(isActor(raw.lastEditedBy) ? { lastEditedBy: raw.lastEditedBy } : {}),
368
374
  data,
369
375
  };
370
376
  }
371
377
 
378
+ function isActor(value: unknown): value is NodeActor {
379
+ if (!value || typeof value !== 'object') return false;
380
+ const record = value as Record<string, unknown>;
381
+ return typeof record.actor === 'string' && typeof record.source === 'string';
382
+ }
383
+
372
384
  function parseCanvasEdge(raw: Record<string, unknown>): CanvasEdge | null {
373
385
  if (typeof raw.id !== 'string' || !raw.id) return null;
374
386
  if (typeof raw.from !== 'string' || !raw.from) return null;
@@ -481,6 +493,7 @@ function resyncForBoardSwitch(): void {
481
493
  resetCanvasInteractionState();
482
494
  resetAttentionBridge();
483
495
  dismissSessionReceipt();
496
+ applyContextStatus({});
484
497
  axSurfaceState.value = null;
485
498
  void refreshAxSurface();
486
499
  const layoutAtRequest = layoutRevision;
@@ -499,21 +512,21 @@ setBoardSwitchHandler(resyncForBoardSwitch);
499
512
  function handleConnected(data: Record<string, unknown>): void {
500
513
  sessionId.value = (data.sessionId as string) || '';
501
514
  connectionStatus.value = 'connected';
502
- void loadBoards();
515
+ // Hosted writes capture the active board. Load its identity before reporting
516
+ // the viewport, rather than sending a spurious Home precondition on boot.
517
+ void loadBoards().then(reportClientViewportSize);
503
518
  // Reconnect marker for holders of server-minted URLs (Finding S).
504
519
  workbenchConnectionEpoch.value += 1;
505
520
  reloadIfServerUpgraded(data.version, data.bundleStamp);
506
- // Tell the server how big this window actually is, so an agent `fit` sizes
507
- // the board to the human's window (0.4.6 orb feedback #2).
508
- void reportClientViewportSize();
509
521
  // Agent presence: read the snapshot on (re)connect; `agent-presence` frames
510
522
  // keep it live from here on.
511
523
  void fetchAgentPresence().then(applyPresenceSnapshot);
524
+ void refreshContextStatus();
512
525
  // Human collaborators (phase 8): announce this tab and read who else is here.
513
526
  // An `?agent=<key>` tab is an AGENT's view — it must not heartbeat a human
514
527
  // guest cursor (its writes already book as the agent).
515
528
  stopHumanPresence?.();
516
- if (!tabAgentKey) stopHumanPresence = startHumanPresence();
529
+ if (!tabAgentKey && !isHostedWorkbench()) stopHumanPresence = startHumanPresence();
517
530
  void requestJson<Partial<HumanPresenceSnapshot> | null>(
518
531
  'fetchHumanPresence',
519
532
  '/api/canvas/human-presence',
@@ -1033,6 +1046,11 @@ function handleContextPinsChanged(data: Record<string, unknown>): void {
1033
1046
  const nodeIds = Array.isArray(data.nodeIds) ? data.nodeIds.filter((id): id is string => typeof id === 'string') : [];
1034
1047
  replaceContextPinsFromServer(nodeIds);
1035
1048
  syncAttentionFromSse({ event: 'context-pins-changed', data });
1049
+ void refreshContextStatus();
1050
+ }
1051
+
1052
+ function handleContextStatusChanged(): void {
1053
+ void refreshContextStatus();
1036
1054
  }
1037
1055
 
1038
1056
  // AX state changes arrive as per-primitive deltas; rather than reduce them, treat
@@ -1109,6 +1127,7 @@ export const EVENT_HANDLERS: Record<string, (data: Record<string, unknown>) => v
1109
1127
  'ext-app-update': handleExtAppUpdate,
1110
1128
  'ext-app-result': handleExtAppResult,
1111
1129
  'context-pins-changed': handleContextPinsChanged,
1130
+ 'context-status-changed': handleContextStatusChanged,
1112
1131
  'boards-changed': applyBoards,
1113
1132
  'canvas-layout-update': handleCanvasLayoutUpdate,
1114
1133
  'canvas-focus-node': handleCanvasFocusNode,
@@ -1170,7 +1189,7 @@ function startPollingTransport(): () => void {
1170
1189
  const sid = sessionId.value;
1171
1190
  if (sid) params.set('session', sid);
1172
1191
  const query = params.toString();
1173
- const response = await fetch(`/api/workbench/poll${query ? `?${query}` : ''}`);
1192
+ const response = await workbenchFetch(`/api/workbench/poll${query ? `?${query}` : ''}`);
1174
1193
  if (generation !== pollGeneration) return;
1175
1194
  if (!response.ok) throw new Error(`poll failed with HTTP ${response.status}`);
1176
1195
  const body = (await response.json()) as {
@@ -76,7 +76,7 @@ export function initSessionThemeOverride(apply: (theme: CanvasThemeName) => void
76
76
  }
77
77
  schemeQuery = window.matchMedia('(prefers-color-scheme: light)');
78
78
  schemeListener = () => {
79
- if (active && schemeQuery) apply(schemeQuery.matches ? 'light' : 'dark');
79
+ if (active && schemeQuery) apply(schemeQuery.matches ? 'daylight' : 'harbor');
80
80
  };
81
81
  schemeListener();
82
82
  schemeQuery.addEventListener('change', schemeListener);
@@ -0,0 +1,36 @@
1
+ /** The normal workbench uses HTTP; an MCP App installs its host transport before mounting. */
2
+ type WorkbenchFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
3
+ let hostFetch: WorkbenchFetch | undefined;
4
+
5
+ export function installWorkbenchTransport(transport: WorkbenchFetch | undefined): void {
6
+ hostFetch = transport;
7
+ }
8
+
9
+ export function isHostedWorkbench(): boolean {
10
+ return hostFetch !== undefined;
11
+ }
12
+
13
+ export const workbenchFetch: WorkbenchFetch = (input, init) =>
14
+ hostFetch ? hostFetch(input, init) : globalThis.fetch(input, init);
15
+
16
+ type WorkbenchDownload = (file: { name: string; mimeType: string; text: string }) => Promise<{ isError?: boolean }>;
17
+ let hostDownload: WorkbenchDownload | undefined;
18
+
19
+ export function installWorkbenchDownload(download: WorkbenchDownload | undefined): void {
20
+ hostDownload = download;
21
+ }
22
+
23
+ /** Export files live on PMX, not on the embedded frame's sandbox origin. */
24
+ export async function downloadWorkbenchExport(path: string): Promise<void> {
25
+ if (!hostDownload)
26
+ throw new Error('This host does not support downloads. The export is saved in the local PMX workspace.');
27
+ if (!/^\/api\/canvas\/exports\/[^/?#]+$/.test(path)) throw new Error('Invalid export path.');
28
+ const response = await workbenchFetch(path);
29
+ if (!response.ok) throw new Error('Could not read the export. Try again.');
30
+ const result = await hostDownload({
31
+ name: decodeURIComponent(path.slice(path.lastIndexOf('/') + 1)),
32
+ mimeType: 'text/html',
33
+ text: await response.text(),
34
+ });
35
+ if (result.isError) throw new Error('The download was cancelled or declined by the host.');
36
+ }