@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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas HTTP API helpers for integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Wraps fetch calls to /_storyboard/canvas/* endpoints.
|
|
5
|
+
* Every call is automatically timed via perf.js.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as perf from './perf.js'
|
|
9
|
+
|
|
10
|
+
let baseUrl = 'http://localhost:1234'
|
|
11
|
+
|
|
12
|
+
/** Set the base URL for all API calls. */
|
|
13
|
+
export function setBaseUrl(url) {
|
|
14
|
+
baseUrl = url.replace(/\/$/, '')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Get the current base URL. */
|
|
18
|
+
export function getBaseUrl() {
|
|
19
|
+
return baseUrl
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function apiFetch(path, options = {}) {
|
|
23
|
+
const url = `${baseUrl}/_storyboard/canvas${path}`
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
headers: { 'Content-Type': 'application/json', ...options.headers },
|
|
26
|
+
...options,
|
|
27
|
+
})
|
|
28
|
+
const text = await res.text()
|
|
29
|
+
let json
|
|
30
|
+
try {
|
|
31
|
+
json = JSON.parse(text)
|
|
32
|
+
} catch {
|
|
33
|
+
json = { raw: text }
|
|
34
|
+
}
|
|
35
|
+
if (!res.ok && !options.allowFailure) {
|
|
36
|
+
throw new Error(`API ${options.method || 'GET'} ${path} returned ${res.status}: ${JSON.stringify(json)}`)
|
|
37
|
+
}
|
|
38
|
+
return { status: res.status, ok: res.ok, data: json }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Create a widget on a canvas. */
|
|
42
|
+
export async function createWidget(canvasName, type, props = {}, position = { x: 0, y: 0 }) {
|
|
43
|
+
const timer = perf.start(`widget.create`, { type })
|
|
44
|
+
const res = await apiFetch('/widget', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: JSON.stringify({ name: canvasName, type, props, position }),
|
|
47
|
+
})
|
|
48
|
+
timer.end({ widgetId: res.data?.widget?.id })
|
|
49
|
+
return res.data
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Read full canvas state. */
|
|
53
|
+
export async function readCanvas(canvasName) {
|
|
54
|
+
const timer = perf.start('canvas.read', { canvas: canvasName })
|
|
55
|
+
const res = await apiFetch(`/read?name=${encodeURIComponent(canvasName)}`)
|
|
56
|
+
timer.end({ widgetCount: res.data?.widgets?.length })
|
|
57
|
+
return res.data
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Read a specific widget from a canvas. */
|
|
61
|
+
export async function readWidget(canvasName, widgetId) {
|
|
62
|
+
const timer = perf.start('canvas.readWidget', { canvas: canvasName, widgetId })
|
|
63
|
+
const res = await apiFetch(`/read?name=${encodeURIComponent(canvasName)}&widget=${encodeURIComponent(widgetId)}`)
|
|
64
|
+
timer.end()
|
|
65
|
+
return res.data
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Update a widget's props and/or position. */
|
|
69
|
+
export async function updateWidget(canvasName, widgetId, { props, position } = {}) {
|
|
70
|
+
const timer = perf.start('widget.update', { widgetId })
|
|
71
|
+
const body = { name: canvasName, widgetId }
|
|
72
|
+
if (props) body.props = props
|
|
73
|
+
if (position) body.position = position
|
|
74
|
+
const res = await apiFetch('/widget', {
|
|
75
|
+
method: 'PATCH',
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
})
|
|
78
|
+
timer.end()
|
|
79
|
+
return res.data
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Delete a widget from a canvas. */
|
|
83
|
+
export async function deleteWidget(canvasName, widgetId) {
|
|
84
|
+
const timer = perf.start('widget.delete', { widgetId })
|
|
85
|
+
const res = await apiFetch('/widget', {
|
|
86
|
+
method: 'DELETE',
|
|
87
|
+
body: JSON.stringify({ name: canvasName, widgetId }),
|
|
88
|
+
})
|
|
89
|
+
timer.end()
|
|
90
|
+
return res.data
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Add a connector between two widgets. */
|
|
94
|
+
export async function addConnector(canvasName, startWidgetId, startAnchor, endWidgetId, endAnchor) {
|
|
95
|
+
const timer = perf.start('connector.add')
|
|
96
|
+
const res = await apiFetch('/connector', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
body: JSON.stringify({ name: canvasName, startWidgetId, startAnchor, endWidgetId, endAnchor }),
|
|
99
|
+
})
|
|
100
|
+
timer.end({ connectorId: res.data?.connector?.id })
|
|
101
|
+
return res.data
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Remove a connector. */
|
|
105
|
+
export async function removeConnector(canvasName, connectorId) {
|
|
106
|
+
const timer = perf.start('connector.remove')
|
|
107
|
+
const res = await apiFetch('/connector', {
|
|
108
|
+
method: 'DELETE',
|
|
109
|
+
body: JSON.stringify({ name: canvasName, connectorId }),
|
|
110
|
+
})
|
|
111
|
+
timer.end()
|
|
112
|
+
return res.data
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** List terminal sessions. */
|
|
116
|
+
export async function listTerminalSessions() {
|
|
117
|
+
const res = await apiFetch('/../terminal/sessions')
|
|
118
|
+
return res.data
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Check if the dev server is reachable. */
|
|
122
|
+
export async function healthCheck() {
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch(`${baseUrl}/_storyboard/canvas/list`, { signal: AbortSignal.timeout(5000) })
|
|
125
|
+
return res.ok
|
|
126
|
+
} catch {
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance timing and reporting for integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Every timed operation records metric name, duration, and optional metadata.
|
|
5
|
+
* After all tests, call report() for a summary table and toJSON() for file output.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const metrics = []
|
|
9
|
+
|
|
10
|
+
const THRESHOLDS = {
|
|
11
|
+
'widget.create': 1000,
|
|
12
|
+
'widget.update': 1000,
|
|
13
|
+
'widget.delete': 1000,
|
|
14
|
+
'canvas.read': 1000,
|
|
15
|
+
'connector.add': 1000,
|
|
16
|
+
'connector.remove': 1000,
|
|
17
|
+
'tmux.session.start': 5000,
|
|
18
|
+
'tmux.welcome.render': 10000,
|
|
19
|
+
'agent.startup': 60000,
|
|
20
|
+
'agent.response': 45000,
|
|
21
|
+
'agent.full_chain': 300000,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Start a timer. Returns { end() } that records the metric. */
|
|
25
|
+
export function start(name, meta = {}) {
|
|
26
|
+
const t0 = performance.now()
|
|
27
|
+
return {
|
|
28
|
+
end(extraMeta = {}) {
|
|
29
|
+
const duration = performance.now() - t0
|
|
30
|
+
const entry = { name, duration, ...meta, ...extraMeta, timestamp: new Date().toISOString() }
|
|
31
|
+
metrics.push(entry)
|
|
32
|
+
|
|
33
|
+
// Check threshold — emit warning inline
|
|
34
|
+
const thresholdKey = Object.keys(THRESHOLDS).find((k) => name.startsWith(k) || name === k)
|
|
35
|
+
if (thresholdKey && duration > THRESHOLDS[thresholdKey]) {
|
|
36
|
+
console.warn(`[SLOW] ${name}: ${(duration / 1000).toFixed(1)}s (threshold: ${(THRESHOLDS[thresholdKey] / 1000).toFixed(1)}s)`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return entry
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Manually record a metric. */
|
|
45
|
+
export function record(name, durationMs, meta = {}) {
|
|
46
|
+
metrics.push({ name, duration: durationMs, ...meta, timestamp: new Date().toISOString() })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Get all recorded metrics. */
|
|
50
|
+
export function getMetrics() {
|
|
51
|
+
return [...metrics]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Get metrics exceeding thresholds. */
|
|
55
|
+
export function getWarnings() {
|
|
56
|
+
return metrics.filter((m) => {
|
|
57
|
+
const key = Object.keys(THRESHOLDS).find((k) => m.name.startsWith(k) || m.name === k)
|
|
58
|
+
return key && m.duration > THRESHOLDS[key]
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Print a summary table to console. */
|
|
63
|
+
export function report() {
|
|
64
|
+
if (metrics.length === 0) {
|
|
65
|
+
console.log('\n[perf] No metrics recorded.\n')
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Group by metric name
|
|
70
|
+
const groups = {}
|
|
71
|
+
for (const m of metrics) {
|
|
72
|
+
if (!groups[m.name]) groups[m.name] = []
|
|
73
|
+
groups[m.name].push(m.duration)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('\n┌─────────────────────────────────────────────────────────────────┐')
|
|
77
|
+
console.log('│ Performance Summary │')
|
|
78
|
+
console.log('├───────────────────────────────┬───────┬───────┬───────┬─────────┤')
|
|
79
|
+
console.log('│ Metric │ Min │ Avg │ Max │ Warning │')
|
|
80
|
+
console.log('├───────────────────────────────┼───────┼───────┼───────┼─────────┤')
|
|
81
|
+
|
|
82
|
+
for (const [name, durations] of Object.entries(groups).sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
83
|
+
const min = Math.min(...durations)
|
|
84
|
+
const max = Math.max(...durations)
|
|
85
|
+
const avg = durations.reduce((a, b) => a + b, 0) / durations.length
|
|
86
|
+
const thresholdKey = Object.keys(THRESHOLDS).find((k) => name.startsWith(k) || name === k)
|
|
87
|
+
const warn = thresholdKey && max > THRESHOLDS[thresholdKey] ? ' ⚠️' : ' ✓'
|
|
88
|
+
const pad = (s, n) => s.slice(0, n).padEnd(n)
|
|
89
|
+
const fmt = (ms) => {
|
|
90
|
+
if (ms < 1000) return `${Math.round(ms)}ms`.padStart(5)
|
|
91
|
+
return `${(ms / 1000).toFixed(1)}s`.padStart(5)
|
|
92
|
+
}
|
|
93
|
+
console.log(`│ ${pad(name, 29)} │ ${fmt(min)} │ ${fmt(avg)} │ ${fmt(max)} │ ${pad(warn, 7)} │`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log('└───────────────────────────────┴───────┴───────┴───────┴─────────┘')
|
|
97
|
+
|
|
98
|
+
const warnings = getWarnings()
|
|
99
|
+
if (warnings.length > 0) {
|
|
100
|
+
console.log(`\n⚠️ ${warnings.length} operation(s) exceeded performance thresholds.`)
|
|
101
|
+
}
|
|
102
|
+
console.log('')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Return JSON-serializable metrics for file output. */
|
|
106
|
+
export function toJSON() {
|
|
107
|
+
return {
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
totalMetrics: metrics.length,
|
|
110
|
+
metrics: metrics.map((m) => ({ ...m, duration: Math.round(m.duration) })),
|
|
111
|
+
warnings: getWarnings().map((m) => ({ ...m, duration: Math.round(m.duration) })),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Reset all metrics (for test isolation). */
|
|
116
|
+
export function reset() {
|
|
117
|
+
metrics.length = 0
|
|
118
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test setup helpers — canvas lifecycle, server URL, agent availability.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { writeFileSync, unlinkSync, existsSync, readFileSync, mkdirSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { execSync } from 'node:child_process'
|
|
8
|
+
import * as canvasApi from './canvas-api.js'
|
|
9
|
+
|
|
10
|
+
const ROOT = process.cwd()
|
|
11
|
+
|
|
12
|
+
/** Resolve the dev server URL from ports.json or worktree registry. */
|
|
13
|
+
export function resolveServerUrl() {
|
|
14
|
+
// Try .storyboard/ports.json
|
|
15
|
+
try {
|
|
16
|
+
const portsPath = join(ROOT, '.storyboard', 'ports.json')
|
|
17
|
+
if (existsSync(portsPath)) {
|
|
18
|
+
const ports = JSON.parse(readFileSync(portsPath, 'utf8'))
|
|
19
|
+
if (ports.vite) return `http://localhost:${ports.vite}`
|
|
20
|
+
}
|
|
21
|
+
} catch { /* ignore */ }
|
|
22
|
+
|
|
23
|
+
// Try worktree server registry
|
|
24
|
+
try {
|
|
25
|
+
const registryPath = join(ROOT, '.storyboard', 'servers.json')
|
|
26
|
+
if (existsSync(registryPath)) {
|
|
27
|
+
const servers = JSON.parse(readFileSync(registryPath, 'utf8'))
|
|
28
|
+
if (Array.isArray(servers) && servers.length > 0) {
|
|
29
|
+
return `http://localhost:${servers[0].port}`
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch { /* ignore */ }
|
|
33
|
+
|
|
34
|
+
return 'http://localhost:1234'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Create a fresh test canvas file. Returns the canvas name. */
|
|
38
|
+
export function createTestCanvas(name = '__test__-terminal') {
|
|
39
|
+
const canvasDir = join(ROOT, 'src', 'canvas')
|
|
40
|
+
if (!existsSync(canvasDir)) mkdirSync(canvasDir, { recursive: true })
|
|
41
|
+
|
|
42
|
+
const filePath = join(canvasDir, `${name}.canvas.jsonl`)
|
|
43
|
+
const event = JSON.stringify({
|
|
44
|
+
event: 'canvas_created',
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
meta: { name, description: 'Integration test canvas' },
|
|
47
|
+
})
|
|
48
|
+
writeFileSync(filePath, event + '\n')
|
|
49
|
+
return name
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Delete the test canvas file. */
|
|
53
|
+
export function deleteTestCanvas(name = '__test__-terminal') {
|
|
54
|
+
const filePath = join(ROOT, 'src', 'canvas', `${name}.canvas.jsonl`)
|
|
55
|
+
try {
|
|
56
|
+
if (existsSync(filePath)) unlinkSync(filePath)
|
|
57
|
+
} catch { /* ignore */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load configured agents from storyboard.config.json.
|
|
62
|
+
* Returns array of { id, label, startupCommand }.
|
|
63
|
+
*/
|
|
64
|
+
export function loadConfiguredAgents() {
|
|
65
|
+
try {
|
|
66
|
+
const raw = readFileSync(join(ROOT, 'storyboard.config.json'), 'utf8')
|
|
67
|
+
const config = JSON.parse(raw)
|
|
68
|
+
const agents = config?.canvas?.agents
|
|
69
|
+
if (!agents || typeof agents !== 'object') return []
|
|
70
|
+
return Object.entries(agents)
|
|
71
|
+
.map(([id, cfg]) => ({
|
|
72
|
+
id,
|
|
73
|
+
label: cfg.label || id,
|
|
74
|
+
startupCommand: cfg.startupCommand || null,
|
|
75
|
+
}))
|
|
76
|
+
.filter((a) => a.startupCommand)
|
|
77
|
+
} catch {
|
|
78
|
+
return []
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if an agent binary is available on this machine.
|
|
84
|
+
* Returns true if the binary is found, false otherwise.
|
|
85
|
+
*/
|
|
86
|
+
export function checkAgentAvailability(agentId) {
|
|
87
|
+
// Map agent IDs to binary names
|
|
88
|
+
const binaryMap = {
|
|
89
|
+
copilot: 'copilot',
|
|
90
|
+
claude: 'claude',
|
|
91
|
+
}
|
|
92
|
+
const binary = binaryMap[agentId] || agentId
|
|
93
|
+
try {
|
|
94
|
+
execSync(`which ${binary} 2>/dev/null`, { encoding: 'utf8', timeout: 5000 })
|
|
95
|
+
return true
|
|
96
|
+
} catch {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Kill all tmux sessions for the test canvas.
|
|
103
|
+
* Finds sessions by the canvas name pattern in their tmux name.
|
|
104
|
+
*/
|
|
105
|
+
export function cleanupTerminalSessions() {
|
|
106
|
+
try {
|
|
107
|
+
const out = execSync('tmux list-sessions -F "#{session_name}" 2>/dev/null', { encoding: 'utf8', timeout: 5000 })
|
|
108
|
+
const sessions = out.trim().split('\n').filter(Boolean)
|
|
109
|
+
for (const s of sessions) {
|
|
110
|
+
// Kill sessions that are from our test canvas (sb- prefix + test widget IDs)
|
|
111
|
+
if (s.startsWith('sb-')) {
|
|
112
|
+
// We can't know for sure which are test sessions, so we track widget IDs
|
|
113
|
+
// in the test and clean those up specifically. This is a fallback.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch { /* no tmux sessions */ }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Full preflight check. Verifies:
|
|
121
|
+
* 1. Dev server is reachable
|
|
122
|
+
* 2. tmux is available
|
|
123
|
+
* 3. agent-browser is available
|
|
124
|
+
*
|
|
125
|
+
* Throws with a clear message if any check fails.
|
|
126
|
+
*/
|
|
127
|
+
export async function preflight() {
|
|
128
|
+
const url = resolveServerUrl()
|
|
129
|
+
canvasApi.setBaseUrl(url)
|
|
130
|
+
|
|
131
|
+
// Check dev server
|
|
132
|
+
const serverOk = await canvasApi.healthCheck()
|
|
133
|
+
if (!serverOk) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`[preflight] Dev server not reachable at ${url}. Start it with 'storyboard dev' or 'npm run dev' before running integration tests.`,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check tmux
|
|
140
|
+
try {
|
|
141
|
+
execSync('which tmux', { timeout: 5000 })
|
|
142
|
+
} catch {
|
|
143
|
+
throw new Error('[preflight] tmux is not installed. Install it with: brew install tmux')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check agent-browser
|
|
147
|
+
try {
|
|
148
|
+
execSync('which agent-browser', { timeout: 5000 })
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error('[preflight] agent-browser is not installed. Install it with: npm install -g agent-browser')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { url }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Write the perf + summary results to test-results/. */
|
|
157
|
+
export function writeResults(perfData, transcriptPaths = []) {
|
|
158
|
+
const dir = join(ROOT, 'test-results')
|
|
159
|
+
mkdirSync(dir, { recursive: true })
|
|
160
|
+
|
|
161
|
+
// Write perf JSON
|
|
162
|
+
writeFileSync(join(dir, 'integration-perf.json'), JSON.stringify(perfData, null, 2))
|
|
163
|
+
|
|
164
|
+
// Write summary
|
|
165
|
+
const summary = [
|
|
166
|
+
`Integration Test Run — ${new Date().toISOString()}`,
|
|
167
|
+
'',
|
|
168
|
+
`Metrics recorded: ${perfData.totalMetrics}`,
|
|
169
|
+
`Warnings: ${perfData.warnings.length}`,
|
|
170
|
+
'',
|
|
171
|
+
'Transcripts:',
|
|
172
|
+
...transcriptPaths.map((p) => ` ${p}`),
|
|
173
|
+
'',
|
|
174
|
+
].join('\n')
|
|
175
|
+
writeFileSync(join(dir, 'summary.txt'), summary)
|
|
176
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmux helpers for integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Every operation is timed via perf.js and recorded via transcript.js.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'node:child_process'
|
|
8
|
+
import * as perf from './perf.js'
|
|
9
|
+
import * as transcript from './transcript.js'
|
|
10
|
+
|
|
11
|
+
/** List all tmux sessions. Returns array of session name strings. */
|
|
12
|
+
export function listSessions() {
|
|
13
|
+
try {
|
|
14
|
+
const out = execSync('tmux list-sessions -F "#{session_name}"', { encoding: 'utf8', timeout: 5000 })
|
|
15
|
+
return out.trim().split('\n').filter(Boolean)
|
|
16
|
+
} catch {
|
|
17
|
+
return []
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Check if a tmux session exists. */
|
|
22
|
+
export function hasSession(name) {
|
|
23
|
+
try {
|
|
24
|
+
execSync(`tmux has-session -t "${name}" 2>/dev/null`, { timeout: 5000 })
|
|
25
|
+
return true
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Capture the full pane content of a tmux session. */
|
|
32
|
+
export function capturePane(sessionName) {
|
|
33
|
+
const timer = perf.start('tmux.capture', { session: sessionName })
|
|
34
|
+
try {
|
|
35
|
+
const out = execSync(`tmux capture-pane -t "${sessionName}" -p`, { encoding: 'utf8', timeout: 10000 })
|
|
36
|
+
timer.end()
|
|
37
|
+
transcript.logStdout(sessionName, out)
|
|
38
|
+
return out
|
|
39
|
+
} catch (err) {
|
|
40
|
+
timer.end({ error: err.message })
|
|
41
|
+
return ''
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Send keystrokes to a tmux session. */
|
|
46
|
+
export function sendKeys(sessionName, keys) {
|
|
47
|
+
transcript.logStdin(sessionName, keys)
|
|
48
|
+
try {
|
|
49
|
+
execSync(`tmux send-keys -t "${sessionName}" ${keys}`, { timeout: 5000 })
|
|
50
|
+
} catch (err) {
|
|
51
|
+
transcript.logEvent(sessionName, `sendKeys error: ${err.message}`)
|
|
52
|
+
throw err
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Send literal text (properly escaped) to a tmux session. */
|
|
57
|
+
export function sendText(sessionName, text) {
|
|
58
|
+
transcript.logStdin(sessionName, text)
|
|
59
|
+
try {
|
|
60
|
+
// Use -- to prevent tmux from interpreting flags in the text
|
|
61
|
+
execSync(`tmux send-keys -t "${sessionName}" -l -- ${JSON.stringify(text)}`, { timeout: 5000 })
|
|
62
|
+
} catch (err) {
|
|
63
|
+
transcript.logEvent(sessionName, `sendText error: ${err.message}`)
|
|
64
|
+
throw err
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Poll capturePane until the output matches a pattern, or timeout.
|
|
70
|
+
* Returns the matched output on success, throws on timeout.
|
|
71
|
+
*/
|
|
72
|
+
export async function waitForOutput(sessionName, pattern, timeoutMs = 30000, intervalMs = 1000) {
|
|
73
|
+
const timer = perf.start('tmux.waitForOutput', { session: sessionName, pattern: String(pattern) })
|
|
74
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern, 'i') : pattern
|
|
75
|
+
const deadline = Date.now() + timeoutMs
|
|
76
|
+
|
|
77
|
+
while (Date.now() < deadline) {
|
|
78
|
+
const output = capturePane(sessionName)
|
|
79
|
+
if (regex.test(output)) {
|
|
80
|
+
timer.end({ matched: true })
|
|
81
|
+
return output
|
|
82
|
+
}
|
|
83
|
+
await new Promise((r) => setTimeout(r, intervalMs))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Final attempt
|
|
87
|
+
const output = capturePane(sessionName)
|
|
88
|
+
if (regex.test(output)) {
|
|
89
|
+
timer.end({ matched: true })
|
|
90
|
+
return output
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
timer.end({ matched: false, timedOut: true })
|
|
94
|
+
const err = new Error(`[tmux] waitForOutput timed out after ${timeoutMs}ms waiting for ${pattern}\nLast capture:\n${output}`)
|
|
95
|
+
err.lastCapture = output
|
|
96
|
+
throw err
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Wait for a tmux session to appear. Polls tmux list-sessions.
|
|
101
|
+
* Returns the matching session name, or throws on timeout.
|
|
102
|
+
*/
|
|
103
|
+
export async function waitForSession(namePattern, timeoutMs = 15000, intervalMs = 500) {
|
|
104
|
+
const timer = perf.start('tmux.session.start')
|
|
105
|
+
const regex = typeof namePattern === 'string' ? new RegExp(namePattern) : namePattern
|
|
106
|
+
const deadline = Date.now() + timeoutMs
|
|
107
|
+
|
|
108
|
+
while (Date.now() < deadline) {
|
|
109
|
+
const sessions = listSessions()
|
|
110
|
+
const match = sessions.find((s) => regex.test(s))
|
|
111
|
+
if (match) {
|
|
112
|
+
timer.end({ session: match })
|
|
113
|
+
return match
|
|
114
|
+
}
|
|
115
|
+
await new Promise((r) => setTimeout(r, intervalMs))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
timer.end({ timedOut: true })
|
|
119
|
+
throw new Error(`[tmux] No session matching ${namePattern} appeared within ${timeoutMs}ms`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Kill a tmux session. */
|
|
123
|
+
export function killSession(name) {
|
|
124
|
+
try {
|
|
125
|
+
execSync(`tmux kill-session -t "${name}" 2>/dev/null`, { timeout: 5000 })
|
|
126
|
+
transcript.logEvent(name, 'Session killed')
|
|
127
|
+
} catch {
|
|
128
|
+
// Session may already be dead
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session transcript recording for integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Wraps tmux interactions to produce a full stdin/stdout log per session.
|
|
5
|
+
* Transcripts are written to test-results/transcripts/ on every run.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
const transcripts = new Map()
|
|
12
|
+
|
|
13
|
+
const TRANSCRIPTS_DIR = join(process.cwd(), 'test-results', 'transcripts')
|
|
14
|
+
|
|
15
|
+
function ensureDir() {
|
|
16
|
+
mkdirSync(TRANSCRIPTS_DIR, { recursive: true })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function elapsed(startTime) {
|
|
20
|
+
const ms = Date.now() - startTime
|
|
21
|
+
const secs = Math.floor(ms / 1000)
|
|
22
|
+
const mins = Math.floor(secs / 60)
|
|
23
|
+
const s = secs % 60
|
|
24
|
+
const tenths = Math.floor((ms % 1000) / 100)
|
|
25
|
+
if (mins > 0) return `${String(mins).padStart(2, '0')}:${String(s).padStart(2, '0')}.${tenths}`
|
|
26
|
+
return `${String(s).padStart(2, '0')}.${tenths}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Initialize a transcript for a session. */
|
|
30
|
+
export function createTranscript(sessionName, metadata = {}) {
|
|
31
|
+
const transcript = {
|
|
32
|
+
sessionName,
|
|
33
|
+
metadata,
|
|
34
|
+
startTime: Date.now(),
|
|
35
|
+
lines: [],
|
|
36
|
+
}
|
|
37
|
+
transcript.lines.push(`=== Session: ${sessionName} ===`)
|
|
38
|
+
transcript.lines.push(`Started: ${new Date().toISOString()}`)
|
|
39
|
+
if (metadata.widgetId) transcript.lines.push(`Widget ID: ${metadata.widgetId}`)
|
|
40
|
+
if (metadata.canvasName) transcript.lines.push(`Canvas: ${metadata.canvasName}`)
|
|
41
|
+
if (metadata.agentId) transcript.lines.push(`Agent: ${metadata.agentId}`)
|
|
42
|
+
transcript.lines.push('')
|
|
43
|
+
transcripts.set(sessionName, transcript)
|
|
44
|
+
return transcript
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Log a test section header. */
|
|
48
|
+
export function logSection(sessionName, testId) {
|
|
49
|
+
const t = transcripts.get(sessionName)
|
|
50
|
+
if (!t) return
|
|
51
|
+
t.lines.push(`--- [${testId}] ---`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Log stdin (input sent via sendKeys). */
|
|
55
|
+
export function logStdin(sessionName, input, testId = '') {
|
|
56
|
+
const t = transcripts.get(sessionName)
|
|
57
|
+
if (!t) return
|
|
58
|
+
const ts = elapsed(t.startTime)
|
|
59
|
+
const prefix = `[${ts}] [stdin] `
|
|
60
|
+
// Split multi-line input
|
|
61
|
+
const lines = String(input).split('\n')
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
t.lines.push(`${prefix}${line}`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Log stdout (captured pane output). Only logs the diff from previous capture. */
|
|
68
|
+
export function logStdout(sessionName, fullOutput, testId = '') {
|
|
69
|
+
const t = transcripts.get(sessionName)
|
|
70
|
+
if (!t) return
|
|
71
|
+
const ts = elapsed(t.startTime)
|
|
72
|
+
const prefix = `[${ts}] [stdout] `
|
|
73
|
+
|
|
74
|
+
// Compute diff from last stdout
|
|
75
|
+
const prevLen = t._lastStdoutLen || 0
|
|
76
|
+
const lines = String(fullOutput).split('\n')
|
|
77
|
+
const newLines = lines.slice(prevLen)
|
|
78
|
+
t._lastStdoutLen = lines.length
|
|
79
|
+
|
|
80
|
+
for (const line of newLines) {
|
|
81
|
+
// Skip empty trailing lines
|
|
82
|
+
if (line.trim() === '' && newLines.indexOf(line) === newLines.length - 1) continue
|
|
83
|
+
t.lines.push(`${prefix}${line}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Log a non-IO event (session start, resize, etc.). */
|
|
88
|
+
export function logEvent(sessionName, event, testId = '') {
|
|
89
|
+
const t = transcripts.get(sessionName)
|
|
90
|
+
if (!t) return
|
|
91
|
+
const ts = elapsed(t.startTime)
|
|
92
|
+
t.lines.push(`[${ts}] [event] ${event}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Get the in-memory transcript text. */
|
|
96
|
+
export function getTranscript(sessionName) {
|
|
97
|
+
const t = transcripts.get(sessionName)
|
|
98
|
+
if (!t) return ''
|
|
99
|
+
return t.lines.join('\n')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Flush a single transcript to disk. */
|
|
103
|
+
export function flush(sessionName) {
|
|
104
|
+
const t = transcripts.get(sessionName)
|
|
105
|
+
if (!t) return null
|
|
106
|
+
|
|
107
|
+
const totalMs = Date.now() - t.startTime
|
|
108
|
+
t.lines.push('')
|
|
109
|
+
t.lines.push(`=== Session ended: ${new Date().toISOString()} (total: ${(totalMs / 1000).toFixed(1)}s) ===`)
|
|
110
|
+
|
|
111
|
+
ensureDir()
|
|
112
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
113
|
+
const filename = `${sessionName}-${ts}.log`
|
|
114
|
+
const filepath = join(TRANSCRIPTS_DIR, filename)
|
|
115
|
+
writeFileSync(filepath, t.lines.join('\n') + '\n')
|
|
116
|
+
console.log(`[transcript] Written: ${filepath}`)
|
|
117
|
+
return filepath
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Flush all open transcripts. Call in afterAll. */
|
|
121
|
+
export function flushAll() {
|
|
122
|
+
const paths = []
|
|
123
|
+
for (const sessionName of transcripts.keys()) {
|
|
124
|
+
const p = flush(sessionName)
|
|
125
|
+
if (p) paths.push(p)
|
|
126
|
+
}
|
|
127
|
+
transcripts.clear()
|
|
128
|
+
return paths
|
|
129
|
+
}
|