@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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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] ||
|
|
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)
|