gloomberb 0.5.0 → 0.6.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 (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -13
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +186 -6
  440. package/src/utils/api-client.ts +433 -33
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,29 +1,49 @@
1
- import { useEffect, useMemo, useSyncExternalStore } from "react";
2
- import type { NewsItem, SecFilingItem } from "../types/data-provider";
1
+ import { useCallback, useEffect, useMemo, useSyncExternalStore } from "react";
2
+ import type { SecFilingItem } from "../types/data-provider";
3
3
  import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
4
4
  import type { TickerRecord } from "../types/ticker";
5
- import type { ChartRequest, InstrumentRef, NewsRequest, OptionsRequest, SecFilingsRequest } from "./request-types";
5
+ import type { ChartRequest, InstrumentRef, OptionsRequest, SecFilingsRequest } from "./request-types";
6
6
  import { instrumentFromTicker } from "./request-types";
7
7
  import {
8
8
  getSharedMarketDataCoordinator,
9
9
  resolveEntryValue,
10
10
  } from "./coordinator";
11
11
  import type { QueryEntry } from "./result-types";
12
- import { buildChartKey } from "./selectors";
13
-
14
- function useCoordinatorVersion(): number {
12
+ import {
13
+ buildArticleSummaryKey,
14
+ buildChartKey,
15
+ buildFxKey,
16
+ buildOptionsKey,
17
+ buildQuoteKey,
18
+ buildSecContentKey,
19
+ buildSecFilingsKey,
20
+ buildSnapshotKey,
21
+ } from "./selectors";
22
+
23
+ const TICKER_FINANCIALS_LOAD_DELAY_MS = 250;
24
+
25
+ function useCoordinatorKeysVersion(keys: readonly string[]): number {
15
26
  const coordinator = getSharedMarketDataCoordinator();
16
- return useSyncExternalStore(
17
- coordinator?.subscribe.bind(coordinator) ?? (() => () => {}),
18
- () => coordinator?.getVersion() ?? 0,
19
- () => 0,
27
+ const keyString = keys.join("\u001f");
28
+ const stableKeys = useMemo(
29
+ () => (keyString ? keyString.split("\u001f") : []),
30
+ [keyString],
20
31
  );
21
- }
22
-
23
- function useCoordinatorSelector<T>(selector: (coordinator: NonNullable<ReturnType<typeof getSharedMarketDataCoordinator>>) => T, fallback: T): T {
24
- const coordinator = getSharedMarketDataCoordinator();
25
- useCoordinatorVersion();
26
- return coordinator ? selector(coordinator) : fallback;
32
+ const subscribe = useCallback((listener: () => void) => {
33
+ if (!coordinator) return () => {};
34
+ if (typeof coordinator.subscribeKeys === "function") {
35
+ return coordinator.subscribeKeys(stableKeys, listener);
36
+ }
37
+ return coordinator.subscribe(listener);
38
+ }, [coordinator, stableKeys]);
39
+ const getSnapshot = useCallback(() => {
40
+ if (!coordinator) return 0;
41
+ if (typeof coordinator.getKeysVersion === "function") {
42
+ return coordinator.getKeysVersion(stableKeys);
43
+ }
44
+ return coordinator.getVersion();
45
+ }, [coordinator, stableKeys]);
46
+ return useSyncExternalStore(subscribe, getSnapshot, () => 0);
27
47
  }
28
48
 
29
49
  function stableCurrencyList(currencies: Array<string | null | undefined>): string[] {
@@ -55,24 +75,52 @@ export function useTickerInstrument(symbol: string | null | undefined, ticker: T
55
75
 
56
76
  export function useTickerFinancials(symbol: string | null | undefined, ticker: TickerRecord | null | undefined): TickerFinancials | null {
57
77
  const instrument = useTickerInstrument(symbol, ticker);
58
- const financials = useCoordinatorSelector(
59
- (coordinator) => (instrument ? coordinator.getTickerFinancialsSync(instrument) : null),
60
- null,
61
- );
78
+ const keys = useMemo(() => (
79
+ instrument
80
+ ? [
81
+ buildSnapshotKey(instrument),
82
+ buildQuoteKey(instrument),
83
+ buildChartKey({ instrument, bufferRange: "5Y", granularity: "range" }),
84
+ ]
85
+ : []
86
+ ), [instrument?.brokerId, instrument?.brokerInstanceId, instrument?.exchange, instrument?.instrument?.conId, instrument?.symbol]);
87
+ useCoordinatorKeysVersion(keys);
88
+ const coordinator = getSharedMarketDataCoordinator();
89
+ const financials = coordinator && instrument
90
+ ? coordinator.getTickerFinancialsSync(instrument)
91
+ : null;
62
92
 
63
93
  useEffect(() => {
64
94
  const coordinator = getSharedMarketDataCoordinator();
65
95
  if (!coordinator || !instrument) return;
66
- void coordinator.loadSnapshot(instrument);
96
+ const timeoutId = setTimeout(() => {
97
+ void coordinator.loadSnapshot(instrument);
98
+ }, TICKER_FINANCIALS_LOAD_DELAY_MS);
99
+ return () => clearTimeout(timeoutId);
67
100
  }, [instrument?.brokerId, instrument?.brokerInstanceId, instrument?.exchange, instrument?.instrument?.conId, instrument?.symbol]);
68
101
 
69
102
  return financials;
70
103
  }
71
104
 
105
+ function buildTickerFinancialsKeys(tickers: TickerRecord[]): string[] {
106
+ const keys: string[] = [];
107
+ for (const ticker of tickers) {
108
+ const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
109
+ if (!instrument) continue;
110
+ keys.push(
111
+ buildSnapshotKey(instrument),
112
+ buildQuoteKey(instrument),
113
+ buildChartKey({ instrument, bufferRange: "5Y", granularity: "range" }),
114
+ );
115
+ }
116
+ return keys;
117
+ }
118
+
72
119
  export function useTickerFinancialsMap(tickers: TickerRecord[]): Map<string, TickerFinancials> {
73
120
  const coordinator = getSharedMarketDataCoordinator();
74
- const version = useCoordinatorVersion();
75
121
  const tickerKey = buildTickerFinancialsMapKey(tickers);
122
+ const subscriptionKeys = useMemo(() => buildTickerFinancialsKeys(tickers), [tickerKey]);
123
+ const keysVersion = useCoordinatorKeysVersion(subscriptionKeys);
76
124
 
77
125
  return useMemo(() => {
78
126
  if (!coordinator) return new Map<string, TickerFinancials>();
@@ -86,15 +134,18 @@ export function useTickerFinancialsMap(tickers: TickerRecord[]): Map<string, Tic
86
134
  }
87
135
  }
88
136
  return result;
89
- }, [coordinator, tickerKey, version]);
137
+ }, [coordinator, keysVersion, tickerKey, subscriptionKeys]);
90
138
  }
91
139
 
92
140
  export function useQuoteEntry(symbol: string | null | undefined, ticker: TickerRecord | null | undefined): QueryEntry<Quote> | null {
93
141
  const instrument = useTickerInstrument(symbol, ticker);
94
- const entry = useCoordinatorSelector(
95
- (coordinator) => (instrument ? coordinator.getQuoteEntry(instrument) : null),
96
- null,
142
+ const keys = useMemo(
143
+ () => (instrument ? [buildQuoteKey(instrument)] : []),
144
+ [instrument?.brokerId, instrument?.brokerInstanceId, instrument?.exchange, instrument?.instrument?.conId, instrument?.symbol],
97
145
  );
146
+ useCoordinatorKeysVersion(keys);
147
+ const coordinator = getSharedMarketDataCoordinator();
148
+ const entry = coordinator && instrument ? coordinator.getQuoteEntry(instrument) : null;
98
149
 
99
150
  useEffect(() => {
100
151
  const coordinator = getSharedMarketDataCoordinator();
@@ -107,10 +158,9 @@ export function useQuoteEntry(symbol: string | null | undefined, ticker: TickerR
107
158
 
108
159
  export function useChartQuery(request: ChartRequest | null | undefined): QueryEntry<PricePoint[]> | null {
109
160
  const key = request ? buildChartKey(request) : null;
110
- const entry = useCoordinatorSelector(
111
- (coordinator) => (request ? coordinator.getChartEntry(request) : null),
112
- null,
113
- );
161
+ useCoordinatorKeysVersion(key ? [key] : []);
162
+ const coordinator = getSharedMarketDataCoordinator();
163
+ const entry = coordinator && request ? coordinator.getChartEntry(request) : null;
114
164
 
115
165
  useEffect(() => {
116
166
  const coordinator = getSharedMarketDataCoordinator();
@@ -127,9 +177,14 @@ export function useChartQueries(
127
177
  ): Map<string, QueryEntry<PricePoint[]>> {
128
178
  const requestKey = requests.map((request) => buildChartKey(request)).join(",");
129
179
  const debounceMs = Math.max(0, options.debounceMs ?? 0);
130
- const entries = useCoordinatorSelector((coordinator) => (
131
- requests.map((request) => [buildChartKey(request), coordinator.getChartEntry(request)] as const)
132
- ), [] as Array<readonly [string, QueryEntry<PricePoint[]>]>);
180
+ const keys = useMemo(() => requests.map((request) => buildChartKey(request)), [requestKey]);
181
+ const keysVersion = useCoordinatorKeysVersion(keys);
182
+ const coordinator = getSharedMarketDataCoordinator();
183
+ const entries = useMemo(() => (
184
+ coordinator
185
+ ? requests.map((request) => [buildChartKey(request), coordinator.getChartEntry(request)] as const)
186
+ : [] as Array<readonly [string, QueryEntry<PricePoint[]>]>
187
+ ), [coordinator, keys, keysVersion, requestKey]);
133
188
 
134
189
  useEffect(() => {
135
190
  const coordinator = getSharedMarketDataCoordinator();
@@ -150,60 +205,41 @@ export function useChartQueries(
150
205
  return useMemo(() => new Map(entries), [entries]);
151
206
  }
152
207
 
153
- export function useNewsQuery(request: NewsRequest | null | undefined): QueryEntry<NewsItem[]> | null {
154
- const key = request ? `${request.instrument.symbol}|${request.instrument.exchange ?? ""}|${request.count ?? 50}` : null;
155
- const entry = useCoordinatorSelector(
156
- (coordinator) => (request ? coordinator.getNewsEntry(request) : null),
157
- null,
158
- );
159
-
160
- useEffect(() => {
161
- const coordinator = getSharedMarketDataCoordinator();
162
- if (!coordinator || !request) return;
163
- void coordinator.loadNews(request);
164
- }, [key]);
165
-
166
- return entry;
167
- }
168
-
169
208
  export function useOptionsQuery(request: OptionsRequest | null | undefined): QueryEntry<OptionsChain> | null {
170
- const key = request ? `${request.instrument.symbol}|${request.instrument.exchange ?? ""}|${request.expirationDate ?? "default"}` : null;
171
- const entry = useCoordinatorSelector(
172
- (coordinator) => (request ? coordinator.getOptionsEntry(request) : null),
173
- null,
174
- );
209
+ const requestKey = request ? buildOptionsKey(request) : null;
210
+ useCoordinatorKeysVersion(requestKey ? [requestKey] : []);
211
+ const coordinator = getSharedMarketDataCoordinator();
212
+ const entry = coordinator && request ? coordinator.getOptionsEntry(request) : null;
175
213
 
176
214
  useEffect(() => {
177
215
  const coordinator = getSharedMarketDataCoordinator();
178
216
  if (!coordinator || !request) return;
179
217
  void coordinator.loadOptions(request);
180
- }, [key]);
218
+ }, [requestKey]);
181
219
 
182
220
  return entry;
183
221
  }
184
222
 
185
223
  export function useSecFilingsQuery(request: SecFilingsRequest | null | undefined): QueryEntry<SecFilingItem[]> | null {
186
- const key = request ? `${request.instrument.symbol}|${request.instrument.exchange ?? ""}|${request.count ?? 50}` : null;
187
- const entry = useCoordinatorSelector(
188
- (coordinator) => (request ? coordinator.getSecFilingsEntry(request) : null),
189
- null,
190
- );
224
+ const requestKey = request ? buildSecFilingsKey(request) : null;
225
+ useCoordinatorKeysVersion(requestKey ? [requestKey] : []);
226
+ const coordinator = getSharedMarketDataCoordinator();
227
+ const entry = coordinator && request ? coordinator.getSecFilingsEntry(request) : null;
191
228
 
192
229
  useEffect(() => {
193
230
  const coordinator = getSharedMarketDataCoordinator();
194
231
  if (!coordinator || !request) return;
195
232
  void coordinator.loadSecFilings(request);
196
- }, [key]);
233
+ }, [requestKey]);
197
234
 
198
235
  return entry;
199
236
  }
200
237
 
201
238
  export function useSecFilingContent(filing: SecFilingItem | null | undefined): QueryEntry<string | null> | null {
202
- const key = filing?.accessionNumber ?? null;
203
- const entry = useCoordinatorSelector(
204
- (coordinator) => (filing ? coordinator.getSecContentEntry(filing.accessionNumber) : null),
205
- null,
206
- );
239
+ const key = filing ? buildSecContentKey(filing.accessionNumber) : null;
240
+ useCoordinatorKeysVersion(key ? [key] : []);
241
+ const coordinator = getSharedMarketDataCoordinator();
242
+ const entry = coordinator && filing ? coordinator.getSecContentEntry(filing.accessionNumber) : null;
207
243
 
208
244
  useEffect(() => {
209
245
  const coordinator = getSharedMarketDataCoordinator();
@@ -215,10 +251,10 @@ export function useSecFilingContent(filing: SecFilingItem | null | undefined): Q
215
251
  }
216
252
 
217
253
  export function useArticleSummary(url: string | null | undefined): QueryEntry<string | null> | null {
218
- const entry = useCoordinatorSelector(
219
- (coordinator) => (url ? coordinator.getArticleSummaryEntry(url) : null),
220
- null,
221
- );
254
+ const key = url ? buildArticleSummaryKey(url) : null;
255
+ useCoordinatorKeysVersion(key ? [key] : []);
256
+ const coordinator = getSharedMarketDataCoordinator();
257
+ const entry = coordinator && url ? coordinator.getArticleSummaryEntry(url) : null;
222
258
 
223
259
  useEffect(() => {
224
260
  const coordinator = getSharedMarketDataCoordinator();
@@ -229,33 +265,19 @@ export function useArticleSummary(url: string | null | undefined): QueryEntry<st
229
265
  return entry;
230
266
  }
231
267
 
232
- export function useFxRate(currency: string | null | undefined): QueryEntry<number> | null {
233
- const entry = useCoordinatorSelector(
234
- (coordinator) => (currency ? coordinator.getFxEntry(currency) : null),
235
- null,
236
- );
237
-
238
- useEffect(() => {
239
- const coordinator = getSharedMarketDataCoordinator();
240
- if (!coordinator || !currency) return;
241
- void coordinator.loadFxRate(currency);
242
- }, [currency]);
243
-
244
- return entry;
245
- }
246
-
247
268
  export function useFxRatesMap(currencies: Array<string | null | undefined>): Map<string, number> {
248
269
  const coordinator = getSharedMarketDataCoordinator();
249
- const version = useCoordinatorVersion();
250
270
  const normalizedCurrencyKey = stableCurrencyList(currencies).join("|");
251
271
  const normalizedCurrencies = useMemo(
252
272
  () => (normalizedCurrencyKey ? normalizedCurrencyKey.split("|") : []),
253
273
  [normalizedCurrencyKey],
254
274
  );
275
+ const keys = useMemo(() => normalizedCurrencies.map(buildFxKey), [normalizedCurrencies]);
276
+ const keysVersion = useCoordinatorKeysVersion(keys);
255
277
  const entries = useMemo(() => {
256
278
  if (!coordinator) return [] as Array<readonly [string, QueryEntry<number>]>;
257
279
  return normalizedCurrencies.map((currency) => [currency, coordinator.getFxEntry(currency)] as const);
258
- }, [coordinator, normalizedCurrencies, version]);
280
+ }, [coordinator, keys, keysVersion, normalizedCurrencies]);
259
281
 
260
282
  useEffect(() => {
261
283
  const coordinator = getSharedMarketDataCoordinator();
@@ -4,7 +4,7 @@ import { createIdleEntry } from "./result-types";
4
4
  export class QueryStore<T> {
5
5
  private readonly entries = new Map<string, QueryEntry<T>>();
6
6
 
7
- constructor(private readonly onChange: () => void) {}
7
+ constructor(private readonly onChange: (key: string) => void) {}
8
8
 
9
9
  get(key: string): QueryEntry<T> {
10
10
  return this.entries.get(key) ?? createIdleEntry<T>();
@@ -12,13 +12,13 @@ export class QueryStore<T> {
12
12
 
13
13
  set(key: string, entry: QueryEntry<T>): void {
14
14
  this.entries.set(key, entry);
15
- this.onChange();
15
+ this.onChange(key);
16
16
  }
17
17
 
18
18
  update(key: string, updater: (current: QueryEntry<T>) => QueryEntry<T>): QueryEntry<T> {
19
19
  const next = updater(this.get(key));
20
20
  this.entries.set(key, next);
21
- this.onChange();
21
+ this.onChange(key);
22
22
  return next;
23
23
  }
24
24
 
@@ -47,18 +47,4 @@ describe("quoteSubscriptionTargetFromTicker", () => {
47
47
  });
48
48
  });
49
49
 
50
- test("still returns a valid unscoped target when no broker contract exists", () => {
51
- const ticker = makeTicker();
52
-
53
- expect(quoteSubscriptionTargetFromTicker(ticker, ticker.metadata.ticker)).toEqual({
54
- symbol: "AAPL",
55
- exchange: "NASDAQ",
56
- route: "auto",
57
- context: {
58
- brokerId: undefined,
59
- brokerInstanceId: undefined,
60
- instrument: null,
61
- },
62
- });
63
- });
64
50
  });
@@ -1,3 +1,4 @@
1
+
1
2
  export type ProviderStatus = "success" | "partial" | "empty" | "unsupported" | "retryable_error" | "fatal_error";
2
3
 
3
4
  export type ProviderReasonCode =
@@ -0,0 +1,267 @@
1
+ import { describe, it, expect, beforeEach, mock } from "bun:test";
2
+ import { NewsService } from "./aggregator";
3
+ import { newsProvider, type NewsCapability } from "../capabilities";
4
+ import type { MarketNewsItem } from "../types/news-source";
5
+
6
+ function makeItem(overrides: Partial<MarketNewsItem> & { url: string }): MarketNewsItem {
7
+ return {
8
+ ...overrides,
9
+ id: overrides.url,
10
+ title: "Test headline",
11
+ url: overrides.url,
12
+ source: "Test",
13
+ publishedAt: overrides.publishedAt ?? new Date(),
14
+ topic: overrides.topic ?? "general",
15
+ topics: overrides.topics ?? [overrides.topic ?? "general"],
16
+ sectors: overrides.sectors ?? [],
17
+ categories: overrides.categories ?? [],
18
+ tickers: [],
19
+ scores: overrides.scores ?? {
20
+ importance: overrides.importance ?? 50,
21
+ urgency: overrides.isBreaking ? 80 : 0,
22
+ marketImpact: overrides.importance ?? 50,
23
+ novelty: 0,
24
+ confidence: 0,
25
+ },
26
+ importance: overrides.importance ?? 50,
27
+ isBreaking: overrides.isBreaking ?? false,
28
+ isDeveloping: overrides.isDeveloping ?? false,
29
+ summary: undefined,
30
+ };
31
+ }
32
+
33
+ function makeSource(id: string, items: MarketNewsItem[]): NewsCapability {
34
+ return newsProvider({
35
+ id,
36
+ name: id,
37
+ provider: {
38
+ fetchNews: mock(async () => items),
39
+ },
40
+ });
41
+ }
42
+
43
+ function makeCachedSource(id: string, cachedItems: MarketNewsItem[], fetchItems: MarketNewsItem[] = []): NewsCapability {
44
+ return newsProvider({
45
+ id,
46
+ name: id,
47
+ provider: {
48
+ getCachedNews: () => cachedItems,
49
+ fetchNews: mock(async () => fetchItems),
50
+ },
51
+ });
52
+ }
53
+
54
+ describe("NewsService", () => {
55
+ let agg: NewsService;
56
+
57
+ beforeEach(() => {
58
+ agg = new NewsService();
59
+ });
60
+
61
+ it("deduplicates by URL, keeping higher importance", async () => {
62
+ const low = makeItem({ url: "https://example.com/1", importance: 40 });
63
+ const high = makeItem({ url: "https://example.com/1", importance: 80 });
64
+
65
+ agg.register(makeSource("a", [low]));
66
+ agg.register(makeSource("b", [high]));
67
+ await agg.poll();
68
+
69
+ const stories = agg.getTopStories(10);
70
+ expect(stories).toHaveLength(1);
71
+ expect(stories[0]!.importance).toBe(80);
72
+ });
73
+
74
+ it("getTopStories returns items sorted by importance descending", async () => {
75
+ const items = [
76
+ makeItem({ url: "https://a.com/1", importance: 30 }),
77
+ makeItem({ url: "https://a.com/2", importance: 90 }),
78
+ makeItem({ url: "https://a.com/3", importance: 60 }),
79
+ ];
80
+ agg.register(makeSource("a", items));
81
+ await agg.poll();
82
+
83
+ const stories = agg.getTopStories(10);
84
+ expect(stories[0]!.importance).toBe(90);
85
+ expect(stories[1]!.importance).toBe(60);
86
+ expect(stories[2]!.importance).toBe(30);
87
+ });
88
+
89
+ it("getFirehose returns items sorted by publishedAt descending", async () => {
90
+ const now = Date.now();
91
+ const items = [
92
+ makeItem({ url: "https://b.com/1", publishedAt: new Date(now - 3000) }),
93
+ makeItem({ url: "https://b.com/2", publishedAt: new Date(now - 1000) }),
94
+ makeItem({ url: "https://b.com/3", publishedAt: new Date(now - 2000) }),
95
+ ];
96
+ agg.register(makeSource("b", items));
97
+ await agg.poll();
98
+
99
+ const firehose = agg.getFirehose(undefined, 10);
100
+ expect(firehose[0]!.url).toBe("https://b.com/2");
101
+ expect(firehose[1]!.url).toBe("https://b.com/3");
102
+ expect(firehose[2]!.url).toBe("https://b.com/1");
103
+ });
104
+
105
+ it("getFirehose filters to items after `since`", async () => {
106
+ const now = Date.now();
107
+ const cutoff = new Date(now - 2000);
108
+ const items = [
109
+ makeItem({ url: "https://c.com/old", publishedAt: new Date(now - 5000) }),
110
+ makeItem({ url: "https://c.com/new", publishedAt: new Date(now - 1000) }),
111
+ ];
112
+ agg.register(makeSource("c", items));
113
+ await agg.poll();
114
+
115
+ const firehose = agg.getFirehose(cutoff, 10);
116
+ expect(firehose).toHaveLength(1);
117
+ expect(firehose[0]!.url).toBe("https://c.com/new");
118
+ });
119
+
120
+ it("getBySector filters to items containing the sector in categories", async () => {
121
+ const items = [
122
+ makeItem({ url: "https://d.com/1", categories: ["tech", "earnings"] }),
123
+ makeItem({ url: "https://d.com/2", categories: ["energy"] }),
124
+ makeItem({ url: "https://d.com/3", categories: ["tech"] }),
125
+ ];
126
+ agg.register(makeSource("d", items));
127
+ await agg.poll();
128
+
129
+ const tech = agg.getBySector("tech", 10);
130
+ expect(tech).toHaveLength(2);
131
+ expect(tech.every((i) => i.categories.includes("tech"))).toBe(true);
132
+ });
133
+
134
+ it("getBreaking returns items that are isBreaking=true", async () => {
135
+ const items = [
136
+ makeItem({ url: "https://e.com/1", isBreaking: true }),
137
+ makeItem({ url: "https://e.com/2", isBreaking: false }),
138
+ ];
139
+ agg.register(makeSource("e", items));
140
+ await agg.poll();
141
+
142
+ const breaking = agg.getBreaking(10);
143
+ expect(breaking.some((i) => i.url === "https://e.com/1")).toBe(true);
144
+ expect(breaking.every((i) => i.url !== "https://e.com/2")).toBe(true);
145
+ });
146
+
147
+ it("getBreaking returns recent items with importance >= 70", async () => {
148
+ const now = Date.now();
149
+ const items = [
150
+ makeItem({ url: "https://f.com/recent-high", publishedAt: new Date(now - 30 * 60 * 1000), importance: 75, isBreaking: false }),
151
+ makeItem({ url: "https://f.com/recent-low", publishedAt: new Date(now - 30 * 60 * 1000), importance: 50, isBreaking: false }),
152
+ makeItem({ url: "https://f.com/old-high", publishedAt: new Date(now - 2 * 60 * 60 * 1000), importance: 90, isBreaking: false }),
153
+ ];
154
+ agg.register(makeSource("f", items));
155
+ await agg.poll();
156
+
157
+ const breaking = agg.getBreaking(10);
158
+ expect(breaking.some((i) => i.url === "https://f.com/recent-high")).toBe(true);
159
+ expect(breaking.every((i) => i.url !== "https://f.com/recent-low")).toBe(true);
160
+ expect(breaking.every((i) => i.url !== "https://f.com/old-high")).toBe(true);
161
+ });
162
+
163
+ it("retains at most 500 articles", async () => {
164
+ const items = Array.from({ length: 600 }, (_, i) =>
165
+ makeItem({
166
+ url: `https://g.com/${i}`,
167
+ publishedAt: new Date(Date.now() - i * 1000),
168
+ }),
169
+ );
170
+ agg.register(makeSource("g", items));
171
+ await agg.poll();
172
+
173
+ expect(agg.getFirehose(undefined, 1000)).toHaveLength(500);
174
+ });
175
+
176
+ it("subscribe callback fires on poll and getVersion increments", async () => {
177
+ let callCount = 0;
178
+ const unsub = agg.subscribe(() => { callCount++; });
179
+
180
+ const initialVersion = agg.getVersion();
181
+ agg.register(makeSource("h", []));
182
+ await agg.poll();
183
+
184
+ expect(callCount).toBe(1);
185
+ expect(agg.getVersion()).toBe(initialVersion + 1);
186
+
187
+ await agg.poll();
188
+ expect(callCount).toBe(2);
189
+ expect(agg.getVersion()).toBe(initialVersion + 2);
190
+
191
+ unsub();
192
+ await agg.poll();
193
+ expect(callCount).toBe(2); // unsubscribed, no more calls
194
+ });
195
+
196
+ it("watchQuery refreshes a query without a mounted pane", async () => {
197
+ const item = makeItem({ url: "https://watch.example.com/1", isBreaking: true });
198
+ const states: string[][] = [];
199
+
200
+ agg.register(makeSource("watch", [item]));
201
+ const dispose = agg.watchQuery(
202
+ { feed: "breaking", breaking: true, limit: 20 },
203
+ (state) => states.push(state.articles.map((article) => article.url)),
204
+ );
205
+
206
+ await new Promise((resolve) => setTimeout(resolve, 0));
207
+
208
+ expect(states).toContainEqual([item.url]);
209
+
210
+ const callCount = states.length;
211
+ dispose();
212
+ await agg.poll({ feed: "breaking", breaking: true, limit: 20 });
213
+ expect(states).toHaveLength(callCount);
214
+ });
215
+
216
+ it("seeds cached source items immediately on register", () => {
217
+ const cached = makeItem({ url: "https://cached.example.com/1", importance: 70 });
218
+ let callCount = 0;
219
+ agg.subscribe(() => { callCount++; });
220
+
221
+ const dispose = agg.register(makeCachedSource("cached", [cached]));
222
+
223
+ expect(agg.getFirehose(undefined, 10)).toHaveLength(1);
224
+ expect(agg.getFirehose(undefined, 10)[0]!.url).toBe(cached.url);
225
+ expect(callCount).toBe(1);
226
+
227
+ dispose();
228
+ });
229
+
230
+ it("register disposer removes the source", async () => {
231
+ const item = makeItem({ url: "https://dispose.example.com/1" });
232
+ const dispose = agg.register(makeSource("disposable", [item]));
233
+ dispose();
234
+
235
+ await agg.poll();
236
+
237
+ expect(agg.getFirehose(undefined, 10)).toHaveLength(0);
238
+ });
239
+
240
+ it("ticker queries continue past empty high-priority sources", async () => {
241
+ const empty = makeSource("empty", []);
242
+ const fallbackItem = makeItem({ url: "https://fallback.example.com/1", tickers: ["AAPL"] });
243
+ const fallback = makeSource("fallback", [fallbackItem]);
244
+ agg.register({
245
+ ...empty,
246
+ priority: 10,
247
+ provider: {
248
+ ...empty.provider,
249
+ supports: (query) => query.feed === "ticker" || query.scope === "ticker",
250
+ },
251
+ });
252
+ agg.register({
253
+ ...fallback,
254
+ priority: 100,
255
+ provider: {
256
+ ...fallback.provider,
257
+ supports: (query) => query.feed === "ticker" || query.scope === "ticker",
258
+ },
259
+ });
260
+
261
+ const state = await agg.load({ feed: "ticker", ticker: "AAPL", limit: 10 });
262
+
263
+ expect(state.articles).toHaveLength(1);
264
+ expect(state.articles[0]!.url).toBe(fallbackItem.url);
265
+ expect(state.sourceIds).toEqual(["fallback"]);
266
+ });
267
+ });