gloomberb 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (531) hide show
  1. package/README.md +7 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +108 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +134 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +220 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/capabilities/chart-series.ts +3 -2
  20. package/src/cli/commands/market.ts +2 -2
  21. package/src/cli/commands/plugins.ts +39 -11
  22. package/src/cli/desktop-pane-shot.ts +285 -16
  23. package/src/cli/index.ts +1 -1
  24. package/src/cli/pane-functions/capabilities.ts +61 -31
  25. package/src/cli/pane-functions/catalog.ts +16 -4
  26. package/src/cli/pane-functions/discovery.ts +1 -0
  27. package/src/cli/pane-functions/headless.test.ts +247 -0
  28. package/src/cli/pane-functions/headless.ts +423 -0
  29. package/src/cli/pane-functions/index.test.ts +9 -0
  30. package/src/cli/pane-functions/index.ts +6 -2
  31. package/src/cli/pane-functions/options.ts +27 -2
  32. package/src/cli/pane-functions/report.ts +4 -0
  33. package/src/cli/pane-functions/resolver.ts +26 -8
  34. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  35. package/src/cli/pane-functions/screenshot.ts +236 -20
  36. package/src/cli/registry.ts +0 -22
  37. package/src/cli/restore-plugins.ts +34 -0
  38. package/src/cli/result.ts +7 -10
  39. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  40. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  41. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  42. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  43. package/src/components/chart/composite/composite-chart.tsx +301 -204
  44. package/src/components/chart/composite/format.ts +8 -2
  45. package/src/components/chart/composite/interactions.test.ts +68 -102
  46. package/src/components/chart/composite/interactions.ts +214 -264
  47. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  48. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  49. package/src/components/chart/composite/scene.ts +57 -3
  50. package/src/components/chart/composite/text-renderer.ts +3 -2
  51. package/src/components/chart/composite/time-scale.ts +138 -57
  52. package/src/components/chart/composite/types.ts +25 -1
  53. package/src/components/chart/core/data.ts +9 -13
  54. package/src/components/chart/core/date-window.ts +7 -1
  55. package/src/components/chart/core/pointer.ts +3 -0
  56. package/src/components/chart/core/types.ts +0 -45
  57. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  58. package/src/components/chart/native/kitty/index.test.ts +0 -188
  59. package/src/components/chart/native/raster/crosshair.ts +6 -5
  60. package/src/components/chart/native/raster/primitives.ts +0 -23
  61. package/src/components/chart/native/renderer-selection.ts +81 -31
  62. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  63. package/src/components/chart/static/chart-surface.tsx +150 -0
  64. package/src/components/chart/static/index.ts +8 -0
  65. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  66. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  67. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  68. package/src/components/chart/static/series.ts +49 -0
  69. package/src/components/command-bar/action-runtime.ts +1 -3
  70. package/src/components/command-bar/assist/model.test.ts +17 -2
  71. package/src/components/command-bar/assist/model.ts +40 -9
  72. package/src/components/command-bar/commands/direct/index.ts +1 -1
  73. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  74. package/src/components/command-bar/commands/registry.ts +0 -11
  75. package/src/components/command-bar/helpers.test.ts +2 -2
  76. package/src/components/command-bar/helpers.ts +2 -6
  77. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  78. package/src/components/command-bar/list/badge.test.ts +54 -0
  79. package/src/components/command-bar/list/badge.ts +62 -0
  80. package/src/components/command-bar/list/model.test.ts +103 -0
  81. package/src/components/command-bar/list/model.ts +61 -4
  82. package/src/components/command-bar/list/view.test.tsx +113 -0
  83. package/src/components/command-bar/list/view.tsx +147 -131
  84. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  85. package/src/components/command-bar/panel/body.tsx +51 -65
  86. package/src/components/command-bar/panel/index.tsx +48 -13
  87. package/src/components/command-bar/panel/layout.test.ts +139 -0
  88. package/src/components/command-bar/panel/layout.ts +65 -28
  89. package/src/components/command-bar/panel/native-surface.ts +24 -0
  90. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  91. package/src/components/command-bar/panel/runtime.ts +6 -3
  92. package/src/components/command-bar/panel/state.ts +41 -7
  93. package/src/components/command-bar/panel/types.ts +7 -1
  94. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  95. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  96. package/src/components/command-bar/routes/root/results.ts +30 -28
  97. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  98. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  99. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  100. package/src/components/command-bar/routes/root/selection.ts +1 -12
  101. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  102. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  103. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  104. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  105. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  106. package/src/components/command-bar/routing/actions.ts +1 -81
  107. package/src/components/command-bar/routing/launch-request.ts +1 -1
  108. package/src/components/command-bar/routing/list-state.ts +6 -21
  109. package/src/components/command-bar/selection-runtime.ts +1 -1
  110. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  111. package/src/components/command-bar/surface/environment.ts +8 -1
  112. package/src/components/command-bar/surface/index.test.tsx +84 -68
  113. package/src/components/command-bar/surface/index.tsx +33 -36
  114. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  115. package/src/components/command-bar/theme-picker.tsx +43 -15
  116. package/src/components/command-bar/view-model.test.ts +53 -8
  117. package/src/components/command-bar/view-model.ts +43 -14
  118. package/src/components/command-bar/workflow/tickers.ts +1 -0
  119. package/src/components/command-bar/workflow/types.ts +1 -1
  120. package/src/components/data-table/export.test.ts +21 -0
  121. package/src/components/data-table/export.ts +21 -0
  122. package/src/components/data-table/view.test.tsx +45 -6
  123. package/src/components/data-table/view.tsx +55 -9
  124. package/src/components/index.ts +10 -2
  125. package/src/components/input-search-bar.test.tsx +36 -2
  126. package/src/components/input-search-bar.tsx +18 -1
  127. package/src/components/layout/header.test.tsx +57 -10
  128. package/src/components/layout/header.tsx +292 -149
  129. package/src/components/layout/market-summary.test.ts +81 -0
  130. package/src/components/layout/market-summary.ts +109 -0
  131. package/src/components/layout/shell/chrome.ts +96 -1
  132. package/src/components/layout/shell/index.tsx +11 -3
  133. package/src/components/layout/shell/menu.ts +9 -0
  134. package/src/components/layout/shell/pane/actions.ts +29 -0
  135. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  136. package/src/components/layout/shell/shortcuts.ts +3 -0
  137. package/src/components/layout/status-bar.test.tsx +9 -8
  138. package/src/components/layout/status-bar.tsx +85 -54
  139. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  140. package/src/components/price-sparkline/index.ts +61 -34
  141. package/src/components/price-sparkline/view.tsx +1 -1
  142. package/src/components/sign-in-gate.test.tsx +128 -0
  143. package/src/components/sign-in-gate.tsx +149 -0
  144. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  145. package/src/components/ticker/badge/list.tsx +17 -3
  146. package/src/components/ticker/list-table-view.tsx +6 -3
  147. package/src/components/ui/data-table/index.tsx +22 -4
  148. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  149. package/src/components/ui/frame.tsx +28 -0
  150. package/src/components/ui/index.ts +3 -0
  151. package/src/components/ui/native-select.tsx +20 -10
  152. package/src/components/ui/select-button.tsx +138 -0
  153. package/src/components/ui/status.tsx +2 -2
  154. package/src/components/ui/table-layout.ts +1 -1
  155. package/src/components/use-double-click-activation.ts +7 -3
  156. package/src/core/app-services.ts +3 -3
  157. package/src/data/config/chart-settings.ts +4 -4
  158. package/src/data/config/layout.ts +12 -6
  159. package/src/data/config/store/index.test.ts +97 -0
  160. package/src/data/config/store/migrations.ts +41 -0
  161. package/src/data/config/store/normalize.ts +5 -3
  162. package/src/i18n/es.ts +6 -2
  163. package/src/i18n/ja.ts +6 -2
  164. package/src/i18n/ko.ts +6 -2
  165. package/src/i18n/zh-cn.ts +6 -2
  166. package/src/i18n/zh-tw.ts +6 -2
  167. package/src/layout-marketplace/gallery-terminal.tsx +126 -96
  168. package/src/layout-marketplace/gallery.test.tsx +65 -1
  169. package/src/layout-marketplace/pane-imagery.ts +1 -0
  170. package/src/layout-marketplace/payload.test.ts +17 -0
  171. package/src/layout-marketplace/payload.ts +40 -5
  172. package/src/market-data/coordinator/index.ts +3 -2
  173. package/src/market-data/coordinator/quotes.ts +173 -66
  174. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  175. package/src/pane-settings.ts +1 -1
  176. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  177. package/src/plugins/builtin/analytics/view.tsx +1 -1
  178. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  179. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  180. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  181. package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
  182. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  183. package/src/plugins/builtin/chart-composer/settings.ts +2 -13
  184. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  185. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  186. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  188. package/src/plugins/builtin/composite-plugins.ts +16 -1
  189. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  190. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  191. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  192. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  193. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  194. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  195. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  196. package/src/plugins/builtin/earnings/index.tsx +1 -0
  197. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  198. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  199. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  200. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  201. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  202. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  203. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  204. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  205. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  206. package/src/plugins/builtin/econ/index.tsx +3 -1
  207. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  208. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  209. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  210. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  211. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  212. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  213. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  214. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  215. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  216. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  217. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  218. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  219. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  220. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  221. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  222. package/src/plugins/builtin/futures/index.tsx +1 -0
  223. package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
  224. package/src/plugins/builtin/help/index.tsx +4 -0
  225. package/src/plugins/builtin/holders/index.tsx +1 -0
  226. package/src/plugins/builtin/insider/index.tsx +1 -0
  227. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
  228. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  229. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  230. package/src/plugins/builtin/market-halts/index.tsx +1 -0
  231. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  232. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  233. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  234. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  235. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  236. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  237. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  238. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  239. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  240. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  241. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  242. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  243. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  244. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  245. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  246. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  247. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  248. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  249. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  250. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  251. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  252. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  253. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  254. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  255. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  256. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  257. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  258. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  259. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  260. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  261. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  262. package/src/plugins/builtin/options/analytics.test.ts +83 -0
  263. package/src/plugins/builtin/options/analytics.ts +110 -0
  264. package/src/plugins/builtin/options/index.tsx +18 -2
  265. package/src/plugins/builtin/options/table.ts +121 -51
  266. package/src/plugins/builtin/options/types.ts +22 -15
  267. package/src/plugins/builtin/options/view.test.tsx +53 -1
  268. package/src/plugins/builtin/options/view.tsx +92 -41
  269. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  270. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  271. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  272. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  273. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  274. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  275. package/src/plugins/builtin/plugin-module.test.ts +2 -4
  276. package/src/plugins/builtin/plugin-module.ts +9 -8
  277. package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
  278. package/src/plugins/builtin/research/index.tsx +4 -0
  279. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  280. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  281. package/src/plugins/builtin/research-search/data.ts +109 -0
  282. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  283. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  284. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  285. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  286. package/src/plugins/builtin/research-search/index.tsx +85 -0
  287. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  288. package/src/plugins/builtin/research-search/model.ts +291 -0
  289. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  290. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  291. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  292. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  293. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  294. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  295. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  296. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  297. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  298. package/src/plugins/builtin/scanner/index.ts +2 -0
  299. package/src/plugins/builtin/sec/index.tsx +1 -0
  300. package/src/plugins/builtin/sectors/index.tsx +1 -0
  301. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  302. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  303. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  304. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  305. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
  306. package/src/plugins/builtin/ticker-detail/index.tsx +2 -14
  307. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  308. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  309. package/src/plugins/builtin/volatility/index.tsx +1 -1
  310. package/src/plugins/builtin/world-indices/index.tsx +1 -0
  311. package/src/plugins/builtin/world-venue-map/index.ts +27 -0
  312. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  313. package/src/plugins/builtin/world-venue-map/map.tsx +481 -0
  314. package/src/plugins/builtin/world-venue-map/model.test.ts +85 -0
  315. package/src/plugins/builtin/world-venue-map/model.ts +268 -0
  316. package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
  317. package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
  318. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  319. package/src/plugins/bundle.test.ts +109 -0
  320. package/src/plugins/bundle.ts +139 -0
  321. package/src/plugins/catalog-backend.ts +6 -44
  322. package/src/plugins/catalog-browser.test.ts +1 -0
  323. package/src/plugins/catalog-browser.ts +12 -0
  324. package/src/plugins/catalog-manifest.test.ts +43 -0
  325. package/src/plugins/catalog-manifest.ts +92 -0
  326. package/src/plugins/catalog-ui.ts +20 -4
  327. package/src/plugins/catalog.ts +9 -2
  328. package/src/plugins/current-target.ts +28 -0
  329. package/src/plugins/host-contract.ts +37 -0
  330. package/src/plugins/host-link.ts +158 -0
  331. package/src/plugins/host-modules.ts +61 -0
  332. package/src/plugins/loader.ts +41 -22
  333. package/src/plugins/ownership.ts +1 -0
  334. package/src/plugins/prediction-markets/index.tsx +1 -0
  335. package/src/plugins/registry/context.ts +3 -0
  336. package/src/plugins/registry/contributions.ts +27 -0
  337. package/src/plugins/registry/index.test.ts +25 -0
  338. package/src/plugins/registry/index.ts +8 -0
  339. package/src/plugins/registry/pane-settings.test.ts +53 -0
  340. package/src/plugins/registry/pane-settings.ts +29 -3
  341. package/src/plugins/seed.test.ts +83 -0
  342. package/src/plugins/seed.ts +80 -0
  343. package/src/public/broker.ts +31 -0
  344. package/src/public/market-data.ts +9 -0
  345. package/src/public/pane-hooks.ts +57 -0
  346. package/src/public/public-api.test.ts +285 -0
  347. package/src/public/react.ts +62 -0
  348. package/src/public/test-support.ts +23 -0
  349. package/src/public/time-series.ts +8 -0
  350. package/src/public/utils.ts +77 -0
  351. package/src/remote/layout-helpers.ts +6 -8
  352. package/src/remote/resources.ts +2 -0
  353. package/src/remote/schema.ts +1 -0
  354. package/src/remote/semantic-tree.test.tsx +105 -0
  355. package/src/remote/semantic-tree.tsx +53 -35
  356. package/src/renderers/browser/main.tsx +6 -0
  357. package/src/renderers/browser/ui-host.tsx +11 -0
  358. package/src/renderers/cloudflare/worker.test.ts +19 -0
  359. package/src/renderers/cloudflare/worker.ts +1 -1
  360. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
  361. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  362. package/src/renderers/electrobun/bun/index.ts +6 -2
  363. package/src/renderers/electrobun/shared/protocol.ts +25 -0
  364. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  365. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  366. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  367. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  368. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  369. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  370. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  371. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  372. package/src/renderers/electrobun/view/main.tsx +27 -2
  373. package/src/renderers/electrobun/view/styles.css +5 -0
  374. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  375. package/src/renderers/electrobun/view/ui-host.tsx +3 -0
  376. package/src/renderers/opentui/chart-surface.tsx +5 -1
  377. package/src/renderers/opentui/host.tsx +20 -2
  378. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  379. package/src/renderers/opentui/interaction-performance.ts +188 -0
  380. package/src/renderers/opentui/start.tsx +26 -1
  381. package/src/renderers/opentui/test-utils.tsx +13 -0
  382. package/src/renderers/opentui/toast-host.tsx +26 -2
  383. package/src/renderers/opentui/ui-host.tsx +1 -0
  384. package/src/sources/provider-router/search.test.ts +191 -0
  385. package/src/sources/provider-router/search.ts +92 -39
  386. package/src/state/app/context/index.tsx +2 -0
  387. package/src/state/app/context/selector.test.tsx +33 -1
  388. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  389. package/src/state/hooks/quote-streaming.ts +106 -30
  390. package/src/state/pane-table-export-registry.ts +50 -0
  391. package/src/theme/color-utils.ts +1 -1
  392. package/src/theme/colors.ts +50 -0
  393. package/src/theme/themes.test.ts +15 -2
  394. package/src/theme/themes.ts +19 -1
  395. package/src/tickers/search/index.test.ts +26 -0
  396. package/src/tickers/search/index.ts +42 -17
  397. package/src/tickers/search/ranking.ts +14 -0
  398. package/src/time-series/chart-data.test.ts +216 -0
  399. package/src/time-series/range.ts +12 -0
  400. package/src/time-series/resolution.test.ts +25 -26
  401. package/src/time-series/resolution.ts +44 -25
  402. package/src/time-series/resolve.ts +7 -0
  403. package/src/time-series/spec.ts +5 -5
  404. package/src/time-series/types.ts +3 -1
  405. package/src/time-series/use-chart-resolution.ts +5 -0
  406. package/src/types/config.ts +123 -3
  407. package/src/types/data-provider.ts +11 -0
  408. package/src/types/headless.ts +161 -0
  409. package/src/types/plugin.ts +104 -20
  410. package/src/ui/host.tsx +19 -1
  411. package/src/ui/index.tsx +1 -0
  412. package/src/ui/toast.tsx +8 -4
  413. package/src/utils/csv.test.ts +13 -0
  414. package/src/utils/csv.ts +37 -0
  415. package/src/utils/format.ts +35 -0
  416. package/src/utils/fuzzy-search.test.ts +94 -0
  417. package/src/utils/fuzzy-search.ts +81 -22
  418. package/src/utils/save-text-file.test.ts +24 -0
  419. package/src/utils/save-text-file.ts +36 -0
  420. package/src/utils/shortcut-labels.ts +14 -0
  421. package/src/components/chart/core/axis-format.ts +0 -157
  422. package/src/components/chart/core/index.test.ts +0 -1075
  423. package/src/components/chart/core/projection.ts +0 -357
  424. package/src/components/chart/core/render-utils.ts +0 -4
  425. package/src/components/chart/core/renderer.ts +0 -244
  426. package/src/components/chart/core/scene.ts +0 -180
  427. package/src/components/chart/core/time-axis.ts +0 -330
  428. package/src/components/chart/cursor-motion.ts +0 -47
  429. package/src/components/chart/indicators/types.ts +0 -26
  430. package/src/components/chart/market-session.ts +0 -176
  431. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  432. package/src/components/chart/multi-line/renderer.ts +0 -161
  433. package/src/components/chart/multi-line/scene.ts +0 -188
  434. package/src/components/chart/native/raster/layout.ts +0 -129
  435. package/src/components/chart/native/raster/price-chart.ts +0 -244
  436. package/src/components/chart/rendering/chart-draw.ts +0 -270
  437. package/src/components/chart/rendering/geometry.ts +0 -95
  438. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  439. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  440. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  441. package/src/components/chart/static/chart/surface.tsx +0 -358
  442. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  443. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  444. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  445. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  446. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  447. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  448. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  449. package/src/plugins/builtin/substack/api/store.ts +0 -186
  450. package/src/plugins/builtin/substack/api/types.ts +0 -42
  451. package/src/plugins/builtin/substack/api.test.ts +0 -298
  452. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  453. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  454. package/src/plugins/builtin/substack/content.ts +0 -478
  455. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  456. package/src/plugins/builtin/substack/index.tsx +0 -49
  457. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  458. package/src/plugins/builtin/substack/model.test.ts +0 -155
  459. package/src/plugins/builtin/substack/normalize.ts +0 -343
  460. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  461. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  462. package/src/plugins/builtin/substack/pane.tsx +0 -547
  463. package/src/plugins/builtin/substack/read-state.ts +0 -30
  464. package/src/plugins/builtin/substack/table.ts +0 -111
  465. package/src/plugins/builtin/substack/types.ts +0 -71
  466. package/src/plugins/builtin/substack/utils.ts +0 -131
  467. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  468. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  469. package/src/plugins/ibkr/account-cache.ts +0 -24
  470. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  471. package/src/plugins/ibkr/config.test.ts +0 -79
  472. package/src/plugins/ibkr/config.ts +0 -250
  473. package/src/plugins/ibkr/dialogs.tsx +0 -40
  474. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  475. package/src/plugins/ibkr/flex/index.ts +0 -367
  476. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  477. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  478. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  479. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  480. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  481. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  482. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  483. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  484. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  485. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  486. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  487. package/src/plugins/ibkr/gateway/history.ts +0 -120
  488. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  489. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  490. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  491. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  492. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  493. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  494. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  495. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  496. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  497. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  498. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  499. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  500. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  501. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  502. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  503. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  504. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  505. package/src/plugins/ibkr/gateway/types.ts +0 -27
  506. package/src/plugins/ibkr/index.test.tsx +0 -162
  507. package/src/plugins/ibkr/index.tsx +0 -159
  508. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  509. package/src/plugins/ibkr/instance-selection.ts +0 -36
  510. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  511. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  512. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  513. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  514. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  515. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  516. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  517. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  518. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  519. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  520. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  521. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  522. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  523. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  524. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  525. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  526. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  527. package/src/plugins/ibkr/trade/utils.ts +0 -144
  528. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  529. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  530. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  531. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -0,0 +1,171 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import {
3
+ DataTableView,
4
+ type DataTableCell,
5
+ type DataTableColumn,
6
+ type DataTableKeyEvent,
7
+ } from "../../../components";
8
+ import { colors } from "../../../theme/colors";
9
+ import { isPlainKey } from "../../../utils/keyboard";
10
+ import type { CloudSavedSearch } from "../../../api-client";
11
+ import { describeFilters, formatHitDate } from "./model";
12
+
13
+ type SavedColumnId = "name" | "filters" | "alert" | "last" | "hits";
14
+
15
+ interface SavedColumn extends DataTableColumn {
16
+ id: SavedColumnId;
17
+ }
18
+
19
+ function buildSavedColumns(width: number): SavedColumn[] {
20
+ const alertWidth = 6;
21
+ const lastWidth = 10;
22
+ const hitsWidth = 6;
23
+ const remaining = Math.max(24, width - (alertWidth + lastWidth + hitsWidth + 8));
24
+ const nameWidth = Math.max(12, Math.round(remaining * 0.5));
25
+ return [
26
+ { id: "name", label: "SAVED SEARCH", width: nameWidth, align: "left" },
27
+ { id: "filters", label: "FILTERS", width: Math.max(10, remaining - nameWidth), align: "left" },
28
+ { id: "alert", label: "ALERT", width: alertWidth, align: "left" },
29
+ { id: "last", label: "LAST HIT", width: lastWidth, align: "left" },
30
+ { id: "hits", label: "HITS", width: hitsWidth, align: "right" },
31
+ ];
32
+ }
33
+
34
+ interface SavedSort {
35
+ columnId: SavedColumnId;
36
+ direction: "asc" | "desc";
37
+ }
38
+
39
+ function sortValue(search: CloudSavedSearch, columnId: SavedColumnId): string | number {
40
+ switch (columnId) {
41
+ case "last":
42
+ return search.lastMatchAt ? Date.parse(search.lastMatchAt) : 0;
43
+ case "hits":
44
+ return search.matchCount;
45
+ case "alert":
46
+ return search.alertEnabled ? 1 : 0;
47
+ case "filters":
48
+ return describeFilters(search.filters).toLowerCase();
49
+ default:
50
+ return (search.name || search.query).toLowerCase();
51
+ }
52
+ }
53
+
54
+ export function SavedSearchesView({
55
+ searches,
56
+ selectedId,
57
+ onSelect,
58
+ onRun,
59
+ onToggleAlert,
60
+ onDelete,
61
+ focused,
62
+ width,
63
+ height,
64
+ emptyTitle,
65
+ }: {
66
+ searches: CloudSavedSearch[];
67
+ selectedId: string | null;
68
+ onSelect: (id: string) => void;
69
+ onRun: (search: CloudSavedSearch) => void;
70
+ onToggleAlert: (search: CloudSavedSearch) => void;
71
+ onDelete: (search: CloudSavedSearch) => void;
72
+ focused: boolean;
73
+ width: number;
74
+ height: number;
75
+ emptyTitle: string;
76
+ }) {
77
+ const [sort, setSort] = useState<SavedSort>({ columnId: "last", direction: "desc" });
78
+ const columns = useMemo(() => buildSavedColumns(width), [width]);
79
+ const rows = useMemo(() => [...searches].sort((left, right) => {
80
+ const a = sortValue(left, sort.columnId);
81
+ const b = sortValue(right, sort.columnId);
82
+ if (a === b) return 0;
83
+ return (a < b ? -1 : 1) * (sort.direction === "asc" ? 1 : -1);
84
+ }), [searches, sort]);
85
+
86
+ const renderCell = useCallback((
87
+ search: CloudSavedSearch,
88
+ column: SavedColumn,
89
+ _index: number,
90
+ rowState: { selected: boolean },
91
+ ): DataTableCell => {
92
+ const selectedColor = rowState.selected ? colors.selectedText : undefined;
93
+ switch (column.id) {
94
+ case "name":
95
+ return { text: search.name || search.query, color: selectedColor ?? colors.text };
96
+ case "filters":
97
+ return {
98
+ text: describeFilters(search.filters) || "\u2014",
99
+ color: selectedColor ?? colors.textDim,
100
+ };
101
+ case "alert":
102
+ return {
103
+ text: search.alertEnabled ? "on" : "off",
104
+ color: selectedColor ?? (search.alertEnabled ? colors.positive : colors.textMuted),
105
+ // The cell is the switch, so alerts can be flipped without the keyboard.
106
+ onMouseDown: (event: any) => {
107
+ event.preventDefault?.();
108
+ event.stopPropagation?.();
109
+ onToggleAlert(search);
110
+ },
111
+ };
112
+ case "last":
113
+ return {
114
+ text: search.lastMatchAt ? formatHitDate(search.lastMatchAt) : "\u2014",
115
+ color: selectedColor ?? colors.textDim,
116
+ };
117
+ case "hits":
118
+ return {
119
+ text: search.matchCount > 0 ? String(search.matchCount) : "\u2014",
120
+ color: selectedColor ?? colors.textDim,
121
+ };
122
+ }
123
+ }, [onToggleAlert]);
124
+
125
+ const handleKeyDown = useCallback((event: DataTableKeyEvent) => {
126
+ const selected = searches.find((search) => search.id === selectedId);
127
+ if (!selected) return false;
128
+ if (isPlainKey(event, "a")) {
129
+ event.preventDefault?.();
130
+ event.stopPropagation?.();
131
+ onToggleAlert(selected);
132
+ return true;
133
+ }
134
+ if (isPlainKey(event, "d")) {
135
+ event.preventDefault?.();
136
+ event.stopPropagation?.();
137
+ onDelete(selected);
138
+ return true;
139
+ }
140
+ return false;
141
+ }, [onDelete, onToggleAlert, searches, selectedId]);
142
+
143
+ return (
144
+ <DataTableView<CloudSavedSearch, SavedColumn>
145
+ focused={focused}
146
+ rootWidth={width}
147
+ rootHeight={height}
148
+ columns={columns}
149
+ items={rows}
150
+ selection={{
151
+ kind: "id",
152
+ selectedId,
153
+ getId: (search) => search.id,
154
+ onChange: onSelect,
155
+ }}
156
+ onActivate={onRun}
157
+ onRootKeyDown={handleKeyDown}
158
+ sortColumnId={sort.columnId}
159
+ sortDirection={sort.direction}
160
+ onHeaderClick={(columnId) => setSort((current) => (
161
+ current.columnId === columnId
162
+ ? { columnId, direction: current.direction === "asc" ? "desc" : "asc" }
163
+ : { columnId: columnId as SavedColumnId, direction: columnId === "name" ? "asc" : "desc" }
164
+ ))}
165
+ getItemKey={(search) => search.id}
166
+ renderCell={renderCell}
167
+ showHorizontalScrollbar={false}
168
+ emptyStateTitle={emptyTitle}
169
+ />
170
+ );
171
+ }
@@ -0,0 +1,35 @@
1
+ import { Box, Text, TextAttributes } from "../../../ui";
2
+ import { colors } from "../../../theme/colors";
3
+ import type { SnippetSegment } from "./snippet";
4
+
5
+ /**
6
+ * Renders one line of text where matched terms are styled instead of shown as
7
+ * literal `<mark>` tags. Marked runs are foreground-only so a selected table row
8
+ * keeps its own background.
9
+ */
10
+ export function SnippetText({
11
+ segments,
12
+ color = colors.text,
13
+ dimColor,
14
+ backgroundColor,
15
+ }: {
16
+ segments: readonly SnippetSegment[];
17
+ color?: string;
18
+ /** Overrides the unmatched-run color, e.g. to dim context around a match. */
19
+ dimColor?: string;
20
+ backgroundColor?: string;
21
+ }) {
22
+ return (
23
+ <Box flexDirection="row" height={1} overflow="hidden" backgroundColor={backgroundColor}>
24
+ {segments.map((segment, index) => (
25
+ <Text
26
+ key={index}
27
+ fg={segment.marked ? colors.warning : dimColor ?? color}
28
+ attributes={segment.marked ? TextAttributes.BOLD : TextAttributes.NONE}
29
+ >
30
+ {segment.text}
31
+ </Text>
32
+ ))}
33
+ </Box>
34
+ );
35
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ highlightTerms,
4
+ parseMarkedSnippet,
5
+ snippetMatchTerms,
6
+ snippetPlainText,
7
+ truncateSegments,
8
+ } from "./snippet";
9
+
10
+ describe("parseMarkedSnippet", () => {
11
+ test("splits matched runs and joins fragments on one line", () => {
12
+ expect(parseMarkedSnippet("we saw <mark>margin</mark> pressure \u2026 and\nmore <mark>margin</mark> talk")).toEqual([
13
+ { text: "we saw ", marked: false },
14
+ { text: "margin", marked: true },
15
+ { text: " pressure \u2026 and more ", marked: false },
16
+ { text: "margin", marked: true },
17
+ { text: " talk", marked: false },
18
+ ]);
19
+ });
20
+
21
+ test("keeps text visible when tags are unbalanced", () => {
22
+ expect(parseMarkedSnippet("cut off at <mark>guidance")).toEqual([
23
+ { text: "cut off at ", marked: false },
24
+ { text: "guidance", marked: true },
25
+ ]);
26
+ expect(parseMarkedSnippet("stray</mark> closer")).toEqual([
27
+ { text: "stray closer", marked: false },
28
+ ]);
29
+ });
30
+
31
+ test("treats nested marks as one highlight", () => {
32
+ expect(parseMarkedSnippet("<mark>free <mark>cash</mark> flow</mark> grew")).toEqual([
33
+ { text: "free cash flow", marked: true },
34
+ { text: " grew", marked: false },
35
+ ]);
36
+ });
37
+
38
+ test("decodes entities once, so escaped markup stays literal", () => {
39
+ expect(snippetPlainText("AT&amp;T and &amp;lt;mark&amp;gt; and &#39;quoted&#39;")).toBe(
40
+ "AT&T and &lt;mark&gt; and 'quoted'",
41
+ );
42
+ });
43
+
44
+ test("collects distinct matched terms", () => {
45
+ expect(snippetMatchTerms("<mark>Margin</mark> and <mark>margin</mark> and <mark>FCF</mark>"))
46
+ .toEqual(["margin", "fcf"]);
47
+ });
48
+ });
49
+
50
+ describe("truncateSegments", () => {
51
+ test("clips inside the run that overflows and marks the cut", () => {
52
+ const segments = parseMarkedSnippet("alpha <mark>bravo</mark> charlie");
53
+ expect(truncateSegments(segments, 9)).toEqual([
54
+ { text: "alpha ", marked: false },
55
+ { text: "br", marked: true },
56
+ { text: "\u2026", marked: false },
57
+ ]);
58
+ });
59
+
60
+ test("returns the segments untouched when they already fit", () => {
61
+ const segments = parseMarkedSnippet("<mark>fits</mark>");
62
+ expect(truncateSegments(segments, 10)).toBe(segments);
63
+ });
64
+ });
65
+
66
+ describe("highlightTerms", () => {
67
+ test("matches case-insensitively and prefers the longest term", () => {
68
+ expect(highlightTerms("Free cash flow and cash", ["cash", "free cash flow"])).toEqual([
69
+ { text: "Free cash flow", marked: true },
70
+ { text: " and ", marked: false },
71
+ { text: "cash", marked: true },
72
+ ]);
73
+ });
74
+
75
+ test("leaves text intact when no term is usable", () => {
76
+ expect(highlightTerms("plain body", [])).toEqual([{ text: "plain body", marked: false }]);
77
+ });
78
+ });
@@ -0,0 +1,134 @@
1
+ import { truncateTextSegments } from "../../../utils/format";
2
+
3
+ export interface SnippetSegment {
4
+ text: string;
5
+ /** Inside a `<mark>` the server put around a matched term. */
6
+ marked: boolean;
7
+ }
8
+
9
+ const MARK_TAG = /<(\/?)mark\s*>/gi;
10
+ const ENTITY = /&(?:#(\d+)|#x([0-9a-f]+)|(amp|lt|gt|quot|apos|nbsp));/gi;
11
+
12
+ const NAMED_ENTITIES: Record<string, string> = {
13
+ amp: "&",
14
+ lt: "<",
15
+ gt: ">",
16
+ quot: "\"",
17
+ apos: "'",
18
+ nbsp: " ",
19
+ };
20
+
21
+ /**
22
+ * Decodes in one pass so `&amp;lt;` yields the literal `&lt;` rather than being
23
+ * decoded twice into `<`.
24
+ */
25
+ function decodeEntities(value: string): string {
26
+ return value.replace(ENTITY, (match, decimal?: string, hex?: string, name?: string) => {
27
+ if (decimal) return codePoint(Number.parseInt(decimal, 10)) ?? match;
28
+ if (hex) return codePoint(Number.parseInt(hex, 16)) ?? match;
29
+ return NAMED_ENTITIES[(name ?? "").toLowerCase()] ?? match;
30
+ });
31
+ }
32
+
33
+ function codePoint(value: number): string | null {
34
+ if (!Number.isFinite(value) || value < 0 || value > 0x10ffff) return null;
35
+ try {
36
+ return String.fromCodePoint(value);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /** Snippets are rendered on one line, so every run of whitespace becomes a space. */
43
+ function flatten(value: string): string {
44
+ return value.replace(/\s+/g, " ");
45
+ }
46
+
47
+ function pushSegment(segments: SnippetSegment[], text: string, marked: boolean): void {
48
+ if (!text) return;
49
+ const last = segments[segments.length - 1];
50
+ if (last && last.marked === marked) {
51
+ last.text += text;
52
+ return;
53
+ }
54
+ segments.push({ text, marked });
55
+ }
56
+
57
+ /**
58
+ * Splits a server snippet into plain and matched runs.
59
+ *
60
+ * The server is the only writer of these tags, but a snippet is cut out of
61
+ * arbitrary document text: a truncated fragment can end mid-tag, and an
62
+ * unbalanced `</mark>` must not swallow the words around it. Unclosed marks run
63
+ * to the end, stray closers are dropped, and nested marks stay one highlight.
64
+ */
65
+ export function parseMarkedSnippet(snippet: string): SnippetSegment[] {
66
+ const segments: SnippetSegment[] = [];
67
+ let depth = 0;
68
+ let cursor = 0;
69
+
70
+ MARK_TAG.lastIndex = 0;
71
+ for (let match = MARK_TAG.exec(snippet); match; match = MARK_TAG.exec(snippet)) {
72
+ pushSegment(segments, flatten(decodeEntities(snippet.slice(cursor, match.index))), depth > 0);
73
+ depth = match[1] === "/" ? Math.max(0, depth - 1) : depth + 1;
74
+ cursor = match.index + match[0].length;
75
+ }
76
+ pushSegment(segments, flatten(decodeEntities(snippet.slice(cursor))), depth > 0);
77
+
78
+ const first = segments[0];
79
+ if (first) first.text = first.text.replace(/^ +/, "");
80
+ const last = segments[segments.length - 1];
81
+ if (last) last.text = last.text.replace(/ +$/, "");
82
+ return segments.filter((segment) => segment.text.length > 0);
83
+ }
84
+
85
+ export function snippetPlainText(snippet: string): string {
86
+ return parseMarkedSnippet(snippet).map((segment) => segment.text).join("");
87
+ }
88
+
89
+ /** Distinct matched terms, so a document view can highlight them outside the snippet. */
90
+ export function snippetMatchTerms(snippet: string): string[] {
91
+ const terms = new Set<string>();
92
+ for (const segment of parseMarkedSnippet(snippet)) {
93
+ const term = segment.marked ? segment.text.trim() : "";
94
+ if (term) terms.add(term.toLowerCase());
95
+ }
96
+ return [...terms];
97
+ }
98
+
99
+ /**
100
+ * Fits segments into a cell. Table cells normally get `fitTableCellText`, but a
101
+ * highlighted snippet is many text nodes, so the clipping happens here instead.
102
+ */
103
+ export function truncateSegments(segments: SnippetSegment[], width: number): SnippetSegment[] {
104
+ return truncateTextSegments(segments, width, (ellipsis) => ({ text: ellipsis, marked: false }));
105
+ }
106
+
107
+ /** Splits plain text on the matched terms so a full chunk can show the same highlights. */
108
+ export function highlightTerms(text: string, terms: readonly string[]): SnippetSegment[] {
109
+ const usable = terms.filter((term) => term.length > 0);
110
+ if (usable.length === 0) return [{ text, marked: false }];
111
+
112
+ const pattern = new RegExp(
113
+ `(${usable.map(escapeRegExp).sort((a, b) => b.length - a.length).join("|")})`,
114
+ "gi",
115
+ );
116
+ const segments: SnippetSegment[] = [];
117
+ let cursor = 0;
118
+ for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
119
+ // A zero-width match would loop forever on the same index.
120
+ if (match[0].length === 0) {
121
+ pattern.lastIndex += 1;
122
+ continue;
123
+ }
124
+ pushSegment(segments, text.slice(cursor, match.index), false);
125
+ pushSegment(segments, match[0], true);
126
+ cursor = match.index + match[0].length;
127
+ }
128
+ pushSegment(segments, text.slice(cursor), false);
129
+ return segments.filter((segment) => segment.text.length > 0);
130
+ }
131
+
132
+ function escapeRegExp(value: string): string {
133
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
134
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { ScannerFlowEvent } from "../../../api-client";
3
- import { DEFAULT_FLOW_FILTERS, filterFlowEvents, type FlowFilters } from "./flow-model";
3
+ import { DEFAULT_FLOW_FILTERS, filterFlowEvents, flowEmptyState, type FlowFilters } from "./flow-model";
4
4
 
5
5
  const NOW = Date.parse("2026-03-02T15:00:00Z");
6
6
 
@@ -83,3 +83,13 @@ describe("client-side flow filters", () => {
83
83
  .toEqual(["today", "in-7"]);
84
84
  });
85
85
  });
86
+
87
+ describe("flow empty state", () => {
88
+ test("blames the filters only when the feed actually sent prints", () => {
89
+ expect(flowEmptyState(12, 0, "live").title).toBe("12 prints hidden by filters.");
90
+ expect(flowEmptyState(1, 0, "live").title).toBe("1 print hidden by filters.");
91
+ // An empty tape is not a filter problem, whatever the filters are set to.
92
+ expect(flowEmptyState(0, 0, "live").title).toBe("No prints on the tape yet.");
93
+ expect(flowEmptyState(0, 0, "closed").hint).toContain("next session");
94
+ });
95
+ });
@@ -1,4 +1,4 @@
1
- import type { ScannerFlowEvent } from "../../../api-client";
1
+ import type { ScannerFlowEvent, ScannerStatus } from "../../../api-client";
2
2
 
3
3
  export type FlowMinPremium = "50000" | "250000" | "1000000";
4
4
  export type FlowSide = "calls" | "puts" | "both";
@@ -101,6 +101,34 @@ export function filterFlowEvents(
101
101
  });
102
102
  }
103
103
 
104
+ /**
105
+ * An empty table has two unrelated causes, and blaming the filters for a tape
106
+ * the server never sent sends people to tune filters that were never the
107
+ * problem. Say which one happened.
108
+ */
109
+ export function flowEmptyState(
110
+ received: number,
111
+ visible: number,
112
+ status: ScannerStatus | undefined,
113
+ ): { title: string; hint: string } {
114
+ if (received > visible) {
115
+ return {
116
+ title: `${received - visible} ${received - visible === 1 ? "print" : "prints"} hidden by filters.`,
117
+ hint: "Loosen the premium, expiry, or universe filter.",
118
+ };
119
+ }
120
+ if (status === "closed") {
121
+ return {
122
+ title: "No prints on the tape.",
123
+ hint: "Options are closed; the tape fills again at the next session.",
124
+ };
125
+ }
126
+ return {
127
+ title: "No prints on the tape yet.",
128
+ hint: "Large sweeps, blocks, and premium prints appear here as they cross.",
129
+ };
130
+ }
131
+
104
132
  export function formatFlowPremium(premium: number): string {
105
133
  if (premium >= 1e9) return `$${(premium / 1e9).toFixed(1)}B`;
106
134
  if (premium >= 1e6) return `$${(premium / 1e6).toFixed(1)}M`;
@@ -1,10 +1,11 @@
1
1
  import { useCallback, useMemo, useState } from "react";
2
2
  import { Box, TextAttributes } from "../../../ui";
3
3
  import {
4
- Button,
5
4
  DataTableView,
5
+ SelectButton,
6
6
  type DataTableCell,
7
7
  type DataTableColumn,
8
+ type SelectButtonOption,
8
9
  } from "../../../components";
9
10
  import { ScannerWaitingState } from "./waiting";
10
11
  import { useAppSelector, usePaneSettingValue } from "../../../state/app/context";
@@ -20,6 +21,7 @@ import {
20
21
  DEFAULT_FLOW_FILTERS,
21
22
  FLOW_FILTER_OPTIONS,
22
23
  filterFlowEvents,
24
+ flowEmptyState,
23
25
  formatFlowExpiry,
24
26
  formatFlowPremium,
25
27
  formatFlowSide,
@@ -35,28 +37,33 @@ import {
35
37
  type FlowVolOi,
36
38
  } from "./flow-model";
37
39
 
38
- /** Cycles a select-style pane setting, so every filter is reachable without the settings dialog. */
39
- function FilterChip<T extends string>({
40
+ /** Every filter is reachable from the pane body, not just the settings dialog. */
41
+ function FlowFilter<T extends string>({
42
+ id,
40
43
  label,
41
44
  value,
42
45
  options,
46
+ defaultValue,
43
47
  onChange,
44
48
  }: {
49
+ id: string;
45
50
  label: string;
46
51
  value: T;
47
- options: readonly { value: T; label: string; short: string }[];
52
+ options: readonly SelectButtonOption<T>[];
53
+ defaultValue: T;
48
54
  onChange: (value: T) => void;
49
55
  }) {
50
- const current = options.find((option) => option.value === value) ?? options[0]!;
51
56
  return (
52
- <Button
53
- label={`${label} ${current.short}`}
54
- variant="ghost"
55
- onPress={() => {
56
- const index = options.findIndex((option) => option.value === value);
57
- onChange(options[(index + 1 + options.length) % options.length]!.value);
58
- }}
59
- />
57
+ <Box marginRight={2}>
58
+ <SelectButton
59
+ label={label}
60
+ value={value}
61
+ options={options}
62
+ emphasized={value !== defaultValue}
63
+ onChange={onChange}
64
+ idPrefix={`flow-filter-${id}`}
65
+ />
66
+ </Box>
60
67
  );
61
68
  }
62
69
 
@@ -145,6 +152,11 @@ function FlowPane({ focused, width, height }: PaneProps) {
145
152
  [feed.payload?.events, filters, watchlist],
146
153
  );
147
154
 
155
+ const emptyState = useMemo(
156
+ () => flowEmptyState(feed.payload?.events.length ?? 0, events.length, feed.payload?.status),
157
+ [events.length, feed.payload?.events.length, feed.payload?.status],
158
+ );
159
+
148
160
  useScannerStatusFooter("flow", feed, focused);
149
161
 
150
162
  const columns = useMemo(() => buildColumns(width), [width]);
@@ -160,13 +172,57 @@ function FlowPane({ focused, width, height }: PaneProps) {
160
172
 
161
173
  return (
162
174
  <Box flexDirection="column" width={width} height={height}>
163
- <Box height={1} flexDirection="row" overflow="hidden">
164
- <FilterChip label="Prem" value={minPremium} options={FLOW_FILTER_OPTIONS.minPremium} onChange={setMinPremium} />
165
- <FilterChip label="Side" value={side} options={FLOW_FILTER_OPTIONS.side} onChange={setSide} />
166
- <FilterChip label="Kind" value={kind} options={FLOW_FILTER_OPTIONS.kind} onChange={setKind} />
167
- <FilterChip label="V/OI" value={volOi} options={FLOW_FILTER_OPTIONS.volOi} onChange={setVolOi} />
168
- <FilterChip label="Exp" value={expiry} options={FLOW_FILTER_OPTIONS.expiry} onChange={setExpiry} />
169
- <FilterChip label="Univ" value={universe} options={FLOW_FILTER_OPTIONS.universe} onChange={setUniverse} />
175
+ {/* paddingLeft matches the table's own left inset, so the filter row and
176
+ the TIME column start on the same cell. */}
177
+ <Box height={1} flexDirection="row" overflow="hidden" paddingLeft={1}>
178
+ <FlowFilter
179
+ id="premium"
180
+ label="Prem"
181
+ value={minPremium}
182
+ options={FLOW_FILTER_OPTIONS.minPremium}
183
+ defaultValue={DEFAULT_FLOW_FILTERS.minPremium}
184
+ onChange={setMinPremium}
185
+ />
186
+ <FlowFilter
187
+ id="side"
188
+ label="Side"
189
+ value={side}
190
+ options={FLOW_FILTER_OPTIONS.side}
191
+ defaultValue={DEFAULT_FLOW_FILTERS.side}
192
+ onChange={setSide}
193
+ />
194
+ <FlowFilter
195
+ id="kind"
196
+ label="Kind"
197
+ value={kind}
198
+ options={FLOW_FILTER_OPTIONS.kind}
199
+ defaultValue={DEFAULT_FLOW_FILTERS.kind}
200
+ onChange={setKind}
201
+ />
202
+ <FlowFilter
203
+ id="voloi"
204
+ label="V/OI"
205
+ value={volOi}
206
+ options={FLOW_FILTER_OPTIONS.volOi}
207
+ defaultValue={DEFAULT_FLOW_FILTERS.volOi}
208
+ onChange={setVolOi}
209
+ />
210
+ <FlowFilter
211
+ id="expiry"
212
+ label="Exp"
213
+ value={expiry}
214
+ options={FLOW_FILTER_OPTIONS.expiry}
215
+ defaultValue={DEFAULT_FLOW_FILTERS.expiry}
216
+ onChange={setExpiry}
217
+ />
218
+ <FlowFilter
219
+ id="universe"
220
+ label="Univ"
221
+ value={universe}
222
+ options={FLOW_FILTER_OPTIONS.universe}
223
+ defaultValue={DEFAULT_FLOW_FILTERS.universe}
224
+ onChange={setUniverse}
225
+ />
170
226
  </Box>
171
227
  <DataTableView<ScannerFlowEvent>
172
228
  focused={focused}
@@ -187,8 +243,8 @@ function FlowPane({ focused, width, height }: PaneProps) {
187
243
  onActivate={(event) => pinTicker(event.underlying, { floating: true, paneType: TICKER_RESEARCH_PANE_ID })}
188
244
  renderCell={(event, column, _index, rowState) => renderCell(event, column, rowState)}
189
245
  emptyContent={feed.payload ? undefined : <ScannerWaitingState />}
190
- emptyStateTitle="No prints match these filters."
191
- emptyStateHint="Loosen the premium, expiry, or universe filter."
246
+ emptyStateTitle={emptyState.title}
247
+ emptyStateHint={emptyState.hint}
192
248
  />
193
249
  </Box>
194
250
  );
@@ -72,6 +72,7 @@ export const scannerModule: PluginModule = {
72
72
  defaultPosition: "right",
73
73
  defaultMode: "floating",
74
74
  defaultFloatingSize: { width: 76, height: 26 },
75
+ tableExport: true,
75
76
  settings: hiloSettings(),
76
77
  },
77
78
  {
@@ -82,6 +83,7 @@ export const scannerModule: PluginModule = {
82
83
  defaultPosition: "right",
83
84
  defaultMode: "floating",
84
85
  defaultFloatingSize: { width: 88, height: 28 },
86
+ tableExport: true,
85
87
  settings: flowSettings(),
86
88
  },
87
89
  ],
@@ -355,6 +355,7 @@ export const secModule: PluginModule = {
355
355
  defaultPosition: "right",
356
356
  defaultMode: "floating",
357
357
  defaultFloatingSize: { width: 100, height: 32 },
358
+ tableExport: true,
358
359
  },
359
360
  ],
360
361
 
@@ -320,6 +320,7 @@ export const sectorsModule: PluginModule = {
320
320
  defaultPosition: "right",
321
321
  defaultMode: "floating",
322
322
  defaultFloatingSize: { width: 82, height: 18 },
323
+ tableExport: true,
323
324
  settings: (context) => ({
324
325
  title: "Sector Performance Settings",
325
326
  values: {