@dfosco/storyboard-core 4.2.3 → 4.2.5
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/dist/storyboard-ui.js +90 -82
- package/dist/storyboard-ui.js.map +1 -1
- package/package.json +1 -1
- package/scaffold/gitignore +64 -0
- package/scaffold/manifest.json +1 -1
- package/src/ActionMenuButton.jsx +11 -0
- package/src/canvas/hot-pool.js +15 -5
- package/src/canvas/server.js +2 -2
- package/src/configSchema.js +1 -1
- package/src/index.js +1 -1
- package/src/viewfinder.js +52 -23
- package/src/viewfinder.test.js +112 -1
- package/src/vite/server-plugin.js +9 -0
package/package.json
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
/packages/**/node_modules
|
|
6
|
+
/.pnp
|
|
7
|
+
.pnp.js
|
|
8
|
+
|
|
9
|
+
# testing
|
|
10
|
+
/coverage
|
|
11
|
+
|
|
12
|
+
# production
|
|
13
|
+
/build
|
|
14
|
+
/dist
|
|
15
|
+
|
|
16
|
+
# misc
|
|
17
|
+
.DS_Store
|
|
18
|
+
.env.local
|
|
19
|
+
.env.development.local
|
|
20
|
+
.env.test.local
|
|
21
|
+
.env.production.local
|
|
22
|
+
|
|
23
|
+
npm-debug.log*
|
|
24
|
+
yarn-debug.log*
|
|
25
|
+
yarn-error.log*
|
|
26
|
+
.playwright-profile
|
|
27
|
+
|
|
28
|
+
.github/skills/_archive
|
|
29
|
+
.github/skills/primer-primitives
|
|
30
|
+
.github/skills/primer-components-catalog
|
|
31
|
+
.github/skills/primer-screenshot-builder
|
|
32
|
+
.github/skills/primer-screenshot-patterns
|
|
33
|
+
.github/skills/primer-url-builder
|
|
34
|
+
.github/skills/playwright-cli
|
|
35
|
+
.worktrees
|
|
36
|
+
# Agent symlinks are build targets — source of truth is .agents/agents/
|
|
37
|
+
# storyboard setup creates symlinks for Copilot CLI and Claude Code
|
|
38
|
+
.github/agents/_buddy-rails.md
|
|
39
|
+
.github/agents/_buddy.md
|
|
40
|
+
.github/agents/terminal-agent.md
|
|
41
|
+
.github/agents/prompt-agent.md
|
|
42
|
+
.claude/agents/
|
|
43
|
+
_*.md
|
|
44
|
+
|
|
45
|
+
# Compiled UI bundle (built before publish, not tracked in git)
|
|
46
|
+
packages/core/dist/storyboard-ui.*
|
|
47
|
+
|
|
48
|
+
.clips
|
|
49
|
+
|
|
50
|
+
# Agent Browser
|
|
51
|
+
agent-browser.json
|
|
52
|
+
|
|
53
|
+
# Selected widgets bridge (real-time canvas selection for Copilot)
|
|
54
|
+
.storyboard
|
|
55
|
+
|
|
56
|
+
# Private canvas images (tilde prefix = not committed)
|
|
57
|
+
src/canvas/images/~*
|
|
58
|
+
assets/canvas/images/~*
|
|
59
|
+
assets/canvas/snapshots/~*
|
|
60
|
+
assets/.storyboard-public/terminal-snapshots/~*
|
|
61
|
+
.sync-target
|
|
62
|
+
|
|
63
|
+
# Integration test results (ephemeral local artifacts)
|
|
64
|
+
test-results/
|
package/scaffold/manifest.json
CHANGED
package/src/ActionMenuButton.jsx
CHANGED
|
@@ -16,6 +16,17 @@ export default function ActionMenuButton({ config = {}, data: _data, localOnly:
|
|
|
16
16
|
return unsub
|
|
17
17
|
}, [])
|
|
18
18
|
|
|
19
|
+
// Allow external callers (e.g. command palette) to open this menu
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const actionId = config.action
|
|
22
|
+
if (!actionId) return
|
|
23
|
+
function onTrigger(e) {
|
|
24
|
+
if (e.detail?.action === actionId) setMenuOpen(true)
|
|
25
|
+
}
|
|
26
|
+
window.addEventListener('storyboard:open-tool-menu', onTrigger)
|
|
27
|
+
return () => window.removeEventListener('storyboard:open-tool-menu', onTrigger)
|
|
28
|
+
}, [config.action])
|
|
29
|
+
|
|
19
30
|
const children = config.action ? getActionChildren(config.action) : []
|
|
20
31
|
const hasRadio = children.some((c) => c.type === 'radio')
|
|
21
32
|
const activeValue = children.find((c) => c.type === 'radio' && c.active)?.id || ''
|
package/src/canvas/hot-pool.js
CHANGED
|
@@ -81,6 +81,7 @@ export class HotPool {
|
|
|
81
81
|
#healthTimer = null
|
|
82
82
|
#prereqsAvailable = null
|
|
83
83
|
#wsSend = null
|
|
84
|
+
#webglReadySlots = 0
|
|
84
85
|
|
|
85
86
|
// Agent config (null for bare shell pools)
|
|
86
87
|
#agentConfig = null
|
|
@@ -100,7 +101,7 @@ export class HotPool {
|
|
|
100
101
|
constructor({ root, poolId = 'terminal', config = {}, agentConfig = null, wsSend = null }) {
|
|
101
102
|
this.#root = root
|
|
102
103
|
this.#poolId = poolId
|
|
103
|
-
this.#poolSize = Math.max(
|
|
104
|
+
this.#poolSize = Math.max(0, config.pool_size ?? DEFAULT_POOL_SIZE)
|
|
104
105
|
this.#maxPoolSize = Math.max(this.#poolSize, config.max_pool_size ?? DEFAULT_MAX_POOL_SIZE)
|
|
105
106
|
this.#cooldownMs = (config.load_balancer_cooldown_mins ?? DEFAULT_COOLDOWN_MINS) * 60_000
|
|
106
107
|
this.#enabled = config.enabled !== false
|
|
@@ -108,6 +109,7 @@ export class HotPool {
|
|
|
108
109
|
this.#loadBalancer = config.load_balancer !== false
|
|
109
110
|
this.#wsSend = wsSend
|
|
110
111
|
this.#agentConfig = agentConfig
|
|
112
|
+
this.#webglReadySlots = Math.max(0, config.webgl_ready_slots ?? 0)
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
get poolId() { return this.#poolId }
|
|
@@ -177,8 +179,12 @@ export class HotPool {
|
|
|
177
179
|
return null
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
// Session is WebGL-ready if its queue position was within webgl_ready_slots
|
|
183
|
+
const webglReady = this.#webglReadySlots > 0 && idx < this.#webglReadySlots
|
|
184
|
+
|
|
180
185
|
const session = this.#queue.splice(idx, 1)[0]
|
|
181
186
|
session.state = 'acquired'
|
|
187
|
+
session.webglReady = webglReady
|
|
182
188
|
this.#acquired.set(session.id, session)
|
|
183
189
|
const age = ((Date.now() - session.createdAt) / 1000).toFixed(1)
|
|
184
190
|
const readyCount = this.#queue.filter(s => s.state === 'ready').length
|
|
@@ -186,10 +192,10 @@ export class HotPool {
|
|
|
186
192
|
// Scale-up: queue drained to 0 ready → enter pressure mode
|
|
187
193
|
if (readyCount === 0 && !this.#pressured) {
|
|
188
194
|
this.#pressured = true
|
|
189
|
-
this.#log(`→ ACQUIRED ${session.id} tmux=${session.tmuxName} (age: ${age}s) — ⚡ PRESSURE ON (scaling to max_pool_size=${this.#maxPoolSize})`)
|
|
195
|
+
this.#log(`→ ACQUIRED ${session.id} tmux=${session.tmuxName} (age: ${age}s, webglReady: ${webglReady}) — ⚡ PRESSURE ON (scaling to max_pool_size=${this.#maxPoolSize})`)
|
|
190
196
|
this.#resetCooldown()
|
|
191
197
|
} else {
|
|
192
|
-
this.#log(`→ ACQUIRED ${session.id} tmux=${session.tmuxName} (age: ${age}s, queue: ${readyCount}/${this.#fillTarget})`)
|
|
198
|
+
this.#log(`→ ACQUIRED ${session.id} tmux=${session.tmuxName} (age: ${age}s, webglReady: ${webglReady}, queue: ${readyCount}/${this.#fillTarget})`)
|
|
193
199
|
this.#resetCooldown()
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -237,6 +243,7 @@ export class HotPool {
|
|
|
237
243
|
load_balancer: this.#loadBalancer,
|
|
238
244
|
load_balancer_cooldown_mins: this.#cooldownMs / 60_000,
|
|
239
245
|
verbose: this.#verbose,
|
|
246
|
+
webgl_ready_slots: this.#webglReadySlots,
|
|
240
247
|
},
|
|
241
248
|
agentConfig: this.#agentConfig ? {
|
|
242
249
|
startupCommand: this.#agentConfig.startupCommand,
|
|
@@ -254,10 +261,11 @@ export class HotPool {
|
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
reconfigure(config) {
|
|
257
|
-
if (config.max_pool_size !== undefined) this.#maxPoolSize = Math.max(
|
|
264
|
+
if (config.max_pool_size !== undefined) this.#maxPoolSize = Math.max(0, config.max_pool_size)
|
|
258
265
|
if (config.load_balancer_cooldown_mins !== undefined) this.#cooldownMs = config.load_balancer_cooldown_mins * 60_000
|
|
259
266
|
if (config.load_balancer !== undefined) this.#loadBalancer = !!config.load_balancer
|
|
260
|
-
|
|
267
|
+
if (config.webgl_ready_slots !== undefined) this.#webglReadySlots = Math.max(0, config.webgl_ready_slots)
|
|
268
|
+
const newSize = Math.min(Math.max(0, config.pool_size ?? this.#poolSize), this.#maxPoolSize)
|
|
261
269
|
const newEnabled = config.enabled !== false
|
|
262
270
|
if (config.verbose !== undefined) this.#verbose = !!config.verbose
|
|
263
271
|
|
|
@@ -644,6 +652,7 @@ export class HotPoolManager {
|
|
|
644
652
|
load_balancer: config.load_balancer !== false,
|
|
645
653
|
enabled: this.#enabled,
|
|
646
654
|
verbose: !!config.verbose,
|
|
655
|
+
webgl_ready_slots: poolsConfig[poolId]?.webgl_ready_slots ?? 0,
|
|
647
656
|
})
|
|
648
657
|
|
|
649
658
|
// Terminal pool (bare shells)
|
|
@@ -738,6 +747,7 @@ export class HotPoolManager {
|
|
|
738
747
|
load_balancer: config.load_balancer,
|
|
739
748
|
enabled: config.enabled,
|
|
740
749
|
verbose: config.verbose,
|
|
750
|
+
webgl_ready_slots: poolsConfig[id]?.webgl_ready_slots,
|
|
741
751
|
}
|
|
742
752
|
pool.reconfigure(poolConfig)
|
|
743
753
|
}
|
package/src/canvas/server.js
CHANGED
|
@@ -770,7 +770,7 @@ export function createCanvasHandler(ctx) {
|
|
|
770
770
|
})
|
|
771
771
|
|
|
772
772
|
const response = { success: true, widget }
|
|
773
|
-
if (hotSession) response.hotSession = { id: hotSession.id, tmuxName: hotSession.tmuxName || null }
|
|
773
|
+
if (hotSession) response.hotSession = { id: hotSession.id, tmuxName: hotSession.tmuxName || null, webglReady: !!hotSession.webglReady }
|
|
774
774
|
sendJson(res, 201, response)
|
|
775
775
|
pushCanvasUpdate(name, filePath, __viteWs)
|
|
776
776
|
} catch (err) {
|
|
@@ -1334,7 +1334,7 @@ export function createCanvasHandler(ctx) {
|
|
|
1334
1334
|
if (ref) refs[ref] = widgetId
|
|
1335
1335
|
|
|
1336
1336
|
const result = { index: i, op: 'create-widget', ref: ref || undefined, widgetId, widget }
|
|
1337
|
-
if (hotSession) result.hotSession = { id: hotSession.id, tmuxName: hotSession.tmuxName || null }
|
|
1337
|
+
if (hotSession) result.hotSession = { id: hotSession.id, tmuxName: hotSession.tmuxName || null, webglReady: !!hotSession.webglReady }
|
|
1338
1338
|
results.push(result)
|
|
1339
1339
|
break
|
|
1340
1340
|
}
|
package/src/configSchema.js
CHANGED
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
* @property {number} [default_max_pool_size] — default surge cap per pool (default: 3)
|
|
66
66
|
* @property {boolean} [load_balancer] — enable auto-scaling (default: true)
|
|
67
67
|
* @property {number} [load_balancer_cooldown_mins] — minutes idle before scale-down (default: 10)
|
|
68
|
-
* @property {Record<string, { pool_size?: number, max_pool_size?: number }>} [pools] — per-pool overrides (terminal, prompt, copilot, claude, codex)
|
|
68
|
+
* @property {Record<string, { pool_size?: number, max_pool_size?: number, webgl_ready_slots?: number }>} [pools] — per-pool overrides (terminal, prompt, copilot, claude, codex). webgl_ready_slots: how many front-of-queue sessions should start with PINNED WebGL priority (default: 0)
|
|
69
69
|
*/
|
|
70
70
|
|
|
71
71
|
/**
|
package/src/index.js
CHANGED
|
@@ -62,7 +62,7 @@ export { mountSceneDebug } from './sceneDebug.js'
|
|
|
62
62
|
export { mountStoryboardCore } from './mountStoryboardCore.js'
|
|
63
63
|
|
|
64
64
|
// Viewfinder utilities
|
|
65
|
-
export { hash, resolveFlowRoute, getFlowMeta, buildPrototypeIndex } from './viewfinder.js'
|
|
65
|
+
export { hash, resolveFlowRoute, getFlowMeta, buildPrototypeIndex, appendTokens } from './viewfinder.js'
|
|
66
66
|
// Deprecated aliases
|
|
67
67
|
export { resolveSceneRoute, getSceneMeta } from './viewfinder.js'
|
|
68
68
|
|
package/src/viewfinder.js
CHANGED
|
@@ -13,6 +13,26 @@ export function hash(str) {
|
|
|
13
13
|
return Math.abs(h)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Append `tokens` from flow/prototype data as URL search params.
|
|
18
|
+
* Filters out reserved keys (`flow`, `scene`) and non-scalar values.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} url - Base URL (may already contain query params)
|
|
21
|
+
* @param {object|null|undefined} tokens - Key-value pairs to append
|
|
22
|
+
* @returns {string} URL with token params appended
|
|
23
|
+
*/
|
|
24
|
+
export function appendTokens(url, tokens) {
|
|
25
|
+
if (!tokens || typeof tokens !== 'object') return url
|
|
26
|
+
const reserved = new Set(['flow', 'scene'])
|
|
27
|
+
const entries = Object.entries(tokens).filter(
|
|
28
|
+
([k, v]) => v != null && !reserved.has(k) && typeof v !== 'object',
|
|
29
|
+
)
|
|
30
|
+
if (entries.length === 0) return url
|
|
31
|
+
const sep = url.includes('?') ? '&' : '?'
|
|
32
|
+
const qs = entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&')
|
|
33
|
+
return `${url}${sep}${qs}`
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
/**
|
|
17
37
|
* Resolve the target route path for a flow.
|
|
18
38
|
*
|
|
@@ -23,42 +43,51 @@ export function hash(str) {
|
|
|
23
43
|
* 4. Fall back to root "/"
|
|
24
44
|
*
|
|
25
45
|
* Flows with `meta.default: true` targeting a route omit the `?flow=` param.
|
|
46
|
+
* If the flow data contains a `tokens` object, its entries are appended as query params.
|
|
26
47
|
*
|
|
27
48
|
* @param {string} flowName
|
|
28
49
|
* @param {string[]} knownRoutes - Array of route names (e.g. ["Dashboard", "Repositories"])
|
|
29
50
|
* @returns {string} Full path with optional ?flow= param
|
|
30
51
|
*/
|
|
31
52
|
export function resolveFlowRoute(flowName, knownRoutes = []) {
|
|
53
|
+
let routeUrl
|
|
54
|
+
let data = null
|
|
55
|
+
|
|
32
56
|
// Case-insensitive match against known routes
|
|
33
|
-
|
|
34
|
-
if (route.toLowerCase() === flowName.toLowerCase()) {
|
|
35
|
-
// Flow name matches the route — no ?flow= needed,
|
|
36
|
-
// StoryboardProvider auto-matches by page name
|
|
37
|
-
return `/${route}`
|
|
38
|
-
}
|
|
39
|
-
}
|
|
57
|
+
const matchedRoute = knownRoutes.find(r => r.toLowerCase() === flowName.toLowerCase())
|
|
40
58
|
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
if (matchedRoute) {
|
|
60
|
+
routeUrl = `/${matchedRoute}`
|
|
61
|
+
} else {
|
|
62
|
+
try {
|
|
63
|
+
data = loadFlow(flowName)
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
// Check for explicit route: top-level `route`, then meta.route, then legacy sceneMeta.route
|
|
66
|
+
const explicitRoute = data?.route || data?.meta?.route || data?.flowMeta?.route || data?.sceneMeta?.route
|
|
67
|
+
if (explicitRoute) {
|
|
68
|
+
const normalized = explicitRoute.startsWith('/') ? explicitRoute : `/${explicitRoute}`
|
|
69
|
+
routeUrl = data?.meta?.default === true
|
|
70
|
+
? normalized
|
|
71
|
+
: `${normalized}?flow=${encodeURIComponent(flowName)}`
|
|
72
|
+
} else if (data?._route) {
|
|
73
|
+
// Use inferred route from file path (injected by Vite data plugin)
|
|
74
|
+
routeUrl = data?.meta?.default === true
|
|
75
|
+
? data._route
|
|
76
|
+
: `${data._route}?flow=${encodeURIComponent(flowName)}`
|
|
77
|
+
} else {
|
|
78
|
+
routeUrl = `/?flow=${encodeURIComponent(flowName)}`
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
routeUrl = `/?flow=${encodeURIComponent(flowName)}`
|
|
50
82
|
}
|
|
83
|
+
}
|
|
51
84
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return `${data._route}?flow=${encodeURIComponent(flowName)}`
|
|
56
|
-
}
|
|
57
|
-
} catch {
|
|
58
|
-
// ignore load errors
|
|
85
|
+
// Load flow data for tokens if not already loaded (e.g. known-route early match)
|
|
86
|
+
if (!data) {
|
|
87
|
+
try { data = loadFlow(flowName) } catch { /* ignore */ }
|
|
59
88
|
}
|
|
60
89
|
|
|
61
|
-
return
|
|
90
|
+
return appendTokens(routeUrl, data?.tokens)
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
/** @deprecated Use resolveFlowRoute() */
|
package/src/viewfinder.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { init } from './loader.js'
|
|
2
|
-
import { hash, resolveFlowRoute, getFlowMeta, resolveSceneRoute, getSceneMeta, buildPrototypeIndex } from './viewfinder.js'
|
|
2
|
+
import { hash, resolveFlowRoute, getFlowMeta, resolveSceneRoute, getSceneMeta, buildPrototypeIndex, appendTokens } from './viewfinder.js'
|
|
3
3
|
|
|
4
4
|
const makeIndex = () => ({
|
|
5
5
|
flows: {
|
|
@@ -343,3 +343,114 @@ describe('buildPrototypeIndex', () => {
|
|
|
343
343
|
expect(result.prototypes[0].lastModified).toBeNull()
|
|
344
344
|
})
|
|
345
345
|
})
|
|
346
|
+
|
|
347
|
+
// ── appendTokens ──
|
|
348
|
+
|
|
349
|
+
describe('appendTokens', () => {
|
|
350
|
+
it('returns url unchanged when tokens is null', () => {
|
|
351
|
+
expect(appendTokens('/foo', null)).toBe('/foo')
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('returns url unchanged when tokens is undefined', () => {
|
|
355
|
+
expect(appendTokens('/foo', undefined)).toBe('/foo')
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('returns url unchanged when tokens is empty', () => {
|
|
359
|
+
expect(appendTokens('/foo', {})).toBe('/foo')
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('appends single token with ?', () => {
|
|
363
|
+
expect(appendTokens('/foo', { token: 'abc' })).toBe('/foo?token=abc')
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('appends multiple tokens', () => {
|
|
367
|
+
const result = appendTokens('/foo', { token: 'abc', model: 'gpt-4' })
|
|
368
|
+
expect(result).toBe('/foo?token=abc&model=gpt-4')
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('uses & separator when url already has query params', () => {
|
|
372
|
+
expect(appendTokens('/foo?flow=bar', { token: 'abc' })).toBe('/foo?flow=bar&token=abc')
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('filters out reserved key "flow"', () => {
|
|
376
|
+
expect(appendTokens('/foo', { flow: 'bad', token: 'good' })).toBe('/foo?token=good')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('filters out reserved key "scene"', () => {
|
|
380
|
+
expect(appendTokens('/foo', { scene: 'bad', token: 'good' })).toBe('/foo?token=good')
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('filters out null and undefined values', () => {
|
|
384
|
+
expect(appendTokens('/foo', { a: null, b: undefined, c: 'ok' })).toBe('/foo?c=ok')
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('filters out object and array values', () => {
|
|
388
|
+
expect(appendTokens('/foo', { a: { nested: true }, b: [1, 2], c: 'ok' })).toBe('/foo?c=ok')
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('encodes special characters', () => {
|
|
392
|
+
expect(appendTokens('/foo', { key: 'hello world' })).toBe('/foo?key=hello%20world')
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('handles numeric and boolean values', () => {
|
|
396
|
+
expect(appendTokens('/foo', { count: 42, active: true })).toBe('/foo?count=42&active=true')
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// ── resolveFlowRoute with tokens ──
|
|
401
|
+
|
|
402
|
+
describe('resolveFlowRoute with tokens', () => {
|
|
403
|
+
it('appends tokens to a flow with explicit route', () => {
|
|
404
|
+
init({
|
|
405
|
+
flows: { 'with-tokens': { route: '/Overview', tokens: { token: 'sk-abc' } } },
|
|
406
|
+
objects: {},
|
|
407
|
+
records: {},
|
|
408
|
+
})
|
|
409
|
+
expect(resolveFlowRoute('with-tokens', [])).toBe('/Overview?flow=with-tokens&token=sk-abc')
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('appends tokens to a known-route match', () => {
|
|
413
|
+
init({
|
|
414
|
+
flows: { Dashboard: { heading: 'Hi', tokens: { model: 'gpt-4' } } },
|
|
415
|
+
objects: {},
|
|
416
|
+
records: {},
|
|
417
|
+
})
|
|
418
|
+
expect(resolveFlowRoute('Dashboard', ['Dashboard'])).toBe('/Dashboard?model=gpt-4')
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('appends tokens to inferred route', () => {
|
|
422
|
+
init({
|
|
423
|
+
flows: { 'inferred': { _route: '/Settings', tokens: { key: 'val' } } },
|
|
424
|
+
objects: {},
|
|
425
|
+
records: {},
|
|
426
|
+
})
|
|
427
|
+
expect(resolveFlowRoute('inferred', [])).toBe('/Settings?flow=inferred&key=val')
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('appends tokens to default flow fallback', () => {
|
|
431
|
+
init({
|
|
432
|
+
flows: { 'no-route': { title: 'No route', tokens: { api: '123' } } },
|
|
433
|
+
objects: {},
|
|
434
|
+
records: {},
|
|
435
|
+
})
|
|
436
|
+
expect(resolveFlowRoute('no-route', [])).toBe('/?flow=no-route&api=123')
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('does not append tokens when flow has none', () => {
|
|
440
|
+
init({
|
|
441
|
+
flows: { simple: { route: '/Page' } },
|
|
442
|
+
objects: {},
|
|
443
|
+
records: {},
|
|
444
|
+
})
|
|
445
|
+
expect(resolveFlowRoute('simple', [])).toBe('/Page?flow=simple')
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('appends tokens with meta.default route (no ?flow=)', () => {
|
|
449
|
+
init({
|
|
450
|
+
flows: { 'default-tokens': { _route: '/Home', meta: { default: true }, tokens: { key: 'val' } } },
|
|
451
|
+
objects: {},
|
|
452
|
+
records: {},
|
|
453
|
+
})
|
|
454
|
+
expect(resolveFlowRoute('default-tokens', [])).toBe('/Home?key=val')
|
|
455
|
+
})
|
|
456
|
+
})
|
|
@@ -567,6 +567,15 @@ export default function storyboardServer() {
|
|
|
567
567
|
injectTo: 'head',
|
|
568
568
|
})
|
|
569
569
|
|
|
570
|
+
// Inject dev domain name for branch bar display
|
|
571
|
+
if (config.devDomain) {
|
|
572
|
+
tags.push({
|
|
573
|
+
tag: 'script',
|
|
574
|
+
children: `window.__SB_DEV_DOMAIN__=${JSON.stringify(config.devDomain)}`,
|
|
575
|
+
injectTo: 'head',
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
|
|
570
579
|
// Inject per-domain branch bar color (configurable via devDomainColor)
|
|
571
580
|
if (config.devDomainColor) {
|
|
572
581
|
tags.push({
|