@dfosco/storyboard 0.6.0-beta.18 → 0.6.0-beta.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard",
3
- "version": "0.6.0-beta.18",
3
+ "version": "0.6.0-beta.19",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Storyboard prototyping framework — core engine, React integration, and canvas",
@@ -1376,41 +1376,53 @@ function handleConnection(ws, widgetId, canvasId, prettyName, widgetStartupComma
1376
1376
  } catch { /* empty */ }
1377
1377
 
1378
1378
  if (readinessSignal || useReadyFile) {
1379
- // Poll for readiness, then send postStartup command and deliver messages
1380
- let sent = false
1381
- const finalize = (reason) => {
1382
- if (sent) return
1383
- sent = true
1379
+ // Poll for readiness, then send postStartup command and deliver messages.
1380
+ // The .ready marker is the accurate signal (Copilot/Claude/Codex
1381
+ // sessionStart hook touches it once the TUI is interactive). On
1382
+ // consumer repos with cold caches + resume mode, that can easily
1383
+ // take 30–60s — well past any reasonable hard timeout. So we:
1384
+ // 1. Keep polling indefinitely at 3s intervals for .ready.
1385
+ // 2. At 30s, do a "soft commit": inject identity and bind to
1386
+ // the messaging bus so the widget is at least addressable
1387
+ // for hub broadcast and live messages.
1388
+ // 3. When .ready finally appears, run the real postStartup.
1389
+ let softBound = false
1390
+ let postStartupSent = false
1391
+ const softBind = () => {
1392
+ if (softBound) return
1393
+ softBound = true
1394
+ injectIdentityMessage(tmuxName, { widgetId, displayName: prettyName, canvasId, branch, serverUrl })
1395
+ injectRoleMessageForWidget(tmuxName, widgetId)
1396
+ setTimeout(() => {
1397
+ migratePendingMessages(widgetId, branch, canvasId).then(() => {
1398
+ bindWidget({ widgetId, tmuxName, branch, canvasId, displayName: prettyName }).catch(() => {})
1399
+ }).catch(() => {
1400
+ bindWidget({ widgetId, tmuxName, branch, canvasId, displayName: prettyName }).catch(() => {})
1401
+ })
1402
+ joinPresence({ widgetId, senderName: prettyName || widgetId, branch, canvasId }).catch(() => {})
1403
+ }, 2000)
1404
+ }
1405
+ const runPostStartup = (reason) => {
1406
+ if (postStartupSent) return
1407
+ postStartupSent = true
1384
1408
  clearInterval(pollInterval)
1385
1409
  try { rmSync(readyFilePath, { force: true }) } catch { /* empty */ }
1386
1410
  setTimeout(() => {
1387
1411
  if (postStartup) {
1388
1412
  tmuxSubmit(tmuxName, postStartup)
1389
1413
  }
1390
- // Inject identity, then bind to messaging bus. This restores
1391
- // hub/role/broadcast context after a tmux restart — the
1392
- // widget's terminal-config preserves hubs/role/connectedWidgets,
1393
- // and bindWidget reattaches live message delivery + backfill.
1394
- injectIdentityMessage(tmuxName, { widgetId, displayName: prettyName, canvasId, branch, serverUrl })
1395
- injectRoleMessageForWidget(tmuxName, widgetId)
1396
- setTimeout(() => {
1397
- migratePendingMessages(widgetId, branch, canvasId).then(() => {
1398
- bindWidget({ widgetId, tmuxName, branch, canvasId, displayName: prettyName }).catch(() => {})
1399
- }).catch(() => {
1400
- bindWidget({ widgetId, tmuxName, branch, canvasId, displayName: prettyName }).catch(() => {})
1401
- })
1402
- joinPresence({ widgetId, senderName: prettyName || widgetId, branch, canvasId }).catch(() => {})
1403
- }, 2000)
1414
+ // Ensure identity/bind happened (no-op if already soft-bound)
1415
+ softBind()
1404
1416
  }, 500)
1405
- if (reason === 'timeout') {
1406
- devLog(`[terminal-server] readiness timeout for ${widgetId} (${tmuxName}); proceeding with bind anyway (likely resume mode)`)
1417
+ if (reason === 'soft-bind-timeout') {
1418
+ devLog(`[terminal-server] readiness slow for ${widgetId} (${tmuxName}); soft-binding now, will run postStartup when .ready arrives`)
1407
1419
  }
1408
1420
  }
1409
1421
  const pollInterval = setInterval(() => {
1410
- if (sent) { clearInterval(pollInterval); return }
1422
+ if (postStartupSent) { clearInterval(pollInterval); return }
1411
1423
  // Primary: sessionStart hook touches the marker once the
1412
1424
  // agent is fully loaded and interactive.
1413
- if (useReadyFile && existsSync(readyFilePath)) { finalize('file'); return }
1425
+ if (useReadyFile && existsSync(readyFilePath)) { runPostStartup('file'); return }
1414
1426
  if (!readinessSignal) return
1415
1427
  try {
1416
1428
  // Fallback for agents without a hook: scan pane + 200
@@ -1419,14 +1431,13 @@ function handleConnection(ws, widgetId, canvasId, prettyName, widgetStartupComma
1419
1431
  `tmux capture-pane -t "${tmuxName}" -p -S -200`,
1420
1432
  { encoding: 'utf8', timeout: 1000 }
1421
1433
  )
1422
- if (paneContent.includes(readinessSignal)) finalize('signal')
1434
+ if (paneContent.includes(readinessSignal)) runPostStartup('signal')
1423
1435
  } catch { /* empty */ }
1424
- }, 1000)
1425
- // Fallback: if readiness signal never matches (e.g. resume mode
1426
- // doesn't print "Environment loaded:", or the agent shows a
1427
- // prompt we can't detect), bind anyway after 30s so the widget
1428
- // is at least addressable for live messages and hub broadcast.
1429
- setTimeout(() => finalize('timeout'), 30000)
1436
+ }, 500)
1437
+ // Soft-bind at 30s so the widget is addressable for hub/live
1438
+ // messages even if Copilot's TUI is still warming up. The poller
1439
+ // keeps running and will fire postStartup once .ready appears.
1440
+ setTimeout(softBind, 30000)
1430
1441
  } else {
1431
1442
  // No readiness signal — inject identity and bind to bus after a delay
1432
1443
  setTimeout(() => {
@@ -32,12 +32,18 @@ function resolveStorySetUrl(storyId, layout, selected, density, theme) {
32
32
  if (!story?._storyModule) return ''
33
33
  const base = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
34
34
  const params = new URLSearchParams()
35
- params.set('module', story._storyModule)
35
+ // Route via the real story page (works in dev AND prod). The dev-only
36
+ // `_storyboard/canvas/isolate-set` middleware doesn't exist in deployed
37
+ // builds, so we mount ComponentSetPage at the story's route with
38
+ // `_sb_component_set` instead. `_sb_embed` keeps the canvas chrome off.
39
+ params.set('_sb_embed', '')
40
+ params.set('_sb_component_set', '')
36
41
  if (layout) params.set('layout', layout)
37
42
  if (selected) params.set('selected', selected)
38
43
  if (density) params.set('density', density)
39
44
  if (theme) params.set('theme', theme)
40
- return `${base}/_storyboard/canvas/isolate-set?${params}`
45
+ const route = story._route || `/components/${storyId}`
46
+ return `${base}${route}?${params}`
41
47
  }
42
48
 
43
49
  export default forwardRef(function StorySetWidget({ id: widgetId, props, onUpdate, resizable }, ref) {
@@ -103,7 +109,7 @@ export default forwardRef(function StorySetWidget({ id: widgetId, props, onUpdat
103
109
  if (typeof width === 'number' && typeof height === 'number') return
104
110
  const headerH = 37
105
111
  const newW = typeof width === 'number' ? width : Math.max(200, Math.ceil(e.data.width))
106
- const newH = typeof height === 'number' ? height : Math.max(120, Math.ceil(e.data.height) + headerH)
112
+ const newH = typeof height === 'number' ? height : Math.max(120, Math.ceil(e.data.height) + headerH + 8)
107
113
  onUpdate?.({ width: newW, height: newH })
108
114
  } else if (e.data?.type === 'storyboard:component-set:content-size') {
109
115
  contentSizeRef.current = {
@@ -128,7 +134,7 @@ export default forwardRef(function StorySetWidget({ id: widgetId, props, onUpdat
128
134
  const contentH = contentSizeRef.current.height
129
135
  const contentW = contentSizeRef.current.width
130
136
  if (!contentH && !contentW) return
131
- const fitH = contentH ? contentH + headerH : h
137
+ const fitH = contentH ? contentH + headerH + 8 : h
132
138
  const fitW = contentW || w
133
139
  const shouldSnapH = contentH && h > fitH + 2
134
140
  const shouldSnapW = contentW && w > fitW + 2
@@ -513,12 +513,14 @@ export function buildSecondaryIframeUrl(widget) {
513
513
  const storyData = getStoryData(storyId)
514
514
  if (storyData?._storyModule) {
515
515
  const params = new URLSearchParams()
516
- params.set('module', storyData._storyModule)
516
+ params.set('_sb_embed', '')
517
+ params.set('_sb_component_set', '')
517
518
  const layout = widget.props?.layout
518
519
  if (layout) params.set('layout', layout)
519
520
  const selected = widget.props?.selected
520
521
  if (selected) params.set('selected', selected)
521
- return `${baseClean}/_storyboard/canvas/isolate-set?${params}`
522
+ const route = storyData._route || `/components/${storyId}`
523
+ return `${baseClean}${route}?${params}`
522
524
  }
523
525
  return null
524
526
  }