pmx-canvas 0.8.2 → 0.9.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.
- package/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/tools.json +601 -22
- package/CHANGELOG.md +49 -0
- package/Readme.md +131 -532
- package/dist/canvas/global.css +2724 -476
- package/dist/canvas/index.js +168 -107
- package/dist/canvas/mcp-app.html +2899 -596
- package/dist/json-render/index.js +20 -20
- package/dist/types/client/canvas/BarHint.d.ts +12 -2
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
- package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandBar.d.ts +3 -4
- package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
- package/dist/types/client/canvas/ContextChip.d.ts +3 -0
- package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
- package/dist/types/client/canvas/EmptyState.d.ts +4 -4
- package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
- package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
- package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -7
- package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
- package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
- package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
- package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
- package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
- package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
- package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
- package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
- package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
- package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
- package/dist/types/client/canvas/canvas-area.d.ts +1 -1
- package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
- package/dist/types/client/canvas/graph-model.d.ts +37 -0
- package/dist/types/client/canvas/memory-layout.d.ts +23 -0
- package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
- package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
- package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
- package/dist/types/client/icons.d.ts +0 -10
- package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
- package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
- package/dist/types/client/state/boards-store.d.ts +81 -9
- package/dist/types/client/state/canvas-store.d.ts +14 -1
- package/dist/types/client/state/context-chip-store.d.ts +22 -0
- package/dist/types/client/state/context-status-store.d.ts +35 -0
- package/dist/types/client/state/intent-bridge.d.ts +11 -8
- package/dist/types/client/state/knowledge-store.d.ts +22 -0
- package/dist/types/client/state/near-pin-store.d.ts +44 -1
- package/dist/types/client/state/presence-store.d.ts +4 -4
- package/dist/types/client/state/presentation.d.ts +28 -0
- package/dist/types/client/state/relation-store.d.ts +10 -0
- package/dist/types/client/state/session-store.d.ts +41 -2
- package/dist/types/client/state/workspace-settings.d.ts +7 -0
- package/dist/types/client/types.d.ts +13 -1
- package/dist/types/client/utils/keyboard.d.ts +2 -0
- package/dist/types/client/utils/time-ago.d.ts +4 -0
- package/dist/types/mcp/context-uri-template.d.ts +3 -1
- package/dist/types/server/agent-presence.d.ts +10 -3
- package/dist/types/server/ax-context.d.ts +1 -1
- package/dist/types/server/canvas-db.d.ts +30 -7
- package/dist/types/server/canvas-operations.d.ts +24 -2
- package/dist/types/server/canvas-provenance.d.ts +22 -2
- package/dist/types/server/canvas-serialization.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +93 -15
- package/dist/types/server/canvas-validation.d.ts +1 -1
- package/dist/types/server/context-brief.d.ts +94 -6
- package/dist/types/server/context-reads.d.ts +33 -7
- package/dist/types/server/edit-summary.d.ts +20 -0
- package/dist/types/server/index.d.ts +71 -10
- package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
- package/dist/types/server/lifecycle-status.d.ts +10 -0
- package/dist/types/server/memory-graph.d.ts +9 -0
- package/dist/types/server/mutation-history.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
- package/dist/types/server/operations/ops/boards.d.ts +2 -1
- package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
- package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +5 -0
- package/dist/types/server/operations/ops/query.d.ts +23 -0
- package/dist/types/server/operations/registry.d.ts +1 -0
- package/dist/types/server/placement.d.ts +7 -0
- package/dist/types/server/scope-fence.d.ts +2 -1
- package/dist/types/server/spatial-analysis.d.ts +6 -3
- package/dist/types/server/tour-control.d.ts +32 -0
- package/dist/types/server/wiki-links.d.ts +14 -0
- package/dist/types/server/workbench-auth.d.ts +2 -0
- package/dist/types/shared/agent-context.d.ts +2 -0
- package/dist/types/shared/agent-presence.d.ts +15 -12
- package/dist/types/shared/auto-arrange.d.ts +1 -0
- package/dist/types/shared/boards.d.ts +4 -4
- package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
- package/dist/types/shared/knowledge-presets.d.ts +6 -0
- package/dist/types/shared/lifecycle-status.d.ts +24 -0
- package/dist/types/shared/memory-graph.d.ts +53 -0
- package/dist/types/shared/relations.d.ts +68 -0
- package/dist/types/shared/semantic-attention.d.ts +2 -0
- package/dist/types/shared/tour.d.ts +38 -0
- package/dist/types/shared/workspace-settings.d.ts +11 -0
- package/docs/RELEASE.md +21 -3
- package/docs/adr-001-bun-only-runtime.md +7 -1
- package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
- package/docs/api-reference.md +109 -0
- package/docs/ax-host-adapter-contract.md +1 -1
- package/docs/ax-state-contract.md +5 -4
- package/docs/cli.md +153 -15
- package/docs/demos/pmx-canvas-demo.mp4 +0 -0
- package/docs/design/AgentContext.dc.html +3 -3
- package/docs/design/BoardPins.dc.html +37 -22
- package/docs/design/Categories.dc.html +208 -0
- package/docs/design/CategoryEditor.dc.html +33 -0
- package/docs/design/Chrome.dc.html +52 -9
- package/docs/design/ContextChip.dc.html +224 -0
- package/docs/design/ContextChip600.dc.html +136 -0
- package/docs/design/EmptyState.dc.html +45 -0
- package/docs/design/Flows.dc.html +23 -1
- package/docs/design/HeaderMarks.dc.html +183 -0
- package/docs/design/Home.dc.html +19 -1
- package/docs/design/HomeGraph.dc.html +86 -202
- package/docs/design/HomeMap.dc.html +23 -1
- package/docs/design/HomeTeam.dc.html +32 -14
- package/docs/design/Lifecycle.dc.html +22 -0
- package/docs/design/Lifecycle600.dc.html +18 -0
- package/docs/design/LifecycleVariants.dc.html +157 -0
- package/docs/design/LinkAuthorship.dc.html +139 -0
- package/docs/design/LinksChanged.dc.html +203 -0
- package/docs/design/LinksOptions.dc.html +137 -0
- package/docs/design/LinksOptions600.dc.html +134 -0
- package/docs/design/NearPin.dc.html +9 -10
- package/docs/design/Pane600.dc.html +4 -3
- package/docs/design/Relations.dc.html +3 -2
- package/docs/design/Restore.dc.html +152 -0
- package/docs/design/SeeChange.dc.html +267 -0
- package/docs/design/Uses.dc.html +12 -6
- package/docs/design/canvas.json +146 -19
- package/docs/design.md +449 -16
- package/docs/environment.md +0 -1
- package/docs/evals/curation-effect.md +128 -0
- package/docs/http-api.md +459 -37
- package/docs/mcp.md +146 -18
- package/docs/node-types.md +58 -3
- package/docs/plans/design-gaps-2026-10-08.md +53 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
- package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
- package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
- package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
- package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
- package/docs/product-context-vision-2026-09.md +17 -1
- package/docs/product-vision-2026-09.md +1082 -43
- package/docs/product-vision-review-2026-09.md +13 -1
- package/docs/screenshots/workbench-live-dark.png +0 -0
- package/docs/screenshots/workbench-live-light.png +0 -0
- package/docs/sdk.md +62 -2
- package/package.json +1 -1
- 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/tools.json +601 -22
- package/skills/pmx-canvas/SKILL.md +216 -42
- package/skills/pmx-canvas/references/full-reference.md +120 -21
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
- package/skills/pmx-canvas/references/importing-context.md +132 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
- package/skills/pmx-canvas-testing/SKILL.md +20 -0
- package/src/cli/agent.ts +2 -0
- package/src/cli/commands/ax.ts +12 -0
- package/src/cli/commands/boards.ts +87 -11
- package/src/cli/commands/edges.ts +28 -0
- package/src/cli/commands/groups.ts +7 -0
- package/src/cli/commands/nodes.ts +1 -0
- package/src/cli/commands/query.ts +34 -2
- package/src/cli/commands/record.ts +199 -48
- package/src/cli/index.ts +6 -1
- package/src/client/App.tsx +177 -70
- package/src/client/canvas/AttentionHistory.tsx +0 -2
- package/src/client/canvas/BarHint.tsx +40 -2
- package/src/client/canvas/BoardCopyDialog.tsx +1 -1
- package/src/client/canvas/BoardFolderDialog.tsx +7 -7
- package/src/client/canvas/BoardLinkDialog.tsx +138 -33
- package/src/client/canvas/BoardPinButton.tsx +33 -0
- package/src/client/canvas/BoardSwitcher.tsx +26 -40
- package/src/client/canvas/CanvasNode.tsx +360 -237
- package/src/client/canvas/CanvasViewport.tsx +45 -7
- package/src/client/canvas/CommandBar.tsx +29 -23
- package/src/client/canvas/CommandPalette.tsx +5 -12
- package/src/client/canvas/ConnectionBanner.tsx +3 -3
- package/src/client/canvas/ContextChip.tsx +296 -0
- package/src/client/canvas/ContextMenu.tsx +63 -5
- package/src/client/canvas/CrossBoardReferences.tsx +60 -0
- package/src/client/canvas/EdgeLayer.tsx +339 -76
- package/src/client/canvas/EmptyState.tsx +33 -18
- package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
- package/src/client/canvas/ExternalWriters.tsx +57 -42
- package/src/client/canvas/HomeGraphView.tsx +942 -0
- package/src/client/canvas/HomeMapView.tsx +641 -0
- package/src/client/canvas/HomeReviewView.tsx +167 -0
- package/src/client/canvas/HomeView.tsx +687 -160
- package/src/client/canvas/IntentLayer.tsx +19 -12
- package/src/client/canvas/KnowledgeDialog.tsx +488 -0
- package/src/client/canvas/LifecycleChrome.tsx +133 -0
- package/src/client/canvas/LifecycleFields.tsx +91 -0
- package/src/client/canvas/LifecyclePanel.tsx +533 -0
- package/src/client/canvas/MemoryInspector.tsx +104 -0
- package/src/client/canvas/Minimap.tsx +3 -3
- package/src/client/canvas/NearPinLayer.tsx +187 -0
- package/src/client/canvas/NodeContextMark.tsx +175 -97
- package/src/client/canvas/Presentation.tsx +23 -57
- package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
- package/src/client/canvas/RelationsPanel.tsx +82 -0
- package/src/client/canvas/SelectionBar.tsx +3 -6
- package/src/client/canvas/SessionPanel.tsx +90 -50
- package/src/client/canvas/SessionReceipt.tsx +311 -39
- package/src/client/canvas/ShortcutOverlay.tsx +1 -1
- package/src/client/canvas/SnapshotPanel.tsx +27 -36
- package/src/client/canvas/ToolRail.tsx +367 -192
- package/src/client/canvas/TopBar.tsx +354 -124
- package/src/client/canvas/canvas-area.ts +10 -3
- package/src/client/canvas/graph-geometry.ts +195 -0
- package/src/client/canvas/graph-model.ts +254 -0
- package/src/client/canvas/memory-layout.ts +106 -0
- package/src/client/canvas/use-chrome-height.ts +20 -0
- package/src/client/canvas/use-focus-trap.ts +31 -5
- package/src/client/canvas/use-memory-graph.ts +58 -0
- package/src/client/canvas/use-node-drag.ts +2 -1
- package/src/client/canvas/use-pan-zoom.ts +64 -7
- package/src/client/icons.tsx +0 -63
- package/src/client/mcp-app/style.css +0 -6
- package/src/client/nodes/AxStepControls.tsx +74 -50
- package/src/client/nodes/BoardNode.tsx +5 -1
- package/src/client/nodes/ContextNode.tsx +18 -11
- package/src/client/nodes/ExtAppFrame.tsx +0 -1
- package/src/client/nodes/FileNode.tsx +46 -42
- package/src/client/nodes/HtmlNode.tsx +32 -0
- package/src/client/nodes/ImageNode.tsx +1 -1
- package/src/client/nodes/InlineFormatBar.tsx +6 -9
- package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
- package/src/client/nodes/MarkdownNode.tsx +377 -109
- package/src/client/nodes/McpAppNode.tsx +50 -20
- package/src/client/nodes/MdFormatBar.tsx +21 -18
- package/src/client/nodes/PromptNode.tsx +1 -1
- package/src/client/nodes/ResponseNode.tsx +1 -1
- package/src/client/nodes/viewer-runtime-state.ts +54 -22
- package/src/client/state/attention-bridge.ts +64 -15
- package/src/client/state/boards-store.ts +193 -39
- package/src/client/state/canvas-store.ts +99 -12
- package/src/client/state/context-chip-store.ts +70 -0
- package/src/client/state/context-status-store.ts +53 -0
- package/src/client/state/intent-bridge.ts +51 -30
- package/src/client/state/knowledge-store.ts +43 -0
- package/src/client/state/near-pin-store.ts +115 -6
- package/src/client/state/presence-store.ts +7 -10
- package/src/client/state/presentation.ts +91 -0
- package/src/client/state/relation-store.ts +81 -0
- package/src/client/state/session-store.ts +144 -18
- package/src/client/state/sse-bridge.ts +206 -29
- package/src/client/state/workspace-settings.ts +11 -0
- package/src/client/theme/global.css +2724 -476
- package/src/client/types.ts +12 -0
- package/src/client/utils/keyboard.ts +10 -0
- package/src/client/utils/time-ago.ts +14 -0
- package/src/json-render/renderer/index.tsx +31 -0
- package/src/mcp/canvas-app.ts +12 -2
- package/src/mcp/context-uri-template.ts +7 -4
- package/src/mcp/server.ts +63 -41
- package/src/mcp/workbench-app.ts +39 -6
- package/src/server/agent-presence.ts +128 -28
- package/src/server/ax-context.ts +27 -13
- package/src/server/board-export.ts +12 -1
- package/src/server/canvas-db.ts +331 -42
- package/src/server/canvas-operations.ts +195 -83
- package/src/server/canvas-provenance.ts +53 -22
- package/src/server/canvas-serialization.ts +2 -0
- package/src/server/canvas-state.ts +950 -122
- package/src/server/canvas-validation.ts +9 -2
- package/src/server/context-brief.ts +345 -38
- package/src/server/context-reads.ts +178 -30
- package/src/server/demo-state.json +355 -24
- package/src/server/edit-summary.ts +74 -0
- package/src/server/html-surface.ts +8 -1
- package/src/server/index.ts +181 -11
- package/src/server/knowledge-lifecycle.ts +304 -0
- package/src/server/lifecycle-status.ts +232 -0
- package/src/server/memory-graph.ts +166 -0
- package/src/server/mutation-history.ts +18 -6
- package/src/server/operations/composites.ts +32 -11
- package/src/server/operations/http.ts +7 -2
- package/src/server/operations/index.ts +4 -0
- package/src/server/operations/mcp.ts +26 -1
- package/src/server/operations/ops/ax-presence.ts +1 -1
- package/src/server/operations/ops/ax-read.ts +57 -6
- package/src/server/operations/ops/ax-timeline.ts +28 -2
- package/src/server/operations/ops/backup.ts +5 -3
- package/src/server/operations/ops/batch.ts +35 -4
- package/src/server/operations/ops/boards.ts +188 -47
- package/src/server/operations/ops/canvas-wire.ts +31 -1
- package/src/server/operations/ops/edges.ts +36 -2
- package/src/server/operations/ops/groups.ts +6 -1
- package/src/server/operations/ops/lifecycle.ts +262 -0
- package/src/server/operations/ops/memory-graph.ts +45 -0
- package/src/server/operations/ops/nodes.ts +107 -24
- package/src/server/operations/ops/query.ts +281 -68
- package/src/server/operations/ops/snapshots.ts +73 -1
- package/src/server/operations/ops/validate.ts +5 -1
- package/src/server/operations/ops/viewport.ts +97 -3
- package/src/server/operations/registry.ts +74 -25
- package/src/server/placement.ts +29 -0
- package/src/server/scope-fence.ts +78 -6
- package/src/server/server.ts +115 -16
- package/src/server/spatial-analysis.ts +48 -23
- package/src/server/tour-control.ts +82 -0
- package/src/server/wiki-links.ts +137 -0
- package/src/server/workbench-auth.ts +11 -0
- package/src/shared/agent-context.ts +3 -0
- package/src/shared/agent-presence.ts +15 -15
- package/src/shared/auto-arrange.ts +2 -0
- package/src/shared/boards.ts +4 -4
- package/src/shared/knowledge-lifecycle.ts +22 -0
- package/src/shared/knowledge-presets.ts +80 -0
- package/src/shared/lifecycle-status.ts +26 -0
- package/src/shared/memory-graph.ts +74 -0
- package/src/shared/relations.ts +98 -0
- package/src/shared/semantic-attention.ts +15 -1
- package/src/shared/tour.ts +23 -5
- package/src/shared/workspace-settings.ts +13 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
- package/dist/types/client/canvas/kind-colors.d.ts +0 -3
- package/dist/types/server/agent-context.d.ts +0 -22
- package/src/client/canvas/ContextPinBar.tsx +0 -25
|
@@ -30,6 +30,7 @@ import { executeOperation, runWithSuppressedEmits } from '../registry.js';
|
|
|
30
30
|
import { defineOperation, type Operation, type OperationContext } from '../types.js';
|
|
31
31
|
import { readJsonValue } from '../http.js';
|
|
32
32
|
import { isRecord } from './nodes.js';
|
|
33
|
+
import { boardsPayload } from './boards.js';
|
|
33
34
|
|
|
34
35
|
interface BatchEntry {
|
|
35
36
|
op: string;
|
|
@@ -157,8 +158,10 @@ function createInternalBatchNode(args: Record<string, unknown>): Record<string,
|
|
|
157
158
|
* entry shape. Most ops match the wire shape verbatim; the cases below diverge
|
|
158
159
|
* because the legacy switch shaped them by hand.
|
|
159
160
|
*/
|
|
160
|
-
function shapeBatchEntry(op: string, result: unknown): Record<string, unknown> {
|
|
161
|
+
function shapeBatchEntry(op: string, result: unknown, creationReceipt = false): Record<string, unknown> {
|
|
161
162
|
const body = isRecord(result) ? result : {};
|
|
163
|
+
// A retry's refs must describe its original creation, not a later live edit.
|
|
164
|
+
if (creationReceipt) return body;
|
|
162
165
|
|
|
163
166
|
// node.add / node.update / group.create / graph.add: the legacy switch pushed
|
|
164
167
|
// { ok:true, ...serializeCanvasNodeCompact(node), <extras> }. The wire shape
|
|
@@ -237,7 +240,13 @@ async function runBatch(operations: BatchEntry[]): Promise<BatchEnvelope> {
|
|
|
237
240
|
refs,
|
|
238
241
|
};
|
|
239
242
|
}
|
|
240
|
-
if (
|
|
243
|
+
if (
|
|
244
|
+
operation.op === 'node.add' &&
|
|
245
|
+
typeof args.type === 'string' &&
|
|
246
|
+
isInternalBatchNodeType(args.type) &&
|
|
247
|
+
args.destinationBoardId === undefined &&
|
|
248
|
+
args.requestId === undefined
|
|
249
|
+
) {
|
|
241
250
|
const result = createInternalBatchNode(args);
|
|
242
251
|
results.push(result);
|
|
243
252
|
if (typeof operation.assign === 'string' && operation.assign.trim().length > 0) {
|
|
@@ -258,7 +267,12 @@ async function runBatch(operations: BatchEntry[]): Promise<BatchEnvelope> {
|
|
|
258
267
|
if (canvasState.boardGeneration !== boardGeneration) {
|
|
259
268
|
throw new Error('The board changed while the batch was running.');
|
|
260
269
|
}
|
|
261
|
-
const result = shapeBatchEntry(
|
|
270
|
+
const result = shapeBatchEntry(
|
|
271
|
+
operation.op,
|
|
272
|
+
raw,
|
|
273
|
+
['node.add', 'group.create', 'edge.add'].includes(operation.op) &&
|
|
274
|
+
(args.requestId !== undefined || args.destinationBoardId !== undefined),
|
|
275
|
+
);
|
|
262
276
|
results.push(result);
|
|
263
277
|
if (typeof operation.assign === 'string' && operation.assign.trim().length > 0) {
|
|
264
278
|
refs[operation.assign] = result;
|
|
@@ -391,8 +405,25 @@ const batchOperation = defineOperation<z.infer<typeof batchSchema>, BatchEnvelop
|
|
|
391
405
|
const operations = normalizeOperations(input.operations);
|
|
392
406
|
// Suppress every per-entry SSE frame (auto layout emit + each handler's
|
|
393
407
|
// ctx.emit). After the loop completes, emit ONE final canvas-layout-update.
|
|
408
|
+
const revision = canvasState.getContentRevision().revision;
|
|
394
409
|
const envelope = await runWithSuppressedEmits(() => runBatch(operations));
|
|
395
|
-
|
|
410
|
+
if (
|
|
411
|
+
revision !== canvasState.getContentRevision().revision ||
|
|
412
|
+
operations.some(
|
|
413
|
+
(entry) =>
|
|
414
|
+
!['node.add', 'group.create', 'edge.add'].includes(entry.op) ||
|
|
415
|
+
entry.args.destinationBoardId === undefined ||
|
|
416
|
+
entry.args.destinationBoardId === canvasState.activeBoardId,
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
420
|
+
if (
|
|
421
|
+
operations.some(
|
|
422
|
+
(entry) =>
|
|
423
|
+
['node.add', 'group.create', 'edge.add'].includes(entry.op) && entry.args.destinationBoardId !== undefined,
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
ctx.emit('boards-changed', boardsPayload());
|
|
396
427
|
// The suppression window also swallows the pins op's context-pins-changed
|
|
397
428
|
// frame, leaving the client's pin state (menus, indicators) stale until
|
|
398
429
|
// reload (0.4.5 report Finding X). If the batch touched pins, follow the
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Many boards (plan 012): board.list / board.get / board.create /
|
|
3
|
-
* board.update / board.open / board.delete.
|
|
3
|
+
* board.update / board.open / board.delete, plus board.pin / board.unpin
|
|
4
|
+
* (vision move 0a: the working set across boards).
|
|
4
5
|
*
|
|
5
6
|
* Agents can explicitly open boards as well as list, read and create them.
|
|
6
7
|
* Deletion requires the human (HUMAN_ONLY_OPS in registry.ts).
|
|
@@ -11,46 +12,111 @@
|
|
|
11
12
|
*
|
|
12
13
|
* This module must never import server.ts or index.ts.
|
|
13
14
|
*/
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
14
16
|
import { z } from 'zod';
|
|
15
|
-
import {
|
|
17
|
+
import { normalizeBoardFolder } from '../../../shared/boards.js';
|
|
18
|
+
import { relationVocabularySchema } from '../../../shared/relations.js';
|
|
16
19
|
import { canvasState, type CanvasLayout } from '../../canvas-state.js';
|
|
17
20
|
import { openCanvasBoard } from '../../canvas-operations.js';
|
|
18
21
|
import { summarizeNodeForAgentContext } from '../../../shared/agent-context.js';
|
|
22
|
+
import { BRIEF_SUMMARY_LENGTH } from '../../context-brief.js';
|
|
23
|
+
import { memoryGraph, boardConnections } from '../../memory-graph.js';
|
|
19
24
|
import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
/** The most cards a Home thumbnail draws (the largest first); the rest are noise at that size. */
|
|
27
|
+
const PREVIEW_CARDS = 24;
|
|
28
|
+
export const boardCatalogEpoch = randomUUID();
|
|
29
|
+
let catalogRevision = 0;
|
|
30
|
+
let reloadRevision = 0;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A board's thumbnail on Home (docs/design/Home.dc.html): its cards as
|
|
34
|
+
* rectangles in 0–1 of the board's extent, pinned ones flagged.
|
|
35
|
+
*/
|
|
36
|
+
function boardPreview(
|
|
37
|
+
nodes: CanvasLayout['nodes'],
|
|
38
|
+
pins: readonly string[],
|
|
39
|
+
): Array<{ x: number; y: number; w: number; h: number; pinned?: true }> {
|
|
40
|
+
const cards = nodes.filter((node) => node.type !== 'group');
|
|
41
|
+
if (cards.length === 0) return [];
|
|
42
|
+
const left = Math.min(...cards.map((node) => node.position.x));
|
|
43
|
+
const top = Math.min(...cards.map((node) => node.position.y));
|
|
44
|
+
const width = Math.max(...cards.map((node) => node.position.x + node.size.width)) - left || 1;
|
|
45
|
+
const height = Math.max(...cards.map((node) => node.position.y + node.size.height)) - top || 1;
|
|
46
|
+
const round = (value: number) => Math.round(value * 1000) / 1000;
|
|
47
|
+
return [...cards]
|
|
48
|
+
.sort((a, b) => b.size.width * b.size.height - a.size.width * a.size.height)
|
|
49
|
+
.slice(0, PREVIEW_CARDS)
|
|
50
|
+
.map((node) => ({
|
|
51
|
+
x: round((node.position.x - left) / width),
|
|
52
|
+
y: round((node.position.y - top) / height),
|
|
53
|
+
w: round(node.size.width / width),
|
|
54
|
+
h: round(node.size.height / height),
|
|
55
|
+
...(pins.includes(node.id) ? { pinned: true as const } : {}),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function boardsPayload(reloaded = false): Record<string, unknown> {
|
|
60
|
+
const revision = ++catalogRevision;
|
|
61
|
+
if (reloaded) reloadRevision = revision;
|
|
22
62
|
const boards = canvasState.listBoards();
|
|
23
63
|
const names = new Map(boards.map((board) => [board.id, board.name]));
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
links.set(
|
|
28
|
-
board.id,
|
|
29
|
-
nodes
|
|
30
|
-
.filter((node) => node.type === 'board')
|
|
31
|
-
.map((node) => {
|
|
32
|
-
const boardId = typeof node.data.boardId === 'string' ? node.data.boardId : '';
|
|
33
|
-
return { nodeId: node.id, boardId, title: names.get(boardId) ?? null, missing: !names.has(boardId) };
|
|
34
|
-
}),
|
|
35
|
-
);
|
|
36
|
-
}
|
|
64
|
+
const graph = memoryGraph();
|
|
65
|
+
const titles = new Map(graph.nodes.map((node) => [node.key, node.title]));
|
|
66
|
+
const connections = boardConnections(graph);
|
|
37
67
|
return {
|
|
68
|
+
catalogEpoch: boardCatalogEpoch,
|
|
69
|
+
catalogRevision: revision,
|
|
70
|
+
reloadRevision,
|
|
71
|
+
boardGeneration: canvasState.boardGeneration,
|
|
38
72
|
activeBoardId: canvasState.activeBoardId,
|
|
39
73
|
boards: boards.map((board) => {
|
|
40
74
|
const read = canvasState.readBoard(board.id, true);
|
|
41
75
|
const readme = read?.layout.nodes.find((node) => node.id === board.readmeNodeId && node.type === 'markdown');
|
|
76
|
+
const pins = read?.state.contextPins ?? [];
|
|
77
|
+
const pinnedNodes = (read?.layout.nodes ?? []).filter((node) => pins.includes(node.id));
|
|
78
|
+
const summary = readme ? summarizeNodeForAgentContext(readme, { defaultTextLength: 1000 }) : null;
|
|
42
79
|
return {
|
|
43
80
|
...board,
|
|
44
|
-
summary
|
|
45
|
-
pinnedTitles: (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
81
|
+
summary,
|
|
82
|
+
pinnedTitles: pinnedNodes.map((node) => ({
|
|
83
|
+
nodeId: node.id,
|
|
84
|
+
title: typeof node.data.title === 'string' ? node.data.title : node.id,
|
|
85
|
+
})),
|
|
86
|
+
preview: boardPreview(read?.layout.nodes ?? [], pins),
|
|
87
|
+
// What a pinned board's map roughly costs in the brief: its README and pinned cards as summaries.
|
|
88
|
+
mapChars:
|
|
89
|
+
(summary?.length ?? 0) +
|
|
90
|
+
pinnedNodes.reduce(
|
|
91
|
+
(sum, node) =>
|
|
92
|
+
sum +
|
|
93
|
+
summarizeNodeForAgentContext(node, {
|
|
94
|
+
defaultTextLength: BRIEF_SUMMARY_LENGTH,
|
|
95
|
+
webpageTextLength: BRIEF_SUMMARY_LENGTH,
|
|
96
|
+
}).length,
|
|
97
|
+
0,
|
|
98
|
+
),
|
|
99
|
+
links: connections
|
|
100
|
+
.filter((edge) => edge.source.boardId === board.id)
|
|
101
|
+
.map((edge) => ({
|
|
102
|
+
id: edge.id,
|
|
103
|
+
nodeId: edge.source.cardId ?? '',
|
|
104
|
+
boardId: edge.target.boardId,
|
|
105
|
+
...(edge.target.cardId ? { cardId: edge.target.cardId, cardTitle: titles.get(edge.to) ?? null } : {}),
|
|
106
|
+
title: names.get(edge.target.boardId) ?? null,
|
|
107
|
+
missing: edge.missing,
|
|
108
|
+
kind: edge.kind,
|
|
109
|
+
})),
|
|
110
|
+
backlinks: connections
|
|
111
|
+
.filter((edge) => edge.target.boardId === board.id)
|
|
112
|
+
.map((edge) => ({
|
|
113
|
+
id: edge.id,
|
|
114
|
+
boardId: edge.source.boardId,
|
|
115
|
+
title: names.get(edge.source.boardId) ?? '',
|
|
116
|
+
nodeId: edge.source.cardId ?? '',
|
|
117
|
+
kind: edge.kind,
|
|
118
|
+
...(edge.target.cardId ? { cardId: edge.target.cardId } : {}),
|
|
119
|
+
})),
|
|
54
120
|
};
|
|
55
121
|
}),
|
|
56
122
|
};
|
|
@@ -82,19 +148,21 @@ export function readTargetBoard(
|
|
|
82
148
|
};
|
|
83
149
|
}
|
|
84
150
|
|
|
85
|
-
function emitBoardsChanged(ctx: OperationContext):
|
|
86
|
-
|
|
151
|
+
function emitBoardsChanged(ctx: OperationContext): Record<string, unknown> {
|
|
152
|
+
const payload = boardsPayload();
|
|
153
|
+
ctx.emit('boards-changed', payload);
|
|
154
|
+
return payload;
|
|
87
155
|
}
|
|
88
156
|
|
|
89
157
|
const jsonResult = (result: unknown) => ({
|
|
90
158
|
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
91
159
|
});
|
|
92
160
|
|
|
93
|
-
/** '' or null clears the
|
|
94
|
-
function
|
|
161
|
+
/** '' or null clears the folder; undefined leaves it alone. */
|
|
162
|
+
function boardFolder(value: unknown): string | null | undefined {
|
|
95
163
|
if (value === undefined) return undefined;
|
|
96
164
|
try {
|
|
97
|
-
return
|
|
165
|
+
return normalizeBoardFolder(typeof value === 'string' ? value : null);
|
|
98
166
|
} catch (error) {
|
|
99
167
|
throw new OperationError(error instanceof Error ? error.message : String(error));
|
|
100
168
|
}
|
|
@@ -158,7 +226,7 @@ const boardGetOperation = defineOperation<z.infer<typeof getSchema>, Record<stri
|
|
|
158
226
|
|
|
159
227
|
const createShape = {
|
|
160
228
|
name: z.unknown().optional().describe('Board name'),
|
|
161
|
-
|
|
229
|
+
folder: z.unknown().optional().describe('Folder path on Home, e.g. Engineering/Canvas'),
|
|
162
230
|
};
|
|
163
231
|
const createSchema = z.looseObject(createShape);
|
|
164
232
|
|
|
@@ -174,12 +242,12 @@ const boardCreateOperation = defineOperation<z.infer<typeof createSchema>, Recor
|
|
|
174
242
|
'Create a new empty board without opening it. Use canvas_board action "open" with the returned board id before writing to it.',
|
|
175
243
|
extraShape: {
|
|
176
244
|
name: z.string().describe('Board name'),
|
|
177
|
-
|
|
245
|
+
folder: z.string().optional().describe('Folder path on Home, e.g. Engineering/Canvas'),
|
|
178
246
|
},
|
|
179
247
|
formatResult: jsonResult,
|
|
180
248
|
},
|
|
181
249
|
handler: (input, ctx) => {
|
|
182
|
-
const board = canvasState.createBoard(boardName(input.name),
|
|
250
|
+
const board = canvasState.createBoard(boardName(input.name), boardFolder(input.folder) ?? null);
|
|
183
251
|
if (!board) throw new OperationError('Boards need a workspace database.', 409);
|
|
184
252
|
emitBoardsChanged(ctx);
|
|
185
253
|
return { ok: true, board };
|
|
@@ -209,8 +277,7 @@ const boardOpenOperation = defineOperation<z.infer<typeof openSchema>, Record<st
|
|
|
209
277
|
handler: (input, ctx) => {
|
|
210
278
|
const id = typeof input.id === 'string' && input.id.trim() ? input.id.trim() : null;
|
|
211
279
|
if (!openCanvasBoard(id).ok) throw new OperationError(`Board "${id}" not found.`, 404);
|
|
212
|
-
emitBoardsChanged(ctx);
|
|
213
|
-
return { ok: true, ...boardsPayload() };
|
|
280
|
+
return { ok: true, ...emitBoardsChanged(ctx) };
|
|
214
281
|
},
|
|
215
282
|
});
|
|
216
283
|
|
|
@@ -219,8 +286,16 @@ const boardOpenOperation = defineOperation<z.infer<typeof openSchema>, Record<st
|
|
|
219
286
|
const updateShape = {
|
|
220
287
|
id: z.unknown().optional().describe('Board id'),
|
|
221
288
|
name: z.unknown().optional().describe('New name'),
|
|
222
|
-
|
|
289
|
+
relationVocabulary: relationVocabularySchema
|
|
290
|
+
.optional()
|
|
291
|
+
.describe('Board relation vocabulary: open kinds with a meaning ink and direction'),
|
|
292
|
+
folder: z.unknown().optional().describe('Folder path on Home; "" or null unfiles it'),
|
|
223
293
|
readmeNodeId: z.unknown().optional().describe('Markdown node id to use as README; null clears it'),
|
|
294
|
+
mapPosition: z
|
|
295
|
+
.object({ x: z.number().finite(), y: z.number().finite() })
|
|
296
|
+
.nullable()
|
|
297
|
+
.optional()
|
|
298
|
+
.describe('Persist a Home Map position; null returns to the generated folder layout'),
|
|
224
299
|
};
|
|
225
300
|
const updateSchema = z.looseObject(updateShape);
|
|
226
301
|
|
|
@@ -233,11 +308,11 @@ const boardUpdateOperation = defineOperation<z.infer<typeof updateSchema>, Recor
|
|
|
233
308
|
mcp: {
|
|
234
309
|
toolName: 'canvas_update_board',
|
|
235
310
|
description:
|
|
236
|
-
'Rename a board, move it into a nested folder on Home (e.g. Engineering/Canvas;
|
|
311
|
+
'Rename a board, move it into a nested folder on Home (e.g. Engineering/Canvas; folder "" unfiles it), or designate its markdown introduction with readmeNodeId. During board authoring, set a useful README when none exists; preserve an existing designation unless the human requests a change. Moving preserves the board ID and content.',
|
|
237
312
|
extraShape: {
|
|
238
313
|
id: z.string().describe('Board id'),
|
|
239
314
|
name: z.string().optional().describe('New name'),
|
|
240
|
-
|
|
315
|
+
folder: z.string().optional().describe('Folder path, up to 8 levels of 1–60 characters; "" unfiles it'),
|
|
241
316
|
readmeNodeId: z.string().nullable().optional().describe('Markdown node id used as the board README'),
|
|
242
317
|
},
|
|
243
318
|
formatResult: jsonResult,
|
|
@@ -246,7 +321,9 @@ const boardUpdateOperation = defineOperation<z.infer<typeof updateSchema>, Recor
|
|
|
246
321
|
const id = typeof input.id === 'string' ? input.id : '';
|
|
247
322
|
const patch = {
|
|
248
323
|
...(input.name !== undefined ? { name: boardName(input.name) } : {}),
|
|
249
|
-
...(input.
|
|
324
|
+
...(input.relationVocabulary !== undefined ? { relationVocabulary: input.relationVocabulary } : {}),
|
|
325
|
+
...(input.folder !== undefined ? { folder: boardFolder(input.folder) ?? null } : {}),
|
|
326
|
+
...(input.mapPosition !== undefined ? { mapPosition: input.mapPosition } : {}),
|
|
250
327
|
};
|
|
251
328
|
if (input.readmeNodeId !== undefined) {
|
|
252
329
|
const board = canvasState.readBoard(id, false);
|
|
@@ -267,15 +344,14 @@ const boardUpdateOperation = defineOperation<z.infer<typeof updateSchema>, Recor
|
|
|
267
344
|
throw new OperationError('README must identify a markdown node on this board.');
|
|
268
345
|
}
|
|
269
346
|
}
|
|
270
|
-
emitBoardsChanged(ctx);
|
|
271
|
-
return { ok: true, ...boardsPayload() };
|
|
347
|
+
return { ok: true, ...emitBoardsChanged(ctx) };
|
|
272
348
|
},
|
|
273
349
|
});
|
|
274
350
|
|
|
275
351
|
const fromShape = {
|
|
276
352
|
sourceBoardId: z.string().describe('Source board id'),
|
|
277
353
|
name: z.string().describe('New board name'),
|
|
278
|
-
|
|
354
|
+
folder: z.string().optional(),
|
|
279
355
|
nodeIds: z.array(z.string()).optional(),
|
|
280
356
|
includeReadme: z.boolean().optional(),
|
|
281
357
|
includeStructure: z.boolean().optional(),
|
|
@@ -310,13 +386,19 @@ const boardCreateFromOperation = defineOperation<z.infer<typeof fromSchema>, Rec
|
|
|
310
386
|
title: typeof node.data.title === 'string' ? node.data.title : null,
|
|
311
387
|
reusable: !['prompt', 'response', 'trace', 'mcp-app'].includes(node.type),
|
|
312
388
|
})),
|
|
313
|
-
edges: source.layout.edges.map((edge) => ({
|
|
389
|
+
edges: source.layout.edges.map((edge) => ({
|
|
390
|
+
id: edge.id,
|
|
391
|
+
from: edge.from,
|
|
392
|
+
to: edge.to,
|
|
393
|
+
...(edge.toBoardId !== undefined ? { toBoardId: edge.toBoardId } : {}),
|
|
394
|
+
type: edge.type,
|
|
395
|
+
})),
|
|
314
396
|
};
|
|
315
397
|
}
|
|
316
398
|
const board = canvasState.createBoardFromBoard({
|
|
317
399
|
...input,
|
|
318
400
|
name: boardName(input.name),
|
|
319
|
-
|
|
401
|
+
folder: boardFolder(input.folder),
|
|
320
402
|
});
|
|
321
403
|
if (!board) throw new OperationError('Could not create board from source.', 409);
|
|
322
404
|
emitBoardsChanged(ctx);
|
|
@@ -324,6 +406,64 @@ const boardCreateFromOperation = defineOperation<z.infer<typeof fromSchema>, Rec
|
|
|
324
406
|
},
|
|
325
407
|
});
|
|
326
408
|
|
|
409
|
+
// ── board.pin / board.unpin ───────────────────────────────────
|
|
410
|
+
// A pinned board joins the agent's working set: its README and pinned cards
|
|
411
|
+
// travel in the brief whichever board is open. Pinning is context, never a
|
|
412
|
+
// write target; writes still go to the open board.
|
|
413
|
+
|
|
414
|
+
const pinShape = {
|
|
415
|
+
id: z.unknown().optional().describe('Board id'),
|
|
416
|
+
reason: z.unknown().optional().describe('Why this board matters; shown to the human on hover'),
|
|
417
|
+
};
|
|
418
|
+
const pinSchema = z.looseObject(pinShape);
|
|
419
|
+
|
|
420
|
+
const boardPinOperation = defineOperation<z.infer<typeof pinSchema>, Record<string, unknown>>({
|
|
421
|
+
name: 'board.pin',
|
|
422
|
+
mutates: false,
|
|
423
|
+
input: pinSchema,
|
|
424
|
+
inputShape: pinShape,
|
|
425
|
+
http: { method: 'POST', path: '/api/canvas/boards/:id/pin' },
|
|
426
|
+
mcp: {
|
|
427
|
+
toolName: 'canvas_pin_board',
|
|
428
|
+
description:
|
|
429
|
+
"Pin a board into the agent's working set: its README and pinned cards reach the brief whichever board is open. Read-only context; writes still go to the open board.",
|
|
430
|
+
extraShape: {
|
|
431
|
+
id: z.string().describe('Board id'),
|
|
432
|
+
reason: z.string().optional().describe('Why this board matters; shown to the human on hover'),
|
|
433
|
+
},
|
|
434
|
+
formatResult: jsonResult,
|
|
435
|
+
},
|
|
436
|
+
handler: (input, ctx) => {
|
|
437
|
+
const id = typeof input.id === 'string' ? input.id.trim() : '';
|
|
438
|
+
const reason =
|
|
439
|
+
typeof input.reason === 'string' && input.reason.trim() ? input.reason.trim().slice(0, 280) : undefined;
|
|
440
|
+
if (!canvasState.pinBoard(id, reason)) throw new OperationError(`Board "${id}" not found.`, 404);
|
|
441
|
+
return { ok: true, ...emitBoardsChanged(ctx) };
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const unpinShape = { id: z.unknown().optional().describe('Board id') };
|
|
446
|
+
const unpinSchema = z.looseObject(unpinShape);
|
|
447
|
+
|
|
448
|
+
const boardUnpinOperation = defineOperation<z.infer<typeof unpinSchema>, Record<string, unknown>>({
|
|
449
|
+
name: 'board.unpin',
|
|
450
|
+
mutates: false,
|
|
451
|
+
input: unpinSchema,
|
|
452
|
+
inputShape: unpinShape,
|
|
453
|
+
http: { method: 'POST', path: '/api/canvas/boards/:id/unpin' },
|
|
454
|
+
mcp: {
|
|
455
|
+
toolName: 'canvas_unpin_board',
|
|
456
|
+
description: "Remove a board from the agent's working set.",
|
|
457
|
+
extraShape: { id: z.string().describe('Board id') },
|
|
458
|
+
formatResult: jsonResult,
|
|
459
|
+
},
|
|
460
|
+
handler: (input, ctx) => {
|
|
461
|
+
const id = typeof input.id === 'string' ? input.id.trim() : '';
|
|
462
|
+
if (!canvasState.unpinBoard(id)) throw new OperationError(`Board "${id}" not found.`, 404);
|
|
463
|
+
return { ok: true, ...emitBoardsChanged(ctx) };
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
327
467
|
// ── board.delete (human only) ─────────────────────────────────
|
|
328
468
|
|
|
329
469
|
const deleteShape = {
|
|
@@ -342,8 +482,7 @@ const boardDeleteOperation = defineOperation<z.infer<typeof deleteSchema>, Recor
|
|
|
342
482
|
// Deleting the open board returns to Home through the same path as opening it.
|
|
343
483
|
if (id === canvasState.activeBoardId) openCanvasBoard(null);
|
|
344
484
|
if (!canvasState.deleteBoard(id)) throw new OperationError(`Board "${id}" not found.`, 404);
|
|
345
|
-
emitBoardsChanged(ctx);
|
|
346
|
-
return { ok: true, ...boardsPayload() };
|
|
485
|
+
return { ok: true, ...emitBoardsChanged(ctx) };
|
|
347
486
|
},
|
|
348
487
|
});
|
|
349
488
|
|
|
@@ -354,5 +493,7 @@ export const boardOperations: Operation[] = [
|
|
|
354
493
|
boardCreateOperation,
|
|
355
494
|
boardCreateFromOperation,
|
|
356
495
|
boardUpdateOperation,
|
|
496
|
+
boardPinOperation,
|
|
497
|
+
boardUnpinOperation,
|
|
357
498
|
boardDeleteOperation,
|
|
358
499
|
];
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* This module must never import server.ts or index.ts.
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
+
import { workspaceSettingsPatchSchema } from '../../../shared/workspace-settings.js';
|
|
10
11
|
import { normalizeCanvasTheme } from '../../canvas-db.js';
|
|
11
12
|
import { applyCanvasNodeUpdates } from '../../canvas-operations.js';
|
|
12
13
|
import { canvasState } from '../../canvas-state.js';
|
|
@@ -98,4 +99,33 @@ const applyUpdatesOperation = defineOperation<z.infer<typeof applyUpdatesSchema>
|
|
|
98
99
|
},
|
|
99
100
|
});
|
|
100
101
|
|
|
101
|
-
|
|
102
|
+
const settingsGetOperation = defineOperation({
|
|
103
|
+
name: 'settings.get',
|
|
104
|
+
mutates: false,
|
|
105
|
+
input: emptySchema,
|
|
106
|
+
inputShape: emptyShape,
|
|
107
|
+
http: { method: 'GET', path: '/api/canvas/settings' },
|
|
108
|
+
handler: () => ({ ok: true, workspaceSettings: canvasState.getWorkspaceSettings() }),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const settingsSetOperation = defineOperation({
|
|
112
|
+
name: 'settings.set',
|
|
113
|
+
mutates: false,
|
|
114
|
+
// Strip transport attribution/precondition fields before storing the patch.
|
|
115
|
+
input: z.object(workspaceSettingsPatchSchema.shape),
|
|
116
|
+
inputShape: workspaceSettingsPatchSchema.shape,
|
|
117
|
+
http: { method: 'POST', path: '/api/canvas/settings' },
|
|
118
|
+
handler: (input, ctx) => {
|
|
119
|
+
const workspaceSettings = canvasState.updateWorkspaceSettings(input);
|
|
120
|
+
ctx.emit('settings-changed', { workspaceSettings });
|
|
121
|
+
return { ok: true, workspaceSettings };
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export const canvasWireOperations: Operation[] = [
|
|
126
|
+
themeGetOperation,
|
|
127
|
+
themeSetOperation,
|
|
128
|
+
settingsGetOperation,
|
|
129
|
+
settingsSetOperation,
|
|
130
|
+
applyUpdatesOperation,
|
|
131
|
+
];
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This module must never import server.ts or index.ts.
|
|
5
5
|
*/
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { relationKindSchema, relationReasonSchema } from '../../../shared/relations.js';
|
|
7
8
|
import type { CanvasEdge } from '../../canvas-state.js';
|
|
8
9
|
import { addCanvasEdge, removeCanvasEdge, updateCanvasEdge } from '../../canvas-operations.js';
|
|
9
10
|
import { readJsonValue } from '../http.js';
|
|
@@ -16,6 +17,8 @@ const VALID_EDGE_STYLES = new Set(['solid', 'dashed', 'dotted']);
|
|
|
16
17
|
// ── edge.add ──────────────────────────────────────────────────
|
|
17
18
|
|
|
18
19
|
const edgeAddShape = {
|
|
20
|
+
destinationBoardId: z.string().min(1).optional().describe('Board owning the source and edge; does not activate it'),
|
|
21
|
+
requestId: z.string().min(1).optional().describe('Logical creation identity for retry-safe edge creation'),
|
|
19
22
|
intentId: z
|
|
20
23
|
.string()
|
|
21
24
|
.optional()
|
|
@@ -23,6 +26,11 @@ const edgeAddShape = {
|
|
|
23
26
|
.describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
|
|
24
27
|
from: z.string().optional().catch(undefined).describe('Source node ID'),
|
|
25
28
|
to: z.string().optional().catch(undefined).describe('Target node ID'),
|
|
29
|
+
toBoardId: z
|
|
30
|
+
.string()
|
|
31
|
+
.min(1)
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Target board ID for a cross-board edge; independent of the source destinationBoardId'),
|
|
26
34
|
fromSearch: z
|
|
27
35
|
.string()
|
|
28
36
|
.optional()
|
|
@@ -34,6 +42,10 @@ const edgeAddShape = {
|
|
|
34
42
|
.catch(undefined)
|
|
35
43
|
.describe('Resolve the target node by exact or fuzzy title/content search'),
|
|
36
44
|
type: z.unknown().optional().describe('Edge type: flow, depends-on, relation, or references'),
|
|
45
|
+
kind: relationKindSchema
|
|
46
|
+
.optional()
|
|
47
|
+
.describe('Open semantic kind, e.g. supports, contradicts, derived-from, supersedes'),
|
|
48
|
+
reason: relationReasonSchema.optional().describe('Why this relation exists'),
|
|
37
49
|
label: z.unknown().optional().describe('Edge label text'),
|
|
38
50
|
style: z.unknown().optional().describe('Optional edge stroke style: solid, dashed, or dotted'),
|
|
39
51
|
animated: z.unknown().optional().describe('Animate the edge stroke'),
|
|
@@ -80,7 +92,10 @@ const edgeAddOperation = defineOperation<z.infer<typeof edgeAddSchema>, CanvasEd
|
|
|
80
92
|
id: body.id,
|
|
81
93
|
from: body.from,
|
|
82
94
|
to: body.to,
|
|
95
|
+
...(body.toBoardId ? { toBoardId: body.toBoardId } : {}),
|
|
83
96
|
type: body.type,
|
|
97
|
+
kind: body.kind,
|
|
98
|
+
reason: body.reason,
|
|
84
99
|
label: body.label,
|
|
85
100
|
style: body.style,
|
|
86
101
|
animated: body.animated,
|
|
@@ -108,16 +123,23 @@ const edgeAddOperation = defineOperation<z.infer<typeof edgeAddSchema>, CanvasEd
|
|
|
108
123
|
}
|
|
109
124
|
try {
|
|
110
125
|
return addCanvasEdge({
|
|
126
|
+
destinationBoardId: input.destinationBoardId,
|
|
127
|
+
requestId: input.requestId,
|
|
128
|
+
intentId: input.intentId,
|
|
111
129
|
...(typeof body.from === 'string' ? { from: body.from } : {}),
|
|
112
130
|
...(typeof body.to === 'string' ? { to: body.to } : {}),
|
|
131
|
+
...(typeof body.toBoardId === 'string' ? { toBoardId: body.toBoardId } : {}),
|
|
113
132
|
...(typeof body.fromSearch === 'string' ? { fromSearch: body.fromSearch } : {}),
|
|
114
133
|
...(typeof body.toSearch === 'string' ? { toSearch: body.toSearch } : {}),
|
|
115
134
|
type: rawType as CanvasEdge['type'],
|
|
135
|
+
...(input.kind ? { kind: input.kind } : {}),
|
|
136
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
116
137
|
...(body.label ? { label: String(body.label) } : {}),
|
|
117
138
|
...(style ? { style: style as CanvasEdge['style'] } : {}),
|
|
118
139
|
...(body.animated !== undefined ? { animated: Boolean(body.animated) } : {}),
|
|
119
140
|
});
|
|
120
141
|
} catch (error) {
|
|
142
|
+
if (error instanceof OperationError) throw error;
|
|
121
143
|
throw new OperationError(error instanceof Error ? error.message : 'Duplicate or self-edge.');
|
|
122
144
|
}
|
|
123
145
|
},
|
|
@@ -129,6 +151,8 @@ const edgeAddOperation = defineOperation<z.infer<typeof edgeAddSchema>, CanvasEd
|
|
|
129
151
|
const edgeUpdateShape = {
|
|
130
152
|
id: z.string().optional().catch(undefined).describe('Edge ID to update'),
|
|
131
153
|
type: z.unknown().optional().describe('New edge type: flow, depends-on, relation, or references'),
|
|
154
|
+
kind: relationKindSchema.nullable().optional().describe('Semantic kind; null clears it'),
|
|
155
|
+
reason: relationReasonSchema.nullable().optional().describe('Why this relation exists; null clears it'),
|
|
132
156
|
label: z.unknown().optional().describe('New label text; empty string or null clears the label'),
|
|
133
157
|
style: z.unknown().optional().describe('New stroke style: solid, dashed, or dotted'),
|
|
134
158
|
animated: z.unknown().optional().describe('Animate the edge stroke'),
|
|
@@ -162,7 +186,15 @@ const edgeUpdateOperation = defineOperation<z.infer<typeof edgeUpdateSchema>, Ca
|
|
|
162
186
|
{
|
|
163
187
|
type: 'text' as const,
|
|
164
188
|
text: JSON.stringify(
|
|
165
|
-
{
|
|
189
|
+
{
|
|
190
|
+
id: body.id,
|
|
191
|
+
type: body.type,
|
|
192
|
+
kind: body.kind,
|
|
193
|
+
reason: body.reason,
|
|
194
|
+
label: body.label,
|
|
195
|
+
style: body.style,
|
|
196
|
+
animated: body.animated,
|
|
197
|
+
},
|
|
166
198
|
null,
|
|
167
199
|
2,
|
|
168
200
|
),
|
|
@@ -177,7 +209,9 @@ const edgeUpdateOperation = defineOperation<z.infer<typeof edgeUpdateSchema>, Ca
|
|
|
177
209
|
if (!id) {
|
|
178
210
|
throw new OperationError('Missing required field: id.');
|
|
179
211
|
}
|
|
180
|
-
const patch: Partial<Pick<CanvasEdge, 'type' | 'label' | 'style' | 'animated'>> = {};
|
|
212
|
+
const patch: Partial<Pick<CanvasEdge, 'type' | 'kind' | 'reason' | 'label' | 'style' | 'animated'>> = {};
|
|
213
|
+
if ('kind' in input) patch.kind = input.kind ?? undefined;
|
|
214
|
+
if ('reason' in input) patch.reason = input.reason || undefined;
|
|
181
215
|
if ('type' in body && body.type !== undefined) {
|
|
182
216
|
if (typeof body.type !== 'string' || !VALID_EDGE_TYPES.has(body.type)) {
|
|
183
217
|
throw new OperationError(`Invalid edge type: "${String(body.type)}".`);
|
|
@@ -21,6 +21,8 @@ function pickChildLayout(value: unknown): 'grid' | 'column' | 'flow' | undefined
|
|
|
21
21
|
// ── group.create ──────────────────────────────────────────────
|
|
22
22
|
|
|
23
23
|
const groupCreateShape = {
|
|
24
|
+
destinationBoardId: z.string().min(1).optional().describe('Create the group on this board without activating it'),
|
|
25
|
+
requestId: z.string().min(1).optional().describe('Logical creation identity for retry-safe group creation'),
|
|
24
26
|
intentId: z
|
|
25
27
|
.string()
|
|
26
28
|
.optional()
|
|
@@ -90,7 +92,7 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
|
90
92
|
const width = typeof body.width === 'number' ? body.width : undefined;
|
|
91
93
|
const height = typeof body.height === 'number' ? body.height : undefined;
|
|
92
94
|
const childLayout = pickChildLayout(body.childLayout);
|
|
93
|
-
if (childIds.length > 0) {
|
|
95
|
+
if (childIds.length > 0 && input.destinationBoardId === undefined && input.requestId === undefined) {
|
|
94
96
|
const missingChildIds = childIds.filter((id) => !canvasState.getNode(id));
|
|
95
97
|
if (missingChildIds.length > 0) {
|
|
96
98
|
throw new OperationError(
|
|
@@ -99,6 +101,9 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
return createCanvasGroup({
|
|
104
|
+
destinationBoardId: input.destinationBoardId,
|
|
105
|
+
requestId: input.requestId,
|
|
106
|
+
intentId: input.intentId,
|
|
102
107
|
title,
|
|
103
108
|
childIds,
|
|
104
109
|
color,
|