@dfosco/storyboard-react 4.2.0-beta.17 → 4.2.0-beta.18
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/package.json +3 -3
- package/src/BranchBar/BranchBar.jsx +3 -1
- package/src/BranchBar/BranchBar.module.css +2 -2
- package/src/BranchBar/useBranches.js +20 -6
- package/src/BranchBar/useBranches.test.js +68 -0
- package/src/CommandPalette/CommandPalette.jsx +250 -61
- package/src/CommandPalette/command-palette.css +12 -0
- package/src/Icon.jsx +46 -11
- package/src/Viewfinder.jsx +53 -133
- package/src/Viewfinder.module.css +20 -91
- package/src/Workspace.jsx +7 -0
- package/src/canvas/CanvasPage.jsx +601 -62
- package/src/canvas/CanvasPage.module.css +15 -2
- package/src/canvas/CanvasPage.multiselect.test.jsx +7 -0
- package/src/canvas/ConnectorLayer.jsx +120 -152
- package/src/canvas/ConnectorLayer.module.css +69 -0
- package/src/canvas/canvasApi.js +68 -2
- package/src/canvas/connectorGeometry.js +132 -0
- package/src/canvas/hotPoolDevLogs.js +25 -0
- package/src/canvas/useMarqueeSelect.js +30 -4
- package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
- package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
- package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
- package/src/canvas/widgets/ComponentWidget.jsx +1 -0
- package/src/canvas/widgets/CropOverlay.jsx +219 -0
- package/src/canvas/widgets/CropOverlay.module.css +118 -0
- package/src/canvas/widgets/ExpandedPane.jsx +472 -0
- package/src/canvas/widgets/ExpandedPane.module.css +179 -0
- package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +49 -102
- package/src/canvas/widgets/ImageWidget.jsx +129 -8
- package/src/canvas/widgets/ImageWidget.module.css +30 -0
- package/src/canvas/widgets/LinkPreview.jsx +93 -44
- package/src/canvas/widgets/MarkdownBlock.jsx +141 -16
- package/src/canvas/widgets/MarkdownBlock.module.css +25 -0
- package/src/canvas/widgets/PromptWidget.jsx +414 -0
- package/src/canvas/widgets/PromptWidget.module.css +273 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +46 -170
- package/src/canvas/widgets/ResizeHandle.jsx +17 -6
- package/src/canvas/widgets/StoryWidget.jsx +65 -11
- package/src/canvas/widgets/TerminalReadWidget.jsx +11 -5
- package/src/canvas/widgets/TerminalReadWidget.module.css +3 -1
- package/src/canvas/widgets/TerminalWidget.jsx +301 -124
- package/src/canvas/widgets/TerminalWidget.module.css +121 -12
- package/src/canvas/widgets/TilesWidget.jsx +302 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +67 -152
- package/src/canvas/widgets/WidgetChrome.module.css +20 -1
- package/src/canvas/widgets/expandUtils.js +385 -16
- package/src/canvas/widgets/expandUtils.test.js +155 -0
- package/src/canvas/widgets/index.js +6 -2
- package/src/canvas/widgets/tilePool.js +23 -0
- package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
- package/src/canvas/widgets/tiles/leaf.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
- package/src/canvas/widgets/tiles/solid-a.png +0 -0
- package/src/canvas/widgets/tiles/solid-b.png +0 -0
- package/src/canvas/widgets/widgetConfig.js +37 -4
- package/src/canvas/widgets/widgetIcons.jsx +190 -0
- package/src/canvas/widgets/widgetProps.js +1 -0
- package/src/context.jsx +47 -19
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/index.js +4 -2
- package/src/story/ComponentSetPage.jsx +186 -0
- package/src/story/ComponentSetPage.module.css +121 -0
- package/src/story/StoryPage.jsx +32 -2
- package/src/vite/data-plugin.js +79 -35
- package/src/canvas/widgets/ActionWidget.jsx +0 -200
- package/src/canvas/widgets/ActionWidget.module.css +0 -122
- package/src/canvas/widgets/SplitExpandModal.jsx +0 -234
- package/src/canvas/widgets/SplitExpandModal.module.css +0 -335
- package/src/canvas/widgets/SplitScreenTopBar.jsx +0 -30
- package/src/canvas/widgets/SplitScreenTopBar.module.css +0 -58
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* ComponentSetPage — grid layout for all exports of a story */
|
|
2
|
+
|
|
3
|
+
.grid {
|
|
4
|
+
background-color: var(--bgColor-muted, #f6f8fa);
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-wrap: nowrap;
|
|
7
|
+
gap: 12px;
|
|
8
|
+
padding: 12px;
|
|
9
|
+
min-height: 100vh;
|
|
10
|
+
width: max-content;
|
|
11
|
+
min-width: 100%;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.grid[data-layout="horizontal"] {
|
|
16
|
+
flex-direction: row;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.grid[data-layout="vertical"] {
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.cell {
|
|
24
|
+
flex: 0 0 auto;
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
border: 2px solid var(--borderColor-muted, #d8dee4);
|
|
28
|
+
border-radius: 2px;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
31
|
+
position: relative;
|
|
32
|
+
background: var(--bgColor-default, #ffffff);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* In horizontal layout, each cell snaps to the widest component */
|
|
36
|
+
.grid[data-layout="horizontal"] .cell {
|
|
37
|
+
min-width: var(--cell-snap-w, 200px);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* In vertical layout, each cell snaps to the tallest component */
|
|
41
|
+
.grid[data-layout="vertical"] .cell {
|
|
42
|
+
min-height: var(--cell-snap-h, 120px);
|
|
43
|
+
min-width: 100%;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.cell[data-selected] {
|
|
47
|
+
border-color: var(--fgColor-accent, #0969da);
|
|
48
|
+
box-shadow: 0 0 0 1px var(--fgColor-accent, #0969da);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.cellLabel {
|
|
52
|
+
all: unset;
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 6px;
|
|
56
|
+
padding: 6px 10px;
|
|
57
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
58
|
+
font-size: 11px;
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
color: var(--fgColor-muted, #656d76);
|
|
61
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
62
|
+
border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
user-select: none;
|
|
65
|
+
transition: background 100ms ease, color 100ms ease;
|
|
66
|
+
flex-shrink: 0;
|
|
67
|
+
/* Round top corners to match cell */
|
|
68
|
+
border-radius: 6px 6px 0 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.cellLabel:hover {
|
|
72
|
+
background: var(--bgColor-neutral-muted, #eaeef2);
|
|
73
|
+
color: var(--fgColor-default, #1f2328);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.cellLabel[data-selected] {
|
|
77
|
+
color: var(--fgColor-accent, #0969da);
|
|
78
|
+
background: var(--bgColor-accent-muted, #ddf4ff);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.cellRadio {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
width: 12px;
|
|
86
|
+
height: 12px;
|
|
87
|
+
border-radius: 50%;
|
|
88
|
+
border: 2px solid var(--borderColor-accent, #d8dee4);
|
|
89
|
+
flex-shrink: 0;
|
|
90
|
+
transition: border-color 100ms ease, background 100ms ease;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.cellRadio[data-selected] {
|
|
94
|
+
border-color: var(--fgColor-accent, #0969da);
|
|
95
|
+
background: var(--fgColor-accent, #0969da);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.cellContent {
|
|
99
|
+
flex: 1;
|
|
100
|
+
overflow: visible;
|
|
101
|
+
position: relative;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.error {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
gap: 4px;
|
|
108
|
+
padding: 1rem;
|
|
109
|
+
color: var(--fgColor-danger, #cf222e);
|
|
110
|
+
font-size: 0.875rem;
|
|
111
|
+
line-height: 1.5;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.loading {
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
padding: 3rem;
|
|
119
|
+
color: var(--fgColor-muted, #656d76);
|
|
120
|
+
font-size: 0.875rem;
|
|
121
|
+
}
|
package/src/story/StoryPage.jsx
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* When ?export=ExportName is present, renders that single export.
|
|
6
6
|
* Without ?export, renders all named exports stacked.
|
|
7
7
|
*/
|
|
8
|
-
import { useState, useEffect, useMemo } from 'react'
|
|
8
|
+
import { useState, useEffect, useMemo, lazy, Suspense } from 'react'
|
|
9
9
|
import { useLocation } from 'react-router-dom'
|
|
10
10
|
import { getStoryData } from '@dfosco/storyboard-core'
|
|
11
11
|
import { ThemeProvider, BaseStyles } from '@primer/react'
|
|
12
12
|
import styles from './StoryPage.module.css'
|
|
13
13
|
|
|
14
|
+
const ComponentSetPageLazy = lazy(() => import('./ComponentSetPage.jsx'))
|
|
15
|
+
|
|
14
16
|
function StoryErrorFallback({ name, error }) {
|
|
15
17
|
return (
|
|
16
18
|
<div className={styles.error}>
|
|
@@ -25,12 +27,31 @@ export default function StoryPage({ name }) {
|
|
|
25
27
|
const searchParams = new URLSearchParams(location.search)
|
|
26
28
|
const exportFilter = searchParams.get('export')
|
|
27
29
|
const isEmbed = searchParams.has('_sb_embed')
|
|
30
|
+
const isComponentSet = searchParams.has('_sb_component_set')
|
|
31
|
+
|
|
32
|
+
// When embedded as a canvas iframe, suppress HMR full-reloads.
|
|
33
|
+
// Story content updates via React Fast Refresh; a full-reload
|
|
34
|
+
// causes a visible flash and can create a reload loop when
|
|
35
|
+
// multiple story iframes are on the same canvas.
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!isEmbed || !import.meta.hot) return
|
|
38
|
+
const msg = { active: true }
|
|
39
|
+
import.meta.hot.send('storyboard:canvas-hmr-guard', msg)
|
|
40
|
+
const interval = setInterval(() => {
|
|
41
|
+
import.meta.hot.send('storyboard:canvas-hmr-guard', msg)
|
|
42
|
+
}, 3000)
|
|
43
|
+
return () => {
|
|
44
|
+
clearInterval(interval)
|
|
45
|
+
import.meta.hot.send('storyboard:canvas-hmr-guard', { active: false })
|
|
46
|
+
}
|
|
47
|
+
}, [isEmbed])
|
|
28
48
|
|
|
29
49
|
const story = useMemo(() => getStoryData(name), [name])
|
|
30
50
|
const [exports, setExports] = useState(null)
|
|
31
51
|
const [error, setError] = useState(null)
|
|
32
52
|
|
|
33
53
|
useEffect(() => {
|
|
54
|
+
if (isComponentSet) return
|
|
34
55
|
if (!story?._storyImport) {
|
|
35
56
|
Promise.resolve().then(() => setError(`Story "${name}" not found or missing import`))
|
|
36
57
|
return
|
|
@@ -55,7 +76,7 @@ export default function StoryPage({ name }) {
|
|
|
55
76
|
})
|
|
56
77
|
|
|
57
78
|
return () => { cancelled = true }
|
|
58
|
-
}, [name, story])
|
|
79
|
+
}, [name, story, isComponentSet])
|
|
59
80
|
|
|
60
81
|
// Signal snapshot-ready after story renders in embed mode.
|
|
61
82
|
useEffect(() => {
|
|
@@ -67,6 +88,15 @@ export default function StoryPage({ name }) {
|
|
|
67
88
|
})
|
|
68
89
|
}, [isEmbed, exports])
|
|
69
90
|
|
|
91
|
+
// Delegate to ComponentSetPage for grid view (after all hooks)
|
|
92
|
+
if (isComponentSet) {
|
|
93
|
+
return (
|
|
94
|
+
<Suspense fallback={isEmbed ? null : <div className={styles.loading}>Loading component set…</div>}>
|
|
95
|
+
<ComponentSetPageLazy name={name} />
|
|
96
|
+
</Suspense>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
70
100
|
if (error) {
|
|
71
101
|
return (
|
|
72
102
|
<StoryErrorFallback name={name} error={error} />
|
package/src/vite/data-plugin.js
CHANGED
|
@@ -5,7 +5,9 @@ import { globSync } from 'glob'
|
|
|
5
5
|
import { parse as parseJsonc } from 'jsonc-parser'
|
|
6
6
|
import { materializeFromText } from '@dfosco/storyboard-core/canvas/materializer'
|
|
7
7
|
import { toCanvasId } from '@dfosco/storyboard-core/canvas/identity'
|
|
8
|
+
import { isCanvasWriteInFlight } from '../../../core/src/canvas/writeGuard.js'
|
|
8
9
|
import { getConfig } from '@dfosco/storyboard-core/config'
|
|
10
|
+
import { list as listRunningServers } from '../../../core/src/worktree/serverRegistry.js'
|
|
9
11
|
|
|
10
12
|
const VIRTUAL_MODULE_ID = 'virtual:storyboard-data-index'
|
|
11
13
|
const RESOLVED_ID = '\0' + VIRTUAL_MODULE_ID
|
|
@@ -1153,14 +1155,20 @@ export default function storyboardDataPlugin() {
|
|
|
1153
1155
|
// custom HMR event with updated metadata so the canvas page and
|
|
1154
1156
|
// viewfinder can react in place.
|
|
1155
1157
|
if (/\.canvas\.jsonl$/.test(normalized)) {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1158
|
+
// If this file change was caused by the canvas server API, it has
|
|
1159
|
+
// already pushed an HMR event via pushCanvasUpdate(). Skip the
|
|
1160
|
+
// duplicate watcher-triggered event to prevent stale-data rollbacks.
|
|
1161
|
+
const absPath = path.resolve(root, filePath)
|
|
1162
|
+
if (!isCanvasWriteInFlight(absPath)) {
|
|
1163
|
+
const parsed = parseDataFile(filePath)
|
|
1164
|
+
if (parsed?.suffix === 'canvas' && parsed?.id) {
|
|
1165
|
+
const metadata = readCanvasMetadata(filePath, parsed)
|
|
1166
|
+
server.ws.send({
|
|
1167
|
+
type: 'custom',
|
|
1168
|
+
event: 'storyboard:canvas-file-changed',
|
|
1169
|
+
data: { canvasId: parsed.id, name: parsed.id, ...(metadata ? { metadata } : {}) },
|
|
1170
|
+
})
|
|
1171
|
+
}
|
|
1164
1172
|
}
|
|
1165
1173
|
softInvalidate()
|
|
1166
1174
|
return
|
|
@@ -1197,6 +1205,17 @@ export default function storyboardDataPlugin() {
|
|
|
1197
1205
|
// Source files inside .folder/ dirs (jsx, css, etc.) are handled by
|
|
1198
1206
|
// Vite's built-in HMR / React Fast Refresh — don't full-reload for them.
|
|
1199
1207
|
if (!parsed && inFolder) return
|
|
1208
|
+
|
|
1209
|
+
// Story file content changes are handled by Vite's built-in HMR
|
|
1210
|
+
// (React Fast Refresh). Only soft-invalidate the virtual module so
|
|
1211
|
+
// the next page load picks up updated metadata — don't full-reload,
|
|
1212
|
+
// which would destroy canvas state and cause embedded iframes to
|
|
1213
|
+
// reload unnecessarily.
|
|
1214
|
+
if (parsed?.suffix === 'story') {
|
|
1215
|
+
softInvalidate()
|
|
1216
|
+
return
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1200
1219
|
// Rebuild index and invalidate virtual module
|
|
1201
1220
|
buildResult = null
|
|
1202
1221
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
@@ -1345,19 +1364,16 @@ export default function storyboardDataPlugin() {
|
|
|
1345
1364
|
},
|
|
1346
1365
|
|
|
1347
1366
|
// Inject __SB_BRANCHES__ into HTML so the Viewfinder branch selector works.
|
|
1348
|
-
//
|
|
1367
|
+
// Uses server registry (live running processes) instead of stale ports.json.
|
|
1349
1368
|
transformIndexHtml(html, ctx) {
|
|
1350
1369
|
// Only inject in dev mode
|
|
1351
1370
|
if (!ctx.server) return html
|
|
1352
1371
|
|
|
1353
1372
|
try {
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
const branches = Object.entries(ports)
|
|
1359
|
-
.filter(([name]) => name !== 'main')
|
|
1360
|
-
.map(([name, port]) => ({ branch: name, folder: `branch--${name}`, port }))
|
|
1373
|
+
const servers = listRunningServers()
|
|
1374
|
+
const branches = servers
|
|
1375
|
+
.filter(srv => srv.worktree !== 'main')
|
|
1376
|
+
.map(srv => ({ branch: srv.worktree, folder: `branch--${srv.worktree}`, port: srv.port }))
|
|
1361
1377
|
|
|
1362
1378
|
if (branches.length === 0) return html
|
|
1363
1379
|
|
|
@@ -1376,34 +1392,62 @@ export default function storyboardDataPlugin() {
|
|
|
1376
1392
|
}
|
|
1377
1393
|
|
|
1378
1394
|
/**
|
|
1379
|
-
* Vite plugin that copies
|
|
1380
|
-
*
|
|
1395
|
+
* Vite plugin that copies terminal snapshots into the build output
|
|
1396
|
+
* so TerminalReadWidget can fetch them as static files in production.
|
|
1397
|
+
*
|
|
1398
|
+
* Sources (in priority order):
|
|
1399
|
+
* 1. assets/.storyboard-public/terminal-snapshots/<widgetId>.snapshot.json (new, flat)
|
|
1400
|
+
* 2. .storyboard/terminal-snapshots/<canvasDir>/<widgetId>.json (legacy, nested)
|
|
1401
|
+
*
|
|
1402
|
+
* Both are emitted to `_storyboard/terminal-snapshots/` in the build.
|
|
1403
|
+
* Tilde-prefixed files (~) are excluded (private).
|
|
1381
1404
|
*/
|
|
1382
1405
|
export function terminalSnapshotPlugin() {
|
|
1383
1406
|
return {
|
|
1384
1407
|
name: 'storyboard-terminal-snapshots',
|
|
1385
1408
|
|
|
1386
1409
|
generateBundle() {
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
for (const
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1410
|
+
const emittedIds = new Set()
|
|
1411
|
+
|
|
1412
|
+
// 1. New public snapshots (flat structure)
|
|
1413
|
+
const publicDir = path.resolve('assets/.storyboard-public/terminal-snapshots')
|
|
1414
|
+
if (fs.existsSync(publicDir)) {
|
|
1415
|
+
for (const file of fs.readdirSync(publicDir)) {
|
|
1416
|
+
if (file.startsWith('~') || file.startsWith('.') || !file.endsWith('.json')) continue
|
|
1417
|
+
// Extract widgetId from filename: <widgetId>.snapshot.json
|
|
1418
|
+
const widgetId = file.replace(/\.snapshot\.json$/, '')
|
|
1419
|
+
if (widgetId) emittedIds.add(widgetId)
|
|
1420
|
+
this.emitFile({
|
|
1421
|
+
type: 'asset',
|
|
1422
|
+
fileName: `_storyboard/terminal-snapshots/${file}`,
|
|
1423
|
+
source: fs.readFileSync(path.join(publicDir, file), 'utf-8'),
|
|
1424
|
+
})
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// 2. Legacy snapshots (nested by canvas dir) — skip if already emitted
|
|
1429
|
+
const legacyDir = path.resolve('.storyboard/terminal-snapshots')
|
|
1430
|
+
if (fs.existsSync(legacyDir)) {
|
|
1431
|
+
const walk = (dir) => {
|
|
1432
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
1433
|
+
for (const entry of entries) {
|
|
1434
|
+
const full = path.join(dir, entry.name)
|
|
1435
|
+
if (entry.isDirectory()) {
|
|
1436
|
+
walk(full)
|
|
1437
|
+
} else if (entry.name.endsWith('.json') && !entry.name.startsWith('~')) {
|
|
1438
|
+
const widgetId = entry.name.replace(/\.json$/, '')
|
|
1439
|
+
if (emittedIds.has(widgetId)) continue // new format takes priority
|
|
1440
|
+
const rel = path.relative(legacyDir, full).replace(/\\/g, '/')
|
|
1441
|
+
this.emitFile({
|
|
1442
|
+
type: 'asset',
|
|
1443
|
+
fileName: `_storyboard/terminal-snapshots/${rel}`,
|
|
1444
|
+
source: fs.readFileSync(full, 'utf-8'),
|
|
1445
|
+
})
|
|
1446
|
+
}
|
|
1403
1447
|
}
|
|
1404
1448
|
}
|
|
1449
|
+
walk(legacyDir)
|
|
1405
1450
|
}
|
|
1406
|
-
walk(snapshotsDir)
|
|
1407
1451
|
},
|
|
1408
1452
|
}
|
|
1409
1453
|
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react'
|
|
2
|
-
import { readProp } from './widgetProps.js'
|
|
3
|
-
import { schemas } from './widgetProps.js'
|
|
4
|
-
import ResizeHandle from './ResizeHandle.jsx'
|
|
5
|
-
import styles from './ActionWidget.module.css'
|
|
6
|
-
|
|
7
|
-
const actionSchema = schemas['action']
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* ActionWidget — a canvas widget that runs a background agent.
|
|
11
|
-
*
|
|
12
|
-
* Displays a "Run" button. When clicked, spawns a headless tmux+copilot
|
|
13
|
-
* session via the /agent/spawn endpoint. Shows status indicators
|
|
14
|
-
* (running/done/error) and allows peeking into errored sessions.
|
|
15
|
-
*/
|
|
16
|
-
export default forwardRef(function ActionWidget({ id, props, onUpdate, resizable }, ref) {
|
|
17
|
-
const width = readProp(props, 'width', actionSchema)
|
|
18
|
-
const height = readProp(props, 'height', actionSchema)
|
|
19
|
-
const prompt = readProp(props, 'prompt', actionSchema) || ''
|
|
20
|
-
const label = readProp(props, 'label', actionSchema) || 'Run Agent'
|
|
21
|
-
|
|
22
|
-
const [status, setStatus] = useState('idle') // idle | running | done | error
|
|
23
|
-
const [message, setMessage] = useState(null)
|
|
24
|
-
|
|
25
|
-
useImperativeHandle(ref, () => ({
|
|
26
|
-
handleAction(actionId) {
|
|
27
|
-
// ActionWidget doesn't handle expand/split-screen itself
|
|
28
|
-
return false
|
|
29
|
-
},
|
|
30
|
-
}), [])
|
|
31
|
-
|
|
32
|
-
// Listen for agent status updates via Vite HMR custom events
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (!import.meta.hot) return
|
|
35
|
-
|
|
36
|
-
const handler = (data) => {
|
|
37
|
-
if (data.widgetId === id) {
|
|
38
|
-
setStatus(data.status)
|
|
39
|
-
setMessage(data.message || null)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
import.meta.hot.on('storyboard:agent-status', handler)
|
|
44
|
-
return () => {
|
|
45
|
-
// Vite HMR doesn't support removeListener, but cleanup on unmount
|
|
46
|
-
}
|
|
47
|
-
}, [id])
|
|
48
|
-
|
|
49
|
-
// Poll for status on mount (in case we missed a WS event)
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
const base = (typeof import.meta !== 'undefined' && import.meta.env?.BASE_URL) || '/'
|
|
52
|
-
const baseClean = base.endsWith('/') ? base : base + '/'
|
|
53
|
-
|
|
54
|
-
fetch(`${baseClean}_storyboard/canvas/agent/status?widgetId=${id}`)
|
|
55
|
-
.then((r) => r.json())
|
|
56
|
-
.then((data) => {
|
|
57
|
-
if (data.agentStatus?.status) {
|
|
58
|
-
setStatus(data.agentStatus.status)
|
|
59
|
-
setMessage(data.agentStatus.message || null)
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
.catch(() => {})
|
|
63
|
-
}, [id])
|
|
64
|
-
|
|
65
|
-
const handleRun = useCallback(async () => {
|
|
66
|
-
if (status === 'running') return
|
|
67
|
-
|
|
68
|
-
setStatus('running')
|
|
69
|
-
setMessage('Spawning agent...')
|
|
70
|
-
|
|
71
|
-
const base = (typeof import.meta !== 'undefined' && import.meta.env?.BASE_URL) || '/'
|
|
72
|
-
const baseClean = base.endsWith('/') ? base : base + '/'
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const res = await fetch(`${baseClean}_storyboard/canvas/agent/spawn`, {
|
|
76
|
-
method: 'POST',
|
|
77
|
-
headers: { 'Content-Type': 'application/json' },
|
|
78
|
-
body: JSON.stringify({
|
|
79
|
-
canvasId: window.__storyboardCanvasBridgeState?.canvasId || 'unknown',
|
|
80
|
-
widgetId: id,
|
|
81
|
-
prompt,
|
|
82
|
-
autopilot: true,
|
|
83
|
-
}),
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
if (!res.ok) {
|
|
87
|
-
const data = await res.json().catch(() => ({}))
|
|
88
|
-
setStatus('error')
|
|
89
|
-
setMessage(data.error || 'Spawn failed')
|
|
90
|
-
}
|
|
91
|
-
} catch (err) {
|
|
92
|
-
setStatus('error')
|
|
93
|
-
setMessage(err.message || 'Connection failed')
|
|
94
|
-
}
|
|
95
|
-
}, [id, prompt, status])
|
|
96
|
-
|
|
97
|
-
const handlePeek = useCallback(async () => {
|
|
98
|
-
const base = (typeof import.meta !== 'undefined' && import.meta.env?.BASE_URL) || '/'
|
|
99
|
-
const baseClean = base.endsWith('/') ? base : base + '/'
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
const res = await fetch(`${baseClean}_storyboard/canvas/agent/peek`, {
|
|
103
|
-
method: 'POST',
|
|
104
|
-
headers: { 'Content-Type': 'application/json' },
|
|
105
|
-
body: JSON.stringify({
|
|
106
|
-
widgetId: id,
|
|
107
|
-
canvasId: window.__storyboardCanvasBridgeState?.canvasId || 'unknown',
|
|
108
|
-
}),
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
if (res.ok) {
|
|
112
|
-
setMessage('Session opened — check the new terminal widget')
|
|
113
|
-
} else {
|
|
114
|
-
const data = await res.json().catch(() => ({}))
|
|
115
|
-
setMessage(data.error || 'Peek failed')
|
|
116
|
-
}
|
|
117
|
-
} catch (err) {
|
|
118
|
-
setMessage(err.message || 'Connection failed')
|
|
119
|
-
}
|
|
120
|
-
}, [id])
|
|
121
|
-
|
|
122
|
-
const handleDismiss = useCallback(() => {
|
|
123
|
-
setStatus('idle')
|
|
124
|
-
setMessage(null)
|
|
125
|
-
}, [])
|
|
126
|
-
|
|
127
|
-
const handleResize = useCallback((w, h) => {
|
|
128
|
-
onUpdate?.({ width: w, height: h })
|
|
129
|
-
}, [onUpdate])
|
|
130
|
-
|
|
131
|
-
const statusIcon = {
|
|
132
|
-
idle: '⚡',
|
|
133
|
-
running: '⏳',
|
|
134
|
-
done: '✓',
|
|
135
|
-
error: '!',
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const statusClass = {
|
|
139
|
-
idle: styles.idle,
|
|
140
|
-
running: styles.running,
|
|
141
|
-
done: styles.done,
|
|
142
|
-
error: styles.error,
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
<div
|
|
147
|
-
className={`${styles.container} ${statusClass[status] || ''}`}
|
|
148
|
-
style={{
|
|
149
|
-
...(typeof width === 'number' ? { width: `${width}px` } : undefined),
|
|
150
|
-
...(typeof height === 'number' ? { height: `${height}px` } : undefined),
|
|
151
|
-
}}
|
|
152
|
-
>
|
|
153
|
-
<div className={styles.header}>
|
|
154
|
-
<span className={styles.icon}>{statusIcon[status]}</span>
|
|
155
|
-
<span className={styles.label}>{label}</span>
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
{prompt && (
|
|
159
|
-
<div className={styles.prompt}>
|
|
160
|
-
{prompt.length > 100 ? prompt.slice(0, 100) + '…' : prompt}
|
|
161
|
-
</div>
|
|
162
|
-
)}
|
|
163
|
-
|
|
164
|
-
<div className={styles.actions}>
|
|
165
|
-
{(status === 'idle' || status === 'done') && (
|
|
166
|
-
<button className={styles.runButton} onClick={handleRun}>
|
|
167
|
-
{status === 'done' ? 'Run Again' : 'Run'}
|
|
168
|
-
</button>
|
|
169
|
-
)}
|
|
170
|
-
|
|
171
|
-
{status === 'running' && (
|
|
172
|
-
<div className={styles.spinner}>Running…</div>
|
|
173
|
-
)}
|
|
174
|
-
|
|
175
|
-
{status === 'error' && (
|
|
176
|
-
<div className={styles.errorActions}>
|
|
177
|
-
<button className={styles.peekButton} onClick={handlePeek}>
|
|
178
|
-
Peek Session
|
|
179
|
-
</button>
|
|
180
|
-
<button className={styles.dismissButton} onClick={handleDismiss}>
|
|
181
|
-
Dismiss
|
|
182
|
-
</button>
|
|
183
|
-
</div>
|
|
184
|
-
)}
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
{message && (
|
|
188
|
-
<div className={styles.message}>{message}</div>
|
|
189
|
-
)}
|
|
190
|
-
|
|
191
|
-
{resizable && (
|
|
192
|
-
<ResizeHandle
|
|
193
|
-
onResize={handleResize}
|
|
194
|
-
minWidth={200}
|
|
195
|
-
minHeight={120}
|
|
196
|
-
/>
|
|
197
|
-
)}
|
|
198
|
-
</div>
|
|
199
|
-
)
|
|
200
|
-
})
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
.container {
|
|
2
|
-
display: flex;
|
|
3
|
-
flex-direction: column;
|
|
4
|
-
gap: 8px;
|
|
5
|
-
padding: 16px;
|
|
6
|
-
border-radius: 8px;
|
|
7
|
-
background: var(--bgColor-default, #0d1117);
|
|
8
|
-
border: 1px solid var(--borderColor-default, #30363d);
|
|
9
|
-
color: var(--fgColor-default, #e6edf3);
|
|
10
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
11
|
-
font-size: 13px;
|
|
12
|
-
overflow: hidden;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.container.running {
|
|
16
|
-
border-color: var(--borderColor-accent-emphasis, #58a6ff);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
.container.done {
|
|
20
|
-
border-color: var(--borderColor-success-emphasis, #3fb950);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.container.error {
|
|
24
|
-
border-color: var(--borderColor-danger-emphasis, #f85149);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.header {
|
|
28
|
-
display: flex;
|
|
29
|
-
align-items: center;
|
|
30
|
-
gap: 8px;
|
|
31
|
-
font-weight: 600;
|
|
32
|
-
font-size: 14px;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.icon {
|
|
36
|
-
font-size: 16px;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.label {
|
|
40
|
-
flex: 1;
|
|
41
|
-
overflow: hidden;
|
|
42
|
-
text-overflow: ellipsis;
|
|
43
|
-
white-space: nowrap;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.prompt {
|
|
47
|
-
color: var(--fgColor-muted, #8b949e);
|
|
48
|
-
font-size: 12px;
|
|
49
|
-
line-height: 1.4;
|
|
50
|
-
overflow: hidden;
|
|
51
|
-
display: -webkit-box;
|
|
52
|
-
-webkit-line-clamp: 3;
|
|
53
|
-
-webkit-box-orient: vertical;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.actions {
|
|
57
|
-
display: flex;
|
|
58
|
-
gap: 8px;
|
|
59
|
-
margin-top: auto;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.runButton {
|
|
63
|
-
padding: 6px 16px;
|
|
64
|
-
border-radius: 6px;
|
|
65
|
-
border: none;
|
|
66
|
-
background: var(--bgColor-accent-emphasis, #1f6feb);
|
|
67
|
-
color: #fff;
|
|
68
|
-
font-size: 13px;
|
|
69
|
-
font-weight: 500;
|
|
70
|
-
cursor: pointer;
|
|
71
|
-
transition: background 0.15s;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.runButton:hover {
|
|
75
|
-
background: var(--bgColor-accent-emphasis, #388bfd);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.spinner {
|
|
79
|
-
color: var(--fgColor-accent, #58a6ff);
|
|
80
|
-
font-size: 12px;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.errorActions {
|
|
84
|
-
display: flex;
|
|
85
|
-
gap: 8px;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.peekButton {
|
|
89
|
-
padding: 4px 12px;
|
|
90
|
-
border-radius: 6px;
|
|
91
|
-
border: 1px solid var(--borderColor-danger-emphasis, #f85149);
|
|
92
|
-
background: transparent;
|
|
93
|
-
color: var(--fgColor-danger, #f85149);
|
|
94
|
-
font-size: 12px;
|
|
95
|
-
cursor: pointer;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.peekButton:hover {
|
|
99
|
-
background: var(--bgColor-danger-muted, rgba(248, 81, 73, 0.1));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.dismissButton {
|
|
103
|
-
padding: 4px 12px;
|
|
104
|
-
border-radius: 6px;
|
|
105
|
-
border: 1px solid var(--borderColor-default, #30363d);
|
|
106
|
-
background: transparent;
|
|
107
|
-
color: var(--fgColor-muted, #8b949e);
|
|
108
|
-
font-size: 12px;
|
|
109
|
-
cursor: pointer;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.dismissButton:hover {
|
|
113
|
-
background: var(--bgColor-muted, #161b22);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.message {
|
|
117
|
-
color: var(--fgColor-muted, #8b949e);
|
|
118
|
-
font-size: 11px;
|
|
119
|
-
overflow: hidden;
|
|
120
|
-
text-overflow: ellipsis;
|
|
121
|
-
white-space: nowrap;
|
|
122
|
-
}
|