@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.
Files changed (49) hide show
  1. package/dist/storyboard-ui.js +3112 -3098
  2. package/dist/storyboard-ui.js.map +1 -1
  3. package/mascot/frame-01-peek-left.txt +4 -0
  4. package/mascot/frame-02-eyes-open.txt +4 -0
  5. package/mascot/frame-03-peek-right.txt +4 -0
  6. package/mascot/frame-04-eyes-open.txt +4 -0
  7. package/mascot/frame-05-eyes-closed.txt +4 -0
  8. package/mascot/frame-06-eyes-open.txt +4 -0
  9. package/mascot.config.json +13 -0
  10. package/package.json +5 -2
  11. package/scaffold/AGENTS.md +1 -0
  12. package/scaffold/gitignore +12 -2
  13. package/scaffold/skills/design-system-catalog/SKILL.md +98 -0
  14. package/scaffold/skills/design-system-catalog/extract-components.mjs +441 -0
  15. package/scaffold/skills/design-system-catalog/generate-catalog.sh +255 -0
  16. package/scaffold/skills/migrate/SKILL.md +72 -50
  17. package/scaffold/terminal-agent.agent.md +8 -1
  18. package/src/core/canvas/agent-session.js +103 -17
  19. package/src/core/canvas/agent-session.test.js +29 -1
  20. package/src/core/canvas/collision.js +54 -45
  21. package/src/core/canvas/collision.test.js +39 -0
  22. package/src/core/canvas/configReader.js +110 -0
  23. package/src/core/canvas/hot-pool.js +5 -3
  24. package/src/core/canvas/server.js +32 -13
  25. package/src/core/canvas/terminal-server.js +156 -91
  26. package/src/core/cli/agent.js +86 -33
  27. package/src/core/cli/dev.js +303 -17
  28. package/src/core/cli/server.js +1 -1
  29. package/src/core/cli/setup.js +203 -60
  30. package/src/core/cli/terminal-welcome.js +5 -6
  31. package/src/core/cli/userState.js +63 -0
  32. package/src/core/stores/configSchema.js +1 -0
  33. package/src/core/stores/themeStore.ts +24 -0
  34. package/src/core/tools/handlers/devtools.test.js +1 -1
  35. package/src/core/vite/server-plugin.js +107 -10
  36. package/src/internals/CommandPalette/CommandPalette.jsx +1 -1
  37. package/src/internals/Viewfinder.jsx +10 -2
  38. package/src/internals/canvas/CanvasPage.jsx +30 -9
  39. package/src/internals/canvas/WebGLContextPool.jsx +6 -7
  40. package/src/internals/canvas/componentIsolate.jsx +7 -8
  41. package/src/internals/canvas/componentSetIsolate.jsx +7 -8
  42. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +3 -1
  43. package/src/internals/canvas/widgets/StorySetWidget.jsx +19 -7
  44. package/src/internals/canvas/widgets/StoryWidget.jsx +9 -3
  45. package/src/internals/canvas/widgets/TerminalWidget.jsx +74 -13
  46. package/src/internals/canvas/widgets/expandUtils.js +4 -2
  47. package/src/internals/hooks/usePrototypeReloadGuard.js +9 -5
  48. package/src/internals/vite/data-plugin.js +126 -3
  49. 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. If collision, find the max endX among all colliders and move past it
166
- * 3. Repeat until no collisions or maxIterations exhausted
167
- * 4. If horizontal resolution exhausted, fall back to moving down
168
- * 5. Snap final position to grid
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
- // Phase 1: Try moving right past all colliders
199
- for (let i = 0; i < maxIterations; i++) {
200
- const rect = { x: currentX, y: currentY, width, height }
201
- const colliders = findCollisions(rect, widgets, excludeId)
202
-
203
- if (colliders.length === 0) {
204
- return {
205
- x: snapToGrid(currentX, gridSize),
206
- y: snapToGrid(currentY, gridSize),
207
- adjusted,
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
- // Phase 2: Reset X, try moving down
223
- currentX = x
224
-
225
- for (let i = 0; i < maxIterations; i++) {
226
- const rect = { x: currentX, y: currentY, width, height }
227
- const colliders = findCollisions(rect, widgets, excludeId)
228
-
229
- if (colliders.length === 0) {
230
- return {
231
- x: snapToGrid(currentX, gridSize),
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
- // Jump past the bottommost edge of all colliders
238
- let maxEndY = 0
239
- for (const c of colliders) {
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
- currentY = maxEndY + spacing
245
- adjusted = true
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 --full-auto` running & ready
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
- `tmux capture-pane -t "${tmuxName}" -p`,
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 configPath = path.join(root, 'storyboard.config.json')
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 configPath = path.join(root, 'storyboard.config.json')
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: (canvasData && canvasData.gridSize) || 24,
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: canvasData.gridSize || 24,
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
- const envContent = Object.entries(envMap).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join('\n') + '\n'
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 configPath = path.join(root, 'storyboard.config.json')
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
- const pane = execSync(`tmux capture-pane -t "${tmuxName}" -p`, { encoding: 'utf8', timeout: 1000 })
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)