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.
- package/.github/extensions/pmx-canvas/extension.mjs +9 -3
- package/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/start-mcp.mjs +29 -29
- package/CHANGELOG.md +19 -0
- package/Readme.md +1 -1
- package/dist/canvas/global.css +12 -17
- package/dist/canvas/index.js +58 -58
- package/dist/canvas/mcp-app.html +75 -80
- package/dist/json-render/index.js +50 -50
- package/dist/types/client/canvas/submit-without-navigation.d.ts +3 -0
- package/dist/types/client/state/intent-bridge.d.ts +0 -3
- package/dist/types/client/state/presence-store.d.ts +3 -3
- package/dist/types/client/types.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/index.d.ts +2 -2
- package/dist/types/server/knowledge-lifecycle.d.ts +11 -2
- package/dist/types/server/lifecycle-status.d.ts +3 -1
- package/dist/types/server/tour-control.d.ts +6 -3
- package/docs/bun-webview-integration.md +2 -2
- package/docs/cli.md +10 -1
- package/docs/design/Chrome.dc.html +1 -0
- package/docs/design.md +3 -1
- package/docs/mcp.md +5 -1
- package/docs/node-types.md +3 -0
- package/docs/plans/plan-019-post-0.9-roadmap.md +68 -9
- package/docs/sdk.md +7 -0
- package/package.json +2 -2
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +9 -3
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +1 -1
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/start-mcp.mjs +29 -29
- package/skills/pmx-canvas/SKILL.md +4 -4
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +11 -6
- package/src/client/canvas/BoardCopyDialog.tsx +2 -1
- package/src/client/canvas/KnowledgeDialog.tsx +3 -0
- package/src/client/canvas/LifecyclePanel.tsx +4 -0
- package/src/client/canvas/RelationPresetsDialog.tsx +2 -0
- package/src/client/canvas/submit-without-navigation.ts +14 -0
- package/src/client/state/canvas-store.ts +12 -24
- package/src/client/state/intent-bridge.ts +0 -6
- package/src/client/state/presence-store.ts +3 -3
- package/src/client/state/sse-bridge.ts +11 -6
- package/src/client/theme/global.css +12 -17
- package/src/server/canvas-state.ts +10 -0
- package/src/server/index.ts +10 -30
- package/src/server/knowledge-lifecycle.ts +66 -0
- package/src/server/lifecycle-status.ts +79 -7
- package/src/server/operations/ops/lifecycle.ts +3 -7
- package/src/server/operations/ops/nodes.ts +2 -5
- package/src/server/operations/ops/viewport.ts +3 -7
- package/src/server/server.ts +13 -10
- 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
|
|
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
|
-
|
|
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 } =
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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
|
|
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 ────────────────────────
|
package/src/server/server.ts
CHANGED
|
@@ -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
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
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():
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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,
|
|
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
|
}
|