@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
package/src/modes.test.js CHANGED
@@ -359,7 +359,7 @@ describe('locked mode', () => {
359
359
  describe('tool registry', () => {
360
360
  const SAMPLE_TOOLS = {
361
361
  '*': [
362
- { id: 'viewfinder', label: 'Viewfinder', group: 'dev' },
362
+ { id: 'workspace', label: 'Workspace', group: 'dev' },
363
363
  { id: 'reset-params', label: 'Reset all params', group: 'dev' },
364
364
  ],
365
365
  'present': [
@@ -372,14 +372,14 @@ describe('tool registry', () => {
372
372
  initTools(SAMPLE_TOOLS)
373
373
  const tools = getToolsForMode('present')
374
374
  const ids = tools.map(t => t.id)
375
- expect(ids).toContain('viewfinder')
375
+ expect(ids).toContain('workspace')
376
376
  expect(ids).toContain('comments-toggle')
377
377
  })
378
378
 
379
379
  it('wildcard tools appear in all modes', () => {
380
380
  initTools(SAMPLE_TOOLS)
381
381
  const protoTools = getToolsForMode('prototype')
382
- expect(protoTools.map(t => t.id)).toContain('viewfinder')
382
+ expect(protoTools.map(t => t.id)).toContain('workspace')
383
383
  })
384
384
 
385
385
  it('mode-specific tools only appear in their mode', () => {
@@ -390,7 +390,7 @@ describe('tool registry', () => {
390
390
 
391
391
  it('tools start with default state', () => {
392
392
  initTools(SAMPLE_TOOLS)
393
- const state = getToolState('viewfinder')
393
+ const state = getToolState('workspace')
394
394
  expect(state).toEqual({
395
395
  enabled: true,
396
396
  active: false,
@@ -13,17 +13,65 @@
13
13
  import { installHideParamListener } from './interceptHideParams.js'
14
14
  import { installHistorySync } from './hideMode.js'
15
15
  import { installBodyClassSync } from './bodyClasses.js'
16
- import { initCommentsConfig, isCommentsEnabled } from './comments/config.js'
17
- import { initFeatureFlags } from './featureFlags.js'
18
- import { initPlugins } from './plugins.js'
19
- import { initUIConfig } from './uiConfig.js'
20
- import { initCanvasConfig } from './canvasConfig.js'
21
- import { initCommandPaletteConfig } from './commandPaletteConfig.js'
22
- import { initToolbarConfig } from './toolbarConfigStore.js'
23
- import { initCustomerModeConfig } from './customerModeConfig.js'
16
+ import {
17
+ initCommentsConfig, isCommentsEnabled,
18
+ initFeatureFlags,
19
+ initPlugins,
20
+ initUIConfig,
21
+ initCanvasConfig,
22
+ initCommandPaletteConfig,
23
+ initToolbarConfig, consumeClientToolbarOverrides,
24
+ initCustomerModeConfig,
25
+ getConfig,
26
+ } from '@dfosco/storyboard-core'
24
27
 
25
28
  let _mounted = false
26
29
 
30
+ const CHROME_HIDDEN_KEY = 'sb-chrome-hidden'
31
+
32
+ /**
33
+ * Migrate localStorage keys renamed in 4.3.0.
34
+ * Runs once at startup, idempotent: only copies if new key doesn't exist yet.
35
+ */
36
+ function migrateLocalStorageKeys() {
37
+ if (typeof localStorage === 'undefined') return
38
+ const renames = [
39
+ ['sb-viewfinder-starred', 'sb-workspace-starred'],
40
+ ['sb-viewfinder-recent', 'sb-workspace-recent'],
41
+ ['sb-viewfinder-group-folders', 'sb-workspace-group-folders'],
42
+ ]
43
+ for (const [oldKey, newKey] of renames) {
44
+ if (localStorage.getItem(newKey) === null && localStorage.getItem(oldKey) !== null) {
45
+ localStorage.setItem(newKey, localStorage.getItem(oldKey))
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Restore the saved chrome-hidden state immediately, before React mounts.
52
+ * Prevents a flash of toolbars appearing then disappearing.
53
+ */
54
+ function applyEarlyChromeState() {
55
+ if (typeof document === 'undefined' || typeof localStorage === 'undefined') return
56
+ const hidden = localStorage.getItem(CHROME_HIDDEN_KEY) === '1'
57
+ if (hidden) {
58
+ document.documentElement.classList.add('storyboard-chrome-hidden')
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Watch for changes to the storyboard-chrome-hidden class and persist to
64
+ * localStorage. Works regardless of which code path toggles the class.
65
+ */
66
+ function installChromeStatePersistence() {
67
+ if (typeof document === 'undefined' || typeof localStorage === 'undefined') return
68
+ const observer = new MutationObserver(() => {
69
+ const hidden = document.documentElement.classList.contains('storyboard-chrome-hidden')
70
+ localStorage.setItem(CHROME_HIDDEN_KEY, hidden ? '1' : '0')
71
+ })
72
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
73
+ }
74
+
27
75
  /**
28
76
  * Apply the saved theme to Primer CSS attributes immediately, before
29
77
  * React or Svelte mount. This prevents a flash of wrong-theme content.
@@ -138,6 +186,12 @@ export async function mountStoryboardCore(config = {}, options = {}) {
138
186
  const basePath = options.basePath || '/'
139
187
  const customHandlers = options.handlers || {}
140
188
 
189
+ // Migrate renamed localStorage keys (4.3.0: viewfinder → workspace)
190
+ migrateLocalStorageKeys()
191
+
192
+ // Apply saved chrome-hidden state immediately — before React mount
193
+ applyEarlyChromeState()
194
+
141
195
  // Apply saved theme to DOM immediately — before Svelte/React mount
142
196
  applyEarlyTheme()
143
197
 
@@ -145,60 +199,104 @@ export async function mountStoryboardCore(config = {}, options = {}) {
145
199
  installHideParamListener()
146
200
  installHistorySync()
147
201
  installBodyClassSync()
202
+ installChromeStatePersistence()
148
203
 
149
- // Initialize config-driven systems
150
- if (config.featureFlags) {
204
+ // Initialize config-driven systems.
205
+ // The unified config store is already seeded by the virtual module's initConfig().
206
+ // Individual stores are initialized here for backward compatibility — consumers
207
+ // that import directly from these stores still work.
208
+ const uc = getConfig()
209
+
210
+ if (uc.featureFlags && Object.keys(uc.featureFlags).length > 0) {
211
+ initFeatureFlags(uc.featureFlags)
212
+ } else if (config.featureFlags) {
151
213
  initFeatureFlags(config.featureFlags)
152
214
  }
153
215
 
154
- if (config.plugins) {
216
+ if (uc.plugins && Object.keys(uc.plugins).length > 0) {
217
+ initPlugins(uc.plugins)
218
+ } else if (config.plugins) {
155
219
  initPlugins(config.plugins)
156
220
  }
157
221
 
158
- if (config.ui) {
222
+ if (uc.ui && Object.keys(uc.ui).length > 0) {
223
+ initUIConfig(uc.ui)
224
+ } else if (config.ui) {
159
225
  initUIConfig(config.ui)
160
226
  }
161
227
 
162
- if (config.canvas) {
228
+ if (uc.canvas && Object.keys(uc.canvas).length > 0) {
229
+ initCanvasConfig(uc.canvas)
230
+ } else if (config.canvas) {
163
231
  initCanvasConfig(config.canvas)
164
232
  }
165
233
 
166
234
  // Load and merge command palette config.
167
- // Core defaults come from commandpalette.config.json (bundled).
168
- // Client can provide overrides via config.commandPalette (legacy) or a commandpalette.config.json at repo root.
169
- const defaultCmdPaletteConfig = (await import('../commandpalette.config.json')).default
170
- if (config.commandPalette) {
171
- const { deepMerge: deepMergeCp } = await import('./loader.js')
172
- initCommandPaletteConfig(deepMergeCp(defaultCmdPaletteConfig, config.commandPalette))
235
+ // If the unified store has commandPalette data, use it directly.
236
+ // Otherwise fall back to legacy merging with bundled defaults.
237
+ const ucCmdPalette = uc.commandPalette
238
+ if (ucCmdPalette && Object.keys(ucCmdPalette).length > 0) {
239
+ initCommandPaletteConfig(ucCmdPalette)
173
240
  } else {
174
- initCommandPaletteConfig({ ...defaultCmdPaletteConfig })
241
+ const defaultCmdPaletteConfig = (await import('../commandpalette.config.json')).default
242
+ if (config.commandPalette) {
243
+ const merged = { ...defaultCmdPaletteConfig, ...config.commandPalette }
244
+ if (config.commandPalette.sections && defaultCmdPaletteConfig.sections) {
245
+ const clientIds = new Set(config.commandPalette.sections.map(s => s.id))
246
+ const preserved = defaultCmdPaletteConfig.sections.filter(s => !clientIds.has(s.id))
247
+ merged.sections = [...config.commandPalette.sections, ...preserved]
248
+ }
249
+ initCommandPaletteConfig(merged)
250
+ } else {
251
+ initCommandPaletteConfig({ ...defaultCmdPaletteConfig })
252
+ }
175
253
  }
176
254
 
177
255
  // Initialize customer mode config
178
- if (config.customerMode) {
256
+ if (uc.customerMode && Object.keys(uc.customerMode).length > 0) {
257
+ initCustomerModeConfig(uc.customerMode)
258
+ } else if (config.customerMode) {
179
259
  initCustomerModeConfig(config.customerMode)
180
260
  }
181
261
 
182
262
  // Initialize comments config (framework-agnostic)
183
- if (config.comments) {
184
- initCommentsConfig(config, { basePath })
263
+ const commentsConfig = uc.comments && Object.keys(uc.comments).length > 0 ? uc.comments : config.comments
264
+ if (commentsConfig) {
265
+ initCommentsConfig({ ...config, comments: commentsConfig }, { basePath })
185
266
  }
186
267
 
187
268
  // Inject compiled UI styles (await to prevent late restyle / FOUC)
188
269
  await injectUIStyles()
189
270
 
190
- // Load and merge toolbar config.
191
- // Core defaults come from toolbar.config.json (bundled).
192
- // Client can provide overrides via config.toolbar or a toolbar.config.json at repo root.
193
- const { deepMerge } = await import('./loader.js')
194
- const defaultConfig = (await import('../toolbar.config.json')).default
195
- let toolbarConfig = config.toolbar
196
- ? deepMerge(defaultConfig, config.toolbar)
197
- : { ...defaultConfig }
198
-
199
- // Inject repository URL from storyboard.config.json into the toolbar config
200
- if (config.repository?.owner && config.repository?.name) {
201
- const repoUrl = `https://github.com/${config.repository.owner}/${config.repository.name}`
271
+ // Load toolbar config from the unified store.
272
+ // The unified store already has core defaults merged with client overrides.
273
+ // Fall back to legacy merging if unified store wasn't seeded.
274
+ const { deepMerge } = await import('@dfosco/storyboard-core')
275
+ let toolbarConfig = uc.toolbar && Object.keys(uc.toolbar).length > 0
276
+ ? { ...uc.toolbar }
277
+ : null
278
+
279
+ if (!toolbarConfig) {
280
+ // Legacy path: unified store not seeded, merge manually
281
+ const defaultConfig = (await import('../toolbar.config.json')).default
282
+ const clientOverrides = consumeClientToolbarOverrides()
283
+ const explicitToolbar = config.toolbar
284
+
285
+ if (explicitToolbar && clientOverrides) {
286
+ toolbarConfig = deepMerge(deepMerge(defaultConfig, clientOverrides), explicitToolbar)
287
+ } else if (explicitToolbar) {
288
+ toolbarConfig = deepMerge(defaultConfig, explicitToolbar)
289
+ } else if (clientOverrides) {
290
+ toolbarConfig = deepMerge(defaultConfig, clientOverrides)
291
+ } else {
292
+ toolbarConfig = { ...defaultConfig }
293
+ }
294
+ }
295
+
296
+ // Inject repository URL into the toolbar config
297
+ const repo = uc.repository || config.repository
298
+ if (repo?.owner && repo?.name) {
299
+ const repoUrl = `https://github.com/${repo.owner}/${repo.name}`
202
300
 
203
301
  // New tools schema
204
302
  if (toolbarConfig.tools?.repository) {
@@ -55,6 +55,9 @@ export function buildCommandItems(mode, basePath) {
55
55
  if (state === 'disabled' || state === 'hidden') continue
56
56
  }
57
57
 
58
+ // Skip items hidden from search
59
+ if (action.hideFromCommandPaletteSearch) continue
60
+
58
61
  if (action.type === 'submenu') {
59
62
  // Flatten submenu children into individual items
60
63
  const children = getActionChildren(action.id)
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi } from 'vitest'
2
2
 
3
- // Mock loader and viewfinder before importing providers
3
+ // Mock loader and workspace before importing providers
4
4
  vi.mock('./loader.js', () => ({
5
5
  listStories: vi.fn(() => ['button', 'card']),
6
6
  getStoryData: vi.fn((name) => ({
@@ -34,7 +34,7 @@ vi.mock('./loader.js', () => ({
34
34
 
35
35
  vi.mock('./commandActions.js', () => ({
36
36
  getActionsForMode: vi.fn(() => [
37
- { id: 'core/viewfinder', label: 'Go to Viewfinder', type: 'link', url: '/', toolKey: 'viewfinder' },
37
+ { id: 'core/workspace', label: 'Go to Workspace', type: 'link', url: '/', toolKey: 'workspace' },
38
38
  { id: 'core/docs', label: 'Documentation', type: 'default', toolKey: 'docs' },
39
39
  { id: 'core/devtools', label: 'DevTools', type: 'submenu', toolKey: 'devtools' },
40
40
  { type: 'header', label: 'Command Menu' },
@@ -14,11 +14,13 @@
14
14
 
15
15
  import http from 'node:http'
16
16
  import { spawn } from 'node:child_process'
17
- import { existsSync, readFileSync, mkdirSync } from 'node:fs'
18
- import { resolve, join, dirname } from 'node:path'
19
- import { getPort, portsFilePath, repoRoot, worktreeDir, listWorktrees } from '../worktree/port.js'
17
+ import { existsSync } from 'node:fs'
18
+ import { resolve, join } from 'node:path'
19
+ import { getPort, releasePort, repoRoot } from '../worktree/port.js'
20
20
  import { generateCaddyfile, generateRouteConfig, upsertCaddyRoute, isCaddyRunning, reloadCaddy, readDevDomain } from '../cli/proxy.js'
21
21
  import { compactAll } from '../canvas/compact.js'
22
+ import { register, unregister, generateId, list, findByWorktree } from '../worktree/serverRegistry.js'
23
+ import { createDevLogger } from '../logger/devLogger.js'
22
24
 
23
25
  const SERVER_PORT_BASE = 4100
24
26
 
@@ -46,6 +48,12 @@ const processes = new Map()
46
48
  /** Route handlers: prefix → handler function */
47
49
  const routeHandlers = new Map()
48
50
 
51
+ // ─── Dev Logger ───
52
+
53
+ let _currentBranch = null
54
+ try { _currentBranch = (await import('node:child_process')).execSync('git branch --show-current', { encoding: 'utf8', cwd: repoRoot() }).trim() } catch { /* empty */ }
55
+ const devLogger = createDevLogger({ root: repoRoot(), devDomain: DEV_DOMAIN.replace('.localhost', ''), branch: _currentBranch })
56
+
49
57
  // ─── JSON helpers ───
50
58
 
51
59
  function sendJson(res, status, data) {
@@ -54,6 +62,21 @@ function sendJson(res, status, data) {
54
62
  res.end(body)
55
63
  }
56
64
 
65
+ function sendJsonLogged(res, status, data) {
66
+ sendJson(res, status, data)
67
+ if (status >= 400) {
68
+ const ctx = res.__sbLogCtx || {}
69
+ devLogger.logResponse({
70
+ status,
71
+ method: ctx.method || 'UNKNOWN',
72
+ url: ctx.url || '',
73
+ route: ctx.route || null,
74
+ subRoute: ctx.subRoute || null,
75
+ error: data?.error || null,
76
+ })
77
+ }
78
+ }
79
+
57
80
  async function parseBody(req) {
58
81
  const chunks = []
59
82
  for await (const chunk of req) chunks.push(chunk)
@@ -117,15 +140,23 @@ function spawnVite(branch) {
117
140
  stdio: ['ignore', 'pipe', 'pipe'],
118
141
  })
119
142
 
120
- const entry = { child, port, status: 'starting', cwd, branch }
143
+ const entry = { child, port, status: 'starting', cwd, branch, serverId: generateId() }
121
144
  processes.set(branch, entry)
122
145
 
146
+ // Register in persistent server registry
147
+ register({ id: entry.serverId, worktree: branch, pid: child.pid, port, background: true })
148
+
123
149
  // Detect ready state from stdout
124
150
  child.stdout.on('data', (data) => {
125
151
  const text = data.toString()
126
152
  const portMatch = text.match(/localhost:(\d+)/)
127
153
  if (portMatch) {
128
- entry.port = Number(portMatch[1])
154
+ const actualPort = Number(portMatch[1])
155
+ if (actualPort !== entry.port) {
156
+ entry.port = actualPort
157
+ // Re-register with the actual port Vite bound to
158
+ register({ id: entry.serverId, worktree: branch, pid: child.pid, port: actualPort, background: true })
159
+ }
129
160
  }
130
161
  if (text.includes('ready in')) {
131
162
  entry.status = 'ready'
@@ -135,9 +166,12 @@ function spawnVite(branch) {
135
166
 
136
167
  child.stderr.on('data', () => { /* suppress */ })
137
168
 
138
- child.on('exit', (code) => {
169
+ child.on('exit', (_code) => {
170
+ void _code
139
171
  entry.status = 'stopped'
140
172
  processes.delete(branch)
173
+ unregister(entry.serverId)
174
+ releasePort(branch)
141
175
  })
142
176
 
143
177
  return entry
@@ -167,13 +201,13 @@ function registerCaddyRoute(branch, port) {
167
201
  // POST /_storyboard/switch-branch
168
202
  routeHandlers.set('switch-branch', async (req, res, ctx) => {
169
203
  if (ctx.method !== 'POST') {
170
- sendJson(res, 405, { error: 'POST required' })
204
+ sendJsonLogged(res, 405, { error: 'POST required' })
171
205
  return
172
206
  }
173
207
 
174
208
  const { branch } = ctx.body
175
209
  if (!branch) {
176
- sendJson(res, 400, { error: 'branch is required' })
210
+ sendJsonLogged(res, 400, { error: 'branch is required' })
177
211
  return
178
212
  }
179
213
 
@@ -188,59 +222,77 @@ routeHandlers.set('switch-branch', async (req, res, ctx) => {
188
222
  if (existing && existing.status === 'ready') {
189
223
  const alive = await isPortReady(existing.port)
190
224
  if (alive) {
191
- sendJson(res, 200, { url: targetUrl, status: 'already_running' })
225
+ sendJsonLogged(res, 200, { url: targetUrl, status: 'already_running' })
192
226
  return
193
227
  }
194
228
  // Stale entry — remove it
195
229
  processes.delete(branch)
196
230
  }
197
231
 
198
- // Check if Vite is already running on the assigned port (started outside this server)
199
- const port = getPort(branch)
200
- if (await isPortReady(port)) {
201
- registerCaddyRoute(branch, port)
202
- sendJson(res, 200, { url: targetUrl, status: 'already_running' })
203
- return
232
+ // Check if Vite is already running in this server's process map
233
+ const _port = getPort(branch)
234
+ void _port
235
+ const existingInRegistry = findByWorktree(branch)
236
+ if (existingInRegistry.length > 0) {
237
+ const latest = existingInRegistry.reduce((a, b) =>
238
+ (a.startedAt || '') >= (b.startedAt || '') ? a : b
239
+ )
240
+ if (await isPortReady(latest.port)) {
241
+ registerCaddyRoute(branch, latest.port)
242
+ sendJsonLogged(res, 200, { url: targetUrl, status: 'already_running' })
243
+ return
244
+ }
204
245
  }
205
246
 
206
247
  // Check worktree exists
207
248
  const cwd = resolveWorktreeCwd(branch)
208
249
  if (!cwd) {
209
- sendJson(res, 404, { error: `Worktree not found: ${branch}. Create it with: npx storyboard dev ${branch}` })
250
+ sendJsonLogged(res, 404, { error: `Worktree not found: ${branch}. Create it with: npx storyboard dev ${branch}` })
210
251
  return
211
252
  }
212
253
 
213
254
  // Spawn Vite
214
255
  const entry = spawnVite(branch)
215
256
 
216
- // Wait for ready
217
- const ready = await waitForPort(entry.port, HEALTH_TIMEOUT)
257
+ // Wait for Vite to report ready via stdout (not TCP polling, which
258
+ // can false-positive on occupied ports from other processes)
259
+ const ready = await (async () => {
260
+ const start = Date.now()
261
+ while (Date.now() - start < HEALTH_TIMEOUT) {
262
+ if (entry.status === 'ready') return true
263
+ await new Promise(r => setTimeout(r, 300))
264
+ }
265
+ return false
266
+ })()
218
267
  if (!ready) {
219
268
  stopVite(branch)
220
- sendJson(res, 504, { error: `Vite server for ${branch} did not become ready in time` })
269
+ sendJsonLogged(res, 504, { error: `Vite server for ${branch} did not become ready in time` })
221
270
  return
222
271
  }
223
272
 
224
- sendJson(res, 200, { url: targetUrl, status: 'started' })
273
+ sendJsonLogged(res, 200, { url: targetUrl, status: 'started' })
225
274
  } catch (err) {
226
- sendJson(res, 500, { error: err.message })
275
+ sendJsonLogged(res, 500, { error: err.message })
227
276
  }
228
277
  })
229
278
 
230
279
  // GET /_storyboard/worktrees
231
280
  routeHandlers.set('worktrees', async (req, res) => {
232
281
  try {
233
- const pf = portsFilePath()
234
- if (!existsSync(pf)) { sendJson(res, 200, []); return }
235
- const ports = JSON.parse(readFileSync(pf, 'utf8'))
236
- const branches = Object.keys(ports).map(name => ({
237
- branch: name,
238
- folder: name === 'main' ? '' : `branch--${name}/`,
239
- port: ports[name],
240
- running: processes.has(name) ? processes.get(name).status : null,
282
+ // Use the server registry (live processes) instead of stale ports.json
283
+ const servers = list()
284
+ const branches = servers.map(srv => ({
285
+ branch: srv.worktree,
286
+ folder: srv.worktree === 'main' ? '' : `branch--${srv.worktree}/`,
287
+ port: srv.port,
288
+ running: processes.has(srv.worktree) ? processes.get(srv.worktree).status : 'background',
241
289
  }))
242
- sendJson(res, 200, branches)
243
- } catch { sendJson(res, 200, []) }
290
+ // Always include main even if no server is registered for it
291
+ if (!branches.some(b => b.branch === 'main')) {
292
+ branches.unshift({ branch: 'main', folder: '', port: 1234, running: null })
293
+ }
294
+ sendJsonLogged(res, 200, branches)
295
+ } catch { sendJsonLogged(res, 200, []) }
244
296
  })
245
297
 
246
298
  // GET /_storyboard/server/status
@@ -250,10 +302,10 @@ routeHandlers.set('server', async (req, res, ctx) => {
250
302
  for (const [branch, entry] of processes) {
251
303
  active.push({ branch, port: entry.port, status: entry.status })
252
304
  }
253
- sendJson(res, 200, { active, serverPort: SERVER_PORT })
305
+ sendJsonLogged(res, 200, { active, serverPort: SERVER_PORT })
254
306
  return
255
307
  }
256
- sendJson(res, 404, { error: 'Unknown server route' })
308
+ sendJsonLogged(res, 404, { error: 'Unknown server route' })
257
309
  })
258
310
 
259
311
  // ─── HTTP Server ───
@@ -280,6 +332,9 @@ const server = http.createServer(async (req, res) => {
280
332
  const prefix = slashIdx === -1 ? after : after.slice(0, slashIdx)
281
333
  const restPath = slashIdx === -1 ? '/' : after.slice(slashIdx)
282
334
 
335
+ // Attach route context for the logging sendJson wrapper
336
+ res.__sbLogCtx = { method: req.method, url: req.url, route: prefix, subRoute: restPath }
337
+
283
338
  const handler = routeHandlers.get(prefix)
284
339
  if (handler) {
285
340
  let body = {}
@@ -289,8 +344,7 @@ const server = http.createServer(async (req, res) => {
289
344
  try {
290
345
  await handler(req, res, { body, path: restPath, method: req.method })
291
346
  } catch (err) {
292
- console.error(`[storyboard-server] Error in ${prefix}:`, err)
293
- sendJson(res, 500, { error: err.message })
347
+ sendJsonLogged(res, 500, { error: err.message })
294
348
  }
295
349
  return
296
350
  }
@@ -298,18 +352,28 @@ const server = http.createServer(async (req, res) => {
298
352
 
299
353
  // Health check
300
354
  if (pathname === '/health') {
301
- sendJson(res, 200, { ok: true, devDomain: DEV_DOMAIN })
355
+ sendJsonLogged(res, 200, { ok: true, devDomain: DEV_DOMAIN })
302
356
  return
303
357
  }
304
358
 
305
- sendJson(res, 404, { error: 'Not found' })
359
+ // Set context for catch-all 404
360
+ res.__sbLogCtx = { method: req.method, url: req.url, route: null, subRoute: null }
361
+ sendJsonLogged(res, 404, { error: 'Not found' })
306
362
  })
307
363
 
308
364
  export function startServer(port = SERVER_PORT) {
309
- server.listen(port, () => {
310
- // port logged by CLI via onReady; suppress here to avoid duplicate output
365
+ return new Promise((resolve, reject) => {
366
+ server.on('error', (err) => {
367
+ if (err.code === 'EADDRINUSE') {
368
+ reject(Object.assign(new Error(`Port ${port} already in use`), { code: 'EADDRINUSE' }))
369
+ return
370
+ }
371
+ reject(err)
372
+ })
373
+ server.listen(port, () => {
374
+ resolve(server)
375
+ })
311
376
  })
312
- return server
313
377
  }
314
378
 
315
379
  /** Public API for spawning Vite from CLI (with stdout piping) */