gloomberb 0.10.4 → 0.11.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 (555) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +30 -3
  3. package/README.zh-CN.md +1 -1
  4. package/package.json +5 -5
  5. package/src/api-client/auth.ts +34 -0
  6. package/src/api-client/data.ts +25 -0
  7. package/src/api-client/index.test.ts +81 -0
  8. package/src/api-client/index.ts +37 -0
  9. package/src/api-client/request.test.ts +30 -0
  10. package/src/api-client/request.ts +44 -20
  11. package/src/api-client/socket-health.test.ts +79 -0
  12. package/src/api-client/socket.ts +108 -5
  13. package/src/api-client/types.ts +139 -0
  14. package/src/app/global-shortcuts.test.tsx +144 -9
  15. package/src/app/global-shortcuts.ts +33 -11
  16. package/src/app/runtime/broker-import.ts +39 -5
  17. package/src/app/runtime/startup.ts +7 -1
  18. package/src/app/runtime/update.ts +26 -1
  19. package/src/app.tsx +36 -19
  20. package/src/brokers/broker-ticker-sync.ts +3 -8
  21. package/src/brokers/sync-broker-instance.test.ts +59 -0
  22. package/src/brokers/sync-broker-instance.ts +37 -13
  23. package/src/capabilities/chart-series.ts +318 -0
  24. package/src/capabilities/factories.ts +46 -0
  25. package/src/capabilities/index.ts +1 -0
  26. package/src/capabilities/registry.test.ts +135 -0
  27. package/src/capabilities/registry.ts +31 -4
  28. package/src/capabilities/types.ts +52 -0
  29. package/src/cli/commands/overview.ts +2 -2
  30. package/src/cli/desktop-pane-shot.ts +2 -0
  31. package/src/cli/entry.ts +11 -1
  32. package/src/cli/native-smoke.ts +5 -0
  33. package/src/cli/pane-functions/catalog.ts +4 -0
  34. package/src/cli/pane-functions/data.ts +6 -1
  35. package/src/cli/pane-functions/discovery.ts +76 -10
  36. package/src/cli/pane-functions/report.test.ts +76 -0
  37. package/src/cli/pane-functions/report.ts +7 -1
  38. package/src/cli/pane-functions/resolver.ts +4 -1
  39. package/src/cli/pane-functions/screenshot.test.ts +56 -3
  40. package/src/cli/pane-functions/screenshot.ts +68 -5
  41. package/src/components/chart/composite/composite-chart.test.tsx +54 -0
  42. package/src/components/chart/composite/composite-chart.tsx +92 -15
  43. package/src/components/chart/composite/downsample.test.ts +136 -0
  44. package/src/components/chart/composite/downsample.ts +208 -0
  45. package/src/components/chart/composite/panel-series.test.ts +66 -0
  46. package/src/components/chart/composite/panel-series.ts +101 -0
  47. package/src/components/chart/multi-line/renderer.ts +1 -0
  48. package/src/components/chart/native/kitty/index.test.ts +25 -3
  49. package/src/components/chart/native/kitty/support.ts +37 -3
  50. package/src/components/chart/native/use-chart-text-fallback.ts +19 -0
  51. package/src/components/chart/static/chart/surface.tsx +43 -12
  52. package/src/components/chart/static/multi-line-chart-surface.tsx +41 -5
  53. package/src/components/command-bar/commands/direct/index.ts +18 -0
  54. package/src/components/command-bar/commands/registry.ts +14 -0
  55. package/src/components/command-bar/routes/root/article-results.test.ts +83 -0
  56. package/src/components/command-bar/routes/root/article-results.ts +55 -0
  57. package/src/components/command-bar/routes/root/command-items.ts +3 -0
  58. package/src/components/command-bar/routes/root/provider-search.ts +7 -3
  59. package/src/components/command-bar/routes/root/results.ts +38 -11
  60. package/src/components/command-bar/routes/root/runtime.ts +6 -2
  61. package/src/components/command-bar/routes/ticker-search/actions.ts +1 -1
  62. package/src/components/command-bar/routes/ticker-search/results.test.ts +34 -0
  63. package/src/components/command-bar/routes/ticker-search/results.ts +4 -6
  64. package/src/components/command-bar/routes/ticker-search/route.ts +2 -2
  65. package/src/components/command-bar/routing/list-state.ts +2 -1
  66. package/src/components/command-bar/surface/assist.test.tsx +13 -6
  67. package/src/components/command-bar/surface/index.test.tsx +65 -0
  68. package/src/components/command-bar/surface/index.tsx +35 -0
  69. package/src/components/command-bar/view-model.test.ts +43 -0
  70. package/src/components/command-bar/view-model.ts +41 -4
  71. package/src/components/command-bar/workflow/collection-actions.test.ts +94 -0
  72. package/src/components/command-bar/workflow/collection-actions.ts +14 -3
  73. package/src/components/command-bar/workflow/ops.test.ts +136 -0
  74. package/src/components/command-bar/workflow/ops.ts +71 -1
  75. package/src/components/index.ts +1 -1
  76. package/src/components/layout/header.test.tsx +35 -0
  77. package/src/components/layout/header.tsx +101 -9
  78. package/src/components/layout/pane/cycle-order.test.ts +0 -1
  79. package/src/components/layout/pane/footer/index.test.tsx +19 -19
  80. package/src/components/layout/pane/header.tsx +2 -13
  81. package/src/components/layout/shell/index.tsx +3 -0
  82. package/src/components/layout/status-bar.test.tsx +36 -1
  83. package/src/components/layout/status-bar.tsx +48 -2
  84. package/src/components/onboarding/account-step/chooser-panel.tsx +27 -50
  85. package/src/components/onboarding/account-step/form-panel.tsx +85 -29
  86. package/src/components/onboarding/account-step/index.tsx +10 -1
  87. package/src/components/onboarding/account-step/model.ts +3 -3
  88. package/src/components/onboarding/account-step/qr-panel.tsx +68 -0
  89. package/src/components/onboarding/account-step/signed-in-panel.tsx +4 -7
  90. package/src/components/onboarding/onboarding-frame.tsx +517 -0
  91. package/src/components/onboarding/onboarding-steps.tsx +0 -264
  92. package/src/components/onboarding/onboarding-wizard.test.tsx +626 -200
  93. package/src/components/onboarding/onboarding-wizard.tsx +848 -266
  94. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +81 -51
  95. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +14 -20
  96. package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +8 -11
  97. package/src/components/onboarding/portfolio-step/choose-panel.tsx +18 -22
  98. package/src/components/onboarding/portfolio-step/index.tsx +3 -0
  99. package/src/components/onboarding/portfolio-step/types.ts +3 -0
  100. package/src/components/onboarding/wizard-account.ts +38 -3
  101. package/src/components/onboarding/wizard-broker-sync.ts +45 -67
  102. package/src/components/onboarding/wizard-model.ts +18 -2
  103. package/src/components/ui/data-table/opentui/index.tsx +29 -9
  104. package/src/components/ui/data-table/types.ts +2 -0
  105. package/src/components/ui/external-link.tsx +4 -2
  106. package/src/components/ui/loading.tsx +1 -1
  107. package/src/components/ui/status.tsx +76 -4
  108. package/src/components/ui/table-layout.test.ts +24 -0
  109. package/src/components/ui/table-layout.ts +90 -23
  110. package/src/components/ui/tabs.tsx +86 -4
  111. package/src/core/app-services.ts +14 -3
  112. package/src/core/connection-health.test.ts +143 -0
  113. package/src/core/connection-health.ts +295 -0
  114. package/src/core/state/app/layout-reducer.ts +34 -0
  115. package/src/core/state/app/layout.ts +21 -0
  116. package/src/core/state/app/state.ts +10 -0
  117. package/src/core/state/app/types.ts +7 -1
  118. package/src/data/config/chart-settings.ts +11 -2
  119. package/src/data/config/store/index.test.ts +83 -9
  120. package/src/data/config/store/migrations.ts +16 -0
  121. package/src/data/config/store/normalize.ts +59 -1
  122. package/src/data/fred-series.ts +8 -2
  123. package/src/i18n/es.ts +126 -0
  124. package/src/i18n/ja.ts +126 -0
  125. package/src/i18n/ko.ts +126 -0
  126. package/src/i18n/zh-cn.ts +126 -0
  127. package/src/i18n/zh-tw.ts +126 -0
  128. package/src/market-data/coordinator/chart.ts +3 -2
  129. package/src/market-data/hooks.tsx +3 -2
  130. package/src/market-data/market/status.test.ts +12 -0
  131. package/src/market-data/market/status.ts +34 -17
  132. package/src/news/aggregator.test.ts +39 -0
  133. package/src/news/aggregator.ts +78 -22
  134. package/src/news/hooks.ts +4 -0
  135. package/src/notifications/app-notifier.test.ts +28 -6
  136. package/src/notifications/app-notifier.ts +24 -5
  137. package/src/plugins/broker-sync/broker-sync.test.ts +130 -0
  138. package/src/plugins/broker-sync/native-loader.ts +10 -0
  139. package/src/plugins/broker-sync/normalize.ts +271 -0
  140. package/src/plugins/broker-sync/public.ts +122 -0
  141. package/src/plugins/broker-sync/robinhood-native.ts +367 -0
  142. package/src/plugins/broker-sync/robinhood.ts +114 -0
  143. package/src/plugins/broker-sync/simplefin-native.ts +201 -0
  144. package/src/plugins/broker-sync/simplefin.ts +70 -0
  145. package/src/plugins/builtin/account-management/footer.ts +8 -19
  146. package/src/plugins/builtin/account-management/pane.tsx +13 -8
  147. package/src/plugins/builtin/ai/pi/host.ts +6 -5
  148. package/src/plugins/builtin/ai/providers.ts +1 -1
  149. package/src/plugins/builtin/ai/screener/footer.ts +0 -7
  150. package/src/plugins/builtin/ai/screener/pane.test.tsx +2 -1
  151. package/src/plugins/builtin/ai/screener/pane.tsx +3 -1
  152. package/src/plugins/builtin/ai/screener/results-view.tsx +8 -0
  153. package/src/plugins/builtin/ai/screener/runner.ts +7 -3
  154. package/src/plugins/builtin/ai/workspace/pane.tsx +13 -2
  155. package/src/plugins/builtin/alerts/alert-engine.ts +13 -0
  156. package/src/plugins/builtin/alerts/constants.ts +2 -1
  157. package/src/plugins/builtin/alerts/index.tsx +67 -36
  158. package/src/plugins/builtin/alerts/pane.tsx +17 -65
  159. package/src/plugins/builtin/analytics/index.tsx +24 -11
  160. package/src/plugins/builtin/analytics/pane-model.ts +48 -14
  161. package/src/plugins/builtin/analytics/portfolio-selection.ts +19 -0
  162. package/src/plugins/builtin/analytics/view.tsx +5 -3
  163. package/src/plugins/builtin/broker-manager/detail.tsx +7 -2
  164. package/src/plugins/builtin/broker-manager/footer.ts +4 -9
  165. package/src/plugins/builtin/broker-manager/index.tsx +1 -6
  166. package/src/plugins/builtin/buildout/detail/ui.tsx +15 -4
  167. package/src/plugins/builtin/buildout/pane/header.tsx +1 -0
  168. package/src/plugins/builtin/changelog/index.tsx +60 -45
  169. package/src/plugins/builtin/chart-composer/catalog-inventory.test.ts +97 -0
  170. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +366 -0
  171. package/src/plugins/builtin/chart-composer/chart-spec.ts +21 -1
  172. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +307 -0
  173. package/src/plugins/builtin/chart-composer/editor/controller.ts +1 -1
  174. package/src/plugins/builtin/chart-composer/index.tsx +45 -5
  175. package/src/plugins/builtin/chart-composer/pane.tsx +7 -88
  176. package/src/plugins/builtin/chart-composer/presets.test.ts +48 -0
  177. package/src/plugins/builtin/chart-composer/presets.ts +69 -3
  178. package/src/plugins/builtin/chart-composer/quick-add.tsx +8 -5
  179. package/src/plugins/builtin/chart-composer/semantic.ts +18 -14
  180. package/src/plugins/builtin/chart-composer/series-catalog.test.ts +39 -0
  181. package/src/plugins/builtin/chart-composer/series-catalog.ts +104 -3
  182. package/src/plugins/builtin/chart-composer/shortcuts.test.ts +10 -10
  183. package/src/plugins/builtin/chart-composer/shortcuts.ts +4 -12
  184. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +230 -23
  185. package/src/plugins/builtin/chat/chat.test.tsx +22 -0
  186. package/src/plugins/builtin/chat/content/composer.tsx +6 -4
  187. package/src/plugins/builtin/chat/content/index.tsx +7 -0
  188. package/src/plugins/builtin/chat/content/snapshot.ts +9 -2
  189. package/src/plugins/builtin/chat/content/transcript.tsx +10 -0
  190. package/src/plugins/builtin/chat/controller/channel-actions.ts +11 -1
  191. package/src/plugins/builtin/chat/controller/channels.ts +13 -3
  192. package/src/plugins/builtin/chat/controller/index.ts +61 -7
  193. package/src/plugins/builtin/chat/controller/lifecycle.ts +3 -0
  194. package/src/plugins/builtin/chat/controller/message-loading.ts +5 -1
  195. package/src/plugins/builtin/chat/controller/messages.ts +3 -1
  196. package/src/plugins/builtin/chat/controller/notifications.ts +32 -11
  197. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  198. package/src/plugins/builtin/chat/controller/view.ts +1 -0
  199. package/src/plugins/builtin/chat/controller.test.ts +117 -28
  200. package/src/plugins/builtin/cloud/auth-commands.ts +18 -0
  201. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +243 -0
  202. package/src/plugins/builtin/cloud/device-signin.test.ts +217 -0
  203. package/src/plugins/builtin/cloud/device-signin.ts +240 -0
  204. package/src/plugins/builtin/cloud/plugin.tsx +7 -1
  205. package/src/plugins/builtin/cloud-tweets/table.tsx +57 -10
  206. package/src/plugins/builtin/composite-plugins.ts +39 -5
  207. package/src/plugins/builtin/congress-trades/detail.tsx +2 -4
  208. package/src/plugins/builtin/congress-trades/footer.ts +7 -16
  209. package/src/plugins/builtin/congress-trades/pane.tsx +12 -20
  210. package/src/plugins/builtin/connections/index.ts +53 -0
  211. package/src/plugins/builtin/connections/pane.test.tsx +79 -0
  212. package/src/plugins/builtin/connections/pane.tsx +258 -0
  213. package/src/plugins/builtin/correlation/index.test.tsx +2 -1
  214. package/src/plugins/builtin/correlation/index.tsx +58 -19
  215. package/src/plugins/builtin/correlation/matrix/model.ts +18 -7
  216. package/src/plugins/builtin/correlation/relationship/pane.tsx +43 -19
  217. package/src/plugins/builtin/correlation/settings.ts +1 -1
  218. package/src/plugins/builtin/credit-conditions/client.test.ts +80 -0
  219. package/src/plugins/builtin/credit-conditions/client.ts +105 -0
  220. package/src/plugins/builtin/credit-conditions/index.test.tsx +111 -0
  221. package/src/plugins/builtin/credit-conditions/index.tsx +227 -0
  222. package/src/plugins/builtin/credit-conditions/model.test.ts +43 -0
  223. package/src/plugins/builtin/credit-conditions/model.ts +64 -0
  224. package/src/plugins/builtin/debug/index.tsx +11 -6
  225. package/src/plugins/builtin/dividend-yield/client.test.ts +58 -0
  226. package/src/plugins/builtin/dividend-yield/client.ts +252 -0
  227. package/src/plugins/builtin/dividend-yield/index.tsx +61 -0
  228. package/src/plugins/builtin/dividend-yield/model.ts +83 -0
  229. package/src/plugins/builtin/dividend-yield/pane.tsx +305 -0
  230. package/src/plugins/builtin/dividend-yield/types.ts +22 -0
  231. package/src/plugins/builtin/earnings/data/cache.test.ts +10 -7
  232. package/src/plugins/builtin/earnings/data/cache.ts +23 -9
  233. package/src/plugins/builtin/earnings/index.tsx +57 -6
  234. package/src/plugins/builtin/econ/calendar-model.test.ts +37 -3
  235. package/src/plugins/builtin/econ/calendar-model.ts +32 -26
  236. package/src/plugins/builtin/econ/fred-cache.test.ts +8 -1
  237. package/src/plugins/builtin/econ/fred-series-map.ts +18 -0
  238. package/src/plugins/builtin/econ/index.test.tsx +105 -0
  239. package/src/plugins/builtin/econ/index.tsx +95 -52
  240. package/src/plugins/builtin/fear-greed/cache.test.ts +18 -3
  241. package/src/plugins/builtin/fear-greed/cache.ts +18 -6
  242. package/src/plugins/builtin/fear-greed/charts.tsx +9 -2
  243. package/src/plugins/builtin/fear-greed/format.ts +0 -9
  244. package/src/plugins/builtin/fear-greed/pane.tsx +21 -21
  245. package/src/plugins/builtin/futures/contracts.ts +100 -0
  246. package/src/plugins/builtin/futures/index.tsx +298 -0
  247. package/src/plugins/builtin/futures/model.test.ts +129 -0
  248. package/src/plugins/builtin/futures/model.ts +136 -0
  249. package/src/plugins/builtin/futures/pane.test.tsx +239 -0
  250. package/src/plugins/builtin/futures/table.test.ts +155 -0
  251. package/src/plugins/builtin/futures/table.tsx +241 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +160 -134
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +12 -1
  254. package/src/plugins/builtin/fx-matrix/pane.test.tsx +109 -0
  255. package/src/plugins/builtin/help/components.tsx +1 -30
  256. package/src/plugins/builtin/help/index.tsx +5 -25
  257. package/src/plugins/builtin/holders/pane.tsx +17 -12
  258. package/src/plugins/builtin/holders/treemap.tsx +4 -2
  259. package/src/plugins/builtin/insider/index.tsx +20 -10
  260. package/src/plugins/builtin/ipo-calendar/cache.ts +101 -0
  261. package/src/plugins/builtin/ipo-calendar/client.test.ts +31 -0
  262. package/src/plugins/builtin/ipo-calendar/client.ts +296 -0
  263. package/src/plugins/builtin/ipo-calendar/index.tsx +65 -0
  264. package/src/plugins/builtin/ipo-calendar/model.ts +145 -0
  265. package/src/plugins/builtin/ipo-calendar/pane.tsx +328 -0
  266. package/src/plugins/builtin/ipo-calendar/types.ts +19 -0
  267. package/src/plugins/builtin/kelly-sizer/index.test.tsx +3 -3
  268. package/src/plugins/builtin/kelly-sizer/model.ts +8 -1
  269. package/src/plugins/builtin/kelly-sizer/pane.tsx +33 -15
  270. package/src/plugins/builtin/kelly-sizer/view.tsx +12 -5
  271. package/src/plugins/builtin/market-halts/client.ts +114 -0
  272. package/src/plugins/builtin/market-halts/index.tsx +29 -0
  273. package/src/plugins/builtin/market-halts/model.test.ts +187 -0
  274. package/src/plugins/builtin/market-halts/model.ts +291 -0
  275. package/src/plugins/builtin/market-halts/pane.tsx +256 -0
  276. package/src/plugins/builtin/market-heatmap/data.test.ts +1 -0
  277. package/src/plugins/builtin/market-heatmap/data.ts +12 -3
  278. package/src/plugins/builtin/market-heatmap/index.tsx +131 -46
  279. package/src/plugins/builtin/market-movers/index.tsx +145 -63
  280. package/src/plugins/builtin/market-movers/model.ts +19 -3
  281. package/src/plugins/builtin/market-movers/screener.test.ts +108 -0
  282. package/src/plugins/builtin/market-movers/screener.ts +168 -10
  283. package/src/plugins/builtin/news/index.tsx +78 -99
  284. package/src/plugins/builtin/news/wire/article-search.test.ts +72 -0
  285. package/src/plugins/builtin/news/wire/article-search.ts +140 -0
  286. package/src/plugins/builtin/news/wire/breaking/pane.tsx +18 -8
  287. package/src/plugins/builtin/news/wire/categories.ts +35 -0
  288. package/src/plugins/builtin/news/wire/default-feeds.ts +47 -1
  289. package/src/plugins/builtin/news/wire/feed-config.test.ts +10 -5
  290. package/src/plugins/builtin/news/wire/index.tsx +41 -4
  291. package/src/plugins/builtin/news/wire/industry-pane.tsx +38 -19
  292. package/src/plugins/builtin/news/wire/news/detail-view.tsx +10 -5
  293. package/src/plugins/builtin/news/wire/news/footer.ts +1 -0
  294. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +15 -6
  295. package/src/plugins/builtin/news/wire/news/query-presets.ts +16 -2
  296. package/src/plugins/builtin/news/wire/news/table.test.tsx +54 -0
  297. package/src/plugins/builtin/news/wire/news/table.tsx +111 -17
  298. package/src/plugins/builtin/notes/files.test.ts +18 -0
  299. package/src/plugins/builtin/notes/files.ts +10 -4
  300. package/src/plugins/builtin/notes/index.tsx +4 -1
  301. package/src/plugins/builtin/notes/quick-notes-pane.test.tsx +182 -0
  302. package/src/plugins/builtin/notes/quick-notes-pane.tsx +74 -31
  303. package/src/plugins/builtin/notes/ticker-notes-tab.test.tsx +380 -0
  304. package/src/plugins/builtin/notes/ticker-notes-tab.tsx +97 -40
  305. package/src/plugins/builtin/options/calc-seed.test.ts +118 -0
  306. package/src/plugins/builtin/options/calc-seed.ts +52 -0
  307. package/src/plugins/builtin/options/footer.ts +4 -1
  308. package/src/plugins/builtin/options/index.tsx +22 -1
  309. package/src/plugins/builtin/options/live-quotes.ts +8 -0
  310. package/src/plugins/builtin/options/table.ts +3 -1
  311. package/src/plugins/builtin/options/view.test.tsx +5 -1
  312. package/src/plugins/builtin/options/view.tsx +112 -17
  313. package/src/plugins/builtin/options-calculator/index.tsx +35 -0
  314. package/src/plugins/builtin/options-calculator/model.test.ts +172 -0
  315. package/src/plugins/builtin/options-calculator/model.ts +273 -0
  316. package/src/plugins/builtin/options-calculator/pane.test.tsx +138 -0
  317. package/src/plugins/builtin/options-calculator/pane.tsx +218 -0
  318. package/src/plugins/builtin/plugin-module.test.ts +16 -2
  319. package/src/plugins/builtin/plugin-module.ts +5 -1
  320. package/src/plugins/builtin/polls/client.ts +57 -0
  321. package/src/plugins/builtin/polls/hooks.ts +20 -0
  322. package/src/plugins/builtin/polls/index.tsx +35 -0
  323. package/src/plugins/builtin/polls/normalize.test.ts +230 -0
  324. package/src/plugins/builtin/polls/normalize.ts +319 -0
  325. package/src/plugins/builtin/polls/pane.tsx +722 -0
  326. package/src/plugins/builtin/polls/types.ts +77 -0
  327. package/src/plugins/builtin/portfolio-list/header.tsx +15 -4
  328. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -3
  329. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +1 -1
  330. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -0
  331. package/src/plugins/builtin/portfolio-list/summary/index.tsx +2 -1
  332. package/src/plugins/builtin/research/analyst-pane.tsx +40 -67
  333. package/src/plugins/builtin/research/corporate-actions-pane.tsx +25 -19
  334. package/src/plugins/builtin/research/index.test.ts +6 -25
  335. package/src/plugins/builtin/research/index.tsx +11 -5
  336. package/src/plugins/builtin/research/relative-valuation-pane.tsx +86 -26
  337. package/src/plugins/builtin/scanner/denied.tsx +31 -0
  338. package/src/plugins/builtin/scanner/feed.ts +94 -0
  339. package/src/plugins/builtin/scanner/flow-model.test.ts +85 -0
  340. package/src/plugins/builtin/scanner/flow-model.ts +133 -0
  341. package/src/plugins/builtin/scanner/flow-pane.tsx +197 -0
  342. package/src/plugins/builtin/scanner/hilo-bars.tsx +121 -0
  343. package/src/plugins/builtin/scanner/hilo-model.test.ts +44 -0
  344. package/src/plugins/builtin/scanner/hilo-model.ts +75 -0
  345. package/src/plugins/builtin/scanner/hilo-pane.tsx +165 -0
  346. package/src/plugins/builtin/scanner/index.ts +109 -0
  347. package/src/plugins/builtin/scanner/waiting.tsx +11 -0
  348. package/src/plugins/builtin/sec/index.tsx +11 -7
  349. package/src/plugins/builtin/sectors/index.tsx +105 -81
  350. package/src/plugins/builtin/sectors/move-bar.tsx +43 -0
  351. package/src/plugins/builtin/sectors/sector-data.ts +18 -0
  352. package/src/plugins/builtin/sectors/sector-model.ts +13 -15
  353. package/src/plugins/builtin/shared/auto-refresh.ts +51 -0
  354. package/src/plugins/builtin/shared/cloud-upgrade.ts +29 -5
  355. package/src/plugins/builtin/shared/pane-footer.ts +16 -3
  356. package/src/plugins/builtin/shared/screener-live-quotes.test.ts +91 -0
  357. package/src/plugins/builtin/shared/screener-live-quotes.ts +134 -0
  358. package/src/plugins/builtin/shared/table-pane.ts +1 -5
  359. package/src/plugins/builtin/shared/use-quote-board.test.tsx +211 -0
  360. package/src/plugins/builtin/shared/use-quote-board.ts +195 -0
  361. package/src/plugins/builtin/short-interest/client.ts +124 -0
  362. package/src/plugins/builtin/short-interest/index.tsx +62 -0
  363. package/src/plugins/builtin/short-interest/model.ts +113 -0
  364. package/src/plugins/builtin/short-interest/pane.tsx +252 -0
  365. package/src/plugins/builtin/short-interest/types.ts +9 -0
  366. package/src/plugins/builtin/substack/article-detail.tsx +15 -1
  367. package/src/plugins/builtin/substack/pane-footer.ts +3 -22
  368. package/src/plugins/builtin/substack/pane.tsx +1 -2
  369. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  370. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  371. package/src/plugins/builtin/thirteenf/pane.tsx +6 -2
  372. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +10 -4
  373. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +67 -11
  374. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +15 -8
  375. package/src/plugins/builtin/ticker-detail/index.tsx +5 -21
  376. package/src/plugins/builtin/ticker-detail/overview/components.tsx +72 -32
  377. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -10
  378. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -1
  379. package/src/plugins/builtin/ticker-detail/quote-monitor/card.test.tsx +70 -0
  380. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +34 -26
  381. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +96 -49
  382. package/src/plugins/builtin/ticker-detail/research-tabs.ts +32 -0
  383. package/src/plugins/builtin/ticker-research-backend-plugin.ts +2 -9
  384. package/src/plugins/builtin/ticker-research-plugin.ts +6 -0
  385. package/src/plugins/builtin/treasury-auctions/cache.test.ts +173 -0
  386. package/src/plugins/builtin/treasury-auctions/cache.ts +104 -0
  387. package/src/plugins/builtin/treasury-auctions/client.test.ts +181 -0
  388. package/src/plugins/builtin/treasury-auctions/client.ts +151 -0
  389. package/src/plugins/builtin/treasury-auctions/index.tsx +90 -0
  390. package/src/plugins/builtin/treasury-auctions/model.test.ts +147 -0
  391. package/src/plugins/builtin/treasury-auctions/model.ts +188 -0
  392. package/src/plugins/builtin/treasury-auctions/pane.test.tsx +170 -0
  393. package/src/plugins/builtin/treasury-auctions/pane.tsx +417 -0
  394. package/src/plugins/builtin/treasury-auctions/types.ts +59 -0
  395. package/src/plugins/builtin/tv/pane.tsx +67 -7
  396. package/src/plugins/builtin/tv/youtube-embed.test.ts +84 -0
  397. package/src/plugins/builtin/tv/youtube-embed.ts +173 -0
  398. package/src/plugins/builtin/tv/youtube-stream.ts +27 -2
  399. package/src/plugins/builtin/volatility/client.ts +87 -0
  400. package/src/plugins/builtin/volatility/index.test.tsx +107 -0
  401. package/src/plugins/builtin/volatility/index.tsx +193 -0
  402. package/src/plugins/builtin/volatility/model.test.ts +66 -0
  403. package/src/plugins/builtin/volatility/model.ts +113 -0
  404. package/src/plugins/builtin/world-indices/footer.ts +43 -23
  405. package/src/plugins/builtin/world-indices/index.tsx +44 -93
  406. package/src/plugins/builtin/world-indices/indices.ts +11 -2
  407. package/src/plugins/builtin/world-indices/model.ts +17 -24
  408. package/src/plugins/builtin/world-indices/pane.test.tsx +116 -0
  409. package/src/plugins/builtin/world-indices/table.tsx +82 -37
  410. package/src/plugins/builtin/yield-curve/index.tsx +16 -7
  411. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +25 -0
  412. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -0
  413. package/src/plugins/catalog-backend.ts +8 -0
  414. package/src/plugins/catalog-ui.ts +8 -0
  415. package/src/plugins/event-bus.ts +1 -0
  416. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  417. package/src/plugins/ibkr/trade/tab/footer.ts +9 -23
  418. package/src/plugins/ibkr/trade/tab/index.tsx +1 -5
  419. package/src/plugins/ibkr/trading/pane-view.tsx +149 -87
  420. package/src/plugins/ibkr/trading/pane.tsx +2 -2
  421. package/src/plugins/ownership.ts +3 -0
  422. package/src/plugins/pane-manager/floating.ts +8 -2
  423. package/src/plugins/pane-manager.test.ts +13 -0
  424. package/src/plugins/prediction-markets/backend-plugin.ts +2 -0
  425. package/src/plugins/prediction-markets/capability.test.ts +281 -0
  426. package/src/plugins/prediction-markets/capability.ts +171 -0
  427. package/src/plugins/prediction-markets/chart.tsx +18 -26
  428. package/src/plugins/prediction-markets/columns.ts +3 -2
  429. package/src/plugins/prediction-markets/controller/catalog.ts +15 -12
  430. package/src/plugins/prediction-markets/controller/detail.ts +4 -0
  431. package/src/plugins/prediction-markets/controller/index.ts +0 -15
  432. package/src/plugins/prediction-markets/detail/book.tsx +3 -23
  433. package/src/plugins/prediction-markets/detail/overview.tsx +0 -18
  434. package/src/plugins/prediction-markets/detail/pane.tsx +36 -12
  435. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  436. package/src/plugins/prediction-markets/index.tsx +2 -0
  437. package/src/plugins/prediction-markets/metrics.ts +28 -18
  438. package/src/plugins/prediction-markets/navigation.ts +1 -0
  439. package/src/plugins/prediction-markets/pane.test.tsx +1 -1
  440. package/src/plugins/prediction-markets/pane.tsx +0 -1
  441. package/src/plugins/prediction-markets/services/fetch.ts +3 -2
  442. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +82 -23
  443. package/src/plugins/prediction-markets/services/kalshi/normalize.ts +3 -1
  444. package/src/plugins/prediction-markets/services/kalshi/types.ts +14 -0
  445. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +26 -14
  446. package/src/plugins/prediction-markets/services/polymarket/detail.ts +67 -18
  447. package/src/plugins/prediction-markets/test-helpers.tsx +0 -1
  448. package/src/plugins/prediction-markets/types.ts +3 -9
  449. package/src/plugins/registry/context.ts +6 -1
  450. package/src/plugins/registry/index.test.ts +2 -0
  451. package/src/plugins/registry/index.ts +53 -3
  452. package/src/plugins/runtime/context.tsx +4 -2
  453. package/src/plugins/runtime/index.tsx +9 -0
  454. package/src/remote/controller.test.ts +17 -4
  455. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  456. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -2
  457. package/src/renderers/electrobun/bun/desktop/capability-bridge.test.ts +64 -0
  458. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +41 -7
  459. package/src/renderers/electrobun/bun/desktop/host-requests.ts +5 -2
  460. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +33 -0
  461. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +53 -6
  462. package/src/renderers/electrobun/bun/desktop/initialization.test.ts +27 -0
  463. package/src/renderers/electrobun/bun/desktop/initialization.ts +6 -1
  464. package/src/renderers/electrobun/bun/index.ts +1 -0
  465. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  466. package/src/renderers/electrobun/view/app-services.ts +25 -2
  467. package/src/renderers/electrobun/view/build-assets.ts +5 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +186 -65
  469. package/src/renderers/electrobun/view/cli-pane-shot-health.test.ts +25 -0
  470. package/src/renderers/electrobun/view/cli-pane-shot-health.ts +31 -0
  471. package/src/renderers/electrobun/view/data-table/index.tsx +10 -11
  472. package/src/renderers/electrobun/view/data-table/row.tsx +2 -0
  473. package/src/renderers/electrobun/view/desktop/controls.test.tsx +68 -0
  474. package/src/renderers/electrobun/view/desktop/controls.tsx +25 -1
  475. package/src/renderers/electrobun/view/host/box.test.tsx +80 -0
  476. package/src/renderers/electrobun/view/host/media-surface.tsx +70 -3
  477. package/src/renderers/electrobun/view/host/tabs.tsx +97 -5
  478. package/src/renderers/electrobun/view/host/text.tsx +5 -2
  479. package/src/renderers/electrobun/view/input-host.test.tsx +140 -0
  480. package/src/renderers/electrobun/view/input-host.tsx +4 -2
  481. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +19 -1
  482. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +7 -0
  483. package/src/renderers/electrobun/view/native-stubs/chart/kitty-support.ts +10 -0
  484. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +46 -0
  485. package/src/renderers/electrobun/view/remote/capability-invoker.ts +34 -13
  486. package/src/renderers/electrobun/view/remote/connection-health-backend.ts +19 -0
  487. package/src/renderers/electrobun/view/remote/connection-health.test.ts +44 -0
  488. package/src/renderers/electrobun/view/remote/connection-health.ts +49 -0
  489. package/src/renderers/electrobun/view/styles.css +5 -0
  490. package/src/renderers/opentui/dialog-host.tsx +279 -80
  491. package/src/renderers/opentui/host.tsx +15 -1
  492. package/src/renderers/opentui/terminal-media.test.ts +102 -0
  493. package/src/renderers/opentui/terminal-media.ts +142 -0
  494. package/src/renderers/opentui/test-utils.tsx +9 -6
  495. package/src/renderers/opentui/toast-host.tsx +136 -7
  496. package/src/renderers/opentui/ui-host.tsx +19 -3
  497. package/src/sources/provider-router/connection-health.test.ts +28 -0
  498. package/src/sources/provider-router/connection-health.ts +47 -0
  499. package/src/sources/provider-router/history.test.ts +53 -4
  500. package/src/sources/provider-router/history.ts +119 -14
  501. package/src/sources/provider-router/index.ts +12 -1
  502. package/src/sources/yahoo-finance/history.ts +2 -9
  503. package/src/sources/yahoo-finance/http.ts +1 -1
  504. package/src/sources/yahoo-finance/symbols.ts +1 -1
  505. package/src/sources/yahoo-finance/types.ts +19 -0
  506. package/src/state/app/context/index.test.tsx +101 -0
  507. package/src/state/app/context/index.tsx +10 -0
  508. package/src/state/config-save-scheduler.ts +1 -6
  509. package/src/state/persist-scheduler.test.ts +31 -0
  510. package/src/state/persist-scheduler.ts +21 -8
  511. package/src/sync/auth-transition.test.ts +36 -0
  512. package/src/sync/auth-transition.ts +22 -0
  513. package/src/sync/core-contributors.test.ts +58 -1
  514. package/src/sync/core-contributors.ts +39 -2
  515. package/src/sync/react.ts +9 -0
  516. package/src/test-support/plugin-runtime.ts +3 -0
  517. package/src/theme/font-scale.test.ts +14 -0
  518. package/src/theme/font-scale.ts +69 -0
  519. package/src/tickers/search/index.test.ts +284 -2
  520. package/src/tickers/search/index.ts +55 -18
  521. package/src/tickers/search/ranking.ts +143 -18
  522. package/src/tickers/search/types.ts +1 -0
  523. package/src/time-series/alignment.ts +15 -11
  524. package/src/time-series/field-catalog.ts +10 -0
  525. package/src/time-series/fundamentals.ts +19 -11
  526. package/src/time-series/hooks.ts +8 -2
  527. package/src/time-series/parsed-history-cache.ts +35 -0
  528. package/src/time-series/resolution.ts +10 -0
  529. package/src/time-series/resolve.test.ts +332 -3
  530. package/src/time-series/resolve.ts +225 -25
  531. package/src/time-series/spec-studies.test.ts +23 -0
  532. package/src/time-series/spec.ts +21 -2
  533. package/src/time-series/transforms.test.ts +76 -1
  534. package/src/time-series/transforms.ts +39 -8
  535. package/src/time-series/types.ts +9 -2
  536. package/src/time-series/use-chart-resolution.test.tsx +105 -1
  537. package/src/time-series/use-chart-resolution.ts +67 -13
  538. package/src/types/config.ts +30 -27
  539. package/src/types/media.ts +2 -0
  540. package/src/types/plugin.ts +8 -1
  541. package/src/ui/host.tsx +4 -0
  542. package/src/ui/pointer-drag.ts +12 -0
  543. package/src/ui/qr.test.ts +79 -0
  544. package/src/ui/qr.ts +82 -0
  545. package/src/updater/github-releases.test.ts +3 -3
  546. package/src/updater/github-releases.ts +1 -1
  547. package/src/updater.test.ts +117 -16
  548. package/src/updater.ts +32 -5
  549. package/src/utils/exchanges.ts +1 -0
  550. package/src/utils/external-url.test.ts +32 -0
  551. package/src/utils/external-url.ts +23 -0
  552. package/src/utils/sort-values.ts +31 -0
  553. package/src/utils/zoned-date-time.ts +67 -0
  554. package/src/components/onboarding/wizard-keyboard.ts +0 -306
  555. package/src/plugins/builtin/chart-composer/date-window-dialog.tsx +0 -120
@@ -1,5 +1,13 @@
1
1
  import { createThrottledFetch, type ThrottledFetchTransport } from "../../../utils/throttled-fetch";
2
2
  import type { PluginPersistence } from "../../../types/plugin";
3
+ import { apiClient } from "../../../api-client";
4
+ import type {
5
+ CloudMarketResponse,
6
+ CloudMarketScreenerCategory,
7
+ CloudMarketScreenerItem,
8
+ CloudMarketScreenerPayload,
9
+ } from "../../../api-client/types";
10
+ import { hasProAccess } from "../shared/plan-access";
3
11
 
4
12
  const YAHOO_FINANCE_HOSTS = [
5
13
  "query2.finance.yahoo.com",
@@ -12,6 +20,7 @@ const CACHE_POLICY = {
12
20
  staleMs: 5 * 60 * 1000,
13
21
  expireMs: 60 * 60 * 1000,
14
22
  } as const;
23
+ const YAHOO_METADATA_WAIT_MS = 1_500;
15
24
 
16
25
  const YAHOO_FINANCE_HEADERS = {
17
26
  "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
@@ -101,27 +110,34 @@ function writeCache<T>(key: string, data: T): void {
101
110
  });
102
111
  }
103
112
 
113
+ /** A cached value plus whether it is a fallback rather than a fresh load. */
114
+ export interface CachedResult<T> {
115
+ data: T;
116
+ stale: boolean;
117
+ }
118
+
104
119
  async function loadCached<T>(
105
120
  key: string,
106
121
  fetcher: () => Promise<T>,
107
122
  options?: FetchCacheOptions,
108
- ): Promise<T> {
109
- if (options?.cache === false) return fetcher();
123
+ ): Promise<CachedResult<T>> {
124
+ if (options?.cache === false) return { data: await fetcher(), stale: false };
110
125
 
111
126
  const cached = readCache<T>(key);
112
- if (!options?.forceRefresh && cached && !cached.stale) return cached.data;
127
+ if (!options?.forceRefresh && cached && !cached.stale) return { data: cached.data, stale: false };
113
128
 
114
- const activeFetch = activeFetches.get(key) as Promise<T> | undefined;
129
+ const activeFetch = activeFetches.get(key) as Promise<CachedResult<T>> | undefined;
115
130
  if (activeFetch) return activeFetch;
116
131
 
117
132
  const fallback = cached ?? readCache<T>(key, { allowExpired: true });
118
133
  const fetchPromise = fetcher()
119
134
  .then((data) => {
120
135
  writeCache(key, data);
121
- return data;
136
+ return { data, stale: false };
122
137
  })
123
138
  .catch((error) => {
124
- if (fallback) return fallback.data;
139
+ // Serving the expired copy is right; hiding that it is expired is not.
140
+ if (fallback) return { data: fallback.data, stale: true };
125
141
  throw error;
126
142
  })
127
143
  .finally(() => {
@@ -152,6 +168,29 @@ export interface ScreenerQuote {
152
168
  dayHigh: number | undefined;
153
169
  dayLow: number | undefined;
154
170
  exchange: string;
171
+ lastUpdated?: number;
172
+ }
173
+
174
+ export type MarketMoversDataSource = "cloud" | "yahoo";
175
+
176
+ export interface MarketMoversResult {
177
+ quotes: ScreenerQuote[];
178
+ source: MarketMoversDataSource;
179
+ stale: boolean;
180
+ }
181
+
182
+ export interface PreferredMarketMoverSources {
183
+ isCloudEligible(): boolean;
184
+ fetchCloud(
185
+ category: CloudMarketScreenerCategory,
186
+ count: number,
187
+ mode: "cache-first" | "refresh",
188
+ ): Promise<CloudMarketResponse<CloudMarketScreenerPayload>>;
189
+ fetchYahoo(
190
+ category: ScreenerCategory,
191
+ count: number,
192
+ options?: FetchCacheOptions,
193
+ ): Promise<CachedResult<ScreenerQuote[]>>;
155
194
  }
156
195
 
157
196
  export interface TrendingSymbol {
@@ -199,12 +238,12 @@ export function parseScreenerResponse(data: any): ScreenerQuote[] {
199
238
  }
200
239
  }
201
240
 
202
- export async function fetchScreener(
241
+ export async function fetchScreenerResult(
203
242
  category: ScreenerCategory,
204
243
  count = 25,
205
244
  api: YahooScreenerApi = screenerApi,
206
245
  options?: FetchCacheOptions,
207
- ): Promise<ScreenerQuote[]> {
246
+ ): Promise<CachedResult<ScreenerQuote[]>> {
208
247
  const load = async () => {
209
248
  const data = await api.fetchJson("/v1/finance/screener/predefined/saved", {
210
249
  formatted: "false",
@@ -215,10 +254,129 @@ export async function fetchScreener(
215
254
  });
216
255
  return parseScreenerResponse(data);
217
256
  };
218
- if (!shouldUseCache(api, options)) return load();
257
+ if (!shouldUseCache(api, options)) return { data: await load(), stale: false };
219
258
  return loadCached(`screener:${category}:count=${count}`, load, options);
220
259
  }
221
260
 
261
+ export async function fetchScreener(
262
+ category: ScreenerCategory,
263
+ count = 25,
264
+ api: YahooScreenerApi = screenerApi,
265
+ options?: FetchCacheOptions,
266
+ ): Promise<ScreenerQuote[]> {
267
+ return (await fetchScreenerResult(category, count, api, options)).data;
268
+ }
269
+
270
+ function cloudScreenerCategory(category: ScreenerCategory): CloudMarketScreenerCategory {
271
+ if (category === "day_gainers") return "gainers";
272
+ if (category === "day_losers") return "losers";
273
+ return "most-active";
274
+ }
275
+
276
+ function isCloudScreenerEligible(): boolean {
277
+ const user = apiClient.getCurrentUser();
278
+ return apiClient.isVerified() && hasProAccess(user);
279
+ }
280
+
281
+ const defaultPreferredMarketMoverSources: PreferredMarketMoverSources = {
282
+ isCloudEligible: isCloudScreenerEligible,
283
+ fetchCloud: (category, count, mode) => apiClient.getCloudMarketScreener(category, count, mode),
284
+ fetchYahoo: (category, count, options) => fetchScreenerResult(category, count, undefined, options),
285
+ };
286
+
287
+ function mergeCloudScreenerItem(
288
+ item: CloudMarketScreenerItem,
289
+ metadata?: ScreenerQuote,
290
+ ): ScreenerQuote {
291
+ return {
292
+ symbol: item.symbol,
293
+ name: item.name && item.name !== item.symbol
294
+ ? item.name
295
+ : metadata?.name ?? item.symbol,
296
+ price: item.price,
297
+ change: item.change,
298
+ changePercent: item.changePercent,
299
+ volume: item.volume,
300
+ avgVolume: metadata?.avgVolume ?? 0,
301
+ volumeRatio: metadata?.avgVolume
302
+ ? item.volume / metadata.avgVolume
303
+ : 0,
304
+ marketCap: metadata?.marketCap,
305
+ currency: item.currency || metadata?.currency || "USD",
306
+ fiftyTwoWeekHigh: item.high52w ?? metadata?.fiftyTwoWeekHigh,
307
+ fiftyTwoWeekLow: item.low52w ?? metadata?.fiftyTwoWeekLow,
308
+ dayHigh: item.dayHigh ?? metadata?.dayHigh,
309
+ dayLow: item.dayLow ?? metadata?.dayLow,
310
+ exchange: item.exchange || metadata?.exchange || "",
311
+ lastUpdated: item.lastUpdated,
312
+ };
313
+ }
314
+
315
+ async function bestEffortYahooMetadata(
316
+ request: Promise<CachedResult<ScreenerQuote[]>>,
317
+ ): Promise<ScreenerQuote[]> {
318
+ let timeout: ReturnType<typeof setTimeout> | undefined;
319
+ try {
320
+ return await Promise.race([
321
+ request.then((result) => result.data).catch(() => []),
322
+ new Promise<ScreenerQuote[]>((resolve) => {
323
+ timeout = setTimeout(() => resolve([]), YAHOO_METADATA_WAIT_MS);
324
+ }),
325
+ ]);
326
+ } finally {
327
+ if (timeout) clearTimeout(timeout);
328
+ }
329
+ }
330
+
331
+ export async function fetchPreferredMarketMovers(
332
+ category: ScreenerCategory,
333
+ count = 25,
334
+ options?: FetchCacheOptions,
335
+ sources: PreferredMarketMoverSources = defaultPreferredMarketMoverSources,
336
+ ): Promise<MarketMoversResult> {
337
+ if (!sources.isCloudEligible()) {
338
+ const result = await sources.fetchYahoo(category, count, options);
339
+ return { quotes: result.data, source: "yahoo", stale: result.stale };
340
+ }
341
+
342
+ const yahooMetadata = sources.fetchYahoo(
343
+ category,
344
+ Math.max(count, 50),
345
+ options,
346
+ );
347
+ try {
348
+ const response = await sources.fetchCloud(
349
+ cloudScreenerCategory(category),
350
+ count,
351
+ options?.forceRefresh ? "refresh" : "cache-first",
352
+ );
353
+ if (
354
+ (response.status === "success" || response.status === "partial")
355
+ && response.data
356
+ && response.data.items.length > 0
357
+ ) {
358
+ const metadata = await bestEffortYahooMetadata(yahooMetadata);
359
+ const metadataBySymbol = new Map(metadata.map((quote) => [quote.symbol, quote]));
360
+ return {
361
+ quotes: response.data.items.map((item) => (
362
+ mergeCloudScreenerItem(item, metadataBySymbol.get(item.symbol))
363
+ )),
364
+ source: "cloud",
365
+ stale: response.stale === true || response.data.stale === true,
366
+ };
367
+ }
368
+ } catch {
369
+ // Yahoo remains the resilient fallback when Cloud is unavailable.
370
+ }
371
+
372
+ const fallback = await yahooMetadata;
373
+ return {
374
+ quotes: fallback.data,
375
+ source: "yahoo",
376
+ stale: fallback.stale,
377
+ };
378
+ }
379
+
222
380
  export function parseTrendingResponse(data: any): TrendingSymbol[] {
223
381
  try {
224
382
  const quotes = data?.finance?.result?.[0]?.quotes;
@@ -246,7 +404,7 @@ export async function fetchTrending(
246
404
  return parseTrendingResponse(data);
247
405
  };
248
406
  if (!shouldUseCache(api, options)) return load();
249
- return loadCached(`trending:US:count=${count}`, load, options);
407
+ return (await loadCached(`trending:US:count=${count}`, load, options)).data;
250
408
  }
251
409
 
252
410
  export const MARKET_SUMMARY_SYMBOLS = ["^GSPC", "^DJI", "^IXIC", "^RUT"] as const;
@@ -1,61 +1,37 @@
1
- import { Text } from "../../../ui";
2
- import { useRef, useEffect, useMemo, useState } from "react";
1
+ import { Box } from "../../../ui";
3
2
  import { composeBuiltinPlugin, type PluginModule } from "../plugin-module";
4
3
  import { usePaneTicker } from "../../../state/app/context";
5
- import { colors } from "../../../theme/colors";
6
- import type { NewsArticle } from "../../../types/news-source";
7
4
  import { useArticleSummary, useResolvedEntryValue } from "../../../market-data/hooks";
8
5
  import { instrumentFromTicker } from "../../../market-data/request-types";
9
- import { useDebouncedPluginPaneState } from "../../runtime";
10
- import { FeedDataTableStackView, Spinner, type FeedDataTableItem } from "../../../components";
11
- import { useNewsArticles } from "../../../news/hooks";
6
+ import { useDebouncedPluginPaneState, usePluginPaneState } from "../../runtime";
7
+ import { EmptyState } from "../../../components";
8
+ import { useLoadNewsStory, useNewsArticles } from "../../../news/hooks";
12
9
  import { newsWireModule } from "./wire";
10
+ import { NewsDetailView, useNewsArticleDetail } from "./wire/news/detail-view";
11
+ import {
12
+ NewsArticleStackView,
13
+ newsTableStatusContent,
14
+ type NewsSortPreference,
15
+ } from "./wire/news/table";
13
16
  import { useNewsArticleFooter } from "./wire/news/footer";
14
17
  import { usePersistedNewsArticles } from "./wire/persisted-articles";
15
18
  import { useNewsReadState } from "./wire/read-state";
16
19
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
17
20
 
18
21
  const NEWS_ITEM_LIMIT = 50;
19
-
20
- function getFeedItems(
21
- news: NewsArticle[],
22
- selectedUrl: string | undefined,
23
- summaryCache: Map<string, string>,
24
- loadingSummary: boolean,
25
- ): FeedDataTableItem[] {
26
- return news.map((item) => {
27
- const preview = summaryCache.get(item.url) ?? item.summary ?? undefined;
28
- const isSelected = item.url === selectedUrl;
29
- return {
30
- id: item.id,
31
- eyebrow: item.source,
32
- title: item.title,
33
- timestamp: item.publishedAt,
34
- detailTitle: item.title,
35
- detailMeta: [
36
- item.source,
37
- `Published ${item.publishedAt.toLocaleString("en-US", {
38
- month: "short",
39
- day: "numeric",
40
- year: "numeric",
41
- hour: "numeric",
42
- minute: "2-digit",
43
- })}`,
44
- ],
45
- detailBody: isSelected
46
- ? preview ?? (loadingSummary ? "Loading preview..." : "No preview available.")
47
- : preview ?? "",
48
- detailNote: item.url,
49
- };
50
- });
51
- }
22
+ const DEFAULT_SORT: NewsSortPreference = { columnId: "time", direction: "desc" };
52
23
 
53
24
  function TickerNewsView({ width, height, focused }: { width: number; height: number; focused: boolean }) {
54
25
  const { ticker } = usePaneTicker();
55
- const selectionKey = `selectedIdx:${ticker?.metadata.ticker ?? "none"}`;
56
- const [selectedIdx, setSelectedIdx] = useDebouncedPluginPaneState<number>(selectionKey, 0);
57
- const [summaryCache, setSummaryCache] = useState<Map<string, string>>(new Map());
58
- const summaryFetchRef = useRef(0);
26
+ const symbol = ticker?.metadata.ticker ?? "none";
27
+ const [selectedArticleId, setSelectedArticleId] = useDebouncedPluginPaneState<string | null>(
28
+ `selectedArticleId:${symbol}`,
29
+ null,
30
+ );
31
+ const [sortPreference, setSortPreference] = usePluginPaneState<NewsSortPreference>(
32
+ "ticker-news:sort",
33
+ DEFAULT_SORT,
34
+ );
59
35
  const instrument = instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null);
60
36
  const newsState = useNewsArticles(instrument ? {
61
37
  feed: "ticker",
@@ -64,81 +40,84 @@ function TickerNewsView({ width, height, focused }: { width: number; height: num
64
40
  tickerTier: "primary",
65
41
  limit: NEWS_ITEM_LIMIT,
66
42
  } : null);
67
- const liveNews = newsState.articles;
68
43
  const news = usePersistedNewsArticles(
69
44
  `articles:${instrument?.symbol ?? "none"}:${instrument?.exchange ?? ""}`,
70
- liveNews,
45
+ newsState.articles,
71
46
  );
72
47
  const { readArticleIds, markArticleRead } = useNewsReadState();
73
- const [openItemId, setOpenItemId] = useState<string | null>(null);
74
- const loading = newsState.phase === "loading" || (newsState.phase === "refreshing" && news.length === 0);
75
- const error = newsState.phase === "error" ? newsState.error ?? "Failed to load news" : null;
76
-
77
- useEffect(() => {
78
- summaryFetchRef.current += 1;
79
- setSummaryCache(new Map());
80
- }, [ticker?.metadata.ticker]);
48
+ const loadNewsStory = useLoadNewsStory();
49
+ const { detailArticle, openArticle, closeDetail } = useNewsArticleDetail(news, loadNewsStory);
50
+ const loading = newsState.phase === "loading"
51
+ || (newsState.phase === "refreshing" && news.length === 0);
52
+ const error = newsState.error;
81
53
 
82
- const selected = news[selectedIdx];
83
- const openArticle = openItemId
84
- ? news.find((article) => article.id === openItemId) ?? null
85
- : null;
86
- const cachedSelectedSummary = selected ? summaryCache.get(selected.url) : undefined;
87
54
  const articleSummaryEntry = useArticleSummary(
88
- selected && !selected.summary && !cachedSelectedSummary ? selected.url : null,
55
+ detailArticle && !detailArticle.summary ? detailArticle.url : null,
89
56
  );
90
- const selectedSummary = useResolvedEntryValue(articleSummaryEntry);
91
- const loadingSummary = articleSummaryEntry?.phase === "loading" || articleSummaryEntry?.phase === "refreshing";
92
-
93
- useEffect(() => {
94
- if (!selected?.summary) return;
95
- const summary = selected.summary;
96
- setSummaryCache((prev) => prev.has(selected.url) ? prev : new Map(prev).set(selected.url, summary));
97
- }, [selected?.summary, selected?.url]);
98
-
99
- useEffect(() => {
100
- if (!selected?.url || !selectedSummary) return;
101
- setSummaryCache((prev) => new Map(prev).set(selected.url, selectedSummary));
102
- }, [selected?.url, selectedSummary]);
103
-
104
- useEffect(() => {
105
- if (news.length > 0 && selectedIdx >= news.length) {
106
- setSelectedIdx(Math.max(0, news.length - 1));
107
- }
108
- }, [news.length, selectedIdx, setSelectedIdx]);
57
+ const fetchedSummary = useResolvedEntryValue(articleSummaryEntry);
58
+ const loadingSummary = articleSummaryEntry?.phase === "loading"
59
+ || articleSummaryEntry?.phase === "refreshing";
60
+ const detailWithSummary = detailArticle && !detailArticle.summary && fetchedSummary
61
+ ? { ...detailArticle, summary: fetchedSummary }
62
+ : detailArticle;
109
63
 
110
64
  useNewsArticleFooter({
111
65
  registrationId: "news",
112
66
  focused,
113
- article: openArticle,
114
- loading,
67
+ article: detailArticle,
68
+ // Stale rows stay on screen during a refresh or a failure, so the pane says
69
+ // so in the footer instead of replacing them.
70
+ loading: loading && news.length > 0,
115
71
  error,
116
72
  info: loadingSummary
117
73
  ? [{ id: "summary", parts: [{ text: "summary loading", tone: "muted" as const }] }]
118
74
  : undefined,
119
75
  });
120
76
 
121
- if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view news.</Text>;
122
- if (loading && news.length === 0) return <Spinner label="Loading news..." />;
123
- if (error) return <Text fg={colors.textDim}>Error: {error}</Text>;
124
- if (news.length === 0) return <Text fg={colors.textDim}>No news available for {ticker.metadata.ticker}.</Text>;
125
-
126
- const items = getFeedItems(news, selected?.url, summaryCache, loadingSummary);
77
+ if (!ticker) {
78
+ return (
79
+ <Box paddingX={1} paddingY={1}>
80
+ <EmptyState title="No ticker selected." message="Pick a ticker to load its news." />
81
+ </Box>
82
+ );
83
+ }
127
84
 
128
85
  return (
129
- <FeedDataTableStackView
130
- width={width}
131
- height={height}
86
+ <NewsArticleStackView
87
+ articles={news}
132
88
  focused={focused}
133
- items={items}
134
- selectedIdx={selectedIdx}
135
- onSelect={setSelectedIdx}
136
- isItemRead={(item) => readArticleIds.has(item.id)}
137
- onOpenItem={(item) => markArticleRead(item.id)}
138
- onOpenItemIdChange={setOpenItemId}
139
- sourceLabel="Source"
140
- titleLabel="Headline"
141
- emptyStateTitle="No news."
89
+ width={width}
90
+ rootHeight={height}
91
+ readArticleIds={readArticleIds}
92
+ selectedArticleId={selectedArticleId}
93
+ setSelectedArticleId={setSelectedArticleId}
94
+ sortPreference={sortPreference}
95
+ setSortPreference={setSortPreference}
96
+ onOpenArticle={openArticle}
97
+ onArticleRead={markArticleRead}
98
+ detailOpen={!!detailWithSummary}
99
+ onBack={closeDetail}
100
+ detailContent={detailWithSummary ? (
101
+ <NewsDetailView
102
+ item={detailWithSummary}
103
+ focused={focused}
104
+ width={width}
105
+ showTitle={false}
106
+ />
107
+ ) : (
108
+ <Box flexGrow={1} />
109
+ )}
110
+ detailTitle={detailWithSummary?.title}
111
+ columns={["time", "source", "title", "categories", "sentiment"]}
112
+ emptyContent={newsTableStatusContent({
113
+ loading,
114
+ error,
115
+ subject: "News",
116
+ emptyTitle: `No news for ${ticker.metadata.ticker}`,
117
+ emptyMessage: "Stories appear as sources publish them.",
118
+ })}
119
+ emptyStateTitle={`No news for ${ticker.metadata.ticker}`}
120
+ emptyStateHint="Stories appear as sources publish them."
142
121
  />
143
122
  );
144
123
  }
@@ -0,0 +1,72 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { NewsArticle } from "../../../../news/types";
3
+ import {
4
+ looksLikeArticleQuery,
5
+ searchNewsArticles,
6
+ tokenizeArticleQuery,
7
+ } from "./article-search";
8
+
9
+ function article(overrides: Partial<NewsArticle> & Pick<NewsArticle, "id" | "title" | "source">): NewsArticle {
10
+ return {
11
+ url: `https://example.com/${overrides.id}`,
12
+ publishedAt: new Date("2026-08-14T12:00:00Z"),
13
+ topic: "general",
14
+ topics: [],
15
+ sectors: [],
16
+ categories: [],
17
+ tickers: [],
18
+ scores: { importance: 50, urgency: 0, marketImpact: 0, novelty: 0, confidence: 0 },
19
+ isBreaking: false,
20
+ isDeveloping: false,
21
+ importance: 50,
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ const hormuz = article({
27
+ id: "hormuz",
28
+ title: "Iran Threatens to Close the Strait of Hormuz",
29
+ source: "Reuters",
30
+ summary: "Shipping risk rises as Tehran warns it may shut the strait.",
31
+ publishedAt: new Date("2026-08-14T18:00:00Z"),
32
+ });
33
+
34
+ const other = article({
35
+ id: "fed",
36
+ title: "Fed holds rates as inflation cools",
37
+ source: "CNBC Top News",
38
+ publishedAt: new Date("2026-08-13T12:00:00Z"),
39
+ });
40
+
41
+ describe("looksLikeArticleQuery", () => {
42
+ test("treats article, news, and ART lookups as article searches", () => {
43
+ expect(looksLikeArticleQuery("article on the strait")).toBe(true);
44
+ expect(looksLikeArticleQuery("ART hormuz")).toBe(true);
45
+ expect(looksLikeArticleQuery("open press story")).toBe(true);
46
+ expect(looksLikeArticleQuery("ADI")).toBe(false);
47
+ expect(looksLikeArticleQuery("nvda")).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe("searchNewsArticles", () => {
52
+ test("matches headlines after dropping filler and intent words", () => {
53
+ expect(tokenizeArticleQuery("article on the strait of hormuz")).toEqual(["strait", "hormuz"]);
54
+ const matches = searchNewsArticles([hormuz, other], "article on the strait of hormuz");
55
+ expect(matches.map((item) => item.id)).toEqual(["hormuz"]);
56
+ });
57
+
58
+ test("returns most recent articles when the query is only an intent word", () => {
59
+ const matches = searchNewsArticles([other, hormuz], "article");
60
+ expect(matches.map((item) => item.id)).toEqual(["hormuz", "fed"]);
61
+ });
62
+
63
+ test("strips the ART command prefix so local headlines match immediately", () => {
64
+ const trump = article({
65
+ id: "trump",
66
+ title: "Trump administration pauses contentious trade talks",
67
+ source: "AP",
68
+ });
69
+ expect(tokenizeArticleQuery("art trum")).toEqual(["trum"]);
70
+ expect(searchNewsArticles([trump, other], "ART trum").map((item) => item.id)).toEqual(["trump"]);
71
+ });
72
+ });
@@ -0,0 +1,140 @@
1
+ import type { NewsArticle, NewsQuery } from "../../../../news/types";
2
+ import { getSharedNewsService } from "../../../../news/hooks";
3
+
4
+ const ARTICLE_SEARCH_LIMIT = 8;
5
+
6
+ const STOPWORDS = new Set([
7
+ "a",
8
+ "an",
9
+ "the",
10
+ "on",
11
+ "of",
12
+ "in",
13
+ "to",
14
+ "for",
15
+ "and",
16
+ "or",
17
+ "from",
18
+ "with",
19
+ "about",
20
+ "by",
21
+ "at",
22
+ "as",
23
+ "is",
24
+ "are",
25
+ "was",
26
+ "be",
27
+ ]);
28
+
29
+ const INTENT_WORDS = new Set([
30
+ "article",
31
+ "articles",
32
+ "news",
33
+ "headline",
34
+ "headlines",
35
+ "story",
36
+ "stories",
37
+ "rss",
38
+ "feed",
39
+ "feeds",
40
+ "press",
41
+ ]);
42
+
43
+ const COMMAND_WORDS = new Set([
44
+ "open",
45
+ "read",
46
+ "show",
47
+ "find",
48
+ "search",
49
+ "please",
50
+ "me",
51
+ "latest",
52
+ "recent",
53
+ "lookup",
54
+ "look",
55
+ "up",
56
+ ]);
57
+
58
+ /** Pulls the freshest slice of the global feed to search over. */
59
+ export const ARTICLE_SEARCH_QUERY: NewsQuery = { feed: "latest", limit: 200 };
60
+
61
+ export function tokenizeArticleQuery(query: string): string[] {
62
+ // `ART trump` is the command prefix plus a topic — "art" is not a search term.
63
+ const withoutArtPrefix = query.replace(/^\s*art\b/i, " ");
64
+ return withoutArtPrefix
65
+ .toLowerCase()
66
+ .split(/[^a-z0-9]+/)
67
+ .filter((token) => (
68
+ token.length >= 2
69
+ && !STOPWORDS.has(token)
70
+ && !INTENT_WORDS.has(token)
71
+ && !COMMAND_WORDS.has(token)
72
+ ));
73
+ }
74
+
75
+ export function looksLikeArticleQuery(query: string): boolean {
76
+ const tokens = query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
77
+ if (tokens.some((token) => INTENT_WORDS.has(token))) return true;
78
+ return /^\s*art\b/i.test(query);
79
+ }
80
+
81
+ export function scoreArticleMatch(article: NewsArticle, tokens: readonly string[]): number {
82
+ if (tokens.length === 0) return 0;
83
+ const title = article.title.toLowerCase();
84
+ const source = article.source.toLowerCase();
85
+ const haystack = [
86
+ title,
87
+ source,
88
+ article.summary ?? "",
89
+ ...article.topics,
90
+ ...article.categories,
91
+ ...article.tickers,
92
+ ].join(" ").toLowerCase();
93
+
94
+ let score = 0;
95
+ for (const token of tokens) {
96
+ if (title.includes(token)) score += 10;
97
+ else if (source.includes(token)) score += 6;
98
+ else if (haystack.includes(token)) score += 3;
99
+ else return 0;
100
+ }
101
+ return score;
102
+ }
103
+
104
+ export function searchNewsArticles(
105
+ articles: readonly NewsArticle[],
106
+ query: string,
107
+ limit = ARTICLE_SEARCH_LIMIT,
108
+ ): NewsArticle[] {
109
+ const tokens = tokenizeArticleQuery(query);
110
+ if (tokens.length === 0) {
111
+ return [...articles]
112
+ .sort((left, right) => right.publishedAt.getTime() - left.publishedAt.getTime())
113
+ .slice(0, limit);
114
+ }
115
+
116
+ return articles
117
+ .map((article) => ({ article, score: scoreArticleMatch(article, tokens) }))
118
+ .filter((entry) => entry.score > 0)
119
+ .sort((left, right) => {
120
+ if (right.score !== left.score) return right.score - left.score;
121
+ return right.article.publishedAt.getTime() - left.article.publishedAt.getTime();
122
+ })
123
+ .slice(0, limit)
124
+ .map((entry) => entry.article);
125
+ }
126
+
127
+ /** Headlines already sitting in the shared news pool (firehose/RSS/breaking). */
128
+ export function cachedNewsArticles(): NewsArticle[] {
129
+ const service = getSharedNewsService();
130
+ if (!service) return [];
131
+ const pooled = service.getFirehose(undefined, 500);
132
+ if (pooled.length > 0) return pooled;
133
+ return service.getQueryState(ARTICLE_SEARCH_QUERY).articles;
134
+ }
135
+
136
+ export async function loadNewsArticles(): Promise<NewsArticle[]> {
137
+ const service = getSharedNewsService();
138
+ if (!service) return [];
139
+ return (await service.load(ARTICLE_SEARCH_QUERY)).articles;
140
+ }