gloomberb 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -13
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +186 -6
  440. package/src/utils/api-client.ts +433 -33
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,11 +1,43 @@
1
- import type { CompanyProfile, Fundamentals, PricePoint, Quote, TickerFinancials } from "../types/financials";
1
+ import type {
2
+ AnalystResearchData,
3
+ CompanyProfile,
4
+ CorporateActionsData,
5
+ Fundamentals,
6
+ HolderData,
7
+ HolderRecord,
8
+ PricePoint,
9
+ Quote,
10
+ TickerFinancials,
11
+ } from "../types/financials";
2
12
  import type { InstrumentSearchResult } from "../types/instrument";
3
13
  import { debugLog } from "./debug-log";
14
+ import { canonicalExchange, normalizeSymbol, publicTickerKey } from "./exchanges";
4
15
  import { normalizeTimestamp } from "./timestamp";
5
16
 
6
17
  const DEFAULT_API_URL = "https://api.gloom.sh";
7
18
  const SESSION_COOKIE_NAMES = ["__Secure-gloomberb.session_token", "gloomberb.session_token"] as const;
8
19
  const cloudApiLog = debugLog.createLogger("cloud-api");
20
+ const HARD_SESSION_INVALID_PATTERNS = [
21
+ /\b(user|account)\b.*\b(not found|deleted|removed|disabled|deactivated|suspended)\b/i,
22
+ /\b(user|account)\b.*\bdoes(?:\s+not|n't)\s+exist\b/i,
23
+ /\b(no|unknown|missing)\s+(user|account)\b/i,
24
+ ];
25
+
26
+ type CloudApiResponse = Pick<Response, "ok" | "status" | "headers" | "text">;
27
+ type CloudApiFetchTransport = (url: string, init?: RequestInit) => Promise<CloudApiResponse>;
28
+
29
+ let cloudApiFetchTransport: CloudApiFetchTransport = (url, init) => fetch(url, init);
30
+
31
+ export function setCloudApiFetchTransport(transport: CloudApiFetchTransport | null): void {
32
+ cloudApiFetchTransport = transport ?? ((url, init) => fetch(url, init));
33
+ }
34
+
35
+ function getCloudApiBaseUrl(): string {
36
+ if (typeof process === "undefined") {
37
+ return DEFAULT_API_URL;
38
+ }
39
+ return process.env.GLOOMBERB_API_URL ?? DEFAULT_API_URL;
40
+ }
9
41
 
10
42
  export interface ChatMessage {
11
43
  id: string;
@@ -48,6 +80,24 @@ export interface CloudCompanyProfile extends CompanyProfile {}
48
80
 
49
81
  export interface CloudFundamentals extends Fundamentals {}
50
82
 
83
+ export interface CloudHolderPayload extends HolderRecord {
84
+ providerId: "gloomberb-cloud";
85
+ ownerType: "institution";
86
+ }
87
+
88
+ export interface CloudHoldersPayload extends HolderData {
89
+ providerId: "gloomberb-cloud";
90
+ holders: CloudHolderPayload[];
91
+ }
92
+
93
+ export interface CloudAnalystResearchPayload extends AnalystResearchData {
94
+ providerId: "gloomberb-cloud";
95
+ }
96
+
97
+ export interface CloudCorporateActionsPayload extends CorporateActionsData {
98
+ providerId: "gloomberb-cloud";
99
+ }
100
+
51
101
  export interface CloudPricePointPayload {
52
102
  date: string;
53
103
  open?: number;
@@ -57,6 +107,178 @@ export interface CloudPricePointPayload {
57
107
  volume?: number;
58
108
  }
59
109
 
110
+ export type CloudEconImpact = "high" | "medium" | "low";
111
+
112
+ export interface CloudEconEventPayload {
113
+ id: string;
114
+ date: string;
115
+ time: string;
116
+ country: string;
117
+ event: string;
118
+ actual: string | null;
119
+ forecast: string | null;
120
+ prior: string | null;
121
+ impact: CloudEconImpact;
122
+ }
123
+
124
+ export interface CloudFredObservationPayload {
125
+ date: string;
126
+ value: number | null;
127
+ }
128
+
129
+ export interface CloudFredSeriesInfoPayload {
130
+ id: string;
131
+ title: string;
132
+ units: string;
133
+ frequency: string;
134
+ seasonalAdjustment: string;
135
+ source: string;
136
+ notes: string;
137
+ }
138
+
139
+ export interface CloudFredSeriesPayload {
140
+ observations: CloudFredObservationPayload[];
141
+ info: CloudFredSeriesInfoPayload | null;
142
+ }
143
+
144
+ export interface CloudYieldPointPayload {
145
+ maturity: string;
146
+ maturityYears: number;
147
+ yield: number | null;
148
+ }
149
+
150
+ export interface CloudNewsEntityPayload {
151
+ id: string;
152
+ entityType: string;
153
+ name: string;
154
+ symbol: string | null;
155
+ exchange: string | null;
156
+ canonicalTicker: string | null;
157
+ role: string | null;
158
+ confidence: number | null;
159
+ }
160
+
161
+ export interface CloudNewsTickerLinkPayload {
162
+ symbol: string;
163
+ exchange: string;
164
+ canonicalTicker: string;
165
+ relationType: string;
166
+ displayTier: "primary" | "related";
167
+ confidence: number;
168
+ relevanceScore: number;
169
+ impactScore?: number;
170
+ sentiment?: "positive" | "neutral" | "negative" | null;
171
+ }
172
+
173
+ export interface CloudNewsStoryItemPayload {
174
+ id: string;
175
+ sourceKey: string;
176
+ sourceName: string;
177
+ title: string;
178
+ summary?: string;
179
+ url: string;
180
+ publishedAt: string;
181
+ hasArticleText?: boolean;
182
+ }
183
+
184
+ export interface CloudNewsPayload {
185
+ id: string;
186
+ headline: string;
187
+ summary: string;
188
+ topic?: string;
189
+ topics?: string[];
190
+ category: string;
191
+ sentiment: "positive" | "neutral" | "negative";
192
+ sectors: string[];
193
+ scope?: string;
194
+ firstPublishedAt: string;
195
+ lastPublishedAt: string;
196
+ firstSeenAt: string;
197
+ lastSeenAt: string;
198
+ primaryUrl: string;
199
+ primarySource: string;
200
+ scores?: {
201
+ importance?: number;
202
+ urgency?: number;
203
+ marketImpact?: number;
204
+ novelty?: number;
205
+ confidence?: number;
206
+ };
207
+ flags?: {
208
+ breaking?: boolean;
209
+ developing?: boolean;
210
+ stale?: boolean;
211
+ };
212
+ variantCount: number;
213
+ sourceCount: number;
214
+ sources: string[];
215
+ entities: CloudNewsEntityPayload[];
216
+ tickerLinks: CloudNewsTickerLinkPayload[];
217
+ items?: CloudNewsStoryItemPayload[];
218
+ }
219
+
220
+ export interface CloudNewsListResponse {
221
+ items: CloudNewsPayload[];
222
+ nextCursor: string | null;
223
+ }
224
+
225
+ export interface CloudTweetUserPayload {
226
+ id: string;
227
+ userName: string;
228
+ name: string;
229
+ }
230
+
231
+ export interface CloudTweetMetricsPayload {
232
+ retweets: number | null;
233
+ replies: number | null;
234
+ likes: number | null;
235
+ quotes: number | null;
236
+ views: number | null;
237
+ bookmarks: number | null;
238
+ }
239
+
240
+ export interface CloudTweetMediaPayload {
241
+ type?: string;
242
+ url?: string;
243
+ mediaUrl?: string;
244
+ media_url?: string;
245
+ media_url_https?: string;
246
+ previewImageUrl?: string;
247
+ preview_image_url?: string;
248
+ }
249
+
250
+ export interface CloudTweetPayload {
251
+ id: string;
252
+ url: string;
253
+ text: string;
254
+ createdAt: string;
255
+ lang: string;
256
+ isReply: boolean;
257
+ author: CloudTweetUserPayload;
258
+ metrics: CloudTweetMetricsPayload;
259
+ media?: CloudTweetMediaPayload[];
260
+ photos?: CloudTweetMediaPayload[];
261
+ images?: CloudTweetMediaPayload[];
262
+ }
263
+
264
+ export type CloudTweetQueryType = "Latest" | "Top";
265
+
266
+ export interface CloudTweetSearchResponse {
267
+ ticker?: string;
268
+ cashtag?: string;
269
+ query: string;
270
+ queryType: CloudTweetQueryType;
271
+ since: string;
272
+ until: string;
273
+ limit: number;
274
+ hours: number;
275
+ includeReplies?: boolean;
276
+ cached: boolean;
277
+ cacheTtlMs: number;
278
+ asOf: string;
279
+ tweets: CloudTweetPayload[];
280
+ }
281
+
60
282
  export type CloudMarketStatus =
61
283
  | "success"
62
284
  | "partial"
@@ -95,17 +317,9 @@ export interface QuoteStreamTarget {
95
317
  exchange?: string;
96
318
  }
97
319
 
98
- function normalizeSymbol(value: string): string {
99
- return value.trim().toUpperCase();
100
- }
101
-
102
- function normalizeExchange(value?: string): string {
103
- return (value ?? "").trim().toUpperCase();
104
- }
105
-
106
320
  function marketKey(symbol: string, exchange?: string): string {
107
321
  const normalizedSymbolValue = normalizeSymbol(symbol);
108
- const normalizedExchangeValue = normalizeExchange(exchange);
322
+ const normalizedExchangeValue = canonicalExchange(exchange);
109
323
  return normalizedExchangeValue ? `${normalizedSymbolValue}:${normalizedExchangeValue}` : normalizedSymbolValue;
110
324
  }
111
325
 
@@ -120,6 +334,23 @@ function normalizeChatMessages(messages: ChatMessage[]): ChatMessage[] {
120
334
  return messages.map((message) => normalizeChatMessage(message));
121
335
  }
122
336
 
337
+ function normalizeTweet(tweet: CloudTweetPayload): CloudTweetPayload {
338
+ return {
339
+ ...tweet,
340
+ createdAt: normalizeTimestamp(tweet.createdAt),
341
+ };
342
+ }
343
+
344
+ function normalizeTweetSearchResponse(response: CloudTweetSearchResponse): CloudTweetSearchResponse {
345
+ return {
346
+ ...response,
347
+ since: normalizeTimestamp(response.since),
348
+ until: normalizeTimestamp(response.until),
349
+ asOf: normalizeTimestamp(response.asOf),
350
+ tweets: response.tweets.map(normalizeTweet),
351
+ };
352
+ }
353
+
123
354
  type ChannelListener = (message: ChatMessage) => void;
124
355
  type QuoteListener = (target: QuoteStreamTarget, quote: CloudQuotePayload) => void;
125
356
  type SessionCookieName = (typeof SESSION_COOKIE_NAMES)[number];
@@ -131,6 +362,22 @@ class ApiRequestError extends Error {
131
362
  }
132
363
  }
133
364
 
365
+ function parseApiErrorMessage(body: string): string {
366
+ try {
367
+ const parsed = JSON.parse(body) as Record<string, unknown>;
368
+ const parts = [parsed.message, parsed.error, parsed.code, parsed.reason]
369
+ .filter((value): value is string => typeof value === "string" && value.trim().length > 0);
370
+ return parts.join(" ") || body;
371
+ } catch {
372
+ return body;
373
+ }
374
+ }
375
+
376
+ function isHardSessionInvalidMessage(message: string): boolean {
377
+ const normalized = message.replace(/[_-]+/g, " ");
378
+ return HARD_SESSION_INVALID_PATTERNS.some((pattern) => pattern.test(normalized));
379
+ }
380
+
134
381
  class GloomApiClient {
135
382
  private sessionToken: string | null = null;
136
383
  private sessionCookieName: SessionCookieName | null = null;
@@ -147,7 +394,7 @@ class GloomApiClient {
147
394
  private readonly quoteTargets = new Map<string, QuoteStreamTarget>();
148
395
 
149
396
  constructor() {
150
- this.baseUrl = process.env.GLOOMBERB_API_URL ?? DEFAULT_API_URL;
397
+ this.baseUrl = getCloudApiBaseUrl();
151
398
  }
152
399
 
153
400
  getSessionToken(): string | null {
@@ -213,6 +460,13 @@ class GloomApiClient {
213
460
  this.ensureSocket();
214
461
  }
215
462
 
463
+ private requireCapturedSession(message: string): void {
464
+ if (this.sessionToken) return;
465
+ this.websocketToken = null;
466
+ this.setCurrentUser(null);
467
+ throw new Error(message);
468
+ }
469
+
216
470
  private extractSessionCookie(res: Response): void {
217
471
  const setCookie = res.headers.getSetCookie?.() ?? [];
218
472
  const fallbackHeader = res.headers.get("set-cookie");
@@ -252,7 +506,7 @@ class GloomApiClient {
252
506
  this.setSessionCookieHeader(headers);
253
507
  headers.set("Origin", this.baseUrl);
254
508
 
255
- const res = await fetch(`${this.baseUrl}${path}`, {
509
+ const res = await cloudApiFetchTransport(`${this.baseUrl}${path}`, {
256
510
  ...options,
257
511
  headers,
258
512
  credentials: "include",
@@ -262,12 +516,7 @@ class GloomApiClient {
262
516
 
263
517
  if (!res.ok) {
264
518
  const body = await res.text();
265
- let msg: string;
266
- try {
267
- msg = JSON.parse(body).message ?? body;
268
- } catch {
269
- msg = body;
270
- }
519
+ const msg = parseApiErrorMessage(body);
271
520
  throw new ApiRequestError(msg, res.status);
272
521
  }
273
522
 
@@ -300,10 +549,11 @@ class GloomApiClient {
300
549
 
301
550
  const socketToken = this.getSocketAuthToken();
302
551
  if (!socketToken) return;
552
+ const usingWebSocketToken = !!this.websocketToken;
303
553
  const url = `${this.getWebSocketBaseUrl()}/cloud/ws?token=${encodeURIComponent(socketToken)}`;
304
554
  cloudApiLog.info("open websocket", {
305
555
  hasToken: !!socketToken,
306
- tokenSource: this.websocketToken ? "websocket" : "session",
556
+ tokenSource: usingWebSocketToken ? "websocket" : "session",
307
557
  quoteTargets: this.quoteTargets.size,
308
558
  channelTargets: this.channelListeners.size,
309
559
  });
@@ -321,11 +571,24 @@ class GloomApiClient {
321
571
  void this.handleSocketMessage(String(event.data));
322
572
  };
323
573
 
324
- ws.onclose = () => {
574
+ ws.onclose = (event) => {
575
+ const activeSocket = this.ws === ws;
325
576
  if (this.ws === ws) {
326
577
  this.ws = null;
327
578
  }
328
- cloudApiLog.warn("websocket closed", { quoteTargets: this.quoteTargets.size, channelTargets: this.channelListeners.size });
579
+ const closeEvent = event as CloseEvent | undefined;
580
+ cloudApiLog.warn("websocket closed", {
581
+ quoteTargets: this.quoteTargets.size,
582
+ channelTargets: this.channelListeners.size,
583
+ code: closeEvent?.code,
584
+ reason: closeEvent?.reason,
585
+ tokenSource: usingWebSocketToken ? "websocket" : "session",
586
+ });
587
+ if (activeSocket && usingWebSocketToken && this.sessionToken) {
588
+ this.websocketToken = null;
589
+ this.reconnectDelayMs = 1000;
590
+ cloudApiLog.warn("cleared websocket token after socket close; falling back to session token");
591
+ }
329
592
  if (!this.shouldKeepSocketOpen()) return;
330
593
  this.scheduleReconnect();
331
594
  };
@@ -410,6 +673,14 @@ class GloomApiClient {
410
673
 
411
674
  if (parsed?.type === "auth.unverified") {
412
675
  cloudApiLog.warn("websocket marked unverified");
676
+ if (this.websocketToken && this.sessionToken) {
677
+ this.websocketToken = null;
678
+ this.reconnectDelayMs = 1000;
679
+ cloudApiLog.warn("cleared websocket token after auth rejection; falling back to session token");
680
+ this.teardownSocket();
681
+ this.scheduleReconnect();
682
+ return;
683
+ }
413
684
  if (this.currentUser) {
414
685
  this.currentUser = { ...this.currentUser, emailVerified: false };
415
686
  }
@@ -429,7 +700,7 @@ class GloomApiClient {
429
700
  const key = marketKey(parsed.symbol, parsed.exchange);
430
701
  const target = this.quoteTargets.get(key) ?? {
431
702
  symbol: normalizeSymbol(parsed.symbol),
432
- exchange: normalizeExchange(parsed.exchange),
703
+ exchange: canonicalExchange(parsed.exchange),
433
704
  };
434
705
  for (const listener of this.quoteListeners.get(key) ?? []) {
435
706
  listener(target, parsed.quote as CloudQuotePayload);
@@ -446,43 +717,51 @@ class GloomApiClient {
446
717
  }
447
718
 
448
719
  async signUp(email: string, username: string, name: string, password: string): Promise<AuthUser> {
449
- const result = await this.request<{ user: AuthUser }>("/api/auth/api/auth/sign-up/email", {
720
+ const result = await this.request<{ user: AuthUser }>("/auth/sign-up/email", {
450
721
  method: "POST",
451
722
  body: JSON.stringify({ email, username, name, password }),
452
723
  });
724
+ this.requireCapturedSession(
725
+ "Account created, but Gloomberb could not save the login session. Please try logging in again.",
726
+ );
453
727
  this.setCurrentUser(result.user);
454
728
  return result.user;
455
729
  }
456
730
 
457
731
  async signIn(email: string, password: string): Promise<AuthUser> {
458
- const result = await this.request<{ user: AuthUser }>("/api/auth/api/auth/sign-in/email", {
732
+ const result = await this.request<{ user: AuthUser }>("/auth/sign-in/email", {
459
733
  method: "POST",
460
734
  body: JSON.stringify({ email, password }),
461
735
  });
736
+ this.requireCapturedSession(
737
+ "Logged in, but Gloomberb could not save the login session. Please try again.",
738
+ );
462
739
  this.setCurrentUser(result.user);
463
740
  return result.user;
464
741
  }
465
742
 
466
743
  async signOut(): Promise<void> {
467
- await this.request("/api/auth/api/auth/sign-out", { method: "POST" });
468
- this.sessionToken = null;
469
- this.setCurrentUser(null);
744
+ try {
745
+ await this.request("/auth/sign-out", { method: "POST" });
746
+ } finally {
747
+ this.setSessionToken(null);
748
+ }
470
749
  }
471
750
 
472
751
  async getSession(): Promise<AuthUser | null> {
473
752
  try {
474
- const result = await this.request<{ user: AuthUser }>("/api/auth/api/auth/get-session", {
753
+ const result = await this.request<{ user: AuthUser }>("/auth/get-session", {
475
754
  method: "GET",
476
755
  });
477
756
  const user = result?.user ?? null;
478
757
  this.setCurrentUser(user);
479
758
  return user;
480
759
  } catch (error) {
481
- if (!(error instanceof ApiRequestError) || (error.status !== 401 && error.status !== 403)) {
482
- throw error;
760
+ if (error instanceof ApiRequestError && isHardSessionInvalidMessage(error.message)) {
761
+ this.setSessionToken(null);
762
+ return null;
483
763
  }
484
- this.setCurrentUser(null);
485
- return null;
764
+ throw error;
486
765
  }
487
766
  }
488
767
 
@@ -576,7 +855,7 @@ class GloomApiClient {
576
855
  .map((target) => {
577
856
  const normalized = {
578
857
  symbol: normalizeSymbol(target.symbol),
579
- exchange: normalizeExchange(target.exchange),
858
+ exchange: canonicalExchange(target.exchange),
580
859
  } satisfies QuoteStreamTarget;
581
860
  return [marketKey(normalized.symbol, normalized.exchange), normalized] as const;
582
861
  }),
@@ -686,6 +965,24 @@ class GloomApiClient {
686
965
  return this.request<CloudMarketResponse<CloudFundamentals>>(`/market/fundamentals?${params.toString()}`);
687
966
  }
688
967
 
968
+ async getCloudHolders(symbol: string, exchange?: string): Promise<CloudMarketResponse<CloudHoldersPayload>> {
969
+ const params = new URLSearchParams({ symbol });
970
+ if (exchange) params.set("exchange", exchange);
971
+ return this.request<CloudMarketResponse<CloudHoldersPayload>>(`/market/holders?${params.toString()}`);
972
+ }
973
+
974
+ async getCloudAnalystResearch(symbol: string, exchange?: string): Promise<CloudMarketResponse<CloudAnalystResearchPayload>> {
975
+ const params = new URLSearchParams({ symbol });
976
+ if (exchange) params.set("exchange", exchange);
977
+ return this.request<CloudMarketResponse<CloudAnalystResearchPayload>>(`/market/analyst?${params.toString()}`);
978
+ }
979
+
980
+ async getCloudCorporateActions(symbol: string, exchange?: string): Promise<CloudMarketResponse<CloudCorporateActionsPayload>> {
981
+ const params = new URLSearchParams({ symbol });
982
+ if (exchange) params.set("exchange", exchange);
983
+ return this.request<CloudMarketResponse<CloudCorporateActionsPayload>>(`/market/corporate-actions?${params.toString()}`);
984
+ }
985
+
689
986
  async getCloudStatements(
690
987
  symbol: string,
691
988
  exchange?: string,
@@ -720,6 +1017,109 @@ class GloomApiClient {
720
1017
  const params = new URLSearchParams({ fromCurrency });
721
1018
  return this.request<CloudMarketResponse<{ rate: number }>>(`/market/exchange-rate?${params.toString()}`);
722
1019
  }
1020
+
1021
+ async getCloudEconomicCalendar(): Promise<CloudEconEventPayload[]> {
1022
+ return this.request<CloudEconEventPayload[]>("/cloud/econ/calendar");
1023
+ }
1024
+
1025
+ async getCloudFredSeries(
1026
+ seriesId: string,
1027
+ params: {
1028
+ startDate?: string;
1029
+ endDate?: string;
1030
+ limit?: number;
1031
+ sortOrder?: "asc" | "desc";
1032
+ } = {},
1033
+ ): Promise<CloudFredSeriesPayload> {
1034
+ const search = new URLSearchParams();
1035
+ if (params.startDate) search.set("startDate", params.startDate);
1036
+ if (params.endDate) search.set("endDate", params.endDate);
1037
+ if (params.limit != null) search.set("limit", String(params.limit));
1038
+ if (params.sortOrder) search.set("sortOrder", params.sortOrder);
1039
+ const qs = search.toString();
1040
+ return this.request<CloudFredSeriesPayload>(`/cloud/econ/series/${encodeURIComponent(seriesId)}${qs ? `?${qs}` : ""}`);
1041
+ }
1042
+
1043
+ async getCloudYieldCurve(): Promise<CloudYieldPointPayload[]> {
1044
+ return this.request<CloudYieldPointPayload[]>("/cloud/econ/yield-curve");
1045
+ }
1046
+
1047
+ async getCloudNews(params: {
1048
+ feed?: "latest" | "top" | "breaking" | "ticker" | "sector" | "topic";
1049
+ ticker?: string;
1050
+ exchange?: string;
1051
+ tickerTier?: "primary" | "related" | "any";
1052
+ tickerRelations?: string[];
1053
+ limit?: number;
1054
+ topics?: string[];
1055
+ categories?: string[];
1056
+ sectors?: string[];
1057
+ sources?: string[];
1058
+ excludeSources?: string[];
1059
+ sentiment?: "positive" | "neutral" | "negative";
1060
+ minImportance?: number;
1061
+ minUrgency?: number;
1062
+ breaking?: boolean;
1063
+ since?: Date;
1064
+ until?: Date;
1065
+ cursor?: string;
1066
+ } = {}): Promise<CloudNewsListResponse> {
1067
+ const search = new URLSearchParams();
1068
+ if (params.feed) search.set("feed", params.feed);
1069
+ if (params.ticker) {
1070
+ const tickerFilter = params.exchange
1071
+ ? publicTickerKey(params.ticker, params.exchange)
1072
+ : normalizeSymbol(params.ticker);
1073
+ search.set("tickers", tickerFilter);
1074
+ }
1075
+ if (params.tickerTier) search.set("tickerTier", params.tickerTier);
1076
+ if (params.tickerRelations?.length) search.set("tickerRelations", params.tickerRelations.join(","));
1077
+ if (params.limit != null) search.set("limit", String(params.limit));
1078
+ if (params.topics?.length) search.set("topics", params.topics.join(","));
1079
+ if (params.categories?.length) {
1080
+ search.set("categories", params.categories.join(","));
1081
+ }
1082
+ if (params.sectors?.length) search.set("sectors", params.sectors.join(","));
1083
+ if (params.sources?.length) search.set("sources", params.sources.join(","));
1084
+ if (params.excludeSources?.length) search.set("excludeSources", params.excludeSources.join(","));
1085
+ if (params.sentiment) search.set("sentiment", params.sentiment);
1086
+ if (params.minImportance != null) search.set("minImportance", String(params.minImportance));
1087
+ if (params.minUrgency != null) search.set("minUrgency", String(params.minUrgency));
1088
+ if (params.breaking != null) search.set("breaking", String(params.breaking));
1089
+ if (params.since) search.set("since", params.since.toISOString());
1090
+ if (params.until) search.set("until", params.until.toISOString());
1091
+ if (params.cursor) search.set("cursor", params.cursor);
1092
+ const query = search.toString();
1093
+ return this.request<CloudNewsListResponse>(`/news${query ? `?${query}` : ""}`);
1094
+ }
1095
+
1096
+ async getCloudTickerTweets(params: {
1097
+ ticker: string;
1098
+ limit?: number;
1099
+ hours?: number;
1100
+ includeReplies?: boolean;
1101
+ }): Promise<CloudTweetSearchResponse> {
1102
+ const search = new URLSearchParams({ ticker: params.ticker });
1103
+ if (params.limit != null) search.set("limit", String(params.limit));
1104
+ if (params.hours != null) search.set("hours", String(params.hours));
1105
+ if (params.includeReplies != null) search.set("includeReplies", String(params.includeReplies));
1106
+ const response = await this.request<CloudTweetSearchResponse>(`/news/tweets?${search.toString()}`);
1107
+ return normalizeTweetSearchResponse(response);
1108
+ }
1109
+
1110
+ async searchCloudTweets(params: {
1111
+ query: string;
1112
+ queryType?: CloudTweetQueryType;
1113
+ limit?: number;
1114
+ hours?: number;
1115
+ }): Promise<CloudTweetSearchResponse> {
1116
+ const search = new URLSearchParams({ query: params.query });
1117
+ if (params.queryType) search.set("queryType", params.queryType);
1118
+ if (params.limit != null) search.set("limit", String(params.limit));
1119
+ if (params.hours != null) search.set("hours", String(params.hours));
1120
+ const response = await this.request<CloudTweetSearchResponse>(`/news/tweets/search?${search.toString()}`);
1121
+ return normalizeTweetSearchResponse(response);
1122
+ }
723
1123
  }
724
1124
 
725
1125
  export const apiClient = new GloomApiClient();
@@ -25,3 +25,9 @@ export function isPlainEscape(event: BackNavigationEventLike): boolean {
25
25
  export function isBackNavigationKey(event: BackNavigationEventLike): boolean {
26
26
  return isPlainBackspace(event) || isPlainEscape(event);
27
27
  }
28
+
29
+ export function isDetailBackNavigationKey(
30
+ event: BackNavigationEventLike,
31
+ ): boolean {
32
+ return isPlainBackspace(event);
33
+ }
@@ -1,3 +1,4 @@
1
+
1
2
  export type CliAlign = "left" | "right" | "center";
2
3
 
3
4
  export interface CliTableColumn {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { hasLikelyQuoteUnitMismatch, normalizePriceValueByDivisor, resolveCurrencyUnit } from "./currency-units";
2
+ import { hasLikelyQuoteUnitMismatch, resolveCurrencyUnit } from "./currency-units";
3
3
 
4
4
  describe("currency unit helpers", () => {
5
5
  test("normalizes known sub-unit currencies to their main currency", () => {
@@ -7,11 +7,6 @@ describe("currency unit helpers", () => {
7
7
  expect(resolveCurrencyUnit("GBP")).toEqual({ currency: "GBP", divisor: 1 });
8
8
  });
9
9
 
10
- test("scales quote prices by the configured divisor", () => {
11
- expect(normalizePriceValueByDivisor(23.1, 100)).toBeCloseTo(0.231, 8);
12
- expect(normalizePriceValueByDivisor(125, 1)).toBe(125);
13
- });
14
-
15
10
  test("detects sub-unit quotes that match the same main-currency price", () => {
16
11
  expect(hasLikelyQuoteUnitMismatch(
17
12
  { price: 23.1, currency: "GBp" },
@@ -1,3 +1,4 @@
1
+
1
2
  const SUB_UNIT_CURRENCIES: Record<string, { currency: string; divisor: number }> = {
2
3
  GBp: { currency: "GBP", divisor: 100 },
3
4
  GBX: { currency: "GBP", divisor: 100 },