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,191 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { DataProvider, SearchRequestContext } from "../../types/data-provider";
3
+ import type { InstrumentSearchResult } from "../../types/instrument";
4
+ import { ProviderRouterSearchRoutes } from "./search";
5
+
6
+ function result(symbol: string, extra: Partial<InstrumentSearchResult> = {}): InstrumentSearchResult {
7
+ return { symbol, name: symbol, exchange: "NASDAQ", type: "EQUITY", ...extra };
8
+ }
9
+
10
+ function provider(
11
+ id: string,
12
+ items: InstrumentSearchResult[],
13
+ delayMs = 0,
14
+ ): DataProvider {
15
+ return {
16
+ id,
17
+ search: async () => {
18
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
19
+ return items;
20
+ },
21
+ } as unknown as DataProvider;
22
+ }
23
+
24
+ function broker(
25
+ id: string,
26
+ items: InstrumentSearchResult[] | null,
27
+ delayMs = 0,
28
+ ) {
29
+ return {
30
+ brokerId: id,
31
+ brokerInstanceId: `${id}-1`,
32
+ brokerLabel: id,
33
+ instance: {},
34
+ broker: {
35
+ searchInstruments: async () => {
36
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
37
+ if (!items) throw new Error("broker down");
38
+ return items;
39
+ },
40
+ },
41
+ };
42
+ }
43
+
44
+ function makeRoutes(options: {
45
+ brokers?: ReturnType<typeof broker>[];
46
+ providers?: DataProvider[];
47
+ }) {
48
+ return new ProviderRouterSearchRoutes({
49
+ getBrokerCandidates: () => (options.brokers ?? []) as never,
50
+ providersInPriorityOrder: () => options.providers ?? [],
51
+ logProviderError: () => {},
52
+ });
53
+ }
54
+
55
+ describe("provider search racing", () => {
56
+ /**
57
+ * The reason this exists: brokers used to be awaited one at a time before the
58
+ * cloud was asked at all, so a single unreachable broker delayed every ticker
59
+ * lookup in the app by its full timeout.
60
+ */
61
+ test("a hanging broker does not delay a provider that can answer", async () => {
62
+ const routes = makeRoutes({
63
+ brokers: [broker("dead", [result("NVDA")], 5_000)],
64
+ providers: [provider("cloud", [result("NVDA")])],
65
+ });
66
+
67
+ const started = Date.now();
68
+ const results = await routes.search("nvidia", { preferBroker: true, interactive: true });
69
+
70
+ expect(results.map((item) => item.symbol)).toEqual(["NVDA"]);
71
+ // Sequentially this waited on the broker's timeout first.
72
+ expect(Date.now() - started).toBeLessThan(400);
73
+ });
74
+
75
+ test("interactive lookups abandon a stalled source in well under a second", async () => {
76
+ const routes = makeRoutes({ providers: [provider("slow", [result("AAPL")], 5_000)] });
77
+
78
+ const started = Date.now();
79
+ const results = await routes.search("apple", { interactive: true });
80
+
81
+ expect(results).toEqual([]);
82
+ expect(Date.now() - started).toBeLessThan(1_500);
83
+ });
84
+
85
+ test("a broker that answers after the cloud upgrades the row in place", async () => {
86
+ const partials: InstrumentSearchResult[][] = [];
87
+ const routes = makeRoutes({
88
+ brokers: [broker("ibkr", [result("NVDA", { brokerContract: { brokerId: "ibkr" } as never })], 60)],
89
+ providers: [provider("cloud", [result("NVDA")])],
90
+ });
91
+
92
+ const first = await routes.search("nvidia", {
93
+ preferBroker: true,
94
+ interactive: true,
95
+ onPartial: (items) => partials.push(items),
96
+ });
97
+
98
+ // The cloud answers immediately and that is what the caller renders.
99
+ expect(first.map((item) => item.symbol)).toEqual(["NVDA"]);
100
+ expect(first[0]?.brokerContract).toBeUndefined();
101
+
102
+ await new Promise((resolve) => setTimeout(resolve, 200));
103
+
104
+ // The broker's richer copy replaces it without duplicating the symbol.
105
+ expect(partials).toHaveLength(1);
106
+ expect(partials[0]).toHaveLength(1);
107
+ expect(partials[0]?.[0]?.brokerContract).toBeDefined();
108
+ });
109
+
110
+ /**
111
+ * Racing everything would bill a metered fallback on every keystroke for an
112
+ * answer already in hand, so the second tier only runs when the first found
113
+ * nothing.
114
+ */
115
+ test("a fallback provider is not asked when the preferred one answers", async () => {
116
+ let fallbackCalls = 0;
117
+ const fallback = {
118
+ id: "fallback",
119
+ search: async () => {
120
+ fallbackCalls += 1;
121
+ return [result("WRONG")];
122
+ },
123
+ } as unknown as DataProvider;
124
+ const routes = makeRoutes({ providers: [provider("preferred", [result("NVDA")]), fallback] });
125
+
126
+ const results = await routes.search("nvidia", { interactive: true });
127
+
128
+ expect(results.map((item) => item.symbol)).toEqual(["NVDA"]);
129
+ expect(fallbackCalls).toBe(0);
130
+ });
131
+
132
+ test("a fallback provider is asked when the preferred one finds nothing", async () => {
133
+ let fallbackCalls = 0;
134
+ const fallback = {
135
+ id: "fallback",
136
+ search: async () => {
137
+ fallbackCalls += 1;
138
+ return [result("NVDA")];
139
+ },
140
+ } as unknown as DataProvider;
141
+ const routes = makeRoutes({ providers: [provider("preferred", []), fallback] });
142
+
143
+ const results = await routes.search("nvidia", { interactive: true });
144
+
145
+ expect(results.map((item) => item.symbol)).toEqual(["NVDA"]);
146
+ expect(fallbackCalls).toBe(1);
147
+ });
148
+
149
+ test("a failing broker leaves the provider result intact", async () => {
150
+ const routes = makeRoutes({
151
+ brokers: [broker("dead", null)],
152
+ providers: [provider("cloud", [result("MSFT")])],
153
+ });
154
+
155
+ const results = await routes.search("microsoft", { preferBroker: true });
156
+ expect(results.map((item) => item.symbol)).toEqual(["MSFT"]);
157
+ });
158
+
159
+ test("concurrent identical queries share one run", async () => {
160
+ let calls = 0;
161
+ const counting = {
162
+ id: "cloud",
163
+ search: async () => {
164
+ calls += 1;
165
+ await new Promise((resolve) => setTimeout(resolve, 30));
166
+ return [result("TSLA")];
167
+ },
168
+ } as unknown as DataProvider;
169
+ const routes = makeRoutes({ providers: [counting] });
170
+
171
+ const [a, b] = await Promise.all([routes.search("tesla"), routes.search("tesla")]);
172
+
173
+ expect(a.map((item) => item.symbol)).toEqual(["TSLA"]);
174
+ expect(b.map((item) => item.symbol)).toEqual(["TSLA"]);
175
+ expect(calls).toBe(1);
176
+ });
177
+
178
+ test("results returned to one caller are not mutated by a later source", async () => {
179
+ const routes = makeRoutes({
180
+ brokers: [broker("late", [result("AMD")], 50)],
181
+ providers: [provider("cloud", [result("NVDA")])],
182
+ });
183
+
184
+ const first = await routes.search("chips", { preferBroker: true } as SearchRequestContext);
185
+ expect(first).toHaveLength(1);
186
+
187
+ await new Promise((resolve) => setTimeout(resolve, 200));
188
+ // The array handed back stays the answer that was handed back.
189
+ expect(first).toHaveLength(1);
190
+ });
191
+ });
@@ -7,6 +7,12 @@ const SEARCH_CACHE_TTL_MS = 30_000;
7
7
  const SEARCH_CACHE_MAX_ENTRIES = 100;
8
8
 
9
9
  const SEARCH_PROVIDER_TIMEOUT_MS = 5_000;
10
+ /**
11
+ * A keystroke path cannot wait the batch budget on a source that is failing
12
+ * slowly. A broker whose gateway is down is the common case, and five seconds
13
+ * of it is indistinguishable from the feature being broken.
14
+ */
15
+ const INTERACTIVE_SEARCH_TIMEOUT_MS = 800;
10
16
 
11
17
  interface BrokerSearchCandidate {
12
18
  brokerId: string;
@@ -14,9 +20,9 @@ interface BrokerSearchCandidate {
14
20
  brokerLabel: string;
15
21
  }
16
22
 
17
- function withSearchTimeout<T>(promise: Promise<T>): Promise<T | null> {
23
+ function withSearchTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | null> {
18
24
  return new Promise<T | null>((resolve) => {
19
- const timer = setTimeout(() => resolve(null), SEARCH_PROVIDER_TIMEOUT_MS);
25
+ const timer = setTimeout(() => resolve(null), timeoutMs);
20
26
  promise.then(
21
27
  (value) => { clearTimeout(timer); resolve(value); },
22
28
  () => { clearTimeout(timer); resolve(null); },
@@ -93,72 +99,119 @@ export class ProviderRouterSearchRoutes {
93
99
  const inFlight = this.searchInFlight.get(cacheKey);
94
100
  if (inFlight) return inFlight;
95
101
 
96
- const task = this.fetchSearchResults(query, context, cacheKey).finally(() => {
102
+ const { first, settled } = this.fetchSearchResults(query, context, cacheKey);
103
+ // Held until every source settles, not until the first answer, so a repeat
104
+ // of the same query joins this run instead of starting a second one.
105
+ this.searchInFlight.set(cacheKey, first);
106
+ void settled.finally(() => {
97
107
  this.searchInFlight.delete(cacheKey);
98
108
  });
99
-
100
- this.searchInFlight.set(cacheKey, task);
101
- return task;
109
+ return first;
102
110
  }
103
111
 
104
- private async fetchSearchResults(
112
+ /**
113
+ * Brokers and the preferred provider run at once, rather than each being
114
+ * awaited in turn. The old order tried brokers one at a time before the
115
+ * preferred provider was asked at all, so one unreachable broker delayed
116
+ * every lookup by its full timeout.
117
+ *
118
+ * Fallback providers stay a fallback: they are only asked when that first
119
+ * round found nothing, because they are metered upstreams and racing them on
120
+ * every keystroke would bill for answers already in hand.
121
+ *
122
+ * Ordering still matters, it just no longer costs latency: `mergeSearchResults`
123
+ * keeps the richest entry per symbol, so a broker arriving after the cloud
124
+ * still upgrades the row, and `onPartial` hands that upgrade to the caller.
125
+ */
126
+ private fetchSearchResults(
105
127
  query: string,
106
128
  context: SearchRequestContext | undefined,
107
129
  cacheKey: string,
108
- ): Promise<InstrumentSearchResult[]> {
130
+ ): { first: Promise<InstrumentSearchResult[]>; settled: Promise<void> } {
109
131
  const results: InstrumentSearchResult[] = [];
110
132
  const resultIndexByKey = new Map<string, number>();
133
+ const timeoutMs = context?.interactive
134
+ ? INTERACTIVE_SEARCH_TIMEOUT_MS
135
+ : SEARCH_PROVIDER_TIMEOUT_MS;
136
+
111
137
  const cacheResults = (ttl = SEARCH_CACHE_TTL_MS) => {
112
138
  this.searchCache.set(cacheKey, {
113
139
  expiresAt: Date.now() + ttl,
114
- results,
140
+ results: [...results],
115
141
  });
116
142
  if (this.searchCache.size > SEARCH_CACHE_MAX_ENTRIES) {
117
143
  this.searchCache.delete(this.searchCache.keys().next().value!);
118
144
  }
119
145
  };
120
- const push = (items: InstrumentSearchResult[]) => {
146
+
147
+ let resolveFirst!: (value: InstrumentSearchResult[]) => void;
148
+ const first = new Promise<InstrumentSearchResult[]>((resolve) => {
149
+ resolveFirst = resolve;
150
+ });
151
+ let answered = false;
152
+
153
+ const record = (items: InstrumentSearchResult[] | null) => {
154
+ if (!items?.length) return;
121
155
  mergeSearchResults(results, resultIndexByKey, items, context);
156
+ cacheResults();
157
+ if (!answered) {
158
+ answered = true;
159
+ resolveFirst([...results]);
160
+ return;
161
+ }
162
+ // The caller already has an answer, so a later, richer source is handed
163
+ // over rather than silently dropped into the cache.
164
+ context?.onPartial?.([...results]);
122
165
  };
123
166
 
124
- if (context?.preferBroker !== false) {
125
- for (const candidate of this.deps.getBrokerCandidates(
126
- context?.brokerInstanceId,
127
- context?.brokerId,
128
- )) {
129
- if (!candidate.broker.searchInstruments) continue;
130
- try {
131
- const items = await withSearchTimeout(candidate.broker.searchInstruments(query, candidate.instance));
132
- if (!items) continue;
133
- push(annotateBrokerSearchResults(items, candidate));
134
- if (results.length > 0) {
135
- cacheResults();
136
- return results;
137
- }
138
- } catch {
139
- // continue through broker candidates
140
- }
167
+ const searchBroker = (candidate: BrokerCandidate) => (async () => {
168
+ try {
169
+ const items = await withSearchTimeout(
170
+ candidate.broker.searchInstruments!(query, candidate.instance),
171
+ timeoutMs,
172
+ );
173
+ record(items && annotateBrokerSearchResults(items, candidate));
174
+ } catch {
175
+ // A broker that cannot answer must not hold up the ones that can.
141
176
  }
142
- }
177
+ })();
143
178
 
144
- for (const provider of this.deps.providersInPriorityOrder()) {
179
+ const searchProvider = (provider: DataProvider) => (async () => {
145
180
  try {
146
- const items = await withSearchTimeout(provider.search(query, context));
147
- if (!items) continue;
148
- push(items);
149
- if (results.length > 0) {
150
- cacheResults();
151
- return results;
152
- }
181
+ record(await withSearchTimeout(provider.search(query, context), timeoutMs));
153
182
  } catch (error) {
154
183
  if (shouldLogProviderError(error)) {
155
184
  this.deps.logProviderError(`${provider.id} failed: ${error}`);
156
185
  }
157
186
  }
158
- }
187
+ })();
188
+
189
+ const brokers = context?.preferBroker === false
190
+ ? []
191
+ : this.deps
192
+ .getBrokerCandidates(context?.brokerInstanceId, context?.brokerId)
193
+ .filter((candidate) => candidate.broker.searchInstruments);
194
+ const [preferred, ...fallbacks] = this.deps.providersInPriorityOrder();
195
+
196
+ const settled = (async () => {
197
+ await Promise.allSettled([
198
+ ...brokers.map(searchBroker),
199
+ ...(preferred ? [searchProvider(preferred)] : []),
200
+ ]);
201
+ if (results.length === 0) {
202
+ await Promise.allSettled(fallbacks.map(searchProvider));
203
+ }
204
+ })().then(() => {
205
+ if (!answered) {
206
+ answered = true;
207
+ // Nothing matched anywhere, so hold the empty answer briefly rather
208
+ // than asking every source again on the next keystroke.
209
+ cacheResults(Math.min(SEARCH_CACHE_TTL_MS, 5_000));
210
+ resolveFirst([]);
211
+ }
212
+ });
159
213
 
160
- cacheResults(Math.min(SEARCH_CACHE_TTL_MS, 5_000));
161
- return results;
214
+ return { first, settled };
162
215
  }
163
216
  }
164
217
 
@@ -287,6 +287,7 @@ export function usePaneStateValue<T>(key: string, fallback: T, paneId?: string):
287
287
  const resolved = typeof nextValue === "function"
288
288
  ? (nextValue as (previousValue: T) => T)(currentValue)
289
289
  : nextValue;
290
+ if (Object.is(currentValue, resolved)) return;
290
291
  dispatch({ type: "UPDATE_PANE_STATE", paneId: scopedPaneId, patch: { [key]: resolved } });
291
292
  }, [dispatch, key, scopedPaneId, stateRef]);
292
293
  return [value, setValue];
@@ -309,6 +310,7 @@ export function usePaneSettingValue<T>(
309
310
  const resolved = typeof nextValue === "function"
310
311
  ? (nextValue as (previousValue: T) => T)(currentValue)
311
312
  : nextValue;
313
+ if (Object.is(currentValue, resolved)) return;
312
314
  const layout = setPaneSetting(currentState.config.layout, scopedPaneId, key, resolved);
313
315
  const nextConfig = syncConfigActiveLayoutState(
314
316
  { ...currentState.config, layout },
@@ -1,7 +1,7 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act, useRef, type Dispatch } from "react";
3
3
  import { testRender } from "../../../renderers/opentui/test-utils";
4
- import { AppProvider, PaneInstanceProvider, useAppDispatch, useAppSelector, usePaneTicker, type AppAction } from "./index";
4
+ import { AppProvider, PaneInstanceProvider, useAppDispatch, useAppSelector, usePaneSettingValue, usePaneTicker, type AppAction } from "./index";
5
5
  import { cloneLayout, createDefaultConfig, type AppConfig } from "../../../types/config";
6
6
  import { applyTheme } from "../../../theme/colors";
7
7
  import { useThemeId } from "../../../theme/theme-context";
@@ -11,6 +11,7 @@ const TEST_PANE_ID = "ticker-detail:test";
11
11
 
12
12
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
13
13
  let capturedDispatch: Dispatch<AppAction> | null = null;
14
+ let capturedPaneSetting: ((value: string) => void) | null = null;
14
15
 
15
16
  function createTickerDetailConfig(symbol: string): AppConfig {
16
17
  const config = createDefaultConfig("/tmp/gloomberb-test");
@@ -44,6 +45,14 @@ function PaneTickerHarness() {
44
45
  return <text>{`${symbol ?? "none"}:${renderCountRef.current}`}</text>;
45
46
  }
46
47
 
48
+ function PaneSettingHarness() {
49
+ const renderCountRef = useRef(0);
50
+ renderCountRef.current += 1;
51
+ const [value, setValue] = usePaneSettingValue("mode", "table");
52
+ capturedPaneSetting = setValue;
53
+ return <text>{`${value}:${renderCountRef.current}`}</text>;
54
+ }
55
+
47
56
  function ThemeSelectorHarness() {
48
57
  const focusedPaneId = useAppSelector((state) => state.focusedPaneId);
49
58
  const themeId = useThemeId();
@@ -99,6 +108,7 @@ describe("pane selectors", () => {
99
108
  }
100
109
  testSetup = undefined;
101
110
  capturedDispatch = null;
111
+ capturedPaneSetting = null;
102
112
  applyTheme("amber");
103
113
  });
104
114
 
@@ -125,6 +135,28 @@ describe("pane selectors", () => {
125
135
  expect(testSetup.captureCharFrame()).toContain("AAPL:1");
126
136
  });
127
137
 
138
+ test("does not dispatch or rerender when a pane setting keeps its effective value", async () => {
139
+ testSetup = await testRender(
140
+ <AppProvider config={createTickerDetailConfig("AAPL")}>
141
+ <PaneInstanceProvider paneId={TEST_PANE_ID}>
142
+ <PaneSettingHarness />
143
+ </PaneInstanceProvider>
144
+ </AppProvider>,
145
+ { width: 24, height: 4 },
146
+ );
147
+
148
+ await testSetup.renderOnce();
149
+ expect(testSetup.captureCharFrame()).toContain("table:1");
150
+
151
+ await act(() => {
152
+ capturedPaneSetting?.("table");
153
+ });
154
+ await testSetup.renderOnce();
155
+ await testSetup.renderOnce();
156
+
157
+ expect(testSetup.captureCharFrame()).toContain("table:1");
158
+ });
159
+
128
160
  test("rerenders theme hook consumers when the theme changes", async () => {
129
161
  testSetup = await testRender(
130
162
  <AppProvider config={createTickerDetailConfig("AAPL")}>
@@ -10,6 +10,7 @@ import { useLiveQuoteEntries, useQuoteStreaming, useQuoteUpdates } from "./quote
10
10
 
11
11
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
12
12
  let bumpHarness: (() => void) | null = null;
13
+ let toggleStreamingPriority: (() => void) | null = null;
13
14
  let togglePollingPriority: (() => void) | null = null;
14
15
  let updateLiveTargets: ((symbol: string) => void) | null = null;
15
16
  let updateFreshnessScope: ((scope: string) => void) | null = null;
@@ -27,6 +28,31 @@ function QuoteStreamingHarness() {
27
28
  return <text>{String(tick)}</text>;
28
29
  }
29
30
 
31
+ function QuoteStreamingPriorityHarness() {
32
+ const [selected, setSelected] = useState(false);
33
+ toggleStreamingPriority = () => setSelected((current) => !current);
34
+ useQuoteStreaming([{
35
+ symbol: "AAPL",
36
+ exchange: "NASDAQ",
37
+ context: {
38
+ brokerId: "ibkr",
39
+ brokerInstanceId: "ibkr-work",
40
+ instrument: {
41
+ brokerId: "ibkr",
42
+ brokerInstanceId: "ibkr-work",
43
+ conId: 1001,
44
+ symbol: "AAPL",
45
+ },
46
+ },
47
+ route: "provider",
48
+ surface: selected ? "detail" : "portfolio",
49
+ visible: true,
50
+ selected,
51
+ weight: selected ? 100 : 10,
52
+ }]);
53
+ return <text>{selected ? "selected" : "idle"}</text>;
54
+ }
55
+
30
56
  function QuotePollingHarness() {
31
57
  useQuoteUpdates([{
32
58
  symbol: "AAPL",
@@ -82,6 +108,7 @@ afterEach(async () => {
82
108
  testSetup = undefined;
83
109
  }
84
110
  bumpHarness = null;
111
+ toggleStreamingPriority = null;
85
112
  togglePollingPriority = null;
86
113
  updateLiveTargets = null;
87
114
  updateFreshnessScope = null;
@@ -127,6 +154,72 @@ describe("useQuoteStreaming", () => {
127
154
  expect(unsubscribeCalls).toBe(0);
128
155
  });
129
156
 
157
+ test("updates priorities without replacing the hook subscription", async () => {
158
+ type CoordinatorTargets = Parameters<MarketDataCoordinator["subscribeQuotes"]>[0];
159
+ const initialTargets: CoordinatorTargets[] = [];
160
+ const updates: CoordinatorTargets[] = [];
161
+ let unsubscribeCalls = 0;
162
+ const coordinator = {
163
+ subscribeQuotes: (targets: CoordinatorTargets) => {
164
+ initialTargets.push(targets);
165
+ return Object.assign(
166
+ () => { unsubscribeCalls += 1; },
167
+ { update: (nextTargets: CoordinatorTargets) => updates.push(nextTargets) },
168
+ );
169
+ },
170
+ };
171
+ setSharedMarketDataCoordinator(coordinator as unknown as MarketDataCoordinator);
172
+
173
+ testSetup = await testRender(<QuoteStreamingPriorityHarness />, {
174
+ width: 20,
175
+ height: 1,
176
+ });
177
+ await act(async () => testSetup!.renderOnce());
178
+
179
+ expect(initialTargets).toHaveLength(1);
180
+ expect(initialTargets[0]?.[0]).toMatchObject({
181
+ instrument: {
182
+ symbol: "AAPL",
183
+ exchange: "NASDAQ",
184
+ brokerId: "ibkr",
185
+ brokerInstanceId: "ibkr-work",
186
+ instrument: { conId: 1001, symbol: "AAPL" },
187
+ },
188
+ priority: {
189
+ route: "provider",
190
+ surface: "portfolio",
191
+ visible: true,
192
+ selected: false,
193
+ weight: 10,
194
+ },
195
+ });
196
+
197
+ await act(async () => {
198
+ toggleStreamingPriority?.();
199
+ await Promise.resolve();
200
+ await testSetup!.renderOnce();
201
+ });
202
+
203
+ expect(initialTargets).toHaveLength(1);
204
+ expect(unsubscribeCalls).toBe(0);
205
+ expect(updates).toHaveLength(1);
206
+ expect(updates[0]?.[0]).toMatchObject({
207
+ instrument: {
208
+ symbol: "AAPL",
209
+ brokerId: "ibkr",
210
+ brokerInstanceId: "ibkr-work",
211
+ instrument: { conId: 1001 },
212
+ },
213
+ priority: {
214
+ route: "provider",
215
+ surface: "detail",
216
+ visible: true,
217
+ selected: true,
218
+ weight: 100,
219
+ },
220
+ });
221
+ });
222
+
130
223
  test("uses forced polling instead of a subscription when live streaming is disabled", async () => {
131
224
  let subscribeCalls = 0;
132
225
  let loadCalls = 0;