pmx-canvas 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/pmx-canvas/extension.mjs +9 -3
- package/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/start-mcp.mjs +29 -29
- package/CHANGELOG.md +19 -0
- package/Readme.md +1 -1
- package/dist/canvas/global.css +12 -17
- package/dist/canvas/index.js +58 -58
- package/dist/canvas/mcp-app.html +75 -80
- package/dist/json-render/index.js +50 -50
- package/dist/types/client/canvas/submit-without-navigation.d.ts +3 -0
- package/dist/types/client/state/intent-bridge.d.ts +0 -3
- package/dist/types/client/state/presence-store.d.ts +3 -3
- package/dist/types/client/types.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/index.d.ts +2 -2
- package/dist/types/server/knowledge-lifecycle.d.ts +11 -2
- package/dist/types/server/lifecycle-status.d.ts +3 -1
- package/dist/types/server/tour-control.d.ts +6 -3
- package/docs/bun-webview-integration.md +2 -2
- package/docs/cli.md +10 -1
- package/docs/design/Chrome.dc.html +1 -0
- package/docs/design.md +3 -1
- package/docs/mcp.md +5 -1
- package/docs/node-types.md +3 -0
- package/docs/plans/plan-019-post-0.9-roadmap.md +68 -9
- package/docs/sdk.md +7 -0
- package/package.json +2 -2
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +9 -3
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +1 -1
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/start-mcp.mjs +29 -29
- package/skills/pmx-canvas/SKILL.md +4 -4
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +11 -6
- package/src/client/canvas/BoardCopyDialog.tsx +2 -1
- package/src/client/canvas/KnowledgeDialog.tsx +3 -0
- package/src/client/canvas/LifecyclePanel.tsx +4 -0
- package/src/client/canvas/RelationPresetsDialog.tsx +2 -0
- package/src/client/canvas/submit-without-navigation.ts +14 -0
- package/src/client/state/canvas-store.ts +12 -24
- package/src/client/state/intent-bridge.ts +0 -6
- package/src/client/state/presence-store.ts +3 -3
- package/src/client/state/sse-bridge.ts +11 -6
- package/src/client/theme/global.css +12 -17
- package/src/server/canvas-state.ts +10 -0
- package/src/server/index.ts +10 -30
- package/src/server/knowledge-lifecycle.ts +66 -0
- package/src/server/lifecycle-status.ts +79 -7
- package/src/server/operations/ops/lifecycle.ts +3 -7
- package/src/server/operations/ops/nodes.ts +2 -5
- package/src/server/operations/ops/viewport.ts +3 -7
- package/src/server/server.ts +13 -10
- package/src/server/tour-control.ts +17 -8
|
@@ -106,7 +106,7 @@ for the authoritative stop, viewport, persistence, reset, group-order, and captu
|
|
|
106
106
|
|
|
107
107
|
## Runtime prerequisites
|
|
108
108
|
|
|
109
|
-
PMX Canvas 0.9.
|
|
109
|
+
PMX Canvas 0.9.3 requires **Bun >=1.4.4**, including when installed through npm.
|
|
110
110
|
Check `bun --version` and ensure Bun is on the MCP host's PATH, not just your shell's;
|
|
111
111
|
use an absolute executable path if needed. Install/configure only when requested.
|
|
112
112
|
For pinned installation, MCP configuration, managed services, and disposable verification,
|
|
@@ -389,9 +389,9 @@ If the documented receipts or warnings are unexpectedly absent, do not guess aro
|
|
|
389
389
|
payload reads: verify that the CLI/MCP host and daemon use the same 0.7 installation and workspace,
|
|
390
390
|
restart the matching daemon after upgrade, and re-check `/health`.
|
|
391
391
|
|
|
392
|
-
**Token hygiene.** For routine state checks use
|
|
393
|
-
`
|
|
394
|
-
actually consuming
|
|
392
|
+
**Token hygiene.** For routine state checks use
|
|
393
|
+
`canvas_ax_state { action: "get", includeContext: false }` to omit the serialized AX context.
|
|
394
|
+
Omitting `includeContext` defaults to `true`; request full context only when actually consuming it.
|
|
395
395
|
|
|
396
396
|
## Context Pins
|
|
397
397
|
|
|
@@ -4,7 +4,7 @@ Use this reference when PMX Canvas is running inside the GitHub Copilot app as a
|
|
|
4
4
|
extension. The adapter is intentionally thin: PMX Canvas remains the state owner, and the extension
|
|
5
5
|
maps Copilot SDK features onto PMX AX primitives.
|
|
6
6
|
|
|
7
|
-
The [standalone agent plugin](
|
|
7
|
+
The [standalone agent plugin](https://github.com/pskoett/pmx-canvas/blob/main/docs/agent-plugin.md) bundles this adapter and PMX's
|
|
8
8
|
MCP launcher. Its native canvas is Copilot-only; other Agent Plugins-compatible clients use
|
|
9
9
|
the portable MCP tools and browser workbench.
|
|
10
10
|
|
|
@@ -125,7 +125,7 @@ imports the Copilot SDK.
|
|
|
125
125
|
- **Await gates** with `canvas_ax_gate { kind, action: "await", id }` (or surface a native modal
|
|
126
126
|
and await the PMX result) so an approval gate actually blocks the agent until the human resolves it.
|
|
127
127
|
|
|
128
|
-
See [`docs/ax-host-adapter-contract.md`](
|
|
128
|
+
See [`docs/ax-host-adapter-contract.md`](https://github.com/pskoett/pmx-canvas/blob/main/docs/ax-host-adapter-contract.md).
|
|
129
129
|
- Keeps all persistent PMX state in `.pmx-canvas/canvas.db`; the extension does not own canvas
|
|
130
130
|
state.
|
|
131
131
|
|
|
@@ -4,12 +4,17 @@ Use this reference when the `pmx-canvas` skill is installed but the `pmx-canvas`
|
|
|
4
4
|
|
|
5
5
|
## Prerequisites and version
|
|
6
6
|
|
|
7
|
-
The examples pin **PMX Canvas 0.9.
|
|
7
|
+
The examples pin **PMX Canvas 0.9.3**, which requires **Bun >=1.4.4** even when
|
|
8
8
|
installed through npm: the CLI runs through Bun, not Node. Check `bun --version`
|
|
9
9
|
first. If missing or older, follow the [Bun installation instructions](https://bun.sh/docs/installation)
|
|
10
10
|
with the user's authorization. Ensure Bun is on the MCP host's PATH as well as the
|
|
11
11
|
interactive shell's; use an absolute `bun`/`bunx` executable path if needed.
|
|
12
12
|
|
|
13
|
+
Bun 1.4.4 fixes the Brotli decoder vulnerability CVE-2026-111497. Updating the
|
|
14
|
+
PMX package does not update Bun; upgrade the runtime and restart existing PMX/MCP
|
|
15
|
+
processes. Development and build tooling should use Node 24.22.1 or a newer patched
|
|
16
|
+
release on a supported Node line.
|
|
17
|
+
|
|
13
18
|
Install/configure only when requested. After an intentional runtime upgrade, sync the
|
|
14
19
|
skill from that installed package and restart existing server/MCP processes as described
|
|
15
20
|
in `SKILL.md`. Do not pair newer checkout guidance with an older runtime unknowingly.
|
|
@@ -17,11 +22,11 @@ in `SKILL.md`. Do not pair newer checkout guidance with an older runtime unknowi
|
|
|
17
22
|
## Install from npm
|
|
18
23
|
|
|
19
24
|
```bash
|
|
20
|
-
npm install -g pmx-canvas@0.9.
|
|
25
|
+
npm install -g pmx-canvas@0.9.3
|
|
21
26
|
pmx-canvas --version
|
|
22
27
|
```
|
|
23
28
|
|
|
24
|
-
Without a global install, use `bunx pmx-canvas@0.9.
|
|
29
|
+
Without a global install, use `bunx pmx-canvas@0.9.3` in place of `pmx-canvas` in
|
|
25
30
|
the commands below. Do not use unpinned `bunx pmx-canvas` for a version-specific check.
|
|
26
31
|
|
|
27
32
|
## Choose the workspace and target
|
|
@@ -91,7 +96,7 @@ discards empty env values, remove `PMX_CANVAS_URL` from its launch environment i
|
|
|
91
96
|
"mcpServers": {
|
|
92
97
|
"canvas": {
|
|
93
98
|
"command": "bunx",
|
|
94
|
-
"args": ["pmx-canvas@0.9.
|
|
99
|
+
"args": ["pmx-canvas@0.9.3", "--mcp"],
|
|
95
100
|
"env": {
|
|
96
101
|
"PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
|
|
97
102
|
"PMX_CANVAS_PORT": "14313",
|
|
@@ -135,7 +140,7 @@ Amp executes, not only on the user's laptop. Merge this into that workspace's
|
|
|
135
140
|
"amp.mcpServers": {
|
|
136
141
|
"canvas": {
|
|
137
142
|
"command": "bunx",
|
|
138
|
-
"args": ["pmx-canvas@0.9.
|
|
143
|
+
"args": ["pmx-canvas@0.9.3", "--mcp"],
|
|
139
144
|
"includeTools": ["canvas_*"],
|
|
140
145
|
"env": {
|
|
141
146
|
"PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
|
|
@@ -169,7 +174,7 @@ The agent must not run this approval itself or move the command to global settin
|
|
|
169
174
|
`--mcp-config` to bypass workspace trust. Approval authorizes execution of this MCP
|
|
170
175
|
command; it is not a new Canvas import consent or permission to mutate arbitrary boards.
|
|
171
176
|
After approval, reload MCP connections (`reload_mcp` in Amp) and require `connected`.
|
|
172
|
-
Discover `canvas` tools with `tool_search`; for the full unfiltered 0.9.
|
|
177
|
+
Discover `canvas` tools with `tool_search`; for the full unfiltered 0.9.3 server there
|
|
173
178
|
are 24 tools. If still absent, inspect the server status, command/PATH and target instead
|
|
174
179
|
of reinstalling blindly. See [Amp MCP documentation](https://ampcode.com/docs/customize/mcp).
|
|
175
180
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type BoardSummary,
|
|
8
8
|
} from '../state/boards-store';
|
|
9
9
|
import { useFocusTrap } from './use-focus-trap';
|
|
10
|
+
import { submitWithoutNavigation } from './submit-without-navigation';
|
|
10
11
|
|
|
11
12
|
export function BoardCopyDialog({ board, onClose }: { board: BoardSummary; onClose: () => void }) {
|
|
12
13
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
@@ -63,7 +64,7 @@ export function BoardCopyDialog({ board, onClose }: { board: BoardSummary; onClo
|
|
|
63
64
|
aria-labelledby="board-copy-title"
|
|
64
65
|
onMouseDown={(e) => e.stopPropagation()}
|
|
65
66
|
>
|
|
66
|
-
<form onSubmit={submit}>
|
|
67
|
+
<form onClick={submitWithoutNavigation} onSubmit={submit}>
|
|
67
68
|
<h2 id="board-copy-title">Create from {board.name}</h2>
|
|
68
69
|
<label class="board-copy-field">
|
|
69
70
|
Board name
|
|
@@ -8,6 +8,7 @@ import { requestJson } from '../state/intent-bridge';
|
|
|
8
8
|
import { cleanLifecycle, DEFAULT_LIFECYCLE, LifecycleFields } from './LifecycleFields';
|
|
9
9
|
import { useFocusTrap } from './use-focus-trap';
|
|
10
10
|
import { ChoiceMenu } from './ChoiceMenu';
|
|
11
|
+
import { submitWithoutNavigation } from './submit-without-navigation';
|
|
11
12
|
|
|
12
13
|
export function KnowledgeDialog({ folder, onClose }: { folder?: string; onClose: () => void }) {
|
|
13
14
|
const enabled = knowledgeLifecycleEnabled.value;
|
|
@@ -158,6 +159,7 @@ export function KnowledgeDialog({ folder, onClose }: { folder?: string; onClose:
|
|
|
158
159
|
<p>Knowledge settings unavailable. Close and retry.</p>
|
|
159
160
|
) : folder ? (
|
|
160
161
|
<form
|
|
162
|
+
onClick={submitWithoutNavigation}
|
|
161
163
|
onSubmit={async (event) => {
|
|
162
164
|
event.preventDefault();
|
|
163
165
|
if (!knowledgeLifecycleEnabled.value || busy) return;
|
|
@@ -286,6 +288,7 @@ export function KnowledgeDialog({ folder, onClose }: { folder?: string; onClose:
|
|
|
286
288
|
<form
|
|
287
289
|
class="category-inspector"
|
|
288
290
|
noValidate
|
|
291
|
+
onClick={submitWithoutNavigation}
|
|
289
292
|
onSubmit={async (event) => {
|
|
290
293
|
event.preventDefault();
|
|
291
294
|
if (!knowledgeLifecycleEnabled.value || busy) return;
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { sendSteering } from '../state/session-store';
|
|
21
21
|
import { hasUnsavedMarkdown } from '../nodes/MarkdownNode';
|
|
22
22
|
import { cleanLifecycle, DEFAULT_LIFECYCLE, LifecycleFields } from './LifecycleFields';
|
|
23
|
+
import { submitWithoutNavigation } from './submit-without-navigation';
|
|
23
24
|
|
|
24
25
|
export function LifecyclePanel() {
|
|
25
26
|
const target = lifecycleTarget.value;
|
|
@@ -264,6 +265,7 @@ function LifecycleDialog({ target }: { target: NonNullable<typeof lifecycleTarge
|
|
|
264
265
|
<form
|
|
265
266
|
class="lifecycle-quick-review"
|
|
266
267
|
noValidate
|
|
268
|
+
onClick={submitWithoutNavigation}
|
|
267
269
|
onSubmit={(event) => {
|
|
268
270
|
event.preventDefault();
|
|
269
271
|
void recordReview('retain', true).then((ok) => {
|
|
@@ -345,6 +347,7 @@ function LifecycleDialog({ target }: { target: NonNullable<typeof lifecycleTarge
|
|
|
345
347
|
· category belongs to the folder
|
|
346
348
|
</p>
|
|
347
349
|
<form
|
|
350
|
+
onClick={submitWithoutNavigation}
|
|
348
351
|
onSubmit={(event) => {
|
|
349
352
|
event.preventDefault();
|
|
350
353
|
void mutate('/lifecycle', { lifecycleOverride: override ? cleanLifecycle(policy) : null });
|
|
@@ -446,6 +449,7 @@ function LifecycleDialog({ target }: { target: NonNullable<typeof lifecycleTarge
|
|
|
446
449
|
) : (
|
|
447
450
|
<form
|
|
448
451
|
class="lifecycle-review"
|
|
452
|
+
onClick={submitWithoutNavigation}
|
|
449
453
|
onSubmit={(event) => {
|
|
450
454
|
event.preventDefault();
|
|
451
455
|
void recordReview(outcome, applied);
|
|
@@ -4,6 +4,7 @@ import { boardList, updateBoard } from '../state/boards-store';
|
|
|
4
4
|
import { requestJson } from '../state/intent-bridge';
|
|
5
5
|
import { knowledge, refreshKnowledge } from '../state/knowledge-store';
|
|
6
6
|
import { useFocusTrap } from './use-focus-trap';
|
|
7
|
+
import { submitWithoutNavigation } from './submit-without-navigation';
|
|
7
8
|
|
|
8
9
|
/** Shared templates live in Home; applying one never rewrites connection kinds. */
|
|
9
10
|
export function RelationPresetsDialog({ onClose }: { onClose: () => void }) {
|
|
@@ -152,6 +153,7 @@ export function RelationPresetsDialog({ onClose }: { onClose: () => void }) {
|
|
|
152
153
|
{editing ? (
|
|
153
154
|
<form
|
|
154
155
|
class="relation-vocabulary"
|
|
156
|
+
onClick={submitWithoutNavigation}
|
|
155
157
|
onSubmit={async (event) => {
|
|
156
158
|
event.preventDefault();
|
|
157
159
|
if (busy) return;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JSX } from 'preact';
|
|
2
|
+
|
|
3
|
+
/** A scripts-only host blocks native submission before onSubmit can run. */
|
|
4
|
+
export function submitWithoutNavigation(event: JSX.TargetedMouseEvent<HTMLFormElement>): void {
|
|
5
|
+
if (event.defaultPrevented || !(event.target instanceof Element)) return;
|
|
6
|
+
const button = event.target.closest('button');
|
|
7
|
+
const form = event.currentTarget;
|
|
8
|
+
if (button?.type !== 'submit' || button.form !== form || button.matches(':disabled')) return;
|
|
9
|
+
// Enter in a form input also activates its submit button. Keep native
|
|
10
|
+
// validation, but dispatch only the JS event, never a navigation/submission.
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
if (!form.noValidate && !button.formNoValidate && !form.reportValidity()) return;
|
|
13
|
+
form.dispatchEvent(new SubmitEvent('submit', { bubbles: true, cancelable: true, submitter: button }));
|
|
14
|
+
}
|
|
@@ -768,31 +768,19 @@ export function animateViewport(
|
|
|
768
768
|
duration = 300,
|
|
769
769
|
options: { recordHistory?: boolean } = {},
|
|
770
770
|
): void {
|
|
771
|
-
if (animationId !== null) cancelAnimationFrame(animationId);
|
|
772
|
-
|
|
773
771
|
const from = { ...viewport.value };
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (t < 1) {
|
|
788
|
-
animationId = requestAnimationFrame(tick);
|
|
789
|
-
} else {
|
|
790
|
-
animationId = null;
|
|
791
|
-
commitViewport(target, options);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
animationId = requestAnimationFrame(tick);
|
|
772
|
+
animateCameraPath(
|
|
773
|
+
(t) => {
|
|
774
|
+
const e = easeOutCubic(t);
|
|
775
|
+
return {
|
|
776
|
+
x: from.x + (target.x - from.x) * e,
|
|
777
|
+
y: from.y + (target.y - from.y) * e,
|
|
778
|
+
scale: from.scale + (target.scale - from.scale) * e,
|
|
779
|
+
};
|
|
780
|
+
},
|
|
781
|
+
duration,
|
|
782
|
+
() => commitViewport(target, options),
|
|
783
|
+
);
|
|
796
784
|
}
|
|
797
785
|
|
|
798
786
|
/**
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { canvasArea, canvasFitInsets } from '../canvas/canvas-area';
|
|
2
2
|
import { showToast } from './attention-bridge';
|
|
3
3
|
import { isHostedWorkbench, workbenchFetch } from './workbench-transport';
|
|
4
|
-
import type { AgentPresenceSnapshot } from '../../shared/agent-presence.js';
|
|
5
4
|
import type { CanvasEdge, CanvasNodeState } from '../types';
|
|
6
5
|
import { addEdge, canvasInteractionEpoch, nodes } from './canvas-store';
|
|
7
6
|
import { activeBoardId, boardsLoaded } from './boards-store';
|
|
@@ -401,11 +400,6 @@ export async function fetchAxSurfaceState(): Promise<unknown> {
|
|
|
401
400
|
return requestJson<unknown>('fetchAxSurfaceState', '/api/canvas/ax/surface-snapshot', null);
|
|
402
401
|
}
|
|
403
402
|
|
|
404
|
-
/** Connect-time agent-presence read; live updates arrive as `agent-presence` SSE frames. */
|
|
405
|
-
export async function fetchAgentPresence(): Promise<Partial<AgentPresenceSnapshot> | null> {
|
|
406
|
-
return requestJson<Partial<AgentPresenceSnapshot> | null>('fetchAgentPresence', '/api/canvas/ax/presence', null);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
403
|
/** Ask the server to open a node's surface in the system browser. */
|
|
410
404
|
export async function openNodeInSystemBrowserRequest(
|
|
411
405
|
nodeId: string,
|
|
@@ -12,9 +12,9 @@ import {
|
|
|
12
12
|
} from '../../shared/agent-presence.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Agent presence (rail-chrome-v2 phase 2). Fed by the server's
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Agent presence (rail-chrome-v2 phase 2). Fed by the server's ordered
|
|
16
|
+
* `agent-presence` frames: a connect snapshot and every change, including TTL
|
|
17
|
+
* expiry, over SSE or polling. This store never runs its own expiry ticker.
|
|
18
18
|
*
|
|
19
19
|
* `sessionActive` is the ONE selector every agent surface reads — the session
|
|
20
20
|
* panel, command bar, presence layer, and top-bar chip all mount on it.
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
axSurfaceState,
|
|
15
15
|
bringToFront,
|
|
16
16
|
cancelViewportAnimation,
|
|
17
|
-
commitViewport,
|
|
18
17
|
canvasTheme,
|
|
19
18
|
connectionStatus,
|
|
20
19
|
contextPinsRevision,
|
|
@@ -50,12 +49,12 @@ import {
|
|
|
50
49
|
import { applyContextStatus, refreshContextStatus } from './context-status-store';
|
|
51
50
|
import { refreshContextChip } from './context-chip-store';
|
|
52
51
|
import {
|
|
53
|
-
fetchAgentPresence,
|
|
54
52
|
fetchAxSurfaceState,
|
|
55
53
|
fetchCanvasState,
|
|
56
54
|
reportClientViewportSize,
|
|
57
55
|
requestJson,
|
|
58
56
|
tabAgentKey,
|
|
57
|
+
updateViewportFromClient,
|
|
59
58
|
} from './intent-bridge';
|
|
60
59
|
import { agentActivity, applyPresenceSnapshot, sessionActive } from './presence-store';
|
|
61
60
|
import { applyHumanSnapshot, humanClientId, startHumanPresence, stopHumanPresence } from './human-store';
|
|
@@ -577,9 +576,8 @@ function handleConnected(data: Record<string, unknown>): void {
|
|
|
577
576
|
// Reconnect marker for holders of server-minted URLs (Finding S).
|
|
578
577
|
workbenchConnectionEpoch.value += 1;
|
|
579
578
|
reloadIfServerUpgraded(data.version, data.bundleStamp);
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
void fetchAgentPresence().then(applyPresenceSnapshot);
|
|
579
|
+
// Both transports send an ordered `agent-presence` connect snapshot, then
|
|
580
|
+
// live frames. A separate HTTP read can arrive late and overwrite newer state.
|
|
583
581
|
void refreshContextStatus();
|
|
584
582
|
refreshContextChip();
|
|
585
583
|
// Human collaborators (phase 8): announce this tab and read who else is here.
|
|
@@ -1123,7 +1121,14 @@ function handleCanvasCameraMove(data: Record<string, unknown>): void {
|
|
|
1123
1121
|
try {
|
|
1124
1122
|
// The presentation camera is local; otherwise the end camera becomes the
|
|
1125
1123
|
// board viewport — unrecorded, so a camera move is never an undo step.
|
|
1126
|
-
playStop(
|
|
1124
|
+
playStop(
|
|
1125
|
+
stop.data,
|
|
1126
|
+
presenting.value
|
|
1127
|
+
? undefined
|
|
1128
|
+
: (end) => {
|
|
1129
|
+
void updateViewportFromClient(end, { recordHistory: false });
|
|
1130
|
+
},
|
|
1131
|
+
);
|
|
1127
1132
|
} catch {
|
|
1128
1133
|
// Target not in this tab's layout yet: take the server's resolved camera.
|
|
1129
1134
|
const fallback = cameraSchema.safeParse(data.viewport);
|
|
@@ -4050,8 +4050,14 @@ kbd.toolbar-menu-hint {
|
|
|
4050
4050
|
}
|
|
4051
4051
|
|
|
4052
4052
|
/* ── Markdown reader mode (expanded view) ──────────────────── */
|
|
4053
|
+
.md-expanded-document {
|
|
4054
|
+
display: flex;
|
|
4055
|
+
flex-direction: column;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4053
4058
|
.md-reader {
|
|
4054
|
-
|
|
4059
|
+
flex: 1;
|
|
4060
|
+
min-height: 0;
|
|
4055
4061
|
overflow-y: auto;
|
|
4056
4062
|
display: flex;
|
|
4057
4063
|
justify-content: center;
|
|
@@ -4205,11 +4211,11 @@ kbd.toolbar-menu-hint {
|
|
|
4205
4211
|
background: var(--c-surface-subtle);
|
|
4206
4212
|
}
|
|
4207
4213
|
|
|
4208
|
-
/* Source
|
|
4214
|
+
/* Source stays below the scroll viewport, never over the document text. */
|
|
4209
4215
|
.md-edit-fab {
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4216
|
+
align-self: flex-end;
|
|
4217
|
+
flex-shrink: 0;
|
|
4218
|
+
margin: 16px;
|
|
4213
4219
|
padding: 8px 18px;
|
|
4214
4220
|
font-size: 13px;
|
|
4215
4221
|
font-weight: 600;
|
|
@@ -5991,21 +5997,10 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
5991
5997
|
/* Its sticky/backdrop-filter context must paint hints above the Source FAB. */
|
|
5992
5998
|
z-index: 2;
|
|
5993
5999
|
}
|
|
5994
|
-
.expanded-overlay-panel:has(.ax-step-dock) .md-expanded-document {
|
|
5995
|
-
display: flex;
|
|
5996
|
-
flex-direction: column;
|
|
5997
|
-
}
|
|
5998
|
-
.expanded-overlay-panel:has(.ax-step-dock) .md-reader {
|
|
5999
|
-
flex: 1;
|
|
6000
|
-
min-height: 0;
|
|
6001
|
-
}
|
|
6002
6000
|
.expanded-overlay-panel:has(.ax-step-dock) .md-edit-fab {
|
|
6003
6001
|
/* Keep Source and the document out of the bottom dock's hint area. */
|
|
6004
|
-
position: static;
|
|
6005
6002
|
order: -1;
|
|
6006
|
-
|
|
6007
|
-
flex-shrink: 0;
|
|
6008
|
-
margin-bottom: 16px;
|
|
6003
|
+
margin: 0 0 16px;
|
|
6009
6004
|
}
|
|
6010
6005
|
.ax-step-dock:has(.toolbar-tooltip-anchor:hover) .toolbar-tooltip-anchor:not(:hover) > :focus-visible + .toolbar-tooltip {
|
|
6011
6006
|
/* Pointer exploration temporarily takes precedence without moving keyboard focus. */
|
|
@@ -2863,6 +2863,16 @@ class CanvasStateManager {
|
|
|
2863
2863
|
return node ? structuredClone(this.nodeForRead(node)) : undefined;
|
|
2864
2864
|
}
|
|
2865
2865
|
|
|
2866
|
+
/** Camera targets need geometry, not hydrated documents, frames or image blobs. */
|
|
2867
|
+
nodeGeometry(): Array<Pick<CanvasNodeState, 'id' | 'type' | 'position' | 'size'>> {
|
|
2868
|
+
return Array.from(this.nodes.values(), ({ id, type, position, size }) => ({
|
|
2869
|
+
id,
|
|
2870
|
+
type,
|
|
2871
|
+
position: { ...position },
|
|
2872
|
+
size: { ...size },
|
|
2873
|
+
}));
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2866
2876
|
/** Who made and last edited each card, and its content revision — no content, no copies. */
|
|
2867
2877
|
nodeStamps(): Array<Pick<CanvasNodeState, 'id' | 'createdBy' | 'lastEditedBy' | 'contentRevision'>> {
|
|
2868
2878
|
return [...this.nodes.values()].map(({ id, createdBy, lastEditedBy, contentRevision }) => ({
|
package/src/server/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import {
|
|
3
|
-
import { exitTour, goToTourStop, moveCamera, type TourStep, type TourStepResult } from './tour-control.js';
|
|
2
|
+
import type { Camera, Tour, TourStop } from '../shared/tour.js';
|
|
3
|
+
import { currentTour, exitTour, goToTourStop, moveCamera, type TourStep, type TourStepResult } from './tour-control.js';
|
|
4
4
|
export type { Tour, TourStop } from '../shared/tour.js';
|
|
5
5
|
export type { TourStep, TourStepResult } from './tour-control.js';
|
|
6
6
|
import { agentPresence } from './agent-presence.js';
|
|
7
7
|
import { checkFenceTarget, type FenceTarget } from './scope-fence.js';
|
|
8
8
|
import type { AgentPhase, AgentPresence, AgentPresenceSnapshot } from '../shared/agent-presence.js';
|
|
9
|
-
import { canvasState
|
|
10
|
-
import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout
|
|
9
|
+
import { canvasState } from './canvas-state.js';
|
|
10
|
+
import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout } from './canvas-state.js';
|
|
11
11
|
import { buildCanvasAxContext } from './ax-context.js';
|
|
12
12
|
import { applyAxInteraction, type AxInteractionInput, type AxInteractionPublicResult } from './ax-interaction.js';
|
|
13
13
|
import { intentRegistry } from './intent-registry.js';
|
|
@@ -46,12 +46,12 @@ import { workbenchToken } from './workbench-auth.js';
|
|
|
46
46
|
import type { BackupFile, BackupStatus } from './backup.js';
|
|
47
47
|
import type { ExportManifest } from './board-export.js';
|
|
48
48
|
import { onFileNodeChanged } from './file-watcher.js';
|
|
49
|
-
import { findOpenCanvasPosition
|
|
49
|
+
import { findOpenCanvasPosition } from './placement.js';
|
|
50
50
|
import { overlapsAny } from '../shared/placement.js';
|
|
51
51
|
import { buildSpatialContext } from './spatial-analysis.js';
|
|
52
52
|
import { searchCanvasNodes } from './operations/ops/query.js';
|
|
53
53
|
import { mutationHistory, diffLayouts, formatDiff } from './mutation-history.js';
|
|
54
|
-
import {
|
|
54
|
+
import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
55
55
|
import {
|
|
56
56
|
addCanvasNode,
|
|
57
57
|
addCanvasEdge,
|
|
@@ -90,12 +90,7 @@ import { validateCanvasLayout } from './canvas-validation.js';
|
|
|
90
90
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
91
91
|
import { serializeCanvasNode, type SerializedCanvasNode } from './canvas-serialization.js';
|
|
92
92
|
import { contextReadFromPayload } from './context-reads.js';
|
|
93
|
-
import {
|
|
94
|
-
buildHtmlPrimitive,
|
|
95
|
-
getHtmlPrimitiveSemanticMetadata,
|
|
96
|
-
isHtmlPrimitiveKind,
|
|
97
|
-
listHtmlPrimitiveDescriptors,
|
|
98
|
-
} from './html-primitives.js';
|
|
93
|
+
import { buildHtmlPrimitive, getHtmlPrimitiveSemanticMetadata } from './html-primitives.js';
|
|
99
94
|
import type { HtmlPrimitiveKind } from './html-primitives.js';
|
|
100
95
|
import {
|
|
101
96
|
buildWebArtifactOnCanvas,
|
|
@@ -104,15 +99,7 @@ import {
|
|
|
104
99
|
} from './web-artifacts.js';
|
|
105
100
|
import { closeMcpAppSession, type ExternalMcpTransportConfig } from './mcp-app-runtime.js';
|
|
106
101
|
import type { DiagramPresetOpenInput } from './diagram-presets.js';
|
|
107
|
-
import {
|
|
108
|
-
buildGraphSpec,
|
|
109
|
-
GRAPH_NODE_SIZE,
|
|
110
|
-
JSON_RENDER_NODE_SIZE,
|
|
111
|
-
normalizeAndValidateJsonRenderSpec,
|
|
112
|
-
type GraphNodeInput,
|
|
113
|
-
type JsonRenderNodeInput,
|
|
114
|
-
type JsonRenderSpec,
|
|
115
|
-
} from '../json-render/server.js';
|
|
102
|
+
import type { GraphNodeInput, JsonRenderNodeInput, JsonRenderSpec } from '../json-render/server.js';
|
|
116
103
|
import {
|
|
117
104
|
startCanvasServer,
|
|
118
105
|
stopCanvasServer,
|
|
@@ -126,15 +113,8 @@ import {
|
|
|
126
113
|
screenshotCanvasAutomationWebView,
|
|
127
114
|
emitPrimaryWorkbenchEvent,
|
|
128
115
|
setPrimaryWorkbenchCanvasPromptHandler,
|
|
129
|
-
setPrimaryWorkbenchAutoOpenEnabled,
|
|
130
|
-
consumePrimaryWorkbenchIntents,
|
|
131
|
-
} from './server.js';
|
|
132
|
-
import type {
|
|
133
|
-
CanvasAutomationWebViewOptions,
|
|
134
|
-
CanvasAutomationWebViewStatus,
|
|
135
|
-
PrimaryWorkbenchCanvasPromptRequest,
|
|
136
|
-
PrimaryWorkbenchIntent,
|
|
137
116
|
} from './server.js';
|
|
117
|
+
import type { CanvasAutomationWebViewOptions, CanvasAutomationWebViewStatus } from './server.js';
|
|
138
118
|
|
|
139
119
|
/**
|
|
140
120
|
* Node object returned by the SDK's create/get methods. It is the fully
|
|
@@ -633,7 +613,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
633
613
|
}
|
|
634
614
|
|
|
635
615
|
getTour(): Tour {
|
|
636
|
-
return
|
|
616
|
+
return currentTour().tour;
|
|
637
617
|
}
|
|
638
618
|
|
|
639
619
|
setTour(tour: Tour | null): void {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Database } from 'bun:sqlite';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { normalizeBoardFolder } from '../shared/boards.js';
|
|
4
|
+
import { memoryKey, type MemoryTarget } from '../shared/memory-graph.js';
|
|
4
5
|
import { currentActor, type ActorAttribution } from './attribution.js';
|
|
5
6
|
import { OperationError } from './operations/types.js';
|
|
6
7
|
import type { LifecycleQualification } from '../shared/lifecycle-status.js';
|
|
@@ -47,6 +48,16 @@ export interface ReviewRecord {
|
|
|
47
48
|
sourceRevisions?: Record<string, number | null>;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// SQLite trim defaults to ASCII space; match JavaScript evidence.trim(), including Unicode whitespace.
|
|
52
|
+
const evidenceWhitespace =
|
|
53
|
+
'\u0009\u000a\u000b\u000c\u000d\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff';
|
|
54
|
+
const validationPredicate = `json_extract(value, '$.applied') = 1
|
|
55
|
+
AND json_extract(value, '$.outcome') IN ('retain', 'revise', 'externalize')
|
|
56
|
+
AND trim(json_extract(value, '$.evidence'), '${evidenceWhitespace}') <> ''`;
|
|
57
|
+
const retirementPredicate = `(json_extract(value, '$.outcome') = 'restore'
|
|
58
|
+
OR (json_extract(value, '$.applied') = 1 AND json_extract(value, '$.outcome') = 'retire'))`;
|
|
59
|
+
const indexedDatabases = new WeakSet<Database>();
|
|
60
|
+
|
|
50
61
|
export type KnowledgeInput =
|
|
51
62
|
| { action: 'list' }
|
|
52
63
|
| { action: 'category-set'; category: KnowledgeCategory }
|
|
@@ -240,6 +251,61 @@ export class KnowledgeLifecycle {
|
|
|
240
251
|
return resolved;
|
|
241
252
|
}
|
|
242
253
|
|
|
254
|
+
private indexedDb(): Database {
|
|
255
|
+
const db = this.db();
|
|
256
|
+
if (!indexedDatabases.has(db)) {
|
|
257
|
+
db.exec(`CREATE INDEX IF NOT EXISTS review_latest_validation ON review_records
|
|
258
|
+
(board_id, node_id, sequence DESC) WHERE ${validationPredicate};
|
|
259
|
+
CREATE INDEX IF NOT EXISTS review_latest_retirement ON review_records
|
|
260
|
+
(board_id, node_id, sequence DESC) WHERE ${retirementPredicate};
|
|
261
|
+
CREATE INDEX IF NOT EXISTS lifecycle_incoming_sources ON edges (to_board_id, to_node)
|
|
262
|
+
WHERE COALESCE(kind, type) IN ('derived-from', 'contradicts', 'supersedes');`);
|
|
263
|
+
indexedDatabases.add(db);
|
|
264
|
+
}
|
|
265
|
+
return db;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Stored owners of semantic sources; the caller overlays the active board's unsaved edges. */
|
|
269
|
+
incomingSourceBoards(target: MemoryTarget, activeBoardId: string | null): string[] {
|
|
270
|
+
return this.indexedDb()
|
|
271
|
+
.query<{ board_id: string }, [string, string | null, string | null]>(
|
|
272
|
+
`SELECT DISTINCT board_id FROM edges WHERE to_board_id = ? AND to_node = ?
|
|
273
|
+
AND board_id IS NOT ? AND COALESCE(kind, type) IN ('derived-from', 'contradicts', 'supersedes')`,
|
|
274
|
+
)
|
|
275
|
+
.all(target.boardId, target.cardId ?? null, activeBoardId)
|
|
276
|
+
.map((row) => row.board_id);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** One indexed latest validation and retirement per card, never its full audit history. */
|
|
280
|
+
reviewSummaries(targets: MemoryTarget[]): Map<string, { lastValidation: ReviewRecord | null; retired: boolean }> {
|
|
281
|
+
if (!targets.length) return new Map();
|
|
282
|
+
const rows = this.indexedDb()
|
|
283
|
+
.query<{ boardId: string; nodeId: string; validation: string | null; retirement: string | null }, [string]>(
|
|
284
|
+
`WITH targets AS (
|
|
285
|
+
SELECT json_extract(value, '$.boardId') AS boardId, json_extract(value, '$.cardId') AS nodeId
|
|
286
|
+
FROM json_each(?)
|
|
287
|
+
) SELECT boardId, nodeId,
|
|
288
|
+
(SELECT value FROM review_records WHERE board_id = boardId AND node_id = nodeId
|
|
289
|
+
AND ${validationPredicate} ORDER BY sequence DESC LIMIT 1) AS validation,
|
|
290
|
+
(SELECT json_extract(value, '$.outcome') FROM review_records WHERE board_id = boardId AND node_id = nodeId
|
|
291
|
+
AND ${retirementPredicate} ORDER BY sequence DESC LIMIT 1) AS retirement
|
|
292
|
+
FROM targets`,
|
|
293
|
+
)
|
|
294
|
+
.all(JSON.stringify(targets));
|
|
295
|
+
return new Map(
|
|
296
|
+
rows.map((row) => {
|
|
297
|
+
const record: ReviewRecord | null = row.validation ? JSON.parse(row.validation) : null;
|
|
298
|
+
return [
|
|
299
|
+
memoryKey({ boardId: row.boardId, cardId: row.nodeId }),
|
|
300
|
+
{
|
|
301
|
+
lastValidation: record ? { ...record, contentRevision: record.contentRevision ?? null } : null,
|
|
302
|
+
retired: row.retirement === 'retire',
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
243
309
|
reviews(boardId: string, nodeId: string) {
|
|
244
310
|
const records = (
|
|
245
311
|
this.db()
|