pmx-canvas 0.2.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +0 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +35 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +5 -2
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +12 -7
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +58 -56
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +40 -121
- package/src/mcp/server.ts +206 -271
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +185 -41
- package/src/server/ax-state.ts +142 -47
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +500 -102
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +155 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +20 -7
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +4 -20
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +321 -218
- package/src/server/intent-registry.ts +3 -6
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +38 -40
- package/src/server/operations/http.ts +5 -5
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +29 -36
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +102 -77
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +300 -111
- package/src/server/operations/ops/query.ts +62 -33
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +121 -1012
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/ax-intent.ts +1 -1
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
|
@@ -4,13 +4,7 @@ import { promptAndInsertLink, wrapSelectionInCode } from './inline-editor-comman
|
|
|
4
4
|
|
|
5
5
|
const GAP = 8;
|
|
6
6
|
|
|
7
|
-
type ExecCommand =
|
|
8
|
-
| 'bold'
|
|
9
|
-
| 'italic'
|
|
10
|
-
| 'strikeThrough'
|
|
11
|
-
| 'formatBlock'
|
|
12
|
-
| 'insertUnorderedList'
|
|
13
|
-
| 'insertOrderedList';
|
|
7
|
+
type ExecCommand = 'bold' | 'italic' | 'strikeThrough' | 'formatBlock' | 'insertUnorderedList' | 'insertOrderedList';
|
|
14
8
|
|
|
15
9
|
type BlockTag = 'H1' | 'H2' | 'H3' | 'P' | 'BLOCKQUOTE';
|
|
16
10
|
|
|
@@ -54,13 +48,7 @@ function runAction(action: Action): void {
|
|
|
54
48
|
|
|
55
49
|
/** Floating selection toolbar for a contentEditable host. Mounts once; while
|
|
56
50
|
* visible, positions itself above the current selection's viewport rect. */
|
|
57
|
-
export function InlineFormatBar({
|
|
58
|
-
hostRef,
|
|
59
|
-
onChange,
|
|
60
|
-
}: {
|
|
61
|
-
hostRef: RefObject<HTMLElement>;
|
|
62
|
-
onChange: () => void;
|
|
63
|
-
}) {
|
|
51
|
+
export function InlineFormatBar({ hostRef, onChange }: { hostRef: RefObject<HTMLElement>; onChange: () => void }) {
|
|
64
52
|
const [visible, setVisible] = useState(false);
|
|
65
53
|
const [pos, setPos] = useState({ top: 0, left: 0 });
|
|
66
54
|
const [barWidth, setBarWidth] = useState(0);
|
|
@@ -91,10 +79,7 @@ export function InlineFormatBar({
|
|
|
91
79
|
// Use measured width if we have it; fall back to a conservative estimate
|
|
92
80
|
// on the very first show before layout completes.
|
|
93
81
|
const width = barWidth || 420;
|
|
94
|
-
const left = Math.max(
|
|
95
|
-
GAP,
|
|
96
|
-
Math.min(rect.left + rect.width / 2 - width / 2, window.innerWidth - width - GAP),
|
|
97
|
-
);
|
|
82
|
+
const left = Math.max(GAP, Math.min(rect.left + rect.width / 2 - width / 2, window.innerWidth - width - GAP));
|
|
98
83
|
const top = Math.max(GAP, rect.top - GAP - 36);
|
|
99
84
|
setPos({ top, left });
|
|
100
85
|
setVisible(true);
|
|
@@ -160,9 +145,7 @@ export function InlineFormatBar({
|
|
|
160
145
|
{a.icon}
|
|
161
146
|
</button>
|
|
162
147
|
);
|
|
163
|
-
return a.dividerBefore
|
|
164
|
-
? [<span key={`div-${i}`} class="md-inline-format-divider" />, btn]
|
|
165
|
-
: [btn];
|
|
148
|
+
return a.dividerBefore ? [<span key={`div-${i}`} class="md-inline-format-divider" />, btn] : [btn];
|
|
166
149
|
})}
|
|
167
150
|
</div>
|
|
168
151
|
);
|
|
@@ -20,9 +20,7 @@ export function LedgerNode({ node }: { node: CanvasNodeState }) {
|
|
|
20
20
|
const entries = Object.entries(data).filter(([key]) => !HIDDEN_LEDGER_KEYS.has(key));
|
|
21
21
|
|
|
22
22
|
if (lines.length === 0 && entries.length === 0) {
|
|
23
|
-
return (
|
|
24
|
-
<div style={{ color: 'var(--c-dim)', fontSize: '12px', fontStyle: 'italic' }}>No ledger data</div>
|
|
25
|
-
);
|
|
23
|
+
return <div style={{ color: 'var(--c-dim)', fontSize: '12px', fontStyle: 'italic' }}>No ledger data</div>;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
return (
|
|
@@ -61,7 +59,15 @@ export function LedgerNode({ node }: { node: CanvasNodeState }) {
|
|
|
61
59
|
<span style={{ color: 'var(--c-muted)', fontSize: '11px', flexShrink: 0 }}>
|
|
62
60
|
{key.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase())}
|
|
63
61
|
</span>
|
|
64
|
-
<span
|
|
62
|
+
<span
|
|
63
|
+
style={{
|
|
64
|
+
color: 'var(--c-text)',
|
|
65
|
+
fontFamily: 'var(--mono)',
|
|
66
|
+
fontSize: '11px',
|
|
67
|
+
textAlign: 'right',
|
|
68
|
+
wordBreak: 'break-word',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
65
71
|
{typeof value === 'object' ? JSON.stringify(value) : String(value ?? '—')}
|
|
66
72
|
</span>
|
|
67
73
|
</div>
|
|
@@ -31,10 +31,7 @@ function RenderedMarkdown({
|
|
|
31
31
|
return <div ref={containerRef} class={className} style={style} />;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function MarkdownNode({
|
|
35
|
-
node,
|
|
36
|
-
expanded = false,
|
|
37
|
-
}: { node: CanvasNodeState; expanded?: boolean }) {
|
|
34
|
+
export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
38
35
|
const path = node.data.path as string;
|
|
39
36
|
const [content, setContent] = useState('');
|
|
40
37
|
const [rendered, setRendered] = useState('');
|
|
@@ -302,9 +299,7 @@ export function MarkdownNode({
|
|
|
302
299
|
onSave={handleInlineSave}
|
|
303
300
|
/>
|
|
304
301
|
) : (
|
|
305
|
-
<div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '24px' }}>
|
|
306
|
-
Loading…
|
|
307
|
-
</div>
|
|
302
|
+
<div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '24px' }}>Loading…</div>
|
|
308
303
|
)}
|
|
309
304
|
</div>
|
|
310
305
|
<button type="button" class="md-edit-fab" onClick={() => setSourceMode(true)}>
|
|
@@ -323,9 +318,7 @@ export function MarkdownNode({
|
|
|
323
318
|
html={rendered}
|
|
324
319
|
style={{ padding: rendered ? '0' : '12px', color: rendered ? undefined : 'var(--c-dim)' }}
|
|
325
320
|
/>
|
|
326
|
-
{!loaded && (
|
|
327
|
-
<div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>Loading…</div>
|
|
328
|
-
)}
|
|
321
|
+
{!loaded && <div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>Loading…</div>}
|
|
329
322
|
{loaded && !rendered && (
|
|
330
323
|
<div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>Empty node</div>
|
|
331
324
|
)}
|
|
@@ -43,8 +43,7 @@ export function isSameOriginFrameDocumentUrl(url: string, origin = window.locati
|
|
|
43
43
|
try {
|
|
44
44
|
const baseOrigin = new URL(origin).origin;
|
|
45
45
|
const resolved = new URL(url, baseOrigin);
|
|
46
|
-
return resolved.origin === baseOrigin &&
|
|
47
|
-
resolved.pathname.startsWith('/api/canvas/frame-documents/');
|
|
46
|
+
return resolved.origin === baseOrigin && resolved.pathname.startsWith('/api/canvas/frame-documents/');
|
|
48
47
|
} catch {
|
|
49
48
|
return false;
|
|
50
49
|
}
|
|
@@ -89,7 +88,10 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
89
88
|
function onAxMessage(event: MessageEvent) {
|
|
90
89
|
if (event.source !== iframeRef.current?.contentWindow) return;
|
|
91
90
|
const data = event.data as {
|
|
92
|
-
source?: string;
|
|
91
|
+
source?: string;
|
|
92
|
+
token?: string;
|
|
93
|
+
nodeId?: string;
|
|
94
|
+
correlationId?: string;
|
|
93
95
|
interaction?: { type?: unknown; payload?: unknown };
|
|
94
96
|
} | null;
|
|
95
97
|
if (!data || data.source !== 'pmx-canvas-ax' || data.token !== axToken || data.nodeId !== node.id) return;
|
|
@@ -107,13 +109,16 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
107
109
|
if (res.ok) showToast('context', 'AX interaction', interactionType, [node.id]);
|
|
108
110
|
else showToast('remove', 'AX interaction rejected', res.error ?? res.code ?? '', [node.id]);
|
|
109
111
|
// Report #55: ack back to the viewer so the surface can self-confirm.
|
|
110
|
-
iframeRef.current?.contentWindow?.postMessage(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
113
|
+
{
|
|
114
|
+
source: 'pmx-canvas-ax-ack',
|
|
115
|
+
token: axToken,
|
|
116
|
+
...(data.correlationId ? { correlationId: data.correlationId } : {}),
|
|
117
|
+
interaction: { type: interactionType },
|
|
118
|
+
result: res,
|
|
119
|
+
},
|
|
120
|
+
'*',
|
|
121
|
+
);
|
|
117
122
|
});
|
|
118
123
|
}
|
|
119
124
|
window.addEventListener('message', onAxMessage);
|
|
@@ -125,12 +130,15 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
125
130
|
const axStateValue = axSurfaceState.value;
|
|
126
131
|
const pushAxState = () => {
|
|
127
132
|
if (!isAxViewer || !axToken || axStateValue == null) return;
|
|
128
|
-
iframeRef.current?.contentWindow?.postMessage(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
134
|
+
{
|
|
135
|
+
source: 'pmx-canvas-html-node',
|
|
136
|
+
type: 'ax-update',
|
|
137
|
+
token: axToken,
|
|
138
|
+
state: axStateValue,
|
|
139
|
+
},
|
|
140
|
+
'*',
|
|
141
|
+
);
|
|
134
142
|
};
|
|
135
143
|
useEffect(pushAxState, [isAxViewer, axToken, axStateValue]);
|
|
136
144
|
|
|
@@ -156,9 +164,7 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
156
164
|
<span>⚠</span>
|
|
157
165
|
<span>Cannot embed — opened externally</span>
|
|
158
166
|
</div>
|
|
159
|
-
{fallbackReason && (
|
|
160
|
-
<div style={{ color: 'var(--c-muted)', fontSize: '11px' }}>Reason: {fallbackReason}</div>
|
|
161
|
-
)}
|
|
167
|
+
{fallbackReason && <div style={{ color: 'var(--c-muted)', fontSize: '11px' }}>Reason: {fallbackReason}</div>}
|
|
162
168
|
<a
|
|
163
169
|
href={url}
|
|
164
170
|
target="_blank"
|
|
@@ -171,9 +177,7 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
171
177
|
>
|
|
172
178
|
{url}
|
|
173
179
|
</a>
|
|
174
|
-
{sourceServer && (
|
|
175
|
-
<div style={{ color: 'var(--c-dim)', fontSize: '10px' }}>Source: {sourceServer}</div>
|
|
176
|
-
)}
|
|
180
|
+
{sourceServer && <div style={{ color: 'var(--c-dim)', fontSize: '10px' }}>Source: {sourceServer}</div>}
|
|
177
181
|
</div>
|
|
178
182
|
);
|
|
179
183
|
}
|
|
@@ -62,13 +62,16 @@ export function MdFormatBar({ textareaRef }: { textareaRef: { current: HTMLTextA
|
|
|
62
62
|
};
|
|
63
63
|
}, [textareaRef, updatePosition]);
|
|
64
64
|
|
|
65
|
-
const runAction = useCallback(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
const runAction = useCallback(
|
|
66
|
+
(action: FormatAction) => {
|
|
67
|
+
const ta = textareaRef.current;
|
|
68
|
+
if (ta) {
|
|
69
|
+
action.action(ta);
|
|
70
|
+
ta.focus();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[textareaRef],
|
|
74
|
+
);
|
|
72
75
|
|
|
73
76
|
if (!visible) return null;
|
|
74
77
|
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { updateNodeData } from '../state/canvas-store';
|
|
3
|
-
import {
|
|
4
|
-
fetchSlashCommands,
|
|
5
|
-
renderMarkdown,
|
|
6
|
-
submitCanvasPrompt,
|
|
7
|
-
submitThreadReply,
|
|
8
|
-
} from '../state/intent-bridge';
|
|
3
|
+
import { fetchSlashCommands, renderMarkdown, submitCanvasPrompt, submitThreadReply } from '../state/intent-bridge';
|
|
9
4
|
import type { CanvasNodeState } from '../types';
|
|
10
5
|
|
|
11
6
|
// Cached slash commands — fetched once on first use.
|
|
@@ -16,10 +11,7 @@ async function getCommands() {
|
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
/** Find the best matching slash command for a query prefix. */
|
|
19
|
-
function matchSlashCommand(
|
|
20
|
-
query: string,
|
|
21
|
-
commands: Array<{ name: string }>,
|
|
22
|
-
): string | null {
|
|
14
|
+
function matchSlashCommand(query: string, commands: Array<{ name: string }>): string | null {
|
|
23
15
|
if (!query) return null;
|
|
24
16
|
const lower = query.toLowerCase();
|
|
25
17
|
const exact = commands.find((c) => c.name.toLowerCase() === lower);
|
|
@@ -121,15 +113,10 @@ function ErrorBanner({ message, onDismiss }: { message: string | null; onDismiss
|
|
|
121
113
|
);
|
|
122
114
|
}
|
|
123
115
|
|
|
124
|
-
export function PromptNode({
|
|
125
|
-
node,
|
|
126
|
-
expanded = false,
|
|
127
|
-
}: { node: CanvasNodeState; expanded?: boolean }) {
|
|
116
|
+
export function PromptNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
128
117
|
// Backward compat: old canvas states have flat { text, status } data.
|
|
129
118
|
// New threads use { turns[], threadStatus }. Detect format by presence of turns array.
|
|
130
|
-
const turns: ThreadTurn[] = Array.isArray(node.data.turns)
|
|
131
|
-
? (node.data.turns as ThreadTurn[])
|
|
132
|
-
: [];
|
|
119
|
+
const turns: ThreadTurn[] = Array.isArray(node.data.turns) ? (node.data.turns as ThreadTurn[]) : [];
|
|
133
120
|
const isLegacy = turns.length === 0;
|
|
134
121
|
const legacyText = (node.data.text as string) || '';
|
|
135
122
|
const legacyStatus = (node.data.status as string) || 'draft';
|
|
@@ -201,7 +188,9 @@ export function PromptNode({
|
|
|
201
188
|
setRenderedTurns(next);
|
|
202
189
|
});
|
|
203
190
|
|
|
204
|
-
return () => {
|
|
191
|
+
return () => {
|
|
192
|
+
cancelled = true;
|
|
193
|
+
};
|
|
205
194
|
};
|
|
206
195
|
|
|
207
196
|
if (!hasStreaming) {
|
|
@@ -243,9 +232,7 @@ export function PromptNode({
|
|
|
243
232
|
text: trimmed, // keep for backward compat display
|
|
244
233
|
});
|
|
245
234
|
setDraft('');
|
|
246
|
-
const ctxIds = Array.isArray(node.data.contextNodeIds)
|
|
247
|
-
? (node.data.contextNodeIds as string[])
|
|
248
|
-
: undefined;
|
|
235
|
+
const ctxIds = Array.isArray(node.data.contextNodeIds) ? (node.data.contextNodeIds as string[]) : undefined;
|
|
249
236
|
const result = await submitCanvasPrompt(trimmed, node.position, node.id, ctxIds, node.id);
|
|
250
237
|
if (!result.ok) {
|
|
251
238
|
updateNodeData(node.id, { turns: [], threadStatus: 'draft', text: '', status: 'draft' });
|
|
@@ -260,9 +247,7 @@ export function PromptNode({
|
|
|
260
247
|
const savedReply = replyDraft;
|
|
261
248
|
setError(null);
|
|
262
249
|
// Optimistically add user turn
|
|
263
|
-
const currentTurns = Array.isArray(node.data.turns)
|
|
264
|
-
? [...(node.data.turns as ThreadTurn[])]
|
|
265
|
-
: [];
|
|
250
|
+
const currentTurns = Array.isArray(node.data.turns) ? [...(node.data.turns as ThreadTurn[])] : [];
|
|
266
251
|
currentTurns.push({ role: 'user', text: trimmed, status: 'pending' });
|
|
267
252
|
updateNodeData(node.id, { turns: currentTurns, threadStatus: 'pending' });
|
|
268
253
|
setReplyDraft('');
|
|
@@ -319,17 +304,12 @@ export function PromptNode({
|
|
|
319
304
|
[handleReplySubmit, replyDraft],
|
|
320
305
|
);
|
|
321
306
|
|
|
322
|
-
const ctxCount = Array.isArray(node.data.contextNodeIds)
|
|
323
|
-
? (node.data.contextNodeIds as string[]).length
|
|
324
|
-
: 0;
|
|
307
|
+
const ctxCount = Array.isArray(node.data.contextNodeIds) ? (node.data.contextNodeIds as string[]).length : 0;
|
|
325
308
|
|
|
326
309
|
// ── Draft mode (no turns yet): show initial textarea ──
|
|
327
310
|
if (isDraft && turns.length === 0) {
|
|
328
311
|
return (
|
|
329
|
-
<div
|
|
330
|
-
class="prompt-node-inner"
|
|
331
|
-
style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: '8px' }}
|
|
332
|
-
>
|
|
312
|
+
<div class="prompt-node-inner" style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: '8px' }}>
|
|
333
313
|
<ContextBadge count={ctxCount} />
|
|
334
314
|
<ErrorBanner message={error} onDismiss={() => setError(null)} />
|
|
335
315
|
<textarea
|
|
@@ -380,10 +360,7 @@ export function PromptNode({
|
|
|
380
360
|
// ── Legacy flat format (old nodes without turns) ──
|
|
381
361
|
if (isLegacy) {
|
|
382
362
|
return (
|
|
383
|
-
<div
|
|
384
|
-
class="prompt-node-inner"
|
|
385
|
-
style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
|
|
386
|
-
>
|
|
363
|
+
<div class="prompt-node-inner" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
387
364
|
<div
|
|
388
365
|
style={{
|
|
389
366
|
flex: 1,
|
|
@@ -425,10 +402,7 @@ export function PromptNode({
|
|
|
425
402
|
|
|
426
403
|
// ── Thread view: render all turns ──
|
|
427
404
|
return (
|
|
428
|
-
<div
|
|
429
|
-
class="prompt-node-inner"
|
|
430
|
-
style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
|
|
431
|
-
>
|
|
405
|
+
<div class="prompt-node-inner" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
432
406
|
<ContextBadge count={ctxCount} />
|
|
433
407
|
<ErrorBanner message={error} onDismiss={() => setError(null)} />
|
|
434
408
|
|
|
@@ -439,7 +413,10 @@ export function PromptNode({
|
|
|
439
413
|
{i > 0 && <div class="thread-turn-divider" />}
|
|
440
414
|
{turn.role === 'user' ? (
|
|
441
415
|
<div class="thread-turn-user">
|
|
442
|
-
<div class="thread-turn-role"
|
|
416
|
+
<div class="thread-turn-role">
|
|
417
|
+
<span class="status-dot" />
|
|
418
|
+
You
|
|
419
|
+
</div>
|
|
443
420
|
<div
|
|
444
421
|
style={{
|
|
445
422
|
fontSize: expanded ? '15px' : '13px',
|
|
@@ -454,7 +431,10 @@ export function PromptNode({
|
|
|
454
431
|
</div>
|
|
455
432
|
) : (
|
|
456
433
|
<div class="thread-turn-assistant">
|
|
457
|
-
<div class="thread-turn-role"
|
|
434
|
+
<div class="thread-turn-role">
|
|
435
|
+
<span class={`status-dot${turn.status === 'streaming' ? ' pulsing' : ''}`} />
|
|
436
|
+
PMX
|
|
437
|
+
</div>
|
|
458
438
|
{turn.status === 'streaming' && !turn.text && (
|
|
459
439
|
<div
|
|
460
440
|
style={{
|
|
@@ -481,9 +461,7 @@ export function PromptNode({
|
|
|
481
461
|
</>
|
|
482
462
|
) : (
|
|
483
463
|
<div style={{ color: 'var(--c-muted)', fontStyle: 'italic' }}>
|
|
484
|
-
{turn.status === 'streaming'
|
|
485
|
-
? 'Waiting for response…'
|
|
486
|
-
: turn.text || 'Empty response'}
|
|
464
|
+
{turn.status === 'streaming' ? 'Waiting for response…' : turn.text || 'Empty response'}
|
|
487
465
|
</div>
|
|
488
466
|
)}
|
|
489
467
|
</div>
|
|
@@ -525,13 +503,7 @@ export function PromptNode({
|
|
|
525
503
|
: 'var(--c-muted)',
|
|
526
504
|
}}
|
|
527
505
|
>
|
|
528
|
-
{isStreaming
|
|
529
|
-
? 'Streaming…'
|
|
530
|
-
: isPending
|
|
531
|
-
? 'Sending…'
|
|
532
|
-
: isAnswered
|
|
533
|
-
? 'Answered'
|
|
534
|
-
: threadStatus}
|
|
506
|
+
{isStreaming ? 'Streaming…' : isPending ? 'Sending…' : isAnswered ? 'Answered' : threadStatus}
|
|
535
507
|
</span>
|
|
536
508
|
<span style={{ fontSize: '10px', color: 'var(--c-muted)' }}>
|
|
537
509
|
{turns.length} turn{turns.length !== 1 ? 's' : ''}
|
|
@@ -32,10 +32,7 @@ function RenderedMarkdown({ html }: { html: string }) {
|
|
|
32
32
|
return <div ref={containerRef} />;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function ResponseNode({
|
|
36
|
-
node,
|
|
37
|
-
expanded = false,
|
|
38
|
-
}: { node: CanvasNodeState; expanded?: boolean }) {
|
|
35
|
+
export function ResponseNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
39
36
|
const content = (node.data.content as string) || '';
|
|
40
37
|
const status = (node.data.status as string) || 'streaming';
|
|
41
38
|
const [rendered, setRendered] = useState('');
|
|
@@ -59,18 +56,11 @@ export function ResponseNode({
|
|
|
59
56
|
}, [content]);
|
|
60
57
|
|
|
61
58
|
const handleReply = useCallback(() => {
|
|
62
|
-
submitCanvasPrompt(
|
|
63
|
-
'',
|
|
64
|
-
{ x: node.position.x, y: node.position.y + node.size.height + 24 },
|
|
65
|
-
node.id,
|
|
66
|
-
);
|
|
59
|
+
submitCanvasPrompt('', { x: node.position.x, y: node.position.y + node.size.height + 24 }, node.id);
|
|
67
60
|
}, [node]);
|
|
68
61
|
|
|
69
62
|
return (
|
|
70
|
-
<div
|
|
71
|
-
class="response-node-inner"
|
|
72
|
-
style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
|
|
73
|
-
>
|
|
63
|
+
<div class="response-node-inner" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
74
64
|
{/* Streaming indicator */}
|
|
75
65
|
{isStreaming && (
|
|
76
66
|
<div
|
|
@@ -2,17 +2,13 @@ import { PHASE_COLORS } from '../theme/tokens';
|
|
|
2
2
|
import type { CanvasNodeState } from '../types';
|
|
3
3
|
|
|
4
4
|
export function getStatusDisplayPhase(node: CanvasNodeState): string {
|
|
5
|
-
const phase = typeof node.data.phase === 'string' && node.data.phase.trim().length > 0
|
|
6
|
-
? node.data.phase.trim()
|
|
7
|
-
: '';
|
|
5
|
+
const phase = typeof node.data.phase === 'string' && node.data.phase.trim().length > 0 ? node.data.phase.trim() : '';
|
|
8
6
|
if (phase) return phase;
|
|
9
|
-
const content =
|
|
10
|
-
? node.data.content.trim()
|
|
11
|
-
: '';
|
|
7
|
+
const content =
|
|
8
|
+
typeof node.data.content === 'string' && node.data.content.trim().length > 0 ? node.data.content.trim() : '';
|
|
12
9
|
if (content) return content;
|
|
13
|
-
const status =
|
|
14
|
-
? node.data.status.trim()
|
|
15
|
-
: '';
|
|
10
|
+
const status =
|
|
11
|
+
typeof node.data.status === 'string' && node.data.status.trim().length > 0 ? node.data.status.trim() : '';
|
|
16
12
|
return status || 'idle';
|
|
17
13
|
}
|
|
18
14
|
|
|
@@ -21,15 +21,9 @@ export function StatusSummary({ node }: { node: CanvasNodeState }) {
|
|
|
21
21
|
flexShrink: 0,
|
|
22
22
|
}}
|
|
23
23
|
/>
|
|
24
|
-
<span
|
|
25
|
-
style={{ color: phaseColor, fontSize: '10px', textTransform: 'uppercase', fontWeight: 600 }}
|
|
26
|
-
>
|
|
27
|
-
{phase}
|
|
28
|
-
</span>
|
|
24
|
+
<span style={{ color: phaseColor, fontSize: '10px', textTransform: 'uppercase', fontWeight: 600 }}>{phase}</span>
|
|
29
25
|
{activeTool && (
|
|
30
|
-
<span style={{ color: 'var(--c-warn)', fontSize: '10px', fontFamily: 'var(--mono)' }}>
|
|
31
|
-
⚙ {activeTool}
|
|
32
|
-
</span>
|
|
26
|
+
<span style={{ color: 'var(--c-warn)', fontSize: '10px', fontFamily: 'var(--mono)' }}>⚙ {activeTool}</span>
|
|
33
27
|
)}
|
|
34
28
|
{subagent && subagent.state !== 'completed' && (
|
|
35
29
|
<span style={{ color: 'var(--c-subagent)', fontSize: '10px' }}>⠉ {subagent.name}</span>
|
|
@@ -20,11 +20,12 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
20
20
|
const url = typeof node.data.url === 'string' ? node.data.url : '';
|
|
21
21
|
const pageTitle = typeof node.data.pageTitle === 'string' ? node.data.pageTitle : '';
|
|
22
22
|
const description = typeof node.data.description === 'string' ? node.data.description : '';
|
|
23
|
-
const excerpt =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const excerpt =
|
|
24
|
+
typeof node.data.excerpt === 'string'
|
|
25
|
+
? node.data.excerpt
|
|
26
|
+
: typeof node.data.content === 'string'
|
|
27
|
+
? node.data.content
|
|
28
|
+
: '';
|
|
28
29
|
const status = typeof node.data.status === 'string' ? node.data.status : 'idle';
|
|
29
30
|
const error = typeof node.data.error === 'string' ? node.data.error : '';
|
|
30
31
|
const fetchedAt = formatFetchedAt(typeof node.data.fetchedAt === 'string' ? node.data.fetchedAt : undefined);
|
|
@@ -55,12 +56,7 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
55
56
|
return <div style={{ color: 'var(--c-dim)', fontStyle: 'italic', padding: '12px' }}>No webpage URL set</div>;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
const statusTone =
|
|
59
|
-
status === 'ready'
|
|
60
|
-
? 'var(--c-ok)'
|
|
61
|
-
: status === 'error'
|
|
62
|
-
? 'var(--c-danger)'
|
|
63
|
-
: 'var(--c-warn)';
|
|
59
|
+
const statusTone = status === 'ready' ? 'var(--c-ok)' : status === 'error' ? 'var(--c-danger)' : 'var(--c-warn)';
|
|
64
60
|
|
|
65
61
|
return (
|
|
66
62
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', height: '100%' }}>
|
|
@@ -123,7 +119,9 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
123
119
|
/>
|
|
124
120
|
)}
|
|
125
121
|
{description && (
|
|
126
|
-
<p
|
|
122
|
+
<p
|
|
123
|
+
style={{ margin: 0, color: 'var(--c-text-soft)', lineHeight: 1.5, fontSize: expanded ? '14px' : '12px' }}
|
|
124
|
+
>
|
|
127
125
|
{description}
|
|
128
126
|
</p>
|
|
129
127
|
)}
|
|
@@ -232,7 +230,8 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
232
230
|
background: 'rgba(255,255,255,0.03)',
|
|
233
231
|
}}
|
|
234
232
|
>
|
|
235
|
-
Live preview (best effort). If this stays blank, the site likely blocks framing. The cached text snapshot
|
|
233
|
+
Live preview (best effort). If this stays blank, the site likely blocks framing. The cached text snapshot
|
|
234
|
+
below still works.
|
|
236
235
|
</div>
|
|
237
236
|
<iframe
|
|
238
237
|
class="webpage-node-iframe"
|
|
@@ -274,7 +273,14 @@ export function WebpageNode({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
274
273
|
}}
|
|
275
274
|
>
|
|
276
275
|
{excerpt ? (
|
|
277
|
-
<div
|
|
276
|
+
<div
|
|
277
|
+
style={{
|
|
278
|
+
whiteSpace: 'pre-wrap',
|
|
279
|
+
lineHeight: 1.55,
|
|
280
|
+
color: 'var(--c-text)',
|
|
281
|
+
fontSize: expanded ? '14px' : '12px',
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
278
284
|
{excerpt}
|
|
279
285
|
</div>
|
|
280
286
|
) : (
|
|
@@ -7,12 +7,14 @@ interface FrameDocumentCreateResponse {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
function isFrameDocumentCreateResponse(value: unknown): value is FrameDocumentCreateResponse {
|
|
10
|
-
return
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
&&
|
|
14
|
-
&&
|
|
15
|
-
|
|
10
|
+
return (
|
|
11
|
+
Boolean(value) &&
|
|
12
|
+
typeof value === 'object' &&
|
|
13
|
+
value !== null &&
|
|
14
|
+
!Array.isArray(value) &&
|
|
15
|
+
'ok' in value &&
|
|
16
|
+
typeof (value as { ok: unknown }).ok === 'boolean'
|
|
17
|
+
);
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export async function createIframeDocumentUrl(html: string, sandbox: string): Promise<string> {
|
|
@@ -21,17 +23,21 @@ export async function createIframeDocumentUrl(html: string, sandbox: string): Pr
|
|
|
21
23
|
headers: { 'Content-Type': 'application/json' },
|
|
22
24
|
body: JSON.stringify({ html, sandbox }),
|
|
23
25
|
});
|
|
24
|
-
const json = await response.json() as unknown;
|
|
26
|
+
const json = (await response.json()) as unknown;
|
|
25
27
|
if (!response.ok || !isFrameDocumentCreateResponse(json) || !json.ok || typeof json.url !== 'string') {
|
|
26
|
-
const message =
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const message =
|
|
29
|
+
isFrameDocumentCreateResponse(json) && json.error
|
|
30
|
+
? json.error
|
|
31
|
+
: `Frame document request failed with HTTP ${response.status}`;
|
|
29
32
|
throw new Error(message);
|
|
30
33
|
}
|
|
31
34
|
return json.url;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
export function useIframeDocument(
|
|
37
|
+
export function useIframeDocument(
|
|
38
|
+
html: string,
|
|
39
|
+
sandbox: string,
|
|
40
|
+
): { attributes: { src?: string }; ready: boolean; key: string } {
|
|
35
41
|
const [src, setSrc] = useState<string | null>(null);
|
|
36
42
|
|
|
37
43
|
useEffect(() => {
|
|
@@ -50,9 +56,12 @@ export function useIframeDocument(html: string, sandbox: string): { attributes:
|
|
|
50
56
|
};
|
|
51
57
|
}, [html, sandbox]);
|
|
52
58
|
|
|
53
|
-
return useMemo(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
return useMemo(
|
|
60
|
+
() => ({
|
|
61
|
+
attributes: src ? { src } : {},
|
|
62
|
+
ready: Boolean(src),
|
|
63
|
+
key: src ?? '',
|
|
64
|
+
}),
|
|
65
|
+
[src],
|
|
66
|
+
);
|
|
58
67
|
}
|
|
@@ -52,13 +52,7 @@ function metadataWarnings(node: CanvasNodeState): ImageNodeWarning[] {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function looksLikeLoginCapture(node: CanvasNodeState): boolean {
|
|
55
|
-
const haystack = [
|
|
56
|
-
node.data.title,
|
|
57
|
-
node.data.caption,
|
|
58
|
-
node.data.alt,
|
|
59
|
-
node.data.src,
|
|
60
|
-
node.data.path,
|
|
61
|
-
]
|
|
55
|
+
const haystack = [node.data.title, node.data.caption, node.data.alt, node.data.src, node.data.path]
|
|
62
56
|
.map((value) => readTrimmedString(value)?.toLowerCase() ?? '')
|
|
63
57
|
.join(' ');
|
|
64
58
|
|
|
@@ -168,11 +168,26 @@ export function getSelectionRect(ta: HTMLTextAreaElement): { top: number; left:
|
|
|
168
168
|
const mirror = document.createElement('div');
|
|
169
169
|
const style = getComputedStyle(ta);
|
|
170
170
|
const props = [
|
|
171
|
-
'fontFamily',
|
|
172
|
-
'
|
|
173
|
-
'
|
|
174
|
-
'
|
|
175
|
-
'
|
|
171
|
+
'fontFamily',
|
|
172
|
+
'fontSize',
|
|
173
|
+
'fontWeight',
|
|
174
|
+
'lineHeight',
|
|
175
|
+
'letterSpacing',
|
|
176
|
+
'wordSpacing',
|
|
177
|
+
'textIndent',
|
|
178
|
+
'whiteSpace',
|
|
179
|
+
'wordWrap',
|
|
180
|
+
'overflowWrap',
|
|
181
|
+
'paddingTop',
|
|
182
|
+
'paddingRight',
|
|
183
|
+
'paddingBottom',
|
|
184
|
+
'paddingLeft',
|
|
185
|
+
'borderTopWidth',
|
|
186
|
+
'borderRightWidth',
|
|
187
|
+
'borderBottomWidth',
|
|
188
|
+
'borderLeftWidth',
|
|
189
|
+
'boxSizing',
|
|
190
|
+
'width',
|
|
176
191
|
] as const;
|
|
177
192
|
for (const p of props) {
|
|
178
193
|
mirror.style[p] = style[p];
|