gloomberb 0.18.1 → 0.18.2

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 (307) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/types.ts +5 -0
  8. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  9. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  10. package/src/app/runtime/plugin-bindings.ts +6 -2
  11. package/src/brokers/logos.ts +66 -0
  12. package/src/brokers/profile-form.ts +3 -0
  13. package/src/brokers/remote-broker-adapter.ts +13 -3
  14. package/src/capabilities/index.ts +1 -1
  15. package/src/capabilities/service-capabilities.ts +7 -1
  16. package/src/cli/base-converter.ts +60 -18
  17. package/src/cli/commands/market.ts +281 -154
  18. package/src/cli/commands/news-search.ts +35 -8
  19. package/src/cli/commands/system.ts +35 -1
  20. package/src/cli/commands/ticker.ts +100 -50
  21. package/src/cli/currency-hints.ts +27 -0
  22. package/src/cli/fx-pair.ts +41 -4
  23. package/src/cli/glossary.ts +57 -4
  24. package/src/cli/helpers.ts +36 -3
  25. package/src/cli/index.ts +4 -1
  26. package/src/cli/known-gaps.ts +37 -1
  27. package/src/cli/listing-arg.ts +37 -2
  28. package/src/cli/pane-functions/catalog.ts +3 -1
  29. package/src/cli/pane-functions/discovery.ts +15 -1
  30. package/src/cli/pane-functions/dom.ts +9 -4
  31. package/src/cli/pane-functions/freshness.ts +4 -4
  32. package/src/cli/pane-functions/function-help.ts +27 -3
  33. package/src/cli/pane-functions/headless.ts +6 -5
  34. package/src/cli/pane-functions/index.ts +3 -2
  35. package/src/cli/pane-functions/lock-note.ts +7 -4
  36. package/src/cli/pane-functions/resolver.ts +2 -1
  37. package/src/cli/quote-answer.ts +179 -0
  38. package/src/cli/series-search.ts +2 -1
  39. package/src/cli/types.ts +4 -0
  40. package/src/components/chart/composite/composite-chart.tsx +11 -2
  41. package/src/components/chart/composite/format.ts +13 -1
  42. package/src/components/chart/composite/level-labels.ts +30 -0
  43. package/src/components/chart/composite/levels.ts +17 -6
  44. package/src/components/chart/composite/panel-surface.tsx +58 -8
  45. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  46. package/src/components/index.ts +2 -0
  47. package/src/components/layout/pane/content.tsx +16 -14
  48. package/src/components/layout/pane/footer/index.tsx +22 -3
  49. package/src/components/layout/pane/footer/model.ts +99 -4
  50. package/src/components/layout/pane/header-tabs.tsx +15 -1
  51. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  52. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  53. package/src/components/onboarding/wizard-model.ts +10 -3
  54. package/src/components/ui/button.tsx +11 -4
  55. package/src/components/ui/choice-dialog.tsx +8 -5
  56. package/src/components/ui/display.tsx +11 -6
  57. package/src/components/ui/fields.tsx +100 -20
  58. package/src/components/ui/icon.tsx +6 -1
  59. package/src/components/ui/list-row-lines.tsx +87 -0
  60. package/src/components/ui/list-view.tsx +14 -0
  61. package/src/components/ui/logo-mark.tsx +64 -0
  62. package/src/components/ui/number-step.ts +54 -0
  63. package/src/components/ui/select-button.tsx +5 -4
  64. package/src/components/ui/select-field.tsx +3 -3
  65. package/src/components/ui/status.tsx +17 -3
  66. package/src/components/ui/tabs.tsx +9 -1
  67. package/src/components/ui/toggle.tsx +17 -3
  68. package/src/core/state/app/instrument.ts +8 -3
  69. package/src/data/config/store/normalize.ts +4 -1
  70. package/src/data/plugin-cache.ts +32 -6
  71. package/src/docs/snapshot.json +5 -5
  72. package/src/i18n/es.ts +9 -3
  73. package/src/i18n/ja.ts +9 -3
  74. package/src/i18n/ko.ts +9 -3
  75. package/src/i18n/zh-cn.ts +10 -4
  76. package/src/i18n/zh-tw.ts +10 -4
  77. package/src/market-data/commodity-names.ts +80 -0
  78. package/src/market-data/coordinator/auxiliary.ts +3 -1
  79. package/src/market-data/coordinator/chart.ts +3 -1
  80. package/src/market-data/futures-board.ts +38 -1
  81. package/src/market-data/futures-contract.ts +91 -0
  82. package/src/market-data/fx-closes.ts +78 -1
  83. package/src/market-data/market/change-basis.ts +219 -0
  84. package/src/market-data/market/format.ts +28 -12
  85. package/src/market-data/market/freshness.ts +38 -0
  86. package/src/market-data/market/status.ts +55 -9
  87. package/src/market-data/market/year-to-date.ts +46 -0
  88. package/src/market-data/quotes/contributions.ts +12 -0
  89. package/src/market-data/quotes/delisting.ts +34 -0
  90. package/src/market-data/quotes/live-valuation.ts +23 -0
  91. package/src/market-data/quotes/resolution.ts +8 -2
  92. package/src/market-data/tax-equivalent.ts +10 -2
  93. package/src/notifications/app-notifier.ts +2 -2
  94. package/src/notifications/notification-log.ts +8 -6
  95. package/src/plugins/builtin/alerts/cli.ts +26 -18
  96. package/src/plugins/builtin/alerts/headless.ts +1 -1
  97. package/src/plugins/builtin/alerts/index.tsx +13 -4
  98. package/src/plugins/builtin/attention/trending.tsx +8 -6
  99. package/src/plugins/builtin/backtest/client.ts +5 -2
  100. package/src/plugins/builtin/backtest/window.ts +16 -3
  101. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  102. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  103. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  104. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  105. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  106. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  107. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  108. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  109. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  110. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  111. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  112. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  113. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  114. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  115. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  116. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  117. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  118. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  119. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  120. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  121. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  122. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  123. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  124. package/src/plugins/builtin/buildout/format.ts +1 -0
  125. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  126. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  127. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  128. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  129. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  130. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  131. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  132. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  133. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  134. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  135. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  136. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  137. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  138. package/src/plugins/builtin/chat/index.tsx +1 -1
  139. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  140. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  141. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  142. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  143. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  144. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  145. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  146. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  147. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  148. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  149. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  150. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  151. package/src/plugins/builtin/composite-plugins.ts +3 -2
  152. package/src/plugins/builtin/cot/headless.ts +1 -1
  153. package/src/plugins/builtin/cot/index.tsx +1 -1
  154. package/src/plugins/builtin/cot/model.ts +35 -12
  155. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  156. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  157. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  158. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  159. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  160. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  161. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  162. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  163. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  164. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  165. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  166. package/src/plugins/builtin/econ/index.tsx +31 -4
  167. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  168. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  169. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  170. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  171. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  172. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  173. package/src/plugins/builtin/exposure/client.ts +4 -1
  174. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  175. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  176. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  177. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  178. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  179. package/src/plugins/builtin/futures/table.tsx +6 -3
  180. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  181. package/src/plugins/builtin/insider/base-values.ts +72 -0
  182. package/src/plugins/builtin/insider/headless.ts +29 -4
  183. package/src/plugins/builtin/jobs/headless.ts +2 -2
  184. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  185. package/src/plugins/builtin/market-halts/model.ts +31 -6
  186. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  187. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  188. package/src/plugins/builtin/members/pane.tsx +8 -3
  189. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  190. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  191. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  192. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  193. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  194. package/src/plugins/builtin/options/footer.ts +25 -20
  195. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  196. package/src/plugins/builtin/options/view.tsx +77 -22
  197. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  198. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  199. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  200. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  201. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  202. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  203. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  204. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  205. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  206. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  207. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  208. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  209. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  210. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  211. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  212. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  213. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  214. package/src/plugins/builtin/scanner/feed.ts +6 -1
  215. package/src/plugins/builtin/scanner/headless.ts +1 -1
  216. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  217. package/src/plugins/builtin/series-inventory.ts +1 -0
  218. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  219. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  220. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  221. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  222. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  223. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  224. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  225. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  226. package/src/plugins/builtin/themes/headless.ts +8 -2
  227. package/src/plugins/builtin/themes/model.ts +43 -10
  228. package/src/plugins/builtin/themes/pane.tsx +8 -6
  229. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  230. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  231. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  232. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  233. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  234. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  235. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  236. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  237. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  238. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  239. package/src/plugins/builtin-editorial.ts +1 -1
  240. package/src/plugins/compat.ts +3 -0
  241. package/src/plugins/external-runtime.ts +5 -0
  242. package/src/plugins/loader.ts +6 -0
  243. package/src/plugins/plugin-icon.ts +61 -0
  244. package/src/plugins/registry/host-actions.ts +2 -1
  245. package/src/plugins/registry/index.ts +9 -1
  246. package/src/plugins/runtime/context.tsx +5 -0
  247. package/src/plugins/runtime/index.tsx +3 -0
  248. package/src/portfolio/column-values.ts +13 -4
  249. package/src/portfolio/cost-fx.ts +5 -28
  250. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  251. package/src/public/broker.ts +8 -0
  252. package/src/public/market-data.ts +5 -0
  253. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  254. package/src/renderers/dom/desktop/controls.tsx +39 -7
  255. package/src/renderers/dom/desktop/icons.tsx +14 -0
  256. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  257. package/src/renderers/dom/desktop/menu.tsx +10 -1
  258. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  259. package/src/renderers/dom/host/style.ts +6 -1
  260. package/src/renderers/dom/host/tabs.tsx +6 -0
  261. package/src/renderers/dom/styles.css +12 -5
  262. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  263. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  264. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  265. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  266. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  267. package/src/series/validate.ts +2 -2
  268. package/src/sources/gloomberb-cloud/index.ts +50 -17
  269. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  270. package/src/sources/history-result.ts +3 -0
  271. package/src/sources/provider-errors.ts +3 -1
  272. package/src/sources/provider-router/history.ts +49 -6
  273. package/src/sources/provider-router/index.ts +2 -1
  274. package/src/state/app/activity.ts +13 -0
  275. package/src/sync/controller.ts +4 -0
  276. package/src/sync/core-contributors.ts +62 -11
  277. package/src/sync/local-only.ts +121 -0
  278. package/src/sync/react.ts +6 -0
  279. package/src/sync/types.ts +3 -0
  280. package/src/tickers/listing-fate.ts +27 -8
  281. package/src/tickers/search/index.ts +24 -3
  282. package/src/time-series/price-comparison.ts +35 -0
  283. package/src/time-series/resolve.ts +52 -11
  284. package/src/time-series/spec.ts +1 -1
  285. package/src/time-series/types.ts +5 -0
  286. package/src/types/broker.ts +56 -1
  287. package/src/types/config.ts +6 -0
  288. package/src/types/data-provider.ts +5 -1
  289. package/src/types/financials.ts +23 -0
  290. package/src/types/headless.ts +12 -2
  291. package/src/types/instrument.ts +4 -0
  292. package/src/types/plugin.ts +24 -6
  293. package/src/types/price-history.ts +6 -1
  294. package/src/types/trading.ts +67 -0
  295. package/src/ui/host.tsx +4 -0
  296. package/src/utils/base-currency.ts +108 -0
  297. package/src/utils/currency-names.ts +128 -0
  298. package/src/utils/currency-pair.ts +2 -1
  299. package/src/utils/currency-units.ts +32 -1
  300. package/src/utils/dom-screenshot.ts +22 -9
  301. package/src/utils/exchange-notation.generated.ts +360 -0
  302. package/src/utils/exchanges.ts +15 -322
  303. package/src/utils/format.ts +31 -1
  304. package/src/utils/instrument-identity.ts +38 -1
  305. package/src/utils/options.ts +46 -1
  306. package/src/utils/price-history.ts +4 -0
  307. package/src/utils/reported-money.ts +8 -4
@@ -9,11 +9,13 @@ const USER_FEEDS_KEY = "feeds";
9
9
  const DISABLED_DEFAULT_FEED_IDS_KEY = "disabledDefaultFeedIds";
10
10
 
11
11
  const DEFAULT_FEED_IDS = new Set(DEFAULT_FEEDS.map((feed) => feed.id));
12
+ const UNREADABLE_SETTINGS = "News feed settings could not be read. Saved settings were kept.";
12
13
 
13
14
  export interface NewsFeedSettings {
14
15
  userFeeds: RssFeedConfig[];
15
16
  disabledDefaultFeedIds: string[];
16
17
  needsMigration: boolean;
18
+ error?: string;
17
19
  }
18
20
 
19
21
  export interface UserNewsFeedInput {
@@ -25,15 +27,15 @@ export interface UserNewsFeedInput {
25
27
  enabled?: boolean;
26
28
  }
27
29
 
28
- function parseJsonArray(value: unknown): unknown[] {
30
+ function parseJsonArray(value: unknown): unknown[] | null {
29
31
  if (Array.isArray(value)) return value;
30
- if (typeof value !== "string" || !value.trim()) return [];
32
+ if (typeof value !== "string") return [];
31
33
  try {
32
34
  const parsed = JSON.parse(value);
33
35
  return Array.isArray(parsed) ? parsed : [];
34
36
  } catch (error) {
35
37
  logFailure("news.feed-config", "Read stored news feed settings failed")(error);
36
- return [];
38
+ return null;
37
39
  }
38
40
  }
39
41
 
@@ -134,14 +136,20 @@ function sameIdList(raw: unknown, normalized: string[]): boolean {
134
136
  }
135
137
 
136
138
  export function loadNewsFeedSettings(configState: PluginConfigState): NewsFeedSettings {
137
- const rawUserFeeds = configState.get<unknown>(USER_FEEDS_KEY);
139
+ return parseNewsFeedSettings(configState.get(USER_FEEDS_KEY), configState.get(DISABLED_DEFAULT_FEED_IDS_KEY));
140
+ }
141
+
142
+ export function parseNewsFeedSettings(rawUserFeeds: unknown, rawDisabledIds: unknown): NewsFeedSettings {
138
143
  const userFeedValues = parseJsonArray(rawUserFeeds);
139
- const userFeeds = userFeedValues
144
+ const userFeeds = (userFeedValues ?? [])
140
145
  .map(normalizeUserFeed)
141
146
  .filter((feed): feed is RssFeedConfig => !!feed);
142
147
 
143
- const rawDisabledIds = configState.get<unknown>(DISABLED_DEFAULT_FEED_IDS_KEY);
144
- const disabledDefaultFeedIds = normalizeDisabledDefaultFeedIds(parseJsonArray(rawDisabledIds));
148
+ const disabledIdValues = parseJsonArray(rawDisabledIds);
149
+ const disabledDefaultFeedIds = normalizeDisabledDefaultFeedIds(disabledIdValues ?? []);
150
+ if (userFeedValues === null || disabledIdValues === null) {
151
+ return { userFeeds, disabledDefaultFeedIds, needsMigration: false, error: UNREADABLE_SETTINGS };
152
+ }
145
153
 
146
154
  const needsMigration = (rawUserFeeds !== null && !sameUserFeeds(rawUserFeeds, userFeeds))
147
155
  || (rawDisabledIds !== null && !sameIdList(rawDisabledIds, disabledDefaultFeedIds));
@@ -149,15 +157,24 @@ export function loadNewsFeedSettings(configState: PluginConfigState): NewsFeedSe
149
157
  return { userFeeds, disabledDefaultFeedIds, needsMigration };
150
158
  }
151
159
 
160
+ function requireReadableSettings(configState: PluginConfigState): NewsFeedSettings {
161
+ const settings = loadNewsFeedSettings(configState);
162
+ if (settings.error) throw new Error(settings.error);
163
+ return settings;
164
+ }
165
+
152
166
  export async function saveNewsFeedSettings(
153
167
  configState: PluginConfigState,
154
168
  settings: Pick<NewsFeedSettings, "userFeeds" | "disabledDefaultFeedIds">,
155
169
  ): Promise<void> {
170
+ requireReadableSettings(configState);
156
171
  await configState.set(USER_FEEDS_KEY, settings.userFeeds);
172
+ requireReadableSettings(configState);
157
173
  await configState.set(DISABLED_DEFAULT_FEED_IDS_KEY, settings.disabledDefaultFeedIds);
158
174
  }
159
175
 
160
- export function getEnabledNewsFeeds(settings: Pick<NewsFeedSettings, "userFeeds" | "disabledDefaultFeedIds">): RssFeedConfig[] {
176
+ export function getEnabledNewsFeeds(settings: Pick<NewsFeedSettings, "userFeeds" | "disabledDefaultFeedIds" | "error">): RssFeedConfig[] {
177
+ if (settings.error) return [];
161
178
  const disabled = new Set(settings.disabledDefaultFeedIds);
162
179
  return [
163
180
  ...DEFAULT_FEEDS.filter((feed) => feed.enabled && !disabled.has(feed.id)),
@@ -169,7 +186,7 @@ export async function addUserNewsFeed(
169
186
  configState: PluginConfigState,
170
187
  input: UserNewsFeedInput,
171
188
  ): Promise<RssFeedConfig> {
172
- const settings = loadNewsFeedSettings(configState);
189
+ const settings = requireReadableSettings(configState);
173
190
  const feed = createUserFeed(input);
174
191
  const nextUserFeeds = [
175
192
  ...settings.userFeeds.filter((entry) => entry.id !== feed.id),
@@ -187,7 +204,7 @@ export async function updateUserNewsFeed(
187
204
  feedId: string,
188
205
  patch: Partial<UserNewsFeedInput>,
189
206
  ): Promise<RssFeedConfig | null> {
190
- const settings = loadNewsFeedSettings(configState);
207
+ const settings = requireReadableSettings(configState);
191
208
  const existing = settings.userFeeds.find((feed) => feed.id === feedId);
192
209
  if (!existing) return null;
193
210
  const nextFeed = createUserFeed({
@@ -206,7 +223,7 @@ export async function removeUserNewsFeed(
206
223
  configState: PluginConfigState,
207
224
  feedId: string,
208
225
  ): Promise<boolean> {
209
- const settings = loadNewsFeedSettings(configState);
226
+ const settings = requireReadableSettings(configState);
210
227
  const nextUserFeeds = settings.userFeeds.filter((feed) => feed.id !== feedId);
211
228
  if (nextUserFeeds.length === settings.userFeeds.length) return false;
212
229
  await saveNewsFeedSettings(configState, {
@@ -222,7 +239,7 @@ export async function setDefaultNewsFeedEnabled(
222
239
  enabled: boolean,
223
240
  ): Promise<boolean> {
224
241
  if (!DEFAULT_FEED_IDS.has(feedId)) return false;
225
- const settings = loadNewsFeedSettings(configState);
242
+ const settings = requireReadableSettings(configState);
226
243
  const disabled = new Set(settings.disabledDefaultFeedIds);
227
244
  if (enabled) disabled.delete(feedId);
228
245
  else disabled.add(feedId);
@@ -26,6 +26,7 @@ import { createRssNewsCapability } from "./rss/source";
26
26
  import { rssCliCommand } from "./rss/cli";
27
27
  import { newsFeedHeadless } from "../headless";
28
28
  import type { PaneDef } from "../../../../types/plugin";
29
+ import { logFailure } from "../../../../utils/log-failure";
29
30
 
30
31
  const TopPane = createNewsPresetPane({
31
32
  paneKey: "top:curated",
@@ -195,11 +196,15 @@ export const newsWireModule: PluginModule = {
195
196
  setup(ctx) {
196
197
  const initialSettings = loadNewsFeedSettings(ctx.configState);
197
198
  if (initialSettings.needsMigration) {
198
- void saveNewsFeedSettings(ctx.configState, initialSettings);
199
+ void saveNewsFeedSettings(ctx.configState, initialSettings)
200
+ .catch(logFailure("news.feed-config", "Migrate news feed settings failed"));
199
201
  }
200
202
 
201
203
  const source = createRssNewsCapability(
202
- () => getEnabledNewsFeeds(loadNewsFeedSettings(ctx.configState)),
204
+ () => {
205
+ const settings = loadNewsFeedSettings(ctx.configState);
206
+ return { feeds: getEnabledNewsFeeds(settings), error: settings.error };
207
+ },
203
208
  { persistence: ctx.persistence },
204
209
  );
205
210
  ctx.registerCapability(source);
@@ -1,9 +1,9 @@
1
- import { useCallback } from "react";
1
+ import { useCallback, useMemo } from "react";
2
2
  import { Box } from "../../../../../ui";
3
3
  import type { NewsArticle, NewsQuery } from "../../../../../news/types";
4
4
  import { useLoadNewsStory, useNewsArticles, useNewsTableLoadMore } from "../../../../../news/hooks";
5
5
  import type { PaneProps } from "../../../../../types/plugin";
6
- import { useDebouncedPluginPaneState, usePluginPaneState } from "../../../../runtime";
6
+ import { useDebouncedPluginPaneState, usePluginConfigState, usePluginPaneState } from "../../../../runtime";
7
7
  import { PaneStatusBody, QueryBar, type QueryBarSearch } from "../../../../../components";
8
8
  import { NewsDetailView, useNewsArticleDetail } from "./detail-view";
9
9
  import {
@@ -18,6 +18,7 @@ import { newsListEmptyCopy } from "../filter-articles";
18
18
  import { NEWS_LIST_SEARCH_PLACEHOLDER, useNewsListSearch, useNewsListSearchHint } from "./list-search";
19
19
  import { newsMutesApplyToFeed, useNewsMuteFilter } from "../mutes";
20
20
  import { usePopOutNewsArticle } from "./pop-out";
21
+ import { parseNewsFeedSettings } from "../feed-config";
21
22
 
22
23
  interface NewsArticleStackOptions {
23
24
  /** Prefix of every key the pane persists, and of its footer registration. */
@@ -146,6 +147,13 @@ export function NewsPresetPane({
146
147
  emptyStateTitle,
147
148
  emptyStateHint,
148
149
  }: PaneProps & NewsPresetPaneConfig) {
150
+ const [storedFeeds] = usePluginConfigState<unknown>("feeds", null);
151
+ const [storedDisabledIds] = usePluginConfigState<unknown>("disabledDefaultFeedIds", null);
152
+ // The first query can finish before RSS registers, and desktop queries may
153
+ // use Cloud alone. A settings failure must remain visible in either case.
154
+ const settingsError = useMemo(() => query.feed === "latest"
155
+ ? parseNewsFeedSettings(storedFeeds, storedDisabledIds).error
156
+ : undefined, [query.feed, storedFeeds, storedDisabledIds]);
149
157
  const newsState = useNewsArticles(query);
150
158
  const persisted = usePersistedNewsArticles(`${paneKey}:articles`, newsState.articles);
151
159
  const articles = useNewsMuteFilter(persisted, newsMutesApplyToFeed(query.feed));
@@ -154,7 +162,7 @@ export function NewsPresetPane({
154
162
  // body rather than a definitive empty wire.
155
163
  const loading = newsState.phase === "loading"
156
164
  || (newsState.phase === "refreshing" && persisted.length === 0);
157
- const error = newsState.error;
165
+ const error = settingsError ?? newsState.error;
158
166
  const { search, searchQuery, ...stack } = useNewsArticleStack({
159
167
  paneKey,
160
168
  articles,
@@ -26,8 +26,9 @@ export const rssCliCommand: CliCommandDef = {
26
26
  authority: 50,
27
27
  enabled: true,
28
28
  }]);
29
- const articles = await capability.provider.fetchNews({ feed: "latest", limit: ctx.cliOptions.limit ?? 20 });
30
- ctx.printResult({ data: articles.map((article) => ({
29
+ const page = await capability.provider.fetchNewsPage!({ feed: "latest", limit: ctx.cliOptions.limit ?? 20 });
30
+ if (page.error) ctx.fail(page.error);
31
+ ctx.printResult({ data: page.articles.map((article) => ({
31
32
  title: article.title,
32
33
  source: article.source,
33
34
  publishedAt: article.publishedAt.toISOString(),
@@ -129,11 +129,14 @@ function writeFeedCache(
129
129
  }
130
130
 
131
131
  export function createRssNewsCapability(
132
- feedsOrGetter: RssFeedConfig[] | (() => RssFeedConfig[]),
132
+ feedsOrGetter: RssFeedConfig[] | (() => RssFeedConfig[] | { feeds: RssFeedConfig[]; error?: string }),
133
133
  options: RssNewsCapabilityOptions = {},
134
134
  ): NewsCapability {
135
135
  const fetchText = options.fetchText ?? ((url: string) => rssClient.fetch(url));
136
- const getFeeds = () => Array.isArray(feedsOrGetter) ? feedsOrGetter : feedsOrGetter();
136
+ const getFeeds = () => {
137
+ const selection = Array.isArray(feedsOrGetter) ? feedsOrGetter : feedsOrGetter();
138
+ return Array.isArray(selection) ? { feeds: selection } : selection;
139
+ };
137
140
 
138
141
  async function fetchFeed(feed: RssFeedConfig): Promise<{ articles: MarketNewsItem[]; failed: boolean }> {
139
142
  const freshCache = readFeedCache(options.persistence, feed);
@@ -153,7 +156,9 @@ export function createRssNewsCapability(
153
156
 
154
157
  async function fetchPage(query: NewsQuery) {
155
158
  if (!supportsQuery(query)) return { articles: [] };
156
- const feeds = getFeeds().filter((feed) => feed.enabled);
159
+ const selection = getFeeds();
160
+ if (selection.error) return { articles: [], error: selection.error };
161
+ const feeds = selection.feeds.filter((feed) => feed.enabled);
157
162
  const results = await Promise.all(feeds.map(fetchFeed));
158
163
  const failed = results.filter((result) => result.failed).length;
159
164
  return {
@@ -170,11 +175,15 @@ export function createRssNewsCapability(
170
175
  supports: supportsQuery,
171
176
  getCachedNews(query: NewsQuery): MarketNewsItem[] {
172
177
  if (!supportsQuery(query)) return [];
173
- const enabledFeeds = getFeeds().filter((feed) => feed.enabled);
178
+ const selection = getFeeds();
179
+ if (selection.error) return [];
180
+ const enabledFeeds = selection.feeds.filter((feed) => feed.enabled);
174
181
  return enabledFeeds.flatMap((feed) => readFeedCache(options.persistence, feed, { allowExpired: true }) ?? []);
175
182
  },
176
183
  fetchNewsPage: fetchPage,
177
184
  async fetchNews(query: NewsQuery): Promise<MarketNewsItem[]> {
185
+ const error = supportsQuery(query) ? getFeeds().error : undefined;
186
+ if (error) throw new Error(error);
178
187
  return (await fetchPage(query)).articles;
179
188
  },
180
189
  },
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import type { PaneFooterSegment, PaneHint } from "../../../components";
3
+ import { delayedFooterSegment } from "../../../components/layout/pane/footer/model";
3
4
  import { t, tf } from "../../../i18n";
4
5
  import type { OptionsChain } from "../../../types/financials";
5
6
  import { formatRelativeTime } from "../../../utils/datetime-format";
@@ -9,18 +10,18 @@ import { CLOUD_QUOTE_DELAY_MINUTES } from "../../../api-client/plan-access";
9
10
  import type { OptionQuoteCoverage, OptionQuoteCoverageStatus } from "./live-quotes";
10
11
  import { formatOptionPrice, optionSpread, type OptionMarketReference } from "./market-reference";
11
12
 
12
- interface OptionsCoverageState {
13
- text: string;
14
- tone: "muted" | "positive" | "warning";
15
- }
16
-
17
- /** Stream coverage the pane reports once the account is entitled to real-time options. */
18
- function resolveOptionsCoverageState(status: OptionQuoteCoverageStatus): OptionsCoverageState | null {
19
- if (status === "live") return { text: t("real-time options"), tone: "positive" };
20
- if (status === "mixed") return { text: t("mixed real-time and delayed options"), tone: "warning" };
13
+ /**
14
+ * Stream coverage the pane reports once the account is entitled to real-time
15
+ * options. Delayed quotes, all or some, are the footer's clock, its detail on
16
+ * hover.
17
+ */
18
+ export function optionsCoverageSegment(status: OptionQuoteCoverageStatus, delayLabel?: string): PaneFooterSegment | null {
19
+ const id = "options-access";
20
+ if (status === "live") return { id, parts: [{ text: t("real-time options"), tone: "positive" }] };
21
+ if (status === "mixed") return delayedFooterSegment({ id, detail: t("mixed real-time and delayed options") });
21
22
  // The stream connects in the background; nothing to report until it answers.
22
23
  if (status === "connecting") return null;
23
- return { text: t("options delayed fallback"), tone: "warning" };
24
+ return delayedFooterSegment({ id, detail: t("Delayed options quotes, real-time stream unavailable"), delay: delayLabel });
24
25
  }
25
26
 
26
27
  /**
@@ -82,6 +83,7 @@ export function resolveOptionsDelayLabel(chain: OptionsChain | null | undefined)
82
83
  }
83
84
 
84
85
  export function useOptionsAccessFooter({
86
+ broker = false,
85
87
  chain,
86
88
  error,
87
89
  focused,
@@ -91,6 +93,8 @@ export function useOptionsAccessFooter({
91
93
  reference,
92
94
  spreadColumnVisible,
93
95
  }: {
96
+ /** The chain is a broker profile's, quoted by that broker: it has no Gloom Cloud delay to report or upgrade from. */
97
+ broker?: boolean;
94
98
  chain: OptionsChain | null | undefined;
95
99
  error?: string | null;
96
100
  focused: boolean;
@@ -102,24 +106,25 @@ export function useOptionsAccessFooter({
102
106
  /** Set while the chain prints a SPRD column, so the status bar stops repeating it. */
103
107
  spreadColumnVisible?: boolean;
104
108
  }): void {
109
+ const delayLabel = resolveOptionsDelayLabel(chain);
105
110
  const { access, hint: upgradeHint, segment } = useCloudAccessFooter({
106
- delayLabel: resolveOptionsDelayLabel(chain),
111
+ delayLabel,
112
+ degraded: !broker,
107
113
  focused,
108
114
  segmentId: "options-access",
109
115
  placement: "omon-footer",
110
116
  shortcutScope: "options:upgrade",
111
117
  });
112
- // Trial accounts stream real-time too, but the countdown is the status worth the row.
113
- const coverage = access.hasProAccess && !access.isTrialActive
114
- ? resolveOptionsCoverageState(quoteCoverage.status)
115
- : null;
116
-
118
+ // Trial accounts stream real-time too, but the countdown is the status worth the row. A broker's
119
+ // chain streams from the broker whatever the plan, so its coverage is the status, and its delay
120
+ // is the chain's own, not Gloom Cloud's.
121
+ const proCoverage = broker || (access.hasProAccess && !access.isTrialActive);
122
+ const coverageDelay = broker && !(chain?.delayMinutes && chain.delayMinutes > 0) ? undefined : delayLabel;
117
123
  const accessInfo = useMemo<PaneFooterSegment[]>(() => {
118
- if (coverage) {
119
- return [{ id: "options-access", parts: [{ text: coverage.text, tone: coverage.tone }] }];
120
- }
124
+ const coverage = proCoverage ? optionsCoverageSegment(quoteCoverage.status, coverageDelay) : null;
125
+ if (coverage) return [coverage];
121
126
  return segment ? [segment] : [];
122
- }, [coverage?.text, coverage?.tone, segment]);
127
+ }, [coverageDelay, proCoverage, quoteCoverage.status, segment]);
123
128
 
124
129
  // Rebuilt every render so the quote and trade ages keep counting with the
125
130
  // stream; the registration compares by value, so only a changed label
@@ -1,6 +1,6 @@
1
1
  import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
2
2
  import type { OptionContract, OptionsChain, Quote } from "../../../types/financials";
3
- import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors";
3
+ import { buildQuoteKey, instrumentFromQuoteTarget, resolveEntryData } from "../../../market-data/selectors";
4
4
  import type { QueryEntry } from "../../../market-data/result-types";
5
5
  import type { OptionTableRow } from "./types";
6
6
  import { isFiniteNumber } from "../../../utils/guards";
@@ -75,9 +75,20 @@ function normalizeVisibleRange(
75
75
  return { start, end };
76
76
  }
77
77
 
78
+ /**
79
+ * The broker profile a chain came from. Its contracts' quotes are asked of the
80
+ * same profile, so a broker that streams options (and no cloud stream) keeps
81
+ * the chain it served current, rather than leaving it to a slower source.
82
+ */
83
+ export interface OptionQuoteBroker {
84
+ brokerId?: string;
85
+ brokerInstanceId?: string;
86
+ }
87
+
78
88
  export function buildOptionQuoteTargets(
79
89
  rows: readonly OptionTableRow[],
80
90
  window: OptionQuoteTargetWindow,
91
+ broker?: OptionQuoteBroker,
81
92
  ): QuoteSubscriptionTarget[] {
82
93
  if (rows.length === 0) return [];
83
94
  const visibleRange = normalizeVisibleRange(rows.length, window);
@@ -101,6 +112,7 @@ export function buildOptionQuoteTargets(
101
112
  targets.set(symbol, {
102
113
  symbol,
103
114
  exchange: OPTIONS_QUOTE_EXCHANGE,
115
+ ...(broker ? { context: { brokerId: broker.brokerId, brokerInstanceId: broker.brokerInstanceId } } : {}),
104
116
  surface: "options",
105
117
  visible,
106
118
  selected,
@@ -119,10 +131,12 @@ export function buildOptionQuoteTargets(
119
131
  return [...targets.values()];
120
132
  }
121
133
 
122
- export function buildOptionQuoteKey(contractSymbol: string): string {
134
+ export function buildOptionQuoteKey(contractSymbol: string, broker?: OptionQuoteBroker): string {
123
135
  return buildQuoteKey({
124
136
  symbol: contractSymbol,
125
137
  exchange: OPTIONS_QUOTE_EXCHANGE,
138
+ brokerId: broker?.brokerId,
139
+ brokerInstanceId: broker?.brokerInstanceId,
126
140
  });
127
141
  }
128
142
 
@@ -179,17 +193,20 @@ export interface LiveOptionsChain {
179
193
  }
180
194
 
181
195
  const EMPTY_STRIKES: ReadonlySet<number> = new Set();
182
- const contractKeyIndexes = new WeakMap<readonly OptionContract[], Map<string, number[]>>();
196
+ const contractKeyIndexes = new WeakMap<readonly OptionContract[], Map<string, Map<string, number[]>>>();
183
197
 
184
- function contractKeyIndex(contracts: readonly OptionContract[]): Map<string, number[]> {
185
- let index = contractKeyIndexes.get(contracts);
198
+ function contractKeyIndex(contracts: readonly OptionContract[], broker?: OptionQuoteBroker): Map<string, number[]> {
199
+ const byBroker = contractKeyIndexes.get(contracts) ?? new Map<string, Map<string, number[]>>();
200
+ contractKeyIndexes.set(contracts, byBroker);
201
+ const brokerKey = `${broker?.brokerId ?? ""}|${broker?.brokerInstanceId ?? ""}`;
202
+ let index = byBroker.get(brokerKey);
186
203
  if (!index) {
187
204
  index = new Map();
188
205
  contracts.forEach((contract, position) => {
189
- const key = buildOptionQuoteKey(contract.contractSymbol);
206
+ const key = buildOptionQuoteKey(contract.contractSymbol, broker);
190
207
  index!.set(key, [...(index!.get(key) ?? []), position]);
191
208
  });
192
- contractKeyIndexes.set(contracts, index);
209
+ byBroker.set(brokerKey, index);
193
210
  }
194
211
  return index;
195
212
  }
@@ -204,12 +221,13 @@ export function overlayOptionChainQuotes(
204
221
  chain: OptionsChain,
205
222
  quoteEntries: ReadonlyMap<string, QueryEntry<Quote>>,
206
223
  freshness: OptionsQuoteFreshness,
224
+ broker?: OptionQuoteBroker,
207
225
  ): LiveOptionsChain {
208
226
  if (quoteEntries.size === 0) return { chain, streamedStrikes: EMPTY_STRIKES, quotedAt: null };
209
227
  const streamedStrikes = new Set<number>();
210
228
  let quotedAt: number | null = null;
211
229
  const overlay = (contracts: OptionContract[]): OptionContract[] => {
212
- const index = contractKeyIndex(contracts);
230
+ const index = contractKeyIndex(contracts, broker);
213
231
  let next: OptionContract[] | null = null;
214
232
  for (const [key, entry] of quoteEntries) {
215
233
  const positions = index.get(key);
@@ -242,19 +260,16 @@ export function resolveOptionQuoteCoverage(
242
260
  quoteEntries: ReadonlyMap<string, QueryEntry<Quote>>,
243
261
  freshness: OptionsQuoteFreshness,
244
262
  ): OptionQuoteCoverage {
245
- const visibleSymbols = new Set(
263
+ // Each target's own key: a chain's broker profile is part of it.
264
+ const visibleKeys = new Set(
246
265
  targets
247
- .filter((target) => target.visible === true)
248
- .map((target) => target.symbol.trim().toUpperCase())
249
- .filter(Boolean),
266
+ .filter((target) => target.visible === true && target.symbol.trim())
267
+ .map((target) => buildQuoteKey(instrumentFromQuoteTarget(target))),
250
268
  );
251
269
  let liveCount = 0;
252
270
  let fallbackCount = 0;
253
- for (const symbol of visibleSymbols) {
254
- const quote = freshOptionQuote(
255
- quoteEntries.get(buildOptionQuoteKey(symbol)),
256
- freshness,
257
- );
271
+ for (const key of visibleKeys) {
272
+ const quote = freshOptionQuote(quoteEntries.get(key), freshness);
258
273
  if (
259
274
  quote?.dataSource === "live"
260
275
  && quote.delivery === "stream"
@@ -266,7 +281,7 @@ export function resolveOptionQuoteCoverage(
266
281
  }
267
282
  }
268
283
 
269
- const totalCount = visibleSymbols.size;
284
+ const totalCount = visibleKeys.size;
270
285
  const status: OptionQuoteCoverageStatus = totalCount > 0 && liveCount === totalCount
271
286
  ? "live"
272
287
  : liveCount > 0