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
@@ -3,6 +3,7 @@ import { Box, Text } from "../../../ui";
3
3
  import { usePaneSettingValue, usePaneTicker } from "../../../state/app/context";
4
4
  import { colors } from "../../../theme/colors";
5
5
  import { isPlainKey } from "../../../utils/keyboard";
6
+ import { formatCompact } from "../../../utils/format";
6
7
  import { formatExpDate, resolveOptionsTarget } from "../../../utils/options";
7
8
  import { useOptionsQuery, useResolvedEntryValue, useTickerFinancials } from "../../../market-data/hooks";
8
9
  import {
@@ -21,15 +22,19 @@ import {
21
22
  type OptionSide,
22
23
  } from "../options-calculator/model";
23
24
  import { buildChainCalcParams, resolveCalcSide } from "./calc-seed";
25
+ import { calculateOptionGreeks, calculateOptionsSummary, type OptionsSummary } from "./analytics";
24
26
  import {
25
- OPTION_COLUMNS,
27
+ DEFAULT_OPTION_FIELD_IDS,
26
28
  buildStrikeList,
29
+ createOptionColumns,
27
30
  findNearestStrikeIndex,
31
+ formatIv,
28
32
  optionColumnColor,
29
33
  renderOptionCell,
30
34
  resolveDefaultStrikeTarget,
35
+ resolveOptionFieldIds,
31
36
  } from "./table";
32
- import type { OptionColumn, OptionTableRow, OptionsViewProps } from "./types";
37
+ import type { OptionColumn, OptionFieldId, OptionTableRow, OptionsViewProps } from "./types";
33
38
  import {
34
39
  buildOptionQuoteTargets,
35
40
  overlayOptionRowQuotes,
@@ -39,6 +44,48 @@ import {
39
44
  import { useOptionsAccessFooter } from "./footer";
40
45
  import { useLiveStreamingSetting } from "../shared/live-streaming";
41
46
 
47
+ type SummaryMetric = { label: string; value: string };
48
+
49
+ function formatRatio(value: number | null | undefined): string {
50
+ return value == null || !Number.isFinite(value) ? "—" : value.toFixed(2);
51
+ }
52
+
53
+ function SummaryRow({ metrics }: { metrics: SummaryMetric[] }) {
54
+ return (
55
+ <Box flexDirection="row" height={1} gap={3} overflow="hidden">
56
+ {metrics.map((metric) => (
57
+ <Box key={metric.label} flexDirection="row" flexShrink={0}>
58
+ <Text fg={colors.textDim}>{`${metric.label} `}</Text>
59
+ <Text fg={colors.textBright}>{metric.value}</Text>
60
+ </Box>
61
+ ))}
62
+ </Box>
63
+ );
64
+ }
65
+
66
+ function OptionsSummaryStrip({ summary, secondary }: {
67
+ summary: OptionsSummary | null;
68
+ secondary: boolean;
69
+ }) {
70
+ const volatility: SummaryMetric[] = [
71
+ { label: "ATM IV", value: formatIv(summary?.atmImpliedVolatility ?? undefined) },
72
+ { label: "HV30", value: formatIv(summary?.historicalVolatility30d ?? undefined) },
73
+ { label: "IV/HV", value: formatRatio(summary?.impliedHistoricalRatio) },
74
+ ];
75
+ return (
76
+ <Box flexDirection="column" height={secondary ? 2 : 1}>
77
+ <SummaryRow metrics={volatility} />
78
+ {secondary && (
79
+ <SummaryRow metrics={[
80
+ { label: "EXP VOL", value: summary ? formatCompact(summary.expirationVolume) : "—" },
81
+ { label: "P/C VOL", value: formatRatio(summary?.putCallVolumeRatio) },
82
+ { label: "P/C OI", value: formatRatio(summary?.putCallOpenInterestRatio) },
83
+ ]} />
84
+ )}
85
+ </Box>
86
+ );
87
+ }
88
+
42
89
  export function OptionsView({ width, height, focused, onCapture = () => {} }: OptionsViewProps) {
43
90
  const { ticker, financials } = usePaneTicker();
44
91
  const { createPaneFromTemplate } = usePluginAppActions();
@@ -61,6 +108,9 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
61
108
  const effectiveTicker = target?.effectiveTicker ?? "";
62
109
  const effectiveExchange = target?.effectiveExchange ?? "";
63
110
  const underlyingFinancials = useTickerFinancials(isOpt ? effectiveTicker : null, null);
111
+ const underlying = isOpt ? underlyingFinancials : financials;
112
+ const spot = underlying?.quote?.price;
113
+ const dividendYield = underlying?.fundamentals?.dividendYield;
64
114
  const instrument = target?.instrument ?? null;
65
115
  const baseRequest = target
66
116
  ? {
@@ -74,6 +124,8 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
74
124
  }
75
125
  : null;
76
126
  const [chainRefreshMinutes] = usePaneSettingValue<string>("chainRefreshMinutes", "");
127
+ const [storedOptionFieldIds] = usePaneSettingValue<OptionFieldId[]>("optionColumnIds", DEFAULT_OPTION_FIELD_IDS);
128
+ const optionFieldIds = useMemo(() => resolveOptionFieldIds(storedOptionFieldIds), [storedOptionFieldIds]);
77
129
  const initialChainEntry = useOptionsQuery(baseRequest);
78
130
  const initialChain = useResolvedEntryValue(initialChainEntry);
79
131
  const selectedExpiration = initialChain?.expirationDates[expIdx];
@@ -157,12 +209,27 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
157
209
  () => new Map(strikeChain?.puts.map((p) => [p.strike, p]) ?? []),
158
210
  [strikeChain],
159
211
  );
160
- const snapshotRows = useMemo<OptionTableRow[]>(() => strikes.map((strike) => ({
161
- strike,
162
- call: callsByStrike.get(strike),
163
- put: putsByStrike.get(strike),
164
- isPositionStrike: !!parsed && Math.abs(strike - parsed.strike) < 0.01,
165
- })), [callsByStrike, parsed, putsByStrike, strikes]);
212
+ const snapshotRows = useMemo<OptionTableRow[]>(() => {
213
+ const now = Date.now();
214
+ return strikes.map((strike) => {
215
+ const call = callsByStrike.get(strike);
216
+ const put = putsByStrike.get(strike);
217
+ return {
218
+ strike,
219
+ call,
220
+ put,
221
+ callGreeks: calculateOptionGreeks(call, "call", spot, dividendYield, now),
222
+ putGreeks: calculateOptionGreeks(put, "put", spot, dividendYield, now),
223
+ isPositionStrike: !!parsed && Math.abs(strike - parsed.strike) < 0.01,
224
+ };
225
+ });
226
+ }, [callsByStrike, dividendYield, parsed, putsByStrike, spot, strikes]);
227
+ const summary = useMemo(
228
+ () => strikeChain
229
+ ? calculateOptionsSummary(strikeChain, spot, underlying?.priceHistory ?? [])
230
+ : null,
231
+ [spot, strikeChain, underlying?.priceHistory],
232
+ );
166
233
  const visibleStrikeRange = visibleStrikeViewport?.key === viewportKey
167
234
  ? visibleStrikeViewport.range
168
235
  : null;
@@ -210,10 +277,10 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
210
277
  ),
211
278
  [optionQuoteEntries, optionQuoteFreshness, optionQuoteTargets],
212
279
  );
213
- const optionColumns: OptionColumn[] = OPTION_COLUMNS.map((column) => ({
280
+ const optionColumns = useMemo<OptionColumn[]>(() => createOptionColumns(optionFieldIds).map((column) => ({
214
281
  ...column,
215
- headerColor: optionColumnColor(column.id, colors.panel),
216
- }));
282
+ headerColor: optionColumnColor(column, colors.panel),
283
+ })), [optionFieldIds]);
217
284
 
218
285
  const selectedRow = rows[strikeIdx] ?? null;
219
286
  const calcParams = useMemo(() => buildChainCalcParams({
@@ -222,9 +289,9 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
222
289
  side: resolveCalcSide(calcSide, parsed?.side, selectedRow),
223
290
  // On an option ticker the pane quote is the contract's own price, so load
224
291
  // the underlying snapshot rather than silently using the option mark as spot.
225
- spot: (isOpt ? underlyingFinancials : financials)?.quote?.price,
226
- dividendYield: (isOpt ? underlyingFinancials : financials)?.fundamentals?.dividendYield,
227
- }), [calcSide, effectiveTicker, financials, isOpt, parsed?.side, selectedRow, underlyingFinancials]);
292
+ spot,
293
+ dividendYield,
294
+ }), [calcSide, dividendYield, effectiveTicker, parsed?.side, selectedRow, spot]);
228
295
 
229
296
  const openCalculator = useCallback(() => {
230
297
  if (!calcParams) return;
@@ -243,10 +310,10 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
243
310
  rowState: { selected: boolean },
244
311
  ) => {
245
312
  const cell = renderOptionCell(row, column, index, rowState);
246
- if (column.id === "strike") return cell;
313
+ if (!column.side) return cell;
247
314
  // Clicking a call or put cell is the mouse way to choose which contract
248
315
  // [c]alc opens, so it has to select the row itself as well.
249
- const side: OptionSide = column.id.startsWith("call") ? "call" : "put";
316
+ const side: OptionSide = column.side;
250
317
  return {
251
318
  ...cell,
252
319
  onMouseDown: () => {
@@ -277,12 +344,12 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
277
344
 
278
345
  useEffect(() => {
279
346
  if (strikes.length === 0 || userSelectedStrikeRef.current) return;
280
- const targetStrike = resolveDefaultStrikeTarget(parsed?.strike, financials?.quote?.price);
347
+ const targetStrike = resolveDefaultStrikeTarget(parsed?.strike, spot);
281
348
  if (targetStrike == null) return;
282
349
  setScrollToIndexAlign("center");
283
350
  setStrikeIdx(findNearestStrikeIndex(strikes, targetStrike));
284
351
  setAutoScrollVersion((version) => version + 1);
285
- }, [expIdx, financials?.quote?.price, parsed?.strike, strikes]);
352
+ }, [expIdx, parsed?.strike, spot, strikes]);
286
353
 
287
354
  useShortcut((event) => {
288
355
  if (event.defaultPrevented || event.propagationStopped || event.targetEditable) return;
@@ -356,25 +423,6 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
356
423
  return true;
357
424
  }
358
425
 
359
- if (isPlainKey(event, "j", "down")) {
360
- if (strikes.length === 0) return true;
361
- event.preventDefault?.();
362
- event.stopPropagation?.();
363
- userSelectedStrikeRef.current = true;
364
- setScrollToIndexAlign("nearest");
365
- setStrikeIdx((i) => Math.min(i + 1, strikes.length - 1));
366
- return true;
367
- }
368
- if (isPlainKey(event, "k", "up")) {
369
- if (strikes.length === 0) return true;
370
- event.preventDefault?.();
371
- event.stopPropagation?.();
372
- userSelectedStrikeRef.current = true;
373
- setScrollToIndexAlign("nearest");
374
- setStrikeIdx((i) => Math.max(i - 1, 0));
375
- return true;
376
- }
377
-
378
426
  return false;
379
427
  }, [
380
428
  calcParams,
@@ -383,7 +431,6 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
383
431
  interactive,
384
432
  openCalculator,
385
433
  selectAdjacentExpiration,
386
- strikes.length,
387
434
  ]);
388
435
 
389
436
  if (!ticker) {
@@ -399,13 +446,18 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
399
446
  ? ticker.metadata.positions.reduce((sum, p) => sum + p.shares, 0)
400
447
  : 0;
401
448
  const expirationTabsWidth = Math.max(width - 9 - (loading ? 2 : 0), 8);
402
- const tableHeight = Math.max(1, height - 1 - (isOpt && parsed ? 1 : 0));
449
+ const summaryRowCount = height >= 10 ? 2 : height >= 7 ? 1 : 0;
450
+ const tableHeight = Math.max(1, height - 1 - summaryRowCount - (isOpt && parsed ? 1 : 0));
403
451
  // The strip scrolls; without a marker a clipped last date reads as the last expiry.
404
452
  const expirationStripOverflows = chain.expirationDates
405
453
  .reduce((total, ts) => total + formatExpDate(ts).length + 2, 0) > expirationTabsWidth;
406
454
 
407
455
  return (
408
456
  <Box flexDirection="column" flexGrow={1} paddingX={1} onMouseDown={() => { if (!interactive) enterInteractive(); }}>
457
+ {summaryRowCount > 0 && (
458
+ <OptionsSummaryStrip summary={summary} secondary={summaryRowCount > 1} />
459
+ )}
460
+
409
461
  <Box flexDirection="row" height={1} gap={1}>
410
462
  <Text fg={colors.textDim}>Exp:</Text>
411
463
  <Box width={expirationTabsWidth} height={1} overflow="hidden">
@@ -450,11 +502,10 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
450
502
  setStrikeIdx(index);
451
503
  },
452
504
  }}
453
- onCursorChange={(_row, index) => {
505
+ onCursorChange={() => {
454
506
  userSelectedStrikeRef.current = true;
455
507
  setScrollToIndexAlign("nearest");
456
508
  enterInteractive();
457
- setStrikeIdx(index);
458
509
  }}
459
510
  onRootKeyDown={handleTableKeyDown}
460
511
  headerScrollId="options-table-header-scroll"
@@ -0,0 +1,80 @@
1
+ import { createThrottledFetch } from "../../../utils/throttled-fetch";
2
+ import type { RegistryFeed, RegistryPlugin } from "./model";
3
+
4
+ /**
5
+ * Exported so the hosted build's CSP can be checked against it. The browser
6
+ * renderer can only reach origins listed in `connect-src`, and a blocked fetch
7
+ * shows up as an empty catalog rather than an error, so the two are pinned
8
+ * together by a test.
9
+ */
10
+ export const REGISTRY_ORIGIN = "https://plugins.gloom.sh";
11
+ const REGISTRY_URL = `${REGISTRY_ORIGIN}/registry.json`;
12
+ const FRESH_MS = 15 * 60_000;
13
+
14
+ const registryFetch = createThrottledFetch({
15
+ requestsPerMinute: 20,
16
+ maxRetries: 2,
17
+ timeoutMs: 10_000,
18
+ backoffBaseMs: 500,
19
+ dedupeGetRequests: true,
20
+ defaultHeaders: { Accept: "application/json" },
21
+ });
22
+
23
+ interface CacheEntry {
24
+ plugins: RegistryPlugin[];
25
+ fetchedAt: number;
26
+ }
27
+
28
+ let cache: CacheEntry | null = null;
29
+ let inFlight: Promise<RegistryPlugin[]> | null = null;
30
+
31
+ export interface FeedResult {
32
+ plugins: RegistryPlugin[];
33
+ fetchedAt: number | null;
34
+ /** Served from cache after a failed refresh. */
35
+ stale: boolean;
36
+ error: string | null;
37
+ }
38
+
39
+ function parseFeed(body: unknown): RegistryPlugin[] {
40
+ const feed = body as RegistryFeed | null;
41
+ if (!feed || !Array.isArray(feed.plugins)) throw new Error("Malformed registry feed");
42
+ // Only fields the pane actually reads are required; the rest degrade to defaults
43
+ // so a feed built by a newer registry version still renders.
44
+ return feed.plugins.filter((plugin): plugin is RegistryPlugin => (
45
+ !!plugin && typeof plugin.id === "string" && typeof plugin.name === "string"
46
+ ));
47
+ }
48
+
49
+ export async function loadRegistry(options: { force?: boolean } = {}): Promise<FeedResult> {
50
+ const now = Date.now();
51
+ if (!options.force && cache && now - cache.fetchedAt < FRESH_MS) {
52
+ return { plugins: cache.plugins, fetchedAt: cache.fetchedAt, stale: false, error: null };
53
+ }
54
+
55
+ if (!inFlight) {
56
+ inFlight = (async () => {
57
+ const response = await registryFetch.fetch(REGISTRY_URL);
58
+ if (!response.ok) throw new Error(`Registry request failed (${response.status})`);
59
+ return parseFeed(await response.json());
60
+ })().finally(() => {
61
+ inFlight = null;
62
+ });
63
+ }
64
+
65
+ try {
66
+ const plugins = await inFlight;
67
+ cache = { plugins, fetchedAt: Date.now() };
68
+ return { plugins, fetchedAt: cache.fetchedAt, stale: false, error: null };
69
+ } catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ // A stale catalog is far more useful than an empty pane, and the footer
72
+ // says which one the user is looking at.
73
+ if (cache) return { plugins: cache.plugins, fetchedAt: cache.fetchedAt, stale: true, error: message };
74
+ return { plugins: [], fetchedAt: null, stale: false, error: message };
75
+ }
76
+ }
77
+
78
+ export function registryPluginUrl(id: string): string {
79
+ return `https://gloom.sh/plugins/${id}`;
80
+ }
@@ -0,0 +1,31 @@
1
+ import type { PluginModule } from "../plugin-module";
2
+ import { PluginMarketplacePane, PLUGIN_MARKETPLACE_PANE_ID } from "./pane";
3
+
4
+ export { PLUGIN_MARKETPLACE_PANE_ID } from "./pane";
5
+ export { setMarketplaceHost } from "./store";
6
+
7
+ export const pluginMarketplaceModule: PluginModule = {
8
+ panes: [
9
+ {
10
+ id: PLUGIN_MARKETPLACE_PANE_ID,
11
+ name: "Plugins",
12
+ icon: "P",
13
+ component: PluginMarketplacePane,
14
+ defaultPosition: "right",
15
+ defaultMode: "floating",
16
+ defaultFloatingSize: { width: 118, height: 34 },
17
+ },
18
+ ],
19
+
20
+ paneTemplates: [
21
+ {
22
+ id: "plugin-marketplace-pane",
23
+ paneId: PLUGIN_MARKETPLACE_PANE_ID,
24
+ label: "Plugins",
25
+ description: "Browse and install Gloomberb plugins, and enable or disable the ones you have.",
26
+ keywords: ["plugin", "plugins", "marketplace", "install", "extend", "addon", "extension"],
27
+ shortcut: { prefix: "PL" },
28
+ createInstance: () => ({ placement: "floating" }),
29
+ },
30
+ ],
31
+ };
@@ -0,0 +1,200 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ collectCategories,
5
+ isInstallable,
6
+ mergeCatalog,
7
+ sortEntries,
8
+ unsupportedLabel,
9
+ type InstalledPlugin,
10
+ type RegistryPlugin,
11
+ } from "./model";
12
+
13
+ function registryPlugin(overrides: Partial<RegistryPlugin> & Pick<RegistryPlugin, "id">): RegistryPlugin {
14
+ return {
15
+ name: overrides.id,
16
+ repo: `gloom-sh/${overrides.id}`,
17
+ tagline: "",
18
+ author: { name: "Someone" },
19
+ categories: ["data"],
20
+ targets: ["cli", "tui", "desktop", "web"],
21
+ hosts: [],
22
+ contributes: { panes: [], capabilities: [], broker: false },
23
+ tier: "community",
24
+ bundled: false,
25
+ stars: 0,
26
+ ...overrides,
27
+ };
28
+ }
29
+
30
+ function installedPlugin(overrides: Partial<InstalledPlugin> & Pick<InstalledPlugin, "id">): InstalledPlugin {
31
+ return {
32
+ name: overrides.id,
33
+ version: "1.0.0",
34
+ toggleable: true,
35
+ enabled: true,
36
+ source: "external",
37
+ ...overrides,
38
+ };
39
+ }
40
+
41
+ /**
42
+ * The merge is the only real logic in this pane, and it reconciles three
43
+ * sources that disagree during normal use. Each case below is a state a user
44
+ * can actually reach, and getting one wrong either hides a plugin they have
45
+ * installed or offers an install button for something already present.
46
+ */
47
+ describe("mergeCatalog", () => {
48
+ test("marks a bundled plugin installed even when the local catalog has not reported it", () => {
49
+ // Happens whenever the feed ships an entry before the user upgrades.
50
+ const [entry] = mergeCatalog({
51
+ registry: [registryPlugin({ id: "gloomberb-cloud", bundled: true, tier: "official" })],
52
+ installed: [],
53
+ target: "tui",
54
+ });
55
+
56
+ expect(entry?.installed).toBe(true);
57
+ expect(entry?.enabled).toBe(true);
58
+ });
59
+
60
+ test("takes enabled state from the local catalog, not the registry", () => {
61
+ const [entry] = mergeCatalog({
62
+ registry: [registryPlugin({ id: "hackernews" })],
63
+ installed: [installedPlugin({ id: "hackernews", enabled: false, version: "0.2.0" })],
64
+ target: "tui",
65
+ });
66
+
67
+ expect(entry?.installed).toBe(true);
68
+ expect(entry?.enabled).toBe(false);
69
+ expect(entry?.installedVersion).toBe("0.2.0");
70
+ });
71
+
72
+ test("keeps a side-loaded plugin the registry has never seen", () => {
73
+ const entries = mergeCatalog({
74
+ registry: [registryPlugin({ id: "hackernews" })],
75
+ installed: [installedPlugin({ id: "my-private-plugin" })],
76
+ target: "tui",
77
+ });
78
+
79
+ const sideloaded = entries.find((entry) => entry.id === "my-private-plugin");
80
+ expect(sideloaded?.installed).toBe(true);
81
+ expect(sideloaded?.categories).toEqual(["unlisted"]);
82
+ });
83
+
84
+ test("flags a plugin the current renderer cannot run without calling it uninstalled", () => {
85
+ const [entry] = mergeCatalog({
86
+ registry: [registryPlugin({ id: "ibkr-gateway", targets: ["cli", "tui", "desktop"] })],
87
+ installed: [installedPlugin({ id: "ibkr-gateway" })],
88
+ target: "web",
89
+ });
90
+
91
+ expect(entry?.installed).toBe(true);
92
+ expect(entry?.unsupportedHere).toBe(true);
93
+ expect(unsupportedLabel(entry!)).toBe("Desktop only");
94
+ });
95
+
96
+ test("does not flag a plugin that supports the current renderer", () => {
97
+ const [entry] = mergeCatalog({
98
+ registry: [registryPlugin({ id: "hackernews" })],
99
+ installed: [],
100
+ target: "web",
101
+ });
102
+
103
+ expect(entry?.unsupportedHere).toBe(false);
104
+ expect(unsupportedLabel(entry!)).toBeNull();
105
+ });
106
+
107
+ test("surfaces a load error so a broken install is visible rather than missing", () => {
108
+ const [entry] = mergeCatalog({
109
+ registry: [registryPlugin({ id: "broken" })],
110
+ installed: [installedPlugin({ id: "broken", loadError: "SyntaxError" })],
111
+ target: "tui",
112
+ });
113
+
114
+ expect(entry?.loadError).toBe("SyntaxError");
115
+ });
116
+ });
117
+
118
+ describe("sortEntries", () => {
119
+ test("puts what is installed above the rest of the catalog", () => {
120
+ // One list rather than two tabs: acting on what you already have should not
121
+ // require a mode switch, and an uninstalled plugin should never outrank one
122
+ // that is present just because it has more stars.
123
+ const entries = mergeCatalog({
124
+ registry: [
125
+ registryPlugin({ id: "popular-uninstalled", tier: "official", stars: 5000 }),
126
+ registryPlugin({ id: "quiet-installed", tier: "community", stars: 0 }),
127
+ ],
128
+ installed: [installedPlugin({ id: "quiet-installed" })],
129
+ target: "tui",
130
+ });
131
+
132
+ expect(sortEntries(entries).map((entry) => entry.id))
133
+ .toEqual(["quiet-installed", "popular-uninstalled"]);
134
+ });
135
+
136
+ test("puts the featured plugin first, then tier, then stars", () => {
137
+ const entries = mergeCatalog({
138
+ registry: [
139
+ registryPlugin({ id: "community-popular", tier: "community", stars: 900 }),
140
+ registryPlugin({ id: "official-quiet", tier: "official", stars: 2 }),
141
+ registryPlugin({ id: "cloud", tier: "official", featured: true, bundled: true, stars: 0 }),
142
+ registryPlugin({ id: "verified-mid", tier: "verified", stars: 50 }),
143
+ ],
144
+ installed: [],
145
+ target: "tui",
146
+ });
147
+
148
+ // All uninstalled, so the curated order applies within that half.
149
+ expect(sortEntries(entries).map((entry) => entry.id)).toEqual([
150
+ "cloud",
151
+ "official-quiet",
152
+ "verified-mid",
153
+ "community-popular",
154
+ ]);
155
+ });
156
+ });
157
+
158
+ describe("collectCategories", () => {
159
+ test("deduplicates and sorts categories across the catalog", () => {
160
+ const entries = mergeCatalog({
161
+ registry: [
162
+ registryPlugin({ id: "a", categories: ["news", "data"] }),
163
+ registryPlugin({ id: "b", categories: ["data"] }),
164
+ ],
165
+ installed: [],
166
+ target: "tui",
167
+ });
168
+
169
+ expect(collectCategories(entries)).toEqual(["data", "news"]);
170
+ });
171
+ });
172
+
173
+ describe("isInstallable", () => {
174
+ test("offers an install only for a catalog plugin that is absent", () => {
175
+ const [available, bundled, installed] = mergeCatalog({
176
+ registry: [
177
+ registryPlugin({ id: "available" }),
178
+ registryPlugin({ id: "bundled", bundled: true }),
179
+ registryPlugin({ id: "installed" }),
180
+ ],
181
+ installed: [installedPlugin({ id: "installed" })],
182
+ target: "tui",
183
+ });
184
+
185
+ expect(isInstallable(available!)).toBe(true);
186
+ // Ships with the app; there is nothing to fetch.
187
+ expect(isInstallable(bundled!)).toBe(false);
188
+ expect(isInstallable(installed!)).toBe(false);
189
+ });
190
+
191
+ test("does not offer an install without a repository to clone", () => {
192
+ const [entry] = mergeCatalog({
193
+ registry: [],
194
+ installed: [installedPlugin({ id: "sideloaded" })],
195
+ target: "tui",
196
+ });
197
+
198
+ expect(isInstallable(entry!)).toBe(false);
199
+ });
200
+ });