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,4 +1,3 @@
1
- import { hostname } from "os";
2
1
  import type { SecFilingItem } from "../types/data-provider";
3
2
 
4
3
  const LOOKUP_URL = "https://www.sec.gov/files/company_tickers_exchange.json";
@@ -19,14 +18,28 @@ function extractEmail(value: string | undefined): string | null {
19
18
  return match?.[0] ?? null;
20
19
  }
21
20
 
21
+ function getEnv(name: string): string | undefined {
22
+ return typeof process !== "undefined" ? process.env[name] : undefined;
23
+ }
24
+
25
+ function getRuntimeHostName(): string {
26
+ const maybeLocation = (globalThis as { location?: { hostname?: string } }).location;
27
+ if (maybeLocation?.hostname) {
28
+ return maybeLocation.hostname;
29
+ }
30
+ return "localhost";
31
+ }
32
+
33
+ const runtimeHostName = getRuntimeHostName();
34
+
22
35
  const DEFAULT_SEC_FROM =
23
- process.env.SEC_FROM_EMAIL?.trim()
24
- || extractEmail(process.env.SEC_USER_AGENT)
25
- || `${sanitizeIdentityPart(process.env.USER ?? "gloomberb", "gloomberb")}@${sanitizeIdentityPart(`${hostname()}.local`, "localhost.localdomain")}`;
36
+ getEnv("SEC_FROM_EMAIL")?.trim()
37
+ || extractEmail(getEnv("SEC_USER_AGENT"))
38
+ || `${sanitizeIdentityPart(getEnv("USER") ?? "gloomberb", "gloomberb")}@${sanitizeIdentityPart(`${runtimeHostName}.local`, "localhost.localdomain")}`;
26
39
 
27
40
  export const DEFAULT_SEC_USER_AGENT =
28
- process.env.SEC_USER_AGENT?.trim()
29
- || `Gloomberb/0.1 (${sanitizeIdentityPart(hostname(), "localhost")}; contact=${DEFAULT_SEC_FROM})`;
41
+ getEnv("SEC_USER_AGENT")?.trim()
42
+ || `Gloomberb/0.1 (${sanitizeIdentityPart(runtimeHostName, "localhost")}; contact=${DEFAULT_SEC_FROM})`;
30
43
 
31
44
  type LookupEntry = {
32
45
  cik: string;
@@ -12,11 +12,6 @@ describe("YahooFinanceClient exchange aliases", () => {
12
12
  expect(provider.getSymbolsToTry("HY9H", "FWB2")).toEqual(["HY9H.F", "HY9H.DE"]);
13
13
  });
14
14
 
15
- test("reports its manual chart resolution capabilities", () => {
16
- const provider = new YahooFinanceClient();
17
- expect(provider.getChartResolutionCapabilities()).toEqual(["5m", "15m", "1h", "1d", "1wk", "1mo"]);
18
- });
19
-
20
15
  test("maps manual resolution requests to yahoo chart range plus interval", async () => {
21
16
  const provider = new YahooFinanceClient() as any;
22
17
  let request: { symbol: string; range: string; interval: string } | null = null;
@@ -1,4 +1,4 @@
1
- import type { Quote, Fundamentals, FinancialStatement, PricePoint, TickerFinancials, MarketState, OptionContract, OptionsChain, CompanyProfile } from "../types/financials";
1
+ import type { Quote, Fundamentals, FinancialStatement, PricePoint, TickerFinancials, MarketState, OptionContract, OptionsChain, CompanyProfile, HolderData, HolderRecord, AnalystResearchData, AnalystEstimateRecord } from "../types/financials";
2
2
  import type { DataProvider, EarningsEvent, MarketDataRequestContext, NewsItem, SecFilingItem } from "../types/data-provider";
3
3
  import type { TimeRange } from "../components/chart/chart-types";
4
4
  import {
@@ -146,6 +146,63 @@ type TimeseriesResponse = { timeseries?: { result?: Array<Record<string, any>>;
146
146
  type QuoteSummaryResponse = {
147
147
  quoteSummary?: {
148
148
  result?: Array<{
149
+ price?: {
150
+ symbol?: string;
151
+ currency?: string;
152
+ shortName?: string;
153
+ longName?: string;
154
+ exchangeName?: string;
155
+ };
156
+ financialData?: {
157
+ currentPrice?: { raw?: number } | number | null;
158
+ targetHighPrice?: { raw?: number } | number | null;
159
+ targetLowPrice?: { raw?: number } | number | null;
160
+ targetMeanPrice?: { raw?: number } | number | null;
161
+ targetMedianPrice?: { raw?: number } | number | null;
162
+ recommendationMean?: { raw?: number } | number | null;
163
+ };
164
+ recommendationTrend?: {
165
+ trend?: Array<{
166
+ period?: string;
167
+ strongBuy?: number;
168
+ buy?: number;
169
+ hold?: number;
170
+ sell?: number;
171
+ strongSell?: number;
172
+ }>;
173
+ };
174
+ upgradeDowngradeHistory?: {
175
+ history?: Array<{
176
+ epochGradeDate?: number;
177
+ firm?: string;
178
+ action?: string;
179
+ priceTargetAction?: string;
180
+ toGrade?: string;
181
+ fromGrade?: string;
182
+ }>;
183
+ };
184
+ earningsTrend?: {
185
+ trend?: Array<{
186
+ period?: string;
187
+ endDate?: string;
188
+ earningsEstimate?: {
189
+ avg?: { raw?: number } | number | null;
190
+ low?: { raw?: number } | number | null;
191
+ high?: { raw?: number } | number | null;
192
+ yearAgoEps?: { raw?: number } | number | null;
193
+ numberOfAnalysts?: { raw?: number } | number | null;
194
+ growth?: { raw?: number } | number | null;
195
+ };
196
+ revenueEstimate?: {
197
+ avg?: { raw?: number } | number | null;
198
+ low?: { raw?: number } | number | null;
199
+ high?: { raw?: number } | number | null;
200
+ yearAgoRevenue?: { raw?: number } | number | null;
201
+ numberOfAnalysts?: { raw?: number } | number | null;
202
+ growth?: { raw?: number } | number | null;
203
+ };
204
+ }>;
205
+ };
149
206
  assetProfile?: {
150
207
  longBusinessSummary?: string;
151
208
  sector?: string;
@@ -161,10 +218,28 @@ type QuoteSummaryResponse = {
161
218
  dayHigh?: { raw?: number } | number | null;
162
219
  dayLow?: { raw?: number } | number | null;
163
220
  };
221
+ majorHoldersBreakdown?: {
222
+ insidersPercentHeld?: { raw?: number } | number | null;
223
+ institutionsPercentHeld?: { raw?: number } | number | null;
224
+ institutionsFloatPercentHeld?: { raw?: number } | number | null;
225
+ institutionsCount?: { raw?: number } | number | null;
226
+ };
227
+ institutionOwnership?: {
228
+ ownershipList?: Array<{
229
+ organization?: string;
230
+ reportDate?: { raw?: number; fmt?: string } | number | string | null;
231
+ position?: { raw?: number } | number | null;
232
+ value?: { raw?: number } | number | null;
233
+ pctHeld?: { raw?: number } | number | null;
234
+ pctChange?: { raw?: number } | number | null;
235
+ }>;
236
+ };
164
237
  }>;
165
238
  error?: { description?: string } | null;
166
239
  };
167
240
  };
241
+ type YahooQuoteSummaryResult = NonNullable<NonNullable<QuoteSummaryResponse["quoteSummary"]>["result"]>[number];
242
+ type YahooEarningsTrend = NonNullable<NonNullable<YahooQuoteSummaryResult["earningsTrend"]>["trend"]>[number];
168
243
 
169
244
  const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
170
245
 
@@ -195,6 +270,168 @@ function financeRawNumber(value: unknown): number | undefined {
195
270
  return undefined;
196
271
  }
197
272
 
273
+ function financeRawNumberOrNull(value: unknown): number | null {
274
+ return financeRawNumber(value) ?? null;
275
+ }
276
+
277
+ function hasAnalystResearchValue(data: AnalystResearchData): boolean {
278
+ return !!data.priceTarget
279
+ || data.recommendations.length > 0
280
+ || data.ratings.length > 0
281
+ || data.earningsEstimates.length > 0
282
+ || data.revenueEstimates.length > 0;
283
+ }
284
+
285
+ function normalizeYahooRecommendationPeriod(period?: string): string {
286
+ switch (period) {
287
+ case "0m":
288
+ return "current month";
289
+ case "-1m":
290
+ return "previous month";
291
+ case "-2m":
292
+ return "previous 2 months";
293
+ case "-3m":
294
+ return "previous 3 months";
295
+ case "0q":
296
+ return "current quarter";
297
+ case "+1q":
298
+ return "next quarter";
299
+ case "0y":
300
+ return "current year";
301
+ case "+1y":
302
+ return "next year";
303
+ default:
304
+ return period ?? "";
305
+ }
306
+ }
307
+
308
+ function normalizeYahooRatingAction(action?: string, priceTargetAction?: string): string | undefined {
309
+ const targetAction = priceTargetAction?.trim();
310
+ switch ((action ?? "").toLowerCase()) {
311
+ case "up":
312
+ return "Upgrade";
313
+ case "down":
314
+ return "Downgrade";
315
+ case "init":
316
+ return "Initiated";
317
+ case "reit":
318
+ return targetAction || "Reiterated";
319
+ case "main":
320
+ return targetAction || "Maintained";
321
+ default:
322
+ return action?.trim() || undefined;
323
+ }
324
+ }
325
+
326
+ function yahooRecommendationMeanToRating(value: number | undefined): number | undefined {
327
+ if (value == null) return undefined;
328
+ return Math.max(0, Math.min(10, ((5 - value) / 4) * 10));
329
+ }
330
+
331
+ function mapYahooEstimate(
332
+ trend: YahooEarningsTrend,
333
+ estimate: unknown,
334
+ yearAgoKey: "yearAgoEps" | "yearAgoRevenue",
335
+ ): AnalystEstimateRecord | null {
336
+ if (!estimate || typeof estimate !== "object") return null;
337
+ const record = estimate as Record<string, unknown>;
338
+ const average = financeRawNumber(record.avg);
339
+ const low = financeRawNumber(record.low);
340
+ const high = financeRawNumber(record.high);
341
+ const analysts = financeRawNumber(record.numberOfAnalysts);
342
+ const yearAgo = financeRawNumber(record[yearAgoKey]);
343
+ const growth = financeRawNumber(record.growth);
344
+ if (
345
+ average == null
346
+ && low == null
347
+ && high == null
348
+ && analysts == null
349
+ && yearAgo == null
350
+ && growth == null
351
+ ) {
352
+ return null;
353
+ }
354
+ return {
355
+ date: trend.endDate ?? "",
356
+ period: normalizeYahooRecommendationPeriod(trend.period),
357
+ analysts,
358
+ average,
359
+ low,
360
+ high,
361
+ yearAgo,
362
+ growth,
363
+ };
364
+ }
365
+
366
+ function mapYahooAnalystResearchResponse(
367
+ result: YahooQuoteSummaryResult,
368
+ fallbackSymbol: string,
369
+ ): AnalystResearchData {
370
+ const financialData = result.financialData;
371
+ const targetHigh = financeRawNumber(financialData?.targetHighPrice);
372
+ const targetLow = financeRawNumber(financialData?.targetLowPrice);
373
+ const targetMean = financeRawNumber(financialData?.targetMeanPrice);
374
+ const targetMedian = financeRawNumber(financialData?.targetMedianPrice);
375
+ const currentPrice = financeRawNumber(financialData?.currentPrice);
376
+ const trend = result.earningsTrend?.trend ?? [];
377
+
378
+ return {
379
+ providerId: "yahoo",
380
+ symbol: result.price?.symbol ?? fallbackSymbol,
381
+ name: result.price?.shortName ?? result.price?.longName,
382
+ currency: result.price?.currency,
383
+ exchange: result.price?.exchangeName,
384
+ priceTarget: targetHigh != null || targetLow != null || targetMean != null || targetMedian != null
385
+ ? {
386
+ high: targetHigh,
387
+ median: targetMedian,
388
+ low: targetLow,
389
+ average: targetMean,
390
+ current: currentPrice,
391
+ currency: result.price?.currency,
392
+ }
393
+ : undefined,
394
+ recommendationRating: yahooRecommendationMeanToRating(financeRawNumber(financialData?.recommendationMean)),
395
+ recommendations: (result.recommendationTrend?.trend ?? [])
396
+ .filter((row) => row.period)
397
+ .map((row) => ({
398
+ period: normalizeYahooRecommendationPeriod(row.period),
399
+ strongBuy: row.strongBuy,
400
+ buy: row.buy,
401
+ hold: row.hold,
402
+ sell: row.sell,
403
+ strongSell: row.strongSell,
404
+ })),
405
+ ratings: (result.upgradeDowngradeHistory?.history ?? [])
406
+ .map((rating): AnalystResearchData["ratings"][number] | null => {
407
+ const firm = rating.firm?.trim();
408
+ const date = rating.epochGradeDate
409
+ ? new Date(rating.epochGradeDate * 1000).toISOString().slice(0, 10)
410
+ : "";
411
+ if (!firm || !date) return null;
412
+ const action = normalizeYahooRatingAction(rating.action, rating.priceTargetAction);
413
+ const current = rating.toGrade?.trim();
414
+ const prior = rating.fromGrade?.trim();
415
+ return {
416
+ date,
417
+ firm,
418
+ ...(action ? { action } : {}),
419
+ ...(current ? { current } : {}),
420
+ ...(prior ? { prior } : {}),
421
+ };
422
+ })
423
+ .filter((rating): rating is AnalystResearchData["ratings"][number] => rating !== null)
424
+ .sort((left, right) => right.date.localeCompare(left.date))
425
+ .slice(0, 100),
426
+ earningsEstimates: trend
427
+ .map((row) => mapYahooEstimate(row, row.earningsEstimate, "yearAgoEps"))
428
+ .filter((estimate): estimate is AnalystEstimateRecord => estimate !== null),
429
+ revenueEstimates: trend
430
+ .map((row) => mapYahooEstimate(row, row.revenueEstimate, "yearAgoRevenue"))
431
+ .filter((estimate): estimate is AnalystEstimateRecord => estimate !== null),
432
+ };
433
+ }
434
+
198
435
  function normalizePositiveMarketValue(value: number | undefined, divisor = 1): number | undefined {
199
436
  if (value == null || !Number.isFinite(value) || value <= 0) return undefined;
200
437
  return value / divisor;
@@ -205,6 +442,39 @@ function normalizeMarketValue(value: number | undefined, divisor = 1): number |
205
442
  return value / divisor;
206
443
  }
207
444
 
445
+ function yahooRawDateTime(value: unknown): Date | null {
446
+ const raw = financeRawNumber(value);
447
+ if (raw == null) return null;
448
+ const date = new Date(raw * 1000);
449
+ return Number.isNaN(date.getTime()) ? null : date;
450
+ }
451
+
452
+ function inferEarningsTiming(date: Date): EarningsEvent["timing"] {
453
+ const hour = date.getUTCHours();
454
+ if (hour >= 20) return "AMC";
455
+ if (hour <= 13) return "BMO";
456
+ return "";
457
+ }
458
+
459
+ function yahooRawDate(value: unknown): string | undefined {
460
+ if (value && typeof value === "object") {
461
+ const fmt = (value as { fmt?: unknown }).fmt;
462
+ if (typeof fmt === "string" && /^\d{4}-\d{2}-\d{2}$/.test(fmt)) return fmt;
463
+ }
464
+ if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
465
+ const raw = financeRawNumber(value);
466
+ if (raw == null) return undefined;
467
+ const date = new Date(raw * 1000);
468
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString().slice(0, 10);
469
+ }
470
+
471
+ function deriveShareChange(position: number | undefined, changePercent: number | undefined): number | undefined {
472
+ if (position == null || changePercent == null || !Number.isFinite(position) || !Number.isFinite(changePercent)) return undefined;
473
+ const denominator = 1 + changePercent;
474
+ if (denominator <= 0) return undefined;
475
+ return Math.round(position - position / denominator);
476
+ }
477
+
208
478
  function deriveMarketState(meta: NonNullable<ChartResult["meta"]>): MarketState {
209
479
  const ctp = meta.currentTradingPeriod;
210
480
  if (!ctp) return "CLOSED";
@@ -269,10 +539,7 @@ function extractExtendedHoursPrices(
269
539
 
270
540
  // Cache TTLs
271
541
  const QUOTE_TTL = 5 * 60_000; // 5 min
272
- const FUNDAMENTALS_TTL = 60 * 60_000; // 1 hour
273
542
  const HISTORY_TTL = 24 * 60 * 60_000; // 24 hours
274
- const NEWS_TTL = 15 * 60_000; // 15 min
275
- const OPTIONS_TTL = 5 * 60_000; // 5 min
276
543
  const INTRADAY_HISTORY_TTL = 5 * 60_000; // 5 min for intraday ranges
277
544
 
278
545
  // Maps TimeRange → Yahoo API { range, interval } for optimal granularity
@@ -691,7 +958,7 @@ export class YahooFinanceClient implements DataProvider {
691
958
  listingExchangeFullName: meta.fullExchangeName,
692
959
  marketState,
693
960
  sessionConfidence: "derived",
694
- dataSource: "yahoo",
961
+ dataSource: "delayed",
695
962
  ...quoteSupplement,
696
963
  ...extHours,
697
964
  };
@@ -833,7 +1100,7 @@ export class YahooFinanceClient implements DataProvider {
833
1100
  listingExchangeFullName: meta.fullExchangeName,
834
1101
  marketState,
835
1102
  sessionConfidence: "derived",
836
- dataSource: "yahoo",
1103
+ dataSource: "delayed",
837
1104
  ...quoteSupplement,
838
1105
  ...extHours,
839
1106
  };
@@ -910,6 +1177,95 @@ export class YahooFinanceClient implements DataProvider {
910
1177
  }
911
1178
  }
912
1179
 
1180
+ async getHolders(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<HolderData> {
1181
+ const symbolsToTry = this.getSymbolsToTry(ticker, exchange);
1182
+ let lastError: any;
1183
+
1184
+ for (const symbol of symbolsToTry) {
1185
+ try {
1186
+ const params = new URLSearchParams({
1187
+ modules: "price,majorHoldersBreakdown,institutionOwnership",
1188
+ });
1189
+ const url = `https://query1.finance.yahoo.com/v10/finance/quoteSummary/${encodeURIComponent(symbol)}?${params}`;
1190
+ const data = await this.fetchJsonWithCrumb<QuoteSummaryResponse>(`holders ${symbol}`, url);
1191
+ const result = data.quoteSummary?.result?.[0];
1192
+ if (!result) throw new Error(`No holder data for ${symbol}`);
1193
+
1194
+ const holders: HolderRecord[] = (result.institutionOwnership?.ownershipList ?? [])
1195
+ .map((item): HolderRecord | null => {
1196
+ const name = item.organization?.trim();
1197
+ if (!name) return null;
1198
+ const shares = financeRawNumber(item.position);
1199
+ const changePercent = financeRawNumber(item.pctChange);
1200
+ return {
1201
+ providerId: this.id,
1202
+ ownerType: "institution",
1203
+ name,
1204
+ reportDate: yahooRawDate(item.reportDate),
1205
+ shares,
1206
+ value: financeRawNumber(item.value),
1207
+ percentHeld: financeRawNumber(item.pctHeld),
1208
+ changePercent,
1209
+ changeShares: deriveShareChange(shares, changePercent),
1210
+ };
1211
+ })
1212
+ .filter((holder): holder is HolderRecord => holder !== null);
1213
+ const asOf = holders
1214
+ .map((holder) => holder.reportDate)
1215
+ .filter((date): date is string => !!date)
1216
+ .sort()
1217
+ .at(-1);
1218
+
1219
+ return {
1220
+ providerId: this.id,
1221
+ symbol: result.price?.symbol ?? symbol,
1222
+ name: result.price?.shortName ?? result.price?.longName,
1223
+ currency: result.price?.currency,
1224
+ exchange: result.price?.exchangeName,
1225
+ asOf,
1226
+ summary: {
1227
+ insidersPercentHeld: financeRawNumber(result.majorHoldersBreakdown?.insidersPercentHeld),
1228
+ institutionsPercentHeld: financeRawNumber(result.majorHoldersBreakdown?.institutionsPercentHeld),
1229
+ institutionsFloatPercentHeld: financeRawNumber(result.majorHoldersBreakdown?.institutionsFloatPercentHeld),
1230
+ institutionsCount: financeRawNumber(result.majorHoldersBreakdown?.institutionsCount),
1231
+ },
1232
+ holders,
1233
+ };
1234
+ } catch (err) {
1235
+ lastError = err;
1236
+ }
1237
+ }
1238
+
1239
+ throw lastError || new Error(`No holder data for ${ticker}`);
1240
+ }
1241
+
1242
+ async getAnalystResearch(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<AnalystResearchData> {
1243
+ const symbolsToTry = this.getSymbolsToTry(ticker, exchange);
1244
+ let firstEmpty: AnalystResearchData | null = null;
1245
+ let lastError: any;
1246
+
1247
+ for (const symbol of symbolsToTry) {
1248
+ try {
1249
+ const params = new URLSearchParams({
1250
+ modules: "price,financialData,recommendationTrend,upgradeDowngradeHistory,earningsTrend",
1251
+ });
1252
+ const url = `https://query1.finance.yahoo.com/v10/finance/quoteSummary/${encodeURIComponent(symbol)}?${params}`;
1253
+ const data = await this.fetchJsonWithCrumb<QuoteSummaryResponse>(`analyst research ${symbol}`, url);
1254
+ const result = data.quoteSummary?.result?.[0];
1255
+ if (!result) throw new Error(`No analyst data for ${symbol}`);
1256
+
1257
+ const research = mapYahooAnalystResearchResponse(result, symbol);
1258
+ if (hasAnalystResearchValue(research)) return research;
1259
+ firstEmpty ??= research;
1260
+ } catch (err) {
1261
+ lastError = err;
1262
+ }
1263
+ }
1264
+
1265
+ if (firstEmpty) return firstEmpty;
1266
+ throw lastError || new Error(`No analyst data for ${ticker}`);
1267
+ }
1268
+
913
1269
  async getSecFilings(ticker: string, count = 10, _exchange = "", _context?: MarketDataRequestContext): Promise<SecFilingItem[]> {
914
1270
  return this.secClient.getRecentFilings(ticker, count);
915
1271
  }
@@ -1084,15 +1440,46 @@ export class YahooFinanceClient implements DataProvider {
1084
1440
  calendarEvents?: {
1085
1441
  earnings?: {
1086
1442
  earningsDate?: Array<{ raw: number }>;
1443
+ earningsCallDate?: Array<{ raw: number }>;
1444
+ isEarningsDateEstimate?: boolean;
1087
1445
  earningsAverage?: { raw: number };
1446
+ earningsLow?: { raw: number };
1447
+ earningsHigh?: { raw: number };
1088
1448
  revenueAverage?: { raw: number };
1449
+ revenueLow?: { raw: number };
1450
+ revenueHigh?: { raw: number };
1089
1451
  };
1090
1452
  };
1091
1453
  earningsTrend?: {
1092
1454
  trend?: Array<{
1093
1455
  period: string;
1094
- earningsEstimate?: { avg?: { raw: number } };
1095
- revenueEstimate?: { avg?: { raw: number } };
1456
+ earningsEstimate?: {
1457
+ avg?: { raw: number };
1458
+ low?: { raw: number };
1459
+ high?: { raw: number };
1460
+ yearAgoEps?: { raw: number };
1461
+ numberOfAnalysts?: { raw: number };
1462
+ growth?: { raw: number };
1463
+ };
1464
+ revenueEstimate?: {
1465
+ avg?: { raw: number };
1466
+ low?: { raw: number };
1467
+ high?: { raw: number };
1468
+ yearAgoRevenue?: { raw: number };
1469
+ numberOfAnalysts?: { raw: number };
1470
+ growth?: { raw: number };
1471
+ };
1472
+ epsTrend?: {
1473
+ current?: { raw: number };
1474
+ "7daysAgo"?: { raw: number };
1475
+ "30daysAgo"?: { raw: number };
1476
+ };
1477
+ epsRevisions?: {
1478
+ upLast7days?: { raw: number };
1479
+ upLast30days?: { raw: number };
1480
+ downLast7Days?: { raw: number };
1481
+ downLast30days?: { raw: number };
1482
+ };
1096
1483
  }>;
1097
1484
  };
1098
1485
  quoteType?: {
@@ -1113,18 +1500,40 @@ export class YahooFinanceClient implements DataProvider {
1113
1500
  if (isNaN(earningsDate.getTime())) return null;
1114
1501
 
1115
1502
  const currentQtr = mod.earningsTrend?.trend?.find((t) => t.period === "0q");
1503
+ const earningsEstimate = currentQtr?.earningsEstimate;
1504
+ const revenueEstimate = currentQtr?.revenueEstimate;
1505
+ const epsTrend = currentQtr?.epsTrend;
1506
+ const epsRevisions = currentQtr?.epsRevisions;
1116
1507
  const name = mod.quoteType?.shortName || mod.quoteType?.longName || symbol;
1117
1508
 
1118
1509
  return {
1119
1510
  symbol,
1120
1511
  name,
1121
1512
  earningsDate,
1122
- epsEstimate: currentQtr?.earningsEstimate?.avg?.raw ?? cal.earningsAverage?.raw ?? null,
1513
+ earningsCallDate: yahooRawDateTime(cal.earningsCallDate?.[0]),
1514
+ isDateEstimate: cal.isEarningsDateEstimate ?? null,
1515
+ epsEstimate: financeRawNumberOrNull(earningsEstimate?.avg ?? cal.earningsAverage),
1516
+ epsLow: financeRawNumberOrNull(earningsEstimate?.low ?? cal.earningsLow),
1517
+ epsHigh: financeRawNumberOrNull(earningsEstimate?.high ?? cal.earningsHigh),
1518
+ epsYearAgo: financeRawNumberOrNull(earningsEstimate?.yearAgoEps),
1519
+ epsGrowth: financeRawNumberOrNull(earningsEstimate?.growth),
1520
+ epsAnalysts: financeRawNumberOrNull(earningsEstimate?.numberOfAnalysts),
1521
+ epsTrend7dAgo: financeRawNumberOrNull(epsTrend?.["7daysAgo"]),
1522
+ epsTrend30dAgo: financeRawNumberOrNull(epsTrend?.["30daysAgo"]),
1523
+ epsRevisionUp7d: financeRawNumberOrNull(epsRevisions?.upLast7days),
1524
+ epsRevisionUp30d: financeRawNumberOrNull(epsRevisions?.upLast30days),
1525
+ epsRevisionDown7d: financeRawNumberOrNull(epsRevisions?.downLast7Days),
1526
+ epsRevisionDown30d: financeRawNumberOrNull(epsRevisions?.downLast30days),
1123
1527
  epsActual: null,
1124
- revenueEstimate: currentQtr?.revenueEstimate?.avg?.raw ?? cal.revenueAverage?.raw ?? null,
1528
+ revenueEstimate: financeRawNumberOrNull(revenueEstimate?.avg ?? cal.revenueAverage),
1529
+ revenueLow: financeRawNumberOrNull(revenueEstimate?.low ?? cal.revenueLow),
1530
+ revenueHigh: financeRawNumberOrNull(revenueEstimate?.high ?? cal.revenueHigh),
1531
+ revenueYearAgo: financeRawNumberOrNull(revenueEstimate?.yearAgoRevenue),
1532
+ revenueGrowth: financeRawNumberOrNull(revenueEstimate?.growth),
1533
+ revenueAnalysts: financeRawNumberOrNull(revenueEstimate?.numberOfAnalysts),
1125
1534
  revenueActual: null,
1126
1535
  surprise: null,
1127
- timing: "" as const,
1536
+ timing: inferEarningsTiming(earningsDate),
1128
1537
  };
1129
1538
  }),
1130
1539
  );
@@ -1,4 +1,4 @@
1
- import { CliRenderEvents, type CliRenderer } from "@opentui/core";
1
+ import { type NativeRendererHost as CliRenderer } from "../ui";
2
2
  import { useSyncExternalStore } from "react";
3
3
  import { debugLog } from "../utils/debug-log";
4
4
 
@@ -36,15 +36,15 @@ class AppActivityController {
36
36
  };
37
37
  const handleDestroy = () => this.reset();
38
38
 
39
- renderer.on(CliRenderEvents.FOCUS, handleFocus);
40
- renderer.on(CliRenderEvents.BLUR, handleBlur);
41
- renderer.on(CliRenderEvents.DESTROY, handleDestroy);
39
+ renderer.on("focus", handleFocus);
40
+ renderer.on("blur", handleBlur);
41
+ renderer.on("destroy", handleDestroy);
42
42
 
43
43
  this.boundRenderer = renderer;
44
44
  this.teardownRenderer = () => {
45
- renderer.off(CliRenderEvents.FOCUS, handleFocus);
46
- renderer.off(CliRenderEvents.BLUR, handleBlur);
47
- renderer.off(CliRenderEvents.DESTROY, handleDestroy);
45
+ renderer.off("focus", handleFocus);
46
+ renderer.off("blur", handleBlur);
47
+ renderer.off("destroy", handleDestroy);
48
48
  if (this.boundRenderer === renderer) {
49
49
  this.boundRenderer = null;
50
50
  this.teardownRenderer = null;