@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard",
3
- "version": "0.6.1-beta.0",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Storyboard prototyping framework — core engine, React integration, and canvas",
@@ -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 = resolveWidgetId(flags.sender)
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 = resolveWidgetId(flags.sender)
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 = resolveWidgetId(flags.widget)
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 = resolveWidgetId(flags.sender)
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 = resolveWidgetId(flags.sender)
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 = resolveWidgetId(flags.sender)
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 = resolveWidgetId(flags.sender)
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 = resolveWidgetId(flags.sender)
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 = resolveWidgetId(null)
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 = resolveWidgetId(flags.widget)
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] || resolveWidgetId(null)
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
- expect(file.inferredRoute).toBe('/canvas/~scratch')
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
- }