gloomberb 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (489) hide show
  1. package/README.md +6 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +103 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +129 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +196 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/cli/commands/plugins.ts +39 -11
  20. package/src/cli/desktop-pane-shot.ts +285 -16
  21. package/src/cli/index.ts +1 -1
  22. package/src/cli/pane-functions/capabilities.ts +58 -28
  23. package/src/cli/pane-functions/catalog.ts +16 -4
  24. package/src/cli/pane-functions/discovery.ts +1 -0
  25. package/src/cli/pane-functions/headless.test.ts +247 -0
  26. package/src/cli/pane-functions/headless.ts +423 -0
  27. package/src/cli/pane-functions/index.test.ts +9 -0
  28. package/src/cli/pane-functions/index.ts +6 -2
  29. package/src/cli/pane-functions/options.ts +27 -2
  30. package/src/cli/pane-functions/report.ts +4 -0
  31. package/src/cli/pane-functions/resolver.ts +26 -8
  32. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  33. package/src/cli/pane-functions/screenshot.ts +236 -20
  34. package/src/cli/restore-plugins.ts +34 -0
  35. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  36. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  37. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  38. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  39. package/src/components/chart/composite/composite-chart.tsx +301 -204
  40. package/src/components/chart/composite/format.ts +8 -2
  41. package/src/components/chart/composite/interactions.test.ts +68 -102
  42. package/src/components/chart/composite/interactions.ts +214 -264
  43. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  44. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  45. package/src/components/chart/composite/scene.ts +57 -3
  46. package/src/components/chart/composite/text-renderer.ts +3 -2
  47. package/src/components/chart/composite/time-scale.ts +138 -57
  48. package/src/components/chart/composite/types.ts +25 -1
  49. package/src/components/chart/core/data.ts +9 -13
  50. package/src/components/chart/core/date-window.ts +7 -1
  51. package/src/components/chart/core/pointer.ts +3 -0
  52. package/src/components/chart/core/types.ts +0 -45
  53. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  54. package/src/components/chart/native/kitty/index.test.ts +0 -188
  55. package/src/components/chart/native/raster/crosshair.ts +6 -5
  56. package/src/components/chart/native/raster/primitives.ts +0 -23
  57. package/src/components/chart/native/renderer-selection.ts +81 -31
  58. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  59. package/src/components/chart/static/chart-surface.tsx +150 -0
  60. package/src/components/chart/static/index.ts +8 -0
  61. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  62. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  63. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  64. package/src/components/chart/static/series.ts +49 -0
  65. package/src/components/command-bar/action-runtime.ts +1 -3
  66. package/src/components/command-bar/assist/model.test.ts +17 -2
  67. package/src/components/command-bar/assist/model.ts +40 -9
  68. package/src/components/command-bar/commands/direct/index.ts +1 -1
  69. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  70. package/src/components/command-bar/commands/registry.ts +0 -11
  71. package/src/components/command-bar/helpers.test.ts +2 -2
  72. package/src/components/command-bar/helpers.ts +2 -6
  73. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  74. package/src/components/command-bar/list/badge.test.ts +54 -0
  75. package/src/components/command-bar/list/badge.ts +62 -0
  76. package/src/components/command-bar/list/model.test.ts +103 -0
  77. package/src/components/command-bar/list/model.ts +61 -4
  78. package/src/components/command-bar/list/view.test.tsx +113 -0
  79. package/src/components/command-bar/list/view.tsx +147 -131
  80. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  81. package/src/components/command-bar/panel/body.tsx +51 -65
  82. package/src/components/command-bar/panel/index.tsx +48 -13
  83. package/src/components/command-bar/panel/layout.test.ts +139 -0
  84. package/src/components/command-bar/panel/layout.ts +65 -28
  85. package/src/components/command-bar/panel/native-surface.ts +24 -0
  86. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  87. package/src/components/command-bar/panel/runtime.ts +6 -3
  88. package/src/components/command-bar/panel/state.ts +41 -7
  89. package/src/components/command-bar/panel/types.ts +7 -1
  90. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  91. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  92. package/src/components/command-bar/routes/root/results.ts +30 -28
  93. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  94. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  95. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  96. package/src/components/command-bar/routes/root/selection.ts +1 -12
  97. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  98. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  99. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  100. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  101. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  102. package/src/components/command-bar/routing/actions.ts +1 -81
  103. package/src/components/command-bar/routing/launch-request.ts +1 -1
  104. package/src/components/command-bar/routing/list-state.ts +6 -21
  105. package/src/components/command-bar/selection-runtime.ts +1 -1
  106. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  107. package/src/components/command-bar/surface/environment.ts +8 -1
  108. package/src/components/command-bar/surface/index.test.tsx +84 -68
  109. package/src/components/command-bar/surface/index.tsx +33 -36
  110. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  111. package/src/components/command-bar/theme-picker.tsx +43 -15
  112. package/src/components/command-bar/view-model.test.ts +53 -8
  113. package/src/components/command-bar/view-model.ts +43 -14
  114. package/src/components/command-bar/workflow/tickers.ts +1 -0
  115. package/src/components/command-bar/workflow/types.ts +1 -1
  116. package/src/components/data-table/view.test.tsx +45 -6
  117. package/src/components/data-table/view.tsx +55 -9
  118. package/src/components/index.ts +10 -2
  119. package/src/components/input-search-bar.test.tsx +36 -2
  120. package/src/components/input-search-bar.tsx +18 -1
  121. package/src/components/layout/header.test.tsx +57 -10
  122. package/src/components/layout/header.tsx +292 -149
  123. package/src/components/layout/market-summary.test.ts +81 -0
  124. package/src/components/layout/market-summary.ts +109 -0
  125. package/src/components/layout/shell/chrome.ts +96 -1
  126. package/src/components/layout/shell/index.tsx +11 -3
  127. package/src/components/layout/shell/menu.ts +9 -0
  128. package/src/components/layout/shell/pane/actions.ts +29 -0
  129. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  130. package/src/components/layout/shell/shortcuts.ts +3 -0
  131. package/src/components/layout/status-bar.test.tsx +9 -8
  132. package/src/components/layout/status-bar.tsx +85 -54
  133. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  134. package/src/components/price-sparkline/index.ts +61 -34
  135. package/src/components/price-sparkline/view.tsx +1 -1
  136. package/src/components/sign-in-gate.test.tsx +128 -0
  137. package/src/components/sign-in-gate.tsx +149 -0
  138. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  139. package/src/components/ticker/badge/list.tsx +17 -3
  140. package/src/components/ticker/list-table-view.tsx +6 -3
  141. package/src/components/ui/data-table/index.tsx +3 -2
  142. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  143. package/src/components/ui/frame.tsx +28 -0
  144. package/src/components/ui/index.ts +3 -0
  145. package/src/components/ui/native-select.tsx +20 -10
  146. package/src/components/ui/select-button.tsx +138 -0
  147. package/src/components/ui/status.tsx +2 -2
  148. package/src/components/ui/table-layout.ts +1 -1
  149. package/src/components/use-double-click-activation.ts +7 -3
  150. package/src/core/app-services.ts +3 -3
  151. package/src/data/config/layout.ts +12 -6
  152. package/src/data/config/store/index.test.ts +97 -0
  153. package/src/data/config/store/migrations.ts +41 -0
  154. package/src/data/config/store/normalize.ts +5 -3
  155. package/src/i18n/es.ts +6 -2
  156. package/src/i18n/ja.ts +6 -2
  157. package/src/i18n/ko.ts +6 -2
  158. package/src/i18n/zh-cn.ts +6 -2
  159. package/src/i18n/zh-tw.ts +6 -2
  160. package/src/layout-marketplace/pane-imagery.ts +1 -0
  161. package/src/layout-marketplace/payload.test.ts +17 -0
  162. package/src/layout-marketplace/payload.ts +40 -5
  163. package/src/market-data/coordinator/index.ts +3 -2
  164. package/src/market-data/coordinator/quotes.ts +173 -66
  165. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  166. package/src/pane-settings.ts +1 -1
  167. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  168. package/src/plugins/builtin/analytics/view.tsx +1 -1
  169. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  170. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  171. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  172. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  173. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  174. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  175. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  176. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  177. package/src/plugins/builtin/composite-plugins.ts +14 -1
  178. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  179. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  180. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  181. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  182. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  183. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  184. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  185. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  186. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  187. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  188. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  189. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  190. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  191. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  192. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  193. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  194. package/src/plugins/builtin/econ/index.tsx +2 -1
  195. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  196. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  197. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  198. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  199. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  200. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  201. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  202. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  203. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  204. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  205. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  206. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  207. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  208. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  209. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  210. package/src/plugins/builtin/help/index.tsx +4 -0
  211. package/src/plugins/builtin/holders/index.tsx +1 -0
  212. package/src/plugins/builtin/insider/index.tsx +1 -0
  213. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  214. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  215. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  216. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  217. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  218. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  219. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  220. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  221. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  222. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  223. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  224. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  225. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  226. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  227. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  228. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  229. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  230. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  231. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  232. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  233. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  234. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  235. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  236. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  237. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  238. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  239. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  240. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  241. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  242. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  243. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  244. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  245. package/src/plugins/builtin/options/index.tsx +1 -0
  246. package/src/plugins/builtin/options/view.test.tsx +29 -1
  247. package/src/plugins/builtin/options/view.tsx +1 -22
  248. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  249. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  250. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  251. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  252. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  253. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  254. package/src/plugins/builtin/plugin-module.ts +9 -1
  255. package/src/plugins/builtin/research/index.tsx +4 -0
  256. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  257. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  258. package/src/plugins/builtin/research-search/data.ts +109 -0
  259. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  260. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  261. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  262. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  263. package/src/plugins/builtin/research-search/index.tsx +85 -0
  264. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  265. package/src/plugins/builtin/research-search/model.ts +291 -0
  266. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  267. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  268. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  269. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  270. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  271. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  272. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  273. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  274. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  275. package/src/plugins/builtin/sec/index.tsx +1 -0
  276. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  277. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  278. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  279. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  280. package/src/plugins/builtin/ticker-detail/index.tsx +0 -14
  281. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  282. package/src/plugins/builtin/volatility/index.tsx +1 -1
  283. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  284. package/src/plugins/builtin/world-venue-map/map.tsx +213 -69
  285. package/src/plugins/builtin/world-venue-map/model.test.ts +32 -0
  286. package/src/plugins/builtin/world-venue-map/model.ts +134 -13
  287. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  288. package/src/plugins/bundle.test.ts +109 -0
  289. package/src/plugins/bundle.ts +139 -0
  290. package/src/plugins/catalog-browser.test.ts +1 -0
  291. package/src/plugins/catalog-browser.ts +10 -0
  292. package/src/plugins/catalog-manifest.test.ts +43 -0
  293. package/src/plugins/catalog-manifest.ts +92 -0
  294. package/src/plugins/catalog-ui.ts +20 -4
  295. package/src/plugins/catalog.ts +9 -2
  296. package/src/plugins/current-target.ts +28 -0
  297. package/src/plugins/host-contract.ts +37 -0
  298. package/src/plugins/host-link.ts +158 -0
  299. package/src/plugins/host-modules.ts +61 -0
  300. package/src/plugins/loader.ts +41 -22
  301. package/src/plugins/ownership.ts +1 -0
  302. package/src/plugins/prediction-markets/index.tsx +1 -0
  303. package/src/plugins/registry/context.ts +3 -0
  304. package/src/plugins/registry/contributions.ts +27 -0
  305. package/src/plugins/registry/index.test.ts +25 -0
  306. package/src/plugins/registry/index.ts +8 -0
  307. package/src/plugins/registry/pane-settings.ts +6 -13
  308. package/src/plugins/seed.test.ts +83 -0
  309. package/src/plugins/seed.ts +80 -0
  310. package/src/public/broker.ts +31 -0
  311. package/src/public/market-data.ts +9 -0
  312. package/src/public/pane-hooks.ts +57 -0
  313. package/src/public/public-api.test.ts +285 -0
  314. package/src/public/react.ts +62 -0
  315. package/src/public/test-support.ts +23 -0
  316. package/src/public/time-series.ts +8 -0
  317. package/src/public/utils.ts +77 -0
  318. package/src/remote/layout-helpers.ts +6 -8
  319. package/src/remote/resources.ts +2 -0
  320. package/src/remote/schema.ts +1 -0
  321. package/src/remote/semantic-tree.test.tsx +105 -0
  322. package/src/remote/semantic-tree.tsx +53 -35
  323. package/src/renderers/browser/main.tsx +6 -0
  324. package/src/renderers/cloudflare/worker.test.ts +19 -0
  325. package/src/renderers/cloudflare/worker.ts +1 -1
  326. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  327. package/src/renderers/electrobun/bun/index.ts +5 -2
  328. package/src/renderers/electrobun/shared/protocol.ts +21 -0
  329. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  330. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  331. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  332. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  333. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  334. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  335. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  336. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  337. package/src/renderers/electrobun/view/main.tsx +27 -2
  338. package/src/renderers/electrobun/view/styles.css +5 -0
  339. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  340. package/src/renderers/opentui/chart-surface.tsx +5 -1
  341. package/src/renderers/opentui/host.tsx +16 -2
  342. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  343. package/src/renderers/opentui/interaction-performance.ts +188 -0
  344. package/src/renderers/opentui/start.tsx +26 -1
  345. package/src/renderers/opentui/test-utils.tsx +13 -0
  346. package/src/renderers/opentui/toast-host.tsx +26 -2
  347. package/src/renderers/opentui/ui-host.tsx +1 -0
  348. package/src/sources/provider-router/search.test.ts +191 -0
  349. package/src/sources/provider-router/search.ts +92 -39
  350. package/src/state/app/context/index.tsx +2 -0
  351. package/src/state/app/context/selector.test.tsx +33 -1
  352. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  353. package/src/state/hooks/quote-streaming.ts +106 -30
  354. package/src/state/pane-table-export-registry.ts +19 -0
  355. package/src/theme/color-utils.ts +1 -1
  356. package/src/theme/colors.ts +50 -0
  357. package/src/theme/themes.test.ts +15 -2
  358. package/src/theme/themes.ts +19 -1
  359. package/src/tickers/search/index.test.ts +26 -0
  360. package/src/tickers/search/index.ts +42 -17
  361. package/src/tickers/search/ranking.ts +14 -0
  362. package/src/time-series/chart-data.test.ts +216 -0
  363. package/src/time-series/resolution.test.ts +25 -26
  364. package/src/time-series/resolution.ts +41 -10
  365. package/src/time-series/resolve.ts +7 -0
  366. package/src/time-series/types.ts +3 -1
  367. package/src/time-series/use-chart-resolution.ts +5 -0
  368. package/src/types/config.ts +123 -3
  369. package/src/types/data-provider.ts +11 -0
  370. package/src/types/headless.ts +161 -0
  371. package/src/types/plugin.ts +102 -0
  372. package/src/ui/host.tsx +12 -1
  373. package/src/ui/index.tsx +1 -0
  374. package/src/ui/toast.tsx +8 -4
  375. package/src/utils/format.ts +35 -0
  376. package/src/utils/fuzzy-search.test.ts +94 -0
  377. package/src/utils/fuzzy-search.ts +81 -22
  378. package/src/utils/shortcut-labels.ts +14 -0
  379. package/src/components/chart/core/axis-format.ts +0 -157
  380. package/src/components/chart/core/index.test.ts +0 -1075
  381. package/src/components/chart/core/projection.ts +0 -357
  382. package/src/components/chart/core/render-utils.ts +0 -4
  383. package/src/components/chart/core/renderer.ts +0 -244
  384. package/src/components/chart/core/scene.ts +0 -180
  385. package/src/components/chart/core/time-axis.ts +0 -330
  386. package/src/components/chart/cursor-motion.ts +0 -47
  387. package/src/components/chart/indicators/types.ts +0 -26
  388. package/src/components/chart/market-session.ts +0 -176
  389. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  390. package/src/components/chart/multi-line/renderer.ts +0 -161
  391. package/src/components/chart/multi-line/scene.ts +0 -188
  392. package/src/components/chart/native/raster/layout.ts +0 -129
  393. package/src/components/chart/native/raster/price-chart.ts +0 -244
  394. package/src/components/chart/rendering/chart-draw.ts +0 -270
  395. package/src/components/chart/rendering/geometry.ts +0 -95
  396. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  397. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  398. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  399. package/src/components/chart/static/chart/surface.tsx +0 -358
  400. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  401. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  402. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  403. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  404. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  405. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  406. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  407. package/src/plugins/builtin/substack/api/store.ts +0 -186
  408. package/src/plugins/builtin/substack/api/types.ts +0 -42
  409. package/src/plugins/builtin/substack/api.test.ts +0 -298
  410. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  411. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  412. package/src/plugins/builtin/substack/content.ts +0 -478
  413. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  414. package/src/plugins/builtin/substack/index.tsx +0 -49
  415. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  416. package/src/plugins/builtin/substack/model.test.ts +0 -155
  417. package/src/plugins/builtin/substack/normalize.ts +0 -343
  418. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  419. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  420. package/src/plugins/builtin/substack/pane.tsx +0 -547
  421. package/src/plugins/builtin/substack/read-state.ts +0 -30
  422. package/src/plugins/builtin/substack/table.ts +0 -111
  423. package/src/plugins/builtin/substack/types.ts +0 -71
  424. package/src/plugins/builtin/substack/utils.ts +0 -131
  425. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  426. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  427. package/src/plugins/ibkr/account-cache.ts +0 -24
  428. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  429. package/src/plugins/ibkr/config.test.ts +0 -79
  430. package/src/plugins/ibkr/config.ts +0 -250
  431. package/src/plugins/ibkr/dialogs.tsx +0 -40
  432. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  433. package/src/plugins/ibkr/flex/index.ts +0 -367
  434. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  435. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  436. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  437. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  438. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  439. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  440. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  441. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  442. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  443. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  444. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  445. package/src/plugins/ibkr/gateway/history.ts +0 -120
  446. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  447. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  448. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  449. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  450. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  451. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  452. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  453. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  454. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  455. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  456. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  457. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  458. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  459. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  460. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  461. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  462. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  463. package/src/plugins/ibkr/gateway/types.ts +0 -27
  464. package/src/plugins/ibkr/index.test.tsx +0 -162
  465. package/src/plugins/ibkr/index.tsx +0 -159
  466. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  467. package/src/plugins/ibkr/instance-selection.ts +0 -36
  468. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  469. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  470. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  471. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  472. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  473. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  474. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  475. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  476. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  477. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  478. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  479. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  480. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  481. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  482. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  483. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  484. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  485. package/src/plugins/ibkr/trade/utils.ts +0 -144
  486. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  487. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  488. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  489. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -11,8 +11,49 @@ export interface WorldVenueCluster extends WorldMapPoint {
11
11
  isOpen: boolean;
12
12
  }
13
13
 
14
+ export interface WorldMapViewport {
15
+ zoom: number;
16
+ centerLongitude: number;
17
+ centerLatitude: number;
18
+ }
19
+
14
20
  const MAX_LATITUDE = 85;
15
21
  const MIN_LATITUDE = -60;
22
+ const LONGITUDE_SPAN = 360;
23
+ export const MAX_WORLD_MAP_ZOOM = 18;
24
+
25
+ export const DEFAULT_WORLD_MAP_VIEWPORT: WorldMapViewport = {
26
+ zoom: 1,
27
+ centerLongitude: 0,
28
+ centerLatitude: (MAX_LATITUDE + MIN_LATITUDE) / 2,
29
+ };
30
+
31
+ function clamp(value: number, min: number, max: number): number {
32
+ return Math.max(min, Math.min(max, value));
33
+ }
34
+
35
+ function mapExtent(width: number, height: number, yUnitAspect = 1) {
36
+ const availableWidth = Math.max(width - 1, 0);
37
+ const availableHeight = Math.max(height - 1, 0);
38
+ const unitAspect = Math.max(yUnitAspect, Number.EPSILON);
39
+ const effectiveHeight = availableHeight * unitAspect;
40
+ const latitudeSpan = MAX_LATITUDE - MIN_LATITUDE;
41
+ return { availableWidth, availableHeight, unitAspect, effectiveHeight, latitudeSpan };
42
+ }
43
+
44
+ function fittedWorldMapScale(width: number, height: number, yUnitAspect = 1): number {
45
+ const { availableWidth, effectiveHeight, latitudeSpan } = mapExtent(width, height, yUnitAspect);
46
+ return Math.min(availableWidth / LONGITUDE_SPAN, effectiveHeight / latitudeSpan);
47
+ }
48
+
49
+ function worldMapScale(
50
+ width: number,
51
+ height: number,
52
+ yUnitAspect: number,
53
+ viewport: WorldMapViewport,
54
+ ): number {
55
+ return fittedWorldMapScale(width, height, yUnitAspect) * Math.max(viewport.zoom, 1);
56
+ }
16
57
 
17
58
  export function projectWorldPoint(
18
59
  longitude: number,
@@ -20,35 +61,115 @@ export function projectWorldPoint(
20
61
  width: number,
21
62
  height: number,
22
63
  yUnitAspect = 1,
64
+ viewport: WorldMapViewport = DEFAULT_WORLD_MAP_VIEWPORT,
23
65
  ): WorldMapPoint {
24
- const availableWidth = Math.max(width - 1, 0);
25
- const availableHeight = Math.max(height - 1, 0);
26
- const unitAspect = Math.max(yUnitAspect, Number.EPSILON);
27
- const effectiveHeight = availableHeight * unitAspect;
28
- const latitudeSpan = MAX_LATITUDE - MIN_LATITUDE;
29
- const scale = Math.min(availableWidth / 360, effectiveHeight / latitudeSpan);
30
- const mapWidth = 360 * scale;
31
- const mapHeight = latitudeSpan * scale;
32
- const clampedLatitude = Math.max(MIN_LATITUDE, Math.min(MAX_LATITUDE, latitude));
66
+ const { availableWidth, unitAspect, effectiveHeight } = mapExtent(width, height, yUnitAspect);
67
+ const scale = worldMapScale(width, height, yUnitAspect, viewport);
68
+ const clampedLatitude = clamp(latitude, MIN_LATITUDE, MAX_LATITUDE);
33
69
 
34
70
  return {
35
- x: (availableWidth - mapWidth) / 2 + (longitude + 180) * scale,
36
- y: ((effectiveHeight - mapHeight) / 2 + (MAX_LATITUDE - clampedLatitude) * scale) / unitAspect,
71
+ x: availableWidth / 2 + (longitude - viewport.centerLongitude) * scale,
72
+ y: (effectiveHeight / 2 - (clampedLatitude - viewport.centerLatitude) * scale) / unitAspect,
37
73
  };
38
74
  }
39
75
 
76
+ export function unprojectWorldPoint(
77
+ x: number,
78
+ y: number,
79
+ width: number,
80
+ height: number,
81
+ yUnitAspect = 1,
82
+ viewport: WorldMapViewport = DEFAULT_WORLD_MAP_VIEWPORT,
83
+ ): { longitude: number; latitude: number } {
84
+ const { availableWidth, unitAspect, effectiveHeight } = mapExtent(width, height, yUnitAspect);
85
+ const scale = worldMapScale(width, height, yUnitAspect, viewport);
86
+ return {
87
+ longitude: viewport.centerLongitude + (x - availableWidth / 2) / scale,
88
+ latitude: viewport.centerLatitude - (y * unitAspect - effectiveHeight / 2) / scale,
89
+ };
90
+ }
91
+
92
+ export function clampWorldMapViewport(
93
+ viewport: WorldMapViewport,
94
+ width: number,
95
+ height: number,
96
+ yUnitAspect = 1,
97
+ ): WorldMapViewport {
98
+ const zoom = clamp(viewport.zoom, 1, MAX_WORLD_MAP_ZOOM);
99
+ if (zoom <= 1) return DEFAULT_WORLD_MAP_VIEWPORT;
100
+
101
+ const { availableWidth, effectiveHeight, latitudeSpan } = mapExtent(width, height, yUnitAspect);
102
+ const scale = fittedWorldMapScale(width, height, yUnitAspect) * zoom;
103
+ const halfLongitude = availableWidth / (2 * scale);
104
+ const halfLatitude = effectiveHeight / (2 * scale);
105
+ const minLongitude = -180 + halfLongitude;
106
+ const maxLongitude = 180 - halfLongitude;
107
+ const minLatitude = MIN_LATITUDE + halfLatitude;
108
+ const maxLatitude = MAX_LATITUDE - halfLatitude;
109
+
110
+ return {
111
+ zoom,
112
+ centerLongitude: minLongitude < maxLongitude
113
+ ? clamp(viewport.centerLongitude, minLongitude, maxLongitude)
114
+ : DEFAULT_WORLD_MAP_VIEWPORT.centerLongitude,
115
+ centerLatitude: halfLatitude < latitudeSpan / 2
116
+ ? clamp(viewport.centerLatitude, minLatitude, maxLatitude)
117
+ : DEFAULT_WORLD_MAP_VIEWPORT.centerLatitude,
118
+ };
119
+ }
120
+
121
+ export function zoomWorldMapViewport(
122
+ viewport: WorldMapViewport,
123
+ width: number,
124
+ height: number,
125
+ point: WorldMapPoint,
126
+ zoomFactor: number,
127
+ yUnitAspect = 1,
128
+ ): WorldMapViewport {
129
+ const nextZoom = clamp(viewport.zoom * zoomFactor, 1, MAX_WORLD_MAP_ZOOM);
130
+ if (nextZoom <= 1) return DEFAULT_WORLD_MAP_VIEWPORT;
131
+
132
+ const geographic = unprojectWorldPoint(point.x, point.y, width, height, yUnitAspect, viewport);
133
+ const { availableWidth, unitAspect, effectiveHeight } = mapExtent(width, height, yUnitAspect);
134
+ const scale = fittedWorldMapScale(width, height, yUnitAspect) * nextZoom;
135
+ return clampWorldMapViewport({
136
+ zoom: nextZoom,
137
+ centerLongitude: geographic.longitude - (point.x - availableWidth / 2) / scale,
138
+ centerLatitude: geographic.latitude + (point.y * unitAspect - effectiveHeight / 2) / scale,
139
+ }, width, height, yUnitAspect);
140
+ }
141
+
142
+ export function panWorldMapViewport(
143
+ viewport: WorldMapViewport,
144
+ width: number,
145
+ height: number,
146
+ deltaX: number,
147
+ deltaY: number,
148
+ yUnitAspect = 1,
149
+ ): WorldMapViewport {
150
+ if (viewport.zoom <= 1) return DEFAULT_WORLD_MAP_VIEWPORT;
151
+ const scale = worldMapScale(width, height, yUnitAspect, viewport);
152
+ const unitAspect = Math.max(yUnitAspect, Number.EPSILON);
153
+ return clampWorldMapViewport({
154
+ zoom: viewport.zoom,
155
+ centerLongitude: viewport.centerLongitude - deltaX / scale,
156
+ centerLatitude: viewport.centerLatitude + (deltaY * unitAspect) / scale,
157
+ }, width, height, yUnitAspect);
158
+ }
159
+
40
160
  export function clusterWorldVenues(
41
161
  venues: readonly CloudWorldVenuePayload[],
42
162
  width: number,
43
163
  height: number,
44
164
  yUnitAspect = 1,
165
+ viewport: WorldMapViewport = DEFAULT_WORLD_MAP_VIEWPORT,
45
166
  ): WorldVenueCluster[] {
46
167
  const buckets = new Map<string, CloudWorldVenuePayload[]>();
47
168
  const cellWidth = Math.max(4, Math.round(width / 18));
48
169
  const cellHeight = Math.max(2, Math.round(height / 12));
49
170
 
50
171
  for (const venue of venues) {
51
- const point = projectWorldPoint(venue.longitude, venue.latitude, width, height, yUnitAspect);
172
+ const point = projectWorldPoint(venue.longitude, venue.latitude, width, height, yUnitAspect, viewport);
52
173
  const key = `${Math.floor(point.x / cellWidth)}:${Math.floor(point.y / cellHeight)}`;
53
174
  const bucket = buckets.get(key) ?? [];
54
175
  bucket.push(venue);
@@ -58,7 +179,7 @@ export function clusterWorldVenues(
58
179
  return [...buckets.entries()].map(([id, grouped]) => {
59
180
  const point = grouped.reduce(
60
181
  (sum, venue) => {
61
- const projected = projectWorldPoint(venue.longitude, venue.latitude, width, height, yUnitAspect);
182
+ const projected = projectWorldPoint(venue.longitude, venue.latitude, width, height, yUnitAspect, viewport);
62
183
  return { x: sum.x + projected.x, y: sum.y + projected.y };
63
184
  },
64
185
  { x: 0, y: 0 },
@@ -5,7 +5,7 @@ import { Button, EmptyState, Spinner, StaticChartSurface, type PaneFooterSegment
5
5
  import type { PaneProps } from "../../../types/plugin";
6
6
  import type { PluginModule } from "../plugin-module";
7
7
  import { colors } from "../../../theme/colors";
8
- import { resolveChartPalette } from "../../../components/chart/core/renderer";
8
+ import { resolveChartPalette } from "../../../components/chart/core/palette";
9
9
  import type { ProjectedChartPoint } from "../../../components/chart/core/data";
10
10
  import {
11
11
  curveAsOf,
@@ -0,0 +1,109 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+
6
+ import { bundleExternalPlugin, buildSharedModuleSource } from "./bundle";
7
+ import { PLUGIN_HOST_GLOBAL } from "./host-contract";
8
+
9
+ /**
10
+ * This is the seam that lets a plugin on disk run inside the desktop and
11
+ * browser renderers, and the failure it prevents is nasty: if `react` or a
12
+ * `gloomberb/*` module gets bundled into the plugin instead of shared with the
13
+ * host, the plugin loads fine and then throws on its first hook, or silently
14
+ * renders against a second copy of the theme. That is worth pinning down.
15
+ */
16
+
17
+ function scratchPlugin(source: string): string {
18
+ const dir = mkdtempSync(join(tmpdir(), "gloom-bundle-"));
19
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "scratch", main: "index.tsx" }));
20
+ writeFileSync(join(dir, "index.tsx"), source);
21
+ return dir;
22
+ }
23
+
24
+ const fakeExports = async (specifier: string) => (
25
+ specifier === "gloomberb/ui" ? ["Box", "Text"] : specifier === "react" ? ["useState"] : []
26
+ );
27
+
28
+ describe("buildSharedModuleSource", () => {
29
+ test("re-exports each name from the host registry", () => {
30
+ const source = buildSharedModuleSource("gloomberb/ui", ["Box", "Text"]);
31
+
32
+ expect(source).toContain(PLUGIN_HOST_GLOBAL);
33
+ expect(source).toContain('export const Box = mod["Box"];');
34
+ expect(source).toContain('export const Text = mod["Text"];');
35
+ });
36
+
37
+ test("throws a directed error when the host registry is missing", () => {
38
+ // A plugin bundle loaded before the host installs its modules should say so,
39
+ // not fail later with an undefined property read deep in a render.
40
+ const source = buildSharedModuleSource("gloomberb/ui", []);
41
+ expect(source).toContain("was not installed before this plugin loaded");
42
+ });
43
+
44
+ test("skips a default export, which the module already provides", () => {
45
+ expect(buildSharedModuleSource("react", ["default", "useState"]))
46
+ .not.toContain("export const default");
47
+ });
48
+ });
49
+
50
+ describe("bundleExternalPlugin", () => {
51
+ test("shares react and gloomberb modules instead of bundling them", async () => {
52
+ const dir = scratchPlugin(`
53
+ import { useState } from "react";
54
+ import { Box } from "gloomberb/ui";
55
+ export default { id: "scratch", name: "Scratch", version: "1.0.0", useState, Box };
56
+ `);
57
+ const out = join(dir, "out");
58
+ try {
59
+ const result = await bundleExternalPlugin(dir, out, { exportNamesFor: fakeExports });
60
+ const code = await Bun.file(result.outputPath).text();
61
+
62
+ expect(result.shared).toEqual(["gloomberb/ui", "react"]);
63
+ expect(code).toContain(PLUGIN_HOST_GLOBAL);
64
+ // The giveaway that React got inlined rather than shared.
65
+ expect(code).not.toContain("react-dom/client");
66
+ expect(code).not.toContain("Invalid hook call");
67
+ } finally {
68
+ rmSync(dir, { recursive: true, force: true });
69
+ }
70
+ });
71
+
72
+ test("bundles a plugin's own dependencies rather than sharing them", async () => {
73
+ const dir = scratchPlugin(`
74
+ import { double } from "./helper";
75
+ export default { id: "scratch", name: "Scratch", version: "1.0.0", value: double(21) };
76
+ `);
77
+ writeFileSync(join(dir, "helper.ts"), "export function double(n: number) { return n * 2; }");
78
+ const out = join(dir, "out");
79
+ try {
80
+ const result = await bundleExternalPlugin(dir, out, { exportNamesFor: fakeExports });
81
+ const code = await Bun.file(result.outputPath).text();
82
+
83
+ expect(result.shared).toEqual([]);
84
+ expect(code).toContain("* 2");
85
+ } finally {
86
+ rmSync(dir, { recursive: true, force: true });
87
+ }
88
+ });
89
+
90
+ test("reports a compile error against the plugin instead of throwing something opaque", async () => {
91
+ const dir = scratchPlugin(`import { missing } from "./nope"; export default missing;`);
92
+ try {
93
+ await expect(bundleExternalPlugin(dir, join(dir, "out"), { exportNamesFor: fakeExports }))
94
+ .rejects.toThrow();
95
+ } finally {
96
+ rmSync(dir, { recursive: true, force: true });
97
+ }
98
+ });
99
+
100
+ test("rejects a directory with no entry file", async () => {
101
+ const dir = mkdtempSync(join(tmpdir(), "gloom-bundle-empty-"));
102
+ mkdirSync(join(dir, "src"), { recursive: true });
103
+ try {
104
+ await expect(bundleExternalPlugin(dir, join(dir, "out"))).rejects.toThrow("No plugin entry file");
105
+ } finally {
106
+ rmSync(dir, { recursive: true, force: true });
107
+ }
108
+ });
109
+ });
@@ -0,0 +1,139 @@
1
+ import { mkdir } from "fs/promises";
2
+ import { join } from "path";
3
+
4
+ import { resolvePluginEntry } from "./loader";
5
+ import { PLUGIN_HOST_GLOBAL, SHARED_SPECIFIERS } from "./host-contract";
6
+
7
+ /**
8
+ * Compiles an external plugin for a renderer that cannot read the filesystem.
9
+ *
10
+ * The terminal renderer runs in Bun and can `import()` a plugin directly from
11
+ * `~/.gloomberb/plugins`. The desktop view and the hosted browser app cannot:
12
+ * they are browser contexts running a sealed bundle, so plugin source has to be
13
+ * compiled to an ES module they can fetch.
14
+ *
15
+ * The hard part is not compiling, it is *sharing*. A plugin bundle that carries
16
+ * its own React would give the process a second React instance and throw on the
17
+ * first hook, and it has no way to reach the host's `gloomberb/*` modules at
18
+ * all. So those specifiers are not bundled — they are rewritten to read from a
19
+ * registry the host publishes on `globalThis` before it loads any plugin. One
20
+ * instance of every shared module, exactly as on the terminal side where the
21
+ * host is symlinked in.
22
+ */
23
+
24
+
25
+
26
+ /**
27
+ * Source for a module that re-exports one shared specifier from the host
28
+ * registry. Named exports have to be listed statically — `export *` cannot
29
+ * forward from a runtime object — so the caller passes the names it found by
30
+ * importing the real module.
31
+ */
32
+ export function buildSharedModuleSource(specifier: string, exportNames: readonly string[]): string {
33
+ const lines = [
34
+ `const mod = globalThis[${JSON.stringify(PLUGIN_HOST_GLOBAL)}]?.[${JSON.stringify(specifier)}];`,
35
+ `if (!mod) throw new Error(${JSON.stringify(`Gloomberb host module "${specifier}" is unavailable. The plugin host was not installed before this plugin loaded.`)});`,
36
+ "export default mod;",
37
+ ];
38
+ for (const name of exportNames) {
39
+ if (name === "default") continue;
40
+ // Not `export const`: a getter keeps live bindings working and avoids
41
+ // capturing a value the host may replace on a theme or locale change.
42
+ lines.push(`export const ${name} = mod[${JSON.stringify(name)}];`);
43
+ }
44
+ return lines.join("\n");
45
+ }
46
+
47
+ export interface BundlePluginResult {
48
+ /** Absolute path of the emitted ES module. */
49
+ outputPath: string;
50
+ /** Shared specifiers this plugin actually imported. */
51
+ shared: string[];
52
+ warnings: string[];
53
+ }
54
+
55
+ /**
56
+ * Bun plugin that redirects the shared specifiers to generated modules.
57
+ *
58
+ * `exportNamesFor` is injected so this stays testable and so the caller decides
59
+ * how names are discovered — the bundler runs in Bun and can simply import the
60
+ * real module, but a test should not have to.
61
+ */
62
+ export function createSharedModuleResolver(
63
+ exportNamesFor: (specifier: string) => Promise<readonly string[]>,
64
+ onShared?: (specifier: string) => void,
65
+ ): import("bun").BunPlugin {
66
+ const namespace = "gloom-host";
67
+ const shared = new Set<string>(SHARED_SPECIFIERS);
68
+
69
+ return {
70
+ name: "gloomberb-host-modules",
71
+ setup(build) {
72
+ build.onResolve({ filter: /.*/ }, (args) => {
73
+ if (!shared.has(args.path)) return undefined;
74
+ onShared?.(args.path);
75
+ return { path: args.path, namespace };
76
+ });
77
+
78
+ build.onLoad({ filter: /.*/, namespace }, async (args) => ({
79
+ contents: buildSharedModuleSource(args.path, await exportNamesFor(args.path)),
80
+ loader: "js",
81
+ }));
82
+ },
83
+ };
84
+ }
85
+
86
+ async function hostExportNames(specifier: string): Promise<readonly string[]> {
87
+ const mod = await import(specifier);
88
+ return Object.keys(mod).sort();
89
+ }
90
+
91
+ /**
92
+ * Bundles one plugin directory to `outDir`, returning the emitted module path.
93
+ * Throws on a compile error so the caller can surface it against the plugin
94
+ * rather than failing the whole renderer.
95
+ */
96
+ export async function bundleExternalPlugin(
97
+ pluginDir: string,
98
+ outDir: string,
99
+ options: { exportNamesFor?: (specifier: string) => Promise<readonly string[]> } = {},
100
+ ): Promise<BundlePluginResult> {
101
+ const entry = await resolvePluginEntry(pluginDir);
102
+ if (!entry) throw new Error(`No plugin entry file in ${pluginDir}`);
103
+
104
+ await mkdir(outDir, { recursive: true });
105
+
106
+ const shared = new Set<string>();
107
+ const result = await Bun.build({
108
+ entrypoints: [entry],
109
+ outdir: outDir,
110
+ target: "browser",
111
+ format: "esm",
112
+ splitting: false,
113
+ minify: false,
114
+ sourcemap: "none",
115
+ plugins: [
116
+ createSharedModuleResolver(
117
+ options.exportNamesFor ?? hostExportNames,
118
+ (specifier) => shared.add(specifier),
119
+ ),
120
+ ],
121
+ });
122
+
123
+ if (!result.success) {
124
+ throw new Error(result.logs.map((log) => log.message).join("\n"));
125
+ }
126
+
127
+ const output = result.outputs.find((entryOutput) => entryOutput.kind === "entry-point");
128
+ if (!output) throw new Error(`Bundling ${pluginDir} produced no entry point`);
129
+
130
+ return {
131
+ outputPath: output.path,
132
+ shared: [...shared].sort(),
133
+ warnings: result.logs.filter((log) => log.level === "warning").map((log) => log.message),
134
+ };
135
+ }
136
+
137
+ export function pluginBundleCacheDir(baseDir: string): string {
138
+ return join(baseDir, "bundles");
139
+ }
@@ -63,6 +63,7 @@ describe("browser plugin catalog", () => {
63
63
  "market-heatmap",
64
64
  "market-movers",
65
65
  "market-halts",
66
+ "fear-greed",
66
67
  "earnings-calendar",
67
68
  "ipo-calendar",
68
69
  "tv",
@@ -12,7 +12,13 @@ import { connectionsModule } from "./builtin/connections";
12
12
  import { correlationModule } from "./builtin/correlation";
13
13
  import { cdsModule } from "./builtin/cds";
14
14
  import { creditConditionsModule } from "./builtin/credit-conditions";
15
+ import { marketValuationModule } from "./builtin/market-valuation";
16
+ import {
17
+ attachValuationPersistence,
18
+ resetValuationPersistence,
19
+ } from "./builtin/market-valuation/cache";
15
20
  import { economicCalendarModule } from "./builtin/econ";
21
+ import { econStatisticsModule } from "./builtin/econ-statistics";
16
22
  import { futuresModule } from "./builtin/futures";
17
23
  import { fxMatrixModule } from "./builtin/fx-matrix";
18
24
  import { helpModule } from "./builtin/help";
@@ -94,9 +100,11 @@ const browserMarketOverviewPlugin = composeBuiltinPlugin({
94
100
  const browserFredResourcesModule: PluginModule = {
95
101
  setup(ctx) {
96
102
  attachFredSeriesPersistence(ctx.persistence);
103
+ attachValuationPersistence(ctx.persistence);
97
104
  },
98
105
  dispose() {
99
106
  resetFredSeriesPersistence();
107
+ resetValuationPersistence();
100
108
  },
101
109
  };
102
110
 
@@ -109,9 +117,11 @@ const browserMacroPlugin = composeBuiltinPlugin({
109
117
  modules: [
110
118
  browserFredResourcesModule,
111
119
  economicCalendarModule,
120
+ econStatisticsModule,
112
121
  yieldCurveModule,
113
122
  volatilityModule,
114
123
  creditConditionsModule,
124
+ marketValuationModule,
115
125
  cdsModule,
116
126
  treasuryAuctionsModule,
117
127
  ],
@@ -0,0 +1,43 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { buildBuiltinManifest } from "./catalog-manifest";
4
+
5
+ /**
6
+ * The published manifest is the built-in half of the plugin directory. It used
7
+ * to be hand-maintained in a separate repository, where it drifted from the
8
+ * real catalog with nothing to catch it: a built-in could rename itself, gain a
9
+ * pane, or be added outright, and the directory would keep showing the old
10
+ * description indefinitely.
11
+ *
12
+ * These tests exist so that cannot recur. Drift now fails here rather than
13
+ * being discovered by a user reading a stale listing.
14
+ */
15
+ describe("built-in plugin manifest", () => {
16
+ test("matches the committed file, so the directory cannot serve a stale catalog", async () => {
17
+ const committed = JSON.parse(
18
+ await Bun.file(new URL("../../plugin-manifest.json", import.meta.url)).text(),
19
+ );
20
+ const { uncategorised: _drop, ...generated } = buildBuiltinManifest();
21
+
22
+ expect(generated).toEqual(committed);
23
+ });
24
+
25
+ test("every built-in has editorial metadata", () => {
26
+ // A new built-in with no categories would appear unfiltered and
27
+ // uncategorised in the directory, so generation refuses instead.
28
+ expect(buildBuiltinManifest().uncategorised).toEqual([]);
29
+ });
30
+
31
+ test("exactly one plugin is featured", () => {
32
+ const featured = buildBuiltinManifest().plugins.filter((plugin) => plugin.featured);
33
+
34
+ expect(featured.map((plugin) => plugin.id)).toEqual(["gloomberb-cloud"]);
35
+ });
36
+
37
+ test("describes what each plugin contributes, which is what the directory renders", () => {
38
+ const cloud = buildBuiltinManifest().plugins.find((plugin) => plugin.id === "gloomberb-cloud");
39
+
40
+ expect(cloud?.contributes.panes).toContain("chat");
41
+ expect(cloud?.contributes.capabilities.length).toBeGreaterThan(0);
42
+ });
43
+ });
@@ -0,0 +1,92 @@
1
+ import { getPluginCatalog } from "./catalog";
2
+
3
+ /**
4
+ * The public description of the plugins that ship inside Gloomberb.
5
+ *
6
+ * The plugin directory has to list built-ins alongside installable ones, but a
7
+ * built-in has no repository to read metadata from. Hand-maintaining that half
8
+ * elsewhere meant it silently drifted every time a built-in changed its panes
9
+ * or description, so it is derived from the live catalog here instead and
10
+ * checked against a committed snapshot in CI.
11
+ */
12
+
13
+ export interface BuiltinPluginManifestEntry {
14
+ id: string;
15
+ name: string;
16
+ description: string;
17
+ /** Curated, since the catalog has no notion of categories. */
18
+ categories: string[];
19
+ toggleable: boolean;
20
+ featured?: true;
21
+ contributes: {
22
+ panes: string[];
23
+ capabilities: string[];
24
+ broker: boolean;
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Editorial metadata the runtime catalog does not carry. Anything missing here
30
+ * is reported by `generate-plugin-manifest.ts` rather than silently defaulted,
31
+ * so a new built-in cannot slip into the directory uncategorised.
32
+ */
33
+ const EDITORIAL: Record<string, { categories: string[]; featured?: true }> = {
34
+ "gloomberb-cloud": { categories: ["data", "cloud"], featured: true },
35
+ ai: { categories: ["ai"] },
36
+ alerts: { categories: ["alerts"] },
37
+ application: { categories: ["core"] },
38
+ broker: { categories: ["broker"] },
39
+ debug: { categories: ["developer"] },
40
+ macro: { categories: ["macro"] },
41
+ "market-overview": { categories: ["markets"] },
42
+ news: { categories: ["news"] },
43
+ notes: { categories: ["productivity"] },
44
+ polls: { categories: ["data"] },
45
+ portfolio: { categories: ["portfolio"] },
46
+ "prediction-markets": { categories: ["markets"] },
47
+ public: { categories: ["broker"] },
48
+ "research-search": { categories: ["research", "news"] },
49
+ robinhood: { categories: ["broker"] },
50
+ simplefin: { categories: ["broker"] },
51
+ "ticker-research": { categories: ["research"] },
52
+ yahoo: { categories: ["data"] },
53
+ };
54
+
55
+ export interface BuiltinManifest {
56
+ version: 1;
57
+ plugins: BuiltinPluginManifestEntry[];
58
+ /** Built-in ids with no editorial entry; a non-empty list fails generation. */
59
+ uncategorised: string[];
60
+ }
61
+
62
+ export function buildBuiltinManifest(): BuiltinManifest {
63
+ const uncategorised: string[] = [];
64
+
65
+ const plugins = getPluginCatalog()
66
+ .map(({ plugin }) => {
67
+ const editorial = EDITORIAL[plugin.id];
68
+ if (!editorial) uncategorised.push(plugin.id);
69
+
70
+ return {
71
+ id: plugin.id,
72
+ name: plugin.name,
73
+ description: plugin.description ?? "",
74
+ categories: editorial?.categories ?? [],
75
+ toggleable: plugin.toggleable === true,
76
+ ...(editorial?.featured ? { featured: true as const } : {}),
77
+ contributes: {
78
+ panes: (plugin.panes ?? []).map((pane) => pane.id).sort(),
79
+ capabilities: (plugin.capabilities ?? [])
80
+ .map((capability) => {
81
+ const value = capability as { id?: string; kind?: string };
82
+ return value.id ?? value.kind ?? "capability";
83
+ })
84
+ .sort(),
85
+ broker: !!plugin.broker,
86
+ },
87
+ };
88
+ })
89
+ .sort((left, right) => left.id.localeCompare(right.id));
90
+
91
+ return { version: 1, plugins, uncategorised: uncategorised.sort() };
92
+ }
@@ -1,16 +1,16 @@
1
1
  import type { GloomPlugin } from "../types/plugin";
2
+ import type { LoadedExternalPlugin } from "./loader";
2
3
  import { newsPlugin } from "./builtin/news";
3
4
  import { notesPlugin } from "./builtin/notes";
4
- import { substackPlugin } from "./builtin/substack";
5
5
  import { aiPlugin } from "./builtin/ai";
6
6
  import { gloomberbCloudPlugin } from "./builtin/cloud";
7
- import { ibkrPlugin } from "./ibkr";
8
7
  import { publicPlugin } from "./broker-sync/public";
9
8
  import { robinhoodPlugin } from "./broker-sync/robinhood";
10
9
  import { simpleFinPlugin } from "./broker-sync/simplefin";
11
10
  import { predictionMarketsPlugin } from "./prediction-markets";
12
11
  import { pollsPlugin } from "./builtin/polls";
13
12
  import { alertsPlugin } from "./builtin/alerts";
13
+ import { researchSearchPlugin } from "./builtin/research-search";
14
14
  import {
15
15
  applicationPlugin,
16
16
  brokerPlugin,
@@ -25,13 +25,11 @@ export const uiBuiltinPlugins: GloomPlugin[] = [
25
25
  portfolioPlugin,
26
26
  tickerResearchPlugin,
27
27
  brokerPlugin,
28
- ibkrPlugin,
29
28
  publicPlugin,
30
29
  robinhoodPlugin,
31
30
  simpleFinPlugin,
32
31
  applicationPlugin,
33
32
  newsPlugin,
34
- substackPlugin,
35
33
  notesPlugin,
36
34
  aiPlugin,
37
35
  predictionMarketsPlugin,
@@ -39,8 +37,26 @@ export const uiBuiltinPlugins: GloomPlugin[] = [
39
37
  marketOverviewPlugin,
40
38
  macroPlugin,
41
39
  alertsPlugin,
40
+ researchSearchPlugin,
42
41
  ];
43
42
 
44
43
  export function getRendererBuiltinPlugins(): GloomPlugin[] {
45
44
  return uiBuiltinPlugins;
46
45
  }
46
+
47
+ /**
48
+ * The plugin list for a UI renderer: the built-ins it ships with, plus any
49
+ * external plugins that loaded and support this renderer.
50
+ *
51
+ * Deliberately not `getLoadablePlugins`, which is the CLI catalog and also
52
+ * carries the Yahoo fallback provider and the debug plugin. Routing the desktop
53
+ * through it would quietly change which plugins the app runs.
54
+ */
55
+ export function getRendererPlugins(externalPlugins: readonly LoadedExternalPlugin[] = []): GloomPlugin[] {
56
+ return [
57
+ ...uiBuiltinPlugins,
58
+ ...externalPlugins
59
+ .filter((entry) => !entry.error && !entry.unsupportedTarget)
60
+ .map((entry) => entry.plugin),
61
+ ];
62
+ }