pmx-canvas 0.7.0 → 0.7.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/.github/extensions/pmx-canvas/extension.mjs +149 -70
- package/CHANGELOG.md +32 -1
- package/Readme.md +40 -13
- package/dist/canvas/global.css +94 -15
- package/dist/canvas/index.js +76 -76
- package/dist/types/cli/daemon.d.ts +5 -5
- package/dist/types/cli/shared.d.ts +3 -1
- package/dist/types/client/canvas/ExternalWriters.d.ts +3 -3
- package/dist/types/client/icons.d.ts +15 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +0 -12
- package/dist/types/shared/delimited-text.d.ts +14 -0
- package/docs/cli.md +14 -2
- package/docs/http-api.md +12 -8
- package/docs/mcp.md +8 -1
- package/docs/sdk.md +2 -2
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +35 -14
- package/skills/pmx-canvas/evals/evals.json +171 -0
- package/skills/pmx-canvas/references/full-reference.md +6 -1
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +17 -3
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas/references/tours-and-recording.md +69 -0
- package/skills/pmx-canvas-testing/SKILL.md +7 -2
- package/src/cli/commands/apps.ts +1 -1
- package/src/cli/commands/backup.ts +15 -4
- package/src/cli/commands/boards.ts +16 -3
- package/src/cli/commands/export.ts +9 -2
- package/src/cli/daemon.ts +37 -7
- package/src/cli/shared.ts +11 -2
- package/src/client/App.tsx +0 -1
- package/src/client/canvas/AttentionHistory.tsx +2 -1
- package/src/client/canvas/AttentionToast.tsx +5 -3
- package/src/client/canvas/BoardSwitcher.tsx +3 -3
- package/src/client/canvas/CanvasNode.tsx +17 -9
- package/src/client/canvas/CommandBar.tsx +2 -2
- package/src/client/canvas/ContextMenu.tsx +0 -1
- package/src/client/canvas/ContextPinBar.tsx +3 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -3
- package/src/client/canvas/ExportDialog.tsx +3 -1
- package/src/client/canvas/ExternalWriters.tsx +4 -3
- package/src/client/canvas/FeedbackDialog.tsx +2 -1
- package/src/client/canvas/IntentLayer.tsx +2 -2
- package/src/client/canvas/Presentation.tsx +39 -9
- package/src/client/canvas/SelectionBar.tsx +2 -4
- package/src/client/canvas/SessionPanel.tsx +6 -4
- package/src/client/canvas/SessionReceipt.tsx +2 -1
- package/src/client/canvas/SnapshotPanel.tsx +3 -2
- package/src/client/canvas/ToolRail.tsx +2 -1
- package/src/client/canvas/TopBar.tsx +2 -2
- package/src/client/icons.tsx +118 -1
- package/src/client/nodes/AttachmentNode.tsx +27 -30
- package/src/client/nodes/BoardNode.tsx +2 -1
- package/src/client/nodes/ExtAppFrame.tsx +3 -1
- package/src/client/nodes/FileNode.tsx +4 -78
- package/src/client/nodes/ImageNode.tsx +2 -1
- package/src/client/nodes/MarkdownNode.tsx +2 -1
- package/src/client/nodes/PromptNode.tsx +3 -1
- package/src/client/state/attention-bridge.ts +3 -1
- package/src/client/state/sse-bridge.ts +3 -1
- package/src/client/theme/global.css +94 -15
- package/src/mcp/server.ts +2 -2
- package/src/server/agent-presence.ts +17 -11
- package/src/server/board-export.ts +97 -32
- package/src/server/index.ts +9 -7
- package/src/server/operations/composites.ts +3 -2
- package/src/server/operations/ops/ax-presence.ts +7 -3
- package/src/server/operations/ops/boards.ts +12 -5
- package/src/server/operations/ops/imports.ts +19 -3
- package/src/server/operations/registry.ts +4 -13
- package/src/server/server.ts +10 -3
- package/src/shared/delimited-text.ts +75 -0
|
@@ -4,7 +4,7 @@ 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.7.
|
|
7
|
+
The examples pin **PMX Canvas 0.7.2**, which requires **Bun >=1.4.2** 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
|
|
@@ -17,11 +17,11 @@ in `SKILL.md`. Do not pair newer checkout guidance with an older runtime unknowi
|
|
|
17
17
|
## Install from npm
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npm install -g pmx-canvas@0.7.
|
|
20
|
+
npm install -g pmx-canvas@0.7.2
|
|
21
21
|
pmx-canvas --version
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Without a global install, use `bunx pmx-canvas@0.7.
|
|
24
|
+
Without a global install, use `bunx pmx-canvas@0.7.2` in place of `pmx-canvas` in
|
|
25
25
|
the commands below. Do not use unpinned `bunx pmx-canvas` for a version-specific check.
|
|
26
26
|
|
|
27
27
|
## Choose the workspace and target
|
|
@@ -91,7 +91,7 @@ discards empty env values, remove `PMX_CANVAS_URL` from its launch environment i
|
|
|
91
91
|
"mcpServers": {
|
|
92
92
|
"canvas": {
|
|
93
93
|
"command": "bunx",
|
|
94
|
-
"args": ["pmx-canvas@0.7.
|
|
94
|
+
"args": ["pmx-canvas@0.7.2", "--mcp"],
|
|
95
95
|
"env": {
|
|
96
96
|
"PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
|
|
97
97
|
"PMX_CANVAS_PORT": "14313",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Tours and recording
|
|
2
|
+
|
|
3
|
+
Tours use the existing board cards as presentation content. Interactive presentation and
|
|
4
|
+
deterministic capture share this one stop model; recording does not create a second deck.
|
|
5
|
+
|
|
6
|
+
## Author a tour
|
|
7
|
+
|
|
8
|
+
Persist a tour with `canvas_view { action: "set-tour", tour: { stops: [...] } }` and read it with
|
|
9
|
+
`canvas_view { action: "get-tour" }`. A stop has exactly one target:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"stops": [
|
|
14
|
+
{ "target": { "nodeId": "overview-group" }, "duration": 1.5, "padding": 60 },
|
|
15
|
+
{ "target": { "nodeId": "detail-note" }, "duration": 2, "easing": "ease-in-out", "pullback": 0.7 },
|
|
16
|
+
{ "target": { "viewport": { "x": 80, "y": 100, "scale": 0.5 } }, "duration": 1 }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `nodeId` may identify a card or group. Its persisted position and size determine the camera.
|
|
22
|
+
- An explicit viewport is `{ x, y, scale }`, with positive `scale`. Translation is screen-space:
|
|
23
|
+
`screen = world * scale + offset`.
|
|
24
|
+
- `duration` is transition time in seconds (default 1; zero jumps). Valid values are 0–3600.
|
|
25
|
+
- `padding` is screen pixels around a node target (default 40).
|
|
26
|
+
- `easing` is `linear`, `ease-in-out` (default), or `ease-out`.
|
|
27
|
+
- `pullback` is 0–4 (default 0). It zooms out during the move without changing either endpoint.
|
|
28
|
+
- A tour contains at most 1000 stops.
|
|
29
|
+
|
|
30
|
+
Open `/workbench?present=1` for chrome-free viewing. Right, Down, or Space advances; Left or Up
|
|
31
|
+
goes back; Escape or **Exit presentation** exits. Right-drag pans and interrupts an ongoing
|
|
32
|
+
transition without editing cards. Stops do not auto-advance. A missing node target reports an
|
|
33
|
+
error and may be skipped. The presentation camera is local to that viewer while board content
|
|
34
|
+
continues to update. Recording hides the exit control.
|
|
35
|
+
|
|
36
|
+
Tours are board-scoped, persist with that board and its snapshots, participate in undo, and clear
|
|
37
|
+
with the board. Setting `tour: null` resets to a derived tour: groups ordered by y, then x, then
|
|
38
|
+
ID. A saved `{ "stops": [] }` intentionally has no stops; it does not enable group derivation.
|
|
39
|
+
Preserve the tour of each board when working across boards and target the intended board before
|
|
40
|
+
setting or reading it.
|
|
41
|
+
|
|
42
|
+
## Capture prerequisites and behavior
|
|
43
|
+
|
|
44
|
+
Start and verify the matching PMX daemon first: its `/health` workspace and version must match the
|
|
45
|
+
installation and workspace used by the CLI/MCP host. Capture is a local CLI/file operation. It
|
|
46
|
+
requires Bun with WebView support and Chrome (or `--chrome-path`); MP4 encoding additionally
|
|
47
|
+
requires ffmpeg. Recording starts a separate Chrome session; it does not capture the native host's
|
|
48
|
+
Browser pane. Use a new output path because existing output and frame paths are refused.
|
|
49
|
+
|
|
50
|
+
Capture the saved or derived tour:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pmx-canvas record --mode deterministic --present --fps 30 --resolution 1920x1080 --output tour.mp4
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `--tour-file tour.json` in deterministic mode to try the same model without changing the
|
|
57
|
+
board. Resolution defaults to 1280x720 and FPS to 30 (valid 1–120). Deterministic mode resolves
|
|
58
|
+
target geometry once and emits `max(1, ceil(duration * fps))` frames per stop, including each
|
|
59
|
+
endpoint. Duplicate a stop to hold it. It makes camera timing deterministic, not iframe animation,
|
|
60
|
+
late fonts/network assets, or concurrent board edits.
|
|
61
|
+
|
|
62
|
+
Realtime mode instead needs one stopping rule: `--duration SECONDS` or `--stop-on-signal`. It
|
|
63
|
+
samples the live board and repeats frames when capture is slow to preserve wall-clock duration.
|
|
64
|
+
Capture has no audio and does not move other viewers' cameras or replace an existing automation
|
|
65
|
+
session.
|
|
66
|
+
|
|
67
|
+
An `.mp4` output uses ffmpeg/H.264. Frames and `recording.json` remain in `<output>.frames`; if
|
|
68
|
+
ffmpeg is unavailable, the command reports and returns the PNG sequence directory. Any other
|
|
69
|
+
output path is itself a PNG sequence directory. Encoding failures retain the frames and fail.
|
|
@@ -182,8 +182,13 @@ reason to scope verification more narrowly.
|
|
|
182
182
|
the object handed to the emitter — the envelope overwrites `sessionId` and `timestamp`.
|
|
183
183
|
- Two-tab behaviour (human cursors, the edit lock) needs two browser contexts; name them with
|
|
184
184
|
`/workbench?name=…` so assertions can target a cursor by its tag.
|
|
185
|
-
-
|
|
186
|
-
|
|
185
|
+
- Do not categorically infer Browser-pane behavior from the host name. Probe the actual surface:
|
|
186
|
+
record `document.visibilityState`, race a `requestAnimationFrame` callback against a short
|
|
187
|
+
timeout, and capture a screenshot of the claimed rendered state. If hidden or rAF times out,
|
|
188
|
+
use the pane only for static checks and run animation/drag/drop/edge-preview assertions in
|
|
189
|
+
Playwright. If the probe succeeds, it is still scheduling evidence rather than paint proof;
|
|
190
|
+
retain screenshot evidence for a visual pass, especially for iframe-backed or native-host
|
|
191
|
+
surfaces.
|
|
187
192
|
- Selection is shift-click on the node BODY (the titlebar drags); groups have no ports and
|
|
188
193
|
their drag handle is the edge row; the selection bar and command bar both float bottom-center
|
|
189
194
|
(the selection bar lifts during a session).
|
package/src/cli/commands/apps.ts
CHANGED
|
@@ -101,7 +101,7 @@ cmd(
|
|
|
101
101
|
'cat ops.json | pmx-canvas batch --stdin',
|
|
102
102
|
],
|
|
103
103
|
async (args) => {
|
|
104
|
-
const { flags } = parseFlags(args);
|
|
104
|
+
const { flags } = parseFlags(args, { valueFlags: ['json'] });
|
|
105
105
|
if (flags.help || flags.h) return showCommandHelp('batch');
|
|
106
106
|
|
|
107
107
|
let raw = '';
|
|
@@ -12,12 +12,23 @@ import {
|
|
|
12
12
|
showCommandHelp,
|
|
13
13
|
} from '../shared.js';
|
|
14
14
|
|
|
15
|
+
function backupArgs(command: string, args: string[], supported: string[]) {
|
|
16
|
+
const parsed = parseFlags(args, { boolFlags: ['off'] });
|
|
17
|
+
const allowed = new Set(['help', 'h', ...supported]);
|
|
18
|
+
const unknown = Object.keys(parsed.flags).find((flag) => !allowed.has(flag));
|
|
19
|
+
if (unknown) die(`Unknown ${command} flag: --${unknown}`, `Run: pmx-canvas ${command} --help`);
|
|
20
|
+
for (const name of ['to', 'keep', 'every']) {
|
|
21
|
+
if (parsed.flags[name] === true) die(`Missing value for --${name}`, `Run: pmx-canvas ${command} --help`);
|
|
22
|
+
}
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
cmd(
|
|
16
27
|
'backup',
|
|
17
28
|
'Back up every board to a folder (safe while the server runs)',
|
|
18
29
|
['pmx-canvas backup', 'pmx-canvas backup --to ~/Backups/pmx --keep 14'],
|
|
19
30
|
async (args) => {
|
|
20
|
-
const { flags } =
|
|
31
|
+
const { flags } = backupArgs('backup', args, ['to', 'keep']);
|
|
21
32
|
if (flags.help || flags.h) return showCommandHelp('backup');
|
|
22
33
|
const to = getStringFlag(flags, 'to');
|
|
23
34
|
const keep = optionalNumberFlag(flags, 'keep', 'pmx-canvas backup --keep <n>');
|
|
@@ -26,7 +37,7 @@ cmd(
|
|
|
26
37
|
);
|
|
27
38
|
|
|
28
39
|
cmd('backup status', 'Show the backup folder, schedule and backups', ['pmx-canvas backup status'], async (args) => {
|
|
29
|
-
const { flags } =
|
|
40
|
+
const { flags } = backupArgs('backup status', args, []);
|
|
30
41
|
if (flags.help || flags.h) return showCommandHelp('backup status');
|
|
31
42
|
output(await invokeOperation('backup.status', {}));
|
|
32
43
|
});
|
|
@@ -36,7 +47,7 @@ cmd(
|
|
|
36
47
|
'Back up on a schedule, run by the server',
|
|
37
48
|
['pmx-canvas backup schedule --every 24h --keep 14 --to ~/Backups/pmx', 'pmx-canvas backup schedule --off'],
|
|
38
49
|
async (args) => {
|
|
39
|
-
const { flags } =
|
|
50
|
+
const { flags } = backupArgs('backup schedule', args, ['every', 'off', 'to', 'keep']);
|
|
40
51
|
if (flags.help || flags.h) return showCommandHelp('backup schedule');
|
|
41
52
|
const every = flags.off ? 'off' : getStringFlag(flags, 'every');
|
|
42
53
|
if (!every) die('Usage: pmx-canvas backup schedule --every <30m|24h|7d> [--keep <n>] [--to <folder>] | --off');
|
|
@@ -57,7 +68,7 @@ cmd(
|
|
|
57
68
|
'Replace every board with a backup (the current file is kept as canvas.db.before-restore)',
|
|
58
69
|
['pmx-canvas restore ~/Backups/pmx/canvas-20260927T090000000Z.db'],
|
|
59
70
|
async (args) => {
|
|
60
|
-
const { positional, flags } =
|
|
71
|
+
const { positional, flags } = backupArgs('restore', args, []);
|
|
61
72
|
if (flags.help || flags.h) return showCommandHelp('restore');
|
|
62
73
|
const file = positional[0];
|
|
63
74
|
if (!file) die('Usage: pmx-canvas restore <backup-file>');
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// Board commands
|
|
2
|
-
// Opening, switching and deleting boards is the human's, in the workbench.
|
|
1
|
+
// Board commands. Opening changes the shared workbench; deletion remains human-only.
|
|
3
2
|
|
|
4
3
|
import { cmd, die, getStringFlag, invokeOperation, output, parseFlags, showCommandHelp } from '../shared.js';
|
|
5
4
|
|
|
@@ -9,6 +8,20 @@ cmd('board list', 'List boards, most recently opened first', ['pmx-canvas board
|
|
|
9
8
|
output(await invokeOperation('board.list', {}));
|
|
10
9
|
});
|
|
11
10
|
|
|
11
|
+
cmd(
|
|
12
|
+
'board open',
|
|
13
|
+
'Open a board in the shared workbench, or return Home with --home',
|
|
14
|
+
['pmx-canvas board open <board-id>', 'pmx-canvas board open --home'],
|
|
15
|
+
async (args) => {
|
|
16
|
+
const { positional, flags } = parseFlags(args, { boolFlags: ['home'] });
|
|
17
|
+
if (flags.help || flags.h) return showCommandHelp('board open');
|
|
18
|
+
if (positional.length > 1 || (flags.home ? positional.length !== 0 : !positional[0])) {
|
|
19
|
+
die('Usage: pmx-canvas board open <board-id> | --home');
|
|
20
|
+
}
|
|
21
|
+
output(await invokeOperation('board.open', { id: flags.home ? null : positional[0] }));
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
12
25
|
cmd(
|
|
13
26
|
'board from',
|
|
14
27
|
'Preview or create a board from selected cards on another board',
|
|
@@ -35,7 +48,7 @@ cmd(
|
|
|
35
48
|
|
|
36
49
|
cmd(
|
|
37
50
|
'board create',
|
|
38
|
-
'Create an empty board (
|
|
51
|
+
'Create an empty board (use board open with its returned id before writing)',
|
|
39
52
|
['pmx-canvas board create "Q4 planning"', 'pmx-canvas board create "Q4 planning" --category Planning'],
|
|
40
53
|
async (args) => {
|
|
41
54
|
const { positional, flags } = parseFlags(args);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { copyFileSync } from 'node:fs';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
-
import { cmd, getStringFlag, invokeOperation, output, parseFlags, showCommandHelp } from '../shared.js';
|
|
5
|
+
import { cmd, die, getStringFlag, invokeOperation, output, parseFlags, showCommandHelp } from '../shared.js';
|
|
6
6
|
|
|
7
7
|
cmd(
|
|
8
8
|
'export',
|
|
@@ -11,15 +11,22 @@ cmd(
|
|
|
11
11
|
'pmx-canvas export',
|
|
12
12
|
'pmx-canvas export --board board-abc123 --out ~/Desktop/okrs.html',
|
|
13
13
|
'pmx-canvas export --include-files',
|
|
14
|
+
'pmx-canvas export --include-derived-text',
|
|
14
15
|
],
|
|
15
16
|
async (args) => {
|
|
16
|
-
const { flags } = parseFlags(args);
|
|
17
|
+
const { flags } = parseFlags(args, { boolFlags: ['include-files', 'include-derived-text'] });
|
|
18
|
+
const supportedFlags = new Set(['help', 'h', 'board', 'out', 'include-files', 'include-derived-text']);
|
|
19
|
+
const unknownFlag = Object.keys(flags).find((flag) => !supportedFlags.has(flag));
|
|
20
|
+
if (unknownFlag) {
|
|
21
|
+
die(`Unknown export flag: --${unknownFlag}`, 'Run: pmx-canvas export --help');
|
|
22
|
+
}
|
|
17
23
|
if (flags.help || flags.h) return showCommandHelp('export');
|
|
18
24
|
const board = getStringFlag(flags, 'board');
|
|
19
25
|
const out = getStringFlag(flags, 'out');
|
|
20
26
|
const result = (await invokeOperation('export.run', {
|
|
21
27
|
...(board ? { board } : {}),
|
|
22
28
|
includeFiles: flags['include-files'] === true,
|
|
29
|
+
includeDerivedText: flags['include-derived-text'] === true,
|
|
23
30
|
})) as { path: string };
|
|
24
31
|
if (out) {
|
|
25
32
|
copyFileSync(result.path, resolve(out));
|
package/src/cli/daemon.ts
CHANGED
|
@@ -49,14 +49,24 @@ export function isProcessRunning(pid: number): boolean {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Whether pid's command line contains `needle`. Returns null when
|
|
53
|
-
* answer (no such process
|
|
54
|
-
*
|
|
52
|
+
* Whether pid's command line contains `needle`. Returns null when the OS probe
|
|
53
|
+
* cannot answer (no such process or unavailable command). Stop must not treat
|
|
54
|
+
* an unknown identity as permission to signal a live process.
|
|
55
55
|
*/
|
|
56
56
|
export function processCommandMatches(pid: number, needle: string): boolean | null {
|
|
57
57
|
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
58
58
|
try {
|
|
59
|
-
const
|
|
59
|
+
const executable = process.platform === 'win32' ? 'powershell.exe' : 'ps';
|
|
60
|
+
const args =
|
|
61
|
+
process.platform === 'win32'
|
|
62
|
+
? [
|
|
63
|
+
'-NoProfile',
|
|
64
|
+
'-NonInteractive',
|
|
65
|
+
'-Command',
|
|
66
|
+
`(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`,
|
|
67
|
+
]
|
|
68
|
+
: ['-o', 'command=', '-p', String(pid)];
|
|
69
|
+
const command = execFileSync(executable, args, {
|
|
60
70
|
encoding: 'utf-8',
|
|
61
71
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
62
72
|
}).trim();
|
|
@@ -69,8 +79,8 @@ export function processCommandMatches(pid: number, needle: string): boolean | nu
|
|
|
69
79
|
|
|
70
80
|
/**
|
|
71
81
|
* Liveness with a PID-recycling guard: the pid must be alive AND its command
|
|
72
|
-
* line must still look like our daemon
|
|
73
|
-
*
|
|
82
|
+
* line must still look like our daemon. Status falls back to plain liveness
|
|
83
|
+
* when the command probe is unavailable; stop requires a verified match.
|
|
74
84
|
*/
|
|
75
85
|
export function isOwnDaemonProcess(pid: number, entryNeedle: string): boolean {
|
|
76
86
|
if (!isProcessRunning(pid)) return false;
|
|
@@ -439,7 +449,27 @@ export async function stopServeDaemon(
|
|
|
439
449
|
process.exit(1);
|
|
440
450
|
}
|
|
441
451
|
|
|
442
|
-
|
|
452
|
+
const commandMatches = processCommandMatches(pid, options.entry);
|
|
453
|
+
if (commandMatches !== true) {
|
|
454
|
+
if (isProcessRunning(pid)) {
|
|
455
|
+
outputJson({
|
|
456
|
+
ok: false,
|
|
457
|
+
daemon: true,
|
|
458
|
+
stopped: false,
|
|
459
|
+
running: responsive,
|
|
460
|
+
error:
|
|
461
|
+
commandMatches === false
|
|
462
|
+
? `PID ${pid} is live but does not belong to this installation. The pid file was preserved.`
|
|
463
|
+
: `PID ${pid} is live but its installation could not be verified. The pid file was preserved.`,
|
|
464
|
+
hint: 'Use the matching installation and workspace to stop the daemon; verify the process before removing its pid file.',
|
|
465
|
+
pid,
|
|
466
|
+
url,
|
|
467
|
+
healthUrl,
|
|
468
|
+
logFile: options.logFile,
|
|
469
|
+
pidFile: options.pidFile,
|
|
470
|
+
});
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
443
473
|
removePidFile(options.pidFile);
|
|
444
474
|
outputJson({
|
|
445
475
|
ok: true,
|
package/src/cli/shared.ts
CHANGED
|
@@ -133,7 +133,8 @@ async function invokeOperation(name: string, input: Record<string, unknown>): Pr
|
|
|
133
133
|
// ── Flag parsing ─────────────────────────────────────────────
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
|
-
* `options.boolFlags` marks flags that are boolean FOR THIS COMMAND
|
|
136
|
+
* `options.boolFlags` marks flags that are boolean FOR THIS COMMAND, while
|
|
137
|
+
* `options.valueFlags` overrides a globally boolean flag for one command. Value
|
|
137
138
|
* flags consume the next token verbatim (so a unified diff beginning with
|
|
138
139
|
* `--- a/file` survives), which makes a flag that is boolean here and
|
|
139
140
|
* value-taking elsewhere ambiguous — `--summary` is boolean in `history` /
|
|
@@ -144,7 +145,7 @@ async function invokeOperation(name: string, input: Record<string, unknown>): Pr
|
|
|
144
145
|
*/
|
|
145
146
|
function parseFlags(
|
|
146
147
|
args: string[],
|
|
147
|
-
options: { boolFlags?: readonly string[] } = {},
|
|
148
|
+
options: { boolFlags?: readonly string[]; valueFlags?: readonly string[] } = {},
|
|
148
149
|
): { positional: string[]; flags: Record<string, string | true> } {
|
|
149
150
|
const positional: string[] = [];
|
|
150
151
|
const flags: Record<string, string | true> = {};
|
|
@@ -187,6 +188,7 @@ function parseFlags(
|
|
|
187
188
|
'check',
|
|
188
189
|
'skip-mcp',
|
|
189
190
|
]);
|
|
191
|
+
for (const flag of options.valueFlags ?? []) BOOL_FLAGS.delete(flag);
|
|
190
192
|
for (let i = 0; i < args.length; i++) {
|
|
191
193
|
const arg = args[i];
|
|
192
194
|
// Conventional end-of-flags separator: the rest is positional, never flags.
|
|
@@ -847,6 +849,13 @@ function showCommandHelp(name: string): void {
|
|
|
847
849
|
console.log('\nAlias:');
|
|
848
850
|
console.log(' Equivalent to: pmx-canvas external-app add --kind excalidraw ...');
|
|
849
851
|
}
|
|
852
|
+
if (name === 'export') {
|
|
853
|
+
console.log('\nOptions:');
|
|
854
|
+
console.log(' --board <id> Export this board instead of the open board');
|
|
855
|
+
console.log(' --out <path> Copy the export to this path');
|
|
856
|
+
console.log(' --include-files Include local file contents (default: excluded)');
|
|
857
|
+
console.log(' --include-derived-text Include imported document text (default: excluded)');
|
|
858
|
+
}
|
|
850
859
|
console.log('');
|
|
851
860
|
}
|
|
852
861
|
|
package/src/client/App.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
closeAttentionHistory,
|
|
6
6
|
openAttentionHistory,
|
|
7
7
|
} from '../state/attention-store';
|
|
8
|
+
import { IconClose } from '../icons';
|
|
8
9
|
|
|
9
10
|
function formatTimestamp(timestamp: number): string {
|
|
10
11
|
return new Date(timestamp).toLocaleTimeString([], {
|
|
@@ -73,7 +74,7 @@ export function AttentionHistory() {
|
|
|
73
74
|
aria-label="Collapse changes panel"
|
|
74
75
|
title="Collapse"
|
|
75
76
|
>
|
|
76
|
-
|
|
77
|
+
<IconClose />
|
|
77
78
|
</button>
|
|
78
79
|
</div>
|
|
79
80
|
<div class="attention-history-list">
|
|
@@ -9,11 +9,13 @@ export function AttentionToast() {
|
|
|
9
9
|
type="button"
|
|
10
10
|
class={`attention-toast attention-tone-${toast.tone}`}
|
|
11
11
|
onClick={openAttentionHistory}
|
|
12
|
-
aria-label={`${toast.title} — open change history`}
|
|
13
|
-
title={toast.detail || toast.title}
|
|
12
|
+
aria-label={`${toast.title}${toast.detail ? `: ${toast.detail}` : ''} — open change history`}
|
|
14
13
|
>
|
|
15
14
|
<span class="attention-toast-dot" aria-hidden="true" />
|
|
16
|
-
<span class="attention-toast-
|
|
15
|
+
<span class="attention-toast-copy">
|
|
16
|
+
<span class="attention-toast-title">{toast.title}</span>
|
|
17
|
+
{toast.detail && <span class="attention-toast-detail">{toast.detail}</span>}
|
|
18
|
+
</span>
|
|
17
19
|
</button>
|
|
18
20
|
);
|
|
19
21
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { render, type VNode } from 'preact';
|
|
2
2
|
import { useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
3
3
|
import { activeBoard, activeBoardId, boardList, boardsLoaded, openBoard } from '../state/boards-store';
|
|
4
|
-
import { IconLogo, IconArrange } from '../icons';
|
|
4
|
+
import { IconLogo, IconArrange, IconChevronDown, IconPlus } from '../icons';
|
|
5
5
|
import { promptNewBoard } from './HomeView';
|
|
6
6
|
|
|
7
7
|
const RECENT_LIMIT = 8;
|
|
@@ -76,7 +76,7 @@ export function BoardSwitcher({ fallbackName }: { fallbackName: string }) {
|
|
|
76
76
|
>
|
|
77
77
|
<span class="board-switcher-name">{label}</span>
|
|
78
78
|
<span class="board-switcher-caret" aria-hidden="true">
|
|
79
|
-
|
|
79
|
+
<IconChevronDown />
|
|
80
80
|
</span>
|
|
81
81
|
</button>
|
|
82
82
|
{open && anchor && (
|
|
@@ -130,7 +130,7 @@ export function BoardSwitcher({ fallbackName }: { fallbackName: string }) {
|
|
|
130
130
|
}}
|
|
131
131
|
>
|
|
132
132
|
<span class="board-switcher-icon" aria-hidden="true">
|
|
133
|
-
|
|
133
|
+
<IconPlus size={18} />
|
|
134
134
|
</span>
|
|
135
135
|
New board…
|
|
136
136
|
</button>
|
|
@@ -38,7 +38,16 @@ import { KIND_COLOR } from './kind-colors';
|
|
|
38
38
|
import { reportHumanGrab, takeOverNode, yieldedNodes } from '../state/human-store';
|
|
39
39
|
import { AxStepControls } from '../nodes/AxStepControls';
|
|
40
40
|
import { canOpenAsSite, openNodeAsSite } from '../nodes/surface-url';
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
getNodeIcon,
|
|
43
|
+
IconChevronRight,
|
|
44
|
+
IconChevronDown,
|
|
45
|
+
IconClose,
|
|
46
|
+
IconMore,
|
|
47
|
+
IconExternalLink,
|
|
48
|
+
IconExpand,
|
|
49
|
+
IconSparkle,
|
|
50
|
+
} from '../icons';
|
|
42
51
|
import { EXPANDABLE_TYPES, TYPE_LABELS } from '../types';
|
|
43
52
|
import type { CanvasNodeState } from '../types';
|
|
44
53
|
import { AUTO_FIT_TITLEBAR_HEIGHT, computeAutoFitHeight, shouldAutoFitNode } from './auto-fit';
|
|
@@ -472,7 +481,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
472
481
|
toggleCollapsed(node.id);
|
|
473
482
|
}}
|
|
474
483
|
>
|
|
475
|
-
|
|
484
|
+
<IconChevronRight size={14} />
|
|
476
485
|
</button>
|
|
477
486
|
<div class="group-chip-text">
|
|
478
487
|
<div class="group-chip-name">{title}</div>
|
|
@@ -591,7 +600,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
591
600
|
setGroupMenuOpen((open) => !open);
|
|
592
601
|
}}
|
|
593
602
|
>
|
|
594
|
-
|
|
603
|
+
<IconMore size={14} />
|
|
595
604
|
</button>
|
|
596
605
|
{groupMenuOpen && (
|
|
597
606
|
<div class="group-menu" role="menu" onPointerDown={(e) => e.stopPropagation()}>
|
|
@@ -683,7 +692,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
683
692
|
}}
|
|
684
693
|
title={isContextPinned ? 'Remove from context' : 'Add to context'}
|
|
685
694
|
>
|
|
686
|
-
{
|
|
695
|
+
<IconSparkle size={14} />
|
|
687
696
|
</button>
|
|
688
697
|
{/* Open as site — full-page standalone view of this node's surface,
|
|
689
698
|
served from /api/canvas/surface/:id (same document as the canvas
|
|
@@ -698,7 +707,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
698
707
|
}}
|
|
699
708
|
title="Open as site"
|
|
700
709
|
>
|
|
701
|
-
|
|
710
|
+
<IconExternalLink size={14} />
|
|
702
711
|
</button>
|
|
703
712
|
)}
|
|
704
713
|
{/* Expand — opens node as full-viewport overlay for focused work */}
|
|
@@ -711,7 +720,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
711
720
|
}}
|
|
712
721
|
title="Expand (focus mode)"
|
|
713
722
|
>
|
|
714
|
-
|
|
723
|
+
<IconExpand size={14} />
|
|
715
724
|
</button>
|
|
716
725
|
)}
|
|
717
726
|
<button
|
|
@@ -722,7 +731,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
722
731
|
}}
|
|
723
732
|
title={node.collapsed ? 'Expand' : 'Collapse'}
|
|
724
733
|
>
|
|
725
|
-
{node.collapsed ?
|
|
734
|
+
{node.collapsed ? <IconChevronRight size={14} /> : <IconChevronDown size={14} />}
|
|
726
735
|
</button>
|
|
727
736
|
{/* Report #64: status nodes get the same remove control as every other
|
|
728
737
|
node type (backend removal + undo/history handle status uniformly). */}
|
|
@@ -730,12 +739,11 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
730
739
|
type="button"
|
|
731
740
|
onClick={(e) => {
|
|
732
741
|
e.stopPropagation();
|
|
733
|
-
removeNode(node.id);
|
|
734
742
|
void removeNodeFromClient(node.id);
|
|
735
743
|
}}
|
|
736
744
|
title="Close"
|
|
737
745
|
>
|
|
738
|
-
|
|
746
|
+
<IconClose size={14} />
|
|
739
747
|
</button>
|
|
740
748
|
</div>
|
|
741
749
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'preact/hooks';
|
|
2
|
-
import { IconSteer } from '../icons';
|
|
2
|
+
import { IconSteer, IconClose } from '../icons';
|
|
3
3
|
import { contextPinnedNodeIds, nodes, toggleContextPin } from '../state/canvas-store';
|
|
4
4
|
import { steerableAgents } from '../state/presence-store';
|
|
5
5
|
import { sendSteering } from '../state/session-store';
|
|
@@ -72,7 +72,7 @@ export function CommandBar() {
|
|
|
72
72
|
title="Remove from context"
|
|
73
73
|
onClick={() => toggleContextPin(id)}
|
|
74
74
|
>
|
|
75
|
-
|
|
75
|
+
<IconClose size={14} />
|
|
76
76
|
</button>
|
|
77
77
|
</span>
|
|
78
78
|
);
|
|
@@ -669,7 +669,6 @@ function buildNodeMenuItems(node: CanvasNodeState): MenuItem[] {
|
|
|
669
669
|
label: node.type === 'group' ? 'Delete frame (children stay)' : 'Delete',
|
|
670
670
|
danger: true,
|
|
671
671
|
action: () => {
|
|
672
|
-
removeNode(node.id);
|
|
673
672
|
void removeNodeFromClient(node.id);
|
|
674
673
|
},
|
|
675
674
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { clearContextPins, contextPinnedNodeIds } from '../state/canvas-store';
|
|
2
2
|
import { attentionHistoryOpen } from '../state/attention-store';
|
|
3
|
+
import { IconClose } from '../icons';
|
|
3
4
|
|
|
4
5
|
export function ContextPinBar() {
|
|
5
6
|
const count = contextPinnedNodeIds.value.size;
|
|
@@ -15,8 +16,9 @@ export function ContextPinBar() {
|
|
|
15
16
|
class="context-pin-bar-btn context-pin-bar-clear"
|
|
16
17
|
onClick={clearContextPins}
|
|
17
18
|
title="Clear all context pins"
|
|
19
|
+
aria-label="Clear all context pins"
|
|
18
20
|
>
|
|
19
|
-
{
|
|
21
|
+
<IconClose size={14} />
|
|
20
22
|
</button>
|
|
21
23
|
</div>
|
|
22
24
|
);
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
pendingExpandedNodeCloseId,
|
|
26
26
|
toggleContextPin,
|
|
27
27
|
} from '../state/canvas-store';
|
|
28
|
-
import { getNodeIcon } from '../icons';
|
|
28
|
+
import { getNodeIcon, IconClose, IconExternalLink } from '../icons';
|
|
29
29
|
import { useFocusTrap } from './use-focus-trap';
|
|
30
30
|
import { TYPE_LABELS } from '../types';
|
|
31
31
|
import type { CanvasNodeState } from '../types';
|
|
@@ -316,7 +316,7 @@ export function ExpandedNodeOverlay() {
|
|
|
316
316
|
onClick={() => void openNodeAsSite(node)}
|
|
317
317
|
title="Open as a full-page site in the system browser"
|
|
318
318
|
>
|
|
319
|
-
Open in tab
|
|
319
|
+
Open in tab <IconExternalLink size={14} />
|
|
320
320
|
</button>
|
|
321
321
|
)}
|
|
322
322
|
{canPresent && (
|
|
@@ -331,7 +331,7 @@ export function ExpandedNodeOverlay() {
|
|
|
331
331
|
)}
|
|
332
332
|
</div>
|
|
333
333
|
<button type="button" class="expanded-close" onClick={handleClose} title="Close (Esc)" aria-label="Close">
|
|
334
|
-
|
|
334
|
+
<IconClose />
|
|
335
335
|
</button>
|
|
336
336
|
</div>
|
|
337
337
|
|
|
@@ -177,7 +177,9 @@ export function ExportDialog() {
|
|
|
177
177
|
<a href={result.url} target="_blank" rel="noopener noreferrer">
|
|
178
178
|
Open
|
|
179
179
|
</a>
|
|
180
|
-
<a href={`${result.url}?download=1`}>
|
|
180
|
+
<a class="is-primary" href={`${result.url}?download=1`}>
|
|
181
|
+
Download
|
|
182
|
+
</a>
|
|
181
183
|
</div>
|
|
182
184
|
</div>
|
|
183
185
|
)}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import { IconClose, IconExternalLink } from '../icons';
|
|
2
3
|
import type { AgentPresence } from '../../shared/agent-presence.js';
|
|
3
4
|
import { vetoGhostIntent } from '../state/intent-bridge';
|
|
4
5
|
import { type ClientIntent, intents, removeIntent } from '../state/intent-store';
|
|
@@ -149,7 +150,7 @@ export function ActivityFeed() {
|
|
|
149
150
|
activityFeedOpen.value = false;
|
|
150
151
|
}}
|
|
151
152
|
>
|
|
152
|
-
|
|
153
|
+
<IconClose />
|
|
153
154
|
</button>
|
|
154
155
|
</div>
|
|
155
156
|
<div class="activity-filters">
|
|
@@ -226,7 +227,7 @@ export function ActivityFeed() {
|
|
|
226
227
|
void startSession();
|
|
227
228
|
}}
|
|
228
229
|
>
|
|
229
|
-
Start session
|
|
230
|
+
Start session <IconExternalLink size={14} />
|
|
230
231
|
</button>
|
|
231
232
|
</div>
|
|
232
233
|
</div>
|
|
@@ -267,7 +268,7 @@ export function WritersSheet() {
|
|
|
267
268
|
<span class="writers-title">Connected writers</span>
|
|
268
269
|
<span class="writers-board">{sessionId.value.slice(0, 12)}</span>
|
|
269
270
|
<button type="button" class="activity-feed-close" aria-label="Close writers" onClick={close}>
|
|
270
|
-
|
|
271
|
+
<IconClose />
|
|
271
272
|
</button>
|
|
272
273
|
</div>
|
|
273
274
|
<div class="writers-body">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { useFocusTrap } from './use-focus-trap';
|
|
3
|
+
import { IconExternalLink } from '../icons';
|
|
3
4
|
|
|
4
5
|
/** Draft only: GitHub owns authentication and the final public submission. */
|
|
5
6
|
export function FeedbackDialog({ onClose }: { onClose: () => void }) {
|
|
@@ -77,7 +78,7 @@ export function FeedbackDialog({ onClose }: { onClose: () => void }) {
|
|
|
77
78
|
Cancel
|
|
78
79
|
</button>
|
|
79
80
|
<button type="submit" class="text-prompt-confirm" disabled={!title.trim() || !description.trim()}>
|
|
80
|
-
Continue on GitHub
|
|
81
|
+
Continue on GitHub <IconExternalLink size={14} />
|
|
81
82
|
</button>
|
|
82
83
|
</div>
|
|
83
84
|
</form>
|