@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.
Files changed (79) hide show
  1. package/package.json +3 -3
  2. package/src/BranchBar/BranchBar.jsx +3 -1
  3. package/src/BranchBar/BranchBar.module.css +2 -2
  4. package/src/BranchBar/useBranches.js +20 -6
  5. package/src/BranchBar/useBranches.test.js +68 -0
  6. package/src/CommandPalette/CommandPalette.jsx +250 -61
  7. package/src/CommandPalette/command-palette.css +12 -0
  8. package/src/Icon.jsx +46 -11
  9. package/src/Viewfinder.jsx +53 -133
  10. package/src/Viewfinder.module.css +20 -91
  11. package/src/Workspace.jsx +7 -0
  12. package/src/canvas/CanvasPage.jsx +601 -62
  13. package/src/canvas/CanvasPage.module.css +15 -2
  14. package/src/canvas/CanvasPage.multiselect.test.jsx +7 -0
  15. package/src/canvas/ConnectorLayer.jsx +120 -152
  16. package/src/canvas/ConnectorLayer.module.css +69 -0
  17. package/src/canvas/canvasApi.js +68 -2
  18. package/src/canvas/connectorGeometry.js +132 -0
  19. package/src/canvas/hotPoolDevLogs.js +25 -0
  20. package/src/canvas/useMarqueeSelect.js +30 -4
  21. package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
  22. package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
  23. package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
  24. package/src/canvas/widgets/ComponentWidget.jsx +1 -0
  25. package/src/canvas/widgets/CropOverlay.jsx +219 -0
  26. package/src/canvas/widgets/CropOverlay.module.css +118 -0
  27. package/src/canvas/widgets/ExpandedPane.jsx +472 -0
  28. package/src/canvas/widgets/ExpandedPane.module.css +179 -0
  29. package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
  30. package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  31. package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  32. package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  33. package/src/canvas/widgets/FigmaEmbed.jsx +49 -102
  34. package/src/canvas/widgets/ImageWidget.jsx +129 -8
  35. package/src/canvas/widgets/ImageWidget.module.css +30 -0
  36. package/src/canvas/widgets/LinkPreview.jsx +93 -44
  37. package/src/canvas/widgets/MarkdownBlock.jsx +141 -16
  38. package/src/canvas/widgets/MarkdownBlock.module.css +25 -0
  39. package/src/canvas/widgets/PromptWidget.jsx +414 -0
  40. package/src/canvas/widgets/PromptWidget.module.css +273 -0
  41. package/src/canvas/widgets/PrototypeEmbed.jsx +46 -170
  42. package/src/canvas/widgets/ResizeHandle.jsx +17 -6
  43. package/src/canvas/widgets/StoryWidget.jsx +65 -11
  44. package/src/canvas/widgets/TerminalReadWidget.jsx +11 -5
  45. package/src/canvas/widgets/TerminalReadWidget.module.css +3 -1
  46. package/src/canvas/widgets/TerminalWidget.jsx +301 -124
  47. package/src/canvas/widgets/TerminalWidget.module.css +121 -12
  48. package/src/canvas/widgets/TilesWidget.jsx +302 -0
  49. package/src/canvas/widgets/TilesWidget.module.css +133 -0
  50. package/src/canvas/widgets/WidgetChrome.jsx +67 -152
  51. package/src/canvas/widgets/WidgetChrome.module.css +20 -1
  52. package/src/canvas/widgets/expandUtils.js +385 -16
  53. package/src/canvas/widgets/expandUtils.test.js +155 -0
  54. package/src/canvas/widgets/index.js +6 -2
  55. package/src/canvas/widgets/tilePool.js +23 -0
  56. package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
  57. package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
  58. package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
  59. package/src/canvas/widgets/tiles/leaf.png +0 -0
  60. package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
  61. package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
  62. package/src/canvas/widgets/tiles/solid-a.png +0 -0
  63. package/src/canvas/widgets/tiles/solid-b.png +0 -0
  64. package/src/canvas/widgets/widgetConfig.js +37 -4
  65. package/src/canvas/widgets/widgetIcons.jsx +190 -0
  66. package/src/canvas/widgets/widgetProps.js +1 -0
  67. package/src/context.jsx +47 -19
  68. package/src/hooks/usePrototypeReloadGuard.js +64 -0
  69. package/src/index.js +4 -2
  70. package/src/story/ComponentSetPage.jsx +186 -0
  71. package/src/story/ComponentSetPage.module.css +121 -0
  72. package/src/story/StoryPage.jsx +32 -2
  73. package/src/vite/data-plugin.js +79 -35
  74. package/src/canvas/widgets/ActionWidget.jsx +0 -200
  75. package/src/canvas/widgets/ActionWidget.module.css +0 -122
  76. package/src/canvas/widgets/SplitExpandModal.jsx +0 -234
  77. package/src/canvas/widgets/SplitExpandModal.module.css +0 -335
  78. package/src/canvas/widgets/SplitScreenTopBar.jsx +0 -30
  79. 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
+ }
@@ -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} />
@@ -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
- const parsed = parseDataFile(filePath)
1157
- if (parsed?.suffix === 'canvas' && parsed?.id) {
1158
- const metadata = readCanvasMetadata(filePath, parsed)
1159
- server.ws.send({
1160
- type: 'custom',
1161
- event: 'storyboard:canvas-file-changed',
1162
- data: { canvasId: parsed.id, name: parsed.id, ...(metadata ? { metadata } : {}) },
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
- // Reads .worktrees/ports.json to enumerate active worktree dev servers.
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 portsJsonPath = path.resolve(root, '.worktrees', 'ports.json')
1355
- if (!fs.existsSync(portsJsonPath)) return html
1356
-
1357
- const ports = JSON.parse(fs.readFileSync(portsJsonPath, 'utf-8'))
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 `.storyboard/terminal-snapshots/` into the build
1380
- * output so TerminalReadWidget can fetch them as static files in production.
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 snapshotsDir = path.resolve('.storyboard/terminal-snapshots')
1388
- if (!fs.existsSync(snapshotsDir)) return
1389
-
1390
- const walk = (dir) => {
1391
- const entries = fs.readdirSync(dir, { withFileTypes: true })
1392
- for (const entry of entries) {
1393
- const full = path.join(dir, entry.name)
1394
- if (entry.isDirectory()) {
1395
- walk(full)
1396
- } else if (entry.name.endsWith('.json')) {
1397
- const rel = path.relative(snapshotsDir, full).replace(/\\/g, '/')
1398
- this.emitFile({
1399
- type: 'asset',
1400
- fileName: `_storyboard/terminal-snapshots/${rel}`,
1401
- source: fs.readFileSync(full, 'utf-8'),
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
- }