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