gloomberb 0.11.3 → 0.13.1

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 (574) hide show
  1. package/README.md +8 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -2
  4. package/src/api-client/auth.ts +12 -1
  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 +139 -3
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/research-activity.ts +61 -0
  11. package/src/api-client/types.ts +197 -0
  12. package/src/app/pane-runtime/index.tsx +15 -12
  13. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  14. package/src/app/pane-runtime/layout-placement.ts +13 -0
  15. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  16. package/src/app.tsx +60 -18
  17. package/src/architecture/browser-entry-graph.test.ts +96 -0
  18. package/src/brokers/profile-form.test.ts +25 -27
  19. package/src/brokers/test-broker.ts +68 -0
  20. package/src/cli/commands/plugins.ts +43 -14
  21. package/src/cli/desktop-pane-shot.ts +362 -24
  22. package/src/cli/index.ts +1 -1
  23. package/src/cli/pane-functions/capabilities.ts +126 -43
  24. package/src/cli/pane-functions/catalog.ts +16 -4
  25. package/src/cli/pane-functions/discovery.ts +1 -0
  26. package/src/cli/pane-functions/dom.test.ts +41 -0
  27. package/src/cli/pane-functions/dom.ts +199 -0
  28. package/src/cli/pane-functions/headless.test.ts +247 -0
  29. package/src/cli/pane-functions/headless.ts +423 -0
  30. package/src/cli/pane-functions/index.test.ts +20 -0
  31. package/src/cli/pane-functions/index.ts +10 -6
  32. package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
  33. package/src/cli/pane-functions/intraday-shot.ts +334 -0
  34. package/src/cli/pane-functions/options.ts +27 -2
  35. package/src/cli/pane-functions/report.test.ts +29 -1
  36. package/src/cli/pane-functions/report.ts +69 -4
  37. package/src/cli/pane-functions/resolver.ts +26 -8
  38. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  39. package/src/cli/pane-functions/screenshot.ts +428 -42
  40. package/src/cli/restore-plugins.ts +41 -0
  41. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  42. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  43. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  44. package/src/components/chart/composite/composite-chart.test.tsx +136 -21
  45. package/src/components/chart/composite/composite-chart.tsx +378 -215
  46. package/src/components/chart/composite/format.ts +8 -2
  47. package/src/components/chart/composite/interactions.test.ts +68 -102
  48. package/src/components/chart/composite/interactions.ts +214 -264
  49. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  50. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  51. package/src/components/chart/composite/rasterizer.ts +33 -0
  52. package/src/components/chart/composite/renderers.test.ts +51 -4
  53. package/src/components/chart/composite/scene.test.ts +128 -2
  54. package/src/components/chart/composite/scene.ts +149 -19
  55. package/src/components/chart/composite/text-renderer.ts +16 -2
  56. package/src/components/chart/composite/time-scale.ts +163 -62
  57. package/src/components/chart/composite/types.ts +51 -1
  58. package/src/components/chart/core/data.ts +9 -13
  59. package/src/components/chart/core/date-window.ts +7 -1
  60. package/src/components/chart/core/pointer.ts +3 -0
  61. package/src/components/chart/core/types.ts +0 -45
  62. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  63. package/src/components/chart/native/kitty/index.test.ts +0 -188
  64. package/src/components/chart/native/raster/crosshair.ts +6 -5
  65. package/src/components/chart/native/raster/primitives.ts +0 -23
  66. package/src/components/chart/native/renderer-selection.ts +81 -31
  67. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  68. package/src/components/chart/static/chart-surface.tsx +150 -0
  69. package/src/components/chart/static/index.ts +8 -0
  70. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  71. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  72. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  73. package/src/components/chart/static/series.ts +49 -0
  74. package/src/components/command-bar/action-runtime.ts +1 -3
  75. package/src/components/command-bar/assist/model.test.ts +17 -2
  76. package/src/components/command-bar/assist/model.ts +40 -9
  77. package/src/components/command-bar/commands/direct/index.ts +1 -1
  78. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  79. package/src/components/command-bar/commands/registry.ts +0 -11
  80. package/src/components/command-bar/helpers.test.ts +2 -2
  81. package/src/components/command-bar/helpers.ts +2 -6
  82. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  83. package/src/components/command-bar/list/badge.test.ts +54 -0
  84. package/src/components/command-bar/list/badge.ts +62 -0
  85. package/src/components/command-bar/list/model.test.ts +103 -0
  86. package/src/components/command-bar/list/model.ts +61 -4
  87. package/src/components/command-bar/list/view.test.tsx +113 -0
  88. package/src/components/command-bar/list/view.tsx +147 -131
  89. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  90. package/src/components/command-bar/panel/body.tsx +51 -65
  91. package/src/components/command-bar/panel/index.tsx +48 -13
  92. package/src/components/command-bar/panel/layout.test.ts +139 -0
  93. package/src/components/command-bar/panel/layout.ts +65 -28
  94. package/src/components/command-bar/panel/native-surface.ts +24 -0
  95. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  96. package/src/components/command-bar/panel/runtime.ts +6 -3
  97. package/src/components/command-bar/panel/state.ts +41 -7
  98. package/src/components/command-bar/panel/types.ts +7 -1
  99. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  100. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  101. package/src/components/command-bar/routes/root/results.ts +30 -28
  102. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  103. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  104. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  105. package/src/components/command-bar/routes/root/selection.ts +1 -12
  106. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  107. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  108. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  109. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  110. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  111. package/src/components/command-bar/routing/actions.ts +1 -81
  112. package/src/components/command-bar/routing/launch-request.ts +1 -1
  113. package/src/components/command-bar/routing/list-state.ts +6 -21
  114. package/src/components/command-bar/selection-runtime.ts +1 -1
  115. package/src/components/command-bar/surface/assist.test.tsx +34 -19
  116. package/src/components/command-bar/surface/environment.ts +8 -1
  117. package/src/components/command-bar/surface/index.test.tsx +84 -68
  118. package/src/components/command-bar/surface/index.tsx +33 -36
  119. package/src/components/command-bar/surface/test-harness.tsx +38 -7
  120. package/src/components/command-bar/theme-picker.tsx +43 -15
  121. package/src/components/command-bar/view-model.test.ts +53 -8
  122. package/src/components/command-bar/view-model.ts +43 -14
  123. package/src/components/command-bar/workflow/tickers.ts +1 -0
  124. package/src/components/command-bar/workflow/types.ts +1 -1
  125. package/src/components/data-table/view.test.tsx +45 -6
  126. package/src/components/data-table/view.tsx +55 -9
  127. package/src/components/index.ts +10 -2
  128. package/src/components/input-search-bar.test.tsx +36 -2
  129. package/src/components/input-search-bar.tsx +18 -1
  130. package/src/components/layout/header.test.tsx +57 -10
  131. package/src/components/layout/header.tsx +292 -149
  132. package/src/components/layout/market-summary.test.ts +81 -0
  133. package/src/components/layout/market-summary.ts +109 -0
  134. package/src/components/layout/shell/chrome.ts +96 -1
  135. package/src/components/layout/shell/index.tsx +11 -3
  136. package/src/components/layout/shell/menu.ts +9 -0
  137. package/src/components/layout/shell/pane/actions.ts +29 -0
  138. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  139. package/src/components/layout/shell/shortcuts.ts +3 -0
  140. package/src/components/layout/status-bar.test.tsx +9 -8
  141. package/src/components/layout/status-bar.tsx +85 -54
  142. package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
  143. package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
  144. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  145. package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
  146. package/src/components/onboarding/onboarding-wizard.tsx +49 -14
  147. package/src/components/price-sparkline/index.ts +61 -34
  148. package/src/components/price-sparkline/view.tsx +1 -1
  149. package/src/components/sign-in-gate.test.tsx +128 -0
  150. package/src/components/sign-in-gate.tsx +149 -0
  151. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  152. package/src/components/ticker/badge/list.tsx +17 -3
  153. package/src/components/ticker/list-table-view.tsx +6 -3
  154. package/src/components/ui/data-table/index.tsx +3 -2
  155. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  156. package/src/components/ui/frame.tsx +28 -0
  157. package/src/components/ui/index.ts +3 -0
  158. package/src/components/ui/native-select.tsx +20 -10
  159. package/src/components/ui/select-button.tsx +138 -0
  160. package/src/components/ui/status.tsx +2 -2
  161. package/src/components/ui/table-layout.ts +1 -1
  162. package/src/components/use-double-click-activation.ts +7 -3
  163. package/src/core/app-services.ts +3 -3
  164. package/src/data/config/layout.ts +12 -6
  165. package/src/data/config/store/index.test.ts +120 -0
  166. package/src/data/config/store/migrations.ts +41 -0
  167. package/src/data/config/store/node.ts +7 -1
  168. package/src/data/config/store/normalize.ts +9 -3
  169. package/src/i18n/es.ts +6 -2
  170. package/src/i18n/ja.ts +6 -2
  171. package/src/i18n/ko.ts +6 -2
  172. package/src/i18n/zh-cn.ts +6 -2
  173. package/src/i18n/zh-tw.ts +6 -2
  174. package/src/layout-marketplace/pane-imagery.ts +1 -0
  175. package/src/layout-marketplace/payload.test.ts +17 -0
  176. package/src/layout-marketplace/payload.ts +40 -5
  177. package/src/market-data/coordinator/index.ts +3 -2
  178. package/src/market-data/coordinator/quotes.ts +173 -66
  179. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  180. package/src/pane-settings.ts +1 -1
  181. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  182. package/src/plugins/builtin/analytics/view.tsx +1 -1
  183. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  184. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  185. package/src/plugins/builtin/cds/client.test.ts +20 -0
  186. package/src/plugins/builtin/cds/client.ts +13 -3
  187. package/src/plugins/builtin/cds/headless.test.ts +30 -0
  188. package/src/plugins/builtin/cds/headless.ts +113 -0
  189. package/src/plugins/builtin/cds/index.tsx +4 -0
  190. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  191. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
  192. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  193. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  194. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  195. package/src/plugins/builtin/cloud/askg/protocol.test.ts +189 -0
  196. package/src/plugins/builtin/cloud/askg/protocol.ts +274 -0
  197. package/src/plugins/builtin/cloud/auth-dialog.tsx +19 -227
  198. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  199. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
  200. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  201. package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
  202. package/src/plugins/builtin/composite-plugins.ts +14 -1
  203. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  204. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  205. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  206. package/src/plugins/builtin/credit-conditions/client.ts +1 -1
  207. package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
  208. package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
  209. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  210. package/src/plugins/builtin/credit-conditions/index.tsx +6 -21
  211. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  212. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  213. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  214. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  215. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  216. package/src/plugins/builtin/earnings-calls/pane.tsx +760 -0
  217. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  218. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  219. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +337 -0
  220. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  221. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  222. package/src/plugins/builtin/econ/index.tsx +2 -1
  223. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  224. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  225. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  226. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  227. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  228. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  229. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  230. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  231. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  232. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  233. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  234. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  235. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  236. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  237. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  238. package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
  239. package/src/plugins/builtin/fear-greed/headless.ts +84 -0
  240. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  241. package/src/plugins/builtin/help/index.tsx +4 -0
  242. package/src/plugins/builtin/holders/index.tsx +1 -0
  243. package/src/plugins/builtin/insider/index.tsx +1 -0
  244. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  245. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  246. package/src/plugins/builtin/market-movers/client.ts +76 -0
  247. package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
  248. package/src/plugins/builtin/market-movers/headless.ts +73 -0
  249. package/src/plugins/builtin/market-movers/index.tsx +11 -51
  250. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  251. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  252. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  253. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  254. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  255. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  256. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  257. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  258. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  259. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  260. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  261. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  262. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  263. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  264. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  265. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  266. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  267. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  268. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  269. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  270. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  271. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  272. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  273. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  274. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  275. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  276. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  277. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  278. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  279. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  280. package/src/plugins/builtin/options/index.tsx +1 -0
  281. package/src/plugins/builtin/options/view.test.tsx +29 -1
  282. package/src/plugins/builtin/options/view.tsx +1 -22
  283. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  284. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  285. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  286. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  287. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  288. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  289. package/src/plugins/builtin/plugin-module.ts +9 -1
  290. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
  291. package/src/plugins/builtin/research/event-model.ts +245 -0
  292. package/src/plugins/builtin/research/events-headless.test.ts +32 -0
  293. package/src/plugins/builtin/research/events-headless.ts +87 -0
  294. package/src/plugins/builtin/research/index.tsx +8 -0
  295. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  296. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  297. package/src/plugins/builtin/research-search/data.ts +109 -0
  298. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  299. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  300. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  301. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  302. package/src/plugins/builtin/research-search/index.tsx +85 -0
  303. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  304. package/src/plugins/builtin/research-search/model.ts +291 -0
  305. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  306. package/src/plugins/builtin/research-search/pane.tsx +682 -0
  307. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  308. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  309. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  310. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  311. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  312. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  313. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  314. package/src/plugins/builtin/scanner/headless.test.ts +59 -0
  315. package/src/plugins/builtin/scanner/headless.ts +125 -0
  316. package/src/plugins/builtin/scanner/index.ts +4 -0
  317. package/src/plugins/builtin/sec/index.tsx +1 -0
  318. package/src/plugins/builtin/sectors/client.ts +54 -0
  319. package/src/plugins/builtin/sectors/headless.test.ts +28 -0
  320. package/src/plugins/builtin/sectors/headless.ts +80 -0
  321. package/src/plugins/builtin/sectors/index.tsx +6 -51
  322. package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
  323. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  324. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  325. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  326. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  327. package/src/plugins/builtin/ticker-detail/index.tsx +0 -14
  328. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  329. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
  330. package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
  331. package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
  332. package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
  333. package/src/plugins/builtin/volatility/client.ts +16 -3
  334. package/src/plugins/builtin/volatility/headless.test.ts +35 -0
  335. package/src/plugins/builtin/volatility/headless.ts +75 -0
  336. package/src/plugins/builtin/volatility/index.tsx +5 -1
  337. package/src/plugins/builtin/world-indices/client.ts +43 -0
  338. package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
  339. package/src/plugins/builtin/world-indices/headless.ts +109 -0
  340. package/src/plugins/builtin/world-indices/index.tsx +4 -0
  341. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  342. package/src/plugins/builtin/world-venue-map/map.tsx +213 -69
  343. package/src/plugins/builtin/world-venue-map/model.test.ts +32 -0
  344. package/src/plugins/builtin/world-venue-map/model.ts +134 -13
  345. package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
  346. package/src/plugins/builtin/yield-curve/headless.ts +67 -0
  347. package/src/plugins/builtin/yield-curve/index.tsx +7 -9
  348. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
  349. package/src/plugins/bundle.test.ts +166 -0
  350. package/src/plugins/bundle.ts +141 -0
  351. package/src/plugins/catalog-backend.test.ts +26 -0
  352. package/src/plugins/catalog-backend.ts +9 -2
  353. package/src/plugins/catalog-browser.test.ts +1 -13
  354. package/src/plugins/catalog-browser.ts +14 -0
  355. package/src/plugins/catalog-manifest.test.ts +58 -0
  356. package/src/plugins/catalog-manifest.ts +99 -0
  357. package/src/plugins/catalog-ui.ts +20 -10
  358. package/src/plugins/catalog.ts +9 -2
  359. package/src/plugins/current-target.ts +28 -0
  360. package/src/plugins/host-contract.ts +46 -0
  361. package/src/plugins/host-link.ts +158 -0
  362. package/src/plugins/host-modules.ts +73 -0
  363. package/src/plugins/loader.ts +90 -23
  364. package/src/plugins/ownership.ts +1 -0
  365. package/src/plugins/prediction-markets/index.tsx +1 -0
  366. package/src/plugins/registry/context.ts +3 -0
  367. package/src/plugins/registry/contributions.ts +27 -0
  368. package/src/plugins/registry/index.test.ts +25 -0
  369. package/src/plugins/registry/index.ts +8 -0
  370. package/src/plugins/registry/pane-settings.ts +6 -13
  371. package/src/plugins/seed.test.ts +83 -0
  372. package/src/plugins/seed.ts +83 -0
  373. package/src/public/broker.ts +37 -0
  374. package/src/public/market-data.ts +9 -0
  375. package/src/public/pane-hooks.ts +57 -0
  376. package/src/public/public-api.test.ts +285 -0
  377. package/src/public/react.ts +62 -0
  378. package/src/public/test-support.ts +23 -0
  379. package/src/public/time-series.ts +8 -0
  380. package/src/public/utils.ts +77 -0
  381. package/src/remote/layout-helpers.ts +6 -8
  382. package/src/remote/resources.ts +2 -0
  383. package/src/remote/schema.test.ts +65 -0
  384. package/src/remote/schema.ts +323 -39
  385. package/src/remote/semantic-tree.test.tsx +105 -0
  386. package/src/remote/semantic-tree.tsx +53 -35
  387. package/src/remote/types.ts +49 -2
  388. package/src/renderers/browser/app-services.ts +3 -0
  389. package/src/renderers/browser/config-host.ts +24 -8
  390. package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
  391. package/src/renderers/browser/deeplink-bridge.ts +15 -3
  392. package/src/renderers/browser/main.tsx +8 -0
  393. package/src/renderers/browser/research-entry.test.ts +8 -0
  394. package/src/renderers/browser/research-entry.ts +7 -0
  395. package/src/renderers/cloudflare/worker.test.ts +19 -0
  396. package/src/renderers/cloudflare/worker.ts +1 -1
  397. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
  398. package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
  399. package/src/renderers/electrobun/bun/external-plugins.ts +156 -0
  400. package/src/renderers/electrobun/bun/index.ts +5 -2
  401. package/src/renderers/electrobun/shared/protocol.ts +21 -0
  402. package/src/renderers/electrobun/view/build-assets.ts +0 -3
  403. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +114 -11
  404. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  405. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  406. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  407. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  408. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  409. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  410. package/src/renderers/electrobun/view/main.tsx +27 -2
  411. package/src/renderers/electrobun/view/styles.css +5 -0
  412. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  413. package/src/renderers/opentui/chart-surface.tsx +5 -1
  414. package/src/renderers/opentui/host.tsx +16 -2
  415. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  416. package/src/renderers/opentui/interaction-performance.ts +188 -0
  417. package/src/renderers/opentui/start.tsx +26 -1
  418. package/src/renderers/opentui/test-utils.tsx +13 -0
  419. package/src/renderers/opentui/toast-host.tsx +26 -2
  420. package/src/renderers/opentui/ui-host.tsx +1 -0
  421. package/src/sources/provider-router/search.test.ts +191 -0
  422. package/src/sources/provider-router/search.ts +92 -39
  423. package/src/state/app/context/index.tsx +2 -0
  424. package/src/state/app/context/selector.test.tsx +33 -1
  425. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  426. package/src/state/hooks/quote-streaming.ts +106 -30
  427. package/src/state/pane-table-export-registry.ts +19 -0
  428. package/src/sync/core-contributors.test.ts +111 -0
  429. package/src/sync/core-contributors.ts +155 -1
  430. package/src/theme/color-utils.ts +1 -1
  431. package/src/theme/colors.ts +50 -0
  432. package/src/theme/themes.test.ts +15 -2
  433. package/src/theme/themes.ts +19 -1
  434. package/src/tickers/search/index.test.ts +26 -0
  435. package/src/tickers/search/index.ts +42 -17
  436. package/src/tickers/search/ranking.ts +14 -0
  437. package/src/time-series/chart-data.test.ts +216 -0
  438. package/src/time-series/resolution.test.ts +25 -26
  439. package/src/time-series/resolution.ts +41 -10
  440. package/src/time-series/resolve.test.ts +118 -1
  441. package/src/time-series/resolve.ts +21 -16
  442. package/src/time-series/types.ts +3 -1
  443. package/src/time-series/use-chart-resolution.ts +5 -0
  444. package/src/types/config.ts +125 -3
  445. package/src/types/data-provider.ts +11 -0
  446. package/src/types/headless.ts +161 -0
  447. package/src/types/plugin.ts +102 -0
  448. package/src/ui/host.tsx +12 -1
  449. package/src/ui/index.tsx +1 -0
  450. package/src/ui/toast.tsx +8 -4
  451. package/src/utils/format.ts +35 -0
  452. package/src/utils/fuzzy-search.test.ts +94 -0
  453. package/src/utils/fuzzy-search.ts +81 -22
  454. package/src/utils/shortcut-labels.ts +14 -0
  455. package/src/components/chart/core/axis-format.ts +0 -157
  456. package/src/components/chart/core/index.test.ts +0 -1075
  457. package/src/components/chart/core/projection.ts +0 -357
  458. package/src/components/chart/core/render-utils.ts +0 -4
  459. package/src/components/chart/core/renderer.ts +0 -244
  460. package/src/components/chart/core/scene.ts +0 -180
  461. package/src/components/chart/core/time-axis.ts +0 -330
  462. package/src/components/chart/cursor-motion.ts +0 -47
  463. package/src/components/chart/indicators/types.ts +0 -26
  464. package/src/components/chart/market-session.ts +0 -176
  465. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  466. package/src/components/chart/multi-line/renderer.ts +0 -161
  467. package/src/components/chart/multi-line/scene.ts +0 -188
  468. package/src/components/chart/native/raster/layout.ts +0 -129
  469. package/src/components/chart/native/raster/price-chart.ts +0 -244
  470. package/src/components/chart/rendering/chart-draw.ts +0 -270
  471. package/src/components/chart/rendering/geometry.ts +0 -95
  472. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  473. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  474. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  475. package/src/components/chart/static/chart/surface.tsx +0 -358
  476. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  477. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  478. package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
  479. package/src/plugins/broker-sync/native-loader.ts +0 -10
  480. package/src/plugins/broker-sync/normalize.ts +0 -308
  481. package/src/plugins/broker-sync/public.ts +0 -122
  482. package/src/plugins/broker-sync/robinhood-native.ts +0 -367
  483. package/src/plugins/broker-sync/robinhood.ts +0 -114
  484. package/src/plugins/broker-sync/simplefin-native.ts +0 -201
  485. package/src/plugins/broker-sync/simplefin.ts +0 -70
  486. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  487. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  488. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  489. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  490. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  491. package/src/plugins/builtin/substack/api/store.ts +0 -186
  492. package/src/plugins/builtin/substack/api/types.ts +0 -42
  493. package/src/plugins/builtin/substack/api.test.ts +0 -298
  494. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  495. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  496. package/src/plugins/builtin/substack/content.ts +0 -478
  497. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  498. package/src/plugins/builtin/substack/index.tsx +0 -49
  499. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  500. package/src/plugins/builtin/substack/model.test.ts +0 -155
  501. package/src/plugins/builtin/substack/normalize.ts +0 -343
  502. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  503. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  504. package/src/plugins/builtin/substack/pane.tsx +0 -547
  505. package/src/plugins/builtin/substack/read-state.ts +0 -30
  506. package/src/plugins/builtin/substack/table.ts +0 -111
  507. package/src/plugins/builtin/substack/types.ts +0 -71
  508. package/src/plugins/builtin/substack/utils.ts +0 -131
  509. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  510. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  511. package/src/plugins/ibkr/account-cache.ts +0 -24
  512. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  513. package/src/plugins/ibkr/config.test.ts +0 -79
  514. package/src/plugins/ibkr/config.ts +0 -250
  515. package/src/plugins/ibkr/dialogs.tsx +0 -40
  516. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  517. package/src/plugins/ibkr/flex/index.ts +0 -367
  518. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  519. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  520. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  521. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  522. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  523. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  524. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  525. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  526. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  527. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  528. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  529. package/src/plugins/ibkr/gateway/history.ts +0 -120
  530. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  531. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  532. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  533. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  534. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  535. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  536. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  537. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  538. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  539. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  540. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  541. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  542. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  543. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  544. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  545. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  546. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  547. package/src/plugins/ibkr/gateway/types.ts +0 -27
  548. package/src/plugins/ibkr/index.test.tsx +0 -162
  549. package/src/plugins/ibkr/index.tsx +0 -159
  550. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  551. package/src/plugins/ibkr/instance-selection.ts +0 -36
  552. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  553. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  554. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  555. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  556. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  557. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  558. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  559. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  560. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  561. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  562. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  563. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  564. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  565. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  566. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  567. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  568. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  569. package/src/plugins/ibkr/trade/utils.ts +0 -144
  570. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  571. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  572. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  573. package/src/plugins/ibkr/trading/state.ts +0 -335
  574. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
@@ -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;
@@ -4,7 +4,7 @@ import type { QuoteSubscriptionTarget } from "../../types/data-provider";
4
4
  import type { Quote } from "../../types/financials";
5
5
  import { debugLog } from "../../utils/debug-log";
6
6
  import { normalizeSymbol } from "../../utils/exchanges";
7
- import { getSharedMarketDataCoordinator } from "../../market-data/coordinator";
7
+ import { getSharedMarketDataCoordinator, type MarketDataCoordinator } from "../../market-data/coordinator";
8
8
  import { useQuoteEntries } from "../../market-data/hooks";
9
9
  import type { InstrumentRef } from "../../market-data/request-types";
10
10
  import type { QueryEntry } from "../../market-data/result-types";
@@ -33,12 +33,11 @@ export function normalizeQuoteStreamSubscriptionTarget(target: QuoteSubscription
33
33
  };
34
34
  }
35
35
 
36
- export function buildQuoteStreamSubscriptionKey(target: QuoteSubscriptionTarget): string {
36
+ export function buildQuoteStreamSubscriptionIdentityKey(target: QuoteSubscriptionTarget): string {
37
37
  const contractKey = target.context?.instrument?.conId
38
38
  ?? target.context?.instrument?.localSymbol
39
39
  ?? target.context?.instrument?.symbol
40
40
  ?? "";
41
- const weight = Number.isFinite(target.weight) ? String(target.weight) : "";
42
41
  return [
43
42
  target.symbol,
44
43
  target.exchange ?? "",
@@ -46,6 +45,13 @@ export function buildQuoteStreamSubscriptionKey(target: QuoteSubscriptionTarget)
46
45
  target.context?.brokerInstanceId ?? "",
47
46
  contractKey,
48
47
  target.route ?? "auto",
48
+ ].join("|");
49
+ }
50
+
51
+ export function buildQuoteStreamSubscriptionKey(target: QuoteSubscriptionTarget): string {
52
+ const weight = Number.isFinite(target.weight) ? String(target.weight) : "";
53
+ return [
54
+ buildQuoteStreamSubscriptionIdentityKey(target),
49
55
  target.surface ?? "",
50
56
  target.visible ? "visible" : "",
51
57
  target.selected ? "selected" : "",
@@ -53,6 +59,37 @@ export function buildQuoteStreamSubscriptionKey(target: QuoteSubscriptionTarget)
53
59
  ].join("|");
54
60
  }
55
61
 
62
+ type CoordinatorQuoteTargets = Parameters<MarketDataCoordinator["subscribeQuotes"]>[0];
63
+ type UpdateableQuoteSubscription = (() => void) & {
64
+ update?: (targets: CoordinatorQuoteTargets) => void;
65
+ };
66
+
67
+ interface ActiveQuoteSubscription {
68
+ identityKey: string;
69
+ subscriptionKey: string;
70
+ count: number;
71
+ dispose: UpdateableQuoteSubscription;
72
+ }
73
+
74
+ function toCoordinatorQuoteTargets(targets: QuoteSubscriptionTarget[]): CoordinatorQuoteTargets {
75
+ return targets.map((target) => ({
76
+ instrument: {
77
+ symbol: target.symbol,
78
+ exchange: target.exchange,
79
+ brokerId: target.context?.brokerId,
80
+ brokerInstanceId: target.context?.brokerInstanceId,
81
+ instrument: target.context?.instrument ?? null,
82
+ },
83
+ priority: {
84
+ route: target.route,
85
+ surface: target.surface,
86
+ visible: target.visible,
87
+ selected: target.selected,
88
+ weight: target.weight,
89
+ },
90
+ }));
91
+ }
92
+
56
93
  export function useQuoteStreaming(
57
94
  targets: QuoteSubscriptionTarget[],
58
95
  { enabled = true }: QuoteStreamingOptions = {},
@@ -70,48 +107,86 @@ export function useQuoteStreaming(
70
107
  const sortedEntries = [...normalizedEntries.entries()].sort(([left], [right]) => left.localeCompare(right));
71
108
  const normalizedTargets = sortedEntries.map(([, target]) => target);
72
109
  const subscriptionKey = sortedEntries.map(([key]) => key).join("|");
110
+ const identityKey = [...new Set(normalizedTargets.map(buildQuoteStreamSubscriptionIdentityKey))]
111
+ .sort()
112
+ .join("\u001f");
113
+ const coordinatorTargets = toCoordinatorQuoteTargets(normalizedTargets);
114
+ const latestSubscriptionRef = useRef({
115
+ identityKey,
116
+ subscriptionKey,
117
+ targets: coordinatorTargets,
118
+ });
119
+ latestSubscriptionRef.current = {
120
+ identityKey,
121
+ subscriptionKey,
122
+ targets: coordinatorTargets,
123
+ };
124
+ const activeSubscriptionRef = useRef<ActiveQuoteSubscription | null>(null);
73
125
 
74
126
  useEffect(() => {
127
+ const latest = latestSubscriptionRef.current;
75
128
  if (!enabled || !appActive) {
76
- if (normalizedTargets.length > 0) {
129
+ if (latest.targets.length > 0) {
77
130
  quoteStreamLog.info("skipping subscription", {
78
131
  reason: enabled ? "inactive" : "disabled",
79
- targets: subscriptionKey,
132
+ targets: latest.subscriptionKey,
80
133
  });
81
134
  }
82
135
  return;
83
136
  }
84
- if (!coordinator || normalizedTargets.length === 0) return;
137
+ if (!coordinator || latest.targets.length === 0) return;
85
138
  quoteStreamLog.info("subscribe", {
86
139
  providerId: "market-data",
87
- count: normalizedTargets.length,
88
- targets: subscriptionKey,
140
+ count: latest.targets.length,
141
+ targets: latest.subscriptionKey,
89
142
  });
90
- const unsubscribe = coordinator.subscribeQuotes(normalizedTargets.map((target) => ({
91
- instrument: {
92
- symbol: target.symbol,
93
- exchange: target.exchange,
94
- brokerId: target.context?.brokerId,
95
- brokerInstanceId: target.context?.brokerInstanceId,
96
- instrument: target.context?.instrument ?? null,
97
- },
98
- priority: {
99
- route: target.route,
100
- surface: target.surface,
101
- visible: target.visible,
102
- selected: target.selected,
103
- weight: target.weight,
104
- },
105
- })));
143
+ const active: ActiveQuoteSubscription = {
144
+ identityKey: latest.identityKey,
145
+ subscriptionKey: latest.subscriptionKey,
146
+ count: latest.targets.length,
147
+ dispose: coordinator.subscribeQuotes(latest.targets) as UpdateableQuoteSubscription,
148
+ };
149
+ activeSubscriptionRef.current = active;
106
150
  return () => {
151
+ if (activeSubscriptionRef.current === active) {
152
+ activeSubscriptionRef.current = null;
153
+ }
107
154
  quoteStreamLog.info("unsubscribe", {
108
155
  providerId: "market-data",
109
- count: normalizedTargets.length,
110
- targets: subscriptionKey,
156
+ count: active.count,
157
+ targets: active.subscriptionKey,
111
158
  });
112
- unsubscribe?.();
159
+ active.dispose();
113
160
  };
114
- }, [appActive, coordinator, enabled, subscriptionKey]);
161
+ }, [appActive, coordinator, enabled, identityKey]);
162
+
163
+ useEffect(() => {
164
+ if (!enabled || !appActive || !coordinator) return;
165
+ const active = activeSubscriptionRef.current;
166
+ const latest = latestSubscriptionRef.current;
167
+ if (
168
+ !active
169
+ || active.identityKey !== latest.identityKey
170
+ || active.subscriptionKey === latest.subscriptionKey
171
+ ) {
172
+ return;
173
+ }
174
+
175
+ quoteStreamLog.info("update subscription", {
176
+ providerId: "market-data",
177
+ count: latest.targets.length,
178
+ targets: latest.subscriptionKey,
179
+ });
180
+ if (typeof active.dispose.update === "function") {
181
+ active.dispose.update(latest.targets);
182
+ } else {
183
+ const previousDispose = active.dispose;
184
+ active.dispose = coordinator.subscribeQuotes(latest.targets) as UpdateableQuoteSubscription;
185
+ previousDispose();
186
+ }
187
+ active.subscriptionKey = latest.subscriptionKey;
188
+ active.count = latest.targets.length;
189
+ }, [appActive, coordinator, enabled, identityKey, subscriptionKey]);
115
190
  }
116
191
 
117
192
  export function useQuoteUpdates(
@@ -195,8 +270,9 @@ export function useLiveQuoteEntries(
195
270
  const normalized = normalizeQuoteStreamSubscriptionTarget(target);
196
271
  return normalized ? [normalized] : [];
197
272
  });
198
- const targetKey = normalizedTargets
199
- .map((target) => buildQuoteStreamSubscriptionKey(target))
273
+ const targetKey = [...new Set(
274
+ normalizedTargets.map((target) => buildQuoteStreamSubscriptionIdentityKey(target)),
275
+ )]
200
276
  .sort()
201
277
  .join("\u001f");
202
278
  const liveStreaming = options.liveStreaming !== false;
@@ -1,3 +1,5 @@
1
+ import { createCsvExportFilename } from "../utils/csv";
2
+
1
3
  export type PaneTableExporter = (filename: string) => Promise<string>;
2
4
 
3
5
  const exporters = new Map<string, Map<symbol, PaneTableExporter>>();
@@ -29,3 +31,20 @@ export async function exportPaneTable(paneId: string, filename: string): Promise
29
31
  }
30
32
  return [...paneExporters.values()][0]!(filename);
31
33
  }
34
+
35
+ /**
36
+ * Shared entry point for the pane menu, the keyboard shortcut, and pane settings so
37
+ * every surface produces the same filename and the same success/failure notification.
38
+ */
39
+ export async function exportPaneTableCsv(
40
+ paneId: string,
41
+ title: string,
42
+ notify: (notification: { body: string; type: "success" | "error" }) => void,
43
+ ): Promise<void> {
44
+ try {
45
+ const location = await exportPaneTable(paneId, createCsvExportFilename(title));
46
+ notify({ body: `Exported to ${location}`, type: "success" });
47
+ } catch {
48
+ notify({ body: "Failed to export CSV", type: "error" });
49
+ }
50
+ }