gloomberb 0.10.0 → 0.10.2

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 (353) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
  274. package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
  275. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  276. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  277. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  278. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  279. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  280. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  281. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  282. package/src/renderers/electrobun/view/update-host.ts +1 -1
  283. package/src/renderers/opentui/chart-surface.tsx +2 -1
  284. package/src/renderers/opentui/dialog-host.tsx +20 -4
  285. package/src/renderers/opentui/image/loader.ts +12 -2
  286. package/src/renderers/opentui/input-host.tsx +7 -49
  287. package/src/renderers/opentui/start.tsx +2 -0
  288. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  289. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  290. package/src/sources/gloomberb-cloud/index.ts +42 -20
  291. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  292. package/src/sources/provider-router/cache.ts +1 -1
  293. package/src/sources/provider-router/document-routes.ts +10 -5
  294. package/src/sources/provider-router/financials.test.ts +14 -0
  295. package/src/sources/provider-router/financials.ts +6 -1
  296. package/src/sources/provider-router/history.test.ts +163 -1
  297. package/src/sources/provider-router/history.ts +278 -347
  298. package/src/sources/provider-router/index.ts +2 -2
  299. package/src/sources/provider-router/routing.test.ts +39 -0
  300. package/src/sources/provider-router/routing.ts +24 -11
  301. package/src/sources/provider-router/supplemental.ts +37 -17
  302. package/src/sources/sec-edgar/content.ts +2 -12
  303. package/src/sources/sec-edgar.ts +2 -13
  304. package/src/sources/yahoo-finance/history.ts +2 -2
  305. package/src/sources/yahoo-finance/options.ts +5 -0
  306. package/src/sources/yahoo-finance.ts +2 -2
  307. package/src/state/app/bootstrap.ts +2 -2
  308. package/src/state/app/context/index.tsx +2 -2
  309. package/src/state/hooks/quote-streaming.ts +66 -1
  310. package/src/state/hooks/session-snapshot.ts +2 -2
  311. package/src/state/selectors.ts +0 -9
  312. package/src/sync/controller.test.ts +98 -0
  313. package/src/sync/controller.ts +29 -5
  314. package/src/sync/react.ts +2 -2
  315. package/src/sync/types.ts +2 -2
  316. package/src/theme/colors.test.ts +10 -0
  317. package/src/theme/colors.ts +75 -78
  318. package/src/theme/theme-context.test.tsx +67 -0
  319. package/src/theme/theme-context.tsx +9 -5
  320. package/src/tickers/open-target.ts +2 -2
  321. package/src/tickers/search/upsert.ts +2 -2
  322. package/src/time-series/alignment.ts +0 -7
  323. package/src/time-series/chart-data.ts +145 -0
  324. package/src/time-series/date-window.ts +44 -0
  325. package/src/time-series/history-window.ts +21 -0
  326. package/src/time-series/hooks.test.ts +59 -0
  327. package/src/time-series/hooks.ts +33 -12
  328. package/src/time-series/market-resolution.ts +2 -2
  329. package/src/time-series/market.ts +1 -1
  330. package/src/time-series/range.ts +9 -0
  331. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  332. package/src/time-series/resolve.test.ts +203 -1
  333. package/src/time-series/resolve.ts +96 -18
  334. package/src/time-series/spec-studies.test.ts +15 -0
  335. package/src/time-series/spec.ts +2 -2
  336. package/src/time-series/studies.ts +5 -2
  337. package/src/time-series/types.ts +3 -1
  338. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  339. package/src/time-series/use-chart-resolution.ts +46 -4
  340. package/src/types/broker.ts +2 -2
  341. package/src/types/data-provider.ts +3 -3
  342. package/src/types/financials.ts +12 -0
  343. package/src/types/plugin.ts +2 -2
  344. package/src/ui/context-menu.tsx +3 -2
  345. package/src/ui/dialog.tsx +2 -2
  346. package/src/utils/html-entities.ts +3 -5
  347. package/src/utils/main-thread-monitor.ts +1 -1
  348. package/src/utils/price-history.ts +5 -5
  349. package/src/version.ts +3 -1
  350. package/src/components/chart/core/viewport.test.ts +0 -8
  351. package/src/components/chart/core/viewport.ts +0 -24
  352. package/src/time-series/index.ts +0 -14
  353. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -17,17 +17,30 @@ export { buildNewsQueryKey } from "./news-model";
17
17
 
18
18
  export interface NewsServiceOptions {
19
19
  pollIntervalMs?: number;
20
+ inactiveQueryTtlMs?: number;
21
+ maxInactiveQueries?: number;
22
+ now?: () => number;
20
23
  }
21
24
 
22
25
  export type NewsQueryListener = (state: NewsQueryState) => void;
23
26
 
24
27
  const DEFAULT_POLL_INTERVAL_MS = 2 * 60 * 1000;
28
+ const DEFAULT_INACTIVE_QUERY_TTL_MS = 10 * 60 * 1000;
29
+ const DEFAULT_MAX_INACTIVE_QUERIES = 50;
25
30
 
26
31
  interface SourceFetchResult {
27
32
  articles: NewsArticle[];
28
33
  sourceIds: string[];
29
34
  }
30
35
 
36
+ interface NewsQueryEntry {
37
+ query: NewsQuery;
38
+ state: NewsQueryState;
39
+ inFlight: Promise<NewsQueryState> | null;
40
+ refs: number;
41
+ lastAccessedAt: number;
42
+ }
43
+
31
44
  function newsCapabilityPriority(source: NewsCapability): number {
32
45
  return source.priority ?? 1000;
33
46
  }
@@ -39,17 +52,20 @@ function newsCapabilitySourceId(source: NewsCapability): string {
39
52
  export class NewsService {
40
53
  private readonly sources = new Map<string, NewsCapability>();
41
54
  private readonly listeners = new Set<() => void>();
42
- private readonly queryStates = new Map<string, NewsQueryState>();
43
- private readonly queryByKey = new Map<string, NewsQuery>();
44
- private readonly watchedQueries = new Map<string, { query: NewsQuery; refs: number }>();
45
- private readonly inFlight = new Map<string, Promise<NewsQueryState>>();
55
+ private readonly queries = new Map<string, NewsQueryEntry>();
46
56
  private articles: NewsArticle[] = [];
47
57
  private version = 0;
48
58
  private pollTimer: ReturnType<typeof setInterval> | null = null;
49
59
  private readonly pollIntervalMs: number;
60
+ private readonly inactiveQueryTtlMs: number;
61
+ private readonly maxInactiveQueries: number;
62
+ private readonly now: () => number;
50
63
 
51
64
  constructor(options: NewsServiceOptions = {}) {
52
65
  this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
66
+ this.inactiveQueryTtlMs = Math.max(1, options.inactiveQueryTtlMs ?? DEFAULT_INACTIVE_QUERY_TTL_MS);
67
+ this.maxInactiveQueries = Math.max(1, Math.floor(options.maxInactiveQueries ?? DEFAULT_MAX_INACTIVE_QUERIES));
68
+ this.now = options.now ?? Date.now;
53
69
  }
54
70
 
55
71
  register(source: NewsCapability): () => void {
@@ -89,29 +105,24 @@ export class NewsService {
89
105
  watchQuery(query: NewsQuery, listener: NewsQueryListener): () => void {
90
106
  const normalized = normalizeNewsQuery(query);
91
107
  const key = buildNewsQueryKey(normalized);
92
- const currentWatch = this.watchedQueries.get(key);
93
- this.watchedQueries.set(key, {
94
- query: normalized,
95
- refs: (currentWatch?.refs ?? 0) + 1,
96
- });
108
+ const entry = this.getOrCreateQueryEntry(normalized);
109
+ entry.refs++;
97
110
 
98
- const emit = () => listener(this.queryStates.get(key) ?? createIdleNewsQueryState());
111
+ const emit = () => listener(this.queries.get(key)?.state ?? createIdleNewsQueryState());
99
112
  const unsubscribe = this.subscribe(emit);
100
113
  emit();
101
- void this.refreshQuery(normalized, false, { track: false });
114
+ void this.refreshQuery(normalized, false);
102
115
 
103
116
  let disposed = false;
104
117
  return () => {
105
118
  if (disposed) return;
106
119
  disposed = true;
107
120
  unsubscribe();
108
- const watch = this.watchedQueries.get(key);
109
- if (!watch) return;
110
- if (watch.refs <= 1) {
111
- this.watchedQueries.delete(key);
112
- } else {
113
- this.watchedQueries.set(key, { query: watch.query, refs: watch.refs - 1 });
114
- }
121
+ const current = this.queries.get(key);
122
+ if (!current) return;
123
+ current.refs = Math.max(0, current.refs - 1);
124
+ current.lastAccessedAt = this.now();
125
+ this.pruneInactiveQueries();
115
126
  };
116
127
  }
117
128
 
@@ -128,9 +139,7 @@ export class NewsService {
128
139
 
129
140
  getQueryState(query: NewsQuery): NewsQueryState {
130
141
  const normalized = normalizeNewsQuery(query);
131
- const key = buildNewsQueryKey(normalized);
132
- this.queryByKey.set(key, normalized);
133
- return this.queryStates.get(key) ?? createIdleNewsQueryState();
142
+ return this.getOrCreateQueryEntry(normalized).state;
134
143
  }
135
144
 
136
145
  async load(query: NewsQuery): Promise<NewsQueryState> {
@@ -160,36 +169,28 @@ export class NewsService {
160
169
  }
161
170
 
162
171
  private async pollActiveQueries(): Promise<void> {
163
- const queries = new Map<string, NewsQuery>();
164
- for (const query of this.queryByKey.values()) {
165
- queries.set(buildNewsQueryKey(query), query);
166
- }
167
- for (const [key, watch] of this.watchedQueries) {
168
- queries.set(key, watch.query);
169
- }
170
- if (queries.size === 0) return;
171
- await Promise.allSettled([...queries.values()].map((query) => this.refreshQuery(query, false, { track: false })));
172
+ this.pruneInactiveQueries();
173
+ const queries = [...this.queries.values()]
174
+ .filter((entry) => entry.refs > 0)
175
+ .map((entry) => entry.query);
176
+ if (queries.length === 0) return;
177
+ await Promise.allSettled(queries.map((query) => this.refreshQuery(query, false)));
172
178
  }
173
179
 
174
180
  private async refreshQuery(
175
181
  query: NewsQuery,
176
182
  showLoading: boolean,
177
- options: { track?: boolean } = {},
178
183
  ): Promise<NewsQueryState> {
179
- const key = buildNewsQueryKey(query);
180
- if (options.track !== false) {
181
- this.queryByKey.set(key, query);
182
- }
183
- const existingFlight = this.inFlight.get(key);
184
- if (existingFlight) return existingFlight;
184
+ const entry = this.getOrCreateQueryEntry(query);
185
+ if (entry.inFlight) return entry.inFlight;
185
186
 
186
- const current = this.queryStates.get(key) ?? createIdleNewsQueryState();
187
+ const current = entry.state;
187
188
  if (showLoading) {
188
- this.queryStates.set(key, {
189
+ entry.state = {
189
190
  ...current,
190
191
  phase: current.articles.length > 0 ? "refreshing" : "loading",
191
192
  error: null,
192
- });
193
+ };
193
194
  this.notify();
194
195
  }
195
196
 
@@ -201,10 +202,11 @@ export class NewsService {
201
202
  phase: "ready",
202
203
  articles,
203
204
  error: null,
204
- updatedAt: Date.now(),
205
+ updatedAt: this.now(),
205
206
  sourceIds: result.sourceIds,
206
207
  };
207
- this.queryStates.set(key, state);
208
+ entry.state = state;
209
+ entry.lastAccessedAt = this.now();
208
210
  this.rebuildArticlePool();
209
211
  this.notify();
210
212
  return state;
@@ -214,18 +216,60 @@ export class NewsService {
214
216
  phase: "error",
215
217
  error: error instanceof Error ? error.message : String(error),
216
218
  };
217
- this.queryStates.set(key, state);
219
+ entry.state = state;
220
+ entry.lastAccessedAt = this.now();
218
221
  this.notify();
219
222
  return state;
220
223
  } finally {
221
- this.inFlight.delete(key);
224
+ entry.inFlight = null;
225
+ this.pruneInactiveQueries();
222
226
  }
223
227
  })();
224
228
 
225
- this.inFlight.set(key, promise);
229
+ entry.inFlight = promise;
226
230
  return promise;
227
231
  }
228
232
 
233
+ private getOrCreateQueryEntry(query: NewsQuery): NewsQueryEntry {
234
+ const key = buildNewsQueryKey(query);
235
+ const now = this.now();
236
+ this.pruneInactiveQueries(now);
237
+ const existing = this.queries.get(key);
238
+ if (existing) {
239
+ existing.lastAccessedAt = now;
240
+ return existing;
241
+ }
242
+ const entry: NewsQueryEntry = {
243
+ query,
244
+ state: createIdleNewsQueryState(),
245
+ inFlight: null,
246
+ refs: 0,
247
+ lastAccessedAt: now,
248
+ };
249
+ this.queries.set(key, entry);
250
+ this.pruneInactiveQueries(now);
251
+ return entry;
252
+ }
253
+
254
+ private pruneInactiveQueries(now = this.now()): void {
255
+ const inactive = [...this.queries.entries()]
256
+ .filter(([, entry]) => entry.refs === 0 && entry.inFlight === null)
257
+ .sort((left, right) => right[1].lastAccessedAt - left[1].lastAccessedAt);
258
+
259
+ let retained = 0;
260
+ let changed = false;
261
+ for (const [key, entry] of inactive) {
262
+ const expired = now - entry.lastAccessedAt >= this.inactiveQueryTtlMs;
263
+ if (expired || retained >= this.maxInactiveQueries) {
264
+ this.queries.delete(key);
265
+ changed = true;
266
+ } else {
267
+ retained++;
268
+ }
269
+ }
270
+ if (changed) this.rebuildArticlePool();
271
+ }
272
+
229
273
  private enabledSources(query: NewsQuery): NewsCapability[] {
230
274
  return [...this.sources.values()]
231
275
  .filter((source) => source.isEnabled?.() !== false)
@@ -277,7 +321,7 @@ export class NewsService {
277
321
 
278
322
  private seedCachedSource(source: NewsCapability): void {
279
323
  const news = source.provider;
280
- const queries = [...this.queryByKey.values()];
324
+ const queries = [...this.queries.values()].map((entry) => entry.query);
281
325
  if (queries.length === 0) queries.push(DEFAULT_GLOBAL_QUERY);
282
326
 
283
327
  let changed = false;
@@ -286,15 +330,14 @@ export class NewsService {
286
330
  const cached = (news.getCachedNews?.(query) ?? [])
287
331
  .map((article) => markDetailCapableArticle(source, article));
288
332
  if (cached.length === 0) continue;
289
- const key = buildNewsQueryKey(query);
290
- const current = this.queryStates.get(key) ?? createIdleNewsQueryState();
291
- this.queryStates.set(key, {
333
+ const entry = this.getOrCreateQueryEntry(query);
334
+ entry.state = {
292
335
  phase: "ready",
293
- articles: filterNewsArticlesForQuery(dedupeNewsArticles([...current.articles, ...cached]), query),
336
+ articles: filterNewsArticlesForQuery(dedupeNewsArticles([...entry.state.articles, ...cached]), query),
294
337
  error: null,
295
- updatedAt: Date.now(),
296
- sourceIds: [...new Set([...current.sourceIds, newsCapabilitySourceId(source)])],
297
- });
338
+ updatedAt: this.now(),
339
+ sourceIds: [...new Set([...entry.state.sourceIds, newsCapabilitySourceId(source)])],
340
+ };
298
341
  changed = true;
299
342
  }
300
343
  if (changed) {
@@ -304,21 +347,21 @@ export class NewsService {
304
347
  }
305
348
 
306
349
  private rebuildArticlePool(): void {
307
- this.articles = dedupeNewsArticles([...this.queryStates.values()].flatMap((state) => state.articles));
350
+ this.articles = dedupeNewsArticles([...this.queries.values()].flatMap((entry) => entry.state.articles));
308
351
  }
309
352
 
310
353
  private mergeStoryDetail(article: NewsArticle): void {
311
354
  let changed = false;
312
- for (const [key, state] of this.queryStates) {
355
+ for (const entry of this.queries.values()) {
313
356
  let stateChanged = false;
314
- const nextArticles = state.articles.map((existing) => {
357
+ const nextArticles = entry.state.articles.map((existing) => {
315
358
  if (existing.id !== article.id) return existing;
316
359
  stateChanged = true;
317
360
  changed = true;
318
361
  return mergeNewsArticle(existing, article);
319
362
  });
320
363
  if (stateChanged) {
321
- this.queryStates.set(key, { ...state, articles: nextArticles });
364
+ entry.state = { ...entry.state, articles: nextArticles };
322
365
  }
323
366
  }
324
367
 
@@ -0,0 +1,64 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { act, useState } from "react";
3
+ import { testRender } from "../renderers/opentui/test-utils";
4
+ import type { NewsService } from "./aggregator";
5
+ import { setSharedNewsService, useNewsArticles } from "./hooks";
6
+ import type { NewsQueryState } from "./types";
7
+
8
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
9
+ let rerenderHarness: (() => void) | null = null;
10
+
11
+ function NewsHookHarness() {
12
+ const [renderCount, setRenderCount] = useState(0);
13
+ rerenderHarness = () => setRenderCount((current) => current + 1);
14
+ const state = useNewsArticles({ feed: "latest", limit: 20 });
15
+ return <text>{state.phase}:{renderCount}</text>;
16
+ }
17
+
18
+ afterEach(async () => {
19
+ if (testSetup) {
20
+ await act(async () => testSetup?.renderer.destroy());
21
+ testSetup = undefined;
22
+ }
23
+ rerenderHarness = null;
24
+ setSharedNewsService(null);
25
+ });
26
+
27
+ test("useNewsArticles watches once and unwatches on unmount", async () => {
28
+ let watchCount = 0;
29
+ let unwatchCount = 0;
30
+ const readyState: NewsQueryState = {
31
+ phase: "ready",
32
+ articles: [],
33
+ error: null,
34
+ updatedAt: 1,
35
+ sourceIds: [],
36
+ };
37
+ const service = {
38
+ getQueryState: () => readyState,
39
+ watchQuery: () => {
40
+ watchCount++;
41
+ return () => {
42
+ unwatchCount++;
43
+ };
44
+ },
45
+ } as unknown as NewsService;
46
+ setSharedNewsService(service);
47
+
48
+ testSetup = await testRender(<NewsHookHarness />, { width: 20, height: 1 });
49
+ await act(async () => testSetup?.renderOnce());
50
+
51
+ expect(watchCount).toBe(1);
52
+ expect(unwatchCount).toBe(0);
53
+
54
+ await act(async () => {
55
+ rerenderHarness?.();
56
+ await Promise.resolve();
57
+ await testSetup?.renderOnce();
58
+ });
59
+ expect(watchCount).toBe(1);
60
+
61
+ await act(async () => testSetup?.renderer.destroy());
62
+ testSetup = undefined;
63
+ expect(unwatchCount).toBe(1);
64
+ });
package/src/news/hooks.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useSyncExternalStore } from "react";
1
+ import { useCallback, useSyncExternalStore } from "react";
2
2
  import { buildNewsQueryKey, type NewsService } from "./aggregator";
3
3
  import type { NewsArticle, NewsQuery, NewsQueryState } from "./types";
4
4
 
@@ -8,34 +8,27 @@ export function setSharedNewsService(service: NewsService | null): void {
8
8
  sharedService = service;
9
9
  }
10
10
 
11
- function idleState(): NewsQueryState {
12
- return {
13
- phase: "idle",
14
- articles: [],
15
- error: null,
16
- updatedAt: null,
17
- sourceIds: [],
18
- };
19
- }
20
-
21
- function useNewsServiceVersion(): number {
22
- if (!sharedService) return 0;
23
- return useSyncExternalStore(
24
- (cb) => sharedService!.subscribe(cb),
25
- () => sharedService!.getVersion(),
26
- );
27
- }
11
+ const IDLE_NEWS_QUERY_STATE: NewsQueryState = {
12
+ phase: "idle",
13
+ articles: [],
14
+ error: null,
15
+ updatedAt: null,
16
+ sourceIds: [],
17
+ };
28
18
 
29
19
  export function useNewsArticles(query: NewsQuery | null | undefined): NewsQueryState {
20
+ const service = sharedService;
30
21
  const key = query ? buildNewsQueryKey(query) : null;
31
- useNewsServiceVersion();
32
-
33
- useEffect(() => {
34
- if (!query || !sharedService) return;
35
- void sharedService.load(query);
36
- }, [key]);
22
+ const subscribe = useCallback((listener: () => void) => {
23
+ if (!query || !service) return () => {};
24
+ return service.watchQuery(query, () => listener());
25
+ }, [key, service]);
26
+ const getSnapshot = useCallback(
27
+ () => query && service ? service.getQueryState(query) : IDLE_NEWS_QUERY_STATE,
28
+ [key, service],
29
+ );
37
30
 
38
- return query && sharedService ? sharedService.getQueryState(query) : idleState();
31
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
39
32
  }
40
33
 
41
34
  export function useLoadNewsStory(): (storyId: string) => Promise<NewsArticle | null> {
@@ -3,6 +3,7 @@ import { usePaneFooter, type PaneHint } from "../../../components";
3
3
  import type { AccountProfile } from "../../../api-client";
4
4
  import { formatPlan, type AccountDraft } from "./model";
5
5
  import { t } from "../../../i18n";
6
+ import { useAppLanguage } from "../../../i18n/react";
6
7
 
7
8
  export function useAccountManagementFooter({
8
9
  busy,
@@ -19,9 +20,10 @@ export function useAccountManagementFooter({
19
20
  profile: AccountProfile | null;
20
21
  saveProfile: () => Promise<void>;
21
22
  }) {
23
+ const language = useAppLanguage();
22
24
  const footerHints = useMemo<PaneHint[]>(() => [
23
25
  { id: "save", key: "Ctrl+S", label: t("save"), onPress: () => { void saveProfile(); }, disabled: !!busy || !hasSession },
24
- ], [busy, hasSession, saveProfile]);
26
+ ], [busy, hasSession, language, saveProfile]);
25
27
 
26
28
  usePaneFooter("account-management", () => ({
27
29
  info: [
@@ -33,5 +35,5 @@ export function useAccountManagementFooter({
33
35
  ] : []),
34
36
  ],
35
37
  hints: footerHints,
36
- }), [draft.profilePublic, footerHints, message, profile]);
38
+ }), [draft.profilePublic, footerHints, language, message, profile]);
37
39
  }
@@ -48,15 +48,15 @@ import { useCloudSyncStatus } from "../../../sync/react";
48
48
  import { cloudSyncController } from "../../../sync/controller";
49
49
  import { setSyncedProfileAnalytics } from "../../../sync/profile-analytics";
50
50
  import { t } from "../../../i18n";
51
+ import { useAppLanguage } from "../../../i18n/react";
51
52
  import {
52
53
  consumeRequestedAccountManagementTab,
53
54
  subscribeRequestedAccountManagementTab,
54
55
  type AccountManagementTab,
55
56
  } from "./navigation";
57
+ import { CLOUD_UPGRADE_URL } from "../shared/cloud-upgrade";
56
58
 
57
59
  type AccountBusy = "profile" | "password" | "alerts" | "billing" | "delete" | null;
58
- const CLOUD_UPGRADE_URL = "https://gloom.sh/cloud?upgrade=pro";
59
-
60
60
  const ACCOUNT_TAB_DEFS: Array<{ label: string; value: AccountManagementTab }> = [
61
61
  { label: "Profile", value: "profile" },
62
62
  { label: "Emails", value: "emails" },
@@ -88,7 +88,8 @@ const ACCOUNT_TAB_FIELD_ORDER: Record<AccountManagementTab, AccountFieldKey[]> =
88
88
  };
89
89
 
90
90
  const PLAN_COMPARISON_ROWS = [
91
- { capability: "Market data", free: "Delayed", pro: "Real-time", proTone: "positive" },
91
+ { capability: "US equities", free: "15m delay", pro: "Real-time", proTone: "positive" },
92
+ { capability: "Options data", free: "15m delay", pro: "Real-time", proTone: "positive" },
92
93
  { capability: "News", free: "12h delay", pro: "Real-time wire", proTone: "positive" },
93
94
  { capability: "Cloud sync", free: "Included", pro: "Included", proTone: "neutral" },
94
95
  { capability: "X data", free: "No", pro: "Included", proTone: "positive" },
@@ -286,6 +287,7 @@ function PlanComparison({
286
287
  }
287
288
 
288
289
  export function AccountManagementPane({ focused, width, height }: PaneProps) {
290
+ const language = useAppLanguage();
289
291
  const dialog = useDialog();
290
292
  const renderer = useRendererHost();
291
293
  const isDesktop = useUiHost().kind === "desktop-web";
@@ -335,7 +337,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
335
337
  const portfolioHoldingCounts = useMemo(() => countPortfolioHoldings(tickers), [tickers]);
336
338
  const portfolioChoices = useMemo(
337
339
  () => buildPortfolioChoices(portfolios, portfolioHoldingCounts),
338
- [portfolioHoldingCounts, portfolios],
340
+ [language, portfolioHoldingCounts, portfolios],
339
341
  );
340
342
  const selectedAnalyticsPortfolio = useMemo(
341
343
  () => portfolios.find((portfolio) => portfolio.id === draft.sharedPortfolioId) ?? null,
@@ -419,6 +421,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
419
421
  [
420
422
  beta,
421
423
  draft.sharedPortfolioId,
424
+ language,
422
425
  portfolioTickers,
423
426
  selectedAnalyticsPortfolio,
424
427
  oneYearReturn,
@@ -448,6 +451,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
448
451
  [
449
452
  draft.profilePublic,
450
453
  draft.sharedPortfolioId,
454
+ language,
451
455
  profile,
452
456
  selectedAnalyticsPortfolio,
453
457
  syncStatus.phase,
@@ -469,6 +473,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
469
473
  }, [
470
474
  draft.profilePublic,
471
475
  draft.sharedPortfolioId,
476
+ language,
472
477
  profile,
473
478
  syncStatus.phase,
474
479
  ]);
@@ -52,18 +52,20 @@ function optionalString(value: unknown): string | undefined {
52
52
  function normalizeAssistantContent(value: unknown): AiAgentAssistantContent | null {
53
53
  if (!isRecord(value)) return null;
54
54
  if (value.type === "text" && typeof value.text === "string") {
55
+ const textSignature = optionalString(value.textSignature);
55
56
  return {
56
57
  type: "text",
57
58
  text: value.text,
58
- ...(optionalString(value.textSignature) ? { textSignature: value.textSignature } : {}),
59
+ ...(textSignature ? { textSignature } : {}),
59
60
  };
60
61
  }
61
62
  if (value.type === "thinking" && typeof value.thinking === "string") {
63
+ const thinkingSignature = optionalString(value.thinkingSignature);
62
64
  return {
63
65
  type: "thinking",
64
66
  thinking: value.thinking,
65
- ...(optionalString(value.thinkingSignature)
66
- ? { thinkingSignature: value.thinkingSignature }
67
+ ...(thinkingSignature
68
+ ? { thinkingSignature }
67
69
  : {}),
68
70
  ...(typeof value.redacted === "boolean" ? { redacted: value.redacted } : {}),
69
71
  };
@@ -74,13 +76,14 @@ function normalizeAssistantContent(value: unknown): AiAgentAssistantContent | nu
74
76
  && typeof value.name === "string"
75
77
  && isRecord(value.arguments)
76
78
  ) {
79
+ const thoughtSignature = optionalString(value.thoughtSignature);
77
80
  return {
78
81
  type: "toolCall",
79
82
  id: value.id,
80
83
  name: value.name,
81
84
  arguments: { ...value.arguments },
82
- ...(optionalString(value.thoughtSignature)
83
- ? { thoughtSignature: value.thoughtSignature }
85
+ ...(thoughtSignature
86
+ ? { thoughtSignature }
84
87
  : {}),
85
88
  };
86
89
  }
@@ -91,14 +94,15 @@ function normalizeToolResultContent(value: unknown): AiAgentTextContent | null {
91
94
  if (!isRecord(value) || value.type !== "text" || typeof value.text !== "string") {
92
95
  return null;
93
96
  }
97
+ const textSignature = optionalString(value.textSignature);
94
98
  return {
95
99
  type: "text",
96
100
  text: value.text,
97
- ...(optionalString(value.textSignature) ? { textSignature: value.textSignature } : {}),
101
+ ...(textSignature ? { textSignature } : {}),
98
102
  };
99
103
  }
100
104
 
101
- export function normalizeAiAgentHistoryMessage(value: unknown): AiAgentHistoryMessage | null {
105
+ function normalizeAiAgentHistoryMessage(value: unknown): AiAgentHistoryMessage | null {
102
106
  if (!isRecord(value)) return null;
103
107
  if (value.role === "user" && typeof value.content === "string") {
104
108
  return { role: "user", content: value.content };
@@ -16,8 +16,6 @@ import { buildTickerAiContext } from "./ticker-context";
16
16
  import {
17
17
  migrateLegacyAiProviderId,
18
18
  resolveDefaultAiProviderId,
19
- __setDetectedProvidersForTests,
20
- type AiProvider,
21
19
  } from "./providers";
22
20
  import { runAiPrompt, type AiConversationMessage } from "./runner";
23
21
  import { isAiProviderReady, resolveReadyAiRunnerDefault } from "./runner-selection";
@@ -47,9 +45,6 @@ const LEGACY_HISTORY_PROVIDER_IDS: Readonly<Record<string, string>> = {
47
45
  google: "gemini",
48
46
  };
49
47
 
50
- export { __setDetectedProvidersForTests };
51
- export type { AiProvider };
52
-
53
48
  function sameMessages(left: readonly ChatMessage[], right: readonly ChatMessage[]): boolean {
54
49
  return left.length === right.length && left.every((message, index) => {
55
50
  const other = right[index];
@@ -12,9 +12,8 @@ import {
12
12
  AskAiResearchTab as AskAiTab,
13
13
  __resetAskAiHistoryForTests,
14
14
  __setAskAiHistoryForTests,
15
- __setDetectedProvidersForTests,
16
- type AiProvider,
17
15
  } from "./ask-ai-detail-tab";
16
+ import { setDetectedProviders, type AiProvider } from "./providers";
18
17
  import { setAiRunHost, setAiRuntimeCatalog, type AiRunHost } from "./runner";
19
18
  import type { TickerRecord } from "../../../types/ticker";
20
19
 
@@ -93,7 +92,7 @@ async function flushFrame() {
93
92
  }
94
93
 
95
94
  function setProviders(providers: AiProvider[]) {
96
- __setDetectedProvidersForTests(providers);
95
+ setDetectedProviders(providers);
97
96
  }
98
97
 
99
98
  function readyProvider(id: AiProvider["id"] = "anthropic"): AiProvider {
@@ -113,7 +112,7 @@ afterEach(async () => {
113
112
  });
114
113
  testSetup = undefined;
115
114
  }
116
- __setDetectedProvidersForTests(null);
115
+ setDetectedProviders(null);
117
116
  __resetAskAiHistoryForTests();
118
117
  setAiRunHost(null);
119
118
  setAiRuntimeCatalog({ providers: [], accounts: [], models: [] });
@@ -5,7 +5,7 @@ import { aiPlugin } from "./index";
5
5
  import {
6
6
  AI_PROVIDER_IDS,
7
7
  getAiProviderDefinition,
8
- __setDetectedProvidersForTests,
8
+ setDetectedProviders,
9
9
  type AiProviderId,
10
10
  } from "./providers";
11
11
  import {
@@ -109,7 +109,7 @@ function setupPlugin(config = createDefaultConfig("/tmp/gloomberb-ai-plugin")) {
109
109
  }
110
110
 
111
111
  afterEach(() => {
112
- __setDetectedProvidersForTests(null);
112
+ setDetectedProviders(null);
113
113
  setAiRunHost(null);
114
114
  setAiRuntimeCatalog({ providers: [], accounts: [], models: [] });
115
115
  });