@dfosco/storyboard 0.5.0-beta.36 → 0.5.0-beta.37
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
|
@@ -237,7 +237,10 @@ async function launchAgent(agent, { isInitialStartup = false } = {}) {
|
|
|
237
237
|
|
|
238
238
|
try {
|
|
239
239
|
const shell = process.env.SHELL || '/bin/zsh'
|
|
240
|
-
|
|
240
|
+
// -ilc: interactive + login so both .zprofile and .zshrc are sourced.
|
|
241
|
+
// Many users install agent CLIs (claude, copilot, etc.) via nvm/volta/asdf
|
|
242
|
+
// shims that only register PATH in .zshrc. Without -i the binary is not found.
|
|
243
|
+
const child = spawn(shell, ['-ilc', agent.startupCommand], {
|
|
241
244
|
stdio: 'inherit',
|
|
242
245
|
env: agentEnv(),
|
|
243
246
|
})
|
|
@@ -498,7 +501,7 @@ async function welcomeLoop() {
|
|
|
498
501
|
setMouse(true)
|
|
499
502
|
try {
|
|
500
503
|
const shell = process.env.SHELL || '/bin/zsh'
|
|
501
|
-
const child = spawn(shell, ['-
|
|
504
|
+
const child = spawn(shell, ['-ilc', agent.resumeCommand], {
|
|
502
505
|
stdio: 'inherit',
|
|
503
506
|
env: agentEnv(),
|
|
504
507
|
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const SOURCE = readFileSync(join(__dirname, 'terminal-welcome.js'), 'utf8')
|
|
8
|
+
|
|
9
|
+
// Regression guard for the "command not found: claude" bug.
|
|
10
|
+
//
|
|
11
|
+
// Background: agents spawn via the user's $SHELL with -lc (login shell). On
|
|
12
|
+
// zsh that sources /etc/zprofile + ~/.zprofile but NOT ~/.zshrc. Many agent
|
|
13
|
+
// CLIs (claude, copilot, nvm/volta/asdf shims) only register PATH in
|
|
14
|
+
// ~/.zshrc, so a login-only shell cannot find them. Always use -ilc
|
|
15
|
+
// (interactive + login) so both rc files are sourced.
|
|
16
|
+
//
|
|
17
|
+
// This test scans the source rather than importing the module because
|
|
18
|
+
// terminal-welcome.js is a CLI entrypoint with top-level side effects and
|
|
19
|
+
// no exports. If you later refactor to export the spawn args, replace this
|
|
20
|
+
// with a stub-spawn unit test.
|
|
21
|
+
describe('terminal-welcome: agent spawn shell flags', () => {
|
|
22
|
+
it('uses -ilc (not -lc) for every spawn(shell, ...) call', () => {
|
|
23
|
+
const spawnCalls = SOURCE.match(/spawn\(\s*shell\s*,\s*\[[^\]]*\]/g) || []
|
|
24
|
+
expect(spawnCalls.length).toBeGreaterThan(0)
|
|
25
|
+
|
|
26
|
+
for (const call of spawnCalls) {
|
|
27
|
+
// Bare interactive shell (no -c flag, no command) is allowed —
|
|
28
|
+
// that's the fallback shell session. Anything passing a command
|
|
29
|
+
// string MUST source .zshrc.
|
|
30
|
+
const passesCommand = /-[il]+c['"]/.test(call)
|
|
31
|
+
if (!passesCommand) continue
|
|
32
|
+
|
|
33
|
+
expect(call, `spawn call missing -i flag: ${call}`).toMatch(/['"]-i?l?i?l?c['"]/)
|
|
34
|
+
expect(call, `spawn call must include -i: ${call}`).toMatch(/['"]-(i[l]?c|li?c|il?c)['"]/)
|
|
35
|
+
expect(call, `spawn call must not use bare -lc: ${call}`).not.toMatch(/['"]-lc['"]/)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -379,6 +379,27 @@ const ChromeWrappedWidget = memo(function ChromeWrappedWidget({
|
|
|
379
379
|
// Dynamically adjust features based on widget state
|
|
380
380
|
const features = useMemo(() => {
|
|
381
381
|
const isGitHub = !!widget.props?.github
|
|
382
|
+
const isTerminalOrAgent = widget.type === 'terminal' || widget.type === 'agent'
|
|
383
|
+
|
|
384
|
+
// Detect connected terminal/agent peers — used to gate hub-related features.
|
|
385
|
+
// Hub role and broadcast are meaningless without a peer to coordinate with.
|
|
386
|
+
let hasHubPeers = false
|
|
387
|
+
let allBroadcastActive = true
|
|
388
|
+
const broadcastConnectorIds = []
|
|
389
|
+
if (isTerminalOrAgent) {
|
|
390
|
+
const widgetConnectors = connectorCount || []
|
|
391
|
+
const widgetList = allWidgets || []
|
|
392
|
+
for (const conn of widgetConnectors) {
|
|
393
|
+
const peerId = conn.start?.widgetId === widget.id ? conn.end?.widgetId : conn.start?.widgetId
|
|
394
|
+
const peer = widgetList.find((w) => w.id === peerId)
|
|
395
|
+
if (peer && (peer.type === 'terminal' || peer.type === 'agent')) {
|
|
396
|
+
hasHubPeers = true
|
|
397
|
+
broadcastConnectorIds.push(conn.id)
|
|
398
|
+
if (conn.meta?.messagingMode !== 'two-way') allBroadcastActive = false
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
382
403
|
const adjusted = rawFeatures.map((f) => {
|
|
383
404
|
// Toggle collapse label and hide when content is short (no github = no collapse)
|
|
384
405
|
if (f.action === 'toggle-collapse') {
|
|
@@ -391,6 +412,9 @@ const ChromeWrappedWidget = memo(function ChromeWrappedWidget({
|
|
|
391
412
|
}
|
|
392
413
|
// Hide refresh-github for non-GitHub link previews
|
|
393
414
|
if (f.action === 'refresh-github' && !isGitHub) return null
|
|
415
|
+
// Hide hub-role selector when terminal/agent has no connected peers —
|
|
416
|
+
// a hub of one is not a hub.
|
|
417
|
+
if (f.type === 'role-selector' && isTerminalOrAgent && !hasHubPeers) return null
|
|
394
418
|
return f
|
|
395
419
|
}).filter(Boolean)
|
|
396
420
|
|
|
@@ -420,37 +444,19 @@ const ChromeWrappedWidget = memo(function ChromeWrappedWidget({
|
|
|
420
444
|
}
|
|
421
445
|
|
|
422
446
|
// Add dynamic "Broadcast" toggle for terminal/agent widgets with connected peers
|
|
423
|
-
if (
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (conn.meta?.messagingMode !== 'two-way') allBroadcastActive = false
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (hasBroadcastPeers) {
|
|
441
|
-
const isActive = allBroadcastActive
|
|
442
|
-
const insertIdx = adjusted.findIndex((f) => f.menu)
|
|
443
|
-
const broadcastFeature = {
|
|
444
|
-
id: 'broadcast',
|
|
445
|
-
type: 'action',
|
|
446
|
-
action: `broadcast-toggle:${broadcastConnectorIds.join(',')}:${isActive ? 'off' : 'on'}`,
|
|
447
|
-
label: isActive ? 'Broadcast On' : 'Broadcast',
|
|
448
|
-
icon: 'broadcast',
|
|
449
|
-
active: isActive,
|
|
450
|
-
}
|
|
451
|
-
if (insertIdx >= 0) adjusted.splice(insertIdx, 0, broadcastFeature)
|
|
452
|
-
else adjusted.push(broadcastFeature)
|
|
453
|
-
}
|
|
447
|
+
if (isTerminalOrAgent && hasHubPeers) {
|
|
448
|
+
const isActive = allBroadcastActive
|
|
449
|
+
const insertIdx = adjusted.findIndex((f) => f.menu)
|
|
450
|
+
const broadcastFeature = {
|
|
451
|
+
id: 'broadcast',
|
|
452
|
+
type: 'action',
|
|
453
|
+
action: `broadcast-toggle:${broadcastConnectorIds.join(',')}:${isActive ? 'off' : 'on'}`,
|
|
454
|
+
label: isActive ? 'Broadcast On' : 'Broadcast',
|
|
455
|
+
icon: 'broadcast',
|
|
456
|
+
active: isActive,
|
|
457
|
+
}
|
|
458
|
+
if (insertIdx >= 0) adjusted.splice(insertIdx, 0, broadcastFeature)
|
|
459
|
+
else adjusted.push(broadcastFeature)
|
|
454
460
|
}
|
|
455
461
|
|
|
456
462
|
return adjusted
|