pmx-canvas 0.8.2 → 0.9.1
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 +59 -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/human-store.d.ts +2 -1
- 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/evals/evals.json +224 -0
- 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 +88 -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 +543 -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 +6 -3
- 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/human-store.ts +13 -7
- 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 +235 -32
- 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
|
@@ -2,33 +2,39 @@
|
|
|
2
2
|
* Slice 3 operations (plan-005, migration item 4): pin.set / search /
|
|
3
3
|
* history.get / canvas.undo / canvas.redo / spatial.get / summary.get.
|
|
4
4
|
*
|
|
5
|
-
* Event notes
|
|
5
|
+
* Event notes:
|
|
6
6
|
* - pin.set: legacy POST /api/canvas/context-pins emitted ONLY
|
|
7
7
|
* context-pins-changed (no canvas-layout-update), so mutates: false with a
|
|
8
8
|
* manual ctx.emit. The injected emitter adds the sessionId/timestamp fields
|
|
9
9
|
* the legacy handler set explicitly. (The SDK's setContextPins emits the
|
|
10
10
|
* same context-pins-changed event — its old layout-update drift was erased
|
|
11
11
|
* in v0.3.0.)
|
|
12
|
-
* - canvas.undo / canvas.redo:
|
|
13
|
-
*
|
|
14
|
-
* so mutates: false with conditional manual emits.
|
|
15
|
-
*
|
|
16
|
-
* methods are untouched.)
|
|
12
|
+
* - canvas.undo / canvas.redo: successful replay emits viewport, layout and raw
|
|
13
|
+
* pin membership, since layout updates do not replace browser pins. Empty
|
|
14
|
+
* history emits nothing, so mutates: false with conditional manual emits.
|
|
15
|
+
* The SDK also emits ax-state-changed.
|
|
17
16
|
*
|
|
18
17
|
* This module must never import server.ts or index.ts.
|
|
19
18
|
*/
|
|
20
19
|
import { z } from 'zod';
|
|
21
|
-
import { canvasState } from '../../canvas-state.js';
|
|
20
|
+
import { canvasState, type CanvasNodeState } from '../../canvas-state.js';
|
|
22
21
|
import { setCanvasContextPins, syncCanvasRuntimeBackends } from '../../canvas-operations.js';
|
|
23
22
|
import { buildCanvasSummary } from '../../canvas-serialization.js';
|
|
24
23
|
import { mutationHistory } from '../../mutation-history.js';
|
|
25
|
-
import { buildSpatialContext, searchNodes } from '../../spatial-analysis.js';
|
|
24
|
+
import { buildSpatialContext, searchNodes, searchRelevance } from '../../spatial-analysis.js';
|
|
25
|
+
import { boardConnections, memoryGraph } from '../../memory-graph.js';
|
|
26
|
+
import { relationText } from '../../../shared/relations.js';
|
|
27
|
+
import { normalizeBoardFolder } from '../../../shared/boards.js';
|
|
28
|
+
import { lifecycleQualifications } from '../../lifecycle-status.js';
|
|
26
29
|
import {
|
|
27
30
|
compileContextBrief,
|
|
31
|
+
BRIEF_SUMMARY_LENGTH,
|
|
28
32
|
type ContextBriefLibraryBoard,
|
|
33
|
+
type ContextBriefPinnedBoard,
|
|
29
34
|
type ContextBriefSourceEntry,
|
|
30
35
|
} from '../../context-brief.js';
|
|
31
36
|
import { summarizeNodeForAgentContext } from '../../../shared/agent-context.js';
|
|
37
|
+
import { memoryKey, type MemoryTarget } from '../../../shared/memory-graph.js';
|
|
32
38
|
import { defineOperation, OperationError, type Operation, type OperationMcpToolHost } from '../types.js';
|
|
33
39
|
import { buildSummaryFromLayout, isRecord } from './nodes.js';
|
|
34
40
|
import { readTargetBoard } from './boards.js';
|
|
@@ -37,6 +43,11 @@ import { readTargetBoard } from './boards.js';
|
|
|
37
43
|
|
|
38
44
|
/** Legacy server.ts handleContextPinsUpdate capped the requested list at 20. */
|
|
39
45
|
const MAX_PINS = 20;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The brief's page size in UTF-16 characters (canvas://context?budget=N). It is
|
|
49
|
+
* not shown to the human: a brief longer than one page continues on the next read.
|
|
50
|
+
*/
|
|
40
51
|
export const DEFAULT_CONTEXT_BRIEF_BUDGET = 16_000;
|
|
41
52
|
export const MAX_CONTEXT_BRIEF_BUDGET = 100_000;
|
|
42
53
|
|
|
@@ -108,6 +119,8 @@ const pinOperation = defineOperation<z.infer<typeof pinSchema>, Record<string, u
|
|
|
108
119
|
const searchShape = {
|
|
109
120
|
q: z.unknown().optional().describe('Search query — matches against node titles, content, and file paths'),
|
|
110
121
|
scope: z.unknown().optional().describe('active (default) or library (all boards)'),
|
|
122
|
+
boardId: z.string().optional().describe('Stable board ID to search; defaults scope to library when supplied'),
|
|
123
|
+
folder: z.string().optional().describe('Folder and descendants; empty string selects unfiled boards'),
|
|
111
124
|
limit: z.unknown().optional().describe('Max results to return (default: all over HTTP, 10 via the MCP tool).'),
|
|
112
125
|
};
|
|
113
126
|
|
|
@@ -130,13 +143,17 @@ const searchOperation = defineOperation<z.infer<typeof searchSchema>, Record<str
|
|
|
130
143
|
query: z.string().describe('Search query — matches against node titles, content, and file paths'),
|
|
131
144
|
limit: z.number().optional().describe('Max results to return (default: 10)'),
|
|
132
145
|
scope: z.enum(['active', 'library']).optional().describe('Search the active board or the full board library'),
|
|
146
|
+
boardId: searchShape.boardId,
|
|
147
|
+
folder: searchShape.folder,
|
|
133
148
|
},
|
|
134
149
|
// Map the MCP-facing `query` arg onto the wire's `q`. The handler caps by
|
|
135
150
|
// `limit` on every transport; the MCP tool additionally defaults it to 10.
|
|
136
151
|
buildInput: (input) => ({
|
|
137
152
|
q: typeof input.query === 'string' ? input.query : '',
|
|
138
153
|
...(typeof input.limit === 'number' ? { limit: input.limit } : {}),
|
|
139
|
-
...(input.scope === 'library' ? { scope:
|
|
154
|
+
...(input.scope === 'library' || input.scope === 'active' ? { scope: input.scope } : {}),
|
|
155
|
+
...(typeof input.boardId === 'string' ? { boardId: input.boardId } : {}),
|
|
156
|
+
...(typeof input.folder === 'string' ? { folder: input.folder } : {}),
|
|
140
157
|
}),
|
|
141
158
|
formatResult: (result, input) => {
|
|
142
159
|
const body = isRecord(result) ? result : {};
|
|
@@ -160,58 +177,106 @@ const searchOperation = defineOperation<z.infer<typeof searchSchema>, Record<str
|
|
|
160
177
|
};
|
|
161
178
|
},
|
|
162
179
|
},
|
|
163
|
-
handler: (input) =>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
180
|
+
handler: (input) =>
|
|
181
|
+
searchCanvasNodes(typeof input.q === 'string' ? input.q : '', {
|
|
182
|
+
scope: input.scope === 'active' || input.scope === 'library' ? input.scope : undefined,
|
|
183
|
+
boardId: input.boardId,
|
|
184
|
+
folder: input.folder,
|
|
185
|
+
limit: typeof input.limit === 'number' || typeof input.limit === 'string' ? input.limit : undefined,
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/** Search without opening boards; HTTP, MCP and SDK share discovery and ranking. */
|
|
190
|
+
export function searchCanvasNodes(
|
|
191
|
+
q: string,
|
|
192
|
+
options: { scope?: 'active' | 'library'; boardId?: string; folder?: string; limit?: number | string } = {},
|
|
193
|
+
) {
|
|
194
|
+
if (!q.trim()) {
|
|
195
|
+
return { results: [], query: q };
|
|
196
|
+
}
|
|
197
|
+
const rawLimit = options.limit;
|
|
198
|
+
const limit =
|
|
199
|
+
typeof rawLimit === 'number'
|
|
200
|
+
? rawLimit
|
|
201
|
+
: typeof rawLimit === 'string' && rawLimit.trim() !== ''
|
|
202
|
+
? Number(rawLimit)
|
|
203
|
+
: Number.NaN;
|
|
204
|
+
const hasFilter = options.boardId !== undefined || options.folder !== undefined;
|
|
205
|
+
const library = options.scope === 'library' || (options.scope === undefined && hasFilter);
|
|
206
|
+
const folder = options.folder === undefined ? undefined : normalizeBoardFolder(options.folder);
|
|
207
|
+
const selectedBoards = canvasState
|
|
208
|
+
.listBoards()
|
|
209
|
+
.filter(
|
|
210
|
+
(board) =>
|
|
211
|
+
(options.boardId === undefined || board.id === options.boardId) &&
|
|
212
|
+
(folder === undefined ||
|
|
213
|
+
board.folder === folder ||
|
|
214
|
+
(folder !== null && board.folder?.startsWith(`${folder}/`))),
|
|
215
|
+
);
|
|
216
|
+
const results = library
|
|
217
|
+
? selectedBoards.flatMap((board) => {
|
|
218
|
+
const read = canvasState.readBoard(board.id, true);
|
|
219
|
+
if (!read) return [];
|
|
220
|
+
const cards = searchNodes(read.layout.nodes, q).map((result) => ({
|
|
204
221
|
...result,
|
|
205
|
-
boardId:
|
|
222
|
+
boardId: board.id,
|
|
223
|
+
boardTitle: board.name,
|
|
206
224
|
cardId: result.id,
|
|
207
225
|
}));
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
226
|
+
const readme = read.layout.nodes.find((node) => node.id === board.readmeNodeId);
|
|
227
|
+
const readmeText = typeof readme?.data.content === 'string' ? readme.data.content : '';
|
|
228
|
+
const score = searchRelevance(
|
|
229
|
+
board.name,
|
|
230
|
+
[
|
|
231
|
+
[board.folder ?? '', 2],
|
|
232
|
+
[readmeText, 1],
|
|
233
|
+
],
|
|
234
|
+
q,
|
|
235
|
+
);
|
|
236
|
+
if (score > 0) {
|
|
237
|
+
cards.unshift({
|
|
238
|
+
id: board.id,
|
|
239
|
+
type: 'board',
|
|
240
|
+
title: board.name,
|
|
241
|
+
snippet: readmeText.slice(0, 160) || board.folder || board.name,
|
|
242
|
+
score,
|
|
243
|
+
boardId: board.id,
|
|
244
|
+
boardTitle: board.name,
|
|
245
|
+
cardId: readme?.id ?? board.id,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return cards;
|
|
249
|
+
})
|
|
250
|
+
: searchNodes(
|
|
251
|
+
!hasFilter || selectedBoards.some((board) => board.id === canvasState.activeBoardId)
|
|
252
|
+
? canvasState.getLayout().nodes
|
|
253
|
+
: [],
|
|
254
|
+
q,
|
|
255
|
+
).map((result) => ({
|
|
256
|
+
...result,
|
|
257
|
+
boardId: canvasState.activeBoardId,
|
|
258
|
+
cardId: result.id,
|
|
259
|
+
}));
|
|
260
|
+
results.sort((a, b) => {
|
|
261
|
+
const aKey = `${a.boardId ?? ''}/${a.id}`;
|
|
262
|
+
const bKey = `${b.boardId ?? ''}/${b.id}`;
|
|
263
|
+
return b.score - a.score || (aKey < bKey ? -1 : aKey > bKey ? 1 : 0);
|
|
264
|
+
});
|
|
265
|
+
const delivered = Number.isFinite(limit) && limit > 0 ? results.slice(0, Math.floor(limit)) : results;
|
|
266
|
+
const qualifications = lifecycleQualifications(
|
|
267
|
+
Date.now(),
|
|
268
|
+
delivered.flatMap((result) => (result.boardId ? [{ boardId: result.boardId, cardId: result.cardId }] : [])),
|
|
269
|
+
);
|
|
270
|
+
return {
|
|
271
|
+
results: delivered.map((result) => ({
|
|
272
|
+
...result,
|
|
273
|
+
...(result.boardId
|
|
274
|
+
? { qualification: qualifications[memoryKey({ boardId: result.boardId, cardId: result.cardId })] }
|
|
275
|
+
: {}),
|
|
276
|
+
})),
|
|
277
|
+
query: q,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
215
280
|
|
|
216
281
|
// ── context.get ───────────────────────────────────────────────
|
|
217
282
|
|
|
@@ -246,28 +311,83 @@ const contextOperation = defineOperation<z.infer<typeof contextSchema>, Record<s
|
|
|
246
311
|
|
|
247
312
|
// Capture the active target and revision synchronously. Library boards are
|
|
248
313
|
// read from SQLite and are never opened or made writable by this operation.
|
|
314
|
+
let qualifications = lifecycleQualifications();
|
|
315
|
+
const included = (boardId: string, node: CanvasNodeState) =>
|
|
316
|
+
!qualifications[memoryKey({ boardId, cardId: node.id })]?.retired;
|
|
249
317
|
const layout = canvasState.getLayout();
|
|
318
|
+
layout.nodes = layout.nodes.filter((node) => included(active.id, node));
|
|
319
|
+
// One read per board per brief: linked/same-folder tiers, README summaries and
|
|
320
|
+
// pinned boards all share it (the link scan below reads without blobs).
|
|
321
|
+
const fullReads = new Map<string, ReturnType<typeof canvasState.readBoard>>();
|
|
322
|
+
const readFull = (boardId: string) => {
|
|
323
|
+
if (!fullReads.has(boardId)) {
|
|
324
|
+
const read = canvasState.readBoard(boardId, true);
|
|
325
|
+
if (read) read.layout.nodes = read.layout.nodes.filter((node) => included(boardId, node));
|
|
326
|
+
fullReads.set(boardId, read);
|
|
327
|
+
}
|
|
328
|
+
return fullReads.get(boardId) ?? null;
|
|
329
|
+
};
|
|
250
330
|
const revision = canvasState.getContentRevision();
|
|
251
331
|
const pinnedNodeIds = [...canvasState.contextPinnedNodeIds];
|
|
252
332
|
const linkedIds = new Map<string, string[]>();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
333
|
+
const graph = memoryGraph();
|
|
334
|
+
const graphNodes = new Map(graph.nodes.map((node) => [node.key, node]));
|
|
335
|
+
const qualifiedTitle = (target: MemoryTarget): string | undefined => {
|
|
336
|
+
const card = graphNodes.get(memoryKey(target));
|
|
337
|
+
const board = graphNodes.get(memoryKey({ boardId: target.boardId }));
|
|
338
|
+
return card && board ? (target.cardId ? `${board.title} / ${card.title}` : board.title) : undefined;
|
|
339
|
+
};
|
|
340
|
+
const connections = boardConnections(graph).filter((edge) => !edge.missing);
|
|
341
|
+
const memoryRelations: Record<string, string[]> = {};
|
|
342
|
+
for (const edge of graph.edges) {
|
|
343
|
+
if (edge.kind === 'member') continue;
|
|
344
|
+
// Local and outgoing card edges are compiled below with their revision marks.
|
|
345
|
+
// Only incoming cross-board card edges need the library graph here.
|
|
346
|
+
if (edge.kind === 'card' && (edge.source.boardId === edge.target.boardId || edge.target.boardId !== active.id))
|
|
347
|
+
continue;
|
|
348
|
+
for (const [source, target, direction] of [
|
|
349
|
+
[edge.source, edge.target, '→'],
|
|
350
|
+
[edge.target, edge.source, '←'],
|
|
351
|
+
] as const) {
|
|
352
|
+
if (source.boardId !== active.id || !source.cardId) continue;
|
|
353
|
+
const other = graphNodes.get(direction === '→' ? edge.to : edge.from);
|
|
354
|
+
const kind = edge.kind === 'card' ? relationText({ ...edge, kind: edge.relationKind }) : edge.kind;
|
|
355
|
+
memoryRelations[source.cardId] ??= [];
|
|
356
|
+
memoryRelations[source.cardId].push(
|
|
357
|
+
`${direction} ${kind}: ${qualifiedTitle(target) ?? other?.title ?? 'Missing target'} (${target.boardId}${target.cardId ? ` / ${target.cardId}` : ''})`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (const link of graph.unresolvedLinks) {
|
|
362
|
+
if (link.source.boardId === active.id && link.source.cardId) {
|
|
363
|
+
memoryRelations[link.source.cardId] ??= [];
|
|
364
|
+
memoryRelations[link.source.cardId].push(`Unresolved wiki: [[${link.ref}]]`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
for (const edge of connections) {
|
|
368
|
+
const other =
|
|
369
|
+
edge.source.boardId === active.id
|
|
370
|
+
? edge.target.boardId
|
|
371
|
+
: edge.target.boardId === active.id
|
|
372
|
+
? edge.source.boardId
|
|
373
|
+
: null;
|
|
374
|
+
if (!other) continue;
|
|
375
|
+
const ids = linkedIds.get(other) ?? [];
|
|
376
|
+
if (edge.source.boardId === active.id && edge.source.cardId) ids.push(edge.source.cardId);
|
|
377
|
+
linkedIds.set(other, ids);
|
|
258
378
|
}
|
|
259
379
|
const libraryBoards: ContextBriefLibraryBoard[] = [];
|
|
260
380
|
for (const board of canvasState.listBoards()) {
|
|
261
381
|
if (board.id === active.id) continue;
|
|
262
|
-
if (!linkedIds.has(board.id) && (!active.
|
|
263
|
-
const read =
|
|
382
|
+
if (!linkedIds.has(board.id) && (!active.folder || active.folder !== board.folder)) continue;
|
|
383
|
+
const read = readFull(board.id);
|
|
264
384
|
if (!read) continue;
|
|
265
385
|
const readme = read.layout.nodes.find((node) => node.id === board.readmeNodeId);
|
|
266
386
|
const pins = new Set(read.state.contextPins);
|
|
267
387
|
libraryBoards.push({
|
|
268
388
|
boardId: board.id,
|
|
269
389
|
name: board.name,
|
|
270
|
-
|
|
390
|
+
folder: board.folder,
|
|
271
391
|
...(readme
|
|
272
392
|
? {
|
|
273
393
|
readme: {
|
|
@@ -286,6 +406,65 @@ const contextOperation = defineOperation<z.infer<typeof contextSchema>, Record<s
|
|
|
286
406
|
linkIds: linkedIds.get(board.id) ?? [],
|
|
287
407
|
});
|
|
288
408
|
}
|
|
409
|
+
// The brief is a map (vision move 0a and the maintainer's 2026-10-07 rule):
|
|
410
|
+
// titles, short summaries and relations; the agent pulls what it wants in full.
|
|
411
|
+
const title = (node: { id: string; data: Record<string, unknown> }) =>
|
|
412
|
+
typeof node.data.title === 'string' && node.data.title ? node.data.title : node.id;
|
|
413
|
+
const short = (node: CanvasNodeState) =>
|
|
414
|
+
summarizeNodeForAgentContext(node, {
|
|
415
|
+
defaultTextLength: BRIEF_SUMMARY_LENGTH,
|
|
416
|
+
webpageTextLength: BRIEF_SUMMARY_LENGTH,
|
|
417
|
+
});
|
|
418
|
+
// Connections come from the shared graph; README summaries are read
|
|
419
|
+
// only for the open board and pinned boards, so a large library stays cheap.
|
|
420
|
+
const library = canvasState.listBoards();
|
|
421
|
+
const names = new Map(library.map((board) => [board.id, board.name]));
|
|
422
|
+
const linksFrom = new Map<string, string[]>();
|
|
423
|
+
for (const board of library) {
|
|
424
|
+
linksFrom.set(
|
|
425
|
+
board.id,
|
|
426
|
+
connections.filter((edge) => edge.source.boardId === board.id).map((edge) => edge.target.boardId),
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const readmeSummary = (boardId: string): string | null => {
|
|
430
|
+
const board = library.find((entry) => entry.id === boardId);
|
|
431
|
+
if (!board?.readmeNodeId) return null;
|
|
432
|
+
const nodes = boardId === active.id ? layout.nodes : (readFull(boardId)?.layout.nodes ?? []);
|
|
433
|
+
const readme = nodes.find((node) => node.id === board.readmeNodeId && node.type === 'markdown');
|
|
434
|
+
return readme ? summarizeNodeForAgentContext(readme, { defaultTextLength: 1_000 }) : null;
|
|
435
|
+
};
|
|
436
|
+
const relationsOf = (boardId: string) => ({
|
|
437
|
+
folder: library.find((board) => board.id === boardId)?.folder ?? null,
|
|
438
|
+
readmeSummary: readmeSummary(boardId),
|
|
439
|
+
readmeNodeId: library.find((board) => board.id === boardId)?.readmeNodeId ?? undefined,
|
|
440
|
+
links: [...new Set((linksFrom.get(boardId) ?? []).flatMap((id) => names.get(id) ?? []))],
|
|
441
|
+
backlinks: [
|
|
442
|
+
...new Set(
|
|
443
|
+
[...linksFrom].filter(([, targets]) => targets.includes(boardId)).flatMap(([id]) => names.get(id) ?? []),
|
|
444
|
+
),
|
|
445
|
+
],
|
|
446
|
+
});
|
|
447
|
+
const overview = relationsOf(active.id);
|
|
448
|
+
const pinReasons: Record<string, string> = {};
|
|
449
|
+
for (const [nodeId, meta] of Object.entries(canvasState.getContextPinMeta())) {
|
|
450
|
+
if (meta.reason) pinReasons[nodeId] = meta.reason;
|
|
451
|
+
}
|
|
452
|
+
const pinnedBoards: ContextBriefPinnedBoard[] = [];
|
|
453
|
+
for (const board of library) {
|
|
454
|
+
if (!board.pin || board.id === active.id) continue;
|
|
455
|
+
const read = readFull(board.id);
|
|
456
|
+
if (!read) continue;
|
|
457
|
+
const pins = new Set(read.state.contextPins);
|
|
458
|
+
pinnedBoards.push({
|
|
459
|
+
boardId: board.id,
|
|
460
|
+
name: board.name,
|
|
461
|
+
...relationsOf(board.id),
|
|
462
|
+
cards: read.layout.nodes
|
|
463
|
+
.filter((node) => pins.has(node.id))
|
|
464
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
465
|
+
.map((node) => ({ nodeId: node.id, title: title(node), summary: short(node) })),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
289
468
|
const entries: ContextBriefSourceEntry[] = layout.nodes
|
|
290
469
|
.filter((node) => node.createdBy?.actor === 'human')
|
|
291
470
|
.map((node) => ({
|
|
@@ -293,7 +472,7 @@ const contextOperation = defineOperation<z.infer<typeof contextSchema>, Record<s
|
|
|
293
472
|
nodeId: node.id,
|
|
294
473
|
reason: 'human' as const,
|
|
295
474
|
title: typeof node.data.title === 'string' ? node.data.title : node.id,
|
|
296
|
-
text:
|
|
475
|
+
text: short(node),
|
|
297
476
|
}));
|
|
298
477
|
entries.push(
|
|
299
478
|
...canvasState
|
|
@@ -336,15 +515,41 @@ const contextOperation = defineOperation<z.infer<typeof contextSchema>, Record<s
|
|
|
336
515
|
text: item.message,
|
|
337
516
|
})),
|
|
338
517
|
);
|
|
518
|
+
qualifications = lifecycleQualifications(
|
|
519
|
+
Date.now(),
|
|
520
|
+
[...fullReads.keys()].map((boardId) => ({ boardId })),
|
|
521
|
+
);
|
|
339
522
|
const result = compileContextBrief({
|
|
340
|
-
|
|
523
|
+
qualifications,
|
|
524
|
+
activeBoard: { boardId: active.id, name: active.name, folder: active.folder },
|
|
341
525
|
nodes: layout.nodes,
|
|
342
526
|
pinnedNodeIds,
|
|
343
527
|
contentRevision: revision.revision,
|
|
344
528
|
retentionFloor: revision.retentionFloor,
|
|
345
529
|
tombstones: canvasState.readContentDelta(sinceValue ?? 0).deleted,
|
|
346
530
|
since: explicitSince ? (Number.isSafeInteger(sinceValue) ? sinceValue : Number.NaN) : sinceValue,
|
|
531
|
+
overview,
|
|
532
|
+
memoryRelations,
|
|
533
|
+
edges: layout.edges.map((edge) => ({
|
|
534
|
+
from: edge.from,
|
|
535
|
+
to: edge.to,
|
|
536
|
+
toBoardId: edge.toBoardId,
|
|
537
|
+
remoteTitle: edge.toBoardId
|
|
538
|
+
? (qualifiedTitle({ boardId: edge.toBoardId, cardId: edge.to }) ??
|
|
539
|
+
`Missing target (${edge.toBoardId} / ${edge.to})`)
|
|
540
|
+
: undefined,
|
|
541
|
+
type: edge.type,
|
|
542
|
+
kind: edge.kind,
|
|
543
|
+
reason: edge.reason,
|
|
544
|
+
label: edge.label ?? null,
|
|
545
|
+
revision: edge.revision,
|
|
546
|
+
changedBy: edge.changedBy,
|
|
547
|
+
createdAt: edge.createdAt,
|
|
548
|
+
changedAt: edge.changedAt,
|
|
549
|
+
})),
|
|
550
|
+
pinReasons,
|
|
347
551
|
libraryBoards,
|
|
552
|
+
pinnedBoards,
|
|
348
553
|
entries,
|
|
349
554
|
budget,
|
|
350
555
|
});
|
|
@@ -425,6 +630,10 @@ const undoOperation = defineOperation<z.infer<typeof undoRedoSchema>, Record<str
|
|
|
425
630
|
await syncCanvasRuntimeBackends();
|
|
426
631
|
ctx.emit('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
427
632
|
ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
633
|
+
ctx.emit('context-pins-changed', {
|
|
634
|
+
count: canvasState.contextPinnedNodeIds.size,
|
|
635
|
+
nodeIds: Array.from(canvasState.contextPinnedNodeIds),
|
|
636
|
+
});
|
|
428
637
|
return { ok: true, description: `Undid: ${entry.description}` };
|
|
429
638
|
},
|
|
430
639
|
});
|
|
@@ -449,6 +658,10 @@ const redoOperation = defineOperation<z.infer<typeof undoRedoSchema>, Record<str
|
|
|
449
658
|
await syncCanvasRuntimeBackends();
|
|
450
659
|
ctx.emit('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
451
660
|
ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
661
|
+
ctx.emit('context-pins-changed', {
|
|
662
|
+
count: canvasState.contextPinnedNodeIds.size,
|
|
663
|
+
nodeIds: Array.from(canvasState.contextPinnedNodeIds),
|
|
664
|
+
});
|
|
452
665
|
return { ok: true, description: `Redid: ${entry.description}` };
|
|
453
666
|
},
|
|
454
667
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slice 3 operations (plan-005, migration item 5): snapshot.list /
|
|
3
3
|
* snapshot.save / snapshot.gc / snapshot.diff (+ query-param variant) /
|
|
4
|
-
* snapshot.restore / snapshot.delete.
|
|
4
|
+
* snapshot.restore / snapshot.restore-node / snapshot.delete.
|
|
5
5
|
*
|
|
6
6
|
* Wire notes:
|
|
7
7
|
* - snapshot.list serves the bare-array body (tests/e2e clearSnapshots reads
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { z } from 'zod';
|
|
26
26
|
import { canvasState } from '../../canvas-state.js';
|
|
27
|
+
import { wikiLinksFromData } from '../../../shared/memory-graph.js';
|
|
27
28
|
import {
|
|
28
29
|
deleteCanvasSnapshot,
|
|
29
30
|
gcCanvasSnapshots,
|
|
@@ -333,6 +334,76 @@ const snapshotRestoreOperation = defineOperation<z.infer<typeof snapshotRestoreS
|
|
|
333
334
|
},
|
|
334
335
|
});
|
|
335
336
|
|
|
337
|
+
// ── snapshot.restore-node ─────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
const snapshotRestoreNodeShape = {
|
|
340
|
+
id: z.string().optional().catch(undefined).describe('Snapshot ID or name holding the earlier version'),
|
|
341
|
+
nodeId: z.string().optional().catch(undefined).describe('The card to put back as it was in that snapshot'),
|
|
342
|
+
preserveHumanEdits: z.boolean().optional().describe('Skip a card whose latest editor is human (receipt Undo)'),
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const snapshotRestoreNodeSchema = z.looseObject(snapshotRestoreNodeShape);
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* One card back to its content in a snapshot — the receipt's Undo on an edit
|
|
349
|
+
* (docs/design/AgentContext.dc.html), without restoring the rest of the board.
|
|
350
|
+
* A normal, undoable node edit: position, links and pins stay as they are.
|
|
351
|
+
*/
|
|
352
|
+
const snapshotRestoreNodeOperation = defineOperation<
|
|
353
|
+
z.infer<typeof snapshotRestoreNodeSchema>,
|
|
354
|
+
Record<string, unknown>
|
|
355
|
+
>({
|
|
356
|
+
name: 'snapshot.restore-node',
|
|
357
|
+
mutates: true,
|
|
358
|
+
input: snapshotRestoreNodeSchema,
|
|
359
|
+
inputShape: snapshotRestoreNodeShape,
|
|
360
|
+
http: {
|
|
361
|
+
method: 'POST',
|
|
362
|
+
path: '/api/canvas/snapshots/:id/restore-node',
|
|
363
|
+
},
|
|
364
|
+
handler: ({ id, nodeId, preserveHumanEdits }) => {
|
|
365
|
+
const snapshot = canvasState.getSnapshotData(id ?? '');
|
|
366
|
+
if (!snapshot) throw new OperationError('Snapshot not found', 404);
|
|
367
|
+
const current = canvasState.getNode(nodeId ?? '');
|
|
368
|
+
const earlier = snapshot.nodes.find((node) => node.id === nodeId);
|
|
369
|
+
if (!current || !earlier)
|
|
370
|
+
throw new OperationError(`Node "${nodeId}" is not in both the board and the snapshot.`, 404);
|
|
371
|
+
// A file card mirrors its file on disk; old text would only be overwritten by the watcher.
|
|
372
|
+
if (current.type === 'file') throw new OperationError('A file card follows its file; restore the file instead.');
|
|
373
|
+
if (earlier.type !== current.type) {
|
|
374
|
+
throw new OperationError(
|
|
375
|
+
`Node "${current.id}" was a ${earlier.type} card in that snapshot; it is a ${current.type} now.`,
|
|
376
|
+
409,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
// Check current ownership at the write boundary, not a browser's last poll.
|
|
380
|
+
if (preserveHumanEdits && current.lastEditedBy?.actor === 'human') {
|
|
381
|
+
return { ok: true, id: current.id, restored: false, skipped: 'human-edit' };
|
|
382
|
+
}
|
|
383
|
+
// Content only: where the card sits in groups is today's, not the snapshot's.
|
|
384
|
+
const data = { ...earlier.data };
|
|
385
|
+
for (const key of ['parentGroup', 'children'] as const) {
|
|
386
|
+
if (current.data[key] === undefined) delete data[key];
|
|
387
|
+
else data[key] = current.data[key];
|
|
388
|
+
}
|
|
389
|
+
canvasState.updateNode(
|
|
390
|
+
current.id,
|
|
391
|
+
{ data },
|
|
392
|
+
earlier.type === 'markdown'
|
|
393
|
+
? earlier.data.wikiLinks === undefined
|
|
394
|
+
? null
|
|
395
|
+
: wikiLinksFromData(earlier.data)
|
|
396
|
+
: undefined,
|
|
397
|
+
);
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
id: current.id,
|
|
401
|
+
restored: true,
|
|
402
|
+
contentRevision: canvasState.getNode(current.id)?.contentRevision ?? null,
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
336
407
|
// ── snapshot.delete ───────────────────────────────────────────
|
|
337
408
|
|
|
338
409
|
const snapshotDeleteShape = {
|
|
@@ -375,6 +446,7 @@ export const snapshotOperations: Operation[] = [
|
|
|
375
446
|
snapshotGcOperation,
|
|
376
447
|
snapshotDiffQueryOperation,
|
|
377
448
|
snapshotDiffOperation,
|
|
449
|
+
snapshotRestoreNodeOperation,
|
|
378
450
|
snapshotRestoreOperation,
|
|
379
451
|
snapshotDeleteOperation,
|
|
380
452
|
];
|
|
@@ -32,7 +32,11 @@ const validateGetOperation = defineOperation<z.infer<typeof validateGetSchema>,
|
|
|
32
32
|
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
33
33
|
}),
|
|
34
34
|
},
|
|
35
|
-
handler: () =>
|
|
35
|
+
handler: () =>
|
|
36
|
+
validateCanvasLayout(
|
|
37
|
+
canvasState.getLayout(),
|
|
38
|
+
(boardId, cardId) => !!canvasState.readBoard(boardId, false)?.layout.nodes.some((node) => node.id === cardId),
|
|
39
|
+
),
|
|
36
40
|
});
|
|
37
41
|
|
|
38
42
|
export const validateOperations: Operation[] = [validateGetOperation];
|