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
@@ -1,4 +1,5 @@
1
1
  /** @jsxImportSource react */
2
+ import { setCurrentPluginTarget } from "../../../plugins/current-target";
2
3
  import { createRoot } from "react-dom/client";
3
4
  import { App } from "../../../app";
4
5
  import { applyLanguageFromConfig } from "../../../i18n";
@@ -30,7 +31,13 @@ import { createDesktopDeepLinkBridge } from "./desktop-deeplink-bridge";
30
31
  import { createDesktopWindowBridge } from "./desktop/window/bridge";
31
32
  import { prepareDetachedSnapshot } from "./desktop/window/snapshot";
32
33
  import { createElectrobunAppServices } from "./app-services";
33
- import { getRendererBuiltinPlugins } from "../../../plugins/catalog-ui";
34
+ import { getRendererPlugins } from "../../../plugins/catalog-ui";
35
+ import { loadDesktopExternalPlugins } from "./external-plugins";
36
+ import { setPluginInstaller } from "../../../plugins/builtin/plugin-marketplace/store";
37
+
38
+ // Declared here rather than sniffed: the desktop view and the hosted browser
39
+ // app are both browser contexts but differ in what plugins may do.
40
+ setCurrentPluginTarget("desktop");
34
41
 
35
42
  const rootElement = document.getElementById("root");
36
43
  if (!rootElement) {
@@ -100,6 +107,23 @@ async function boot() {
100
107
  const desktopApplicationMenuBridge = createApplicationMenuBridge();
101
108
  const desktopDeepLinkBridge = createDesktopDeepLinkBridge();
102
109
  const webUiHost = createWebUiHost(init.desktopPlatform);
110
+ // Compiled by the Bun process, which owns the filesystem. A failure here must
111
+ // not stop the app from starting: the marketplace reports broken plugins, and
112
+ // the built-in catalog is enough to run on.
113
+ const externalPlugins = await measurePerfAsync(
114
+ "startup.electrobun.load-external-plugins",
115
+ async () => {
116
+ try {
117
+ return await loadDesktopExternalPlugins(await backendRequest("plugins.listExternal"));
118
+ } catch (error) {
119
+ debugLog.createLogger("desktop-plugins").error(`External plugin load failed: ${error}`);
120
+ return [];
121
+ }
122
+ },
123
+ );
124
+
125
+ setPluginInstaller((ref) => backendRequest("plugins.install", { ref }));
126
+
103
127
  const remoteControlAdapter = init.windowKind === "main"
104
128
  ? { registerHandler: setElectrobunRemoteRequestHandler }
105
129
  : undefined;
@@ -113,7 +137,8 @@ async function boot() {
113
137
  <App
114
138
  config={config}
115
139
  servicesFactory={createElectrobunAppServices}
116
- plugins={getRendererBuiltinPlugins()}
140
+ externalPlugins={externalPlugins}
141
+ plugins={getRendererPlugins(externalPlugins)}
117
142
  desktopWindowBridge={desktopWindowBridge}
118
143
  desktopApplicationMenuBridge={desktopApplicationMenuBridge}
119
144
  desktopDeepLinkBridge={desktopDeepLinkBridge}
@@ -504,6 +504,11 @@ body.gloom-dragging [data-gloom-role="chart-surface"] {
504
504
  border-radius: 4px;
505
505
  color: var(--gloom-text-bright);
506
506
  }
507
+ /* Secondary reads as the quieter option next to the primary action. */
508
+ .gloom-toast-action-secondary {
509
+ border-color: var(--gloom-border);
510
+ color: var(--gloom-text-dim);
511
+ }
507
512
  .gloom-toast-dismiss {
508
513
  display: grid;
509
514
  width: 22px;
@@ -11,6 +11,7 @@ interface ToastEntry {
11
11
  title?: string;
12
12
  subtitle?: string;
13
13
  action?: ToastOptions["action"];
14
+ secondaryAction?: ToastOptions["secondaryAction"];
14
15
  }
15
16
 
16
17
  let nextToastId = 1;
@@ -31,6 +32,7 @@ export function WebToastHostProvider({ children }: { children: ReactNode }) {
31
32
  title: options?.title,
32
33
  subtitle: options?.subtitle,
33
34
  action: options?.action,
35
+ secondaryAction: options?.secondaryAction,
34
36
  }]);
35
37
  if (options?.duration !== 0) {
36
38
  setTimeout(() => dismiss(id), options?.duration ?? 4500);
@@ -82,6 +84,22 @@ export function WebToastHostProvider({ children }: { children: ReactNode }) {
82
84
  {toast.action.label}
83
85
  </button>
84
86
  )}
87
+ {toast.secondaryAction && (
88
+ <button
89
+ type="button"
90
+ className="gloom-toast-action gloom-toast-action-secondary"
91
+ onClick={(event) => {
92
+ event.stopPropagation();
93
+ try {
94
+ toast.secondaryAction?.onClick();
95
+ } finally {
96
+ dismiss(toast.id);
97
+ }
98
+ }}
99
+ >
100
+ {toast.secondaryAction.label}
101
+ </button>
102
+ )}
85
103
  <button
86
104
  type="button"
87
105
  className="gloom-toast-dismiss"
@@ -61,6 +61,9 @@ export const webRendererHost: RendererHost = {
61
61
  return backendRequest("host.readText");
62
62
  }
63
63
  },
64
+ async saveTextFile({ name, text, mimeType }) {
65
+ return backendRequest("host.saveTextFile", { name, text, mimeType });
66
+ },
64
67
  notify(notification) {
65
68
  void backendRequest("host.notify", {
66
69
  title: notification.title,
@@ -200,7 +200,9 @@ export const OpenTuiChartSurface = forwardRef<unknown, ChartSurfaceProps>(functi
200
200
  const size = computeBitmapSize(rect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
201
201
  const strips = renderCrosshairStrips({
202
202
  pixelX: scaleLocalPixelCoordinate(nativeCrosshair.pixelX, nativeBitmap.width, size.pixelWidth) ?? 0,
203
- pixelY: scaleLocalPixelCoordinate(nativeCrosshair.pixelY, nativeBitmap.height, size.pixelHeight) ?? 0,
203
+ pixelY: nativeCrosshair.pixelY === null
204
+ ? null
205
+ : scaleLocalPixelCoordinate(nativeCrosshair.pixelY, nativeBitmap.height, size.pixelHeight) ?? 0,
204
206
  pixelWidth: size.pixelWidth,
205
207
  pixelHeight: size.pixelHeight,
206
208
  cols: rect.width,
@@ -218,6 +220,8 @@ export const OpenTuiChartSurface = forwardRef<unknown, ChartSurfaceProps>(functi
218
220
  return;
219
221
  }
220
222
 
223
+ // A column-only cursor has no level strip; drop the stale one if it existed.
224
+ for (const id of crosshairSurfaceIds.slice(strips.length)) nativeSurfaceManager.removeSurface(id);
221
225
  strips.forEach((strip, index) => {
222
226
  const id = crosshairSurfaceIds[index];
223
227
  if (!id) return;
@@ -1,12 +1,14 @@
1
1
  import { createCliRenderer, type CliRenderer } from "@opentui/core";
2
2
  import { createRoot, useKeyboard, useTerminalDimensions } from "@opentui/react";
3
- import type { ReactNode } from "react";
3
+ import { Profiler, type ReactNode } from "react";
4
4
  import { resetTerminalInputState } from "../../utils/terminal-input-reset";
5
5
  import type { KeyEventLike } from "../../react/input";
6
6
  import type { NativeRendererHost, PixelResolution, RendererHost } from "../../ui/host";
7
7
  import { colors } from "../../theme/colors";
8
8
  import { safeExternalUrl } from "../../utils/external-url";
9
9
  import { createTerminalMediaReaper, terminalMediaStateFile } from "./terminal-media";
10
+ import { saveTextFileToDownloads } from "../../utils/save-text-file";
11
+ import { installInteractionPerformanceRecorder } from "./interaction-performance";
10
12
 
11
13
  export { useKeyboard, useTerminalDimensions };
12
14
 
@@ -96,6 +98,8 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
96
98
  });
97
99
  const root = createRoot(renderer);
98
100
  installResolutionEventBridge(renderer);
101
+ const stopInteractionPerformanceRecorder = installInteractionPerformanceRecorder(renderer);
102
+ renderer.once("destroy", stopInteractionPerformanceRecorder);
99
103
 
100
104
  const rendererHost: RendererHost = {
101
105
  requestExit: () => renderer.destroy(),
@@ -134,6 +138,9 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
134
138
  });
135
139
  return await new Response(proc.stdout).text();
136
140
  },
141
+ async saveTextFile({ name, text }) {
142
+ return saveTextFileToDownloads(name, text);
143
+ },
137
144
  notify() {
138
145
  // The app-level notifier still owns toast/desktop notification behavior.
139
146
  },
@@ -241,7 +248,18 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
241
248
  renderer,
242
249
  rendererHost,
243
250
  nativeRenderer,
244
- render: (node) => root.render(node),
251
+ render: (node) => root.render(
252
+ stopInteractionPerformanceRecorder.enabled
253
+ ? (
254
+ <Profiler
255
+ id="interaction-performance"
256
+ onRender={stopInteractionPerformanceRecorder.markCommit}
257
+ >
258
+ {node}
259
+ </Profiler>
260
+ )
261
+ : node,
262
+ ),
245
263
  destroy: () => renderer.destroy(),
246
264
  };
247
265
  }
@@ -0,0 +1,79 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { EventEmitter } from "events";
3
+ import { mkdtempSync, readFileSync, rmSync } from "fs";
4
+ import { tmpdir } from "os";
5
+ import { join } from "path";
6
+ import type { CliRenderer } from "@opentui/core";
7
+ import {
8
+ installInteractionPerformanceRecorder,
9
+ summarizeInteractionPerformance,
10
+ type InteractionPerformanceSample,
11
+ } from "./interaction-performance";
12
+
13
+ const temporaryDirectories: string[] = [];
14
+
15
+ afterEach(() => {
16
+ for (const directory of temporaryDirectories.splice(0)) {
17
+ rmSync(directory, { recursive: true, force: true });
18
+ }
19
+ });
20
+
21
+ function sample(latencyMs: number): InteractionPerformanceSample {
22
+ return {
23
+ id: latencyMs,
24
+ key: "down",
25
+ frameId: 1,
26
+ inputsInFrame: 1,
27
+ latencyMs,
28
+ frameCallbackMs: 0,
29
+ cellsUpdated: 0,
30
+ rssBytes: 0,
31
+ };
32
+ }
33
+
34
+ test("summarizes key-to-frame latency with nearest-rank percentiles", () => {
35
+ const samples = Array.from({ length: 20 }, (_, index) => sample(index + 1));
36
+ expect(summarizeInteractionPerformance(samples)).toEqual({
37
+ count: 20,
38
+ p50Ms: 10,
39
+ p95Ms: 19,
40
+ maxMs: 20,
41
+ });
42
+ });
43
+
44
+ test("waits for the key's React commit before attributing a renderer frame", () => {
45
+ const directory = mkdtempSync(join(tmpdir(), "gloomberb-interaction-performance-"));
46
+ temporaryDirectories.push(directory);
47
+ const output = join(directory, "report.json");
48
+ const rendererEvents = new EventEmitter();
49
+ const keyInput = new EventEmitter();
50
+ const renderer = Object.assign(rendererEvents, {
51
+ keyInput,
52
+ setGatherStats: () => {},
53
+ getStats: () => ({ frameCallbackTime: 1.25, cellsUpdated: 42 }),
54
+ }) as unknown as CliRenderer;
55
+ const recorder = installInteractionPerformanceRecorder(renderer, output);
56
+
57
+ keyInput.emit("keypress", { name: "down" });
58
+ keyInput.emit("keypress", { name: "up" });
59
+ rendererEvents.emit("frame", { frameId: 10 });
60
+ recorder.markCommit();
61
+ rendererEvents.emit("frame", { frameId: 11 });
62
+ recorder();
63
+
64
+ const report = JSON.parse(readFileSync(output, "utf8")) as {
65
+ unframedInputCount: number;
66
+ samples: InteractionPerformanceSample[];
67
+ };
68
+ expect(report.unframedInputCount).toBe(0);
69
+ expect(report.samples).toHaveLength(2);
70
+ expect(report.samples.map((entry) => entry.key)).toEqual(["down", "up"]);
71
+ for (const entry of report.samples) {
72
+ expect(entry).toMatchObject({
73
+ frameId: 11,
74
+ inputsInFrame: 2,
75
+ frameCallbackMs: 1.25,
76
+ cellsUpdated: 42,
77
+ });
78
+ }
79
+ });
@@ -0,0 +1,188 @@
1
+ import { mkdirSync, writeFileSync } from "fs";
2
+ import { dirname, resolve } from "path";
3
+ import type { CliRenderer, CliRendererStats } from "@opentui/core";
4
+
5
+ const SAFE_INTERACTION_KEYS = new Set([
6
+ "up",
7
+ "down",
8
+ "left",
9
+ "right",
10
+ "pageup",
11
+ "pagedown",
12
+ "home",
13
+ "end",
14
+ "enter",
15
+ "return",
16
+ "tab",
17
+ "escape",
18
+ ]);
19
+
20
+ interface PendingInteraction {
21
+ id: number;
22
+ key: string;
23
+ startedAtMs: number;
24
+ }
25
+
26
+ export interface InteractionPerformanceSample {
27
+ id: number;
28
+ key: string;
29
+ frameId: number;
30
+ inputsInFrame: number;
31
+ latencyMs: number;
32
+ frameCallbackMs: number;
33
+ /** Renderer-wide count shared by every input completed in this frame. */
34
+ cellsUpdated: number;
35
+ rssBytes: number;
36
+ }
37
+
38
+ export interface InteractionPerformanceSummary {
39
+ count: number;
40
+ p50Ms: number;
41
+ p95Ms: number;
42
+ maxMs: number;
43
+ }
44
+
45
+ export interface InteractionPerformanceRecorder {
46
+ (): void;
47
+ readonly enabled: boolean;
48
+ markCommit(): void;
49
+ }
50
+
51
+ function round(value: number): number {
52
+ return Math.round(value * 100) / 100;
53
+ }
54
+
55
+ function percentile(sorted: readonly number[], quantile: number): number {
56
+ if (sorted.length === 0) return 0;
57
+ const index = Math.max(0, Math.ceil(sorted.length * quantile) - 1);
58
+ return sorted[Math.min(index, sorted.length - 1)]!;
59
+ }
60
+
61
+ export function summarizeInteractionPerformance(
62
+ samples: readonly InteractionPerformanceSample[],
63
+ ): InteractionPerformanceSummary {
64
+ const latencies = samples.map((sample) => sample.latencyMs).sort((left, right) => left - right);
65
+ return {
66
+ count: latencies.length,
67
+ p50Ms: round(percentile(latencies, 0.5)),
68
+ p95Ms: round(percentile(latencies, 0.95)),
69
+ maxMs: round(latencies.at(-1) ?? 0),
70
+ };
71
+ }
72
+
73
+ function statsForFrame(renderer: CliRenderer): CliRendererStats {
74
+ try {
75
+ return renderer.getStats();
76
+ } catch {
77
+ return {
78
+ fps: 0,
79
+ frameCount: 0,
80
+ frameTimes: [],
81
+ averageFrameTime: 0,
82
+ minFrameTime: 0,
83
+ maxFrameTime: 0,
84
+ frameCallbackTime: 0,
85
+ nativeLastFrameTime: 0,
86
+ nativeAverageFrameTime: 0,
87
+ nativeFrameCount: 0,
88
+ cellsUpdated: 0,
89
+ averageCellsUpdated: 0,
90
+ };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Opt-in key-to-frame telemetry for repeatable tmux benchmarks. The recorder
96
+ * ignores printable keys, buffers in memory, and writes once during renderer
97
+ * shutdown so measurement does not add filesystem work to the hot path.
98
+ */
99
+ export function installInteractionPerformanceRecorder(
100
+ renderer: CliRenderer,
101
+ outputPath = process.env.GLOOMBERB_INTERACTION_PERF,
102
+ ): InteractionPerformanceRecorder {
103
+ if (!outputPath) {
104
+ const stop = (() => {}) as InteractionPerformanceRecorder;
105
+ Object.defineProperties(stop, {
106
+ enabled: { value: false },
107
+ markCommit: { value: () => {} },
108
+ });
109
+ return stop;
110
+ }
111
+
112
+ const startedAt = new Date().toISOString();
113
+ const pending: PendingInteraction[] = [];
114
+ const samples: InteractionPerformanceSample[] = [];
115
+ let committedCount = 0;
116
+ let nextId = 1;
117
+ let stopped = false;
118
+
119
+ renderer.setGatherStats(true);
120
+
121
+ const onKeypress = (event: { name?: string }) => {
122
+ const key = event.name?.toLowerCase() ?? "";
123
+ if (!SAFE_INTERACTION_KEYS.has(key)) return;
124
+ pending.push({ id: nextId++, key, startedAtMs: performance.now() });
125
+ };
126
+ const onFrame = (event: { frameId: number }) => {
127
+ if (committedCount === 0) return;
128
+ const completedAtMs = performance.now();
129
+ const stats = statsForFrame(renderer);
130
+ const rssBytes = process.memoryUsage.rss();
131
+ const completed = pending.splice(0, committedCount);
132
+ committedCount = 0;
133
+ for (const interaction of completed) {
134
+ samples.push({
135
+ id: interaction.id,
136
+ key: interaction.key,
137
+ frameId: event.frameId,
138
+ inputsInFrame: completed.length,
139
+ latencyMs: round(completedAtMs - interaction.startedAtMs),
140
+ frameCallbackMs: round(stats.frameCallbackTime),
141
+ cellsUpdated: stats.cellsUpdated,
142
+ rssBytes,
143
+ });
144
+ }
145
+ };
146
+
147
+ renderer.keyInput.on("keypress", onKeypress);
148
+ renderer.on("frame", onFrame);
149
+
150
+ const stop = (() => {
151
+ if (stopped) return;
152
+ stopped = true;
153
+ renderer.keyInput.off("keypress", onKeypress);
154
+ renderer.off("frame", onFrame);
155
+
156
+ const byKey = Object.fromEntries(
157
+ [...new Set(samples.map((sample) => sample.key))]
158
+ .sort()
159
+ .map((key) => [
160
+ key,
161
+ summarizeInteractionPerformance(samples.filter((sample) => sample.key === key)),
162
+ ]),
163
+ );
164
+ const report = {
165
+ version: 2,
166
+ startedAt,
167
+ endedAt: new Date().toISOString(),
168
+ summary: summarizeInteractionPerformance(samples),
169
+ byKey,
170
+ unframedInputCount: pending.length,
171
+ samples,
172
+ };
173
+ const target = resolve(outputPath);
174
+ mkdirSync(dirname(target), { recursive: true });
175
+ writeFileSync(target, `${JSON.stringify(report, null, 2)}\n`, "utf8");
176
+ }) as InteractionPerformanceRecorder;
177
+ Object.defineProperties(stop, {
178
+ enabled: { value: true },
179
+ markCommit: {
180
+ value: () => {
181
+ // A React Profiler callback runs after host mutations commit. Only
182
+ // inputs present by then may be completed by the following frame.
183
+ committedCount = pending.length;
184
+ },
185
+ },
186
+ });
187
+ return stop;
188
+ }
@@ -6,6 +6,9 @@ import { getDataDir, initDataDir, setConfigStoreHost } from "../../data/config/s
6
6
  import { applyLanguageFromConfig } from "../../i18n";
7
7
  import * as nodeConfigStoreHost from "../../data/config/store/node";
8
8
  import { loadExternalPlugins } from "../../plugins/loader";
9
+ import { restoreExtractedPlugins } from "../../cli/restore-plugins";
10
+ import { setPluginInstaller } from "../../plugins/builtin/plugin-marketplace/store";
11
+ import { setCurrentPluginTarget } from "../../plugins/current-target";
9
12
  import { getLoadablePlugins } from "../../plugins/catalog";
10
13
  import { OpenTuiInputHostProvider } from "./input-host";
11
14
  import { debugLog } from "../../utils/debug-log";
@@ -27,6 +30,10 @@ import {
27
30
  } from "../../plugins/builtin/ai/runner";
28
31
  import { createAppServices } from "../../core/app-services";
29
32
 
33
+ // Declared here rather than sniffed: the desktop view and the hosted browser
34
+ // app are both browser contexts but differ in what plugins may do.
35
+ setCurrentPluginTarget("tui");
36
+
30
37
  const AI_STARTUP_READINESS_TIMEOUT_MS = 5_000;
31
38
 
32
39
  export interface StartOpenTuiAppOptions {
@@ -73,7 +80,25 @@ export async function startOpenTuiApp(options: StartOpenTuiAppOptions = {}): Pro
73
80
  };
74
81
 
75
82
  const cliArgs = options.cliArgs ?? process.argv.slice(2);
76
- const externalPlugins = options.externalPlugins ?? await measurePerfAsync("startup.opentui.load-external-plugins", () => loadExternalPlugins());
83
+ // Before the catalog is read, so a plugin that moved out of this repository is
84
+ // available in the same session rather than only after a restart. Placed here
85
+ // rather than in the CLI entry because `src/index.tsx` starts the app directly
86
+ // and would otherwise skip it.
87
+ if (!options.externalPlugins) {
88
+ await measurePerfAsync("startup.opentui.restore-plugins", restoreExtractedPlugins);
89
+ }
90
+
91
+ setPluginInstaller(async (ref) => {
92
+ try {
93
+ const { installPlugin } = await import("../../cli/commands/plugins");
94
+ await installPlugin(ref, { quiet: true });
95
+ return { ok: true };
96
+ } catch (error) {
97
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
98
+ }
99
+ });
100
+
101
+ const externalPlugins = options.externalPlugins ?? await measurePerfAsync("startup.opentui.load-external-plugins", () => loadExternalPlugins("tui"));
77
102
  let cliLaunchRequest = options.cliLaunchRequest ?? null;
78
103
  if (!options.skipCliDispatch && cliArgs.length > 0) {
79
104
  const dispatchResult = await dispatchCli(cliArgs, { externalPlugins });
@@ -9,6 +9,15 @@ import { openTuiUiHost } from "./ui-host";
9
9
  import { OpenTuiDialogHostProvider } from "./dialog-host";
10
10
  import { openTuiToastHost } from "./toast-host";
11
11
 
12
+ let lastSavedTextFile: { name: string; text: string } | null = null;
13
+
14
+ /** Returns the most recent file written through the test renderer's `saveTextFile`. */
15
+ export function takeSavedTextFile(): { name: string; text: string } | null {
16
+ const saved = lastSavedTextFile;
17
+ lastSavedTextFile = null;
18
+ return saved;
19
+ }
20
+
12
21
  export function TestDialogProvider({ children }: { children: ReactNode }) {
13
22
  return (
14
23
  <OpenTuiDialogHostProvider
@@ -46,6 +55,10 @@ function OpenTuiTestProviders({ children }: { children: ReactNode }) {
46
55
  copyText: async () => {},
47
56
  readText: async () => "",
48
57
  notify: () => {},
58
+ saveTextFile: async ({ name, text }) => {
59
+ lastSavedTextFile = { name, text };
60
+ return `~/Downloads/${name}`;
61
+ },
49
62
  }), [renderer]);
50
63
  const nativeRenderer = useMemo(() => createTestNativeRendererHost(renderer), [renderer]);
51
64
 
@@ -13,6 +13,10 @@ interface ToastRecord {
13
13
  options?: ToastOptions;
14
14
  }
15
15
 
16
+ const MAX_VISIBLE_TOASTS = 4;
17
+ /** Small buffer above the visible cap so a dismissal can reveal a recent toast. */
18
+ const MAX_RETAINED_TOASTS = 8;
19
+
16
20
  let nextToastId = 1;
17
21
  let toasts: ToastRecord[] = [];
18
22
  const listeners = new Set<() => void>();
@@ -36,7 +40,16 @@ function dismissToast(id: string | number): void {
36
40
 
37
41
  function addToast(tone: ToastTone, body: string, options?: ToastOptions): number {
38
42
  const id = nextToastId++;
39
- toasts = [...toasts, { id, body, tone, options }];
43
+ // Only the newest MAX_VISIBLE_TOASTS render, so anything past the cap is
44
+ // unreachable. Keeping it would let a dismissal resurface a stale toast.
45
+ const kept = [...toasts, { id, body, tone, options }].slice(-MAX_RETAINED_TOASTS);
46
+ for (const dropped of toasts) {
47
+ if (kept.some((toast) => toast.id === dropped.id)) continue;
48
+ const timer = timers.get(dropped.id);
49
+ if (timer) clearTimeout(timer);
50
+ timers.delete(dropped.id);
51
+ }
52
+ toasts = kept;
40
53
  publish();
41
54
  const duration = options?.duration ?? 4_000;
42
55
  if (Number.isFinite(duration) && duration > 0) {
@@ -89,7 +102,7 @@ function ToastViewport({ position = "bottom-right" }: { position?: string }) {
89
102
  flexDirection="column"
90
103
  gap={1}
91
104
  >
92
- {visibleToasts.slice(-4).map((toast) => (
105
+ {visibleToasts.slice(-MAX_VISIBLE_TOASTS).map((toast) => (
93
106
  <box
94
107
  key={toast.id}
95
108
  width="100%"
@@ -115,6 +128,17 @@ function ToastViewport({ position = "bottom-right" }: { position?: string }) {
115
128
  {`[${toast.options.action.label}]`}
116
129
  </text>
117
130
  )}
131
+ {toast.options?.secondaryAction && (
132
+ <text
133
+ fg={colors.textMuted}
134
+ onMouseDown={(event) => {
135
+ event.stopPropagation();
136
+ toast.options?.secondaryAction?.onClick();
137
+ }}
138
+ >
139
+ {`[${toast.options.secondaryAction.label}]`}
140
+ </text>
141
+ )}
118
142
  <text
119
143
  fg={colors.textMuted}
120
144
  onMouseDown={(event) => {
@@ -109,6 +109,7 @@ export const openTuiUiHost: UiHost = {
109
109
  kind: "opentui",
110
110
  capabilities: {
111
111
  nativeCharts: true,
112
+ publicSharing: true,
112
113
  },
113
114
  Box: OpenTuiBox as UiHost["Box"],
114
115
  Text: OpenTuiText as UiHost["Text"],