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
@@ -1,12 +1,20 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { AppNotificationRequest, GloomPluginContext } from "../../../../../types/plugin";
3
3
  import type { MarketNewsItem, NewsQueryState } from "../../../../../types/news-source";
4
+ import {
5
+ BREAKING_NEWS_MUTED_SECTORS_KEY,
6
+ BREAKING_NEWS_SCOPE_KEY,
7
+ } from "./filters";
4
8
  import {
5
9
  BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY,
6
10
  setupBreakingNewsNotifications,
7
11
  } from "./notifications";
8
12
 
9
- function article(id: string, title = id): MarketNewsItem {
13
+ function article(
14
+ id: string,
15
+ title = id,
16
+ overrides: Partial<MarketNewsItem> = {},
17
+ ): MarketNewsItem {
10
18
  return {
11
19
  id,
12
20
  title,
@@ -18,16 +26,11 @@ function article(id: string, title = id): MarketNewsItem {
18
26
  sectors: [],
19
27
  categories: ["general"],
20
28
  tickers: ["AAPL"],
21
- scores: {
22
- importance: 90,
23
- urgency: 90,
24
- marketImpact: 80,
25
- novelty: 80,
26
- confidence: 90,
27
- },
29
+ scores: { importance: 90, urgency: 90, marketImpact: 80, novelty: 80, confidence: 90 },
28
30
  importance: 90,
29
31
  isBreaking: true,
30
32
  isDeveloping: false,
33
+ ...overrides,
31
34
  };
32
35
  }
33
36
 
@@ -43,58 +46,159 @@ function ready(articles: MarketNewsItem[]): NewsQueryState {
43
46
  };
44
47
  }
45
48
 
49
+ interface Harness {
50
+ ctx: GloomPluginContext;
51
+ notifications: AppNotificationRequest[];
52
+ shownPanes: string[];
53
+ state: Map<string, unknown>;
54
+ emit(articles: MarketNewsItem[]): void;
55
+ isWatching(): boolean;
56
+ }
57
+
58
+ function harness(options: {
59
+ settings?: Record<string, unknown>;
60
+ watchedSymbols?: string[];
61
+ } = {}): Harness {
62
+ let listener: ((state: NewsQueryState) => void) | null = null;
63
+ const notifications: AppNotificationRequest[] = [];
64
+ const shownPanes: string[] = [];
65
+ const state = new Map<string, unknown>();
66
+ const settings: Record<string, unknown> = {
67
+ [BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY]: true,
68
+ ...options.settings,
69
+ };
70
+ const watched = options.watchedSymbols ?? ["AAPL"];
71
+
72
+ const ctx = {
73
+ configState: { get: (key: string) => settings[key] ?? null },
74
+ persistence: {
75
+ getState: (key: string) => state.get(key) ?? null,
76
+ setState: (key: string, value: unknown) => void state.set(key, value),
77
+ },
78
+ tickerRepository: {
79
+ loadAllTickers: async () =>
80
+ watched.map((symbol) => ({
81
+ metadata: { ticker: symbol, watchlists: ["default"], portfolios: [], positions: [] },
82
+ })),
83
+ },
84
+ watchNewsQuery: (_query: unknown, next: (state: NewsQueryState) => void) => {
85
+ listener = next;
86
+ return () => {
87
+ listener = null;
88
+ };
89
+ },
90
+ on: () => () => {},
91
+ notify: (notification: AppNotificationRequest) => void notifications.push(notification),
92
+ showPane: (paneId: string) => void shownPanes.push(paneId),
93
+ log: { warn: () => {} },
94
+ } as unknown as GloomPluginContext;
95
+
96
+ return {
97
+ ctx,
98
+ notifications,
99
+ shownPanes,
100
+ state,
101
+ emit: (articles) => listener?.(ready(articles)),
102
+ isWatching: () => listener !== null,
103
+ };
104
+ }
105
+
106
+ /** Lets the async watchlist load settle before articles arrive. */
107
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
108
+
46
109
  describe("breaking news notifications", () => {
47
- test("primes on the first ready batch and notifies for later unseen articles", () => {
48
- let enabled = true;
49
- let listener: ((state: NewsQueryState) => void) | null = null;
50
- const notifications: AppNotificationRequest[] = [];
51
- const shownPanes: string[] = [];
52
-
53
- const ctx = {
54
- configState: {
55
- get: (key: string) => key === BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY ? enabled : null,
56
- },
57
- watchNewsQuery: (_query: unknown, nextListener: (state: NewsQueryState) => void) => {
58
- listener = nextListener;
59
- return () => {
60
- listener = null;
61
- };
62
- },
63
- on: (_event: string, _handler: unknown) => () => {},
64
- notify: (notification: AppNotificationRequest) => {
65
- notifications.push(notification);
66
- },
67
- showPane: (paneId: string) => {
68
- shownPanes.push(paneId);
69
- },
70
- log: {
71
- warn: () => {},
72
- },
73
- } as unknown as GloomPluginContext;
74
-
75
- const dispose = setupBreakingNewsNotifications(ctx);
76
- const oldArticle = article("old", "Old headline");
77
- const newArticle = article("new", "New headline");
78
-
79
- listener?.(ready([oldArticle]));
80
- listener?.(ready([newArticle, oldArticle]));
81
-
82
- expect(notifications).toHaveLength(1);
83
- expect(notifications[0]).toMatchObject({
110
+ test("primes on the first ready batch and notifies for later unseen articles", async () => {
111
+ const h = harness();
112
+ const dispose = setupBreakingNewsNotifications(h.ctx);
113
+ await settle();
114
+
115
+ const old = article("old", "Old headline");
116
+ const fresh = article("new", "New headline");
117
+
118
+ h.emit([old]);
119
+ h.emit([fresh, old]);
120
+
121
+ expect(h.notifications).toHaveLength(1);
122
+ expect(h.notifications[0]).toMatchObject({
84
123
  title: "Breaking News",
85
124
  body: "New headline",
86
125
  desktop: "always",
87
- persistent: true,
88
126
  });
127
+ // Breaking toasts must expire on their own; persistent ones stacked up.
128
+ expect(h.notifications[0]?.persistent).toBeUndefined();
129
+ expect(h.notifications[0]?.duration).toBeGreaterThan(0);
89
130
 
90
- notifications[0]!.action?.onClick();
91
- expect(shownPanes).toEqual(["news-breaking"]);
92
-
93
- enabled = false;
94
- listener?.(ready([article("newer", "Newer headline"), newArticle, oldArticle]));
95
- expect(notifications).toHaveLength(1);
131
+ h.notifications[0]!.action?.onClick();
132
+ expect(h.shownPanes).toEqual(["news-breaking"]);
96
133
 
97
134
  dispose();
98
- expect(listener).toBeNull();
135
+ expect(h.isWatching()).toBe(false);
136
+ });
137
+
138
+ test("default scope drops stories about tickers the user does not track", async () => {
139
+ const h = harness({ watchedSymbols: ["AAPL"] });
140
+ setupBreakingNewsNotifications(h.ctx);
141
+ await settle();
142
+
143
+ h.emit([article("seed")]);
144
+ h.emit([
145
+ article("unrelated", "Unrelated ticker", { tickers: ["XOM"] }),
146
+ article("seed"),
147
+ ]);
148
+ expect(h.notifications).toHaveLength(0);
149
+
150
+ h.emit([article("macro", "Market-wide event", { tickers: [] }), article("seed")]);
151
+ expect(h.notifications).toHaveLength(1);
152
+ expect(h.notifications[0]?.body).toBe("Market-wide event");
153
+ });
154
+
155
+ test("muted sectors suppress otherwise in-scope stories", async () => {
156
+ const h = harness({
157
+ settings: { [BREAKING_NEWS_MUTED_SECTORS_KEY]: ["health_care"] },
158
+ });
159
+ setupBreakingNewsNotifications(h.ctx);
160
+ await settle();
161
+
162
+ h.emit([article("seed")]);
163
+ h.emit([
164
+ article("pharma", "Pharma story", { sectors: ["health_care"] }),
165
+ article("seed"),
166
+ ]);
167
+ expect(h.notifications).toHaveLength(0);
168
+ });
169
+
170
+ test("filtered-out articles are still marked seen so relaxing a filter replays nothing", async () => {
171
+ const h = harness({ settings: { [BREAKING_NEWS_SCOPE_KEY]: "watchlist" } });
172
+ setupBreakingNewsNotifications(h.ctx);
173
+ await settle();
174
+
175
+ h.emit([article("seed")]);
176
+ const macro = article("macro", "Market-wide event", { tickers: [] });
177
+ h.emit([macro, article("seed")]);
178
+ expect(h.notifications).toHaveLength(0);
179
+
180
+ // Same article arriving again must not notify once it has been observed.
181
+ h.emit([macro, article("seed")]);
182
+ expect(h.notifications).toHaveLength(0);
183
+ });
184
+
185
+ test("snoozing suppresses notifications until the window passes", async () => {
186
+ const h = harness();
187
+ setupBreakingNewsNotifications(h.ctx);
188
+ await settle();
189
+
190
+ h.emit([article("seed")]);
191
+ h.emit([article("first", "First"), article("seed")]);
192
+ expect(h.notifications).toHaveLength(1);
193
+
194
+ h.notifications[0]!.secondaryAction?.onClick();
195
+ const confirmation = h.notifications.length;
196
+
197
+ h.emit([article("second", "Second"), article("seed")]);
198
+ expect(h.notifications).toHaveLength(confirmation);
199
+
200
+ h.state.set("breaking-news-snooze-until", Date.now() - 1);
201
+ h.emit([article("third", "Third"), article("seed")]);
202
+ expect(h.notifications.at(-1)?.body).toBe("Third");
99
203
  });
100
204
  });
@@ -1,10 +1,22 @@
1
1
  import type { GloomPluginContext } from "../../../../../types/plugin";
2
2
  import type { MarketNewsItem, NewsQueryState } from "../../../../../types/news-source";
3
3
  import { NEWS_QUERY_PRESETS } from "../news/query-presets";
4
+ import {
5
+ BREAKING_NEWS_MUTED_SECTORS_KEY,
6
+ BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY,
7
+ BREAKING_NEWS_SCOPE_KEY,
8
+ parseBreakingScope,
9
+ parseMutedSectors,
10
+ selectNotifiableArticles,
11
+ } from "./filters";
4
12
 
5
- export const BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY = "breakingNewsNotificationsEnabled";
13
+ export { BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY };
6
14
 
7
15
  const MAX_SEEN_ARTICLE_IDS = 500;
16
+ /** Long enough to read a stacked burst, short enough to clear itself. */
17
+ const BREAKING_TOAST_DURATION_MS = 20_000;
18
+ const SNOOZE_DURATION_MS = 60 * 60_000;
19
+ const SNOOZE_STATE_KEY = "breaking-news-snooze-until";
8
20
 
9
21
  function isReadyState(state: NewsQueryState): boolean {
10
22
  return state.phase === "ready" || state.phase === "refreshing";
@@ -35,32 +47,70 @@ function notificationSubtitle(article: MarketNewsItem): string {
35
47
  return tickers ? `${article.source} ${tickers}` : article.source;
36
48
  }
37
49
 
38
- function notifyNewBreakingArticles(ctx: GloomPluginContext, articles: MarketNewsItem[]): void {
39
- const latest = [...articles].sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime())[0];
40
- if (!latest) return;
41
-
42
- const extraCount = articles.length - 1;
43
- ctx.notify({
44
- title: "Breaking News",
45
- subtitle: notificationSubtitle(latest),
46
- body: extraCount > 0 ? `${latest.title} (+${extraCount} more)` : latest.title,
47
- type: "info",
48
- desktop: "always",
49
- persistent: true,
50
- action: {
51
- label: "Open",
52
- onClick: () => ctx.showPane("news-breaking"),
53
- },
54
- });
55
- }
56
-
57
50
  export function setupBreakingNewsNotifications(ctx: GloomPluginContext): () => void {
58
51
  let disposeWatch: (() => void) | null = null;
59
52
  let primed = false;
60
53
  let seenArticleIds = new Set<string>();
54
+ let watchedSymbols = new Set<string>();
61
55
 
62
56
  const enabled = () => ctx.configState.get<boolean>(BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY) === true;
63
57
 
58
+ // Snooze is intentionally local: it is short-lived and must not race the
59
+ // cloud sync snapshot across devices.
60
+ const snoozedUntil = () => ctx.persistence.getState<number>(SNOOZE_STATE_KEY) ?? 0;
61
+ const isSnoozed = () => snoozedUntil() > Date.now();
62
+ const snooze = () => {
63
+ ctx.persistence.setState(SNOOZE_STATE_KEY, Date.now() + SNOOZE_DURATION_MS);
64
+ ctx.notify({
65
+ body: "Breaking news snoozed for 1 hour.",
66
+ type: "info",
67
+ desktop: "never",
68
+ });
69
+ };
70
+
71
+ const refreshWatchedSymbols = async () => {
72
+ try {
73
+ const tickers = await ctx.tickerRepository.loadAllTickers();
74
+ const next = new Set<string>();
75
+ for (const ticker of tickers) {
76
+ const metadata = ticker.metadata;
77
+ const tracked =
78
+ (metadata.watchlists?.length ?? 0) > 0 ||
79
+ (metadata.portfolios?.length ?? 0) > 0 ||
80
+ (metadata.positions?.length ?? 0) > 0;
81
+ if (tracked) next.add(metadata.ticker.toUpperCase());
82
+ }
83
+ watchedSymbols = next;
84
+ } catch (error) {
85
+ ctx.log.warn(`breaking notifications could not load watched tickers: ${error}`);
86
+ }
87
+ };
88
+
89
+ const notifyNewBreakingArticles = (articles: MarketNewsItem[]): void => {
90
+ const latest = [...articles].sort(
91
+ (a, b) => b.publishedAt.getTime() - a.publishedAt.getTime(),
92
+ )[0];
93
+ if (!latest) return;
94
+
95
+ const extraCount = articles.length - 1;
96
+ ctx.notify({
97
+ title: "Breaking News",
98
+ subtitle: notificationSubtitle(latest),
99
+ body: extraCount > 0 ? `${latest.title} (+${extraCount} more)` : latest.title,
100
+ type: "info",
101
+ desktop: "always",
102
+ duration: BREAKING_TOAST_DURATION_MS,
103
+ action: {
104
+ label: "Open",
105
+ onClick: () => ctx.showPane("news-breaking"),
106
+ },
107
+ secondaryAction: {
108
+ label: "Snooze 1h",
109
+ onClick: snooze,
110
+ },
111
+ });
112
+ };
113
+
64
114
  const handleState = (state: NewsQueryState) => {
65
115
  if (!isReadyState(state)) return;
66
116
 
@@ -71,12 +121,20 @@ export function setupBreakingNewsNotifications(ctx: GloomPluginContext): () => v
71
121
  }
72
122
 
73
123
  const freshArticles = state.articles.filter((article) => !seenArticleIds.has(article.id));
124
+ // Mark everything seen regardless of filtering, so relaxing a filter later
125
+ // does not replay a backlog of old stories as if they just broke.
126
+ seenArticleIds = rememberArticleIds(seenArticleIds, state.articles);
74
127
  if (freshArticles.length === 0) return;
128
+ if (!enabled() || isSnoozed()) return;
75
129
 
76
- seenArticleIds = rememberArticleIds(seenArticleIds, state.articles);
77
- if (enabled()) {
78
- notifyNewBreakingArticles(ctx, freshArticles);
79
- }
130
+ const notifiable = selectNotifiableArticles(freshArticles, {
131
+ scope: parseBreakingScope(ctx.configState.get(BREAKING_NEWS_SCOPE_KEY)),
132
+ mutedSectors: parseMutedSectors(ctx.configState.get(BREAKING_NEWS_MUTED_SECTORS_KEY)),
133
+ watchedSymbols,
134
+ });
135
+ if (notifiable.length === 0) return;
136
+
137
+ notifyNewBreakingArticles(notifiable);
80
138
  };
81
139
 
82
140
  const stop = () => {
@@ -92,6 +150,7 @@ export function setupBreakingNewsNotifications(ctx: GloomPluginContext): () => v
92
150
  ctx.log.warn("breaking notifications unavailable: news query watcher missing");
93
151
  return;
94
152
  }
153
+ void refreshWatchedSymbols();
95
154
  disposeWatch = ctx.watchNewsQuery(NEWS_QUERY_PRESETS.breaking, handleState);
96
155
  };
97
156
 
@@ -100,11 +159,18 @@ export function setupBreakingNewsNotifications(ctx: GloomPluginContext): () => v
100
159
  else stop();
101
160
  };
102
161
 
103
- const disposeConfigListener = ctx.on("config:changed", sync);
162
+ const disposeConfigListener = ctx.on("config:changed", () => {
163
+ void refreshWatchedSymbols();
164
+ sync();
165
+ });
166
+ const disposeTickerAdded = ctx.on("ticker:added", () => void refreshWatchedSymbols());
167
+ const disposeTickerRemoved = ctx.on("ticker:removed", () => void refreshWatchedSymbols());
104
168
  sync();
105
169
 
106
170
  return () => {
107
171
  disposeConfigListener();
172
+ disposeTickerAdded();
173
+ disposeTickerRemoved();
108
174
  stop();
109
175
  };
110
176
  }
@@ -6,6 +6,12 @@ import {
6
6
  BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY,
7
7
  setupBreakingNewsNotifications,
8
8
  } from "./breaking/notifications";
9
+ import {
10
+ BREAKING_MUTED_SECTOR_OPTIONS,
11
+ BREAKING_NEWS_MUTED_SECTORS_KEY,
12
+ BREAKING_NEWS_SCOPE_KEY,
13
+ BREAKING_SCOPE_OPTIONS,
14
+ } from "./breaking/filters";
9
15
  import {
10
16
  addUserNewsFeed,
11
17
  getEnabledNewsFeeds,
@@ -17,12 +23,6 @@ import { NewsPresetPane } from "./news/preset-pane";
17
23
  import { NEWS_QUERY_PRESETS } from "./news/query-presets";
18
24
  import type { NewsColumnId, NewsSortPreference } from "./news/table";
19
25
  import { createRssNewsCapability } from "./rss/source";
20
- import { openUrl } from "../../../../components/ui/external-link";
21
- import {
22
- cachedNewsArticles,
23
- loadNewsArticles,
24
- searchNewsArticles,
25
- } from "./article-search";
26
26
 
27
27
  interface NewsPresetPaneConfig {
28
28
  paneKey: string;
@@ -77,13 +77,31 @@ export const newsWireModule: PluginModule = {
77
77
  defaultFloatingSize: { width: 85, height: 20 },
78
78
  settings: {
79
79
  title: "Breaking News Settings",
80
- fields: [{
81
- key: BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY,
82
- label: "Notifications",
83
- description: "Notify when new breaking stories arrive, even while this pane is closed.",
84
- type: "toggle",
85
- storage: "plugin",
86
- }],
80
+ fields: [
81
+ {
82
+ key: BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY,
83
+ label: "Notifications",
84
+ description: "Notify when new breaking stories arrive, even while this pane is closed.",
85
+ type: "toggle",
86
+ storage: "plugin",
87
+ },
88
+ {
89
+ key: BREAKING_NEWS_SCOPE_KEY,
90
+ label: "Notify About",
91
+ description: "Which breaking stories are worth interrupting you for.",
92
+ type: "select",
93
+ storage: "plugin",
94
+ options: BREAKING_SCOPE_OPTIONS,
95
+ },
96
+ {
97
+ key: BREAKING_NEWS_MUTED_SECTORS_KEY,
98
+ label: "Muted Sectors",
99
+ description: "Never notify about stories confined to these sectors.",
100
+ type: "multi-select",
101
+ storage: "plugin",
102
+ options: BREAKING_MUTED_SECTOR_OPTIONS,
103
+ },
104
+ ],
87
105
  },
88
106
  },
89
107
  ],
@@ -105,37 +123,6 @@ export const newsWireModule: PluginModule = {
105
123
  );
106
124
  ctx.registerCapability(source);
107
125
 
108
- ctx.registerCommand({
109
- id: "open-news-article",
110
- label: "Open Article",
111
- description: "Search loaded news headlines by topic, e.g. ART hormuz.",
112
- keywords: ["article", "news", "rss", "headline", "story", "open"],
113
- category: "navigation",
114
- shortcut: "ART",
115
- shortcutArg: {
116
- placeholder: "headline or topic",
117
- kind: "text",
118
- parse: (arg) => ({ query: arg.trim() }),
119
- },
120
- async execute(values) {
121
- const query = values?.query ?? values?.shortcut ?? "";
122
- const articles = cachedNewsArticles().length > 0
123
- ? cachedNewsArticles()
124
- : await loadNewsArticles();
125
- const match = searchNewsArticles(articles, query)[0];
126
- if (!match) {
127
- ctx.notify({
128
- body: query.trim()
129
- ? `No article matched "${query.trim()}".`
130
- : "No articles loaded yet.",
131
- type: "error",
132
- });
133
- return;
134
- }
135
- openUrl(match.url);
136
- },
137
- });
138
-
139
126
  ctx.registerCommand({
140
127
  id: "add-news-feed",
141
128
  label: "Add News Feed",
@@ -54,6 +54,7 @@ export const optionsModule: PluginModule = {
54
54
  defaultFloatingSize: { width: 112, height: 28 },
55
55
  quickSettings: [LIVE_STREAMING_QUICK_SETTING],
56
56
  settings: (context) => withLiveStreamingSetting(optionsSettings(context.settings), context.settings),
57
+ tableExport: true,
57
58
  },
58
59
  ],
59
60
 
@@ -1,7 +1,8 @@
1
1
  import { afterEach, expect, test } from "bun:test";
2
2
  import { act, useState } from "react";
3
3
  import type { ScrollBoxRenderable } from "@opentui/core";
4
- import { testRender } from "../../../renderers/opentui/test-utils";
4
+ import { takeSavedTextFile, testRender } from "../../../renderers/opentui/test-utils";
5
+ import { exportPaneTable, hasPaneTableExporter } from "../../../state/pane-table-export-registry";
5
6
  import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../../market-data/coordinator";
6
7
  import { AppContext, PaneInstanceProvider, createInitialState } from "../../../state/app/context";
7
8
  import { createTestDataProvider } from "../../../test-support/data-provider";
@@ -152,6 +153,33 @@ afterEach(async () => {
152
153
  setSharedMarketDataCoordinator(null);
153
154
  });
154
155
 
156
+ test("exposes exactly one exportable table so CSV export stays wired up", async () => {
157
+ const provider = createTestDataProvider({
158
+ getOptionsChain: async () => makeChain([100, 101], 101),
159
+ });
160
+ setSharedMarketDataCoordinator(new MarketDataCoordinator(provider));
161
+
162
+ await act(async () => {
163
+ testSetup = await testRender(
164
+ <OptionsHarness ticker={makeTicker("AAPL")} quotePrice={101} />,
165
+ { width: 124, height: 12 },
166
+ );
167
+ });
168
+ await renderSettled();
169
+
170
+ // `tableExport: true` on the options pane only works when the pane mounts a single
171
+ // DataTable; a second concurrent table would silently disable the export action.
172
+ expect(hasPaneTableExporter(TEST_PANE_ID)).toBe(true);
173
+
174
+ const location = await exportPaneTable(TEST_PANE_ID, "options.csv");
175
+ expect(location).toBe("~/Downloads/options.csv");
176
+
177
+ const saved = takeSavedTextFile();
178
+ expect(saved?.name).toBe("options.csv");
179
+ expect(saved?.text.split("\n")[0]).toContain("STRIKE");
180
+ expect(saved?.text).toContain("101");
181
+ });
182
+
155
183
  test("defaults the table around the nearest strike to the current quote", async () => {
156
184
  const strikes = Array.from({ length: 25 }, (_, index) => 50 + index * 5);
157
185
  const provider = createTestDataProvider({
@@ -423,25 +423,6 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
423
423
  return true;
424
424
  }
425
425
 
426
- if (isPlainKey(event, "j", "down")) {
427
- if (strikes.length === 0) return true;
428
- event.preventDefault?.();
429
- event.stopPropagation?.();
430
- userSelectedStrikeRef.current = true;
431
- setScrollToIndexAlign("nearest");
432
- setStrikeIdx((i) => Math.min(i + 1, strikes.length - 1));
433
- return true;
434
- }
435
- if (isPlainKey(event, "k", "up")) {
436
- if (strikes.length === 0) return true;
437
- event.preventDefault?.();
438
- event.stopPropagation?.();
439
- userSelectedStrikeRef.current = true;
440
- setScrollToIndexAlign("nearest");
441
- setStrikeIdx((i) => Math.max(i - 1, 0));
442
- return true;
443
- }
444
-
445
426
  return false;
446
427
  }, [
447
428
  calcParams,
@@ -450,7 +431,6 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
450
431
  interactive,
451
432
  openCalculator,
452
433
  selectAdjacentExpiration,
453
- strikes.length,
454
434
  ]);
455
435
 
456
436
  if (!ticker) {
@@ -522,11 +502,10 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
522
502
  setStrikeIdx(index);
523
503
  },
524
504
  }}
525
- onCursorChange={(_row, index) => {
505
+ onCursorChange={() => {
526
506
  userSelectedStrikeRef.current = true;
527
507
  setScrollToIndexAlign("nearest");
528
508
  enterInteractive();
529
- setStrikeIdx(index);
530
509
  }}
531
510
  onRootKeyDown={handleTableKeyDown}
532
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
+ }