pmx-canvas 0.2.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +0 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +35 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +5 -2
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +12 -7
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +58 -56
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +40 -121
- package/src/mcp/server.ts +206 -271
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +185 -41
- package/src/server/ax-state.ts +142 -47
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +500 -102
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +155 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +20 -7
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +4 -20
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +321 -218
- package/src/server/intent-registry.ts +3 -6
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +38 -40
- package/src/server/operations/http.ts +5 -5
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +29 -36
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +102 -77
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +300 -111
- package/src/server/operations/ops/query.ts +62 -33
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +121 -1012
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/ax-intent.ts +1 -1
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
package/src/cli/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { showServeStatus, startDaemonMode, stopServeDaemon } from './daemon.js';
|
|
6
7
|
import { runAgentCli } from './agent.js';
|
|
7
8
|
import { createCanvas } from '../server/index.js';
|
|
8
9
|
import { seedDemoCanvas } from '../server/demo.js';
|
|
@@ -31,9 +32,39 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
31
32
|
// ── Agent CLI subcommands ────────────────────────────────────
|
|
32
33
|
// If first arg is a known subcommand (not a --flag), route to the agent CLI.
|
|
33
34
|
const AGENT_COMMANDS = new Set([
|
|
34
|
-
'node',
|
|
35
|
-
'
|
|
36
|
-
'
|
|
35
|
+
'node',
|
|
36
|
+
'edge',
|
|
37
|
+
'json-render',
|
|
38
|
+
'search',
|
|
39
|
+
'layout',
|
|
40
|
+
'status',
|
|
41
|
+
'arrange',
|
|
42
|
+
'focus',
|
|
43
|
+
'fit',
|
|
44
|
+
'screenshot',
|
|
45
|
+
'pin',
|
|
46
|
+
'ax',
|
|
47
|
+
'undo',
|
|
48
|
+
'redo',
|
|
49
|
+
'history',
|
|
50
|
+
'snapshot',
|
|
51
|
+
'diff',
|
|
52
|
+
'group',
|
|
53
|
+
'webview',
|
|
54
|
+
'open',
|
|
55
|
+
'clear',
|
|
56
|
+
'code-graph',
|
|
57
|
+
'spatial',
|
|
58
|
+
'watch',
|
|
59
|
+
'web-artifact',
|
|
60
|
+
'external-app',
|
|
61
|
+
'diagram',
|
|
62
|
+
'graph',
|
|
63
|
+
'html',
|
|
64
|
+
'batch',
|
|
65
|
+
'validate',
|
|
66
|
+
'serve',
|
|
67
|
+
'copilot',
|
|
37
68
|
]);
|
|
38
69
|
|
|
39
70
|
const firstArg = args[0] ?? '';
|
|
@@ -66,7 +97,10 @@ function readNumberOption(name: string): number | undefined {
|
|
|
66
97
|
function readCsvOption(name: string): string[] | undefined {
|
|
67
98
|
const raw = readOption(name);
|
|
68
99
|
if (!raw) return undefined;
|
|
69
|
-
const values = raw
|
|
100
|
+
const values = raw
|
|
101
|
+
.split(',')
|
|
102
|
+
.map((value) => value.trim())
|
|
103
|
+
.filter((value) => value.length > 0);
|
|
70
104
|
return values.length > 0 ? values : undefined;
|
|
71
105
|
}
|
|
72
106
|
|
|
@@ -86,306 +120,6 @@ function stripOption(argv: string[], name: string): string[] {
|
|
|
86
120
|
return stripped;
|
|
87
121
|
}
|
|
88
122
|
|
|
89
|
-
function outputJson(data: unknown): void {
|
|
90
|
-
console.log(JSON.stringify(data, null, 2));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function readPidFile(path: string): number | null {
|
|
94
|
-
try {
|
|
95
|
-
if (!existsSync(path)) return null;
|
|
96
|
-
const raw = readFileSync(path, 'utf-8').trim();
|
|
97
|
-
if (!raw) return null;
|
|
98
|
-
const pid = Number(raw);
|
|
99
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
100
|
-
} catch {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function isProcessRunning(pid: number): boolean {
|
|
106
|
-
try {
|
|
107
|
-
process.kill(pid, 0);
|
|
108
|
-
return true;
|
|
109
|
-
} catch (error) {
|
|
110
|
-
if (error && typeof error === 'object' && 'code' in error) {
|
|
111
|
-
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
112
|
-
}
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function removePidFile(path: string): void {
|
|
118
|
-
try {
|
|
119
|
-
rmSync(path, { force: true });
|
|
120
|
-
} catch {
|
|
121
|
-
// Ignore cleanup failures for stale pid files.
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
interface HealthStatus {
|
|
126
|
-
responsive: boolean;
|
|
127
|
-
workspace: string | null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function readHealthStatus(url: string): Promise<HealthStatus> {
|
|
131
|
-
try {
|
|
132
|
-
const response = await fetch(url);
|
|
133
|
-
if (!response.ok) return { responsive: false, workspace: null };
|
|
134
|
-
const payload = await response.json().catch(() => null) as unknown;
|
|
135
|
-
const workspace = payload && typeof payload === 'object' && 'workspace' in payload
|
|
136
|
-
&& typeof payload.workspace === 'string'
|
|
137
|
-
? payload.workspace
|
|
138
|
-
: null;
|
|
139
|
-
return { responsive: true, workspace };
|
|
140
|
-
} catch {
|
|
141
|
-
return { responsive: false, workspace: null };
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function isHealthy(url: string): Promise<boolean> {
|
|
146
|
-
return (await readHealthStatus(url)).responsive;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function readLogTail(path: string, maxLines = 20): string | null {
|
|
150
|
-
try {
|
|
151
|
-
if (!existsSync(path)) return null;
|
|
152
|
-
const lines = readFileSync(path, 'utf-8').trim().split('\n');
|
|
153
|
-
return lines.slice(-maxLines).join('\n') || null;
|
|
154
|
-
} catch {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function waitForHealth(
|
|
160
|
-
healthUrl: string,
|
|
161
|
-
timeoutMs: number,
|
|
162
|
-
getExitMessage: () => string | null,
|
|
163
|
-
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
164
|
-
const deadline = Date.now() + timeoutMs;
|
|
165
|
-
while (Date.now() < deadline) {
|
|
166
|
-
if (await isHealthy(healthUrl)) {
|
|
167
|
-
return { ok: true };
|
|
168
|
-
}
|
|
169
|
-
const exitMessage = getExitMessage();
|
|
170
|
-
if (exitMessage) {
|
|
171
|
-
return { ok: false, reason: exitMessage };
|
|
172
|
-
}
|
|
173
|
-
await Bun.sleep(250);
|
|
174
|
-
}
|
|
175
|
-
return { ok: false, reason: `Timed out waiting for ${healthUrl}` };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function waitForShutdown(
|
|
179
|
-
healthUrl: string,
|
|
180
|
-
timeoutMs: number,
|
|
181
|
-
pid: number | null,
|
|
182
|
-
): Promise<boolean> {
|
|
183
|
-
const deadline = Date.now() + timeoutMs;
|
|
184
|
-
while (Date.now() < deadline) {
|
|
185
|
-
const responsive = await isHealthy(healthUrl);
|
|
186
|
-
const alive = pid ? isProcessRunning(pid) : false;
|
|
187
|
-
if (!responsive && !alive) {
|
|
188
|
-
return true;
|
|
189
|
-
}
|
|
190
|
-
await Bun.sleep(250);
|
|
191
|
-
}
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function startDaemonMode(options: {
|
|
196
|
-
port: number;
|
|
197
|
-
baseArgs: string[];
|
|
198
|
-
logFile: string;
|
|
199
|
-
pidFile: string;
|
|
200
|
-
waitMs: number;
|
|
201
|
-
}): Promise<void> {
|
|
202
|
-
const healthUrl = `http://localhost:${options.port}/health`;
|
|
203
|
-
const workbenchUrl = `http://localhost:${options.port}/workbench`;
|
|
204
|
-
const existingPid = readPidFile(options.pidFile);
|
|
205
|
-
|
|
206
|
-
if (await isHealthy(healthUrl)) {
|
|
207
|
-
outputJson({
|
|
208
|
-
ok: true,
|
|
209
|
-
daemon: true,
|
|
210
|
-
alreadyRunning: true,
|
|
211
|
-
pid: existingPid,
|
|
212
|
-
url: workbenchUrl,
|
|
213
|
-
healthUrl,
|
|
214
|
-
logFile: options.logFile,
|
|
215
|
-
pidFile: options.pidFile,
|
|
216
|
-
});
|
|
217
|
-
process.exit(0);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
mkdirSync(dirname(options.logFile), { recursive: true });
|
|
221
|
-
const logFd = openSync(options.logFile, 'a');
|
|
222
|
-
const childArgs = options.baseArgs.includes('--no-open')
|
|
223
|
-
? options.baseArgs
|
|
224
|
-
: [...options.baseArgs, '--no-open'];
|
|
225
|
-
const child = spawn(process.execPath, ['run', fileURLToPath(import.meta.url), ...childArgs], {
|
|
226
|
-
cwd: process.cwd(),
|
|
227
|
-
detached: true,
|
|
228
|
-
env: process.env,
|
|
229
|
-
stdio: ['ignore', logFd, logFd],
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
let exitMessage: string | null = null;
|
|
233
|
-
child.once('exit', (code, signal) => {
|
|
234
|
-
exitMessage = signal
|
|
235
|
-
? `Daemon exited via signal ${signal}`
|
|
236
|
-
: `Daemon exited with code ${code ?? 'unknown'}`;
|
|
237
|
-
});
|
|
238
|
-
child.unref();
|
|
239
|
-
|
|
240
|
-
const health = await waitForHealth(healthUrl, options.waitMs, () => exitMessage);
|
|
241
|
-
if (!health.ok) {
|
|
242
|
-
const logTail = readLogTail(options.logFile);
|
|
243
|
-
const details = logTail ? `${health.reason}\n\nRecent log output:\n${logTail}` : health.reason;
|
|
244
|
-
console.error(details);
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
mkdirSync(dirname(options.pidFile), { recursive: true });
|
|
249
|
-
writeFileSync(options.pidFile, `${child.pid}\n`, 'utf-8');
|
|
250
|
-
|
|
251
|
-
outputJson({
|
|
252
|
-
ok: true,
|
|
253
|
-
daemon: true,
|
|
254
|
-
pid: child.pid,
|
|
255
|
-
url: workbenchUrl,
|
|
256
|
-
healthUrl,
|
|
257
|
-
logFile: options.logFile,
|
|
258
|
-
pidFile: options.pidFile,
|
|
259
|
-
});
|
|
260
|
-
process.exit(0);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function showServeStatus(options: {
|
|
264
|
-
port: number;
|
|
265
|
-
logFile: string;
|
|
266
|
-
pidFile: string;
|
|
267
|
-
}): Promise<void> {
|
|
268
|
-
const healthUrl = `http://localhost:${options.port}/health`;
|
|
269
|
-
const url = `http://localhost:${options.port}/workbench`;
|
|
270
|
-
const pid = readPidFile(options.pidFile);
|
|
271
|
-
const pidRunning = pid ? isProcessRunning(pid) : false;
|
|
272
|
-
const health = await readHealthStatus(healthUrl);
|
|
273
|
-
const responsive = health.responsive;
|
|
274
|
-
const running = responsive || pidRunning;
|
|
275
|
-
if (!running && existsSync(options.pidFile) && !pidRunning) {
|
|
276
|
-
removePidFile(options.pidFile);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
outputJson({
|
|
280
|
-
ok: true,
|
|
281
|
-
daemon: true,
|
|
282
|
-
running,
|
|
283
|
-
responsive,
|
|
284
|
-
workspace: health.workspace,
|
|
285
|
-
pid,
|
|
286
|
-
pidRunning,
|
|
287
|
-
url,
|
|
288
|
-
healthUrl,
|
|
289
|
-
logFile: options.logFile,
|
|
290
|
-
pidFile: options.pidFile,
|
|
291
|
-
pidFileExists: existsSync(options.pidFile),
|
|
292
|
-
});
|
|
293
|
-
process.exit(0);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function stopServeDaemon(options: {
|
|
297
|
-
port: number;
|
|
298
|
-
logFile: string;
|
|
299
|
-
pidFile: string;
|
|
300
|
-
waitMs: number;
|
|
301
|
-
}): Promise<void> {
|
|
302
|
-
const healthUrl = `http://localhost:${options.port}/health`;
|
|
303
|
-
const url = `http://localhost:${options.port}/workbench`;
|
|
304
|
-
const pid = readPidFile(options.pidFile);
|
|
305
|
-
const responsive = await isHealthy(healthUrl);
|
|
306
|
-
|
|
307
|
-
if (!pid) {
|
|
308
|
-
if (!responsive) {
|
|
309
|
-
removePidFile(options.pidFile);
|
|
310
|
-
outputJson({
|
|
311
|
-
ok: true,
|
|
312
|
-
daemon: true,
|
|
313
|
-
stopped: false,
|
|
314
|
-
running: false,
|
|
315
|
-
reason: 'No running daemon found.',
|
|
316
|
-
url,
|
|
317
|
-
healthUrl,
|
|
318
|
-
logFile: options.logFile,
|
|
319
|
-
pidFile: options.pidFile,
|
|
320
|
-
});
|
|
321
|
-
process.exit(0);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
outputJson({
|
|
325
|
-
ok: false,
|
|
326
|
-
daemon: true,
|
|
327
|
-
error: `Server on port ${options.port} is responsive, but no pid file was found at ${options.pidFile}.`,
|
|
328
|
-
hint: 'Restart with `pmx-canvas serve --daemon` or provide the correct --pid-file.',
|
|
329
|
-
url,
|
|
330
|
-
healthUrl,
|
|
331
|
-
logFile: options.logFile,
|
|
332
|
-
pidFile: options.pidFile,
|
|
333
|
-
});
|
|
334
|
-
process.exit(1);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (!isProcessRunning(pid)) {
|
|
338
|
-
removePidFile(options.pidFile);
|
|
339
|
-
outputJson({
|
|
340
|
-
ok: true,
|
|
341
|
-
daemon: true,
|
|
342
|
-
stopped: false,
|
|
343
|
-
running: responsive,
|
|
344
|
-
reason: `Removed stale pid file for ${pid}.`,
|
|
345
|
-
pid,
|
|
346
|
-
url,
|
|
347
|
-
healthUrl,
|
|
348
|
-
logFile: options.logFile,
|
|
349
|
-
pidFile: options.pidFile,
|
|
350
|
-
});
|
|
351
|
-
process.exit(0);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
process.kill(pid, 'SIGTERM');
|
|
355
|
-
const stopped = await waitForShutdown(healthUrl, options.waitMs, pid);
|
|
356
|
-
const stillResponsive = await isHealthy(healthUrl);
|
|
357
|
-
const pidRunning = isProcessRunning(pid);
|
|
358
|
-
if (stopped || (!stillResponsive && !pidRunning)) {
|
|
359
|
-
removePidFile(options.pidFile);
|
|
360
|
-
outputJson({
|
|
361
|
-
ok: true,
|
|
362
|
-
daemon: true,
|
|
363
|
-
stopped: true,
|
|
364
|
-
pid,
|
|
365
|
-
url,
|
|
366
|
-
healthUrl,
|
|
367
|
-
logFile: options.logFile,
|
|
368
|
-
pidFile: options.pidFile,
|
|
369
|
-
});
|
|
370
|
-
process.exit(0);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
outputJson({
|
|
374
|
-
ok: false,
|
|
375
|
-
daemon: true,
|
|
376
|
-
stopped: false,
|
|
377
|
-
error: `Timed out waiting for daemon ${pid} to stop.`,
|
|
378
|
-
pid,
|
|
379
|
-
responsive: stillResponsive,
|
|
380
|
-
pidRunning,
|
|
381
|
-
url,
|
|
382
|
-
healthUrl,
|
|
383
|
-
logFile: options.logFile,
|
|
384
|
-
pidFile: options.pidFile,
|
|
385
|
-
});
|
|
386
|
-
process.exit(1);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
123
|
function runMcpServerProcess(): Promise<void> {
|
|
390
124
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
391
125
|
const child = spawn(process.execPath, ['run', mcpServerEntry], {
|
|
@@ -398,16 +132,16 @@ function runMcpServerProcess(): Promise<void> {
|
|
|
398
132
|
resolvePromise();
|
|
399
133
|
return;
|
|
400
134
|
}
|
|
401
|
-
rejectPromise(
|
|
402
|
-
|
|
403
|
-
? `MCP server exited via signal ${signal}`
|
|
404
|
-
|
|
405
|
-
)
|
|
135
|
+
rejectPromise(
|
|
136
|
+
new Error(
|
|
137
|
+
signal ? `MCP server exited via signal ${signal}` : `MCP server exited with code ${code ?? 'unknown'}`,
|
|
138
|
+
),
|
|
139
|
+
);
|
|
406
140
|
});
|
|
407
141
|
});
|
|
408
142
|
}
|
|
409
143
|
|
|
410
|
-
const serveSubcommand = firstArg === 'serve' ? args[1] ?? '' : '';
|
|
144
|
+
const serveSubcommand = firstArg === 'serve' ? (args[1] ?? '') : '';
|
|
411
145
|
|
|
412
146
|
if (firstArg === 'serve' && (serveSubcommand === 'status' || serveSubcommand === 'stop')) {
|
|
413
147
|
const port = parseInt(readOption('port') ?? process.env.PMX_WEB_CANVAS_PORT ?? '4313');
|
|
@@ -430,6 +164,7 @@ Usage:
|
|
|
430
164
|
port,
|
|
431
165
|
logFile: daemonLogFile,
|
|
432
166
|
pidFile: daemonPidFile,
|
|
167
|
+
entry: fileURLToPath(import.meta.url),
|
|
433
168
|
});
|
|
434
169
|
} else {
|
|
435
170
|
await stopServeDaemon({
|
|
@@ -437,6 +172,7 @@ Usage:
|
|
|
437
172
|
logFile: daemonLogFile,
|
|
438
173
|
pidFile: daemonPidFile,
|
|
439
174
|
waitMs: daemonWaitMs,
|
|
175
|
+
entry: fileURLToPath(import.meta.url),
|
|
440
176
|
});
|
|
441
177
|
}
|
|
442
178
|
}
|
|
@@ -468,9 +204,7 @@ if (AGENT_COMMANDS.has(firstArg) && firstArg !== 'serve') {
|
|
|
468
204
|
const daemonPidFile = resolve(readOption('pid-file') ?? `.pmx-canvas/daemon-${port}.pid`);
|
|
469
205
|
const daemonWaitMs = readNumberOption('wait-ms') ?? 10_000;
|
|
470
206
|
const webviewBackendOption: 'chrome' | 'webkit' | undefined =
|
|
471
|
-
webviewBackend === 'chrome' || webviewBackend === 'webkit'
|
|
472
|
-
? webviewBackend
|
|
473
|
-
: undefined;
|
|
207
|
+
webviewBackend === 'chrome' || webviewBackend === 'webkit' ? webviewBackend : undefined;
|
|
474
208
|
if (themeArg && ['dark', 'light', 'high-contrast'].includes(themeArg)) {
|
|
475
209
|
process.env.PMX_CANVAS_THEME = themeArg;
|
|
476
210
|
}
|
|
@@ -587,28 +321,31 @@ Examples:
|
|
|
587
321
|
}
|
|
588
322
|
|
|
589
323
|
if (daemon) {
|
|
590
|
-
const baseArgs = stripOption(
|
|
324
|
+
const baseArgs = stripOption(
|
|
325
|
+
stripOption(stripOption(stripOption(args, 'daemon'), 'log-file'), 'pid-file'),
|
|
326
|
+
'wait-ms',
|
|
327
|
+
);
|
|
591
328
|
await startDaemonMode({
|
|
592
329
|
port,
|
|
593
330
|
baseArgs,
|
|
594
331
|
logFile: daemonLogFile,
|
|
595
332
|
pidFile: daemonPidFile,
|
|
596
333
|
waitMs: daemonWaitMs,
|
|
334
|
+
entry: fileURLToPath(import.meta.url),
|
|
597
335
|
});
|
|
598
336
|
}
|
|
599
337
|
|
|
600
338
|
const canvas = createCanvas({ port });
|
|
601
|
-
const automationWebView =
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
: false;
|
|
339
|
+
const automationWebView = webviewAutomation
|
|
340
|
+
? {
|
|
341
|
+
...(webviewBackendOption ? { backend: webviewBackendOption } : {}),
|
|
342
|
+
...(webviewChromePath ? { chromePath: webviewChromePath } : {}),
|
|
343
|
+
...(webviewChromeArgv ? { chromeArgv: webviewChromeArgv } : {}),
|
|
344
|
+
...(webviewDataDir ? { dataStoreDir: webviewDataDir } : {}),
|
|
345
|
+
...(webviewWidth !== undefined ? { width: webviewWidth } : {}),
|
|
346
|
+
...(webviewHeight !== undefined ? { height: webviewHeight } : {}),
|
|
347
|
+
}
|
|
348
|
+
: false;
|
|
612
349
|
try {
|
|
613
350
|
await canvas.start({ open: !noOpen, automationWebView });
|
|
614
351
|
} catch (error) {
|
package/src/cli/watch.ts
CHANGED
|
@@ -7,16 +7,8 @@ import {
|
|
|
7
7
|
type SseMessage,
|
|
8
8
|
} from '../shared/semantic-attention.js';
|
|
9
9
|
|
|
10
|
-
export {
|
|
11
|
-
|
|
12
|
-
formatCompactWatchEvent,
|
|
13
|
-
SemanticWatchReducer,
|
|
14
|
-
};
|
|
15
|
-
export type {
|
|
16
|
-
SemanticWatchEvent,
|
|
17
|
-
SemanticWatchEventType,
|
|
18
|
-
SseMessage,
|
|
19
|
-
};
|
|
10
|
+
export { ALL_SEMANTIC_WATCH_EVENT_TYPES, formatCompactWatchEvent, SemanticWatchReducer };
|
|
11
|
+
export type { SemanticWatchEvent, SemanticWatchEventType, SseMessage };
|
|
20
12
|
|
|
21
13
|
export function parseSemanticEventFilter(raw: string | undefined): Set<SemanticWatchEventType> {
|
|
22
14
|
const all = new Set<SemanticWatchEventType>(ALL_SEMANTIC_WATCH_EVENT_TYPES);
|
package/src/client/App.tsx
CHANGED
|
@@ -165,7 +165,11 @@ function Toolbar({
|
|
|
165
165
|
|
|
166
166
|
<div class="separator" />
|
|
167
167
|
|
|
168
|
-
<ToolbarHint
|
|
168
|
+
<ToolbarHint
|
|
169
|
+
label="Canvas status"
|
|
170
|
+
detail={hasSynced ? statusLabel : 'Syncing canvas from server'}
|
|
171
|
+
align="start"
|
|
172
|
+
>
|
|
169
173
|
<span class={`connection-dot ${status}`} aria-label={`Canvas status: ${statusTitle}`} />
|
|
170
174
|
</ToolbarHint>
|
|
171
175
|
<span class="hud-collapsible-text" style={{ fontSize: '11px', color: 'var(--c-muted)' }}>
|
|
@@ -175,20 +179,12 @@ function Toolbar({
|
|
|
175
179
|
<div class="separator" />
|
|
176
180
|
|
|
177
181
|
<ToolbarHint label="Fit canvas" detail="Frame every node on screen">
|
|
178
|
-
<button
|
|
179
|
-
type="button"
|
|
180
|
-
onClick={() => fitAll(window.innerWidth, window.innerHeight)}
|
|
181
|
-
aria-label="Fit canvas"
|
|
182
|
-
>
|
|
182
|
+
<button type="button" onClick={() => fitAll(window.innerWidth, window.innerHeight)} aria-label="Fit canvas">
|
|
183
183
|
<IconFitAll />
|
|
184
184
|
</button>
|
|
185
185
|
</ToolbarHint>
|
|
186
186
|
<ToolbarHint label="Reset view" shortcut={`${MOD_KEY}+0`}>
|
|
187
|
-
<button
|
|
188
|
-
type="button"
|
|
189
|
-
onClick={() => animateViewport({ x: 0, y: 0, scale: 1 }, 250)}
|
|
190
|
-
aria-label="Reset view"
|
|
191
|
-
>
|
|
187
|
+
<button type="button" onClick={() => animateViewport({ x: 0, y: 0, scale: 1 }, 250)} aria-label="Reset view">
|
|
192
188
|
<IconResetView />
|
|
193
189
|
</button>
|
|
194
190
|
</ToolbarHint>
|
|
@@ -210,7 +206,10 @@ function Toolbar({
|
|
|
210
206
|
<IconZoomOut />
|
|
211
207
|
</button>
|
|
212
208
|
</ToolbarHint>
|
|
213
|
-
<span
|
|
209
|
+
<span
|
|
210
|
+
class="hud-collapsible-text"
|
|
211
|
+
style={{ fontSize: '10px', color: 'var(--c-dim)', minWidth: '36px', textAlign: 'center' }}
|
|
212
|
+
>
|
|
214
213
|
{Math.round(v.scale * 100)}%
|
|
215
214
|
</span>
|
|
216
215
|
|
|
@@ -220,7 +219,11 @@ function Toolbar({
|
|
|
220
219
|
label="Arrange layout"
|
|
221
220
|
detail={edgeCount > 0 ? 'Graph-aware layout for connected nodes' : 'Grid layout for loose nodes'}
|
|
222
221
|
>
|
|
223
|
-
<button
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
onClick={() => (edgeCount > 0 ? forceDirectedArrange() : autoArrange())}
|
|
225
|
+
aria-label="Arrange layout"
|
|
226
|
+
>
|
|
224
227
|
<IconArrange />
|
|
225
228
|
</button>
|
|
226
229
|
</ToolbarHint>
|
|
@@ -234,7 +237,10 @@ function Toolbar({
|
|
|
234
237
|
<IconMinimap />
|
|
235
238
|
</button>
|
|
236
239
|
</ToolbarHint>
|
|
237
|
-
<ToolbarHint
|
|
240
|
+
<ToolbarHint
|
|
241
|
+
label={`Switch to ${canvasTheme.value === 'dark' ? 'light' : 'dark'} theme`}
|
|
242
|
+
detail={`Current theme: ${canvasTheme.value}`}
|
|
243
|
+
>
|
|
238
244
|
<button
|
|
239
245
|
type="button"
|
|
240
246
|
onClick={() => {
|
|
@@ -280,13 +286,13 @@ function Toolbar({
|
|
|
280
286
|
{(isTraceOn || traceNodeCount > 0) && (
|
|
281
287
|
<ToolbarHint
|
|
282
288
|
label="Clear trace"
|
|
283
|
-
detail={
|
|
289
|
+
detail={
|
|
290
|
+
traceNodeCount > 0
|
|
291
|
+
? `Remove ${traceNodeCount} trace node${traceNodeCount === 1 ? '' : 's'}`
|
|
292
|
+
: 'Trace is enabled but still empty'
|
|
293
|
+
}
|
|
284
294
|
>
|
|
285
|
-
<button
|
|
286
|
-
type="button"
|
|
287
|
-
onClick={() => sendIntent('trace-clear')}
|
|
288
|
-
aria-label="Clear trace"
|
|
289
|
-
>
|
|
295
|
+
<button type="button" onClick={() => sendIntent('trace-clear')} aria-label="Clear trace">
|
|
290
296
|
<IconClearTrace />
|
|
291
297
|
</button>
|
|
292
298
|
</ToolbarHint>
|
|
@@ -340,25 +346,19 @@ function Toolbar({
|
|
|
340
346
|
<div class="separator" />
|
|
341
347
|
|
|
342
348
|
<ToolbarHint label="Search nodes and actions" shortcut={`${MOD_KEY}+K`}>
|
|
343
|
-
<button
|
|
344
|
-
type="button"
|
|
345
|
-
onClick={onOpenPalette}
|
|
346
|
-
aria-label="Search nodes and actions"
|
|
347
|
-
>
|
|
349
|
+
<button type="button" onClick={onOpenPalette} aria-label="Search nodes and actions">
|
|
348
350
|
<IconSearch />
|
|
349
351
|
</button>
|
|
350
352
|
</ToolbarHint>
|
|
351
353
|
<ToolbarHint label="Keyboard shortcuts" shortcut="?" align="end">
|
|
352
|
-
<button
|
|
353
|
-
type="button"
|
|
354
|
-
onClick={onOpenShortcuts}
|
|
355
|
-
aria-label="Keyboard shortcuts"
|
|
356
|
-
>
|
|
354
|
+
<button type="button" onClick={onOpenShortcuts} aria-label="Keyboard shortcuts">
|
|
357
355
|
<IconShortcuts />
|
|
358
356
|
</button>
|
|
359
357
|
</ToolbarHint>
|
|
360
358
|
|
|
361
|
-
<span class="hud-collapsible-text" style={{ fontSize: '10px', color: 'var(--c-dim)' }}>
|
|
359
|
+
<span class="hud-collapsible-text" style={{ fontSize: '10px', color: 'var(--c-dim)' }}>
|
|
360
|
+
{countsLabel}
|
|
361
|
+
</span>
|
|
362
362
|
</div>
|
|
363
363
|
</div>
|
|
364
364
|
);
|
|
@@ -370,7 +370,8 @@ function WelcomeCard({ onOpenPalette }: { onOpenPalette: () => void }) {
|
|
|
370
370
|
<div class="welcome-icon">◇</div>
|
|
371
371
|
<div class="welcome-title">Shape What The Agent Sees</div>
|
|
372
372
|
<div class="welcome-subtitle">
|
|
373
|
-
Lay out notes, files, and evidence. Bring related nodes together. Pin what matters. The board will reflect the
|
|
373
|
+
Lay out notes, files, and evidence. Bring related nodes together. Pin what matters. The board will reflect the
|
|
374
|
+
active focus.
|
|
374
375
|
</div>
|
|
375
376
|
<div class="welcome-hints">
|
|
376
377
|
<button type="button" class="welcome-hint" onClick={onOpenPalette}>
|
|
@@ -390,9 +391,7 @@ function WelcomeCard({ onOpenPalette }: { onOpenPalette: () => void }) {
|
|
|
390
391
|
<span>Shape the focus field</span>
|
|
391
392
|
</div>
|
|
392
393
|
</div>
|
|
393
|
-
<div class="welcome-footer">
|
|
394
|
-
The canvas is a shared attention surface, not just an editor.
|
|
395
|
-
</div>
|
|
394
|
+
<div class="welcome-footer">The canvas is a shared attention surface, not just an editor.</div>
|
|
396
395
|
</div>
|
|
397
396
|
);
|
|
398
397
|
}
|
|
@@ -410,9 +409,18 @@ export function App() {
|
|
|
410
409
|
const handleToggleMinimap = useCallback(() => setMinimapVisible((v) => !v), []);
|
|
411
410
|
const handleToggleSnapshot = useCallback(() => setSnapshotOpen((v) => !v), []);
|
|
412
411
|
const handleCloseSnapshot = useCallback(() => setSnapshotOpen(false), []);
|
|
413
|
-
const handleToggleAnnotationMode = useCallback(
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
const handleToggleAnnotationMode = useCallback(
|
|
413
|
+
() => setAnnotationTool((tool) => (tool === 'pen' ? null : 'pen')),
|
|
414
|
+
[],
|
|
415
|
+
);
|
|
416
|
+
const handleToggleAnnotationEraser = useCallback(
|
|
417
|
+
() => setAnnotationTool((tool) => (tool === 'eraser' ? null : 'eraser')),
|
|
418
|
+
[],
|
|
419
|
+
);
|
|
420
|
+
const handleToggleTextAnnotation = useCallback(
|
|
421
|
+
() => setAnnotationTool((tool) => (tool === 'text' ? null : 'text')),
|
|
422
|
+
[],
|
|
423
|
+
);
|
|
416
424
|
|
|
417
425
|
const handleMinimapNavigate = useCallback((x: number, y: number) => {
|
|
418
426
|
animateViewport({ x, y, scale: viewport.value.scale }, 200);
|
|
@@ -508,8 +516,7 @@ export function App() {
|
|
|
508
516
|
|
|
509
517
|
useEffect(() => {
|
|
510
518
|
if (!hasInitialLayout) return;
|
|
511
|
-
const ready = (window as Window & { __pmxCanvasBootstrapReady?: () => void })
|
|
512
|
-
.__pmxCanvasBootstrapReady;
|
|
519
|
+
const ready = (window as Window & { __pmxCanvasBootstrapReady?: () => void }).__pmxCanvasBootstrapReady;
|
|
513
520
|
if (typeof ready === 'function') ready();
|
|
514
521
|
}, [hasInitialLayout]);
|
|
515
522
|
|
|
@@ -575,12 +582,7 @@ export function App() {
|
|
|
575
582
|
/>
|
|
576
583
|
)}
|
|
577
584
|
{menu && <ContextMenu menu={menu} onClose={closeMenu} />}
|
|
578
|
-
{paletteOpen && (
|
|
579
|
-
<CommandPalette
|
|
580
|
-
onClose={() => setPaletteOpen(false)}
|
|
581
|
-
onToggleMinimap={handleToggleMinimap}
|
|
582
|
-
/>
|
|
583
|
-
)}
|
|
585
|
+
{paletteOpen && <CommandPalette onClose={() => setPaletteOpen(false)} onToggleMinimap={handleToggleMinimap} />}
|
|
584
586
|
{shortcutsOpen && <ShortcutOverlay onClose={() => setShortcutsOpen(false)} />}
|
|
585
587
|
</div>
|
|
586
588
|
);
|
|
@@ -42,7 +42,17 @@ export function AttentionHistory() {
|
|
|
42
42
|
aria-label={unread > 0 ? `Recent updates — ${unread} new` : 'Recent updates'}
|
|
43
43
|
title={unread > 0 ? `${unread} new updates since last viewed` : 'Recent updates'}
|
|
44
44
|
>
|
|
45
|
-
<svg
|
|
45
|
+
<svg
|
|
46
|
+
width="14"
|
|
47
|
+
height="14"
|
|
48
|
+
viewBox="0 0 16 16"
|
|
49
|
+
fill="none"
|
|
50
|
+
stroke="currentColor"
|
|
51
|
+
stroke-width="1.5"
|
|
52
|
+
stroke-linecap="round"
|
|
53
|
+
stroke-linejoin="round"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
>
|
|
46
56
|
<rect x="1.5" y="2.5" width="13" height="11" rx="1.5" />
|
|
47
57
|
<circle cx="4.5" cy="8" r="1.1" fill="currentColor" stroke="none" />
|
|
48
58
|
<line x1="6.5" y1="6.5" x2="12.5" y2="6.5" />
|