@dfosco/storyboard 0.5.0-beta.47 → 0.5.0-beta.49
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
|
@@ -3015,9 +3015,17 @@ export function Default() {
|
|
|
3015
3015
|
}
|
|
3016
3016
|
|
|
3017
3017
|
try {
|
|
3018
|
-
const { readTerminalConfig, initTerminalConfig } = await import('./terminal-config.js')
|
|
3018
|
+
const { readTerminalConfig, readTerminalConfigById, initTerminalConfig } = await import('./terminal-config.js')
|
|
3019
3019
|
initTerminalConfig(root)
|
|
3020
|
-
|
|
3020
|
+
let config = readTerminalConfig({ branch, canvasId, widgetId })
|
|
3021
|
+
// Fallback: when canvasId/branch are missing, mismatched, or stale
|
|
3022
|
+
// (e.g. the persisted canvasId only captured the parent folder),
|
|
3023
|
+
// recover via the widget-id-named symlink that writeTerminalConfig
|
|
3024
|
+
// creates next to the hashed config.
|
|
3025
|
+
if (!config?.agentStatus) {
|
|
3026
|
+
const byId = readTerminalConfigById(widgetId)
|
|
3027
|
+
if (byId?.agentStatus) config = byId
|
|
3028
|
+
}
|
|
3021
3029
|
sendJson(res, 200, { agentStatus: config?.agentStatus || null })
|
|
3022
3030
|
} catch (err) {
|
|
3023
3031
|
sendJson(res, 500, { error: `Failed to read agent status: ${err.message}` })
|
|
@@ -9,6 +9,27 @@ function getBase() {
|
|
|
9
9
|
return (import.meta.env?.BASE_URL || '/').replace(/\/$/, '')
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the **full** canvas-page id for the currently-mounted canvas.
|
|
14
|
+
* Prefer the live bridge state (set by CanvasPage), which always carries the
|
|
15
|
+
* complete nested id (e.g. `dfosco-explorations/adaptive-threat-view`). Fall
|
|
16
|
+
* back to URL parsing when the bridge isn't ready yet — and crucially, take
|
|
17
|
+
* **every** path segment after `/canvas/`, not just the first one, so nested
|
|
18
|
+
* canvas ids round-trip correctly.
|
|
19
|
+
*/
|
|
20
|
+
function resolveCanvasId() {
|
|
21
|
+
if (typeof window !== 'undefined') {
|
|
22
|
+
const fromBridge = window.__storyboardCanvasBridgeState?.canvasId
|
|
23
|
+
if (fromBridge) return fromBridge
|
|
24
|
+
const pathParts = window.location.pathname.split('/').filter(Boolean)
|
|
25
|
+
const canvasIdx = pathParts.indexOf('canvas')
|
|
26
|
+
if (canvasIdx >= 0 && canvasIdx < pathParts.length - 1) {
|
|
27
|
+
return pathParts.slice(canvasIdx + 1).join('/')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return 'default'
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
async function spawnPromptAgent({ canvasId, widgetId, prompt }) {
|
|
13
34
|
const res = await fetch(`${getBase()}/_storyboard/canvas/prompt/spawn`, {
|
|
14
35
|
method: 'POST',
|
|
@@ -116,37 +137,84 @@ const PromptWidget = forwardRef(function PromptWidget({ id, props, onUpdate, res
|
|
|
116
137
|
const onUpdateRef = useRef(onUpdate)
|
|
117
138
|
useEffect(() => { onUpdateRef.current = onUpdate }, [onUpdate])
|
|
118
139
|
|
|
140
|
+
// Apply a status payload (from WS event or polled config) to local + persisted state.
|
|
141
|
+
const applyStatus = useCallback((data) => {
|
|
142
|
+
if (!data || !data.status) return
|
|
143
|
+
if (data.status === 'done' || data.status === 'completed') {
|
|
144
|
+
setExecStatus('done')
|
|
145
|
+
onUpdateRef.current?.({ status: 'done' })
|
|
146
|
+
} else if (data.status === 'error') {
|
|
147
|
+
setExecStatus('error')
|
|
148
|
+
setExecError(data.message || 'Unknown error')
|
|
149
|
+
onUpdateRef.current?.({ status: 'error', errorMessage: data.message || 'Unknown error' })
|
|
150
|
+
} else if (data.status === 'cancelled') {
|
|
151
|
+
setExecStatus('idle')
|
|
152
|
+
onUpdateRef.current?.({ status: 'idle', sessionId: '', errorMessage: '' })
|
|
153
|
+
} else if (data.status === 'working' || data.status === 'running' || data.status === 'pending') {
|
|
154
|
+
setExecStatus('pending')
|
|
155
|
+
onUpdateRef.current?.({ status: 'pending' })
|
|
156
|
+
}
|
|
157
|
+
}, [])
|
|
158
|
+
|
|
119
159
|
useEffect(() => {
|
|
120
160
|
if (!import.meta.hot) return
|
|
121
|
-
|
|
122
161
|
const handler = (data) => {
|
|
123
162
|
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
|
-
}
|
|
163
|
+
applyStatus(data)
|
|
141
164
|
}
|
|
142
|
-
|
|
143
165
|
import.meta.hot.on('storyboard:agent-status', handler)
|
|
144
166
|
return () => {
|
|
145
167
|
if (typeof import.meta.hot.off === 'function') {
|
|
146
168
|
import.meta.hot.off('storyboard:agent-status', handler)
|
|
147
169
|
}
|
|
148
170
|
}
|
|
149
|
-
}, [id])
|
|
171
|
+
}, [id, applyStatus])
|
|
172
|
+
|
|
173
|
+
// Reconcile with the server-side persisted agentStatus.
|
|
174
|
+
// The agent always writes its final status to .storyboard/terminals/{id}.json
|
|
175
|
+
// via POST /agent/signal, but the live `storyboard:agent-status` WS event
|
|
176
|
+
// can be missed (tab in background, HMR reconnect, page navigation). On
|
|
177
|
+
// mount and while we believe the agent is still pending, poll the persisted
|
|
178
|
+
// status as a safety net so the widget can never get stuck.
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (execStatus !== 'pending' && execStatus !== 'idle') return
|
|
181
|
+
|
|
182
|
+
let cancelled = false
|
|
183
|
+
let timer = null
|
|
184
|
+
|
|
185
|
+
async function pollOnce() {
|
|
186
|
+
try {
|
|
187
|
+
const params = new URLSearchParams({ widgetId: id })
|
|
188
|
+
const cid = resolveCanvasId()
|
|
189
|
+
if (cid && cid !== 'default') params.set('canvasId', cid)
|
|
190
|
+
const url = `${getBase()}/_storyboard/canvas/agent/status?${params}`
|
|
191
|
+
const res = await fetch(url)
|
|
192
|
+
if (!res.ok) return
|
|
193
|
+
const json = await res.json().catch(() => null)
|
|
194
|
+
const persisted = json?.agentStatus
|
|
195
|
+
if (cancelled || !persisted?.status) return
|
|
196
|
+
// Only apply terminal states or transitions we don't already reflect.
|
|
197
|
+
if (persisted.status === 'done' || persisted.status === 'error' || persisted.status === 'cancelled') {
|
|
198
|
+
applyStatus(persisted)
|
|
199
|
+
} else if (execStatus === 'idle' && (persisted.status === 'running' || persisted.status === 'working' || persisted.status === 'pending')) {
|
|
200
|
+
applyStatus(persisted)
|
|
201
|
+
}
|
|
202
|
+
} catch { /* offline — try again next tick */ }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Reconcile immediately on mount / when we enter pending.
|
|
206
|
+
pollOnce()
|
|
207
|
+
// And keep polling at a low cadence while pending, in case the WS event
|
|
208
|
+
// is missed entirely.
|
|
209
|
+
if (execStatus === 'pending') {
|
|
210
|
+
timer = setInterval(pollOnce, 5000)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return () => {
|
|
214
|
+
cancelled = true
|
|
215
|
+
if (timer) clearInterval(timer)
|
|
216
|
+
}
|
|
217
|
+
}, [id, execStatus, applyStatus])
|
|
150
218
|
|
|
151
219
|
useImperativeHandle(ref, () => ({
|
|
152
220
|
handleAction(action) {
|
|
@@ -169,9 +237,7 @@ const PromptWidget = forwardRef(function PromptWidget({ id, props, onUpdate, res
|
|
|
169
237
|
setExecStatus('pending')
|
|
170
238
|
setExecError('')
|
|
171
239
|
|
|
172
|
-
const
|
|
173
|
-
const canvasIdx = pathParts.indexOf('canvas')
|
|
174
|
-
const canvasId = canvasIdx >= 0 ? pathParts[canvasIdx + 1] : 'default'
|
|
240
|
+
const canvasId = resolveCanvasId()
|
|
175
241
|
|
|
176
242
|
onUpdate?.({ text: draftText, status: 'pending' })
|
|
177
243
|
|
|
@@ -271,9 +337,7 @@ const PromptWidget = forwardRef(function PromptWidget({ id, props, onUpdate, res
|
|
|
271
337
|
term.open(termContainerRef.current)
|
|
272
338
|
termRef.current = term
|
|
273
339
|
|
|
274
|
-
const
|
|
275
|
-
const canvasIdx = pathParts.indexOf('canvas')
|
|
276
|
-
const canvasId = canvasIdx >= 0 ? pathParts[canvasIdx + 1] : 'default'
|
|
340
|
+
const canvasId = resolveCanvasId()
|
|
277
341
|
|
|
278
342
|
const url = getWsUrl(id, canvasId, true)
|
|
279
343
|
ws = new WebSocket(url)
|