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
@@ -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",
@@ -0,0 +1,83 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { OptionContract, OptionsChain, PricePoint } from "../../../types/financials";
3
+ import {
4
+ calculateOptionGreeks,
5
+ calculateOptionsSummary,
6
+ historicalVolatility30d,
7
+ } from "./analytics";
8
+
9
+ function contract(
10
+ strike: number,
11
+ impliedVolatility: number,
12
+ volume: number,
13
+ openInterest: number,
14
+ ): OptionContract {
15
+ return {
16
+ contractSymbol: String(strike),
17
+ strike,
18
+ currency: "USD",
19
+ lastPrice: 1,
20
+ change: 0,
21
+ percentChange: 0,
22
+ volume,
23
+ openInterest,
24
+ bid: 0.95,
25
+ ask: 1.05,
26
+ impliedVolatility,
27
+ inTheMoney: false,
28
+ expiration: Date.UTC(2027, 0, 15) / 1000,
29
+ lastTradeDate: 0,
30
+ };
31
+ }
32
+
33
+ function priceHistory(): PricePoint[] {
34
+ const points: PricePoint[] = [{ date: new Date(Date.UTC(2026, 0, 1)), close: 100 }];
35
+ for (let index = 0; index < 30; index += 1) {
36
+ points.push({
37
+ date: new Date(Date.UTC(2026, 0, index + 2)),
38
+ close: points.at(-1)!.close * Math.exp(index % 2 === 0 ? 0.01 : -0.01),
39
+ });
40
+ }
41
+ return points;
42
+ }
43
+
44
+ test("annualizes the latest 30 daily log returns, including persisted dates", () => {
45
+ const expected = Math.sqrt((30 * 0.01 ** 2 / 29) * 252);
46
+ expect(historicalVolatility30d(priceHistory())).toBeCloseTo(expected, 10);
47
+ expect(historicalVolatility30d(priceHistory().map((point) => ({
48
+ ...point,
49
+ date: point.date.toISOString() as unknown as Date,
50
+ })))).toBeCloseTo(expected, 10);
51
+ expect(historicalVolatility30d(priceHistory().slice(1))).toBeNull();
52
+ });
53
+
54
+ test("summarizes the selected expiration without presenting it as whole-chain volume", () => {
55
+ const chain: OptionsChain = {
56
+ underlyingSymbol: "AAPL",
57
+ expirationDates: [Date.UTC(2027, 0, 15) / 1000],
58
+ calls: [contract(100, 0.2, 100, 200), contract(105, 0.3, 200, 400)],
59
+ puts: [contract(100, 0.22, 150, 180), contract(105, 0.4, 300, 420)],
60
+ };
61
+ const summary = calculateOptionsSummary(chain, 101, priceHistory());
62
+
63
+ expect(summary.atmImpliedVolatility).toBeCloseTo(0.21, 10);
64
+ expect(summary.expirationVolume).toBe(750);
65
+ expect(summary.putCallVolumeRatio).toBe(1.5);
66
+ expect(summary.putCallOpenInterestRatio).toBe(1);
67
+ expect(summary.impliedHistoricalRatio).toBeCloseTo(
68
+ summary.atmImpliedVolatility! / summary.historicalVolatility30d!,
69
+ 10,
70
+ );
71
+ });
72
+
73
+ test("derives call and put Greeks from the chain IV", () => {
74
+ const option = contract(100, 0.25, 0, 0);
75
+ const now = Date.UTC(2026, 11, 15);
76
+ const call = calculateOptionGreeks(option, "call", 100, 0, now);
77
+ const put = calculateOptionGreeks(option, "put", 100, 0, now);
78
+
79
+ expect(call?.delta).toBeGreaterThan(0);
80
+ expect(put?.delta).toBeLessThan(0);
81
+ expect(call?.gamma).toBeCloseTo(put!.gamma, 10);
82
+ expect(call?.vegaPerPoint).toBeCloseTo(put!.vegaPerPoint, 10);
83
+ });