pmx-canvas 0.9.1 → 0.9.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 (54) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +9 -3
  2. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  3. package/.github/extensions/pmx-canvas/start-mcp.mjs +29 -29
  4. package/CHANGELOG.md +19 -0
  5. package/Readme.md +1 -1
  6. package/dist/canvas/global.css +12 -17
  7. package/dist/canvas/index.js +58 -58
  8. package/dist/canvas/mcp-app.html +75 -80
  9. package/dist/json-render/index.js +50 -50
  10. package/dist/types/client/canvas/submit-without-navigation.d.ts +3 -0
  11. package/dist/types/client/state/intent-bridge.d.ts +0 -3
  12. package/dist/types/client/state/presence-store.d.ts +3 -3
  13. package/dist/types/client/types.d.ts +1 -1
  14. package/dist/types/server/canvas-state.d.ts +2 -0
  15. package/dist/types/server/index.d.ts +2 -2
  16. package/dist/types/server/knowledge-lifecycle.d.ts +11 -2
  17. package/dist/types/server/lifecycle-status.d.ts +3 -1
  18. package/dist/types/server/tour-control.d.ts +6 -3
  19. package/docs/bun-webview-integration.md +2 -2
  20. package/docs/cli.md +10 -1
  21. package/docs/design/Chrome.dc.html +1 -0
  22. package/docs/design.md +3 -1
  23. package/docs/mcp.md +5 -1
  24. package/docs/node-types.md +3 -0
  25. package/docs/plans/plan-019-post-0.9-roadmap.md +68 -9
  26. package/docs/sdk.md +7 -0
  27. package/package.json +2 -2
  28. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +9 -3
  29. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  30. package/plugins/pmx-canvas/plugin.json +1 -1
  31. package/plugins/pmx-canvas/runtime.json +1 -1
  32. package/plugins/pmx-canvas/start-mcp.mjs +29 -29
  33. package/skills/pmx-canvas/SKILL.md +4 -4
  34. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  35. package/skills/pmx-canvas/references/installing-pmx-canvas.md +11 -6
  36. package/src/client/canvas/BoardCopyDialog.tsx +2 -1
  37. package/src/client/canvas/KnowledgeDialog.tsx +3 -0
  38. package/src/client/canvas/LifecyclePanel.tsx +4 -0
  39. package/src/client/canvas/RelationPresetsDialog.tsx +2 -0
  40. package/src/client/canvas/submit-without-navigation.ts +14 -0
  41. package/src/client/state/canvas-store.ts +12 -24
  42. package/src/client/state/intent-bridge.ts +0 -6
  43. package/src/client/state/presence-store.ts +3 -3
  44. package/src/client/state/sse-bridge.ts +11 -6
  45. package/src/client/theme/global.css +12 -17
  46. package/src/server/canvas-state.ts +10 -0
  47. package/src/server/index.ts +10 -30
  48. package/src/server/knowledge-lifecycle.ts +66 -0
  49. package/src/server/lifecycle-status.ts +79 -7
  50. package/src/server/operations/ops/lifecycle.ts +3 -7
  51. package/src/server/operations/ops/nodes.ts +2 -5
  52. package/src/server/operations/ops/viewport.ts +3 -7
  53. package/src/server/server.ts +13 -10
  54. package/src/server/tour-control.ts +17 -8
@@ -25,8 +25,70 @@ export function lifecycleQualifications(
25
25
  return lifecycleSnapshot(now, atUse).cards;
26
26
  }
27
27
 
28
+ /** A card being inspected is in use, including its transitive sources and portal contents. */
29
+ export function lifecycleQualification(target: MemoryTarget, now = Date.now()): LifecycleQualification | undefined {
30
+ return lifecycleSnapshot(now, [target], target).cards[memoryKey(target)];
31
+ }
32
+
33
+ /** Read closure boards once; never activate them or flush the active board to discover edges. */
34
+ function dependencyClosure(target: MemoryTarget) {
35
+ const reads = new Map<string, ReturnType<typeof canvasState.readBoard>>();
36
+ const nodes = new Map<string, CanvasNodeState>();
37
+ const dependencies = new Map<string, MemoryTarget[]>();
38
+ const selected = new Set<string>();
39
+ const queue: MemoryTarget[] = [target];
40
+ const read = (boardId: string) => {
41
+ if (!reads.has(boardId)) {
42
+ const board = canvasState.readBoard(boardId, false);
43
+ reads.set(boardId, board);
44
+ for (const node of board?.layout.nodes ?? []) nodes.set(memoryKey({ boardId, cardId: node.id }), node);
45
+ for (const edge of board?.layout.edges ?? []) {
46
+ const end = relationSourceEnd(edge);
47
+ if (!end) continue;
48
+ const from = { boardId, cardId: edge.from };
49
+ const to = { boardId: edge.toBoardId ?? boardId, cardId: edge.to };
50
+ const key = memoryKey(end === 'from' ? to : from);
51
+ const sources = dependencies.get(key) ?? [];
52
+ sources.push(end === 'from' ? from : to);
53
+ dependencies.set(key, sources);
54
+ }
55
+ }
56
+ return reads.get(boardId);
57
+ };
58
+ const activeBoardId = canvasState.activeBoardId;
59
+ const incomingActive = new Set(
60
+ canvasState
61
+ .getEdges()
62
+ .flatMap((edge) =>
63
+ edge.toBoardId && relationSourceEnd(edge) === 'from'
64
+ ? [memoryKey({ boardId: edge.toBoardId, cardId: edge.to })]
65
+ : [],
66
+ ),
67
+ );
68
+ for (const current of queue) {
69
+ const key = memoryKey(current);
70
+ if (selected.has(key)) continue;
71
+ selected.add(key);
72
+ const board = read(current.boardId);
73
+ if (!current.cardId) {
74
+ for (const node of board?.layout.nodes ?? []) queue.push({ boardId: current.boardId, cardId: node.id });
75
+ continue;
76
+ }
77
+ const node = nodes.get(key);
78
+ if (!node) continue;
79
+ const provenance = normalizeCanvasNodeData(node.type, node.data).provenance as CanvasNodeProvenance | undefined;
80
+ queue.push(...(provenance?.sources ?? []).map(({ boardId, cardId }) => ({ boardId, cardId })));
81
+ if (node.type === 'board' && typeof node.data.boardId === 'string') queue.push({ boardId: node.data.boardId });
82
+ const owners = new Set([current.boardId, ...canvasState.knowledge.incomingSourceBoards(current, activeBoardId)]);
83
+ if (activeBoardId && incomingActive.has(key)) owners.add(activeBoardId);
84
+ for (const owner of owners) read(owner);
85
+ queue.push(...(dependencies.get(key) ?? []));
86
+ }
87
+ return { reads, selected };
88
+ }
89
+
28
90
  /** Compact queue metadata and qualifications come from the same authoritative read. */
29
- export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = []) {
91
+ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [], only?: MemoryTarget) {
30
92
  const result: Record<string, LifecycleQualification> = {};
31
93
  const cards = new Map<string, CanvasNodeState>();
32
94
  const validations = new Map<string, ReviewRecord | null>();
@@ -34,26 +96,36 @@ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [])
34
96
  const portals = new Map<string, string>();
35
97
  const policies = new Map<string, Lifecycle | null>();
36
98
  const used = new Set<string>();
37
- const reads = canvasState.listBoards().map((board) => ({ board, read: canvasState.readBoard(board.id, false) }));
99
+ const boards = canvasState.listBoards();
100
+ const closure = only ? dependencyClosure(only) : null;
101
+ const reads = boards
102
+ .filter((board) => !closure || closure.reads.has(board.id))
103
+ .map((board) => ({
104
+ board,
105
+ read: closure ? closure.reads.get(board.id) : canvasState.readBoard(board.id, false),
106
+ }));
107
+ const targets = reads
108
+ .flatMap(({ board, read }) => (read?.layout.nodes ?? []).map((node) => ({ boardId: board.id, cardId: node.id })))
109
+ .filter((target) => !closure || closure.selected.has(memoryKey(target)));
110
+ const summaries = canvasState.databasePath ? canvasState.knowledge.reviewSummaries(targets) : null;
38
111
  const catalog = canvasState.databasePath
39
112
  ? canvasState.knowledge.list()
40
113
  : { categories: [], folders: [], relationPresets: [] };
41
114
  const session =
42
115
  agentPresence.reviewSessionStartedAt() ??
43
116
  Date.parse(
44
- reads.find(({ board }) => board.id === canvasState.activeBoardId)?.board.lastOpenedAt ??
117
+ boards.find((board) => board.id === canvasState.activeBoardId)?.lastOpenedAt ??
45
118
  new Date(processStartedAt).toISOString(),
46
119
  );
47
120
  for (const { board, read } of reads) {
48
121
  const seen = new Map(canvasState.getNodeReadStatus(board.id).map((status) => [status.nodeId, status]));
49
122
  for (const node of read?.layout.nodes ?? []) {
50
123
  const key = memoryKey({ boardId: board.id, cardId: node.id });
124
+ if (closure && !closure.selected.has(key)) continue;
51
125
  cards.set(key, node);
52
126
  const { lifecycle } = canvasState.knowledge.resolveCard(board.folder, node, catalog);
53
127
  policies.set(key, lifecycle);
54
- const { lastValidation, retired } = canvasState.databasePath
55
- ? canvasState.knowledge.reviews(board.id, node.id)
56
- : { lastValidation: null, retired: false };
128
+ const { lastValidation, retired } = summaries?.get(key) ?? { lastValidation: null, retired: false };
57
129
  validations.set(key, lastValidation);
58
130
  const lastRead = seen.get(node.id);
59
131
  const pinned = read!.state.contextPins.includes(node.id);
@@ -218,7 +290,7 @@ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [])
218
290
  for (const { board, read } of reads) {
219
291
  for (const node of read?.layout.nodes ?? []) {
220
292
  const status = result[memoryKey({ boardId: board.id, cardId: node.id })];
221
- if (status.retired || !(status.due || status.sources.length)) continue;
293
+ if (!status || status.retired || !(status.due || status.sources.length)) continue;
222
294
  reviewCards.push({
223
295
  boardId: board.id,
224
296
  cardId: node.id,
@@ -3,8 +3,7 @@ import { canvasState } from '../../canvas-state.js';
3
3
  import { categorySchema, folderPropertiesSchema, lifecycleSchema } from '../../knowledge-lifecycle.js';
4
4
  import { defineOperation, OperationError, type Operation } from '../types.js';
5
5
  import { readTargetBoard } from './boards.js';
6
- import { lifecycleQualifications, lifecycleSnapshot } from '../../lifecycle-status.js';
7
- import { memoryKey } from '../../../shared/memory-graph.js';
6
+ import { lifecycleQualification, lifecycleSnapshot } from '../../lifecycle-status.js';
8
7
  import { folderAncestors } from '../../../shared/boards.js';
9
8
  import { relationPresetSchema } from '../../../shared/relations.js';
10
9
 
@@ -155,9 +154,7 @@ export const lifecycleOperations: Operation[] = [
155
154
  contentRevision: node.contentRevision ?? 0,
156
155
  ...canvasState.knowledge.resolveCard(folder, node),
157
156
  ...canvasState.knowledge.reviews(read.boardId, input.id),
158
- qualification: lifecycleQualifications(Date.now(), [{ boardId: read.boardId, cardId: input.id }])[
159
- memoryKey({ boardId: read.boardId, cardId: input.id })
160
- ],
157
+ qualification: lifecycleQualification({ boardId: read.boardId, cardId: input.id }),
161
158
  };
162
159
  },
163
160
  }),
@@ -216,8 +213,7 @@ export const lifecycleOperations: Operation[] = [
216
213
  if (!canvasState.getNode(input.id) || !canvasState.activeBoardId)
217
214
  throw new OperationError('Card not found.', 404);
218
215
  const observations =
219
- lifecycleQualifications()[memoryKey({ boardId: canvasState.activeBoardId, cardId: input.id })]?.observations ??
220
- {};
216
+ lifecycleQualification({ boardId: canvasState.activeBoardId, cardId: input.id })?.observations ?? {};
221
217
  const validates =
222
218
  input.applied && input.evidence.trim() && ['retain', 'revise', 'externalize'].includes(input.outcome);
223
219
  if (validates) {
@@ -55,8 +55,7 @@ import {
55
55
  import { WEBPAGE_NODE_DEFAULT_SIZE, normalizeWebpageUrl } from '../../webpage-node.js';
56
56
  import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
57
57
  import { readTargetBoard } from './boards.js';
58
- import { lifecycleQualifications } from '../../lifecycle-status.js';
59
- import { memoryKey } from '../../../shared/memory-graph.js';
58
+ import { lifecycleQualification } from '../../lifecycle-status.js';
60
59
 
61
60
  // ── Node types ────────────────────────────────────────────────
62
61
  // Single definition site for the basic node-type list: drives the HTTP type
@@ -1005,9 +1004,7 @@ const nodeGetOperation = defineOperation<z.infer<typeof nodeGetSchema>, Serializ
1005
1004
  const full = includeBlobs === true || includeBlobs === 'true';
1006
1005
  const qualify = (node: CanvasNodeState, boardId: string | null) => ({
1007
1006
  ...(full ? serializeCanvasNode(node) : serializeCanvasNodeWithBlobSummaries(node)),
1008
- qualification: boardId
1009
- ? lifecycleQualifications(Date.now(), [{ boardId, cardId: id }])[memoryKey({ boardId, cardId: id })]
1010
- : undefined,
1007
+ qualification: boardId ? lifecycleQualification({ boardId, cardId: id }) : undefined,
1011
1008
  });
1012
1009
  if (typeof board === 'string' && board.trim()) {
1013
1010
  const target = readTargetBoard(board, full);
@@ -13,7 +13,7 @@
13
13
  * This module must never import server.ts or index.ts.
14
14
  */
15
15
  import { z } from 'zod';
16
- import { tourSchema, derivedTour, rectSchema, cameraSchema, tourStopSchema } from '../../../shared/tour.js';
16
+ import { tourSchema, rectSchema, cameraSchema, tourStopSchema } from '../../../shared/tour.js';
17
17
  import { canvasState } from '../../canvas-state.js';
18
18
  import {
19
19
  arrangeCanvasNodes,
@@ -23,7 +23,7 @@ import {
23
23
  setClientViewportSize,
24
24
  } from '../../canvas-operations.js';
25
25
  import { validateCanvasLayout } from '../../canvas-validation.js';
26
- import { exitTour, goToTourStop, moveCamera, tourPosition } from '../../tour-control.js';
26
+ import { currentTour, exitTour, goToTourStop, moveCamera } from '../../tour-control.js';
27
27
  import { defineOperation, OperationError, type Operation } from '../types.js';
28
28
  import { closeNodeAppSession, isRecord } from './nodes.js';
29
29
 
@@ -329,11 +329,7 @@ const tourGetOperation = defineOperation({
329
329
  toolName: 'canvas_get_tour',
330
330
  description: 'Read the board tour, deriving groups in reading order when absent.',
331
331
  },
332
- handler: () => ({
333
- tour: canvasState.getTour() ?? derivedTour(canvasState.getLayout().nodes),
334
- derived: !canvasState.getTour(),
335
- position: tourPosition(),
336
- }),
332
+ handler: () => currentTour(),
337
333
  });
338
334
 
339
335
  // ── tour.go / tour.exit / camera.move ────────────────────────
@@ -2483,16 +2483,19 @@ async function handleRender(req: Request): Promise<Response> {
2483
2483
  // The editor sends its draft through this same endpoint. Only changed/unsaved
2484
2484
  // text may resolve titles; persisted text must replay even unresolved bindings.
2485
2485
  const persisted = node?.type === 'markdown' && node.data.content === markdown;
2486
- const initializedRefs =
2487
- node?.type === 'markdown' && node.data.wikiLinks !== undefined && typeof node.data.content === 'string'
2488
- ? wikiReferences(node.data.content)
2489
- : [];
2490
- const refs = wikiReferences(markdown).filter(
2491
- (ref) => !initializedRefs.includes(ref) || previous.some((link) => link.ref === ref),
2492
- );
2493
- const needsResolution = !persisted && refs.some((ref) => !previous.some((link) => link.ref === ref && link.target));
2494
- const catalog = needsResolution ? canvasState.wikiCatalog() : [];
2495
- const links = persisted ? previous : refs.map((ref) => resolveWikiLinks(`[[${ref}]]`, previous, catalog)[0]);
2486
+ let links = previous;
2487
+ if (!persisted) {
2488
+ const initializedRefs =
2489
+ node?.type === 'markdown' && node.data.wikiLinks !== undefined && typeof node.data.content === 'string'
2490
+ ? wikiReferences(node.data.content)
2491
+ : [];
2492
+ const refs = wikiReferences(markdown).filter(
2493
+ (ref) => !initializedRefs.includes(ref) || previous.some((link) => link.ref === ref),
2494
+ );
2495
+ const needsResolution = refs.some((ref) => !previous.some((link) => link.ref === ref && link.target));
2496
+ const catalog = needsResolution ? canvasState.wikiCatalog() : [];
2497
+ links = refs.map((ref) => resolveWikiLinks(`[[${ref}]]`, previous, catalog)[0]);
2498
+ }
2496
2499
  const targets = new Map<string, ReturnType<typeof canvasState.readBoard>>();
2497
2500
  const html = renderWikiMarkdown(normalizeMarkdownExternalUrls(markdown), links, (target) => {
2498
2501
  if (!targets.has(target.boardId)) targets.set(target.boardId, canvasState.readBoard(target.boardId, false));
@@ -21,14 +21,22 @@ export interface TourStepResult {
21
21
  stop: TourStop;
22
22
  }
23
23
 
24
+ export interface TourState {
25
+ tour: Tour;
26
+ derived: boolean;
27
+ position: number | null;
28
+ }
29
+
24
30
  let cursor: { boardGeneration: number; tour: string; index: number } | null = null;
25
31
 
26
- export function currentTour(): Tour {
27
- return canvasState.getTour() ?? derivedTour(canvasState.getLayout().nodes);
32
+ export function currentTour(): TourState {
33
+ const saved = canvasState.getTour();
34
+ const tour = saved ?? derivedTour(canvasState.nodeGeometry());
35
+ return { tour, derived: !saved, position: tourPosition(tour) };
28
36
  }
29
37
 
30
38
  /** The stop the tour is on for the active board, or null when not touring. */
31
- export function tourPosition(tour: Tour = currentTour()): number | null {
39
+ function tourPosition(tour: Tour): number | null {
32
40
  if (cursor && (cursor.boardGeneration !== canvasState.boardGeneration || cursor.tour !== JSON.stringify(tour))) {
33
41
  cursor = null;
34
42
  }
@@ -37,9 +45,8 @@ export function tourPosition(tour: Tour = currentTour()): number | null {
37
45
 
38
46
  /** Move the shared cursor. `next`/`previous` clamp at the ends; an explicit index must exist. */
39
47
  export function goToTourStop(step: TourStep): TourStepResult {
40
- const tour = currentTour();
48
+ const { tour, position: at } = currentTour();
41
49
  const { stops } = tour;
42
- const at = tourPosition(tour);
43
50
  if (stops.length === 0) {
44
51
  throw new OperationError('This board has no tour stops. Save a tour or add groups.', 400);
45
52
  }
@@ -67,13 +74,15 @@ export function moveCamera(input: unknown): { stop: TourStop; viewport: Camera }
67
74
  throw new OperationError('Give exactly one of nodeId, rect {x,y,width,height} or viewport {x,y,scale}.', 400);
68
75
  }
69
76
  const stop = parsed.data;
70
- if ('nodeId' in stop.target && !canvasState.getNode(stop.target.nodeId)) {
71
- throw new OperationError(`Node "${stop.target.nodeId}" not found.`, 404);
77
+ const nodes = 'nodeId' in stop.target ? canvasState.nodeGeometry() : [];
78
+ const target = stop.target;
79
+ if ('nodeId' in target && !nodes.some((node) => node.id === target.nodeId)) {
80
+ throw new OperationError(`Node "${target.nodeId}" not found.`, 404);
72
81
  }
73
82
  const size = getClientViewportSize() ?? { width: 1440, height: 900 };
74
83
  let viewport: Camera;
75
84
  try {
76
- viewport = resolveStop(stop, canvasState.getLayout().nodes, size.width, size.height);
85
+ viewport = resolveStop(stop, nodes, size.width, size.height);
77
86
  } catch (error) {
78
87
  throw new OperationError(error instanceof Error ? error.message : String(error), 400);
79
88
  }