pmx-canvas 0.7.1 → 0.7.2
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 +16 -1
- package/Readme.md +31 -6
- package/dist/canvas/global.css +21 -5
- package/dist/canvas/index.js +73 -73
- 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/shared/delimited-text.d.ts +14 -0
- 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 +19 -6
- package/skills/pmx-canvas/evals/evals.json +43 -2
- package/skills/pmx-canvas/references/full-reference.md +6 -1
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +17 -3
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- 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 +17 -8
- package/src/client/canvas/CommandBar.tsx +2 -2
- 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 +2 -1
- 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 +2 -1
- 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/PromptNode.tsx +3 -1
- package/src/client/theme/global.css +21 -5
- 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 +10 -3
- package/src/shared/delimited-text.ts +75 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'preact/hooks';
|
|
2
|
-
import { IconSteer } from '../icons';
|
|
2
|
+
import { IconSteer, IconClose } from '../icons';
|
|
3
3
|
import { contextPinnedNodeIds, nodes, toggleContextPin } from '../state/canvas-store';
|
|
4
4
|
import { steerableAgents } from '../state/presence-store';
|
|
5
5
|
import { sendSteering } from '../state/session-store';
|
|
@@ -72,7 +72,7 @@ export function CommandBar() {
|
|
|
72
72
|
title="Remove from context"
|
|
73
73
|
onClick={() => toggleContextPin(id)}
|
|
74
74
|
>
|
|
75
|
-
|
|
75
|
+
<IconClose size={14} />
|
|
76
76
|
</button>
|
|
77
77
|
</span>
|
|
78
78
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { clearContextPins, contextPinnedNodeIds } from '../state/canvas-store';
|
|
2
2
|
import { attentionHistoryOpen } from '../state/attention-store';
|
|
3
|
+
import { IconClose } from '../icons';
|
|
3
4
|
|
|
4
5
|
export function ContextPinBar() {
|
|
5
6
|
const count = contextPinnedNodeIds.value.size;
|
|
@@ -15,8 +16,9 @@ export function ContextPinBar() {
|
|
|
15
16
|
class="context-pin-bar-btn context-pin-bar-clear"
|
|
16
17
|
onClick={clearContextPins}
|
|
17
18
|
title="Clear all context pins"
|
|
19
|
+
aria-label="Clear all context pins"
|
|
18
20
|
>
|
|
19
|
-
{
|
|
21
|
+
<IconClose size={14} />
|
|
20
22
|
</button>
|
|
21
23
|
</div>
|
|
22
24
|
);
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
pendingExpandedNodeCloseId,
|
|
26
26
|
toggleContextPin,
|
|
27
27
|
} from '../state/canvas-store';
|
|
28
|
-
import { getNodeIcon } from '../icons';
|
|
28
|
+
import { getNodeIcon, IconClose, IconExternalLink } from '../icons';
|
|
29
29
|
import { useFocusTrap } from './use-focus-trap';
|
|
30
30
|
import { TYPE_LABELS } from '../types';
|
|
31
31
|
import type { CanvasNodeState } from '../types';
|
|
@@ -316,7 +316,7 @@ export function ExpandedNodeOverlay() {
|
|
|
316
316
|
onClick={() => void openNodeAsSite(node)}
|
|
317
317
|
title="Open as a full-page site in the system browser"
|
|
318
318
|
>
|
|
319
|
-
Open in tab
|
|
319
|
+
Open in tab <IconExternalLink size={14} />
|
|
320
320
|
</button>
|
|
321
321
|
)}
|
|
322
322
|
{canPresent && (
|
|
@@ -331,7 +331,7 @@ export function ExpandedNodeOverlay() {
|
|
|
331
331
|
)}
|
|
332
332
|
</div>
|
|
333
333
|
<button type="button" class="expanded-close" onClick={handleClose} title="Close (Esc)" aria-label="Close">
|
|
334
|
-
|
|
334
|
+
<IconClose />
|
|
335
335
|
</button>
|
|
336
336
|
</div>
|
|
337
337
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import { IconClose, IconExternalLink } from '../icons';
|
|
2
3
|
import type { AgentPresence } from '../../shared/agent-presence.js';
|
|
3
4
|
import { vetoGhostIntent } from '../state/intent-bridge';
|
|
4
5
|
import { type ClientIntent, intents, removeIntent } from '../state/intent-store';
|
|
@@ -149,7 +150,7 @@ export function ActivityFeed() {
|
|
|
149
150
|
activityFeedOpen.value = false;
|
|
150
151
|
}}
|
|
151
152
|
>
|
|
152
|
-
|
|
153
|
+
<IconClose />
|
|
153
154
|
</button>
|
|
154
155
|
</div>
|
|
155
156
|
<div class="activity-filters">
|
|
@@ -226,7 +227,7 @@ export function ActivityFeed() {
|
|
|
226
227
|
void startSession();
|
|
227
228
|
}}
|
|
228
229
|
>
|
|
229
|
-
Start session
|
|
230
|
+
Start session <IconExternalLink size={14} />
|
|
230
231
|
</button>
|
|
231
232
|
</div>
|
|
232
233
|
</div>
|
|
@@ -267,7 +268,7 @@ export function WritersSheet() {
|
|
|
267
268
|
<span class="writers-title">Connected writers</span>
|
|
268
269
|
<span class="writers-board">{sessionId.value.slice(0, 12)}</span>
|
|
269
270
|
<button type="button" class="activity-feed-close" aria-label="Close writers" onClick={close}>
|
|
270
|
-
|
|
271
|
+
<IconClose />
|
|
271
272
|
</button>
|
|
272
273
|
</div>
|
|
273
274
|
<div class="writers-body">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { useFocusTrap } from './use-focus-trap';
|
|
3
|
+
import { IconExternalLink } from '../icons';
|
|
3
4
|
|
|
4
5
|
/** Draft only: GitHub owns authentication and the final public submission. */
|
|
5
6
|
export function FeedbackDialog({ onClose }: { onClose: () => void }) {
|
|
@@ -77,7 +78,7 @@ export function FeedbackDialog({ onClose }: { onClose: () => void }) {
|
|
|
77
78
|
Cancel
|
|
78
79
|
</button>
|
|
79
80
|
<button type="submit" class="text-prompt-confirm" disabled={!title.trim() || !description.trim()}>
|
|
80
|
-
Continue on GitHub
|
|
81
|
+
Continue on GitHub <IconExternalLink size={14} />
|
|
81
82
|
</button>
|
|
82
83
|
</div>
|
|
83
84
|
</form>
|
|
@@ -2,7 +2,7 @@ import { useEffect } from 'preact/hooks';
|
|
|
2
2
|
import { nodes } from '../state/canvas-store';
|
|
3
3
|
import { hoveredIntentId, intents, removeIntent, type ClientIntent } from '../state/intent-store';
|
|
4
4
|
import { vetoGhostIntent } from '../state/intent-bridge';
|
|
5
|
-
import { getNodeIcon } from '../icons';
|
|
5
|
+
import { getNodeIcon, IconClose } from '../icons';
|
|
6
6
|
import { TYPE_LABELS } from '../types';
|
|
7
7
|
import type { CanvasNodeState } from '../types';
|
|
8
8
|
|
|
@@ -102,7 +102,7 @@ function GhostInfo({ intent }: { intent: ClientIntent }) {
|
|
|
102
102
|
void vetoGhostIntent(intent);
|
|
103
103
|
}}
|
|
104
104
|
>
|
|
105
|
-
|
|
105
|
+
<IconClose size={14} />
|
|
106
106
|
</button>
|
|
107
107
|
)}
|
|
108
108
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback } from 'preact/hooks';
|
|
2
|
-
import { IconArrange } from '../icons';
|
|
2
|
+
import { IconArrange, IconClose } from '../icons';
|
|
3
3
|
import { attentionHistoryOpen } from '../state/attention-store';
|
|
4
4
|
import {
|
|
5
5
|
addContextPins,
|
|
@@ -204,7 +204,7 @@ export function SelectionBar() {
|
|
|
204
204
|
</BarHint>
|
|
205
205
|
<BarHint label="Clear selection" shortcut="Esc" side="up">
|
|
206
206
|
<button type="button" class="selection-bar-icon" onClick={clearSelection} aria-label="Clear selection">
|
|
207
|
-
|
|
207
|
+
<IconClose size={14} />
|
|
208
208
|
</button>
|
|
209
209
|
</BarHint>
|
|
210
210
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from 'preact/hooks';
|
|
2
|
+
import { IconChevronRight, IconUndo } from '../icons';
|
|
2
3
|
import { useNow } from './use-now';
|
|
3
4
|
import { agentIdentityHue, agentPhaseLabel } from '../../shared/agent-presence.js';
|
|
4
5
|
import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
|
|
@@ -301,7 +302,7 @@ function TimelineRow({ entry }: { entry: TimelineEntry }) {
|
|
|
301
302
|
data-testid="timeline-undo"
|
|
302
303
|
onClick={() => void undoAgentEdit(entry)}
|
|
303
304
|
>
|
|
304
|
-
|
|
305
|
+
<IconUndo size={14} /> undo this edit
|
|
305
306
|
</button>
|
|
306
307
|
)}
|
|
307
308
|
{undone && <div class="session-timeline-undone">undone · steering sent</div>}
|
|
@@ -387,8 +388,9 @@ export function SessionPanel() {
|
|
|
387
388
|
class="session-collapse"
|
|
388
389
|
onClick={() => setManuallyOpened(false)}
|
|
389
390
|
title="Collapse session panel"
|
|
391
|
+
aria-label="Collapse session panel"
|
|
390
392
|
>
|
|
391
|
-
|
|
393
|
+
<IconChevronRight />
|
|
392
394
|
</button>
|
|
393
395
|
</header>
|
|
394
396
|
|
|
@@ -406,7 +408,7 @@ export function SessionPanel() {
|
|
|
406
408
|
Agent edit on top: {undoable.body}
|
|
407
409
|
</span>
|
|
408
410
|
<button type="button" class="session-undo-btn" onClick={() => void undoAgentEdit(undoable)}>
|
|
409
|
-
|
|
411
|
+
<IconUndo size={14} /> Undo
|
|
410
412
|
</button>
|
|
411
413
|
</div>
|
|
412
414
|
);
|
|
@@ -455,7 +457,7 @@ export function SessionPanel() {
|
|
|
455
457
|
{summary}
|
|
456
458
|
</span>
|
|
457
459
|
<span class={`session-section-chevron${showList ? ' is-open' : ''}`} aria-hidden="true">
|
|
458
|
-
|
|
460
|
+
<IconChevronRight size={14} />
|
|
459
461
|
</span>
|
|
460
462
|
</button>
|
|
461
463
|
{showList &&
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from 'preact/hooks';
|
|
2
2
|
import { dismissSessionReceipt, sessionReceipt } from '../state/session-store';
|
|
3
|
+
import { IconClose } from '../icons';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Session receipt (rail-chrome-v2 phase 5, design item 2): a dismissible card
|
|
@@ -74,7 +75,7 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
|
|
|
74
75
|
onClick={dismissSessionReceipt}
|
|
75
76
|
aria-label="Dismiss receipt"
|
|
76
77
|
>
|
|
77
|
-
|
|
78
|
+
<IconClose />
|
|
78
79
|
</button>
|
|
79
80
|
</div>
|
|
80
81
|
<div class="session-receipt-tiles">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
import { IconClose } from '../icons';
|
|
2
3
|
import {
|
|
3
4
|
type CanvasSnapshotInfo,
|
|
4
5
|
deleteSnapshot,
|
|
@@ -183,7 +184,7 @@ export function SnapshotPanel({
|
|
|
183
184
|
aria-label="Delete snapshot"
|
|
184
185
|
disabled={restoringId !== null}
|
|
185
186
|
>
|
|
186
|
-
|
|
187
|
+
<IconClose size={14} />
|
|
187
188
|
</button>
|
|
188
189
|
</>
|
|
189
190
|
);
|
|
@@ -195,7 +196,7 @@ export function SnapshotPanel({
|
|
|
195
196
|
<span class="snapshot-panel-sub">snapshots + sessions</span>
|
|
196
197
|
<span class="snapshot-panel-spacer" />
|
|
197
198
|
<button type="button" class="snapshot-panel-close" onClick={onClose} title="Close" aria-label="Close history">
|
|
198
|
-
|
|
199
|
+
<IconClose />
|
|
199
200
|
</button>
|
|
200
201
|
</div>
|
|
201
202
|
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
IconSun,
|
|
22
22
|
IconTextAnnotation,
|
|
23
23
|
IconTrace,
|
|
24
|
+
IconUpload,
|
|
24
25
|
} from '../icons';
|
|
25
26
|
import {
|
|
26
27
|
autoArrange,
|
|
@@ -337,7 +338,7 @@ export function ToolRail({
|
|
|
337
338
|
<IconNodeFile size={15} />
|
|
338
339
|
</RailButton>
|
|
339
340
|
<RailButton label="Attach document" onClick={() => documentInput.current?.click()}>
|
|
340
|
-
<
|
|
341
|
+
<IconUpload size={15} />
|
|
341
342
|
</RailButton>
|
|
342
343
|
<input
|
|
343
344
|
ref={documentInput}
|
|
@@ -4,7 +4,7 @@ import { BoardSwitcher } from './BoardSwitcher';
|
|
|
4
4
|
import type { ComponentChildren } from 'preact';
|
|
5
5
|
import { presenting } from '../state/presentation';
|
|
6
6
|
import { useEffect, useState } from 'preact/hooks';
|
|
7
|
-
import { IconFitAll, IconZoomIn, IconZoomOut } from '../icons';
|
|
7
|
+
import { IconFitAll, IconZoomIn, IconZoomOut, IconClose } from '../icons';
|
|
8
8
|
import {
|
|
9
9
|
animateViewport,
|
|
10
10
|
connectionStatus,
|
|
@@ -121,7 +121,7 @@ function AgentChip() {
|
|
|
121
121
|
void endSession({ source: session.source, agentId: session.agentId });
|
|
122
122
|
}}
|
|
123
123
|
>
|
|
124
|
-
|
|
124
|
+
<IconClose size={14} />
|
|
125
125
|
</button>
|
|
126
126
|
</span>
|
|
127
127
|
</BarHint>
|
package/src/client/icons.tsx
CHANGED
|
@@ -11,16 +11,133 @@ const defaults = {
|
|
|
11
11
|
'stroke-width': '1.5',
|
|
12
12
|
'stroke-linecap': 'round',
|
|
13
13
|
'stroke-linejoin': 'round',
|
|
14
|
+
'aria-hidden': 'true',
|
|
14
15
|
} as const;
|
|
15
16
|
|
|
16
17
|
function Icon({ size = 16, children, ...rest }: IconProps & { children: JSX.Element | JSX.Element[] }): JSX.Element {
|
|
17
18
|
return (
|
|
18
|
-
<svg
|
|
19
|
+
<svg
|
|
20
|
+
width={size}
|
|
21
|
+
height={size}
|
|
22
|
+
viewBox="0 0 16 16"
|
|
23
|
+
style={{ verticalAlign: 'middle', flexShrink: 0 }}
|
|
24
|
+
{...defaults}
|
|
25
|
+
{...rest}
|
|
26
|
+
>
|
|
19
27
|
{children}
|
|
20
28
|
</svg>
|
|
21
29
|
);
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
export function IconClose(p: IconProps): JSX.Element {
|
|
33
|
+
return (
|
|
34
|
+
<Icon {...p}>
|
|
35
|
+
<path d="m4 4 8 8M12 4l-8 8" />
|
|
36
|
+
</Icon>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function IconPlus(p: IconProps): JSX.Element {
|
|
41
|
+
return (
|
|
42
|
+
<Icon {...p}>
|
|
43
|
+
<path d="M8 2v12M2 8h12" />
|
|
44
|
+
</Icon>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function IconPlusCircle(p: IconProps): JSX.Element {
|
|
49
|
+
return (
|
|
50
|
+
<Icon {...p}>
|
|
51
|
+
<circle cx="8" cy="8" r="6" />
|
|
52
|
+
<path d="M8 5v6M5 8h6" />
|
|
53
|
+
</Icon>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function IconChevronRight(p: IconProps): JSX.Element {
|
|
58
|
+
return (
|
|
59
|
+
<Icon {...p}>
|
|
60
|
+
<polyline points="6 3 11 8 6 13" />
|
|
61
|
+
</Icon>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function IconMore(p: IconProps): JSX.Element {
|
|
66
|
+
return (
|
|
67
|
+
<Icon {...p}>
|
|
68
|
+
<circle cx="3" cy="8" r="0.75" />
|
|
69
|
+
<circle cx="8" cy="8" r="0.75" />
|
|
70
|
+
<circle cx="13" cy="8" r="0.75" />
|
|
71
|
+
</Icon>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function IconExternalLink(p: IconProps): JSX.Element {
|
|
76
|
+
return (
|
|
77
|
+
<Icon {...p}>
|
|
78
|
+
<path d="M9 2h5v5M14 2 7 9M6 3H2v11h11v-4" />
|
|
79
|
+
</Icon>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function IconExpand(p: IconProps): JSX.Element {
|
|
84
|
+
return (
|
|
85
|
+
<Icon {...p}>
|
|
86
|
+
<path d="M9 2h5v5M14 2l-5 5M7 14H2V9M2 14l5-5" />
|
|
87
|
+
</Icon>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function IconArrowLeft(p: IconProps): JSX.Element {
|
|
92
|
+
return (
|
|
93
|
+
<Icon {...p}>
|
|
94
|
+
<path d="M14 8H2m5-5-5 5 5 5" />
|
|
95
|
+
</Icon>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function IconRefresh(p: IconProps): JSX.Element {
|
|
100
|
+
return (
|
|
101
|
+
<Icon {...p}>
|
|
102
|
+
<path d="M13.5 6A5.5 5.5 0 1 0 13 11M14 2v4h-4" />
|
|
103
|
+
</Icon>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function IconUndo(p: IconProps): JSX.Element {
|
|
108
|
+
return (
|
|
109
|
+
<Icon {...p}>
|
|
110
|
+
<path d="M6 2 2 6l4 4M2 6h7a4 4 0 0 1 0 8" />
|
|
111
|
+
</Icon>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function IconSparkle(p: IconProps): JSX.Element {
|
|
116
|
+
return (
|
|
117
|
+
<Icon {...p}>
|
|
118
|
+
<path d="m8 1 2 5 5 2-5 2-2 5-2-5-5-2 5-2Z" />
|
|
119
|
+
</Icon>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Upload into a tray — attach a document. */
|
|
124
|
+
export function IconUpload(p: IconProps): JSX.Element {
|
|
125
|
+
return (
|
|
126
|
+
<Icon {...p}>
|
|
127
|
+
<path d="M8 10V2M4.5 5.5 8 2l3.5 3.5M2 10v3.5h12V10" />
|
|
128
|
+
</Icon>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Dropdown disclosure, sized like the other toolbar icons. */
|
|
133
|
+
export function IconChevronDown(p: IconProps): JSX.Element {
|
|
134
|
+
return (
|
|
135
|
+
<Icon {...p}>
|
|
136
|
+
<polyline points="3 6 8 11 13 6" />
|
|
137
|
+
</Icon>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
24
141
|
/** Expand-arrows — fit all nodes */
|
|
25
142
|
export function IconFitAll(p: IconProps): JSX.Element {
|
|
26
143
|
return (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CanvasNodeState } from '../types';
|
|
2
2
|
import { boardList, openBoard } from '../state/boards-store';
|
|
3
|
+
import { IconExternalLink } from '../icons';
|
|
3
4
|
|
|
4
5
|
/** Native text-only cross-board link. Target identity is the durable board id. */
|
|
5
6
|
export function BoardNode({ node }: { node: CanvasNodeState }) {
|
|
@@ -28,7 +29,7 @@ export function BoardNode({ node }: { node: CanvasNodeState }) {
|
|
|
28
29
|
onClick={() => target && void openBoard(target.id)}
|
|
29
30
|
>
|
|
30
31
|
{target ? 'Open board' : 'Target unavailable'}
|
|
31
|
-
{target && <
|
|
32
|
+
{target && <IconExternalLink size={14} />}
|
|
32
33
|
</button>
|
|
33
34
|
</div>
|
|
34
35
|
);
|
|
@@ -2,6 +2,7 @@ import type { CallToolResult, ListToolsResult, RequestId, Tool } from '@modelcon
|
|
|
2
2
|
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { AppBridge, PostMessageTransport, buildAllowAttribute } from '@modelcontextprotocol/ext-apps/app-bridge';
|
|
4
4
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
5
|
+
import { IconClose } from '../icons';
|
|
5
6
|
import {
|
|
6
7
|
AX_SURFACE_ACK_SOURCE,
|
|
7
8
|
AX_SURFACE_EMIT_SOURCE,
|
|
@@ -1429,6 +1430,7 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
1429
1430
|
<button
|
|
1430
1431
|
type="button"
|
|
1431
1432
|
onClick={() => setError(null)}
|
|
1433
|
+
aria-label="Dismiss error"
|
|
1432
1434
|
style={{
|
|
1433
1435
|
background: 'none',
|
|
1434
1436
|
border: 'none',
|
|
@@ -1438,7 +1440,7 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
1438
1440
|
padding: '0 2px',
|
|
1439
1441
|
}}
|
|
1440
1442
|
>
|
|
1441
|
-
|
|
1443
|
+
<IconClose size={14} />
|
|
1442
1444
|
</button>
|
|
1443
1445
|
</div>
|
|
1444
1446
|
)}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
|
|
2
|
+
import { IconPlusCircle, IconRefresh } from '../icons';
|
|
3
|
+
import { delimiterForPath, parseDelimitedText } from '../../shared/delimited-text';
|
|
2
4
|
import { updateNodeData } from '../state/canvas-store';
|
|
3
5
|
import { iframeMode } from '../state/iframe-mode';
|
|
4
6
|
import { fetchFile, updateNodeFromClient } from '../state/intent-bridge';
|
|
@@ -44,82 +46,6 @@ function langFromPath(path: string): string {
|
|
|
44
46
|
/** Rendered-row ceiling for delimited files — a 200k-row CSV must not lock the browser. */
|
|
45
47
|
const MAX_TABLE_ROWS = 500;
|
|
46
48
|
|
|
47
|
-
export interface DelimitedTable {
|
|
48
|
-
header: string[];
|
|
49
|
-
/** Rectangular: every row padded (short) or truncated (long) to the header width. */
|
|
50
|
-
rows: string[][];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Column delimiter for a delimited-data path, or null when the file is not one. */
|
|
54
|
-
function delimiterForPath(path: string): string | null {
|
|
55
|
-
const lower = path.toLowerCase();
|
|
56
|
-
if (lower.endsWith('.csv')) return ',';
|
|
57
|
-
if (lower.endsWith('.tsv')) return '\t';
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* RFC4180-ish delimited-text parser. Quoted fields may contain the delimiter,
|
|
63
|
-
* newlines, and `""` escapes; CRLF, LF and lone CR all end a record; a trailing
|
|
64
|
-
* newline produces no phantom empty row. Ragged records are squared off against
|
|
65
|
-
* the header width instead of throwing.
|
|
66
|
-
*/
|
|
67
|
-
export function parseDelimitedText(text: string, delimiter: string): DelimitedTable {
|
|
68
|
-
const records: string[][] = [];
|
|
69
|
-
let row: string[] = [];
|
|
70
|
-
let field = '';
|
|
71
|
-
let quoted = false;
|
|
72
|
-
// Tracks whether the current record consumed anything — distinguishes a real
|
|
73
|
-
// empty-quoted record from the dangling state after a terminating newline.
|
|
74
|
-
let started = false;
|
|
75
|
-
|
|
76
|
-
for (let i = 0; i < text.length; i++) {
|
|
77
|
-
const char = text[i];
|
|
78
|
-
started = true;
|
|
79
|
-
if (quoted) {
|
|
80
|
-
if (char === '"') {
|
|
81
|
-
if (text[i + 1] === '"') {
|
|
82
|
-
field += '"';
|
|
83
|
-
i++;
|
|
84
|
-
} else {
|
|
85
|
-
quoted = false;
|
|
86
|
-
}
|
|
87
|
-
} else if (char === '\r' && text[i + 1] === '\n') {
|
|
88
|
-
field += '\n';
|
|
89
|
-
i++;
|
|
90
|
-
} else {
|
|
91
|
-
field += char;
|
|
92
|
-
}
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (char === '"' && field === '') {
|
|
96
|
-
quoted = true;
|
|
97
|
-
} else if (char === delimiter) {
|
|
98
|
-
row.push(field);
|
|
99
|
-
field = '';
|
|
100
|
-
} else if (char === '\n' || char === '\r') {
|
|
101
|
-
if (char === '\r' && text[i + 1] === '\n') i++;
|
|
102
|
-
row.push(field);
|
|
103
|
-
field = '';
|
|
104
|
-
records.push(row);
|
|
105
|
-
row = [];
|
|
106
|
-
started = false;
|
|
107
|
-
} else {
|
|
108
|
-
field += char;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (started) {
|
|
112
|
-
row.push(field);
|
|
113
|
-
records.push(row);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const header = records.shift() ?? [];
|
|
117
|
-
return {
|
|
118
|
-
header,
|
|
119
|
-
rows: records.map((record) => Array.from({ length: header.length }, (_, i) => record[i] ?? '')),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
49
|
/** Human-readable byte size, e.g. `4.2 MB`. */
|
|
124
50
|
export function formatBytes(bytes: number): string {
|
|
125
51
|
if (!Number.isFinite(bytes) || bytes < 0) return '';
|
|
@@ -349,7 +275,7 @@ function TextFileNode({ node, expanded = false }: { node: CanvasNodeState; expan
|
|
|
349
275
|
flexShrink: 0,
|
|
350
276
|
}}
|
|
351
277
|
>
|
|
352
|
-
|
|
278
|
+
<IconPlusCircle size={14} />
|
|
353
279
|
</button>
|
|
354
280
|
<button
|
|
355
281
|
type="button"
|
|
@@ -365,7 +291,7 @@ function TextFileNode({ node, expanded = false }: { node: CanvasNodeState; expan
|
|
|
365
291
|
flexShrink: 0,
|
|
366
292
|
}}
|
|
367
293
|
>
|
|
368
|
-
|
|
294
|
+
<IconRefresh size={14} />
|
|
369
295
|
</button>
|
|
370
296
|
</div>
|
|
371
297
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
import { IconRefresh } from '../icons';
|
|
2
3
|
import type { CanvasNodeState } from '../types';
|
|
3
4
|
import { getImageNodeWarnings } from './image-warnings';
|
|
4
5
|
|
|
@@ -140,7 +141,7 @@ export function ImageNode({ node, expanded = false }: { node: CanvasNodeState; e
|
|
|
140
141
|
{sizeLabel && <span>{sizeLabel}</span>}
|
|
141
142
|
{zoom !== 1 && (
|
|
142
143
|
<button type="button" class="image-node-zoom-reset" onClick={resetView} title="Reset zoom">
|
|
143
|
-
{zoomPct}%
|
|
144
|
+
{zoomPct}% <IconRefresh size={14} />
|
|
144
145
|
</button>
|
|
145
146
|
)}
|
|
146
147
|
</span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JSX } from 'preact';
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
3
|
+
import { IconArrowLeft } from '../icons';
|
|
3
4
|
import { expandNode, updateNodeData } from '../state/canvas-store';
|
|
4
5
|
import { fetchFile, renderMarkdown, saveFile, updateNodeFromClient } from '../state/intent-bridge';
|
|
5
6
|
import type { CanvasNodeState } from '../types';
|
|
@@ -254,7 +255,7 @@ export function MarkdownNode({ node, expanded = false }: { node: CanvasNodeState
|
|
|
254
255
|
<div class="md-editor-expanded" onKeyDown={handleKeyDown}>
|
|
255
256
|
<div class="md-editor-toolbar">
|
|
256
257
|
<button type="button" class="md-toolbar-btn" onClick={() => setSourceMode(false)}>
|
|
257
|
-
|
|
258
|
+
<IconArrowLeft size={14} /> Back to document
|
|
258
259
|
</button>
|
|
259
260
|
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
|
|
260
261
|
{path && <span class="md-toolbar-path">{path.split('/').pop()}</span>}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
import { IconClose } from '../icons';
|
|
2
3
|
import { updateNodeData } from '../state/canvas-store';
|
|
3
4
|
import { fetchSlashCommands, renderMarkdown, submitCanvasPrompt, submitThreadReply } from '../state/intent-bridge';
|
|
4
5
|
import type { CanvasNodeState } from '../types';
|
|
@@ -98,6 +99,7 @@ function ErrorBanner({ message, onDismiss }: { message: string | null; onDismiss
|
|
|
98
99
|
<button
|
|
99
100
|
type="button"
|
|
100
101
|
onClick={onDismiss}
|
|
102
|
+
aria-label="Dismiss error"
|
|
101
103
|
style={{
|
|
102
104
|
background: 'none',
|
|
103
105
|
border: 'none',
|
|
@@ -107,7 +109,7 @@ function ErrorBanner({ message, onDismiss }: { message: string | null; onDismiss
|
|
|
107
109
|
padding: '0 2px',
|
|
108
110
|
}}
|
|
109
111
|
>
|
|
110
|
-
{
|
|
112
|
+
<IconClose size={14} />
|
|
111
113
|
</button>
|
|
112
114
|
</div>
|
|
113
115
|
);
|
|
@@ -760,6 +760,11 @@ body,
|
|
|
760
760
|
border-radius: calc(3px * var(--node-chrome-scale, 1));
|
|
761
761
|
}
|
|
762
762
|
|
|
763
|
+
.canvas-node .node-controls button svg {
|
|
764
|
+
width: 1em;
|
|
765
|
+
height: 1em;
|
|
766
|
+
}
|
|
767
|
+
|
|
763
768
|
.canvas-node .node-controls button:hover {
|
|
764
769
|
background: var(--c-surface-hover);
|
|
765
770
|
color: var(--c-text);
|
|
@@ -1466,6 +1471,10 @@ body,
|
|
|
1466
1471
|
animation: presence-pulse 1.4s ease-in-out infinite;
|
|
1467
1472
|
}
|
|
1468
1473
|
.session-collapse {
|
|
1474
|
+
display: inline-flex;
|
|
1475
|
+
align-items: center;
|
|
1476
|
+
justify-content: center;
|
|
1477
|
+
padding: 0;
|
|
1469
1478
|
width: 26px;
|
|
1470
1479
|
height: 26px;
|
|
1471
1480
|
border: 1px solid transparent;
|
|
@@ -1682,8 +1691,9 @@ body,
|
|
|
1682
1691
|
}
|
|
1683
1692
|
}
|
|
1684
1693
|
.session-section-chevron {
|
|
1694
|
+
display: inline-flex;
|
|
1695
|
+
flex-shrink: 0;
|
|
1685
1696
|
color: var(--c-muted);
|
|
1686
|
-
font-size: 13px;
|
|
1687
1697
|
transition: transform 0.15s ease;
|
|
1688
1698
|
}
|
|
1689
1699
|
.session-section-chevron.is-open {
|
|
@@ -1966,8 +1976,13 @@ body,
|
|
|
1966
1976
|
white-space: nowrap;
|
|
1967
1977
|
}
|
|
1968
1978
|
.command-bar-chip-unpin {
|
|
1969
|
-
|
|
1970
|
-
|
|
1979
|
+
display: inline-flex;
|
|
1980
|
+
align-items: center;
|
|
1981
|
+
justify-content: center;
|
|
1982
|
+
flex-shrink: 0;
|
|
1983
|
+
padding: 0;
|
|
1984
|
+
width: 20px;
|
|
1985
|
+
height: 20px;
|
|
1971
1986
|
border: none;
|
|
1972
1987
|
border-radius: 50%;
|
|
1973
1988
|
background: none;
|
|
@@ -6896,7 +6911,7 @@ html.is-node-dragging .intent-line-layer {
|
|
|
6896
6911
|
align-items: center;
|
|
6897
6912
|
gap: 6px;
|
|
6898
6913
|
max-width: 280px;
|
|
6899
|
-
padding:
|
|
6914
|
+
padding: 6px 8px;
|
|
6900
6915
|
margin-left: -8px;
|
|
6901
6916
|
background: none;
|
|
6902
6917
|
border: 1px solid transparent;
|
|
@@ -6914,7 +6929,8 @@ html.is-node-dragging .intent-line-layer {
|
|
|
6914
6929
|
white-space: nowrap;
|
|
6915
6930
|
}
|
|
6916
6931
|
.board-switcher-caret {
|
|
6917
|
-
|
|
6932
|
+
display: inline-flex;
|
|
6933
|
+
flex-shrink: 0;
|
|
6918
6934
|
color: var(--c-text-soft);
|
|
6919
6935
|
}
|
|
6920
6936
|
.board-switcher-menu {
|