gloomberb 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (489) hide show
  1. package/README.md +6 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +103 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +129 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +196 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/cli/commands/plugins.ts +39 -11
  20. package/src/cli/desktop-pane-shot.ts +285 -16
  21. package/src/cli/index.ts +1 -1
  22. package/src/cli/pane-functions/capabilities.ts +58 -28
  23. package/src/cli/pane-functions/catalog.ts +16 -4
  24. package/src/cli/pane-functions/discovery.ts +1 -0
  25. package/src/cli/pane-functions/headless.test.ts +247 -0
  26. package/src/cli/pane-functions/headless.ts +423 -0
  27. package/src/cli/pane-functions/index.test.ts +9 -0
  28. package/src/cli/pane-functions/index.ts +6 -2
  29. package/src/cli/pane-functions/options.ts +27 -2
  30. package/src/cli/pane-functions/report.ts +4 -0
  31. package/src/cli/pane-functions/resolver.ts +26 -8
  32. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  33. package/src/cli/pane-functions/screenshot.ts +236 -20
  34. package/src/cli/restore-plugins.ts +34 -0
  35. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  36. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  37. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  38. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  39. package/src/components/chart/composite/composite-chart.tsx +301 -204
  40. package/src/components/chart/composite/format.ts +8 -2
  41. package/src/components/chart/composite/interactions.test.ts +68 -102
  42. package/src/components/chart/composite/interactions.ts +214 -264
  43. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  44. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  45. package/src/components/chart/composite/scene.ts +57 -3
  46. package/src/components/chart/composite/text-renderer.ts +3 -2
  47. package/src/components/chart/composite/time-scale.ts +138 -57
  48. package/src/components/chart/composite/types.ts +25 -1
  49. package/src/components/chart/core/data.ts +9 -13
  50. package/src/components/chart/core/date-window.ts +7 -1
  51. package/src/components/chart/core/pointer.ts +3 -0
  52. package/src/components/chart/core/types.ts +0 -45
  53. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  54. package/src/components/chart/native/kitty/index.test.ts +0 -188
  55. package/src/components/chart/native/raster/crosshair.ts +6 -5
  56. package/src/components/chart/native/raster/primitives.ts +0 -23
  57. package/src/components/chart/native/renderer-selection.ts +81 -31
  58. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  59. package/src/components/chart/static/chart-surface.tsx +150 -0
  60. package/src/components/chart/static/index.ts +8 -0
  61. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  62. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  63. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  64. package/src/components/chart/static/series.ts +49 -0
  65. package/src/components/command-bar/action-runtime.ts +1 -3
  66. package/src/components/command-bar/assist/model.test.ts +17 -2
  67. package/src/components/command-bar/assist/model.ts +40 -9
  68. package/src/components/command-bar/commands/direct/index.ts +1 -1
  69. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  70. package/src/components/command-bar/commands/registry.ts +0 -11
  71. package/src/components/command-bar/helpers.test.ts +2 -2
  72. package/src/components/command-bar/helpers.ts +2 -6
  73. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  74. package/src/components/command-bar/list/badge.test.ts +54 -0
  75. package/src/components/command-bar/list/badge.ts +62 -0
  76. package/src/components/command-bar/list/model.test.ts +103 -0
  77. package/src/components/command-bar/list/model.ts +61 -4
  78. package/src/components/command-bar/list/view.test.tsx +113 -0
  79. package/src/components/command-bar/list/view.tsx +147 -131
  80. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  81. package/src/components/command-bar/panel/body.tsx +51 -65
  82. package/src/components/command-bar/panel/index.tsx +48 -13
  83. package/src/components/command-bar/panel/layout.test.ts +139 -0
  84. package/src/components/command-bar/panel/layout.ts +65 -28
  85. package/src/components/command-bar/panel/native-surface.ts +24 -0
  86. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  87. package/src/components/command-bar/panel/runtime.ts +6 -3
  88. package/src/components/command-bar/panel/state.ts +41 -7
  89. package/src/components/command-bar/panel/types.ts +7 -1
  90. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  91. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  92. package/src/components/command-bar/routes/root/results.ts +30 -28
  93. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  94. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  95. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  96. package/src/components/command-bar/routes/root/selection.ts +1 -12
  97. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  98. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  99. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  100. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  101. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  102. package/src/components/command-bar/routing/actions.ts +1 -81
  103. package/src/components/command-bar/routing/launch-request.ts +1 -1
  104. package/src/components/command-bar/routing/list-state.ts +6 -21
  105. package/src/components/command-bar/selection-runtime.ts +1 -1
  106. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  107. package/src/components/command-bar/surface/environment.ts +8 -1
  108. package/src/components/command-bar/surface/index.test.tsx +84 -68
  109. package/src/components/command-bar/surface/index.tsx +33 -36
  110. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  111. package/src/components/command-bar/theme-picker.tsx +43 -15
  112. package/src/components/command-bar/view-model.test.ts +53 -8
  113. package/src/components/command-bar/view-model.ts +43 -14
  114. package/src/components/command-bar/workflow/tickers.ts +1 -0
  115. package/src/components/command-bar/workflow/types.ts +1 -1
  116. package/src/components/data-table/view.test.tsx +45 -6
  117. package/src/components/data-table/view.tsx +55 -9
  118. package/src/components/index.ts +10 -2
  119. package/src/components/input-search-bar.test.tsx +36 -2
  120. package/src/components/input-search-bar.tsx +18 -1
  121. package/src/components/layout/header.test.tsx +57 -10
  122. package/src/components/layout/header.tsx +292 -149
  123. package/src/components/layout/market-summary.test.ts +81 -0
  124. package/src/components/layout/market-summary.ts +109 -0
  125. package/src/components/layout/shell/chrome.ts +96 -1
  126. package/src/components/layout/shell/index.tsx +11 -3
  127. package/src/components/layout/shell/menu.ts +9 -0
  128. package/src/components/layout/shell/pane/actions.ts +29 -0
  129. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  130. package/src/components/layout/shell/shortcuts.ts +3 -0
  131. package/src/components/layout/status-bar.test.tsx +9 -8
  132. package/src/components/layout/status-bar.tsx +85 -54
  133. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  134. package/src/components/price-sparkline/index.ts +61 -34
  135. package/src/components/price-sparkline/view.tsx +1 -1
  136. package/src/components/sign-in-gate.test.tsx +128 -0
  137. package/src/components/sign-in-gate.tsx +149 -0
  138. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  139. package/src/components/ticker/badge/list.tsx +17 -3
  140. package/src/components/ticker/list-table-view.tsx +6 -3
  141. package/src/components/ui/data-table/index.tsx +3 -2
  142. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  143. package/src/components/ui/frame.tsx +28 -0
  144. package/src/components/ui/index.ts +3 -0
  145. package/src/components/ui/native-select.tsx +20 -10
  146. package/src/components/ui/select-button.tsx +138 -0
  147. package/src/components/ui/status.tsx +2 -2
  148. package/src/components/ui/table-layout.ts +1 -1
  149. package/src/components/use-double-click-activation.ts +7 -3
  150. package/src/core/app-services.ts +3 -3
  151. package/src/data/config/layout.ts +12 -6
  152. package/src/data/config/store/index.test.ts +97 -0
  153. package/src/data/config/store/migrations.ts +41 -0
  154. package/src/data/config/store/normalize.ts +5 -3
  155. package/src/i18n/es.ts +6 -2
  156. package/src/i18n/ja.ts +6 -2
  157. package/src/i18n/ko.ts +6 -2
  158. package/src/i18n/zh-cn.ts +6 -2
  159. package/src/i18n/zh-tw.ts +6 -2
  160. package/src/layout-marketplace/pane-imagery.ts +1 -0
  161. package/src/layout-marketplace/payload.test.ts +17 -0
  162. package/src/layout-marketplace/payload.ts +40 -5
  163. package/src/market-data/coordinator/index.ts +3 -2
  164. package/src/market-data/coordinator/quotes.ts +173 -66
  165. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  166. package/src/pane-settings.ts +1 -1
  167. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  168. package/src/plugins/builtin/analytics/view.tsx +1 -1
  169. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  170. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  171. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  172. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  173. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  174. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  175. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  176. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  177. package/src/plugins/builtin/composite-plugins.ts +14 -1
  178. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  179. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  180. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  181. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  182. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  183. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  184. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  185. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  186. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  187. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  188. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  189. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  190. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  191. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  192. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  193. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  194. package/src/plugins/builtin/econ/index.tsx +2 -1
  195. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  196. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  197. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  198. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  199. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  200. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  201. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  202. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  203. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  204. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  205. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  206. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  207. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  208. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  209. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  210. package/src/plugins/builtin/help/index.tsx +4 -0
  211. package/src/plugins/builtin/holders/index.tsx +1 -0
  212. package/src/plugins/builtin/insider/index.tsx +1 -0
  213. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  214. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  215. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  216. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  217. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  218. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  219. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  220. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  221. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  222. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  223. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  224. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  225. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  226. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  227. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  228. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  229. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  230. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  231. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  232. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  233. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  234. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  235. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  236. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  237. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  238. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  239. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  240. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  241. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  242. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  243. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  244. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  245. package/src/plugins/builtin/options/index.tsx +1 -0
  246. package/src/plugins/builtin/options/view.test.tsx +29 -1
  247. package/src/plugins/builtin/options/view.tsx +1 -22
  248. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  249. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  250. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  251. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  252. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  253. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  254. package/src/plugins/builtin/plugin-module.ts +9 -1
  255. package/src/plugins/builtin/research/index.tsx +4 -0
  256. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  257. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  258. package/src/plugins/builtin/research-search/data.ts +109 -0
  259. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  260. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  261. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  262. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  263. package/src/plugins/builtin/research-search/index.tsx +85 -0
  264. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  265. package/src/plugins/builtin/research-search/model.ts +291 -0
  266. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  267. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  268. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  269. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  270. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  271. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  272. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  273. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  274. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  275. package/src/plugins/builtin/sec/index.tsx +1 -0
  276. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  277. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  278. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  279. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  280. package/src/plugins/builtin/ticker-detail/index.tsx +0 -14
  281. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  282. package/src/plugins/builtin/volatility/index.tsx +1 -1
  283. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  284. package/src/plugins/builtin/world-venue-map/map.tsx +213 -69
  285. package/src/plugins/builtin/world-venue-map/model.test.ts +32 -0
  286. package/src/plugins/builtin/world-venue-map/model.ts +134 -13
  287. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  288. package/src/plugins/bundle.test.ts +109 -0
  289. package/src/plugins/bundle.ts +139 -0
  290. package/src/plugins/catalog-browser.test.ts +1 -0
  291. package/src/plugins/catalog-browser.ts +10 -0
  292. package/src/plugins/catalog-manifest.test.ts +43 -0
  293. package/src/plugins/catalog-manifest.ts +92 -0
  294. package/src/plugins/catalog-ui.ts +20 -4
  295. package/src/plugins/catalog.ts +9 -2
  296. package/src/plugins/current-target.ts +28 -0
  297. package/src/plugins/host-contract.ts +37 -0
  298. package/src/plugins/host-link.ts +158 -0
  299. package/src/plugins/host-modules.ts +61 -0
  300. package/src/plugins/loader.ts +41 -22
  301. package/src/plugins/ownership.ts +1 -0
  302. package/src/plugins/prediction-markets/index.tsx +1 -0
  303. package/src/plugins/registry/context.ts +3 -0
  304. package/src/plugins/registry/contributions.ts +27 -0
  305. package/src/plugins/registry/index.test.ts +25 -0
  306. package/src/plugins/registry/index.ts +8 -0
  307. package/src/plugins/registry/pane-settings.ts +6 -13
  308. package/src/plugins/seed.test.ts +83 -0
  309. package/src/plugins/seed.ts +80 -0
  310. package/src/public/broker.ts +31 -0
  311. package/src/public/market-data.ts +9 -0
  312. package/src/public/pane-hooks.ts +57 -0
  313. package/src/public/public-api.test.ts +285 -0
  314. package/src/public/react.ts +62 -0
  315. package/src/public/test-support.ts +23 -0
  316. package/src/public/time-series.ts +8 -0
  317. package/src/public/utils.ts +77 -0
  318. package/src/remote/layout-helpers.ts +6 -8
  319. package/src/remote/resources.ts +2 -0
  320. package/src/remote/schema.ts +1 -0
  321. package/src/remote/semantic-tree.test.tsx +105 -0
  322. package/src/remote/semantic-tree.tsx +53 -35
  323. package/src/renderers/browser/main.tsx +6 -0
  324. package/src/renderers/cloudflare/worker.test.ts +19 -0
  325. package/src/renderers/cloudflare/worker.ts +1 -1
  326. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  327. package/src/renderers/electrobun/bun/index.ts +5 -2
  328. package/src/renderers/electrobun/shared/protocol.ts +21 -0
  329. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  330. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  331. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  332. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  333. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  334. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  335. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  336. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  337. package/src/renderers/electrobun/view/main.tsx +27 -2
  338. package/src/renderers/electrobun/view/styles.css +5 -0
  339. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  340. package/src/renderers/opentui/chart-surface.tsx +5 -1
  341. package/src/renderers/opentui/host.tsx +16 -2
  342. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  343. package/src/renderers/opentui/interaction-performance.ts +188 -0
  344. package/src/renderers/opentui/start.tsx +26 -1
  345. package/src/renderers/opentui/test-utils.tsx +13 -0
  346. package/src/renderers/opentui/toast-host.tsx +26 -2
  347. package/src/renderers/opentui/ui-host.tsx +1 -0
  348. package/src/sources/provider-router/search.test.ts +191 -0
  349. package/src/sources/provider-router/search.ts +92 -39
  350. package/src/state/app/context/index.tsx +2 -0
  351. package/src/state/app/context/selector.test.tsx +33 -1
  352. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  353. package/src/state/hooks/quote-streaming.ts +106 -30
  354. package/src/state/pane-table-export-registry.ts +19 -0
  355. package/src/theme/color-utils.ts +1 -1
  356. package/src/theme/colors.ts +50 -0
  357. package/src/theme/themes.test.ts +15 -2
  358. package/src/theme/themes.ts +19 -1
  359. package/src/tickers/search/index.test.ts +26 -0
  360. package/src/tickers/search/index.ts +42 -17
  361. package/src/tickers/search/ranking.ts +14 -0
  362. package/src/time-series/chart-data.test.ts +216 -0
  363. package/src/time-series/resolution.test.ts +25 -26
  364. package/src/time-series/resolution.ts +41 -10
  365. package/src/time-series/resolve.ts +7 -0
  366. package/src/time-series/types.ts +3 -1
  367. package/src/time-series/use-chart-resolution.ts +5 -0
  368. package/src/types/config.ts +123 -3
  369. package/src/types/data-provider.ts +11 -0
  370. package/src/types/headless.ts +161 -0
  371. package/src/types/plugin.ts +102 -0
  372. package/src/ui/host.tsx +12 -1
  373. package/src/ui/index.tsx +1 -0
  374. package/src/ui/toast.tsx +8 -4
  375. package/src/utils/format.ts +35 -0
  376. package/src/utils/fuzzy-search.test.ts +94 -0
  377. package/src/utils/fuzzy-search.ts +81 -22
  378. package/src/utils/shortcut-labels.ts +14 -0
  379. package/src/components/chart/core/axis-format.ts +0 -157
  380. package/src/components/chart/core/index.test.ts +0 -1075
  381. package/src/components/chart/core/projection.ts +0 -357
  382. package/src/components/chart/core/render-utils.ts +0 -4
  383. package/src/components/chart/core/renderer.ts +0 -244
  384. package/src/components/chart/core/scene.ts +0 -180
  385. package/src/components/chart/core/time-axis.ts +0 -330
  386. package/src/components/chart/cursor-motion.ts +0 -47
  387. package/src/components/chart/indicators/types.ts +0 -26
  388. package/src/components/chart/market-session.ts +0 -176
  389. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  390. package/src/components/chart/multi-line/renderer.ts +0 -161
  391. package/src/components/chart/multi-line/scene.ts +0 -188
  392. package/src/components/chart/native/raster/layout.ts +0 -129
  393. package/src/components/chart/native/raster/price-chart.ts +0 -244
  394. package/src/components/chart/rendering/chart-draw.ts +0 -270
  395. package/src/components/chart/rendering/geometry.ts +0 -95
  396. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  397. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  398. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  399. package/src/components/chart/static/chart/surface.tsx +0 -358
  400. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  401. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  402. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  403. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  404. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  405. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  406. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  407. package/src/plugins/builtin/substack/api/store.ts +0 -186
  408. package/src/plugins/builtin/substack/api/types.ts +0 -42
  409. package/src/plugins/builtin/substack/api.test.ts +0 -298
  410. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  411. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  412. package/src/plugins/builtin/substack/content.ts +0 -478
  413. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  414. package/src/plugins/builtin/substack/index.tsx +0 -49
  415. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  416. package/src/plugins/builtin/substack/model.test.ts +0 -155
  417. package/src/plugins/builtin/substack/normalize.ts +0 -343
  418. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  419. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  420. package/src/plugins/builtin/substack/pane.tsx +0 -547
  421. package/src/plugins/builtin/substack/read-state.ts +0 -30
  422. package/src/plugins/builtin/substack/table.ts +0 -111
  423. package/src/plugins/builtin/substack/types.ts +0 -71
  424. package/src/plugins/builtin/substack/utils.ts +0 -131
  425. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  426. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  427. package/src/plugins/ibkr/account-cache.ts +0 -24
  428. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  429. package/src/plugins/ibkr/config.test.ts +0 -79
  430. package/src/plugins/ibkr/config.ts +0 -250
  431. package/src/plugins/ibkr/dialogs.tsx +0 -40
  432. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  433. package/src/plugins/ibkr/flex/index.ts +0 -367
  434. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  435. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  436. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  437. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  438. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  439. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  440. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  441. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  442. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  443. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  444. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  445. package/src/plugins/ibkr/gateway/history.ts +0 -120
  446. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  447. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  448. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  449. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  450. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  451. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  452. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  453. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  454. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  455. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  456. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  457. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  458. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  459. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  460. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  461. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  462. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  463. package/src/plugins/ibkr/gateway/types.ts +0 -27
  464. package/src/plugins/ibkr/index.test.tsx +0 -162
  465. package/src/plugins/ibkr/index.tsx +0 -159
  466. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  467. package/src/plugins/ibkr/instance-selection.ts +0 -36
  468. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  469. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  470. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  471. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  472. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  473. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  474. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  475. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  476. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  477. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  478. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  479. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  480. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  481. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  482. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  483. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  484. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  485. package/src/plugins/ibkr/trade/utils.ts +0 -144
  486. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  487. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  488. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  489. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -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;
@@ -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
  });