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,204 @@
1
+ import type { PluginTarget } from "../../../types/plugin";
2
+
3
+ export type PluginTier = "official" | "verified" | "community";
4
+
5
+ /** One record from https://plugins.gloom.sh/registry.json. */
6
+ export interface RegistryPlugin {
7
+ id: string;
8
+ name: string;
9
+ tagline: string;
10
+ description?: string;
11
+ repo?: string;
12
+ ref?: string;
13
+ commit?: string;
14
+ author: { name: string; github?: string };
15
+ categories: string[];
16
+ targets: PluginTarget[];
17
+ hosts: string[];
18
+ contributes: { panes: string[]; capabilities: string[]; broker: boolean };
19
+ minGloomberb?: string;
20
+ tier: PluginTier;
21
+ bundled: boolean;
22
+ featured?: boolean;
23
+ stars: number;
24
+ updatedAt?: string;
25
+ readme?: string;
26
+ }
27
+
28
+ export interface RegistryFeed {
29
+ version: number;
30
+ generatedAt: string;
31
+ plugins: RegistryPlugin[];
32
+ }
33
+
34
+ /** What the app knows about a plugin it has actually loaded. */
35
+ export interface InstalledPlugin {
36
+ id: string;
37
+ name: string;
38
+ version: string;
39
+ description?: string;
40
+ toggleable: boolean;
41
+ enabled: boolean;
42
+ source: "builtin" | "external";
43
+ /** Set when the plugin is installed but cannot run on this renderer. */
44
+ unsupportedTarget?: PluginTarget;
45
+ loadError?: string;
46
+ }
47
+
48
+ export interface MarketplaceEntry {
49
+ id: string;
50
+ name: string;
51
+ tagline: string;
52
+ description?: string;
53
+ categories: string[];
54
+ tier: PluginTier;
55
+ targets: PluginTarget[];
56
+ hosts: string[];
57
+ repo?: string;
58
+ stars: number;
59
+ featured: boolean;
60
+ /** Ships inside Gloomberb; there is nothing to install or remove. */
61
+ bundled: boolean;
62
+ installed: boolean;
63
+ enabled: boolean;
64
+ toggleable: boolean;
65
+ installedVersion?: string;
66
+ /** The registry's tag, when the plugin is installable. */
67
+ availableVersion?: string;
68
+ contributes?: RegistryPlugin["contributes"];
69
+ loadError?: string;
70
+ /**
71
+ * The plugin cannot run on the current renderer — IBKR Gateway in a browser,
72
+ * for example. Distinct from "not installed": the user may have it installed
73
+ * and working elsewhere.
74
+ */
75
+ unsupportedHere: boolean;
76
+ }
77
+
78
+ const TIER_RANK: Record<PluginTier, number> = { official: 0, verified: 1, community: 2 };
79
+
80
+ /**
81
+ * Merges the remote catalog with what is actually loaded.
82
+ *
83
+ * Three sources disagree in normal operation and all three matter: the registry
84
+ * knows about plugins the user has not installed, the app knows about plugins
85
+ * the registry has never heard of (installed straight from a git URL), and a
86
+ * plugin can be installed but inert because this renderer cannot run it. The
87
+ * merge keeps all three visible rather than showing whichever list is handy.
88
+ */
89
+ export function mergeCatalog(options: {
90
+ registry: readonly RegistryPlugin[];
91
+ installed: readonly InstalledPlugin[];
92
+ target: PluginTarget;
93
+ }): MarketplaceEntry[] {
94
+ const { registry, installed, target } = options;
95
+ const installedById = new Map(installed.map((entry) => [entry.id, entry]));
96
+ const entries: MarketplaceEntry[] = [];
97
+
98
+ for (const plugin of registry) {
99
+ const local = installedById.get(plugin.id);
100
+ installedById.delete(plugin.id);
101
+
102
+ entries.push({
103
+ id: plugin.id,
104
+ name: plugin.name,
105
+ tagline: plugin.tagline,
106
+ description: plugin.description,
107
+ categories: plugin.categories,
108
+ tier: plugin.tier,
109
+ targets: plugin.targets,
110
+ hosts: plugin.hosts,
111
+ repo: plugin.repo,
112
+ stars: plugin.stars,
113
+ featured: plugin.featured === true,
114
+ bundled: plugin.bundled,
115
+ // A bundled plugin is present whether or not the local catalog reports it,
116
+ // which matters when the feed is newer than the running build.
117
+ installed: plugin.bundled || !!local,
118
+ enabled: local ? local.enabled : plugin.bundled,
119
+ toggleable: local ? local.toggleable : true,
120
+ installedVersion: local?.version,
121
+ availableVersion: plugin.ref,
122
+ contributes: plugin.contributes,
123
+ loadError: local?.loadError,
124
+ unsupportedHere: !plugin.targets.includes(target),
125
+ });
126
+ }
127
+
128
+ // Installed but unlisted: side-loaded from a git URL, or listed under a
129
+ // different id. Still needs to be manageable.
130
+ for (const local of installedById.values()) {
131
+ entries.push({
132
+ id: local.id,
133
+ name: local.name,
134
+ tagline: local.description ?? "Installed outside the registry",
135
+ description: local.description,
136
+ categories: ["unlisted"],
137
+ tier: "community",
138
+ targets: local.unsupportedTarget ? [] : [target],
139
+ hosts: [],
140
+ stars: 0,
141
+ featured: false,
142
+ bundled: local.source === "builtin",
143
+ installed: true,
144
+ enabled: local.enabled,
145
+ toggleable: local.toggleable,
146
+ installedVersion: local.version,
147
+ loadError: local.loadError,
148
+ unsupportedHere: !!local.unsupportedTarget,
149
+ });
150
+ }
151
+
152
+ return entries;
153
+ }
154
+
155
+ /**
156
+ * One ordered list rather than an installed/available split.
157
+ *
158
+ * What you already have is what you act on most, so it sorts to the top; the
159
+ * rest of the catalog continues below without a mode switch to find it. Within
160
+ * each half the order is the curated one: featured, then tier, then stars.
161
+ */
162
+ export function sortEntries(entries: readonly MarketplaceEntry[]): MarketplaceEntry[] {
163
+ return [...entries].sort((a, b) => {
164
+ if (a.installed !== b.installed) return a.installed ? -1 : 1;
165
+ if (a.featured !== b.featured) return a.featured ? -1 : 1;
166
+ if (a.tier !== b.tier) return TIER_RANK[a.tier] - TIER_RANK[b.tier];
167
+ if (a.stars !== b.stars) return b.stars - a.stars;
168
+ return a.name.localeCompare(b.name);
169
+ });
170
+ }
171
+
172
+ export function filterEntries(
173
+ entries: readonly MarketplaceEntry[],
174
+ options: { query: string; category: string | null },
175
+ ): MarketplaceEntry[] {
176
+ const query = options.query.trim().toLowerCase();
177
+
178
+ return entries.filter((entry) => {
179
+ if (options.category && !entry.categories.includes(options.category)) return false;
180
+ if (!query) return true;
181
+ return [entry.name, entry.id, entry.tagline, entry.description, ...entry.categories]
182
+ .some((field) => typeof field === "string" && field.toLowerCase().includes(query));
183
+ });
184
+ }
185
+
186
+ export function collectCategories(entries: readonly MarketplaceEntry[]): string[] {
187
+ const seen = new Set<string>();
188
+ for (const entry of entries) {
189
+ for (const category of entry.categories) seen.add(category);
190
+ }
191
+ return [...seen].sort();
192
+ }
193
+
194
+ /** Whether this entry can be installed from inside the app right now. */
195
+ export function isInstallable(entry: MarketplaceEntry): boolean {
196
+ return !entry.installed && !entry.bundled && !!entry.repo;
197
+ }
198
+
199
+ /** Short label for a plugin that cannot run on the current renderer. */
200
+ export function unsupportedLabel(entry: MarketplaceEntry): string | null {
201
+ if (!entry.unsupportedHere) return null;
202
+ if (entry.targets.length === 0) return "Unavailable here";
203
+ return entry.targets.includes("desktop") ? "Desktop only" : "Terminal only";
204
+ }
@@ -0,0 +1,385 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ import {
4
+ DataTableStackView,
5
+ EmptyState,
6
+ InputSearchBar,
7
+ Spinner,
8
+ useExternalLinkFooter,
9
+ type DataTableCell,
10
+ type DataTableColumn,
11
+ type DataTableKeyEvent,
12
+ type PaneFooterSegment,
13
+ } from "../../../components";
14
+ import { useShortcut } from "../../../react/input";
15
+ import { colors } from "../../../theme/colors";
16
+ import type { PaneProps } from "../../../types/plugin";
17
+ import { Box, ScrollBox, Text, TextAttributes, type InputRenderable } from "../../../ui";
18
+ import { formatCompact } from "../../../utils/format";
19
+ import { isPlainKey } from "../../../utils/keyboard";
20
+ import { formatRelativeAge } from "../../../utils/relative-time";
21
+ import { canInstallPlugins, getCurrentPluginTarget } from "../../current-target";
22
+ import { loadRegistry, registryPluginUrl } from "./feed";
23
+ import {
24
+ collectCategories,
25
+ filterEntries,
26
+ mergeCatalog,
27
+ sortEntries,
28
+ unsupportedLabel,
29
+ isInstallable,
30
+ type MarketplaceEntry,
31
+ type RegistryPlugin,
32
+ } from "./model";
33
+ import { getMarketplaceHost, getPluginInstaller } from "./store";
34
+
35
+ export const PLUGIN_MARKETPLACE_PANE_ID = "plugin-marketplace";
36
+
37
+ type Column = DataTableColumn & { id: "name" | "tagline" | "stars" | "status" };
38
+
39
+ function buildColumns(width: number): Column[] {
40
+ const starsWidth = 6;
41
+ const statusWidth = 14;
42
+ const nameWidth = Math.min(26, Math.max(14, Math.floor(width * 0.24)));
43
+ const taglineWidth = Math.max(16, width - nameWidth - starsWidth - statusWidth - 8);
44
+ return [
45
+ { id: "name", label: "PLUGIN", width: nameWidth, align: "left" },
46
+ { id: "tagline", label: "DESCRIPTION", width: taglineWidth, align: "left" },
47
+ { id: "stars", label: "STARS", width: starsWidth, align: "right" },
48
+ { id: "status", label: "STATUS", width: statusWidth, align: "left" },
49
+ ];
50
+ }
51
+
52
+ function statusOf(
53
+ entry: MarketplaceEntry,
54
+ installedNow: readonly string[],
55
+ ): { text: string; color: string } {
56
+ if (installedNow.includes(entry.id)) return { text: "restart to load", color: colors.warning };
57
+ if (entry.loadError) return { text: "failed", color: colors.negative };
58
+ const unsupported = unsupportedLabel(entry);
59
+ if (unsupported) return { text: unsupported.toLowerCase(), color: colors.warning };
60
+ if (entry.bundled) return { text: "included", color: colors.textDim };
61
+ if (entry.installed) {
62
+ return entry.enabled
63
+ ? { text: "enabled", color: colors.positive }
64
+ : { text: "disabled", color: colors.textDim };
65
+ }
66
+ return { text: "available", color: colors.textBright };
67
+ }
68
+
69
+ function renderCell(
70
+ entry: MarketplaceEntry,
71
+ column: Column,
72
+ rowState: { selected: boolean },
73
+ installedNow: readonly string[],
74
+ ): DataTableCell {
75
+ const selected = rowState.selected ? colors.selectedText : undefined;
76
+
77
+ switch (column.id) {
78
+ case "name":
79
+ return {
80
+ text: entry.name,
81
+ color: selected ?? (entry.featured ? colors.textBright : colors.text),
82
+ ...(entry.featured ? { attributes: TextAttributes.BOLD } : {}),
83
+ };
84
+ case "tagline":
85
+ return { text: entry.tagline, color: selected ?? colors.textDim };
86
+ case "stars":
87
+ return {
88
+ text: entry.bundled || entry.stars === 0 ? "—" : formatCompact(entry.stars),
89
+ color: selected ?? colors.textDim,
90
+ };
91
+ case "status": {
92
+ const status = statusOf(entry, installedNow);
93
+ return { text: status.text, color: rowState.selected ? colors.selectedText : status.color };
94
+ }
95
+ }
96
+ }
97
+
98
+ function DetailRow({ label, value }: { label: string; value: string }) {
99
+ return (
100
+ <Box flexDirection="row" height={1} gap={1}>
101
+ <Text fg={colors.textDim}>{`${label}:`}</Text>
102
+ <Text fg={colors.text}>{value}</Text>
103
+ </Box>
104
+ );
105
+ }
106
+
107
+ function EntryDetail({ entry, width }: { entry: MarketplaceEntry; width: number }) {
108
+ const contributes: string[] = [];
109
+ if (entry.contributes) {
110
+ const { panes, capabilities, broker } = entry.contributes;
111
+ if (panes.length > 0) contributes.push(`${panes.length} pane${panes.length === 1 ? "" : "s"}`);
112
+ if (capabilities.length > 0) contributes.push(`${capabilities.length} data source${capabilities.length === 1 ? "" : "s"}`);
113
+ if (broker) contributes.push("a broker integration");
114
+ }
115
+
116
+ return (
117
+ <ScrollBox flexDirection="column" width={width} paddingLeft={1} paddingRight={1}>
118
+ <Box flexDirection="row" gap={2} height={1}>
119
+ <Text fg={colors.textDim}>{entry.tier}</Text>
120
+ <Text fg={colors.textDim}>{entry.categories.join(", ")}</Text>
121
+ {entry.installedVersion ? <Text fg={colors.textDim}>{`v${entry.installedVersion}`}</Text> : null}
122
+ {!entry.bundled && entry.stars > 0 ? <Text fg={colors.textDim}>{`${entry.stars} stars`}</Text> : null}
123
+ </Box>
124
+
125
+ {entry.description ? (
126
+ <Box paddingTop={1} flexDirection="column">
127
+ <Text fg={colors.text}>{entry.description}</Text>
128
+ </Box>
129
+ ) : null}
130
+
131
+ <Box paddingTop={1} flexDirection="column">
132
+ {contributes.length > 0 ? <DetailRow label="Adds" value={contributes.join(", ")} /> : null}
133
+ {/*
134
+ * Declared by the plugin author and not enforced: plugins are not
135
+ * sandboxed, so this is a hint about intent, not a limit. Labelled
136
+ * "Declares" rather than "Network" so it does not read as a guarantee.
137
+ */}
138
+ {entry.hosts.length > 0 ? <DetailRow label="Declares" value={entry.hosts.join(", ")} /> : null}
139
+ {entry.repo ? <DetailRow label="Source" value={`github.com/${entry.repo}`} /> : null}
140
+ {entry.loadError ? <DetailRow label="Error" value={entry.loadError} /> : null}
141
+ </Box>
142
+
143
+ {!entry.installed && !entry.bundled && entry.repo ? (
144
+ <Box paddingTop={1} flexDirection="column">
145
+ <Text fg={colors.textDim}>Runs with your full permissions. Read the source first.</Text>
146
+ <Text fg={colors.textBright}>{`gloomberb install ${entry.repo}`}</Text>
147
+ </Box>
148
+ ) : null}
149
+ </ScrollBox>
150
+ );
151
+ }
152
+
153
+ export function PluginMarketplacePane({ focused, width, height }: PaneProps) {
154
+ const [query, setQuery] = useState("");
155
+ const [selectedId, setSelectedId] = useState<string | null>(null);
156
+ const [detailOpen, setDetailOpen] = useState(false);
157
+ const [searchFocused, setSearchFocused] = useState(false);
158
+ // null keeps the curated order: featured first, then tier, then stars.
159
+ const [sortColumn, setSortColumn] = useState<"name" | "stars" | null>(null);
160
+ const [searchFocusToken, setSearchFocusToken] = useState(0);
161
+ const searchInputRef = useRef<InputRenderable | null>(null);
162
+
163
+ const [registry, setRegistry] = useState<RegistryPlugin[]>([]);
164
+ const [status, setStatus] = useState<"loading" | "ready" | "error">("loading");
165
+ const [stale, setStale] = useState(false);
166
+ const [fetchedAt, setFetchedAt] = useState<number | null>(null);
167
+ // Bumped after a toggle or install so the list is re-read from the host.
168
+ const [localRevision, setLocalRevision] = useState(0);
169
+ const [installing, setInstalling] = useState<string | null>(null);
170
+ const [installError, setInstallError] = useState<string | null>(null);
171
+ // Installed during this session. A plugin is only registered at startup, so
172
+ // saying "enabled" here would be a lie — the pane it adds is not there yet.
173
+ const [installedNow, setInstalledNow] = useState<readonly string[]>([]);
174
+
175
+ const refresh = useCallback((force: boolean) => {
176
+ setStatus((current) => (current === "ready" ? current : "loading"));
177
+ void loadRegistry({ force }).then((result) => {
178
+ setRegistry(result.plugins);
179
+ setStale(result.stale);
180
+ setFetchedAt(result.fetchedAt);
181
+ setStatus(result.error && result.plugins.length === 0 ? "error" : "ready");
182
+ });
183
+ }, []);
184
+
185
+ useEffect(() => refresh(false), [refresh]);
186
+
187
+ const target = getCurrentPluginTarget();
188
+ const entries = useMemo(() => {
189
+ void localRevision;
190
+ const installed = getMarketplaceHost()?.listInstalled() ?? [];
191
+ return sortEntries(mergeCatalog({ registry, installed, target }));
192
+ }, [registry, target, localRevision]);
193
+
194
+ const rows = useMemo(() => {
195
+ const filtered = filterEntries(entries, { query, category: null });
196
+ if (sortColumn === "name") return [...filtered].sort((a, b) => a.name.localeCompare(b.name));
197
+ if (sortColumn === "stars") return [...filtered].sort((a, b) => b.stars - a.stars);
198
+ return filtered;
199
+ }, [entries, query, sortColumn]);
200
+
201
+ const selected = useMemo(
202
+ () => rows.find((entry) => entry.id === selectedId) ?? rows[0] ?? null,
203
+ [rows, selectedId],
204
+ );
205
+
206
+ const focusSearch = useCallback(() => {
207
+ setSearchFocused(true);
208
+ setSearchFocusToken((token) => token + 1);
209
+ }, []);
210
+ const blurSearch = useCallback(() => setSearchFocused(false), []);
211
+
212
+ const toggleSelected = useCallback(() => {
213
+ const host = getMarketplaceHost();
214
+ if (!host || !selected || !selected.installed || !selected.toggleable) return;
215
+ host.setPluginEnabled(selected.id, !selected.enabled);
216
+ setLocalRevision((value) => value + 1);
217
+ }, [selected]);
218
+
219
+ const installSelected = useCallback(() => {
220
+ const install = getPluginInstaller();
221
+ if (!selected || !isInstallable(selected) || !install || installing) return;
222
+ if (installedNow.includes(selected.id)) return;
223
+ // Installs address the repository, not the plugin id: there is no central
224
+ // name resolution, so owner/repo is the only unambiguous reference.
225
+ const repo = selected.repo;
226
+ if (!repo) return;
227
+ const target = selected.id;
228
+ setInstalling(target);
229
+ setInstallError(null);
230
+ void install(repo).then((result) => {
231
+ setInstalling(null);
232
+ if (result.ok) {
233
+ setInstalledNow((current) => [...current, target]);
234
+ setLocalRevision((value) => value + 1);
235
+ return;
236
+ }
237
+ setInstallError(result.error ?? "Install failed.");
238
+ });
239
+ }, [installedNow, installing, selected]);
240
+
241
+ /**
242
+ * Pane keys go through the table's key handler rather than a global shortcut:
243
+ * while the DataTable holds key focus it consumes plain letters, so a
244
+ * `useShortcut` for "i" never fires.
245
+ */
246
+ const handleRootKeyDown = useCallback((event: DataTableKeyEvent) => {
247
+ if (searchFocused) return;
248
+ const key = (event.name ?? "").toLowerCase();
249
+ if (key === "i") {
250
+ installSelected();
251
+ return true;
252
+ }
253
+ if (key === "e") {
254
+ toggleSelected();
255
+ return true;
256
+ }
257
+ if (key === "r") {
258
+ refresh(true);
259
+ return true;
260
+ }
261
+ if (key === "/") {
262
+ focusSearch();
263
+ return true;
264
+ }
265
+ return undefined;
266
+ }, [focusSearch, installSelected, refresh, searchFocused, toggleSelected]);
267
+
268
+ useShortcut((event) => {
269
+ if (!focused || searchFocused) return;
270
+ const key = (event.name ?? event.key ?? "").toLowerCase();
271
+ if (key === "r" && isPlainKey(event)) {
272
+ event.preventDefault?.();
273
+ refresh(true);
274
+ return;
275
+ }
276
+ if (key === "e" && isPlainKey(event)) {
277
+ event.preventDefault?.();
278
+ toggleSelected();
279
+ return;
280
+ }
281
+ if (key === "/" && isPlainKey(event)) {
282
+ event.preventDefault?.();
283
+ focusSearch();
284
+ return;
285
+ }
286
+ if (key === "i" && isPlainKey(event)) {
287
+ event.preventDefault?.();
288
+ installSelected();
289
+ }
290
+ });
291
+
292
+ const info: PaneFooterSegment[] = [];
293
+ if (status === "loading") info.push({ id: "loading", parts: [{ text: "loading", tone: "muted" }] });
294
+ if (status === "error") info.push({ id: "error", parts: [{ text: "catalog unavailable", tone: "warning" }] });
295
+ if (stale) info.push({ id: "stale", parts: [{ text: "stale catalog", tone: "warning" }] });
296
+ if (installing) {
297
+ info.push({ id: "installing", parts: [{ text: `installing ${installing}`, tone: "muted" }] });
298
+ }
299
+ if (installError) info.push({ id: "install-error", parts: [{ text: installError, tone: "warning" }] });
300
+ if (installedNow.length > 0 && !installing) {
301
+ info.push({ id: "restart", parts: [{ text: "restart to load new plugins", tone: "warning" }] });
302
+ }
303
+ if (status === "ready" && fetchedAt && !stale) {
304
+ info.push({ id: "updated", parts: [{ text: formatRelativeAge(fetchedAt), tone: "muted" }] });
305
+ }
306
+
307
+ useExternalLinkFooter({
308
+ registrationId: PLUGIN_MARKETPLACE_PANE_ID,
309
+ focused,
310
+ url: selected ? registryPluginUrl(selected.id) : null,
311
+ source: selected ? "gloom.sh" : null,
312
+ info,
313
+ hints: selected && isInstallable(selected) && !installedNow.includes(selected.id) && getPluginInstaller()
314
+ ? [{ id: "install", key: "i", label: "nstall", onPress: installSelected }]
315
+ : selected?.installed && selected.toggleable
316
+ ? [{ id: "toggle", key: "e", label: selected.enabled ? "disable" : "nable", onPress: toggleSelected }]
317
+ : [],
318
+ });
319
+
320
+ const columns = useMemo(() => buildColumns(width), [width]);
321
+
322
+ if (status === "loading" && entries.length === 0) {
323
+ return (
324
+ <Box flexDirection="column" width={width} height={height}>
325
+ <Box flexGrow={1} alignItems="center" justifyContent="center">
326
+ <Spinner />
327
+ </Box>
328
+ </Box>
329
+ );
330
+ }
331
+
332
+ return (
333
+ <Box flexDirection="column" width={width} height={height}>
334
+ <DataTableStackView<MarketplaceEntry, Column>
335
+ focused={focused && !searchFocused}
336
+ detailOpen={detailOpen && !!selected}
337
+ onBack={() => setDetailOpen(false)}
338
+ detailContent={selected ? <EntryDetail entry={selected} width={width} /> : null}
339
+ detailTitle={selected?.name}
340
+ rootBefore={(
341
+ <InputSearchBar
342
+ value={query}
343
+ focused={focused && !detailOpen}
344
+ active={searchFocused}
345
+ width={width}
346
+ focusToken={searchFocusToken}
347
+ inputRef={searchInputRef}
348
+ placeholder="name or category"
349
+ debounceMs={80}
350
+ onFocus={focusSearch}
351
+ onBlur={blurSearch}
352
+ onNavigateDown={blurSearch}
353
+ onQueryChange={setQuery}
354
+ />
355
+ )}
356
+ selection={{
357
+ kind: "id",
358
+ selectedId: selected?.id ?? null,
359
+ getId: (entry) => entry.id,
360
+ onChange: (id) => setSelectedId(typeof id === "string" ? id : null),
361
+ }}
362
+ onRootKeyDown={handleRootKeyDown}
363
+ onDetailKeyDown={handleRootKeyDown}
364
+ onActivate={() => setDetailOpen(true)}
365
+ rootWidth={width}
366
+ rootHeight={height}
367
+ columns={columns}
368
+ items={rows}
369
+ getItemKey={(entry) => entry.id}
370
+ sortColumnId={sortColumn}
371
+ sortDirection={sortColumn === "name" ? "asc" : "desc"}
372
+ onHeaderClick={(columnId) => {
373
+ // Only these two columns have a meaningful order; the rest keep the
374
+ // curated ranking rather than pretending to be sortable.
375
+ if (columnId === "name" || columnId === "stars") {
376
+ setSortColumn((current) => (current === columnId ? null : columnId));
377
+ }
378
+ }}
379
+ renderCell={(entry, column, _index, rowState) => renderCell(entry, column, rowState, installedNow)}
380
+ emptyStateTitle={status === "error" ? "Plugin catalog unavailable." : "No plugins match."}
381
+ emptyStateHint={status === "error" ? "Press r to retry." : undefined}
382
+ />
383
+ </Box>
384
+ );
385
+ }
@@ -0,0 +1,46 @@
1
+ import type { InstalledPlugin } from "./model";
2
+
3
+ /**
4
+ * The marketplace pane needs two things no other pane does: the full plugin
5
+ * catalog including entries that failed to load, and the ability to enable and
6
+ * disable them. Neither is on the render-time plugin runtime, and widening the
7
+ * public plugin API for a single built-in consumer would be the wrong trade.
8
+ *
9
+ * So the app binds a host here at startup, the same way the layout manager
10
+ * receives its dispatch from `bindAppPanePluginRegistry`.
11
+ */
12
+
13
+ export interface MarketplaceHost {
14
+ listInstalled(): InstalledPlugin[];
15
+ setPluginEnabled(pluginId: string, enabled: boolean): void;
16
+ }
17
+
18
+ let host: MarketplaceHost | null = null;
19
+
20
+ export function setMarketplaceHost(next: MarketplaceHost | null): void {
21
+ host = next;
22
+ }
23
+
24
+ export function getMarketplaceHost(): MarketplaceHost | null {
25
+ return host;
26
+ }
27
+
28
+ /**
29
+ * Installing a plugin means running git and bun, which only the Bun-hosted
30
+ * renderers can do — and the desktop view has to ask its Bun process over RPC.
31
+ * Rather than let this pane reach for a renderer API and break the
32
+ * renderer-neutrality rule, each renderer installs its own implementation at
33
+ * startup. A renderer that leaves it unset (the browser) gets the install
34
+ * command shown instead of a button.
35
+ */
36
+ export type PluginInstaller = (ref: string) => Promise<{ ok: boolean; error?: string }>;
37
+
38
+ let installer: PluginInstaller | null = null;
39
+
40
+ export function setPluginInstaller(next: PluginInstaller | null): void {
41
+ installer = next;
42
+ }
43
+
44
+ export function getPluginInstaller(): PluginInstaller | null {
45
+ return installer;
46
+ }
@@ -19,8 +19,7 @@ describe("composeBuiltinPlugin", () => {
19
19
  test("combines every declarative contribution under the parent plugin", async () => {
20
20
  const registeredBrokers: string[] = [];
21
21
  const first: PluginModule = {
22
- cliCommands: [{ name: "legacy", description: "Legacy", execute: () => {} }],
23
- cli: { commands: [{ name: "typed", summary: "Typed" }] },
22
+ cliCommands: [{ name: "example", description: "Example", execute: () => {} }],
24
23
  panes: [{ id: "one", name: "One", component: () => null, defaultPosition: "right" }],
25
24
  paneTemplates: [{ id: "one-pane", paneId: "one", label: "One", description: "One" }],
26
25
  capabilities: [{ id: "capability-one" } as never],
@@ -40,8 +39,7 @@ describe("composeBuiltinPlugin", () => {
40
39
  modules: [first, second],
41
40
  });
42
41
 
43
- expect(plugin.cliCommands?.map((command) => command.name)).toEqual(["legacy"]);
44
- expect(plugin.cli?.commands?.map((command) => command.name)).toEqual(["typed"]);
42
+ expect(plugin.cliCommands?.map((command) => command.name)).toEqual(["example"]);
45
43
  expect(plugin.panes?.map((pane) => pane.id)).toEqual(["one", "two"]);
46
44
  expect(plugin.paneTemplates?.map((template) => template.id)).toEqual(["one-pane"]);
47
45
  expect(plugin.capabilities?.map((capability) => capability.id)).toEqual(["capability-one"]);