gloomberb 0.18.1 → 0.18.3

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 (416) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/commodity-balances.ts +6 -0
  4. package/src/api-client/company-kpis.ts +9 -0
  5. package/src/api-client/crypto-markets.ts +9 -0
  6. package/src/api-client/data.ts +21 -5
  7. package/src/api-client/equity-screener.ts +2 -0
  8. package/src/api-client/index.ts +2 -0
  9. package/src/api-client/request.ts +5 -3
  10. package/src/api-client/telemetry.ts +17 -0
  11. package/src/api-client/types.ts +5 -0
  12. package/src/app/global-shortcuts.ts +1 -16
  13. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  14. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  15. package/src/app/runtime/plugin-bindings.ts +6 -2
  16. package/src/app/runtime/update.ts +28 -8
  17. package/src/architecture/pane-tabs-allowlist.json +65 -0
  18. package/src/brokers/logos.ts +66 -0
  19. package/src/brokers/profile-form.ts +3 -0
  20. package/src/brokers/remote-broker-adapter.ts +13 -3
  21. package/src/capabilities/index.ts +1 -1
  22. package/src/capabilities/service-capabilities.ts +7 -1
  23. package/src/cli/base-converter.ts +60 -18
  24. package/src/cli/commands/market.ts +354 -187
  25. package/src/cli/commands/news-search.ts +35 -8
  26. package/src/cli/commands/system.ts +35 -1
  27. package/src/cli/commands/ticker.ts +167 -60
  28. package/src/cli/currency-hints.ts +27 -0
  29. package/src/cli/fx-pair.ts +41 -4
  30. package/src/cli/glossary.ts +57 -4
  31. package/src/cli/help.ts +1 -1
  32. package/src/cli/helpers.ts +36 -3
  33. package/src/cli/index.ts +12 -1
  34. package/src/cli/issuer-membership.ts +103 -0
  35. package/src/cli/known-gaps.ts +37 -1
  36. package/src/cli/listing-arg.ts +51 -3
  37. package/src/cli/name-entries.ts +51 -12
  38. package/src/cli/pane-functions/capabilities.ts +2 -0
  39. package/src/cli/pane-functions/catalog.ts +19 -1
  40. package/src/cli/pane-functions/discovery.ts +15 -1
  41. package/src/cli/pane-functions/dom.ts +11 -5
  42. package/src/cli/pane-functions/freshness.ts +4 -4
  43. package/src/cli/pane-functions/function-help.ts +27 -3
  44. package/src/cli/pane-functions/headless.ts +31 -11
  45. package/src/cli/pane-functions/index.ts +67 -7
  46. package/src/cli/pane-functions/lock-note.ts +7 -4
  47. package/src/cli/pane-functions/resolver.ts +21 -6
  48. package/src/cli/quote-answer.ts +179 -0
  49. package/src/cli/report-tables.ts +136 -18
  50. package/src/cli/result.ts +12 -4
  51. package/src/cli/series-search.ts +2 -1
  52. package/src/cli/symbol-list.ts +47 -11
  53. package/src/cli/types.ts +4 -0
  54. package/src/components/chart/composite/composite-chart.tsx +25 -7
  55. package/src/components/chart/composite/drawing-store.ts +26 -6
  56. package/src/components/chart/composite/format.ts +13 -1
  57. package/src/components/chart/composite/interactions.ts +2 -1
  58. package/src/components/chart/composite/level-labels.ts +30 -0
  59. package/src/components/chart/composite/levels.ts +17 -6
  60. package/src/components/chart/composite/panel-surface.tsx +69 -8
  61. package/src/components/chart/composite/types.ts +6 -0
  62. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  63. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  64. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  65. package/src/components/data-table/view.tsx +23 -3
  66. package/src/components/index.ts +3 -0
  67. package/src/components/layout/detached-pane-shell.tsx +1 -0
  68. package/src/components/layout/floating-pane.tsx +1 -0
  69. package/src/components/layout/header.tsx +228 -55
  70. package/src/components/layout/pane/content.tsx +16 -14
  71. package/src/components/layout/pane/footer/index.tsx +22 -3
  72. package/src/components/layout/pane/footer/model.ts +99 -4
  73. package/src/components/layout/pane/header-tabs.tsx +15 -1
  74. package/src/components/layout/pane/header.tsx +10 -3
  75. package/src/components/layout/pane/index.tsx +1 -0
  76. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  77. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  78. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  79. package/src/components/onboarding/wizard-model.ts +10 -3
  80. package/src/components/ui/button.tsx +11 -4
  81. package/src/components/ui/choice-dialog.tsx +8 -5
  82. package/src/components/ui/display.tsx +11 -6
  83. package/src/components/ui/fields.tsx +100 -20
  84. package/src/components/ui/icon.tsx +6 -1
  85. package/src/components/ui/list-row-lines.tsx +87 -0
  86. package/src/components/ui/list-view.tsx +14 -0
  87. package/src/components/ui/logo-mark.tsx +64 -0
  88. package/src/components/ui/number-step.ts +54 -0
  89. package/src/components/ui/select-button.tsx +5 -4
  90. package/src/components/ui/select-field.tsx +3 -3
  91. package/src/components/ui/stat-grid.tsx +28 -3
  92. package/src/components/ui/status.tsx +20 -4
  93. package/src/components/ui/tabs.tsx +9 -1
  94. package/src/components/ui/toggle.tsx +17 -3
  95. package/src/core/app-services.ts +4 -0
  96. package/src/core/state/app/instrument.ts +8 -3
  97. package/src/data/config/store/normalize.ts +4 -1
  98. package/src/data/plugin-cache.ts +32 -6
  99. package/src/docs/snapshot.json +5 -5
  100. package/src/i18n/es.ts +31 -3
  101. package/src/i18n/ja.ts +31 -3
  102. package/src/i18n/ko.ts +31 -3
  103. package/src/i18n/zh-cn.ts +32 -4
  104. package/src/i18n/zh-tw.ts +32 -4
  105. package/src/market-data/commodity-names.ts +80 -0
  106. package/src/market-data/coordinator/auxiliary.ts +3 -1
  107. package/src/market-data/coordinator/chart.ts +3 -1
  108. package/src/market-data/futures-board.ts +38 -1
  109. package/src/market-data/futures-contract.ts +91 -0
  110. package/src/market-data/fx-closes.ts +78 -1
  111. package/src/market-data/market/change-basis.ts +219 -0
  112. package/src/market-data/market/format.ts +28 -12
  113. package/src/market-data/market/freshness.ts +38 -0
  114. package/src/market-data/market/status.ts +55 -9
  115. package/src/market-data/market/year-to-date.ts +46 -0
  116. package/src/market-data/quotes/contributions.ts +12 -0
  117. package/src/market-data/quotes/delisting.ts +34 -0
  118. package/src/market-data/quotes/live-valuation.ts +23 -0
  119. package/src/market-data/quotes/resolution.ts +8 -2
  120. package/src/market-data/tax-equivalent.ts +10 -2
  121. package/src/notifications/app-notifier.ts +2 -2
  122. package/src/notifications/notification-log.ts +8 -6
  123. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  124. package/src/plugins/builtin/alerts/cli.ts +26 -18
  125. package/src/plugins/builtin/alerts/events.ts +1 -0
  126. package/src/plugins/builtin/alerts/headless.ts +1 -1
  127. package/src/plugins/builtin/alerts/index.tsx +13 -4
  128. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  129. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  130. package/src/plugins/builtin/attention/pane.tsx +16 -14
  131. package/src/plugins/builtin/attention/trending.tsx +8 -6
  132. package/src/plugins/builtin/awards/pane.tsx +6 -6
  133. package/src/plugins/builtin/backtest/client.ts +5 -2
  134. package/src/plugins/builtin/backtest/export.ts +1 -1
  135. package/src/plugins/builtin/backtest/window.ts +16 -3
  136. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  137. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  138. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  139. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  140. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  141. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  142. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  143. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  144. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  145. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  146. package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
  147. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  148. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  149. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  150. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  151. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  152. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  153. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  154. package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
  155. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  156. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  157. package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
  158. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  159. package/src/plugins/builtin/buildout/format.ts +1 -0
  160. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  161. package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
  162. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  163. package/src/plugins/builtin/catalysts/client.ts +2 -2
  164. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  165. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  166. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  167. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  168. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  169. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  170. package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
  171. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  172. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  173. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  174. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  175. package/src/plugins/builtin/chat/index.tsx +1 -1
  176. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  177. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  178. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  179. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  180. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  181. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  182. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  183. package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
  184. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  185. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  186. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  187. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  188. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  189. package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
  190. package/src/plugins/builtin/composite-plugins.ts +3 -2
  191. package/src/plugins/builtin/correlation/headless.ts +2 -1
  192. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  193. package/src/plugins/builtin/correlation/settings.ts +1 -0
  194. package/src/plugins/builtin/cot/headless.ts +1 -1
  195. package/src/plugins/builtin/cot/index.tsx +1 -1
  196. package/src/plugins/builtin/cot/model.ts +35 -12
  197. package/src/plugins/builtin/cot/pane.tsx +4 -3
  198. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  199. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  200. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  201. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  202. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  203. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  204. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  205. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  206. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  207. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  208. package/src/plugins/builtin/doe/pane.tsx +1 -1
  209. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  210. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  211. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  212. package/src/plugins/builtin/econ/index.tsx +31 -4
  213. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  214. package/src/plugins/builtin/equity-screener/headless.ts +30 -5
  215. package/src/plugins/builtin/equity-screener/model.ts +68 -2
  216. package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
  217. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  218. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  219. package/src/plugins/builtin/exposure/client.ts +4 -1
  220. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  221. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  222. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  223. package/src/plugins/builtin/exposure/pane.tsx +43 -21
  224. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  225. package/src/plugins/builtin/futures/table.tsx +6 -3
  226. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  227. package/src/plugins/builtin/gpu/board.tsx +4 -1
  228. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  229. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  230. package/src/plugins/builtin/gpu/history.tsx +4 -2
  231. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  232. package/src/plugins/builtin/holders/client.ts +2 -1
  233. package/src/plugins/builtin/holders/headless.ts +2 -1
  234. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  235. package/src/plugins/builtin/insider/base-values.ts +76 -0
  236. package/src/plugins/builtin/insider/client.ts +3 -0
  237. package/src/plugins/builtin/insider/headless.ts +48 -10
  238. package/src/plugins/builtin/insider/index.tsx +17 -8
  239. package/src/plugins/builtin/insider/issuer.ts +34 -4
  240. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  241. package/src/plugins/builtin/insider/table-model.ts +35 -13
  242. package/src/plugins/builtin/insider/table.tsx +5 -2
  243. package/src/plugins/builtin/iv-history/events.ts +145 -0
  244. package/src/plugins/builtin/iv-history/format.ts +3 -0
  245. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  246. package/src/plugins/builtin/jobs/headless.ts +2 -2
  247. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  248. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  249. package/src/plugins/builtin/market-halts/model.ts +31 -6
  250. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  251. package/src/plugins/builtin/market-movers/client.ts +4 -3
  252. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  253. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  254. package/src/plugins/builtin/members/pane.tsx +8 -3
  255. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  256. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  257. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  258. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  259. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  260. package/src/plugins/builtin/notes/cli.ts +1 -1
  261. package/src/plugins/builtin/notes/export.ts +17 -6
  262. package/src/plugins/builtin/notes/files.ts +16 -5
  263. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  264. package/src/plugins/builtin/options/footer.ts +25 -20
  265. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  266. package/src/plugins/builtin/options/model-spot.ts +70 -4
  267. package/src/plugins/builtin/options/view.tsx +118 -35
  268. package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
  269. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  270. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  271. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  272. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  273. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  274. package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
  275. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  276. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  277. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  278. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  279. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  280. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  281. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  282. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  283. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  284. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  285. package/src/plugins/builtin/power/client.ts +2 -2
  286. package/src/plugins/builtin/power/pane.tsx +36 -31
  287. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  288. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  289. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  290. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  291. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  292. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  293. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  294. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  295. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  296. package/src/plugins/builtin/scanner/feed.ts +6 -1
  297. package/src/plugins/builtin/scanner/headless.ts +1 -1
  298. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  299. package/src/plugins/builtin/seasonality/client.ts +2 -1
  300. package/src/plugins/builtin/sectors/client.ts +4 -3
  301. package/src/plugins/builtin/series-inventory.ts +1 -0
  302. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  303. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  304. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  305. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  306. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  307. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  308. package/src/plugins/builtin/shared/research-trading.tsx +64 -0
  309. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  310. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  311. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  312. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  313. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  314. package/src/plugins/builtin/themes/headless.ts +8 -2
  315. package/src/plugins/builtin/themes/model.ts +43 -10
  316. package/src/plugins/builtin/themes/pane.tsx +8 -6
  317. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  318. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  319. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  320. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  321. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  322. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  323. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  324. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  325. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  326. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  327. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  328. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  329. package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
  330. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  331. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  332. package/src/plugins/builtin-editorial.ts +1 -1
  333. package/src/plugins/compat.ts +3 -0
  334. package/src/plugins/external-runtime.ts +5 -0
  335. package/src/plugins/loader.ts +6 -0
  336. package/src/plugins/plugin-icon.ts +61 -0
  337. package/src/plugins/registry/host-actions.ts +2 -1
  338. package/src/plugins/registry/index.ts +9 -1
  339. package/src/plugins/runtime/context.tsx +5 -0
  340. package/src/plugins/runtime/index.tsx +3 -0
  341. package/src/portfolio/column-values.ts +13 -4
  342. package/src/portfolio/cost-fx.ts +5 -28
  343. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  344. package/src/public/broker.ts +8 -0
  345. package/src/public/market-data.ts +5 -0
  346. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  347. package/src/renderers/dom/desktop/controls.tsx +40 -8
  348. package/src/renderers/dom/desktop/icons.tsx +14 -0
  349. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  350. package/src/renderers/dom/desktop/menu.tsx +10 -1
  351. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  352. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  353. package/src/renderers/dom/host/style.ts +6 -1
  354. package/src/renderers/dom/host/tabs.tsx +22 -5
  355. package/src/renderers/dom/host/top-surface.ts +127 -0
  356. package/src/renderers/dom/styles.css +51 -5
  357. package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
  358. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  359. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  360. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  361. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  362. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  363. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  364. package/src/renderers/electrobun/view/news-source.ts +4 -2
  365. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  366. package/src/renderers/opentui/start.tsx +7 -0
  367. package/src/series/validate.ts +2 -2
  368. package/src/sources/gloomberb-cloud/index.ts +57 -17
  369. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  370. package/src/sources/history-result.ts +3 -0
  371. package/src/sources/provider-errors.ts +3 -1
  372. package/src/sources/provider-router/history.ts +49 -6
  373. package/src/sources/provider-router/index.ts +18 -1
  374. package/src/state/app/activity.ts +13 -0
  375. package/src/sync/controller.ts +4 -0
  376. package/src/sync/core-contributors.ts +62 -11
  377. package/src/sync/local-only.ts +121 -0
  378. package/src/sync/react.ts +6 -0
  379. package/src/sync/types.ts +3 -0
  380. package/src/telemetry/crash-reports.ts +4 -0
  381. package/src/telemetry/update-failures-node.ts +59 -0
  382. package/src/telemetry/update-failures.ts +307 -0
  383. package/src/tickers/company-name-listing.ts +9 -7
  384. package/src/tickers/company-name.ts +43 -0
  385. package/src/tickers/listing-fate.ts +57 -8
  386. package/src/tickers/search/index.ts +24 -3
  387. package/src/time-series/price-comparison.ts +35 -0
  388. package/src/time-series/resolve.ts +52 -11
  389. package/src/time-series/spec.ts +1 -1
  390. package/src/time-series/types.ts +5 -0
  391. package/src/types/broker.ts +56 -1
  392. package/src/types/config.ts +6 -0
  393. package/src/types/data-provider.ts +11 -1
  394. package/src/types/financials.ts +23 -0
  395. package/src/types/headless.ts +19 -2
  396. package/src/types/instrument.ts +4 -0
  397. package/src/types/issuer-listings.ts +28 -0
  398. package/src/types/plugin.ts +24 -6
  399. package/src/types/price-history.ts +6 -1
  400. package/src/types/trading.ts +67 -0
  401. package/src/ui/host.tsx +4 -0
  402. package/src/updater/failure.ts +163 -0
  403. package/src/updater/index.ts +87 -23
  404. package/src/utils/base-currency.ts +108 -0
  405. package/src/utils/currency-names.ts +128 -0
  406. package/src/utils/currency-pair.ts +28 -1
  407. package/src/utils/currency-units.ts +32 -1
  408. package/src/utils/dom-screenshot.ts +22 -9
  409. package/src/utils/exchange-notation.generated.ts +360 -0
  410. package/src/utils/exchanges.ts +15 -322
  411. package/src/utils/format.ts +31 -1
  412. package/src/utils/instrument-identity.ts +38 -1
  413. package/src/utils/options.ts +46 -1
  414. package/src/utils/price-history.ts +4 -0
  415. package/src/utils/reported-money.ts +8 -4
  416. package/src/utils/sec.ts +68 -0
@@ -2,10 +2,19 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
2
  import { resolveHeadlessInstrument } from "../shared/headless-market-data";
3
3
  import { createRealizedVolatilityDependencies, loadRealizedVolatilityHistory } from "../realized-vol/client";
4
4
  import { createHvDependencies, loadIvHistory, loadIvScreen, loadScreenVolatilities } from "./client";
5
- import { formatPoints, formatRank, formatSpot, formatStat, formatVol, readingLabel, sharedDates, skewHeader, VCA_DEFINITIONS, verdictLabel } from "./format";
5
+ import { formatPoints, formatRank, formatSpot, formatStat, formatVol, IMPLIED_VOL_WORDS, readingLabel, sharedDates, skewHeader, VCA_DEFINITIONS, verdictLabel } from "./format";
6
6
  import { HV_WINDOWS, type HvWindow, type IvLookback, type IvStatRow, projectIvHistory, projectRichCheap, type RichCheapRow, richCheapDates, screenCoverage, VCA_LIMIT, VCA_PRESETS } from "./model";
7
7
  import { vcaUniverse } from "./universe";
8
- import { listingFateNotes, loadListingFate, MovedSymbolError } from "../../../tickers/listing-fate";
8
+ import { offsetText, releaseOffsetRows } from "./events";
9
+ import { loadMacroReleases } from "../macro-day/client";
10
+ import { MACRO_EVENT_KINDS, MACRO_EVENT_LABELS, MACRO_RELEASE_SOURCES, staleMacroReleasesNotice, type MacroEventKind, type MacroRelease,
11
+ type MacroReleaseList } from "../macro-day/releases";
12
+ import { lookupScheduledDates } from "../alerts/scheduled";
13
+ import { fetchCpiBoard } from "../cpi/client";
14
+ import { fetchRatePath } from "../rate-path/client";
15
+ import type { HeadlessPaneContext } from "../../../types/headless";
16
+ import { zonedDateKey } from "../../../utils/zoned-date-time";
17
+ import { listingFateNotes, listingFateReport, loadListingFate, MovedSymbolError } from "../../../tickers/listing-fate";
9
18
  import { startTickerChangeLookup } from "../../../tickers/ticker-changes";
10
19
 
11
20
  const METHODOLOGY = "docs/research-data.md#implied-volatility-history";
@@ -17,23 +26,33 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
17
26
  usListingsOnly: "Implied volatility history",
18
27
  freshness: { status: "not-a-feed", basis: "daily implied volatility", cadence: "daily", observedKey: "date", oldest: null },
19
28
  describe: (args) => `HIVG ${args.symbols[0] ?? ""}`,
20
- discovery: { aliases: ["HIVG"], screenshotReadiness: "live-dom", dataRequirements: ["Cloud stored implied volatility", "Daily price history"],
29
+ discovery: { aliases: ["HIVG"], suggestFor: IMPLIED_VOL_WORDS, screenshotReadiness: "live-dom", dataRequirements: ["Cloud stored implied volatility", "Daily price history"],
21
30
  limitations: ["History starts February 2024 (daily option trade closes).", "Rank and percentile use trade-close readings only.",
22
31
  "IV1Y needs long-dated trades: it can start later, has gaps, and ranks from 60 prior sessions.",
23
32
  "skew25d (put-call) and rr25d (call-put) come from the daily quote captures and are blank before the first one."] },
24
33
  options: [
25
34
  { key: "lookback", type: "enum", values: [{ value: "1Y" }, { value: "2Y" }, { value: "ALL" }], defaultValue: "1Y", description: "Visible history" },
26
35
  { key: "hvWindow", type: "enum", values: HV_WINDOWS.map((window) => ({ value: String(window) })), defaultValue: "20", description: "Realized volatility window in sessions" },
36
+ { key: "event", type: "enum", aliases: ["release"], values: MACRO_EVENT_KINDS.map((value) => ({ value, ...value === "jobs" ? { aliases: ["nfp", "payrolls"] } : {} })),
37
+ description: "One row per release of this kind whose session at --offset has closed (the next scheduled FOMC or CPI included), in place of the daily history",
38
+ example: "--event fomc --offset -6", shot: { reportOnly: "lists the readings around past release days" } },
39
+ { key: "offset", type: "integer", minimum: -60, maximum: 60, defaultValue: 0,
40
+ description: "Sessions from each release with --event: 0 is the first session on or after the release day, -6 the sixth session before that",
41
+ example: "--event cpi --offset -1", shot: { reportOnly: "picks the session read around each release" } },
27
42
  ],
28
43
  async load(args, ctx) {
44
+ const event = typeof args.options.event === "string" && args.options.event ? args.options.event as MacroEventKind : null;
45
+ const offset = Number(args.options.offset) || 0;
46
+ if (!event && offset !== 0) throw new Error("--offset counts sessions from a release day: add --event fomc, cpi or jobs.");
29
47
  const instrument = await resolveHeadlessInstrument(ctx, args.symbols[0]!);
30
48
  // What became of the symbol, as `quote` reads it, looked up beside the figures: FB's are the fund listed
31
49
  // there now, TWTR stopped trading, and HFC, which became DINO, has none of its own.
32
50
  const changes = startTickerChangeLookup(ctx.marketData, instrument.symbol, instrument.exchange);
33
51
  const quote = ctx.marketData.getQuote(instrument.symbol, instrument.exchange ?? "").catch(() => null);
34
- const [payload, prices] = await Promise.all([
52
+ const [payload, prices, releases] = await Promise.all([
35
53
  loadIvHistory(instrument.symbol, { signal: ctx.signal }, ctx.apiClient),
36
54
  loadRealizedVolatilityHistory({ instrument, signal: ctx.signal }, createRealizedVolatilityDependencies(ctx.marketData)),
55
+ event ? eventReleases(event, ctx) : null,
37
56
  ]);
38
57
  const fate = await loadListingFate(instrument, ctx.marketData, { quote, changes });
39
58
  if (fate.moved) throw new MovedSymbolError(fate.moved);
@@ -57,11 +76,47 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
57
76
  const skewColumns = [{ key: "skew25d", header: skewHeader("skew25d", "put-call"), format: points },
58
77
  { key: "rr25d", header: skewHeader("rr25d", "call-put"), format: points }];
59
78
  const served = model.iv30.length > 0 || prices.history.length > 0;
60
- const fateNotes = listingFateNotes(fate, { served });
61
79
  // With nothing served, the fate is why: no "not a ticker" or missing-history errors beside it.
62
- const errors = !served && fateNotes.length > 0 ? []
63
- : [prices.error, ...model.warnings].filter((value): value is string => !!value);
80
+ const { notes: fateNotes, errors } = listingFateReport(fate, { served },
81
+ [prices.error, ...model.warnings].filter((value): value is string => !!value));
64
82
  const notes = [...fateNotes, ...[model.oneYearNote, model.skewNote].filter((note): note is string => !!note)];
83
+ if (event && releases) {
84
+ const lookback = String(args.options.lookback) as IvLookback;
85
+ const label = MACRO_EVENT_LABELS[event];
86
+ const offsets = releaseOffsetRows(model, releases.list.releases, event, offset, {
87
+ lookback, readThrough: payload.coverage?.backfilledThrough ?? null });
88
+ const plural = (count: number, one: string, many: string) => count === 1 ? one : many;
89
+ const eventNotes = [
90
+ offsets.beforeLookback > 0
91
+ ? `${offsets.beforeLookback} earlier ${label} ${plural(offsets.beforeLookback, "release is", "releases are")} outside the ${lookback} lookback; --lookback ALL adds ${plural(offsets.beforeLookback, "it", "them")}.` : null,
92
+ // Said once the lookback says nothing: the history's own start is then where the rows stop.
93
+ offsets.beforeHistory > 0 && offsets.beforeLookback === 0
94
+ ? `${offsets.beforeHistory} earlier ${label} ${plural(offsets.beforeHistory, "release predates", "releases predate")} the IV history, which starts ${offsets.historyStart}.` : null,
95
+ offsets.pending.length > 0
96
+ ? `Left out ${label} ${offsets.pending.join(", ")}: no closing reading yet for ${plural(offsets.pending.length, "its session", "their sessions")} at --offset ${offset}.` : null,
97
+ releases.upcomingNote,
98
+ staleMacroReleasesNotice(releases.list),
99
+ ].filter((note): note is string => !!note);
100
+ const eventColumns = [{ key: "releaseDate", header: "Release date" }, { key: "session", header: "Session" },
101
+ ...historyColumns.slice(1), ...skewColumns];
102
+ return {
103
+ sections: [{
104
+ title: `${label} releases, ${offsetText(offset)}`,
105
+ // The export names the release and the offset on every row, so files of several offsets or releases join.
106
+ columns: [{ key: "release", header: "Release" }, eventColumns[0]!, { key: "offset", header: "Offset (sessions)" }, ...eventColumns.slice(1)],
107
+ rows: offsets.rows.map(({ date, ...row }) => ({ ...row, session: date, release: label })),
108
+ textColumns: offsets.rows.some((row) => row.skew25d != null) ? eventColumns : eventColumns.slice(0, -skewColumns.length),
109
+ }],
110
+ complete: payload.status === "ready" && errors.length === 0,
111
+ unavailableSymbols: offsets.rows.some((row) => row.iv30 != null) ? [] : [instrument.symbol], errors,
112
+ ...(notes.length + eventNotes.length > 0 ? { notes: [...notes, ...eventNotes] } : {}),
113
+ // Dated by the history read, not by the newest release's session: an old release is no stale reading.
114
+ ...(model.asOf ? { freshness: { asOf: model.asOf } } : {}),
115
+ metadata: { status: payload.status, coverage: payload.coverage, event, offset, releasesThrough: releases.list.coveredThrough,
116
+ nextRelease: releases.upcoming,
117
+ releaseSource: MACRO_RELEASE_SOURCES[event], unit: "decimal annualized volatility", methodology: METHODOLOGY },
118
+ };
119
+ }
65
120
  return {
66
121
  sections: [
67
122
  { title: "Statistics", columns: [
@@ -153,3 +208,32 @@ export const ivScreenHeadless: HeadlessPaneDefinition<"bundle"> = {
153
208
  };
154
209
  },
155
210
  };
211
+
212
+ /**
213
+ * The releases of `event` HIVG reads around: the days MDAY studies (the list
214
+ * the app ships with, extended by Gloom Cloud's), and the next one scheduled,
215
+ * from the CPI board's release calendar or the FOMC meetings WIRP prices, as
216
+ * scheduled alerts read them. Payrolls have no schedule to read.
217
+ */
218
+ async function eventReleases(event: MacroEventKind, ctx: HeadlessPaneContext): Promise<{
219
+ list: MacroReleaseList; upcoming: string | null; upcomingNote: string | null;
220
+ }> {
221
+ const label = MACRO_EVENT_LABELS[event];
222
+ const [published, next] = await Promise.all([
223
+ loadMacroReleases({}, () => ctx.apiClient.getCloudMacroReleaseDays()),
224
+ event === "jobs" ? null : lookupScheduledDates([{ kind: "macro", event }], {
225
+ earnings: async () => [],
226
+ cpi: () => fetchCpiBoard(ctx.apiClient),
227
+ fomc: async () => (await fetchRatePath(ctx.apiClient)).meetings,
228
+ }, zonedDateKey(Date.now(), "America/New_York")).then((dates) => dates.get(`macro:${event}`) ?? { date: null }),
229
+ ]);
230
+ const upcoming = next && "date" in next ? next.date : null;
231
+ const listed = upcoming != null && published.releases.some((release) => release.kind === event && release.date === upcoming);
232
+ const releases: MacroRelease[] = upcoming && !listed ? [...published.releases, { kind: event, date: upcoming }] : published.releases;
233
+ return {
234
+ list: { ...published, releases },
235
+ upcoming,
236
+ upcomingNote: next == null ? `The next ${label} release is not scheduled here, so it has no row yet.`
237
+ : "error" in next ? `The next ${label} date could not be loaded (${next.error}), so it has no row.` : null,
238
+ };
239
+ }
@@ -183,11 +183,11 @@ export const jobsHeadless: HeadlessPaneDefinition<"bundle"> = {
183
183
  const symbol = args.symbols[0];
184
184
  if (symbol) {
185
185
  const state = await fetchJobs(symbol, { force: true, client: ctx.apiClient as JobsCloudClient });
186
- if (state.kind === "denied") throw explainRefusal(new ApiRequestError(state.message, state.status), jobsLockNote, { symbol });
186
+ if (state.kind === "denied") throw await explainRefusal(new ApiRequestError(state.message, state.status), jobsLockNote, { symbol, context: ctx });
187
187
  return companyBundle(symbol, state, limit);
188
188
  }
189
189
  const payload = await fetchJobsMovers(ctx.apiClient as JobsCloudClient)
190
- .catch((error: unknown) => { throw explainRefusal(error, jobsLockNote, { symbol: null }); });
190
+ .catch(async (error: unknown) => { throw await explainRefusal(error, jobsLockNote, { symbol: null }); });
191
191
  const rows = buildMoverRows(payload.movers).slice(0, Math.max(limit, 15)).map((row) => ({
192
192
  ticker: row.ticker,
193
193
  company: row.company,
@@ -1,16 +1,27 @@
1
1
  import type { PaneLock } from "../../../types/plugin";
2
- import { proLockNote } from "../shared/pro-lock-note";
2
+ import type { HiringPayload } from "../../../api-client/hiring";
3
+ import { fetchAttention } from "../company-attention/client";
4
+ import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
5
+
6
+ /** HIRE's preview, for a company whose careers system it reads: Coffee Holding (JVA) is mapped but uncovered. */
7
+ const HIRE_ROUTE: FreeRoute = {
8
+ text: (ticker, company) => `gloomberb fn HIRE ${ticker} previews ${company}'s hiring with its latest roles, without the full history.`,
9
+ answers: async (symbol, context) => {
10
+ const data = await fetchAttention("hiring", symbol, { limit: 1 }, context.apiClient, context.signal) as HiringPayload;
11
+ return data.evidence.length + data.series.length + data.functions.length > 0;
12
+ },
13
+ };
3
14
 
4
15
  /**
5
16
  * JOBS is Pro whole. HIRE reads the same careers systems and needs no
6
17
  * account: a company's latest roles, without the full history.
7
18
  */
8
- export function jobsLockNote(lock: PaneLock): string {
19
+ export function jobsLockNote(lock: PaneLock): Promise<string> {
9
20
  return proLockNote(lock, {
10
21
  token: "JOBS",
11
22
  argument: "optional",
12
23
  does: "it reads each company's careers system into open roles, hiring pace and the mix by function and country",
13
24
  shows: "it",
14
- free: (ticker, company) => `gloomberb fn HIRE ${ticker} previews ${company}'s hiring with its latest roles, without the full history.`,
25
+ free: [HIRE_ROUTE],
15
26
  });
16
27
  }
@@ -39,7 +39,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
39
39
  const balances = resource.data;
40
40
  const gas = balances?.gas ?? null;
41
41
  const [storedTab, setTab] = usePluginPaneState<string>("commodity", "oil");
42
- const tab: BalanceCommodity = gas && storedTab === "gas" ? "gas" : "oil";
42
+ const tab: BalanceCommodity = storedTab === "gas" ? "gas" : "oil";
43
43
  const board = tab === "gas" ? gas : balances?.oil ?? null;
44
44
  const rows = board?.rows ?? EMPTY_ROWS;
45
45
  const [selectedId, setSelectedId] = usePluginPaneState<string | null>("selected", null);
@@ -54,7 +54,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
54
54
  const selectTab = useCallback((value: string) => {
55
55
  setTab(value === "gas" ? "gas" : "oil");
56
56
  }, [setTab]);
57
- const { strip: tabs, rows: tabRows } = usePaneTabs(gas ? {
57
+ const { strip: tabs, rows: tabRows } = usePaneTabs({
58
58
  tabs: TABS,
59
59
  activeValue: tab,
60
60
  onSelect: selectTab,
@@ -62,7 +62,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
62
62
  compact: true,
63
63
  variant: "underline",
64
64
  queryBarWidth: width,
65
- } : null);
65
+ });
66
66
  const info = useMemo(
67
67
  () => (resource.error || !board ? [] : [{ id: "month", parts: [{ text: formatBalanceMonth(board.month) }] }]),
68
68
  [board, resource.error],
@@ -139,7 +139,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
139
139
  onRootKeyDown={onRootKeyDown}
140
140
  renderCell={renderCell}
141
141
  selectedTextOverridesCellColor
142
- emptyStateTitle="No balances for this month."
142
+ emptyStateTitle={board ? "No balances for this month." : tab === "gas" ? "Gas balances unavailable." : "Oil balances unavailable."}
143
143
  />
144
144
  </Box>
145
145
  );
@@ -180,14 +180,33 @@ const DAY_MS = 86_400_000;
180
180
  /** Unresolved halts move out of the recent views at 30 elapsed days. */
181
181
  const LONG_TERM_MS = 30 * DAY_MS;
182
182
 
183
- export function formatHaltAge(record: HaltRecord, now: number): string {
183
+ /** How long a halt has kept trading stopped: to the trade resumption, or to now while it lasts. Quotes resuming do not end it. */
184
+ function haltDurationMs(record: HaltRecord, now: number): number {
184
185
  const end = record.tradeResumeAt == null ? now : Math.min(now, record.tradeResumeAt);
185
- const elapsed = Math.max(0, end - record.haltedAt);
186
+ return Math.max(0, end - record.haltedAt);
187
+ }
188
+
189
+ /** How long since the halt began, whether or not trading has resumed. */
190
+ function haltAgeMs(record: HaltRecord, now: number): number {
191
+ return Math.max(0, now - record.haltedAt);
192
+ }
193
+
194
+ function formatElapsed(elapsed: number): string {
186
195
  if (elapsed >= DAY_MS) return `${Math.floor(elapsed / DAY_MS)}d`;
187
196
  if (elapsed >= 3_600_000) return `${Math.floor(elapsed / 3_600_000)}h`;
188
197
  return `${Math.floor(elapsed / 60_000)}m`;
189
198
  }
190
199
 
200
+ /** HALTED FOR: RAIL halted 16:40 and resumed 17:15 was halted for 35m, read at any later time. */
201
+ export function formatHaltDuration(record: HaltRecord, now: number): string {
202
+ return formatElapsed(haltDurationMs(record, now));
203
+ }
204
+
205
+ /** AGE: RAIL's 16:40 halt is 1h old at 17:45, though it lasted 35m. */
206
+ export function formatHaltAge(record: HaltRecord, now: number): string {
207
+ return formatElapsed(haltAgeMs(record, now));
208
+ }
209
+
191
210
  export const HALT_FILTERS: ReadonlyArray<{ label: string; value: HaltFilter }> = [
192
211
  { label: "All", value: "all" },
193
212
  { label: "Active", value: "active" },
@@ -221,6 +240,7 @@ export type HaltColumnId =
221
240
  | "halted"
222
241
  | "quote"
223
242
  | "trade"
243
+ | "duration"
224
244
  | "status";
225
245
 
226
246
  export type HaltColumn = DataTableColumn & { id: HaltColumnId };
@@ -235,6 +255,7 @@ export const HALT_SORT_COLUMN_IDS: readonly HaltColumnId[] = [
235
255
  "halted",
236
256
  "quote",
237
257
  "trade",
258
+ "duration",
238
259
  "status",
239
260
  ];
240
261
 
@@ -262,16 +283,18 @@ const HALT_COLUMNS: readonly HaltColumn[] = [
262
283
  { id: "halted", label: "HALT ET", width: 9, align: "left" },
263
284
  { id: "quote", label: "QUOTE ET", width: 11, align: "left" },
264
285
  { id: "trade", label: "TRADE ET", width: 11, align: "left" },
286
+ { id: "duration", label: "HALTED FOR", width: 12, align: "right" },
265
287
  { id: "status", label: "STATUS", width: 8, align: "left" },
266
288
  ];
267
289
 
268
290
  /**
269
291
  * What a narrow pane gives up first, so STATUS never scrolls out of view. MKT
270
292
  * is only the venue, STATUS already reads QUOTE once quoting resumes, and
271
- * REASON spells out CODE. Trade resumption and halt time give way only in
272
- * the narrowest panes; symbol, reason, halt date, age and status always stay.
293
+ * REASON spells out CODE. Trade resumption, halt time and then the age, which
294
+ * the date and halt time also give, go only in the narrowest panes; symbol,
295
+ * reason, halt date, how long it was halted for and status always stay.
273
296
  */
274
- const NARROW_DROP_ORDER: readonly HaltColumnId[] = ["market", "quote", "code", "company", "trade", "halted"];
297
+ const NARROW_DROP_ORDER: readonly HaltColumnId[] = ["market", "quote", "code", "company", "trade", "halted", "age"];
275
298
 
276
299
  export function buildHaltColumns(width: number): HaltColumn[] {
277
300
  const columns = fitChartTableColumns(HALT_COLUMNS, width, NARROW_DROP_ORDER);
@@ -304,7 +327,9 @@ function haltSortValue(columnId: HaltColumnId, record: HaltRecord, now: number):
304
327
  case "halted":
305
328
  return record.haltedAt;
306
329
  case "age":
307
- return Math.max(0, Math.min(now, record.tradeResumeAt ?? now) - record.haltedAt);
330
+ return haltAgeMs(record, now);
331
+ case "duration":
332
+ return haltDurationMs(record, now);
308
333
  case "quote":
309
334
  return record.quoteResumeAt;
310
335
  case "trade":
@@ -29,6 +29,7 @@ import {
29
29
  formatEtResumption,
30
30
  formatEtTime,
31
31
  formatHaltAge,
32
+ formatHaltDuration,
32
33
  haltStatusColor,
33
34
  haltStatusLabel,
34
35
  nextHaltFilter,
@@ -179,6 +180,8 @@ export function MarketHaltsPane({ focused, width, height }: PaneProps) {
179
180
  text: formatEtResumption(row.tradeResumeAt, row.haltedAt),
180
181
  color: selectedColor ?? colors.textDim,
181
182
  };
183
+ case "duration":
184
+ return { text: formatHaltDuration(row, now), color: selectedColor ?? colors.textMuted };
182
185
  case "status": {
183
186
  const rowStatus = resolveHaltStatus(row, now);
184
187
  return {
@@ -5,6 +5,7 @@ import type {
5
5
  CloudSessionMoversSide,
6
6
  } from "../../../api-client/market-movers";
7
7
  import type { DataProvider } from "../../../types/data-provider";
8
+ import { logFailure } from "../../../utils/log-failure";
8
9
  import {
9
10
  CATEGORY_MAP,
10
11
  moverRegularSession,
@@ -63,7 +64,7 @@ async function hydrateTrending(
63
64
  if (provider.getQuotesBatch) {
64
65
  const results = await provider.getQuotesBatch(
65
66
  symbols.map((symbol) => ({ symbol, exchange: "" })),
66
- ).catch(() => []);
67
+ ).catch(logFailure("market-movers", "Trending quote batch failed", []));
67
68
  for (const result of results) {
68
69
  if (result.quote) bySymbol.set(result.target.symbol, screenerQuoteFromQuote(result.target.symbol, result.quote));
69
70
  }
@@ -72,8 +73,8 @@ async function hydrateTrending(
72
73
  try {
73
74
  const quote = await provider.getQuote(symbol, "");
74
75
  bySymbol.set(symbol, screenerQuoteFromQuote(symbol, quote));
75
- } catch {
76
- // Missing quotes are omitted from a ranked list.
76
+ } catch (error) {
77
+ logFailure("market-movers", "Trending quote failed")(error);
77
78
  }
78
79
  }));
79
80
  }
@@ -1,6 +1,7 @@
1
1
  import type { ScreenerQuote } from "../../../api-client/market-discovery";
2
2
  export type { ScreenerQuote } from "../../../api-client/market-discovery";
3
3
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
4
+ import { logFailure } from "../../../utils/log-failure";
4
5
  import type { PluginPersistence } from "../../../types/plugin";
5
6
  import { apiClient } from "../../../api-client";
6
7
  import type {
@@ -324,7 +325,7 @@ async function bestEffortMarketMetadata(
324
325
  let timeout: ReturnType<typeof setTimeout> | undefined;
325
326
  try {
326
327
  return await Promise.race([
327
- request.then((result) => result.data).catch(() => []),
328
+ request.then((result) => result.data).catch(logFailure("market-movers", "Market metadata failed", [])),
328
329
  new Promise<ScreenerQuote[]>((resolve) => {
329
330
  timeout = setTimeout(() => resolve([]), waitMs);
330
331
  }),
@@ -392,8 +393,8 @@ export async function fetchPreferredMarketMovers(
392
393
  stale: response.stale === true || response.data.stale === true,
393
394
  };
394
395
  }
395
- } catch {
396
- // The public backend snapshot remains available when live rankings fail.
396
+ } catch (error) {
397
+ logFailure("market-movers", "Live rankings failed")(error);
397
398
  }
398
399
 
399
400
  const fallback = await marketMetadata;
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import {
3
+ DELAYED_SEGMENT,
3
4
  PaneStatusBody, type DataTableCell,
4
5
  type DataTableColumn,
5
6
  type PaneFooterSegment
@@ -140,7 +141,7 @@ export function MarketValuationPane({ focused, width, height }: PaneProps) {
140
141
  if (!selectedView) return [];
141
142
  const info: PaneFooterSegment[] = [
142
143
  { id: "as-of", parts: [{ text: `as of ${selectedView.asOf}`, tone: "muted" }] },
143
- ...(width >= 80 ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
144
+ ...(width >= 80 ? [DELAYED_SEGMENT] : []),
144
145
  ];
145
146
  if (selectedView.observationStale) {
146
147
  info.push({ id: "stale", parts: [{ text: "STALE", tone: "warning", bold: true }] });
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useMemo, useRef, useState } from "react";
2
2
  import type { FundChange, FundMember } from "../../../api-client/members";
3
- import { DataTableStackView, DataTableView, DetailScrollBody, EmptyState, KeyValueRow, PaneFooterScope, PaneStatusBody, Prose, QueryBar, StatGrid,
3
+ import { delayedFooterSegment, DataTableStackView, DataTableView, DetailScrollBody, EmptyState, KeyValueRow, PaneFooterScope, PaneStatusBody, Prose, QueryBar, StatGrid,
4
4
  isSectionedItemRow, renderSectionedRowHeader, useExternalLinkFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, useQueryBarSearch,
5
5
  type DataTableCell, type DataTableColumn, type PaneFooterSegment, type SectionedRow } from "../../../components";
6
6
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
@@ -49,8 +49,13 @@ function Holdings({ fund, tab, width, height, focused, active }: ViewProps & { t
49
49
  return [
50
50
  { id: "coverage", parts: [{ text: coverageText(data.aggregate, width < 60), tone: partial ? "warning" : "muted" }] },
51
51
  // A capture's status line says the quotes' date and delay too; it does not say returns are unavailable.
52
- { id: "quote-date", title: `${date ?? "Quote date unavailable"} · 15m delayed · holdings ${data.asOf}`, quoteStatus: !data.quotesUnavailable,
53
- parts: [{ text: data.quotesUnavailable ? "returns unavailable" : width < 60 && dates.length === 1 ? `${date!.slice(5)} delayed` : `${date ? `${date} · ` : ""}15m delayed`, tone: "muted" }] },
52
+ ...(data.quotesUnavailable
53
+ ? [{ id: "quote-date", title: `${date ?? "Quote date unavailable"} · 15m delayed · holdings ${data.asOf}`, parts: [{ text: "returns unavailable", tone: "muted" as const }] }]
54
+ : [
55
+ delayedFooterSegment({ id: "quote-delay", detail: "15m delayed", delay: "15m", quoteStatus: true }),
56
+ ...(date ? [{ id: "quote-date", title: `${date} · 15m delayed · holdings ${data.asOf}`, quoteStatus: true,
57
+ parts: [{ text: width < 60 && dates.length === 1 ? date.slice(5) : date, tone: "muted" as const }] }] : []),
58
+ ]),
54
59
  ...(width >= 60 ? [{ id: "holdings-date", asOf: true, parts: [{ text: `holdings ${data.asOf}`, tone: "muted" as const }] }] : []),
55
60
  ];
56
61
  }, [data, width]);
@@ -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(),