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
@@ -21,7 +21,7 @@ export function dedupeById<T extends { id: string }>(items: T[]): T[] {
21
21
  return result;
22
22
  }
23
23
 
24
- export type CommandBarMode = "default" | "search" | "themes" | "plugins" | "layout" | "direct-command";
24
+ export type CommandBarMode = "default" | "search" | "themes" | "layout" | "direct-command";
25
25
 
26
26
  export interface CommandBarModeInfo {
27
27
  kind: CommandBarMode;
@@ -36,6 +36,14 @@ export interface CommandBarSection<T> {
36
36
 
37
37
  export type CommandBarSectionOrder = "default" | "app-first" | "ranked";
38
38
 
39
+ /** Sort positions contributed by plugin search providers, keyed by section heading. */
40
+ export type CommandBarCategoryPriorities = ReadonlyMap<string, number>;
41
+
42
+ export interface CommandBarSectionOptions {
43
+ sectionOrder?: CommandBarSectionOrder;
44
+ categoryPriorities?: CommandBarCategoryPriorities;
45
+ }
46
+
39
47
  export interface CommandBarItemView {
40
48
  id: string;
41
49
  label: string;
@@ -74,8 +82,6 @@ export function resolveCommandBarMode(query: string, commandList?: Command[]): C
74
82
  return { kind: "search", badge: match.prefix, hint: t("Open security details for a ticker") };
75
83
  case "theme":
76
84
  return { kind: "themes", badge: "THEMES", hint: t("Preview with arrows, Enter to save, Esc to revert") };
77
- case "plugins":
78
- return { kind: "plugins", badge: "PLUGINS", hint: t("Toggle plugins without leaving the list") };
79
85
  case "layout":
80
86
  return { kind: "layout", badge: "LAYOUT", hint: t("Organize panes, history, and saved layouts") };
81
87
  default:
@@ -85,9 +91,10 @@ export function resolveCommandBarMode(query: string, commandList?: Command[]): C
85
91
 
86
92
  /**
87
93
  * A section whose every row is disabled or unselectable is an offer, not an
88
- * answer, so it sorts below real matches however its category is prioritized.
94
+ * answer, so it sorts below real matches however its category is prioritized:
95
+ * past the async bands (100 to 200) and the AI's lead, short of danger (900).
89
96
  */
90
- const OFFER_SECTION_DEMOTION = 200;
97
+ const OFFER_SECTION_DEMOTION = 500;
91
98
 
92
99
  interface SectionSortableItem {
93
100
  category: string;
@@ -102,7 +109,7 @@ function isOfferOnlySection<T extends SectionSortableItem>(items: T[]): boolean
102
109
 
103
110
  export function buildSections<T extends SectionSortableItem>(
104
111
  items: T[],
105
- options?: { sectionOrder?: CommandBarSectionOrder },
112
+ options?: CommandBarSectionOptions,
106
113
  ): Array<CommandBarSection<T>> {
107
114
  const sections: Array<CommandBarSection<T>> = [];
108
115
  for (const item of items) {
@@ -116,9 +123,9 @@ export function buildSections<T extends SectionSortableItem>(
116
123
  return sections
117
124
  .map((section, index) => ({ section, index }))
118
125
  .sort((a, b) => {
119
- const leftPriority = getCategoryPriority(a.section.category, options?.sectionOrder)
126
+ const leftPriority = getCategoryPriority(a.section.category, options)
120
127
  + (isOfferOnlySection(a.section.items) ? OFFER_SECTION_DEMOTION : 0);
121
- const rightPriority = getCategoryPriority(b.section.category, options?.sectionOrder)
128
+ const rightPriority = getCategoryPriority(b.section.category, options)
122
129
  + (isOfferOnlySection(b.section.items) ? OFFER_SECTION_DEMOTION : 0);
123
130
  const priorityDiff = leftPriority - rightPriority;
124
131
  return priorityDiff !== 0 ? priorityDiff : a.index - b.index;
@@ -133,8 +140,6 @@ export function getEmptyState(mode: CommandBarMode, query: string, searchQuery?:
133
140
  return { label: t("Type a ticker symbol"), detail: t("Open security details after resolving a ticker") };
134
141
  }
135
142
  return { label: tf('No matches for "{query}"', { query: searchQuery }), detail: t("Try a symbol, company name, or exchange variant") };
136
- case "plugins":
137
- return { label: t("No plugins match"), detail: query.trim() || t("Toggleable plugins will appear here") };
138
143
  case "themes":
139
144
  return { label: t("No themes match"), detail: query.trim() || t("Installed themes will appear here") };
140
145
  case "layout":
@@ -172,12 +177,36 @@ export function truncateText(text: string, width: number): string {
172
177
  return truncateToDisplayWidth(text, width);
173
178
  }
174
179
 
175
- function getCategoryPriority(category: string, sectionOrder: CommandBarSectionOrder = "default"): number {
180
+ /**
181
+ * An exactly matching symbol is the most certain answer the terminal has, so
182
+ * nothing outranks it. Typing "sive" put the AI's "DES SIVE" above the SIVE row
183
+ * it was derived from, which is a guess sitting above the fact behind it.
184
+ */
185
+ const EXACT_MATCH_SECTION_PRIORITY = -150;
186
+ /**
187
+ * The AI leads the rest even though it is the slowest source (~600ms+): it
188
+ * translates the sentence the user typed into commands, which is the answer to
189
+ * what they asked when no symbol matched outright. Its Thinking placeholder
190
+ * reserves the rows from the start, so the arrival replaces a row instead of
191
+ * shifting the list.
192
+ */
193
+ const ASSIST_SECTION_PRIORITY = -100;
194
+ /**
195
+ * The other async sections sit below the local matches in arrival order, so
196
+ * each answer only ever pushes rows below itself: instruments at 100, then news
197
+ * at 190 and the rest of the corpus at 200 (both contributed by their provider).
198
+ */
199
+ const INSTRUMENTS_SECTION_PRIORITY = 100;
200
+
201
+ function getCategoryPriority(category: string, options?: CommandBarSectionOptions): number {
202
+ const contributed = options?.categoryPriorities?.get(category);
203
+ if (contributed !== undefined) return contributed;
204
+ const sectionOrder = options?.sectionOrder ?? "default";
176
205
  const normalized = category.trim().toLowerCase();
177
206
  if (sectionOrder === "ranked") return 0;
178
- // The AI answers the question the user actually typed, so it leads the list.
179
- if (normalized === "ask ai") return -100;
180
- if (normalized === "exact match") return -50;
207
+ if (normalized === "ask ai") return ASSIST_SECTION_PRIORITY;
208
+ if (normalized === "exact match") return EXACT_MATCH_SECTION_PRIORITY;
209
+ if (normalized === "instruments") return INSTRUMENTS_SECTION_PRIORITY;
181
210
  if (sectionOrder === "app-first") {
182
211
  if (normalized === "saved") return 100;
183
212
  if (normalized === "primary listing") return 110;
@@ -36,6 +36,7 @@ function getTickerSearchContext(state: AppState, collectionId: string | null) {
36
36
  const activePortfolio = state.config.portfolios.find((portfolio) => portfolio.id === collectionId);
37
37
  return {
38
38
  preferBroker: true,
39
+ interactive: true,
39
40
  brokerId: activePortfolio?.brokerId,
40
41
  brokerInstanceId: activePortfolio?.brokerInstanceId,
41
42
  };
@@ -39,7 +39,7 @@ interface CommandBarRouteBase {
39
39
 
40
40
  interface CommandBarModeRoute extends CommandBarRouteBase {
41
41
  kind: "mode";
42
- screen: "ticker-search" | "plugins" | "layout";
42
+ screen: "ticker-search" | "layout";
43
43
  query: string;
44
44
  selectedIdx: number;
45
45
  hoveredIdx: number | null;
@@ -0,0 +1,21 @@
1
+ import { expect, test } from "bun:test";
2
+ import { EXCEL_CSV_BOM } from "../../utils/csv";
3
+ import { createDataTableCsv } from "./export";
4
+
5
+ test("exports displayed table cells and skips section headers", () => {
6
+ const items = [
7
+ { type: "section", name: "Americas", value: "" },
8
+ { type: "row", name: "S&P 500", value: "+1.25%" },
9
+ ];
10
+ const csv = createDataTableCsv({
11
+ columns: [
12
+ { id: "name", label: "Name", width: 20, align: "left" },
13
+ { id: "value", label: "Change", width: 10, align: "right" },
14
+ ],
15
+ items,
16
+ renderSectionHeader: (item) => item.type === "section" ? { text: item.name } : null,
17
+ renderCell: (item, column) => ({ text: item[column.id as "name" | "value"] }),
18
+ });
19
+
20
+ expect(csv).toBe(`${EXCEL_CSV_BOM}Name,Change\nS&P 500,'+1.25%`);
21
+ });
@@ -0,0 +1,21 @@
1
+ import type { DataTableColumn, DataTableProps } from "../ui";
2
+ import { serializeCsv } from "../../utils/csv";
3
+
4
+ export function createDataTableCsv<
5
+ T,
6
+ C extends DataTableColumn = DataTableColumn,
7
+ >({
8
+ columns,
9
+ items,
10
+ renderCell,
11
+ renderSectionHeader,
12
+ }: Pick<DataTableProps<T, C>, "columns" | "items" | "renderCell" | "renderSectionHeader">): string {
13
+ const rows = items.flatMap((item, index) => {
14
+ if (renderSectionHeader?.(item, index)) return [];
15
+ return [columns.map((column) => renderCell(item, column, index, { selected: false }).text)];
16
+ });
17
+ return serializeCsv([
18
+ columns.map((column) => column.label),
19
+ ...rows,
20
+ ], { excelCompatible: true });
21
+ }
@@ -43,7 +43,7 @@ afterEach(async () => {
43
43
  testSetup = undefined;
44
44
  });
45
45
 
46
- function Harness() {
46
+ function Harness({ onCursor = () => {} }: { onCursor?: () => void }) {
47
47
  const [selectedIndex, setSelectedIndex] = useState(1);
48
48
  const [cursorIndex, setCursorIndex] = useState(1);
49
49
  const [activatedTitle, setActivatedTitle] = useState("");
@@ -64,7 +64,10 @@ function Harness() {
64
64
  selectedIndex,
65
65
  onChange: (index) => setSelectedIndex(index),
66
66
  }}
67
- onCursorChange={(_row, index) => setCursorIndex(index)}
67
+ onCursorChange={(_row, index) => {
68
+ onCursor();
69
+ setCursorIndex(index);
70
+ }}
68
71
  onActivate={(row) => {
69
72
  if (row.type === "row") setActivatedTitle(row.title);
70
73
  }}
@@ -122,6 +125,7 @@ function LargeSelectionHarness({
122
125
  return { text: row.title + (index === 500 ? "" : "") };
123
126
  }}
124
127
  emptyStateTitle="No rows"
128
+ scrollToIndex={500}
125
129
  />
126
130
  </PaneInstanceProvider>
127
131
  </AppContext>
@@ -228,6 +232,20 @@ describe("DataTableView", () => {
228
232
  expect(testSetup.captureCharFrame()).toContain("cursor=Second row selected=First row activated=First row");
229
233
  });
230
234
 
235
+ test("does no cursor or scroll work when navigation is already at an edge", async () => {
236
+ let cursorChanges = 0;
237
+ testSetup = await testRender(
238
+ <Harness onCursor={() => { cursorChanges += 1; }} />,
239
+ { width: 60, height: 12 },
240
+ );
241
+
242
+ await renderSettled();
243
+ await emitKeypress({ name: "up", sequence: "\u001B[A" });
244
+ await renderSettled();
245
+
246
+ expect(cursorChanges).toBe(0);
247
+ });
248
+
231
249
  test("keeps selection current across repeated keypresses before the next render", async () => {
232
250
  testSetup = await testRender(<Harness />, { width: 60, height: 12 });
233
251
 
@@ -242,19 +260,40 @@ describe("DataTableView", () => {
242
260
  expect(testSetup.captureCharFrame()).toContain("selected=Third row activated=Third row");
243
261
  });
244
262
 
245
- test("does not scan every row when the selected index is explicit", async () => {
246
- let isSelectedCalls = 0;
263
+ test("keeps the immediate cursor visible while a controlled selection is deferred", async () => {
264
+ testSetup = await testRender(
265
+ <LargeSelectionHarness onIsSelected={() => {}} />,
266
+ { width: 60, height: 12 },
267
+ );
268
+
269
+ await renderSettled();
270
+ await emitKeypressBatch(Array.from({ length: 30 }, () => ({
271
+ name: "down",
272
+ sequence: "\u001B[B",
273
+ })));
274
+ await renderSettled();
275
+
276
+ expect(testSetup.captureCharFrame()).toContain("Row 530");
277
+ });
278
+
279
+ test("renders only the visible rows and the rows changed by navigation", async () => {
280
+ let renderedCells = 0;
247
281
  testSetup = await testRender(
248
282
  <LargeSelectionHarness
249
283
  onIsSelected={() => {
250
- isSelectedCalls += 1;
284
+ renderedCells += 1;
251
285
  }}
252
286
  />,
253
287
  { width: 60, height: 12 },
254
288
  );
255
289
 
290
+ await renderSettled();
291
+ expect(renderedCells).toBeLessThan(150);
292
+
293
+ const beforeNavigation = renderedCells;
294
+ await emitKeypress({ name: "down", sequence: "\u001B[B" });
256
295
  await renderSettled();
257
296
 
258
- expect(isSelectedCalls).toBeLessThan(150);
297
+ expect(renderedCells - beforeNavigation).toBeLessThanOrEqual(4);
259
298
  });
260
299
  });
@@ -80,6 +80,11 @@ export interface DataTableViewProps<
80
80
  focused?: boolean;
81
81
  selection: DataTableSelection<T>;
82
82
  onActivate?: (item: T, index: number) => void;
83
+ /**
84
+ * Fires immediately as the lightweight visual cursor moves. Expensive detail
85
+ * work and persisted selection belong in selection.onChange, which is
86
+ * coalesced across keyboard repeats.
87
+ */
83
88
  onCursorChange?: (
84
89
  item: T,
85
90
  index: number,
@@ -198,20 +203,54 @@ export function DataTableView<
198
203
  effectiveSelectedIndexRef.current = effectiveSelectedIndex;
199
204
  const [selectionScrollVersion, setSelectionScrollVersion] = useState(0);
200
205
  const [selectionScrollTarget, setSelectionScrollTarget] = useState<number | null>(null);
206
+ const selectionScrollTargetRef = useRef<number | null>(null);
201
207
  const lastExternalSelectionScrollRef = useRef<{
202
208
  kind: DataTableSelection<T>["kind"];
203
209
  key: string | number | null;
204
210
  resolved: boolean;
205
211
  } | null>(null);
206
- const effectiveScrollToIndex = scrollToIndex !== undefined
212
+ const lastControlledScrollRequestRef = useRef({
213
+ index: scrollToIndex,
214
+ version: scrollToIndexVersion,
215
+ });
216
+ const controlledScrollRequestChanged = scrollToIndex !== undefined
217
+ && (
218
+ lastControlledScrollRequestRef.current.index !== scrollToIndex
219
+ || lastControlledScrollRequestRef.current.version !== scrollToIndexVersion
220
+ );
221
+ const effectiveScrollToIndex = controlledScrollRequestChanged
207
222
  ? scrollToIndex
208
- : selectionScrollTarget;
223
+ : selectionScrollTarget ?? scrollToIndex;
209
224
 
210
225
  const requestSelectionScroll = useCallback((index: number) => {
211
- if (scrollToIndex !== undefined || index < 0) return;
226
+ if (index < 0 || selectionScrollTargetRef.current === index) return;
227
+ const scrollBox = effectiveScrollRef.current;
228
+ const viewportHeight = scrollBox?.viewport?.height;
229
+ const scrollTop = scrollBox?.scrollTop;
230
+ if (
231
+ typeof viewportHeight === "number"
232
+ && typeof scrollTop === "number"
233
+ && index >= Math.floor(scrollTop)
234
+ && index < Math.floor(scrollTop) + Math.max(1, Math.floor(viewportHeight))
235
+ ) return;
236
+ selectionScrollTargetRef.current = index;
212
237
  setSelectionScrollTarget(index);
213
238
  setSelectionScrollVersion((current) => current + 1);
214
- }, [scrollToIndex]);
239
+ }, [effectiveScrollRef]);
240
+ const clearSelectionScrollTarget = useCallback(() => {
241
+ if (selectionScrollTargetRef.current === null) return;
242
+ selectionScrollTargetRef.current = null;
243
+ setSelectionScrollTarget(null);
244
+ }, []);
245
+
246
+ useEffect(() => {
247
+ lastControlledScrollRequestRef.current = {
248
+ index: scrollToIndex,
249
+ version: scrollToIndexVersion,
250
+ };
251
+ if (scrollToIndex === undefined) return;
252
+ clearSelectionScrollTarget();
253
+ }, [clearSelectionScrollTarget, scrollToIndex, scrollToIndexVersion]);
215
254
 
216
255
  const clearPendingCommit = useCallback(() => {
217
256
  if (pendingCommitTimerRef.current) {
@@ -220,7 +259,8 @@ export function DataTableView<
220
259
  }
221
260
  pendingCommitRef.current = false;
222
261
  pendingCommitTargetRef.current = null;
223
- }, []);
262
+ clearSelectionScrollTarget();
263
+ }, [clearSelectionScrollTarget]);
224
264
 
225
265
  useEffect(() => {
226
266
  if (selection.kind === "none") {
@@ -268,7 +308,7 @@ export function DataTableView<
268
308
  lastExternalSelectionScrollRef.current = current;
269
309
 
270
310
  if (selection.kind === "none") {
271
- if (scrollToIndex === undefined) setSelectionScrollTarget(null);
311
+ clearSelectionScrollTarget();
272
312
  return;
273
313
  }
274
314
  if (effectiveSelectedIndex < 0) return;
@@ -281,6 +321,7 @@ export function DataTableView<
281
321
  requestSelectionScroll(effectiveSelectedIndex);
282
322
  }
283
323
  }, [
324
+ clearSelectionScrollTarget,
284
325
  effectiveSelectedIndex,
285
326
  requestSelectionScroll,
286
327
  scrollToIndex,
@@ -360,9 +401,10 @@ export function DataTableView<
360
401
  pendingCommitRef.current = false;
361
402
  const pendingTarget = pendingCommitTargetRef.current;
362
403
  pendingCommitTargetRef.current = null;
404
+ clearSelectionScrollTarget();
363
405
  commitTarget(pendingTarget, "keyboard");
364
406
  }, DATA_TABLE_SELECTION_COMMIT_DELAY_MS);
365
- }, [commitTarget, getCommitTarget, selection.kind]);
407
+ }, [clearSelectionScrollTarget, commitTarget, getCommitTarget, selection.kind]);
366
408
 
367
409
  const updateCursorIndex = useCallback((
368
410
  index: number,
@@ -375,13 +417,17 @@ export function DataTableView<
375
417
  if (index < 0 || index >= tableProps.items.length) return;
376
418
  const item = tableProps.items[index]!;
377
419
  if (isNavigable && !isNavigable(item, index)) return;
378
- effectiveSelectedIndexRef.current = index;
379
- setCursorIndex(index);
380
420
  const reason = options.reason ?? (options.commit === "deferred"
381
421
  ? "keyboard"
382
422
  : options.commit === "immediate"
383
423
  ? "pointer"
384
424
  : "keyboard");
425
+ if (effectiveSelectedIndexRef.current === index) {
426
+ if (options.commit === "immediate") commitIndexImmediately(index, reason);
427
+ return;
428
+ }
429
+ effectiveSelectedIndexRef.current = index;
430
+ setCursorIndex(index);
385
431
  onCursorChange?.(item, index, reason);
386
432
  if (options.commit === "deferred") {
387
433
  requestSelectionScroll(index);
@@ -1,6 +1,6 @@
1
1
 
2
2
  export { PriceSelectorDialog } from "./price-selector-dialog";
3
- export { StaticChartSurface } from "./chart/static/chart/surface";
3
+ export { StaticChartSurface } from "./chart/static";
4
4
  export {
5
5
  buildMetricTreemapNavigationTiles,
6
6
  findMetricTreemapNeighbor,
@@ -13,10 +13,15 @@ export type { SpeedometerSegment } from "./speedometer-gauge";
13
13
  export { TickerListTableView } from "./ticker/list-table-view";
14
14
  export type { TickerListVisibleRange } from "./ticker/list-table-view";
15
15
  export { TickerBadgeList } from "./ticker/badge/list";
16
+ export { TickerBadgeText } from "./ticker/badge/text";
16
17
  export { InputSearchBar } from "./input-search-bar";
17
18
  export { isTableScrollNearEnd, useTableLoadMore } from "./table-view-shared";
18
19
  export { DataTableView } from "./data-table/view";
19
- export type { DataTableKeyEvent, DataTableRootKeyContext } from "./data-table/view";
20
+ export type {
21
+ DataTableKeyEvent,
22
+ DataTableRootKeyContext,
23
+ DataTableSelectionChangeReason,
24
+ } from "./data-table/view";
20
25
  export { DataTableStackView } from "./data-table/stack-view";
21
26
  export { FeedDataTableStackView } from "./feed-data-table/stack-view";
22
27
  export type { FeedDataTableItem } from "./feed-data-table/stack-view";
@@ -47,6 +52,9 @@ export { EmptyState, PaneStatusBody, loadingText, unavailableText } from "./ui/s
47
52
  export { getMessageComposerBlockHeight, MessageComposer } from "./ui/message-composer";
48
53
  export { NumberField, TextField } from "./ui/fields";
49
54
  export { SegmentedControl } from "./ui/toggle";
55
+ export { SelectButton } from "./ui/select-button";
56
+ export type { SelectButtonOption, SelectButtonProps } from "./ui/select-button";
50
57
  export { Spinner } from "./ui/loading";
58
+ export { RemoteImage } from "./ui";
51
59
  export { Tabs } from "./ui/tabs";
52
60
  export { usePaneTicker } from "../state/app/context";
@@ -21,9 +21,13 @@ afterEach(async () => {
21
21
  function Harness({
22
22
  actions,
23
23
  onNavigateDown,
24
+ onBlur,
25
+ onQueryChange,
24
26
  }: {
25
27
  actions: AppAction[];
26
28
  onNavigateDown?: () => void;
29
+ onBlur?: () => void;
30
+ onQueryChange?: (query: string) => void;
27
31
  }) {
28
32
  const state = createInitialState(createDefaultConfig("/tmp/gloomberb-input-search-bar"));
29
33
  const inputRef = useRef<InputRenderable | null>(null);
@@ -46,8 +50,8 @@ function Harness({
46
50
  debounceMs={100}
47
51
  onNavigateDown={onNavigateDown}
48
52
  onFocus={() => {}}
49
- onBlur={() => {}}
50
- onQueryChange={() => {}}
53
+ onBlur={onBlur ?? (() => {})}
54
+ onQueryChange={onQueryChange ?? (() => {})}
51
55
  />
52
56
  </AppContext>
53
57
  );
@@ -76,6 +80,36 @@ describe("InputSearchBar", () => {
76
80
  ]);
77
81
  });
78
82
 
83
+ test("Escape clears the query and releases the field", async () => {
84
+ // The input consumes every key while focused, so without this there is no
85
+ // way out of a search once it is entered.
86
+ const actions: AppAction[] = [];
87
+ const queries: string[] = [];
88
+ let blurred = false;
89
+
90
+ testSetup = await testRender(
91
+ <Harness
92
+ actions={actions}
93
+ onBlur={() => {
94
+ blurred = true;
95
+ }}
96
+ onQueryChange={(query) => queries.push(query)}
97
+ />,
98
+ { width: 40, height: 4 },
99
+ );
100
+ await act(async () => {
101
+ await testSetup!.renderOnce();
102
+ });
103
+
104
+ await act(async () => {
105
+ testSetup!.renderer.keyInput.emit("keypress", { name: "escape" });
106
+ await testSetup!.renderOnce();
107
+ });
108
+
109
+ expect(queries).toEqual([""]);
110
+ expect(blurred).toBe(true);
111
+ });
112
+
79
113
  test("moves from the active search input to the table with Down", async () => {
80
114
  const actions: AppAction[] = [];
81
115
  let navigatedDown = 0;
@@ -14,6 +14,7 @@ export function InputSearchBar({
14
14
  inputRef,
15
15
  placeholder,
16
16
  debounceMs,
17
+ glyph = "/",
17
18
  normalizeValue = identity,
18
19
  onNavigateDown,
19
20
  onFocus,
@@ -28,6 +29,8 @@ export function InputSearchBar({
28
29
  inputRef: RefObject<InputRenderable | null>;
29
30
  placeholder: string;
30
31
  debounceMs: number;
32
+ /** Leading marker; override when a pane shows more than one field. */
33
+ glyph?: string;
31
34
  normalizeValue?: (value: string) => string;
32
35
  onNavigateDown?: () => void;
33
36
  onFocus: () => void;
@@ -47,6 +50,20 @@ export function InputSearchBar({
47
50
  phase: "before",
48
51
  });
49
52
 
53
+ // A focused input consumes every key, so Escape has to be intercepted before
54
+ // it reaches the field. Without this there is no way back out of a search.
55
+ useShortcut((event) => {
56
+ if (event.name !== "escape") return;
57
+ stopSearchFocusNavigation(event);
58
+ setDraft("");
59
+ onQueryChange("");
60
+ onBlur();
61
+ }, {
62
+ allowEditable: true,
63
+ enabled: focused && active,
64
+ phase: "before",
65
+ });
66
+
50
67
  useEffect(() => {
51
68
  setDraft(value);
52
69
  }, [value]);
@@ -81,7 +98,7 @@ export function InputSearchBar({
81
98
  inputRef.current?.focus?.();
82
99
  }}
83
100
  >
84
- <Text fg={active ? colors.textBright : colors.textDim}>/</Text>
101
+ <Text fg={active ? colors.textBright : colors.textDim}>{glyph}</Text>
85
102
  <Box width={1} />
86
103
  <Input
87
104
  ref={inputRef}
@@ -2,34 +2,81 @@ import { afterEach, expect, test } from "bun:test";
2
2
  import { testRender } from "../../renderers/opentui/test-utils";
3
3
  import { AppContext, createInitialState } from "../../state/app/context";
4
4
  import { createDefaultConfig } from "../../types/config";
5
- import { VERSION } from "../../version";
6
5
  import { act } from "react";
7
6
  import { Header } from "./header";
7
+ import { publishCommandBarPrompt } from "../command-bar/panel/prompt-binding";
8
8
 
9
9
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
10
10
 
11
11
  afterEach(() => {
12
+ publishCommandBarPrompt(null);
12
13
  testSetup?.renderer.destroy();
13
14
  testSetup = undefined;
14
15
  });
15
16
 
16
- test("opens the current version changelog from the header", async () => {
17
- let openedVersion = "";
17
+ test("opens the command bar by clicking the header prompt", async () => {
18
18
  const state = createInitialState(createDefaultConfig("/tmp/gloomberb-header-test"));
19
+ const actions: Array<{ type: string; open?: boolean; query?: string }> = [];
19
20
  testSetup = await testRender(
20
- <AppContext value={{ state, dispatch: () => {} }}>
21
- <Header onOpenChangelog={(version) => { openedVersion = version; }} />
21
+ <AppContext value={{ state, dispatch: (action) => actions.push(action as { type: string }) }}>
22
+ <Header />
22
23
  </AppContext>,
23
- { width: 100, height: 1 },
24
+ { width: 120, height: 1 },
24
25
  );
25
26
 
26
27
  await testSetup.renderOnce();
27
- const versionX = testSetup.captureCharFrame().indexOf(`v${VERSION}`);
28
- expect(versionX).toBeGreaterThanOrEqual(0);
28
+ const promptX = testSetup.captureCharFrame().indexOf(">");
29
+ expect(promptX).toBeGreaterThanOrEqual(0);
30
+
31
+ await act(async () => {
32
+ await testSetup!.mockMouse.click(promptX + 2, 0);
33
+ await testSetup!.renderOnce();
34
+ });
35
+
36
+ expect(actions).toContainEqual({ type: "SET_COMMAND_BAR", open: true, query: "" });
37
+ });
38
+
39
+ /**
40
+ * While the bar is open the header prompt is its input: what the panel
41
+ * publishes is what the user types into, and a screen without a binding (a
42
+ * workflow with its own fields) must not put a focused input in the header.
43
+ */
44
+ test("hosts the command bar input while a list screen is published", async () => {
45
+ const state = {
46
+ ...createInitialState(createDefaultConfig("/tmp/gloomberb-header-test")),
47
+ commandBarOpen: true,
48
+ };
49
+ const typed: string[] = [];
50
+ testSetup = await testRender(
51
+ <AppContext value={{ state, dispatch: () => {} }}>
52
+ <Header />
53
+ </AppContext>,
54
+ { width: 120, height: 1 },
55
+ );
56
+ await testSetup.renderOnce();
57
+ expect(testSetup.captureCharFrame()).not.toContain("Search or run a command");
58
+
59
+ await act(async () => {
60
+ publishCommandBarPrompt({
61
+ screenKey: "root:Commands",
62
+ query: "QQ",
63
+ placeholder: "Command or plain English…",
64
+ ghostSuffix: " AAPL",
65
+ onQueryChange: (query) => typed.push(query),
66
+ });
67
+ await testSetup!.renderOnce();
68
+ });
69
+ expect(testSetup.captureCharFrame()).toContain("QQ AAPL");
70
+
71
+ await act(async () => {
72
+ await testSetup!.mockInput.typeText("x");
73
+ await testSetup!.renderOnce();
74
+ });
75
+ expect(typed).toEqual(["QQx"]);
29
76
 
30
77
  await act(async () => {
31
- await testSetup!.mockMouse.click(versionX + 1, 0);
78
+ publishCommandBarPrompt(null);
32
79
  await testSetup!.renderOnce();
33
80
  });
34
- expect(openedVersion).toBe(VERSION);
81
+ expect(testSetup.captureCharFrame()).not.toContain("QQ");
35
82
  });