pmx-canvas 0.6.3 → 0.6.5
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 +31 -0
- package/Readme.md +27 -6
- package/dist/canvas/global.css +90 -4
- package/dist/canvas/index.js +93 -91
- package/dist/canvas/mermaid-entry.js +258 -258
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/record.d.ts +12 -0
- package/dist/types/client/canvas/FeedbackDialog.d.ts +4 -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/McpAppNode.d.ts +24 -1
- package/dist/types/client/state/presentation.d.ts +3 -0
- package/dist/types/client/types.d.ts +1 -1
- package/dist/types/json-render/catalog.d.ts +2 -2
- package/dist/types/json-render/server.d.ts +2 -0
- package/dist/types/server/canvas-db.d.ts +2 -0
- package/dist/types/server/canvas-operations.d.ts +22 -0
- package/dist/types/server/canvas-schema.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +6 -0
- package/dist/types/server/canvas-validation.d.ts +3 -0
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/operations/ops/app.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 +3 -1
- package/dist/types/server/web-artifacts.d.ts +3 -0
- package/dist/types/shared/tour.d.ts +72 -0
- package/docs/cli.md +96 -2
- package/docs/http-api.md +79 -4
- package/docs/mcp.md +25 -1
- package/docs/node-types.md +123 -1
- package/docs/product-context-vision-2026-09.md +1 -1
- package/docs/product-vision-2026-09.md +135 -42
- package/docs/product-vision-review-2026-09.md +277 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +19 -4
- package/skills/pmx-canvas/evals/evals.json +46 -0
- package/skills/pmx-canvas/references/full-reference.md +1 -1
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/src/cli/agent.ts +5 -0
- package/src/cli/commands/nodes.ts +10 -0
- package/src/cli/commands/record.ts +218 -0
- package/src/cli/index.ts +2 -0
- package/src/client/App.tsx +7 -1
- package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
- package/src/client/canvas/CanvasNode.tsx +8 -0
- package/src/client/canvas/FeedbackDialog.tsx +86 -0
- package/src/client/canvas/IntentLayer.tsx +9 -2
- package/src/client/canvas/Presentation.tsx +108 -0
- package/src/client/canvas/ToolRail.tsx +225 -189
- package/src/client/canvas/TopBar.tsx +28 -2
- package/src/client/canvas/use-node-drag.ts +8 -3
- package/src/client/mermaid-entry.ts +39 -0
- package/src/client/nodes/HtmlNode.tsx +12 -13
- package/src/client/nodes/McpAppNode.tsx +105 -13
- package/src/client/nodes/MermaidNode.tsx +3 -2
- package/src/client/state/intent-store.ts +5 -1
- package/src/client/state/presentation.ts +7 -0
- package/src/client/state/session-store.ts +3 -0
- package/src/client/state/sse-bridge.ts +3 -0
- package/src/client/theme/global.css +90 -4
- package/src/json-render/server.ts +29 -0
- package/src/server/agent-presence.ts +15 -9
- package/src/server/canvas-db.ts +15 -0
- package/src/server/canvas-operations.ts +115 -22
- package/src/server/canvas-schema.ts +10 -0
- package/src/server/canvas-state.ts +39 -4
- package/src/server/canvas-validation.ts +9 -1
- package/src/server/demo-state.json +11 -7
- package/src/server/html-primitives.ts +199 -91
- package/src/server/html-surface.ts +8 -3
- package/src/server/index.ts +12 -0
- package/src/server/operations/composites.ts +3 -1
- package/src/server/operations/ops/app.ts +10 -0
- package/src/server/operations/ops/batch.ts +12 -3
- package/src/server/operations/ops/groups.ts +14 -7
- package/src/server/operations/ops/json-render.ts +37 -5
- package/src/server/operations/ops/nodes.ts +42 -16
- package/src/server/operations/ops/viewport.ts +32 -0
- package/src/server/operations/registry.ts +6 -0
- package/src/server/scope-fence.ts +1 -1
- package/src/server/server.ts +9 -6
- package/src/server/web-artifacts.ts +9 -2
- package/src/server/workboard.ts +3 -3
- package/src/shared/tour.ts +87 -0
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { saveCanvasTheme } from '../state/intent-bridge';
|
|
35
35
|
import { createNodeInView } from './create-in-view';
|
|
36
36
|
import { askText } from './TextPrompt';
|
|
37
|
+
import { FeedbackDialog } from './FeedbackDialog';
|
|
37
38
|
import { clearThemeOverride } from '../state/theme-override';
|
|
38
39
|
import { invalidateTokenCache } from '../theme/tokens';
|
|
39
40
|
import type { AnnotationTool } from '../types';
|
|
@@ -170,6 +171,7 @@ export function ToolRail({
|
|
|
170
171
|
const isTraceOn = traceEnabled.value;
|
|
171
172
|
const traceNodeCount = Array.from(nodes.value.values()).filter((n) => n.type === 'trace').length;
|
|
172
173
|
const edgeCount = edges.value.size;
|
|
174
|
+
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
|
173
175
|
const [openMenu, setOpenMenu] = useState<null | 'theme' | 'annotate'>(null);
|
|
174
176
|
const [menuAnchor, setMenuAnchor] = useState<{ top: number; bottom: number; right: number } | null>(null);
|
|
175
177
|
const railRef = useRef<HTMLDivElement>(null);
|
|
@@ -236,212 +238,246 @@ export function ToolRail({
|
|
|
236
238
|
};
|
|
237
239
|
|
|
238
240
|
return (
|
|
239
|
-
|
|
240
|
-
<
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
<div class="rail-divider" />
|
|
241
|
+
<>
|
|
242
|
+
<div class="tool-rail" ref={railRef} role="toolbar" aria-label="Canvas tools" aria-orientation="vertical">
|
|
243
|
+
<span class="rail-brand" title="PMX Canvas" aria-label="PMX Canvas">
|
|
244
|
+
<IconLogo size={22} />
|
|
245
|
+
</span>
|
|
245
246
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
label="Connect"
|
|
254
|
-
shortcut="C"
|
|
255
|
-
active={tool === 'connect'}
|
|
256
|
-
onClick={() => (canvasTool.value = 'connect')}
|
|
257
|
-
>
|
|
258
|
-
<svg
|
|
259
|
-
width="15"
|
|
260
|
-
height="15"
|
|
261
|
-
viewBox="0 0 16 16"
|
|
262
|
-
fill="none"
|
|
263
|
-
stroke="currentColor"
|
|
264
|
-
stroke-width="1.5"
|
|
265
|
-
stroke-linecap="round"
|
|
266
|
-
aria-hidden="true"
|
|
247
|
+
<RailButton
|
|
248
|
+
label="Bug and feedback"
|
|
249
|
+
menuOpen={feedbackOpen}
|
|
250
|
+
onClick={() => {
|
|
251
|
+
setOpenMenu(null);
|
|
252
|
+
setFeedbackOpen(true);
|
|
253
|
+
}}
|
|
267
254
|
>
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
255
|
+
<svg
|
|
256
|
+
width="16"
|
|
257
|
+
height="16"
|
|
258
|
+
viewBox="0 0 16 16"
|
|
259
|
+
fill="none"
|
|
260
|
+
stroke="currentColor"
|
|
261
|
+
stroke-width="1.5"
|
|
262
|
+
stroke-linecap="round"
|
|
263
|
+
stroke-linejoin="round"
|
|
264
|
+
aria-hidden="true"
|
|
265
|
+
>
|
|
266
|
+
<rect x="4.5" y="5" width="7" height="9" rx="3.5" />
|
|
267
|
+
<path d="M6 5V4a2 2 0 0 1 4 0v1M8 8v6M2 4l2.5 2M14 4l-2.5 2M1.5 9h3M11.5 9h3M2 14l2.5-2M14 14l-2.5-2" />
|
|
268
|
+
</svg>
|
|
269
|
+
</RailButton>
|
|
273
270
|
|
|
274
|
-
|
|
271
|
+
<div class="rail-divider" />
|
|
275
272
|
|
|
276
|
-
<RailButton
|
|
277
|
-
label="Markdown note"
|
|
278
|
-
shortcut="M"
|
|
279
|
-
onClick={() =>
|
|
280
|
-
void createNodeInView({ type: 'markdown', title: 'New note', width: 520, height: 360 }).catch((error) =>
|
|
281
|
-
logRailError('create markdown', error),
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
>
|
|
285
|
-
<IconNodeMarkdown size={15} />
|
|
286
|
-
</RailButton>
|
|
287
|
-
<RailButton label="Image" shortcut="I" onClick={() => promptedCreate('image')}>
|
|
288
|
-
<IconNodeImage size={15} />
|
|
289
|
-
</RailButton>
|
|
290
|
-
<RailButton label="File" shortcut="Shift+F" onClick={() => promptedCreate('file')}>
|
|
291
|
-
<IconNodeFile size={15} />
|
|
292
|
-
</RailButton>
|
|
293
|
-
<RailButton label="Webpage" shortcut="W" onClick={() => promptedCreate('webpage')}>
|
|
294
|
-
<IconNodeWebpage size={15} />
|
|
295
|
-
</RailButton>
|
|
296
|
-
<RailButton
|
|
297
|
-
label="HTML surface"
|
|
298
|
-
shortcut="H"
|
|
299
|
-
onClick={() =>
|
|
300
|
-
void createNodeInView({ type: 'html', title: 'HTML surface' }).catch((error) =>
|
|
301
|
-
logRailError('create html', error),
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
>
|
|
305
|
-
<IconNodeHtml />
|
|
306
|
-
</RailButton>
|
|
307
|
-
<RailButton
|
|
308
|
-
label="Group"
|
|
309
|
-
shortcut="G"
|
|
310
|
-
onClick={() =>
|
|
311
|
-
void createNodeInView({ type: 'group', title: 'Group' }).catch((error) => logRailError('create group', error))
|
|
312
|
-
}
|
|
313
|
-
>
|
|
314
|
-
<IconNodeGroup size={15} />
|
|
315
|
-
</RailButton>
|
|
316
|
-
<span class="toolbar-menu-anchor">
|
|
317
273
|
<RailButton
|
|
318
|
-
label="
|
|
319
|
-
shortcut="
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
menuOpen={openMenu === 'annotate'}
|
|
323
|
-
onClick={toggleMenu('annotate')}
|
|
274
|
+
label="Select"
|
|
275
|
+
shortcut="V"
|
|
276
|
+
active={tool === 'select'}
|
|
277
|
+
onClick={() => (canvasTool.value = 'select')}
|
|
324
278
|
>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
279
|
+
<IconCursorTool />
|
|
280
|
+
</RailButton>
|
|
281
|
+
<RailButton label="Pan" shortcut="Space" active={tool === 'pan'} onClick={() => (canvasTool.value = 'pan')}>
|
|
282
|
+
<IconHandTool />
|
|
283
|
+
</RailButton>
|
|
284
|
+
<RailButton
|
|
285
|
+
label="Connect"
|
|
286
|
+
shortcut="C"
|
|
287
|
+
active={tool === 'connect'}
|
|
288
|
+
onClick={() => (canvasTool.value = 'connect')}
|
|
289
|
+
>
|
|
290
|
+
<svg
|
|
291
|
+
width="15"
|
|
292
|
+
height="15"
|
|
293
|
+
viewBox="0 0 16 16"
|
|
294
|
+
fill="none"
|
|
295
|
+
stroke="currentColor"
|
|
296
|
+
stroke-width="1.5"
|
|
297
|
+
stroke-linecap="round"
|
|
298
|
+
aria-hidden="true"
|
|
299
|
+
>
|
|
300
|
+
<circle cx="4" cy="4" r="2" />
|
|
301
|
+
<circle cx="12" cy="12" r="2" />
|
|
302
|
+
<path d="M5.5 5.5 C 8 8, 8 8, 10.5 10.5" />
|
|
303
|
+
</svg>
|
|
332
304
|
</RailButton>
|
|
333
|
-
{openMenu === 'annotate' && (
|
|
334
|
-
<div class="toolbar-menu" style={sideMenuStyle(false)} role="menu" aria-label="Annotate">
|
|
335
|
-
<button
|
|
336
|
-
type="button"
|
|
337
|
-
class={`toolbar-menu-item${annotationTool === 'pen' ? ' active' : ''}`}
|
|
338
|
-
onClick={() => {
|
|
339
|
-
onSetAnnotationTool(annotationTool === 'pen' ? null : 'pen');
|
|
340
|
-
setOpenMenu(null);
|
|
341
|
-
}}
|
|
342
|
-
>
|
|
343
|
-
<IconPen />
|
|
344
|
-
<span>{annotationTool === 'pen' ? 'Stop annotating' : 'Draw (A)'}</span>
|
|
345
|
-
</button>
|
|
346
|
-
<button
|
|
347
|
-
type="button"
|
|
348
|
-
class={`toolbar-menu-item${annotationTool === 'text' ? ' active' : ''}`}
|
|
349
|
-
onClick={() => {
|
|
350
|
-
onSetAnnotationTool(annotationTool === 'text' ? null : 'text');
|
|
351
|
-
setOpenMenu(null);
|
|
352
|
-
}}
|
|
353
|
-
>
|
|
354
|
-
<IconTextAnnotation />
|
|
355
|
-
<span>{annotationTool === 'text' ? 'Stop text notes' : 'Text note'}</span>
|
|
356
|
-
</button>
|
|
357
|
-
<button
|
|
358
|
-
type="button"
|
|
359
|
-
class={`toolbar-menu-item${annotationTool === 'eraser' ? ' active' : ''}`}
|
|
360
|
-
onClick={() => {
|
|
361
|
-
onSetAnnotationTool(annotationTool === 'eraser' ? null : 'eraser');
|
|
362
|
-
setOpenMenu(null);
|
|
363
|
-
}}
|
|
364
|
-
>
|
|
365
|
-
<IconEraser />
|
|
366
|
-
<span>{annotationTool === 'eraser' ? 'Stop erasing' : 'Eraser'}</span>
|
|
367
|
-
</button>
|
|
368
|
-
</div>
|
|
369
|
-
)}
|
|
370
|
-
</span>
|
|
371
305
|
|
|
372
|
-
|
|
306
|
+
<div class="rail-divider" />
|
|
373
307
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
label=
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
308
|
+
<RailButton
|
|
309
|
+
label="Markdown note"
|
|
310
|
+
shortcut="M"
|
|
311
|
+
onClick={() =>
|
|
312
|
+
void createNodeInView({ type: 'markdown', title: 'New note', width: 520, height: 360 }).catch((error) =>
|
|
313
|
+
logRailError('create markdown', error),
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
>
|
|
317
|
+
<IconNodeMarkdown size={15} />
|
|
318
|
+
</RailButton>
|
|
319
|
+
<RailButton label="Image" shortcut="I" onClick={() => promptedCreate('image')}>
|
|
320
|
+
<IconNodeImage size={15} />
|
|
321
|
+
</RailButton>
|
|
322
|
+
<RailButton label="File" shortcut="Shift+F" onClick={() => promptedCreate('file')}>
|
|
323
|
+
<IconNodeFile size={15} />
|
|
324
|
+
</RailButton>
|
|
325
|
+
<RailButton label="Webpage" shortcut="W" onClick={() => promptedCreate('webpage')}>
|
|
326
|
+
<IconNodeWebpage size={15} />
|
|
327
|
+
</RailButton>
|
|
328
|
+
<RailButton
|
|
329
|
+
label="HTML surface"
|
|
330
|
+
shortcut="H"
|
|
331
|
+
onClick={() =>
|
|
332
|
+
void createNodeInView({ type: 'html', title: 'HTML surface' }).catch((error) =>
|
|
333
|
+
logRailError('create html', error),
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
>
|
|
337
|
+
<IconNodeHtml />
|
|
394
338
|
</RailButton>
|
|
395
|
-
)}
|
|
396
|
-
<RailButton
|
|
397
|
-
label={minimapVisible ? 'Hide minimap' : 'Show minimap'}
|
|
398
|
-
active={minimapVisible}
|
|
399
|
-
onClick={onToggleMinimap}
|
|
400
|
-
>
|
|
401
|
-
<IconMinimap />
|
|
402
|
-
</RailButton>
|
|
403
|
-
<RailButton label="Snapshots" active={snapshotOpen} onClick={onToggleSnapshot} btnRef={snapshotBtnRef}>
|
|
404
|
-
<IconSnapshot />
|
|
405
|
-
</RailButton>
|
|
406
|
-
<span class="toolbar-menu-anchor">
|
|
407
339
|
<RailButton
|
|
408
|
-
label="
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
340
|
+
label="Group"
|
|
341
|
+
shortcut="G"
|
|
342
|
+
onClick={() =>
|
|
343
|
+
void createNodeInView({ type: 'group', title: 'Group' }).catch((error) =>
|
|
344
|
+
logRailError('create group', error),
|
|
345
|
+
)
|
|
346
|
+
}
|
|
414
347
|
>
|
|
415
|
-
|
|
348
|
+
<IconNodeGroup size={15} />
|
|
416
349
|
</RailButton>
|
|
417
|
-
|
|
418
|
-
<
|
|
419
|
-
|
|
350
|
+
<span class="toolbar-menu-anchor">
|
|
351
|
+
<RailButton
|
|
352
|
+
label="Annotate"
|
|
353
|
+
shortcut="A"
|
|
354
|
+
detail="Draw · Text note · Eraser"
|
|
355
|
+
active={annotationTool !== null}
|
|
356
|
+
menuOpen={openMenu === 'annotate'}
|
|
357
|
+
onClick={toggleMenu('annotate')}
|
|
358
|
+
>
|
|
359
|
+
{annotationTool === 'eraser' ? (
|
|
360
|
+
<IconEraser />
|
|
361
|
+
) : annotationTool === 'text' ? (
|
|
362
|
+
<IconTextAnnotation />
|
|
363
|
+
) : (
|
|
364
|
+
<IconPen />
|
|
365
|
+
)}
|
|
366
|
+
</RailButton>
|
|
367
|
+
{openMenu === 'annotate' && (
|
|
368
|
+
<div class="toolbar-menu" style={sideMenuStyle(false)} role="menu" aria-label="Annotate">
|
|
420
369
|
<button
|
|
421
|
-
key={name}
|
|
422
370
|
type="button"
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
371
|
+
class={`toolbar-menu-item${annotationTool === 'pen' ? ' active' : ''}`}
|
|
372
|
+
onClick={() => {
|
|
373
|
+
onSetAnnotationTool(annotationTool === 'pen' ? null : 'pen');
|
|
374
|
+
setOpenMenu(null);
|
|
375
|
+
}}
|
|
427
376
|
>
|
|
428
|
-
<
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
377
|
+
<IconPen />
|
|
378
|
+
<span>{annotationTool === 'pen' ? 'Stop annotating' : 'Draw (A)'}</span>
|
|
379
|
+
</button>
|
|
380
|
+
<button
|
|
381
|
+
type="button"
|
|
382
|
+
class={`toolbar-menu-item${annotationTool === 'text' ? ' active' : ''}`}
|
|
383
|
+
onClick={() => {
|
|
384
|
+
onSetAnnotationTool(annotationTool === 'text' ? null : 'text');
|
|
385
|
+
setOpenMenu(null);
|
|
386
|
+
}}
|
|
387
|
+
>
|
|
388
|
+
<IconTextAnnotation />
|
|
389
|
+
<span>{annotationTool === 'text' ? 'Stop text notes' : 'Text note'}</span>
|
|
437
390
|
</button>
|
|
438
|
-
|
|
439
|
-
|
|
391
|
+
<button
|
|
392
|
+
type="button"
|
|
393
|
+
class={`toolbar-menu-item${annotationTool === 'eraser' ? ' active' : ''}`}
|
|
394
|
+
onClick={() => {
|
|
395
|
+
onSetAnnotationTool(annotationTool === 'eraser' ? null : 'eraser');
|
|
396
|
+
setOpenMenu(null);
|
|
397
|
+
}}
|
|
398
|
+
>
|
|
399
|
+
<IconEraser />
|
|
400
|
+
<span>{annotationTool === 'eraser' ? 'Stop erasing' : 'Eraser'}</span>
|
|
401
|
+
</button>
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
</span>
|
|
405
|
+
|
|
406
|
+
<div class="rail-spacer" />
|
|
407
|
+
|
|
408
|
+
<RailButton label="Search & commands" shortcut={modChord('K')} onClick={onOpenPalette}>
|
|
409
|
+
<IconSearch />
|
|
410
|
+
</RailButton>
|
|
411
|
+
<RailButton
|
|
412
|
+
label="Arrange"
|
|
413
|
+
detail={edgeCount > 0 ? 'graph-aware' : 'grid'}
|
|
414
|
+
onClick={() => (edgeCount > 0 ? forceDirectedArrange() : autoArrange())}
|
|
415
|
+
>
|
|
416
|
+
<IconArrange />
|
|
417
|
+
</RailButton>
|
|
418
|
+
<RailButton
|
|
419
|
+
label={isTraceOn ? 'Disable trace' : 'Enable trace'}
|
|
420
|
+
active={isTraceOn}
|
|
421
|
+
onClick={() => sendIntent('trace-toggle', { enabled: !isTraceOn })}
|
|
422
|
+
>
|
|
423
|
+
<IconTrace />
|
|
424
|
+
</RailButton>
|
|
425
|
+
{(isTraceOn || traceNodeCount > 0) && (
|
|
426
|
+
<RailButton label="Clear trace" onClick={() => sendIntent('trace-clear')}>
|
|
427
|
+
<IconClearTrace />
|
|
428
|
+
</RailButton>
|
|
440
429
|
)}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
430
|
+
<RailButton
|
|
431
|
+
label={minimapVisible ? 'Hide minimap' : 'Show minimap'}
|
|
432
|
+
active={minimapVisible}
|
|
433
|
+
onClick={onToggleMinimap}
|
|
434
|
+
>
|
|
435
|
+
<IconMinimap />
|
|
436
|
+
</RailButton>
|
|
437
|
+
<RailButton label="Snapshots" active={snapshotOpen} onClick={onToggleSnapshot} btnRef={snapshotBtnRef}>
|
|
438
|
+
<IconSnapshot />
|
|
439
|
+
</RailButton>
|
|
440
|
+
<span class="toolbar-menu-anchor">
|
|
441
|
+
<RailButton
|
|
442
|
+
label="Theme"
|
|
443
|
+
detail={CANVAS_THEME_META[activeTheme].label}
|
|
444
|
+
menuOpen={openMenu === 'theme'}
|
|
445
|
+
ariaLabel="Choose theme"
|
|
446
|
+
active={openMenu === 'theme'}
|
|
447
|
+
onClick={toggleMenu('theme')}
|
|
448
|
+
>
|
|
449
|
+
{canvasThemeScheme(activeTheme) === 'dark' ? <IconSun /> : <IconMoon />}
|
|
450
|
+
</RailButton>
|
|
451
|
+
{openMenu === 'theme' && (
|
|
452
|
+
<div class="toolbar-menu" style={sideMenuStyle(true)} role="menu" aria-label="Theme">
|
|
453
|
+
{CANVAS_THEMES.map((name) => (
|
|
454
|
+
<button
|
|
455
|
+
key={name}
|
|
456
|
+
type="button"
|
|
457
|
+
role="menuitemradio"
|
|
458
|
+
aria-checked={activeTheme === name}
|
|
459
|
+
class={`toolbar-menu-item${activeTheme === name ? ' active' : ''}`}
|
|
460
|
+
onClick={() => applyTheme(name)}
|
|
461
|
+
>
|
|
462
|
+
<span class="theme-swatch" style={{ background: CANVAS_THEME_META[name].swatchBg }}>
|
|
463
|
+
<span class="theme-swatch-dot" style={{ background: CANVAS_THEME_META[name].swatchAccent }} />
|
|
464
|
+
</span>
|
|
465
|
+
<span>{CANVAS_THEME_META[name].label}</span>
|
|
466
|
+
{activeTheme === name && (
|
|
467
|
+
<span class="toolbar-menu-check" aria-hidden="true">
|
|
468
|
+
✓
|
|
469
|
+
</span>
|
|
470
|
+
)}
|
|
471
|
+
</button>
|
|
472
|
+
))}
|
|
473
|
+
</div>
|
|
474
|
+
)}
|
|
475
|
+
</span>
|
|
476
|
+
<RailButton label="Shortcuts" shortcut="?" onClick={onOpenShortcuts}>
|
|
477
|
+
<IconShortcuts />
|
|
478
|
+
</RailButton>
|
|
479
|
+
</div>
|
|
480
|
+
{feedbackOpen && <FeedbackDialog onClose={() => setFeedbackOpen(false)} />}
|
|
481
|
+
</>
|
|
446
482
|
);
|
|
447
483
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentChildren } from 'preact';
|
|
2
|
+
import { presenting } from '../state/presentation';
|
|
2
3
|
import { useEffect, useState } from 'preact/hooks';
|
|
3
4
|
import { IconFitAll, IconZoomIn, IconZoomOut } from '../icons';
|
|
4
5
|
import {
|
|
@@ -42,8 +43,19 @@ function AgentChip() {
|
|
|
42
43
|
// bar and over the session panel, and the total was invisible): the most
|
|
43
44
|
// active few wear their own chips and the rest fold into a census chip
|
|
44
45
|
// that always names the total.
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
46
|
+
const allSessions = attachedSessions.value;
|
|
47
|
+
if (allSessions.length === 0) return null;
|
|
48
|
+
// Attached workers are represented by their orchestrator's roll-up, not a
|
|
49
|
+
// second top-level chip of their own. Orphans remain visible.
|
|
50
|
+
const sessions = allSessions.filter(
|
|
51
|
+
(session) =>
|
|
52
|
+
!session.parentAgentId ||
|
|
53
|
+
!allSessions.some(
|
|
54
|
+
(candidate) =>
|
|
55
|
+
candidate.sessionId !== session.sessionId &&
|
|
56
|
+
(candidate.sessionId === session.parentAgentId || candidate.source === session.parentAgentId),
|
|
57
|
+
),
|
|
58
|
+
);
|
|
47
59
|
const ranked = [...sessions].sort((a, b) => (AGENT_PHASE_RANK[a.phase] ?? 4) - (AGENT_PHASE_RANK[b.phase] ?? 4));
|
|
48
60
|
const visible = ranked.slice(0, AGENT_CHIP_LIMIT);
|
|
49
61
|
const overflow = ranked.slice(AGENT_CHIP_LIMIT);
|
|
@@ -318,6 +330,20 @@ export function TopBar() {
|
|
|
318
330
|
|
|
319
331
|
<div class="top-bar-sep" />
|
|
320
332
|
|
|
333
|
+
<BarHint
|
|
334
|
+
label="Present board"
|
|
335
|
+
body="Arrow keys or Space step through stops; Esc exits. Without a saved tour, groups are read top-to-bottom, left-to-right."
|
|
336
|
+
>
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
class="present-button"
|
|
340
|
+
onClick={() => {
|
|
341
|
+
presenting.value = true;
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
Present
|
|
345
|
+
</button>
|
|
346
|
+
</BarHint>
|
|
321
347
|
<BarHint label="Zoom out" shortcut={modChord('\u2212')}>
|
|
322
348
|
<button type="button" class="top-bar-btn" onClick={() => zoomByFactor(1 / 1.25)} aria-label="Zoom out">
|
|
323
349
|
<IconZoomOut />
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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
|
}
|