@dfosco/storyboard 0.6.0-beta.2 → 0.6.0-beta.21
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 +3112 -3098
- package/dist/storyboard-ui.js.map +1 -1
- package/mascot/frame-01-peek-left.txt +4 -0
- package/mascot/frame-02-eyes-open.txt +4 -0
- package/mascot/frame-03-peek-right.txt +4 -0
- package/mascot/frame-04-eyes-open.txt +4 -0
- package/mascot/frame-05-eyes-closed.txt +4 -0
- package/mascot/frame-06-eyes-open.txt +4 -0
- package/mascot.config.json +13 -0
- package/package.json +5 -2
- package/scaffold/AGENTS.md +1 -0
- package/scaffold/gitignore +12 -2
- package/scaffold/skills/design-system-catalog/SKILL.md +98 -0
- package/scaffold/skills/design-system-catalog/extract-components.mjs +441 -0
- package/scaffold/skills/design-system-catalog/generate-catalog.sh +255 -0
- package/scaffold/skills/migrate/SKILL.md +72 -50
- package/scaffold/terminal-agent.agent.md +8 -1
- package/src/core/canvas/agent-session.js +103 -17
- package/src/core/canvas/agent-session.test.js +29 -1
- package/src/core/canvas/collision.js +54 -45
- package/src/core/canvas/collision.test.js +39 -0
- package/src/core/canvas/configReader.js +110 -0
- package/src/core/canvas/hot-pool.js +5 -3
- package/src/core/canvas/server.js +32 -13
- package/src/core/canvas/terminal-server.js +156 -91
- package/src/core/cli/agent.js +86 -33
- package/src/core/cli/dev.js +303 -17
- package/src/core/cli/server.js +1 -1
- package/src/core/cli/setup.js +203 -60
- package/src/core/cli/terminal-welcome.js +5 -6
- package/src/core/cli/userState.js +63 -0
- package/src/core/stores/configSchema.js +1 -0
- package/src/core/stores/themeStore.ts +24 -0
- package/src/core/tools/handlers/devtools.test.js +1 -1
- package/src/core/vite/server-plugin.js +107 -10
- package/src/internals/CommandPalette/CommandPalette.jsx +1 -1
- package/src/internals/Viewfinder.jsx +10 -2
- package/src/internals/canvas/CanvasPage.jsx +30 -9
- package/src/internals/canvas/WebGLContextPool.jsx +6 -7
- package/src/internals/canvas/componentIsolate.jsx +7 -8
- package/src/internals/canvas/componentSetIsolate.jsx +7 -8
- package/src/internals/canvas/widgets/PrototypeEmbed.jsx +3 -1
- package/src/internals/canvas/widgets/StorySetWidget.jsx +19 -7
- package/src/internals/canvas/widgets/StoryWidget.jsx +9 -3
- package/src/internals/canvas/widgets/TerminalWidget.jsx +74 -13
- package/src/internals/canvas/widgets/expandUtils.js +4 -2
- package/src/internals/hooks/usePrototypeReloadGuard.js +9 -5
- package/src/internals/vite/data-plugin.js +126 -3
- package/terminal.config.json +66 -0
|
@@ -162,10 +162,14 @@ export function snapToGrid(value, gridSize) {
|
|
|
162
162
|
*
|
|
163
163
|
* Strategy:
|
|
164
164
|
* 1. Try the initial position
|
|
165
|
-
* 2.
|
|
166
|
-
* 3.
|
|
167
|
-
* 4.
|
|
168
|
-
*
|
|
165
|
+
* 2. Phase 1 — move along the primary axis (default horizontal) past colliders
|
|
166
|
+
* 3. Phase 2 — fall back to the orthogonal axis (default vertical) past colliders
|
|
167
|
+
* 4. Snap final position to grid
|
|
168
|
+
*
|
|
169
|
+
* Use `preferAxis: 'vertical'` to swap the phase order — useful when widgets
|
|
170
|
+
* are being fanned out around a reference widget (e.g. hub layouts) where
|
|
171
|
+
* moving horizontally further away from the reference defeats the placement
|
|
172
|
+
* intent. Examples: 'right' / 'left' / 'above-right' / 'below-right'.
|
|
169
173
|
*
|
|
170
174
|
* @param {object} options
|
|
171
175
|
* @param {number} options.x - Initial X position
|
|
@@ -177,6 +181,7 @@ export function snapToGrid(value, gridSize) {
|
|
|
177
181
|
* @param {number} [options.gridSize=24] - Grid size for snapping
|
|
178
182
|
* @param {number} [options.gap] - Gap between widgets (defaults to gridSize)
|
|
179
183
|
* @param {number} [options.maxIterations=50] - Max collision resolution attempts
|
|
184
|
+
* @param {'horizontal'|'vertical'} [options.preferAxis='horizontal'] - Which axis to try first
|
|
180
185
|
* @returns {{ x: number, y: number, adjusted: boolean }}
|
|
181
186
|
*/
|
|
182
187
|
export function findFreePosition({
|
|
@@ -189,63 +194,63 @@ export function findFreePosition({
|
|
|
189
194
|
gridSize = 24,
|
|
190
195
|
gap = null,
|
|
191
196
|
maxIterations = 50,
|
|
197
|
+
preferAxis = 'horizontal',
|
|
192
198
|
}) {
|
|
193
199
|
const spacing = gap ?? gridSize
|
|
194
200
|
let currentX = x
|
|
195
201
|
let currentY = y
|
|
196
202
|
let adjusted = false
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
const horizontalPhase = () => {
|
|
205
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
206
|
+
const rect = { x: currentX, y: currentY, width, height }
|
|
207
|
+
const colliders = findCollisions(rect, widgets, excludeId)
|
|
208
|
+
if (colliders.length === 0) return true
|
|
209
|
+
let maxEndX = 0
|
|
210
|
+
for (const c of colliders) {
|
|
211
|
+
const b = getWidgetBounds(c)
|
|
212
|
+
const endX = b.x + b.width
|
|
213
|
+
if (endX > maxEndX) maxEndX = endX
|
|
208
214
|
}
|
|
215
|
+
currentX = maxEndX + spacing
|
|
216
|
+
adjusted = true
|
|
209
217
|
}
|
|
210
|
-
|
|
211
|
-
// Jump past the rightmost edge of all colliders
|
|
212
|
-
let maxEndX = 0
|
|
213
|
-
for (const c of colliders) {
|
|
214
|
-
const b = getWidgetBounds(c)
|
|
215
|
-
const endX = b.x + b.width
|
|
216
|
-
if (endX > maxEndX) maxEndX = endX
|
|
217
|
-
}
|
|
218
|
-
currentX = maxEndX + spacing
|
|
219
|
-
adjusted = true
|
|
218
|
+
return false
|
|
220
219
|
}
|
|
221
220
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
y: snapToGrid(currentY, gridSize),
|
|
233
|
-
adjusted,
|
|
221
|
+
const verticalPhase = () => {
|
|
222
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
223
|
+
const rect = { x: currentX, y: currentY, width, height }
|
|
224
|
+
const colliders = findCollisions(rect, widgets, excludeId)
|
|
225
|
+
if (colliders.length === 0) return true
|
|
226
|
+
let maxEndY = 0
|
|
227
|
+
for (const c of colliders) {
|
|
228
|
+
const b = getWidgetBounds(c)
|
|
229
|
+
const endY = b.y + b.height
|
|
230
|
+
if (endY > maxEndY) maxEndY = endY
|
|
234
231
|
}
|
|
232
|
+
currentY = maxEndY + spacing
|
|
233
|
+
adjusted = true
|
|
235
234
|
}
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
236
237
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const b = getWidgetBounds(c)
|
|
241
|
-
const endY = b.y + b.height
|
|
242
|
-
if (endY > maxEndY) maxEndY = endY
|
|
238
|
+
if (preferAxis === 'vertical') {
|
|
239
|
+
if (verticalPhase()) {
|
|
240
|
+
return { x: snapToGrid(currentX, gridSize), y: snapToGrid(currentY, gridSize), adjusted }
|
|
243
241
|
}
|
|
244
|
-
|
|
245
|
-
|
|
242
|
+
// Reset Y, try horizontal
|
|
243
|
+
currentY = y
|
|
244
|
+
horizontalPhase()
|
|
245
|
+
} else {
|
|
246
|
+
if (horizontalPhase()) {
|
|
247
|
+
return { x: snapToGrid(currentX, gridSize), y: snapToGrid(currentY, gridSize), adjusted }
|
|
248
|
+
}
|
|
249
|
+
// Reset X, try vertical
|
|
250
|
+
currentX = x
|
|
251
|
+
verticalPhase()
|
|
246
252
|
}
|
|
247
253
|
|
|
248
|
-
// Fallback: return the last attempted position (snapped)
|
|
249
254
|
return {
|
|
250
255
|
x: snapToGrid(currentX, gridSize),
|
|
251
256
|
y: snapToGrid(currentY, gridSize),
|
|
@@ -275,6 +280,8 @@ export function resolvePosition({
|
|
|
275
280
|
widgets,
|
|
276
281
|
excludeId = null,
|
|
277
282
|
gridSize = 24,
|
|
283
|
+
gap = null,
|
|
284
|
+
preferAxis = 'horizontal',
|
|
278
285
|
}) {
|
|
279
286
|
const defaults = getDefaultSize(type)
|
|
280
287
|
const width = props.width ?? defaults.width
|
|
@@ -288,6 +295,8 @@ export function resolvePosition({
|
|
|
288
295
|
widgets,
|
|
289
296
|
excludeId,
|
|
290
297
|
gridSize,
|
|
298
|
+
gap,
|
|
299
|
+
preferAxis,
|
|
291
300
|
})
|
|
292
301
|
}
|
|
293
302
|
|
|
@@ -271,6 +271,45 @@ describe('findFreePosition', () => {
|
|
|
271
271
|
expect(result.x).toBeGreaterThan(294 + 270)
|
|
272
272
|
expect(result.adjusted).toBe(true)
|
|
273
273
|
})
|
|
274
|
+
|
|
275
|
+
it('preferAxis: "vertical" cascades down past colliders instead of right', () => {
|
|
276
|
+
// Simulates a hub fan-out: a leader on the left, two peers already placed
|
|
277
|
+
// above-right and below-right, then trying to place a third peer at the
|
|
278
|
+
// `right` direction (same column as the existing peers, leader-top y).
|
|
279
|
+
const widgets = [
|
|
280
|
+
{ id: 'leader', type: 'sticky-note', position: { x: 0, y: 0 }, props: { width: 200, height: 200 } },
|
|
281
|
+
{ id: 'above-right', type: 'sticky-note', position: { x: 240, y: -100 }, props: { width: 200, height: 200 } },
|
|
282
|
+
{ id: 'below-right', type: 'sticky-note', position: { x: 240, y: 200 }, props: { width: 200, height: 200 } },
|
|
283
|
+
]
|
|
284
|
+
const result = findFreePosition({
|
|
285
|
+
x: 240,
|
|
286
|
+
y: 0,
|
|
287
|
+
width: 200,
|
|
288
|
+
height: 200,
|
|
289
|
+
widgets,
|
|
290
|
+
gridSize: 24,
|
|
291
|
+
preferAxis: 'vertical',
|
|
292
|
+
})
|
|
293
|
+
// Should cascade DOWN past the above-right and below-right widgets,
|
|
294
|
+
// staying in the same X column as the fan, NOT jump far to the right.
|
|
295
|
+
expect(result.x).toBe(240)
|
|
296
|
+
expect(result.y).toBeGreaterThanOrEqual(200 + 200) // past below-right's endY
|
|
297
|
+
expect(result.adjusted).toBe(true)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('preferAxis: "horizontal" (default) still resolves rightward', () => {
|
|
301
|
+
const widgets = [
|
|
302
|
+
{ id: 'w1', type: 'sticky-note', position: { x: 0, y: 0 }, props: { width: 200, height: 200 } },
|
|
303
|
+
]
|
|
304
|
+
const result = findFreePosition({
|
|
305
|
+
x: 50, y: 50,
|
|
306
|
+
width: 200, height: 200,
|
|
307
|
+
widgets,
|
|
308
|
+
gridSize: 24,
|
|
309
|
+
})
|
|
310
|
+
expect(result.x).toBeGreaterThanOrEqual(200)
|
|
311
|
+
expect(result.y).toBe(48) // snapped from 50
|
|
312
|
+
})
|
|
274
313
|
})
|
|
275
314
|
|
|
276
315
|
describe('resolvePosition', () => {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side reader for terminal + agents config.
|
|
3
|
+
*
|
|
4
|
+
* Replaces direct `JSON.parse(readFileSync('storyboard.config.json')).canvas.agents`
|
|
5
|
+
* reads in terminal-server / hot-pool / canvas server / terminal-welcome /
|
|
6
|
+
* server-plugin so the new `terminal.config.json` (and the library's default
|
|
7
|
+
* one shipped under `node_modules/@dfosco/storyboard/terminal.config.json`)
|
|
8
|
+
* is honored everywhere with leaf-level merge.
|
|
9
|
+
*
|
|
10
|
+
* Resolution order (lowest → highest priority), all leaf-merged:
|
|
11
|
+
* 1. Library default `<root>/{packages/storyboard,node_modules/@dfosco/storyboard}/terminal.config.json`
|
|
12
|
+
* 2. `storyboard.config.json` `canvas.terminal` + `canvas.agents` (legacy)
|
|
13
|
+
* 3. Root `terminal.config.json`
|
|
14
|
+
*
|
|
15
|
+
* Returns `{ terminal, agents, showAgentsInAddMenu }`. Empty objects when
|
|
16
|
+
* nothing is configured (rather than null) so callers can spread freely.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
20
|
+
import { resolve, join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
/** Same shape as data-plugin's `deepMergeBuild`. */
|
|
23
|
+
function deepMerge(target, source) {
|
|
24
|
+
if (!source || typeof source !== 'object') return target
|
|
25
|
+
if (!target || typeof target !== 'object') return source
|
|
26
|
+
const result = { ...target }
|
|
27
|
+
for (const key of Object.keys(source)) {
|
|
28
|
+
const sv = source[key]
|
|
29
|
+
const tv = target[key]
|
|
30
|
+
if (sv && typeof sv === 'object' && !Array.isArray(sv) && tv && typeof tv === 'object' && !Array.isArray(tv)) {
|
|
31
|
+
result[key] = deepMerge(tv, sv)
|
|
32
|
+
} else {
|
|
33
|
+
result[key] = sv
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readJson(filePath) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(readFileSync(filePath, 'utf8'))
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveLibTerminalConfig(root) {
|
|
48
|
+
const candidates = [
|
|
49
|
+
join(root, 'packages', 'storyboard', 'terminal.config.json'),
|
|
50
|
+
join(root, 'node_modules', '@dfosco', 'storyboard', 'terminal.config.json'),
|
|
51
|
+
]
|
|
52
|
+
for (const p of candidates) {
|
|
53
|
+
if (existsSync(p)) {
|
|
54
|
+
const parsed = readJson(p)
|
|
55
|
+
if (parsed) return parsed
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Read the merged terminal + agents + hotPool config for a project root.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} [root] - Project root, defaults to `process.cwd()`.
|
|
65
|
+
* @returns {{ terminal: object, agents: object, showAgentsInAddMenu: boolean|undefined, hotPool: object }}
|
|
66
|
+
*/
|
|
67
|
+
export function readTerminalConfigMerged(root = process.cwd()) {
|
|
68
|
+
const lib = resolveLibTerminalConfig(root) || {}
|
|
69
|
+
const sb = readJson(resolve(root, 'storyboard.config.json')) || {}
|
|
70
|
+
const userTerminal = readJson(resolve(root, 'terminal.config.json')) || {}
|
|
71
|
+
|
|
72
|
+
const sbCanvas = sb.canvas || {}
|
|
73
|
+
const sbLayer = {
|
|
74
|
+
...(sbCanvas.terminal ? { terminal: sbCanvas.terminal } : {}),
|
|
75
|
+
...(sbCanvas.agents ? { agents: sbCanvas.agents } : {}),
|
|
76
|
+
...(sbCanvas.showAgentsInAddMenu !== undefined
|
|
77
|
+
? { showAgentsInAddMenu: sbCanvas.showAgentsInAddMenu }
|
|
78
|
+
: {}),
|
|
79
|
+
...(sb.hotPool ? { hotPool: sb.hotPool } : {}),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const merged = deepMerge(deepMerge(lib, sbLayer), userTerminal)
|
|
83
|
+
return {
|
|
84
|
+
terminal: merged.terminal || {},
|
|
85
|
+
agents: merged.agents || {},
|
|
86
|
+
showAgentsInAddMenu: merged.showAgentsInAddMenu,
|
|
87
|
+
hotPool: merged.hotPool || {},
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Convenience: just the agents map.
|
|
93
|
+
*/
|
|
94
|
+
export function readAgentsConfig(root = process.cwd()) {
|
|
95
|
+
return readTerminalConfigMerged(root).agents
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Convenience: just the terminal-widget settings.
|
|
100
|
+
*/
|
|
101
|
+
export function readTerminalSettings(root = process.cwd()) {
|
|
102
|
+
return readTerminalConfigMerged(root).terminal
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Convenience: just the hotPool config.
|
|
107
|
+
*/
|
|
108
|
+
export function readHotPoolConfig(root = process.cwd()) {
|
|
109
|
+
return readTerminalConfigMerged(root).hotPool
|
|
110
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - **prompt** — bare tmux shell (prompt widgets)
|
|
13
13
|
* - **copilot** — tmux + `copilot --agent terminal-agent` running & ready
|
|
14
14
|
* - **claude** — tmux + `claude --agent terminal-agent ...` running & ready
|
|
15
|
-
* - **codex** — tmux + `codex --
|
|
15
|
+
* - **codex** — tmux + `codex --dangerously-bypass-approvals-and-sandbox` running & ready
|
|
16
16
|
*
|
|
17
17
|
* ## Load Balancer (per-pool)
|
|
18
18
|
*
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* Scale-down: After cooldown minutes with no acquisitions, the pool scales
|
|
26
26
|
* back to pool_size by killing excess warm sessions.
|
|
27
27
|
*
|
|
28
|
-
* ## Configuration (storyboard.config.json → hotPool)
|
|
28
|
+
* ## Configuration (terminal.config.json → hotPool, or storyboard.config.json → hotPool for legacy back-compat)
|
|
29
29
|
*
|
|
30
30
|
* hotPool.enabled — enable/disable all pools (default: true)
|
|
31
31
|
* hotPool.verbose — log to Vite terminal (default: false)
|
|
@@ -549,7 +549,9 @@ export class HotPool {
|
|
|
549
549
|
const poll = setInterval(() => {
|
|
550
550
|
try {
|
|
551
551
|
const paneContent = execSync(
|
|
552
|
-
|
|
552
|
+
// H1: include scrollback so the readiness echo can still be
|
|
553
|
+
// matched after the agent's TUI repaints over it.
|
|
554
|
+
`tmux capture-pane -t "${tmuxName}" -p -S -200`,
|
|
553
555
|
{ encoding: 'utf8', timeout: 1000 }
|
|
554
556
|
)
|
|
555
557
|
// Strip ANSI escape sequences — agent CLIs use heavy formatting
|
|
@@ -52,6 +52,7 @@ import { markCanvasWrite, unmarkCanvasWrite } from './writeGuard.js'
|
|
|
52
52
|
import { devLog } from '../logger/devLogger.js'
|
|
53
53
|
import widgetsConfig from '../../../widgets.config.json' with { type: 'json' }
|
|
54
54
|
import { listHubRoles, getDefaultRoleId } from './hub-roles.js'
|
|
55
|
+
import { readAgentsConfig } from './configReader.js'
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* Read the prompt widget's execution config from widgets.config.json.
|
|
@@ -263,6 +264,17 @@ export function createCanvasHandler(ctx) {
|
|
|
263
264
|
* @param {number} gridSize — pixel size of one grid unit (default 24)
|
|
264
265
|
* @returns {{ x: number, y: number }}
|
|
265
266
|
*/
|
|
267
|
+
/**
|
|
268
|
+
* Map a `--direction` value to the preferred collision-resolution axis.
|
|
269
|
+
* Side placements (left/right/diagonals) cascade vertically so a fan of
|
|
270
|
+
* widgets stacks into a clean column instead of getting shoved further
|
|
271
|
+
* away from the reference widget. Above/below cascade horizontally.
|
|
272
|
+
*/
|
|
273
|
+
function directionPreferAxis(direction) {
|
|
274
|
+
if (direction === 'above' || direction === 'below') return 'horizontal'
|
|
275
|
+
return 'vertical'
|
|
276
|
+
}
|
|
277
|
+
|
|
266
278
|
function computeNearPosition(refWidget, direction = 'right', newType = 'sticky-note', newProps = {}, gap = 1, gridSize = 24) {
|
|
267
279
|
gap = gap * gridSize
|
|
268
280
|
const refBounds = getWidgetBounds(refWidget)
|
|
@@ -713,9 +725,7 @@ export function createCanvasHandler(ctx) {
|
|
|
713
725
|
// For agent widgets, resolve startupCommand from canvas.agents config if not provided
|
|
714
726
|
if (type === 'agent' && props.agentId && !props.startupCommand) {
|
|
715
727
|
try {
|
|
716
|
-
const
|
|
717
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
718
|
-
const agentCfg = config?.canvas?.agents?.[props.agentId]
|
|
728
|
+
const agentCfg = readAgentsConfig(root)?.[props.agentId]
|
|
719
729
|
if (agentCfg?.startupCommand) {
|
|
720
730
|
props.startupCommand = agentCfg.startupCommand
|
|
721
731
|
}
|
|
@@ -725,9 +735,7 @@ export function createCanvasHandler(ctx) {
|
|
|
725
735
|
// For agent widgets without agentId, default to the first canvas.agents entry
|
|
726
736
|
if (type === 'agent' && !props.agentId) {
|
|
727
737
|
try {
|
|
728
|
-
const
|
|
729
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
730
|
-
const agents = config?.canvas?.agents || {}
|
|
738
|
+
const agents = readAgentsConfig(root) || {}
|
|
731
739
|
const defaultEntry = Object.entries(agents).find(([, cfg]) => cfg.default) || Object.entries(agents)[0]
|
|
732
740
|
if (defaultEntry) {
|
|
733
741
|
const [id, cfg] = defaultEntry
|
|
@@ -1106,10 +1114,16 @@ export function createCanvasHandler(ctx) {
|
|
|
1106
1114
|
}
|
|
1107
1115
|
|
|
1108
1116
|
if (near || resolve || needsAutoPosition) {
|
|
1117
|
+
const gs = (canvasData && canvasData.gridSize) || 24
|
|
1109
1118
|
const resolved = resolvePosition({
|
|
1110
1119
|
x: position.x, y: position.y, type, props,
|
|
1111
1120
|
widgets: canvasWidgets,
|
|
1112
|
-
gridSize:
|
|
1121
|
+
gridSize: gs,
|
|
1122
|
+
// When the user supplied --gap (grid spaces), use the same spacing
|
|
1123
|
+
// for collision cascades so cascaded widgets keep the gap, not just
|
|
1124
|
+
// sit one gridSize apart.
|
|
1125
|
+
gap: near && typeof gap === 'number' ? gap * gs : null,
|
|
1126
|
+
preferAxis: near ? directionPreferAxis(direction) : 'horizontal',
|
|
1113
1127
|
})
|
|
1114
1128
|
position = { x: resolved.x, y: resolved.y }
|
|
1115
1129
|
}
|
|
@@ -1864,10 +1878,13 @@ export function createCanvasHandler(ctx) {
|
|
|
1864
1878
|
|
|
1865
1879
|
// Collision resolution: uses live widgetMap (includes earlier batch creates)
|
|
1866
1880
|
if (near || doResolve || needsAuto) {
|
|
1881
|
+
const gs = canvasData.gridSize || 24
|
|
1867
1882
|
const resolved = resolvePosition({
|
|
1868
1883
|
x: position.x, y: position.y, type, props,
|
|
1869
1884
|
widgets: Array.from(widgetMap.values()),
|
|
1870
|
-
gridSize:
|
|
1885
|
+
gridSize: gs,
|
|
1886
|
+
gap: near && typeof opGap === 'number' ? opGap * gs : null,
|
|
1887
|
+
preferAxis: near ? directionPreferAxis(direction) : 'horizontal',
|
|
1871
1888
|
})
|
|
1872
1889
|
position = { x: resolved.x, y: resolved.y }
|
|
1873
1890
|
}
|
|
@@ -3106,15 +3123,16 @@ export function Default() {
|
|
|
3106
3123
|
// Write env file for this terminal session — sourced before copilot launch
|
|
3107
3124
|
// This avoids race conditions with tmux send-keys export
|
|
3108
3125
|
const envFile = path.join(root, '.storyboard', 'terminals', `${tmuxName}.env`)
|
|
3109
|
-
|
|
3126
|
+
// Trailing echo is the readiness signal the post-startup poller
|
|
3127
|
+
// matches against. Don't drop it — without it /allow-all and the
|
|
3128
|
+
// identity/role/broadcast bind wait the full 30s timeout fallback.
|
|
3129
|
+
const envContent = Object.entries(envMap).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join('\n') + '\necho "Environment loaded:"\n'
|
|
3110
3130
|
fsModule.writeFileSync(envFile, envContent)
|
|
3111
3131
|
|
|
3112
3132
|
// Resolve agent config from storyboard.config.json
|
|
3113
3133
|
let agentConfig = null
|
|
3114
3134
|
try {
|
|
3115
|
-
const
|
|
3116
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
3117
|
-
const agents = config?.canvas?.agents || {}
|
|
3135
|
+
const agents = readAgentsConfig(root) || {}
|
|
3118
3136
|
if (agentId && agents[agentId]) {
|
|
3119
3137
|
agentConfig = agents[agentId]
|
|
3120
3138
|
} else {
|
|
@@ -3150,7 +3168,8 @@ export function Default() {
|
|
|
3150
3168
|
const poll = setInterval(() => {
|
|
3151
3169
|
if (sent) { clearInterval(poll); return }
|
|
3152
3170
|
try {
|
|
3153
|
-
|
|
3171
|
+
// H1: include scrollback so the echo survives Copilot's TUI repaint.
|
|
3172
|
+
const pane = execSync(`tmux capture-pane -t "${tmuxName}" -p -S -200`, { encoding: 'utf8', timeout: 1000 })
|
|
3154
3173
|
if (pane.includes(readinessSignal)) {
|
|
3155
3174
|
sent = true
|
|
3156
3175
|
clearInterval(poll)
|