gloomberb 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (531) hide show
  1. package/README.md +7 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +108 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +134 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +220 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/capabilities/chart-series.ts +3 -2
  20. package/src/cli/commands/market.ts +2 -2
  21. package/src/cli/commands/plugins.ts +39 -11
  22. package/src/cli/desktop-pane-shot.ts +285 -16
  23. package/src/cli/index.ts +1 -1
  24. package/src/cli/pane-functions/capabilities.ts +61 -31
  25. package/src/cli/pane-functions/catalog.ts +16 -4
  26. package/src/cli/pane-functions/discovery.ts +1 -0
  27. package/src/cli/pane-functions/headless.test.ts +247 -0
  28. package/src/cli/pane-functions/headless.ts +423 -0
  29. package/src/cli/pane-functions/index.test.ts +9 -0
  30. package/src/cli/pane-functions/index.ts +6 -2
  31. package/src/cli/pane-functions/options.ts +27 -2
  32. package/src/cli/pane-functions/report.ts +4 -0
  33. package/src/cli/pane-functions/resolver.ts +26 -8
  34. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  35. package/src/cli/pane-functions/screenshot.ts +236 -20
  36. package/src/cli/registry.ts +0 -22
  37. package/src/cli/restore-plugins.ts +34 -0
  38. package/src/cli/result.ts +7 -10
  39. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  40. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  41. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  42. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  43. package/src/components/chart/composite/composite-chart.tsx +301 -204
  44. package/src/components/chart/composite/format.ts +8 -2
  45. package/src/components/chart/composite/interactions.test.ts +68 -102
  46. package/src/components/chart/composite/interactions.ts +214 -264
  47. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  48. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  49. package/src/components/chart/composite/scene.ts +57 -3
  50. package/src/components/chart/composite/text-renderer.ts +3 -2
  51. package/src/components/chart/composite/time-scale.ts +138 -57
  52. package/src/components/chart/composite/types.ts +25 -1
  53. package/src/components/chart/core/data.ts +9 -13
  54. package/src/components/chart/core/date-window.ts +7 -1
  55. package/src/components/chart/core/pointer.ts +3 -0
  56. package/src/components/chart/core/types.ts +0 -45
  57. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  58. package/src/components/chart/native/kitty/index.test.ts +0 -188
  59. package/src/components/chart/native/raster/crosshair.ts +6 -5
  60. package/src/components/chart/native/raster/primitives.ts +0 -23
  61. package/src/components/chart/native/renderer-selection.ts +81 -31
  62. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  63. package/src/components/chart/static/chart-surface.tsx +150 -0
  64. package/src/components/chart/static/index.ts +8 -0
  65. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  66. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  67. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  68. package/src/components/chart/static/series.ts +49 -0
  69. package/src/components/command-bar/action-runtime.ts +1 -3
  70. package/src/components/command-bar/assist/model.test.ts +17 -2
  71. package/src/components/command-bar/assist/model.ts +40 -9
  72. package/src/components/command-bar/commands/direct/index.ts +1 -1
  73. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  74. package/src/components/command-bar/commands/registry.ts +0 -11
  75. package/src/components/command-bar/helpers.test.ts +2 -2
  76. package/src/components/command-bar/helpers.ts +2 -6
  77. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  78. package/src/components/command-bar/list/badge.test.ts +54 -0
  79. package/src/components/command-bar/list/badge.ts +62 -0
  80. package/src/components/command-bar/list/model.test.ts +103 -0
  81. package/src/components/command-bar/list/model.ts +61 -4
  82. package/src/components/command-bar/list/view.test.tsx +113 -0
  83. package/src/components/command-bar/list/view.tsx +147 -131
  84. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  85. package/src/components/command-bar/panel/body.tsx +51 -65
  86. package/src/components/command-bar/panel/index.tsx +48 -13
  87. package/src/components/command-bar/panel/layout.test.ts +139 -0
  88. package/src/components/command-bar/panel/layout.ts +65 -28
  89. package/src/components/command-bar/panel/native-surface.ts +24 -0
  90. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  91. package/src/components/command-bar/panel/runtime.ts +6 -3
  92. package/src/components/command-bar/panel/state.ts +41 -7
  93. package/src/components/command-bar/panel/types.ts +7 -1
  94. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  95. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  96. package/src/components/command-bar/routes/root/results.ts +30 -28
  97. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  98. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  99. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  100. package/src/components/command-bar/routes/root/selection.ts +1 -12
  101. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  102. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  103. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  104. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  105. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  106. package/src/components/command-bar/routing/actions.ts +1 -81
  107. package/src/components/command-bar/routing/launch-request.ts +1 -1
  108. package/src/components/command-bar/routing/list-state.ts +6 -21
  109. package/src/components/command-bar/selection-runtime.ts +1 -1
  110. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  111. package/src/components/command-bar/surface/environment.ts +8 -1
  112. package/src/components/command-bar/surface/index.test.tsx +84 -68
  113. package/src/components/command-bar/surface/index.tsx +33 -36
  114. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  115. package/src/components/command-bar/theme-picker.tsx +43 -15
  116. package/src/components/command-bar/view-model.test.ts +53 -8
  117. package/src/components/command-bar/view-model.ts +43 -14
  118. package/src/components/command-bar/workflow/tickers.ts +1 -0
  119. package/src/components/command-bar/workflow/types.ts +1 -1
  120. package/src/components/data-table/export.test.ts +21 -0
  121. package/src/components/data-table/export.ts +21 -0
  122. package/src/components/data-table/view.test.tsx +45 -6
  123. package/src/components/data-table/view.tsx +55 -9
  124. package/src/components/index.ts +10 -2
  125. package/src/components/input-search-bar.test.tsx +36 -2
  126. package/src/components/input-search-bar.tsx +18 -1
  127. package/src/components/layout/header.test.tsx +57 -10
  128. package/src/components/layout/header.tsx +292 -149
  129. package/src/components/layout/market-summary.test.ts +81 -0
  130. package/src/components/layout/market-summary.ts +109 -0
  131. package/src/components/layout/shell/chrome.ts +96 -1
  132. package/src/components/layout/shell/index.tsx +11 -3
  133. package/src/components/layout/shell/menu.ts +9 -0
  134. package/src/components/layout/shell/pane/actions.ts +29 -0
  135. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  136. package/src/components/layout/shell/shortcuts.ts +3 -0
  137. package/src/components/layout/status-bar.test.tsx +9 -8
  138. package/src/components/layout/status-bar.tsx +85 -54
  139. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  140. package/src/components/price-sparkline/index.ts +61 -34
  141. package/src/components/price-sparkline/view.tsx +1 -1
  142. package/src/components/sign-in-gate.test.tsx +128 -0
  143. package/src/components/sign-in-gate.tsx +149 -0
  144. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  145. package/src/components/ticker/badge/list.tsx +17 -3
  146. package/src/components/ticker/list-table-view.tsx +6 -3
  147. package/src/components/ui/data-table/index.tsx +22 -4
  148. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  149. package/src/components/ui/frame.tsx +28 -0
  150. package/src/components/ui/index.ts +3 -0
  151. package/src/components/ui/native-select.tsx +20 -10
  152. package/src/components/ui/select-button.tsx +138 -0
  153. package/src/components/ui/status.tsx +2 -2
  154. package/src/components/ui/table-layout.ts +1 -1
  155. package/src/components/use-double-click-activation.ts +7 -3
  156. package/src/core/app-services.ts +3 -3
  157. package/src/data/config/chart-settings.ts +4 -4
  158. package/src/data/config/layout.ts +12 -6
  159. package/src/data/config/store/index.test.ts +97 -0
  160. package/src/data/config/store/migrations.ts +41 -0
  161. package/src/data/config/store/normalize.ts +5 -3
  162. package/src/i18n/es.ts +6 -2
  163. package/src/i18n/ja.ts +6 -2
  164. package/src/i18n/ko.ts +6 -2
  165. package/src/i18n/zh-cn.ts +6 -2
  166. package/src/i18n/zh-tw.ts +6 -2
  167. package/src/layout-marketplace/gallery-terminal.tsx +126 -96
  168. package/src/layout-marketplace/gallery.test.tsx +65 -1
  169. package/src/layout-marketplace/pane-imagery.ts +1 -0
  170. package/src/layout-marketplace/payload.test.ts +17 -0
  171. package/src/layout-marketplace/payload.ts +40 -5
  172. package/src/market-data/coordinator/index.ts +3 -2
  173. package/src/market-data/coordinator/quotes.ts +173 -66
  174. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  175. package/src/pane-settings.ts +1 -1
  176. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  177. package/src/plugins/builtin/analytics/view.tsx +1 -1
  178. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  179. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  180. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  181. package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
  182. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  183. package/src/plugins/builtin/chart-composer/settings.ts +2 -13
  184. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  185. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  186. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  188. package/src/plugins/builtin/composite-plugins.ts +16 -1
  189. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  190. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  191. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  192. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  193. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  194. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  195. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  196. package/src/plugins/builtin/earnings/index.tsx +1 -0
  197. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  198. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  199. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  200. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  201. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  202. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  203. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  204. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  205. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  206. package/src/plugins/builtin/econ/index.tsx +3 -1
  207. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  208. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  209. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  210. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  211. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  212. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  213. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  214. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  215. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  216. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  217. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  218. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  219. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  220. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  221. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  222. package/src/plugins/builtin/futures/index.tsx +1 -0
  223. package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
  224. package/src/plugins/builtin/help/index.tsx +4 -0
  225. package/src/plugins/builtin/holders/index.tsx +1 -0
  226. package/src/plugins/builtin/insider/index.tsx +1 -0
  227. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
  228. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  229. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  230. package/src/plugins/builtin/market-halts/index.tsx +1 -0
  231. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  232. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  233. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  234. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  235. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  236. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  237. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  238. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  239. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  240. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  241. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  242. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  243. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  244. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  245. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  246. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  247. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  248. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  249. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  250. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  251. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  252. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  253. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  254. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  255. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  256. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  257. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  258. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  259. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  260. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  261. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  262. package/src/plugins/builtin/options/analytics.test.ts +83 -0
  263. package/src/plugins/builtin/options/analytics.ts +110 -0
  264. package/src/plugins/builtin/options/index.tsx +18 -2
  265. package/src/plugins/builtin/options/table.ts +121 -51
  266. package/src/plugins/builtin/options/types.ts +22 -15
  267. package/src/plugins/builtin/options/view.test.tsx +53 -1
  268. package/src/plugins/builtin/options/view.tsx +92 -41
  269. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  270. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  271. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  272. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  273. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  274. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  275. package/src/plugins/builtin/plugin-module.test.ts +2 -4
  276. package/src/plugins/builtin/plugin-module.ts +9 -8
  277. package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
  278. package/src/plugins/builtin/research/index.tsx +4 -0
  279. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  280. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  281. package/src/plugins/builtin/research-search/data.ts +109 -0
  282. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  283. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  284. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  285. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  286. package/src/plugins/builtin/research-search/index.tsx +85 -0
  287. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  288. package/src/plugins/builtin/research-search/model.ts +291 -0
  289. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  290. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  291. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  292. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  293. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  294. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  295. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  296. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  297. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  298. package/src/plugins/builtin/scanner/index.ts +2 -0
  299. package/src/plugins/builtin/sec/index.tsx +1 -0
  300. package/src/plugins/builtin/sectors/index.tsx +1 -0
  301. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  302. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  303. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  304. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  305. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
  306. package/src/plugins/builtin/ticker-detail/index.tsx +2 -14
  307. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  308. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  309. package/src/plugins/builtin/volatility/index.tsx +1 -1
  310. package/src/plugins/builtin/world-indices/index.tsx +1 -0
  311. package/src/plugins/builtin/world-venue-map/index.ts +27 -0
  312. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  313. package/src/plugins/builtin/world-venue-map/map.tsx +481 -0
  314. package/src/plugins/builtin/world-venue-map/model.test.ts +85 -0
  315. package/src/plugins/builtin/world-venue-map/model.ts +268 -0
  316. package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
  317. package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
  318. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  319. package/src/plugins/bundle.test.ts +109 -0
  320. package/src/plugins/bundle.ts +139 -0
  321. package/src/plugins/catalog-backend.ts +6 -44
  322. package/src/plugins/catalog-browser.test.ts +1 -0
  323. package/src/plugins/catalog-browser.ts +12 -0
  324. package/src/plugins/catalog-manifest.test.ts +43 -0
  325. package/src/plugins/catalog-manifest.ts +92 -0
  326. package/src/plugins/catalog-ui.ts +20 -4
  327. package/src/plugins/catalog.ts +9 -2
  328. package/src/plugins/current-target.ts +28 -0
  329. package/src/plugins/host-contract.ts +37 -0
  330. package/src/plugins/host-link.ts +158 -0
  331. package/src/plugins/host-modules.ts +61 -0
  332. package/src/plugins/loader.ts +41 -22
  333. package/src/plugins/ownership.ts +1 -0
  334. package/src/plugins/prediction-markets/index.tsx +1 -0
  335. package/src/plugins/registry/context.ts +3 -0
  336. package/src/plugins/registry/contributions.ts +27 -0
  337. package/src/plugins/registry/index.test.ts +25 -0
  338. package/src/plugins/registry/index.ts +8 -0
  339. package/src/plugins/registry/pane-settings.test.ts +53 -0
  340. package/src/plugins/registry/pane-settings.ts +29 -3
  341. package/src/plugins/seed.test.ts +83 -0
  342. package/src/plugins/seed.ts +80 -0
  343. package/src/public/broker.ts +31 -0
  344. package/src/public/market-data.ts +9 -0
  345. package/src/public/pane-hooks.ts +57 -0
  346. package/src/public/public-api.test.ts +285 -0
  347. package/src/public/react.ts +62 -0
  348. package/src/public/test-support.ts +23 -0
  349. package/src/public/time-series.ts +8 -0
  350. package/src/public/utils.ts +77 -0
  351. package/src/remote/layout-helpers.ts +6 -8
  352. package/src/remote/resources.ts +2 -0
  353. package/src/remote/schema.ts +1 -0
  354. package/src/remote/semantic-tree.test.tsx +105 -0
  355. package/src/remote/semantic-tree.tsx +53 -35
  356. package/src/renderers/browser/main.tsx +6 -0
  357. package/src/renderers/browser/ui-host.tsx +11 -0
  358. package/src/renderers/cloudflare/worker.test.ts +19 -0
  359. package/src/renderers/cloudflare/worker.ts +1 -1
  360. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
  361. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  362. package/src/renderers/electrobun/bun/index.ts +6 -2
  363. package/src/renderers/electrobun/shared/protocol.ts +25 -0
  364. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  365. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  366. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  367. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  368. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  369. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  370. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  371. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  372. package/src/renderers/electrobun/view/main.tsx +27 -2
  373. package/src/renderers/electrobun/view/styles.css +5 -0
  374. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  375. package/src/renderers/electrobun/view/ui-host.tsx +3 -0
  376. package/src/renderers/opentui/chart-surface.tsx +5 -1
  377. package/src/renderers/opentui/host.tsx +20 -2
  378. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  379. package/src/renderers/opentui/interaction-performance.ts +188 -0
  380. package/src/renderers/opentui/start.tsx +26 -1
  381. package/src/renderers/opentui/test-utils.tsx +13 -0
  382. package/src/renderers/opentui/toast-host.tsx +26 -2
  383. package/src/renderers/opentui/ui-host.tsx +1 -0
  384. package/src/sources/provider-router/search.test.ts +191 -0
  385. package/src/sources/provider-router/search.ts +92 -39
  386. package/src/state/app/context/index.tsx +2 -0
  387. package/src/state/app/context/selector.test.tsx +33 -1
  388. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  389. package/src/state/hooks/quote-streaming.ts +106 -30
  390. package/src/state/pane-table-export-registry.ts +50 -0
  391. package/src/theme/color-utils.ts +1 -1
  392. package/src/theme/colors.ts +50 -0
  393. package/src/theme/themes.test.ts +15 -2
  394. package/src/theme/themes.ts +19 -1
  395. package/src/tickers/search/index.test.ts +26 -0
  396. package/src/tickers/search/index.ts +42 -17
  397. package/src/tickers/search/ranking.ts +14 -0
  398. package/src/time-series/chart-data.test.ts +216 -0
  399. package/src/time-series/range.ts +12 -0
  400. package/src/time-series/resolution.test.ts +25 -26
  401. package/src/time-series/resolution.ts +44 -25
  402. package/src/time-series/resolve.ts +7 -0
  403. package/src/time-series/spec.ts +5 -5
  404. package/src/time-series/types.ts +3 -1
  405. package/src/time-series/use-chart-resolution.ts +5 -0
  406. package/src/types/config.ts +123 -3
  407. package/src/types/data-provider.ts +11 -0
  408. package/src/types/headless.ts +161 -0
  409. package/src/types/plugin.ts +104 -20
  410. package/src/ui/host.tsx +19 -1
  411. package/src/ui/index.tsx +1 -0
  412. package/src/ui/toast.tsx +8 -4
  413. package/src/utils/csv.test.ts +13 -0
  414. package/src/utils/csv.ts +37 -0
  415. package/src/utils/format.ts +35 -0
  416. package/src/utils/fuzzy-search.test.ts +94 -0
  417. package/src/utils/fuzzy-search.ts +81 -22
  418. package/src/utils/save-text-file.test.ts +24 -0
  419. package/src/utils/save-text-file.ts +36 -0
  420. package/src/utils/shortcut-labels.ts +14 -0
  421. package/src/components/chart/core/axis-format.ts +0 -157
  422. package/src/components/chart/core/index.test.ts +0 -1075
  423. package/src/components/chart/core/projection.ts +0 -357
  424. package/src/components/chart/core/render-utils.ts +0 -4
  425. package/src/components/chart/core/renderer.ts +0 -244
  426. package/src/components/chart/core/scene.ts +0 -180
  427. package/src/components/chart/core/time-axis.ts +0 -330
  428. package/src/components/chart/cursor-motion.ts +0 -47
  429. package/src/components/chart/indicators/types.ts +0 -26
  430. package/src/components/chart/market-session.ts +0 -176
  431. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  432. package/src/components/chart/multi-line/renderer.ts +0 -161
  433. package/src/components/chart/multi-line/scene.ts +0 -188
  434. package/src/components/chart/native/raster/layout.ts +0 -129
  435. package/src/components/chart/native/raster/price-chart.ts +0 -244
  436. package/src/components/chart/rendering/chart-draw.ts +0 -270
  437. package/src/components/chart/rendering/geometry.ts +0 -95
  438. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  439. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  440. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  441. package/src/components/chart/static/chart/surface.tsx +0 -358
  442. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  443. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  444. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  445. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  446. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  447. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  448. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  449. package/src/plugins/builtin/substack/api/store.ts +0 -186
  450. package/src/plugins/builtin/substack/api/types.ts +0 -42
  451. package/src/plugins/builtin/substack/api.test.ts +0 -298
  452. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  453. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  454. package/src/plugins/builtin/substack/content.ts +0 -478
  455. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  456. package/src/plugins/builtin/substack/index.tsx +0 -49
  457. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  458. package/src/plugins/builtin/substack/model.test.ts +0 -155
  459. package/src/plugins/builtin/substack/normalize.ts +0 -343
  460. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  461. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  462. package/src/plugins/builtin/substack/pane.tsx +0 -547
  463. package/src/plugins/builtin/substack/read-state.ts +0 -30
  464. package/src/plugins/builtin/substack/table.ts +0 -111
  465. package/src/plugins/builtin/substack/types.ts +0 -71
  466. package/src/plugins/builtin/substack/utils.ts +0 -131
  467. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  468. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  469. package/src/plugins/ibkr/account-cache.ts +0 -24
  470. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  471. package/src/plugins/ibkr/config.test.ts +0 -79
  472. package/src/plugins/ibkr/config.ts +0 -250
  473. package/src/plugins/ibkr/dialogs.tsx +0 -40
  474. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  475. package/src/plugins/ibkr/flex/index.ts +0 -367
  476. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  477. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  478. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  479. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  480. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  481. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  482. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  483. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  484. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  485. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  486. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  487. package/src/plugins/ibkr/gateway/history.ts +0 -120
  488. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  489. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  490. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  491. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  492. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  493. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  494. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  495. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  496. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  497. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  498. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  499. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  500. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  501. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  502. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  503. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  504. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  505. package/src/plugins/ibkr/gateway/types.ts +0 -27
  506. package/src/plugins/ibkr/index.test.tsx +0 -162
  507. package/src/plugins/ibkr/index.tsx +0 -159
  508. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  509. package/src/plugins/ibkr/instance-selection.ts +0 -36
  510. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  511. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  512. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  513. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  514. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  515. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  516. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  517. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  518. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  519. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  520. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  521. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  522. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  523. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  524. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  525. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  526. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  527. package/src/plugins/ibkr/trade/utils.ts +0 -144
  528. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  529. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  530. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  531. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -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;
@@ -0,0 +1,50 @@
1
+ import { createCsvExportFilename } from "../utils/csv";
2
+
3
+ export type PaneTableExporter = (filename: string) => Promise<string>;
4
+
5
+ const exporters = new Map<string, Map<symbol, PaneTableExporter>>();
6
+
7
+ export function registerPaneTableExporter(
8
+ paneId: string,
9
+ exporter: PaneTableExporter,
10
+ ): () => void {
11
+ const token = Symbol(paneId);
12
+ const paneExporters = exporters.get(paneId) ?? new Map<symbol, PaneTableExporter>();
13
+ paneExporters.set(token, exporter);
14
+ exporters.set(paneId, paneExporters);
15
+
16
+ return () => {
17
+ const current = exporters.get(paneId);
18
+ current?.delete(token);
19
+ if (current?.size === 0) exporters.delete(paneId);
20
+ };
21
+ }
22
+
23
+ export function hasPaneTableExporter(paneId: string): boolean {
24
+ return exporters.get(paneId)?.size === 1;
25
+ }
26
+
27
+ export async function exportPaneTable(paneId: string, filename: string): Promise<string> {
28
+ const paneExporters = exporters.get(paneId);
29
+ if (paneExporters?.size !== 1) {
30
+ throw new Error("This pane does not have one active exportable table.");
31
+ }
32
+ return [...paneExporters.values()][0]!(filename);
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
+ }
@@ -10,7 +10,7 @@ export function blendHex(a: string, b: string, ratio: number): string {
10
10
  return `#${mix(ar, br)}${mix(ag, bg)}${mix(ab, bb)}`;
11
11
  }
12
12
 
13
- function relativeLuminance(hex: string): number {
13
+ export function relativeLuminance(hex: string): number {
14
14
  const h = hex.replace("#", "");
15
15
  const toLinear = (value: string) => {
16
16
  const normalized = parseInt(value, 16) / 255;
@@ -193,6 +193,56 @@ export function commandBarAccentText(palette: ThemeColors = colors): string {
193
193
  );
194
194
  }
195
195
 
196
+ /**
197
+ * Matched terms inside a result row's snippet. It has to stay legible on the
198
+ * selected row too, so contrast is checked against the selection background as
199
+ * well as the two panel surfaces.
200
+ */
201
+ export function commandBarMatchText(palette: ThemeColors = colors): string {
202
+ const surfaces = [
203
+ commandBarBg(palette),
204
+ commandBarPanelBg(palette),
205
+ commandBarSelectedBg(palette),
206
+ ] as const;
207
+ return blendForContrastOnSurfaces(
208
+ palette.warning,
209
+ surfaces,
210
+ higherContrast("#ffffff", "#000000", surfaces[0]),
211
+ 3.6,
212
+ );
213
+ }
214
+
215
+ export type CommandBarBadgeTone = "command" | "instrument" | "document" | "assist";
216
+
217
+ function commandBarBadgeHue(tone: CommandBarBadgeTone, palette: ThemeColors): string {
218
+ if (tone === "instrument") return palette.warning;
219
+ if (tone === "document") return palette.neutral;
220
+ if (tone === "assist") return ASSIST_ACCENT;
221
+ return palette.borderFocused;
222
+ }
223
+
224
+ /**
225
+ * Text-only tag left of a result label. One hue per family so commands,
226
+ * instruments, documents and AI answers scan apart without a filled box, pulled
227
+ * towards the subtle text so the tag stays quieter than the label, then pushed
228
+ * only until it clears every surface it can land on, the selected row included.
229
+ */
230
+ export function commandBarBadgeText(tone: CommandBarBadgeTone, palette: ThemeColors = colors): string {
231
+ const surfaces = [
232
+ commandBarBg(palette),
233
+ commandBarPanelBg(palette),
234
+ commandBarHoverBg(palette),
235
+ commandBarSelectedBg(palette),
236
+ ] as const;
237
+ const muted = blendHex(commandBarBadgeHue(tone, palette), commandBarSubtleText(palette), 0.4);
238
+ return blendForContrastOnSurfaces(
239
+ muted,
240
+ surfaces,
241
+ higherContrast("#ffffff", "#000000", surfaces[0]),
242
+ 3.6,
243
+ );
244
+ }
245
+
196
246
  export function commandBarSelectedText(palette: ThemeColors = colors): string {
197
247
  const base = commandBarSelectedBg(palette);
198
248
  const preferred = higherContrast(palette.selectedText, palette.text, base);
@@ -1,4 +1,4 @@
1
- import { afterEach, describe, test } from "bun:test";
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import {
3
3
  applyTheme,
4
4
  commandBarAccentText,
@@ -16,7 +16,7 @@ import {
16
16
  paneTitleText,
17
17
  } from "./colors";
18
18
  import { contrastRatio } from "./color-utils";
19
- import { DEFAULT_THEME, themes } from "./themes";
19
+ import { DEFAULT_THEME, getThemeIds, isDarkTheme, themes } from "./themes";
20
20
 
21
21
  const BODY_TEXT_MIN = 4.5;
22
22
  const SUBTLE_TEXT_MIN = 3.6;
@@ -34,6 +34,19 @@ afterEach(() => {
34
34
  applyTheme(DEFAULT_THEME);
35
35
  });
36
36
 
37
+ /**
38
+ * The theme picker marks the dark half of its list with a moon, so a palette
39
+ * that lands on the wrong side of this shows the wrong glyph, and the whole
40
+ * gutter stops meaning anything. Pinning the light set catches a palette drifting
41
+ * across the line when someone retunes its colours.
42
+ */
43
+ describe("theme appearance", () => {
44
+ test("reads every light theme as light and everything else as dark", () => {
45
+ const light = getThemeIds().filter((id) => !isDarkTheme(id)).sort();
46
+ expect(light).toEqual(["github-light", "gruvbox-light", "nord-light", "paper", "solarized-light"]);
47
+ });
48
+ });
49
+
37
50
  describe("theme contrast", () => {
38
51
  test("keeps shared text roles readable on body surfaces", () => {
39
52
  for (const [id, theme] of Object.entries(themes)) {
@@ -1,4 +1,11 @@
1
- import { blendForContrast, blendForContrastOnSurfaces, blendHex, contrastRatio, higherContrast } from "./color-utils";
1
+ import {
2
+ blendForContrast,
3
+ blendForContrastOnSurfaces,
4
+ blendHex,
5
+ contrastRatio,
6
+ higherContrast,
7
+ relativeLuminance,
8
+ } from "./color-utils";
2
9
 
3
10
  export interface Theme {
4
11
  name: string;
@@ -638,6 +645,17 @@ export function getThemeIds(): string[] {
638
645
  return Object.keys(themes);
639
646
  }
640
647
 
648
+ /**
649
+ * A theme is dark when its text sits lighter than its background. That is the
650
+ * definition, and it beats thresholding the background on its own, which
651
+ * mislabels the mid-tone palettes sitting near whatever cutoff you pick.
652
+ */
653
+ export function isDarkTheme(themeId: string): boolean {
654
+ const theme = themes[themeId];
655
+ if (!theme) return true;
656
+ return relativeLuminance(theme.text) > relativeLuminance(theme.bg);
657
+ }
658
+
641
659
  export function getTheme(id: string): Theme {
642
660
  return themes[id] ?? themes[DEFAULT_THEME]!;
643
661
  }
@@ -359,6 +359,32 @@ describe("ticker-search utilities", () => {
359
359
  expect(rankTickerSearchItems([...candidates].reverse(), "Apple").map((item) => item.symbol)).toEqual(expected);
360
360
  });
361
361
 
362
+ test("ranks the primary listing above leveraged and depositary look-alikes", () => {
363
+ const make = (symbol: string, right: string, providerRank: number) => ({
364
+ id: symbol,
365
+ label: symbol,
366
+ symbol,
367
+ detail: "NVIDIA Corporation",
368
+ right,
369
+ category: "Other Listings",
370
+ kind: "search" as const,
371
+ instrumentClass: "equity" as const,
372
+ providerRank,
373
+ });
374
+ // Provider order puts the Milan 4x product first; every text signal ties.
375
+ const candidates = [make("4NVDA", "MTA", 0), make("NVDA", "NASDAQ", 1), make("NVDC34", "BOVESPA", 2)];
376
+
377
+ expect(rankTickerSearchItems(candidates, "nvidia").map((item) => item.symbol))
378
+ .toEqual(["NVDA", "4NVDA", "NVDC34"]);
379
+
380
+ // All-digit symbols are Tokyo, Shanghai and Hong Kong primaries, not look-alikes.
381
+ const toyota = [
382
+ { ...make("7203", "TSE", 0), detail: "Toyota Motor Corporation" },
383
+ { ...make("TM", "NYSE", 1), detail: "Toyota Motor Corporation" },
384
+ ];
385
+ expect(rankTickerSearchItems(toyota, "toyota").map((item) => item.symbol)).toEqual(["7203", "TM"]);
386
+ });
387
+
362
388
  test("normalizes punctuated legal suffixes before applying same-issuer provider order", () => {
363
389
  const results = buildTickerSearchCandidates({
364
390
  query: "Acme",
@@ -117,6 +117,7 @@ export async function searchTickerCandidates({
117
117
  localLimit = 6,
118
118
  totalLimit = 8,
119
119
  includeOptionContracts = true,
120
+ onPartial,
120
121
  }: {
121
122
  query: string;
122
123
  tickers: ReadonlyMap<string, TickerRecord>;
@@ -125,15 +126,23 @@ export async function searchTickerCandidates({
125
126
  localLimit?: number;
126
127
  totalLimit?: number;
127
128
  includeOptionContracts?: boolean;
129
+ /** Called when a slower, richer source improves results already returned. */
130
+ onPartial?: (candidates: TickerSearchCandidate[]) => void;
128
131
  }): Promise<TickerSearchCandidate[]> {
129
- return buildTickerSearchCandidates({
132
+ const assemble = (providerResults: InstrumentSearchResult[]) => buildTickerSearchCandidates({
130
133
  query,
131
134
  tickers,
132
- providerResults: await searchProviderResults(dataProvider, query, searchContext),
135
+ providerResults,
133
136
  localLimit,
134
137
  totalLimit,
135
138
  includeOptionContracts,
136
139
  });
140
+ return assemble(await searchProviderResults(
141
+ dataProvider,
142
+ query,
143
+ searchContext,
144
+ onPartial ? (results) => onPartial(assemble(results)) : undefined,
145
+ ));
137
146
  }
138
147
 
139
148
  export function buildTickerSearchCandidates({
@@ -233,27 +242,43 @@ async function searchProviderResults(
233
242
  dataProvider: DataProvider,
234
243
  query: string,
235
244
  searchContext?: SearchRequestContext,
245
+ onPartial?: (results: InstrumentSearchResult[]) => void,
236
246
  ): Promise<InstrumentSearchResult[]> {
237
- const merged: InstrumentSearchResult[] = [];
238
- const seen = new Set<string>();
247
+ // A Map rather than a list plus a seen set, because a later source can send
248
+ // back a richer version of a symbol already recorded. Overwriting a key keeps
249
+ // its original position, so an upgrade does not reorder the list.
250
+ const byKey = new Map<string, InstrumentSearchResult>();
251
+ const add = (results: InstrumentSearchResult[], upgrade = false) => {
252
+ for (const result of results) {
253
+ const key = buildProviderSearchResultKey(result);
254
+ if (!upgrade && byKey.has(key)) continue;
255
+ byKey.set(key, result);
256
+ }
257
+ };
239
258
 
240
- for (const searchQuery of buildProviderSearchQueries(query)) {
241
- let results: InstrumentSearchResult[] = [];
259
+ // The variants are independent lookups of the same words, so they run
260
+ // together. Awaited in turn they multiplied every per-source timeout by the
261
+ // number of spellings tried.
262
+ await Promise.all(buildProviderSearchQueries(query).map(async (searchQuery) => {
242
263
  try {
243
- results = await dataProvider.search(searchQuery, searchContext);
264
+ const results = await dataProvider.search(searchQuery, {
265
+ ...searchContext,
266
+ ...(onPartial
267
+ ? {
268
+ onPartial: (upgraded: InstrumentSearchResult[]) => {
269
+ add(upgraded, true);
270
+ onPartial([...byKey.values()]);
271
+ },
272
+ }
273
+ : {}),
274
+ });
275
+ add(results);
244
276
  } catch {
245
- results = [];
277
+ // One spelling failing must not lose the others.
246
278
  }
279
+ }));
247
280
 
248
- for (const result of results) {
249
- const key = buildProviderSearchResultKey(result);
250
- if (seen.has(key)) continue;
251
- seen.add(key);
252
- merged.push(result);
253
- }
254
- }
255
-
256
- return merged;
281
+ return [...byKey.values()];
257
282
  }
258
283
 
259
284
  function buildProviderSearchQueries(query: string): string[] {
@@ -225,6 +225,15 @@ export function rankTickerSearchItems<T extends Pick<TickerSearchRankableItem, "
225
225
  a.issuerGroupKey
226
226
  && a.issuerGroupKey === b.issuerGroupKey
227
227
  ) {
228
+ // Letters mixed with digits (4NVDA, NVDC34, 0R1I) mark a leveraged
229
+ // product, a depositary receipt or a secondary venue's code filed under
230
+ // the issuer's name, never its primary listing. All-digit symbols are
231
+ // Tokyo, Shanghai and Hong Kong primaries and must not be touched.
232
+ // Provider order is trusted for real listings of one issuer, but it put
233
+ // a Milan 4x product above NVDA, so mixed codes yield first.
234
+ const aSynthetic = isMixedCode(a.item.label);
235
+ const bSynthetic = isMixedCode(b.item.label);
236
+ if (aSynthetic !== bSynthetic) return aSynthetic ? 1 : -1;
228
237
  const aProviderRank = a.item.providerRank ?? Number.POSITIVE_INFINITY;
229
238
  const bProviderRank = b.item.providerRank ?? Number.POSITIVE_INFINITY;
230
239
  if (aProviderRank !== bProviderRank) return aProviderRank - bProviderRank;
@@ -438,6 +447,11 @@ function scoreExchangePreference(
438
447
  return matchesHint ? 2_000 : -800;
439
448
  }
440
449
 
450
+ function isMixedCode(label: string): boolean {
451
+ const symbol = label.split(".")[0] ?? label;
452
+ return /\d/.test(symbol) && /[A-Za-z]/.test(symbol);
453
+ }
454
+
441
455
  function scoreListingPriority(item: Pick<TickerSearchRankableItem, "label"> & Partial<TickerSearchRankableItem>): number {
442
456
  const instrumentClass = item.instrumentClass || "other";
443
457
  if (instrumentClass !== "equity") return 0;