@puredesktop/puredesktop-ui-bridge 1.0.0-beta.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 (412) hide show
  1. package/package.json +96 -0
  2. package/src/agents/runtime/agentLiveReducer.test.ts +72 -0
  3. package/src/agents/runtime/agentLiveReducer.ts +139 -0
  4. package/src/agents/runtime/agentRunEventTypes.ts +115 -0
  5. package/src/agents/runtime/contextWindowUsage.test.ts +38 -0
  6. package/src/agents/runtime/contextWindowUsage.ts +25 -0
  7. package/src/agents/runtime/index.ts +24 -0
  8. package/src/agents/runtime/mapAgentMessages.ts +54 -0
  9. package/src/agents/runtime/mapAgentSnapshot.ts +71 -0
  10. package/src/agents/runtime/mergeUsageTurn.ts +23 -0
  11. package/src/agents/runtime/pendingToolCalls.test.ts +85 -0
  12. package/src/agents/runtime/pendingToolCalls.ts +14 -0
  13. package/src/agents/runtime/toolRequiresApproval.ts +20 -0
  14. package/src/agents/runtime/unresolvedToolCalls.ts +42 -0
  15. package/src/bridge/agentModelTiers.ts +50 -0
  16. package/src/bridge/agentToolHandlers.ts +23 -0
  17. package/src/bridge/agentToolHelpers.test.ts +27 -0
  18. package/src/bridge/agentToolHelpers.ts +20 -0
  19. package/src/bridge/agentTypes.ts +294 -0
  20. package/src/bridge/agents.d.mts +113 -0
  21. package/src/bridge/agents.mjs +233 -0
  22. package/src/bridge/agents.ts +62 -0
  23. package/src/bridge/appSettings.d.mts +9 -0
  24. package/src/bridge/appSettings.mjs +18 -0
  25. package/src/bridge/appSettings.ts +4 -0
  26. package/src/bridge/applyThemeFromHost.d.mts +7 -0
  27. package/src/bridge/applyThemeFromHost.mjs +33 -0
  28. package/src/bridge/assets.d.mts +66 -0
  29. package/src/bridge/assets.mjs +121 -0
  30. package/src/bridge/catalog.d.mts +5 -0
  31. package/src/bridge/catalog.mjs +24 -0
  32. package/src/bridge/catalog.ts +1 -0
  33. package/src/bridge/client.d.mts +19 -0
  34. package/src/bridge/client.mjs +147 -0
  35. package/src/bridge/collectionAssetAdapter.ts +63 -0
  36. package/src/bridge/collectionAssets.d.mts +1 -0
  37. package/src/bridge/collectionAssets.mjs +2 -0
  38. package/src/bridge/collectionAssets.ts +1 -0
  39. package/src/bridge/collectionDocumentHtml.test.ts +71 -0
  40. package/src/bridge/collectionDocumentHtml.ts +118 -0
  41. package/src/bridge/collectionImagePaste.test.ts +32 -0
  42. package/src/bridge/collectionImagePaste.ts +130 -0
  43. package/src/bridge/context.mjs +12 -0
  44. package/src/bridge/credentials.mjs +22 -0
  45. package/src/bridge/dialog.d.mts +5 -0
  46. package/src/bridge/dialog.mjs +13 -0
  47. package/src/bridge/events.d.mts +14 -0
  48. package/src/bridge/events.mjs +12 -0
  49. package/src/bridge/filePreviewKind.test.ts +43 -0
  50. package/src/bridge/filePreviewKind.ts +76 -0
  51. package/src/bridge/fs.d.mts +41 -0
  52. package/src/bridge/fs.mjs +82 -0
  53. package/src/bridge/fs.test.ts +16 -0
  54. package/src/bridge/fs.ts +10 -0
  55. package/src/bridge/index.ts +12 -0
  56. package/src/bridge/inlinePreview.d.mts +35 -0
  57. package/src/bridge/inlinePreview.mjs +99 -0
  58. package/src/bridge/inlinePreview.test.ts +56 -0
  59. package/src/bridge/inlinePreview.ts +13 -0
  60. package/src/bridge/methods.d.mts +115 -0
  61. package/src/bridge/methods.mjs +132 -0
  62. package/src/bridge/os.d.mts +11 -0
  63. package/src/bridge/os.mjs +26 -0
  64. package/src/bridge/plugins.d.mts +9 -0
  65. package/src/bridge/plugins.mjs +31 -0
  66. package/src/bridge/plugins.ts +5 -0
  67. package/src/bridge/preferences.d.mts +7 -0
  68. package/src/bridge/preferences.mjs +17 -0
  69. package/src/bridge/preferences.ts +13 -0
  70. package/src/bridge/printDocument.test.ts +47 -0
  71. package/src/bridge/printDocument.ts +75 -0
  72. package/src/bridge/pureRender/adapters.test.ts +144 -0
  73. package/src/bridge/pureRender/adapters.ts +175 -0
  74. package/src/bridge/pureRender/assets.test.ts +44 -0
  75. package/src/bridge/pureRender/base.css +76 -0
  76. package/src/bridge/pureRender/compiler.test.ts +279 -0
  77. package/src/bridge/pureRender/compiler.ts +660 -0
  78. package/src/bridge/pureRender/document.test.ts +283 -0
  79. package/src/bridge/pureRender/document.ts +35 -0
  80. package/src/bridge/pureRender/extraction.test.ts +432 -0
  81. package/src/bridge/pureRender/extraction.ts +1023 -0
  82. package/src/bridge/pureRender/flow.css +3 -0
  83. package/src/bridge/pureRender/index.ts +66 -0
  84. package/src/bridge/pureRender/normalize.test.ts +442 -0
  85. package/src/bridge/pureRender/normalize.ts +682 -0
  86. package/src/bridge/pureRender/presets.ts +365 -0
  87. package/src/bridge/pureRender/profiles/book.css +19 -0
  88. package/src/bridge/pureRender/profiles/manuscript.css +11 -0
  89. package/src/bridge/pureRender/profiles/writer.css +10 -0
  90. package/src/bridge/pureRender/reset.css +5 -0
  91. package/src/bridge/pureRender/samples.ts +94 -0
  92. package/src/bridge/pureRender/styles.ts +164 -0
  93. package/src/bridge/pureRender/theme.test.ts +53 -0
  94. package/src/bridge/pureRender/theme.ts +258 -0
  95. package/src/bridge/pureRender/tokens/default.css +36 -0
  96. package/src/bridge/pureRender/tokens/default.json +32 -0
  97. package/src/bridge/pureRender/types.ts +264 -0
  98. package/src/bridge/pureRender/visualLayoutInspection.test.ts +57 -0
  99. package/src/bridge/pureRender/visualLayoutInspection.ts +161 -0
  100. package/src/bridge/react/useDebouncedEffect.ts +21 -0
  101. package/src/bridge/react/useIpc.tsx +75 -0
  102. package/src/bridge/react/usePlatformAgentRunLiveState.tsx +78 -0
  103. package/src/bridge/react/usePlatformAgentSessionDrive.tsx +729 -0
  104. package/src/bridge/react/usePlatformAgentSessions.tsx +60 -0
  105. package/src/bridge/react/usePlatformAgentTools.tsx +82 -0
  106. package/src/bridge/react/usePlatformAppManifest.tsx +32 -0
  107. package/src/bridge/react/usePlatformBridge.tsx +56 -0
  108. package/src/bridge/react/usePlatformDeepLink.tsx +57 -0
  109. package/src/bridge/react/usePlatformFileBinaryDataUrl.tsx +54 -0
  110. package/src/bridge/react/usePlatformFilePreview.tsx +92 -0
  111. package/src/bridge/react/usePlatformPreferences.tsx +77 -0
  112. package/src/bridge/react/usePlatformThemeDocumentAttrs.ts +35 -0
  113. package/src/bridge/react/usePlatformThemeFromBridge.tsx +16 -0
  114. package/src/bridge/react/usePlatformViewportResource.test.tsx +106 -0
  115. package/src/bridge/react/usePlatformViewportResource.tsx +72 -0
  116. package/src/bridge/react/useScheduledTask.tsx +59 -0
  117. package/src/bridge/render.d.mts +31 -0
  118. package/src/bridge/render.mjs +62 -0
  119. package/src/bridge/render.ts +8 -0
  120. package/src/bridge/screenAnnotation.test.ts +103 -0
  121. package/src/bridge/screenAnnotation.ts +269 -0
  122. package/src/bridge/theme.d.mts +3 -0
  123. package/src/bridge/theme.mjs +9 -0
  124. package/src/bridge/types.ts +439 -0
  125. package/src/bridge/watch.d.mts +9 -0
  126. package/src/bridge/watch.mjs +34 -0
  127. package/src/bridge/watch.ts +1 -0
  128. package/src/bridge/window.d.mts +44 -0
  129. package/src/bridge/window.mjs +33 -0
  130. package/src/chrome/index.ts +1 -0
  131. package/src/components/accessibility/AccessibilityReviewDialog.test.tsx +104 -0
  132. package/src/components/accessibility/AccessibilityReviewDialog.tsx +350 -0
  133. package/src/components/accessibility/accessibilityReview.test.ts +101 -0
  134. package/src/components/accessibility/accessibilityReview.ts +639 -0
  135. package/src/components/accessibility/index.ts +16 -0
  136. package/src/components/agent-feedback/ScreenAnnotationAgentDialog.tsx +509 -0
  137. package/src/components/agent-feedback/index.ts +5 -0
  138. package/src/components/agents/AgentComposer.tsx +34 -0
  139. package/src/components/agents/AgentContextUsageBar.tsx +127 -0
  140. package/src/components/agents/AgentDrawerPanel.tsx +82 -0
  141. package/src/components/agents/AgentMarkdown.tsx +12 -0
  142. package/src/components/agents/AgentMessageBubble.tsx +82 -0
  143. package/src/components/agents/AgentMessageList.tsx +47 -0
  144. package/src/components/agents/AgentModelMenu.tsx +55 -0
  145. package/src/components/agents/AgentThinkingBlock.tsx +66 -0
  146. package/src/components/agents/AgentToolApprovalActions.tsx +65 -0
  147. package/src/components/agents/AgentToolCallCard.tsx +26 -0
  148. package/src/components/agents/AgentToolPendingList.tsx +39 -0
  149. package/src/components/agents/AgentToolPendingReview.tsx +118 -0
  150. package/src/components/agents/agentPanelStyles.ts +79 -0
  151. package/src/components/agents/agentTypes.ts +96 -0
  152. package/src/components/agents/formatToolCallArguments.test.ts +16 -0
  153. package/src/components/agents/formatToolCallArguments.ts +9 -0
  154. package/src/components/agents/index.ts +40 -0
  155. package/src/components/assets/AssetLibraryDialog.tsx +5 -0
  156. package/src/components/assets/asset-library/AssetLibraryDialog.tsx +43 -0
  157. package/src/components/assets/asset-library/AssetLibraryWorkspace.tsx +54 -0
  158. package/src/components/assets/asset-library/detail/AssetLibraryDetailActions.tsx +68 -0
  159. package/src/components/assets/asset-library/detail/AssetLibraryDetailHeader.tsx +56 -0
  160. package/src/components/assets/asset-library/detail/AssetLibraryDetailPane.tsx +90 -0
  161. package/src/components/assets/asset-library/detail/AssetLibraryDetailPreview.tsx +67 -0
  162. package/src/components/assets/asset-library/detail/AssetLibraryInlinePreview.tsx +87 -0
  163. package/src/components/assets/asset-library/detail/AssetLibraryInlineSourceField.tsx +29 -0
  164. package/src/components/assets/asset-library/detail/AssetLibraryMetadataFields.tsx +34 -0
  165. package/src/components/assets/asset-library/detail/AssetLibraryUsagePanel.tsx +46 -0
  166. package/src/components/assets/asset-library/detail/AssetLibraryVersionsPanel.tsx +145 -0
  167. package/src/components/assets/asset-library/helpers/assetKind.ts +74 -0
  168. package/src/components/assets/asset-library/helpers.ts +41 -0
  169. package/src/components/assets/asset-library/hooks/useAssetDetailDraft.ts +81 -0
  170. package/src/components/assets/asset-library/hooks/useAssetLibraryDialog.ts +233 -0
  171. package/src/components/assets/asset-library/hooks/useAssetLibraryMutations.ts +266 -0
  172. package/src/components/assets/asset-library/hooks/useAssetLibrarySelectionSync.ts +24 -0
  173. package/src/components/assets/asset-library/hooks/useAssetLibraryVersions.ts +145 -0
  174. package/src/components/assets/asset-library/hooks/useInlineAssetPreview.ts +44 -0
  175. package/src/components/assets/asset-library/index.ts +19 -0
  176. package/src/components/assets/asset-library/layout/assetLibraryLayout.ts +201 -0
  177. package/src/components/assets/asset-library/shared/fieldStyles.ts +62 -0
  178. package/src/components/assets/asset-library/sidebar/AssetLibraryFilterPills.tsx +89 -0
  179. package/src/components/assets/asset-library/sidebar/AssetLibraryList.tsx +91 -0
  180. package/src/components/assets/asset-library/sidebar/AssetLibraryListRow.tsx +99 -0
  181. package/src/components/assets/asset-library/sidebar/AssetLibraryListStatus.tsx +24 -0
  182. package/src/components/assets/asset-library/sidebar/AssetLibrarySidebar.tsx +48 -0
  183. package/src/components/assets/asset-library/sidebar/AssetLibrarySidebarActions.tsx +38 -0
  184. package/src/components/assets/asset-library/sidebar/AssetLibrarySidebarToolbar.tsx +78 -0
  185. package/src/components/assets/asset-library/sidebar/AssetListThumbnail.tsx +86 -0
  186. package/src/components/assets/asset-library/types.ts +128 -0
  187. package/src/components/assets/asset-library/utils/assetLibraryUtils.ts +181 -0
  188. package/src/components/assets/asset-library/utils/pathBasename.ts +5 -0
  189. package/src/components/assets/asset-library/utils/sanitizeUploadFileName.ts +7 -0
  190. package/src/components/assets/asset-library/utils/uploadCollectionAsset.ts +52 -0
  191. package/src/components/chrome/OverlayActionCapsule.test.tsx +82 -0
  192. package/src/components/chrome/OverlayActionCapsule.tsx +90 -0
  193. package/src/components/chrome/PanelTabStrip.tsx +113 -0
  194. package/src/components/chrome/PlatformIcon.tsx +27 -0
  195. package/src/components/chrome/PureAppWordmark.tsx +89 -0
  196. package/src/components/chrome/ShellWindowControls.tsx +51 -0
  197. package/src/components/chrome/ThemeModeButton.tsx +16 -0
  198. package/src/components/chrome/Titlebar.tsx +79 -0
  199. package/src/components/chrome/WorkspaceTabStrip.tsx +300 -0
  200. package/src/components/chrome/index.ts +31 -0
  201. package/src/components/chrome/workspaceTabTypes.ts +29 -0
  202. package/src/components/common/buttons/Button.tsx +241 -0
  203. package/src/components/common/buttons/ChoiceChip.tsx +73 -0
  204. package/src/components/common/buttons/CleanButton.tsx +34 -0
  205. package/src/components/common/buttons/IconButton.tsx +89 -0
  206. package/src/components/common/buttons/QuietAction.tsx +29 -0
  207. package/src/components/common/buttons/SegmentedControl.tsx +67 -0
  208. package/src/components/common/buttons/TextActionButton.tsx +55 -0
  209. package/src/components/common/buttons/TextLink.tsx +38 -0
  210. package/src/components/common/canvas/CanvasElementFrame.test.tsx +79 -0
  211. package/src/components/common/canvas/CanvasElementFrame.tsx +182 -0
  212. package/src/components/common/canvas/canvasGeometry.test.ts +66 -0
  213. package/src/components/common/canvas/canvasGeometry.ts +133 -0
  214. package/src/components/common/canvas/index.ts +27 -0
  215. package/src/components/common/canvas/useCanvasDragResize.test.tsx +152 -0
  216. package/src/components/common/canvas/useCanvasDragResize.ts +161 -0
  217. package/src/components/common/chat/ChatAssistantMessage.tsx +81 -0
  218. package/src/components/common/chat/ChatBox.tsx +189 -0
  219. package/src/components/common/chat/ChatMarkdown.tsx +109 -0
  220. package/src/components/common/chat/ChatModelMenu.tsx +258 -0
  221. package/src/components/common/chat/ChatStatusRow.tsx +24 -0
  222. package/src/components/common/chat/ChatThinkingIndicator.tsx +54 -0
  223. package/src/components/common/chat/ChatThread.tsx +59 -0
  224. package/src/components/common/chat/ChatToolRow.tsx +141 -0
  225. package/src/components/common/chat/ChatUserBubble.tsx +45 -0
  226. package/src/components/common/chat/chatUtils.ts +7 -0
  227. package/src/components/common/chat/index.ts +24 -0
  228. package/src/components/common/connections/ConnectionSetup.tsx +529 -0
  229. package/src/components/common/connections/index.ts +26 -0
  230. package/src/components/common/containers/AppFrame.test.tsx +47 -0
  231. package/src/components/common/containers/AppFrame.tsx +65 -0
  232. package/src/components/common/containers/AppHeader.tsx +60 -0
  233. package/src/components/common/containers/AutoFillGrid.tsx +37 -0
  234. package/src/components/common/containers/Collapsible.tsx +43 -0
  235. package/src/components/common/containers/CollapsibleGrid.tsx +174 -0
  236. package/src/components/common/containers/MotionScope.tsx +32 -0
  237. package/src/components/common/containers/PageHeader.tsx +35 -0
  238. package/src/components/common/containers/PanelToolbarHeader.tsx +113 -0
  239. package/src/components/common/containers/QuietDisclosure.tsx +91 -0
  240. package/src/components/common/containers/QuietSurface.tsx +41 -0
  241. package/src/components/common/containers/ScrollPanel.tsx +33 -0
  242. package/src/components/common/containers/StatusBar.tsx +42 -0
  243. package/src/components/common/containers/SurfacePanel.tsx +66 -0
  244. package/src/components/common/containers/ToolbarStrip.tsx +80 -0
  245. package/src/components/common/containers/flex.ts +117 -0
  246. package/src/components/common/containers/useOpenTransition.ts +33 -0
  247. package/src/components/common/context/ContextSelector.tsx +269 -0
  248. package/src/components/common/context/index.ts +7 -0
  249. package/src/components/common/desk/ActionReader.tsx +270 -0
  250. package/src/components/common/desk/ActivityLedger.tsx +143 -0
  251. package/src/components/common/desk/CreationDesk.tsx +139 -0
  252. package/src/components/common/desk/DailyBrief.tsx +160 -0
  253. package/src/components/common/desk/DeskCard.tsx +157 -0
  254. package/src/components/common/desk/index.ts +30 -0
  255. package/src/components/common/dropdown/ContextMenu.tsx +87 -0
  256. package/src/components/common/dropdown/MenuButtonDropdown.tsx +121 -0
  257. package/src/components/common/dropdown/MenuDropdown.tsx +63 -0
  258. package/src/components/common/dropdown/MenuDropdownEntry.tsx +51 -0
  259. package/src/components/common/dropdown/MenuDropdownItem.tsx +82 -0
  260. package/src/components/common/dropdown/MenuDropdownSubmenu.tsx +145 -0
  261. package/src/components/common/dropdown/MenuPanel.tsx +69 -0
  262. package/src/components/common/dropdown/MenuPortal.tsx +162 -0
  263. package/src/components/common/dropdown/MenuPortalLayer.tsx +6 -0
  264. package/src/components/common/dropdown/SelectMenu.tsx +243 -0
  265. package/src/components/common/dropdown/SelectMenuControl.tsx +23 -0
  266. package/src/components/common/dropdown/SelectMenuOptions.tsx +38 -0
  267. package/src/components/common/dropdown/menuPortalConstants.ts +11 -0
  268. package/src/components/common/dropdown/menuTypes.ts +71 -0
  269. package/src/components/common/dropdown/menuViewportPosition.test.ts +139 -0
  270. package/src/components/common/dropdown/menuViewportPosition.ts +122 -0
  271. package/src/components/common/dropdown/selectMenuLayout.test.ts +42 -0
  272. package/src/components/common/dropdown/selectMenuLayout.ts +123 -0
  273. package/src/components/common/dropdown/useMenuAnchorPosition.ts +109 -0
  274. package/src/components/common/dropdown/useSelectMenuAnchor.ts +30 -0
  275. package/src/components/common/evidence/StructuredEvidenceView.test.tsx +77 -0
  276. package/src/components/common/evidence/StructuredEvidenceView.tsx +312 -0
  277. package/src/components/common/evidence/index.ts +13 -0
  278. package/src/components/common/feedback/Badge.tsx +33 -0
  279. package/src/components/common/feedback/CapabilityPills.tsx +36 -0
  280. package/src/components/common/feedback/EmptyState.tsx +132 -0
  281. package/src/components/common/feedback/InfoHint.tsx +67 -0
  282. package/src/components/common/feedback/InlineBanner.tsx +82 -0
  283. package/src/components/common/feedback/PlatformChip.tsx +104 -0
  284. package/src/components/common/feedback/QuietPill.tsx +75 -0
  285. package/src/components/common/feedback/WorkProgressPanel.tsx +156 -0
  286. package/src/components/common/feedback/index.ts +33 -0
  287. package/src/components/common/inputs/BareInput.tsx +31 -0
  288. package/src/components/common/inputs/BareTextarea.tsx +10 -0
  289. package/src/components/common/inputs/Checkbox.tsx +13 -0
  290. package/src/components/common/inputs/FieldControlShell.tsx +49 -0
  291. package/src/components/common/inputs/FormField.tsx +40 -0
  292. package/src/components/common/inputs/HybridSearchField.tsx +280 -0
  293. package/src/components/common/inputs/InlineComposer.tsx +197 -0
  294. package/src/components/common/inputs/PaletteCirclePicker.tsx +26 -0
  295. package/src/components/common/inputs/PaletteDropdown.tsx +413 -0
  296. package/src/components/common/inputs/Radio.tsx +19 -0
  297. package/src/components/common/inputs/SearchField.tsx +128 -0
  298. package/src/components/common/inputs/SelectField.tsx +169 -0
  299. package/src/components/common/inputs/SurfacePalettePicker.tsx +54 -0
  300. package/src/components/common/inputs/TextAreaField.tsx +22 -0
  301. package/src/components/common/inputs/TextField.tsx +51 -0
  302. package/src/components/common/inputs/ToggleSwitch.tsx +110 -0
  303. package/src/components/common/inputs/WorkspacePicker.tsx +349 -0
  304. package/src/components/common/inputs/fieldStyles.ts +137 -0
  305. package/src/components/common/layout/PanelGroup.tsx +479 -0
  306. package/src/components/common/layout/index.ts +31 -0
  307. package/src/components/common/layout/panelLayoutConstants.ts +7 -0
  308. package/src/components/common/layout/panelLayoutPersistence.ts +35 -0
  309. package/src/components/common/layout/panelLayoutState.test.ts +38 -0
  310. package/src/components/common/layout/panelLayoutState.ts +174 -0
  311. package/src/components/common/layout/panelLayoutTypes.ts +19 -0
  312. package/src/components/common/layout/tabs/Tabs.tsx +136 -0
  313. package/src/components/common/lists/listRowStyles.ts +32 -0
  314. package/src/components/common/lists/quiet-ledger/QuietActivityList.tsx +64 -0
  315. package/src/components/common/lists/quiet-ledger/QuietLedgerRow.tsx +119 -0
  316. package/src/components/common/lists/quiet-ledger/index.ts +9 -0
  317. package/src/components/common/lists/reorderable/DragHandle.tsx +65 -0
  318. package/src/components/common/lists/reorderable/ReorderableList.tsx +280 -0
  319. package/src/components/common/lists/reorderable/ReorderableListRow.tsx +102 -0
  320. package/src/components/common/lists/reorderable/ReorderableSections.tsx +344 -0
  321. package/src/components/common/lists/reorderable/ReorderableSectionsDemo.tsx +94 -0
  322. package/src/components/common/lists/reorderable/index.ts +15 -0
  323. package/src/components/common/lists/reorderable/reorderableUtils.test.ts +28 -0
  324. package/src/components/common/lists/reorderable/reorderableUtils.ts +23 -0
  325. package/src/components/common/lists/reorderable/types.ts +40 -0
  326. package/src/components/common/lists/reorderable/useReorderableDragState.ts +49 -0
  327. package/src/components/common/media/ImageReferencePicker.tsx +242 -0
  328. package/src/components/common/menubar/MenuBar.tsx +78 -0
  329. package/src/components/common/menubar/MenuBarTrigger.tsx +70 -0
  330. package/src/components/common/narrative/NarrativeMention.tsx +74 -0
  331. package/src/components/common/navigation/PathBreadcrumbs.tsx +78 -0
  332. package/src/components/common/navigation/UnderlineTabs.tsx +83 -0
  333. package/src/components/common/overlays/InstallAppPromptDialog.tsx +59 -0
  334. package/src/components/common/overlays/Modal.tsx +285 -0
  335. package/src/components/common/overlays/ModalChromeHeader.tsx +105 -0
  336. package/src/components/common/overlays/ObjectActionStrip.tsx +80 -0
  337. package/src/components/common/people/AutoUpdateReviewList.tsx +182 -0
  338. package/src/components/common/people/EntityList.tsx +122 -0
  339. package/src/components/common/people/EntityPortrait.tsx +64 -0
  340. package/src/components/common/people/LinkedDossierList.tsx +99 -0
  341. package/src/components/common/people/OpportunityList.tsx +94 -0
  342. package/src/components/common/people/PeopleSourceList.tsx +145 -0
  343. package/src/components/common/people/RelationshipDossier.tsx +168 -0
  344. package/src/components/common/people/RelationshipMap.tsx +362 -0
  345. package/src/components/common/people/index.ts +40 -0
  346. package/src/components/common/preview/FileAudioPreview.tsx +89 -0
  347. package/src/components/common/preview/FileImagePreview.tsx +88 -0
  348. package/src/components/common/preview/FilePdfPreview.tsx +87 -0
  349. package/src/components/common/preview/FileTextPreview.tsx +96 -0
  350. package/src/components/common/preview/FileVideoPreview.tsx +87 -0
  351. package/src/components/common/preview/PlatformFilePreview.tsx +166 -0
  352. package/src/components/common/preview/previewLayout.ts +27 -0
  353. package/src/components/common/render/Each.tsx +42 -0
  354. package/src/components/common/research/EvidenceDossier.tsx +894 -0
  355. package/src/components/common/research/index.ts +7 -0
  356. package/src/components/common/tiles/CatalogCard.tsx +116 -0
  357. package/src/components/common/tiles/Tile.tsx +45 -0
  358. package/src/components/common/tiles/TileGrid.tsx +13 -0
  359. package/src/components/common/typography/Heading.tsx +27 -0
  360. package/src/components/common/typography/Kicker.tsx +30 -0
  361. package/src/components/common/typography/Text.tsx +80 -0
  362. package/src/components/credentials/CredentialsPanel.tsx +169 -0
  363. package/src/components/credentials/credentialsTypes.ts +14 -0
  364. package/src/components/data/DataChart.tsx +913 -0
  365. package/src/components/data/dataTable.ts +99 -0
  366. package/src/components/editor/EditorCommentPromptDialog.tsx +124 -0
  367. package/src/components/editor/EditorLinkPromptDialog.tsx +65 -0
  368. package/src/components/print-design/PrintDesignPanel.test.tsx +1446 -0
  369. package/src/components/print-design/PrintDesignPanel.tsx +3043 -0
  370. package/src/components/print-design/index.ts +9 -0
  371. package/src/components/proofing/PagedProofDialog.tsx +453 -0
  372. package/src/components/proofing/index.ts +4 -0
  373. package/src/editor/DocumentEditor.tsx +28 -0
  374. package/src/editor/EditorViewModeToggle.tsx +35 -0
  375. package/src/editor/index.ts +97 -0
  376. package/src/editor/toolbarOverlays.ts +8 -0
  377. package/src/index.ts +209 -0
  378. package/src/theme/ThemeProvider.tsx +113 -0
  379. package/src/theme/appAccents.ts +87 -0
  380. package/src/theme/buildThemeBridgePayload.ts +23 -0
  381. package/src/theme/composePlatformTheme.test.ts +30 -0
  382. package/src/theme/composePlatformTheme.ts +23 -0
  383. package/src/theme/platformFont.d.mts +7 -0
  384. package/src/theme/platformFont.mjs +20 -0
  385. package/src/theme/platformFont.ts +6 -0
  386. package/src/theme/reportsShellChromeSelectors.ts +136 -0
  387. package/src/theme/styled.d.ts +5 -0
  388. package/src/theme/surfacePalettes.ts +178 -0
  389. package/src/theme/surfaceRoleSelectors.ts +113 -0
  390. package/src/theme/themeVars.ts +13 -0
  391. package/src/theme/themes/dark.ts +59 -0
  392. package/src/theme/themes/light.ts +60 -0
  393. package/src/theme/themes/palette.ts +44 -0
  394. package/src/theme/themes/shared.ts +68 -0
  395. package/src/theme/tokens.ts +114 -0
  396. package/src/theme/utils/GlobalStyles.ts +88 -0
  397. package/src/theme/utils/appFrameDocumentStyles.d.mts +5 -0
  398. package/src/theme/utils/appFrameDocumentStyles.mjs +89 -0
  399. package/src/theme/utils/appFrameDocumentStyles.ts +5 -0
  400. package/src/theme/utils/applyTheme.ts +50 -0
  401. package/src/theme/utils/cssVariables.ts +49 -0
  402. package/src/theme/utils/useInjectThemeIntoIframe.ts +40 -0
  403. package/src/utils/extensionIconMap.ts +123 -0
  404. package/src/utils/generalUtils.ts +157 -0
  405. package/src/writing/DraftAssistantDialog.tsx +279 -0
  406. package/src/writing/WritingStyleManager.tsx +416 -0
  407. package/src/writing/condenseWritingStyle.ts +103 -0
  408. package/src/writing/draftTextWithAgent.ts +134 -0
  409. package/src/writing/index.ts +39 -0
  410. package/src/writing/styleStore.ts +32 -0
  411. package/src/writing/writingStyle.test.ts +122 -0
  412. package/src/writing/writingStyle.ts +217 -0
@@ -0,0 +1,103 @@
1
+ import {
2
+ callAgentRunModel,
3
+ createAgentSession,
4
+ deleteAgentSession,
5
+ startAgentRun,
6
+ } from '../bridge/agents.js'
7
+
8
+ interface AgentSessionRecord {
9
+ sessionId: string
10
+ config: { modelId: string }
11
+ }
12
+
13
+ interface AgentRunSnapshot {
14
+ runId: string
15
+ state: string
16
+ messages: Array<{ role: string; content: string }>
17
+ error?: string
18
+ }
19
+
20
+ const MAX_TURNS = 4
21
+
22
+ const SYSTEM_PROMPT = [
23
+ 'You distill writing-style guidance into a compact, reusable style guide.',
24
+ 'Capture VOICE only — tone, register, sentence rhythm, word choice, and structural habits.',
25
+ 'Output 4–8 short imperative bullet lines, each starting with "- ".',
26
+ 'If the guidance lists words or phrases to PREFER or to AVOID, keep them verbatim as their own "- Prefer: …" and "- Avoid: …" bullets (these are in addition to the 4–8 voice bullets).',
27
+ 'Do NOT include the sample text, its facts, or its subject matter.',
28
+ 'Return plain text only — no preamble, no headings, no commentary.',
29
+ ].join('\n')
30
+
31
+ function latestAssistant(snapshot: AgentRunSnapshot): string {
32
+ return (
33
+ [...snapshot.messages]
34
+ .reverse()
35
+ .find(message => message.role === 'assistant')
36
+ ?.content.trim() ?? ''
37
+ )
38
+ }
39
+
40
+ /**
41
+ * Distill verbose style guidance (presets + voice samples) into a short,
42
+ * reusable style guide — the book's "style skill". Sent once and reused for
43
+ * every chapter draft/edit instead of the full samples, to save tokens.
44
+ * Falls back to the original guidance if the model is unavailable.
45
+ */
46
+ export async function condenseWritingStyleWithAgent(
47
+ fullGuidance: string,
48
+ ): Promise<string> {
49
+ const trimmed = fullGuidance.trim()
50
+ if (!trimmed) return ''
51
+
52
+ let session: AgentSessionRecord | null = null
53
+ try {
54
+ session = (await createAgentSession({
55
+ scope: 'app',
56
+ appSlug: 'book',
57
+ title: 'Condense writing style',
58
+ config: {
59
+ sessionPrompt: SYSTEM_PROMPT,
60
+ toolPolicy: {
61
+ allowedTargetApps: ['__purebook_style_no_tools__'],
62
+ defaultRequiresApproval: true,
63
+ },
64
+ } as never,
65
+ })) as AgentSessionRecord
66
+
67
+ let snapshot = (await startAgentRun({
68
+ session: { scope: 'app', sessionId: session.sessionId, appSlug: 'book' },
69
+ message: trimmed,
70
+ })) as AgentRunSnapshot
71
+
72
+ for (let turn = 0; turn < MAX_TURNS; turn += 1) {
73
+ if (snapshot.state === 'ready') {
74
+ snapshot = (await callAgentRunModel({
75
+ runId: snapshot.runId,
76
+ sessionId: session.sessionId,
77
+ })) as AgentRunSnapshot
78
+ continue
79
+ }
80
+ break
81
+ }
82
+
83
+ if (snapshot.state === 'awaiting_user') {
84
+ const condensed = latestAssistant(snapshot)
85
+ if (condensed) return condensed
86
+ }
87
+ return trimmed
88
+ } catch {
89
+ return trimmed
90
+ } finally {
91
+ if (session) {
92
+ try {
93
+ await deleteAgentSession({
94
+ scope: 'app',
95
+ sessionId: session.sessionId,
96
+ appSlug: 'book',
97
+ })
98
+ } catch {
99
+ /* best-effort cleanup */
100
+ }
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,134 @@
1
+ import {
2
+ callAgentRunModel,
3
+ createAgentSession,
4
+ deleteAgentSession,
5
+ startAgentRun,
6
+ } from '../bridge/agents.js'
7
+
8
+ interface AgentSessionRecord {
9
+ sessionId: string
10
+ config: { modelId: string }
11
+ }
12
+
13
+ interface AgentRunSnapshot {
14
+ runId: string
15
+ state: string
16
+ messages: Array<{ role: string; content: string }>
17
+ error?: string
18
+ }
19
+
20
+ const MAX_TURNS = 4
21
+
22
+ export type DraftTextMode = 'write' | 'rewrite'
23
+
24
+ export interface DraftTextInput {
25
+ mode: DraftTextMode
26
+ instruction: string
27
+ selectionText?: string
28
+ styleGuidance?: string
29
+ /**
30
+ * Grounding/reference material (e.g. a manuscript's abstract and reference
31
+ * list). Used for ACCURACY — facts, terminology, claims — not for voice.
32
+ */
33
+ context?: string
34
+ }
35
+
36
+ function systemPrompt(mode: DraftTextMode): string {
37
+ return [
38
+ mode === 'rewrite'
39
+ ? 'You rewrite a passage of a book to the author’s instruction, preserving its meaning.'
40
+ : 'You draft a passage of book prose from the author’s instruction.',
41
+ 'Return clean HTML only — paragraphs <p>, sub-headings <h2>, lists where useful.',
42
+ 'No markdown, no code fences, no <h1>, no preamble, no commentary.',
43
+ 'Do not invent precise statistics, dates, quotations, or citations.',
44
+ ].join('\n')
45
+ }
46
+
47
+ function message(input: DraftTextInput): string {
48
+ return [
49
+ input.mode === 'rewrite' && input.selectionText
50
+ ? `Passage to rewrite:\n"""${input.selectionText.slice(0, 6000)}"""`
51
+ : input.selectionText
52
+ ? `Nearby text for context (do not repeat):\n"""${input.selectionText.slice(0, 2000)}"""`
53
+ : '',
54
+ `Instruction: ${input.instruction.trim() || (input.mode === 'rewrite' ? 'Improve this passage.' : 'Draft this.')}`,
55
+ input.context?.trim()
56
+ ? `\nReference material — use it for facts, terminology, and claims; do NOT copy its wording as voice and do NOT invent beyond it:\n"""${input.context.trim().slice(0, 4000)}"""`
57
+ : '',
58
+ input.styleGuidance ? `\n${input.styleGuidance}` : '',
59
+ '',
60
+ 'Return the HTML now.',
61
+ ]
62
+ .filter(Boolean)
63
+ .join('\n')
64
+ }
65
+
66
+ function cleanHtml(raw: string): string {
67
+ let value = raw.trim()
68
+ value = value.replace(/^```(?:html)?\s*/i, '').replace(/\s*```$/i, '')
69
+ value = value.replace(/<h1[\s\S]*?<\/h1>/gi, '')
70
+ return value.trim()
71
+ }
72
+
73
+ function latestAssistant(snapshot: AgentRunSnapshot): string {
74
+ return (
75
+ [...snapshot.messages]
76
+ .reverse()
77
+ .find(entry => entry.role === 'assistant')
78
+ ?.content.trim() ?? ''
79
+ )
80
+ }
81
+
82
+ /** Draft or rewrite a passage via the agent. Returns HTML (empty on failure). */
83
+ export async function draftTextWithAgent(
84
+ input: DraftTextInput,
85
+ ): Promise<string> {
86
+ let session: AgentSessionRecord | null = null
87
+ try {
88
+ session = (await createAgentSession({
89
+ scope: 'app',
90
+ appSlug: 'book',
91
+ title: 'Draft passage',
92
+ config: {
93
+ sessionPrompt: systemPrompt(input.mode),
94
+ toolPolicy: {
95
+ allowedTargetApps: ['__purebook_draft_text_no_tools__'],
96
+ defaultRequiresApproval: true,
97
+ },
98
+ } as never,
99
+ })) as AgentSessionRecord
100
+
101
+ let snapshot = (await startAgentRun({
102
+ session: { scope: 'app', sessionId: session.sessionId, appSlug: 'book' },
103
+ message: message(input),
104
+ })) as AgentRunSnapshot
105
+
106
+ for (let turn = 0; turn < MAX_TURNS; turn += 1) {
107
+ if (snapshot.state === 'ready') {
108
+ snapshot = (await callAgentRunModel({
109
+ runId: snapshot.runId,
110
+ sessionId: session.sessionId,
111
+ })) as AgentRunSnapshot
112
+ continue
113
+ }
114
+ break
115
+ }
116
+
117
+ if (snapshot.state === 'awaiting_user') {
118
+ return cleanHtml(latestAssistant(snapshot))
119
+ }
120
+ throw new Error(snapshot.error || `The model stopped at ${snapshot.state}.`)
121
+ } finally {
122
+ if (session) {
123
+ try {
124
+ await deleteAgentSession({
125
+ scope: 'app',
126
+ sessionId: session.sessionId,
127
+ appSlug: 'book',
128
+ })
129
+ } catch {
130
+ /* best-effort cleanup */
131
+ }
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Reusable writing-style + AI drafting toolkit.
3
+ *
4
+ * App-agnostic: the host owns persistence of the style library (pass `styles`
5
+ * and `onStylesChange`); everything else — the named-style manager, the
6
+ * "distill into skill" flow, and the draft/redraft dialog — lives here so any
7
+ * app can adopt it.
8
+ */
9
+
10
+ export {
11
+ DraftAssistantDialog,
12
+ type DraftAssistantDialogProps,
13
+ } from './DraftAssistantDialog.js'
14
+ export {
15
+ WritingStyleManager,
16
+ type WritingStyleManagerProps,
17
+ } from './WritingStyleManager.js'
18
+ export {
19
+ draftTextWithAgent,
20
+ type DraftTextInput,
21
+ type DraftTextMode,
22
+ } from './draftTextWithAgent.js'
23
+ export { condenseWritingStyleWithAgent } from './condenseWritingStyle.js'
24
+ export {
25
+ loadSharedWritingStyles,
26
+ saveSharedWritingStyles,
27
+ } from './styleStore.js'
28
+ export {
29
+ EMPTY_WRITING_STYLE,
30
+ buildStyleGuidance,
31
+ createStyleSource,
32
+ createWritingStyle,
33
+ normalizeWritingStyle,
34
+ styleHasContent,
35
+ styleRawGuidance,
36
+ type BookWritingStyle,
37
+ type StyleSource,
38
+ type WritingStyle,
39
+ } from './writingStyle.js'
@@ -0,0 +1,32 @@
1
+ import { bridge, PLATFORM_BRIDGE_METHODS } from '../bridge/client.mjs'
2
+ import { normalizeWritingStyle, type BookWritingStyle } from './writingStyle.js'
3
+
4
+ /**
5
+ * Shared writing-style library — ONE set of named styles across every app.
6
+ *
7
+ * Stored as a single JSON file in the suite data dir (`~/PureScience/`), so a
8
+ * style created (and distilled) in one app is available in all of them. The
9
+ * bridge requires an appSlug but does not use it to scope the path, so every
10
+ * app that reads/writes this filename shares the same library.
11
+ */
12
+ const STORAGE_SLUG = 'writing'
13
+ const STORAGE_FILE = 'writing-styles.json'
14
+
15
+ export async function loadSharedWritingStyles(): Promise<BookWritingStyle> {
16
+ try {
17
+ const result = (await bridge.call(PLATFORM_BRIDGE_METHODS.STORAGE_READ_JSON, [
18
+ { appSlug: STORAGE_SLUG, fileName: STORAGE_FILE },
19
+ ])) as { value?: unknown } | null
20
+ return normalizeWritingStyle(result?.value)
21
+ } catch {
22
+ return normalizeWritingStyle(null)
23
+ }
24
+ }
25
+
26
+ export async function saveSharedWritingStyles(
27
+ style: BookWritingStyle,
28
+ ): Promise<void> {
29
+ await bridge.call(PLATFORM_BRIDGE_METHODS.STORAGE_WRITE_JSON, [
30
+ { appSlug: STORAGE_SLUG, fileName: STORAGE_FILE, value: style },
31
+ ])
32
+ }
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ buildStyleGuidance,
4
+ createWritingStyle,
5
+ normalizeWritingStyle,
6
+ styleHasContent,
7
+ type WritingStyle,
8
+ } from './writingStyle.js'
9
+
10
+ function styleWith(overrides: Partial<WritingStyle>): WritingStyle {
11
+ return { ...createWritingStyle('Test'), ...overrides }
12
+ }
13
+
14
+ describe('normalizeWritingStyle', () => {
15
+ it('returns an empty library for junk input', () => {
16
+ expect(normalizeWritingStyle(undefined)).toEqual({ styles: [] })
17
+ expect(normalizeWritingStyle({})).toEqual({ styles: [] })
18
+ expect(normalizeWritingStyle({ styles: 'nope' })).toEqual({ styles: [] })
19
+ })
20
+
21
+ it('keeps current-shape styles and drops empty ones', () => {
22
+ const { styles } = normalizeWritingStyle({
23
+ styles: [
24
+ { id: 'a', name: 'Docs', sources: [{ id: 's', label: 'x', text: 'hi' }] },
25
+ { id: 'b', name: 'Empty', sources: [], notes: '', skill: '' },
26
+ { id: 'c', name: 'Notes only', notes: 'be terse' },
27
+ ],
28
+ })
29
+ expect(styles.map(style => style.name)).toEqual(['Docs', 'Notes only'])
30
+ })
31
+
32
+ it('migrates legacy { examples } samples into styles', () => {
33
+ const { styles } = normalizeWritingStyle({
34
+ presets: ['academic'],
35
+ examples: [{ id: 'e1', label: 'My essays', text: 'A sample paragraph.' }],
36
+ })
37
+ expect(styles).toHaveLength(1)
38
+ expect(styles[0].name).toBe('My essays')
39
+ expect(styles[0].sources[0].text).toBe('A sample paragraph.')
40
+ // Legacy generic presets are intentionally discarded.
41
+ expect(styles[0]).not.toHaveProperty('presets')
42
+ })
43
+
44
+ it('disables a skill flag when there is no skill text', () => {
45
+ const { styles } = normalizeWritingStyle({
46
+ styles: [{ id: 'a', name: 'X', notes: 'n', skill: '', skillEnabled: true }],
47
+ })
48
+ expect(styles[0].skillEnabled).toBe(false)
49
+ })
50
+ })
51
+
52
+ describe('styleHasContent', () => {
53
+ it('is false for an empty style and true once it has sources/notes/skill', () => {
54
+ expect(styleHasContent(createWritingStyle())).toBe(false)
55
+ expect(styleHasContent(styleWith({ notes: 'be terse' }))).toBe(true)
56
+ expect(styleHasContent(styleWith({ skill: '- be terse' }))).toBe(true)
57
+ })
58
+ })
59
+
60
+ describe('buildStyleGuidance', () => {
61
+ it('returns undefined when nothing is selected', () => {
62
+ expect(buildStyleGuidance([])).toBeUndefined()
63
+ expect(buildStyleGuidance([createWritingStyle()])).toBeUndefined()
64
+ })
65
+
66
+ it('uses the distilled skill when enabled', () => {
67
+ const guidance = buildStyleGuidance([
68
+ styleWith({
69
+ name: 'Docs',
70
+ notes: 'raw notes that should be hidden',
71
+ skill: '- short sentences\n- active voice',
72
+ skillEnabled: true,
73
+ }),
74
+ ])
75
+ expect(guidance).toContain('Style — Docs:')
76
+ expect(guidance).toContain('short sentences')
77
+ expect(guidance).not.toContain('raw notes that should be hidden')
78
+ })
79
+
80
+ it('falls back to raw notes + voice-only samples when no skill is enabled', () => {
81
+ const guidance = buildStyleGuidance([
82
+ styleWith({
83
+ name: 'Fiction',
84
+ notes: 'lyrical',
85
+ sources: [{ id: 's', label: 'novel', text: 'Once upon a time.' }],
86
+ skill: '- ignored because disabled',
87
+ skillEnabled: false,
88
+ }),
89
+ ])
90
+ expect(guidance).toContain('Guidance: lyrical')
91
+ expect(guidance).toContain('Match the VOICE')
92
+ expect(guidance).toContain('Once upon a time.')
93
+ expect(guidance).not.toContain('ignored because disabled')
94
+ })
95
+
96
+ it('emits prefer/avoid word lists, normalising separators', () => {
97
+ const guidance = buildStyleGuidance([
98
+ styleWith({
99
+ name: 'Docs',
100
+ include: 'reader, cohort\nmodule',
101
+ exclude: 'utilise, leverage',
102
+ }),
103
+ ])
104
+ expect(guidance).toContain('Prefer these words and phrases')
105
+ expect(guidance).toContain('reader, cohort, module')
106
+ expect(guidance).toContain('Avoid these words and phrases')
107
+ expect(guidance).toContain('utilise, leverage')
108
+ })
109
+
110
+ it('counts a words-only style as having content', () => {
111
+ expect(styleHasContent(styleWith({ include: 'reader' }))).toBe(true)
112
+ expect(styleHasContent(styleWith({ exclude: 'utilise' }))).toBe(true)
113
+ })
114
+
115
+ it('migrates/keeps include and exclude through normalize', () => {
116
+ const { styles } = normalizeWritingStyle({
117
+ styles: [{ id: 'a', name: 'X', include: 'foo', exclude: 'bar' }],
118
+ })
119
+ expect(styles[0].include).toBe('foo')
120
+ expect(styles[0].exclude).toBe('bar')
121
+ })
122
+ })
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Writing-style guidance for book generation and in-editor drafting.
3
+ *
4
+ * There are no built-in generic presets. Every style is authored and named by
5
+ * the user. A style carries:
6
+ * - SOURCES: the author's own writing samples, mirrored for VOICE only and
7
+ * never lifted as content.
8
+ * - NOTES: free-text guidance the author writes themselves (their own rules).
9
+ * - SKILL: a compact, distilled style guide generated from the sources+notes
10
+ * (see condenseWritingStyleWithAgent). It is editable and, when enabled, is
11
+ * sent to the agent instead of the raw sources+notes to save tokens.
12
+ *
13
+ * Styles are saved app-wide (book app settings) and reused across book creation
14
+ * and the editor drafter. Which styles apply is chosen per book / per draft.
15
+ */
16
+
17
+ export interface StyleSource {
18
+ id: string
19
+ label: string
20
+ text: string
21
+ }
22
+
23
+ export interface WritingStyle {
24
+ id: string
25
+ name: string
26
+ sources: StyleSource[]
27
+ notes: string
28
+ /** Words/phrases to prefer (whitelist). Free text — comma or newline separated. */
29
+ include: string
30
+ /** Words/phrases to avoid (blacklist). Free text — comma or newline separated. */
31
+ exclude: string
32
+ /** Distilled compact style guide; editable. Empty until distilled. */
33
+ skill: string
34
+ /** When true and a skill exists, the skill is used instead of raw sources. */
35
+ skillEnabled: boolean
36
+ }
37
+
38
+ export interface BookWritingStyle {
39
+ styles: WritingStyle[]
40
+ }
41
+
42
+ export const EMPTY_WRITING_STYLE: BookWritingStyle = { styles: [] }
43
+
44
+ function randomId(prefix: string): string {
45
+ return `${prefix}-${Math.random().toString(36).slice(2)}-${Date.now()}`
46
+ }
47
+
48
+ export function createWritingStyle(name = 'New style'): WritingStyle {
49
+ return {
50
+ id: randomId('style'),
51
+ name,
52
+ sources: [],
53
+ notes: '',
54
+ include: '',
55
+ exclude: '',
56
+ skill: '',
57
+ skillEnabled: false,
58
+ }
59
+ }
60
+
61
+ export function createStyleSource(label = 'Sample'): StyleSource {
62
+ return { id: randomId('src'), label, text: '' }
63
+ }
64
+
65
+ function coerceSource(item: unknown): StyleSource | null {
66
+ if (!item || typeof item !== 'object') return null
67
+ const entry = item as Record<string, unknown>
68
+ const text = typeof entry.text === 'string' ? entry.text : ''
69
+ return {
70
+ id: typeof entry.id === 'string' && entry.id ? entry.id : randomId('src'),
71
+ label:
72
+ typeof entry.label === 'string' && entry.label.trim()
73
+ ? entry.label.trim()
74
+ : 'Sample',
75
+ text,
76
+ }
77
+ }
78
+
79
+ function coerceStyle(item: unknown): WritingStyle | null {
80
+ if (!item || typeof item !== 'object') return null
81
+ const entry = item as Record<string, unknown>
82
+
83
+ // Current shape: a style with its own sources.
84
+ let sources = Array.isArray(entry.sources)
85
+ ? entry.sources
86
+ .map(coerceSource)
87
+ .filter((source): source is StyleSource => source !== null)
88
+ : []
89
+
90
+ // Legacy migration: a flat StyleExample { label, text } becomes one source.
91
+ if (sources.length === 0 && typeof entry.text === 'string' && entry.text.trim()) {
92
+ sources = [
93
+ {
94
+ id: typeof entry.id === 'string' && entry.id ? entry.id : randomId('src'),
95
+ label:
96
+ typeof entry.label === 'string' && entry.label.trim()
97
+ ? entry.label.trim()
98
+ : 'Sample',
99
+ text: entry.text,
100
+ },
101
+ ]
102
+ }
103
+
104
+ const name =
105
+ typeof entry.name === 'string' && entry.name.trim()
106
+ ? entry.name.trim()
107
+ : typeof entry.label === 'string' && entry.label.trim()
108
+ ? entry.label.trim()
109
+ : 'Untitled style'
110
+ const notes = typeof entry.notes === 'string' ? entry.notes : ''
111
+ const include = typeof entry.include === 'string' ? entry.include : ''
112
+ const exclude = typeof entry.exclude === 'string' ? entry.exclude : ''
113
+ const skill = typeof entry.skill === 'string' ? entry.skill : ''
114
+
115
+ // Keep a style only if it has something to say.
116
+ if (
117
+ sources.length === 0 &&
118
+ !notes.trim() &&
119
+ !include.trim() &&
120
+ !exclude.trim() &&
121
+ !skill.trim()
122
+ ) {
123
+ return null
124
+ }
125
+
126
+ return {
127
+ id: typeof entry.id === 'string' && entry.id ? entry.id : randomId('style'),
128
+ name,
129
+ sources,
130
+ notes,
131
+ include,
132
+ exclude,
133
+ skill,
134
+ skillEnabled: entry.skillEnabled === true && skill.trim().length > 0,
135
+ }
136
+ }
137
+
138
+ export function normalizeWritingStyle(value: unknown): BookWritingStyle {
139
+ const record =
140
+ value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
141
+ // Current shape: { styles }. Legacy shape: { presets, examples }.
142
+ const source = Array.isArray(record.styles)
143
+ ? record.styles
144
+ : Array.isArray(record.examples)
145
+ ? record.examples
146
+ : []
147
+ const styles = source
148
+ .map(coerceStyle)
149
+ .filter((style): style is WritingStyle => style !== null)
150
+ return { styles }
151
+ }
152
+
153
+ /**
154
+ * Raw guidance for ONE style: the author's notes plus their voice samples,
155
+ * explicitly marked voice-only. Used both as the distillation input and as the
156
+ * applied guidance when no skill is enabled.
157
+ */
158
+ export function styleRawGuidance(style: WritingStyle): string {
159
+ const lines: string[] = []
160
+ if (style.notes.trim()) lines.push(`Guidance: ${style.notes.trim()}`)
161
+ if (style.include.trim()) {
162
+ lines.push(
163
+ `Prefer these words and phrases where they fit naturally: ${normalizeWordList(style.include)}.`,
164
+ )
165
+ }
166
+ if (style.exclude.trim()) {
167
+ lines.push(`Avoid these words and phrases entirely: ${normalizeWordList(style.exclude)}.`)
168
+ }
169
+ const samples = style.sources
170
+ .map(source => source.text.trim())
171
+ .filter(Boolean)
172
+ if (samples.length > 0) {
173
+ lines.push(
174
+ 'Match the VOICE of these writing samples — sentence rhythm, tone, and word choice only. Do NOT reuse their wording, facts, or subject matter as content:',
175
+ )
176
+ samples.forEach((text, index) => {
177
+ lines.push(`Sample ${index + 1}: """${text.slice(0, 1200)}"""`)
178
+ })
179
+ }
180
+ return lines.join('\n')
181
+ }
182
+
183
+ /** Flatten a comma/newline-separated word list into a single comma-separated line. */
184
+ function normalizeWordList(value: string): string {
185
+ return value
186
+ .split(/[\n,]/)
187
+ .map(word => word.trim())
188
+ .filter(Boolean)
189
+ .join(', ')
190
+ }
191
+
192
+ /** True when a style contributes anything to the prompt. */
193
+ export function styleHasContent(style: WritingStyle): boolean {
194
+ return (
195
+ style.skill.trim().length > 0 ||
196
+ style.notes.trim().length > 0 ||
197
+ style.include.trim().length > 0 ||
198
+ style.exclude.trim().length > 0 ||
199
+ style.sources.some(source => source.text.trim().length > 0)
200
+ )
201
+ }
202
+
203
+ /**
204
+ * Build the style-guidance prompt block from the SELECTED styles. For each
205
+ * style, prefer its distilled skill when enabled; otherwise fall back to the
206
+ * raw notes + voice samples.
207
+ */
208
+ export function buildStyleGuidance(styles: WritingStyle[]): string | undefined {
209
+ const blocks: string[] = []
210
+ styles.forEach(style => {
211
+ const useSkill = style.skillEnabled && style.skill.trim().length > 0
212
+ const body = useSkill ? style.skill.trim() : styleRawGuidance(style)
213
+ if (body.trim()) blocks.push(`Style — ${style.name}:\n${body.trim()}`)
214
+ })
215
+ if (blocks.length === 0) return undefined
216
+ return ['Writing style guidance:', ...blocks].join('\n\n')
217
+ }