@dfosco/storyboard 0.6.0-beta.13 → 0.6.0-beta.15

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.
@@ -2,7 +2,6 @@
2
2
  "$comment": "Mascot animation rendered above the Vite dev banner. Each entry in `frames` is either a string (uses frameDurationMs as the default delay) or a [filename, delayMs] tuple for per-frame timing. After loops finish, settleFrame is left on screen with the dev URL beside it.",
3
3
  "frames": [
4
4
  ["frame-01-peek-left.txt", 600],
5
- ["frame-02-eyes-open.txt", 300],
6
5
  ["frame-03-peek-right.txt", 600],
7
6
  ["frame-04-eyes-open.txt", 400],
8
7
  ["frame-05-eyes-closed.txt", 200],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard",
3
- "version": "0.6.0-beta.13",
3
+ "version": "0.6.0-beta.15",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Storyboard prototyping framework — core engine, React integration, and canvas",
@@ -221,25 +221,33 @@ export function buildResumeStartupCommand({ startupCommand, sessionId, agentCfg
221
221
  if (!startupCommand) return startupCommand
222
222
 
223
223
  const notice = `printf '\\n\\033[33m[storyboard] resume failed; starting fresh session...\\033[0m\\n'`
224
- const wrapFallback = (cmd) => agentCfg?.resumeFallback === false
225
- ? cmd
226
- : `${cmd} || { ${notice}; ${startupCommand}; }`
224
+ const lastCmd = agentCfg?.resumeLastCommand
225
+
226
+ // Chain: stored-id resume resumeLastCommand (if any) → fresh startupCommand.
227
+ // Each step's non-zero exit cascades to the next via shell `||`. Note: if a
228
+ // resume hangs (vs. exits non-zero), the chain won't progress — users
229
+ // should use the widget's restart button. We deliberately don't wrap in a
230
+ // timeout to avoid killing slow-starting agents on flaky networks.
231
+ const wrapFallback = (cmd) => {
232
+ if (agentCfg?.resumeFallback === false) return cmd
233
+ const last = lastCmd ? `${lastCmd} || { ${notice}; ${startupCommand}; }` : `${notice}; ${startupCommand}`
234
+ return `${cmd} || { ${last}; }`
235
+ }
227
236
 
228
237
  // Primary: per-widget captured sessionId → `resumeCommand` with {id}.
229
- if (sessionId && isResumableSessionId(sessionId, agentCfg)) {
238
+ if (sessionId && UUID_RE.test(sessionId)) {
230
239
  const template = agentCfg?.resumeCommand
231
240
  if (template && template.includes('{id}')) {
232
241
  return wrapFallback(template.replace('{id}', sessionId))
233
242
  }
234
243
  }
235
244
 
236
- // Fallback: agents like Codex provide a `resumeLastCommand` that
237
- // resumes the most recent session in the current cwd without needing
238
- // a captured id (e.g. `codex resume --last`). Useful when the agent's
239
- // sessionStart hook requires manual trust before it can capture ids
240
- // (Codex case hook needs `/hooks` approval).
241
- const lastCmd = agentCfg?.resumeLastCommand
242
- if (lastCmd) return wrapFallback(lastCmd)
245
+ // No id stored try resumeLastCommand (e.g. `--continue` / `resume --last`),
246
+ // falling through to fresh startupCommand if it exits non-zero.
247
+ if (lastCmd) {
248
+ if (agentCfg?.resumeFallback === false) return lastCmd
249
+ return `${lastCmd} || { ${notice}; ${startupCommand}; }`
250
+ }
243
251
 
244
252
  return startupCommand
245
253
  }
@@ -23,6 +23,7 @@ import { compactAll } from '../canvas/compact.js'
23
23
  import { parseFlags } from './flags.js'
24
24
  import { setupNeeded, writeUserState, getInstalledStoryboardVersion } from './userState.js'
25
25
  import { dim, magenta, bold } from './intro.js'
26
+ import { rmSync } from 'node:fs'
26
27
 
27
28
  /** Find the mascot directory shipped with the storyboard package. */
28
29
  function mascotPaths(targetCwd) {
@@ -157,6 +158,28 @@ function readConfiguredPort(cwd) {
157
158
  }
158
159
  }
159
160
 
161
+ /**
162
+ * Resolve the Vite base path for URL display. Priority:
163
+ * 1. `VITE_BASE_PATH` env var (matches vite.config.js convention)
164
+ * 2. `basePath` key in storyboard.config.json (if user wants to override)
165
+ * 3. `/` (Vite default)
166
+ *
167
+ * Trailing slash is normalized so the rendered URL never double-slashes.
168
+ */
169
+ function resolveBasePath(cwd) {
170
+ let base = process.env.VITE_BASE_PATH || null
171
+ if (!base) {
172
+ try {
173
+ const cfg = JSON.parse(readFileSync(resolve(cwd, 'storyboard.config.json'), 'utf8'))
174
+ if (typeof cfg.basePath === 'string' && cfg.basePath) base = cfg.basePath
175
+ } catch { /* empty */ }
176
+ }
177
+ base = base || '/'
178
+ if (!base.startsWith('/')) base = '/' + base
179
+ if (!base.endsWith('/')) base = base + '/'
180
+ return base
181
+ }
182
+
160
183
  async function main() {
161
184
  const { flags } = parseFlags(process.argv.slice(3), flagSchema)
162
185
  const worktreeName = detectWorktreeName()
@@ -169,6 +192,8 @@ async function main() {
169
192
  const configuredPort = readConfiguredPort(targetCwd)
170
193
  const strictPort = flags.port == null && configuredPort != null
171
194
  const port = flags.port || configuredPort || getPort(worktreeName)
195
+ const basePath = resolveBasePath(targetCwd)
196
+ const devUrl = `http://localhost:${port}${basePath}`
172
197
 
173
198
  const verbose = flags.verbose
174
199
 
@@ -193,6 +218,11 @@ async function main() {
193
218
  ? 'first run in this repo'
194
219
  : `version changed ${need.from} → ${need.to}`
195
220
  if (verbose) p.log.info(`Running setup (${why})…`)
221
+
222
+ // Invalidate Vite's optimize-deps cache when the storyboard version
223
+ // changes. Otherwise the browser hits 504 Outdated Optimize Dep
224
+ // because the dep graph IDs no longer match the cached chunks.
225
+ try { rmSync(join(targetCwd, 'node_modules', '.vite'), { recursive: true, force: true }) } catch { /* empty */ }
196
226
  await new Promise((resolveSetup) => {
197
227
  const setupChild = spawn(
198
228
  process.platform === 'win32' ? 'npx.cmd' : 'npx',
@@ -274,11 +304,11 @@ async function main() {
274
304
  console.log()
275
305
  const animated = showBuddy && renderMascot(
276
306
  mascotPaths(targetCwd),
277
- bold(`http://localhost:${port}/storyboard/`),
307
+ bold(devUrl),
278
308
  dim('Stop with Ctrl+C'),
279
309
  )
280
310
  if (!animated) {
281
- console.log(` ${bold(`http://localhost:${port}/storyboard/`)}`)
311
+ console.log(` ${bold(devUrl)}`)
282
312
  console.log(` ${dim('Stop with Ctrl+C')}`)
283
313
  }
284
314
  // Wait for animation to settle, then flush anything Vite emitted
@@ -323,18 +353,30 @@ async function main() {
323
353
  setTimeout(() => { renderOnce() }, 8000).unref?.()
324
354
  }
325
355
 
356
+ let shuttingDown = false
326
357
  function shutdown() {
358
+ if (shuttingDown) {
359
+ // Second Ctrl+C → hard exit, kill child with SIGKILL.
360
+ try { child.kill('SIGKILL') } catch { /* empty */ }
361
+ process.exit(130)
362
+ }
363
+ shuttingDown = true
327
364
  clearInterval(compactInterval)
328
365
  renameWatcher.close()
329
366
  // Suppress Vite's shutdown-time esbuild noise ("Pre-transform error:
330
- // The service was stopped" for every in-flight transform).
367
+ // The service was stopped" for every in-flight transform) AND the
368
+ // orphan-archive log spam from the storyboard-server plugin teardown.
331
369
  try { child.stdout?.removeAllListeners('data') } catch { /* empty */ }
332
370
  try { child.stderr?.removeAllListeners('data') } catch { /* empty */ }
333
371
  try { child.stdout?.destroy() } catch { /* empty */ }
334
372
  try { child.stderr?.destroy() } catch { /* empty */ }
335
- // SIGINT lets Vite shut down esbuild and HMR cleanly; SIGTERM is harsher
336
- // and tends to leave in-flight transforms aborting noisily.
373
+ // SIGINT first (clean esbuild shutdown), then SIGTERM after 2s if
374
+ // Vite is still alive (handles plugins that loop on session teardown),
375
+ // then SIGKILL after 5s as last resort.
337
376
  try { child.kill('SIGINT') } catch { /* already dead */ }
377
+ const term = setTimeout(() => { try { child.kill('SIGTERM') } catch { /* empty */ } }, 2000)
378
+ const kill = setTimeout(() => { try { child.kill('SIGKILL') } catch { /* empty */ } }, 5000)
379
+ term.unref?.(); kill.unref?.()
338
380
  releasePort(worktreeName)
339
381
  }
340
382
  process.on('SIGINT', shutdown)
@@ -774,6 +774,54 @@ export default function storyboardServer() {
774
774
  })
775
775
  }
776
776
 
777
+ // Auto-reload on Vite's "outdated optimize dep" 504 errors.
778
+ // Happens when the dep graph IDs in cached chunks no longer match
779
+ // what Vite is serving (after upgrades, dep additions, etc).
780
+ // We catch failed fetches inside the page and trigger a full reload
781
+ // once — the second load will see the freshly-built optimize deps.
782
+ if (isDev) {
783
+ tags.push({
784
+ tag: 'script',
785
+ children: `
786
+ (function(){
787
+ var reloaded = false;
788
+ function maybeReload(reason){
789
+ if (reloaded) return;
790
+ if (sessionStorage.getItem('__sb_outdated_reload__')) return;
791
+ reloaded = true;
792
+ sessionStorage.setItem('__sb_outdated_reload__', '1');
793
+ console.warn('[storyboard] Reloading: ' + reason);
794
+ setTimeout(function(){ sessionStorage.removeItem('__sb_outdated_reload__'); }, 5000);
795
+ location.reload();
796
+ }
797
+ // Clear stale guard from previous successful loads.
798
+ if (document.readyState === 'complete') {
799
+ sessionStorage.removeItem('__sb_outdated_reload__');
800
+ } else {
801
+ window.addEventListener('load', function(){
802
+ setTimeout(function(){ sessionStorage.removeItem('__sb_outdated_reload__'); }, 2000);
803
+ });
804
+ }
805
+ // Catch module load failures.
806
+ window.addEventListener('error', function(e){
807
+ var msg = (e && e.message) || '';
808
+ if (/Outdated Optimize Dep|Failed to fetch dynamically imported module|504/i.test(msg)) {
809
+ maybeReload('outdated dep / dynamic import failure');
810
+ }
811
+ }, true);
812
+ // Catch unhandled promise rejections from dynamic imports.
813
+ window.addEventListener('unhandledrejection', function(e){
814
+ var msg = (e && e.reason && (e.reason.message || String(e.reason))) || '';
815
+ if (/Outdated Optimize Dep|Failed to fetch dynamically imported module|504/i.test(msg)) {
816
+ maybeReload('outdated dep / dynamic import failure');
817
+ }
818
+ });
819
+ })();
820
+ `.trim(),
821
+ injectTo: 'head',
822
+ })
823
+ }
824
+
777
825
  // Inject base path so the inspector UI can resolve static assets
778
826
  // (e.g. inspector.json) when deployed under a subpath
779
827
  tags.push({
@@ -18,6 +18,7 @@
18
18
  "icon": "primer/copilot",
19
19
  "startupCommand": "copilot --agent terminal-agent",
20
20
  "resumeCommand": "copilot --resume={id} --agent terminal-agent",
21
+ "resumeLastCommand": "copilot --continue --agent terminal-agent",
21
22
  "sessionIdEnv": "COPILOT_AGENT_SESSION_ID",
22
23
  "postStartup": "/allow-all on",
23
24
  "resizable": true
@@ -27,6 +28,7 @@
27
28
  "icon": "claude",
28
29
  "startupCommand": "claude --agent terminal-agent --dangerously-skip-permissions",
29
30
  "resumeCommand": "claude --resume {id} --agent terminal-agent --dangerously-skip-permissions",
31
+ "resumeLastCommand": "claude --continue --agent terminal-agent --dangerously-skip-permissions",
30
32
  "sessionIdEnv": "CLAUDE_SESSION_ID",
31
33
  "sessionStateGlob": "~/.claude/projects/*/{id}.jsonl",
32
34
  "readinessSignal": "bypass permissions",