@dfosco/storyboard 0.5.0-alpha.0

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 (592) hide show
  1. package/commandpalette.config.json +152 -0
  2. package/dist/storyboard-ui.css +1 -0
  3. package/dist/storyboard-ui.js +21328 -0
  4. package/dist/storyboard-ui.js.map +1 -0
  5. package/dist/tailwind.css +2 -0
  6. package/dist/tiny-canvas.css +1 -0
  7. package/dist/tiny-canvas.js +389 -0
  8. package/package.json +121 -0
  9. package/paste.config.json +67 -0
  10. package/scaffold/AGENTS.md +432 -0
  11. package/scaffold/agents/prompt-agent.agent.md +181 -0
  12. package/scaffold/agents/terminal-agent.agent.md +351 -0
  13. package/scaffold/codex/config.toml +246 -0
  14. package/scaffold/deploy.yml +103 -0
  15. package/scaffold/githooks/pre-push +114 -0
  16. package/scaffold/gitignore +64 -0
  17. package/scaffold/manifest.json +56 -0
  18. package/scaffold/preview.yml +181 -0
  19. package/scaffold/scripts/link.sh +26 -0
  20. package/scaffold/scripts/unlink.sh +10 -0
  21. package/scaffold/skills/agent-browser/SKILL.md +260 -0
  22. package/scaffold/skills/canvas/SKILL.md +364 -0
  23. package/scaffold/skills/create/SKILL.md +501 -0
  24. package/scaffold/skills/ship/SKILL.md +237 -0
  25. package/scaffold/skills/storyboard/SKILL.md +360 -0
  26. package/scaffold/skills/update-storyboard/SKILL.md +16 -0
  27. package/scaffold/skills/update-storyboard/update-storyboard-packages.sh +26 -0
  28. package/scaffold/skills/vitest/GENERATION.md +5 -0
  29. package/scaffold/skills/vitest/SKILL.md +52 -0
  30. package/scaffold/skills/vitest/references/advanced-environments.md +264 -0
  31. package/scaffold/skills/vitest/references/advanced-projects.md +300 -0
  32. package/scaffold/skills/vitest/references/advanced-type-testing.md +237 -0
  33. package/scaffold/skills/vitest/references/advanced-vi.md +249 -0
  34. package/scaffold/skills/vitest/references/core-cli.md +166 -0
  35. package/scaffold/skills/vitest/references/core-config.md +174 -0
  36. package/scaffold/skills/vitest/references/core-describe.md +193 -0
  37. package/scaffold/skills/vitest/references/core-expect.md +219 -0
  38. package/scaffold/skills/vitest/references/core-hooks.md +244 -0
  39. package/scaffold/skills/vitest/references/core-test-api.md +233 -0
  40. package/scaffold/skills/vitest/references/features-concurrency.md +250 -0
  41. package/scaffold/skills/vitest/references/features-context.md +238 -0
  42. package/scaffold/skills/vitest/references/features-coverage.md +207 -0
  43. package/scaffold/skills/vitest/references/features-filtering.md +211 -0
  44. package/scaffold/skills/vitest/references/features-mocking.md +265 -0
  45. package/scaffold/skills/vitest/references/features-snapshots.md +207 -0
  46. package/scaffold/skills/worktree/SKILL.md +93 -0
  47. package/scaffold/storyboard.config.json +44 -0
  48. package/src/canvas/Canvas.jsx +78 -0
  49. package/src/canvas/Draggable.jsx +235 -0
  50. package/src/canvas/index.d.ts +41 -0
  51. package/src/canvas/index.js +6 -0
  52. package/src/canvas/style.css +118 -0
  53. package/src/canvas/useResetCanvas.js +17 -0
  54. package/src/canvas/utils.js +136 -0
  55. package/src/core/assets/fonts/IoskeleyMono-Bold.woff2 +0 -0
  56. package/src/core/assets/fonts/IoskeleyMono-Italic.woff2 +0 -0
  57. package/src/core/assets/fonts/IoskeleyMono-Medium.woff2 +0 -0
  58. package/src/core/assets/fonts/IoskeleyMono-Regular.woff2 +0 -0
  59. package/src/core/assets/fonts/IoskeleyMono-SemiBold.woff2 +0 -0
  60. package/src/core/autosync/server.js +714 -0
  61. package/src/core/autosync/server.test.js +158 -0
  62. package/src/core/canvas/__tests__/agent-integration.test.js +596 -0
  63. package/src/core/canvas/__tests__/helpers/browser.js +95 -0
  64. package/src/core/canvas/__tests__/helpers/canvas-api.js +129 -0
  65. package/src/core/canvas/__tests__/helpers/perf.js +118 -0
  66. package/src/core/canvas/__tests__/helpers/setup.js +176 -0
  67. package/src/core/canvas/__tests__/helpers/tmux.js +130 -0
  68. package/src/core/canvas/__tests__/helpers/transcript.js +132 -0
  69. package/src/core/canvas/__tests__/terminal-integration.test.js +177 -0
  70. package/src/core/canvas/collision.js +292 -0
  71. package/src/core/canvas/collision.test.js +371 -0
  72. package/src/core/canvas/compact.js +83 -0
  73. package/src/core/canvas/deriveCanvasId.test.js +40 -0
  74. package/src/core/canvas/githubEmbeds.js +527 -0
  75. package/src/core/canvas/githubEmbeds.test.js +302 -0
  76. package/src/core/canvas/hot-pool.js +766 -0
  77. package/src/core/canvas/identity.js +107 -0
  78. package/src/core/canvas/identity.test.js +100 -0
  79. package/src/core/canvas/materializer.js +259 -0
  80. package/src/core/canvas/materializer.test.js +356 -0
  81. package/src/core/canvas/selectedWidgets.js +270 -0
  82. package/src/core/canvas/selectedWidgets.test.js +321 -0
  83. package/src/core/canvas/server.js +3134 -0
  84. package/src/core/canvas/server.test.js +379 -0
  85. package/src/core/canvas/terminal-config.js +330 -0
  86. package/src/core/canvas/terminal-registry.js +465 -0
  87. package/src/core/canvas/terminal-server.js +1436 -0
  88. package/src/core/canvas/writeGuard.js +53 -0
  89. package/src/core/cli/agent.js +85 -0
  90. package/src/core/cli/branch.js +386 -0
  91. package/src/core/cli/canvasAdd.js +241 -0
  92. package/src/core/cli/canvasBatch.js +98 -0
  93. package/src/core/cli/canvasBounds.js +160 -0
  94. package/src/core/cli/canvasRead.js +236 -0
  95. package/src/core/cli/canvasUpdate.js +179 -0
  96. package/src/core/cli/code.js +67 -0
  97. package/src/core/cli/compact.js +62 -0
  98. package/src/core/cli/create.js +674 -0
  99. package/src/core/cli/dev-helpers.js +53 -0
  100. package/src/core/cli/dev-helpers.test.js +53 -0
  101. package/src/core/cli/dev.js +430 -0
  102. package/src/core/cli/exit.js +38 -0
  103. package/src/core/cli/flags.js +174 -0
  104. package/src/core/cli/flags.test.js +155 -0
  105. package/src/core/cli/index.js +233 -0
  106. package/src/core/cli/intro.js +37 -0
  107. package/src/core/cli/proxy.js +319 -0
  108. package/src/core/cli/proxy.test.js +63 -0
  109. package/src/core/cli/schemas.js +223 -0
  110. package/src/core/cli/server.js +192 -0
  111. package/src/core/cli/serverUrl.js +61 -0
  112. package/src/core/cli/sessions.js +459 -0
  113. package/src/core/cli/setup.js +404 -0
  114. package/src/core/cli/terminal-commands.js +287 -0
  115. package/src/core/cli/terminal-messaging.js +231 -0
  116. package/src/core/cli/terminal-welcome.js +515 -0
  117. package/src/core/cli/updateVersion.js +124 -0
  118. package/src/core/comments/api.js +284 -0
  119. package/src/core/comments/api.test.js +282 -0
  120. package/src/core/comments/auth.js +151 -0
  121. package/src/core/comments/auth.test.js +167 -0
  122. package/src/core/comments/commentCache.js +109 -0
  123. package/src/core/comments/commentCache.test.js +48 -0
  124. package/src/core/comments/commentDrafts.js +68 -0
  125. package/src/core/comments/commentMode.js +63 -0
  126. package/src/core/comments/commentMode.test.js +90 -0
  127. package/src/core/comments/config.js +47 -0
  128. package/src/core/comments/config.test.js +77 -0
  129. package/src/core/comments/graphql.js +65 -0
  130. package/src/core/comments/graphql.test.js +95 -0
  131. package/src/core/comments/index.js +42 -0
  132. package/src/core/comments/metadata.js +52 -0
  133. package/src/core/comments/metadata.test.js +110 -0
  134. package/src/core/comments/queries.js +245 -0
  135. package/src/core/comments/ui/AuthModal.jsx +114 -0
  136. package/src/core/comments/ui/CommentOverlay.js +52 -0
  137. package/src/core/comments/ui/CommentWindow.jsx +329 -0
  138. package/src/core/comments/ui/CommentsDrawer.jsx +102 -0
  139. package/src/core/comments/ui/Composer.jsx +64 -0
  140. package/src/core/comments/ui/authModal.js +66 -0
  141. package/src/core/comments/ui/authModal.test.js +76 -0
  142. package/src/core/comments/ui/comment-cursor-dark.svg +1 -0
  143. package/src/core/comments/ui/comment-cursor.svg +1 -0
  144. package/src/core/comments/ui/comment-layout.css +142 -0
  145. package/src/core/comments/ui/commentWindow.js +121 -0
  146. package/src/core/comments/ui/comments.css +242 -0
  147. package/src/core/comments/ui/commentsDrawer.js +84 -0
  148. package/src/core/comments/ui/composer.js +136 -0
  149. package/src/core/comments/ui/index.js +14 -0
  150. package/src/core/comments/ui/mount.js +687 -0
  151. package/src/core/comments/ui/mount.test.js +336 -0
  152. package/src/core/data/dotPath.js +53 -0
  153. package/src/core/data/dotPath.test.js +114 -0
  154. package/src/core/data/loader.js +409 -0
  155. package/src/core/data/loader.test.js +599 -0
  156. package/src/core/data/viewfinder.js +363 -0
  157. package/src/core/data/viewfinder.test.js +456 -0
  158. package/src/core/devtools/devtools-consumer.js +28 -0
  159. package/src/core/devtools/devtools.js +144 -0
  160. package/src/core/devtools/devtools.test.js +75 -0
  161. package/src/core/devtools/sceneDebug.js +112 -0
  162. package/src/core/devtools/sceneDebug.test.js +141 -0
  163. package/src/core/index.js +124 -0
  164. package/src/core/inspector/fiberWalker.js +239 -0
  165. package/src/core/inspector/highlighter.js +275 -0
  166. package/src/core/inspector/mouseMode.js +259 -0
  167. package/src/core/lib/components/ui/alert/alert-action.jsx +11 -0
  168. package/src/core/lib/components/ui/alert/alert-description.jsx +11 -0
  169. package/src/core/lib/components/ui/alert/alert-title.jsx +11 -0
  170. package/src/core/lib/components/ui/alert/alert.jsx +25 -0
  171. package/src/core/lib/components/ui/alert/index.js +17 -0
  172. package/src/core/lib/components/ui/avatar/avatar-badge.jsx +22 -0
  173. package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +18 -0
  174. package/src/core/lib/components/ui/avatar/avatar-group-count.jsx +19 -0
  175. package/src/core/lib/components/ui/avatar/avatar-group.jsx +19 -0
  176. package/src/core/lib/components/ui/avatar/avatar-image.jsx +15 -0
  177. package/src/core/lib/components/ui/avatar/avatar.jsx +19 -0
  178. package/src/core/lib/components/ui/avatar/index.js +22 -0
  179. package/src/core/lib/components/ui/badge/badge.jsx +31 -0
  180. package/src/core/lib/components/ui/badge/index.js +2 -0
  181. package/src/core/lib/components/ui/button/button.jsx +100 -0
  182. package/src/core/lib/components/ui/button/index.js +12 -0
  183. package/src/core/lib/components/ui/card/card-action.jsx +11 -0
  184. package/src/core/lib/components/ui/card/card-content.jsx +11 -0
  185. package/src/core/lib/components/ui/card/card-description.jsx +11 -0
  186. package/src/core/lib/components/ui/card/card-footer.jsx +11 -0
  187. package/src/core/lib/components/ui/card/card-header.jsx +19 -0
  188. package/src/core/lib/components/ui/card/card-title.jsx +11 -0
  189. package/src/core/lib/components/ui/card/card.jsx +17 -0
  190. package/src/core/lib/components/ui/card/index.js +25 -0
  191. package/src/core/lib/components/ui/checkbox/checkbox.jsx +29 -0
  192. package/src/core/lib/components/ui/checkbox/index.js +6 -0
  193. package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +7 -0
  194. package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +7 -0
  195. package/src/core/lib/components/ui/collapsible/collapsible.jsx +7 -0
  196. package/src/core/lib/components/ui/collapsible/index.js +13 -0
  197. package/src/core/lib/components/ui/dialog/dialog-close.jsx +7 -0
  198. package/src/core/lib/components/ui/dialog/dialog-content.jsx +34 -0
  199. package/src/core/lib/components/ui/dialog/dialog-description.jsx +15 -0
  200. package/src/core/lib/components/ui/dialog/dialog-footer.jsx +23 -0
  201. package/src/core/lib/components/ui/dialog/dialog-header.jsx +11 -0
  202. package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +15 -0
  203. package/src/core/lib/components/ui/dialog/dialog-portal.jsx +4 -0
  204. package/src/core/lib/components/ui/dialog/dialog-title.jsx +15 -0
  205. package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +7 -0
  206. package/src/core/lib/components/ui/dialog/dialog.jsx +4 -0
  207. package/src/core/lib/components/ui/dialog/index.js +34 -0
  208. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +8 -0
  209. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +30 -0
  210. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +22 -0
  211. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +16 -0
  212. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +7 -0
  213. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +20 -0
  214. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-label.jsx +17 -0
  215. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +4 -0
  216. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +7 -0
  217. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +29 -0
  218. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +15 -0
  219. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.jsx +16 -0
  220. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +15 -0
  221. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +23 -0
  222. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +4 -0
  223. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +7 -0
  224. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +4 -0
  225. package/src/core/lib/components/ui/dropdown-menu/index.js +54 -0
  226. package/src/core/lib/components/ui/input/index.js +7 -0
  227. package/src/core/lib/components/ui/input/input.jsx +19 -0
  228. package/src/core/lib/components/ui/label/index.js +7 -0
  229. package/src/core/lib/components/ui/label/label.jsx +19 -0
  230. package/src/core/lib/components/ui/panel/index.js +24 -0
  231. package/src/core/lib/components/ui/panel/panel-body.jsx +11 -0
  232. package/src/core/lib/components/ui/panel/panel-close.jsx +16 -0
  233. package/src/core/lib/components/ui/panel/panel-content.jsx +29 -0
  234. package/src/core/lib/components/ui/panel/panel-footer.jsx +11 -0
  235. package/src/core/lib/components/ui/panel/panel-header.jsx +11 -0
  236. package/src/core/lib/components/ui/panel/panel-title.jsx +12 -0
  237. package/src/core/lib/components/ui/panel/panel.jsx +4 -0
  238. package/src/core/lib/components/ui/popover/index.js +28 -0
  239. package/src/core/lib/components/ui/popover/popover-close.jsx +7 -0
  240. package/src/core/lib/components/ui/popover/popover-content.jsx +22 -0
  241. package/src/core/lib/components/ui/popover/popover-description.jsx +11 -0
  242. package/src/core/lib/components/ui/popover/popover-header.jsx +11 -0
  243. package/src/core/lib/components/ui/popover/popover-portal.jsx +4 -0
  244. package/src/core/lib/components/ui/popover/popover-title.jsx +11 -0
  245. package/src/core/lib/components/ui/popover/popover-trigger.jsx +8 -0
  246. package/src/core/lib/components/ui/popover/popover.jsx +4 -0
  247. package/src/core/lib/components/ui/searchable-list.jsx +160 -0
  248. package/src/core/lib/components/ui/select/index.js +37 -0
  249. package/src/core/lib/components/ui/select/select-content.jsx +30 -0
  250. package/src/core/lib/components/ui/select/select-group-heading.jsx +17 -0
  251. package/src/core/lib/components/ui/select/select-group.jsx +15 -0
  252. package/src/core/lib/components/ui/select/select-item.jsx +26 -0
  253. package/src/core/lib/components/ui/select/select-label.jsx +11 -0
  254. package/src/core/lib/components/ui/select/select-portal.jsx +4 -0
  255. package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +18 -0
  256. package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +18 -0
  257. package/src/core/lib/components/ui/select/select-separator.jsx +15 -0
  258. package/src/core/lib/components/ui/select/select-trigger.jsx +25 -0
  259. package/src/core/lib/components/ui/select/select.jsx +4 -0
  260. package/src/core/lib/components/ui/separator/index.js +7 -0
  261. package/src/core/lib/components/ui/separator/separator.jsx +22 -0
  262. package/src/core/lib/components/ui/sheet/index.js +34 -0
  263. package/src/core/lib/components/ui/sheet/sheet-close.jsx +7 -0
  264. package/src/core/lib/components/ui/sheet/sheet-content.jsx +35 -0
  265. package/src/core/lib/components/ui/sheet/sheet-description.jsx +15 -0
  266. package/src/core/lib/components/ui/sheet/sheet-footer.jsx +11 -0
  267. package/src/core/lib/components/ui/sheet/sheet-header.jsx +11 -0
  268. package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +15 -0
  269. package/src/core/lib/components/ui/sheet/sheet-portal.jsx +4 -0
  270. package/src/core/lib/components/ui/sheet/sheet-title.jsx +15 -0
  271. package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +7 -0
  272. package/src/core/lib/components/ui/sheet/sheet.jsx +4 -0
  273. package/src/core/lib/components/ui/textarea/index.js +7 -0
  274. package/src/core/lib/components/ui/textarea/textarea.jsx +18 -0
  275. package/src/core/lib/components/ui/toggle/index.js +8 -0
  276. package/src/core/lib/components/ui/toggle/toggle.jsx +36 -0
  277. package/src/core/lib/components/ui/toggle-group/index.js +10 -0
  278. package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +29 -0
  279. package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +43 -0
  280. package/src/core/lib/components/ui/tooltip/index.js +3 -0
  281. package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +21 -0
  282. package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +23 -0
  283. package/src/core/lib/components/ui/tooltip/tooltip.jsx +11 -0
  284. package/src/core/lib/components/ui/trigger-button/index.js +6 -0
  285. package/src/core/lib/components/ui/trigger-button/trigger-button.css +38 -0
  286. package/src/core/lib/components/ui/trigger-button/trigger-button.jsx +63 -0
  287. package/src/core/lib/utils/index.js +6 -0
  288. package/src/core/logger/devLogger.js +238 -0
  289. package/src/core/logger/devLogger.test.js +193 -0
  290. package/src/core/modes/modes.css +98 -0
  291. package/src/core/modes/modes.js +492 -0
  292. package/src/core/modes/modes.test.js +562 -0
  293. package/src/core/mountStoryboardCore.js +478 -0
  294. package/src/core/rename-watcher/config.json +23 -0
  295. package/src/core/rename-watcher/watcher.js +531 -0
  296. package/src/core/scaffold.js +100 -0
  297. package/src/core/server/index.js +391 -0
  298. package/src/core/session/bodyClasses.js +128 -0
  299. package/src/core/session/bodyClasses.test.js +192 -0
  300. package/src/core/session/hashSubscribe.js +19 -0
  301. package/src/core/session/hashSubscribe.test.js +62 -0
  302. package/src/core/session/hideMode.js +424 -0
  303. package/src/core/session/hideMode.test.js +268 -0
  304. package/src/core/session/interceptHideParams.js +35 -0
  305. package/src/core/session/interceptHideParams.test.js +90 -0
  306. package/src/core/session/localStorage.js +134 -0
  307. package/src/core/session/localStorage.test.js +148 -0
  308. package/src/core/session/session.js +76 -0
  309. package/src/core/session/session.test.js +91 -0
  310. package/src/core/stores/canvasConfig.js +134 -0
  311. package/src/core/stores/canvasConfig.test.js +120 -0
  312. package/src/core/stores/commandActions.js +284 -0
  313. package/src/core/stores/commandPaletteConfig.js +31 -0
  314. package/src/core/stores/configSchema.js +232 -0
  315. package/src/core/stores/configSchema.test.js +72 -0
  316. package/src/core/stores/configStore.js +161 -0
  317. package/src/core/stores/customerModeConfig.js +30 -0
  318. package/src/core/stores/featureFlags.js +127 -0
  319. package/src/core/stores/paletteProviders.js +360 -0
  320. package/src/core/stores/paletteProviders.test.js +186 -0
  321. package/src/core/stores/plugins.js +40 -0
  322. package/src/core/stores/plugins.test.js +68 -0
  323. package/src/core/stores/recentArtifacts.js +68 -0
  324. package/src/core/stores/recentArtifacts.test.js +71 -0
  325. package/src/core/stores/sidePanelStore.ts +143 -0
  326. package/src/core/stores/themeStore.ts +291 -0
  327. package/src/core/stores/toolRegistry.js +227 -0
  328. package/src/core/stores/toolStateStore.js +183 -0
  329. package/src/core/stores/toolStateStore.test.js +220 -0
  330. package/src/core/stores/toolbarConfigStore.js +165 -0
  331. package/src/core/stores/uiConfig.js +64 -0
  332. package/src/core/stores/uiConfig.test.js +63 -0
  333. package/src/core/styles/tailwind.css +204 -0
  334. package/src/core/tools/handlers/autosync.js +12 -0
  335. package/src/core/tools/handlers/canvasAddWidget.js +11 -0
  336. package/src/core/tools/handlers/canvasAgents.js +20 -0
  337. package/src/core/tools/handlers/canvasToolbar.js +56 -0
  338. package/src/core/tools/handlers/commandPalette.js +9 -0
  339. package/src/core/tools/handlers/comments.js +16 -0
  340. package/src/core/tools/handlers/create.js +39 -0
  341. package/src/core/tools/handlers/devtools.js +122 -0
  342. package/src/core/tools/handlers/devtools.test.js +87 -0
  343. package/src/core/tools/handlers/featureFlags.js +21 -0
  344. package/src/core/tools/handlers/flows.js +68 -0
  345. package/src/core/tools/handlers/hideChrome.js +9 -0
  346. package/src/core/tools/handlers/hideToolbars.js +25 -0
  347. package/src/core/tools/handlers/inspector.js +19 -0
  348. package/src/core/tools/handlers/paletteTheme.js +35 -0
  349. package/src/core/tools/handlers/theme.js +9 -0
  350. package/src/core/tools/registry.js +26 -0
  351. package/src/core/tools/surfaces/canvasToolbar.js +10 -0
  352. package/src/core/tools/surfaces/commandList.js +10 -0
  353. package/src/core/tools/surfaces/mainToolbar.js +11 -0
  354. package/src/core/tools/surfaces/registry.js +19 -0
  355. package/src/core/ui/ActionMenuButton.jsx +114 -0
  356. package/src/core/ui/AutosyncMenuButton.css +67 -0
  357. package/src/core/ui/AutosyncMenuButton.jsx +242 -0
  358. package/src/core/ui/BranchSelect.jsx +29 -0
  359. package/src/core/ui/BranchSelect.module.css +30 -0
  360. package/src/core/ui/CanvasAgentsMenu.jsx +89 -0
  361. package/src/core/ui/CanvasCreateMenu.jsx +611 -0
  362. package/src/core/ui/CanvasSnap.css +27 -0
  363. package/src/core/ui/CanvasSnap.jsx +51 -0
  364. package/src/core/ui/CanvasUndoRedo.css +36 -0
  365. package/src/core/ui/CanvasUndoRedo.jsx +62 -0
  366. package/src/core/ui/CanvasZoomControl.css +53 -0
  367. package/src/core/ui/CanvasZoomControl.jsx +49 -0
  368. package/src/core/ui/CanvasZoomToFit.css +18 -0
  369. package/src/core/ui/CanvasZoomToFit.jsx +26 -0
  370. package/src/core/ui/CommandMenu.css +8 -0
  371. package/src/core/ui/CommandMenu.jsx +287 -0
  372. package/src/core/ui/CommandPalette.jsx +35 -0
  373. package/src/core/ui/CommandPaletteTrigger.jsx +25 -0
  374. package/src/core/ui/CommentsMenuButton.jsx +40 -0
  375. package/src/core/ui/CoreUIBar.css +47 -0
  376. package/src/core/ui/CoreUIBar.jsx +905 -0
  377. package/src/core/ui/CreateMenuButton.jsx +117 -0
  378. package/src/core/ui/HideChromeTrigger.jsx +48 -0
  379. package/src/core/ui/Icon.jsx +279 -0
  380. package/src/core/ui/InspectorPanel.css +109 -0
  381. package/src/core/ui/InspectorPanel.jsx +632 -0
  382. package/src/core/ui/PwaInstallBanner.css +42 -0
  383. package/src/core/ui/PwaInstallBanner.jsx +124 -0
  384. package/src/core/ui/SidePanel.jsx +261 -0
  385. package/src/core/ui/ThemeMenuButton.jsx +139 -0
  386. package/src/core/ui/core-ui-colors.css +129 -0
  387. package/src/core/ui/design-modes.ts +7 -0
  388. package/src/core/ui/sidepanel.css +301 -0
  389. package/src/core/ui/viewfinder.ts +7 -0
  390. package/src/core/ui-entry.js +30 -0
  391. package/src/core/utils/fuzzySearch.js +117 -0
  392. package/src/core/utils/fuzzySearch.test.js +119 -0
  393. package/src/core/utils/mobileViewport.js +57 -0
  394. package/src/core/utils/mobileViewport.test.js +68 -0
  395. package/src/core/utils/prodMode.js +38 -0
  396. package/src/core/utils/smoothCorners.js +20 -0
  397. package/src/core/vite/docs-handler.js +155 -0
  398. package/src/core/vite/server-plugin.js +797 -0
  399. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +260 -0
  400. package/src/core/workshop/features/createCanvas/index.js +14 -0
  401. package/src/core/workshop/features/createFlow/CreateFlowForm.jsx +334 -0
  402. package/src/core/workshop/features/createFlow/index.js +19 -0
  403. package/src/core/workshop/features/createFlow/server.js +663 -0
  404. package/src/core/workshop/features/createPage/CreatePageForm.jsx +304 -0
  405. package/src/core/workshop/features/createPage/index.js +11 -0
  406. package/src/core/workshop/features/createPrototype/CreatePrototypeForm.jsx +289 -0
  407. package/src/core/workshop/features/createPrototype/index.js +19 -0
  408. package/src/core/workshop/features/createPrototype/server.js +433 -0
  409. package/src/core/workshop/features/createStory/CreateStoryForm.jsx +208 -0
  410. package/src/core/workshop/features/createStory/index.js +14 -0
  411. package/src/core/workshop/features/registry-server.js +22 -0
  412. package/src/core/workshop/features/registry.js +28 -0
  413. package/src/core/workshop/features/templateIndex.js +155 -0
  414. package/src/core/workshop/ui/WorkshopPanel.jsx +98 -0
  415. package/src/core/workshop/ui/mount.ts +6 -0
  416. package/src/core/worktree/port.js +268 -0
  417. package/src/core/worktree/port.test.js +222 -0
  418. package/src/core/worktree/serverRegistry.js +120 -0
  419. package/src/internals/AuthModal/AuthModal.jsx +132 -0
  420. package/src/internals/AuthModal/AuthModal.module.css +221 -0
  421. package/src/internals/BranchBar/BranchBar.jsx +87 -0
  422. package/src/internals/BranchBar/BranchBar.module.css +247 -0
  423. package/src/internals/BranchBar/useBranches.js +93 -0
  424. package/src/internals/BranchBar/useBranches.test.js +68 -0
  425. package/src/internals/CommandPalette/CommandPalette.jsx +1361 -0
  426. package/src/internals/CommandPalette/CreateDialog.jsx +219 -0
  427. package/src/internals/CommandPalette/command-palette.css +180 -0
  428. package/src/internals/FlowError.module.css +30 -0
  429. package/src/internals/Icon.jsx +279 -0
  430. package/src/internals/StoryboardContext.js +3 -0
  431. package/src/internals/Viewfinder.jsx +1479 -0
  432. package/src/internals/Viewfinder.module.css +1540 -0
  433. package/src/internals/Workspace.jsx +7 -0
  434. package/src/internals/__mocks__/virtual-storyboard-data-index.js +4 -0
  435. package/src/internals/canvas/CanvasControls.jsx +112 -0
  436. package/src/internals/canvas/CanvasControls.module.css +135 -0
  437. package/src/internals/canvas/CanvasPage.bridge.test.jsx +387 -0
  438. package/src/internals/canvas/CanvasPage.dragdrop.test.jsx +350 -0
  439. package/src/internals/canvas/CanvasPage.jsx +3092 -0
  440. package/src/internals/canvas/CanvasPage.module.css +187 -0
  441. package/src/internals/canvas/CanvasPage.multiselect.test.jsx +358 -0
  442. package/src/internals/canvas/CanvasToolbar.jsx +73 -0
  443. package/src/internals/canvas/CanvasToolbar.module.css +92 -0
  444. package/src/internals/canvas/ComponentErrorBoundary.jsx +50 -0
  445. package/src/internals/canvas/ConnectorLayer.jsx +208 -0
  446. package/src/internals/canvas/ConnectorLayer.module.css +129 -0
  447. package/src/internals/canvas/MarqueeOverlay.jsx +20 -0
  448. package/src/internals/canvas/PageSelector.jsx +587 -0
  449. package/src/internals/canvas/PageSelector.module.css +261 -0
  450. package/src/internals/canvas/PageSelector.test.jsx +113 -0
  451. package/src/internals/canvas/WebGLContextPool.jsx +292 -0
  452. package/src/internals/canvas/WebGLContextPool.test.jsx +165 -0
  453. package/src/internals/canvas/canvasApi.js +164 -0
  454. package/src/internals/canvas/canvasReloadGuard.js +37 -0
  455. package/src/internals/canvas/canvasReloadGuard.test.js +27 -0
  456. package/src/internals/canvas/canvasTheme.js +118 -0
  457. package/src/internals/canvas/componentIsolate.jsx +165 -0
  458. package/src/internals/canvas/componentSetIsolate.jsx +257 -0
  459. package/src/internals/canvas/computeCanvasBounds.test.js +121 -0
  460. package/src/internals/canvas/connectorGeometry.js +132 -0
  461. package/src/internals/canvas/hotPoolDevLogs.js +25 -0
  462. package/src/internals/canvas/textSelection.js +10 -0
  463. package/src/internals/canvas/textSelection.test.js +26 -0
  464. package/src/internals/canvas/useCanvas.js +126 -0
  465. package/src/internals/canvas/useCanvas.test.js +26 -0
  466. package/src/internals/canvas/useMarqueeSelect.js +213 -0
  467. package/src/internals/canvas/useMarqueeSelect.test.js +78 -0
  468. package/src/internals/canvas/useUndoRedo.js +86 -0
  469. package/src/internals/canvas/useUndoRedo.test.js +231 -0
  470. package/src/internals/canvas/widgets/CodePenEmbed.jsx +293 -0
  471. package/src/internals/canvas/widgets/CodePenEmbed.module.css +161 -0
  472. package/src/internals/canvas/widgets/ComponentSetWidget.jsx +2 -0
  473. package/src/internals/canvas/widgets/ComponentSetWidget.module.css +89 -0
  474. package/src/internals/canvas/widgets/ComponentWidget.jsx +14 -0
  475. package/src/internals/canvas/widgets/ComponentWidget.module.css +0 -0
  476. package/src/internals/canvas/widgets/CropOverlay.jsx +179 -0
  477. package/src/internals/canvas/widgets/CropOverlay.module.css +154 -0
  478. package/src/internals/canvas/widgets/ExpandedPane.jsx +474 -0
  479. package/src/internals/canvas/widgets/ExpandedPane.module.css +179 -0
  480. package/src/internals/canvas/widgets/ExpandedPane.test.jsx +240 -0
  481. package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  482. package/src/internals/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  483. package/src/internals/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  484. package/src/internals/canvas/widgets/FigmaEmbed.jsx +296 -0
  485. package/src/internals/canvas/widgets/FigmaEmbed.module.css +222 -0
  486. package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +151 -0
  487. package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +83 -0
  488. package/src/internals/canvas/widgets/ImageWidget.jsx +287 -0
  489. package/src/internals/canvas/widgets/ImageWidget.module.css +81 -0
  490. package/src/internals/canvas/widgets/LinkPreview.jsx +439 -0
  491. package/src/internals/canvas/widgets/LinkPreview.module.css +585 -0
  492. package/src/internals/canvas/widgets/LinkPreview.test.jsx +193 -0
  493. package/src/internals/canvas/widgets/MarkdownBlock.jsx +354 -0
  494. package/src/internals/canvas/widgets/MarkdownBlock.module.css +377 -0
  495. package/src/internals/canvas/widgets/MarkdownBlock.test.jsx +92 -0
  496. package/src/internals/canvas/widgets/PromptWidget.jsx +428 -0
  497. package/src/internals/canvas/widgets/PromptWidget.module.css +273 -0
  498. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +463 -0
  499. package/src/internals/canvas/widgets/PrototypeEmbed.module.css +579 -0
  500. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +10 -0
  501. package/src/internals/canvas/widgets/ResizeHandle.jsx +67 -0
  502. package/src/internals/canvas/widgets/ResizeHandle.module.css +29 -0
  503. package/src/internals/canvas/widgets/StickyNote.jsx +92 -0
  504. package/src/internals/canvas/widgets/StickyNote.module.css +70 -0
  505. package/src/internals/canvas/widgets/StickyNote.test.jsx +116 -0
  506. package/src/internals/canvas/widgets/StorySetWidget.jsx +208 -0
  507. package/src/internals/canvas/widgets/StorySetWidget.module.css +89 -0
  508. package/src/internals/canvas/widgets/StoryWidget.jsx +334 -0
  509. package/src/internals/canvas/widgets/StoryWidget.module.css +211 -0
  510. package/src/internals/canvas/widgets/TerminalReadWidget.jsx +146 -0
  511. package/src/internals/canvas/widgets/TerminalReadWidget.module.css +94 -0
  512. package/src/internals/canvas/widgets/TerminalWidget.jsx +704 -0
  513. package/src/internals/canvas/widgets/TerminalWidget.module.css +444 -0
  514. package/src/internals/canvas/widgets/TilesWidget.jsx +300 -0
  515. package/src/internals/canvas/widgets/TilesWidget.module.css +133 -0
  516. package/src/internals/canvas/widgets/WidgetChrome.jsx +580 -0
  517. package/src/internals/canvas/widgets/WidgetChrome.module.css +421 -0
  518. package/src/internals/canvas/widgets/WidgetWrapper.jsx +15 -0
  519. package/src/internals/canvas/widgets/WidgetWrapper.module.css +25 -0
  520. package/src/internals/canvas/widgets/codepenUrl.js +75 -0
  521. package/src/internals/canvas/widgets/codepenUrl.test.js +76 -0
  522. package/src/internals/canvas/widgets/embedInteraction.test.jsx +173 -0
  523. package/src/internals/canvas/widgets/embedOverlay.module.css +35 -0
  524. package/src/internals/canvas/widgets/embedTheme.js +148 -0
  525. package/src/internals/canvas/widgets/expandUtils.js +559 -0
  526. package/src/internals/canvas/widgets/expandUtils.test.js +155 -0
  527. package/src/internals/canvas/widgets/figmaUrl.js +118 -0
  528. package/src/internals/canvas/widgets/figmaUrl.test.js +139 -0
  529. package/src/internals/canvas/widgets/githubUrl.js +82 -0
  530. package/src/internals/canvas/widgets/githubUrl.test.js +74 -0
  531. package/src/internals/canvas/widgets/iframeDevLogs.js +49 -0
  532. package/src/internals/canvas/widgets/iframeDevLogs.test.jsx +81 -0
  533. package/src/internals/canvas/widgets/index.js +42 -0
  534. package/src/internals/canvas/widgets/pasteRules.js +295 -0
  535. package/src/internals/canvas/widgets/pasteRules.test.js +474 -0
  536. package/src/internals/canvas/widgets/snapshotDisplay.test.jsx +211 -0
  537. package/src/internals/canvas/widgets/tilePool.js +23 -0
  538. package/src/internals/canvas/widgets/tiles/diagonal-bl.png +0 -0
  539. package/src/internals/canvas/widgets/tiles/diagonal-br.png +0 -0
  540. package/src/internals/canvas/widgets/tiles/diagonal-tl.png +0 -0
  541. package/src/internals/canvas/widgets/tiles/leaf.png +0 -0
  542. package/src/internals/canvas/widgets/tiles/quarter-tl.png +0 -0
  543. package/src/internals/canvas/widgets/tiles/quarter-tr.png +0 -0
  544. package/src/internals/canvas/widgets/tiles/solid-a.png +0 -0
  545. package/src/internals/canvas/widgets/tiles/solid-b.png +0 -0
  546. package/src/internals/canvas/widgets/widgetConfig.js +291 -0
  547. package/src/internals/canvas/widgets/widgetConfig.test.js +68 -0
  548. package/src/internals/canvas/widgets/widgetIcons.jsx +190 -0
  549. package/src/internals/canvas/widgets/widgetProps.js +133 -0
  550. package/src/internals/context/FormContext.js +13 -0
  551. package/src/internals/context/FormContext.test.js +48 -0
  552. package/src/internals/context.jsx +481 -0
  553. package/src/internals/context.test.jsx +296 -0
  554. package/src/internals/hashPreserver.js +73 -0
  555. package/src/internals/hashPreserver.test.js +107 -0
  556. package/src/internals/hooks/useConfig.js +14 -0
  557. package/src/internals/hooks/useFeatureFlag.js +14 -0
  558. package/src/internals/hooks/useFlows.js +50 -0
  559. package/src/internals/hooks/useFlows.test.js +134 -0
  560. package/src/internals/hooks/useHideMode.js +31 -0
  561. package/src/internals/hooks/useHideMode.test.js +43 -0
  562. package/src/internals/hooks/useLocalStorage.js +57 -0
  563. package/src/internals/hooks/useLocalStorage.test.js +75 -0
  564. package/src/internals/hooks/useMode.js +43 -0
  565. package/src/internals/hooks/useObject.js +101 -0
  566. package/src/internals/hooks/useObject.test.js +74 -0
  567. package/src/internals/hooks/useOverride.js +84 -0
  568. package/src/internals/hooks/useOverride.test.js +71 -0
  569. package/src/internals/hooks/usePrototypeReloadGuard.js +64 -0
  570. package/src/internals/hooks/useRecord.js +158 -0
  571. package/src/internals/hooks/useRecord.test.js +221 -0
  572. package/src/internals/hooks/useScene.js +38 -0
  573. package/src/internals/hooks/useScene.test.js +66 -0
  574. package/src/internals/hooks/useSceneData.js +108 -0
  575. package/src/internals/hooks/useSceneData.test.js +136 -0
  576. package/src/internals/hooks/useSession.js +4 -0
  577. package/src/internals/hooks/useSession.test.js +8 -0
  578. package/src/internals/hooks/useThemeState.js +61 -0
  579. package/src/internals/hooks/useThemeState.test.js +66 -0
  580. package/src/internals/hooks/useUndoRedo.js +28 -0
  581. package/src/internals/hooks/useUndoRedo.test.js +64 -0
  582. package/src/internals/index.js +58 -0
  583. package/src/internals/story/ComponentSetPage.jsx +198 -0
  584. package/src/internals/story/ComponentSetPage.module.css +129 -0
  585. package/src/internals/story/StoryPage.jsx +147 -0
  586. package/src/internals/story/StoryPage.module.css +18 -0
  587. package/src/internals/test-utils.js +45 -0
  588. package/src/internals/vite/data-plugin.js +1508 -0
  589. package/src/internals/vite/data-plugin.test.js +1223 -0
  590. package/src/test-utils.js +44 -0
  591. package/toolbar.config.json +271 -0
  592. package/widgets.config.json +1537 -0
@@ -0,0 +1,766 @@
1
+ /**
2
+ * Hot Pool — pre-warms tmux sessions for instant agent execution.
3
+ *
4
+ * Maintains typed pools of ready-to-use sessions: bare tmux shells for
5
+ * terminal/prompt widgets, and fully-booted agent sessions (Copilot,
6
+ * Claude, Codex) with the agent CLI running and ready.
7
+ *
8
+ * ## Pool Types
9
+ *
10
+ * The HotPoolManager creates one HotPool per type:
11
+ * - **terminal** — bare tmux shell (terminal widgets without an agent)
12
+ * - **prompt** — bare tmux shell (prompt widgets)
13
+ * - **copilot** — tmux + `copilot --agent terminal-agent` running & ready
14
+ * - **claude** — tmux + `claude --agent terminal-agent ...` running & ready
15
+ * - **codex** — tmux + `codex --full-auto` running & ready
16
+ *
17
+ * ## Load Balancer (per-pool)
18
+ *
19
+ * Each pool has two operating levels:
20
+ * - **pool_size** — baseline warm sessions at rest (default: 1)
21
+ * - **max_pool_size** — surge capacity when under pressure (default: 3)
22
+ *
23
+ * Scale-up: When an acquire() drains the queue to 0, the pool enters
24
+ * "pressure" mode and backfills to max_pool_size.
25
+ * Scale-down: After cooldown minutes with no acquisitions, the pool scales
26
+ * back to pool_size by killing excess warm sessions.
27
+ *
28
+ * ## Configuration (storyboard.config.json → hotPool)
29
+ *
30
+ * hotPool.enabled — enable/disable all pools (default: true)
31
+ * hotPool.verbose — log to Vite terminal (default: false)
32
+ * hotPool.default_pool_size — default baseline per pool (default: 1)
33
+ * hotPool.default_max_pool_size — default surge cap per pool (default: 3)
34
+ * hotPool.load_balancer — enable auto-scaling (default: true)
35
+ * hotPool.load_balancer_cooldown_mins — minutes idle before scale-down (default: 10)
36
+ * hotPool.pools.terminal — per-pool overrides for terminal { pool_size, max_pool_size }
37
+ * hotPool.pools.prompt — per-pool overrides for prompt
38
+ * hotPool.pools.copilot — per-pool overrides for copilot agent
39
+ * hotPool.pools.claude — per-pool overrides for claude agent
40
+ * hotPool.pools.codex — per-pool overrides for codex agent
41
+ *
42
+ * Browser devlogs are sent via the Vite HMR channel and only appear
43
+ * when the "Dev logs" toggle is on in Storyboard DevTools.
44
+ */
45
+
46
+ import { execSync } from 'node:child_process'
47
+ import { writeFileSync, existsSync, unlinkSync, mkdirSync } from 'node:fs'
48
+ import { join } from 'node:path'
49
+ import { devLog } from '../logger/devLogger.js'
50
+
51
+ /**
52
+ * @typedef {Object} WarmSession
53
+ * @property {string} id
54
+ * @property {string} poolId — which pool this session belongs to
55
+ * @property {string} tmuxName — the tmux session name (pool-prefixed)
56
+ * @property {number} createdAt
57
+ * @property {'warming'|'ready'|'acquired'|'consumed'|'dead'} state
58
+ */
59
+
60
+ const DEFAULT_POOL_SIZE = 1
61
+ const DEFAULT_MAX_POOL_SIZE = 3
62
+ const DEFAULT_COOLDOWN_MINS = 10
63
+ const HEALTH_CHECK_INTERVAL_MS = 30_000
64
+ const AGENT_READINESS_TIMEOUT_MS = 60_000
65
+ const AGENT_READINESS_POLL_MS = 2_000
66
+
67
+ export class HotPool {
68
+ /** @type {WarmSession[]} */
69
+ #queue = []
70
+ /** @type {Map<string, WarmSession>} */
71
+ #acquired = new Map()
72
+ #root = ''
73
+ #poolId = 'terminal'
74
+ #poolSize = DEFAULT_POOL_SIZE
75
+ #maxPoolSize = DEFAULT_MAX_POOL_SIZE
76
+ #cooldownMs = DEFAULT_COOLDOWN_MINS * 60_000
77
+ #enabled = true
78
+ #verbose = false
79
+ #loadBalancer = true
80
+ #filling = false
81
+ #healthTimer = null
82
+ #prereqsAvailable = null
83
+ #wsSend = null
84
+ #webglReadySlots = 0
85
+
86
+ // Agent config (null for bare shell pools)
87
+ #agentConfig = null
88
+
89
+ // Load balancer state
90
+ #pressured = false
91
+ #cooldownTimer = null
92
+
93
+ /**
94
+ * @param {Object} opts
95
+ * @param {string} opts.root — project root directory
96
+ * @param {string} opts.poolId — pool identifier (e.g. 'terminal', 'copilot', 'prompt')
97
+ * @param {Object} [opts.config] — pool-specific config (pool_size, max_pool_size, etc.)
98
+ * @param {Object} [opts.agentConfig] — agent config from canvas.agents (startupCommand, readinessSignal, postStartup)
99
+ * @param {Function} [opts.wsSend] — Vite server.ws.send for browser devlog events
100
+ */
101
+ constructor({ root, poolId = 'terminal', config = {}, agentConfig = null, wsSend = null }) {
102
+ this.#root = root
103
+ this.#poolId = poolId
104
+ this.#poolSize = Math.max(0, config.pool_size ?? DEFAULT_POOL_SIZE)
105
+ this.#maxPoolSize = Math.max(this.#poolSize, config.max_pool_size ?? DEFAULT_MAX_POOL_SIZE)
106
+ this.#cooldownMs = (config.load_balancer_cooldown_mins ?? DEFAULT_COOLDOWN_MINS) * 60_000
107
+ this.#enabled = config.enabled !== false
108
+ this.#verbose = !!config.verbose
109
+ this.#loadBalancer = config.load_balancer !== false
110
+ this.#wsSend = wsSend
111
+ this.#agentConfig = agentConfig
112
+ this.#webglReadySlots = Math.max(0, config.webgl_ready_slots ?? 0)
113
+ }
114
+
115
+ get poolId() { return this.#poolId }
116
+ get isAgentPool() { return !!this.#agentConfig }
117
+
118
+ #termLog(...args) {
119
+ if (this.#verbose) console.log(`[hot-pool:${this.#poolId}]`, ...args)
120
+ }
121
+
122
+ #browserLog(message) {
123
+ if (this.#wsSend) {
124
+ this.#wsSend({
125
+ type: 'custom',
126
+ event: 'storyboard:hot-pool-log',
127
+ data: { poolId: this.#poolId, message, timestamp: Date.now() },
128
+ })
129
+ }
130
+ }
131
+
132
+ #log(message) {
133
+ this.#termLog(message)
134
+ this.#browserLog(message)
135
+ }
136
+
137
+ /** Current fill target — pool_size normally, max_pool_size under pressure (if load balancer on). */
138
+ get #fillTarget() {
139
+ return (this.#loadBalancer && this.#pressured) ? this.#maxPoolSize : this.#poolSize
140
+ }
141
+
142
+ async start() {
143
+ if (!this.#enabled || this.#poolSize === 0) {
144
+ this.#log('pool disabled or pool_size=0, skipping start')
145
+ return
146
+ }
147
+
148
+ this.#prereqsAvailable = await this.#checkPrereqs()
149
+ if (!this.#prereqsAvailable) {
150
+ this.#log('prerequisites not met — pool disabled')
151
+ return
152
+ }
153
+
154
+ this.#log(`✦ STARTING (pool_size=${this.#poolSize}, max_pool_size=${this.#maxPoolSize}, cooldown=${this.#cooldownMs / 60_000}min${this.#agentConfig ? ', agent=' + this.#agentConfig.startupCommand : ''})`)
155
+ await this.#fill()
156
+ this.#log(`✦ READY — ${this.#queue.filter(s => s.state === 'ready').length} warm sessions`)
157
+
158
+ this.#healthTimer = setInterval(() => this.#healthCheck(), HEALTH_CHECK_INTERVAL_MS)
159
+ }
160
+
161
+ stop() {
162
+ if (this.#healthTimer) { clearInterval(this.#healthTimer); this.#healthTimer = null }
163
+ if (this.#cooldownTimer) { clearTimeout(this.#cooldownTimer); this.#cooldownTimer = null }
164
+ for (const session of this.#queue) this.#killSession(session)
165
+ this.#queue = []
166
+ this.#pressured = false
167
+ this.#log('■ STOPPED — all sessions killed')
168
+ }
169
+
170
+ acquire() {
171
+ if (!this.#enabled || this.#queue.length === 0) {
172
+ this.#log(`→ ACQUIRE — pool ${!this.#enabled ? 'disabled' : 'empty'}, returning null`)
173
+ return null
174
+ }
175
+
176
+ const idx = this.#queue.findIndex(s => s.state === 'ready')
177
+ if (idx === -1) {
178
+ this.#log(`→ ACQUIRE — ${this.#queue.length} in queue but none ready, returning null`)
179
+ return null
180
+ }
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
+
185
+ const session = this.#queue.splice(idx, 1)[0]
186
+ session.state = 'acquired'
187
+ session.webglReady = webglReady
188
+ this.#acquired.set(session.id, session)
189
+ const age = ((Date.now() - session.createdAt) / 1000).toFixed(1)
190
+ const readyCount = this.#queue.filter(s => s.state === 'ready').length
191
+
192
+ // Scale-up: queue drained to 0 ready → enter pressure mode
193
+ if (readyCount === 0 && !this.#pressured) {
194
+ this.#pressured = true
195
+ this.#log(`→ ACQUIRED ${session.id} tmux=${session.tmuxName} (age: ${age}s, webglReady: ${webglReady}) — ⚡ PRESSURE ON (scaling to max_pool_size=${this.#maxPoolSize})`)
196
+ this.#resetCooldown()
197
+ } else {
198
+ this.#log(`→ ACQUIRED ${session.id} tmux=${session.tmuxName} (age: ${age}s, webglReady: ${webglReady}, queue: ${readyCount}/${this.#fillTarget})`)
199
+ this.#resetCooldown()
200
+ }
201
+
202
+ this.#fill().catch(() => {})
203
+ return session
204
+ }
205
+
206
+ /**
207
+ * Consume a previously acquired session — transfers ownership out of the pool permanently.
208
+ * Use this when the session becomes a widget-owned canonical tmux session.
209
+ */
210
+ consume(sessionId) {
211
+ const session = this.#acquired.get(sessionId)
212
+ if (!session) return
213
+ session.state = 'consumed'
214
+ this.#acquired.delete(sessionId)
215
+ this.#log(`⊘ CONSUMED ${sessionId} (active: ${this.#acquired.size})`)
216
+ }
217
+
218
+ release(sessionId) {
219
+ const session = this.#acquired.get(sessionId)
220
+ if (!session) return
221
+ this.#acquired.delete(sessionId)
222
+ // Return to pool if still alive, otherwise kill
223
+ if (this.#tmuxSessionExists(session.tmuxName)) {
224
+ session.state = 'ready'
225
+ this.#queue.push(session)
226
+ this.#log(`← RELEASED ${sessionId} back to queue (queue: ${this.#queue.length}/${this.#fillTarget})`)
227
+ } else {
228
+ session.state = 'dead'
229
+ this.#log(`← RELEASED ${sessionId} but tmux gone, discarded`)
230
+ }
231
+ }
232
+
233
+ status() {
234
+ return {
235
+ poolId: this.#poolId,
236
+ enabled: this.#enabled,
237
+ prereqsAvailable: this.#prereqsAvailable,
238
+ isAgentPool: this.isAgentPool,
239
+ pressured: this.#pressured,
240
+ config: {
241
+ pool_size: this.#poolSize,
242
+ max_pool_size: this.#maxPoolSize,
243
+ load_balancer: this.#loadBalancer,
244
+ load_balancer_cooldown_mins: this.#cooldownMs / 60_000,
245
+ verbose: this.#verbose,
246
+ webgl_ready_slots: this.#webglReadySlots,
247
+ },
248
+ agentConfig: this.#agentConfig ? {
249
+ startupCommand: this.#agentConfig.startupCommand,
250
+ readinessSignal: this.#agentConfig.readinessSignal,
251
+ } : null,
252
+ queue: this.#queue.map(s => ({
253
+ id: s.id,
254
+ state: s.state,
255
+ age: Date.now() - s.createdAt,
256
+ })),
257
+ acquired: this.#acquired.size,
258
+ ready: this.#queue.filter(s => s.state === 'ready').length,
259
+ fillTarget: this.#fillTarget,
260
+ }
261
+ }
262
+
263
+ reconfigure(config) {
264
+ if (config.max_pool_size !== undefined) this.#maxPoolSize = Math.max(0, config.max_pool_size)
265
+ if (config.load_balancer_cooldown_mins !== undefined) this.#cooldownMs = config.load_balancer_cooldown_mins * 60_000
266
+ if (config.load_balancer !== undefined) this.#loadBalancer = !!config.load_balancer
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)
269
+ const newEnabled = config.enabled !== false
270
+ if (config.verbose !== undefined) this.#verbose = !!config.verbose
271
+
272
+ this.#log(`⚙ RECONFIG pool_size=${newSize} max=${this.#maxPoolSize} cooldown=${this.#cooldownMs / 60_000}min enabled=${newEnabled}`)
273
+
274
+ const sizeChanged = newSize !== this.#poolSize
275
+ this.#poolSize = newSize
276
+
277
+ if (!newEnabled && this.#enabled) { this.stop(); this.#enabled = false; return }
278
+ this.#enabled = newEnabled
279
+
280
+ if (sizeChanged && this.#enabled) {
281
+ // Trim if over new target
282
+ while (this.#queue.length > this.#fillTarget) {
283
+ const excess = this.#queue.pop()
284
+ if (excess) this.#killSession(excess)
285
+ }
286
+ this.#fill().catch(() => {})
287
+ }
288
+ }
289
+
290
+ // ── Load balancer ───────────────────────────────────────────────
291
+
292
+ /** Reset the cooldown timer — called on every acquire. */
293
+ #resetCooldown() {
294
+ if (this.#cooldownTimer) clearTimeout(this.#cooldownTimer)
295
+ this.#cooldownTimer = setTimeout(() => this.#scaleDown(), this.#cooldownMs)
296
+ }
297
+
298
+ /** Scale down from pressure mode back to pool_size. */
299
+ #scaleDown() {
300
+ if (!this.#pressured) return
301
+ this.#pressured = false
302
+ this.#cooldownTimer = null
303
+
304
+ const excess = this.#queue.length - this.#poolSize
305
+ if (excess > 0) {
306
+ let killed = 0
307
+ while (this.#queue.length > this.#poolSize) {
308
+ const session = this.#queue.pop()
309
+ if (session) { this.#killSession(session); killed++ }
310
+ }
311
+ this.#log(`↓ SCALE DOWN — pressure off, killed ${killed} excess (queue: ${this.#queue.length}/${this.#poolSize})`)
312
+ } else {
313
+ this.#log(`↓ SCALE DOWN — pressure off (queue already at ${this.#queue.length}/${this.#poolSize})`)
314
+ }
315
+ }
316
+
317
+ // ── Internal ────────────────────────────────────────────────────
318
+
319
+ async #fill() {
320
+ if (this.#filling || !this.#enabled) return
321
+ this.#filling = true
322
+ const target = this.#fillTarget
323
+ this.#log(`⟳ BACKFILL starting (queue: ${this.#queue.length}/${target}${this.#pressured ? ' ⚡' : ''})`)
324
+
325
+ try {
326
+ let spawned = 0
327
+ while (this.#queue.length < target) {
328
+ const total = this.#queue.length + this.#acquired.size
329
+ if (total >= this.#maxPoolSize) {
330
+ this.#log(`⟳ BACKFILL hit max_pool_size cap (${total}/${this.#maxPoolSize})`)
331
+ break
332
+ }
333
+ const session = await this.#spawnWarmSession()
334
+ if (session) {
335
+ this.#queue.push(session)
336
+ spawned++
337
+ this.#log(`⟳ BACKFILL warmed ${session.id} (queue: ${this.#queue.length}/${target})`)
338
+ } else {
339
+ this.#log('⟳ BACKFILL spawn failed, stopping')
340
+ break
341
+ }
342
+ }
343
+ this.#log(`⟳ BACKFILL done — spawned ${spawned}, queue: ${this.#queue.length}/${target}`)
344
+ } finally {
345
+ this.#filling = false
346
+ }
347
+ }
348
+
349
+ async #spawnWarmSession() {
350
+ const id = `${this.#poolId}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
351
+ const tmuxName = `sb-pool-${id}`
352
+ this.#log(`⊕ SPAWN starting ${id} (tmux: ${tmuxName})…`)
353
+
354
+ try {
355
+ // Create headless tmux session with a warm shell — matches terminal-server bootstrap
356
+ execSync(`tmux -f /dev/null new-session -d -s "${tmuxName}" -c "${this.#root}"`, { stdio: 'ignore' })
357
+ execSync(`tmux set-option -t "${tmuxName}" status off 2>/dev/null`, { stdio: 'ignore' })
358
+ execSync(`tmux set-option -t "${tmuxName}" set-clipboard off 2>/dev/null`, { stdio: 'ignore' })
359
+
360
+ /** @type {WarmSession} */
361
+ const session = { id, poolId: this.#poolId, tmuxName, createdAt: Date.now(), state: 'warming' }
362
+
363
+ // Wait for shell to be responsive
364
+ const shellReady = await this.#waitForShell(tmuxName)
365
+ if (!shellReady) {
366
+ this.#log(`⊕ SPAWN ${id} failed (shell not responsive)`)
367
+ try { execSync(`tmux kill-session -t "${tmuxName}" 2>/dev/null`, { stdio: 'ignore' }) } catch { /* empty */ }
368
+ return null
369
+ }
370
+
371
+ // For agent pools, launch the agent and wait for readiness
372
+ if (this.#agentConfig?.startupCommand) {
373
+ const agentReady = await this.#warmAgent(tmuxName, id)
374
+ if (!agentReady) {
375
+ this.#log(`⊕ SPAWN ${id} failed (agent not ready)`)
376
+ try { execSync(`tmux kill-session -t "${tmuxName}" 2>/dev/null`, { stdio: 'ignore' }) } catch { /* empty */ }
377
+ return null
378
+ }
379
+ }
380
+
381
+ session.state = 'ready'
382
+ this.#log(`⊕ SPAWN ${id} ready (tmux: ${tmuxName})`)
383
+ return session
384
+ } catch (err) {
385
+ this.#log(`⊕ SPAWN ${id} error: ${err.message}`)
386
+ try { execSync(`tmux kill-session -t "${tmuxName}" 2>/dev/null`, { stdio: 'ignore' }) } catch { /* empty */ }
387
+ return null
388
+ }
389
+ }
390
+
391
+ /** Wait for the tmux shell to be responsive (capture-pane has output). */
392
+ async #waitForShell(tmuxName) {
393
+ return new Promise((resolve) => {
394
+ const timer = setTimeout(() => {
395
+ clearInterval(check)
396
+ resolve(this.#tmuxSessionExists(tmuxName))
397
+ }, 2000)
398
+
399
+ const check = setInterval(() => {
400
+ try {
401
+ const output = execSync(`tmux capture-pane -t "${tmuxName}" -p 2>/dev/null`, { encoding: 'utf8', timeout: 1000 })
402
+ if (output.trim().length > 0) {
403
+ clearInterval(check)
404
+ clearTimeout(timer)
405
+ resolve(true)
406
+ }
407
+ } catch { /* not ready yet */ }
408
+ }, 300)
409
+ })
410
+ }
411
+
412
+ /**
413
+ * Launch the agent command and wait for the readiness signal.
414
+ * Returns true if the agent is ready, false on timeout or failure.
415
+ *
416
+ * Supports two readiness modes:
417
+ * 1. readinessFile: true — writes a SessionStart hook that touches a signal
418
+ * file, appends --settings to the command, and polls for the file.
419
+ * More reliable than pane scanning (survives UI changes).
420
+ * 2. readinessSignal: "text" — polls tmux capture-pane for the text.
421
+ * 3. Neither — waits 5s and assumes ready.
422
+ */
423
+ async #warmAgent(tmuxName, sessionId) {
424
+ const { startupCommand, readinessSignal, readinessFile, postStartup } = this.#agentConfig
425
+ this.#log(`⊕ AGENT ${sessionId} launching: ${startupCommand}`)
426
+
427
+ // Set up file-based readiness hook if configured
428
+ let signalFilePath = null
429
+ let settingsFilePath = null
430
+ let finalCommand = startupCommand
431
+
432
+ if (readinessFile) {
433
+ const hookDir = join(this.#root, '.storyboard', 'hot-pool')
434
+ try { mkdirSync(hookDir, { recursive: true }) } catch { /* empty */ }
435
+ signalFilePath = join(hookDir, `${sessionId}.ready`)
436
+ settingsFilePath = join(hookDir, `${sessionId}.settings.json`)
437
+
438
+ // Clean up any stale signal file
439
+ try { unlinkSync(signalFilePath) } catch { /* empty */ }
440
+
441
+ // Write a settings file with a SessionStart hook
442
+ const settings = {
443
+ hooks: {
444
+ SessionStart: [{
445
+ type: 'command',
446
+ command: `touch ${JSON.stringify(signalFilePath)}`,
447
+ }],
448
+ },
449
+ }
450
+ writeFileSync(settingsFilePath, JSON.stringify(settings))
451
+ finalCommand = `${startupCommand} --settings ${JSON.stringify(settingsFilePath)}`
452
+ this.#log(`⊕ AGENT ${sessionId} readinessFile hook → ${signalFilePath}`)
453
+ }
454
+
455
+ try {
456
+ execSync(`tmux send-keys -t "${tmuxName}" -l ${JSON.stringify(finalCommand)}`, { stdio: 'ignore' })
457
+ execSync(`tmux send-keys -t "${tmuxName}" Enter`, { stdio: 'ignore' })
458
+ } catch (err) {
459
+ this.#log(`⊕ AGENT ${sessionId} send-keys failed: ${err.message}`)
460
+ return false
461
+ }
462
+
463
+ // Determine readiness strategy
464
+ let ready = false
465
+
466
+ if (signalFilePath) {
467
+ // File-based readiness — poll for signal file existence
468
+ ready = await new Promise((resolve) => {
469
+ const timeout = setTimeout(() => {
470
+ clearInterval(poll)
471
+ this.#log(`⊕ AGENT ${sessionId} readiness file timeout (${AGENT_READINESS_TIMEOUT_MS / 1000}s)`)
472
+ resolve(false)
473
+ }, AGENT_READINESS_TIMEOUT_MS)
474
+
475
+ const poll = setInterval(() => {
476
+ if (existsSync(signalFilePath)) {
477
+ clearInterval(poll)
478
+ clearTimeout(timeout)
479
+ resolve(true)
480
+ }
481
+ }, AGENT_READINESS_POLL_MS)
482
+ })
483
+
484
+ // Clean up hook files
485
+ try { unlinkSync(signalFilePath) } catch { /* empty */ }
486
+ try { unlinkSync(settingsFilePath) } catch { /* empty */ }
487
+ } else if (readinessSignal) {
488
+ // Pane-content readiness — poll capture-pane for signal text
489
+ ready = await new Promise((resolve) => {
490
+ const timeout = setTimeout(() => {
491
+ clearInterval(poll)
492
+ this.#log(`⊕ AGENT ${sessionId} readiness timeout (${AGENT_READINESS_TIMEOUT_MS / 1000}s)`)
493
+ resolve(false)
494
+ }, AGENT_READINESS_TIMEOUT_MS)
495
+
496
+ const poll = setInterval(() => {
497
+ try {
498
+ const paneContent = execSync(
499
+ `tmux capture-pane -t "${tmuxName}" -p`,
500
+ { encoding: 'utf8', timeout: 1000 }
501
+ )
502
+ // Strip ANSI escape sequences — agent CLIs use heavy formatting
503
+ // eslint-disable-next-line no-control-regex
504
+ const clean = paneContent.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/[^\x20-\x7E\n]/g, '')
505
+ if (clean.includes(readinessSignal)) {
506
+ clearInterval(poll)
507
+ clearTimeout(timeout)
508
+ resolve(true)
509
+ }
510
+ } catch { /* not ready yet */ }
511
+ }, AGENT_READINESS_POLL_MS)
512
+ })
513
+ } else {
514
+ // No readiness mechanism — wait a fixed delay
515
+ await new Promise(r => setTimeout(r, 5000))
516
+ this.#log(`⊕ AGENT ${sessionId} no readiness signal — assuming ready after 5s`)
517
+ return this.#tmuxSessionExists(tmuxName)
518
+ }
519
+
520
+ if (!ready) {
521
+ // Timeout is non-fatal — the agent may be blocked by a CLI prompt
522
+ // (e.g. update notification). A partially-warm session is still
523
+ // better than a cold start.
524
+ this.#log(`⊕ AGENT ${sessionId} readiness timeout — marking ready anyway (better than cold)`)
525
+ return this.#tmuxSessionExists(tmuxName)
526
+ }
527
+
528
+ // Send postStartup command (e.g. "/allow-all on")
529
+ if (postStartup) {
530
+ try {
531
+ await new Promise(r => setTimeout(r, 500))
532
+ execSync(`tmux send-keys -t "${tmuxName}" -l ${JSON.stringify(postStartup)}`, { stdio: 'ignore' })
533
+ execSync(`tmux send-keys -t "${tmuxName}" Enter`, { stdio: 'ignore' })
534
+ this.#log(`⊕ AGENT ${sessionId} postStartup sent: ${postStartup}`)
535
+ } catch { /* empty */ }
536
+ }
537
+
538
+ this.#log(`⊕ AGENT ${sessionId} ready (${signalFilePath ? 'file' : 'signal'}: "${readinessSignal || 'file'}")`)
539
+ return true
540
+ }
541
+
542
+ #tmuxSessionExists(name) {
543
+ try {
544
+ execSync(`tmux has-session -t "${name}" 2>/dev/null`, { stdio: 'ignore' })
545
+ return true
546
+ } catch {
547
+ return false
548
+ }
549
+ }
550
+
551
+ /**
552
+ * For agent pools, verify the agent process is still running in the pane.
553
+ * Returns false if the tmux session is gone or the agent has exited
554
+ * back to a bare shell.
555
+ */
556
+ #isSessionHealthy(session) {
557
+ if (!this.#tmuxSessionExists(session.tmuxName)) return false
558
+
559
+ // For agent pools, check the foreground process hasn't fallen back to a shell
560
+ if (this.#agentConfig?.startupCommand) {
561
+ try {
562
+ const cmd = execSync(
563
+ `tmux display-message -t "${session.tmuxName}" -p "#{pane_current_command}"`,
564
+ { encoding: 'utf8', timeout: 1000 }
565
+ ).trim()
566
+ // Agent exited if the pane is back to a shell
567
+ const shells = ['zsh', 'bash', 'sh', 'fish']
568
+ if (shells.includes(cmd)) {
569
+ this.#log(`♥ HEALTH ${session.id} agent exited (pane_current_command="${cmd}")`)
570
+ return false
571
+ }
572
+ } catch {
573
+ return false
574
+ }
575
+ }
576
+
577
+ return true
578
+ }
579
+
580
+ #killSession(session) {
581
+ try {
582
+ if (session.tmuxName) {
583
+ execSync(`tmux kill-session -t "${session.tmuxName}" 2>/dev/null`, { stdio: 'ignore' })
584
+ }
585
+ } catch { /* ignore */ }
586
+ session.state = 'dead'
587
+ }
588
+
589
+ #healthCheck() {
590
+ const before = this.#queue.length
591
+ this.#queue = this.#queue.filter(s => {
592
+ if (!this.#isSessionHealthy(s)) { s.state = 'dead'; this.#killSession(s); return false }
593
+ return true
594
+ })
595
+
596
+ const removed = before - this.#queue.length
597
+ if (removed > 0) {
598
+ this.#log(`♥ HEALTH removed ${removed} dead (queue: ${this.#queue.length}/${this.#fillTarget})`)
599
+ } else {
600
+ this.#log(`♥ HEALTH ok (queue: ${this.#queue.length}/${this.#fillTarget}, active: ${this.#acquired.size}${this.#pressured ? ' ⚡' : ''})`)
601
+ }
602
+
603
+ this.#fill().catch(() => {})
604
+ }
605
+
606
+ async #checkPrereqs() {
607
+ try {
608
+ execSync('which tmux', { stdio: 'pipe' })
609
+
610
+ // Agent pools also need their CLI binary to be available
611
+ if (this.#agentConfig?.startupCommand) {
612
+ const bin = this.#agentConfig.startupCommand.trim().split(/\s+/)[0]
613
+ execSync(`which ${bin}`, { stdio: 'pipe' })
614
+ }
615
+
616
+ return true
617
+ } catch {
618
+ return false
619
+ }
620
+ }
621
+ }
622
+
623
+ // ── Hot Pool Manager ────────────────────────────────────────────────
624
+
625
+ const STAGGER_DELAY_MS = 5_000
626
+
627
+ /**
628
+ * Manages multiple typed HotPool instances (terminal, prompt, + per-agent).
629
+ * Provides a unified API for acquiring/consuming/releasing sessions by pool ID.
630
+ */
631
+ export class HotPoolManager {
632
+ /** @type {Map<string, HotPool>} */
633
+ #pools = new Map()
634
+ #enabled = true
635
+
636
+ /**
637
+ * @param {Object} opts
638
+ * @param {string} opts.root — project root directory
639
+ * @param {Object} opts.config — hotPool config from storyboard.config.json
640
+ * @param {Object} [opts.agentsConfig] — canvas.agents config
641
+ * @param {Function} [opts.wsSend] — Vite server.ws.send for browser devlog events
642
+ */
643
+ constructor({ root, config = {}, agentsConfig = {}, wsSend = null }) {
644
+ this.#enabled = config.enabled !== false
645
+ const poolsConfig = config.pools || {}
646
+
647
+ // Merge per-pool config with top-level defaults
648
+ const mergeConfig = (poolId) => ({
649
+ pool_size: poolsConfig[poolId]?.pool_size ?? config.default_pool_size ?? DEFAULT_POOL_SIZE,
650
+ max_pool_size: poolsConfig[poolId]?.max_pool_size ?? config.default_max_pool_size ?? DEFAULT_MAX_POOL_SIZE,
651
+ load_balancer_cooldown_mins: config.load_balancer_cooldown_mins ?? DEFAULT_COOLDOWN_MINS,
652
+ load_balancer: config.load_balancer !== false,
653
+ enabled: this.#enabled,
654
+ verbose: !!config.verbose,
655
+ webgl_ready_slots: poolsConfig[poolId]?.webgl_ready_slots ?? 0,
656
+ })
657
+
658
+ // Terminal pool (bare shells)
659
+ this.#pools.set('terminal', new HotPool({
660
+ root, poolId: 'terminal', config: mergeConfig('terminal'), wsSend,
661
+ }))
662
+
663
+ // Prompt pool (bare shells, separate from terminal)
664
+ this.#pools.set('prompt', new HotPool({
665
+ root, poolId: 'prompt', config: mergeConfig('prompt'), wsSend,
666
+ }))
667
+
668
+ // Agent pools (one per configured agent)
669
+ if (agentsConfig && typeof agentsConfig === 'object') {
670
+ for (const [id, agentCfg] of Object.entries(agentsConfig)) {
671
+ if (!agentCfg.startupCommand) continue
672
+ this.#pools.set(id, new HotPool({
673
+ root, poolId: id, config: mergeConfig(id), agentConfig: agentCfg, wsSend,
674
+ }))
675
+ }
676
+ }
677
+ }
678
+
679
+ /** Start all pools with staggered delays to avoid resource spikes. */
680
+ async start() {
681
+ if (!this.#enabled) return
682
+
683
+ const poolEntries = [...this.#pools.entries()]
684
+
685
+ // Start bare-shell pools first (fast), then agent pools (slow) with stagger
686
+ const shellPools = poolEntries.filter(([, p]) => !p.isAgentPool)
687
+ const agentPools = poolEntries.filter(([, p]) => p.isAgentPool)
688
+
689
+ // Shell pools start immediately in parallel
690
+ await Promise.all(shellPools.map(([, pool]) =>
691
+ pool.start().catch(err => {
692
+ devLog().logEvent('error', `Hot pool ${pool.poolId} failed to start`, { poolId: pool.poolId, error: err.message })
693
+ })
694
+ ))
695
+
696
+ // Agent pools start with stagger
697
+ for (let i = 0; i < agentPools.length; i++) {
698
+ const [, pool] = agentPools[i]
699
+ if (i > 0) await new Promise(r => setTimeout(r, STAGGER_DELAY_MS))
700
+ pool.start().catch(err => {
701
+ devLog().logEvent('error', `Hot pool ${pool.poolId} failed to start`, { poolId: pool.poolId, error: err.message })
702
+ })
703
+ }
704
+ }
705
+
706
+ stop() {
707
+ for (const pool of this.#pools.values()) pool.stop()
708
+ }
709
+
710
+ /**
711
+ * Acquire a warm session from the specified pool.
712
+ * @param {string} poolId — pool to acquire from (e.g. 'terminal', 'copilot', 'prompt')
713
+ */
714
+ acquire(poolId) {
715
+ const pool = this.#pools.get(poolId)
716
+ if (!pool) return null
717
+ return pool.acquire()
718
+ }
719
+
720
+ /** Consume a session (transfer ownership out of pool permanently). */
721
+ consume(poolId, sessionId) {
722
+ this.#pools.get(poolId)?.consume(sessionId)
723
+ }
724
+
725
+ /** Release a session back to the pool. */
726
+ release(poolId, sessionId) {
727
+ this.#pools.get(poolId)?.release(sessionId)
728
+ }
729
+
730
+ /** Get status of all pools. */
731
+ status() {
732
+ const pools = {}
733
+ for (const [id, pool] of this.#pools) {
734
+ pools[id] = pool.status()
735
+ }
736
+ return { enabled: this.#enabled, pools }
737
+ }
738
+
739
+ /** Reconfigure pools from updated config. */
740
+ reconfigure(config) {
741
+ const poolsConfig = config.pools || {}
742
+ for (const [id, pool] of this.#pools) {
743
+ const poolConfig = {
744
+ pool_size: poolsConfig[id]?.pool_size ?? config.default_pool_size,
745
+ max_pool_size: poolsConfig[id]?.max_pool_size ?? config.default_max_pool_size,
746
+ load_balancer_cooldown_mins: config.load_balancer_cooldown_mins,
747
+ load_balancer: config.load_balancer,
748
+ enabled: config.enabled,
749
+ verbose: config.verbose,
750
+ webgl_ready_slots: poolsConfig[id]?.webgl_ready_slots,
751
+ }
752
+ pool.reconfigure(poolConfig)
753
+ }
754
+ this.#enabled = config.enabled !== false
755
+ }
756
+
757
+ /** Check if a pool exists for the given ID. */
758
+ has(poolId) {
759
+ return this.#pools.has(poolId)
760
+ }
761
+
762
+ /** Get the list of pool IDs. */
763
+ get poolIds() {
764
+ return [...this.#pools.keys()]
765
+ }
766
+ }