@dfosco/storyboard 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/assets/favicon.svg +12 -0
  2. package/dist/storyboard-ui.css +1 -1
  3. package/dist/storyboard-ui.js +13907 -10816
  4. package/dist/storyboard-ui.js.map +1 -1
  5. package/dist/tailwind.css +1 -1
  6. package/package.json +138 -127
  7. package/scaffold/AGENTS.md +9 -7
  8. package/scaffold/README.md +18 -0
  9. package/scaffold/deploy.yml +21 -0
  10. package/scaffold/gitignore +8 -14
  11. package/scaffold/preview.yml +40 -0
  12. package/scaffold/{manifest.json → scaffold.config.json} +73 -3
  13. package/scaffold/scripts/aggregate-artifacts.mjs +257 -0
  14. package/scaffold/skills/canvas/SKILL.md +1 -1
  15. package/scaffold/skills/migrate-0.5.0/SKILL.md +1 -1
  16. package/scaffold/skills/ship/SKILL.md +2 -2
  17. package/scaffold/skills/worktree/SKILL.md +1 -1
  18. package/scaffold/src/_prototype.jsx +17 -0
  19. package/scaffold/src/_story.jsx +13 -0
  20. package/scaffold/src/library/README.md +34 -0
  21. package/scaffold/src/library/_app.jsx +53 -0
  22. package/scaffold/src/library/_app.module.css +8 -0
  23. package/scaffold/src/library/create.jsx +10 -0
  24. package/scaffold/src/library/index.jsx +14 -0
  25. package/scaffold/src/library/mount.jsx +52 -0
  26. package/scaffold/src/library/prototypes-entry.jsx +57 -0
  27. package/scaffold/src/library/routes.jsx +205 -0
  28. package/scaffold/src/library/viewfinder.jsx +13 -0
  29. package/scaffold/src/library/workspace.jsx +17 -0
  30. package/scripts/aggregate-artifacts.mjs +257 -0
  31. package/src/core/artifact/operations.js +19 -13
  32. package/src/core/autosync/server.test.js +1 -1
  33. package/src/core/canvas/githubEmbeds.js +12 -0
  34. package/src/core/canvas/githubEmbeds.test.js +71 -2
  35. package/src/core/canvas/hot-pool.js +10 -11
  36. package/src/core/canvas/materializer.js +39 -13
  37. package/src/core/canvas/server.js +324 -69
  38. package/src/core/canvas/server.test.js +365 -2
  39. package/src/core/canvas/terminal-server.js +144 -57
  40. package/src/core/canvas/undoRedo.js +174 -0
  41. package/src/core/canvas/undoRedo.test.js +250 -0
  42. package/src/core/cli/buildManifest.js +34 -0
  43. package/src/core/cli/canvasRedo.js +44 -0
  44. package/src/core/cli/canvasUndo.js +40 -0
  45. package/src/core/cli/create.js +116 -18
  46. package/src/core/cli/dev.js +23 -35
  47. package/src/core/cli/index.js +21 -0
  48. package/src/core/cli/schemas.js +2 -3
  49. package/src/core/cli/serverUrl.js +24 -0
  50. package/src/core/cli/setup.js +14 -25
  51. package/src/core/cli/terminal-welcome.js +12 -6
  52. package/src/core/cli/updateVersion.js +42 -2
  53. package/src/core/cli/workleaf.js +140 -0
  54. package/src/core/comments/api.js +1 -1
  55. package/src/core/comments/queries.js +3 -0
  56. package/src/core/comments/ui/CommentWindow.jsx +3 -2
  57. package/src/core/data/artifactIndex.js +235 -0
  58. package/src/core/data/artifactIndex.test.js +203 -0
  59. package/src/core/data/artifactManifest.js +290 -0
  60. package/src/core/data/artifactManifest.test.js +207 -0
  61. package/src/core/data/viewfinder.js +47 -7
  62. package/src/core/index.js +6 -1
  63. package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +2 -2
  64. package/src/core/lib/components/ui/avatar/avatar-image.jsx +2 -2
  65. package/src/core/lib/components/ui/avatar/avatar.jsx +2 -2
  66. package/src/core/lib/components/ui/checkbox/checkbox.jsx +12 -9
  67. package/src/core/lib/components/ui/dialog/dialog-content.jsx +45 -31
  68. package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +9 -5
  69. package/src/core/lib/components/ui/dialog/dialog.jsx +12 -3
  70. package/src/core/lib/components/ui/dialog/dialog.module.css +118 -0
  71. package/src/core/lib/components/ui/dialog/index.js +4 -34
  72. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +3 -3
  73. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +19 -9
  74. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +8 -8
  75. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +2 -2
  76. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +2 -2
  77. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +15 -4
  78. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +2 -2
  79. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +2 -2
  80. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +14 -7
  81. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +2 -2
  82. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +8 -4
  83. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +4 -4
  84. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +2 -2
  85. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +8 -2
  86. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +2 -2
  87. package/src/core/lib/components/ui/label/label.jsx +1 -2
  88. package/src/core/lib/components/ui/panel/panel-close.jsx +7 -4
  89. package/src/core/lib/components/ui/panel/panel-content.jsx +7 -9
  90. package/src/core/lib/components/ui/panel/panel-title.jsx +3 -3
  91. package/src/core/lib/components/ui/panel/panel.jsx +11 -2
  92. package/src/core/lib/components/ui/separator/separator.jsx +6 -4
  93. package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +7 -6
  94. package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +13 -6
  95. package/src/core/lib/components/ui/tooltip/tooltip.jsx +5 -5
  96. package/src/core/mountStoryboardCore.js +234 -74
  97. package/src/core/mountStoryboardCore.test.js +174 -0
  98. package/src/core/rename-watcher/watcher.js +329 -36
  99. package/src/core/rename-watcher/watcher.test.js +253 -0
  100. package/src/core/scaffold/fragments.js +287 -0
  101. package/src/core/scaffold/fragments.test.js +401 -0
  102. package/src/core/scaffold/index.js +291 -0
  103. package/src/core/scaffold/migrateLegacyGitignore.js +157 -0
  104. package/src/core/scaffold/scaffold-integration.test.js +247 -0
  105. package/src/core/scaffold.js +4 -97
  106. package/src/core/stores/configSchema.js +41 -2
  107. package/src/core/stores/recentArtifacts.js +49 -2
  108. package/src/core/stores/recentArtifacts.test.js +59 -6
  109. package/src/core/stores/themeBootstrap.js +264 -0
  110. package/src/core/stores/themeBootstrap.test.js +211 -0
  111. package/src/core/stores/themeStore.test.js +154 -0
  112. package/src/core/stores/themeStore.ts +235 -220
  113. package/src/core/styles/generateThemesCss.js +63 -0
  114. package/src/core/styles/tailwind.css +12 -4
  115. package/src/core/styles/workspace-chrome.css +30 -0
  116. package/src/core/tools/handlers/paletteTheme.js +11 -8
  117. package/src/core/ui/ActionMenuButton.jsx +2 -1
  118. package/src/core/ui/CanvasCreateMenu.jsx +1 -19
  119. package/src/core/ui/CoreUIBar.jsx +10 -7
  120. package/src/core/ui/ThemeMenuButton.jsx +45 -30
  121. package/src/core/vite/server-plugin.js +36 -0
  122. package/src/core/workleaf/cleanup.js +102 -0
  123. package/src/core/workleaf/create.js +199 -0
  124. package/src/core/workleaf/delete.js +101 -0
  125. package/src/core/workleaf/list.js +57 -0
  126. package/src/core/workleaf/merge.js +238 -0
  127. package/src/core/workleaf/paths.js +146 -0
  128. package/src/core/workleaf/paths.test.js +230 -0
  129. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +28 -21
  130. package/src/core/workshop/features/createFlow/server.js +5 -1
  131. package/src/core/workshop/features/createPrototype/server.js +5 -1
  132. package/src/core/workshop/features/templateIndex.js +28 -1
  133. package/src/core/worktree/port.js +48 -5
  134. package/src/core/worktree/port.test.js +42 -0
  135. package/src/internals/ArtifactForm/ArtifactForm.jsx +173 -112
  136. package/src/internals/ArtifactForm/ArtifactForm.module.css +48 -66
  137. package/src/internals/ArtifactForm/artifactSchemas.js +40 -5
  138. package/src/internals/BaseUiForm/BaseUiForm.jsx +293 -0
  139. package/src/internals/BaseUiForm/BaseUiForm.module.css +542 -0
  140. package/src/internals/BranchesDropdown/BranchesDropdown.jsx +69 -0
  141. package/src/internals/BranchesDropdown/BranchesDropdown.module.css +84 -0
  142. package/src/internals/CommandPalette/CommandPalette.jsx +25 -19
  143. package/src/internals/CommandPalette/CreateArtifactForm.jsx +150 -0
  144. package/src/internals/CommandPalette/CreateDialog.jsx +20 -110
  145. package/src/internals/CommandPalette/WidgetArtifactDialog.jsx +1 -1
  146. package/src/internals/CommandPalette/createHelpers.js +30 -0
  147. package/src/internals/CreatePage/CreatePage.jsx +57 -0
  148. package/src/internals/CreatePage/CreatePage.module.css +34 -0
  149. package/src/internals/Dialog.jsx +51 -0
  150. package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +533 -0
  151. package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +323 -0
  152. package/src/internals/Tooltip.jsx +39 -0
  153. package/src/internals/Viewfinder.jsx +196 -48
  154. package/src/internals/Viewfinder.module.css +72 -1
  155. package/src/internals/canvas/CanvasPage.jsx +267 -124
  156. package/src/internals/canvas/CanvasPage.multiselect.test.jsx +39 -26
  157. package/src/internals/canvas/PageSelector.jsx +15 -1
  158. package/src/internals/canvas/PageSelector.module.css +14 -0
  159. package/src/internals/canvas/canvasApi.js +40 -5
  160. package/src/internals/canvas/isolation/createIsolationMiddleware.js +68 -0
  161. package/src/internals/canvas/isolation/mountIsolationApp.jsx +88 -0
  162. package/src/internals/canvas/isolation/prototypeRoutes.jsx +131 -0
  163. package/src/internals/canvas/isolation/prototypesEntry.jsx +28 -0
  164. package/src/internals/canvas/isolation/storiesEntry.jsx +20 -0
  165. package/src/internals/canvas/isolation/storyRoutes.jsx +55 -0
  166. package/src/internals/canvas/useUndoRedo.js +75 -60
  167. package/src/internals/canvas/useUndoRedo.test.js +46 -178
  168. package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +1 -1
  169. package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +30 -21
  170. package/src/internals/canvas/widgets/ImageWidget.jsx +7 -2
  171. package/src/internals/canvas/widgets/LinkPreview.jsx +45 -1
  172. package/src/internals/canvas/widgets/LinkPreview.module.css +71 -0
  173. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +69 -10
  174. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +114 -0
  175. package/src/internals/canvas/widgets/StorySetWidget.jsx +8 -4
  176. package/src/internals/canvas/widgets/StoryWidget.jsx +21 -38
  177. package/src/internals/canvas/widgets/TerminalReadWidget.jsx +13 -15
  178. package/src/internals/canvas/widgets/WidgetChrome.jsx +1 -1
  179. package/src/internals/canvas/widgets/WidgetChrome.module.css +15 -0
  180. package/src/internals/canvas/widgets/embedTheme.js +2 -2
  181. package/src/internals/canvas/widgets/expandUtils.js +19 -19
  182. package/src/internals/canvas/widgets/normalizeLegacyEmbedSrc.js +74 -0
  183. package/src/internals/context.jsx +22 -1
  184. package/src/internals/context.test.jsx +47 -0
  185. package/src/internals/hooks/useArtifactIndex.js +31 -0
  186. package/src/internals/hooks/useArtifactIndex.test.js +45 -0
  187. package/src/internals/hooks/usePrototypeReloadGuard.js +14 -8
  188. package/src/internals/index.js +6 -0
  189. package/src/internals/loadConsumerWrapper.js +60 -0
  190. package/src/internals/story/ComponentSetPage.jsx +75 -34
  191. package/src/internals/story/StoryPage.jsx +16 -25
  192. package/src/internals/vite/data-plugin.js +313 -148
  193. package/src/internals/vite/data-plugin.test.js +11 -11
  194. package/{scaffold/storyboard.config.json → storyboard.config.json} +25 -0
  195. package/terminal.config.json +1 -1
  196. package/src/core/lib/components/ui/card/card-action.jsx +0 -11
  197. package/src/core/lib/components/ui/card/card-content.jsx +0 -11
  198. package/src/core/lib/components/ui/card/card-description.jsx +0 -11
  199. package/src/core/lib/components/ui/card/card-footer.jsx +0 -11
  200. package/src/core/lib/components/ui/card/card-header.jsx +0 -19
  201. package/src/core/lib/components/ui/card/card-title.jsx +0 -11
  202. package/src/core/lib/components/ui/card/card.jsx +0 -17
  203. package/src/core/lib/components/ui/card/index.js +0 -25
  204. package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +0 -7
  205. package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +0 -7
  206. package/src/core/lib/components/ui/collapsible/collapsible.jsx +0 -7
  207. package/src/core/lib/components/ui/collapsible/index.js +0 -13
  208. package/src/core/lib/components/ui/dialog/dialog-close.jsx +0 -7
  209. package/src/core/lib/components/ui/dialog/dialog-description.jsx +0 -15
  210. package/src/core/lib/components/ui/dialog/dialog-footer.jsx +0 -23
  211. package/src/core/lib/components/ui/dialog/dialog-header.jsx +0 -11
  212. package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +0 -15
  213. package/src/core/lib/components/ui/dialog/dialog-portal.jsx +0 -4
  214. package/src/core/lib/components/ui/dialog/dialog-title.jsx +0 -15
  215. package/src/core/lib/components/ui/popover/index.js +0 -28
  216. package/src/core/lib/components/ui/popover/popover-close.jsx +0 -7
  217. package/src/core/lib/components/ui/popover/popover-content.jsx +0 -22
  218. package/src/core/lib/components/ui/popover/popover-description.jsx +0 -11
  219. package/src/core/lib/components/ui/popover/popover-header.jsx +0 -11
  220. package/src/core/lib/components/ui/popover/popover-portal.jsx +0 -4
  221. package/src/core/lib/components/ui/popover/popover-title.jsx +0 -11
  222. package/src/core/lib/components/ui/popover/popover-trigger.jsx +0 -8
  223. package/src/core/lib/components/ui/popover/popover.jsx +0 -4
  224. package/src/core/lib/components/ui/select/index.js +0 -37
  225. package/src/core/lib/components/ui/select/select-content.jsx +0 -30
  226. package/src/core/lib/components/ui/select/select-group-heading.jsx +0 -17
  227. package/src/core/lib/components/ui/select/select-group.jsx +0 -15
  228. package/src/core/lib/components/ui/select/select-item.jsx +0 -26
  229. package/src/core/lib/components/ui/select/select-label.jsx +0 -11
  230. package/src/core/lib/components/ui/select/select-portal.jsx +0 -4
  231. package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +0 -18
  232. package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +0 -18
  233. package/src/core/lib/components/ui/select/select-separator.jsx +0 -15
  234. package/src/core/lib/components/ui/select/select-trigger.jsx +0 -25
  235. package/src/core/lib/components/ui/select/select.jsx +0 -4
  236. package/src/core/lib/components/ui/sheet/index.js +0 -34
  237. package/src/core/lib/components/ui/sheet/sheet-close.jsx +0 -7
  238. package/src/core/lib/components/ui/sheet/sheet-content.jsx +0 -35
  239. package/src/core/lib/components/ui/sheet/sheet-description.jsx +0 -15
  240. package/src/core/lib/components/ui/sheet/sheet-footer.jsx +0 -11
  241. package/src/core/lib/components/ui/sheet/sheet-header.jsx +0 -11
  242. package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +0 -15
  243. package/src/core/lib/components/ui/sheet/sheet-portal.jsx +0 -4
  244. package/src/core/lib/components/ui/sheet/sheet-title.jsx +0 -15
  245. package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +0 -7
  246. package/src/core/lib/components/ui/sheet/sheet.jsx +0 -4
  247. package/src/core/lib/components/ui/toggle/index.js +0 -8
  248. package/src/core/lib/components/ui/toggle/toggle.jsx +0 -36
  249. package/src/core/lib/components/ui/toggle-group/index.js +0 -10
  250. package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +0 -29
  251. package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +0 -43
  252. package/src/internals/canvas/StoryErrorBoundary.jsx +0 -47
  253. package/src/internals/canvas/componentIsolate.jsx +0 -144
  254. package/src/internals/canvas/componentSetIsolate.jsx +0 -300
  255. package/src/internals/canvas/widgets/InlineStoryRenderer.jsx +0 -129
  256. package/src/internals/canvas/widgets/InlineStoryRenderer.test.jsx +0 -68
  257. package/src/primer/ThemeSync.jsx +0 -73
  258. package/src/primer/index.js +0 -1
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import process from 'process'
6
+
7
+ const INDEX_FILENAME = 'artifacts.index.json'
8
+ const TMP_FILENAME = '.artifacts.index.json.tmp'
9
+ const SCHEMA_VERSION = 1
10
+
11
+ function compareBranchNames(a, b) {
12
+ if (a.branch === 'main' && b.branch !== 'main') return -1
13
+ if (b.branch === 'main' && a.branch !== 'main') return 1
14
+ return String(a.branch || '').localeCompare(String(b.branch || ''))
15
+ }
16
+
17
+ function withTrailingSlash(folder) {
18
+ if (!folder) return ''
19
+ return folder.endsWith('/') ? folder : `${folder}/`
20
+ }
21
+
22
+ function generatedAtMs(manifest) {
23
+ const value = Date.parse(manifest?.generatedAt || '')
24
+ return Number.isNaN(value) ? 0 : value
25
+ }
26
+
27
+ function deriveFolder(manifest, dir, ghPagesDir) {
28
+ if (dir === ghPagesDir) return ''
29
+ if (typeof manifest.folder === 'string') return manifest.folder
30
+ return path.basename(dir)
31
+ }
32
+
33
+ function deriveBranch(manifest, folder) {
34
+ if (!folder) return 'main'
35
+ if (typeof manifest.branch === 'string' && manifest.branch) return manifest.branch
36
+ return manifest.deployBranch || folder.replace(/^branch--/, '')
37
+ }
38
+
39
+ function deriveDeployBranch(manifest, branch, folder) {
40
+ if (!folder || branch === 'main') return 'main'
41
+ if (typeof manifest.deployBranch === 'string' && manifest.deployBranch) return manifest.deployBranch
42
+ return folder.replace(/^branch--/, '')
43
+ }
44
+
45
+ function readManifest(dir) {
46
+ const manifestPath = path.join(dir, 'artifacts.json')
47
+ const label = path.basename(dir) || dir
48
+
49
+ let raw
50
+ try {
51
+ raw = fs.readFileSync(manifestPath, 'utf8')
52
+ } catch (err) {
53
+ if (err?.code === 'ENOENT') {
54
+ console.info(`[aggregate-artifacts] Skipping ${label}: artifacts.json not found`)
55
+ return { manifest: null, readError: false }
56
+ }
57
+ console.warn(`[aggregate-artifacts] Skipping ${label}: could not read artifacts.json (${err.message})`)
58
+ return { manifest: null, readError: true }
59
+ }
60
+
61
+ let manifest
62
+ try {
63
+ manifest = JSON.parse(raw)
64
+ } catch (err) {
65
+ console.warn(`[aggregate-artifacts] Skipping ${label}: malformed artifacts.json (${err.message})`)
66
+ return { manifest: null, readError: false }
67
+ }
68
+
69
+ if (manifest?.schemaVersion !== SCHEMA_VERSION) {
70
+ console.warn(`[aggregate-artifacts] Skipping ${label}: unsupported schemaVersion ${manifest?.schemaVersion}`)
71
+ return { manifest: null, readError: false }
72
+ }
73
+
74
+ return { manifest, readError: false }
75
+ }
76
+
77
+ function listManifestDirs(ghPagesDir) {
78
+ const entries = fs.readdirSync(ghPagesDir, { withFileTypes: true })
79
+ const branchDirs = entries
80
+ .filter(entry => entry.isDirectory() && entry.name.startsWith('branch--'))
81
+ .map(entry => path.join(ghPagesDir, entry.name))
82
+ .sort((a, b) => path.basename(a).localeCompare(path.basename(b)))
83
+
84
+ return [ghPagesDir, ...branchDirs]
85
+ }
86
+
87
+ function collectManifests(ghPagesDir) {
88
+ const manifestsByFolder = new Map()
89
+ let readErrorCount = 0
90
+
91
+ for (const dir of listManifestDirs(ghPagesDir)) {
92
+ const { manifest, readError } = readManifest(dir)
93
+ if (readError) readErrorCount += 1
94
+ if (!manifest) continue
95
+
96
+ const folder = deriveFolder(manifest, dir, ghPagesDir)
97
+ const current = { manifest, dir, folder }
98
+ const previous = manifestsByFolder.get(folder)
99
+
100
+ if (!previous) {
101
+ manifestsByFolder.set(folder, current)
102
+ continue
103
+ }
104
+
105
+ const previousBranch = deriveBranch(previous.manifest, previous.folder)
106
+ const currentBranch = deriveBranch(manifest, folder)
107
+ const keepCurrent = generatedAtMs(manifest) > generatedAtMs(previous.manifest)
108
+ const keptBranch = keepCurrent ? currentBranch : previousBranch
109
+
110
+ console.warn(
111
+ `[aggregate-artifacts] Branch folder collision for "${folder || '(root)'}" between "${previousBranch}" and "${currentBranch}"; keeping "${keptBranch}"`,
112
+ )
113
+
114
+ if (keepCurrent) manifestsByFolder.set(folder, current)
115
+ }
116
+
117
+ if (readErrorCount > 1) {
118
+ throw new Error(`Failed to read ${readErrorCount} artifacts manifests; preserving existing ${INDEX_FILENAME}`)
119
+ }
120
+
121
+ const manifests = [...manifestsByFolder.values()]
122
+ if (manifests.length === 0) {
123
+ throw new Error(`No readable artifacts.json manifests found; preserving existing ${INDEX_FILENAME}`)
124
+ }
125
+
126
+ return manifests
127
+ }
128
+
129
+ function addPrototype(index, manifestInfo, item) {
130
+ const id = item?.artifactId || item?.id
131
+ if (!id) return
132
+
133
+ const { manifest, folder } = manifestInfo
134
+ const branch = deriveBranch(manifest, folder)
135
+ const key = String(id)
136
+
137
+ if (!index.prototypes[key]) {
138
+ index.prototypes[key] = { id: key, branches: [] }
139
+ }
140
+
141
+ index.prototypes[key].branches.push({
142
+ branch,
143
+ folder: withTrailingSlash(folder),
144
+ title: item.title ?? item.id ?? key,
145
+ route: item.route ?? null,
146
+ isExternal: Boolean(item.isExternal),
147
+ externalUrl: item.externalUrl ?? null,
148
+ folderName: item.folder ?? null,
149
+ sha: manifest.sha ?? null,
150
+ lastModified: item.lastModified ?? null,
151
+ })
152
+ }
153
+
154
+ function addCanvas(index, manifestInfo, item) {
155
+ const id = item?.artifactId || item?.id
156
+ if (!id) return
157
+
158
+ const { manifest, folder } = manifestInfo
159
+ const branch = deriveBranch(manifest, folder)
160
+ const key = String(id)
161
+
162
+ if (!index.canvases[key]) {
163
+ index.canvases[key] = { id: key, branches: [] }
164
+ }
165
+
166
+ index.canvases[key].branches.push({
167
+ branch,
168
+ folder: withTrailingSlash(folder),
169
+ name: item.name ?? item.id ?? key,
170
+ route: item.route ?? null,
171
+ folderName: item.folder ?? null,
172
+ sha: manifest.sha ?? null,
173
+ lastModified: item.lastModified ?? null,
174
+ })
175
+ }
176
+
177
+ function sortArtifactMap(map) {
178
+ const sorted = {}
179
+
180
+ for (const key of Object.keys(map).sort((a, b) => a.localeCompare(b))) {
181
+ sorted[key] = {
182
+ ...map[key],
183
+ branches: [...map[key].branches].sort(compareBranchNames),
184
+ }
185
+ }
186
+
187
+ return sorted
188
+ }
189
+
190
+ function buildIndex(manifests) {
191
+ const index = {
192
+ schemaVersion: SCHEMA_VERSION,
193
+ generatedAt: new Date().toISOString(),
194
+ branches: [],
195
+ prototypes: {},
196
+ canvases: {},
197
+ }
198
+
199
+ for (const manifestInfo of manifests) {
200
+ const { manifest, folder } = manifestInfo
201
+ const branch = deriveBranch(manifest, folder)
202
+ const deployBranch = deriveDeployBranch(manifest, branch, folder)
203
+
204
+ index.branches.push({ branch, deployBranch, folder })
205
+
206
+ for (const item of Array.isArray(manifest.prototypes) ? manifest.prototypes : []) {
207
+ addPrototype(index, manifestInfo, item)
208
+ }
209
+
210
+ for (const item of Array.isArray(manifest.canvases) ? manifest.canvases : []) {
211
+ addCanvas(index, manifestInfo, item)
212
+ }
213
+ }
214
+
215
+ index.branches = [...index.branches].sort(compareBranchNames)
216
+ index.prototypes = sortArtifactMap(index.prototypes)
217
+ index.canvases = sortArtifactMap(index.canvases)
218
+
219
+ return index
220
+ }
221
+
222
+ function writeIndex(ghPagesDir, index) {
223
+ const targetPath = path.join(ghPagesDir, INDEX_FILENAME)
224
+ const tmpPath = path.join(ghPagesDir, TMP_FILENAME)
225
+ const contents = `${JSON.stringify(index, null, 2)}\n`
226
+
227
+ fs.writeFileSync(tmpPath, contents, 'utf8')
228
+ fs.renameSync(tmpPath, targetPath)
229
+
230
+ return targetPath
231
+ }
232
+
233
+ function aggregateArtifacts(ghPagesDir) {
234
+ const resolvedDir = path.resolve(ghPagesDir)
235
+ const manifests = collectManifests(resolvedDir)
236
+ const index = buildIndex(manifests)
237
+ return writeIndex(resolvedDir, index)
238
+ }
239
+
240
+ function main() {
241
+ const ghPagesDir = process.argv[2]
242
+
243
+ if (!ghPagesDir) {
244
+ console.error('Usage: node aggregate-artifacts.mjs <gh-pages-dir>')
245
+ process.exit(1)
246
+ }
247
+
248
+ try {
249
+ const targetPath = aggregateArtifacts(ghPagesDir)
250
+ console.info(`[aggregate-artifacts] Wrote ${targetPath}`)
251
+ } catch (err) {
252
+ console.error(`[aggregate-artifacts] ${err.message}`)
253
+ process.exit(1)
254
+ }
255
+ }
256
+
257
+ main()
@@ -128,8 +128,6 @@ function createPrototype(values, root) {
128
128
  ? path.join(prototypesDir, `${values.folder}.folder`, values.name)
129
129
  : path.join(prototypesDir, values.name)
130
130
 
131
- fs.mkdirSync(targetDir, { recursive: true })
132
-
133
131
  const files = []
134
132
  const relDir = path.relative(root, targetDir)
135
133
 
@@ -148,6 +146,22 @@ function createPrototype(values, root) {
148
146
  return { success: false, errors: [{ message: partialError }] }
149
147
  }
150
148
 
149
+ // Pre-resolve the partial's component file *before* writing anything.
150
+ // If the partial's template/recipe directory is missing or empty we'd
151
+ // otherwise leave an orphan .prototype.json on disk and Vite would
152
+ // then try (and fail) to lazy-load a non-existent index.jsx. Validate
153
+ // first, write second.
154
+ let componentFile = null
155
+ if (partialEntry && !values.url) {
156
+ const partialDir = path.join(root, 'src', partialEntry.baseDir, partialEntry.name)
157
+ componentFile = findComponentFile(partialDir)
158
+ if (!componentFile) {
159
+ return { success: false, errors: [{ message: `No .jsx or .tsx file found in src/${partialEntry.baseDir}/${partialEntry.name}/` }] }
160
+ }
161
+ }
162
+
163
+ fs.mkdirSync(targetDir, { recursive: true })
164
+
151
165
  // Write .prototype.json (with $global from partial when applicable)
152
166
  const protoFile = `${values.name}.prototype.json`
153
167
  const protoBody = JSON.parse(prototypeJson(values))
@@ -159,22 +173,14 @@ function createPrototype(values, root) {
159
173
 
160
174
  // Write index.jsx (only for non-external prototypes)
161
175
  if (!values.url) {
162
- let body
163
- if (partialEntry) {
164
- const partialDir = path.join(root, 'src', partialEntry.baseDir, partialEntry.name)
165
- const componentFile = findComponentFile(partialDir)
166
- if (!componentFile) {
167
- return { success: false, errors: [{ message: `No .jsx or .tsx file found in src/${partialEntry.baseDir}/${partialEntry.name}/` }] }
168
- }
169
- body = renderPartialIndexJsx({
176
+ const body = partialEntry
177
+ ? renderPartialIndexJsx({
170
178
  partialEntry,
171
179
  componentFile,
172
180
  componentName: toPascalCase(values.name),
173
181
  title: values.title || values.name,
174
182
  })
175
- } else {
176
- body = prototypeIndexJsx(values.name)
177
- }
183
+ : prototypeIndexJsx(values.name)
178
184
  fs.writeFileSync(path.join(targetDir, 'index.jsx'), body, 'utf-8')
179
185
  files.push(`${relDir}/index.jsx`)
180
186
  }
@@ -21,7 +21,7 @@ describe('autosync scope helpers', () => {
21
21
  expect(matchesAutosyncScope('canvas', 'src/canvas/notes.txt')).toBe(true)
22
22
  expect(matchesAutosyncScope('canvas', 'assets/canvas/images/photo.png')).toBe(true)
23
23
  expect(matchesAutosyncScope('canvas', 'assets/canvas/snapshots/snapshot-widget--latest.webp')).toBe(true)
24
- expect(matchesAutosyncScope('canvas', 'assets/.storyboard-public/terminal-snapshots/agent-abc.snapshot.json')).toBe(true)
24
+ expect(matchesAutosyncScope('canvas', 'assets/.storyboard-public/terminal-snapshots/agents.snapshot.json')).toBe(true)
25
25
  expect(matchesAutosyncScope('canvas', 'src/prototypes/demo/default.flow.json')).toBe(false)
26
26
  })
27
27
 
@@ -153,6 +153,10 @@ function toContext(target) {
153
153
 
154
154
  function buildPullRequestSnapshot(target, pr) {
155
155
  const number = pr?.number ?? target.number
156
+ const merged = Boolean(pr?.merged_at || pr?.merged)
157
+ const draft = Boolean(pr?.draft)
158
+ const rawState = pr?.state === 'closed' ? 'closed' : 'open'
159
+ const state = merged ? 'merged' : (draft && rawState === 'open' ? 'draft' : rawState)
156
160
  return {
157
161
  kind: 'pull_request',
158
162
  parentKind: 'pull_request',
@@ -164,6 +168,14 @@ function buildPullRequestSnapshot(target, pr) {
164
168
  createdAt: pr?.created_at ?? null,
165
169
  updatedAt: pr?.updated_at ?? null,
166
170
  url: target.url,
171
+ state,
172
+ merged,
173
+ draft,
174
+ baseRef: typeof pr?.base?.ref === 'string' ? pr.base.ref : null,
175
+ headRef: typeof pr?.head?.ref === 'string' ? pr.head.ref : null,
176
+ additions: typeof pr?.additions === 'number' ? pr.additions : null,
177
+ deletions: typeof pr?.deletions === 'number' ? pr.deletions : null,
178
+ changedFiles: typeof pr?.changed_files === 'number' ? pr.changed_files : null,
167
179
  }
168
180
  }
169
181
 
@@ -202,6 +202,76 @@ describe('fetchGitHubEmbedSnapshot', () => {
202
202
  expect(graphqlArgs.slice(0, 2)).toEqual(['api', 'graphql'])
203
203
  })
204
204
 
205
+ it('hydrates pull request snapshot metadata (state, branches, diff)', () => {
206
+ ghExec
207
+ .mockReturnValueOnce('gh version 2.58.0')
208
+ .mockReturnValueOnce(JSON.stringify({
209
+ number: 79,
210
+ title: 'fix(viewfinder): eliminate FOUC',
211
+ body: 'A PR body',
212
+ state: 'closed',
213
+ merged: true,
214
+ merged_at: '2026-04-10T00:00:00Z',
215
+ draft: false,
216
+ user: { login: 'dfosco' },
217
+ created_at: '2026-04-01T00:00:00Z',
218
+ updated_at: '2026-04-10T00:00:00Z',
219
+ base: { ref: '3.11.0' },
220
+ head: { ref: '3.11.0--fix-viewfinder-fouc' },
221
+ additions: 63,
222
+ deletions: 18,
223
+ changed_files: 4,
224
+ }))
225
+
226
+ const snapshot = fetchGitHubEmbedSnapshot('https://github.com/dfosco/storyboard/pull/79')
227
+
228
+ expect(snapshot).toMatchObject({
229
+ kind: 'pull_request',
230
+ parentKind: 'pull_request',
231
+ title: '#79 fix(viewfinder): eliminate FOUC',
232
+ state: 'merged',
233
+ merged: true,
234
+ draft: false,
235
+ baseRef: '3.11.0',
236
+ headRef: '3.11.0--fix-viewfinder-fouc',
237
+ additions: 63,
238
+ deletions: 18,
239
+ changedFiles: 4,
240
+ })
241
+ })
242
+
243
+ it('marks draft PRs and unmerged closed PRs correctly', () => {
244
+ ghExec
245
+ .mockReturnValueOnce('gh version 2.58.0')
246
+ .mockReturnValueOnce(JSON.stringify({
247
+ number: 5,
248
+ title: 'WIP: refactor',
249
+ state: 'open',
250
+ draft: true,
251
+ merged: false,
252
+ user: { login: 'dfosco' },
253
+ }))
254
+
255
+ const draftSnap = fetchGitHubEmbedSnapshot('https://github.com/dfosco/storyboard/pull/5')
256
+ expect(draftSnap.state).toBe('draft')
257
+ expect(draftSnap.draft).toBe(true)
258
+ expect(draftSnap.merged).toBe(false)
259
+
260
+ ghExec
261
+ .mockReturnValueOnce('gh version 2.58.0')
262
+ .mockReturnValueOnce(JSON.stringify({
263
+ number: 6,
264
+ state: 'closed',
265
+ merged: false,
266
+ merged_at: null,
267
+ user: { login: 'dfosco' },
268
+ }))
269
+
270
+ const closedSnap = fetchGitHubEmbedSnapshot('https://github.com/dfosco/storyboard/pull/6')
271
+ expect(closedSnap.state).toBe('closed')
272
+ expect(closedSnap.merged).toBe(false)
273
+ })
274
+
205
275
  it('rejects issue comment URLs when comment parent does not match', () => {
206
276
  ghExec
207
277
  .mockReturnValueOnce('gh version 2.58.0')
@@ -270,8 +340,7 @@ describe('fetchGitHubEmbedSnapshot', () => {
270
340
  })
271
341
  })
272
342
 
273
- it('hydrates PR top-level comment snapshot metadata', () => {
274
- ghExec
343
+ it('hydrates PR top-level comment snapshot metadata', () => { ghExec
275
344
  .mockReturnValueOnce('gh version 2.58.0')
276
345
  .mockReturnValueOnce(JSON.stringify({
277
346
  body: 'Looks good!',
@@ -551,8 +551,8 @@ export class HotPool {
551
551
  const paneContent = execSync(
552
552
  // H1: include scrollback so the readiness echo can still be
553
553
  // matched after the agent's TUI repaints over it.
554
- `tmux capture-pane -t "${tmuxName}" -p -S -200`,
555
- { encoding: 'utf8', timeout: 1000 }
554
+ `tmux capture-pane -t "${tmuxName}" -p -S -200 2>/dev/null`,
555
+ { encoding: 'utf8', timeout: 1000, stdio: ['ignore', 'pipe', 'ignore'] }
556
556
  )
557
557
  // Strip ANSI escape sequences — agent CLIs use heavy formatting
558
558
  // eslint-disable-next-line no-control-regex
@@ -580,14 +580,13 @@ export class HotPool {
580
580
  return this.#tmuxSessionExists(tmuxName)
581
581
  }
582
582
 
583
- // Send postStartup command (e.g. "/allow-all on")
583
+ // postStartup is NOT sent here intentionally. It's deferred to widget
584
+ // handoff (see terminal-server.js warm path) so it runs *after* the
585
+ // identity/role injection — mode-toggling slash commands like
586
+ // `/autopilot` must come last so canvas context lands in conversation
587
+ // history before the agent enters autopilot mode.
584
588
  if (postStartup) {
585
- try {
586
- await new Promise(r => setTimeout(r, 500))
587
- execSync(`tmux send-keys -t "${tmuxName}" -l ${JSON.stringify(postStartup)}`, { stdio: 'ignore' })
588
- execSync(`tmux send-keys -t "${tmuxName}" Enter`, { stdio: 'ignore' })
589
- this.#log(`⊕ AGENT ${sessionId} postStartup sent: ${postStartup}`)
590
- } catch { /* empty */ }
589
+ this.#log(`⊕ AGENT ${sessionId} postStartup "${postStartup}" deferred to widget handoff`)
591
590
  }
592
591
 
593
592
  this.#log(`⊕ AGENT ${sessionId} ready (${signalFilePath ? 'file' : 'signal'}: "${readinessSignal || 'file'}")`)
@@ -615,8 +614,8 @@ export class HotPool {
615
614
  if (this.#agentConfig?.startupCommand) {
616
615
  try {
617
616
  const cmd = execSync(
618
- `tmux display-message -t "${session.tmuxName}" -p "#{pane_current_command}"`,
619
- { encoding: 'utf8', timeout: 1000 }
617
+ `tmux display-message -t "${session.tmuxName}" -p "#{pane_current_command}" 2>/dev/null`,
618
+ { encoding: 'utf8', timeout: 1000, stdio: ['ignore', 'pipe', 'ignore'] }
620
619
  ).trim()
621
620
  // Agent exited if the pane is back to a shell
622
621
  const shells = ['zsh', 'bash', 'sh', 'fish']
@@ -7,17 +7,36 @@
7
7
  * Event types:
8
8
  * canvas_created — full initial state (first line)
9
9
  * widget_added — append a widget
10
- * widget_updated — patch widget props
11
- * widget_moved — update widget position
12
- * widget_removed — remove a widget by id
13
- * settings_updated patch canvas-level settings
14
- * source_updated replace the sources array
15
- * widgets_replaced — replace the entire widgets array (bulk update)
10
+ * widget_updated — patch widget props (may carry `prevProps` for undo)
11
+ * widget_moved — update widget position (may carry `prevPosition` for undo)
12
+ * widget_removed — remove a widget by id (carries `widget` for undo;
13
+ * legacy events only had `widgetId`)
14
+ * settings_updated patch canvas-level settings (may carry `prevSettings` for undo)
15
+ * source_updated — replace the sources array (may carry `prevSources` for undo)
16
+ * widgets_replaced — replace the entire widgets array (compaction only, not undoable)
16
17
  * connector_added — append a connector between two widgets
17
- * connector_removed — remove a connector by id
18
- * connectors_replaced replace the entire connectors array (bulk update, used by undo/redo)
19
- * connector_waypoints_set set/replace manual routing waypoints on a connector
20
- * connector_waypoints_cleared drop manual routing, revert to auto-routing
18
+ * connector_removed — remove a connector by id (carries `connector` for undo;
19
+ * legacy events only had `connectorId`)
20
+ * connectors_replaced — replace the entire connectors array (compaction only, not undoable)
21
+ * connector_waypoints_set set/replace manual routing waypoints (may carry `prevWaypoints`)
22
+ * connector_waypoints_cleared — drop manual routing (may carry `prevWaypoints`)
23
+ * connector_updated — patch connector anchors/meta (may carry `prevUpdates` for undo)
24
+ *
25
+ * Event metadata (all events):
26
+ * id — short random id stamped by the server, used by undo/redo
27
+ * to target a specific event. Legacy events without an id are
28
+ * non-undoable but materialize normally.
29
+ * meta.kind — present on undo/redo emissions ('undo' | 'redo'). The
30
+ * materializer ignores meta; it's informational for tools
31
+ * and a future timeline UI.
32
+ * meta.of — the id of the event this undo/redo targets.
33
+ *
34
+ * Undo model: the materializer never "skips" events. Undo is a normal
35
+ * forward event (a real `widget_added`/`widget_removed`/`widget_updated`)
36
+ * that happens to carry meta.kind === 'undo'. Folding is one pass, no
37
+ * special cases. Inverse payloads (`prevProps`, `prevPosition`, full
38
+ * `widget`/`connector` on removes, etc.) are read only by the server's
39
+ * POST /undo and POST /redo endpoints when computing the inverse event.
21
40
  */
22
41
 
23
42
  /**
@@ -134,13 +153,16 @@ export function materialize(events) {
134
153
  }
135
154
 
136
155
  case 'widget_removed': {
156
+ // Back-compat: legacy events only had `widgetId`; new events carry
157
+ // the full `widget` object so undo can re-add without history walk.
158
+ const removedId = evt.widget?.id ?? evt.widgetId
137
159
  state.widgets = (state.widgets || []).filter(
138
- (w) => w.id !== evt.widgetId,
160
+ (w) => w.id !== removedId,
139
161
  )
140
162
  // Cascade: remove connectors referencing the deleted widget
141
163
  if (state.connectors?.length) {
142
164
  state.connectors = state.connectors.filter(
143
- (c) => c.start.widgetId !== evt.widgetId && c.end.widgetId !== evt.widgetId,
165
+ (c) => c.start.widgetId !== removedId && c.end.widgetId !== removedId,
144
166
  )
145
167
  }
146
168
  break
@@ -179,8 +201,12 @@ export function materialize(events) {
179
201
  }
180
202
 
181
203
  case 'connector_removed': {
204
+ // Back-compat: legacy events only had `connectorId`; new events
205
+ // carry the full `connector` object so undo can re-add without
206
+ // history walk.
207
+ const removedId = evt.connector?.id ?? evt.connectorId
182
208
  state.connectors = (state.connectors || []).filter(
183
- (c) => c.id !== evt.connectorId,
209
+ (c) => c.id !== removedId,
184
210
  )
185
211
  break
186
212
  }