@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
@@ -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,331 @@
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, lstatSync } from 'node:fs'
15
+ import { join, dirname } from 'node:path'
16
+ import { createHash } from 'node:crypto'
17
+ import { execSync } from 'node:child_process'
18
+ import { findByWorktree } from '../worktree/serverRegistry.js'
19
+ import { detectWorktreeName } from '../worktree/port.js'
20
+ import { readCurrentViewport } from './selectedWidgets.js'
21
+
22
+ const TERMINALS_DIR = '.storyboard/terminals'
23
+
24
+ let rootDir = process.cwd()
25
+
26
+ /** Initialize with the project root directory */
27
+ export function initTerminalConfig(root) {
28
+ rootDir = root
29
+ const dir = join(rootDir, TERMINALS_DIR)
30
+ if (!existsSync(dir)) {
31
+ mkdirSync(dir, { recursive: true })
32
+ }
33
+ }
34
+
35
+ /** Read storyboard.config.json for devDomain */
36
+ function readDevDomain() {
37
+ try {
38
+ const raw = readFileSync(join(rootDir, 'storyboard.config.json'), 'utf8')
39
+ return JSON.parse(raw).devDomain || 'storyboard'
40
+ } catch { return 'storyboard' }
41
+ }
42
+
43
+ /** Detect worktree name */
44
+ function getWorktreeName() {
45
+ try {
46
+ // Check if we're in a .worktrees/ directory
47
+ const cwd = rootDir
48
+ const match = cwd.match(/\.worktrees\/([^/]+)/)
49
+ return match ? match[1] : 'main'
50
+ } catch { return 'main' }
51
+ }
52
+
53
+ /** Generate a stable filename from branch + canvasId + widgetId */
54
+ function configKey(branch, canvasId, widgetId) {
55
+ const input = `${branch}::${canvasId}::${widgetId}`
56
+ return createHash('sha256').update(input).digest('hex').slice(0, 16)
57
+ }
58
+
59
+ /** Get the config file path */
60
+ function configPath(branch, canvasId, widgetId) {
61
+ return join(rootDir, TERMINALS_DIR, `${configKey(branch, canvasId, widgetId)}.json`)
62
+ }
63
+
64
+ /** Atomic write — write to temp then rename */
65
+ function atomicWrite(filePath, data) {
66
+ const tmp = filePath + '.tmp'
67
+ writeFileSync(tmp, JSON.stringify(data, null, 2))
68
+ renameSync(tmp, filePath)
69
+ }
70
+
71
+ /**
72
+ * Pre-reserve terminal identity at widget creation time.
73
+ * Called from POST /widget when a terminal/agent widget is added to the canvas,
74
+ * BEFORE the widget renders or the WebSocket connects.
75
+ *
76
+ * Writes a minimal config file at `.storyboard/terminals/{widgetId}.json` so
77
+ * agents (especially hot-pool sessions) can find their identity immediately.
78
+ * The `reserved` flag marks this as a pre-reserve — writeTerminalConfig() will
79
+ * later overwrite it with the full config.
80
+ */
81
+ export function preReserveTerminalIdentity({ widgetId, preDisplayName, canvasId, branch, serverUrl }) {
82
+ const dir = join(rootDir, TERMINALS_DIR)
83
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
84
+
85
+ const fp = join(dir, `${widgetId}.json`)
86
+ const data = {
87
+ widgetId,
88
+ preDisplayName,
89
+ displayName: preDisplayName,
90
+ canvasId,
91
+ branch,
92
+ serverUrl: serverUrl || null,
93
+ reserved: true,
94
+ connectedWidgets: [],
95
+ messaging: null,
96
+ agentStatus: null,
97
+ viewport: readCurrentViewport(rootDir) || null,
98
+ updatedAt: new Date().toISOString(),
99
+ }
100
+ atomicWrite(fp, data)
101
+ }
102
+
103
+ /**
104
+ * Write or update a terminal config file.
105
+ * Called when a terminal widget is created or reconnected.
106
+ */
107
+ export function writeTerminalConfig({ branch, canvasId, widgetId, canvasFile = null, serverUrl = null, tmuxName = null, widgetProps = null, displayName = null }) {
108
+ const fp = configPath(branch, canvasId, widgetId)
109
+ const dir = dirname(fp)
110
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
111
+
112
+ let existing = {}
113
+ try {
114
+ existing = JSON.parse(readFileSync(fp, 'utf8'))
115
+ } catch { /* new file */ }
116
+
117
+ const worktree = getWorktreeName()
118
+ const devDomain = readDevDomain()
119
+
120
+ // Resolve server URL: use passed value, or query server registry, or default
121
+ if (!serverUrl) {
122
+ try {
123
+ const name = detectWorktreeName()
124
+ const servers = findByWorktree(name)
125
+ if (servers.length > 0) serverUrl = `http://localhost:${servers[0].port}`
126
+ } catch {}
127
+ }
128
+ if (!serverUrl) serverUrl = 'http://localhost:1234'
129
+
130
+ const config = {
131
+ ...existing,
132
+ widgetId,
133
+ displayName: displayName || existing.displayName || widgetProps?.prettyName || existing.widgetProps?.prettyName || null,
134
+ canvasId,
135
+ canvasFile: canvasFile || existing.canvasFile || null,
136
+ branch,
137
+ worktree,
138
+ devDomain,
139
+ serverUrl,
140
+ workingDirectory: rootDir,
141
+ deleted: false,
142
+ widgetProps: widgetProps || existing.widgetProps || null,
143
+ connectedWidgets: existing.connectedWidgets || [],
144
+ agentStatus: existing.agentStatus || null,
145
+ viewport: readCurrentViewport(rootDir) || existing.viewport || null,
146
+ updatedAt: new Date().toISOString(),
147
+ }
148
+
149
+ atomicWrite(fp, config)
150
+
151
+ // Create a widgetId-named symlink so agents can find their config directly
152
+ const hashName = `${configKey(branch, canvasId, widgetId)}.json`
153
+ const symPath = join(dir, `${widgetId}.json`)
154
+ try {
155
+ if (existsSync(symPath)) unlinkSync(symPath)
156
+ symlinkSync(hashName, symPath)
157
+ } catch { /* symlink creation is best-effort */ }
158
+
159
+ // Create a tmuxName-named symlink so agents can resolve identity via tmux session name
160
+ // (tmux session name is always available and never goes stale)
161
+ if (tmuxName) {
162
+ const tmuxSymPath = join(dir, `${tmuxName}.json`)
163
+ try {
164
+ if (existsSync(tmuxSymPath)) unlinkSync(tmuxSymPath)
165
+ symlinkSync(hashName, tmuxSymPath)
166
+ } catch { /* best-effort */ }
167
+ }
168
+
169
+ return config
170
+ }
171
+
172
+ /**
173
+ * Update connected widgets for a terminal.
174
+ * Called when connectors are added/removed.
175
+ * Stores full widget objects (id, type, props, position) so agents
176
+ * can read context directly without additional API calls.
177
+ */
178
+ export function updateTerminalConnections({ branch, canvasId, widgetId, connectedWidgets, widgetProps = null, messaging = null }) {
179
+ const fp = configPath(branch, canvasId, widgetId)
180
+ let config = {}
181
+ try {
182
+ config = JSON.parse(readFileSync(fp, 'utf8'))
183
+ } catch { /* file may not exist yet */ }
184
+
185
+ if (widgetProps) {
186
+ config.widgetProps = widgetProps
187
+ // Promote displayName from prettyName
188
+ if (widgetProps.prettyName) config.displayName = widgetProps.prettyName
189
+ }
190
+ config.connectedWidgets = connectedWidgets || []
191
+ config.messaging = messaging || null
192
+ config.viewport = readCurrentViewport(rootDir) || config.viewport || null
193
+ config.updatedAt = new Date().toISOString()
194
+
195
+ atomicWrite(fp, config)
196
+ return config
197
+ }
198
+
199
+ /**
200
+ * Mark a terminal config as deleted (tombstone).
201
+ * Called when a terminal widget is deleted.
202
+ */
203
+ export function markTerminalDeleted({ branch, canvasId, widgetId }) {
204
+ const fp = configPath(branch, canvasId, widgetId)
205
+ try {
206
+ const config = JSON.parse(readFileSync(fp, 'utf8'))
207
+ config.deleted = true
208
+ config.updatedAt = new Date().toISOString()
209
+ atomicWrite(fp, config)
210
+ } catch { /* file may not exist */ }
211
+ }
212
+
213
+ /**
214
+ * Unmark a terminal config as deleted (undo).
215
+ * Called when a deleted terminal widget is restored.
216
+ */
217
+ export function unmarkTerminalDeleted({ branch, canvasId, widgetId }) {
218
+ const fp = configPath(branch, canvasId, widgetId)
219
+ try {
220
+ const config = JSON.parse(readFileSync(fp, 'utf8'))
221
+ config.deleted = false
222
+ config.updatedAt = new Date().toISOString()
223
+ atomicWrite(fp, config)
224
+ } catch { /* file may not exist */ }
225
+ }
226
+
227
+ /**
228
+ * Read a terminal config. Connected widgets are already inline —
229
+ * no additional resolution needed.
230
+ */
231
+ export function readTerminalConfig({ branch, canvasId, widgetId }) {
232
+ const fp = configPath(branch, canvasId, widgetId)
233
+ try {
234
+ return JSON.parse(readFileSync(fp, 'utf8'))
235
+ } catch {
236
+ return null
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Update agent status in the terminal config.
242
+ * Called by the signal endpoint.
243
+ */
244
+ export function updateAgentStatus({ branch, canvasId, widgetId, status, message = null, data = null }) {
245
+ const fp = configPath(branch, canvasId, widgetId)
246
+ let config = {}
247
+ try {
248
+ config = JSON.parse(readFileSync(fp, 'utf8'))
249
+ } catch { /* may not exist */ }
250
+
251
+ config.agentStatus = {
252
+ status,
253
+ message,
254
+ data,
255
+ updatedAt: new Date().toISOString(),
256
+ }
257
+ config.updatedAt = new Date().toISOString()
258
+
259
+ atomicWrite(fp, config)
260
+ return config
261
+ }
262
+
263
+ /**
264
+ * Read a terminal config by widget ID (searches by symlink).
265
+ * @param {string} widgetId
266
+ * @returns {Object|null}
267
+ */
268
+ export function readTerminalConfigById(widgetId) {
269
+ const dir = join(rootDir, TERMINALS_DIR)
270
+ const symPath = join(dir, `${widgetId}.json`)
271
+ try {
272
+ return JSON.parse(readFileSync(symPath, 'utf8'))
273
+ } catch {
274
+ return null
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Append a pending message to a terminal's config.
280
+ * Messages are delivered when the agent starts or reconnects.
281
+ * @param {string} widgetId
282
+ * @param {{ from: string|null, fromName: string, message: string, createdAt: string }} msg
283
+ */
284
+ export function updatePendingMessages(widgetId, msg) {
285
+ const config = readTerminalConfigById(widgetId)
286
+ if (!config) return
287
+
288
+ if (!Array.isArray(config.pendingMessages)) config.pendingMessages = []
289
+ config.pendingMessages.push(msg)
290
+ config.updatedAt = new Date().toISOString()
291
+
292
+ // Write back using the config's known path
293
+ const fp = configPath(config.branch || 'unknown', config.canvasId || 'unknown', widgetId)
294
+ atomicWrite(fp, config)
295
+ }
296
+
297
+ /**
298
+ * Atomically read and clear pending messages from a terminal config.
299
+ * Returns the messages array (empty if none). Safe to call from any process.
300
+ * @param {string} widgetId
301
+ * @returns {Array<{ from: string|null, fromName: string, message: string, createdAt: string }>}
302
+ */
303
+ export function takePendingMessages(widgetId) {
304
+ const config = readTerminalConfigById(widgetId)
305
+ if (!config?.pendingMessages?.length) return []
306
+
307
+ const messages = config.pendingMessages
308
+ config.pendingMessages = []
309
+ config.updatedAt = new Date().toISOString()
310
+
311
+ // Write back via hash path (not symlink) to preserve symlink integrity
312
+ const fp = configPath(config.branch || 'unknown', config.canvasId || 'unknown', widgetId)
313
+ atomicWrite(fp, config)
314
+ return messages
315
+ }
316
+
317
+ /**
318
+ * Save the latest output from an agent for peers to read.
319
+ * @param {string} widgetId
320
+ * @param {{ content: string, summary: string, updatedAt: string }} output
321
+ */
322
+ export function updateLatestOutput(widgetId, output) {
323
+ const config = readTerminalConfigById(widgetId)
324
+ if (!config) return
325
+
326
+ config.latestOutput = output
327
+ config.updatedAt = new Date().toISOString()
328
+
329
+ const fp = configPath(config.branch || 'unknown', config.canvasId || 'unknown', widgetId)
330
+ atomicWrite(fp, config)
331
+ }
@@ -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
  */