@dfosco/storyboard 0.6.0 → 0.6.1-beta.0

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard",
3
- "version": "0.6.0",
3
+ "version": "0.6.1-beta.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Storyboard prototyping framework — core engine, React integration, and canvas",
@@ -1194,6 +1194,34 @@ function handleConnection(ws, widgetId, canvasId, prettyName, widgetStartupComma
1194
1194
  // via a watcher on the pool-keyed capture file — copilot's env is
1195
1195
  // pool-keyed for the life of the warm process, so the user-level
1196
1196
  // hook always writes there, not to the widget-keyed file.)
1197
+ //
1198
+ // Also write the per-widget env.sh here so any future shells in
1199
+ // the session inherit the correct STORYBOARD_WIDGET_ID. Note: the
1200
+ // already-running TUI's process env is fixed at fork time and
1201
+ // still carries the pool id; CLI commands that need the real
1202
+ // widget id should use resolveWidgetId() (which falls back to
1203
+ // .storyboard/terminal-sessions.json keyed by tmux session name).
1204
+ try {
1205
+ const envParts = [
1206
+ `export STORYBOARD_WIDGET_ID="${widgetId}"`,
1207
+ `export STORYBOARD_CANVAS_ID="${canvasId}"`,
1208
+ `export STORYBOARD_BRANCH="${branch}"`,
1209
+ `export STORYBOARD_SERVER_URL="${serverUrl}"`,
1210
+ `export STORYBOARD_PROJECT_ROOT="${cwd}"`,
1211
+ ]
1212
+ const envScriptDir = join(cwd, '.storyboard', 'terminals')
1213
+ mkdirSync(envScriptDir, { recursive: true })
1214
+ writeFileSync(join(envScriptDir, `${widgetId}.env.sh`), envParts.join('\n') + '\n')
1215
+ // Update tmux session env so newly-forked shells see the right id.
1216
+ execSync(
1217
+ `tmux set-environment -t "${tmuxName}" STORYBOARD_WIDGET_ID "${widgetId}" 2>/dev/null`,
1218
+ { stdio: 'ignore' }
1219
+ )
1220
+ execSync(
1221
+ `tmux set-environment -t "${tmuxName}" STORYBOARD_CANVAS_ID "${canvasId}" 2>/dev/null`,
1222
+ { stdio: 'ignore' }
1223
+ )
1224
+ } catch { /* empty */ }
1197
1225
  const postStartup = resolvedAgentCfg?.postStartup || null
1198
1226
  // ── H2 fix: skip readiness re-poll on warm handoff.
1199
1227
  // The hot pool already verified readiness when it warmed this session.
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import { parseSimpleArgs, jsonOut, die, post, get } from './cliHelpers.js'
19
+ import { resolveWidgetId } from './resolveWidgetId.js'
19
20
 
20
21
  const sub = process.argv[3]
21
22
  const sub2 = process.argv[4]
@@ -84,7 +85,7 @@ async function run() {
84
85
 
85
86
  case 'goal': {
86
87
  const hubId = flags.hub
87
- const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
88
+ const senderId = resolveWidgetId(flags.sender)
88
89
  const goal = flags.goal || positional[0]
89
90
  if (!hubId || !senderId || !goal) die('--hub, --sender, and --goal are required')
90
91
  const data = await post(`${MESSAGING_BASE}/hub/goal`, { hubId, senderId, goal })
@@ -94,7 +95,7 @@ async function run() {
94
95
 
95
96
  case 'send': {
96
97
  const hubId = flags.hub
97
- const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
98
+ const senderId = resolveWidgetId(flags.sender)
98
99
  const body = flags.body || positional[0]
99
100
  if (!hubId || !senderId || !body) die('--hub, --sender, and --body are required')
100
101
  const payload = { hubId, senderId, body }
@@ -111,7 +112,7 @@ async function run() {
111
112
  case 'respond': {
112
113
  const hubId = flags.hub
113
114
  const messageId = flags.message
114
- const widgetId = flags.widget || process.env.STORYBOARD_WIDGET_ID
115
+ const widgetId = resolveWidgetId(flags.widget)
115
116
  const body = flags.body || positional[0]
116
117
  if (!hubId || !messageId || !widgetId || !body) die('--hub, --message, --widget, and --body are required')
117
118
  const data = await post(`${MESSAGING_BASE}/hub/respond`, { hubId, messageId, widgetId, body })
@@ -160,13 +161,13 @@ async function run() {
160
161
 
161
162
  if (sub2 === 'start') {
162
163
  const hubId = flags.hub
163
- const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
164
+ const senderId = resolveWidgetId(flags.sender)
164
165
  if (!hubId || !senderId) die('--hub and --sender are required')
165
166
  const data = await post(`${MESSAGING_BASE}/conversation/start`, { hubId, senderId })
166
167
  jsonOut(data)
167
168
  } else if (sub2 === 'finality') {
168
169
  const hubId = flags.hub
169
- const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
170
+ const senderId = resolveWidgetId(flags.sender)
170
171
  const summary = flags.summary || ''
171
172
  const successor = flags.successor || null
172
173
  if (!hubId || !senderId) die('--hub and --sender are required')
@@ -174,7 +175,7 @@ async function run() {
174
175
  jsonOut(data)
175
176
  } else if (sub2 === 'reopen') {
176
177
  const hubId = flags.hub
177
- const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
178
+ const senderId = resolveWidgetId(flags.sender)
178
179
  const conversationId = flags.conversation
179
180
  const body = flags.body || ''
180
181
  if (!hubId || !senderId || !conversationId) die('--hub, --sender, and --conversation are required')
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { parseSimpleArgs, jsonOut, die, post, get } from './cliHelpers.js'
12
+ import { resolveWidgetId } from './resolveWidgetId.js'
12
13
 
13
14
  const sub = process.argv[3]
14
15
  const MESSAGING_BASE = '/_storyboard/messages'
@@ -47,7 +48,7 @@ async function run() {
47
48
  case 'publish': {
48
49
  const channel = flags.channel
49
50
  const type = flags.type
50
- const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
51
+ const senderId = resolveWidgetId(flags.sender)
51
52
  if (!channel) die('--channel is required')
52
53
 
53
54
  const payload = { channel, senderId }
@@ -67,7 +68,7 @@ async function run() {
67
68
  case 'send': {
68
69
  const channel = flags.channel
69
70
  const type = flags.type
70
- const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
71
+ const senderId = resolveWidgetId(flags.sender)
71
72
  if (!channel) die('--channel is required')
72
73
 
73
74
  const payload = { channel, senderId }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * resolveWidgetId — robust widget-id resolution for CLI commands invoked
3
+ * from inside a tmux pane (typically a terminal-agent shell tool).
4
+ *
5
+ * Resolution order:
6
+ * 1. Explicit argument (e.g. --widget / --sender flag)
7
+ * 2. $STORYBOARD_WIDGET_ID_OVERRIDE (manual escape hatch)
8
+ * 3. $STORYBOARD_WIDGET_ID — but only if it doesn't look like a hot-pool
9
+ * session id (e.g. "pool-copilot-1779236104099-t0pg"). Pool ids leak
10
+ * into the agent's env when a TUI is handed off from the hot pool;
11
+ * the running process's env stays pinned to the pool id even after
12
+ * the warm-handoff env.sh is written, so child shells (like the
13
+ * ones spawned by Copilot/Claude/Codex bash tools) inherit the
14
+ * stale id.
15
+ * 4. Look up the current tmux session in
16
+ * .storyboard/terminal-sessions.json and return its bound widgetId.
17
+ * 5. Fall back to whatever $STORYBOARD_WIDGET_ID held (even if pool-*),
18
+ * so behavior never gets worse than before.
19
+ */
20
+
21
+ import { readFileSync, existsSync } from 'node:fs'
22
+ import { join } from 'node:path'
23
+ import { execSync } from 'node:child_process'
24
+
25
+ const POOL_ID_RE = /^pool-/
26
+
27
+ function looksLikePoolId(value) {
28
+ return typeof value === 'string' && POOL_ID_RE.test(value)
29
+ }
30
+
31
+ function getCurrentTmuxSessionName() {
32
+ if (!process.env.TMUX) return null
33
+ try {
34
+ const out = execSync(`tmux display-message -p '#{session_name}'`, {
35
+ encoding: 'utf8',
36
+ stdio: ['ignore', 'pipe', 'ignore'],
37
+ timeout: 500,
38
+ }).trim()
39
+ return out || null
40
+ } catch {
41
+ return null
42
+ }
43
+ }
44
+
45
+ function findWidgetIdByTmuxName(projectRoot, tmuxName) {
46
+ if (!projectRoot || !tmuxName) return null
47
+ const registryPath = join(projectRoot, '.storyboard', 'terminal-sessions.json')
48
+ if (!existsSync(registryPath)) return null
49
+ try {
50
+ const raw = readFileSync(registryPath, 'utf8')
51
+ const parsed = JSON.parse(raw)
52
+ const arr = Array.isArray(parsed) ? parsed : []
53
+ const match = arr.find((s) => s && s.tmuxName === tmuxName)
54
+ return match?.widgetId || null
55
+ } catch {
56
+ return null
57
+ }
58
+ }
59
+
60
+ /**
61
+ * @param {string|null|undefined} explicit — explicit override (e.g. CLI flag)
62
+ * @returns {string|null}
63
+ */
64
+ export function resolveWidgetId(explicit) {
65
+ if (explicit) return explicit
66
+
67
+ const override = process.env.STORYBOARD_WIDGET_ID_OVERRIDE
68
+ if (override) return override
69
+
70
+ const envId = process.env.STORYBOARD_WIDGET_ID || null
71
+ if (envId && !looksLikePoolId(envId)) return envId
72
+
73
+ const tmuxName = getCurrentTmuxSessionName()
74
+ const projectRoot = process.env.STORYBOARD_PROJECT_ROOT || process.cwd()
75
+ const mapped = findWidgetIdByTmuxName(projectRoot, tmuxName)
76
+ if (mapped) return mapped
77
+
78
+ return envId
79
+ }
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { getServerUrl } from './serverUrl.js'
13
+ import { resolveWidgetId } from './resolveWidgetId.js'
13
14
 
14
15
  function parseArgs(args) {
15
16
  const result = { positional: [], flags: {} }
@@ -35,7 +36,7 @@ export async function handleSend() {
35
36
  const { positional, flags } = parseArgs(args)
36
37
 
37
38
  // Resolve sender identity from env
38
- const senderWidgetId = process.env.STORYBOARD_WIDGET_ID || null
39
+ const senderWidgetId = resolveWidgetId(null)
39
40
 
40
41
  let targetWidgetId = null
41
42
  let message = null
@@ -115,7 +116,7 @@ export async function handleOutput() {
115
116
  const args = process.argv.slice(4) // skip: node, sb, terminal, output
116
117
  const { flags } = parseArgs(args)
117
118
 
118
- const widgetId = flags.widget || process.env.STORYBOARD_WIDGET_ID
119
+ const widgetId = resolveWidgetId(flags.widget)
119
120
  const summary = flags.summary || ''
120
121
  const content = flags.content || ''
121
122
 
@@ -184,7 +185,7 @@ export async function handleRead() {
184
185
  const args = process.argv.slice(4) // skip: node, sb, terminal, read
185
186
  const { positional, flags } = parseArgs(args)
186
187
 
187
- const widgetId = positional[0] || process.env.STORYBOARD_WIDGET_ID
188
+ const widgetId = positional[0] || resolveWidgetId(null)
188
189
  if (!widgetId) {
189
190
  console.error('Usage: storyboard terminal read <widgetId> [--length N]')
190
191
  process.exit(1)