gloomberb 0.11.2 → 0.12.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 (531) hide show
  1. package/README.md +7 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +108 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +134 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +220 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/capabilities/chart-series.ts +3 -2
  20. package/src/cli/commands/market.ts +2 -2
  21. package/src/cli/commands/plugins.ts +39 -11
  22. package/src/cli/desktop-pane-shot.ts +285 -16
  23. package/src/cli/index.ts +1 -1
  24. package/src/cli/pane-functions/capabilities.ts +61 -31
  25. package/src/cli/pane-functions/catalog.ts +16 -4
  26. package/src/cli/pane-functions/discovery.ts +1 -0
  27. package/src/cli/pane-functions/headless.test.ts +247 -0
  28. package/src/cli/pane-functions/headless.ts +423 -0
  29. package/src/cli/pane-functions/index.test.ts +9 -0
  30. package/src/cli/pane-functions/index.ts +6 -2
  31. package/src/cli/pane-functions/options.ts +27 -2
  32. package/src/cli/pane-functions/report.ts +4 -0
  33. package/src/cli/pane-functions/resolver.ts +26 -8
  34. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  35. package/src/cli/pane-functions/screenshot.ts +236 -20
  36. package/src/cli/registry.ts +0 -22
  37. package/src/cli/restore-plugins.ts +34 -0
  38. package/src/cli/result.ts +7 -10
  39. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  40. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  41. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  42. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  43. package/src/components/chart/composite/composite-chart.tsx +301 -204
  44. package/src/components/chart/composite/format.ts +8 -2
  45. package/src/components/chart/composite/interactions.test.ts +68 -102
  46. package/src/components/chart/composite/interactions.ts +214 -264
  47. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  48. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  49. package/src/components/chart/composite/scene.ts +57 -3
  50. package/src/components/chart/composite/text-renderer.ts +3 -2
  51. package/src/components/chart/composite/time-scale.ts +138 -57
  52. package/src/components/chart/composite/types.ts +25 -1
  53. package/src/components/chart/core/data.ts +9 -13
  54. package/src/components/chart/core/date-window.ts +7 -1
  55. package/src/components/chart/core/pointer.ts +3 -0
  56. package/src/components/chart/core/types.ts +0 -45
  57. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  58. package/src/components/chart/native/kitty/index.test.ts +0 -188
  59. package/src/components/chart/native/raster/crosshair.ts +6 -5
  60. package/src/components/chart/native/raster/primitives.ts +0 -23
  61. package/src/components/chart/native/renderer-selection.ts +81 -31
  62. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  63. package/src/components/chart/static/chart-surface.tsx +150 -0
  64. package/src/components/chart/static/index.ts +8 -0
  65. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  66. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  67. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  68. package/src/components/chart/static/series.ts +49 -0
  69. package/src/components/command-bar/action-runtime.ts +1 -3
  70. package/src/components/command-bar/assist/model.test.ts +17 -2
  71. package/src/components/command-bar/assist/model.ts +40 -9
  72. package/src/components/command-bar/commands/direct/index.ts +1 -1
  73. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  74. package/src/components/command-bar/commands/registry.ts +0 -11
  75. package/src/components/command-bar/helpers.test.ts +2 -2
  76. package/src/components/command-bar/helpers.ts +2 -6
  77. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  78. package/src/components/command-bar/list/badge.test.ts +54 -0
  79. package/src/components/command-bar/list/badge.ts +62 -0
  80. package/src/components/command-bar/list/model.test.ts +103 -0
  81. package/src/components/command-bar/list/model.ts +61 -4
  82. package/src/components/command-bar/list/view.test.tsx +113 -0
  83. package/src/components/command-bar/list/view.tsx +147 -131
  84. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  85. package/src/components/command-bar/panel/body.tsx +51 -65
  86. package/src/components/command-bar/panel/index.tsx +48 -13
  87. package/src/components/command-bar/panel/layout.test.ts +139 -0
  88. package/src/components/command-bar/panel/layout.ts +65 -28
  89. package/src/components/command-bar/panel/native-surface.ts +24 -0
  90. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  91. package/src/components/command-bar/panel/runtime.ts +6 -3
  92. package/src/components/command-bar/panel/state.ts +41 -7
  93. package/src/components/command-bar/panel/types.ts +7 -1
  94. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  95. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  96. package/src/components/command-bar/routes/root/results.ts +30 -28
  97. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  98. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  99. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  100. package/src/components/command-bar/routes/root/selection.ts +1 -12
  101. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  102. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  103. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  104. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  105. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  106. package/src/components/command-bar/routing/actions.ts +1 -81
  107. package/src/components/command-bar/routing/launch-request.ts +1 -1
  108. package/src/components/command-bar/routing/list-state.ts +6 -21
  109. package/src/components/command-bar/selection-runtime.ts +1 -1
  110. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  111. package/src/components/command-bar/surface/environment.ts +8 -1
  112. package/src/components/command-bar/surface/index.test.tsx +84 -68
  113. package/src/components/command-bar/surface/index.tsx +33 -36
  114. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  115. package/src/components/command-bar/theme-picker.tsx +43 -15
  116. package/src/components/command-bar/view-model.test.ts +53 -8
  117. package/src/components/command-bar/view-model.ts +43 -14
  118. package/src/components/command-bar/workflow/tickers.ts +1 -0
  119. package/src/components/command-bar/workflow/types.ts +1 -1
  120. package/src/components/data-table/export.test.ts +21 -0
  121. package/src/components/data-table/export.ts +21 -0
  122. package/src/components/data-table/view.test.tsx +45 -6
  123. package/src/components/data-table/view.tsx +55 -9
  124. package/src/components/index.ts +10 -2
  125. package/src/components/input-search-bar.test.tsx +36 -2
  126. package/src/components/input-search-bar.tsx +18 -1
  127. package/src/components/layout/header.test.tsx +57 -10
  128. package/src/components/layout/header.tsx +292 -149
  129. package/src/components/layout/market-summary.test.ts +81 -0
  130. package/src/components/layout/market-summary.ts +109 -0
  131. package/src/components/layout/shell/chrome.ts +96 -1
  132. package/src/components/layout/shell/index.tsx +11 -3
  133. package/src/components/layout/shell/menu.ts +9 -0
  134. package/src/components/layout/shell/pane/actions.ts +29 -0
  135. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  136. package/src/components/layout/shell/shortcuts.ts +3 -0
  137. package/src/components/layout/status-bar.test.tsx +9 -8
  138. package/src/components/layout/status-bar.tsx +85 -54
  139. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  140. package/src/components/price-sparkline/index.ts +61 -34
  141. package/src/components/price-sparkline/view.tsx +1 -1
  142. package/src/components/sign-in-gate.test.tsx +128 -0
  143. package/src/components/sign-in-gate.tsx +149 -0
  144. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  145. package/src/components/ticker/badge/list.tsx +17 -3
  146. package/src/components/ticker/list-table-view.tsx +6 -3
  147. package/src/components/ui/data-table/index.tsx +22 -4
  148. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  149. package/src/components/ui/frame.tsx +28 -0
  150. package/src/components/ui/index.ts +3 -0
  151. package/src/components/ui/native-select.tsx +20 -10
  152. package/src/components/ui/select-button.tsx +138 -0
  153. package/src/components/ui/status.tsx +2 -2
  154. package/src/components/ui/table-layout.ts +1 -1
  155. package/src/components/use-double-click-activation.ts +7 -3
  156. package/src/core/app-services.ts +3 -3
  157. package/src/data/config/chart-settings.ts +4 -4
  158. package/src/data/config/layout.ts +12 -6
  159. package/src/data/config/store/index.test.ts +97 -0
  160. package/src/data/config/store/migrations.ts +41 -0
  161. package/src/data/config/store/normalize.ts +5 -3
  162. package/src/i18n/es.ts +6 -2
  163. package/src/i18n/ja.ts +6 -2
  164. package/src/i18n/ko.ts +6 -2
  165. package/src/i18n/zh-cn.ts +6 -2
  166. package/src/i18n/zh-tw.ts +6 -2
  167. package/src/layout-marketplace/gallery-terminal.tsx +126 -96
  168. package/src/layout-marketplace/gallery.test.tsx +65 -1
  169. package/src/layout-marketplace/pane-imagery.ts +1 -0
  170. package/src/layout-marketplace/payload.test.ts +17 -0
  171. package/src/layout-marketplace/payload.ts +40 -5
  172. package/src/market-data/coordinator/index.ts +3 -2
  173. package/src/market-data/coordinator/quotes.ts +173 -66
  174. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  175. package/src/pane-settings.ts +1 -1
  176. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  177. package/src/plugins/builtin/analytics/view.tsx +1 -1
  178. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  179. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  180. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  181. package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
  182. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  183. package/src/plugins/builtin/chart-composer/settings.ts +2 -13
  184. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  185. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  186. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  188. package/src/plugins/builtin/composite-plugins.ts +16 -1
  189. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  190. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  191. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  192. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  193. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  194. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  195. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  196. package/src/plugins/builtin/earnings/index.tsx +1 -0
  197. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  198. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  199. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  200. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  201. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  202. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  203. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  204. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  205. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  206. package/src/plugins/builtin/econ/index.tsx +3 -1
  207. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  208. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  209. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  210. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  211. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  212. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  213. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  214. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  215. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  216. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  217. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  218. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  219. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  220. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  221. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  222. package/src/plugins/builtin/futures/index.tsx +1 -0
  223. package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
  224. package/src/plugins/builtin/help/index.tsx +4 -0
  225. package/src/plugins/builtin/holders/index.tsx +1 -0
  226. package/src/plugins/builtin/insider/index.tsx +1 -0
  227. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
  228. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  229. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  230. package/src/plugins/builtin/market-halts/index.tsx +1 -0
  231. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  232. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  233. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  234. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  235. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  236. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  237. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  238. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  239. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  240. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  241. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  242. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  243. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  244. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  245. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  246. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  247. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  248. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  249. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  250. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  251. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  252. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  253. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  254. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  255. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  256. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  257. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  258. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  259. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  260. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  261. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  262. package/src/plugins/builtin/options/analytics.test.ts +83 -0
  263. package/src/plugins/builtin/options/analytics.ts +110 -0
  264. package/src/plugins/builtin/options/index.tsx +18 -2
  265. package/src/plugins/builtin/options/table.ts +121 -51
  266. package/src/plugins/builtin/options/types.ts +22 -15
  267. package/src/plugins/builtin/options/view.test.tsx +53 -1
  268. package/src/plugins/builtin/options/view.tsx +92 -41
  269. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  270. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  271. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  272. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  273. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  274. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  275. package/src/plugins/builtin/plugin-module.test.ts +2 -4
  276. package/src/plugins/builtin/plugin-module.ts +9 -8
  277. package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
  278. package/src/plugins/builtin/research/index.tsx +4 -0
  279. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  280. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  281. package/src/plugins/builtin/research-search/data.ts +109 -0
  282. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  283. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  284. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  285. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  286. package/src/plugins/builtin/research-search/index.tsx +85 -0
  287. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  288. package/src/plugins/builtin/research-search/model.ts +291 -0
  289. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  290. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  291. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  292. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  293. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  294. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  295. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  296. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  297. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  298. package/src/plugins/builtin/scanner/index.ts +2 -0
  299. package/src/plugins/builtin/sec/index.tsx +1 -0
  300. package/src/plugins/builtin/sectors/index.tsx +1 -0
  301. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  302. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  303. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  304. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  305. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
  306. package/src/plugins/builtin/ticker-detail/index.tsx +2 -14
  307. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  308. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  309. package/src/plugins/builtin/volatility/index.tsx +1 -1
  310. package/src/plugins/builtin/world-indices/index.tsx +1 -0
  311. package/src/plugins/builtin/world-venue-map/index.ts +27 -0
  312. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  313. package/src/plugins/builtin/world-venue-map/map.tsx +481 -0
  314. package/src/plugins/builtin/world-venue-map/model.test.ts +85 -0
  315. package/src/plugins/builtin/world-venue-map/model.ts +268 -0
  316. package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
  317. package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
  318. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  319. package/src/plugins/bundle.test.ts +109 -0
  320. package/src/plugins/bundle.ts +139 -0
  321. package/src/plugins/catalog-backend.ts +6 -44
  322. package/src/plugins/catalog-browser.test.ts +1 -0
  323. package/src/plugins/catalog-browser.ts +12 -0
  324. package/src/plugins/catalog-manifest.test.ts +43 -0
  325. package/src/plugins/catalog-manifest.ts +92 -0
  326. package/src/plugins/catalog-ui.ts +20 -4
  327. package/src/plugins/catalog.ts +9 -2
  328. package/src/plugins/current-target.ts +28 -0
  329. package/src/plugins/host-contract.ts +37 -0
  330. package/src/plugins/host-link.ts +158 -0
  331. package/src/plugins/host-modules.ts +61 -0
  332. package/src/plugins/loader.ts +41 -22
  333. package/src/plugins/ownership.ts +1 -0
  334. package/src/plugins/prediction-markets/index.tsx +1 -0
  335. package/src/plugins/registry/context.ts +3 -0
  336. package/src/plugins/registry/contributions.ts +27 -0
  337. package/src/plugins/registry/index.test.ts +25 -0
  338. package/src/plugins/registry/index.ts +8 -0
  339. package/src/plugins/registry/pane-settings.test.ts +53 -0
  340. package/src/plugins/registry/pane-settings.ts +29 -3
  341. package/src/plugins/seed.test.ts +83 -0
  342. package/src/plugins/seed.ts +80 -0
  343. package/src/public/broker.ts +31 -0
  344. package/src/public/market-data.ts +9 -0
  345. package/src/public/pane-hooks.ts +57 -0
  346. package/src/public/public-api.test.ts +285 -0
  347. package/src/public/react.ts +62 -0
  348. package/src/public/test-support.ts +23 -0
  349. package/src/public/time-series.ts +8 -0
  350. package/src/public/utils.ts +77 -0
  351. package/src/remote/layout-helpers.ts +6 -8
  352. package/src/remote/resources.ts +2 -0
  353. package/src/remote/schema.ts +1 -0
  354. package/src/remote/semantic-tree.test.tsx +105 -0
  355. package/src/remote/semantic-tree.tsx +53 -35
  356. package/src/renderers/browser/main.tsx +6 -0
  357. package/src/renderers/browser/ui-host.tsx +11 -0
  358. package/src/renderers/cloudflare/worker.test.ts +19 -0
  359. package/src/renderers/cloudflare/worker.ts +1 -1
  360. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
  361. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  362. package/src/renderers/electrobun/bun/index.ts +6 -2
  363. package/src/renderers/electrobun/shared/protocol.ts +25 -0
  364. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  365. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  366. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  367. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  368. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  369. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  370. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  371. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  372. package/src/renderers/electrobun/view/main.tsx +27 -2
  373. package/src/renderers/electrobun/view/styles.css +5 -0
  374. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  375. package/src/renderers/electrobun/view/ui-host.tsx +3 -0
  376. package/src/renderers/opentui/chart-surface.tsx +5 -1
  377. package/src/renderers/opentui/host.tsx +20 -2
  378. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  379. package/src/renderers/opentui/interaction-performance.ts +188 -0
  380. package/src/renderers/opentui/start.tsx +26 -1
  381. package/src/renderers/opentui/test-utils.tsx +13 -0
  382. package/src/renderers/opentui/toast-host.tsx +26 -2
  383. package/src/renderers/opentui/ui-host.tsx +1 -0
  384. package/src/sources/provider-router/search.test.ts +191 -0
  385. package/src/sources/provider-router/search.ts +92 -39
  386. package/src/state/app/context/index.tsx +2 -0
  387. package/src/state/app/context/selector.test.tsx +33 -1
  388. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  389. package/src/state/hooks/quote-streaming.ts +106 -30
  390. package/src/state/pane-table-export-registry.ts +50 -0
  391. package/src/theme/color-utils.ts +1 -1
  392. package/src/theme/colors.ts +50 -0
  393. package/src/theme/themes.test.ts +15 -2
  394. package/src/theme/themes.ts +19 -1
  395. package/src/tickers/search/index.test.ts +26 -0
  396. package/src/tickers/search/index.ts +42 -17
  397. package/src/tickers/search/ranking.ts +14 -0
  398. package/src/time-series/chart-data.test.ts +216 -0
  399. package/src/time-series/range.ts +12 -0
  400. package/src/time-series/resolution.test.ts +25 -26
  401. package/src/time-series/resolution.ts +44 -25
  402. package/src/time-series/resolve.ts +7 -0
  403. package/src/time-series/spec.ts +5 -5
  404. package/src/time-series/types.ts +3 -1
  405. package/src/time-series/use-chart-resolution.ts +5 -0
  406. package/src/types/config.ts +123 -3
  407. package/src/types/data-provider.ts +11 -0
  408. package/src/types/headless.ts +161 -0
  409. package/src/types/plugin.ts +104 -20
  410. package/src/ui/host.tsx +19 -1
  411. package/src/ui/index.tsx +1 -0
  412. package/src/ui/toast.tsx +8 -4
  413. package/src/utils/csv.test.ts +13 -0
  414. package/src/utils/csv.ts +37 -0
  415. package/src/utils/format.ts +35 -0
  416. package/src/utils/fuzzy-search.test.ts +94 -0
  417. package/src/utils/fuzzy-search.ts +81 -22
  418. package/src/utils/save-text-file.test.ts +24 -0
  419. package/src/utils/save-text-file.ts +36 -0
  420. package/src/utils/shortcut-labels.ts +14 -0
  421. package/src/components/chart/core/axis-format.ts +0 -157
  422. package/src/components/chart/core/index.test.ts +0 -1075
  423. package/src/components/chart/core/projection.ts +0 -357
  424. package/src/components/chart/core/render-utils.ts +0 -4
  425. package/src/components/chart/core/renderer.ts +0 -244
  426. package/src/components/chart/core/scene.ts +0 -180
  427. package/src/components/chart/core/time-axis.ts +0 -330
  428. package/src/components/chart/cursor-motion.ts +0 -47
  429. package/src/components/chart/indicators/types.ts +0 -26
  430. package/src/components/chart/market-session.ts +0 -176
  431. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  432. package/src/components/chart/multi-line/renderer.ts +0 -161
  433. package/src/components/chart/multi-line/scene.ts +0 -188
  434. package/src/components/chart/native/raster/layout.ts +0 -129
  435. package/src/components/chart/native/raster/price-chart.ts +0 -244
  436. package/src/components/chart/rendering/chart-draw.ts +0 -270
  437. package/src/components/chart/rendering/geometry.ts +0 -95
  438. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  439. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  440. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  441. package/src/components/chart/static/chart/surface.tsx +0 -358
  442. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  443. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  444. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  445. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  446. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  447. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  448. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  449. package/src/plugins/builtin/substack/api/store.ts +0 -186
  450. package/src/plugins/builtin/substack/api/types.ts +0 -42
  451. package/src/plugins/builtin/substack/api.test.ts +0 -298
  452. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  453. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  454. package/src/plugins/builtin/substack/content.ts +0 -478
  455. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  456. package/src/plugins/builtin/substack/index.tsx +0 -49
  457. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  458. package/src/plugins/builtin/substack/model.test.ts +0 -155
  459. package/src/plugins/builtin/substack/normalize.ts +0 -343
  460. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  461. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  462. package/src/plugins/builtin/substack/pane.tsx +0 -547
  463. package/src/plugins/builtin/substack/read-state.ts +0 -30
  464. package/src/plugins/builtin/substack/table.ts +0 -111
  465. package/src/plugins/builtin/substack/types.ts +0 -71
  466. package/src/plugins/builtin/substack/utils.ts +0 -131
  467. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  468. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  469. package/src/plugins/ibkr/account-cache.ts +0 -24
  470. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  471. package/src/plugins/ibkr/config.test.ts +0 -79
  472. package/src/plugins/ibkr/config.ts +0 -250
  473. package/src/plugins/ibkr/dialogs.tsx +0 -40
  474. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  475. package/src/plugins/ibkr/flex/index.ts +0 -367
  476. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  477. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  478. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  479. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  480. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  481. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  482. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  483. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  484. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  485. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  486. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  487. package/src/plugins/ibkr/gateway/history.ts +0 -120
  488. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  489. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  490. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  491. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  492. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  493. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  494. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  495. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  496. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  497. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  498. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  499. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  500. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  501. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  502. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  503. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  504. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  505. package/src/plugins/ibkr/gateway/types.ts +0 -27
  506. package/src/plugins/ibkr/index.test.tsx +0 -162
  507. package/src/plugins/ibkr/index.tsx +0 -159
  508. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  509. package/src/plugins/ibkr/instance-selection.ts +0 -36
  510. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  511. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  512. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  513. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  514. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  515. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  516. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  517. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  518. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  519. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  520. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  521. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  522. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  523. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  524. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  525. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  526. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  527. package/src/plugins/ibkr/trade/utils.ts +0 -144
  528. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  529. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  530. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  531. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -0,0 +1,756 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import {
3
+ Button,
4
+ DataTableStackView,
5
+ EmptyState,
6
+ InputSearchBar,
7
+ Spinner,
8
+ usePaneFooter,
9
+ type DataTableCell,
10
+ type DataTableKeyEvent,
11
+ type DataTableRootKeyContext,
12
+ type PaneFooterSegment,
13
+ } from "../../../components";
14
+ import { useShortcut } from "../../../react/input";
15
+ import { CloudAuthNotice } from "../cloud/auth-actions";
16
+ import { useCloudPlanAction, useCloudUpgradeAction } from "../shared/cloud-upgrade";
17
+ import { colors } from "../../../theme/colors";
18
+ import {
19
+ Box,
20
+ Text,
21
+ useRendererHost,
22
+ type InputRenderable,
23
+ type ScrollBoxRenderable,
24
+ } from "../../../ui";
25
+ import { isPlainKey } from "../../../utils/keyboard";
26
+ import { isPlainArrowUp, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
27
+ import { useBoundTicker as useSymbolBinding } from "../shared/ticker-request";
28
+ import { usePlanAccess } from "../shared/plan-access";
29
+ import type {
30
+ CloudEarningsCallPayload,
31
+ CloudEarningsTranscriptPayload,
32
+ } from "../../../api-client";
33
+ import {
34
+ callStatusLabel,
35
+ isPendingTranscript,
36
+ loadEarningsCalls,
37
+ loadTranscript,
38
+ statusOf,
39
+ } from "./data";
40
+ import { callTitle, formatCallDate, formatDuration, formatPeriod, formatSentiment } from "./format";
41
+ import { TranscriptView, type ReaderTab } from "./transcript-view";
42
+
43
+ export const EARNINGS_CALLS_PANE_ID = "earnings-calls";
44
+
45
+ interface CallColumn {
46
+ id: string;
47
+ label: string;
48
+ width: number;
49
+ align: "left" | "right";
50
+ }
51
+
52
+ function buildColumns(width: number, showTicker: boolean): CallColumn[] {
53
+ const tickerWidth = showTicker ? 8 : 0;
54
+ const dateWidth = 10;
55
+ const periodWidth = 8;
56
+ const lengthWidth = 7;
57
+ // Wide enough for a state such as "in progress" on calls not yet produced.
58
+ const sentimentWidth = 11;
59
+ const companyWidth = Math.max(
60
+ 10,
61
+ width - tickerWidth - dateWidth - periodWidth - lengthWidth - sentimentWidth - 8,
62
+ );
63
+
64
+ const columns: CallColumn[] = [];
65
+ if (showTicker) columns.push({ id: "ticker", label: "TICKER", width: tickerWidth, align: "left" });
66
+ columns.push(
67
+ { id: "company", label: "COMPANY", width: companyWidth, align: "left" },
68
+ { id: "date", label: "DATE", width: dateWidth, align: "left" },
69
+ { id: "period", label: "PERIOD", width: periodWidth, align: "left" },
70
+ { id: "length", label: "LENGTH", width: lengthWidth, align: "right" },
71
+ { id: "sentiment", label: "TONE", width: sentimentWidth, align: "right" },
72
+ );
73
+ return columns;
74
+ }
75
+
76
+ function renderCell(call: CloudEarningsCallPayload, column: CallColumn): DataTableCell {
77
+ switch (column.id) {
78
+ case "ticker":
79
+ return { text: call.ticker };
80
+ case "company":
81
+ return { text: call.companyName ?? call.ticker, color: colors.textDim };
82
+ case "date":
83
+ return { text: formatCallDate(call.callAt), color: colors.textDim };
84
+ case "period":
85
+ return { text: formatPeriod(call.fiscalYear, call.fiscalQuarter) };
86
+ case "length":
87
+ return { text: formatDuration(call.durationSeconds), color: colors.textDim };
88
+ case "sentiment": {
89
+ if (!call.hasTranscript) return { text: callStatusLabel(call), color: colors.textDim };
90
+ const tone =
91
+ call.sentiment === null
92
+ ? colors.textDim
93
+ : call.sentiment > 0.15
94
+ ? colors.positive
95
+ : call.sentiment < -0.15
96
+ ? colors.negative
97
+ : colors.textDim;
98
+ return { text: formatSentiment(call.sentiment), color: tone };
99
+ }
100
+ default:
101
+ return { text: "" };
102
+ }
103
+ }
104
+
105
+ type SortDirection = "asc" | "desc";
106
+
107
+ interface CallSort {
108
+ columnId: string;
109
+ direction: SortDirection;
110
+ }
111
+
112
+ const DEFAULT_SORT: CallSort = { columnId: "date", direction: "desc" };
113
+
114
+ /** "AVGO", "BRK-B", "brk.b": something worth asking the server about. */
115
+ const TICKER_PATTERN = /^[A-Z]{1,5}(?:[.-][A-Z]{1,2})?$/;
116
+
117
+ /** How often to ask again while a transcript is being produced. */
118
+ const PRODUCE_POLL_MS = 15_000;
119
+ /** How often to ask again while the server is still finding a company's calls. */
120
+ const LOOKUP_POLL_MS = 20_000;
121
+
122
+ interface TickerLookup {
123
+ ticker: string;
124
+ /** "none": a real company, but no reachable call. "unknown": not a listed symbol. */
125
+ status: "found" | "pending" | "none" | "unknown" | "error";
126
+ calls: CloudEarningsCallPayload[];
127
+ }
128
+
129
+ function sortValue(call: CloudEarningsCallPayload, columnId: string): string | number {
130
+ switch (columnId) {
131
+ case "ticker":
132
+ return call.ticker;
133
+ case "company":
134
+ return (call.companyName ?? call.ticker).toLowerCase();
135
+ case "period":
136
+ return (call.fiscalYear ?? 0) * 10 + (call.fiscalQuarter ?? 0);
137
+ case "length":
138
+ return call.durationSeconds ?? 0;
139
+ case "sentiment":
140
+ return call.sentiment ?? Number.NEGATIVE_INFINITY;
141
+ default:
142
+ return call.callAt ? Date.parse(call.callAt) : 0;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Props are the intersection of a pane and a ticker research tab so the same
148
+ * component can serve both surfaces.
149
+ */
150
+ interface EarningsCallsViewProps {
151
+ focused: boolean;
152
+ width: number;
153
+ height: number;
154
+ }
155
+
156
+ export function EarningsCallsPane({ focused, width, height }: EarningsCallsViewProps) {
157
+ const { symbol } = useSymbolBinding();
158
+ const access = usePlanAccess();
159
+ const openUpgrade = useCloudUpgradeAction();
160
+ const openPlan = useCloudPlanAction();
161
+
162
+ const [calls, setCalls] = useState<CloudEarningsCallPayload[]>([]);
163
+ const [listStatus, setListStatus] = useState<"idle" | "loading" | "loaded" | "error">("idle");
164
+ const [listError, setListError] = useState<{ message: string; status?: number } | null>(null);
165
+ const [stale, setStale] = useState(false);
166
+
167
+ const [selectedId, setSelectedId] = useState<string | null>(null);
168
+ const [detailOpen, setDetailOpen] = useState(false);
169
+ const [transcript, setTranscript] = useState<CloudEarningsTranscriptPayload | null>(null);
170
+ const [transcriptLoading, setTranscriptLoading] = useState(false);
171
+ const [transcriptError, setTranscriptError] = useState<{
172
+ message: string;
173
+ status?: number;
174
+ } | null>(null);
175
+ const [readerTab, setReaderTab] = useState<ReaderTab>("summary");
176
+ const [sort, setSort] = useState<CallSort>(DEFAULT_SORT);
177
+ const [searchQuery, setSearchQuery] = useState("");
178
+ const [searchFocused, setSearchFocused] = useState(false);
179
+ const [searchFocusToken, setSearchFocusToken] = useState(0);
180
+ const searchInputRef = useRef<InputRenderable | null>(null);
181
+ const transcriptScrollRef = useRef<ScrollBoxRenderable | null>(null);
182
+
183
+ // Typing a symbol the shelf does not have asks the server for that company.
184
+ const [lookup, setLookup] = useState<TickerLookup | null>(null);
185
+ // Set while a call opened without a transcript is being produced.
186
+ const [producing, setProducing] = useState(false);
187
+ // The list answered "pending": the server is still searching for calls.
188
+ const [listPending, setListPending] = useState(false);
189
+
190
+ const focusSearch = useCallback(() => {
191
+ setSearchFocused(true);
192
+ setSearchFocusToken((current) => current + 1);
193
+ }, []);
194
+ const blurSearch = useCallback(() => setSearchFocused(false), []);
195
+
196
+ const ticker = symbol ? symbol.toUpperCase() : null;
197
+
198
+ const fetchCalls = useCallback(
199
+ (force: boolean) => {
200
+ if (!access.emailVerified || !access.hasProAccess) return;
201
+ setListStatus((current) => (current === "loaded" ? current : "loading"));
202
+ loadEarningsCalls(ticker, { force })
203
+ .then((result) => {
204
+ setCalls(result.calls);
205
+ setStale(result.stale);
206
+ setListPending(result.pending === true && result.calls.length === 0);
207
+ setListError(
208
+ result.refreshError ? { message: result.refreshError, status: result.errorStatus } : null,
209
+ );
210
+ setListStatus("loaded");
211
+ })
212
+ .catch((error: unknown) => {
213
+ setListError({
214
+ message: error instanceof Error ? error.message : String(error),
215
+ status: statusOf(error),
216
+ });
217
+ setListStatus("error");
218
+ });
219
+ },
220
+ [ticker, access.emailVerified, access.hasProAccess],
221
+ );
222
+
223
+ useEffect(() => {
224
+ fetchCalls(false);
225
+ }, [fetchCalls]);
226
+
227
+ // A research tab whose company is still being searched checks back until
228
+ // the calls appear.
229
+ useEffect(() => {
230
+ if (!listPending) return;
231
+ const timer = setTimeout(() => fetchCalls(true), LOOKUP_POLL_MS);
232
+ return () => clearTimeout(timer);
233
+ }, [listPending, fetchCalls, calls.length]);
234
+
235
+ // On the shelf, a query that looks like a symbol we do not have becomes a
236
+ // request for that company's calls.
237
+ const lookupTicker = useMemo(() => {
238
+ if (ticker || detailOpen) return null;
239
+ const candidate = searchQuery.trim().toUpperCase();
240
+ if (!TICKER_PATTERN.test(candidate)) return null;
241
+ if (calls.some((call) => call.ticker === candidate)) return null;
242
+ return candidate;
243
+ }, [ticker, detailOpen, searchQuery, calls]);
244
+
245
+ // Answered for the current query, or still waiting on the server.
246
+ const lookupState: TickerLookup["status"] | "loading" | null = !lookupTicker
247
+ ? null
248
+ : lookup?.ticker === lookupTicker
249
+ ? lookup.status
250
+ : "loading";
251
+
252
+ useEffect(() => {
253
+ if (!lookupTicker) return;
254
+ const answered = lookup?.ticker === lookupTicker;
255
+ if (answered && lookup.status !== "pending") return;
256
+ let cancelled = false;
257
+ // Debounce typing; while the server is still searching, check back slowly.
258
+ const delay = answered ? LOOKUP_POLL_MS : 500;
259
+ const timer = setTimeout(() => {
260
+ loadEarningsCalls(lookupTicker, { force: true })
261
+ .then((result) => {
262
+ if (cancelled) return;
263
+ setLookup({
264
+ ticker: lookupTicker,
265
+ status: result.unknownTicker
266
+ ? "unknown"
267
+ : result.calls.length > 0
268
+ ? "found"
269
+ : result.pending
270
+ ? "pending"
271
+ : "none",
272
+ calls: result.calls,
273
+ });
274
+ })
275
+ .catch(() => {
276
+ if (!cancelled) setLookup({ ticker: lookupTicker, status: "error", calls: [] });
277
+ });
278
+ }, delay);
279
+ return () => {
280
+ cancelled = true;
281
+ clearTimeout(timer);
282
+ };
283
+ }, [lookupTicker, lookup]);
284
+
285
+ const allCalls = useMemo(() => {
286
+ if (!lookup || lookup.calls.length === 0) return calls;
287
+ const known = new Set(calls.map((call) => call.id));
288
+ return [...calls, ...lookup.calls.filter((call) => !known.has(call.id))];
289
+ }, [calls, lookup]);
290
+
291
+ const rows = useMemo(() => {
292
+ const query = searchQuery.trim().toLowerCase();
293
+ const matched = query
294
+ ? allCalls.filter((call) =>
295
+ [
296
+ call.ticker,
297
+ call.companyName ?? "",
298
+ formatPeriod(call.fiscalYear, call.fiscalQuarter),
299
+ formatCallDate(call.callAt),
300
+ ]
301
+ .join(" ")
302
+ .toLowerCase()
303
+ .includes(query),
304
+ )
305
+ : allCalls;
306
+ const sorted = [...matched].sort((a, b) => {
307
+ const left = sortValue(a, sort.columnId);
308
+ const right = sortValue(b, sort.columnId);
309
+ if (left === right) return 0;
310
+ const order = left < right ? -1 : 1;
311
+ return sort.direction === "asc" ? order : -order;
312
+ });
313
+ return sorted;
314
+ }, [allCalls, sort, searchQuery]);
315
+
316
+ const selected = useMemo(
317
+ () => allCalls.find((call) => call.id === selectedId) ?? null,
318
+ [allCalls, selectedId],
319
+ );
320
+ const selectedCallId = selected?.id ?? null;
321
+ const selectedHasTranscript = selected?.hasTranscript ?? false;
322
+
323
+ // Marks a call as transcribed in whichever list holds it, once it is.
324
+ const markTranscribed = useCallback((callId: string) => {
325
+ const update = (list: CloudEarningsCallPayload[]) =>
326
+ list.map((call) =>
327
+ call.id === callId ? { ...call, hasTranscript: true, status: "published" } : call,
328
+ );
329
+ setCalls(update);
330
+ setLookup((current) => (current ? { ...current, calls: update(current.calls) } : current));
331
+ }, []);
332
+
333
+ // A published transcript is immutable, so it loads once per call. Opening
334
+ // a call that has none asks the server to produce it, then checks back
335
+ // until it arrives.
336
+ useEffect(() => {
337
+ if (!detailOpen || !selectedCallId) return;
338
+ let cancelled = false;
339
+ let timer: ReturnType<typeof setTimeout> | null = null;
340
+ setTranscriptError(null);
341
+
342
+ const attempt = (force: boolean) => {
343
+ setTranscriptLoading(true);
344
+ loadTranscript(selectedCallId, { force })
345
+ .then((result) => {
346
+ if (cancelled) return;
347
+ if (isPendingTranscript(result)) {
348
+ setTranscript(null);
349
+ setProducing(true);
350
+ timer = setTimeout(() => attempt(true), PRODUCE_POLL_MS);
351
+ return;
352
+ }
353
+ setTranscript(result);
354
+ setProducing(false);
355
+ if (!selectedHasTranscript) {
356
+ markTranscribed(selectedCallId);
357
+ fetchCalls(true);
358
+ }
359
+ })
360
+ .catch((error: unknown) => {
361
+ if (cancelled) return;
362
+ setTranscript(null);
363
+ setProducing(false);
364
+ setTranscriptError({
365
+ message: error instanceof Error ? error.message : String(error),
366
+ status: statusOf(error),
367
+ });
368
+ })
369
+ .finally(() => {
370
+ if (!cancelled) setTranscriptLoading(false);
371
+ });
372
+ };
373
+ attempt(!selectedHasTranscript);
374
+
375
+ return () => {
376
+ cancelled = true;
377
+ if (timer) clearTimeout(timer);
378
+ setProducing(false);
379
+ };
380
+ // The call's identity is what matters; list refreshes must not restart this.
381
+ // eslint-disable-next-line react-hooks/exhaustive-deps
382
+ }, [detailOpen, selectedCallId]);
383
+
384
+ const scrollTranscriptBy = useCallback((delta: number) => {
385
+ const scrollBox = transcriptScrollRef.current;
386
+ if (!scrollBox?.viewport) return;
387
+ const maxScrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height);
388
+ scrollBox.scrollTop = Math.max(0, Math.min(maxScrollTop, scrollBox.scrollTop + delta));
389
+ }, []);
390
+
391
+ // A new call or a narrowed transcript should start at the top.
392
+ useEffect(() => {
393
+ const scrollBox = transcriptScrollRef.current;
394
+ if (scrollBox) scrollBox.scrollTop = 0;
395
+ }, [selectedId, detailOpen, readerTab, searchQuery]);
396
+
397
+ // The find field filters turns, so typing in it moves off the summary.
398
+ useEffect(() => {
399
+ if (detailOpen && searchQuery.trim() && readerTab === "summary") setReaderTab("transcript");
400
+ }, [detailOpen, searchQuery, readerTab]);
401
+
402
+ const rendererHost = useRendererHost();
403
+ const openSource = useCallback(() => {
404
+ if (selected?.webcastUrl) void rendererHost.openExternal(selected.webcastUrl);
405
+ }, [rendererHost, selected]);
406
+
407
+ const signInRequired = !access.signedIn || listError?.status === 401;
408
+ const verificationRequired =
409
+ !signInRequired && (!access.emailVerified || listError?.status === 403);
410
+ // The whole feature is a Pro entitlement, so the list itself can be refused.
411
+ const proRequired =
412
+ !signInRequired &&
413
+ !verificationRequired &&
414
+ (!access.hasProAccess || listError?.status === 402);
415
+ const transcriptProRequired = transcriptError?.status === 402;
416
+
417
+ const columns = useMemo(() => buildColumns(width, !ticker), [width, ticker]);
418
+
419
+ const handleRootKey = useCallback(
420
+ (event: DataTableKeyEvent, context: DataTableRootKeyContext) => {
421
+ if (context.selectedIndex <= 0 && isPlainArrowUp(event)) {
422
+ stopSearchFocusNavigation(event);
423
+ focusSearch();
424
+ return true;
425
+ }
426
+ if (isPlainKey(event, "/")) {
427
+ stopSearchFocusNavigation(event);
428
+ focusSearch();
429
+ return true;
430
+ }
431
+ if (isPlainKey(event, "r")) {
432
+ stopSearchFocusNavigation(event);
433
+ fetchCalls(true);
434
+ return true;
435
+ }
436
+ return false;
437
+ },
438
+ [fetchCalls, focusSearch],
439
+ );
440
+
441
+ // `q` is the app's quit key, handled ahead of pane handlers. The reader
442
+ // claims it first while it is open, the same way other panes take keys
443
+ // that clash with global ones.
444
+ useShortcut(
445
+ (event) => {
446
+ if (!isPlainKey(event, "q")) return;
447
+ stopSearchFocusNavigation(event);
448
+ setReaderTab("qa");
449
+ },
450
+ {
451
+ enabled: focused && detailOpen && !searchFocused,
452
+ phase: "before",
453
+ scope: "earnings-calls:reader",
454
+ },
455
+ );
456
+
457
+ const handleDetailKey = useCallback(
458
+ (event: DataTableKeyEvent) => {
459
+ if (isPlainKey(event, "/")) {
460
+ stopSearchFocusNavigation(event);
461
+ focusSearch();
462
+ return true;
463
+ }
464
+ if (isPlainKey(event, "s")) {
465
+ stopSearchFocusNavigation(event);
466
+ setReaderTab("summary");
467
+ return true;
468
+ }
469
+ if (isPlainKey(event, "t")) {
470
+ stopSearchFocusNavigation(event);
471
+ setReaderTab("transcript");
472
+ return true;
473
+ }
474
+ if (isPlainKey(event, "o")) {
475
+ stopSearchFocusNavigation(event);
476
+ openSource();
477
+ return true;
478
+ }
479
+ if (isPlainKey(event, "j", "down")) {
480
+ stopSearchFocusNavigation(event);
481
+ scrollTranscriptBy(1);
482
+ return true;
483
+ }
484
+ if (isPlainArrowUp(event) || isPlainKey(event, "k")) {
485
+ stopSearchFocusNavigation(event);
486
+ // Like the list: pressing up at the top moves into the search field.
487
+ if ((transcriptScrollRef.current?.scrollTop ?? 0) <= 0 && isPlainArrowUp(event)) {
488
+ focusSearch();
489
+ return true;
490
+ }
491
+ scrollTranscriptBy(-1);
492
+ return true;
493
+ }
494
+ return false;
495
+ },
496
+ [focusSearch, scrollTranscriptBy, openSource],
497
+ );
498
+
499
+ usePaneFooter(
500
+ EARNINGS_CALLS_PANE_ID,
501
+ () => {
502
+ const info: PaneFooterSegment[] = [];
503
+ if (listStatus === "loading") {
504
+ info.push({ id: "loading", parts: [{ text: "loading", tone: "muted" }] });
505
+ }
506
+ if (producing) {
507
+ info.push({ id: "producing", parts: [{ text: "producing transcript", tone: "muted" }] });
508
+ } else if (transcriptLoading) {
509
+ info.push({ id: "transcribing", parts: [{ text: "loading transcript", tone: "muted" }] });
510
+ }
511
+ if (listPending) {
512
+ info.push({ id: "searching", parts: [{ text: "searching for calls", tone: "muted" }] });
513
+ }
514
+ if (lookupTicker && lookupState) {
515
+ if (lookupState === "loading" || lookupState === "pending") {
516
+ info.push({ id: "lookup", parts: [{ text: `looking up ${lookupTicker}`, tone: "muted" }] });
517
+ } else if (lookupState === "unknown" || lookupState === "none") {
518
+ info.push({ id: "lookup", parts: [{ text: `${lookupTicker} not found`, tone: "warning" }] });
519
+ } else if (lookupState === "error") {
520
+ info.push({ id: "lookup", parts: [{ text: `${lookupTicker} lookup failed`, tone: "warning" }] });
521
+ }
522
+ }
523
+ if (stale) {
524
+ info.push({ id: "stale", parts: [{ text: "stale cache", tone: "warning" }] });
525
+ }
526
+ if (listError && listStatus === "error") {
527
+ info.push({ id: "error", parts: [{ text: "error", tone: "warning" }] });
528
+ }
529
+ if (proRequired || transcriptProRequired) {
530
+ info.push({ id: "pro", parts: [{ text: "pro required", tone: "warning" }] });
531
+ }
532
+ if (searchQuery.trim()) {
533
+ info.push({
534
+ id: "filter",
535
+ parts: [{ text: `"${searchQuery.trim()}"`, tone: "muted" }],
536
+ });
537
+ }
538
+
539
+ const hints = detailOpen
540
+ ? [
541
+ ...(selected?.webcastUrl
542
+ ? [{ id: "open", key: "o", label: "pen source", onPress: openSource }]
543
+ : []),
544
+ { id: "find", key: "/", label: "find", onPress: focusSearch },
545
+ ]
546
+ : [{ id: "search", key: "/", label: "search", onPress: focusSearch }];
547
+
548
+ return { info, hints };
549
+ },
550
+ [
551
+ listStatus,
552
+ transcriptLoading,
553
+ producing,
554
+ listPending,
555
+ lookupState,
556
+ lookupTicker,
557
+ stale,
558
+ listError,
559
+ proRequired,
560
+ transcriptProRequired,
561
+ detailOpen,
562
+ selected,
563
+ searchQuery,
564
+ focusSearch,
565
+ openSource,
566
+ fetchCalls,
567
+ ],
568
+ );
569
+
570
+ if (signInRequired) {
571
+ return <CloudAuthNotice message="Sign in to browse earnings call transcripts." />;
572
+ }
573
+ if (verificationRequired) {
574
+ return (
575
+ <CloudAuthNotice
576
+ needsVerification
577
+ message="Verify your email to browse earnings call transcripts."
578
+ />
579
+ );
580
+ }
581
+
582
+ if (proRequired) {
583
+ return (
584
+ <Box flexDirection="column" paddingX={1}>
585
+ <EmptyState
586
+ title="Earnings call transcripts are part of Gloom Cloud Pro."
587
+ message="Gloomberb transcribes the calls itself: full transcripts with speaker attribution, analyst Q&A, and extracted guidance, risks and tone."
588
+ />
589
+ <Box flexDirection="row" marginTop={1} gap={1}>
590
+ <Button label="Upgrade to Pro" onPress={openUpgrade} />
591
+ <Button label="Manage account" variant="secondary" onPress={openPlan} />
592
+ </Box>
593
+ </Box>
594
+ );
595
+ }
596
+
597
+ if (listStatus === "loading" && calls.length === 0) {
598
+ return (
599
+ <Box flexGrow={1} alignItems="center" justifyContent="center">
600
+ <Spinner label="Loading calls..." />
601
+ </Box>
602
+ );
603
+ }
604
+
605
+ if (listStatus === "error" && calls.length === 0) {
606
+ return (
607
+ <Box flexDirection="column" paddingX={1}>
608
+ <EmptyState title="Could not load earnings calls." message={listError?.message ?? ""} />
609
+ </Box>
610
+ );
611
+ }
612
+
613
+ // A research tab with nothing yet: either the server is still searching
614
+ // for the company, or it has nothing to search for.
615
+ if (ticker && calls.length === 0) {
616
+ return listPending ? (
617
+ <Box flexGrow={1} alignItems="center" justifyContent="center">
618
+ <Spinner label={`Looking for ${ticker}'s earnings calls...`} />
619
+ </Box>
620
+ ) : (
621
+ <EmptyState
622
+ title={`No earnings calls found for ${ticker}.`}
623
+ message="Calls appear here once the company's investor site or filings list a webcast."
624
+ />
625
+ );
626
+ }
627
+
628
+ const detailContent = transcriptProRequired ? (
629
+ <Box flexDirection="column" paddingX={1}>
630
+ <EmptyState
631
+ title="Earnings call transcripts are part of Gloom Cloud Pro."
632
+ message="Full transcripts with speaker attribution, analyst Q&A, guidance and risk extraction, transcribed from the call itself."
633
+ />
634
+ <Box flexDirection="row" marginTop={1} gap={1}>
635
+ <Button label="Upgrade to Pro" onPress={openUpgrade} />
636
+ <Button label="Manage account" variant="secondary" onPress={openPlan} />
637
+ </Box>
638
+ </Box>
639
+ ) : selected && !transcript && (producing || (!selected.hasTranscript && !transcriptError)) ? (
640
+ <Box flexDirection="column" flexGrow={1} paddingX={1} gap={1}>
641
+ <Box height={1}>
642
+ <Text fg={colors.textDim}>
643
+ {[formatCallDate(selected.callAt), formatPeriod(selected.fiscalYear, selected.fiscalQuarter)]
644
+ .filter(Boolean)
645
+ .join(" · ")}
646
+ </Text>
647
+ </Box>
648
+ <Spinner label="Producing the transcript. This usually takes two to three minutes." />
649
+ </Box>
650
+ ) : (
651
+
652
+ // The reader gets its own search bar: a transcript runs to thousands of
653
+ // words, so finding a topic matters as much as filtering the call list.
654
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minHeight={0} overflow="hidden">
655
+ <InputSearchBar
656
+ value={searchQuery}
657
+ focused={focused && detailOpen}
658
+ active={searchFocused}
659
+ width={width}
660
+ focusToken={searchFocusToken}
661
+ inputRef={searchInputRef}
662
+ placeholder="find in transcript"
663
+ debounceMs={80}
664
+ onFocus={focusSearch}
665
+ onBlur={blurSearch}
666
+ onNavigateDown={blurSearch}
667
+ onQueryChange={setSearchQuery}
668
+ />
669
+ <TranscriptView
670
+ transcript={transcript}
671
+ loading={transcriptLoading}
672
+ error={transcriptError?.message ?? null}
673
+ tab={readerTab}
674
+ onTabChange={setReaderTab}
675
+ tabsFocused={focused && detailOpen && !searchFocused}
676
+ query={searchQuery}
677
+ width={width}
678
+ scrollRef={transcriptScrollRef}
679
+ />
680
+ </Box>
681
+ );
682
+
683
+ return (
684
+ <DataTableStackView<CloudEarningsCallPayload, CallColumn>
685
+ focused={focused && !searchFocused}
686
+ detailOpen={detailOpen && !!selected}
687
+ onBack={() => {
688
+ setDetailOpen(false);
689
+ setReaderTab("summary");
690
+ setSearchQuery("");
691
+ blurSearch();
692
+ }}
693
+ detailContent={detailContent}
694
+ detailTitle={selected ? callTitle(selected) : undefined}
695
+ rootBefore={
696
+ <InputSearchBar
697
+ value={searchQuery}
698
+ focused={focused && !detailOpen}
699
+ active={searchFocused}
700
+ width={width}
701
+ focusToken={searchFocusToken}
702
+ inputRef={searchInputRef}
703
+ placeholder="ticker, company, or period"
704
+ debounceMs={80}
705
+ onFocus={focusSearch}
706
+ onBlur={blurSearch}
707
+ onNavigateDown={blurSearch}
708
+ onQueryChange={setSearchQuery}
709
+ />
710
+ }
711
+ onRootKeyDown={handleRootKey}
712
+ onDetailKeyDown={handleDetailKey}
713
+ selection={{
714
+ kind: "id",
715
+ selectedId,
716
+ getId: (call) => call.id,
717
+ onChange: setSelectedId,
718
+ }}
719
+ onActivate={() => {
720
+ blurSearch();
721
+ setSearchQuery("");
722
+ setDetailOpen(true);
723
+ }}
724
+ rootWidth={width}
725
+ // The search bar sits inside the frame, so the frame takes the full height.
726
+ rootHeight={Math.max(2, height)}
727
+ columns={columns}
728
+ items={rows}
729
+ sortColumnId={sort.columnId}
730
+ sortDirection={sort.direction}
731
+ onHeaderClick={(columnId) =>
732
+ setSort((current) =>
733
+ current.columnId === columnId
734
+ ? { columnId, direction: current.direction === "asc" ? "desc" : "asc" }
735
+ : { columnId, direction: "desc" },
736
+ )
737
+ }
738
+ getItemKey={(call) => call.id}
739
+ renderCell={renderCell}
740
+ emptyStateTitle={
741
+ lookupTicker && lookupState
742
+ ? lookupState === "unknown"
743
+ ? `${lookupTicker} is not a listed company.`
744
+ : lookupState === "none"
745
+ ? `No reachable earnings call for ${lookupTicker} yet.`
746
+ : lookupState === "error"
747
+ ? `Could not look up ${lookupTicker}.`
748
+ : `Looking up ${lookupTicker}...`
749
+ : searchQuery.trim()
750
+ ? "No matching calls."
751
+ : "No calls yet."
752
+ }
753
+ emptyStateHint={searchQuery.trim() ? "Clear the search to see all calls." : undefined}
754
+ />
755
+ );
756
+ }