@dfosco/storyboard 0.5.0-beta.46 → 0.5.0-beta.48
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
|
@@ -963,6 +963,10 @@ function BranchNav({ basePath }) {
|
|
|
963
963
|
const { branches, currentBranch, branchBasePath } = useBranches(basePath)
|
|
964
964
|
const [switching, setSwitching] = useState(null)
|
|
965
965
|
|
|
966
|
+
// Branch switcher is meaningful only in deployed environments where users
|
|
967
|
+
// need to jump between branch deploys. In local dev each worktree is its own
|
|
968
|
+
// process; switching is handled via `sb dev` / `sb code`.
|
|
969
|
+
if (isLocalDev) return null
|
|
966
970
|
if (!branches || branches.length === 0) return null
|
|
967
971
|
|
|
968
972
|
const branchNames = branches.map(b => b.branch)
|
|
@@ -116,37 +116,81 @@ const PromptWidget = forwardRef(function PromptWidget({ id, props, onUpdate, res
|
|
|
116
116
|
const onUpdateRef = useRef(onUpdate)
|
|
117
117
|
useEffect(() => { onUpdateRef.current = onUpdate }, [onUpdate])
|
|
118
118
|
|
|
119
|
+
// Apply a status payload (from WS event or polled config) to local + persisted state.
|
|
120
|
+
const applyStatus = useCallback((data) => {
|
|
121
|
+
if (!data || !data.status) return
|
|
122
|
+
if (data.status === 'done' || data.status === 'completed') {
|
|
123
|
+
setExecStatus('done')
|
|
124
|
+
onUpdateRef.current?.({ status: 'done' })
|
|
125
|
+
} else if (data.status === 'error') {
|
|
126
|
+
setExecStatus('error')
|
|
127
|
+
setExecError(data.message || 'Unknown error')
|
|
128
|
+
onUpdateRef.current?.({ status: 'error', errorMessage: data.message || 'Unknown error' })
|
|
129
|
+
} else if (data.status === 'cancelled') {
|
|
130
|
+
setExecStatus('idle')
|
|
131
|
+
onUpdateRef.current?.({ status: 'idle', sessionId: '', errorMessage: '' })
|
|
132
|
+
} else if (data.status === 'working' || data.status === 'running' || data.status === 'pending') {
|
|
133
|
+
setExecStatus('pending')
|
|
134
|
+
onUpdateRef.current?.({ status: 'pending' })
|
|
135
|
+
}
|
|
136
|
+
}, [])
|
|
137
|
+
|
|
119
138
|
useEffect(() => {
|
|
120
139
|
if (!import.meta.hot) return
|
|
121
|
-
|
|
122
140
|
const handler = (data) => {
|
|
123
141
|
if (data.widgetId !== id) return
|
|
124
|
-
|
|
125
|
-
setExecStatus('done')
|
|
126
|
-
onUpdateRef.current?.({ status: 'done' })
|
|
127
|
-
} else if (data.status === 'error') {
|
|
128
|
-
setExecStatus('error')
|
|
129
|
-
setExecError(data.message || 'Unknown error')
|
|
130
|
-
onUpdateRef.current?.({ status: 'error', errorMessage: data.message || 'Unknown error' })
|
|
131
|
-
} else if (data.status === 'cancelled') {
|
|
132
|
-
setExecStatus('idle')
|
|
133
|
-
onUpdateRef.current?.({ status: 'idle', sessionId: '', errorMessage: '' })
|
|
134
|
-
} else if (data.status === 'working') {
|
|
135
|
-
setExecStatus('pending')
|
|
136
|
-
onUpdateRef.current?.({ status: 'pending' })
|
|
137
|
-
} else if (data.status === 'running' || data.status === 'pending') {
|
|
138
|
-
setExecStatus('pending')
|
|
139
|
-
onUpdateRef.current?.({ status: 'pending' })
|
|
140
|
-
}
|
|
142
|
+
applyStatus(data)
|
|
141
143
|
}
|
|
142
|
-
|
|
143
144
|
import.meta.hot.on('storyboard:agent-status', handler)
|
|
144
145
|
return () => {
|
|
145
146
|
if (typeof import.meta.hot.off === 'function') {
|
|
146
147
|
import.meta.hot.off('storyboard:agent-status', handler)
|
|
147
148
|
}
|
|
148
149
|
}
|
|
149
|
-
}, [id])
|
|
150
|
+
}, [id, applyStatus])
|
|
151
|
+
|
|
152
|
+
// Reconcile with the server-side persisted agentStatus.
|
|
153
|
+
// The agent always writes its final status to .storyboard/terminals/{id}.json
|
|
154
|
+
// via POST /agent/signal, but the live `storyboard:agent-status` WS event
|
|
155
|
+
// can be missed (tab in background, HMR reconnect, page navigation). On
|
|
156
|
+
// mount and while we believe the agent is still pending, poll the persisted
|
|
157
|
+
// status as a safety net so the widget can never get stuck.
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (execStatus !== 'pending' && execStatus !== 'idle') return
|
|
160
|
+
|
|
161
|
+
let cancelled = false
|
|
162
|
+
let timer = null
|
|
163
|
+
|
|
164
|
+
async function pollOnce() {
|
|
165
|
+
try {
|
|
166
|
+
const url = `${getBase()}/_storyboard/canvas/agent/status?widgetId=${encodeURIComponent(id)}`
|
|
167
|
+
const res = await fetch(url)
|
|
168
|
+
if (!res.ok) return
|
|
169
|
+
const json = await res.json().catch(() => null)
|
|
170
|
+
const persisted = json?.agentStatus
|
|
171
|
+
if (cancelled || !persisted?.status) return
|
|
172
|
+
// Only apply terminal states or transitions we don't already reflect.
|
|
173
|
+
if (persisted.status === 'done' || persisted.status === 'error' || persisted.status === 'cancelled') {
|
|
174
|
+
applyStatus(persisted)
|
|
175
|
+
} else if (execStatus === 'idle' && (persisted.status === 'running' || persisted.status === 'working' || persisted.status === 'pending')) {
|
|
176
|
+
applyStatus(persisted)
|
|
177
|
+
}
|
|
178
|
+
} catch { /* offline — try again next tick */ }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Reconcile immediately on mount / when we enter pending.
|
|
182
|
+
pollOnce()
|
|
183
|
+
// And keep polling at a low cadence while pending, in case the WS event
|
|
184
|
+
// is missed entirely.
|
|
185
|
+
if (execStatus === 'pending') {
|
|
186
|
+
timer = setInterval(pollOnce, 5000)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return () => {
|
|
190
|
+
cancelled = true
|
|
191
|
+
if (timer) clearInterval(timer)
|
|
192
|
+
}
|
|
193
|
+
}, [id, execStatus, applyStatus])
|
|
150
194
|
|
|
151
195
|
useImperativeHandle(ref, () => ({
|
|
152
196
|
handleAction(action) {
|
|
@@ -1394,6 +1394,41 @@ export default function storyboardDataPlugin() {
|
|
|
1394
1394
|
}
|
|
1395
1395
|
},
|
|
1396
1396
|
|
|
1397
|
+
// Guard against known invalid named imports from @primer/react.
|
|
1398
|
+
// The most common offender is `Octicon`, which lives in
|
|
1399
|
+
// `@primer/octicons-react`, not `@primer/react`. When a consumer
|
|
1400
|
+
// writes `import { Octicon } from '@primer/react'`, Vite happily
|
|
1401
|
+
// pre-bundles the dep and only fails at runtime with a cryptic
|
|
1402
|
+
// "does not provide an export named 'Octicon'" error from inside
|
|
1403
|
+
// `node_modules/.vite/deps/@primer_react.js`. Catch it here at
|
|
1404
|
+
// transform time with a clear, actionable error pointing to the
|
|
1405
|
+
// correct package.
|
|
1406
|
+
transform(code, id) {
|
|
1407
|
+
if (!/\.(jsx?|tsx?|mjs|cjs)(\?|$)/.test(id)) return null
|
|
1408
|
+
if (id.includes('/node_modules/')) return null
|
|
1409
|
+
if (!code.includes('@primer/react')) return null
|
|
1410
|
+
const re = /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]@primer\/react['"]/g
|
|
1411
|
+
let match
|
|
1412
|
+
while ((match = re.exec(code)) !== null) {
|
|
1413
|
+
const names = match[1]
|
|
1414
|
+
.split(',')
|
|
1415
|
+
.map(s => s.replace(/\s+as\s+\w+/, '').trim())
|
|
1416
|
+
.filter(Boolean)
|
|
1417
|
+
if (names.includes('Octicon')) {
|
|
1418
|
+
const before = code.slice(0, match.index)
|
|
1419
|
+
const line = before.split('\n').length
|
|
1420
|
+
const rel = id.replace(root + '/', '')
|
|
1421
|
+
throw new Error(
|
|
1422
|
+
`[storyboard] Invalid import in ${rel}:${line} — \`Octicon\` is not exported by \`@primer/react\`.\n` +
|
|
1423
|
+
` Import the icon you need directly from \`@primer/octicons-react\` instead, e.g.:\n` +
|
|
1424
|
+
` import { GearIcon } from '@primer/octicons-react'\n` +
|
|
1425
|
+
` See AGENTS.md: "Use Primer Octicons from @primer/octicons-react for icons".`
|
|
1426
|
+
)
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return null
|
|
1430
|
+
},
|
|
1431
|
+
|
|
1397
1432
|
handleHotUpdate(ctx) {
|
|
1398
1433
|
const normalized = ctx.file.replace(/\\/g, '/')
|
|
1399
1434
|
if (!/\.canvas\.jsonl$/.test(normalized)) return
|