gloomberb 0.11.3 → 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 (489) hide show
  1. package/README.md +6 -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 +103 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +129 -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 +196 -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/cli/commands/plugins.ts +39 -11
  20. package/src/cli/desktop-pane-shot.ts +285 -16
  21. package/src/cli/index.ts +1 -1
  22. package/src/cli/pane-functions/capabilities.ts +58 -28
  23. package/src/cli/pane-functions/catalog.ts +16 -4
  24. package/src/cli/pane-functions/discovery.ts +1 -0
  25. package/src/cli/pane-functions/headless.test.ts +247 -0
  26. package/src/cli/pane-functions/headless.ts +423 -0
  27. package/src/cli/pane-functions/index.test.ts +9 -0
  28. package/src/cli/pane-functions/index.ts +6 -2
  29. package/src/cli/pane-functions/options.ts +27 -2
  30. package/src/cli/pane-functions/report.ts +4 -0
  31. package/src/cli/pane-functions/resolver.ts +26 -8
  32. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  33. package/src/cli/pane-functions/screenshot.ts +236 -20
  34. package/src/cli/restore-plugins.ts +34 -0
  35. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  36. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  37. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  38. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  39. package/src/components/chart/composite/composite-chart.tsx +301 -204
  40. package/src/components/chart/composite/format.ts +8 -2
  41. package/src/components/chart/composite/interactions.test.ts +68 -102
  42. package/src/components/chart/composite/interactions.ts +214 -264
  43. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  44. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  45. package/src/components/chart/composite/scene.ts +57 -3
  46. package/src/components/chart/composite/text-renderer.ts +3 -2
  47. package/src/components/chart/composite/time-scale.ts +138 -57
  48. package/src/components/chart/composite/types.ts +25 -1
  49. package/src/components/chart/core/data.ts +9 -13
  50. package/src/components/chart/core/date-window.ts +7 -1
  51. package/src/components/chart/core/pointer.ts +3 -0
  52. package/src/components/chart/core/types.ts +0 -45
  53. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  54. package/src/components/chart/native/kitty/index.test.ts +0 -188
  55. package/src/components/chart/native/raster/crosshair.ts +6 -5
  56. package/src/components/chart/native/raster/primitives.ts +0 -23
  57. package/src/components/chart/native/renderer-selection.ts +81 -31
  58. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  59. package/src/components/chart/static/chart-surface.tsx +150 -0
  60. package/src/components/chart/static/index.ts +8 -0
  61. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  62. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  63. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  64. package/src/components/chart/static/series.ts +49 -0
  65. package/src/components/command-bar/action-runtime.ts +1 -3
  66. package/src/components/command-bar/assist/model.test.ts +17 -2
  67. package/src/components/command-bar/assist/model.ts +40 -9
  68. package/src/components/command-bar/commands/direct/index.ts +1 -1
  69. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  70. package/src/components/command-bar/commands/registry.ts +0 -11
  71. package/src/components/command-bar/helpers.test.ts +2 -2
  72. package/src/components/command-bar/helpers.ts +2 -6
  73. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  74. package/src/components/command-bar/list/badge.test.ts +54 -0
  75. package/src/components/command-bar/list/badge.ts +62 -0
  76. package/src/components/command-bar/list/model.test.ts +103 -0
  77. package/src/components/command-bar/list/model.ts +61 -4
  78. package/src/components/command-bar/list/view.test.tsx +113 -0
  79. package/src/components/command-bar/list/view.tsx +147 -131
  80. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  81. package/src/components/command-bar/panel/body.tsx +51 -65
  82. package/src/components/command-bar/panel/index.tsx +48 -13
  83. package/src/components/command-bar/panel/layout.test.ts +139 -0
  84. package/src/components/command-bar/panel/layout.ts +65 -28
  85. package/src/components/command-bar/panel/native-surface.ts +24 -0
  86. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  87. package/src/components/command-bar/panel/runtime.ts +6 -3
  88. package/src/components/command-bar/panel/state.ts +41 -7
  89. package/src/components/command-bar/panel/types.ts +7 -1
  90. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  91. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  92. package/src/components/command-bar/routes/root/results.ts +30 -28
  93. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  94. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  95. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  96. package/src/components/command-bar/routes/root/selection.ts +1 -12
  97. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  98. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  99. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  100. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  101. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  102. package/src/components/command-bar/routing/actions.ts +1 -81
  103. package/src/components/command-bar/routing/launch-request.ts +1 -1
  104. package/src/components/command-bar/routing/list-state.ts +6 -21
  105. package/src/components/command-bar/selection-runtime.ts +1 -1
  106. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  107. package/src/components/command-bar/surface/environment.ts +8 -1
  108. package/src/components/command-bar/surface/index.test.tsx +84 -68
  109. package/src/components/command-bar/surface/index.tsx +33 -36
  110. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  111. package/src/components/command-bar/theme-picker.tsx +43 -15
  112. package/src/components/command-bar/view-model.test.ts +53 -8
  113. package/src/components/command-bar/view-model.ts +43 -14
  114. package/src/components/command-bar/workflow/tickers.ts +1 -0
  115. package/src/components/command-bar/workflow/types.ts +1 -1
  116. package/src/components/data-table/view.test.tsx +45 -6
  117. package/src/components/data-table/view.tsx +55 -9
  118. package/src/components/index.ts +10 -2
  119. package/src/components/input-search-bar.test.tsx +36 -2
  120. package/src/components/input-search-bar.tsx +18 -1
  121. package/src/components/layout/header.test.tsx +57 -10
  122. package/src/components/layout/header.tsx +292 -149
  123. package/src/components/layout/market-summary.test.ts +81 -0
  124. package/src/components/layout/market-summary.ts +109 -0
  125. package/src/components/layout/shell/chrome.ts +96 -1
  126. package/src/components/layout/shell/index.tsx +11 -3
  127. package/src/components/layout/shell/menu.ts +9 -0
  128. package/src/components/layout/shell/pane/actions.ts +29 -0
  129. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  130. package/src/components/layout/shell/shortcuts.ts +3 -0
  131. package/src/components/layout/status-bar.test.tsx +9 -8
  132. package/src/components/layout/status-bar.tsx +85 -54
  133. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  134. package/src/components/price-sparkline/index.ts +61 -34
  135. package/src/components/price-sparkline/view.tsx +1 -1
  136. package/src/components/sign-in-gate.test.tsx +128 -0
  137. package/src/components/sign-in-gate.tsx +149 -0
  138. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  139. package/src/components/ticker/badge/list.tsx +17 -3
  140. package/src/components/ticker/list-table-view.tsx +6 -3
  141. package/src/components/ui/data-table/index.tsx +3 -2
  142. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  143. package/src/components/ui/frame.tsx +28 -0
  144. package/src/components/ui/index.ts +3 -0
  145. package/src/components/ui/native-select.tsx +20 -10
  146. package/src/components/ui/select-button.tsx +138 -0
  147. package/src/components/ui/status.tsx +2 -2
  148. package/src/components/ui/table-layout.ts +1 -1
  149. package/src/components/use-double-click-activation.ts +7 -3
  150. package/src/core/app-services.ts +3 -3
  151. package/src/data/config/layout.ts +12 -6
  152. package/src/data/config/store/index.test.ts +97 -0
  153. package/src/data/config/store/migrations.ts +41 -0
  154. package/src/data/config/store/normalize.ts +5 -3
  155. package/src/i18n/es.ts +6 -2
  156. package/src/i18n/ja.ts +6 -2
  157. package/src/i18n/ko.ts +6 -2
  158. package/src/i18n/zh-cn.ts +6 -2
  159. package/src/i18n/zh-tw.ts +6 -2
  160. package/src/layout-marketplace/pane-imagery.ts +1 -0
  161. package/src/layout-marketplace/payload.test.ts +17 -0
  162. package/src/layout-marketplace/payload.ts +40 -5
  163. package/src/market-data/coordinator/index.ts +3 -2
  164. package/src/market-data/coordinator/quotes.ts +173 -66
  165. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  166. package/src/pane-settings.ts +1 -1
  167. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  168. package/src/plugins/builtin/analytics/view.tsx +1 -1
  169. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  170. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  171. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  172. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  173. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  174. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  175. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  176. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  177. package/src/plugins/builtin/composite-plugins.ts +14 -1
  178. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  179. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  180. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  181. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  182. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  183. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  184. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  185. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  186. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  187. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  188. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  189. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  190. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  191. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  192. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  193. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  194. package/src/plugins/builtin/econ/index.tsx +2 -1
  195. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  196. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  197. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  198. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  199. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  200. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  201. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  202. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  203. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  204. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  205. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  206. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  207. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  208. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  209. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  210. package/src/plugins/builtin/help/index.tsx +4 -0
  211. package/src/plugins/builtin/holders/index.tsx +1 -0
  212. package/src/plugins/builtin/insider/index.tsx +1 -0
  213. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  214. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  215. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  216. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  217. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  218. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  219. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  220. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  221. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  222. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  223. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  224. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  225. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  226. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  227. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  228. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  229. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  230. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  231. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  232. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  233. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  234. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  235. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  236. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  237. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  238. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  239. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  240. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  241. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  242. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  243. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  244. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  245. package/src/plugins/builtin/options/index.tsx +1 -0
  246. package/src/plugins/builtin/options/view.test.tsx +29 -1
  247. package/src/plugins/builtin/options/view.tsx +1 -22
  248. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  249. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  250. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  251. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  252. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  253. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  254. package/src/plugins/builtin/plugin-module.ts +9 -1
  255. package/src/plugins/builtin/research/index.tsx +4 -0
  256. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  257. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  258. package/src/plugins/builtin/research-search/data.ts +109 -0
  259. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  260. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  261. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  262. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  263. package/src/plugins/builtin/research-search/index.tsx +85 -0
  264. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  265. package/src/plugins/builtin/research-search/model.ts +291 -0
  266. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  267. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  268. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  269. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  270. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  271. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  272. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  273. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  274. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  275. package/src/plugins/builtin/sec/index.tsx +1 -0
  276. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  277. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  278. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  279. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  280. package/src/plugins/builtin/ticker-detail/index.tsx +0 -14
  281. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  282. package/src/plugins/builtin/volatility/index.tsx +1 -1
  283. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  284. package/src/plugins/builtin/world-venue-map/map.tsx +213 -69
  285. package/src/plugins/builtin/world-venue-map/model.test.ts +32 -0
  286. package/src/plugins/builtin/world-venue-map/model.ts +134 -13
  287. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  288. package/src/plugins/bundle.test.ts +109 -0
  289. package/src/plugins/bundle.ts +139 -0
  290. package/src/plugins/catalog-browser.test.ts +1 -0
  291. package/src/plugins/catalog-browser.ts +10 -0
  292. package/src/plugins/catalog-manifest.test.ts +43 -0
  293. package/src/plugins/catalog-manifest.ts +92 -0
  294. package/src/plugins/catalog-ui.ts +20 -4
  295. package/src/plugins/catalog.ts +9 -2
  296. package/src/plugins/current-target.ts +28 -0
  297. package/src/plugins/host-contract.ts +37 -0
  298. package/src/plugins/host-link.ts +158 -0
  299. package/src/plugins/host-modules.ts +61 -0
  300. package/src/plugins/loader.ts +41 -22
  301. package/src/plugins/ownership.ts +1 -0
  302. package/src/plugins/prediction-markets/index.tsx +1 -0
  303. package/src/plugins/registry/context.ts +3 -0
  304. package/src/plugins/registry/contributions.ts +27 -0
  305. package/src/plugins/registry/index.test.ts +25 -0
  306. package/src/plugins/registry/index.ts +8 -0
  307. package/src/plugins/registry/pane-settings.ts +6 -13
  308. package/src/plugins/seed.test.ts +83 -0
  309. package/src/plugins/seed.ts +80 -0
  310. package/src/public/broker.ts +31 -0
  311. package/src/public/market-data.ts +9 -0
  312. package/src/public/pane-hooks.ts +57 -0
  313. package/src/public/public-api.test.ts +285 -0
  314. package/src/public/react.ts +62 -0
  315. package/src/public/test-support.ts +23 -0
  316. package/src/public/time-series.ts +8 -0
  317. package/src/public/utils.ts +77 -0
  318. package/src/remote/layout-helpers.ts +6 -8
  319. package/src/remote/resources.ts +2 -0
  320. package/src/remote/schema.ts +1 -0
  321. package/src/remote/semantic-tree.test.tsx +105 -0
  322. package/src/remote/semantic-tree.tsx +53 -35
  323. package/src/renderers/browser/main.tsx +6 -0
  324. package/src/renderers/cloudflare/worker.test.ts +19 -0
  325. package/src/renderers/cloudflare/worker.ts +1 -1
  326. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  327. package/src/renderers/electrobun/bun/index.ts +5 -2
  328. package/src/renderers/electrobun/shared/protocol.ts +21 -0
  329. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  330. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  331. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  332. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  333. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  334. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  335. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  336. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  337. package/src/renderers/electrobun/view/main.tsx +27 -2
  338. package/src/renderers/electrobun/view/styles.css +5 -0
  339. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  340. package/src/renderers/opentui/chart-surface.tsx +5 -1
  341. package/src/renderers/opentui/host.tsx +16 -2
  342. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  343. package/src/renderers/opentui/interaction-performance.ts +188 -0
  344. package/src/renderers/opentui/start.tsx +26 -1
  345. package/src/renderers/opentui/test-utils.tsx +13 -0
  346. package/src/renderers/opentui/toast-host.tsx +26 -2
  347. package/src/renderers/opentui/ui-host.tsx +1 -0
  348. package/src/sources/provider-router/search.test.ts +191 -0
  349. package/src/sources/provider-router/search.ts +92 -39
  350. package/src/state/app/context/index.tsx +2 -0
  351. package/src/state/app/context/selector.test.tsx +33 -1
  352. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  353. package/src/state/hooks/quote-streaming.ts +106 -30
  354. package/src/state/pane-table-export-registry.ts +19 -0
  355. package/src/theme/color-utils.ts +1 -1
  356. package/src/theme/colors.ts +50 -0
  357. package/src/theme/themes.test.ts +15 -2
  358. package/src/theme/themes.ts +19 -1
  359. package/src/tickers/search/index.test.ts +26 -0
  360. package/src/tickers/search/index.ts +42 -17
  361. package/src/tickers/search/ranking.ts +14 -0
  362. package/src/time-series/chart-data.test.ts +216 -0
  363. package/src/time-series/resolution.test.ts +25 -26
  364. package/src/time-series/resolution.ts +41 -10
  365. package/src/time-series/resolve.ts +7 -0
  366. package/src/time-series/types.ts +3 -1
  367. package/src/time-series/use-chart-resolution.ts +5 -0
  368. package/src/types/config.ts +123 -3
  369. package/src/types/data-provider.ts +11 -0
  370. package/src/types/headless.ts +161 -0
  371. package/src/types/plugin.ts +102 -0
  372. package/src/ui/host.tsx +12 -1
  373. package/src/ui/index.tsx +1 -0
  374. package/src/ui/toast.tsx +8 -4
  375. package/src/utils/format.ts +35 -0
  376. package/src/utils/fuzzy-search.test.ts +94 -0
  377. package/src/utils/fuzzy-search.ts +81 -22
  378. package/src/utils/shortcut-labels.ts +14 -0
  379. package/src/components/chart/core/axis-format.ts +0 -157
  380. package/src/components/chart/core/index.test.ts +0 -1075
  381. package/src/components/chart/core/projection.ts +0 -357
  382. package/src/components/chart/core/render-utils.ts +0 -4
  383. package/src/components/chart/core/renderer.ts +0 -244
  384. package/src/components/chart/core/scene.ts +0 -180
  385. package/src/components/chart/core/time-axis.ts +0 -330
  386. package/src/components/chart/cursor-motion.ts +0 -47
  387. package/src/components/chart/indicators/types.ts +0 -26
  388. package/src/components/chart/market-session.ts +0 -176
  389. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  390. package/src/components/chart/multi-line/renderer.ts +0 -161
  391. package/src/components/chart/multi-line/scene.ts +0 -188
  392. package/src/components/chart/native/raster/layout.ts +0 -129
  393. package/src/components/chart/native/raster/price-chart.ts +0 -244
  394. package/src/components/chart/rendering/chart-draw.ts +0 -270
  395. package/src/components/chart/rendering/geometry.ts +0 -95
  396. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  397. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  398. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  399. package/src/components/chart/static/chart/surface.tsx +0 -358
  400. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  401. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  402. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  403. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  404. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  405. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  406. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  407. package/src/plugins/builtin/substack/api/store.ts +0 -186
  408. package/src/plugins/builtin/substack/api/types.ts +0 -42
  409. package/src/plugins/builtin/substack/api.test.ts +0 -298
  410. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  411. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  412. package/src/plugins/builtin/substack/content.ts +0 -478
  413. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  414. package/src/plugins/builtin/substack/index.tsx +0 -49
  415. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  416. package/src/plugins/builtin/substack/model.test.ts +0 -155
  417. package/src/plugins/builtin/substack/normalize.ts +0 -343
  418. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  419. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  420. package/src/plugins/builtin/substack/pane.tsx +0 -547
  421. package/src/plugins/builtin/substack/read-state.ts +0 -30
  422. package/src/plugins/builtin/substack/table.ts +0 -111
  423. package/src/plugins/builtin/substack/types.ts +0 -71
  424. package/src/plugins/builtin/substack/utils.ts +0 -131
  425. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  426. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  427. package/src/plugins/ibkr/account-cache.ts +0 -24
  428. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  429. package/src/plugins/ibkr/config.test.ts +0 -79
  430. package/src/plugins/ibkr/config.ts +0 -250
  431. package/src/plugins/ibkr/dialogs.tsx +0 -40
  432. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  433. package/src/plugins/ibkr/flex/index.ts +0 -367
  434. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  435. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  436. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  437. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  438. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  439. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  440. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  441. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  442. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  443. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  444. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  445. package/src/plugins/ibkr/gateway/history.ts +0 -120
  446. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  447. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  448. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  449. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  450. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  451. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  452. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  453. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  454. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  455. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  456. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  457. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  458. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  459. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  460. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  461. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  462. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  463. package/src/plugins/ibkr/gateway/types.ts +0 -27
  464. package/src/plugins/ibkr/index.test.tsx +0 -162
  465. package/src/plugins/ibkr/index.tsx +0 -159
  466. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  467. package/src/plugins/ibkr/instance-selection.ts +0 -36
  468. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  469. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  470. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  471. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  472. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  473. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  474. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  475. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  476. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  477. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  478. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  479. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  480. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  481. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  482. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  483. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  484. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  485. package/src/plugins/ibkr/trade/utils.ts +0 -144
  486. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  487. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  488. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  489. 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
+ }