@dfosco/storyboard 0.6.1-beta.0 → 0.6.2
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 +1 -1
- package/src/core/canvas/terminal-server.js +0 -28
- package/src/core/cli/hubCommands.js +6 -7
- package/src/core/cli/messagesCommands.js +2 -3
- package/src/core/cli/terminal-messaging.js +3 -4
- package/src/internals/vite/data-plugin.js +7 -2
- package/src/internals/vite/data-plugin.test.js +8 -1
- package/src/core/cli/resolveWidgetId.js +0 -79
package/package.json
CHANGED
|
@@ -1194,34 +1194,6 @@ 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 */ }
|
|
1225
1197
|
const postStartup = resolvedAgentCfg?.postStartup || null
|
|
1226
1198
|
// ── H2 fix: skip readiness re-poll on warm handoff.
|
|
1227
1199
|
// The hot pool already verified readiness when it warmed this session.
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { parseSimpleArgs, jsonOut, die, post, get } from './cliHelpers.js'
|
|
19
|
-
import { resolveWidgetId } from './resolveWidgetId.js'
|
|
20
19
|
|
|
21
20
|
const sub = process.argv[3]
|
|
22
21
|
const sub2 = process.argv[4]
|
|
@@ -85,7 +84,7 @@ async function run() {
|
|
|
85
84
|
|
|
86
85
|
case 'goal': {
|
|
87
86
|
const hubId = flags.hub
|
|
88
|
-
const senderId =
|
|
87
|
+
const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
|
|
89
88
|
const goal = flags.goal || positional[0]
|
|
90
89
|
if (!hubId || !senderId || !goal) die('--hub, --sender, and --goal are required')
|
|
91
90
|
const data = await post(`${MESSAGING_BASE}/hub/goal`, { hubId, senderId, goal })
|
|
@@ -95,7 +94,7 @@ async function run() {
|
|
|
95
94
|
|
|
96
95
|
case 'send': {
|
|
97
96
|
const hubId = flags.hub
|
|
98
|
-
const senderId =
|
|
97
|
+
const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
|
|
99
98
|
const body = flags.body || positional[0]
|
|
100
99
|
if (!hubId || !senderId || !body) die('--hub, --sender, and --body are required')
|
|
101
100
|
const payload = { hubId, senderId, body }
|
|
@@ -112,7 +111,7 @@ async function run() {
|
|
|
112
111
|
case 'respond': {
|
|
113
112
|
const hubId = flags.hub
|
|
114
113
|
const messageId = flags.message
|
|
115
|
-
const widgetId =
|
|
114
|
+
const widgetId = flags.widget || process.env.STORYBOARD_WIDGET_ID
|
|
116
115
|
const body = flags.body || positional[0]
|
|
117
116
|
if (!hubId || !messageId || !widgetId || !body) die('--hub, --message, --widget, and --body are required')
|
|
118
117
|
const data = await post(`${MESSAGING_BASE}/hub/respond`, { hubId, messageId, widgetId, body })
|
|
@@ -161,13 +160,13 @@ async function run() {
|
|
|
161
160
|
|
|
162
161
|
if (sub2 === 'start') {
|
|
163
162
|
const hubId = flags.hub
|
|
164
|
-
const senderId =
|
|
163
|
+
const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
|
|
165
164
|
if (!hubId || !senderId) die('--hub and --sender are required')
|
|
166
165
|
const data = await post(`${MESSAGING_BASE}/conversation/start`, { hubId, senderId })
|
|
167
166
|
jsonOut(data)
|
|
168
167
|
} else if (sub2 === 'finality') {
|
|
169
168
|
const hubId = flags.hub
|
|
170
|
-
const senderId =
|
|
169
|
+
const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
|
|
171
170
|
const summary = flags.summary || ''
|
|
172
171
|
const successor = flags.successor || null
|
|
173
172
|
if (!hubId || !senderId) die('--hub and --sender are required')
|
|
@@ -175,7 +174,7 @@ async function run() {
|
|
|
175
174
|
jsonOut(data)
|
|
176
175
|
} else if (sub2 === 'reopen') {
|
|
177
176
|
const hubId = flags.hub
|
|
178
|
-
const senderId =
|
|
177
|
+
const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
|
|
179
178
|
const conversationId = flags.conversation
|
|
180
179
|
const body = flags.body || ''
|
|
181
180
|
if (!hubId || !senderId || !conversationId) die('--hub, --sender, and --conversation are required')
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { parseSimpleArgs, jsonOut, die, post, get } from './cliHelpers.js'
|
|
12
|
-
import { resolveWidgetId } from './resolveWidgetId.js'
|
|
13
12
|
|
|
14
13
|
const sub = process.argv[3]
|
|
15
14
|
const MESSAGING_BASE = '/_storyboard/messages'
|
|
@@ -48,7 +47,7 @@ async function run() {
|
|
|
48
47
|
case 'publish': {
|
|
49
48
|
const channel = flags.channel
|
|
50
49
|
const type = flags.type
|
|
51
|
-
const senderId =
|
|
50
|
+
const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
|
|
52
51
|
if (!channel) die('--channel is required')
|
|
53
52
|
|
|
54
53
|
const payload = { channel, senderId }
|
|
@@ -68,7 +67,7 @@ async function run() {
|
|
|
68
67
|
case 'send': {
|
|
69
68
|
const channel = flags.channel
|
|
70
69
|
const type = flags.type
|
|
71
|
-
const senderId =
|
|
70
|
+
const senderId = flags.sender || process.env.STORYBOARD_WIDGET_ID
|
|
72
71
|
if (!channel) die('--channel is required')
|
|
73
72
|
|
|
74
73
|
const payload = { channel, senderId }
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { getServerUrl } from './serverUrl.js'
|
|
13
|
-
import { resolveWidgetId } from './resolveWidgetId.js'
|
|
14
13
|
|
|
15
14
|
function parseArgs(args) {
|
|
16
15
|
const result = { positional: [], flags: {} }
|
|
@@ -36,7 +35,7 @@ export async function handleSend() {
|
|
|
36
35
|
const { positional, flags } = parseArgs(args)
|
|
37
36
|
|
|
38
37
|
// Resolve sender identity from env
|
|
39
|
-
const senderWidgetId =
|
|
38
|
+
const senderWidgetId = process.env.STORYBOARD_WIDGET_ID || null
|
|
40
39
|
|
|
41
40
|
let targetWidgetId = null
|
|
42
41
|
let message = null
|
|
@@ -116,7 +115,7 @@ export async function handleOutput() {
|
|
|
116
115
|
const args = process.argv.slice(4) // skip: node, sb, terminal, output
|
|
117
116
|
const { flags } = parseArgs(args)
|
|
118
117
|
|
|
119
|
-
const widgetId =
|
|
118
|
+
const widgetId = flags.widget || process.env.STORYBOARD_WIDGET_ID
|
|
120
119
|
const summary = flags.summary || ''
|
|
121
120
|
const content = flags.content || ''
|
|
122
121
|
|
|
@@ -185,7 +184,7 @@ export async function handleRead() {
|
|
|
185
184
|
const args = process.argv.slice(4) // skip: node, sb, terminal, read
|
|
186
185
|
const { positional, flags } = parseArgs(args)
|
|
187
186
|
|
|
188
|
-
const widgetId = positional[0] ||
|
|
187
|
+
const widgetId = positional[0] || process.env.STORYBOARD_WIDGET_ID
|
|
189
188
|
if (!widgetId) {
|
|
190
189
|
console.error('Usage: storyboard terminal read <widgetId> [--length N]')
|
|
191
190
|
process.exit(1)
|
|
@@ -68,6 +68,11 @@ function parseDataFile(filePath, opts = {}) {
|
|
|
68
68
|
const folderDirMatch = normalized.match(/(?:^|\/)src\/prototypes\/([^/]+)\.folder\//)
|
|
69
69
|
const folderName = folderDirMatch ? folderDirMatch[1] : null
|
|
70
70
|
|
|
71
|
+
// Strip leading `~` from each path segment when building the public URL,
|
|
72
|
+
// so locally-gitignored canvases (e.g. ~notes.canvas.jsonl) are reachable
|
|
73
|
+
// at the same route as their non-prefixed counterpart. The on-disk `name`
|
|
74
|
+
// and `id` keep the `~` so they remain unique vs a sibling without it.
|
|
75
|
+
const stripTilde = (p) => p.split('/').map(seg => seg.replace(/^~/, '')).join('/')
|
|
71
76
|
const canvasCheck = normalized.match(/(?:^|\/)src\/canvas\//)
|
|
72
77
|
if (canvasCheck) {
|
|
73
78
|
const dirPath = normalized.substring(0, normalized.lastIndexOf('/'))
|
|
@@ -79,7 +84,7 @@ function parseDataFile(filePath, opts = {}) {
|
|
|
79
84
|
.replace(/\/+/g, '/')
|
|
80
85
|
.replace(/\/$/, '')
|
|
81
86
|
name = idBase ? `${idBase}/${baseName}` : baseName
|
|
82
|
-
inferredRoute = '/canvas/' + name
|
|
87
|
+
inferredRoute = '/canvas/' + stripTilde(name)
|
|
83
88
|
inferredRoute = inferredRoute.replace(/\/+/g, '/').replace(/\/$/, '') || '/canvas'
|
|
84
89
|
}
|
|
85
90
|
const protoCheck = normalized.match(/(?:^|\/)src\/prototypes\//)
|
|
@@ -92,7 +97,7 @@ function parseDataFile(filePath, opts = {}) {
|
|
|
92
97
|
.replace(/\/+/g, '/')
|
|
93
98
|
.replace(/\/$/, '')
|
|
94
99
|
name = idBase ? `${idBase}/${baseName}` : baseName
|
|
95
|
-
inferredRoute = '/canvas/' + name
|
|
100
|
+
inferredRoute = '/canvas/' + stripTilde(name)
|
|
96
101
|
inferredRoute = inferredRoute.replace(/\/+/g, '/').replace(/\/$/, '') || '/canvas'
|
|
97
102
|
}
|
|
98
103
|
// Derive group: canvases sharing a directory form a group
|
|
@@ -1214,10 +1214,17 @@ describe('parseDataFile — canvas path-based IDs', () => {
|
|
|
1214
1214
|
const file = parseDataFile('src/canvas/~scratch.canvas.jsonl', { includeTilde: true })
|
|
1215
1215
|
expect(file).not.toBeNull()
|
|
1216
1216
|
expect(file.name).toBe('~scratch')
|
|
1217
|
-
|
|
1217
|
+
// Public route strips the `~` so locally-gitignored canvases reuse the
|
|
1218
|
+
// same URL as their non-prefixed counterpart.
|
|
1219
|
+
expect(file.inferredRoute).toBe('/canvas/scratch')
|
|
1218
1220
|
const inDir = parseDataFile('src/canvas/~private/notes.canvas.jsonl', { includeTilde: true })
|
|
1219
1221
|
expect(inDir).not.toBeNull()
|
|
1220
1222
|
expect(inDir.name).toBe('~private/notes')
|
|
1223
|
+
expect(inDir.inferredRoute).toBe('/canvas/private/notes')
|
|
1224
|
+
const inSubdir = parseDataFile('src/canvas/dfosco-explorations/~notes.canvas.jsonl', { includeTilde: true })
|
|
1225
|
+
expect(inSubdir).not.toBeNull()
|
|
1226
|
+
expect(inSubdir.name).toBe('dfosco-explorations/~notes')
|
|
1227
|
+
expect(inSubdir.inferredRoute).toBe('/canvas/dfosco-explorations/notes')
|
|
1221
1228
|
})
|
|
1222
1229
|
|
|
1223
1230
|
it('canvas outside known directories gets basename-only ID', () => {
|
|
@@ -1,79 +0,0 @@
|
|
|
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
|
-
}
|