pmx-canvas 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/pmx-canvas/extension.mjs +67 -3
- package/CHANGELOG.md +73 -0
- package/Readme.md +35 -5
- package/dist/canvas/global.css +498 -28
- package/dist/canvas/index.js +110 -95
- package/dist/canvas/mermaid-entry.js +258 -258
- package/dist/types/cli/agent.d.ts +4 -0
- package/dist/types/cli/commands/backup.d.ts +1 -0
- package/dist/types/cli/commands/boards.d.ts +1 -0
- package/dist/types/cli/commands/export.d.ts +1 -0
- package/dist/types/cli/commands/record.d.ts +12 -0
- package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
- package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
- package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -0
- package/dist/types/client/canvas/Presentation.d.ts +13 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +2 -1
- package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
- package/dist/types/client/nodes/BoardNode.d.ts +5 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +24 -1
- package/dist/types/client/state/boards-store.d.ts +79 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +1 -0
- package/dist/types/client/state/presentation.d.ts +3 -0
- package/dist/types/client/state/session-store.d.ts +7 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +3 -3
- package/dist/types/json-render/charts/definitions.d.ts +1 -1
- package/dist/types/json-render/server.d.ts +8 -0
- package/dist/types/mcp/canvas-access.d.ts +10 -3
- package/dist/types/mcp/context-uri-template.d.ts +6 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/attribution.d.ts +11 -0
- package/dist/types/server/ax-context.d.ts +3 -1
- package/dist/types/server/ax-interaction.d.ts +2 -2
- package/dist/types/server/ax-state.d.ts +13 -3
- package/dist/types/server/backup.d.ts +36 -0
- package/dist/types/server/board-export.d.ts +45 -0
- package/dist/types/server/canvas-db.d.ts +65 -9
- package/dist/types/server/canvas-operations.d.ts +45 -0
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +146 -3
- package/dist/types/server/canvas-validation.d.ts +3 -0
- package/dist/types/server/context-brief.d.ts +111 -0
- package/dist/types/server/context-reads.d.ts +55 -0
- package/dist/types/server/document-import.d.ts +66 -0
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/index.d.ts +107 -4
- package/dist/types/server/mutation-history.d.ts +1 -0
- package/dist/types/server/operations/invoker.d.ts +2 -0
- package/dist/types/server/operations/mcp.d.ts +7 -1
- package/dist/types/server/operations/ops/app.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
- package/dist/types/server/operations/ops/backup.d.ts +2 -0
- package/dist/types/server/operations/ops/boards.d.ts +10 -0
- package/dist/types/server/operations/ops/export.d.ts +2 -0
- package/dist/types/server/operations/ops/imports.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +4 -2
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +9 -1
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/trace-manager.d.ts +7 -0
- package/dist/types/server/web-artifacts.d.ts +3 -0
- package/dist/types/server/webpage-node.d.ts +22 -1
- package/dist/types/server/workbench-auth.d.ts +3 -0
- package/dist/types/shared/boards.d.ts +6 -0
- package/dist/types/shared/tour.d.ts +72 -0
- package/docs/RELEASE.md +5 -0
- package/docs/cli.md +157 -0
- package/docs/http-api.md +277 -6
- package/docs/mcp.md +96 -7
- package/docs/node-types.md +106 -0
- package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
- package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
- package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
- package/docs/plans/plan-013-static-board-export.md +47 -0
- package/docs/plans/plan-014-connected-board-memory.md +140 -0
- package/docs/product-vision-2026-09.md +118 -18
- package/docs/sdk.md +50 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +62 -5
- package/skills/pmx-canvas/references/full-reference.md +44 -8
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +4 -0
- package/src/cli/agent.ts +9 -0
- package/src/cli/commands/ax.ts +13 -0
- package/src/cli/commands/backup.ts +66 -0
- package/src/cli/commands/boards.ts +71 -0
- package/src/cli/commands/export.ts +30 -0
- package/src/cli/commands/nodes.ts +14 -1
- package/src/cli/commands/pins.ts +6 -2
- package/src/cli/commands/pump.ts +3 -0
- package/src/cli/commands/query.ts +33 -7
- package/src/cli/commands/record.ts +218 -0
- package/src/cli/commands/view.ts +7 -5
- package/src/cli/index.ts +21 -9
- package/src/client/App.tsx +28 -4
- package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
- package/src/client/canvas/BoardCopyDialog.tsx +153 -0
- package/src/client/canvas/BoardFolderDialog.tsx +120 -0
- package/src/client/canvas/BoardLinkDialog.tsx +93 -0
- package/src/client/canvas/BoardSwitcher.tsx +149 -0
- package/src/client/canvas/CanvasNode.tsx +8 -0
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/CommandPalette.tsx +107 -4
- package/src/client/canvas/ContextMenu.tsx +4 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
- package/src/client/canvas/ExportDialog.tsx +197 -0
- package/src/client/canvas/HomeView.tsx +277 -0
- package/src/client/canvas/IntentLayer.tsx +9 -2
- package/src/client/canvas/Presentation.tsx +108 -0
- package/src/client/canvas/SessionPanel.tsx +28 -1
- package/src/client/canvas/ToolRail.tsx +38 -3
- package/src/client/canvas/TopBar.tsx +49 -7
- package/src/client/canvas/import-files.ts +19 -1
- package/src/client/canvas/kind-colors.ts +1 -0
- package/src/client/canvas/use-node-drag.ts +8 -3
- package/src/client/icons.tsx +2 -0
- package/src/client/mermaid-entry.ts +39 -0
- package/src/client/nodes/AttachmentNode.tsx +148 -0
- package/src/client/nodes/BoardNode.tsx +35 -0
- package/src/client/nodes/FileNode.tsx +6 -0
- package/src/client/nodes/HtmlNode.tsx +12 -13
- package/src/client/nodes/MarkdownNode.tsx +47 -19
- package/src/client/nodes/McpAppNode.tsx +105 -13
- package/src/client/nodes/MermaidNode.tsx +3 -2
- package/src/client/nodes/ax-node-actions.ts +2 -5
- package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
- package/src/client/state/boards-store.ts +167 -0
- package/src/client/state/canvas-store.ts +22 -0
- package/src/client/state/intent-bridge.ts +30 -2
- package/src/client/state/intent-store.ts +5 -1
- package/src/client/state/node-factory.ts +1 -0
- package/src/client/state/presentation.ts +7 -0
- package/src/client/state/session-store.ts +8 -0
- package/src/client/state/sse-bridge.ts +52 -5
- package/src/client/theme/global.css +498 -28
- package/src/client/types.ts +2 -0
- package/src/json-render/server.ts +45 -9
- package/src/mcp/canvas-access.ts +59 -22
- package/src/mcp/context-uri-template.ts +28 -0
- package/src/mcp/server.ts +122 -15
- package/src/server/agent-context.ts +116 -29
- package/src/server/agent-presence.ts +15 -9
- package/src/server/attribution.ts +29 -0
- package/src/server/ax-context.ts +4 -2
- package/src/server/ax-interaction.ts +1 -0
- package/src/server/ax-state-manager.ts +72 -3
- package/src/server/ax-state.ts +41 -0
- package/src/server/backup.ts +178 -0
- package/src/server/board-export.ts +686 -0
- package/src/server/canvas-db.ts +612 -105
- package/src/server/canvas-operations.ts +183 -31
- package/src/server/canvas-provenance.ts +1 -0
- package/src/server/canvas-schema.ts +22 -0
- package/src/server/canvas-state.ts +812 -48
- package/src/server/canvas-validation.ts +9 -1
- package/src/server/context-brief.ts +372 -0
- package/src/server/context-reads.ts +238 -0
- package/src/server/demo-state.json +670 -9
- package/src/server/document-import.ts +223 -0
- package/src/server/html-primitives.ts +199 -91
- package/src/server/html-surface.ts +8 -3
- package/src/server/index.ts +243 -5
- package/src/server/mutation-history.ts +10 -13
- package/src/server/operations/composites.ts +29 -5
- package/src/server/operations/http.ts +9 -3
- package/src/server/operations/index.ts +8 -0
- package/src/server/operations/invoker.ts +14 -3
- package/src/server/operations/mcp.ts +50 -11
- package/src/server/operations/ops/app.ts +15 -0
- package/src/server/operations/ops/ax-read.ts +10 -6
- package/src/server/operations/ops/ax-timeline.ts +75 -0
- package/src/server/operations/ops/backup.ts +134 -0
- package/src/server/operations/ops/batch.ts +20 -4
- package/src/server/operations/ops/boards.ts +351 -0
- package/src/server/operations/ops/export.ts +74 -0
- package/src/server/operations/ops/ext-app.ts +4 -0
- package/src/server/operations/ops/groups.ts +14 -7
- package/src/server/operations/ops/imports.ts +220 -0
- package/src/server/operations/ops/json-render.ts +37 -5
- package/src/server/operations/ops/nodes.ts +99 -21
- package/src/server/operations/ops/query.ts +206 -9
- package/src/server/operations/ops/viewport.ts +32 -0
- package/src/server/operations/registry.ts +129 -31
- package/src/server/operations/types.ts +1 -1
- package/src/server/scope-fence.ts +1 -1
- package/src/server/server.ts +155 -8
- package/src/server/spatial-analysis.ts +9 -17
- package/src/server/trace-manager.ts +19 -0
- package/src/server/web-artifacts.ts +11 -3
- package/src/server/webpage-node.ts +173 -13
- package/src/server/workbench-auth.ts +11 -0
- package/src/server/workboard.ts +3 -3
- package/src/shared/boards.ts +18 -0
- package/src/shared/node-sizes.ts +1 -0
- package/src/shared/tour.ts +87 -0
|
@@ -30,8 +30,12 @@ import {
|
|
|
30
30
|
forceDirectedArrange,
|
|
31
31
|
nodes,
|
|
32
32
|
traceEnabled,
|
|
33
|
+
viewport,
|
|
33
34
|
} from '../state/canvas-store';
|
|
35
|
+
import { importFiles } from './import-files';
|
|
36
|
+
import { canvasArea } from './canvas-area';
|
|
34
37
|
import { saveCanvasTheme } from '../state/intent-bridge';
|
|
38
|
+
import { openBoard } from '../state/boards-store';
|
|
35
39
|
import { createNodeInView } from './create-in-view';
|
|
36
40
|
import { askText } from './TextPrompt';
|
|
37
41
|
import { FeedbackDialog } from './FeedbackDialog';
|
|
@@ -175,6 +179,7 @@ export function ToolRail({
|
|
|
175
179
|
const [openMenu, setOpenMenu] = useState<null | 'theme' | 'annotate'>(null);
|
|
176
180
|
const [menuAnchor, setMenuAnchor] = useState<{ top: number; bottom: number; right: number } | null>(null);
|
|
177
181
|
const railRef = useRef<HTMLDivElement>(null);
|
|
182
|
+
const documentInput = useRef<HTMLInputElement>(null);
|
|
178
183
|
|
|
179
184
|
const toggleMenu = (menu: 'theme' | 'annotate') => (e: MouseEvent) => {
|
|
180
185
|
if (openMenu === menu) {
|
|
@@ -240,9 +245,18 @@ export function ToolRail({
|
|
|
240
245
|
return (
|
|
241
246
|
<>
|
|
242
247
|
<div class="tool-rail" ref={railRef} role="toolbar" aria-label="Canvas tools" aria-orientation="vertical">
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
248
|
+
<RailButton
|
|
249
|
+
label="Home"
|
|
250
|
+
ariaLabel="PMX Canvas — Home"
|
|
251
|
+
onClick={() => {
|
|
252
|
+
setOpenMenu(null);
|
|
253
|
+
void openBoard(null).catch((error) => logRailError('openHome', error));
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
<span class="rail-brand">
|
|
257
|
+
<IconLogo size={22} />
|
|
258
|
+
</span>
|
|
259
|
+
</RailButton>
|
|
246
260
|
|
|
247
261
|
<RailButton
|
|
248
262
|
label="Bug and feedback"
|
|
@@ -322,6 +336,27 @@ export function ToolRail({
|
|
|
322
336
|
<RailButton label="File" shortcut="Shift+F" onClick={() => promptedCreate('file')}>
|
|
323
337
|
<IconNodeFile size={15} />
|
|
324
338
|
</RailButton>
|
|
339
|
+
<RailButton label="Attach document" onClick={() => documentInput.current?.click()}>
|
|
340
|
+
<span aria-hidden="true">↥</span>
|
|
341
|
+
</RailButton>
|
|
342
|
+
<input
|
|
343
|
+
ref={documentInput}
|
|
344
|
+
type="file"
|
|
345
|
+
hidden
|
|
346
|
+
multiple
|
|
347
|
+
accept=".pdf,.ppt,.pptx,.xls,.xlsx,.doc,.docx,.odt,.ods,.odp"
|
|
348
|
+
aria-label="Attach documents"
|
|
349
|
+
onChange={(event) => {
|
|
350
|
+
const area = canvasArea();
|
|
351
|
+
const view = viewport.value;
|
|
352
|
+
void importFiles(
|
|
353
|
+
Array.from(event.currentTarget.files ?? []),
|
|
354
|
+
(area.width / 2 - view.x) / view.scale,
|
|
355
|
+
(area.height / 2 - view.y) / view.scale,
|
|
356
|
+
);
|
|
357
|
+
event.currentTarget.value = '';
|
|
358
|
+
}}
|
|
359
|
+
/>
|
|
325
360
|
<RailButton label="Webpage" shortcut="W" onClick={() => promptedCreate('webpage')}>
|
|
326
361
|
<IconNodeWebpage size={15} />
|
|
327
362
|
</RailButton>
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { exportDialogOpen } from './ExportDialog';
|
|
2
|
+
import { activeBoardId } from '../state/boards-store';
|
|
3
|
+
import { BoardSwitcher } from './BoardSwitcher';
|
|
1
4
|
import type { ComponentChildren } from 'preact';
|
|
5
|
+
import { presenting } from '../state/presentation';
|
|
2
6
|
import { useEffect, useState } from 'preact/hooks';
|
|
3
7
|
import { IconFitAll, IconZoomIn, IconZoomOut } from '../icons';
|
|
4
8
|
import {
|
|
@@ -42,8 +46,19 @@ function AgentChip() {
|
|
|
42
46
|
// bar and over the session panel, and the total was invisible): the most
|
|
43
47
|
// active few wear their own chips and the rest fold into a census chip
|
|
44
48
|
// that always names the total.
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
49
|
+
const allSessions = attachedSessions.value;
|
|
50
|
+
if (allSessions.length === 0) return null;
|
|
51
|
+
// Attached workers are represented by their orchestrator's roll-up, not a
|
|
52
|
+
// second top-level chip of their own. Orphans remain visible.
|
|
53
|
+
const sessions = allSessions.filter(
|
|
54
|
+
(session) =>
|
|
55
|
+
!session.parentAgentId ||
|
|
56
|
+
!allSessions.some(
|
|
57
|
+
(candidate) =>
|
|
58
|
+
candidate.sessionId !== session.sessionId &&
|
|
59
|
+
(candidate.sessionId === session.parentAgentId || candidate.source === session.parentAgentId),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
47
62
|
const ranked = [...sessions].sort((a, b) => (AGENT_PHASE_RANK[a.phase] ?? 4) - (AGENT_PHASE_RANK[b.phase] ?? 4));
|
|
48
63
|
const visible = ranked.slice(0, AGENT_CHIP_LIMIT);
|
|
49
64
|
const overflow = ranked.slice(AGENT_CHIP_LIMIT);
|
|
@@ -252,8 +267,8 @@ export function TopBar() {
|
|
|
252
267
|
const isTraceOn = traceEnabled.value;
|
|
253
268
|
const traceNodeCount = Array.from(nodes.value.values()).filter((n) => n.type === 'trace').length;
|
|
254
269
|
|
|
255
|
-
//
|
|
256
|
-
//
|
|
270
|
+
// Until the board list arrives, the switcher shows the workspace basename
|
|
271
|
+
// (fetched once from /health).
|
|
257
272
|
const [workspaceName, setWorkspaceName] = useState<string>('');
|
|
258
273
|
useEffect(() => {
|
|
259
274
|
let cancelled = false;
|
|
@@ -302,9 +317,7 @@ export function TopBar() {
|
|
|
302
317
|
>
|
|
303
318
|
<span class={`connection-dot ${degraded ?? status}`} aria-label={`Canvas status: ${statusTitle}`} />
|
|
304
319
|
</BarHint>
|
|
305
|
-
<
|
|
306
|
-
{workspaceName || 'PMX Canvas'}
|
|
307
|
-
</span>
|
|
320
|
+
<BoardSwitcher fallbackName={workspaceName || 'PMX Canvas'} />
|
|
308
321
|
<span class="top-bar-meta hud-collapsible-text">{sessionId.value ? sessionId.value.slice(0, 12) : '…'}</span>
|
|
309
322
|
<span class="top-bar-meta hud-collapsible-text">{countsLabel}</span>
|
|
310
323
|
|
|
@@ -318,6 +331,35 @@ export function TopBar() {
|
|
|
318
331
|
|
|
319
332
|
<div class="top-bar-sep" />
|
|
320
333
|
|
|
334
|
+
{activeBoardId.value && (
|
|
335
|
+
<BarHint label="Export board" body="One HTML file of this board that anyone can open, no install needed.">
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
class="present-button"
|
|
339
|
+
onClick={() => {
|
|
340
|
+
exportDialogOpen.value = true;
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
Export
|
|
344
|
+
</button>
|
|
345
|
+
</BarHint>
|
|
346
|
+
)}
|
|
347
|
+
{activeBoardId.value && (
|
|
348
|
+
<BarHint
|
|
349
|
+
label="Present board"
|
|
350
|
+
body="Arrow keys or Space step through stops; Esc exits. Without a saved tour, groups are read top-to-bottom, left-to-right."
|
|
351
|
+
>
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
class="present-button"
|
|
355
|
+
onClick={() => {
|
|
356
|
+
presenting.value = true;
|
|
357
|
+
}}
|
|
358
|
+
>
|
|
359
|
+
Present
|
|
360
|
+
</button>
|
|
361
|
+
</BarHint>
|
|
362
|
+
)}
|
|
321
363
|
<BarHint label="Zoom out" shortcut={modChord('\u2212')}>
|
|
322
364
|
<button type="button" class="top-bar-btn" onClick={() => zoomByFactor(1 / 1.25)} aria-label="Zoom out">
|
|
323
365
|
<IconZoomOut />
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { createNodeFromClient } from '../state/intent-bridge';
|
|
1
|
+
import { createNodeFromClient, requestJson } from '../state/intent-bridge';
|
|
2
|
+
import { activeBoardId } from '../state/boards-store';
|
|
3
|
+
import { showToast } from '../state/attention-bridge';
|
|
2
4
|
|
|
3
5
|
const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico', 'avif']);
|
|
4
6
|
const MD_EXTS = new Set(['md', 'mdx', 'markdown']);
|
|
@@ -17,6 +19,7 @@ export function nodeTypeFromFilename(name: string): 'image' | 'markdown' | 'file
|
|
|
17
19
|
* markdown, everything else a file node with the text inlined.
|
|
18
20
|
*/
|
|
19
21
|
export async function importFiles(files: File[], baseWx: number, baseWy: number): Promise<void> {
|
|
22
|
+
const boardId = activeBoardId.value;
|
|
20
23
|
const nodeW = 400;
|
|
21
24
|
const nodeH = 300;
|
|
22
25
|
const spacing = 20;
|
|
@@ -32,12 +35,24 @@ export async function importFiles(files: File[], baseWx: number, baseWy: number)
|
|
|
32
35
|
const type = nodeTypeFromFilename(file.name);
|
|
33
36
|
const fileName = file.name;
|
|
34
37
|
|
|
38
|
+
if (/\.(pdf|pptx?|xlsx?|docx?|od[pts])$/i.test(fileName)) {
|
|
39
|
+
if (!boardId || file.size === 0 || file.size > 20 * 1024 * 1024) {
|
|
40
|
+
showToast('remove', 'File not attached', 'Open a board and choose a nonempty file under 20 MiB.');
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const query = new URLSearchParams({ boardId, name: fileName, mime: file.type, x: String(wx), y: String(wy) });
|
|
44
|
+
showToast('context', 'Attaching document', fileName);
|
|
45
|
+
await requestJson('attachDocument', `/api/canvas/attachments?${query}`, null, { method: 'POST', body: file });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (activeBoardId.value !== boardId) return;
|
|
35
49
|
if (type === 'image') {
|
|
36
50
|
const reader = new FileReader();
|
|
37
51
|
const dataUri: string = await new Promise((resolve) => {
|
|
38
52
|
reader.onload = () => resolve(reader.result as string);
|
|
39
53
|
reader.readAsDataURL(file);
|
|
40
54
|
});
|
|
55
|
+
if (activeBoardId.value !== boardId) return;
|
|
41
56
|
await createNodeFromClient({
|
|
42
57
|
type: 'image',
|
|
43
58
|
title: fileName,
|
|
@@ -46,9 +61,11 @@ export async function importFiles(files: File[], baseWx: number, baseWy: number)
|
|
|
46
61
|
y: wy,
|
|
47
62
|
width: nodeW,
|
|
48
63
|
height: nodeH,
|
|
64
|
+
...(boardId ? { boardId } : {}),
|
|
49
65
|
});
|
|
50
66
|
} else {
|
|
51
67
|
const text = await file.text();
|
|
68
|
+
if (activeBoardId.value !== boardId) return;
|
|
52
69
|
const isWide = type === 'markdown' || type === 'file';
|
|
53
70
|
await createNodeFromClient({
|
|
54
71
|
type,
|
|
@@ -58,6 +75,7 @@ export async function importFiles(files: File[], baseWx: number, baseWy: number)
|
|
|
58
75
|
y: wy,
|
|
59
76
|
width: isWide ? 720 : nodeW,
|
|
60
77
|
height: isWide ? 500 : nodeH,
|
|
78
|
+
...(boardId ? { boardId } : {}),
|
|
61
79
|
});
|
|
62
80
|
}
|
|
63
81
|
}
|
|
@@ -7,6 +7,7 @@ export const KIND_COLOR: Record<CanvasNodeState['type'], string> = {
|
|
|
7
7
|
webpage: 'var(--c-warn)',
|
|
8
8
|
'json-render': 'var(--c-ok)',
|
|
9
9
|
graph: 'var(--c-purple)',
|
|
10
|
+
board: 'var(--c-accent)',
|
|
10
11
|
prompt: 'var(--c-accent)',
|
|
11
12
|
response: 'var(--c-ok)',
|
|
12
13
|
status: 'var(--c-warn)',
|
|
@@ -7,6 +7,7 @@ interface NodeDragOptions {
|
|
|
7
7
|
viewport: Signal<ViewportState>;
|
|
8
8
|
onMove: (id: string, x: number, y: number) => void;
|
|
9
9
|
onDragEnd: () => void;
|
|
10
|
+
onClick?: () => void;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -14,7 +15,7 @@ interface NodeDragOptions {
|
|
|
14
15
|
* Converts screen-space pointer delta to canvas-space position delta
|
|
15
16
|
* (accounting for current viewport scale).
|
|
16
17
|
*/
|
|
17
|
-
export function useNodeDrag({ nodeId, viewport, onMove, onDragEnd }: NodeDragOptions) {
|
|
18
|
+
export function useNodeDrag({ nodeId, viewport, onMove, onDragEnd, onClick }: NodeDragOptions) {
|
|
18
19
|
const isDragging = useRef(false);
|
|
19
20
|
const startPointer = useRef({ x: 0, y: 0 });
|
|
20
21
|
const startPosition = useRef({ x: 0, y: 0 });
|
|
@@ -30,6 +31,7 @@ export function useNodeDrag({ nodeId, viewport, onMove, onDragEnd }: NodeDragOpt
|
|
|
30
31
|
startPosition.current = { x: currentX, y: currentY };
|
|
31
32
|
let pendingPointer: { x: number; y: number } | null = null;
|
|
32
33
|
let frameId: number | null = null;
|
|
34
|
+
let moved = false;
|
|
33
35
|
|
|
34
36
|
const flushMove = () => {
|
|
35
37
|
frameId = null;
|
|
@@ -44,12 +46,14 @@ export function useNodeDrag({ nodeId, viewport, onMove, onDragEnd }: NodeDragOpt
|
|
|
44
46
|
|
|
45
47
|
const onPointerMove = (ev: PointerEvent) => {
|
|
46
48
|
if (!isDragging.current) return;
|
|
49
|
+
if (!moved && Math.hypot(ev.clientX - e.clientX, ev.clientY - e.clientY) < 4) return;
|
|
50
|
+
moved = true;
|
|
47
51
|
pendingPointer = { x: ev.clientX, y: ev.clientY };
|
|
48
52
|
if (frameId !== null) return;
|
|
49
53
|
frameId = window.requestAnimationFrame(flushMove);
|
|
50
54
|
};
|
|
51
55
|
|
|
52
|
-
const finishDrag = () => {
|
|
56
|
+
const finishDrag = (ev: PointerEvent) => {
|
|
53
57
|
if (frameId !== null) {
|
|
54
58
|
window.cancelAnimationFrame(frameId);
|
|
55
59
|
flushMove();
|
|
@@ -60,13 +64,14 @@ export function useNodeDrag({ nodeId, viewport, onMove, onDragEnd }: NodeDragOpt
|
|
|
60
64
|
document.removeEventListener('pointerup', finishDrag);
|
|
61
65
|
document.removeEventListener('pointercancel', finishDrag);
|
|
62
66
|
onDragEnd();
|
|
67
|
+
if (!moved && ev.type === 'pointerup') onClick?.();
|
|
63
68
|
};
|
|
64
69
|
|
|
65
70
|
document.addEventListener('pointermove', onPointerMove);
|
|
66
71
|
document.addEventListener('pointerup', finishDrag);
|
|
67
72
|
document.addEventListener('pointercancel', finishDrag);
|
|
68
73
|
},
|
|
69
|
-
[nodeId, viewport, onMove, onDragEnd],
|
|
74
|
+
[nodeId, viewport, onMove, onDragEnd, onClick],
|
|
70
75
|
);
|
|
71
76
|
|
|
72
77
|
return handlePointerDown;
|
package/src/client/icons.tsx
CHANGED
|
@@ -29,6 +29,41 @@ function showError(container: HTMLElement, message: string): void {
|
|
|
29
29
|
|
|
30
30
|
let renderSeq = 0;
|
|
31
31
|
|
|
32
|
+
function fitDiagram(container: HTMLElement): void {
|
|
33
|
+
const svg = container.querySelector('svg');
|
|
34
|
+
const box = svg?.viewBox.baseVal;
|
|
35
|
+
if (!svg || !box?.width || !box.height) return;
|
|
36
|
+
const source = document.querySelector<HTMLElement>('.mermaid-source');
|
|
37
|
+
// Only canvas frames carry a nonce. A standalone site may itself be inside
|
|
38
|
+
// a host browser pane's iframe; it must still offer natural-size viewing.
|
|
39
|
+
const token = source?.dataset.frameToken;
|
|
40
|
+
const embedded = !!token && window.parent !== window;
|
|
41
|
+
const contain = embedded && source?.dataset.fit !== 'none';
|
|
42
|
+
const width = document.documentElement.clientWidth;
|
|
43
|
+
const height = document.documentElement.clientHeight;
|
|
44
|
+
const scale = contain ? Math.min(1, width / box.width, height / box.height) : 1;
|
|
45
|
+
svg.style.maxWidth = 'none';
|
|
46
|
+
svg.style.width = `${box.width * scale}px`;
|
|
47
|
+
svg.style.height = `${box.height * scale}px`;
|
|
48
|
+
svg.style.display = 'block';
|
|
49
|
+
svg.style.margin = contain ? '0 auto' : '0';
|
|
50
|
+
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
|
51
|
+
|
|
52
|
+
// Report the unconstrained, width-fitted height, not the visually contained
|
|
53
|
+
// height. Auto-grow remains useful; strictSize/userResized keep their frame.
|
|
54
|
+
if (embedded && token) {
|
|
55
|
+
window.parent.postMessage(
|
|
56
|
+
{
|
|
57
|
+
source: 'pmx-canvas-frame',
|
|
58
|
+
type: 'content-height',
|
|
59
|
+
token,
|
|
60
|
+
height: box.height * (contain ? Math.min(1, width / box.width) : 1),
|
|
61
|
+
},
|
|
62
|
+
'*',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
32
67
|
async function renderDiagram(source: string, container: HTMLElement): Promise<void> {
|
|
33
68
|
const scheme = documentScheme();
|
|
34
69
|
mermaid.initialize({
|
|
@@ -40,6 +75,7 @@ async function renderDiagram(source: string, container: HTMLElement): Promise<vo
|
|
|
40
75
|
try {
|
|
41
76
|
const { svg } = await mermaid.render(`pmx-mermaid-${renderSeq}`, source);
|
|
42
77
|
container.innerHTML = svg;
|
|
78
|
+
fitDiagram(container);
|
|
43
79
|
} catch (error) {
|
|
44
80
|
// Never leave a blank/black frame: show the parse/render error instead.
|
|
45
81
|
showError(container, error instanceof Error ? error.message : String(error));
|
|
@@ -51,7 +87,10 @@ function boot(): void {
|
|
|
51
87
|
const source = sourceEl?.textContent ?? '';
|
|
52
88
|
const container = document.createElement('div');
|
|
53
89
|
container.className = 'mermaid-diagram';
|
|
90
|
+
document.body.style.margin = '0';
|
|
91
|
+
document.body.style.padding = '0';
|
|
54
92
|
document.body.appendChild(container);
|
|
93
|
+
window.addEventListener('resize', () => fitDiagram(container));
|
|
55
94
|
if (!source.trim()) {
|
|
56
95
|
showError(container, 'Empty mermaid diagram');
|
|
57
96
|
return;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
2
|
+
import type { DocumentImport } from '../../server/document-import';
|
|
3
|
+
import type { CanvasNodeState } from '../types';
|
|
4
|
+
import { activeBoardId } from '../state/boards-store';
|
|
5
|
+
import { requestJson } from '../state/intent-bridge';
|
|
6
|
+
import { expandNode, collapseExpandedNode } from '../state/canvas-store';
|
|
7
|
+
|
|
8
|
+
/** Originals stay useful even when the connected host cannot extract them. */
|
|
9
|
+
export function AttachmentNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
10
|
+
const attachmentId = String(node.data.attachmentId);
|
|
11
|
+
const boardId = activeBoardId.value;
|
|
12
|
+
const [job, setJob] = useState<DocumentImport | null>(null);
|
|
13
|
+
const [review, setReview] = useState(expanded);
|
|
14
|
+
const [consent, setConsent] = useState(false);
|
|
15
|
+
const [busy, setBusy] = useState(false);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let current = true;
|
|
18
|
+
const read = async () => {
|
|
19
|
+
const result = await requestJson<{ imports?: DocumentImport[] } | null>(
|
|
20
|
+
'readImports',
|
|
21
|
+
`/api/canvas/imports?boardId=${encodeURIComponent(boardId ?? '')}`,
|
|
22
|
+
null,
|
|
23
|
+
);
|
|
24
|
+
if (current) setJob(result?.imports?.find((entry) => entry.attachmentId === attachmentId) ?? null);
|
|
25
|
+
};
|
|
26
|
+
void read();
|
|
27
|
+
const timer = window.setInterval(() => void read(), 2000);
|
|
28
|
+
return () => {
|
|
29
|
+
current = false;
|
|
30
|
+
window.clearInterval(timer);
|
|
31
|
+
};
|
|
32
|
+
}, [attachmentId, boardId]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!review) return;
|
|
35
|
+
const onKey = (event: KeyboardEvent) => {
|
|
36
|
+
if (event.key === 'Escape') {
|
|
37
|
+
event.stopPropagation();
|
|
38
|
+
setReview(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
window.addEventListener('keydown', onKey, true);
|
|
42
|
+
return () => window.removeEventListener('keydown', onKey, true);
|
|
43
|
+
}, [review]);
|
|
44
|
+
const act = async (action: 'request' | 'cancel' | 'commit') => {
|
|
45
|
+
if (!job) return;
|
|
46
|
+
setBusy(true);
|
|
47
|
+
const result = await requestJson<{ import?: DocumentImport } | null>(
|
|
48
|
+
`import.${action}`,
|
|
49
|
+
`/api/canvas/imports/${job.id}/${action}`,
|
|
50
|
+
null,
|
|
51
|
+
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ consent: true }) },
|
|
52
|
+
);
|
|
53
|
+
setBusy(false);
|
|
54
|
+
if (result?.import) {
|
|
55
|
+
setJob(result.import);
|
|
56
|
+
setConsent(false);
|
|
57
|
+
setReview(false);
|
|
58
|
+
if (expanded && action === 'commit') collapseExpandedNode();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const canRequest = job && ['attached', 'cancelled', 'unavailable'].includes(job.status);
|
|
62
|
+
return (
|
|
63
|
+
<div class="attachment-node" onPointerDown={(event) => event.stopPropagation()}>
|
|
64
|
+
<div class="attachment-status">
|
|
65
|
+
{job?.status === 'requested'
|
|
66
|
+
? 'Waiting for agent'
|
|
67
|
+
: job?.status === 'drafted'
|
|
68
|
+
? 'Ready for review'
|
|
69
|
+
: job?.status === 'committed'
|
|
70
|
+
? 'Markdown added'
|
|
71
|
+
: 'Original attached'}
|
|
72
|
+
</div>
|
|
73
|
+
<a href={`/api/canvas/attachments/${attachmentId}/bytes`} target="_blank" rel="noopener noreferrer">
|
|
74
|
+
Download original · {Math.ceil(Number(node.data.size ?? 0) / 1024)} KB
|
|
75
|
+
</a>
|
|
76
|
+
{job?.reason && <p>{job.reason}</p>}
|
|
77
|
+
{canRequest && !consent && (
|
|
78
|
+
<button type="button" class="home-board-action" onClick={() => setConsent(true)}>
|
|
79
|
+
Ask agent to import
|
|
80
|
+
</button>
|
|
81
|
+
)}
|
|
82
|
+
{consent && (
|
|
83
|
+
<>
|
|
84
|
+
<p>
|
|
85
|
+
The connected agent may send this file to its model provider. It needs file-reading tools and access to the
|
|
86
|
+
original. Canvas does not convert files itself.
|
|
87
|
+
</p>
|
|
88
|
+
<button type="button" class="home-board-action" disabled={busy} onClick={() => void act('request')}>
|
|
89
|
+
Allow processing & request
|
|
90
|
+
</button>
|
|
91
|
+
<button type="button" class="home-board-action" onClick={() => setConsent(false)}>
|
|
92
|
+
Not now
|
|
93
|
+
</button>
|
|
94
|
+
</>
|
|
95
|
+
)}
|
|
96
|
+
{job?.status === 'requested' && (
|
|
97
|
+
<p>
|
|
98
|
+
No automatic extraction. Ask your connected agent to check its pending steering or use canvas_import list.
|
|
99
|
+
</p>
|
|
100
|
+
)}
|
|
101
|
+
{job?.status === 'drafted' && (
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
class="home-board-action"
|
|
105
|
+
onClick={() => (expanded ? setReview(true) : expandNode(node.id))}
|
|
106
|
+
>
|
|
107
|
+
Review Markdown · {job.sections.length} {job.sections.length === 1 ? 'card' : 'cards'}
|
|
108
|
+
</button>
|
|
109
|
+
)}
|
|
110
|
+
{job && ['requested', 'drafted'].includes(job.status) && (
|
|
111
|
+
<button type="button" class="home-board-action" disabled={busy} onClick={() => void act('cancel')}>
|
|
112
|
+
Cancel import
|
|
113
|
+
</button>
|
|
114
|
+
)}
|
|
115
|
+
{review && job?.status === 'drafted' && (
|
|
116
|
+
<div class="attachment-review" role="dialog" aria-label="Review imported Markdown">
|
|
117
|
+
<h3>Review imported Markdown</h3>
|
|
118
|
+
<p>Source material, not instructions or verified knowledge. {job.agentDescription}</p>
|
|
119
|
+
{job.warnings.length > 0 && (
|
|
120
|
+
<ul>
|
|
121
|
+
{job.warnings.map((warning) => (
|
|
122
|
+
<li key={warning}>{warning}</li>
|
|
123
|
+
))}
|
|
124
|
+
</ul>
|
|
125
|
+
)}
|
|
126
|
+
{job.sections.map((section, index) => (
|
|
127
|
+
<section key={index}>
|
|
128
|
+
<h4>{section.title}</h4>
|
|
129
|
+
<p>{section.reference || 'Source location not supplied'}</p>
|
|
130
|
+
<pre>{section.markdown}</pre>
|
|
131
|
+
</section>
|
|
132
|
+
))}
|
|
133
|
+
<button type="button" class="home-board-action" onClick={() => setReview(false)}>
|
|
134
|
+
Close review
|
|
135
|
+
</button>
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
class="home-view-new"
|
|
139
|
+
disabled={busy || boardId !== job.boardId}
|
|
140
|
+
onClick={() => void act('commit')}
|
|
141
|
+
>
|
|
142
|
+
Add to board
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CanvasNodeState } from '../types';
|
|
2
|
+
import { boardList, openBoard } from '../state/boards-store';
|
|
3
|
+
|
|
4
|
+
/** Native text-only cross-board link. Target identity is the durable board id. */
|
|
5
|
+
export function BoardNode({ node }: { node: CanvasNodeState }) {
|
|
6
|
+
const boardId = typeof node.data.boardId === 'string' ? node.data.boardId : '';
|
|
7
|
+
const target = boardList.value.find((board) => board.id === boardId);
|
|
8
|
+
return (
|
|
9
|
+
<div class="board-node">
|
|
10
|
+
<div class="board-node-content">
|
|
11
|
+
<div class="board-node-label">Linked board</div>
|
|
12
|
+
<strong class="board-node-title">{target?.name ?? 'Missing board'}</strong>
|
|
13
|
+
<div class="board-node-meta">{target ? `${target.nodeCount} nodes` : boardId || 'No target id'}</div>
|
|
14
|
+
{target?.summary && <p class="board-node-summary">{target.summary}</p>}
|
|
15
|
+
{!!target?.pinnedTitles?.length && (
|
|
16
|
+
<p class="board-node-meta">Pinned: {target.pinnedTitles.map((pin) => pin.title).join(', ')}</p>
|
|
17
|
+
)}
|
|
18
|
+
{!!target?.backlinks?.length && (
|
|
19
|
+
<p class="board-node-meta">
|
|
20
|
+
Linked from: {[...new Set(target.backlinks.map((link) => link.title))].join(', ')}
|
|
21
|
+
</p>
|
|
22
|
+
)}
|
|
23
|
+
</div>
|
|
24
|
+
<button
|
|
25
|
+
class="board-node-open"
|
|
26
|
+
type="button"
|
|
27
|
+
disabled={!target}
|
|
28
|
+
onClick={() => target && void openBoard(target.id)}
|
|
29
|
+
>
|
|
30
|
+
{target ? 'Open board' : 'Target unavailable'}
|
|
31
|
+
{target && <span aria-hidden="true">↗</span>}
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -4,6 +4,7 @@ import { iframeMode } from '../state/iframe-mode';
|
|
|
4
4
|
import { fetchFile, updateNodeFromClient } from '../state/intent-bridge';
|
|
5
5
|
import type { CanvasNodeState } from '../types';
|
|
6
6
|
import { runNodeAxInteraction } from './ax-node-actions';
|
|
7
|
+
import { AttachmentNode } from './AttachmentNode';
|
|
7
8
|
|
|
8
9
|
/** Guess a language label from a file extension for display. */
|
|
9
10
|
function langFromPath(path: string): string {
|
|
@@ -134,6 +135,11 @@ export function formatBytes(bytes: number): string {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
export function FileNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
138
|
+
if (typeof node.data.attachmentId === 'string') return <AttachmentNode node={node} expanded={expanded} />;
|
|
139
|
+
return <TextFileNode node={node} expanded={expanded} />;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function TextFileNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
137
143
|
const filePath = (node.data.path as string) || (node.data.content as string) || '';
|
|
138
144
|
const title = (node.data.title as string) || filePath.split('/').pop() || 'File';
|
|
139
145
|
const cachedContent = node.data.fileContent as string | undefined;
|
|
@@ -3,6 +3,7 @@ import { HTML_SURFACE_PUSH_SOURCE } from '../../shared/ax-surface-protocol.js';
|
|
|
3
3
|
import { axSurfaceState, canvasTheme } from '../state/canvas-store';
|
|
4
4
|
import type { CanvasNodeState } from '../types';
|
|
5
5
|
import { nodeSurfaceUrl, surfaceContentHash } from './surface-url';
|
|
6
|
+
import { RefreshingViewerFrame } from './McpAppNode';
|
|
6
7
|
import { useAxSurfaceBridge } from './use-ax-surface-bridge';
|
|
7
8
|
import { useIframeContentHeight } from './use-iframe-content-height';
|
|
8
9
|
import { useSurfaceFrame } from './use-surface-frame';
|
|
@@ -148,23 +149,21 @@ export function HtmlNode({
|
|
|
148
149
|
// standalone tab. The whole html-node tier assumes arbitrary author code runs
|
|
149
150
|
// inside this exact sandbox.
|
|
150
151
|
return (
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
<RefreshingViewerFrame
|
|
153
|
+
source={surfaceFrame}
|
|
154
|
+
iframeRef={iframeRef}
|
|
155
|
+
className={
|
|
156
|
+
presentation
|
|
157
|
+
? 'html-node-frame html-node-frame-presentation'
|
|
158
|
+
: expanded
|
|
159
|
+
? 'html-node-frame html-node-frame-expanded'
|
|
160
|
+
: 'html-node-frame'
|
|
161
|
+
}
|
|
154
162
|
title={typeof node.data.title === 'string' ? node.data.title : 'HTML node'}
|
|
155
163
|
sandbox="allow-scripts"
|
|
156
|
-
|
|
164
|
+
allow=""
|
|
157
165
|
tabIndex={autoFocus ? 0 : undefined}
|
|
158
166
|
onLoad={handleFrameLoad}
|
|
159
|
-
style={{
|
|
160
|
-
width: '100%',
|
|
161
|
-
height: '100%',
|
|
162
|
-
minHeight: presentation ? 0 : expanded ? '70vh' : '300px',
|
|
163
|
-
border: 'none',
|
|
164
|
-
background: 'var(--c-bg)',
|
|
165
|
-
borderRadius: presentation ? 0 : '6px',
|
|
166
|
-
display: 'block',
|
|
167
|
-
}}
|
|
168
167
|
/>
|
|
169
168
|
);
|
|
170
169
|
}
|