gloomberb 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (489) hide show
  1. package/README.md +6 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +103 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +129 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +196 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/cli/commands/plugins.ts +39 -11
  20. package/src/cli/desktop-pane-shot.ts +285 -16
  21. package/src/cli/index.ts +1 -1
  22. package/src/cli/pane-functions/capabilities.ts +58 -28
  23. package/src/cli/pane-functions/catalog.ts +16 -4
  24. package/src/cli/pane-functions/discovery.ts +1 -0
  25. package/src/cli/pane-functions/headless.test.ts +247 -0
  26. package/src/cli/pane-functions/headless.ts +423 -0
  27. package/src/cli/pane-functions/index.test.ts +9 -0
  28. package/src/cli/pane-functions/index.ts +6 -2
  29. package/src/cli/pane-functions/options.ts +27 -2
  30. package/src/cli/pane-functions/report.ts +4 -0
  31. package/src/cli/pane-functions/resolver.ts +26 -8
  32. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  33. package/src/cli/pane-functions/screenshot.ts +236 -20
  34. package/src/cli/restore-plugins.ts +34 -0
  35. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  36. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  37. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  38. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  39. package/src/components/chart/composite/composite-chart.tsx +301 -204
  40. package/src/components/chart/composite/format.ts +8 -2
  41. package/src/components/chart/composite/interactions.test.ts +68 -102
  42. package/src/components/chart/composite/interactions.ts +214 -264
  43. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  44. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  45. package/src/components/chart/composite/scene.ts +57 -3
  46. package/src/components/chart/composite/text-renderer.ts +3 -2
  47. package/src/components/chart/composite/time-scale.ts +138 -57
  48. package/src/components/chart/composite/types.ts +25 -1
  49. package/src/components/chart/core/data.ts +9 -13
  50. package/src/components/chart/core/date-window.ts +7 -1
  51. package/src/components/chart/core/pointer.ts +3 -0
  52. package/src/components/chart/core/types.ts +0 -45
  53. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  54. package/src/components/chart/native/kitty/index.test.ts +0 -188
  55. package/src/components/chart/native/raster/crosshair.ts +6 -5
  56. package/src/components/chart/native/raster/primitives.ts +0 -23
  57. package/src/components/chart/native/renderer-selection.ts +81 -31
  58. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  59. package/src/components/chart/static/chart-surface.tsx +150 -0
  60. package/src/components/chart/static/index.ts +8 -0
  61. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  62. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  63. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  64. package/src/components/chart/static/series.ts +49 -0
  65. package/src/components/command-bar/action-runtime.ts +1 -3
  66. package/src/components/command-bar/assist/model.test.ts +17 -2
  67. package/src/components/command-bar/assist/model.ts +40 -9
  68. package/src/components/command-bar/commands/direct/index.ts +1 -1
  69. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  70. package/src/components/command-bar/commands/registry.ts +0 -11
  71. package/src/components/command-bar/helpers.test.ts +2 -2
  72. package/src/components/command-bar/helpers.ts +2 -6
  73. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  74. package/src/components/command-bar/list/badge.test.ts +54 -0
  75. package/src/components/command-bar/list/badge.ts +62 -0
  76. package/src/components/command-bar/list/model.test.ts +103 -0
  77. package/src/components/command-bar/list/model.ts +61 -4
  78. package/src/components/command-bar/list/view.test.tsx +113 -0
  79. package/src/components/command-bar/list/view.tsx +147 -131
  80. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  81. package/src/components/command-bar/panel/body.tsx +51 -65
  82. package/src/components/command-bar/panel/index.tsx +48 -13
  83. package/src/components/command-bar/panel/layout.test.ts +139 -0
  84. package/src/components/command-bar/panel/layout.ts +65 -28
  85. package/src/components/command-bar/panel/native-surface.ts +24 -0
  86. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  87. package/src/components/command-bar/panel/runtime.ts +6 -3
  88. package/src/components/command-bar/panel/state.ts +41 -7
  89. package/src/components/command-bar/panel/types.ts +7 -1
  90. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  91. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  92. package/src/components/command-bar/routes/root/results.ts +30 -28
  93. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  94. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  95. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  96. package/src/components/command-bar/routes/root/selection.ts +1 -12
  97. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  98. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  99. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  100. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  101. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  102. package/src/components/command-bar/routing/actions.ts +1 -81
  103. package/src/components/command-bar/routing/launch-request.ts +1 -1
  104. package/src/components/command-bar/routing/list-state.ts +6 -21
  105. package/src/components/command-bar/selection-runtime.ts +1 -1
  106. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  107. package/src/components/command-bar/surface/environment.ts +8 -1
  108. package/src/components/command-bar/surface/index.test.tsx +84 -68
  109. package/src/components/command-bar/surface/index.tsx +33 -36
  110. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  111. package/src/components/command-bar/theme-picker.tsx +43 -15
  112. package/src/components/command-bar/view-model.test.ts +53 -8
  113. package/src/components/command-bar/view-model.ts +43 -14
  114. package/src/components/command-bar/workflow/tickers.ts +1 -0
  115. package/src/components/command-bar/workflow/types.ts +1 -1
  116. package/src/components/data-table/view.test.tsx +45 -6
  117. package/src/components/data-table/view.tsx +55 -9
  118. package/src/components/index.ts +10 -2
  119. package/src/components/input-search-bar.test.tsx +36 -2
  120. package/src/components/input-search-bar.tsx +18 -1
  121. package/src/components/layout/header.test.tsx +57 -10
  122. package/src/components/layout/header.tsx +292 -149
  123. package/src/components/layout/market-summary.test.ts +81 -0
  124. package/src/components/layout/market-summary.ts +109 -0
  125. package/src/components/layout/shell/chrome.ts +96 -1
  126. package/src/components/layout/shell/index.tsx +11 -3
  127. package/src/components/layout/shell/menu.ts +9 -0
  128. package/src/components/layout/shell/pane/actions.ts +29 -0
  129. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  130. package/src/components/layout/shell/shortcuts.ts +3 -0
  131. package/src/components/layout/status-bar.test.tsx +9 -8
  132. package/src/components/layout/status-bar.tsx +85 -54
  133. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  134. package/src/components/price-sparkline/index.ts +61 -34
  135. package/src/components/price-sparkline/view.tsx +1 -1
  136. package/src/components/sign-in-gate.test.tsx +128 -0
  137. package/src/components/sign-in-gate.tsx +149 -0
  138. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  139. package/src/components/ticker/badge/list.tsx +17 -3
  140. package/src/components/ticker/list-table-view.tsx +6 -3
  141. package/src/components/ui/data-table/index.tsx +3 -2
  142. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  143. package/src/components/ui/frame.tsx +28 -0
  144. package/src/components/ui/index.ts +3 -0
  145. package/src/components/ui/native-select.tsx +20 -10
  146. package/src/components/ui/select-button.tsx +138 -0
  147. package/src/components/ui/status.tsx +2 -2
  148. package/src/components/ui/table-layout.ts +1 -1
  149. package/src/components/use-double-click-activation.ts +7 -3
  150. package/src/core/app-services.ts +3 -3
  151. package/src/data/config/layout.ts +12 -6
  152. package/src/data/config/store/index.test.ts +97 -0
  153. package/src/data/config/store/migrations.ts +41 -0
  154. package/src/data/config/store/normalize.ts +5 -3
  155. package/src/i18n/es.ts +6 -2
  156. package/src/i18n/ja.ts +6 -2
  157. package/src/i18n/ko.ts +6 -2
  158. package/src/i18n/zh-cn.ts +6 -2
  159. package/src/i18n/zh-tw.ts +6 -2
  160. package/src/layout-marketplace/pane-imagery.ts +1 -0
  161. package/src/layout-marketplace/payload.test.ts +17 -0
  162. package/src/layout-marketplace/payload.ts +40 -5
  163. package/src/market-data/coordinator/index.ts +3 -2
  164. package/src/market-data/coordinator/quotes.ts +173 -66
  165. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  166. package/src/pane-settings.ts +1 -1
  167. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  168. package/src/plugins/builtin/analytics/view.tsx +1 -1
  169. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  170. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  171. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  172. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  173. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  174. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  175. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  176. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  177. package/src/plugins/builtin/composite-plugins.ts +14 -1
  178. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  179. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  180. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  181. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  182. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  183. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  184. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  185. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  186. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  187. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  188. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  189. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  190. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  191. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  192. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  193. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  194. package/src/plugins/builtin/econ/index.tsx +2 -1
  195. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  196. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  197. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  198. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  199. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  200. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  201. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  202. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  203. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  204. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  205. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  206. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  207. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  208. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  209. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  210. package/src/plugins/builtin/help/index.tsx +4 -0
  211. package/src/plugins/builtin/holders/index.tsx +1 -0
  212. package/src/plugins/builtin/insider/index.tsx +1 -0
  213. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  214. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  215. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  216. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  217. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  218. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  219. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  220. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  221. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  222. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  223. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  224. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  225. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  226. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  227. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  228. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  229. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  230. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  231. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  232. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  233. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  234. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  235. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  236. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  237. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  238. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  239. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  240. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  241. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  242. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  243. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  244. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  245. package/src/plugins/builtin/options/index.tsx +1 -0
  246. package/src/plugins/builtin/options/view.test.tsx +29 -1
  247. package/src/plugins/builtin/options/view.tsx +1 -22
  248. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  249. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  250. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  251. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  252. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  253. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  254. package/src/plugins/builtin/plugin-module.ts +9 -1
  255. package/src/plugins/builtin/research/index.tsx +4 -0
  256. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  257. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  258. package/src/plugins/builtin/research-search/data.ts +109 -0
  259. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  260. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  261. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  262. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  263. package/src/plugins/builtin/research-search/index.tsx +85 -0
  264. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  265. package/src/plugins/builtin/research-search/model.ts +291 -0
  266. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  267. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  268. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  269. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  270. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  271. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  272. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  273. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  274. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  275. package/src/plugins/builtin/sec/index.tsx +1 -0
  276. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  277. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  278. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  279. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  280. package/src/plugins/builtin/ticker-detail/index.tsx +0 -14
  281. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  282. package/src/plugins/builtin/volatility/index.tsx +1 -1
  283. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  284. package/src/plugins/builtin/world-venue-map/map.tsx +213 -69
  285. package/src/plugins/builtin/world-venue-map/model.test.ts +32 -0
  286. package/src/plugins/builtin/world-venue-map/model.ts +134 -13
  287. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  288. package/src/plugins/bundle.test.ts +109 -0
  289. package/src/plugins/bundle.ts +139 -0
  290. package/src/plugins/catalog-browser.test.ts +1 -0
  291. package/src/plugins/catalog-browser.ts +10 -0
  292. package/src/plugins/catalog-manifest.test.ts +43 -0
  293. package/src/plugins/catalog-manifest.ts +92 -0
  294. package/src/plugins/catalog-ui.ts +20 -4
  295. package/src/plugins/catalog.ts +9 -2
  296. package/src/plugins/current-target.ts +28 -0
  297. package/src/plugins/host-contract.ts +37 -0
  298. package/src/plugins/host-link.ts +158 -0
  299. package/src/plugins/host-modules.ts +61 -0
  300. package/src/plugins/loader.ts +41 -22
  301. package/src/plugins/ownership.ts +1 -0
  302. package/src/plugins/prediction-markets/index.tsx +1 -0
  303. package/src/plugins/registry/context.ts +3 -0
  304. package/src/plugins/registry/contributions.ts +27 -0
  305. package/src/plugins/registry/index.test.ts +25 -0
  306. package/src/plugins/registry/index.ts +8 -0
  307. package/src/plugins/registry/pane-settings.ts +6 -13
  308. package/src/plugins/seed.test.ts +83 -0
  309. package/src/plugins/seed.ts +80 -0
  310. package/src/public/broker.ts +31 -0
  311. package/src/public/market-data.ts +9 -0
  312. package/src/public/pane-hooks.ts +57 -0
  313. package/src/public/public-api.test.ts +285 -0
  314. package/src/public/react.ts +62 -0
  315. package/src/public/test-support.ts +23 -0
  316. package/src/public/time-series.ts +8 -0
  317. package/src/public/utils.ts +77 -0
  318. package/src/remote/layout-helpers.ts +6 -8
  319. package/src/remote/resources.ts +2 -0
  320. package/src/remote/schema.ts +1 -0
  321. package/src/remote/semantic-tree.test.tsx +105 -0
  322. package/src/remote/semantic-tree.tsx +53 -35
  323. package/src/renderers/browser/main.tsx +6 -0
  324. package/src/renderers/cloudflare/worker.test.ts +19 -0
  325. package/src/renderers/cloudflare/worker.ts +1 -1
  326. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  327. package/src/renderers/electrobun/bun/index.ts +5 -2
  328. package/src/renderers/electrobun/shared/protocol.ts +21 -0
  329. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  330. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  331. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  332. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  333. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  334. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  335. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  336. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  337. package/src/renderers/electrobun/view/main.tsx +27 -2
  338. package/src/renderers/electrobun/view/styles.css +5 -0
  339. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  340. package/src/renderers/opentui/chart-surface.tsx +5 -1
  341. package/src/renderers/opentui/host.tsx +16 -2
  342. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  343. package/src/renderers/opentui/interaction-performance.ts +188 -0
  344. package/src/renderers/opentui/start.tsx +26 -1
  345. package/src/renderers/opentui/test-utils.tsx +13 -0
  346. package/src/renderers/opentui/toast-host.tsx +26 -2
  347. package/src/renderers/opentui/ui-host.tsx +1 -0
  348. package/src/sources/provider-router/search.test.ts +191 -0
  349. package/src/sources/provider-router/search.ts +92 -39
  350. package/src/state/app/context/index.tsx +2 -0
  351. package/src/state/app/context/selector.test.tsx +33 -1
  352. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  353. package/src/state/hooks/quote-streaming.ts +106 -30
  354. package/src/state/pane-table-export-registry.ts +19 -0
  355. package/src/theme/color-utils.ts +1 -1
  356. package/src/theme/colors.ts +50 -0
  357. package/src/theme/themes.test.ts +15 -2
  358. package/src/theme/themes.ts +19 -1
  359. package/src/tickers/search/index.test.ts +26 -0
  360. package/src/tickers/search/index.ts +42 -17
  361. package/src/tickers/search/ranking.ts +14 -0
  362. package/src/time-series/chart-data.test.ts +216 -0
  363. package/src/time-series/resolution.test.ts +25 -26
  364. package/src/time-series/resolution.ts +41 -10
  365. package/src/time-series/resolve.ts +7 -0
  366. package/src/time-series/types.ts +3 -1
  367. package/src/time-series/use-chart-resolution.ts +5 -0
  368. package/src/types/config.ts +123 -3
  369. package/src/types/data-provider.ts +11 -0
  370. package/src/types/headless.ts +161 -0
  371. package/src/types/plugin.ts +102 -0
  372. package/src/ui/host.tsx +12 -1
  373. package/src/ui/index.tsx +1 -0
  374. package/src/ui/toast.tsx +8 -4
  375. package/src/utils/format.ts +35 -0
  376. package/src/utils/fuzzy-search.test.ts +94 -0
  377. package/src/utils/fuzzy-search.ts +81 -22
  378. package/src/utils/shortcut-labels.ts +14 -0
  379. package/src/components/chart/core/axis-format.ts +0 -157
  380. package/src/components/chart/core/index.test.ts +0 -1075
  381. package/src/components/chart/core/projection.ts +0 -357
  382. package/src/components/chart/core/render-utils.ts +0 -4
  383. package/src/components/chart/core/renderer.ts +0 -244
  384. package/src/components/chart/core/scene.ts +0 -180
  385. package/src/components/chart/core/time-axis.ts +0 -330
  386. package/src/components/chart/cursor-motion.ts +0 -47
  387. package/src/components/chart/indicators/types.ts +0 -26
  388. package/src/components/chart/market-session.ts +0 -176
  389. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  390. package/src/components/chart/multi-line/renderer.ts +0 -161
  391. package/src/components/chart/multi-line/scene.ts +0 -188
  392. package/src/components/chart/native/raster/layout.ts +0 -129
  393. package/src/components/chart/native/raster/price-chart.ts +0 -244
  394. package/src/components/chart/rendering/chart-draw.ts +0 -270
  395. package/src/components/chart/rendering/geometry.ts +0 -95
  396. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  397. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  398. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  399. package/src/components/chart/static/chart/surface.tsx +0 -358
  400. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  401. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  402. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  403. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  404. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  405. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  406. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  407. package/src/plugins/builtin/substack/api/store.ts +0 -186
  408. package/src/plugins/builtin/substack/api/types.ts +0 -42
  409. package/src/plugins/builtin/substack/api.test.ts +0 -298
  410. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  411. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  412. package/src/plugins/builtin/substack/content.ts +0 -478
  413. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  414. package/src/plugins/builtin/substack/index.tsx +0 -49
  415. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  416. package/src/plugins/builtin/substack/model.test.ts +0 -155
  417. package/src/plugins/builtin/substack/normalize.ts +0 -343
  418. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  419. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  420. package/src/plugins/builtin/substack/pane.tsx +0 -547
  421. package/src/plugins/builtin/substack/read-state.ts +0 -30
  422. package/src/plugins/builtin/substack/table.ts +0 -111
  423. package/src/plugins/builtin/substack/types.ts +0 -71
  424. package/src/plugins/builtin/substack/utils.ts +0 -131
  425. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  426. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  427. package/src/plugins/ibkr/account-cache.ts +0 -24
  428. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  429. package/src/plugins/ibkr/config.test.ts +0 -79
  430. package/src/plugins/ibkr/config.ts +0 -250
  431. package/src/plugins/ibkr/dialogs.tsx +0 -40
  432. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  433. package/src/plugins/ibkr/flex/index.ts +0 -367
  434. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  435. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  436. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  437. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  438. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  439. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  440. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  441. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  442. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  443. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  444. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  445. package/src/plugins/ibkr/gateway/history.ts +0 -120
  446. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  447. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  448. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  449. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  450. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  451. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  452. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  453. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  454. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  455. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  456. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  457. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  458. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  459. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  460. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  461. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  462. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  463. package/src/plugins/ibkr/gateway/types.ts +0 -27
  464. package/src/plugins/ibkr/index.test.tsx +0 -162
  465. package/src/plugins/ibkr/index.tsx +0 -159
  466. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  467. package/src/plugins/ibkr/instance-selection.ts +0 -36
  468. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  469. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  470. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  471. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  472. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  473. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  474. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  475. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  476. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  477. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  478. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  479. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  480. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  481. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  482. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  483. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  484. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  485. package/src/plugins/ibkr/trade/utils.ts +0 -144
  486. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  487. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  488. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  489. package/src/plugins/ibkr/trading/state.ts +0 -335
package/src/i18n/zh-cn.ts CHANGED
@@ -573,6 +573,7 @@ export const zhCN: Record<string, string> = {
573
573
  "Open the help window": "打开帮助窗口",
574
574
  "Set up a portfolio": "设置投资组合",
575
575
  "How would you like to get started?": "你想如何开始?",
576
+ "{shortcut} to get started.": "按 {shortcut} 开始。",
576
577
  "Edits later from the command bar.": "之后可在命令栏中修改。",
577
578
  "Create Manual Portfolio": "创建手动组合",
578
579
  "Add tickers and positions by hand": "手动添加股票与持仓",
@@ -775,7 +776,7 @@ export const zhCN: Record<string, string> = {
775
776
  "Verify your email to send messages.": "验证邮箱后才能发送消息。",
776
777
  "Verify your email to use Cloud tweets.": "验证邮箱后才能使用 Cloud tweets。",
777
778
  "Resend Verification Email": "重新发送验证邮件",
778
- "Ctrl+P, then Resend Verification Email": "按 Ctrl+P,然后选择“重新发送验证邮件”",
779
+ "{shortcut}, then Resend Verification Email": "按 {shortcut},然后选择“重新发送验证邮件”",
779
780
  "editing": "编辑中",
780
781
  "replying to": "回复",
781
782
  "Reply": "回复",
@@ -1013,7 +1014,7 @@ export const zhCN: Record<string, string> = {
1013
1014
  "No watchlist tickers": "自选列表暂无股票",
1014
1015
  "No tickers in {name}.": "{name} 中暂无股票。",
1015
1016
  "this collection": "此集合",
1016
- "Press Ctrl+P to add one.": "按 Ctrl+P 添加。",
1017
+ "Press {shortcut} to add one.": "按 {shortcut} 添加。",
1017
1018
  "PRE-MKT": "盘前",
1018
1019
  "OPEN": "盘中",
1019
1020
  "AFTER-HRS": "盘后",
@@ -1079,6 +1080,9 @@ export const zhCN: Record<string, string> = {
1079
1080
  "Approve from your phone, no typing": "在手机上确认,无需输入",
1080
1081
  "Scan with the Gloom app": "用 Gloom 应用扫码",
1081
1082
  "Open the Gloom app on your phone and approve this workspace.": "在手机上打开 Gloom 应用并确认此工作区。",
1083
+ "Approve the code from a phone that is already signed in.": "在已登录的手机上确认此代码。",
1084
+ "Layouts, watchlists and portfolios sync to your account.": "布局、自选列表和投资组合会同步到你的账户。",
1085
+ "Use email instead": "改用邮箱",
1082
1086
  "Scan with the Gloom app to sign in": "用 Gloom 应用扫码登录",
1083
1087
  "Waiting for approval...": "等待确认...",
1084
1088
  "Approved as {email}": "已以 {email} 登录",
package/src/i18n/zh-tw.ts CHANGED
@@ -573,6 +573,7 @@ export const zhTW: Record<string, string> = {
573
573
  "Open the help window": "開啟說明視窗",
574
574
  "Set up a portfolio": "設定投資組合",
575
575
  "How would you like to get started?": "你想如何開始?",
576
+ "{shortcut} to get started.": "按 {shortcut} 開始。",
576
577
  "Edits later from the command bar.": "之後可在命令列中修改。",
577
578
  "Create Manual Portfolio": "建立手動投資組合",
578
579
  "Add tickers and positions by hand": "手動新增股票與持倉",
@@ -775,7 +776,7 @@ export const zhTW: Record<string, string> = {
775
776
  "Verify your email to send messages.": "驗證電子郵件後才能傳送訊息。",
776
777
  "Verify your email to use Cloud tweets.": "驗證電子郵件後才能使用 Cloud tweets。",
777
778
  "Resend Verification Email": "重新發送驗證郵件",
778
- "Ctrl+P, then Resend Verification Email": "按 Ctrl+P,然後選擇「重新發送驗證郵件」",
779
+ "{shortcut}, then Resend Verification Email": "按 {shortcut},然後選擇「重新發送驗證郵件」",
779
780
  "editing": "編輯中",
780
781
  "replying to": "回覆",
781
782
  "Reply": "回覆",
@@ -1013,7 +1014,7 @@ export const zhTW: Record<string, string> = {
1013
1014
  "No watchlist tickers": "自選清單暫無股票",
1014
1015
  "No tickers in {name}.": "{name} 中暫無股票。",
1015
1016
  "this collection": "此清單",
1016
- "Press Ctrl+P to add one.": "按 Ctrl+P 新增。",
1017
+ "Press {shortcut} to add one.": "按 {shortcut} 新增。",
1017
1018
  "PRE-MKT": "盤前",
1018
1019
  "OPEN": "盤中",
1019
1020
  "AFTER-HRS": "盤後",
@@ -1079,6 +1080,9 @@ export const zhTW: Record<string, string> = {
1079
1080
  "Approve from your phone, no typing": "在手機上確認,無需輸入",
1080
1081
  "Scan with the Gloom app": "用 Gloom 應用程式掃碼",
1081
1082
  "Open the Gloom app on your phone and approve this workspace.": "在手機上開啟 Gloom 應用程式並確認此工作區。",
1083
+ "Approve the code from a phone that is already signed in.": "在已登入的手機上確認這組代碼。",
1084
+ "Layouts, watchlists and portfolios sync to your account.": "版面、自選清單與投資組合會同步到你的帳號。",
1085
+ "Use email instead": "改用電子郵件",
1082
1086
  "Scan with the Gloom app to sign in": "用 Gloom 應用程式掃碼登入",
1083
1087
  "Waiting for approval...": "等待確認...",
1084
1088
  "Approved as {email}": "已以 {email} 登入",
@@ -22,6 +22,7 @@ const IMAGERY_RULES: ReadonlyArray<readonly [string, PaneImagery]> = [
22
22
  ["sectors", "heatmap"],
23
23
  ["treemap", "heatmap"],
24
24
  ["fear-greed", "gauge"],
25
+ ["valuation", "chart"],
25
26
  ["kelly", "gauge"],
26
27
  ["gauge", "gauge"],
27
28
  ["calendar", "calendar"],
@@ -157,6 +157,23 @@ describe("layout marketplace payloads", () => {
157
157
  expect(JSON.stringify(pane)).not.toContain("secret");
158
158
  });
159
159
 
160
+ test("drops cache-sized pane state instead of failing the share", () => {
161
+ const pane = publishableMarketplacePane({
162
+ instanceId: "news-top:main",
163
+ paneId: "prediction-markets",
164
+ binding: { kind: "none" },
165
+ }, {
166
+ sort: { columnId: "time", direction: "desc" },
167
+ "news-top:articles": Array.from({ length: 400 }, (_, index) => ({
168
+ id: `article-${index}`,
169
+ title: "x".repeat(200),
170
+ publishedAt: "2026-08-26T00:00:00.000Z",
171
+ })),
172
+ }, panes);
173
+
174
+ expect(pane.paneState.p1).toEqual({ sort: { columnId: "time", direction: "desc" } });
175
+ });
176
+
160
177
  test("shares portable pane setup and state while redacting private data", () => {
161
178
  const fixture = portableFixture();
162
179
  const payload = publishableMarketplaceLayout(fixture.layout, fixture.paneState, panes);
@@ -12,6 +12,10 @@ import type {
12
12
  } from "../types/plugin";
13
13
 
14
14
  const MAX_LAYOUT_BYTES = 128 * 1024;
15
+ // Pane runtime state doubles as a cache (news panes keep six figures of bytes of
16
+ // article backlog), so bulky fields drop out of the share rather than failing
17
+ // the whole publish.
18
+ const MAX_PANE_STATE_FIELD_BYTES = 16 * 1024;
15
19
  const MAX_INSTANCES = 40;
16
20
  const MAX_DOCK_DEPTH = 20;
17
21
  const MAX_ID_LENGTH = 160;
@@ -493,14 +497,45 @@ function publishableLayout(
493
497
  height,
494
498
  })),
495
499
  };
496
- const parsed = parseMarketplaceLayoutPayload({
500
+ let publishableState = Object.fromEntries(
501
+ Object.entries(projectedState).flatMap(([id, state]) => {
502
+ const trimmed = withoutCacheFields(state);
503
+ return Object.keys(trimmed).length > 0 ? [[id, trimmed] as const] : [];
504
+ }),
505
+ );
506
+ let parsed = parsePublishablePayload(projected, publishableState);
507
+ while (!parsed && Object.keys(publishableState).length > 0) {
508
+ publishableState = withoutLargestState(publishableState);
509
+ parsed = parsePublishablePayload(projected, publishableState);
510
+ }
511
+ if (!parsed) throw new Error("This layout cannot be published safely.");
512
+ return parsed;
513
+ }
514
+
515
+ function parsePublishablePayload(
516
+ layout: LayoutConfig,
517
+ paneState: Record<string, PaneRuntimeState>,
518
+ ): LayoutMarketplacePayload | null {
519
+ return parseMarketplaceLayoutPayload({
497
520
  schemaVersion: 2,
498
521
  sourceConfigVersion: CURRENT_CONFIG_VERSION,
499
- layout: projected,
500
- paneState: projectedState,
522
+ layout,
523
+ paneState,
501
524
  });
502
- if (!parsed) throw new Error("This layout cannot be published safely.");
503
- return parsed;
525
+ }
526
+
527
+ function withoutCacheFields(state: PaneRuntimeState): PaneRuntimeState {
528
+ return Object.fromEntries(
529
+ Object.entries(state).filter(([, value]) => encodedSize(value) <= MAX_PANE_STATE_FIELD_BYTES),
530
+ ) as PaneRuntimeState;
531
+ }
532
+
533
+ function withoutLargestState(
534
+ paneState: Record<string, PaneRuntimeState>,
535
+ ): Record<string, PaneRuntimeState> {
536
+ const largest = Object.entries(paneState)
537
+ .reduce((worst, entry) => (encodedSize(entry[1]) > encodedSize(worst[1]) ? entry : worst));
538
+ return Object.fromEntries(Object.entries(paneState).filter(([id]) => id !== largest[0]));
504
539
  }
505
540
 
506
541
  export function publishableMarketplaceLayout(
@@ -55,7 +55,8 @@ import {
55
55
  loadQuoteBatchEntries,
56
56
  loadQuoteEntry,
57
57
  QuoteSubscriptionManager,
58
- type QuoteSubscriptionPriority,
58
+ type QuoteSubscriptionHandle,
59
+ type QuoteSubscriptionRequest,
59
60
  } from "./quotes";
60
61
 
61
62
  export class MarketDataCoordinator {
@@ -361,7 +362,7 @@ export class MarketDataCoordinator {
361
362
  });
362
363
  }
363
364
 
364
- subscribeQuotes(targets: Array<{ instrument: InstrumentRef; priority?: QuoteSubscriptionPriority }>): () => void {
365
+ subscribeQuotes(targets: QuoteSubscriptionRequest[]): QuoteSubscriptionHandle {
365
366
  return this.quoteSubscriptionManager.subscribe(targets);
366
367
  }
367
368
 
@@ -19,6 +19,21 @@ import {
19
19
 
20
20
  export type QuoteSubscriptionPriority = Pick<QuoteSubscriptionTarget, "route" | "surface" | "visible" | "selected" | "weight">;
21
21
 
22
+ export interface QuoteSubscriptionRequest {
23
+ instrument: InstrumentRef;
24
+ priority?: QuoteSubscriptionPriority;
25
+ }
26
+
27
+ /**
28
+ * Remains callable for compatibility with the original disposer API. Updating
29
+ * the handle lets long-lived consumers change priorities without unregistering
30
+ * their whole target set first.
31
+ */
32
+ export interface QuoteSubscriptionHandle {
33
+ (): void;
34
+ update(targets: QuoteSubscriptionRequest[]): void;
35
+ }
36
+
22
37
  export interface QuoteSubscriptionEntry {
23
38
  target: QuoteSubscriptionTarget;
24
39
  targets: Map<number, QuoteSubscriptionTarget>;
@@ -30,7 +45,8 @@ interface PendingStreamQuote {
30
45
  quote: Quote;
31
46
  }
32
47
 
33
- const QUOTE_SUBSCRIPTION_REMOVE_GRACE_MS = 250;
48
+ export const QUOTE_SUBSCRIPTION_REMOVE_GRACE_MS = 250;
49
+ export const QUOTE_SUBSCRIPTION_PRIORITY_UPDATE_DELAY_MS = 100;
34
50
 
35
51
  const STREAM_QUOTE_FIELDS: Array<keyof Quote> = [
36
52
  "symbol",
@@ -198,101 +214,192 @@ export class QuoteSubscriptionManager {
198
214
  private readonly pendingStreamQuotes = new Map<string, PendingStreamQuote>();
199
215
  private lastStreamQuoteBatchAppliedAt: number | null = null;
200
216
  private pendingStreamQuoteTimer: ReturnType<typeof setTimeout> | null = null;
217
+ private pendingPriorityUpdateTimer: ReturnType<typeof setTimeout> | null = null;
201
218
  private nextQuoteSubscriptionId = 1;
202
219
  private quoteSubscriptionDispose: (() => void) | null = null;
203
220
  private quoteSubscriptionSignature = "";
221
+ private quoteSubscriptionRoutingSignature = "";
204
222
 
205
223
  constructor(
206
224
  private readonly dataProvider: DataProvider,
207
225
  private readonly applyQuote: (instrument: InstrumentRef, quote: Quote) => void,
208
226
  ) {}
209
227
 
210
- subscribe(targets: Array<{ instrument: InstrumentRef; priority?: QuoteSubscriptionPriority }>): () => void {
211
- if (!this.dataProvider.subscribeQuotes || targets.length === 0) {
212
- return () => {};
228
+ subscribe(targets: QuoteSubscriptionRequest[]): QuoteSubscriptionHandle {
229
+ if (!this.dataProvider.subscribeQuotes) {
230
+ return this.createNoopSubscription();
213
231
  }
214
232
 
215
233
  const subscriptionId = this.nextQuoteSubscriptionId++;
216
- const subscribedKeys = new Set<string>();
234
+ let subscribedKeys = new Set<string>();
235
+ let disposed = false;
236
+ const update = (nextTargets: QuoteSubscriptionRequest[]) => {
237
+ if (disposed) return;
238
+ subscribedKeys = this.updateSubscriptionTargets(subscriptionId, subscribedKeys, nextTargets);
239
+ };
240
+ const dispose = (() => {
241
+ if (disposed) return;
242
+ disposed = true;
243
+ subscribedKeys = this.updateSubscriptionTargets(subscriptionId, subscribedKeys, []);
244
+ }) as QuoteSubscriptionHandle;
245
+ dispose.update = update;
246
+ update(targets);
247
+ return dispose;
248
+ }
249
+
250
+ private createNoopSubscription(): QuoteSubscriptionHandle {
251
+ const dispose = (() => {}) as QuoteSubscriptionHandle;
252
+ dispose.update = () => {};
253
+ return dispose;
254
+ }
255
+
256
+ private updateSubscriptionTargets(
257
+ subscriptionId: number,
258
+ subscribedKeys: Set<string>,
259
+ targets: QuoteSubscriptionRequest[],
260
+ ): Set<string> {
261
+ const nextTargets = new Map<string, QuoteSubscriptionTarget>();
217
262
  for (const { instrument, priority } of targets) {
218
263
  const key = buildQuoteKey(instrument);
219
- subscribedKeys.add(key);
220
264
  const target = quoteTargetFromInstrument(instrument, priority);
221
- const existing = this.quoteSubscriptions.get(key);
222
- if (existing) {
223
- existing.targets.set(subscriptionId, target);
224
- existing.target = mergeQuoteSubscriptionTargets(existing.targets.values()) ?? target;
225
- if (existing.removeTimer) {
226
- clearTimeout(existing.removeTimer);
227
- existing.removeTimer = null;
265
+ const duplicate = nextTargets.get(key);
266
+ nextTargets.set(
267
+ key,
268
+ duplicate
269
+ ? mergeQuoteSubscriptionTargets([duplicate, target]) ?? target
270
+ : target,
271
+ );
272
+ }
273
+
274
+ const nextKeys = new Set(nextTargets.keys());
275
+ const affectedKeys = new Set([...subscribedKeys, ...nextKeys]);
276
+ let shouldFlush = false;
277
+ for (const key of affectedKeys) {
278
+ const nextTarget = nextTargets.get(key);
279
+ let entry = this.quoteSubscriptions.get(key);
280
+ if (nextTarget) {
281
+ if (!entry) {
282
+ entry = {
283
+ target: nextTarget,
284
+ targets: new Map(),
285
+ removeTimer: null,
286
+ };
287
+ this.quoteSubscriptions.set(key, entry);
228
288
  }
289
+ entry.targets.set(subscriptionId, nextTarget);
290
+ if (entry.removeTimer) {
291
+ clearTimeout(entry.removeTimer);
292
+ entry.removeTimer = null;
293
+ }
294
+ } else if (entry) {
295
+ entry.targets.delete(subscriptionId);
296
+ }
297
+ if (!entry) continue;
298
+
299
+ const mergedTarget = mergeQuoteSubscriptionTargets(entry.targets.values());
300
+ if (mergedTarget) {
301
+ entry.target = mergedTarget;
302
+ shouldFlush = true;
229
303
  continue;
230
304
  }
231
- this.quoteSubscriptions.set(key, {
232
- target,
233
- targets: new Map([[subscriptionId, target]]),
234
- removeTimer: null,
235
- });
305
+ this.scheduleSubscriptionRemoval(key, entry);
236
306
  }
237
- this.flush();
238
-
239
- return () => {
240
- for (const key of subscribedKeys) {
241
- const existing = this.quoteSubscriptions.get(key);
242
- if (!existing) continue;
243
- existing.targets.delete(subscriptionId);
244
- const mergedTarget = mergeQuoteSubscriptionTargets(existing.targets.values());
245
- if (mergedTarget) {
246
- existing.target = mergedTarget;
247
- this.flush();
248
- continue;
249
- }
250
- if (existing.removeTimer) continue;
251
- this.clearPendingStreamQuote(key);
252
- existing.removeTimer = setTimeout(() => {
253
- const current = this.quoteSubscriptions.get(key);
254
- if (!current || current.targets.size > 0) return;
255
- this.quoteSubscriptions.delete(key);
256
- this.clearPendingStreamQuote(key);
257
- this.flush();
258
- }, QUOTE_SUBSCRIPTION_REMOVE_GRACE_MS);
259
- }
260
- };
307
+ if (shouldFlush) {
308
+ this.flush();
309
+ } else if (![...this.quoteSubscriptions.values()].some((entry) => entry.targets.size > 0)) {
310
+ this.cancelPendingPriorityUpdate();
311
+ }
312
+ return nextKeys;
313
+ }
314
+
315
+ private scheduleSubscriptionRemoval(key: string, entry: QuoteSubscriptionEntry): void {
316
+ if (entry.removeTimer) return;
317
+ this.clearPendingStreamQuote(key);
318
+ entry.removeTimer = setTimeout(() => {
319
+ const current = this.quoteSubscriptions.get(key);
320
+ if (!current || current.targets.size > 0) return;
321
+ this.quoteSubscriptions.delete(key);
322
+ this.clearPendingStreamQuote(key);
323
+ this.flush();
324
+ }, QUOTE_SUBSCRIPTION_REMOVE_GRACE_MS);
261
325
  }
262
326
 
263
- private flush(): void {
327
+ private flush({
328
+ coalescePriorityChanges = true,
329
+ requireStableRouting = false,
330
+ }: {
331
+ coalescePriorityChanges?: boolean;
332
+ requireStableRouting?: boolean;
333
+ } = {}): void {
264
334
  if (!this.dataProvider.subscribeQuotes) return;
265
335
 
266
336
  const activeEntries = [...this.quoteSubscriptions.entries()]
267
337
  .filter(([, entry]) => entry.targets.size > 0)
268
338
  .sort(([left], [right]) => left.localeCompare(right));
269
- const nextSignature = activeEntries.map(([key, entry]) => [
339
+ const nextRoutingSignature = JSON.stringify(activeEntries.map(([key, entry]) => [
270
340
  key,
271
- entry.target.route ?? "",
341
+ entry.target.route ?? "auto",
342
+ ]));
343
+ const nextSignature = JSON.stringify(activeEntries.map(([key, entry]) => [
344
+ key,
345
+ entry.target.route ?? "auto",
272
346
  entry.target.surface ?? "",
273
- entry.target.visible ? "visible" : "",
274
- entry.target.selected ? "selected" : "",
275
- Number.isFinite(entry.target.weight) ? entry.target.weight : "",
276
- ].join(":")).join("|");
277
- if (nextSignature === this.quoteSubscriptionSignature) return;
278
-
279
- this.quoteSubscriptionDispose?.();
280
- this.quoteSubscriptionDispose = null;
281
- this.quoteSubscriptionSignature = nextSignature;
347
+ entry.target.visible === true,
348
+ entry.target.selected === true,
349
+ Number.isFinite(entry.target.weight) ? entry.target.weight : null,
350
+ ]));
351
+ if (nextSignature === this.quoteSubscriptionSignature) {
352
+ this.cancelPendingPriorityUpdate();
353
+ return;
354
+ }
355
+ if (requireStableRouting && nextRoutingSignature !== this.quoteSubscriptionRoutingSignature) {
356
+ return;
357
+ }
358
+
359
+ const priorityOnlyChange = this.quoteSubscriptionSignature.length > 0
360
+ && nextRoutingSignature === this.quoteSubscriptionRoutingSignature;
361
+ if (coalescePriorityChanges && priorityOnlyChange) {
362
+ this.schedulePriorityUpdate();
363
+ return;
364
+ }
365
+ this.cancelPendingPriorityUpdate();
282
366
 
283
367
  const targets = activeEntries.map(([, entry]) => entry.target);
284
- if (targets.length === 0) return;
285
-
286
- this.quoteSubscriptionDispose = this.dataProvider.subscribeQuotes(targets, (target, quote) => {
287
- const instrument: InstrumentRef = {
288
- symbol: target.symbol,
289
- exchange: target.exchange ?? "",
290
- brokerId: target.context?.brokerId,
291
- brokerInstanceId: target.context?.brokerInstanceId,
292
- instrument: target.context?.instrument ?? null,
293
- };
294
- this.enqueueStreamQuote(instrument, quote);
295
- });
368
+ const nextDispose = targets.length > 0
369
+ ? this.dataProvider.subscribeQuotes(targets, (target, quote) => {
370
+ const instrument: InstrumentRef = {
371
+ symbol: target.symbol,
372
+ exchange: target.exchange ?? "",
373
+ brokerId: target.context?.brokerId,
374
+ brokerInstanceId: target.context?.brokerInstanceId,
375
+ instrument: target.context?.instrument ?? null,
376
+ };
377
+ this.enqueueStreamQuote(instrument, quote);
378
+ })
379
+ : null;
380
+ // Register first so the cloud socket and desktop registries can merge the
381
+ // overlapping listeners without observing a zero-listener disconnect.
382
+ const previousDispose = this.quoteSubscriptionDispose;
383
+ this.quoteSubscriptionDispose = nextDispose;
384
+ this.quoteSubscriptionSignature = nextSignature;
385
+ this.quoteSubscriptionRoutingSignature = nextRoutingSignature;
386
+ previousDispose?.();
387
+ }
388
+
389
+ private schedulePriorityUpdate(): void {
390
+ // Do not reset this timer. Continuous navigation must still publish the
391
+ // latest priorities instead of postponing them indefinitely.
392
+ if (this.pendingPriorityUpdateTimer !== null) return;
393
+ this.pendingPriorityUpdateTimer = setTimeout(() => {
394
+ this.pendingPriorityUpdateTimer = null;
395
+ this.flush({ coalescePriorityChanges: false, requireStableRouting: true });
396
+ }, QUOTE_SUBSCRIPTION_PRIORITY_UPDATE_DELAY_MS);
397
+ }
398
+
399
+ private cancelPendingPriorityUpdate(): void {
400
+ if (this.pendingPriorityUpdateTimer === null) return;
401
+ clearTimeout(this.pendingPriorityUpdateTimer);
402
+ this.pendingPriorityUpdateTimer = null;
296
403
  }
297
404
 
298
405
  private enqueueStreamQuote(instrument: InstrumentRef, quote: Quote): void {