gloomberb 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (531) hide show
  1. package/README.md +7 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +108 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +134 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +220 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/capabilities/chart-series.ts +3 -2
  20. package/src/cli/commands/market.ts +2 -2
  21. package/src/cli/commands/plugins.ts +39 -11
  22. package/src/cli/desktop-pane-shot.ts +285 -16
  23. package/src/cli/index.ts +1 -1
  24. package/src/cli/pane-functions/capabilities.ts +61 -31
  25. package/src/cli/pane-functions/catalog.ts +16 -4
  26. package/src/cli/pane-functions/discovery.ts +1 -0
  27. package/src/cli/pane-functions/headless.test.ts +247 -0
  28. package/src/cli/pane-functions/headless.ts +423 -0
  29. package/src/cli/pane-functions/index.test.ts +9 -0
  30. package/src/cli/pane-functions/index.ts +6 -2
  31. package/src/cli/pane-functions/options.ts +27 -2
  32. package/src/cli/pane-functions/report.ts +4 -0
  33. package/src/cli/pane-functions/resolver.ts +26 -8
  34. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  35. package/src/cli/pane-functions/screenshot.ts +236 -20
  36. package/src/cli/registry.ts +0 -22
  37. package/src/cli/restore-plugins.ts +34 -0
  38. package/src/cli/result.ts +7 -10
  39. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  40. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  41. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  42. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  43. package/src/components/chart/composite/composite-chart.tsx +301 -204
  44. package/src/components/chart/composite/format.ts +8 -2
  45. package/src/components/chart/composite/interactions.test.ts +68 -102
  46. package/src/components/chart/composite/interactions.ts +214 -264
  47. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  48. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  49. package/src/components/chart/composite/scene.ts +57 -3
  50. package/src/components/chart/composite/text-renderer.ts +3 -2
  51. package/src/components/chart/composite/time-scale.ts +138 -57
  52. package/src/components/chart/composite/types.ts +25 -1
  53. package/src/components/chart/core/data.ts +9 -13
  54. package/src/components/chart/core/date-window.ts +7 -1
  55. package/src/components/chart/core/pointer.ts +3 -0
  56. package/src/components/chart/core/types.ts +0 -45
  57. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  58. package/src/components/chart/native/kitty/index.test.ts +0 -188
  59. package/src/components/chart/native/raster/crosshair.ts +6 -5
  60. package/src/components/chart/native/raster/primitives.ts +0 -23
  61. package/src/components/chart/native/renderer-selection.ts +81 -31
  62. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  63. package/src/components/chart/static/chart-surface.tsx +150 -0
  64. package/src/components/chart/static/index.ts +8 -0
  65. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  66. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  67. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  68. package/src/components/chart/static/series.ts +49 -0
  69. package/src/components/command-bar/action-runtime.ts +1 -3
  70. package/src/components/command-bar/assist/model.test.ts +17 -2
  71. package/src/components/command-bar/assist/model.ts +40 -9
  72. package/src/components/command-bar/commands/direct/index.ts +1 -1
  73. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  74. package/src/components/command-bar/commands/registry.ts +0 -11
  75. package/src/components/command-bar/helpers.test.ts +2 -2
  76. package/src/components/command-bar/helpers.ts +2 -6
  77. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  78. package/src/components/command-bar/list/badge.test.ts +54 -0
  79. package/src/components/command-bar/list/badge.ts +62 -0
  80. package/src/components/command-bar/list/model.test.ts +103 -0
  81. package/src/components/command-bar/list/model.ts +61 -4
  82. package/src/components/command-bar/list/view.test.tsx +113 -0
  83. package/src/components/command-bar/list/view.tsx +147 -131
  84. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  85. package/src/components/command-bar/panel/body.tsx +51 -65
  86. package/src/components/command-bar/panel/index.tsx +48 -13
  87. package/src/components/command-bar/panel/layout.test.ts +139 -0
  88. package/src/components/command-bar/panel/layout.ts +65 -28
  89. package/src/components/command-bar/panel/native-surface.ts +24 -0
  90. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  91. package/src/components/command-bar/panel/runtime.ts +6 -3
  92. package/src/components/command-bar/panel/state.ts +41 -7
  93. package/src/components/command-bar/panel/types.ts +7 -1
  94. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  95. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  96. package/src/components/command-bar/routes/root/results.ts +30 -28
  97. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  98. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  99. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  100. package/src/components/command-bar/routes/root/selection.ts +1 -12
  101. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  102. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  103. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  104. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  105. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  106. package/src/components/command-bar/routing/actions.ts +1 -81
  107. package/src/components/command-bar/routing/launch-request.ts +1 -1
  108. package/src/components/command-bar/routing/list-state.ts +6 -21
  109. package/src/components/command-bar/selection-runtime.ts +1 -1
  110. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  111. package/src/components/command-bar/surface/environment.ts +8 -1
  112. package/src/components/command-bar/surface/index.test.tsx +84 -68
  113. package/src/components/command-bar/surface/index.tsx +33 -36
  114. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  115. package/src/components/command-bar/theme-picker.tsx +43 -15
  116. package/src/components/command-bar/view-model.test.ts +53 -8
  117. package/src/components/command-bar/view-model.ts +43 -14
  118. package/src/components/command-bar/workflow/tickers.ts +1 -0
  119. package/src/components/command-bar/workflow/types.ts +1 -1
  120. package/src/components/data-table/export.test.ts +21 -0
  121. package/src/components/data-table/export.ts +21 -0
  122. package/src/components/data-table/view.test.tsx +45 -6
  123. package/src/components/data-table/view.tsx +55 -9
  124. package/src/components/index.ts +10 -2
  125. package/src/components/input-search-bar.test.tsx +36 -2
  126. package/src/components/input-search-bar.tsx +18 -1
  127. package/src/components/layout/header.test.tsx +57 -10
  128. package/src/components/layout/header.tsx +292 -149
  129. package/src/components/layout/market-summary.test.ts +81 -0
  130. package/src/components/layout/market-summary.ts +109 -0
  131. package/src/components/layout/shell/chrome.ts +96 -1
  132. package/src/components/layout/shell/index.tsx +11 -3
  133. package/src/components/layout/shell/menu.ts +9 -0
  134. package/src/components/layout/shell/pane/actions.ts +29 -0
  135. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  136. package/src/components/layout/shell/shortcuts.ts +3 -0
  137. package/src/components/layout/status-bar.test.tsx +9 -8
  138. package/src/components/layout/status-bar.tsx +85 -54
  139. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  140. package/src/components/price-sparkline/index.ts +61 -34
  141. package/src/components/price-sparkline/view.tsx +1 -1
  142. package/src/components/sign-in-gate.test.tsx +128 -0
  143. package/src/components/sign-in-gate.tsx +149 -0
  144. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  145. package/src/components/ticker/badge/list.tsx +17 -3
  146. package/src/components/ticker/list-table-view.tsx +6 -3
  147. package/src/components/ui/data-table/index.tsx +22 -4
  148. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  149. package/src/components/ui/frame.tsx +28 -0
  150. package/src/components/ui/index.ts +3 -0
  151. package/src/components/ui/native-select.tsx +20 -10
  152. package/src/components/ui/select-button.tsx +138 -0
  153. package/src/components/ui/status.tsx +2 -2
  154. package/src/components/ui/table-layout.ts +1 -1
  155. package/src/components/use-double-click-activation.ts +7 -3
  156. package/src/core/app-services.ts +3 -3
  157. package/src/data/config/chart-settings.ts +4 -4
  158. package/src/data/config/layout.ts +12 -6
  159. package/src/data/config/store/index.test.ts +97 -0
  160. package/src/data/config/store/migrations.ts +41 -0
  161. package/src/data/config/store/normalize.ts +5 -3
  162. package/src/i18n/es.ts +6 -2
  163. package/src/i18n/ja.ts +6 -2
  164. package/src/i18n/ko.ts +6 -2
  165. package/src/i18n/zh-cn.ts +6 -2
  166. package/src/i18n/zh-tw.ts +6 -2
  167. package/src/layout-marketplace/gallery-terminal.tsx +126 -96
  168. package/src/layout-marketplace/gallery.test.tsx +65 -1
  169. package/src/layout-marketplace/pane-imagery.ts +1 -0
  170. package/src/layout-marketplace/payload.test.ts +17 -0
  171. package/src/layout-marketplace/payload.ts +40 -5
  172. package/src/market-data/coordinator/index.ts +3 -2
  173. package/src/market-data/coordinator/quotes.ts +173 -66
  174. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  175. package/src/pane-settings.ts +1 -1
  176. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  177. package/src/plugins/builtin/analytics/view.tsx +1 -1
  178. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  179. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  180. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  181. package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
  182. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  183. package/src/plugins/builtin/chart-composer/settings.ts +2 -13
  184. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  185. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  186. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  188. package/src/plugins/builtin/composite-plugins.ts +16 -1
  189. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  190. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  191. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  192. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  193. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  194. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  195. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  196. package/src/plugins/builtin/earnings/index.tsx +1 -0
  197. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  198. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  199. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  200. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  201. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  202. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  203. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  204. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  205. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  206. package/src/plugins/builtin/econ/index.tsx +3 -1
  207. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  208. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  209. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  210. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  211. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  212. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  213. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  214. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  215. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  216. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  217. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  218. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  219. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  220. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  221. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  222. package/src/plugins/builtin/futures/index.tsx +1 -0
  223. package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
  224. package/src/plugins/builtin/help/index.tsx +4 -0
  225. package/src/plugins/builtin/holders/index.tsx +1 -0
  226. package/src/plugins/builtin/insider/index.tsx +1 -0
  227. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
  228. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  229. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  230. package/src/plugins/builtin/market-halts/index.tsx +1 -0
  231. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  232. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  233. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  234. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  235. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  236. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  237. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  238. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  239. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  240. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  241. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  242. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  243. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  244. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  245. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  246. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  247. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  248. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  249. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  250. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  251. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  252. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  253. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  254. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  255. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  256. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  257. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  258. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  259. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  260. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  261. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  262. package/src/plugins/builtin/options/analytics.test.ts +83 -0
  263. package/src/plugins/builtin/options/analytics.ts +110 -0
  264. package/src/plugins/builtin/options/index.tsx +18 -2
  265. package/src/plugins/builtin/options/table.ts +121 -51
  266. package/src/plugins/builtin/options/types.ts +22 -15
  267. package/src/plugins/builtin/options/view.test.tsx +53 -1
  268. package/src/plugins/builtin/options/view.tsx +92 -41
  269. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  270. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  271. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  272. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  273. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  274. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  275. package/src/plugins/builtin/plugin-module.test.ts +2 -4
  276. package/src/plugins/builtin/plugin-module.ts +9 -8
  277. package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
  278. package/src/plugins/builtin/research/index.tsx +4 -0
  279. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  280. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  281. package/src/plugins/builtin/research-search/data.ts +109 -0
  282. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  283. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  284. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  285. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  286. package/src/plugins/builtin/research-search/index.tsx +85 -0
  287. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  288. package/src/plugins/builtin/research-search/model.ts +291 -0
  289. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  290. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  291. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  292. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  293. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  294. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  295. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  296. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  297. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  298. package/src/plugins/builtin/scanner/index.ts +2 -0
  299. package/src/plugins/builtin/sec/index.tsx +1 -0
  300. package/src/plugins/builtin/sectors/index.tsx +1 -0
  301. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  302. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  303. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  304. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  305. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
  306. package/src/plugins/builtin/ticker-detail/index.tsx +2 -14
  307. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  308. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  309. package/src/plugins/builtin/volatility/index.tsx +1 -1
  310. package/src/plugins/builtin/world-indices/index.tsx +1 -0
  311. package/src/plugins/builtin/world-venue-map/index.ts +27 -0
  312. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  313. package/src/plugins/builtin/world-venue-map/map.tsx +481 -0
  314. package/src/plugins/builtin/world-venue-map/model.test.ts +85 -0
  315. package/src/plugins/builtin/world-venue-map/model.ts +268 -0
  316. package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
  317. package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
  318. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  319. package/src/plugins/bundle.test.ts +109 -0
  320. package/src/plugins/bundle.ts +139 -0
  321. package/src/plugins/catalog-backend.ts +6 -44
  322. package/src/plugins/catalog-browser.test.ts +1 -0
  323. package/src/plugins/catalog-browser.ts +12 -0
  324. package/src/plugins/catalog-manifest.test.ts +43 -0
  325. package/src/plugins/catalog-manifest.ts +92 -0
  326. package/src/plugins/catalog-ui.ts +20 -4
  327. package/src/plugins/catalog.ts +9 -2
  328. package/src/plugins/current-target.ts +28 -0
  329. package/src/plugins/host-contract.ts +37 -0
  330. package/src/plugins/host-link.ts +158 -0
  331. package/src/plugins/host-modules.ts +61 -0
  332. package/src/plugins/loader.ts +41 -22
  333. package/src/plugins/ownership.ts +1 -0
  334. package/src/plugins/prediction-markets/index.tsx +1 -0
  335. package/src/plugins/registry/context.ts +3 -0
  336. package/src/plugins/registry/contributions.ts +27 -0
  337. package/src/plugins/registry/index.test.ts +25 -0
  338. package/src/plugins/registry/index.ts +8 -0
  339. package/src/plugins/registry/pane-settings.test.ts +53 -0
  340. package/src/plugins/registry/pane-settings.ts +29 -3
  341. package/src/plugins/seed.test.ts +83 -0
  342. package/src/plugins/seed.ts +80 -0
  343. package/src/public/broker.ts +31 -0
  344. package/src/public/market-data.ts +9 -0
  345. package/src/public/pane-hooks.ts +57 -0
  346. package/src/public/public-api.test.ts +285 -0
  347. package/src/public/react.ts +62 -0
  348. package/src/public/test-support.ts +23 -0
  349. package/src/public/time-series.ts +8 -0
  350. package/src/public/utils.ts +77 -0
  351. package/src/remote/layout-helpers.ts +6 -8
  352. package/src/remote/resources.ts +2 -0
  353. package/src/remote/schema.ts +1 -0
  354. package/src/remote/semantic-tree.test.tsx +105 -0
  355. package/src/remote/semantic-tree.tsx +53 -35
  356. package/src/renderers/browser/main.tsx +6 -0
  357. package/src/renderers/browser/ui-host.tsx +11 -0
  358. package/src/renderers/cloudflare/worker.test.ts +19 -0
  359. package/src/renderers/cloudflare/worker.ts +1 -1
  360. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
  361. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  362. package/src/renderers/electrobun/bun/index.ts +6 -2
  363. package/src/renderers/electrobun/shared/protocol.ts +25 -0
  364. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  365. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  366. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  367. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  368. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  369. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  370. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  371. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  372. package/src/renderers/electrobun/view/main.tsx +27 -2
  373. package/src/renderers/electrobun/view/styles.css +5 -0
  374. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  375. package/src/renderers/electrobun/view/ui-host.tsx +3 -0
  376. package/src/renderers/opentui/chart-surface.tsx +5 -1
  377. package/src/renderers/opentui/host.tsx +20 -2
  378. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  379. package/src/renderers/opentui/interaction-performance.ts +188 -0
  380. package/src/renderers/opentui/start.tsx +26 -1
  381. package/src/renderers/opentui/test-utils.tsx +13 -0
  382. package/src/renderers/opentui/toast-host.tsx +26 -2
  383. package/src/renderers/opentui/ui-host.tsx +1 -0
  384. package/src/sources/provider-router/search.test.ts +191 -0
  385. package/src/sources/provider-router/search.ts +92 -39
  386. package/src/state/app/context/index.tsx +2 -0
  387. package/src/state/app/context/selector.test.tsx +33 -1
  388. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  389. package/src/state/hooks/quote-streaming.ts +106 -30
  390. package/src/state/pane-table-export-registry.ts +50 -0
  391. package/src/theme/color-utils.ts +1 -1
  392. package/src/theme/colors.ts +50 -0
  393. package/src/theme/themes.test.ts +15 -2
  394. package/src/theme/themes.ts +19 -1
  395. package/src/tickers/search/index.test.ts +26 -0
  396. package/src/tickers/search/index.ts +42 -17
  397. package/src/tickers/search/ranking.ts +14 -0
  398. package/src/time-series/chart-data.test.ts +216 -0
  399. package/src/time-series/range.ts +12 -0
  400. package/src/time-series/resolution.test.ts +25 -26
  401. package/src/time-series/resolution.ts +44 -25
  402. package/src/time-series/resolve.ts +7 -0
  403. package/src/time-series/spec.ts +5 -5
  404. package/src/time-series/types.ts +3 -1
  405. package/src/time-series/use-chart-resolution.ts +5 -0
  406. package/src/types/config.ts +123 -3
  407. package/src/types/data-provider.ts +11 -0
  408. package/src/types/headless.ts +161 -0
  409. package/src/types/plugin.ts +104 -20
  410. package/src/ui/host.tsx +19 -1
  411. package/src/ui/index.tsx +1 -0
  412. package/src/ui/toast.tsx +8 -4
  413. package/src/utils/csv.test.ts +13 -0
  414. package/src/utils/csv.ts +37 -0
  415. package/src/utils/format.ts +35 -0
  416. package/src/utils/fuzzy-search.test.ts +94 -0
  417. package/src/utils/fuzzy-search.ts +81 -22
  418. package/src/utils/save-text-file.test.ts +24 -0
  419. package/src/utils/save-text-file.ts +36 -0
  420. package/src/utils/shortcut-labels.ts +14 -0
  421. package/src/components/chart/core/axis-format.ts +0 -157
  422. package/src/components/chart/core/index.test.ts +0 -1075
  423. package/src/components/chart/core/projection.ts +0 -357
  424. package/src/components/chart/core/render-utils.ts +0 -4
  425. package/src/components/chart/core/renderer.ts +0 -244
  426. package/src/components/chart/core/scene.ts +0 -180
  427. package/src/components/chart/core/time-axis.ts +0 -330
  428. package/src/components/chart/cursor-motion.ts +0 -47
  429. package/src/components/chart/indicators/types.ts +0 -26
  430. package/src/components/chart/market-session.ts +0 -176
  431. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  432. package/src/components/chart/multi-line/renderer.ts +0 -161
  433. package/src/components/chart/multi-line/scene.ts +0 -188
  434. package/src/components/chart/native/raster/layout.ts +0 -129
  435. package/src/components/chart/native/raster/price-chart.ts +0 -244
  436. package/src/components/chart/rendering/chart-draw.ts +0 -270
  437. package/src/components/chart/rendering/geometry.ts +0 -95
  438. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  439. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  440. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  441. package/src/components/chart/static/chart/surface.tsx +0 -358
  442. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  443. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  444. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  445. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  446. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  447. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  448. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  449. package/src/plugins/builtin/substack/api/store.ts +0 -186
  450. package/src/plugins/builtin/substack/api/types.ts +0 -42
  451. package/src/plugins/builtin/substack/api.test.ts +0 -298
  452. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  453. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  454. package/src/plugins/builtin/substack/content.ts +0 -478
  455. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  456. package/src/plugins/builtin/substack/index.tsx +0 -49
  457. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  458. package/src/plugins/builtin/substack/model.test.ts +0 -155
  459. package/src/plugins/builtin/substack/normalize.ts +0 -343
  460. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  461. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  462. package/src/plugins/builtin/substack/pane.tsx +0 -547
  463. package/src/plugins/builtin/substack/read-state.ts +0 -30
  464. package/src/plugins/builtin/substack/table.ts +0 -111
  465. package/src/plugins/builtin/substack/types.ts +0 -71
  466. package/src/plugins/builtin/substack/utils.ts +0 -131
  467. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  468. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  469. package/src/plugins/ibkr/account-cache.ts +0 -24
  470. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  471. package/src/plugins/ibkr/config.test.ts +0 -79
  472. package/src/plugins/ibkr/config.ts +0 -250
  473. package/src/plugins/ibkr/dialogs.tsx +0 -40
  474. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  475. package/src/plugins/ibkr/flex/index.ts +0 -367
  476. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  477. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  478. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  479. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  480. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  481. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  482. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  483. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  484. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  485. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  486. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  487. package/src/plugins/ibkr/gateway/history.ts +0 -120
  488. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  489. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  490. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  491. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  492. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  493. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  494. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  495. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  496. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  497. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  498. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  499. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  500. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  501. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  502. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  503. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  504. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  505. package/src/plugins/ibkr/gateway/types.ts +0 -27
  506. package/src/plugins/ibkr/index.test.tsx +0 -162
  507. package/src/plugins/ibkr/index.tsx +0 -159
  508. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  509. package/src/plugins/ibkr/instance-selection.ts +0 -36
  510. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  511. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  512. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  513. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  514. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  515. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  516. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  517. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  518. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  519. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  520. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  521. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  522. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  523. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  524. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  525. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  526. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  527. package/src/plugins/ibkr/trade/utils.ts +0 -144
  528. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  529. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  530. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  531. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Public React runtime surface for external plugins (`gloomberb/react`).
3
+ *
4
+ * These hooks are the renderer-neutral way for plugin panes to reach app
5
+ * services. Plugin render code must use them instead of importing OpenTUI,
6
+ * Electrobun, or DOM APIs directly — the renderer decides how each one is
7
+ * fulfilled, which is what lets the same plugin run in the terminal, on the
8
+ * desktop, and (for web-capable plugins) in the browser.
9
+ *
10
+ * Compatibility commitment: see the note in `./utils.ts`.
11
+ */
12
+
13
+ export {
14
+ deletePluginPaneStateValue,
15
+ getPluginPaneStateValue,
16
+ setPluginPaneStateValue,
17
+ useCapabilityInvoker,
18
+ useAssetData,
19
+ useConnectionHealth,
20
+ useDebouncedPluginPaneState,
21
+ useMarketData,
22
+ usePluginAppActions,
23
+ usePluginBrokerActions,
24
+ usePluginConfigState,
25
+ usePluginPaneActions,
26
+ usePluginPaneState,
27
+ usePluginState,
28
+ usePluginTickerActions,
29
+ useSetPluginConfigStates,
30
+ } from "../plugins/runtime";
31
+ export type { PluginRuntimeAccess } from "../plugins/runtime";
32
+
33
+ export { useInlineTickerOpener, useInlineTickers } from "../state/hooks/inline-tickers";
34
+
35
+ // Any feed plugin needs to remember which items have been read, persisted and
36
+ // capped. Substack and the news wire both use this; a third-party feed plugin
37
+ // would otherwise reimplement it or copy it and drift.
38
+ export {
39
+ DEFAULT_MAX_READ_IDS,
40
+ markPersistedReadId,
41
+ normalizePersistedReadIdState,
42
+ usePersistedReadIds,
43
+ } from "../plugins/builtin/shared/read-state";
44
+ export type { PersistedReadIdAdapter } from "../plugins/builtin/shared/read-state";
45
+ export type { InlineTickerCatalogEntry, UseInlineTickersOptions } from "../state/hooks/inline-tickers";
46
+
47
+ export {
48
+ AppContext,
49
+ PaneInstanceProvider,
50
+ useAppConfig,
51
+ useAppDispatch,
52
+ useAppSelector,
53
+ useBrokerAccounts,
54
+ useInputCapture,
55
+ usePaneCollection,
56
+ usePaneInstanceId,
57
+ usePaneTicker,
58
+ useTickers,
59
+ } from "./pane-hooks";
60
+
61
+ // Keyboard handling for plugin panes; the renderer decides how events arrive.
62
+ export { useShortcut } from "../react/input";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Test helpers for external plugin repositories (`gloomberb/test-support`).
3
+ *
4
+ * Plugins live in their own repos and run their own `bun test`, so the fakes and
5
+ * render harness they need have to ship with the host rather than being copied
6
+ * per plugin and drifting.
7
+ *
8
+ * The harness is OpenTUI-backed. That is the host's business, not the plugin's:
9
+ * a plugin importing a renderer package directly stops working on the other
10
+ * renderers, which is exactly what going through this module avoids.
11
+ */
12
+
13
+ export { MemoryPluginPersistence } from "../test-support/plugin-persistence";
14
+
15
+ export { TestDialogProvider, testRender } from "../renderers/opentui/test-utils";
16
+
17
+ export { AppContext, PaneInstanceProvider } from "../state/app/context";
18
+ export { createInitialState } from "../core/state/app/state";
19
+
20
+ // Broker plugins test against the real account cache and persistence rather
21
+ // than a hand-rolled double that drifts from how the app actually stores rows.
22
+ export { AppPersistence } from "../data/app-persistence";
23
+ export * from "../brokers/account-cache";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Chart range and resolution vocabulary (`gloomberb/time-series`).
3
+ *
4
+ * Shared so a plugin producing history speaks the same range and resolution
5
+ * language as the charts that render it, instead of inventing a parallel one.
6
+ */
7
+ export * from "../time-series/range";
8
+ export * from "../time-series/resolution";
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Public utility surface for external plugins (`gloomberb/utils`).
3
+ *
4
+ * Everything re-exported here is a compatibility commitment: plugins in other
5
+ * repositories import it, so removing or changing a signature is a breaking
6
+ * change for them. Internal helpers stay internal — add to this barrel only
7
+ * when a real plugin needs it, and prefer widening later over exporting
8
+ * speculatively.
9
+ *
10
+ * `scripts/check-public-api.ts` pins the exported names so the surface cannot
11
+ * grow by accident during an unrelated refactor.
12
+ */
13
+
14
+ export { createThrottledFetch } from "../utils/throttled-fetch";
15
+ export type {
16
+ ThrottledFetchClient,
17
+ ThrottledFetchOptions,
18
+ ThrottledFetchTransport,
19
+ } from "../utils/throttled-fetch";
20
+
21
+ export { normalizedHttpUrl } from "../utils/url";
22
+
23
+ export { formatRelativeAge } from "../utils/relative-time";
24
+
25
+ export { decodeHtmlEntities } from "../utils/html-entities";
26
+
27
+ export { isPlainKey, isPlainKeyboardEvent } from "../utils/keyboard";
28
+ export type { KeyboardModifierEventLike } from "../utils/keyboard";
29
+
30
+ export {
31
+ displayWidth,
32
+ formatCompact,
33
+ formatCompactCurrency,
34
+ formatCurrency,
35
+ formatGrowthShort,
36
+ formatNumber,
37
+ formatPercent,
38
+ formatPercentRaw,
39
+ formatTimeAgo,
40
+ formatWithDivisor,
41
+ padTo,
42
+ pickUnit,
43
+ truncateToDisplayWidth,
44
+ } from "../utils/format";
45
+
46
+ // Broker and instrument helpers. IBKR needs all of these, and any broker plugin
47
+ // will: instance lookup, currency minor units, venue normalization, and stable
48
+ // hashing for cache keys.
49
+ export {
50
+ buildBrokerPortfolioId,
51
+ createBrokerInstanceId,
52
+ getBrokerInstance,
53
+ getBrokerInstancesByType,
54
+ isBrokerPortfolioId,
55
+ } from "../utils/broker-instances";
56
+ export {
57
+ hasLikelyQuoteUnitMismatch,
58
+ normalizePriceValueByDivisor,
59
+ resolveCurrencyUnit,
60
+ resolveExchangeSubUnitCurrencyUnit,
61
+ resolvePriceHistoryCurrencyUnit,
62
+ } from "../utils/currency-units";
63
+ export type { CurrencyUnitInfo } from "../utils/currency-units";
64
+ export {
65
+ canonicalExchange,
66
+ canonicalTickerKey,
67
+ normalizeSymbol,
68
+ parsePublicTickerKey,
69
+ publicExchange,
70
+ publicTickerKey,
71
+ resolveExchangeTimeZone,
72
+ } from "../utils/exchanges";
73
+ export { fnv1aHashString } from "../utils/hash";
74
+ export { splitLongTextSegmentByDisplayWidth, truncateWithEllipsis, wrapTextLines } from "../utils/text-wrap";
75
+ export { httpFetch, setHttpFetchTransport } from "../utils/http-transport";
76
+ export type { HttpFetchTransport } from "../utils/http-transport";
77
+ export { debugLog } from "../utils/debug-log";
@@ -1,7 +1,9 @@
1
1
  import {
2
- getDockedPaneIds,
3
- } from "../plugins/pane-manager";
4
- import type { DockLayoutNode, LayoutConfig, PaneInstanceConfig } from "../types/config";
2
+ getPlacedPaneInstanceIds,
3
+ type DockLayoutNode,
4
+ type LayoutConfig,
5
+ type PaneInstanceConfig,
6
+ } from "../types/config";
5
7
 
6
8
  export function requirePaneInstance(layout: LayoutConfig, paneId: string): PaneInstanceConfig {
7
9
  const instance = layout.instances.find((entry) => entry.instanceId === paneId)
@@ -27,11 +29,7 @@ export function buildGridDockRoot(paneIds: string[], columns?: number): DockLayo
27
29
  }
28
30
 
29
31
  export function visiblePaneIds(layout: LayoutConfig): string[] {
30
- const ids = new Set<string>();
31
- getDockedPaneIds(layout).forEach((id) => ids.add(id));
32
- layout.floating.forEach((entry) => ids.add(entry.instanceId));
33
- (layout.detached ?? []).forEach((entry) => ids.add(entry.instanceId));
34
- return [...ids];
32
+ return getPlacedPaneInstanceIds(layout);
35
33
  }
36
34
 
37
35
  export function regionToDockPosition(region: string): "left" | "right" | "above" | "below" {
@@ -1,5 +1,6 @@
1
1
  import type { Dispatch } from "react";
2
2
  import type { PluginRegistry } from "../plugins/registry";
3
+ import { apiClient } from "../api-client";
3
4
  import type { AppAction, AppState } from "../state/app/context";
4
5
  import type { PaneRuntimeState } from "../core/state/app/state";
5
6
  import { setPaneSettings } from "../pane-settings";
@@ -99,6 +100,7 @@ export function createRemoteResources({
99
100
  if (resource === "app://command-bar") return commandBarSnapshot(state, uiNodes);
100
101
  if (resource === "app://command-bar/results") return commandBarResultsFromNodes(uiNodes);
101
102
  if (resource === "app://capabilities") return pluginRegistry.capabilities.manifests();
103
+ if (resource === "app://auth") return apiClient.describeAuthState();
102
104
  if (resource === "app://remote/help") return REMOTE_AGENT_HELP;
103
105
  if (resource === "ui://tree") return uiNodes;
104
106
  throw new Error(`Unknown remote resource "${resource}".`);
@@ -14,6 +14,7 @@ export const REMOTE_RESOURCES: RemoteResourceSchema[] = [
14
14
  { uri: "app://command-bar", description: "Current command-bar state and semantic result rows." },
15
15
  { uri: "app://command-bar/results", description: "Current semantic command-bar result rows." },
16
16
  { uri: "app://capabilities", description: "Registered plugin capability manifests." },
17
+ { uri: "app://auth", description: "What the client believes about its cloud session: credential present, checked, cached user plan and verification. No secrets." },
17
18
  { uri: "app://remote/help", description: "Agent-oriented remote usage guide with efficient recipes and caveats." },
18
19
  { uri: "ui://tree", description: "Live semantic UI node tree populated by shared controls and interactive primitives." },
19
20
  ];
@@ -0,0 +1,105 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { act, useState } from "react";
3
+ import { testRender } from "../renderers/opentui/test-utils";
4
+ import {
5
+ RemoteUiRegistryProvider,
6
+ createRemoteUiRegistry,
7
+ useRemoteUiNode,
8
+ } from "./semantic-tree";
9
+
10
+ let setup: Awaited<ReturnType<typeof testRender>> | undefined;
11
+ let setValue: ((value: number) => void) | null = null;
12
+ let metadataReads = 0;
13
+
14
+ afterEach(async () => {
15
+ if (!setup) return;
16
+ await act(async () => setup?.renderer.destroy());
17
+ setup = undefined;
18
+ setValue = null;
19
+ metadataReads = 0;
20
+ });
21
+
22
+ function LazyMetadataNode() {
23
+ useRemoteUiNode({
24
+ role: "table",
25
+ getMetadata: () => {
26
+ metadataReads += 1;
27
+ return { rowCount: 200 };
28
+ },
29
+ });
30
+ return <text>table</text>;
31
+ }
32
+
33
+ function DynamicNode() {
34
+ const [value, updateValue] = useState(1);
35
+ setValue = updateValue;
36
+ useRemoteUiNode({
37
+ role: "button",
38
+ label: `Value ${value}`,
39
+ actions: { read: () => value },
40
+ metadata: { value },
41
+ });
42
+ return <text>{value}</text>;
43
+ }
44
+
45
+ test("semantic registry rejects non-function actions consistently", async () => {
46
+ const registry = createRemoteUiRegistry();
47
+ registry.register("invalid", {
48
+ role: "button",
49
+ actions: { broken: "not-a-function" as unknown as () => void },
50
+ });
51
+
52
+ expect(registry.snapshot()[0]?.actions).toEqual([]);
53
+ await expect(registry.invoke("invalid", "broken")).rejects.toThrow(
54
+ 'UI node "invalid" does not expose action "broken".',
55
+ );
56
+ });
57
+
58
+ test("semantic metadata is projected only when a remote snapshot requests it", async () => {
59
+ const registry = createRemoteUiRegistry();
60
+ setup = await testRender(
61
+ <RemoteUiRegistryProvider registry={registry}>
62
+ <LazyMetadataNode />
63
+ </RemoteUiRegistryProvider>,
64
+ { width: 20, height: 4 },
65
+ );
66
+ await setup.renderOnce();
67
+
68
+ expect(metadataReads).toBe(0);
69
+ expect(registry.snapshot()[0]?.metadata).toEqual({ rowCount: 200 });
70
+ expect(metadataReads).toBe(1);
71
+ });
72
+
73
+ test("semantic nodes keep current behavior without re-registering on every render", async () => {
74
+ const registry = createRemoteUiRegistry();
75
+ const register = registry.register.bind(registry);
76
+ let registrations = 0;
77
+ registry.register = (id, registration) => {
78
+ registrations += 1;
79
+ register(id, registration);
80
+ };
81
+
82
+ setup = await testRender(
83
+ <RemoteUiRegistryProvider registry={registry}>
84
+ <DynamicNode />
85
+ </RemoteUiRegistryProvider>,
86
+ { width: 20, height: 4 },
87
+ );
88
+ await setup.renderOnce();
89
+
90
+ const nodeId = registry.snapshot()[0]!.id;
91
+ expect(registrations).toBe(1);
92
+ expect(await registry.invoke(nodeId, "read")).toBe(1);
93
+
94
+ await act(async () => {
95
+ setValue?.(2);
96
+ await setup!.renderOnce();
97
+ });
98
+
99
+ expect(registrations).toBe(1);
100
+ expect(registry.snapshot()[0]).toMatchObject({
101
+ label: "Value 2",
102
+ metadata: { value: 2 },
103
+ });
104
+ expect(await registry.invoke(nodeId, "read")).toBe(2);
105
+ });
@@ -17,11 +17,13 @@ export interface RemoteUiNodeRegistration {
17
17
  disabled?: boolean;
18
18
  actions?: Record<string, RemoteUiAction | undefined>;
19
19
  metadata?: Record<string, unknown>;
20
+ /** Defers expensive metadata projection until a remote snapshot requests it. */
21
+ getMetadata?: () => Record<string, unknown>;
20
22
  }
21
23
 
22
- interface RegisteredRemoteUiNode extends RemoteUiNodeRegistration {
24
+ interface RegisteredRemoteUiNode {
23
25
  id: string;
24
- actions: Record<string, RemoteUiAction>;
26
+ registration: RemoteUiNodeRegistration;
25
27
  }
26
28
 
27
29
  export interface RemoteUiRegistry {
@@ -33,46 +35,54 @@ export interface RemoteUiRegistry {
33
35
 
34
36
  const RemoteUiRegistryContext = createContext<RemoteUiRegistry | null>(null);
35
37
 
36
- function createRemoteUiRegistry(): RemoteUiRegistry {
38
+ export function createRemoteUiRegistry(): RemoteUiRegistry {
37
39
  const nodes = new Map<string, RegisteredRemoteUiNode>();
38
40
 
39
41
  return {
40
42
  register(id, registration) {
41
- nodes.set(id, {
42
- ...registration,
43
- id,
44
- actions: Object.fromEntries(
45
- Object.entries(registration.actions ?? {})
46
- .filter((entry): entry is [string, RemoteUiAction] => typeof entry[1] === "function"),
47
- ),
48
- });
43
+ nodes.set(id, { id, registration });
49
44
  },
50
45
  unregister(id) {
51
46
  nodes.delete(id);
52
47
  },
53
48
  snapshot() {
54
- return [...nodes.values()].map((node) => ({
55
- id: node.id,
56
- role: node.role,
57
- label: node.label,
58
- disabled: node.disabled,
59
- actions: Object.keys(node.actions).sort(),
60
- metadata: node.metadata,
61
- }));
49
+ return [...nodes.values()].map((node) => {
50
+ const registration = node.registration;
51
+ return {
52
+ id: node.id,
53
+ role: registration.role,
54
+ label: registration.label,
55
+ disabled: registration.disabled,
56
+ actions: Object.entries(registration.actions ?? {})
57
+ .filter((entry): entry is [string, RemoteUiAction] => typeof entry[1] === "function")
58
+ .map(([action]) => action)
59
+ .sort(),
60
+ metadata: registration.getMetadata?.() ?? registration.metadata,
61
+ };
62
+ });
62
63
  },
63
64
  async invoke(nodeId, action, input) {
64
65
  const node = nodes.get(nodeId);
65
66
  if (!node) throw new Error(`Unknown UI node "${nodeId}".`);
66
- if (node.disabled) throw new Error(`UI node "${nodeId}" is disabled.`);
67
- const handler = node.actions[action];
68
- if (!handler) throw new Error(`UI node "${nodeId}" does not expose action "${action}".`);
67
+ const registration = node.registration;
68
+ if (registration.disabled) throw new Error(`UI node "${nodeId}" is disabled.`);
69
+ const handler = registration.actions?.[action];
70
+ if (typeof handler !== "function") {
71
+ throw new Error(`UI node "${nodeId}" does not expose action "${action}".`);
72
+ }
69
73
  return await handler(input);
70
74
  },
71
75
  };
72
76
  }
73
77
 
74
- export function RemoteUiRegistryProvider({ children }: { children: ReactNode }) {
75
- const registryRef = useRef<RemoteUiRegistry | null>(null);
78
+ export function RemoteUiRegistryProvider({
79
+ children,
80
+ registry,
81
+ }: {
82
+ children: ReactNode;
83
+ registry?: RemoteUiRegistry;
84
+ }) {
85
+ const registryRef = useRef<RemoteUiRegistry | null>(registry ?? null);
76
86
  if (!registryRef.current) {
77
87
  registryRef.current = createRemoteUiRegistry();
78
88
  }
@@ -91,22 +101,30 @@ export function useRemoteUiNode(registration: RemoteUiNodeRegistration | null |
91
101
  const registry = useRemoteUiRegistry();
92
102
  const generatedId = useId();
93
103
  const nodeId = useMemo(() => `ui:${generatedId.replace(/:/g, "")}`, [generatedId]);
104
+ const registrationRef = useRef(registration);
105
+ registrationRef.current = registration;
106
+ const dynamicRegistrationRef = useRef<RemoteUiNodeRegistration | null>(null);
107
+ if (!dynamicRegistrationRef.current) {
108
+ dynamicRegistrationRef.current = {
109
+ get role() { return registrationRef.current?.role ?? "unknown"; },
110
+ get label() { return registrationRef.current?.label; },
111
+ get disabled() { return registrationRef.current?.disabled; },
112
+ get actions() { return registrationRef.current?.actions; },
113
+ get metadata() { return registrationRef.current?.metadata; },
114
+ get getMetadata() { return registrationRef.current?.getMetadata; },
115
+ };
116
+ }
117
+ const registered = registration != null;
94
118
 
95
119
  useEffect(() => {
96
120
  if (!registry) return;
97
- if (!registration) {
121
+ if (!registered) {
98
122
  registry.unregister(nodeId);
99
123
  return;
100
124
  }
101
- registry.register(nodeId, registration);
102
- });
103
-
104
- useEffect(() => {
105
- return () => registry?.unregister(nodeId);
106
- }, [
107
- nodeId,
108
- registry,
109
- ]);
125
+ registry.register(nodeId, dynamicRegistrationRef.current!);
126
+ return () => registry.unregister(nodeId);
127
+ }, [nodeId, registered, registry]);
110
128
 
111
- return registry && registration ? nodeId : null;
129
+ return registry && registered ? nodeId : null;
112
130
  }
@@ -1,4 +1,5 @@
1
1
  /** @jsxImportSource react */
2
+ import { setCurrentPluginTarget } from "../../plugins/current-target";
2
3
  import "../electrobun/view/styles.css";
3
4
  import { createRoot } from "react-dom/client";
4
5
  import { App } from "../../app";
@@ -21,6 +22,10 @@ import { BROWSER_DATA_DIR, installBrowserConfigStore } from "./config-host";
21
22
  import { browserRendererHost, browserUiHost } from "./ui-host";
22
23
  import { createBrowserDeepLinkBridge } from "./deeplink-bridge";
23
24
 
25
+ // Declared here rather than sniffed: the desktop view and the hosted browser
26
+ // app are both browser contexts but differ in what plugins may do.
27
+ setCurrentPluginTarget("web");
28
+
24
29
  const rootElement = document.getElementById("root");
25
30
  if (!rootElement) throw new Error("Missing root element");
26
31
  const appRootElement = rootElement;
@@ -48,6 +53,7 @@ async function boot(): Promise<void> {
48
53
  plugins={getBrowserBuiltinPlugins()}
49
54
  desktopDeepLinkBridge={deepLinkBridge}
50
55
  updatesEnabled={false}
56
+ requireSignIn
51
57
  />
52
58
  </WebDialogHostProvider>
53
59
  </WebToastHostProvider>
@@ -27,6 +27,17 @@ export const browserRendererHost: RendererHost = {
27
27
  async readText() {
28
28
  return navigator.clipboard.readText();
29
29
  },
30
+ async saveTextFile({ name, text, mimeType }) {
31
+ const url = URL.createObjectURL(new Blob([text], { type: mimeType }));
32
+ const anchor = document.createElement("a");
33
+ anchor.href = url;
34
+ anchor.download = name;
35
+ document.body.appendChild(anchor);
36
+ anchor.click();
37
+ anchor.remove();
38
+ setTimeout(() => URL.revokeObjectURL(url), 0);
39
+ return `Downloads/${name}`;
40
+ },
30
41
  async copyPngImage(pngBase64) {
31
42
  const ClipboardItemCtor = globalThis.ClipboardItem;
32
43
  if (!ClipboardItemCtor || !navigator.clipboard.write) {
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { handleRequest, SECURITY_HEADERS, type WorkerEnv } from "./worker";
3
+ import { REGISTRY_ORIGIN } from "../../plugins/builtin/plugin-marketplace/feed";
3
4
 
4
5
  function fixture() {
5
6
  const requests: Request[] = [];
@@ -134,3 +135,21 @@ describe("static Cloudflare host", () => {
134
135
  expect(requests).toHaveLength(0);
135
136
  });
136
137
  });
138
+
139
+ /**
140
+ * The hosted build can only reach origins listed in `connect-src`. A blocked
141
+ * fetch is not an error the user can act on: the plugin marketplace just shows
142
+ * an empty catalog. This shipped that way once, because the pane was verified
143
+ * on the terminal and desktop but not on the web build, so the two values are
144
+ * pinned to each other here.
145
+ */
146
+ describe("content security policy", () => {
147
+ test("allows the plugin registry the marketplace fetches", () => {
148
+ const connectSrc = SECURITY_HEADERS["content-security-policy"]
149
+ .split(";")
150
+ .map((directive) => directive.trim())
151
+ .find((directive) => directive.startsWith("connect-src"));
152
+
153
+ expect(connectSrc).toContain(REGISTRY_ORIGIN);
154
+ });
155
+ });
@@ -39,7 +39,7 @@ const API_METHODS = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "O
39
39
  type ApiFetch = (request: Request) => Promise<Response>;
40
40
 
41
41
  export const SECURITY_HEADERS = {
42
- "content-security-policy": "default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self' https:; connect-src 'self' https://api.github.com https://api.fiscaldata.treasury.gov; form-action 'self'; upgrade-insecure-requests",
42
+ "content-security-policy": "default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self' https:; connect-src 'self' https://api.github.com https://api.fiscaldata.treasury.gov https://plugins.gloom.sh; form-action 'self'; upgrade-insecure-requests",
43
43
  "cross-origin-opener-policy": "same-origin",
44
44
  "cross-origin-resource-policy": "same-origin",
45
45
  "permissions-policy": "camera=(), geolocation=(), microphone=(), payment=(), usb=()",
@@ -10,6 +10,7 @@ import type {
10
10
  import { safeExternalUrl } from "../../../../utils/external-url";
11
11
  import { getContextMenuRequestId, normalizeContextMenuItems } from "../context-menu/normalize";
12
12
  import { MAIN_WINDOW_RPC_KEY } from "../window/focus";
13
+ import { saveTextFileToDownloads } from "../../../../utils/save-text-file";
13
14
 
14
15
  interface DesktopHostRequestOptions<TRpc> {
15
16
  clearMainWindow: () => void;
@@ -51,7 +52,7 @@ function normalizeWindowControlAction(action: unknown): DesktopWindowControlActi
51
52
  throw new Error("host.windowControl requires a valid action.");
52
53
  }
53
54
 
54
- export function handleDesktopHostRequest<TRpc>({
55
+ export async function handleDesktopHostRequest<TRpc>({
55
56
  clearMainWindow,
56
57
  closeAllDetachedWindows,
57
58
  controlWindowForRpcKey,
@@ -63,7 +64,7 @@ export function handleDesktopHostRequest<TRpc>({
63
64
  rpc,
64
65
  teardownServices,
65
66
  trackContextMenuRequest,
66
- }: DesktopHostRequestOptions<TRpc>): DesktopBackendRequestResponse<DesktopHostRequest["method"]> {
67
+ }: DesktopHostRequestOptions<TRpc>): Promise<DesktopBackendRequestResponse<DesktopHostRequest["method"]>> {
67
68
  switch (request.method) {
68
69
  case "host.restart":
69
70
  restartDesktopApp({
@@ -127,6 +128,12 @@ export function handleDesktopHostRequest<TRpc>({
127
128
  }
128
129
  case "host.readText":
129
130
  return Utils.clipboardReadText() ?? "";
131
+ case "host.saveTextFile": {
132
+ if (typeof request.payload.name !== "string" || typeof request.payload.text !== "string") {
133
+ throw new Error("host.saveTextFile requires a name and text.");
134
+ }
135
+ return saveTextFileToDownloads(request.payload.name, request.payload.text);
136
+ }
130
137
  case "host.notify":
131
138
  playNotificationSound(normalizeText(request.payload.sound));
132
139
  Utils.showNotification({