pmx-canvas 0.5.1 → 0.6.2
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/CHANGELOG.md +50 -0
- package/Readme.md +10 -7
- package/dist/canvas/global.css +101 -7
- package/dist/canvas/index.js +171 -128
- package/dist/canvas/mermaid-entry.js +1303 -1303
- package/dist/json-render/index.js +101 -102
- package/dist/types/client/canvas/use-pan-zoom.d.ts +2 -1
- package/docs/adr-001-bun-only-runtime.md +1 -1
- package/docs/bun-webview-integration.md +2 -2
- package/docs/node-types.md +1 -1
- package/docs/product-context-vision-2026-09.md +207 -0
- package/docs/product-review-2026-09.md +174 -0
- package/docs/product-vision-2026-09.md +200 -0
- package/docs/screenshot.png +0 -0
- package/package.json +2 -2
- package/skills/pmx-canvas/SKILL.md +62 -36
- package/skills/pmx-canvas/evals/evals.json +1 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +5 -0
- package/skills/pmx-canvas/references/full-reference.md +5 -4
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +126 -18
- package/skills/pmx-canvas-testing/SKILL.md +13 -4
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +18 -8
- package/src/client/canvas/CanvasNode.tsx +9 -12
- package/src/client/canvas/CanvasViewport.tsx +20 -33
- package/src/client/canvas/ContextMenu.tsx +13 -0
- package/src/client/canvas/EdgeLayer.tsx +2 -2
- package/src/client/canvas/SessionPanel.tsx +23 -4
- package/src/client/canvas/ShortcutOverlay.tsx +2 -2
- package/src/client/canvas/TopBar.tsx +85 -45
- package/src/client/canvas/use-pan-zoom.ts +23 -3
- package/src/client/nodes/ExtAppFrame.tsx +97 -28
- package/src/client/state/canvas-store.ts +17 -5
- package/src/client/state/sse-bridge.ts +3 -1
- package/src/client/theme/global.css +101 -7
- package/src/server/canvas-operations.ts +2 -2
- package/src/server/operations/mcp.ts +45 -31
- package/src/server/operations/ops/ax-presence.ts +3 -0
- package/src/server/operations/ops/viewport.ts +6 -1
- package/src/server/scope-fence.ts +1 -1
- package/src/server/server.ts +9 -9
- package/src/server/web-artifacts/scripts/bundle-artifact.sh +29 -8
|
@@ -2,41 +2,101 @@
|
|
|
2
2
|
|
|
3
3
|
Use this reference when the `pmx-canvas` skill is installed but the `pmx-canvas` command is not available yet.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Prerequisites and version
|
|
6
|
+
|
|
7
|
+
The examples pin **PMX Canvas 0.6.1**, which requires **Bun >=1.4.2** even when
|
|
8
|
+
installed through npm: the CLI runs through Bun, not Node. Check `bun --version`
|
|
9
|
+
first. If missing or older, follow the [Bun installation instructions](https://bun.sh/docs/installation)
|
|
10
|
+
with the user's authorization. Ensure Bun is on the MCP host's PATH as well as the
|
|
11
|
+
interactive shell's; use an absolute `bun`/`bunx` executable path if needed.
|
|
12
|
+
|
|
13
|
+
Install/configure only when requested. After an intentional runtime upgrade, sync the
|
|
14
|
+
skill from that installed package and restart existing server/MCP processes as described
|
|
15
|
+
in `SKILL.md`. Do not pair newer checkout guidance with an older runtime unknowingly.
|
|
16
|
+
|
|
17
|
+
## Install from npm
|
|
6
18
|
|
|
7
19
|
```bash
|
|
8
|
-
npm install -g pmx-canvas
|
|
9
|
-
pmx-canvas
|
|
10
|
-
pmx-canvas open
|
|
20
|
+
npm install -g pmx-canvas@0.6.1
|
|
21
|
+
pmx-canvas --version
|
|
11
22
|
```
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
Without a global install, use `bunx pmx-canvas@0.6.1` in place of `pmx-canvas` in
|
|
25
|
+
the commands below. Do not use unpinned `bunx pmx-canvas` for a version-specific check.
|
|
26
|
+
|
|
27
|
+
## Choose the workspace and target
|
|
28
|
+
|
|
29
|
+
Select an existing absolute root and a dedicated free port. These shell examples use
|
|
30
|
+
POSIX syntax; supply equivalent environment variables on other platforms.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
export PMX_CANVAS_WORKSPACE_ROOT=/absolute/path/to/project
|
|
34
|
+
export PMX_CANVAS_PORT=14313 # Example only: choose a free port for this workspace.
|
|
35
|
+
unset PMX_CANVAS_URL # A stale URL overrides the environment's CLI port target.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use **both root and port**. Root pinning alone does not align the CLI, MCP, and browser
|
|
39
|
+
if a server falls back to another port. Without an explicit root, MCP may attach to a
|
|
40
|
+
different workspace on the preferred port. `PMX_CANVAS_ALLOW_WORKSPACE_SPLIT=1` opts
|
|
41
|
+
out of that heuristic but is not a substitute for checking the actual target.
|
|
42
|
+
|
|
43
|
+
For server startup, precedence is `--port`, then `PMX_WEB_CANVAS_PORT`, then
|
|
44
|
+
`PMX_CANVAS_PORT` (then `PORT` in Amp orbs), then the default 4313. For CLI queries and
|
|
45
|
+
mutations, explicit `--server-url`/`--port` override environment targeting; otherwise
|
|
46
|
+
`PMX_CANVAS_URL` wins over `PMX_CANVAS_PORT`. URL targeting does not set the bind port.
|
|
47
|
+
|
|
48
|
+
## Start the server
|
|
49
|
+
|
|
50
|
+
On an ordinary local machine:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pmx-canvas serve --daemon --no-open --port="$PMX_CANVAS_PORT" --wait-ms=20000
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
In a managed environment, use the host's supervised-service mechanism with foreground
|
|
57
|
+
`pmx-canvas serve --no-open --port=<chosen-port>`, passing the selected environment.
|
|
58
|
+
Do not daemonize inside a supervisor or rely on `nohup`, background shell jobs, or tmux
|
|
59
|
+
to survive an orb restart. In an Amp orb, prefer declared services with
|
|
60
|
+
`amp orb services ensure`; otherwise use `amp orb service start` with the foreground
|
|
61
|
+
command. Use the host's authenticated portal/preview for remote viewing, not a raw
|
|
62
|
+
loopback URL. Verify health below before opening the workbench or making writes.
|
|
63
|
+
|
|
64
|
+
CLI query/mutation commands do not auto-start a server; `serve` or `--mcp` does.
|
|
65
|
+
|
|
66
|
+
## From a local checkout (development)
|
|
14
67
|
|
|
15
68
|
```bash
|
|
16
69
|
git clone https://github.com/pskoett/pmx-canvas.git
|
|
17
70
|
cd pmx-canvas
|
|
18
71
|
bun install
|
|
19
72
|
bun run build
|
|
20
|
-
bun run src/cli/index.ts
|
|
73
|
+
bun run src/cli/index.ts --version
|
|
21
74
|
```
|
|
22
75
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```
|
|
76
|
+
This uses the checked-out source, not necessarily the published 0.6.1 package. Follow
|
|
77
|
+
that checkout's `package.json` Bun requirement and bundled skill. Set the same root/port
|
|
78
|
+
environment above; substitute `bun run /absolute/path/to/pmx-canvas/src/cli/index.ts`
|
|
79
|
+
for `pmx-canvas` in startup and verification commands. The target workspace can be
|
|
80
|
+
different from the source checkout. Build the client before browser verification.
|
|
29
81
|
|
|
30
82
|
## MCP Config
|
|
31
83
|
|
|
32
|
-
For agents that support MCP, configure
|
|
84
|
+
For agents that support MCP, configure the pinned package as a stdio server. Replace
|
|
85
|
+
the root and port with your selections; ensure the port is dedicated to this workspace.
|
|
86
|
+
The empty URL overrides a stale inherited value (empty is treated as unset). If the host
|
|
87
|
+
discards empty env values, remove `PMX_CANVAS_URL` from its launch environment instead.
|
|
33
88
|
|
|
34
89
|
```json
|
|
35
90
|
{
|
|
36
91
|
"mcpServers": {
|
|
37
92
|
"canvas": {
|
|
38
93
|
"command": "bunx",
|
|
39
|
-
"args": ["pmx-canvas", "--mcp"]
|
|
94
|
+
"args": ["pmx-canvas@0.6.1", "--mcp"],
|
|
95
|
+
"env": {
|
|
96
|
+
"PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
|
|
97
|
+
"PMX_CANVAS_PORT": "14313",
|
|
98
|
+
"PMX_CANVAS_URL": ""
|
|
99
|
+
}
|
|
40
100
|
}
|
|
41
101
|
}
|
|
42
102
|
}
|
|
@@ -49,18 +109,66 @@ If you are using a local checkout instead of the published package, point the co
|
|
|
49
109
|
"mcpServers": {
|
|
50
110
|
"canvas": {
|
|
51
111
|
"command": "bun",
|
|
52
|
-
"args": ["run", "/path/to/pmx-canvas/src/cli/index.ts", "--mcp"]
|
|
112
|
+
"args": ["run", "/absolute/path/to/pmx-canvas/src/cli/index.ts", "--mcp"],
|
|
113
|
+
"env": {
|
|
114
|
+
"PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
|
|
115
|
+
"PMX_CANVAS_PORT": "14313",
|
|
116
|
+
"PMX_CANVAS_URL": ""
|
|
117
|
+
}
|
|
53
118
|
}
|
|
54
119
|
}
|
|
55
120
|
}
|
|
56
121
|
```
|
|
57
122
|
|
|
58
|
-
|
|
123
|
+
Restart the host's MCP process after changing configuration. Inspect startup output for
|
|
124
|
+
fallback ports; do not assume the configured port is the one the MCP server bound.
|
|
125
|
+
|
|
126
|
+
## Verify the actual target before writes
|
|
127
|
+
|
|
128
|
+
For the local target selected above:
|
|
59
129
|
|
|
60
130
|
```bash
|
|
61
131
|
pmx-canvas --version
|
|
62
|
-
|
|
132
|
+
curl --fail --silent --show-error "http://localhost:${PMX_CANVAS_PORT}/health"
|
|
133
|
+
pmx-canvas serve status --port="$PMX_CANVAS_PORT"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Require healthy `ok`, the expected running `version`, and a **`workspace`** matching the
|
|
137
|
+
intended canonical absolute root (resolve symlinks on both paths). Inspect persistence
|
|
138
|
+
health too. `serve status` checks a local port, not an arbitrary `PMX_CANVAS_URL`.
|
|
139
|
+
When deliberately targeting a URL or a fallback port, fetch `/health` there instead,
|
|
140
|
+
then align CLI, MCP, and browser to it. Never print a credential-bearing URL.
|
|
141
|
+
On mismatch, stop: leave the other server alone, choose a free port, and repeat.
|
|
142
|
+
|
|
143
|
+
Only after confirming the target:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
63
146
|
pmx-canvas layout
|
|
147
|
+
pmx-canvas open
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Inside an orb, open the registered portal rather than asking the user to open localhost.
|
|
151
|
+
|
|
152
|
+
## Disposable smoke verification
|
|
153
|
+
|
|
154
|
+
`smoke` creates/searches/removes a temporary node and can leave history/activity records.
|
|
155
|
+
It reports the workspace but does **not** compare it with your intended root. Do not run
|
|
156
|
+
it on a valuable board as an identity check. Use a new disposable workspace and its own
|
|
157
|
+
free port, start it with the appropriate startup method above, and verify `/health` first:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
export PMX_CANVAS_WORKSPACE_ROOT="$(mktemp -d)"
|
|
161
|
+
export PMX_CANVAS_PORT=14314 # Choose another free port.
|
|
162
|
+
unset PMX_CANVAS_URL
|
|
163
|
+
# Start this workspace's server, then verify its actual health workspace/version.
|
|
164
|
+
# Only after that verification succeeds:
|
|
165
|
+
pmx-canvas smoke
|
|
64
166
|
```
|
|
65
167
|
|
|
66
|
-
|
|
168
|
+
Inspect the JSON report and exit status: health, MCP initialize, node lifecycle, and
|
|
169
|
+
validation should pass; failures exit 1. Version skew is reported in details and is not
|
|
170
|
+
necessarily a failing check. Smoke proves neither `tools/list` discovery nor browser
|
|
171
|
+
rendering: verify those separately when testing the complete onboarding experience.
|
|
172
|
+
Stop only the test server (service-manager stop, or `serve stop --port=<test-port>` for
|
|
173
|
+
your local daemon) before removing its disposable directory. Never clear a user's board
|
|
174
|
+
or stop an unrelated listener to make a smoke test pass.
|
|
@@ -9,7 +9,14 @@ description: >
|
|
|
9
9
|
|
|
10
10
|
# PMX Canvas Testing
|
|
11
11
|
|
|
12
|
-
Use this skill
|
|
12
|
+
Use this skill when changing the PMX Canvas product checkout and you need a consistent verification
|
|
13
|
+
path. The commands and paths below refer to that source checkout, not to a workspace that merely
|
|
14
|
+
has the published package and skills installed.
|
|
15
|
+
|
|
16
|
+
For an isolated consumer workspace, do not expect the package to contain this checkout's
|
|
17
|
+
`tests/`, source-only scripts, or Playwright configuration. Validate package transports and host
|
|
18
|
+
integration through their public CLI, MCP, HTTP, and SDK surfaces; use the
|
|
19
|
+
`published-consumer-e2e` skill when a packed-install outside-in check is required.
|
|
13
20
|
|
|
14
21
|
## When To Use
|
|
15
22
|
|
|
@@ -59,9 +66,11 @@ and check `$?`, then read the file.
|
|
|
59
66
|
|
|
60
67
|
## Current Project Test Surface
|
|
61
68
|
|
|
62
|
-
- Bun tests live under `tests/unit
|
|
63
|
-
- Playwright browser smoke lives under `tests/e2e
|
|
64
|
-
- CI runs Bun coverage plus the browser smoke flow
|
|
69
|
+
- In the product source checkout, Bun tests live under `tests/unit/`.
|
|
70
|
+
- In the product source checkout, Playwright browser smoke lives under `tests/e2e/`.
|
|
71
|
+
- Product CI runs those checkout suites: Bun coverage plus the browser smoke flow.
|
|
72
|
+
- An isolated installed consumer instead tests the package's public transports and host behavior;
|
|
73
|
+
it does not run or depend on the product checkout's source suites.
|
|
65
74
|
|
|
66
75
|
## WebView Automation Caveat
|
|
67
76
|
|
|
@@ -56,14 +56,12 @@ function replay_filtered_stderr() {
|
|
|
56
56
|
|
|
57
57
|
function run_with_filtered_stderr() {
|
|
58
58
|
local stderr_file
|
|
59
|
+
local status
|
|
59
60
|
stderr_file="$(mktemp)"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
|
-
local status=$?
|
|
61
|
+
set +e
|
|
62
|
+
"$@" 2>"$stderr_file"
|
|
63
|
+
status=$?
|
|
64
|
+
set -e
|
|
67
65
|
replay_filtered_stderr "$stderr_file"
|
|
68
66
|
rm -f "$stderr_file"
|
|
69
67
|
return "$status"
|
|
@@ -161,7 +159,19 @@ rm -rf dist bundle.html
|
|
|
161
159
|
|
|
162
160
|
# Build with Parcel
|
|
163
161
|
echo "🔨 Building with Parcel..."
|
|
164
|
-
|
|
162
|
+
parcel_status=0
|
|
163
|
+
run_with_filtered_stderr run_local_binary parcel build index.html --dist-dir dist --no-source-maps --log-level error || parcel_status=$?
|
|
164
|
+
|
|
165
|
+
# Self-heal a wedged cache: these build projects are REUSED across artifact
|
|
166
|
+
# builds, and a parcel killed mid-write (e.g. the canvas server torn down
|
|
167
|
+
# during a build) leaves a corrupted .parcel-cache that makes every later
|
|
168
|
+
# build die before emitting anything — permanently, until the cache goes.
|
|
169
|
+
# Retry exactly once from a cold cache, then preserve a genuine build failure.
|
|
170
|
+
if [ "$parcel_status" -ne 0 ] || [ ! -s "dist/index.html" ]; then
|
|
171
|
+
echo "🧹 Parcel build failed or produced no output — clearing .parcel-cache and retrying cold..."
|
|
172
|
+
rm -rf .parcel-cache dist
|
|
173
|
+
run_with_filtered_stderr run_local_binary parcel build index.html --dist-dir dist --no-source-maps --log-level error
|
|
174
|
+
fi
|
|
165
175
|
|
|
166
176
|
if [ ! -s "dist/index.html" ]; then
|
|
167
177
|
echo "❌ Error: Parcel did not produce dist/index.html" >&2
|
|
@@ -234,6 +234,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
234
234
|
|
|
235
235
|
const handleTitlePointerDown = useCallback(
|
|
236
236
|
(e: PointerEvent) => {
|
|
237
|
+
if (e.button !== 0) return;
|
|
237
238
|
if (renaming) return;
|
|
238
239
|
if (canvasTool.value === 'connect') {
|
|
239
240
|
startEdgeDrag(e);
|
|
@@ -267,6 +268,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
267
268
|
|
|
268
269
|
const handlePointerDown = useCallback(
|
|
269
270
|
(e: PointerEvent) => {
|
|
271
|
+
if (e.button !== 0) return;
|
|
270
272
|
e.stopPropagation();
|
|
271
273
|
if (canvasTool.value === 'connect') {
|
|
272
274
|
startEdgeDrag(e);
|
|
@@ -281,8 +283,10 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
281
283
|
[node.id, startEdgeDrag],
|
|
282
284
|
);
|
|
283
285
|
|
|
284
|
-
const
|
|
286
|
+
const handleDblClick = useCallback(
|
|
285
287
|
(e: MouseEvent) => {
|
|
288
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
289
|
+
if (target?.closest('button, input, textarea, select, a, [contenteditable="true"]')) return;
|
|
286
290
|
if (onContextMenu) onContextMenu(e, node.id);
|
|
287
291
|
},
|
|
288
292
|
[onContextMenu, node.id],
|
|
@@ -312,13 +316,6 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
312
316
|
[node],
|
|
313
317
|
);
|
|
314
318
|
|
|
315
|
-
// ── Double-click rename ───────────────────────────────
|
|
316
|
-
const handleTitleDblClick = useCallback((e: MouseEvent) => {
|
|
317
|
-
e.stopPropagation();
|
|
318
|
-
setRenaming(true);
|
|
319
|
-
requestAnimationFrame(() => renameRef.current?.focus());
|
|
320
|
-
}, []);
|
|
321
|
-
|
|
322
319
|
const title =
|
|
323
320
|
(node.data.title as string) ||
|
|
324
321
|
// Untitled file nodes (created before titles were synthesized server-side)
|
|
@@ -453,7 +450,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
453
450
|
data-node-type={node.type}
|
|
454
451
|
style={{ ...nodeStyle, width: 'auto', height: 'auto' }}
|
|
455
452
|
onPointerDown={handlePointerDown}
|
|
456
|
-
|
|
453
|
+
onDblClick={handleDblClick}
|
|
457
454
|
title="Expand group"
|
|
458
455
|
data-testid="group-chip"
|
|
459
456
|
>
|
|
@@ -499,7 +496,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
499
496
|
onMouseDown={(e) => {
|
|
500
497
|
if (e.shiftKey) e.preventDefault();
|
|
501
498
|
}}
|
|
502
|
-
|
|
499
|
+
onDblClick={handleDblClick}
|
|
503
500
|
>
|
|
504
501
|
{isGroup && (
|
|
505
502
|
<div class="node-titlebar group-edge-row" onPointerDown={handleTitlePointerDown}>
|
|
@@ -517,7 +514,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
517
514
|
onClick={(e) => e.stopPropagation()}
|
|
518
515
|
/>
|
|
519
516
|
) : (
|
|
520
|
-
<span class="group-name"
|
|
517
|
+
<span class="group-name" title={title}>
|
|
521
518
|
{title}
|
|
522
519
|
</span>
|
|
523
520
|
)}
|
|
@@ -659,7 +656,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
659
656
|
onClick={(e) => e.stopPropagation()}
|
|
660
657
|
/>
|
|
661
658
|
) : (
|
|
662
|
-
<span class="node-title"
|
|
659
|
+
<span class="node-title" title={title}>
|
|
663
660
|
{title}
|
|
664
661
|
</span>
|
|
665
662
|
)}
|
|
@@ -409,7 +409,7 @@ export function CanvasViewport({
|
|
|
409
409
|
if (e.target !== container) return;
|
|
410
410
|
|
|
411
411
|
// Pan tool / held Space: usePanZoom owns this drag.
|
|
412
|
-
if (isPanModeActive() || e.button === 1) {
|
|
412
|
+
if (isPanModeActive() || e.button === 1 || e.button === 2) {
|
|
413
413
|
if (!e.shiftKey && !lassoRef.current) {
|
|
414
414
|
activeNodeId.value = null;
|
|
415
415
|
clearSelection();
|
|
@@ -659,50 +659,36 @@ export function CanvasViewport({
|
|
|
659
659
|
};
|
|
660
660
|
}, [containerRef]);
|
|
661
661
|
|
|
662
|
-
// ── Double-click on background →
|
|
662
|
+
// ── Double-click on background → open node creation menu ──
|
|
663
663
|
const handleDblClick = useCallback(
|
|
664
664
|
(e: MouseEvent) => {
|
|
665
665
|
if (annotationMode) return;
|
|
666
666
|
const container = containerRef.current;
|
|
667
|
-
if (!container || e.target !== container) return;
|
|
667
|
+
if (!container || e.target !== container || !onCanvasContextMenu) return;
|
|
668
668
|
const rect = container.getBoundingClientRect();
|
|
669
669
|
const v = viewport.value;
|
|
670
670
|
const wx = (e.clientX - rect.left - v.x) / v.scale;
|
|
671
671
|
const wy = (e.clientY - rect.top - v.y) / v.scale;
|
|
672
|
-
|
|
673
|
-
const nodeW = 520;
|
|
674
|
-
const nodeH = 360;
|
|
675
|
-
createNodeFromClient({
|
|
676
|
-
type: 'markdown',
|
|
677
|
-
title: 'New note',
|
|
678
|
-
x: wx - nodeW / 2,
|
|
679
|
-
y: wy - nodeH / 2,
|
|
680
|
-
width: nodeW,
|
|
681
|
-
height: nodeH,
|
|
682
|
-
});
|
|
672
|
+
onCanvasContextMenu(e, wx, wy);
|
|
683
673
|
},
|
|
684
|
-
[annotationMode, containerRef],
|
|
674
|
+
[annotationMode, containerRef, onCanvasContextMenu],
|
|
685
675
|
);
|
|
686
676
|
|
|
687
|
-
const handleContextMenu = useCallback(
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const container = containerRef.current;
|
|
693
|
-
if (!container) return;
|
|
694
|
-
|
|
695
|
-
const target = e.target instanceof Element ? e.target : null;
|
|
696
|
-
if (target?.closest('.canvas-node')) return;
|
|
677
|
+
const handleContextMenu = useCallback((e: MouseEvent) => {
|
|
678
|
+
// Right-click is a pan gesture, never a menu trigger. Suppress the
|
|
679
|
+
// browser menu whether it lands on the board or bubbles from a node.
|
|
680
|
+
e.preventDefault();
|
|
681
|
+
}, []);
|
|
697
682
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
683
|
+
// Internal native drags (selected node text, links inside rendered
|
|
684
|
+
// markdown) re-enter this same container as drops and mint webpage nodes
|
|
685
|
+
// from card text (0.5.1 Amp finding D). Kill them at the source — external
|
|
686
|
+
// drags never fire a local dragstart, so file/URL import is untouched.
|
|
687
|
+
// Editable elements keep their native drag (text drag inside an editor).
|
|
688
|
+
const handleDragStart = useCallback((e: DragEvent) => {
|
|
689
|
+
if (isEditableElement(e.target instanceof Element ? e.target : null)) return;
|
|
690
|
+
e.preventDefault();
|
|
691
|
+
}, []);
|
|
706
692
|
|
|
707
693
|
// ── Drag-and-drop files from filesystem ──
|
|
708
694
|
const handleDragEnter = useCallback((e: DragEvent) => {
|
|
@@ -824,6 +810,7 @@ export function CanvasViewport({
|
|
|
824
810
|
onPointerUp={handlePointerUp}
|
|
825
811
|
onContextMenu={handleContextMenu}
|
|
826
812
|
onDblClick={handleDblClick}
|
|
813
|
+
onDragStart={handleDragStart}
|
|
827
814
|
onDragEnter={handleDragEnter}
|
|
828
815
|
onDragOver={handleDragOver}
|
|
829
816
|
onDragLeave={handleDragLeave}
|
|
@@ -466,6 +466,19 @@ function buildNodeMenuItems(node: CanvasNodeState): MenuItem[] {
|
|
|
466
466
|
action: () => focusNode(node.id),
|
|
467
467
|
});
|
|
468
468
|
|
|
469
|
+
items.push({
|
|
470
|
+
label: 'Rename…',
|
|
471
|
+
action: () => {
|
|
472
|
+
void askText('Rename node', 'Node title', {
|
|
473
|
+
initial: typeof node.data.title === 'string' ? node.data.title : '',
|
|
474
|
+
confirm: 'Save',
|
|
475
|
+
}).then((title) => {
|
|
476
|
+
if (!title?.trim()) return;
|
|
477
|
+
void updateNodeFromClient(node.id, { title: title.trim() });
|
|
478
|
+
});
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
|
|
469
482
|
// Expand into full-viewport overlay for focused work
|
|
470
483
|
if (EXPANDABLE_TYPES.has(node.type)) {
|
|
471
484
|
items.push({
|
|
@@ -166,7 +166,7 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed, selected, scale, on
|
|
|
166
166
|
<g>
|
|
167
167
|
{/* Invisible wide hitbox — the ONE interactive part of the edge layer
|
|
168
168
|
(the svg itself is pointer-events:none): click selects the edge
|
|
169
|
-
(Delete then removes it),
|
|
169
|
+
(Delete then removes it), double-click opens the edge menu. */}
|
|
170
170
|
<path
|
|
171
171
|
d={d}
|
|
172
172
|
fill="none"
|
|
@@ -179,7 +179,7 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed, selected, scale, on
|
|
|
179
179
|
e.stopPropagation();
|
|
180
180
|
selectedEdgeId.value = edge.id;
|
|
181
181
|
}}
|
|
182
|
-
|
|
182
|
+
onDblClick={onContextMenu ? (e) => onContextMenu(e as unknown as MouseEvent, edge.id) : undefined}
|
|
183
183
|
/>
|
|
184
184
|
|
|
185
185
|
{/* Glow layer for focused or selected edges */}
|
|
@@ -288,7 +288,10 @@ function TimelineRow({ entry }: { entry: TimelineEntry }) {
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
export function SessionPanel() {
|
|
291
|
-
|
|
291
|
+
// A gate may temporarily expand the panel without turning that expansion
|
|
292
|
+
// into a user preference. Once the last gate settles, only an explicitly
|
|
293
|
+
// opened panel remains open.
|
|
294
|
+
const [manuallyOpened, setManuallyOpened] = useState(false);
|
|
292
295
|
const session = activeSession.value;
|
|
293
296
|
const items = sessionWorkItems.value;
|
|
294
297
|
const gates = pendingGates.value;
|
|
@@ -307,12 +310,28 @@ export function SessionPanel() {
|
|
|
307
310
|
void refreshTimeline();
|
|
308
311
|
}, []);
|
|
309
312
|
|
|
310
|
-
|
|
313
|
+
const expanded = manuallyOpened || gates.length > 0;
|
|
314
|
+
if (!expanded) {
|
|
311
315
|
return (
|
|
312
316
|
<aside class="session-panel is-collapsed" aria-label="Session (collapsed)">
|
|
313
|
-
<button
|
|
317
|
+
<button
|
|
318
|
+
type="button"
|
|
319
|
+
class="session-collapse"
|
|
320
|
+
onClick={() => setManuallyOpened(true)}
|
|
321
|
+
title="Expand session panel"
|
|
322
|
+
>
|
|
314
323
|
‹
|
|
315
324
|
</button>
|
|
325
|
+
{session && (
|
|
326
|
+
<span
|
|
327
|
+
class={`session-collapsed-live phase-${session.phase}`}
|
|
328
|
+
title={agentPhaseLabel(session)}
|
|
329
|
+
aria-label={`Session live: ${agentPhaseLabel(session)}`}
|
|
330
|
+
>
|
|
331
|
+
<span class="session-live-dot" aria-hidden="true" />
|
|
332
|
+
live
|
|
333
|
+
</span>
|
|
334
|
+
)}
|
|
316
335
|
<span class="session-collapsed-label">Session</span>
|
|
317
336
|
</aside>
|
|
318
337
|
);
|
|
@@ -342,7 +361,7 @@ export function SessionPanel() {
|
|
|
342
361
|
<button
|
|
343
362
|
type="button"
|
|
344
363
|
class="session-collapse"
|
|
345
|
-
onClick={() =>
|
|
364
|
+
onClick={() => setManuallyOpened(false)}
|
|
346
365
|
title="Collapse session panel"
|
|
347
366
|
>
|
|
348
367
|
›
|
|
@@ -32,7 +32,7 @@ const GROUPS: ShortcutGroup[] = [
|
|
|
32
32
|
{ keys: 'I / W / H', desc: 'New image / webpage / HTML surface' },
|
|
33
33
|
{ keys: 'Shift+F', desc: 'New file node (workspace path)' },
|
|
34
34
|
{ keys: 'G / Shift+G', desc: 'Group the selection (new frame when empty) / ungroup' },
|
|
35
|
-
{ keys: 'Double-click', desc: '
|
|
35
|
+
{ keys: 'Double-click', desc: 'Open actions on a node or create menu on canvas' },
|
|
36
36
|
{ keys: 'Drag port \u2192 node', desc: 'Connect two nodes (hover to reveal ports)' },
|
|
37
37
|
],
|
|
38
38
|
},
|
|
@@ -52,7 +52,7 @@ const GROUPS: ShortcutGroup[] = [
|
|
|
52
52
|
shortcuts: [
|
|
53
53
|
{ keys: '?', desc: 'Toggle this shortcut overlay' },
|
|
54
54
|
{ keys: 'Minimap', desc: 'Click/drag to navigate (toggle in the rail)' },
|
|
55
|
-
{ keys: 'Right-
|
|
55
|
+
{ keys: 'Right-drag', desc: 'Pan the board from anywhere' },
|
|
56
56
|
],
|
|
57
57
|
},
|
|
58
58
|
];
|