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
@@ -0,0 +1,79 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ ConnectionHealthRegistry,
4
+ GLOOM_CLOUD_SOCKET_CONNECTION_ID,
5
+ type ConnectionHealthStatus,
6
+ } from "../core/connection-health";
7
+ import { CloudApiSocket } from "./socket";
8
+
9
+ function waitForStatus(
10
+ health: ConnectionHealthRegistry,
11
+ status: ConnectionHealthStatus,
12
+ ): Promise<void> {
13
+ if (health.getSnapshot().sources[0]?.status === status) return Promise.resolve();
14
+ return new Promise((resolve, reject) => {
15
+ const timer = setTimeout(() => {
16
+ unsubscribe();
17
+ reject(new Error(`Timed out waiting for ${status}`));
18
+ }, 2_000);
19
+ const unsubscribe = health.subscribe(() => {
20
+ if (health.getSnapshot().sources[0]?.status !== status) return;
21
+ clearTimeout(timer);
22
+ unsubscribe();
23
+ resolve();
24
+ });
25
+ });
26
+ }
27
+
28
+ describe("CloudApiSocket connection health", () => {
29
+ test("reports transitions from a real loopback websocket", async () => {
30
+ let peer: ServerWebSocket<unknown> | null = null;
31
+ const server = Bun.serve({
32
+ hostname: "127.0.0.1",
33
+ port: 0,
34
+ fetch(request, server) {
35
+ return server.upgrade(request) ? undefined : new Response("Upgrade required", { status: 426 });
36
+ },
37
+ websocket: {
38
+ open(socket) {
39
+ peer = socket;
40
+ },
41
+ message() {},
42
+ },
43
+ });
44
+ const health = new ConnectionHealthRegistry();
45
+ health.registerSource({
46
+ id: GLOOM_CLOUD_SOCKET_CONNECTION_ID,
47
+ name: "Gloom Cloud Stream",
48
+ kind: "websocket",
49
+ });
50
+ const socket = new CloudApiSocket({
51
+ getBaseUrl: () => `http://127.0.0.1:${server.port}`,
52
+ getSocketAuthToken: () => null,
53
+ hasVerifiedUser: () => false,
54
+ isUsingWebSocketToken: () => false,
55
+ clearWebSocketTokenForFallback: () => false,
56
+ markCurrentUserUnverified: () => {},
57
+ updateCurrentUserFromSocket: () => {},
58
+ }, health);
59
+
60
+ try {
61
+ socket.subscribeQuotes([{ symbol: "AAPL" }], () => {});
62
+ expect(health.getSnapshot().sources[0]).toMatchObject({
63
+ status: "connecting",
64
+ socketState: "connecting",
65
+ });
66
+
67
+ await waitForStatus(health, "connected");
68
+ peer!.close(1000, "network lost");
69
+ await waitForStatus(health, "disconnected");
70
+ expect(health.getSnapshot().sources[0]).toMatchObject({
71
+ socketState: "closed",
72
+ currentDetail: "network lost",
73
+ });
74
+ } finally {
75
+ socket.dispose();
76
+ void server.stop(true);
77
+ }
78
+ });
79
+ });
@@ -4,6 +4,8 @@ import type {
4
4
  ChatNotification,
5
5
  CloudQuotePayload,
6
6
  QuoteStreamTarget,
7
+ ScannerFeedEvent,
8
+ ScannerKind,
7
9
  } from "./types";
8
10
  import {
9
11
  normalizeChatMessage,
@@ -12,6 +14,11 @@ import {
12
14
  import { debugLog } from "../utils/debug-log";
13
15
  import { canonicalExchange, normalizeSymbol } from "../utils/exchanges";
14
16
  import { mergeQuoteSubscriptionTargets } from "../market-data/quote-subscription-target";
17
+ import {
18
+ connectionHealth,
19
+ GLOOM_CLOUD_SOCKET_CONNECTION_ID,
20
+ type ConnectionHealthRegistry,
21
+ } from "../core/connection-health";
15
22
 
16
23
  const QUOTE_SUBSCRIPTION_FLUSH_MS = 25;
17
24
  const cloudApiLog = debugLog.createLogger("cloud-api");
@@ -24,6 +31,12 @@ type QuoteSubscription = {
24
31
  target: QuoteStreamTarget;
25
32
  listener: QuoteListener;
26
33
  };
34
+ type ScannerListener = (event: ScannerFeedEvent) => void;
35
+
36
+ const SCANNER_MESSAGE_KINDS: Record<string, ScannerKind> = {
37
+ "scanner.hilo": "hilo",
38
+ "scanner.flow": "flow",
39
+ };
27
40
 
28
41
  function mergeQuoteStreamSubscriptions(
29
42
  subscriptions: Iterable<QuoteSubscription>,
@@ -68,8 +81,14 @@ export class CloudApiSocket {
68
81
  private readonly pendingQuoteSubscribes = new Map<string, QuoteStreamTarget>();
69
82
  private readonly pendingQuoteUnsubscribes = new Map<string, QuoteStreamTarget>();
70
83
  private quoteSubscriptionFlushTimer: ReturnType<typeof setTimeout> | null = null;
84
+ private readonly scannerListeners = new Map<ScannerKind, Set<ScannerListener>>();
85
+ /** Latest fan-out payload, so a pane opened mid-stream does not wait for the next tick. */
86
+ private readonly scannerSnapshots = new Map<ScannerKind, ScannerFeedEvent>();
71
87
 
72
- constructor(private readonly delegate: CloudApiSocketDelegate) {}
88
+ constructor(
89
+ private readonly delegate: CloudApiSocketDelegate,
90
+ private readonly health: ConnectionHealthRegistry = connectionHealth,
91
+ ) {}
73
92
 
74
93
  syncAuthState(options: { reconnect?: boolean } = {}): void {
75
94
  if (!this.shouldKeepSocketOpen()) {
@@ -91,6 +110,7 @@ export class CloudApiSocket {
91
110
  this.ws = null;
92
111
  if (ws) {
93
112
  cloudApiLog.info("teardown websocket");
113
+ this.health.reportSocketState(GLOOM_CLOUD_SOCKET_CONNECTION_ID, "idle", "Socket closed locally");
94
114
  }
95
115
  try {
96
116
  ws?.close();
@@ -163,6 +183,37 @@ export class CloudApiSocket {
163
183
  };
164
184
  }
165
185
 
186
+ /**
187
+ * Subscribes to a shared server-computed scanner. Every open pane of the same
188
+ * kind shares one upstream subscription; the socket fans the payload out.
189
+ */
190
+ subscribeScanner(scanner: ScannerKind, listener: ScannerListener): () => void {
191
+ const listeners = this.scannerListeners.get(scanner) ?? new Set<ScannerListener>();
192
+ const firstListener = listeners.size === 0;
193
+ listeners.add(listener);
194
+ this.scannerListeners.set(scanner, listeners);
195
+ this.ensureSocket();
196
+ if (firstListener) {
197
+ this.sendSocketMessage({ type: "scanner.subscribe", scanner });
198
+ } else {
199
+ const snapshot = this.scannerSnapshots.get(scanner);
200
+ if (snapshot) listener(snapshot);
201
+ }
202
+
203
+ return () => {
204
+ const current = this.scannerListeners.get(scanner);
205
+ if (!current || !current.delete(listener)) return;
206
+ if (current.size === 0) {
207
+ this.scannerListeners.delete(scanner);
208
+ this.scannerSnapshots.delete(scanner);
209
+ this.sendSocketMessage({ type: "scanner.unsubscribe", scanner });
210
+ }
211
+ if (!this.shouldKeepSocketOpen()) {
212
+ this.teardown();
213
+ }
214
+ };
215
+ }
216
+
166
217
  subscribeQuotes(
167
218
  targets: QuoteStreamTarget[],
168
219
  onQuote: (target: QuoteStreamTarget, quote: CloudQuotePayload) => void,
@@ -264,6 +315,8 @@ export class CloudApiSocket {
264
315
  this.quoteTargets.clear();
265
316
  this.pendingQuoteSubscribes.clear();
266
317
  this.pendingQuoteUnsubscribes.clear();
318
+ this.scannerListeners.clear();
319
+ this.scannerSnapshots.clear();
267
320
  if (this.quoteSubscriptionFlushTimer) {
268
321
  clearTimeout(this.quoteSubscriptionFlushTimer);
269
322
  this.quoteSubscriptionFlushTimer = null;
@@ -296,7 +349,7 @@ export class CloudApiSocket {
296
349
  return;
297
350
  }
298
351
  this.delegate.markCurrentUserUnverified();
299
- if (this.quoteTargets.size > 0) {
352
+ if (this.quoteTargets.size > 0 || this.scannerListeners.size > 0) {
300
353
  return;
301
354
  }
302
355
  this.teardown();
@@ -326,6 +379,24 @@ export class CloudApiSocket {
326
379
  return;
327
380
  }
328
381
 
382
+ const scannerKind = typeof parsed?.type === "string" ? SCANNER_MESSAGE_KINDS[parsed.type] : undefined;
383
+ if (scannerKind) {
384
+ const { type: _type, ...payload } = parsed;
385
+ this.emitScannerEvent(scannerKind, { type: "data", payload });
386
+ return;
387
+ }
388
+
389
+ if (parsed?.type === "scanner.denied") {
390
+ const denied = SCANNER_MESSAGE_KINDS[`scanner.${parsed.scanner}`];
391
+ if (denied) {
392
+ this.emitScannerEvent(denied, {
393
+ type: "denied",
394
+ reason: typeof parsed.reason === "string" ? parsed.reason : "pro_required",
395
+ });
396
+ }
397
+ return;
398
+ }
399
+
329
400
  if (parsed?.type === "market.quote" && parsed.quote && typeof parsed.symbol === "string") {
330
401
  const key = marketKey(parsed.symbol, parsed.exchange);
331
402
  const quote: CloudQuotePayload = {
@@ -347,8 +418,15 @@ export class CloudApiSocket {
347
418
  return baseUrl.replace(/^https?/, wsProtocol);
348
419
  }
349
420
 
421
+ private emitScannerEvent(scanner: ScannerKind, event: ScannerFeedEvent): void {
422
+ this.scannerSnapshots.set(scanner, event);
423
+ for (const listener of this.scannerListeners.get(scanner) ?? []) {
424
+ listener(event);
425
+ }
426
+ }
427
+
350
428
  private shouldKeepSocketOpen(): boolean {
351
- if (this.quoteTargets.size > 0) return true;
429
+ if (this.quoteTargets.size > 0 || this.scannerListeners.size > 0) return true;
352
430
  return !!this.delegate.getSocketAuthToken()
353
431
  && this.delegate.hasVerifiedUser()
354
432
  && this.channelListeners.size > 0;
@@ -368,12 +446,24 @@ export class CloudApiSocket {
368
446
  quoteTargets: this.quoteTargets.size,
369
447
  channelTargets: this.channelListeners.size,
370
448
  });
371
- const ws = new WebSocket(url);
449
+ this.health.reportSocketState(GLOOM_CLOUD_SOCKET_CONNECTION_ID, "connecting", this.getWebSocketBaseUrl());
450
+ let ws: WebSocket;
451
+ try {
452
+ ws = new WebSocket(url);
453
+ } catch (error) {
454
+ this.health.reportSocketState(
455
+ GLOOM_CLOUD_SOCKET_CONNECTION_ID,
456
+ "error",
457
+ error instanceof Error ? error.message : String(error),
458
+ );
459
+ throw error;
460
+ }
372
461
  this.ws = ws;
373
462
 
374
463
  ws.onopen = () => {
375
464
  if (this.ws !== ws) return;
376
465
  cloudApiLog.info("websocket open");
466
+ this.health.reportSocketState(GLOOM_CLOUD_SOCKET_CONNECTION_ID, "open", this.getWebSocketBaseUrl());
377
467
  this.reconnectDelayMs = 1000;
378
468
  this.flushSubscriptions();
379
469
  };
@@ -396,6 +486,11 @@ export class CloudApiSocket {
396
486
  tokenSource: usingWebSocketToken ? "websocket" : "session",
397
487
  });
398
488
  if (!activeSocket) return;
489
+ this.health.reportSocketState(
490
+ GLOOM_CLOUD_SOCKET_CONNECTION_ID,
491
+ "closed",
492
+ closeEvent?.reason || (closeEvent?.code ? `Closed (${closeEvent.code})` : "Socket closed"),
493
+ );
399
494
  if (usingWebSocketToken && this.delegate.clearWebSocketTokenForFallback()) {
400
495
  this.reconnectDelayMs = 1000;
401
496
  cloudApiLog.warn("cleared websocket token after socket close; falling back to session token");
@@ -405,7 +500,9 @@ export class CloudApiSocket {
405
500
  };
406
501
 
407
502
  ws.onerror = () => {
408
- // reconnect is handled by onclose
503
+ if (this.ws !== ws) return;
504
+ this.health.reportSocketState(GLOOM_CLOUD_SOCKET_CONNECTION_ID, "error", "WebSocket error");
505
+ // Reconnect is handled by onclose.
409
506
  };
410
507
  }
411
508
 
@@ -428,6 +525,8 @@ export class CloudApiSocket {
428
525
  || type === "market.unsubscribe"
429
526
  || type === "chat.subscribe"
430
527
  || type === "chat.unsubscribe"
528
+ || type === "scanner.subscribe"
529
+ || type === "scanner.unsubscribe"
431
530
  ) {
432
531
  cloudApiLog.info("send websocket message", payload);
433
532
  }
@@ -442,6 +541,10 @@ export class CloudApiSocket {
442
541
  this.sendSocketMessage({ type: "chat.subscribe", channelId });
443
542
  }
444
543
 
544
+ for (const scanner of this.scannerListeners.keys()) {
545
+ this.sendSocketMessage({ type: "scanner.subscribe", scanner });
546
+ }
547
+
445
548
  if (this.quoteTargets.size > 0) {
446
549
  this.sendSocketMessage({
447
550
  type: "market.subscribe",
@@ -322,10 +322,28 @@ export interface CloudFredSeriesPayload {
322
322
  info: CloudFredSeriesInfoPayload | null;
323
323
  }
324
324
 
325
+ export interface CloudShortInterestPointPayload {
326
+ settlementDate: string;
327
+ sharesShort: number;
328
+ previousSharesShort: number | null;
329
+ averageDailyVolume: number | null;
330
+ daysToCover: number | null;
331
+ changePercent: number | null;
332
+ revised: boolean;
333
+ }
334
+
335
+ export interface CloudShortInterestPayload {
336
+ symbol: string;
337
+ issueName: string | null;
338
+ points: CloudShortInterestPointPayload[];
339
+ }
340
+
325
341
  export interface CloudYieldPointPayload {
326
342
  maturity: string;
327
343
  maturityYears: number;
328
344
  yield: number | null;
345
+ /** FRED observation date. Absent on servers older than the field. */
346
+ asOf?: string | null;
329
347
  }
330
348
 
331
349
  type CloudCongressTradeSide = "BUY" | "SELL" | "EXCHANGE" | "OTHER";
@@ -570,12 +588,133 @@ export interface CloudMarketBatchPayload<T> {
570
588
  items: Array<CloudMarketBatchItem<T>>;
571
589
  }
572
590
 
591
+ export type CloudMarketScreenerCategory = "gainers" | "losers" | "most-active";
592
+
593
+ export interface CloudMarketScreenerItem {
594
+ rank: number;
595
+ symbol: string;
596
+ name: string;
597
+ price: number;
598
+ change: number;
599
+ changePercent: number;
600
+ volume: number;
601
+ tradeCount?: number;
602
+ currency: string;
603
+ high52w?: number;
604
+ low52w?: number;
605
+ dayHigh?: number;
606
+ dayLow?: number;
607
+ exchange: string;
608
+ lastUpdated: number;
609
+ dataSource: "live";
610
+ }
611
+
612
+ export interface CloudMarketScreenerPayload {
613
+ providerId: "gloomberb-cloud";
614
+ category: CloudMarketScreenerCategory;
615
+ asOf: string;
616
+ stale?: boolean;
617
+ items: CloudMarketScreenerItem[];
618
+ }
619
+
573
620
  export interface CloudVerificationResponse {
574
621
  sent: boolean;
575
622
  email?: string;
576
623
  alreadyVerified?: boolean;
577
624
  }
578
625
 
626
+ /** A short-lived, single-use browser URL that establishes the existing desktop session. */
627
+ export interface CloudBrowserHandoffResponse {
628
+ url: string;
629
+ }
630
+
631
+ export interface DeviceAuthStartResponse {
632
+ deviceCode: string;
633
+ /** 8 characters formatted "XXXX-XXXX", shown so a user without the app can type it. */
634
+ userCode: string;
635
+ expiresAt: string;
636
+ /** Like https://gloom.sh/link/<userCode>; this is what the QR code encodes. */
637
+ verificationUri: string;
638
+ pollIntervalMs: number;
639
+ }
640
+
641
+ /** The approved response is single-use; polling again returns a non-approved status. */
642
+ export type DeviceAuthTokenResponse =
643
+ | { status: "pending" }
644
+ | { status: "denied" }
645
+ | { status: "expired" }
646
+ | { status: "approved"; sessionToken: string; user: AuthUser };
647
+
648
+ /** Shared server-computed scanners fanned out to every subscriber. */
649
+ export type ScannerKind = "hilo" | "flow";
650
+
651
+ export type ScannerStatus = "live" | "starting" | "closed" | "degraded";
652
+
653
+ export interface ScannerWindowCounts {
654
+ highs: number;
655
+ lows: number;
656
+ }
657
+
658
+ export interface ScannerHiloExtreme {
659
+ symbol: string;
660
+ price: number;
661
+ volume?: number | null;
662
+ /** Times this symbol printed a new session extreme today. */
663
+ count: number;
664
+ at: number;
665
+ }
666
+
667
+ /** Which entitlement tier produced the payload: free accounts get a delayed instance. */
668
+ export interface ScannerAccessInfo {
669
+ access: "realtime" | "delayed";
670
+ /** 0 on the realtime instance, 15 on the delayed one. */
671
+ delayMinutes: number;
672
+ }
673
+
674
+ export interface ScannerHiloPayload extends ScannerAccessInfo {
675
+ status: ScannerStatus;
676
+ asOf: number;
677
+ windows: {
678
+ s30: ScannerWindowCounts;
679
+ m1: ScannerWindowCounts;
680
+ m5: ScannerWindowCounts;
681
+ };
682
+ highs: ScannerHiloExtreme[];
683
+ lows: ScannerHiloExtreme[];
684
+ }
685
+
686
+ export interface ScannerFlowEvent {
687
+ id: string;
688
+ at: number;
689
+ underlying: string;
690
+ contract: string;
691
+ right: "C" | "P";
692
+ strike: number;
693
+ expiry: string;
694
+ side: "ask" | "bid" | "mid" | "unknown";
695
+ kind: "sweep" | "block" | "split" | "trade";
696
+ size: number;
697
+ price: number;
698
+ premium: number;
699
+ volume?: number | null;
700
+ openInterest?: number | null;
701
+ volOi?: number | null;
702
+ iv?: number | null;
703
+ }
704
+
705
+ export interface ScannerFlowPayload extends ScannerAccessInfo {
706
+ status: ScannerStatus;
707
+ asOf: number;
708
+ events: ScannerFlowEvent[];
709
+ }
710
+
711
+ export type ScannerPayload = ScannerHiloPayload | ScannerFlowPayload;
712
+
713
+ /** What a scanner subscriber receives: data, or the entitlement refusal. */
714
+ export type ScannerFeedEvent<T extends ScannerPayload = ScannerPayload> =
715
+ | { type: "data"; payload: T }
716
+ | { type: "denied"; reason: string };
717
+
579
718
  export interface QuoteStreamTarget {
580
719
  symbol: string;
581
720
  exchange?: string;
@@ -26,7 +26,10 @@ afterEach(async () => {
26
26
  }
27
27
  });
28
28
 
29
- function createRegistry(shortcutExecute?: () => void): PluginRegistry {
29
+ function createRegistry(
30
+ shortcutExecute?: () => void,
31
+ onShowPane?: (paneId: string) => void,
32
+ ): PluginRegistry {
30
33
  return {
31
34
  shortcuts: new Map(shortcutExecute
32
35
  ? [["test-shortcut", { id: "test-shortcut", key: "x", execute: shortcutExecute }]]
@@ -39,6 +42,7 @@ function createRegistry(shortcutExecute?: () => void): PluginRegistry {
39
42
  ]),
40
43
  getPluginPaneIds: () => [],
41
44
  getShortcutPluginId: () => null,
45
+ showPane: onShowPane ?? (() => {}),
42
46
  } as unknown as PluginRegistry;
43
47
  }
44
48
 
@@ -96,12 +100,28 @@ async function renderHarness(
96
100
  });
97
101
  }
98
102
 
99
- async function emitKeypress(event: { name?: string; ctrl?: boolean; meta?: boolean; super?: boolean; shift?: boolean }) {
103
+ /** The OpenTUI input host derives `targetEditable` from the focused editor. */
104
+ function focusEditor() {
105
+ Object.defineProperty(testSetup!.renderer, "currentFocusedEditor", {
106
+ configurable: true,
107
+ get: () => ({}),
108
+ });
109
+ }
110
+
111
+ async function emitKeypress(event: {
112
+ name?: string;
113
+ ctrl?: boolean;
114
+ meta?: boolean;
115
+ super?: boolean;
116
+ shift?: boolean;
117
+ alt?: boolean;
118
+ }) {
100
119
  const keyEvent = {
101
120
  ctrl: false,
102
121
  meta: false,
103
122
  super: false,
104
123
  option: false,
124
+ alt: false,
105
125
  shift: false,
106
126
  eventType: "press",
107
127
  repeated: false,
@@ -164,17 +184,20 @@ describe("useAppGlobalShortcuts", () => {
164
184
  expect(event.propagationStopped).toBe(true);
165
185
  });
166
186
 
167
- test("switches saved layouts with Ctrl-number and consumes the shortcut", async () => {
168
- const actions: AppAction[] = [];
169
- const config = createDefaultConfig("/tmp/gloomberb-global-shortcuts-layouts");
187
+ function layoutState(suffix: string, options: { commandBarOpen?: boolean } = {}) {
188
+ const config = createDefaultConfig(`/tmp/gloomberb-global-shortcuts-${suffix}`);
170
189
  config.layouts = [
171
190
  { name: "One", layout: cloneLayout(config.layout) },
172
191
  { name: "Two", layout: cloneLayout(config.layout) },
173
192
  { name: "Three", layout: cloneLayout(config.layout) },
174
193
  ];
175
194
  config.activeLayoutIndex = 0;
176
- const state = createInitialState(config);
177
- await renderHarness(state, createRegistry(), (action) => actions.push(action));
195
+ return { ...createInitialState(config), ...options };
196
+ }
197
+
198
+ test("switches saved layouts with Ctrl-number and consumes the shortcut", async () => {
199
+ const actions: AppAction[] = [];
200
+ await renderHarness(layoutState("layouts"), createRegistry(), (action) => actions.push(action));
178
201
 
179
202
  const event = await emitKeypress({ name: "2", ctrl: true });
180
203
 
@@ -183,6 +206,72 @@ describe("useAppGlobalShortcuts", () => {
183
206
  expect(event.propagationStopped).toBe(true);
184
207
  });
185
208
 
209
+ // Browsers and the desktop webview report Cmd as meta; the OpenTUI host maps
210
+ // the kitty `super` modifier onto the same field.
211
+ test("switches saved layouts with the Cmd-number reported by web and kitty hosts", async () => {
212
+ const actions: AppAction[] = [];
213
+ await renderHarness(layoutState("layouts-meta"), createRegistry(), (action) => actions.push(action));
214
+
215
+ const event = await emitKeypress({ name: "3", super: true });
216
+
217
+ expect(actions).toEqual([{ type: "SWITCH_LAYOUT", index: 2 }]);
218
+ expect(event.defaultPrevented).toBe(true);
219
+ expect(event.propagationStopped).toBe(true);
220
+ });
221
+
222
+ // Alt-digit keeps its terminal meaning; only the primary modifier switches.
223
+ test("ignores Alt-number", async () => {
224
+ const actions: AppAction[] = [];
225
+ await renderHarness(layoutState("layouts-alt"), createRegistry(), (action) => actions.push(action));
226
+
227
+ const event = await emitKeypress({ name: "2", alt: true });
228
+
229
+ expect(actions).toEqual([]);
230
+ expect(event.defaultPrevented).toBe(false);
231
+ });
232
+
233
+ // Leaving the digit unclaimed lets the desktop webview treat Cmd-digit as its
234
+ // own browser tab shortcut, which navigates the app away.
235
+ test("consumes layout numbers while the command bar is open without switching", async () => {
236
+ const actions: AppAction[] = [];
237
+ await renderHarness(
238
+ layoutState("layouts-command-bar", { commandBarOpen: true }),
239
+ createRegistry(),
240
+ (action) => actions.push(action),
241
+ );
242
+
243
+ const event = await emitKeypress({ name: "2", ctrl: true });
244
+
245
+ expect(actions).toEqual([]);
246
+ expect(event.defaultPrevented).toBe(true);
247
+ expect(event.propagationStopped).toBe(true);
248
+ });
249
+
250
+ test("consumes primary-modifier numbers with only one layout", async () => {
251
+ const actions: AppAction[] = [];
252
+ const config = createDefaultConfig("/tmp/gloomberb-global-shortcuts-one-layout");
253
+ const state = { ...createInitialState(config), commandBarOpen: true };
254
+ await renderHarness(state, createRegistry(), (action) => actions.push(action));
255
+
256
+ const event = await emitKeypress({ name: "1", super: true });
257
+
258
+ expect(actions).toEqual([]);
259
+ expect(event.defaultPrevented).toBe(true);
260
+ expect(event.propagationStopped).toBe(true);
261
+ });
262
+
263
+ test("consumes layout numbers while an editable field owns the keyboard", async () => {
264
+ const actions: AppAction[] = [];
265
+ await renderHarness(layoutState("layouts-editable"), createRegistry(), (action) => actions.push(action));
266
+ focusEditor();
267
+
268
+ const event = await emitKeypress({ name: "2", super: true });
269
+
270
+ expect(actions).toEqual([]);
271
+ expect(event.defaultPrevented).toBe(true);
272
+ expect(event.propagationStopped).toBe(true);
273
+ });
274
+
186
275
  test("does not run plain plugin shortcuts while input is captured", async () => {
187
276
  let executed = 0;
188
277
  const actions: AppAction[] = [];
@@ -202,6 +291,54 @@ describe("useAppGlobalShortcuts", () => {
202
291
  expect(event.propagationStopped).toBe(false);
203
292
  });
204
293
 
294
+ test("opens Help with question mark after the command bar is closed", async () => {
295
+ const openedPanes: string[] = [];
296
+ const actions: AppAction[] = [];
297
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-global-shortcuts-help"));
298
+ await renderHarness(state, createRegistry(undefined, (paneId) => openedPanes.push(paneId)), (action) => actions.push(action));
299
+
300
+ const event = await emitKeypress({ name: "?", shift: true });
301
+
302
+ expect(openedPanes).toEqual(["help"]);
303
+ expect(actions).toEqual([]);
304
+ expect(event.defaultPrevented).toBe(true);
305
+ expect(event.propagationStopped).toBe(true);
306
+ });
307
+
308
+ test("does not open Help with question mark while using the command bar", async () => {
309
+ const openedPanes: string[] = [];
310
+ const actions: AppAction[] = [];
311
+ const config = createDefaultConfig("/tmp/gloomberb-global-shortcuts-help-guard");
312
+ const commandBarState = {
313
+ ...createInitialState(config),
314
+ commandBarOpen: true,
315
+ };
316
+ await renderHarness(commandBarState, createRegistry(undefined, (paneId) => openedPanes.push(paneId)), (action) => actions.push(action));
317
+
318
+ let event = await emitKeypress({ name: "?", shift: true });
319
+ expect(openedPanes).toEqual([]);
320
+ expect(actions).toEqual([]);
321
+ expect(event.defaultPrevented).toBe(false);
322
+ expect(event.propagationStopped).toBe(false);
323
+ });
324
+
325
+ test("does not open Help with question mark while typing", async () => {
326
+ const openedPanes: string[] = [];
327
+ const actions: AppAction[] = [];
328
+ const config = createDefaultConfig("/tmp/gloomberb-global-shortcuts-help-input");
329
+ const inputCapturedState = {
330
+ ...createInitialState(config),
331
+ inputCaptured: true,
332
+ };
333
+ await renderHarness(inputCapturedState, createRegistry(undefined, (paneId) => openedPanes.push(paneId)), (action) => actions.push(action));
334
+
335
+ const event = await emitKeypress({ name: "?", shift: true });
336
+ expect(openedPanes).toEqual([]);
337
+ expect(actions).toEqual([]);
338
+ expect(event.defaultPrevented).toBe(false);
339
+ expect(event.propagationStopped).toBe(false);
340
+ });
341
+
205
342
  test("cycles panes with Tab while input is captured", async () => {
206
343
  const actions: AppAction[] = [];
207
344
  const state = {
@@ -218,8 +355,6 @@ describe("useAppGlobalShortcuts", () => {
218
355
  "portfolio-list:main",
219
356
  "chat:main",
220
357
  "ticker-detail:main",
221
- "ticker-detail:nvda",
222
- "help:main",
223
358
  ],
224
359
  }]);
225
360
  expect(event.defaultPrevented).toBe(true);