gloomberb 0.11.3 → 0.12.0

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