pmx-canvas 0.7.1 → 0.7.3
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 +79 -61
- package/CHANGELOG.md +29 -1
- package/Readme.md +48 -9
- package/dist/canvas/global.css +144 -45
- package/dist/canvas/index.js +90 -90
- package/dist/json-render/index.js +55 -55
- package/dist/types/client/canvas/ChoiceMenu.d.ts +13 -0
- package/dist/types/client/canvas/ExternalWriters.d.ts +3 -3
- package/dist/types/client/icons.d.ts +15 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +0 -12
- package/dist/types/client/nodes/McpAppNode.d.ts +2 -1
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +1 -1
- package/dist/types/json-render/renderer/index.d.ts +2 -1
- package/dist/types/json-render/server.d.ts +2 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +2 -0
- package/dist/types/shared/delimited-text.d.ts +14 -0
- package/docs/ax-host-adapter-contract.md +13 -2
- package/docs/cli.md +9 -2
- package/docs/http-api.md +12 -8
- package/docs/mcp.md +8 -1
- package/docs/sdk.md +2 -2
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +41 -16
- package/skills/pmx-canvas/evals/evals.json +112 -2
- package/skills/pmx-canvas/references/full-reference.md +9 -4
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +17 -3
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +76 -4
- package/skills/pmx-canvas-testing/SKILL.md +49 -0
- package/src/cli/commands/backup.ts +15 -4
- package/src/cli/commands/boards.ts +16 -3
- package/src/client/canvas/AttentionHistory.tsx +2 -1
- package/src/client/canvas/BoardSwitcher.tsx +3 -3
- package/src/client/canvas/CanvasNode.tsx +211 -197
- package/src/client/canvas/CanvasViewport.tsx +6 -0
- package/src/client/canvas/ChoiceMenu.tsx +122 -0
- package/src/client/canvas/CommandBar.tsx +27 -25
- package/src/client/canvas/ContextPinBar.tsx +3 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -3
- package/src/client/canvas/ExternalWriters.tsx +4 -3
- package/src/client/canvas/FeedbackDialog.tsx +12 -9
- package/src/client/canvas/IntentLayer.tsx +2 -2
- package/src/client/canvas/SelectionBar.tsx +2 -2
- package/src/client/canvas/SessionPanel.tsx +6 -4
- package/src/client/canvas/SessionReceipt.tsx +2 -1
- package/src/client/canvas/SnapshotPanel.tsx +3 -2
- package/src/client/canvas/ToolRail.tsx +48 -100
- package/src/client/canvas/TopBar.tsx +2 -2
- package/src/client/icons.tsx +118 -1
- package/src/client/nodes/BoardNode.tsx +2 -1
- package/src/client/nodes/ExtAppFrame.tsx +3 -1
- package/src/client/nodes/FileNode.tsx +4 -78
- package/src/client/nodes/ImageNode.tsx +2 -1
- package/src/client/nodes/MarkdownNode.tsx +2 -1
- package/src/client/nodes/McpAppNode.tsx +16 -0
- package/src/client/nodes/PromptNode.tsx +3 -1
- package/src/client/nodes/use-ax-surface-bridge.ts +4 -2
- package/src/client/nodes/viewer-runtime-state.ts +78 -0
- package/src/client/state/canvas-store.ts +3 -0
- package/src/client/state/intent-bridge.ts +14 -2
- package/src/client/theme/global.css +144 -45
- package/src/json-render/renderer/index.tsx +58 -38
- package/src/json-render/server.ts +8 -1
- package/src/mcp/server.ts +2 -2
- package/src/server/agent-presence.ts +17 -11
- package/src/server/board-export.ts +97 -32
- package/src/server/index.ts +9 -7
- package/src/server/operations/composites.ts +3 -2
- package/src/server/operations/ops/ax-presence.ts +7 -3
- package/src/server/operations/ops/boards.ts +12 -5
- package/src/server/operations/registry.ts +4 -13
- package/src/server/server.ts +13 -4
- package/src/shared/ax-surface-protocol.ts +3 -0
- package/src/shared/delimited-text.ts +75 -0
|
@@ -572,9 +572,23 @@ body,
|
|
|
572
572
|
box-shadow: 0 4px 24px var(--c-shadow);
|
|
573
573
|
display: flex;
|
|
574
574
|
flex-direction: column;
|
|
575
|
-
overflow:
|
|
575
|
+
overflow: visible;
|
|
576
576
|
transition: box-shadow 0.15s ease;
|
|
577
|
-
contain: layout style
|
|
577
|
+
contain: layout style;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* Clip card content, not the connection handles centered on its border. */
|
|
581
|
+
.node-content {
|
|
582
|
+
display: flex;
|
|
583
|
+
flex-direction: column;
|
|
584
|
+
flex: 1;
|
|
585
|
+
min-height: 0;
|
|
586
|
+
border-radius: inherit;
|
|
587
|
+
overflow: hidden;
|
|
588
|
+
contain: paint;
|
|
589
|
+
}
|
|
590
|
+
.group-node > .node-content {
|
|
591
|
+
display: contents;
|
|
578
592
|
}
|
|
579
593
|
|
|
580
594
|
.canvas-node:hover {
|
|
@@ -610,34 +624,42 @@ body,
|
|
|
610
624
|
/* ── Connection port handles (drag-to-connect) ───────────── */
|
|
611
625
|
.node-port {
|
|
612
626
|
position: absolute;
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
627
|
+
/* The visible dot stays compact; its grab area stays usable at every zoom. */
|
|
628
|
+
width: calc(32px * var(--node-port-scale, 1));
|
|
629
|
+
height: calc(32px * var(--node-port-scale, 1));
|
|
630
|
+
display: grid;
|
|
631
|
+
place-items: center;
|
|
618
632
|
opacity: 0;
|
|
619
633
|
cursor: crosshair;
|
|
620
634
|
z-index: 10;
|
|
621
|
-
|
|
622
|
-
|
|
635
|
+
touch-action: none;
|
|
636
|
+
transition: opacity 0.15s;
|
|
623
637
|
pointer-events: auto;
|
|
624
638
|
}
|
|
625
|
-
.
|
|
639
|
+
.node-port::after {
|
|
640
|
+
content: '';
|
|
641
|
+
width: calc(6px * var(--node-port-scale, 1));
|
|
642
|
+
height: calc(6px * var(--node-port-scale, 1));
|
|
643
|
+
border-radius: 50%;
|
|
644
|
+
background: var(--c-accent);
|
|
645
|
+
border: calc(2px * var(--node-port-scale, 1)) solid var(--c-panel);
|
|
646
|
+
box-shadow: 0 0 0 calc(1px * var(--node-port-scale, 1)) var(--c-accent);
|
|
647
|
+
}
|
|
648
|
+
.canvas-node:hover .node-port,
|
|
649
|
+
.canvas-node.selected .node-port,
|
|
650
|
+
.canvas-node:focus-within .node-port {
|
|
626
651
|
opacity: 1;
|
|
627
|
-
transform: translate(-50%, -50%) scale(1);
|
|
628
652
|
}
|
|
629
|
-
.node-port:hover {
|
|
653
|
+
.node-port:hover::after {
|
|
630
654
|
background: var(--c-accent-hover);
|
|
631
|
-
transform: translate(-50%, -50%) scale(1.3);
|
|
632
655
|
}
|
|
633
|
-
|
|
634
|
-
.node-port-
|
|
635
|
-
.node-port-
|
|
636
|
-
.node-port-
|
|
637
|
-
.
|
|
638
|
-
.
|
|
639
|
-
.node-port-
|
|
640
|
-
.node-port-right:hover { transform: translate(50%, -50%) scale(1.3); }
|
|
656
|
+
/* Bias vertical grab areas outward so they do not steal title/footer clicks. */
|
|
657
|
+
.node-port-top { top: 0; left: 50%; transform: translate(-50%, -75%); }
|
|
658
|
+
.node-port-bottom { bottom: 0; left: 50%; transform: translate(-50%, 75%); }
|
|
659
|
+
.node-port-top::after { transform: translateY(calc(8px * var(--node-port-scale, 1))); }
|
|
660
|
+
.node-port-bottom::after { transform: translateY(calc(-8px * var(--node-port-scale, 1))); }
|
|
661
|
+
.node-port-left { left: 0; top: 50%; transform: translate(-50%, -50%); }
|
|
662
|
+
.node-port-right { right: 0; top: 50%; transform: translate(50%, -50%); }
|
|
641
663
|
|
|
642
664
|
.canvas-node .node-titlebar {
|
|
643
665
|
display: flex;
|
|
@@ -760,6 +782,11 @@ body,
|
|
|
760
782
|
border-radius: calc(3px * var(--node-chrome-scale, 1));
|
|
761
783
|
}
|
|
762
784
|
|
|
785
|
+
.canvas-node .node-controls button svg {
|
|
786
|
+
width: 1em;
|
|
787
|
+
height: 1em;
|
|
788
|
+
}
|
|
789
|
+
|
|
763
790
|
.canvas-node .node-controls button:hover {
|
|
764
791
|
background: var(--c-surface-hover);
|
|
765
792
|
color: var(--c-text);
|
|
@@ -1466,6 +1493,10 @@ body,
|
|
|
1466
1493
|
animation: presence-pulse 1.4s ease-in-out infinite;
|
|
1467
1494
|
}
|
|
1468
1495
|
.session-collapse {
|
|
1496
|
+
display: inline-flex;
|
|
1497
|
+
align-items: center;
|
|
1498
|
+
justify-content: center;
|
|
1499
|
+
padding: 0;
|
|
1469
1500
|
width: 26px;
|
|
1470
1501
|
height: 26px;
|
|
1471
1502
|
border: 1px solid transparent;
|
|
@@ -1682,8 +1713,9 @@ body,
|
|
|
1682
1713
|
}
|
|
1683
1714
|
}
|
|
1684
1715
|
.session-section-chevron {
|
|
1716
|
+
display: inline-flex;
|
|
1717
|
+
flex-shrink: 0;
|
|
1685
1718
|
color: var(--c-muted);
|
|
1686
|
-
font-size: 13px;
|
|
1687
1719
|
transition: transform 0.15s ease;
|
|
1688
1720
|
}
|
|
1689
1721
|
.session-section-chevron.is-open {
|
|
@@ -1966,8 +1998,13 @@ body,
|
|
|
1966
1998
|
white-space: nowrap;
|
|
1967
1999
|
}
|
|
1968
2000
|
.command-bar-chip-unpin {
|
|
1969
|
-
|
|
1970
|
-
|
|
2001
|
+
display: inline-flex;
|
|
2002
|
+
align-items: center;
|
|
2003
|
+
justify-content: center;
|
|
2004
|
+
flex-shrink: 0;
|
|
2005
|
+
padding: 0;
|
|
2006
|
+
width: 20px;
|
|
2007
|
+
height: 20px;
|
|
1971
2008
|
border: none;
|
|
1972
2009
|
border-radius: 50%;
|
|
1973
2010
|
background: none;
|
|
@@ -1986,6 +2023,7 @@ body,
|
|
|
1986
2023
|
color: var(--c-dim);
|
|
1987
2024
|
}
|
|
1988
2025
|
.command-bar-composer {
|
|
2026
|
+
position: relative;
|
|
1989
2027
|
display: flex;
|
|
1990
2028
|
align-items: center;
|
|
1991
2029
|
gap: 10px;
|
|
@@ -1997,24 +2035,72 @@ body,
|
|
|
1997
2035
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
|
|
1998
2036
|
}
|
|
1999
2037
|
.command-bar-target {
|
|
2000
|
-
appearance: none;
|
|
2001
|
-
-webkit-appearance: none;
|
|
2002
2038
|
max-width: 148px;
|
|
2003
|
-
padding: 4px 18px 4px 8px;
|
|
2004
|
-
border: 1px solid var(--c-line);
|
|
2005
|
-
border-radius: 7px;
|
|
2006
|
-
background: color-mix(in srgb, var(--c-panel) 80%, transparent)
|
|
2007
|
-
url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M1 1l3 3 3-3' fill='none' stroke='%237a8699' stroke-width='1.4'/%3E%3C/svg%3E")
|
|
2008
|
-
no-repeat right 6px center;
|
|
2009
|
-
color: var(--c-text-soft);
|
|
2010
|
-
font: 12px/1.3 var(--mono);
|
|
2011
|
-
cursor: pointer;
|
|
2012
2039
|
flex-shrink: 0;
|
|
2013
2040
|
}
|
|
2014
|
-
.
|
|
2015
|
-
|
|
2016
|
-
|
|
2041
|
+
.choice-menu {
|
|
2042
|
+
position: relative;
|
|
2043
|
+
min-width: 0;
|
|
2044
|
+
}
|
|
2045
|
+
.choice-menu-trigger {
|
|
2046
|
+
display: flex;
|
|
2047
|
+
align-items: center;
|
|
2048
|
+
justify-content: space-between;
|
|
2049
|
+
gap: 10px;
|
|
2050
|
+
width: 100%;
|
|
2051
|
+
min-height: 36px;
|
|
2052
|
+
padding: 8px 10px;
|
|
2053
|
+
border: 1px solid color-mix(in srgb, var(--c-line) 65%, var(--c-muted));
|
|
2054
|
+
border-radius: 8px;
|
|
2055
|
+
background: var(--c-panel-overlay);
|
|
2017
2056
|
color: var(--c-text);
|
|
2057
|
+
font: 12px/1.35 var(--font);
|
|
2058
|
+
cursor: pointer;
|
|
2059
|
+
}
|
|
2060
|
+
.choice-menu-trigger > span {
|
|
2061
|
+
overflow: hidden;
|
|
2062
|
+
text-overflow: ellipsis;
|
|
2063
|
+
white-space: nowrap;
|
|
2064
|
+
}
|
|
2065
|
+
.choice-menu-trigger svg { flex-shrink: 0; }
|
|
2066
|
+
.choice-menu-trigger:hover,
|
|
2067
|
+
.choice-menu-trigger[aria-expanded='true'] { border-color: var(--c-accent); }
|
|
2068
|
+
.choice-menu-trigger:focus-visible,
|
|
2069
|
+
.choice-menu-options button:focus-visible {
|
|
2070
|
+
outline: 2px solid var(--c-accent);
|
|
2071
|
+
outline-offset: 2px;
|
|
2072
|
+
}
|
|
2073
|
+
.toolbar-menu.choice-menu-options {
|
|
2074
|
+
top: calc(100% + 6px);
|
|
2075
|
+
left: 0;
|
|
2076
|
+
right: auto;
|
|
2077
|
+
min-width: 100%;
|
|
2078
|
+
width: max-content;
|
|
2079
|
+
max-width: min(320px, calc(100vw - 64px));
|
|
2080
|
+
max-height: min(280px, 40vh);
|
|
2081
|
+
overflow-y: auto;
|
|
2082
|
+
z-index: 20;
|
|
2083
|
+
}
|
|
2084
|
+
.toolbar-menu.choice-menu-options.opens-above {
|
|
2085
|
+
top: auto;
|
|
2086
|
+
bottom: calc(100% + 6px);
|
|
2087
|
+
}
|
|
2088
|
+
.command-bar-target.choice-menu {
|
|
2089
|
+
position: static;
|
|
2090
|
+
}
|
|
2091
|
+
.command-bar-target .choice-menu-options {
|
|
2092
|
+
/* Anchor to the composer, not its inset trigger, to clear the session rail. */
|
|
2093
|
+
min-width: min(200px, 100%);
|
|
2094
|
+
max-width: 100%;
|
|
2095
|
+
}
|
|
2096
|
+
.choice-menu-options .toolbar-menu-item {
|
|
2097
|
+
min-height: 36px;
|
|
2098
|
+
white-space: normal;
|
|
2099
|
+
overflow-wrap: anywhere;
|
|
2100
|
+
}
|
|
2101
|
+
.choice-menu-options button:disabled {
|
|
2102
|
+
opacity: 0.5;
|
|
2103
|
+
cursor: not-allowed;
|
|
2018
2104
|
}
|
|
2019
2105
|
|
|
2020
2106
|
.command-bar-icon {
|
|
@@ -4483,13 +4569,24 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
4483
4569
|
}
|
|
4484
4570
|
.text-prompt-cancel,
|
|
4485
4571
|
.text-prompt-confirm {
|
|
4486
|
-
|
|
4572
|
+
min-height: 36px;
|
|
4573
|
+
padding: 8px 14px;
|
|
4487
4574
|
border-radius: 7px;
|
|
4488
|
-
border: 1px solid var(--c-line);
|
|
4489
|
-
background:
|
|
4490
|
-
color: var(--c-text
|
|
4575
|
+
border: 1px solid color-mix(in srgb, var(--c-line) 65%, var(--c-muted));
|
|
4576
|
+
background: var(--c-panel-overlay);
|
|
4577
|
+
color: var(--c-text);
|
|
4491
4578
|
cursor: pointer;
|
|
4492
4579
|
font-size: 12px;
|
|
4580
|
+
font-weight: 600;
|
|
4581
|
+
}
|
|
4582
|
+
.text-prompt-cancel:hover {
|
|
4583
|
+
background: var(--c-accent-10);
|
|
4584
|
+
border-color: var(--c-muted);
|
|
4585
|
+
}
|
|
4586
|
+
.text-prompt-cancel:focus-visible,
|
|
4587
|
+
.text-prompt-confirm:focus-visible {
|
|
4588
|
+
outline: 2px solid var(--c-accent);
|
|
4589
|
+
outline-offset: 2px;
|
|
4493
4590
|
}
|
|
4494
4591
|
.text-prompt-confirm {
|
|
4495
4592
|
background: var(--c-accent);
|
|
@@ -4509,7 +4606,8 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
4509
4606
|
overflow-y: auto;
|
|
4510
4607
|
color: var(--c-text);
|
|
4511
4608
|
}
|
|
4512
|
-
.feedback-dialog label
|
|
4609
|
+
.feedback-dialog label,
|
|
4610
|
+
.feedback-field {
|
|
4513
4611
|
display: flex;
|
|
4514
4612
|
flex-direction: column;
|
|
4515
4613
|
gap: 6px;
|
|
@@ -6896,7 +6994,7 @@ html.is-node-dragging .intent-line-layer {
|
|
|
6896
6994
|
align-items: center;
|
|
6897
6995
|
gap: 6px;
|
|
6898
6996
|
max-width: 280px;
|
|
6899
|
-
padding:
|
|
6997
|
+
padding: 6px 8px;
|
|
6900
6998
|
margin-left: -8px;
|
|
6901
6999
|
background: none;
|
|
6902
7000
|
border: 1px solid transparent;
|
|
@@ -6914,7 +7012,8 @@ html.is-node-dragging .intent-line-layer {
|
|
|
6914
7012
|
white-space: nowrap;
|
|
6915
7013
|
}
|
|
6916
7014
|
.board-switcher-caret {
|
|
6917
|
-
|
|
7015
|
+
display: inline-flex;
|
|
7016
|
+
flex-shrink: 0;
|
|
6918
7017
|
color: var(--c-text-soft);
|
|
6919
7018
|
}
|
|
6920
7019
|
.board-switcher-menu {
|
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
* from an inline global injected by the server-side viewer route.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type
|
|
11
|
-
import {
|
|
12
|
-
|
|
10
|
+
import { createStateStore, type Spec, type StateStore } from '@json-render/core';
|
|
11
|
+
import {
|
|
12
|
+
AX_SURFACE_EMIT_SOURCE,
|
|
13
|
+
HTML_SURFACE_PUSH_SOURCE,
|
|
14
|
+
JSON_VIEWER_STATE_SOURCE,
|
|
15
|
+
} from '../../shared/ax-surface-protocol.js';
|
|
13
16
|
import { createRoot } from 'react-dom/client';
|
|
14
|
-
import { defineRegistry, JSONUIProvider, Renderer
|
|
17
|
+
import { defineRegistry, JSONUIProvider, Renderer } from '@json-render/react';
|
|
15
18
|
import { shadcnComponents } from '@json-render/shadcn';
|
|
16
19
|
import { catalog } from '../catalog';
|
|
17
20
|
import { chartComponents } from '../charts/components';
|
|
@@ -106,29 +109,10 @@ declare global {
|
|
|
106
109
|
__PMX_CANVAS_JSON_RENDER_NODE_ID__?: string;
|
|
107
110
|
__PMX_CANVAS_AX_TOKEN__?: string;
|
|
108
111
|
__PMX_CANVAS_AX_STATE__?: unknown;
|
|
112
|
+
__PMX_CANVAS_UI_STATE_TOKEN__?: string;
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
// Read-side AX bridge for json-render: keeps the spec-bound `/ax` state live as
|
|
113
|
-
// the parent canvas pushes nonce-validated `ax-update` messages, so a declarative
|
|
114
|
-
// board ({ "$state": "/ax/workItems" }) reflects the work queue in real time.
|
|
115
|
-
function AxStateSync() {
|
|
116
|
-
const [, setAx] = useStateBinding<unknown>('ax');
|
|
117
|
-
useEffect(() => {
|
|
118
|
-
const token = window.__PMX_CANVAS_AX_TOKEN__;
|
|
119
|
-
if (!token) return undefined;
|
|
120
|
-
function onMessage(event: MessageEvent) {
|
|
121
|
-
const m = event.data as { source?: string; type?: string; token?: string; state?: unknown } | null;
|
|
122
|
-
if (!m || m.source !== HTML_SURFACE_PUSH_SOURCE || m.type !== 'ax-update' || m.token !== token) return;
|
|
123
|
-
window.__PMX_CANVAS_AX_STATE__ = m.state;
|
|
124
|
-
setAx(m.state);
|
|
125
|
-
}
|
|
126
|
-
window.addEventListener('message', onMessage);
|
|
127
|
-
return () => window.removeEventListener('message', onMessage);
|
|
128
|
-
}, [setAx]);
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
116
|
// AX interaction types a json-render spec can bind actions to. When an action
|
|
133
117
|
// named like one of these fires, we forward it to the parent canvas (which
|
|
134
118
|
// validates + submits through the capability-gated endpoint). Convention-based
|
|
@@ -188,7 +172,7 @@ function applyTheme(theme: unknown): void {
|
|
|
188
172
|
document.documentElement.style.colorScheme = theme === 'light' ? 'light' : 'dark';
|
|
189
173
|
}
|
|
190
174
|
|
|
191
|
-
function App() {
|
|
175
|
+
function App({ store }: { store: StateStore }) {
|
|
192
176
|
const spec = window.__PMX_CANVAS_JSON_RENDER_SPEC__;
|
|
193
177
|
|
|
194
178
|
if (!spec) {
|
|
@@ -208,11 +192,6 @@ function App() {
|
|
|
208
192
|
);
|
|
209
193
|
}
|
|
210
194
|
|
|
211
|
-
// Seed AX state under a reserved `/ax` key so specs can bind { "$state": "/ax/workItems" }.
|
|
212
|
-
const axState = window.__PMX_CANVAS_AX_STATE__;
|
|
213
|
-
const initialState =
|
|
214
|
-
axState !== undefined && axState !== null ? { ...(spec.state ?? {}), ax: axState } : (spec.state ?? undefined);
|
|
215
|
-
|
|
216
195
|
// Standalone "Open as site" tab (#65): fill the browser viewport instead of the
|
|
217
196
|
// in-canvas card height. The chart child flex-grows; useChartFrameHeight measures
|
|
218
197
|
// the full viewport in this mode. Embedded/expanded keep the padded min-height box.
|
|
@@ -229,13 +208,7 @@ function App() {
|
|
|
229
208
|
: { minHeight: '100vh', padding: 16, boxSizing: 'border-box' as const };
|
|
230
209
|
return (
|
|
231
210
|
<div style={containerStyle}>
|
|
232
|
-
<JSONUIProvider
|
|
233
|
-
registry={registry}
|
|
234
|
-
initialState={initialState}
|
|
235
|
-
directives={pmxCanvasDirectives}
|
|
236
|
-
handlers={buildAxHandlers()}
|
|
237
|
-
>
|
|
238
|
-
<AxStateSync />
|
|
211
|
+
<JSONUIProvider registry={registry} store={store} directives={pmxCanvasDirectives} handlers={buildAxHandlers()}>
|
|
239
212
|
<div style={isSite ? { flex: 1, minHeight: 0 } : undefined}>
|
|
240
213
|
<Renderer spec={spec} registry={registry} loading={false} />
|
|
241
214
|
</div>
|
|
@@ -248,5 +221,52 @@ function App() {
|
|
|
248
221
|
const root = document.getElementById('root');
|
|
249
222
|
if (root) {
|
|
250
223
|
syncPreferredTheme();
|
|
251
|
-
|
|
224
|
+
const stateToken = window.__PMX_CANVAS_UI_STATE_TOKEN__;
|
|
225
|
+
let store: StateStore | undefined;
|
|
226
|
+
const mount = (restored: Record<string, unknown> | null) => {
|
|
227
|
+
if (store) return;
|
|
228
|
+
const seed = restored ?? window.__PMX_CANVAS_JSON_RENDER_SPEC__?.state ?? {};
|
|
229
|
+
const ax = window.__PMX_CANVAS_AX_STATE__;
|
|
230
|
+
store = createStateStore(ax != null ? { ...seed, ax } : seed);
|
|
231
|
+
if (stateToken) {
|
|
232
|
+
// subscribe fires synchronously at the input/action write, not in a React
|
|
233
|
+
// effect that can be lost on unmount. Never mirror the server-owned /ax.
|
|
234
|
+
store.subscribe(() => {
|
|
235
|
+
const { ax: _ax, ...state } = store!.getSnapshot();
|
|
236
|
+
window.parent.postMessage(
|
|
237
|
+
{
|
|
238
|
+
source: JSON_VIEWER_STATE_SOURCE,
|
|
239
|
+
type: 'snapshot',
|
|
240
|
+
token: stateToken,
|
|
241
|
+
nodeId: window.__PMX_CANVAS_JSON_RENDER_NODE_ID__,
|
|
242
|
+
state,
|
|
243
|
+
},
|
|
244
|
+
'*',
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
createRoot(root).render(<App store={store} />);
|
|
249
|
+
};
|
|
250
|
+
// Install before document load. The parent supplies a snapshot (or null) in
|
|
251
|
+
// onLoad; restore BEFORE mounting so watchers don't replay past user actions.
|
|
252
|
+
// Standalone viewers have no state token and render immediately.
|
|
253
|
+
window.addEventListener('message', (event: MessageEvent) => {
|
|
254
|
+
if (event.source !== window.parent) return;
|
|
255
|
+
const m = event.data as { source?: string; type?: string; token?: string; state?: unknown } | null;
|
|
256
|
+
if (!m) return;
|
|
257
|
+
if (m.source === JSON_VIEWER_STATE_SOURCE && m.type === 'restore' && stateToken && m.token === stateToken) {
|
|
258
|
+
if (m.state === null || (typeof m.state === 'object' && !Array.isArray(m.state)))
|
|
259
|
+
mount(m.state as Record<string, unknown> | null);
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
m.source === HTML_SURFACE_PUSH_SOURCE &&
|
|
263
|
+
m.type === 'ax-update' &&
|
|
264
|
+
window.__PMX_CANVAS_AX_TOKEN__ &&
|
|
265
|
+
m.token === window.__PMX_CANVAS_AX_TOKEN__
|
|
266
|
+
) {
|
|
267
|
+
window.__PMX_CANVAS_AX_STATE__ = m.state;
|
|
268
|
+
store?.set('/ax', m.state);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
if (!stateToken) mount(null);
|
|
252
272
|
}
|
|
@@ -979,6 +979,8 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
979
979
|
nodeId?: string;
|
|
980
980
|
axToken?: string;
|
|
981
981
|
axState?: unknown;
|
|
982
|
+
/** Independent nonce for tab-local runtime state across focus transitions. */
|
|
983
|
+
stateToken?: string;
|
|
982
984
|
/** Nonce for the content-height reporter so the node can grow to fit the chart. */
|
|
983
985
|
frameToken?: string;
|
|
984
986
|
/** When true, charts render at their natural (intrinsic) height instead of
|
|
@@ -995,9 +997,14 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
995
997
|
...(options.theme ? [`window.__PMX_CANVAS_JSON_RENDER_THEME__ = ${JSON.stringify(options.theme)};`] : []),
|
|
996
998
|
...(options.display ? [`window.__PMX_CANVAS_JSON_RENDER_DISPLAY__ = ${JSON.stringify(options.display)};`] : []),
|
|
997
999
|
...(options.devtools ? ['window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ = true;'] : []),
|
|
1000
|
+
...(options.nodeId
|
|
1001
|
+
? [`window.__PMX_CANVAS_JSON_RENDER_NODE_ID__ = ${JSON.stringify(sanitizeAxValue(options.nodeId))};`]
|
|
1002
|
+
: []),
|
|
1003
|
+
...(options.stateToken
|
|
1004
|
+
? [`window.__PMX_CANVAS_UI_STATE_TOKEN__ = ${JSON.stringify(sanitizeAxValue(options.stateToken))};`]
|
|
1005
|
+
: []),
|
|
998
1006
|
...(options.nodeId && options.axToken
|
|
999
1007
|
? [
|
|
1000
|
-
`window.__PMX_CANVAS_JSON_RENDER_NODE_ID__ = ${JSON.stringify(sanitizeAxValue(options.nodeId))};`,
|
|
1001
1008
|
`window.__PMX_CANVAS_AX_TOKEN__ = ${JSON.stringify(sanitizeAxValue(options.axToken))};`,
|
|
1002
1009
|
// Read-side AX state: seed for initial render + bound under /ax for specs.
|
|
1003
1010
|
`window.__PMX_CANVAS_AX_STATE__ = ${JSON.stringify(options.axState ?? null).replace(/</g, '\\u003c')};`,
|
package/src/mcp/server.ts
CHANGED
|
@@ -797,8 +797,8 @@ export async function startMcpServer(): Promise<void> {
|
|
|
797
797
|
'canvas://boards',
|
|
798
798
|
{
|
|
799
799
|
description:
|
|
800
|
-
'The boards in this workspace with their categories, most recently opened first, and which one
|
|
801
|
-
'
|
|
800
|
+
'The boards in this workspace with their categories, most recently opened first, and which one is ' +
|
|
801
|
+
'open (activeBoardId; null = Home). Writes go to the open board; use canvas_board action "open" to switch the shared workbench.',
|
|
802
802
|
mimeType: 'application/json',
|
|
803
803
|
},
|
|
804
804
|
async () => {
|
|
@@ -278,10 +278,10 @@ export class AgentPresenceRegistry {
|
|
|
278
278
|
private attributedKey(input: PresenceTouch): string {
|
|
279
279
|
const own = presenceKey(input.source, input.agentId);
|
|
280
280
|
// Identified writers keep their key; attaches run the twin-merge in touch().
|
|
281
|
-
if (input.agentId?.trim() ||
|
|
281
|
+
if (input.agentId?.trim() || this.presences.get(own)?.attached) return own;
|
|
282
282
|
// A channel that attach-merged into a session stays that session's — its
|
|
283
|
-
// writes AND its detach belong to the merged session,
|
|
284
|
-
// sessions are attached.
|
|
283
|
+
// writes, repeat attaches AND its detach belong to the merged session,
|
|
284
|
+
// however many other sessions are attached.
|
|
285
285
|
const alias = this.aliases.get(own);
|
|
286
286
|
if (alias && this.presences.get(alias)?.attached) return alias;
|
|
287
287
|
if (input.attached !== undefined) return own;
|
|
@@ -338,15 +338,21 @@ export class AgentPresenceRegistry {
|
|
|
338
338
|
touch(input: PresenceTouch, now = Date.now()): AgentPresence {
|
|
339
339
|
let key = this.attributedKey(input);
|
|
340
340
|
const own = presenceKey(input.source, input.agentId);
|
|
341
|
-
if (input.attached === true && !input.agentId?.trim()) {
|
|
342
|
-
// One agent, two channels:
|
|
343
|
-
// attached
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
341
|
+
if (input.attached === true && !input.agentId?.trim() && key === own) {
|
|
342
|
+
// One agent, two channels: a transport attach belongs to the sole
|
|
343
|
+
// attached host even when the agent gives its task a different label.
|
|
344
|
+
// With several hosts, only the existing matching-label rule applies.
|
|
345
|
+
// Keep explicit agentIds separate, and preserve the host's consumer key
|
|
346
|
+
// so steering still reaches its poller. Browser placeholders are handled
|
|
347
|
+
// below, where their baseline and adopted-work rules are preserved.
|
|
347
348
|
const label = input.label ?? input.source;
|
|
348
|
-
const
|
|
349
|
-
|
|
349
|
+
const attached = [...this.presences.values()].filter((presence) => presence.attached);
|
|
350
|
+
const twin = attached.find(
|
|
351
|
+
(presence) =>
|
|
352
|
+
presence.sessionId !== own &&
|
|
353
|
+
!presence.agentId &&
|
|
354
|
+
(presence.label === label ||
|
|
355
|
+
(TRANSPORT_SOURCES.includes(input.source) && attached.length === 1 && presence.source !== 'browser')),
|
|
350
356
|
);
|
|
351
357
|
if (twin) {
|
|
352
358
|
this.aliases.set(own, twin.sessionId);
|