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