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
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { canvasArea } from '../canvas/canvas-area';
|
|
1
|
+
import { canvasArea, canvasFitInsets } from '../canvas/canvas-area';
|
|
2
2
|
import { showToast } from './attention-bridge';
|
|
3
3
|
import { isHostedWorkbench, workbenchFetch } from './workbench-transport';
|
|
4
4
|
import type { AgentPresenceSnapshot } from '../../shared/agent-presence.js';
|
|
5
|
-
import type { CanvasEdge } from '../types';
|
|
5
|
+
import type { CanvasEdge, CanvasNodeState } from '../types';
|
|
6
6
|
import { addEdge, canvasInteractionEpoch, nodes } from './canvas-store';
|
|
7
|
-
import { boardsLoaded } from './boards-store';
|
|
7
|
+
import { activeBoardId, boardsLoaded } from './boards-store';
|
|
8
|
+
import { humanClientId } from './human-store';
|
|
8
9
|
function logRequestError(action: string, error: unknown): void {
|
|
9
10
|
console.error(`[intent-bridge] ${action} failed`, error);
|
|
10
11
|
}
|
|
@@ -141,7 +142,11 @@ export async function vetoGhostIntent(intent: {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
/** Fetch rendered markdown HTML from the server. */
|
|
144
|
-
export async function renderMarkdown(
|
|
145
|
+
export async function renderMarkdown(
|
|
146
|
+
markdown: string,
|
|
147
|
+
nodeId?: string,
|
|
148
|
+
expectedBoardId: string | null = activeBoardId.value,
|
|
149
|
+
): Promise<string | null> {
|
|
145
150
|
const data = await requestJson<{ html?: string }>(
|
|
146
151
|
'renderMarkdown',
|
|
147
152
|
'/api/render',
|
|
@@ -149,10 +154,10 @@ export async function renderMarkdown(markdown: string): Promise<string> {
|
|
|
149
154
|
{
|
|
150
155
|
method: 'POST',
|
|
151
156
|
headers: { 'Content-Type': 'application/json' },
|
|
152
|
-
body: JSON.stringify({ markdown }),
|
|
157
|
+
body: JSON.stringify({ markdown, ...(nodeId ? { nodeId, expectedBoardId } : {}) }),
|
|
153
158
|
},
|
|
154
159
|
);
|
|
155
|
-
return data.html ??
|
|
160
|
+
return data.html ?? null;
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
/** Fetch file content from the server. */
|
|
@@ -163,20 +168,6 @@ export async function fetchFile(path: string): Promise<{
|
|
|
163
168
|
return requestJson('fetchFile', `/api/file?path=${encodeURIComponent(path)}`, { content: '' });
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
/** Save file content to the server. */
|
|
167
|
-
export async function saveFile(path: string, content: string): Promise<{ ok: boolean; updatedAt?: string }> {
|
|
168
|
-
return requestJson(
|
|
169
|
-
'saveFile',
|
|
170
|
-
'/api/file/save',
|
|
171
|
-
{ ok: false },
|
|
172
|
-
{
|
|
173
|
-
method: 'POST',
|
|
174
|
-
headers: { 'Content-Type': 'application/json' },
|
|
175
|
-
body: JSON.stringify({ path, content }),
|
|
176
|
-
},
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
171
|
/** Fetch current workbench state. */
|
|
181
172
|
export async function fetchWorkbenchState(): Promise<Record<string, unknown>> {
|
|
182
173
|
return requestJson('fetchWorkbenchState', '/api/workbench/state', {});
|
|
@@ -192,8 +183,12 @@ export async function openWorkbenchFile(path: string): Promise<{ ok: boolean }>
|
|
|
192
183
|
}
|
|
193
184
|
|
|
194
185
|
/** Fetch canvas state from server. */
|
|
195
|
-
export async function fetchCanvasState(): Promise<Record<string, unknown>> {
|
|
196
|
-
return requestJson(
|
|
186
|
+
export async function fetchCanvasState(expectedBoardId: string | null): Promise<Record<string, unknown>> {
|
|
187
|
+
return requestJson(
|
|
188
|
+
'fetchCanvasState',
|
|
189
|
+
`/api/canvas/state?includeBlobs=true&expectedBoardId=${encodeURIComponent(expectedBoardId ?? 'null')}`,
|
|
190
|
+
{},
|
|
191
|
+
);
|
|
197
192
|
}
|
|
198
193
|
|
|
199
194
|
export async function saveCanvasTheme(theme: string): Promise<{ ok: boolean; theme?: string }> {
|
|
@@ -262,7 +257,11 @@ export async function pushCanvasUpdate(
|
|
|
262
257
|
await requestBestEffort('pushCanvasUpdate', '/api/canvas/update', {
|
|
263
258
|
method: 'POST',
|
|
264
259
|
headers: { 'Content-Type': 'application/json' },
|
|
265
|
-
body: JSON.stringify({
|
|
260
|
+
body: JSON.stringify({
|
|
261
|
+
updates,
|
|
262
|
+
...(boardsLoaded.value ? { expectedBoardId: activeBoardId.value } : {}),
|
|
263
|
+
...(options.recordHistory === false ? { recordHistory: false } : {}),
|
|
264
|
+
}),
|
|
266
265
|
});
|
|
267
266
|
}
|
|
268
267
|
|
|
@@ -272,6 +271,7 @@ export async function createEdgeFromClient(
|
|
|
272
271
|
to: string,
|
|
273
272
|
type: string,
|
|
274
273
|
label?: string,
|
|
274
|
+
options: { toBoardId?: string; expectedBoardId?: string | null } = {},
|
|
275
275
|
): Promise<{ ok: boolean; id?: string }> {
|
|
276
276
|
const epoch = canvasInteractionEpoch.value;
|
|
277
277
|
const result = await requestJson<({ ok: true } & CanvasEdge) | { ok: false; error?: string }>(
|
|
@@ -281,10 +281,10 @@ export async function createEdgeFromClient(
|
|
|
281
281
|
{
|
|
282
282
|
method: 'POST',
|
|
283
283
|
headers: { 'Content-Type': 'application/json' },
|
|
284
|
-
body: JSON.stringify({ from, to, type, label }),
|
|
284
|
+
body: JSON.stringify({ from, to, type, label, ...options }),
|
|
285
285
|
},
|
|
286
286
|
);
|
|
287
|
-
if (epoch === canvasInteractionEpoch.value && nodes.value.has(from) && nodes.value.has(to)) {
|
|
287
|
+
if (epoch === canvasInteractionEpoch.value && nodes.value.has(from) && (options.toBoardId || nodes.value.has(to))) {
|
|
288
288
|
if (result.ok) {
|
|
289
289
|
const { ok, ...edge } = result;
|
|
290
290
|
addEdge(edge);
|
|
@@ -333,8 +333,9 @@ export async function updateNodeFromClient(
|
|
|
333
333
|
children?: string[];
|
|
334
334
|
expectedContentRevision?: number;
|
|
335
335
|
expectedBoardId?: string | null;
|
|
336
|
+
persistFile?: boolean;
|
|
336
337
|
},
|
|
337
|
-
): Promise<{ ok: boolean; id?: string; contentRevision?: number }> {
|
|
338
|
+
): Promise<{ ok: boolean; id?: string; contentRevision?: number; lastEditedBy?: CanvasNodeState['lastEditedBy'] }> {
|
|
338
339
|
return requestJson(
|
|
339
340
|
'updateNodeFromClient',
|
|
340
341
|
`/api/canvas/node/${encodeURIComponent(id)}`,
|
|
@@ -451,6 +452,8 @@ export async function updateViewportFromClient(
|
|
|
451
452
|
body: JSON.stringify({
|
|
452
453
|
...viewport,
|
|
453
454
|
...clientWindowSize(),
|
|
455
|
+
clientId: humanClientId,
|
|
456
|
+
...(boardsLoaded.value ? { expectedBoardId: activeBoardId.value } : {}),
|
|
454
457
|
...(options.recordHistory === false ? { recordHistory: false } : {}),
|
|
455
458
|
}),
|
|
456
459
|
},
|
|
@@ -462,7 +465,13 @@ export async function updateViewportFromClient(
|
|
|
462
465
|
* `fit` computes against the human's REAL window instead of a hardcoded
|
|
463
466
|
* 1440x900 (0.4.6 orb feedback #2: fit clipped nodes on both sides).
|
|
464
467
|
*/
|
|
465
|
-
function clientWindowSize():
|
|
468
|
+
function clientWindowSize():
|
|
469
|
+
| {
|
|
470
|
+
clientWidth: number;
|
|
471
|
+
clientHeight: number;
|
|
472
|
+
clientInsets: { top: number; bottom: number };
|
|
473
|
+
}
|
|
474
|
+
| Record<string, never> {
|
|
466
475
|
if (typeof window === 'undefined') return {};
|
|
467
476
|
// The CANVAS REGION, not the window — with the rail + top-bar shell a
|
|
468
477
|
// window-sized fit would hide nodes under the chrome.
|
|
@@ -470,7 +479,8 @@ function clientWindowSize(): { clientWidth: number; clientHeight: number } | Rec
|
|
|
470
479
|
const clientWidth = Math.round(area.width);
|
|
471
480
|
const clientHeight = Math.round(area.height);
|
|
472
481
|
if (!clientWidth || !clientHeight) return {};
|
|
473
|
-
|
|
482
|
+
const { top, bottom } = canvasFitInsets();
|
|
483
|
+
return { clientWidth, clientHeight, clientInsets: { top, bottom } };
|
|
474
484
|
}
|
|
475
485
|
|
|
476
486
|
/**
|
|
@@ -488,7 +498,11 @@ export async function reportClientViewportSize(): Promise<void> {
|
|
|
488
498
|
await requestBestEffort('reportClientViewportSize', '/api/canvas/viewport', {
|
|
489
499
|
method: 'POST',
|
|
490
500
|
headers: { 'Content-Type': 'application/json' },
|
|
491
|
-
body: JSON.stringify({
|
|
501
|
+
body: JSON.stringify({
|
|
502
|
+
...size,
|
|
503
|
+
...(boardsLoaded.value ? { expectedBoardId: activeBoardId.value } : {}),
|
|
504
|
+
recordHistory: false,
|
|
505
|
+
}),
|
|
492
506
|
});
|
|
493
507
|
}
|
|
494
508
|
|
|
@@ -595,7 +609,14 @@ export async function deleteSnapshot(id: string): Promise<{ ok: boolean }> {
|
|
|
595
609
|
/** Patch an edge in place (label/type/style/animated) via the server. */
|
|
596
610
|
export async function updateEdgeFromClient(
|
|
597
611
|
edgeId: string,
|
|
598
|
-
patch: {
|
|
612
|
+
patch: {
|
|
613
|
+
kind?: string | null;
|
|
614
|
+
reason?: string | null;
|
|
615
|
+
label?: string;
|
|
616
|
+
type?: string;
|
|
617
|
+
style?: string;
|
|
618
|
+
animated?: boolean;
|
|
619
|
+
},
|
|
599
620
|
): Promise<{ ok: boolean }> {
|
|
600
621
|
return requestJson(
|
|
601
622
|
'updateEdgeFromClient',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { computed, signal } from '@preact/signals';
|
|
2
|
+
import { knowledgeLifecycleEnabled } from './workspace-settings';
|
|
3
|
+
import type { KnowledgeResults } from '../../server/knowledge-lifecycle.js';
|
|
4
|
+
import { memoryKey } from '../../shared/memory-graph.js';
|
|
5
|
+
import { activeBoardId, boardIncarnation, lifecycleFilter } from './boards-store';
|
|
6
|
+
import { contextPinnedNodeIds } from './canvas-store';
|
|
7
|
+
import { requestJson } from './intent-bridge';
|
|
8
|
+
|
|
9
|
+
import { lifecycleTarget } from './boards-store';
|
|
10
|
+
export { lifecycleFilter, lifecycleTarget } from './boards-store';
|
|
11
|
+
|
|
12
|
+
export const knowledge = signal<KnowledgeResults['list'] | null>(null);
|
|
13
|
+
export const knowledgeUnavailable = signal(false);
|
|
14
|
+
export const effectiveLifecycleFilter = computed(() =>
|
|
15
|
+
knowledgeLifecycleEnabled.value ? lifecycleFilter.value : 'All',
|
|
16
|
+
);
|
|
17
|
+
export const retirementUndo = signal<{ boardId: string; nodeId: string; incarnation: number } | null>(null);
|
|
18
|
+
let sequence = 0;
|
|
19
|
+
|
|
20
|
+
export async function refreshKnowledge(): Promise<void> {
|
|
21
|
+
const request = ++sequence;
|
|
22
|
+
const incarnation = boardIncarnation;
|
|
23
|
+
const result = await requestJson<KnowledgeResults['list'] | null>('knowledge', '/api/canvas/knowledge', null);
|
|
24
|
+
if (request !== sequence || incarnation !== boardIncarnation) return;
|
|
25
|
+
knowledgeUnavailable.value = result?.ok !== true;
|
|
26
|
+
if (result?.ok === true) knowledge.value = result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function cardQualification(nodeId: string) {
|
|
30
|
+
return activeBoardId.value
|
|
31
|
+
? knowledge.value?.cards[memoryKey({ boardId: activeBoardId.value, cardId: nodeId })]
|
|
32
|
+
: undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Raw pin membership survives retirement, but retired cards are not delivered. */
|
|
36
|
+
export const activeContextPinnedNodeIds = computed(
|
|
37
|
+
() => new Set([...contextPinnedNodeIds.value].filter((id) => !cardQualification(id)?.retired)),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export function openLifecycle(nodeId: string): void {
|
|
41
|
+
if (knowledgeLifecycleEnabled.value && activeBoardId.value)
|
|
42
|
+
lifecycleTarget.value = { boardId: activeBoardId.value, nodeId, incarnation: boardIncarnation };
|
|
43
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { computed } from '@preact/signals';
|
|
2
|
-
import { findNeighborhoods } from '../../server/spatial-analysis.js';
|
|
3
|
-
import { contextPinnedNodeIds, nodes } from './canvas-store';
|
|
1
|
+
import { computed, signal } from '@preact/signals';
|
|
2
|
+
import { findNeighborhoods, gapDistance } from '../../server/spatial-analysis.js';
|
|
3
|
+
import { contextPinnedNodeIds, nodes, selectedNodeIds } from './canvas-store';
|
|
4
|
+
import { grabbingNodeId } from './human-store';
|
|
5
|
+
import { activeContextPinnedNodeIds, cardQualification } from './knowledge-store';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Near a pin (docs/design/NearPin.dc.html): each pinned node pulls up to five
|
|
7
|
-
* unpinned nodes within 600 px into the agent's brief as title + short
|
|
9
|
+
* unpinned nodes within a 600 px edge-to-edge gap into the agent's brief as title + short
|
|
8
10
|
* summary. Same rule as the server's brief (`findNeighborhoods`), so the mark
|
|
9
11
|
* claims exactly what the agent receives.
|
|
10
12
|
*/
|
|
@@ -22,11 +24,12 @@ let lastNear = new Map<string, NearPin[]>();
|
|
|
22
24
|
* changes: the chips subscribed to it re-render only when a neighbourhood does.
|
|
23
25
|
*/
|
|
24
26
|
export const nearPins = computed<Map<string, NearPin[]>>(() => {
|
|
25
|
-
const pins =
|
|
27
|
+
const pins = activeContextPinnedNodeIds.value;
|
|
26
28
|
const near = new Map<string, NearPin[]>();
|
|
27
29
|
if (pins.size > 0) {
|
|
28
30
|
const nearest = new Map<string, Array<NearPin & { distance: number }>>();
|
|
29
|
-
|
|
31
|
+
const activeNodes = [...nodes.value.values()].filter((node) => !cardQualification(node.id)?.retired);
|
|
32
|
+
for (const neighborhood of findNeighborhoods(activeNodes, pins)) {
|
|
30
33
|
const pinTitle = neighborhood.pinnedNodeTitle ?? neighborhood.pinnedNodeId;
|
|
31
34
|
for (const neighbor of neighborhood.neighbors) {
|
|
32
35
|
const list = nearest.get(neighbor.id) ?? [];
|
|
@@ -51,3 +54,109 @@ export const nearPins = computed<Map<string, NearPin[]>>(() => {
|
|
|
51
54
|
lastNear = near;
|
|
52
55
|
return near;
|
|
53
56
|
});
|
|
57
|
+
|
|
58
|
+
/** Pin id → its neighbours' ids: the tether layer looks a pin up, never scans the near map. */
|
|
59
|
+
export const neighboursByPin = computed<Map<string, string[]>>(() => {
|
|
60
|
+
const byPin = new Map<string, string[]>();
|
|
61
|
+
for (const [nodeId, pins] of nearPins.value) {
|
|
62
|
+
for (const pin of pins) {
|
|
63
|
+
const list = byPin.get(pin.pinNodeId) ?? [];
|
|
64
|
+
list.push(nodeId);
|
|
65
|
+
byPin.set(pin.pinNodeId, list);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return byPin;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** The node under the pointer: a hovered pin draws its neighbours (NearPin.dc.html §2). */
|
|
72
|
+
export const hoveredNodeId = signal<string | null>(null);
|
|
73
|
+
/** A near node whose pins the human asked to see from its chip ("Show pins"). */
|
|
74
|
+
export const shownPinsFor = signal<string | null>(null);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Pins drawn with tethers and the 600 px boundary around their edges: the pin being dragged,
|
|
78
|
+
* hovered or selected, or the pins of the near node whose chip said "Show
|
|
79
|
+
* pins". Dragging an unpinned node rings only its nearest pin (§3).
|
|
80
|
+
*/
|
|
81
|
+
export const tetherFocus = computed<{ pinIds: string[]; ringOnly: boolean }>(() => {
|
|
82
|
+
const pins = contextPinnedNodeIds.value;
|
|
83
|
+
if (pins.size === 0) return { pinIds: [], ringOnly: false };
|
|
84
|
+
const grabbed = grabbingNodeId.value;
|
|
85
|
+
if (grabbed && !pins.has(grabbed)) {
|
|
86
|
+
const dragged = nodes.value.get(grabbed);
|
|
87
|
+
let nearest: string | null = null;
|
|
88
|
+
let best = Number.POSITIVE_INFINITY;
|
|
89
|
+
for (const id of pins) {
|
|
90
|
+
const pin = nodes.value.get(id);
|
|
91
|
+
if (!dragged || !pin) continue;
|
|
92
|
+
const distance = gapDistance(dragged, pin);
|
|
93
|
+
if (distance < best) {
|
|
94
|
+
best = distance;
|
|
95
|
+
nearest = id;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { pinIds: nearest ? [nearest] : [], ringOnly: true };
|
|
99
|
+
}
|
|
100
|
+
const ids = new Set<string>();
|
|
101
|
+
if (grabbed) ids.add(grabbed);
|
|
102
|
+
const hovered = hoveredNodeId.value;
|
|
103
|
+
if (hovered && pins.has(hovered)) ids.add(hovered);
|
|
104
|
+
for (const id of selectedNodeIds.value) if (pins.has(id)) ids.add(id);
|
|
105
|
+
const shown = shownPinsFor.value;
|
|
106
|
+
for (const pin of (shown && nearPins.value.get(shown)) || []) ids.add(pin.pinNodeId);
|
|
107
|
+
return { pinIds: [...ids], ringOnly: false };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/** The moment it changes (§3): the chip pulses once on a drop inside, fades on a drop outside. */
|
|
111
|
+
export const nearChange = signal<{ nodeId: string; kind: 'in' | 'out'; pins: NearPin[] } | null>(null);
|
|
112
|
+
let nearAtGrab: NearPin[] | undefined;
|
|
113
|
+
|
|
114
|
+
export function noteNearAtGrab(nodeId: string): void {
|
|
115
|
+
clearNearChange(nodeId);
|
|
116
|
+
nearAtGrab = nearPins.value.get(nodeId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function settleNearAfterDrop(nodeId: string): void {
|
|
120
|
+
const before = nearAtGrab;
|
|
121
|
+
nearAtGrab = undefined;
|
|
122
|
+
const after = nearPins.value.get(nodeId);
|
|
123
|
+
if (after && !before) nearChange.value = { nodeId, kind: 'in', pins: after };
|
|
124
|
+
else if (before && !after) nearChange.value = { nodeId, kind: 'out', pins: before };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function clearNearChange(nodeId: string): void {
|
|
128
|
+
if (nearChange.value?.nodeId === nodeId) nearChange.value = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The open near chip's card (§2), drawn over the canvas because a node paints
|
|
133
|
+
* contained. `at` is the chip's screen rect when it opened; the card closes
|
|
134
|
+
* when the view moves rather than chase a stale anchor.
|
|
135
|
+
*/
|
|
136
|
+
export const nearCard = signal<{
|
|
137
|
+
nodeId: string;
|
|
138
|
+
at: { left: number; right: number; top: number; bottom: number };
|
|
139
|
+
view: string;
|
|
140
|
+
} | null>(null);
|
|
141
|
+
let closeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
142
|
+
|
|
143
|
+
export function openNearCard(nodeId: string, chip: Element, view: string): void {
|
|
144
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
145
|
+
closeTimer = null;
|
|
146
|
+
const rect = chip.getBoundingClientRect();
|
|
147
|
+
nearCard.value = { nodeId, at: { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom }, view };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Leaving the chip for the card must not close it: close after a short grace. */
|
|
151
|
+
export function closeNearCardSoon(): void {
|
|
152
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
153
|
+
closeTimer = setTimeout(() => {
|
|
154
|
+
closeTimer = null;
|
|
155
|
+
nearCard.value = null;
|
|
156
|
+
}, 160);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function keepNearCard(): void {
|
|
160
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
161
|
+
closeTimer = null;
|
|
162
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { computed, signal } from '@preact/signals';
|
|
2
2
|
import type { PmxAxIntent } from '../../shared/ax-intent.js';
|
|
3
3
|
import { intents } from './intent-store';
|
|
4
|
+
import { agentSessionsEnabled } from './workspace-settings';
|
|
4
5
|
import {
|
|
5
6
|
HUMAN_STARTED_SESSION_LABEL,
|
|
6
7
|
type AgentActivityEntry,
|
|
7
8
|
type AgentPresence,
|
|
8
9
|
type AgentPresenceSnapshot,
|
|
9
|
-
CONTEXT_BUDGET_DEFAULT_TOKENS,
|
|
10
|
-
type ContextBudget,
|
|
11
10
|
externalWriters,
|
|
12
11
|
isSessionActive,
|
|
13
12
|
} from '../../shared/agent-presence.js';
|
|
@@ -21,7 +20,6 @@ import {
|
|
|
21
20
|
* panel, command bar, presence layer, and top-bar chip all mount on it.
|
|
22
21
|
*/
|
|
23
22
|
export const agentPresences = signal<AgentPresence[]>([]);
|
|
24
|
-
export const contextBudget = signal<ContextBudget>({ used: 0, total: CONTEXT_BUDGET_DEFAULT_TOKENS });
|
|
25
23
|
/** Recent agent writes, newest first — the External Steering activity feed. */
|
|
26
24
|
export const agentActivity = signal<AgentActivityEntry[]>([]);
|
|
27
25
|
|
|
@@ -59,11 +57,14 @@ export function relativeAge(iso: string, now = Date.now()): string {
|
|
|
59
57
|
return `${Math.floor(ms / 3_600_000)}h`;
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
/**
|
|
60
|
+
/** Attachment state is independent of whether the human opted into its UI. */
|
|
63
61
|
export const sessionActive = computed(() => isSessionActive(agentPresences.value));
|
|
62
|
+
export const sessionChromeActive = computed(() => agentSessionsEnabled.value && sessionActive.value);
|
|
64
63
|
|
|
65
|
-
/**
|
|
66
|
-
export const externalWriterPresences = computed(() =>
|
|
64
|
+
/** With session UI off, attached agents still appear as ordinary live writers. */
|
|
65
|
+
export const externalWriterPresences = computed(() =>
|
|
66
|
+
agentSessionsEnabled.value ? externalWriters(agentPresences.value) : agentPresences.value,
|
|
67
|
+
);
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
70
|
* Connected agents the human can ADDRESS from the composer: every live
|
|
@@ -147,15 +148,11 @@ export function presenceWorldPosition(
|
|
|
147
148
|
export function applyPresenceSnapshot(snapshot: Partial<AgentPresenceSnapshot> | null | undefined): void {
|
|
148
149
|
if (!snapshot) return;
|
|
149
150
|
if (Array.isArray(snapshot.presences)) agentPresences.value = snapshot.presences;
|
|
150
|
-
if (snapshot.budget && Number.isFinite(snapshot.budget.used) && Number.isFinite(snapshot.budget.total)) {
|
|
151
|
-
contextBudget.value = { used: snapshot.budget.used, total: snapshot.budget.total };
|
|
152
|
-
}
|
|
153
151
|
if (Array.isArray(snapshot.activity)) agentActivity.value = snapshot.activity;
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
export function resetPresence(): void {
|
|
157
155
|
agentPresences.value = [];
|
|
158
|
-
contextBudget.value = { used: 0, total: CONTEXT_BUDGET_DEFAULT_TOKENS };
|
|
159
156
|
agentActivity.value = [];
|
|
160
157
|
activityFeedOpen.value = false;
|
|
161
158
|
writersSheetOpen.value = false;
|
|
@@ -1,7 +1,98 @@
|
|
|
1
1
|
import { signal } from '@preact/signals';
|
|
2
|
+
import { interpolateCamera, resolveStop, type TourStop } from '../../shared/tour.js';
|
|
3
|
+
import { canvasArea } from '../canvas/canvas-area';
|
|
4
|
+
import type { ViewportState } from '../types';
|
|
5
|
+
import { animateCameraPath, nodes, viewport } from './canvas-store';
|
|
6
|
+
import { requestBestEffort, requestJson } from './intent-bridge';
|
|
2
7
|
|
|
3
8
|
export const presenting = signal(false);
|
|
4
9
|
export const recordingCamera = signal(false);
|
|
10
|
+
/** Why the current stop cannot be shown (missing target, no stops, server unreachable). */
|
|
11
|
+
export const presentationError = signal('');
|
|
12
|
+
|
|
5
13
|
export function ownsCamera(): boolean {
|
|
6
14
|
return presenting.value || recordingCamera.value;
|
|
7
15
|
}
|
|
16
|
+
|
|
17
|
+
function prefersReducedMotion(): boolean {
|
|
18
|
+
return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Ease this viewer's camera to a stop: log-space zoom, optional pull-back.
|
|
23
|
+
* Reduced motion jumps straight to the end. Throws when the target is missing.
|
|
24
|
+
*/
|
|
25
|
+
export function playStop(stop: TourStop, onDone?: (end: ViewportState) => void): void {
|
|
26
|
+
const area = canvasArea();
|
|
27
|
+
const target = resolveStop(stop, [...nodes.value.values()], area.width, area.height);
|
|
28
|
+
const from = { ...viewport.value };
|
|
29
|
+
const duration = prefersReducedMotion() ? 0 : (stop.duration ?? 1) * 1000;
|
|
30
|
+
animateCameraPath(
|
|
31
|
+
(t) => interpolateCamera(from, target, t, area.width, area.height, stop.easing, stop.pullback),
|
|
32
|
+
duration,
|
|
33
|
+
onDone,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let stepSeq = 0;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Show a tour stop, entering presentation if needed. Entering hides the
|
|
41
|
+
* chrome, so the canvas area is measured only after that layout has
|
|
42
|
+
* committed (two frames); the latest step always wins.
|
|
43
|
+
*/
|
|
44
|
+
export function presentStop(stop: TourStop): void {
|
|
45
|
+
const seq = ++stepSeq;
|
|
46
|
+
const entering = !presenting.value;
|
|
47
|
+
presenting.value = true;
|
|
48
|
+
presentationError.value = '';
|
|
49
|
+
const play = () => {
|
|
50
|
+
if (seq !== stepSeq) return;
|
|
51
|
+
try {
|
|
52
|
+
playStop(stop);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
presentationError.value = error instanceof Error ? error.message : String(error);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
if (entering) requestAnimationFrame(() => requestAnimationFrame(play));
|
|
58
|
+
else play();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Move the server's shared tour cursor. The `canvas-tour-step` echo does the
|
|
63
|
+
* animating, here and in every other presenting viewer, so a human's keys and
|
|
64
|
+
* an agent's `tour.go` always agree on where the tour is.
|
|
65
|
+
*/
|
|
66
|
+
export async function stepTour(to: { stop: number } | { step: 'next' | 'previous' }): Promise<void> {
|
|
67
|
+
const result = await requestJson<{ ok: boolean; error?: string }>(
|
|
68
|
+
'step tour',
|
|
69
|
+
'/api/canvas/tour/go',
|
|
70
|
+
{ ok: false, error: 'Could not reach the canvas server.' },
|
|
71
|
+
{
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({ ...to, present: false }),
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
if (!result.ok) presentationError.value = result.error ?? 'Could not change the tour stop.';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Present from the first stop, or resume where the tour already is (`?present=1`). */
|
|
81
|
+
export async function startPresentation(resume = false): Promise<void> {
|
|
82
|
+
presenting.value = true;
|
|
83
|
+
presentationError.value = '';
|
|
84
|
+
const at = resume
|
|
85
|
+
? (await requestJson<{ position?: number | null }>('read tour', '/api/canvas/tour', {})).position
|
|
86
|
+
: null;
|
|
87
|
+
await stepTour({ stop: typeof at === 'number' ? at : 0 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function endPresentation(): void {
|
|
91
|
+
presenting.value = false;
|
|
92
|
+
presentationError.value = '';
|
|
93
|
+
void requestBestEffort('exit tour', '/api/canvas/tour/exit', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: '{}',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { computed, effect, signal } from '@preact/signals';
|
|
2
|
+
import { relationKind, relationPreset, relationSourceEnd } from '../../shared/relations.js';
|
|
3
|
+
import { activeBoard, activeBoardId } from './boards-store';
|
|
4
|
+
import { activeNodeId, edges, nodes, selectedNodeIds } from './canvas-store';
|
|
5
|
+
|
|
6
|
+
export const relationFilter = signal<{ boardId: string; kind: string } | null>(null);
|
|
7
|
+
|
|
8
|
+
effect(() => {
|
|
9
|
+
const filter = relationFilter.value;
|
|
10
|
+
if (
|
|
11
|
+
filter?.boardId === activeBoardId.value &&
|
|
12
|
+
![...edges.value.values()].some((edge) => relationKind(edge) === filter.kind)
|
|
13
|
+
) {
|
|
14
|
+
relationFilter.value = null;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Hover is for hints, not for changing which cards remain readable.
|
|
19
|
+
export const relationFocus = computed(() => activeNodeId.value);
|
|
20
|
+
|
|
21
|
+
/** View-only highlights. They never infer evidence, change a decision or execute a flow. */
|
|
22
|
+
export const relationHighlight = computed(() => {
|
|
23
|
+
const preset = relationPreset(activeBoard()?.relationVocabulary);
|
|
24
|
+
const focus = relationFocus.value;
|
|
25
|
+
const kind = relationFilter.value?.boardId === activeBoardId.value ? relationFilter.value.kind : null;
|
|
26
|
+
const list = [...edges.value.values()].filter((edge) => !edge.toBoardId || edge.toBoardId === activeBoardId.value);
|
|
27
|
+
const focusedCards = new Set(selectedNodeIds.value);
|
|
28
|
+
if (focus) focusedCards.add(focus);
|
|
29
|
+
if (focusedCards.size === 0 && !kind) return null;
|
|
30
|
+
// Selecting a frame focuses its contents too, including nested groups.
|
|
31
|
+
for (const id of focusedCards) {
|
|
32
|
+
const node = nodes.value.get(id);
|
|
33
|
+
if (node?.type === 'group' && Array.isArray(node.data.children))
|
|
34
|
+
for (const child of node.data.children) if (typeof child === 'string') focusedCards.add(child);
|
|
35
|
+
}
|
|
36
|
+
const cards = new Set(focusedCards);
|
|
37
|
+
const links = new Set<string>();
|
|
38
|
+
const add = (edge: (typeof list)[number]) => {
|
|
39
|
+
links.add(edge.id);
|
|
40
|
+
cards.add(edge.from);
|
|
41
|
+
cards.add(edge.to);
|
|
42
|
+
};
|
|
43
|
+
if (!kind) for (const edge of list) if (focusedCards.has(edge.from) && focusedCards.has(edge.to)) add(edge);
|
|
44
|
+
if (kind) {
|
|
45
|
+
for (const edge of list) if (relationKind(edge) === kind) add(edge);
|
|
46
|
+
} else if (preset === 'Dependencies' || preset === 'Decision') {
|
|
47
|
+
let changed = true;
|
|
48
|
+
while (changed) {
|
|
49
|
+
changed = false;
|
|
50
|
+
for (const edge of list) {
|
|
51
|
+
if (links.has(edge.id)) continue;
|
|
52
|
+
const source = relationSourceEnd(edge);
|
|
53
|
+
const follows =
|
|
54
|
+
preset === 'Dependencies'
|
|
55
|
+
? source && cards.has(source === 'from' ? edge.from : edge.to)
|
|
56
|
+
: ['option', 'if', 'chosen'].includes(relationKind(edge)) && cards.has(edge.to);
|
|
57
|
+
if (follows) {
|
|
58
|
+
add(edge);
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} else if (preset === 'Flow') {
|
|
64
|
+
const progressed = new Set<string>();
|
|
65
|
+
for (const node of nodes.value.values())
|
|
66
|
+
if (['done', 'in-progress', 'blocked'].includes(String(node.data.axWorkStatus))) {
|
|
67
|
+
progressed.add(node.id);
|
|
68
|
+
cards.add(node.id);
|
|
69
|
+
}
|
|
70
|
+
for (const edge of list)
|
|
71
|
+
if (['flow', 'next', 'if-fails', 'loop'].includes(relationKind(edge)) && progressed.has(edge.from)) add(edge);
|
|
72
|
+
} else {
|
|
73
|
+
for (const edge of list)
|
|
74
|
+
if (
|
|
75
|
+
(focusedCards.has(edge.from) || focusedCards.has(edge.to)) &&
|
|
76
|
+
(preset !== 'Research' || ['supports', 'contradicts', 'cites', 'derived-from'].includes(relationKind(edge)))
|
|
77
|
+
)
|
|
78
|
+
add(edge);
|
|
79
|
+
}
|
|
80
|
+
return { cards, links };
|
|
81
|
+
});
|