@dfosco/storyboard-core 4.2.0-beta.3 → 4.2.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 (419) hide show
  1. package/commandpalette.config.json +109 -24
  2. package/dist/storyboard-ui.css +1 -1
  3. package/dist/storyboard-ui.js +16711 -27239
  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 -10
  11. package/scaffold/skills/canvas/SKILL.md +5 -4
  12. package/scaffold/skills/ship/SKILL.md +1 -1
  13. package/scaffold/storyboard.config.json +11 -1
  14. package/src/ActionMenuButton.jsx +103 -0
  15. package/src/AutosyncMenuButton.css +67 -0
  16. package/src/AutosyncMenuButton.jsx +242 -0
  17. package/src/BranchSelect.jsx +29 -0
  18. package/src/BranchSelect.module.css +30 -0
  19. package/src/CanvasAgentsMenu.jsx +89 -0
  20. package/src/CanvasCreateMenu.jsx +611 -0
  21. package/src/CanvasSnap.css +27 -0
  22. package/src/CanvasSnap.jsx +51 -0
  23. package/src/CanvasUndoRedo.css +36 -0
  24. package/src/CanvasUndoRedo.jsx +62 -0
  25. package/src/CanvasZoomControl.css +53 -0
  26. package/src/CanvasZoomControl.jsx +49 -0
  27. package/src/CanvasZoomToFit.css +18 -0
  28. package/src/CanvasZoomToFit.jsx +26 -0
  29. package/src/CommandMenu.css +8 -0
  30. package/src/CommandMenu.jsx +287 -0
  31. package/src/CommandPalette.jsx +35 -0
  32. package/src/CommandPaletteTrigger.jsx +25 -0
  33. package/src/CommentsMenuButton.jsx +40 -0
  34. package/src/CoreUIBar.css +47 -0
  35. package/src/CoreUIBar.jsx +858 -0
  36. package/src/CreateMenuButton.jsx +117 -0
  37. package/src/HideChromeTrigger.jsx +40 -0
  38. package/src/InspectorPanel.css +109 -0
  39. package/src/InspectorPanel.jsx +632 -0
  40. package/src/PwaInstallBanner.css +42 -0
  41. package/src/PwaInstallBanner.jsx +124 -0
  42. package/src/SidePanel.jsx +261 -0
  43. package/src/ThemeMenuButton.jsx +139 -0
  44. package/src/autosync/server.js +202 -5
  45. package/src/autosync/server.test.js +111 -0
  46. package/src/canvas/__tests__/agent-integration.test.js +596 -0
  47. package/src/canvas/__tests__/helpers/browser.js +95 -0
  48. package/src/canvas/__tests__/helpers/canvas-api.js +129 -0
  49. package/src/canvas/__tests__/helpers/perf.js +118 -0
  50. package/src/canvas/__tests__/helpers/setup.js +176 -0
  51. package/src/canvas/__tests__/helpers/tmux.js +130 -0
  52. package/src/canvas/__tests__/helpers/transcript.js +132 -0
  53. package/src/canvas/__tests__/terminal-integration.test.js +177 -0
  54. package/src/canvas/hot-pool.js +756 -0
  55. package/src/canvas/materializer.js +31 -0
  56. package/src/canvas/materializer.test.js +56 -0
  57. package/src/canvas/selectedWidgets.js +65 -7
  58. package/src/canvas/server.js +1802 -22
  59. package/src/canvas/server.test.js +239 -0
  60. package/src/canvas/terminal-config.js +330 -0
  61. package/src/canvas/terminal-registry.js +41 -3
  62. package/src/canvas/terminal-server.js +1098 -37
  63. package/src/canvas/writeGuard.js +51 -3
  64. package/src/canvasConfig.js +67 -1
  65. package/src/canvasConfig.test.js +79 -1
  66. package/src/cli/agent.js +85 -0
  67. package/src/cli/branch.js +232 -0
  68. package/src/cli/canvasAdd.js +61 -14
  69. package/src/cli/canvasBatch.js +98 -0
  70. package/src/cli/canvasBounds.js +1 -1
  71. package/src/cli/canvasRead.js +1 -1
  72. package/src/cli/canvasUpdate.js +179 -0
  73. package/src/cli/compact.js +0 -2
  74. package/src/cli/create.js +40 -16
  75. package/src/cli/dev.js +158 -84
  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 +149 -26
  81. package/src/cli/serverUrl.js +8 -3
  82. package/src/cli/sessions.js +133 -7
  83. package/src/cli/setup.js +138 -12
  84. package/src/cli/terminal-commands.js +20 -9
  85. package/src/cli/terminal-messaging.js +231 -0
  86. package/src/cli/terminal-welcome.js +449 -34
  87. package/src/cli/updateVersion.js +1 -1
  88. package/src/commandActions.js +1 -0
  89. package/src/commandPaletteConfig.js +9 -0
  90. package/src/comments/auth.js +2 -1
  91. package/src/comments/ui/AuthModal.jsx +114 -0
  92. package/src/comments/ui/CommentWindow.jsx +329 -0
  93. package/src/comments/ui/CommentsDrawer.jsx +102 -0
  94. package/src/comments/ui/Composer.jsx +64 -0
  95. package/src/comments/ui/authModal.test.js +1 -1
  96. package/src/comments/ui/commentWindow.js +16 -17
  97. package/src/comments/ui/commentsDrawer.js +25 -26
  98. package/src/comments/ui/composer.js +23 -24
  99. package/src/comments/ui/index.js +2 -3
  100. package/src/configSchema.js +59 -1
  101. package/src/configStore.js +161 -0
  102. package/src/core-ui-colors.css +12 -0
  103. package/src/devtools.js +17 -19
  104. package/src/devtools.test.js +18 -9
  105. package/src/featureFlags.js +12 -5
  106. package/src/fuzzySearch.test.js +10 -0
  107. package/src/index.js +15 -3
  108. package/src/lib/components/ui/alert/alert-action.jsx +11 -0
  109. package/src/lib/components/ui/alert/alert-description.jsx +11 -0
  110. package/src/lib/components/ui/alert/alert-title.jsx +11 -0
  111. package/src/lib/components/ui/alert/alert.jsx +25 -0
  112. package/src/lib/components/ui/alert/index.js +15 -15
  113. package/src/lib/components/ui/avatar/avatar-badge.jsx +22 -0
  114. package/src/lib/components/ui/avatar/avatar-fallback.jsx +18 -0
  115. package/src/lib/components/ui/avatar/avatar-group-count.jsx +19 -0
  116. package/src/lib/components/ui/avatar/avatar-group.jsx +19 -0
  117. package/src/lib/components/ui/avatar/avatar-image.jsx +15 -0
  118. package/src/lib/components/ui/avatar/avatar.jsx +19 -0
  119. package/src/lib/components/ui/avatar/index.js +20 -20
  120. package/src/lib/components/ui/badge/badge.jsx +31 -0
  121. package/src/lib/components/ui/badge/index.js +2 -2
  122. package/src/lib/components/ui/button/button.jsx +100 -0
  123. package/src/lib/components/ui/button/index.js +9 -9
  124. package/src/lib/components/ui/card/card-action.jsx +11 -0
  125. package/src/lib/components/ui/card/card-content.jsx +11 -0
  126. package/src/lib/components/ui/card/card-description.jsx +11 -0
  127. package/src/lib/components/ui/card/card-footer.jsx +11 -0
  128. package/src/lib/components/ui/card/card-header.jsx +19 -0
  129. package/src/lib/components/ui/card/card-title.jsx +11 -0
  130. package/src/lib/components/ui/card/card.jsx +17 -0
  131. package/src/lib/components/ui/card/index.js +23 -23
  132. package/src/lib/components/ui/checkbox/checkbox.jsx +29 -0
  133. package/src/lib/components/ui/checkbox/index.js +5 -5
  134. package/src/lib/components/ui/collapsible/collapsible-content.jsx +7 -0
  135. package/src/lib/components/ui/collapsible/collapsible-trigger.jsx +7 -0
  136. package/src/lib/components/ui/collapsible/collapsible.jsx +7 -0
  137. package/src/lib/components/ui/collapsible/index.js +11 -11
  138. package/src/lib/components/ui/dialog/dialog-close.jsx +7 -0
  139. package/src/lib/components/ui/dialog/dialog-content.jsx +34 -0
  140. package/src/lib/components/ui/dialog/dialog-description.jsx +15 -0
  141. package/src/lib/components/ui/dialog/dialog-footer.jsx +23 -0
  142. package/src/lib/components/ui/dialog/dialog-header.jsx +11 -0
  143. package/src/lib/components/ui/dialog/dialog-overlay.jsx +15 -0
  144. package/src/lib/components/ui/dialog/dialog-portal.jsx +4 -0
  145. package/src/lib/components/ui/dialog/dialog-title.jsx +15 -0
  146. package/src/lib/components/ui/dialog/dialog-trigger.jsx +7 -0
  147. package/src/lib/components/ui/dialog/dialog.jsx +4 -0
  148. package/src/lib/components/ui/dialog/index.js +32 -32
  149. package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +8 -0
  150. package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +30 -0
  151. package/src/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +22 -0
  152. package/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +16 -0
  153. package/src/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +7 -0
  154. package/src/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +20 -0
  155. package/src/lib/components/ui/dropdown-menu/dropdown-menu-label.jsx +17 -0
  156. package/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +4 -0
  157. package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +7 -0
  158. package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +29 -0
  159. package/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +15 -0
  160. package/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.jsx +16 -0
  161. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +15 -0
  162. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +23 -0
  163. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +4 -0
  164. package/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +7 -0
  165. package/src/lib/components/ui/dropdown-menu/dropdown-menu.jsx +4 -0
  166. package/src/lib/components/ui/dropdown-menu/index.js +52 -52
  167. package/src/lib/components/ui/input/index.js +5 -5
  168. package/src/lib/components/ui/input/input.jsx +19 -0
  169. package/src/lib/components/ui/label/index.js +5 -5
  170. package/src/lib/components/ui/label/label.jsx +19 -0
  171. package/src/lib/components/ui/panel/index.js +21 -21
  172. package/src/lib/components/ui/panel/panel-body.jsx +11 -0
  173. package/src/lib/components/ui/panel/panel-close.jsx +16 -0
  174. package/src/lib/components/ui/panel/panel-content.jsx +29 -0
  175. package/src/lib/components/ui/panel/panel-footer.jsx +11 -0
  176. package/src/lib/components/ui/panel/panel-header.jsx +11 -0
  177. package/src/lib/components/ui/panel/panel-title.jsx +12 -0
  178. package/src/lib/components/ui/panel/panel.jsx +4 -0
  179. package/src/lib/components/ui/popover/index.js +26 -26
  180. package/src/lib/components/ui/popover/popover-close.jsx +7 -0
  181. package/src/lib/components/ui/popover/popover-content.jsx +22 -0
  182. package/src/lib/components/ui/popover/popover-description.jsx +11 -0
  183. package/src/lib/components/ui/popover/popover-header.jsx +11 -0
  184. package/src/lib/components/ui/popover/popover-portal.jsx +4 -0
  185. package/src/lib/components/ui/popover/popover-title.jsx +11 -0
  186. package/src/lib/components/ui/popover/popover-trigger.jsx +8 -0
  187. package/src/lib/components/ui/popover/popover.jsx +4 -0
  188. package/src/lib/components/ui/searchable-list.jsx +160 -0
  189. package/src/lib/components/ui/select/index.js +35 -35
  190. package/src/lib/components/ui/select/select-content.jsx +30 -0
  191. package/src/lib/components/ui/select/select-group-heading.jsx +17 -0
  192. package/src/lib/components/ui/select/select-group.jsx +15 -0
  193. package/src/lib/components/ui/select/select-item.jsx +26 -0
  194. package/src/lib/components/ui/select/select-label.jsx +11 -0
  195. package/src/lib/components/ui/select/select-portal.jsx +4 -0
  196. package/src/lib/components/ui/select/select-scroll-down-button.jsx +18 -0
  197. package/src/lib/components/ui/select/select-scroll-up-button.jsx +18 -0
  198. package/src/lib/components/ui/select/select-separator.jsx +15 -0
  199. package/src/lib/components/ui/select/select-trigger.jsx +25 -0
  200. package/src/lib/components/ui/select/select.jsx +4 -0
  201. package/src/lib/components/ui/separator/index.js +5 -5
  202. package/src/lib/components/ui/separator/separator.jsx +22 -0
  203. package/src/lib/components/ui/sheet/index.js +32 -32
  204. package/src/lib/components/ui/sheet/sheet-close.jsx +7 -0
  205. package/src/lib/components/ui/sheet/sheet-content.jsx +35 -0
  206. package/src/lib/components/ui/sheet/sheet-description.jsx +15 -0
  207. package/src/lib/components/ui/sheet/sheet-footer.jsx +11 -0
  208. package/src/lib/components/ui/sheet/sheet-header.jsx +11 -0
  209. package/src/lib/components/ui/sheet/sheet-overlay.jsx +15 -0
  210. package/src/lib/components/ui/sheet/sheet-portal.jsx +4 -0
  211. package/src/lib/components/ui/sheet/sheet-title.jsx +15 -0
  212. package/src/lib/components/ui/sheet/sheet-trigger.jsx +7 -0
  213. package/src/lib/components/ui/sheet/sheet.jsx +4 -0
  214. package/src/lib/components/ui/textarea/index.js +5 -5
  215. package/src/lib/components/ui/textarea/textarea.jsx +18 -0
  216. package/src/lib/components/ui/toggle/index.js +6 -9
  217. package/src/lib/components/ui/toggle/toggle.jsx +36 -0
  218. package/src/lib/components/ui/toggle-group/index.js +8 -8
  219. package/src/lib/components/ui/toggle-group/toggle-group-item.jsx +29 -0
  220. package/src/lib/components/ui/toggle-group/toggle-group.jsx +43 -0
  221. package/src/lib/components/ui/tooltip/index.js +3 -3
  222. package/src/lib/components/ui/tooltip/tooltip-content.jsx +21 -0
  223. package/src/lib/components/ui/tooltip/tooltip-trigger.jsx +23 -0
  224. package/src/lib/components/ui/tooltip/tooltip.jsx +11 -0
  225. package/src/lib/components/ui/trigger-button/index.js +3 -3
  226. package/src/lib/components/ui/trigger-button/trigger-button.css +38 -0
  227. package/src/lib/components/ui/trigger-button/trigger-button.jsx +63 -0
  228. package/src/logger/devLogger.js +238 -0
  229. package/src/logger/devLogger.test.js +193 -0
  230. package/src/modes.test.js +4 -4
  231. package/src/mountStoryboardCore.js +133 -35
  232. package/src/paletteProviders.js +3 -0
  233. package/src/paletteProviders.test.js +2 -2
  234. package/src/server/index.js +104 -40
  235. package/src/sidepanel.css +214 -0
  236. package/src/smoothCorners.js +1 -1
  237. package/src/styles/tailwind.css +1 -1
  238. package/src/svelte-plugin-ui/__tests__/ModeSwitch.test.ts +8 -8
  239. package/src/svelte-plugin-ui/__tests__/ToolbarShell.test.ts +11 -10
  240. package/src/svelte-plugin-ui/components/Icon.css +11 -0
  241. package/src/svelte-plugin-ui/components/Icon.jsx +281 -0
  242. package/src/svelte-plugin-ui/components/ModeSwitch.css +90 -0
  243. package/src/svelte-plugin-ui/components/ModeSwitch.jsx +47 -0
  244. package/src/svelte-plugin-ui/components/ToolbarShell.css +80 -0
  245. package/src/svelte-plugin-ui/components/ToolbarShell.jsx +84 -0
  246. package/src/svelte-plugin-ui/components/Viewfinder.css +412 -0
  247. package/src/svelte-plugin-ui/components/Viewfinder.jsx +513 -0
  248. package/src/svelte-plugin-ui/mount.ts +12 -16
  249. package/src/toolRegistry.js +4 -4
  250. package/src/toolbarConfigStore.js +30 -0
  251. package/src/tools/handlers/autosync.js +1 -1
  252. package/src/tools/handlers/canvasAddWidget.js +1 -1
  253. package/src/tools/handlers/canvasAgents.js +20 -0
  254. package/src/tools/handlers/canvasToolbar.js +8 -8
  255. package/src/tools/handlers/commandPalette.js +9 -0
  256. package/src/tools/handlers/comments.js +2 -2
  257. package/src/tools/handlers/create.js +1 -1
  258. package/src/tools/handlers/devtools.js +19 -3
  259. package/src/tools/handlers/devtools.test.js +38 -0
  260. package/src/tools/handlers/featureFlags.js +1 -1
  261. package/src/tools/handlers/flows.js +3 -3
  262. package/src/tools/handlers/hideChrome.js +9 -0
  263. package/src/tools/handlers/paletteTheme.js +35 -0
  264. package/src/tools/handlers/theme.js +1 -1
  265. package/src/tools/registry.js +4 -1
  266. package/src/tools/surfaces/commandList.js +3 -3
  267. package/src/tools/surfaces/mainToolbar.js +3 -3
  268. package/src/tools/surfaces/registry.js +4 -4
  269. package/src/ui/design-modes.ts +2 -2
  270. package/src/ui/viewfinder.ts +1 -1
  271. package/src/vite/server-plugin.js +243 -61
  272. package/src/workshop/features/createCanvas/CreateCanvasForm.jsx +260 -0
  273. package/src/workshop/features/createCanvas/index.js +1 -1
  274. package/src/workshop/features/createFlow/CreateFlowForm.jsx +334 -0
  275. package/src/workshop/features/createFlow/index.js +1 -1
  276. package/src/workshop/features/createPage/CreatePageForm.jsx +304 -0
  277. package/src/workshop/features/createPage/index.js +1 -1
  278. package/src/workshop/features/createPrototype/CreatePrototypeForm.jsx +289 -0
  279. package/src/workshop/features/createPrototype/index.js +1 -1
  280. package/src/workshop/features/createPrototype/server.js +98 -0
  281. package/src/workshop/features/createStory/CreateStoryForm.jsx +208 -0
  282. package/src/workshop/features/createStory/index.js +1 -1
  283. package/src/workshop/ui/WorkshopPanel.jsx +98 -0
  284. package/src/workshop/ui/mount.ts +1 -1
  285. package/src/worktree/port.js +48 -0
  286. package/src/worktree/serverRegistry.js +120 -0
  287. package/toolbar.config.json +93 -42
  288. package/widgets.config.json +580 -12
  289. package/scaffold/commandpalette.config.json +0 -4
  290. package/scaffold/toolbar.config.json +0 -4
  291. package/src/ActionMenuButton.svelte +0 -119
  292. package/src/AutosyncMenuButton.svelte +0 -397
  293. package/src/CanvasCreateMenu.svelte +0 -295
  294. package/src/CanvasSnap.svelte +0 -87
  295. package/src/CanvasUndoRedo.svelte +0 -108
  296. package/src/CanvasZoomControl.svelte +0 -111
  297. package/src/CanvasZoomToFit.svelte +0 -52
  298. package/src/CommandMenu.svelte +0 -249
  299. package/src/CommandPalette.svelte +0 -33
  300. package/src/CommentsMenuButton.svelte +0 -53
  301. package/src/CoreUIBar.svelte +0 -847
  302. package/src/CreateMenuButton.svelte +0 -133
  303. package/src/DocPanel.svelte +0 -299
  304. package/src/InspectorPanel.svelte +0 -745
  305. package/src/PwaInstallBanner.svelte +0 -124
  306. package/src/SidePanel.svelte +0 -480
  307. package/src/ThemeMenuButton.svelte +0 -132
  308. package/src/comments/ui/AuthModal.svelte +0 -108
  309. package/src/comments/ui/CommentWindow.svelte +0 -333
  310. package/src/comments/ui/CommentsDrawer.svelte +0 -96
  311. package/src/comments/ui/Composer.svelte +0 -65
  312. package/src/lib/components/ui/alert/alert-action.svelte +0 -19
  313. package/src/lib/components/ui/alert/alert-description.svelte +0 -22
  314. package/src/lib/components/ui/alert/alert-title.svelte +0 -22
  315. package/src/lib/components/ui/alert/alert.svelte +0 -38
  316. package/src/lib/components/ui/avatar/avatar-badge.svelte +0 -25
  317. package/src/lib/components/ui/avatar/avatar-fallback.svelte +0 -20
  318. package/src/lib/components/ui/avatar/avatar-group-count.svelte +0 -22
  319. package/src/lib/components/ui/avatar/avatar-group.svelte +0 -22
  320. package/src/lib/components/ui/avatar/avatar-image.svelte +0 -17
  321. package/src/lib/components/ui/avatar/avatar.svelte +0 -24
  322. package/src/lib/components/ui/badge/badge.svelte +0 -44
  323. package/src/lib/components/ui/button/button.svelte +0 -108
  324. package/src/lib/components/ui/card/card-action.svelte +0 -21
  325. package/src/lib/components/ui/card/card-content.svelte +0 -19
  326. package/src/lib/components/ui/card/card-description.svelte +0 -19
  327. package/src/lib/components/ui/card/card-footer.svelte +0 -18
  328. package/src/lib/components/ui/card/card-header.svelte +0 -21
  329. package/src/lib/components/ui/card/card-title.svelte +0 -14
  330. package/src/lib/components/ui/card/card.svelte +0 -21
  331. package/src/lib/components/ui/checkbox/checkbox.svelte +0 -39
  332. package/src/lib/components/ui/collapsible/collapsible-content.svelte +0 -7
  333. package/src/lib/components/ui/collapsible/collapsible-trigger.svelte +0 -7
  334. package/src/lib/components/ui/collapsible/collapsible.svelte +0 -11
  335. package/src/lib/components/ui/dialog/dialog-close.svelte +0 -11
  336. package/src/lib/components/ui/dialog/dialog-content.svelte +0 -42
  337. package/src/lib/components/ui/dialog/dialog-description.svelte +0 -17
  338. package/src/lib/components/ui/dialog/dialog-footer.svelte +0 -29
  339. package/src/lib/components/ui/dialog/dialog-header.svelte +0 -19
  340. package/src/lib/components/ui/dialog/dialog-overlay.svelte +0 -17
  341. package/src/lib/components/ui/dialog/dialog-portal.svelte +0 -7
  342. package/src/lib/components/ui/dialog/dialog-title.svelte +0 -17
  343. package/src/lib/components/ui/dialog/dialog-trigger.svelte +0 -11
  344. package/src/lib/components/ui/dialog/dialog.svelte +0 -7
  345. package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +0 -16
  346. package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +0 -40
  347. package/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +0 -27
  348. package/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +0 -18
  349. package/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte +0 -7
  350. package/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +0 -24
  351. package/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +0 -20
  352. package/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte +0 -7
  353. package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +0 -16
  354. package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +0 -34
  355. package/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +0 -17
  356. package/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +0 -19
  357. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +0 -17
  358. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +0 -27
  359. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte +0 -7
  360. package/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +0 -7
  361. package/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte +0 -7
  362. package/src/lib/components/ui/input/input.svelte +0 -40
  363. package/src/lib/components/ui/label/label.svelte +0 -20
  364. package/src/lib/components/ui/panel/panel-body.svelte +0 -13
  365. package/src/lib/components/ui/panel/panel-close.svelte +0 -16
  366. package/src/lib/components/ui/panel/panel-content.svelte +0 -33
  367. package/src/lib/components/ui/panel/panel-footer.svelte +0 -13
  368. package/src/lib/components/ui/panel/panel-header.svelte +0 -16
  369. package/src/lib/components/ui/panel/panel-title.svelte +0 -14
  370. package/src/lib/components/ui/panel/panel.svelte +0 -15
  371. package/src/lib/components/ui/popover/popover-close.svelte +0 -7
  372. package/src/lib/components/ui/popover/popover-content.svelte +0 -27
  373. package/src/lib/components/ui/popover/popover-description.svelte +0 -19
  374. package/src/lib/components/ui/popover/popover-header.svelte +0 -19
  375. package/src/lib/components/ui/popover/popover-portal.svelte +0 -7
  376. package/src/lib/components/ui/popover/popover-title.svelte +0 -19
  377. package/src/lib/components/ui/popover/popover-trigger.svelte +0 -17
  378. package/src/lib/components/ui/popover/popover.svelte +0 -7
  379. package/src/lib/components/ui/select/select-content.svelte +0 -40
  380. package/src/lib/components/ui/select/select-group-heading.svelte +0 -19
  381. package/src/lib/components/ui/select/select-group.svelte +0 -17
  382. package/src/lib/components/ui/select/select-item.svelte +0 -38
  383. package/src/lib/components/ui/select/select-label.svelte +0 -18
  384. package/src/lib/components/ui/select/select-portal.svelte +0 -7
  385. package/src/lib/components/ui/select/select-scroll-down-button.svelte +0 -20
  386. package/src/lib/components/ui/select/select-scroll-up-button.svelte +0 -20
  387. package/src/lib/components/ui/select/select-separator.svelte +0 -17
  388. package/src/lib/components/ui/select/select-trigger.svelte +0 -27
  389. package/src/lib/components/ui/select/select.svelte +0 -11
  390. package/src/lib/components/ui/separator/separator.svelte +0 -23
  391. package/src/lib/components/ui/sheet/sheet-close.svelte +0 -7
  392. package/src/lib/components/ui/sheet/sheet-content.svelte +0 -43
  393. package/src/lib/components/ui/sheet/sheet-description.svelte +0 -17
  394. package/src/lib/components/ui/sheet/sheet-footer.svelte +0 -18
  395. package/src/lib/components/ui/sheet/sheet-header.svelte +0 -19
  396. package/src/lib/components/ui/sheet/sheet-overlay.svelte +0 -17
  397. package/src/lib/components/ui/sheet/sheet-portal.svelte +0 -7
  398. package/src/lib/components/ui/sheet/sheet-title.svelte +0 -17
  399. package/src/lib/components/ui/sheet/sheet-trigger.svelte +0 -7
  400. package/src/lib/components/ui/sheet/sheet.svelte +0 -7
  401. package/src/lib/components/ui/textarea/textarea.svelte +0 -21
  402. package/src/lib/components/ui/toggle/toggle.svelte +0 -45
  403. package/src/lib/components/ui/toggle-group/toggle-group-item.svelte +0 -35
  404. package/src/lib/components/ui/toggle-group/toggle-group.svelte +0 -63
  405. package/src/lib/components/ui/tooltip/tooltip-content.svelte +0 -24
  406. package/src/lib/components/ui/tooltip/tooltip-trigger.svelte +0 -27
  407. package/src/lib/components/ui/tooltip/tooltip.svelte +0 -9
  408. package/src/lib/components/ui/trigger-button/trigger-button.svelte +0 -106
  409. package/src/svelte-plugin-ui/components/Icon.svelte +0 -181
  410. package/src/svelte-plugin-ui/components/ModeSwitch.svelte +0 -121
  411. package/src/svelte-plugin-ui/components/ToolbarShell.svelte +0 -150
  412. package/src/svelte-plugin-ui/components/Viewfinder.svelte +0 -1001
  413. package/src/tools/handlers/docs.js +0 -11
  414. package/src/workshop/features/createCanvas/CreateCanvasForm.svelte +0 -139
  415. package/src/workshop/features/createFlow/CreateFlowForm.svelte +0 -314
  416. package/src/workshop/features/createPage/CreatePageForm.svelte +0 -249
  417. package/src/workshop/features/createPrototype/CreatePrototypeForm.svelte +0 -287
  418. package/src/workshop/features/createStory/CreateStoryForm.svelte +0 -161
  419. package/src/workshop/ui/WorkshopPanel.svelte +0 -97
@@ -138,3 +138,242 @@ describe('POST /create with convertFrom', () => {
138
138
  expect(lastResponse.body.error).toContain('collides')
139
139
  })
140
140
  })
141
+
142
+ // ──────────────────────────────────────────────────
143
+ // POST /batch
144
+ // ──────────────────────────────────────────────────
145
+
146
+ describe('POST /batch', () => {
147
+ let root, canvasDir, invoke, lastResponse
148
+
149
+ beforeEach(() => {
150
+ ({ invoke, root, canvasDir, lastResponse } = setup())
151
+ writeCanvas(canvasDir, 'test-canvas')
152
+ })
153
+
154
+ afterEach(() => {
155
+ fs.rmSync(root, { recursive: true, force: true })
156
+ })
157
+
158
+ it('creates multiple widgets in one batch', async () => {
159
+ await invoke('/batch', 'POST', {
160
+ name: 'test-canvas',
161
+ operations: [
162
+ { op: 'create-widget', type: 'sticky-note', props: { text: 'A' } },
163
+ { op: 'create-widget', type: 'sticky-note', props: { text: 'B' } },
164
+ { op: 'create-widget', type: 'sticky-note', props: { text: 'C' } },
165
+ ],
166
+ })
167
+
168
+ expect(lastResponse.status).toBe(200)
169
+ expect(lastResponse.body.success).toBe(true)
170
+ expect(lastResponse.body.results).toHaveLength(3)
171
+ expect(lastResponse.body.results[0].widget.props.text).toBe('A')
172
+ expect(lastResponse.body.results[1].widget.props.text).toBe('B')
173
+ expect(lastResponse.body.results[2].widget.props.text).toBe('C')
174
+ })
175
+
176
+ it('auto-assigns index refs ($0, $1, ...)', async () => {
177
+ await invoke('/batch', 'POST', {
178
+ name: 'test-canvas',
179
+ operations: [
180
+ { op: 'create-widget', type: 'sticky-note', props: { text: 'first' } },
181
+ { op: 'create-widget', type: 'sticky-note', props: { text: 'second' } },
182
+ ],
183
+ })
184
+
185
+ expect(lastResponse.body.success).toBe(true)
186
+ const { refs } = lastResponse.body
187
+ expect(refs['0']).toBe(lastResponse.body.results[0].widgetId)
188
+ expect(refs['1']).toBe(lastResponse.body.results[1].widgetId)
189
+ })
190
+
191
+ it('supports named refs alongside index refs', async () => {
192
+ await invoke('/batch', 'POST', {
193
+ name: 'test-canvas',
194
+ operations: [
195
+ { op: 'create-widget', type: 'sticky-note', ref: 'header', props: { text: 'H' } },
196
+ ],
197
+ })
198
+
199
+ expect(lastResponse.body.success).toBe(true)
200
+ const { refs } = lastResponse.body
201
+ const widgetId = lastResponse.body.results[0].widgetId
202
+ expect(refs['0']).toBe(widgetId)
203
+ expect(refs['header']).toBe(widgetId)
204
+ })
205
+
206
+ it('resolves $index refs in create-connector', async () => {
207
+ // First create a widget to act as the "existing" terminal widget
208
+ await invoke('/widget', 'POST', { name: 'test-canvas', type: 'terminal', props: {} })
209
+ const terminalId = lastResponse.body.widget.id
210
+
211
+ await invoke('/batch', 'POST', {
212
+ name: 'test-canvas',
213
+ operations: [
214
+ { op: 'create-widget', type: 'sticky-note', props: { text: 'target' } },
215
+ { op: 'create-connector', startWidgetId: terminalId, endWidgetId: '$0', startAnchor: 'right', endAnchor: 'left' },
216
+ ],
217
+ })
218
+
219
+ expect(lastResponse.body.success).toBe(true)
220
+ expect(lastResponse.body.results).toHaveLength(2)
221
+ expect(lastResponse.body.results[1].op).toBe('create-connector')
222
+ expect(lastResponse.body.results[1].connectorId).toBeTruthy()
223
+ })
224
+
225
+ it('resolves $named refs in update-widget', async () => {
226
+ await invoke('/batch', 'POST', {
227
+ name: 'test-canvas',
228
+ operations: [
229
+ { op: 'create-widget', type: 'sticky-note', ref: 'note', props: { text: 'before' } },
230
+ { op: 'update-widget', widgetId: '$note', props: { text: 'after' } },
231
+ ],
232
+ })
233
+
234
+ expect(lastResponse.body.success).toBe(true)
235
+ expect(lastResponse.body.results[1].op).toBe('update-widget')
236
+ expect(lastResponse.body.results[1].success).toBe(true)
237
+ })
238
+
239
+ it('supports move-widget with ref resolution', async () => {
240
+ await invoke('/batch', 'POST', {
241
+ name: 'test-canvas',
242
+ operations: [
243
+ { op: 'create-widget', type: 'sticky-note', position: { x: 0, y: 0 }, props: { text: 'X' } },
244
+ { op: 'move-widget', widgetId: '$0', position: { x: 500, y: 300 } },
245
+ ],
246
+ })
247
+
248
+ expect(lastResponse.body.success).toBe(true)
249
+ expect(lastResponse.body.results[1].op).toBe('move-widget')
250
+ expect(lastResponse.body.results[1].success).toBe(true)
251
+ })
252
+
253
+ it('supports delete-widget with ref resolution', async () => {
254
+ await invoke('/batch', 'POST', {
255
+ name: 'test-canvas',
256
+ operations: [
257
+ { op: 'create-widget', type: 'sticky-note', props: { text: 'temp' } },
258
+ { op: 'delete-widget', widgetId: '$0' },
259
+ ],
260
+ })
261
+
262
+ expect(lastResponse.body.success).toBe(true)
263
+ expect(lastResponse.body.results[1].op).toBe('delete-widget')
264
+ expect(lastResponse.body.results[1].success).toBe(true)
265
+ })
266
+
267
+ it('fails fast on unknown ref', async () => {
268
+ await invoke('/batch', 'POST', {
269
+ name: 'test-canvas',
270
+ operations: [
271
+ { op: 'create-widget', type: 'sticky-note', props: { text: 'ok' } },
272
+ { op: 'update-widget', widgetId: '$nonexistent', props: { text: 'fail' } },
273
+ ],
274
+ })
275
+
276
+ expect(lastResponse.status).toBe(400)
277
+ expect(lastResponse.body.success).toBe(false)
278
+ expect(lastResponse.body.failedAt).toBe(1)
279
+ expect(lastResponse.body.error).toContain('Unknown ref')
280
+ // First operation's result should still be returned
281
+ expect(lastResponse.body.results).toHaveLength(1)
282
+ expect(lastResponse.body.results[0].op).toBe('create-widget')
283
+ })
284
+
285
+ it('fails fast on unknown operation type', async () => {
286
+ await invoke('/batch', 'POST', {
287
+ name: 'test-canvas',
288
+ operations: [
289
+ { op: 'explode-widget' },
290
+ ],
291
+ })
292
+
293
+ expect(lastResponse.status).toBe(400)
294
+ expect(lastResponse.body.success).toBe(false)
295
+ expect(lastResponse.body.failedAt).toBe(0)
296
+ expect(lastResponse.body.error).toContain('Unknown operation')
297
+ })
298
+
299
+ it('rejects empty operations array', async () => {
300
+ await invoke('/batch', 'POST', {
301
+ name: 'test-canvas',
302
+ operations: [],
303
+ })
304
+
305
+ expect(lastResponse.status).toBe(400)
306
+ expect(lastResponse.body.error).toContain('non-empty')
307
+ })
308
+
309
+ it('rejects missing canvas name', async () => {
310
+ await invoke('/batch', 'POST', {
311
+ operations: [{ op: 'create-widget', type: 'sticky-note' }],
312
+ })
313
+
314
+ expect(lastResponse.status).toBe(400)
315
+ expect(lastResponse.body.error).toContain('Canvas name')
316
+ })
317
+
318
+ it('rejects batch exceeding 200 operations', async () => {
319
+ const ops = Array.from({ length: 201 }, (_, i) => ({
320
+ op: 'create-widget', type: 'sticky-note', props: { text: `#${i}` },
321
+ }))
322
+
323
+ await invoke('/batch', 'POST', { name: 'test-canvas', operations: ops })
324
+
325
+ expect(lastResponse.status).toBe(400)
326
+ expect(lastResponse.body.error).toContain('200')
327
+ })
328
+
329
+ it('returns 404 for unknown canvas', async () => {
330
+ await invoke('/batch', 'POST', {
331
+ name: 'nonexistent',
332
+ operations: [{ op: 'create-widget', type: 'sticky-note' }],
333
+ })
334
+
335
+ expect(lastResponse.status).toBe(404)
336
+ })
337
+
338
+ it('supports full create-update-move-connect workflow', async () => {
339
+ // Create a pre-existing terminal widget for connectors
340
+ await invoke('/widget', 'POST', { name: 'test-canvas', type: 'terminal', props: {} })
341
+ const termId = lastResponse.body.widget.id
342
+
343
+ await invoke('/batch', 'POST', {
344
+ name: 'test-canvas',
345
+ operations: [
346
+ { op: 'create-widget', type: 'sticky-note', position: { x: 100, y: 100 }, props: { text: 'Draft' } },
347
+ { op: 'update-widget', widgetId: '$0', props: { text: 'Final', color: 'blue' } },
348
+ { op: 'move-widget', widgetId: '$0', position: { x: 500, y: 300 } },
349
+ { op: 'create-connector', startWidgetId: termId, endWidgetId: '$0', startAnchor: 'right', endAnchor: 'left' },
350
+ ],
351
+ })
352
+
353
+ expect(lastResponse.body.success).toBe(true)
354
+ expect(lastResponse.body.results).toHaveLength(4)
355
+ expect(lastResponse.body.results[0].op).toBe('create-widget')
356
+ expect(lastResponse.body.results[1].op).toBe('update-widget')
357
+ expect(lastResponse.body.results[2].op).toBe('move-widget')
358
+ expect(lastResponse.body.results[3].op).toBe('create-connector')
359
+ })
360
+
361
+ it('connector refs also get index refs', async () => {
362
+ await invoke('/widget', 'POST', { name: 'test-canvas', type: 'terminal', props: {} })
363
+ const termId = lastResponse.body.widget.id
364
+
365
+ await invoke('/batch', 'POST', {
366
+ name: 'test-canvas',
367
+ operations: [
368
+ { op: 'create-widget', type: 'sticky-note', props: { text: 'A' } },
369
+ { op: 'create-connector', startWidgetId: termId, endWidgetId: '$0', startAnchor: 'right', endAnchor: 'left' },
370
+ ],
371
+ })
372
+
373
+ expect(lastResponse.body.success).toBe(true)
374
+ const { refs } = lastResponse.body
375
+ // Op 0 = widget, Op 1 = connector — both get index refs
376
+ expect(refs['0']).toMatch(/^sticky-note-/)
377
+ expect(refs['1']).toMatch(/^connector-/)
378
+ })
379
+ })
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Terminal Config — per-terminal context files for agent awareness.
3
+ *
4
+ * Each terminal widget gets a config at `.storyboard/terminals/{hash}.json`
5
+ * that agents read on startup to understand their canvas context.
6
+ *
7
+ * Files are keyed by a stable hash (same as tmuxName) so renames don't break them.
8
+ * The canvasId/widgetId are stored inside the JSON payload.
9
+ *
10
+ * Connected widgets are stored as IDs only — full widget data is resolved
11
+ * from the materialized canvas state at read time to stay fresh.
12
+ */
13
+
14
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, symlinkSync, unlinkSync } from 'node:fs'
15
+ import { join, dirname } from 'node:path'
16
+ import { createHash } from 'node:crypto'
17
+ import { findByWorktree } from '../worktree/serverRegistry.js'
18
+ import { detectWorktreeName } from '../worktree/port.js'
19
+ import { readCurrentViewport } from './selectedWidgets.js'
20
+
21
+ const TERMINALS_DIR = '.storyboard/terminals'
22
+
23
+ let rootDir = process.cwd()
24
+
25
+ /** Initialize with the project root directory */
26
+ export function initTerminalConfig(root) {
27
+ rootDir = root
28
+ const dir = join(rootDir, TERMINALS_DIR)
29
+ if (!existsSync(dir)) {
30
+ mkdirSync(dir, { recursive: true })
31
+ }
32
+ }
33
+
34
+ /** Read storyboard.config.json for devDomain */
35
+ function readDevDomain() {
36
+ try {
37
+ const raw = readFileSync(join(rootDir, 'storyboard.config.json'), 'utf8')
38
+ return JSON.parse(raw).devDomain || 'storyboard'
39
+ } catch { return 'storyboard' }
40
+ }
41
+
42
+ /** Detect worktree name */
43
+ function getWorktreeName() {
44
+ try {
45
+ // Check if we're in a .worktrees/ directory
46
+ const cwd = rootDir
47
+ const match = cwd.match(/\.worktrees\/([^/]+)/)
48
+ return match ? match[1] : 'main'
49
+ } catch { return 'main' }
50
+ }
51
+
52
+ /** Generate a stable filename from branch + canvasId + widgetId */
53
+ function configKey(branch, canvasId, widgetId) {
54
+ const input = `${branch}::${canvasId}::${widgetId}`
55
+ return createHash('sha256').update(input).digest('hex').slice(0, 16)
56
+ }
57
+
58
+ /** Get the config file path */
59
+ function configPath(branch, canvasId, widgetId) {
60
+ return join(rootDir, TERMINALS_DIR, `${configKey(branch, canvasId, widgetId)}.json`)
61
+ }
62
+
63
+ /** Atomic write — write to temp then rename */
64
+ function atomicWrite(filePath, data) {
65
+ const tmp = filePath + '.tmp'
66
+ writeFileSync(tmp, JSON.stringify(data, null, 2))
67
+ renameSync(tmp, filePath)
68
+ }
69
+
70
+ /**
71
+ * Pre-reserve terminal identity at widget creation time.
72
+ * Called from POST /widget when a terminal/agent widget is added to the canvas,
73
+ * BEFORE the widget renders or the WebSocket connects.
74
+ *
75
+ * Writes a minimal config file at `.storyboard/terminals/{widgetId}.json` so
76
+ * agents (especially hot-pool sessions) can find their identity immediately.
77
+ * The `reserved` flag marks this as a pre-reserve — writeTerminalConfig() will
78
+ * later overwrite it with the full config.
79
+ */
80
+ export function preReserveTerminalIdentity({ widgetId, preDisplayName, canvasId, branch, serverUrl }) {
81
+ const dir = join(rootDir, TERMINALS_DIR)
82
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
83
+
84
+ const fp = join(dir, `${widgetId}.json`)
85
+ const data = {
86
+ widgetId,
87
+ preDisplayName,
88
+ displayName: preDisplayName,
89
+ canvasId,
90
+ branch,
91
+ serverUrl: serverUrl || null,
92
+ reserved: true,
93
+ connectedWidgets: [],
94
+ messaging: null,
95
+ agentStatus: null,
96
+ viewport: readCurrentViewport(rootDir) || null,
97
+ updatedAt: new Date().toISOString(),
98
+ }
99
+ atomicWrite(fp, data)
100
+ }
101
+
102
+ /**
103
+ * Write or update a terminal config file.
104
+ * Called when a terminal widget is created or reconnected.
105
+ */
106
+ export function writeTerminalConfig({ branch, canvasId, widgetId, canvasFile = null, serverUrl = null, tmuxName = null, widgetProps = null, displayName = null }) {
107
+ const fp = configPath(branch, canvasId, widgetId)
108
+ const dir = dirname(fp)
109
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
110
+
111
+ let existing = {}
112
+ try {
113
+ existing = JSON.parse(readFileSync(fp, 'utf8'))
114
+ } catch { /* new file */ }
115
+
116
+ const worktree = getWorktreeName()
117
+ const devDomain = readDevDomain()
118
+
119
+ // Resolve server URL: use passed value, or query server registry, or default
120
+ if (!serverUrl) {
121
+ try {
122
+ const name = detectWorktreeName()
123
+ const servers = findByWorktree(name)
124
+ if (servers.length > 0) serverUrl = `http://localhost:${servers[0].port}`
125
+ } catch { /* empty */ }
126
+ }
127
+ if (!serverUrl) serverUrl = 'http://localhost:1234'
128
+
129
+ const config = {
130
+ ...existing,
131
+ widgetId,
132
+ displayName: displayName || existing.displayName || widgetProps?.prettyName || existing.widgetProps?.prettyName || null,
133
+ canvasId,
134
+ canvasFile: canvasFile || existing.canvasFile || null,
135
+ branch,
136
+ worktree,
137
+ devDomain,
138
+ serverUrl,
139
+ workingDirectory: rootDir,
140
+ deleted: false,
141
+ widgetProps: widgetProps || existing.widgetProps || null,
142
+ connectedWidgets: existing.connectedWidgets || [],
143
+ agentStatus: existing.agentStatus || null,
144
+ viewport: readCurrentViewport(rootDir) || existing.viewport || null,
145
+ updatedAt: new Date().toISOString(),
146
+ }
147
+
148
+ atomicWrite(fp, config)
149
+
150
+ // Create a widgetId-named symlink so agents can find their config directly
151
+ const hashName = `${configKey(branch, canvasId, widgetId)}.json`
152
+ const symPath = join(dir, `${widgetId}.json`)
153
+ try {
154
+ if (existsSync(symPath)) unlinkSync(symPath)
155
+ symlinkSync(hashName, symPath)
156
+ } catch { /* symlink creation is best-effort */ }
157
+
158
+ // Create a tmuxName-named symlink so agents can resolve identity via tmux session name
159
+ // (tmux session name is always available and never goes stale)
160
+ if (tmuxName) {
161
+ const tmuxSymPath = join(dir, `${tmuxName}.json`)
162
+ try {
163
+ if (existsSync(tmuxSymPath)) unlinkSync(tmuxSymPath)
164
+ symlinkSync(hashName, tmuxSymPath)
165
+ } catch { /* best-effort */ }
166
+ }
167
+
168
+ return config
169
+ }
170
+
171
+ /**
172
+ * Update connected widgets for a terminal.
173
+ * Called when connectors are added/removed.
174
+ * Stores full widget objects (id, type, props, position) so agents
175
+ * can read context directly without additional API calls.
176
+ */
177
+ export function updateTerminalConnections({ branch, canvasId, widgetId, connectedWidgets, widgetProps = null, messaging = null }) {
178
+ const fp = configPath(branch, canvasId, widgetId)
179
+ let config = {}
180
+ try {
181
+ config = JSON.parse(readFileSync(fp, 'utf8'))
182
+ } catch { /* file may not exist yet */ }
183
+
184
+ if (widgetProps) {
185
+ config.widgetProps = widgetProps
186
+ // Promote displayName from prettyName
187
+ if (widgetProps.prettyName) config.displayName = widgetProps.prettyName
188
+ }
189
+ config.connectedWidgets = connectedWidgets || []
190
+ config.messaging = messaging || null
191
+ config.viewport = readCurrentViewport(rootDir) || config.viewport || null
192
+ config.updatedAt = new Date().toISOString()
193
+
194
+ atomicWrite(fp, config)
195
+ return config
196
+ }
197
+
198
+ /**
199
+ * Mark a terminal config as deleted (tombstone).
200
+ * Called when a terminal widget is deleted.
201
+ */
202
+ export function markTerminalDeleted({ branch, canvasId, widgetId }) {
203
+ const fp = configPath(branch, canvasId, widgetId)
204
+ try {
205
+ const config = JSON.parse(readFileSync(fp, 'utf8'))
206
+ config.deleted = true
207
+ config.updatedAt = new Date().toISOString()
208
+ atomicWrite(fp, config)
209
+ } catch { /* file may not exist */ }
210
+ }
211
+
212
+ /**
213
+ * Unmark a terminal config as deleted (undo).
214
+ * Called when a deleted terminal widget is restored.
215
+ */
216
+ export function unmarkTerminalDeleted({ branch, canvasId, widgetId }) {
217
+ const fp = configPath(branch, canvasId, widgetId)
218
+ try {
219
+ const config = JSON.parse(readFileSync(fp, 'utf8'))
220
+ config.deleted = false
221
+ config.updatedAt = new Date().toISOString()
222
+ atomicWrite(fp, config)
223
+ } catch { /* file may not exist */ }
224
+ }
225
+
226
+ /**
227
+ * Read a terminal config. Connected widgets are already inline —
228
+ * no additional resolution needed.
229
+ */
230
+ export function readTerminalConfig({ branch, canvasId, widgetId }) {
231
+ const fp = configPath(branch, canvasId, widgetId)
232
+ try {
233
+ return JSON.parse(readFileSync(fp, 'utf8'))
234
+ } catch {
235
+ return null
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Update agent status in the terminal config.
241
+ * Called by the signal endpoint.
242
+ */
243
+ export function updateAgentStatus({ branch, canvasId, widgetId, status, message = null, data = null }) {
244
+ const fp = configPath(branch, canvasId, widgetId)
245
+ let config = {}
246
+ try {
247
+ config = JSON.parse(readFileSync(fp, 'utf8'))
248
+ } catch { /* may not exist */ }
249
+
250
+ config.agentStatus = {
251
+ status,
252
+ message,
253
+ data,
254
+ updatedAt: new Date().toISOString(),
255
+ }
256
+ config.updatedAt = new Date().toISOString()
257
+
258
+ atomicWrite(fp, config)
259
+ return config
260
+ }
261
+
262
+ /**
263
+ * Read a terminal config by widget ID (searches by symlink).
264
+ * @param {string} widgetId
265
+ * @returns {Object|null}
266
+ */
267
+ export function readTerminalConfigById(widgetId) {
268
+ const dir = join(rootDir, TERMINALS_DIR)
269
+ const symPath = join(dir, `${widgetId}.json`)
270
+ try {
271
+ return JSON.parse(readFileSync(symPath, 'utf8'))
272
+ } catch {
273
+ return null
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Append a pending message to a terminal's config.
279
+ * Messages are delivered when the agent starts or reconnects.
280
+ * @param {string} widgetId
281
+ * @param {{ from: string|null, fromName: string, message: string, createdAt: string }} msg
282
+ */
283
+ export function updatePendingMessages(widgetId, msg) {
284
+ const config = readTerminalConfigById(widgetId)
285
+ if (!config) return
286
+
287
+ if (!Array.isArray(config.pendingMessages)) config.pendingMessages = []
288
+ config.pendingMessages.push(msg)
289
+ config.updatedAt = new Date().toISOString()
290
+
291
+ // Write back using the config's known path
292
+ const fp = configPath(config.branch || 'unknown', config.canvasId || 'unknown', widgetId)
293
+ atomicWrite(fp, config)
294
+ }
295
+
296
+ /**
297
+ * Atomically read and clear pending messages from a terminal config.
298
+ * Returns the messages array (empty if none). Safe to call from any process.
299
+ * @param {string} widgetId
300
+ * @returns {Array<{ from: string|null, fromName: string, message: string, createdAt: string }>}
301
+ */
302
+ export function takePendingMessages(widgetId) {
303
+ const config = readTerminalConfigById(widgetId)
304
+ if (!config?.pendingMessages?.length) return []
305
+
306
+ const messages = config.pendingMessages
307
+ config.pendingMessages = []
308
+ config.updatedAt = new Date().toISOString()
309
+
310
+ // Write back via hash path (not symlink) to preserve symlink integrity
311
+ const fp = configPath(config.branch || 'unknown', config.canvasId || 'unknown', widgetId)
312
+ atomicWrite(fp, config)
313
+ return messages
314
+ }
315
+
316
+ /**
317
+ * Save the latest output from an agent for peers to read.
318
+ * @param {string} widgetId
319
+ * @param {{ content: string, summary: string, updatedAt: string }} output
320
+ */
321
+ export function updateLatestOutput(widgetId, output) {
322
+ const config = readTerminalConfigById(widgetId)
323
+ if (!config) return
324
+
325
+ config.latestOutput = output
326
+ config.updatedAt = new Date().toISOString()
327
+
328
+ const fp = configPath(config.branch || 'unknown', config.canvasId || 'unknown', widgetId)
329
+ atomicWrite(fp, config)
330
+ }
@@ -112,7 +112,7 @@ export function initRegistry(root, options = {}) {
112
112
  registryPath = join(dir, REGISTRY_FILE)
113
113
 
114
114
  // Ensure .storyboard/ exists
115
- try { mkdirSync(dir, { recursive: true }) } catch {}
115
+ try { mkdirSync(dir, { recursive: true }) } catch { /* empty */ }
116
116
 
117
117
  // Load persisted state
118
118
  try {
@@ -305,6 +305,44 @@ export function killSession(tmuxName) {
305
305
  persist()
306
306
  }
307
307
 
308
+ /**
309
+ * Bulk cleanup — kill all sessions matching the given statuses.
310
+ * Returns { removed, remaining: { live, background, archived, total } }.
311
+ */
312
+ export function bulkCleanup({ statuses }) {
313
+ const statusSet = new Set(statuses)
314
+ const toKill = []
315
+
316
+ for (const [name, entry] of sessions) {
317
+ if (statusSet.has(entry.status)) {
318
+ toKill.push(name)
319
+ }
320
+ }
321
+
322
+ for (const name of toKill) {
323
+ cancelOrphanTimer(name)
324
+ killTmuxSession(name)
325
+ sessions.delete(name)
326
+ }
327
+
328
+ if (toKill.length > 0) persist()
329
+
330
+ return { removed: toKill.length, remaining: getSessionStats() }
331
+ }
332
+
333
+ /**
334
+ * Get session counts by status.
335
+ */
336
+ export function getSessionStats() {
337
+ let live = 0, background = 0, archived = 0
338
+ for (const entry of sessions.values()) {
339
+ if (entry.status === 'live') live++
340
+ else if (entry.status === 'background') background++
341
+ else if (entry.status === 'archived') archived++
342
+ }
343
+ return { live, background, archived, total: live + background + archived }
344
+ }
345
+
308
346
  /**
309
347
  * Get a session entry by tmux name.
310
348
  */
@@ -353,7 +391,7 @@ function persist() {
353
391
  try {
354
392
  const entries = Array.from(sessions.values())
355
393
  writeFileSync(registryPath, JSON.stringify(entries, null, 2))
356
- } catch {}
394
+ } catch { /* empty */ }
357
395
  }
358
396
 
359
397
  function listTmuxSessions() {
@@ -375,7 +413,7 @@ function listTmuxSessions() {
375
413
  function killTmuxSession(name) {
376
414
  try {
377
415
  execSync(`tmux kill-session -t "${name}" 2>/dev/null`, { stdio: 'ignore' })
378
- } catch {}
416
+ } catch { /* empty */ }
379
417
  }
380
418
 
381
419
  function armOrphanTimer(tmuxName, delayMs) {