@dfosco/storyboard-core 4.2.0-beta.21 → 4.2.0-beta.23
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/dist/storyboard-ui.js +8409 -7533
- package/dist/storyboard-ui.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/setup.js +29 -1
- package/src/cli/terminal-welcome.js +93 -10
- package/src/tools/handlers/canvasAgents.js +1 -1
- package/src/tools/handlers/canvasToolbar.js +1 -1
- package/src/tools/handlers/devtools.js +2 -2
- package/src/tools/handlers/featureFlags.js +1 -1
- package/src/tools/handlers/flows.js +2 -2
package/package.json
CHANGED
package/src/cli/setup.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as p from '@clack/prompts'
|
|
8
|
-
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'fs'
|
|
8
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync, readdirSync, symlinkSync } from 'fs'
|
|
9
9
|
import path from 'path'
|
|
10
10
|
import { execSync } from 'child_process'
|
|
11
11
|
import { generateCaddyfile, isCaddyInstalled, isCaddyRunning, startCaddy, reloadCaddy } from './proxy.js'
|
|
@@ -256,6 +256,34 @@ if (isInstalled('code')) {
|
|
|
256
256
|
}
|
|
257
257
|
} catch { /* ignore */ }
|
|
258
258
|
}
|
|
259
|
+
|
|
260
|
+
// Create symlinks in each CLI's expected agent directory.
|
|
261
|
+
// Source of truth: .agents/*.agent.md
|
|
262
|
+
// Copilot CLI reads .github/agents/*.md
|
|
263
|
+
// Claude Code reads .claude/agents/*.md
|
|
264
|
+
// Codex CLI uses .codex/config.toml (no agent files needed)
|
|
265
|
+
try {
|
|
266
|
+
const agentFiles = readdirSync(agentsDir).filter(f => f.endsWith('.agent.md'))
|
|
267
|
+
const targets = [
|
|
268
|
+
{ dir: path.join('.github', 'agents'), label: 'Copilot CLI' },
|
|
269
|
+
{ dir: path.join('.claude', 'agents'), label: 'Claude Code' },
|
|
270
|
+
]
|
|
271
|
+
for (const { dir, label } of targets) {
|
|
272
|
+
try { mkdirSync(dir, { recursive: true }) } catch { /* ignore */ }
|
|
273
|
+
let linked = 0
|
|
274
|
+
for (const file of agentFiles) {
|
|
275
|
+
const linkName = file.replace('.agent.md', '.md')
|
|
276
|
+
const linkPath = path.join(dir, linkName)
|
|
277
|
+
const target = path.relative(dir, path.join(agentsDir, file))
|
|
278
|
+
if (!existsSync(linkPath)) {
|
|
279
|
+
try { symlinkSync(target, linkPath); linked++ } catch { /* ignore */ }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (linked > 0) {
|
|
283
|
+
p.log.success(`${label} agent symlinks created (${dir}/)`)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch { /* ignore */ }
|
|
259
287
|
}
|
|
260
288
|
|
|
261
289
|
// 8. Proxy
|
|
@@ -24,6 +24,27 @@ import { dim, cyan, bold } from './intro.js'
|
|
|
24
24
|
import { takePendingMessages } from '../canvas/terminal-config.js'
|
|
25
25
|
|
|
26
26
|
const blue = (s) => `\x1b[34m${s}\x1b[0m`
|
|
27
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Drain any pending bytes from stdin to prevent stale mouse escape sequences
|
|
31
|
+
* (or other buffered input) from being consumed by Clack prompts.
|
|
32
|
+
* This is critical after tmux mouse mode was on — mouse events from the
|
|
33
|
+
* browser widget are sent as escape sequences to stdin.
|
|
34
|
+
*/
|
|
35
|
+
function drainStdin() {
|
|
36
|
+
if (!process.stdin.readable) return
|
|
37
|
+
const wasPaused = process.stdin.isPaused?.()
|
|
38
|
+
try {
|
|
39
|
+
process.stdin.setRawMode?.(true)
|
|
40
|
+
process.stdin.resume()
|
|
41
|
+
// Read and discard all buffered data
|
|
42
|
+
while (process.stdin.read() !== null) { /* discard */ }
|
|
43
|
+
} catch { /* best effort */ }
|
|
44
|
+
if (wasPaused) {
|
|
45
|
+
try { process.stdin.pause() } catch {}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
27
48
|
|
|
28
49
|
// Prepend .storyboard/terminals/bin/ to PATH so `start`, `copilot`, etc.
|
|
29
50
|
// are available in child shells. Done once at startup; child shells inherit it.
|
|
@@ -32,6 +53,19 @@ if (existsSync(binDir) && !process.env.PATH?.includes(binDir)) {
|
|
|
32
53
|
process.env.PATH = `${binDir}:${process.env.PATH || ''}`
|
|
33
54
|
}
|
|
34
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Return a copy of process.env with the .storyboard/terminals/bin/ dir
|
|
58
|
+
* stripped from PATH. Used when spawning agent commands so the real binary
|
|
59
|
+
* is found instead of the wrapper scripts that route back through
|
|
60
|
+
* terminal-welcome (which would cause infinite recursion).
|
|
61
|
+
*/
|
|
62
|
+
function agentEnv() {
|
|
63
|
+
const cleanPath = (process.env.PATH || '').split(':')
|
|
64
|
+
.filter(p => !p.endsWith('.storyboard/terminals/bin'))
|
|
65
|
+
.join(':')
|
|
66
|
+
return { ...process.env, PATH: cleanPath }
|
|
67
|
+
}
|
|
68
|
+
|
|
35
69
|
/**
|
|
36
70
|
* Read agents config from storyboard.config.json.
|
|
37
71
|
* Returns an array of { id, label, startupCommand, resumeCommand } entries.
|
|
@@ -189,14 +223,21 @@ async function launchAgent(agent, { isInitialStartup = false } = {}) {
|
|
|
189
223
|
p.outro(dim(`Starting ${agent.label}...`))
|
|
190
224
|
setMouse(true)
|
|
191
225
|
|
|
226
|
+
let exitCode = null
|
|
227
|
+
const startTime = Date.now()
|
|
228
|
+
|
|
192
229
|
try {
|
|
193
230
|
const shell = process.env.SHELL || '/bin/zsh'
|
|
194
|
-
const child = spawn(shell, ['-lc', agent.startupCommand], {
|
|
231
|
+
const child = spawn(shell, ['-lc', agent.startupCommand], {
|
|
232
|
+
stdio: 'inherit',
|
|
233
|
+
env: agentEnv(),
|
|
234
|
+
})
|
|
195
235
|
|
|
196
236
|
// Context injection — inject identity, postStartup, and pending messages
|
|
197
237
|
// after the agent reaches readiness. Skip on initial --startup since
|
|
198
238
|
// terminal-server.js handles that path independently.
|
|
199
239
|
let pollInterval = null
|
|
240
|
+
let readinessTimeout = null
|
|
200
241
|
const tmuxName = !isInitialStartup ? getTmuxName() : null
|
|
201
242
|
const widgetId = process.env.STORYBOARD_WIDGET_ID
|
|
202
243
|
|
|
@@ -234,7 +275,7 @@ async function launchAgent(agent, { isInitialStartup = false } = {}) {
|
|
|
234
275
|
} catch {}
|
|
235
276
|
}, 2000)
|
|
236
277
|
// Timeout after 30s — don't wait forever
|
|
237
|
-
setTimeout(() => {
|
|
278
|
+
readinessTimeout = setTimeout(() => {
|
|
238
279
|
if (!contextSent) {
|
|
239
280
|
contextSent = true
|
|
240
281
|
if (pollInterval) { clearInterval(pollInterval); pollInterval = null }
|
|
@@ -251,30 +292,45 @@ async function launchAgent(agent, { isInitialStartup = false } = {}) {
|
|
|
251
292
|
}
|
|
252
293
|
}
|
|
253
294
|
|
|
254
|
-
await new Promise((resolve) => {
|
|
255
|
-
child.on('close', () => {
|
|
295
|
+
exitCode = await new Promise((resolve) => {
|
|
296
|
+
child.on('close', (code) => {
|
|
256
297
|
if (pollInterval) {
|
|
257
298
|
if (typeof pollInterval.clear === 'function') pollInterval.clear()
|
|
258
299
|
else clearInterval(pollInterval)
|
|
259
300
|
}
|
|
260
|
-
|
|
301
|
+
if (readinessTimeout) clearTimeout(readinessTimeout)
|
|
302
|
+
resolve(code)
|
|
261
303
|
})
|
|
262
304
|
child.on('error', () => {
|
|
263
305
|
if (pollInterval) {
|
|
264
306
|
if (typeof pollInterval.clear === 'function') pollInterval.clear()
|
|
265
307
|
else clearInterval(pollInterval)
|
|
266
308
|
}
|
|
267
|
-
|
|
309
|
+
if (readinessTimeout) clearTimeout(readinessTimeout)
|
|
310
|
+
resolve(1)
|
|
268
311
|
})
|
|
269
312
|
})
|
|
270
313
|
} catch {
|
|
271
314
|
p.log.error(`Failed to start ${agent.label}. Is it installed?`)
|
|
272
315
|
await new Promise(r => setTimeout(r, 2000))
|
|
316
|
+
exitCode = 1
|
|
317
|
+
} finally {
|
|
318
|
+
// Always disable mouse and drain stdin before returning to the welcome
|
|
319
|
+
// loop. Without this, tmux mouse escape sequences from the browser widget
|
|
320
|
+
// accumulate in stdin while the agent runs, and Clack's p.select() reads
|
|
321
|
+
// them as keystrokes — auto-selecting menu options in a tight loop.
|
|
322
|
+
setMouse(false)
|
|
323
|
+
await new Promise(r => setTimeout(r, 50))
|
|
324
|
+
drainStdin()
|
|
273
325
|
}
|
|
326
|
+
|
|
327
|
+
const durationMs = Date.now() - startTime
|
|
328
|
+
return { exitCode, durationMs }
|
|
274
329
|
}
|
|
275
330
|
|
|
276
331
|
async function welcomeLoop() {
|
|
277
332
|
let firstIteration = true
|
|
333
|
+
const MAX_STARTUP_RETRIES = 2
|
|
278
334
|
|
|
279
335
|
while (true) {
|
|
280
336
|
// On first iteration with --startup, auto-launch the command
|
|
@@ -294,14 +350,35 @@ async function welcomeLoop() {
|
|
|
294
350
|
startupCmd.startsWith(a.startupCommand?.split(' ')[0])
|
|
295
351
|
)
|
|
296
352
|
const agent = matchedAgent || { label: startupCmd.split(/\s+/)[0], startupCommand: startupCmd }
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
353
|
+
|
|
354
|
+
let succeeded = false
|
|
355
|
+
for (let attempt = 0; attempt < MAX_STARTUP_RETRIES; attempt++) {
|
|
356
|
+
const result = await launchAgent(agent, { isInitialStartup: true })
|
|
357
|
+
resetTerminal()
|
|
358
|
+
|
|
359
|
+
// Normal exit (user quit the agent) — proceed to welcome menu
|
|
360
|
+
if (result.exitCode === 0 || result.exitCode === null) { succeeded = true; break }
|
|
361
|
+
|
|
362
|
+
// Non-zero exit — agent crashed or failed to start
|
|
363
|
+
const isLastAttempt = attempt === MAX_STARTUP_RETRIES - 1
|
|
364
|
+
if (isLastAttempt) {
|
|
365
|
+
p.log.warn(yellow(`${agent.label} failed to start (exit code ${result.exitCode}).`))
|
|
366
|
+
p.log.info(dim('Falling back to the welcome menu. You can retry from there.'))
|
|
367
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
368
|
+
} else {
|
|
369
|
+
p.log.warn(yellow(`${agent.label} exited unexpectedly (exit code ${result.exitCode}). Retrying...`))
|
|
370
|
+
await new Promise(r => setTimeout(r, 3000))
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (succeeded) continue
|
|
375
|
+
// Fall through to the interactive welcome menu
|
|
300
376
|
}
|
|
301
377
|
firstIteration = false
|
|
302
378
|
|
|
303
379
|
resetTerminal()
|
|
304
380
|
setMouse(false)
|
|
381
|
+
drainStdin()
|
|
305
382
|
console.clear()
|
|
306
383
|
p.intro(`${bold('storyboard terminal')}`)
|
|
307
384
|
|
|
@@ -310,6 +387,7 @@ async function welcomeLoop() {
|
|
|
310
387
|
? { value: 'agents', label: '✦ Start a new agent session' }
|
|
311
388
|
: { value: 'copilot', label: `✦ Start a new ${agents[0]?.label || 'Copilot'} session` }
|
|
312
389
|
|
|
390
|
+
drainStdin()
|
|
313
391
|
const action = await p.select({
|
|
314
392
|
message: 'How would you like to start?',
|
|
315
393
|
options: [
|
|
@@ -326,6 +404,7 @@ async function welcomeLoop() {
|
|
|
326
404
|
|
|
327
405
|
if (action === 'agents') {
|
|
328
406
|
// Multi-agent sub-select
|
|
407
|
+
drainStdin()
|
|
329
408
|
const agentChoice = await p.select({
|
|
330
409
|
message: 'Which agent?',
|
|
331
410
|
options: agents.map(a => ({
|
|
@@ -378,6 +457,7 @@ async function welcomeLoop() {
|
|
|
378
457
|
{ value: 'terminal', label: '⊞ Terminal sessions' },
|
|
379
458
|
]
|
|
380
459
|
|
|
460
|
+
drainStdin()
|
|
381
461
|
const sessionChoice = await p.select({
|
|
382
462
|
message: 'Browse sessions',
|
|
383
463
|
options: sessionOptions,
|
|
@@ -409,7 +489,10 @@ async function welcomeLoop() {
|
|
|
409
489
|
setMouse(true)
|
|
410
490
|
try {
|
|
411
491
|
const shell = process.env.SHELL || '/bin/zsh'
|
|
412
|
-
const child = spawn(shell, ['-lc', agent.resumeCommand], {
|
|
492
|
+
const child = spawn(shell, ['-lc', agent.resumeCommand], {
|
|
493
|
+
stdio: 'inherit',
|
|
494
|
+
env: agentEnv(),
|
|
495
|
+
})
|
|
413
496
|
await new Promise((resolve) => {
|
|
414
497
|
child.on('close', resolve)
|
|
415
498
|
child.on('error', resolve)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
export const id = 'canvas-agents'
|
|
8
8
|
|
|
9
9
|
export async function guard(ctx) {
|
|
10
|
-
const { getConfig } = await import('
|
|
10
|
+
const { getConfig } = await import('@dfosco/storyboard-core')
|
|
11
11
|
const canvasConfig = getConfig('canvas')
|
|
12
12
|
const agents = canvasConfig?.agents
|
|
13
13
|
return agents && typeof agents === 'object' && Object.keys(agents).length > 0
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* handler. The component() export returns the component matching the tool's
|
|
6
6
|
* render type, resolved at load time via the toolConfig passed to component().
|
|
7
7
|
*/
|
|
8
|
-
import { getCanvasZoom } from '
|
|
8
|
+
import { getCanvasZoom } from '@dfosco/storyboard-core'
|
|
9
9
|
|
|
10
10
|
export const id = 'canvas-toolbar'
|
|
11
11
|
|
|
@@ -19,11 +19,11 @@ export async function handler(ctx) {
|
|
|
19
19
|
let commentsAuth = null
|
|
20
20
|
let prodMode = null
|
|
21
21
|
let ff = null
|
|
22
|
-
try { loader = await import('
|
|
22
|
+
try { loader = await import('@dfosco/storyboard-core') } catch { /* optional */ }
|
|
23
23
|
try { hm = await import('../../hideMode.js') } catch { /* optional */ }
|
|
24
24
|
try { commentsAuth = await import('../../comments/auth.js') } catch { /* optional */ }
|
|
25
25
|
try { prodMode = await import('../../prodMode.js') } catch { /* optional */ }
|
|
26
|
-
try { ff = await import('
|
|
26
|
+
try { ff = await import('@dfosco/storyboard-core') } catch { /* optional */ }
|
|
27
27
|
|
|
28
28
|
return {
|
|
29
29
|
getChildren: () => {
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
export const id = 'flows'
|
|
8
8
|
|
|
9
9
|
export async function handler(ctx) {
|
|
10
|
-
const loader = await import('
|
|
11
|
-
const vf =
|
|
10
|
+
const loader = await import('@dfosco/storyboard-core')
|
|
11
|
+
const vf = loader
|
|
12
12
|
const { basePath = '/' } = ctx
|
|
13
13
|
|
|
14
14
|
return {
|