gloomberb 0.18.0 → 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 (365) 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/market-movers.ts +7 -0
  8. package/src/api-client/types.ts +14 -0
  9. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  10. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  11. package/src/app/runtime/plugin-bindings.ts +6 -2
  12. package/src/brokers/logos.ts +66 -0
  13. package/src/brokers/profile-form.ts +3 -0
  14. package/src/brokers/remote-broker-adapter.ts +13 -3
  15. package/src/capabilities/index.ts +1 -1
  16. package/src/capabilities/service-capabilities.ts +7 -1
  17. package/src/cli/base-converter.ts +60 -18
  18. package/src/cli/commands/fred-series.ts +15 -5
  19. package/src/cli/commands/market.ts +290 -159
  20. package/src/cli/commands/news-search.ts +35 -8
  21. package/src/cli/commands/overview.ts +31 -6
  22. package/src/cli/commands/system.ts +35 -1
  23. package/src/cli/commands/ticker.ts +105 -52
  24. package/src/cli/currency-hints.ts +27 -0
  25. package/src/cli/fx-pair.ts +41 -4
  26. package/src/cli/glossary.ts +57 -4
  27. package/src/cli/helpers.ts +36 -3
  28. package/src/cli/index.ts +4 -1
  29. package/src/cli/known-gaps.ts +37 -1
  30. package/src/cli/listing-arg.ts +37 -2
  31. package/src/cli/pane-functions/catalog.ts +3 -1
  32. package/src/cli/pane-functions/discovery.ts +15 -1
  33. package/src/cli/pane-functions/dom.ts +9 -4
  34. package/src/cli/pane-functions/freshness.ts +4 -4
  35. package/src/cli/pane-functions/function-help.ts +31 -7
  36. package/src/cli/pane-functions/headless.ts +6 -5
  37. package/src/cli/pane-functions/index.ts +3 -2
  38. package/src/cli/pane-functions/lock-note.ts +7 -4
  39. package/src/cli/pane-functions/resolver.ts +2 -1
  40. package/src/cli/quote-answer.ts +179 -0
  41. package/src/cli/series-search.ts +2 -1
  42. package/src/cli/symbol-list.ts +51 -16
  43. package/src/cli/types.ts +4 -0
  44. package/src/components/chart/composite/composite-chart.tsx +22 -7
  45. package/src/components/chart/composite/format.ts +13 -1
  46. package/src/components/chart/composite/level-labels.ts +30 -0
  47. package/src/components/chart/composite/levels.ts +17 -6
  48. package/src/components/chart/composite/panel-surface.tsx +64 -16
  49. package/src/components/chart/composite/price-axis-labels.tsx +2 -1
  50. package/src/components/chart/composite/types.ts +4 -0
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  52. package/src/components/command-bar/workflow/collection-actions.ts +4 -0
  53. package/src/components/index.ts +2 -0
  54. package/src/components/layout/pane/content.tsx +16 -14
  55. package/src/components/layout/pane/footer/index.tsx +22 -3
  56. package/src/components/layout/pane/footer/model.ts +99 -4
  57. package/src/components/layout/pane/header-tabs.tsx +15 -1
  58. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  59. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  60. package/src/components/onboarding/wizard-model.ts +10 -3
  61. package/src/components/onboarding/wizard-positions.ts +8 -0
  62. package/src/components/ui/button.tsx +11 -4
  63. package/src/components/ui/choice-dialog.tsx +8 -5
  64. package/src/components/ui/display.tsx +11 -6
  65. package/src/components/ui/fields.tsx +100 -20
  66. package/src/components/ui/icon.tsx +6 -1
  67. package/src/components/ui/list-row-lines.tsx +87 -0
  68. package/src/components/ui/list-view.tsx +14 -0
  69. package/src/components/ui/logo-mark.tsx +64 -0
  70. package/src/components/ui/number-step.ts +54 -0
  71. package/src/components/ui/select-button.tsx +5 -4
  72. package/src/components/ui/select-field.tsx +3 -3
  73. package/src/components/ui/status.tsx +17 -3
  74. package/src/components/ui/tabs.tsx +9 -1
  75. package/src/components/ui/toggle.tsx +17 -3
  76. package/src/core/state/app/instrument.ts +8 -3
  77. package/src/data/config/store/normalize.ts +4 -1
  78. package/src/data/plugin-cache.ts +32 -6
  79. package/src/docs/snapshot.json +57 -22
  80. package/src/i18n/es.ts +9 -3
  81. package/src/i18n/ja.ts +9 -3
  82. package/src/i18n/ko.ts +9 -3
  83. package/src/i18n/zh-cn.ts +10 -4
  84. package/src/i18n/zh-tw.ts +10 -4
  85. package/src/market-data/commodity-names.ts +80 -0
  86. package/src/market-data/coordinator/auxiliary.ts +3 -1
  87. package/src/market-data/coordinator/chart.ts +3 -1
  88. package/src/market-data/futures-board.ts +38 -1
  89. package/src/market-data/futures-contract.ts +91 -0
  90. package/src/market-data/fx-closes.ts +78 -1
  91. package/src/market-data/market/change-basis.ts +219 -0
  92. package/src/market-data/market/format.ts +28 -12
  93. package/src/market-data/market/freshness.ts +38 -0
  94. package/src/market-data/market/status.ts +55 -9
  95. package/src/market-data/market/year-to-date.ts +46 -0
  96. package/src/market-data/quotes/contributions.ts +12 -0
  97. package/src/market-data/quotes/delisting.ts +34 -0
  98. package/src/market-data/quotes/live-valuation.ts +23 -0
  99. package/src/market-data/quotes/resolution.ts +8 -2
  100. package/src/market-data/tax-equivalent.ts +10 -2
  101. package/src/notifications/app-notifier.ts +2 -2
  102. package/src/notifications/notification-log.ts +8 -6
  103. package/src/plugins/builtin/alerts/cli.ts +26 -18
  104. package/src/plugins/builtin/alerts/headless.ts +1 -1
  105. package/src/plugins/builtin/alerts/index.tsx +13 -4
  106. package/src/plugins/builtin/analytics/index.tsx +1 -1
  107. package/src/plugins/builtin/analytics/risk-client.ts +44 -11
  108. package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
  109. package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
  110. package/src/plugins/builtin/analytics/risk-model.ts +79 -16
  111. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  112. package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
  113. package/src/plugins/builtin/attention/trending.tsx +8 -6
  114. package/src/plugins/builtin/backtest/client.ts +5 -2
  115. package/src/plugins/builtin/backtest/window.ts +16 -3
  116. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  117. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  118. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  119. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  120. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  121. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  122. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  123. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  124. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  125. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  126. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  127. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  128. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  129. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  130. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  131. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  132. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  133. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  134. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  135. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  136. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  137. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  138. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  139. package/src/plugins/builtin/buildout/format.ts +1 -0
  140. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  141. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  142. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  143. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  144. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  145. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  146. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  147. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  148. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  149. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  150. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  151. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  152. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  153. package/src/plugins/builtin/chat/index.tsx +1 -1
  154. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  155. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  156. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  157. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
  158. package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
  159. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  160. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  161. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  162. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  163. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  164. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  165. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  166. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  167. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  168. package/src/plugins/builtin/composite-plugins.ts +3 -2
  169. package/src/plugins/builtin/cot/headless.ts +1 -1
  170. package/src/plugins/builtin/cot/index.tsx +1 -1
  171. package/src/plugins/builtin/cot/model.ts +35 -12
  172. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  173. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  174. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  175. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  176. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  177. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  178. package/src/plugins/builtin/custom-view/loader.ts +3 -2
  179. package/src/plugins/builtin/custom-view/pane.tsx +5 -4
  180. package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
  181. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  182. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  183. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  184. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  185. package/src/plugins/builtin/econ/calendar-model.ts +38 -2
  186. package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
  187. package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
  188. package/src/plugins/builtin/econ/index.tsx +31 -4
  189. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  190. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  191. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  192. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  193. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  194. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  195. package/src/plugins/builtin/executives/data.ts +24 -8
  196. package/src/plugins/builtin/executives/index.tsx +2 -18
  197. package/src/plugins/builtin/executives/pane.tsx +34 -10
  198. package/src/plugins/builtin/exposure/client.ts +4 -1
  199. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  200. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  201. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  202. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  203. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  204. package/src/plugins/builtin/futures/table.tsx +6 -3
  205. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  206. package/src/plugins/builtin/insider/base-values.ts +72 -0
  207. package/src/plugins/builtin/insider/headless.ts +29 -4
  208. package/src/plugins/builtin/jobs/headless.ts +2 -2
  209. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  210. package/src/plugins/builtin/market-halts/model.ts +31 -6
  211. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  212. package/src/plugins/builtin/market-movers/client.ts +2 -2
  213. package/src/plugins/builtin/market-movers/index.tsx +0 -3
  214. package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
  215. package/src/plugins/builtin/market-movers/session.ts +25 -10
  216. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  217. package/src/plugins/builtin/members/pane.tsx +8 -3
  218. package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
  219. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  220. package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
  221. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
  222. package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
  223. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  224. package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
  225. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  226. package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
  227. package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
  228. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  229. package/src/plugins/builtin/notes/cli.ts +33 -8
  230. package/src/plugins/builtin/notes/export.ts +49 -3
  231. package/src/plugins/builtin/notes/files.ts +14 -3
  232. package/src/plugins/builtin/notes/index.tsx +3 -3
  233. package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
  234. package/src/plugins/builtin/notes/store.ts +13 -3
  235. package/src/plugins/builtin/options/footer.ts +25 -20
  236. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  237. package/src/plugins/builtin/options/view.tsx +77 -22
  238. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  239. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  240. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  241. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
  242. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
  243. package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
  244. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
  245. package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
  246. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  247. package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
  248. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  249. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
  250. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
  251. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  252. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  253. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  254. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  255. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  256. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  257. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  258. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  259. package/src/plugins/builtin/scanner/feed.ts +6 -1
  260. package/src/plugins/builtin/scanner/headless.ts +1 -1
  261. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  262. package/src/plugins/builtin/series-inventory.ts +1 -0
  263. package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
  264. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  265. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  266. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  267. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  268. package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
  269. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  270. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  271. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  272. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  273. package/src/plugins/builtin/themes/headless.ts +8 -2
  274. package/src/plugins/builtin/themes/model.ts +43 -10
  275. package/src/plugins/builtin/themes/pane.tsx +8 -6
  276. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  277. package/src/plugins/builtin/thirteenf/data.ts +5 -1
  278. package/src/plugins/builtin/thirteenf/headless.ts +3 -1
  279. package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
  280. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  281. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  282. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  283. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  284. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  285. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  286. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  287. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  288. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  289. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  290. package/src/plugins/builtin-editorial.ts +1 -1
  291. package/src/plugins/compat.ts +3 -0
  292. package/src/plugins/external-runtime.ts +5 -0
  293. package/src/plugins/loader.ts +6 -0
  294. package/src/plugins/plugin-icon.ts +61 -0
  295. package/src/plugins/registry/host-actions.ts +2 -1
  296. package/src/plugins/registry/index.ts +9 -1
  297. package/src/plugins/runtime/context.tsx +5 -0
  298. package/src/plugins/runtime/index.tsx +3 -0
  299. package/src/portfolio/column-values.ts +13 -4
  300. package/src/portfolio/cost-fx.ts +61 -0
  301. package/src/portfolio/mutations.ts +16 -2
  302. package/src/portfolio/position-metrics.ts +45 -4
  303. package/src/portfolio/position-quantity.ts +43 -0
  304. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  305. package/src/public/broker.ts +8 -0
  306. package/src/public/market-data.ts +5 -0
  307. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  308. package/src/renderers/dom/desktop/controls.tsx +39 -7
  309. package/src/renderers/dom/desktop/icons.tsx +14 -0
  310. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  311. package/src/renderers/dom/desktop/menu.tsx +10 -1
  312. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  313. package/src/renderers/dom/host/style.ts +6 -1
  314. package/src/renderers/dom/host/tabs.tsx +6 -0
  315. package/src/renderers/dom/styles.css +12 -5
  316. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  317. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  318. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  319. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  320. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  321. package/src/series/validate.ts +2 -2
  322. package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
  323. package/src/sources/gloomberb-cloud/index.ts +50 -17
  324. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  325. package/src/sources/history-result.ts +3 -0
  326. package/src/sources/provider-errors.ts +3 -1
  327. package/src/sources/provider-router/history.ts +49 -6
  328. package/src/sources/provider-router/index.ts +2 -1
  329. package/src/state/app/activity.ts +13 -0
  330. package/src/sync/controller.ts +4 -0
  331. package/src/sync/core-contributors.ts +63 -11
  332. package/src/sync/local-only.ts +121 -0
  333. package/src/sync/react.ts +6 -0
  334. package/src/sync/types.ts +3 -0
  335. package/src/team/collections.ts +2 -0
  336. package/src/tickers/listing-fate.ts +27 -8
  337. package/src/tickers/metadata.ts +10 -0
  338. package/src/tickers/search/index.ts +24 -3
  339. package/src/time-series/price-comparison.ts +35 -0
  340. package/src/time-series/resolve.ts +52 -11
  341. package/src/time-series/spec.ts +1 -1
  342. package/src/time-series/types.ts +5 -0
  343. package/src/types/broker.ts +56 -1
  344. package/src/types/config.ts +6 -0
  345. package/src/types/data-provider.ts +5 -1
  346. package/src/types/financials.ts +23 -0
  347. package/src/types/headless.ts +12 -2
  348. package/src/types/instrument.ts +4 -0
  349. package/src/types/plugin.ts +24 -6
  350. package/src/types/price-history.ts +6 -1
  351. package/src/types/ticker.ts +9 -0
  352. package/src/types/trading.ts +67 -0
  353. package/src/ui/host.tsx +4 -0
  354. package/src/utils/base-currency.ts +108 -0
  355. package/src/utils/currency-names.ts +128 -0
  356. package/src/utils/currency-pair.ts +2 -1
  357. package/src/utils/currency-units.ts +41 -1
  358. package/src/utils/dom-screenshot.ts +22 -9
  359. package/src/utils/exchange-notation.generated.ts +360 -0
  360. package/src/utils/exchanges.ts +15 -322
  361. package/src/utils/format.ts +31 -1
  362. package/src/utils/instrument-identity.ts +38 -1
  363. package/src/utils/options.ts +46 -1
  364. package/src/utils/price-history.ts +4 -0
  365. package/src/utils/reported-money.ts +8 -4
@@ -1,4 +1,5 @@
1
1
  import type { PluginConfigState } from "../../../../types/plugin";
2
+ import { logFailure } from "../../../../utils/log-failure";
2
3
  import { DEFAULT_FEEDS } from "./default-feeds";
3
4
  import { hashString } from "./hash";
4
5
  import type { RssFeedConfig } from "./rss/parser";
@@ -8,11 +9,13 @@ const USER_FEEDS_KEY = "feeds";
8
9
  const DISABLED_DEFAULT_FEED_IDS_KEY = "disabledDefaultFeedIds";
9
10
 
10
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.";
11
13
 
12
14
  export interface NewsFeedSettings {
13
15
  userFeeds: RssFeedConfig[];
14
16
  disabledDefaultFeedIds: string[];
15
17
  needsMigration: boolean;
18
+ error?: string;
16
19
  }
17
20
 
18
21
  export interface UserNewsFeedInput {
@@ -24,14 +27,15 @@ export interface UserNewsFeedInput {
24
27
  enabled?: boolean;
25
28
  }
26
29
 
27
- function parseJsonArray(value: unknown): unknown[] {
30
+ function parseJsonArray(value: unknown): unknown[] | null {
28
31
  if (Array.isArray(value)) return value;
29
- if (typeof value !== "string" || !value.trim()) return [];
32
+ if (typeof value !== "string") return [];
30
33
  try {
31
34
  const parsed = JSON.parse(value);
32
35
  return Array.isArray(parsed) ? parsed : [];
33
- } catch {
34
- return [];
36
+ } catch (error) {
37
+ logFailure("news.feed-config", "Read stored news feed settings failed")(error);
38
+ return null;
35
39
  }
36
40
  }
37
41
 
@@ -93,6 +97,7 @@ function normalizeUserFeed(value: unknown): RssFeedConfig | null {
93
97
  enabled: record.enabled !== false,
94
98
  });
95
99
  } catch {
100
+ // Intentional ignore: invalid stored feed entries are omitted during normalization.
96
101
  return null;
97
102
  }
98
103
  }
@@ -131,14 +136,20 @@ function sameIdList(raw: unknown, normalized: string[]): boolean {
131
136
  }
132
137
 
133
138
  export function loadNewsFeedSettings(configState: PluginConfigState): NewsFeedSettings {
134
- 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 {
135
143
  const userFeedValues = parseJsonArray(rawUserFeeds);
136
- const userFeeds = userFeedValues
144
+ const userFeeds = (userFeedValues ?? [])
137
145
  .map(normalizeUserFeed)
138
146
  .filter((feed): feed is RssFeedConfig => !!feed);
139
147
 
140
- const rawDisabledIds = configState.get<unknown>(DISABLED_DEFAULT_FEED_IDS_KEY);
141
- 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
+ }
142
153
 
143
154
  const needsMigration = (rawUserFeeds !== null && !sameUserFeeds(rawUserFeeds, userFeeds))
144
155
  || (rawDisabledIds !== null && !sameIdList(rawDisabledIds, disabledDefaultFeedIds));
@@ -146,15 +157,24 @@ export function loadNewsFeedSettings(configState: PluginConfigState): NewsFeedSe
146
157
  return { userFeeds, disabledDefaultFeedIds, needsMigration };
147
158
  }
148
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
+
149
166
  export async function saveNewsFeedSettings(
150
167
  configState: PluginConfigState,
151
168
  settings: Pick<NewsFeedSettings, "userFeeds" | "disabledDefaultFeedIds">,
152
169
  ): Promise<void> {
170
+ requireReadableSettings(configState);
153
171
  await configState.set(USER_FEEDS_KEY, settings.userFeeds);
172
+ requireReadableSettings(configState);
154
173
  await configState.set(DISABLED_DEFAULT_FEED_IDS_KEY, settings.disabledDefaultFeedIds);
155
174
  }
156
175
 
157
- 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 [];
158
178
  const disabled = new Set(settings.disabledDefaultFeedIds);
159
179
  return [
160
180
  ...DEFAULT_FEEDS.filter((feed) => feed.enabled && !disabled.has(feed.id)),
@@ -166,7 +186,7 @@ export async function addUserNewsFeed(
166
186
  configState: PluginConfigState,
167
187
  input: UserNewsFeedInput,
168
188
  ): Promise<RssFeedConfig> {
169
- const settings = loadNewsFeedSettings(configState);
189
+ const settings = requireReadableSettings(configState);
170
190
  const feed = createUserFeed(input);
171
191
  const nextUserFeeds = [
172
192
  ...settings.userFeeds.filter((entry) => entry.id !== feed.id),
@@ -184,7 +204,7 @@ export async function updateUserNewsFeed(
184
204
  feedId: string,
185
205
  patch: Partial<UserNewsFeedInput>,
186
206
  ): Promise<RssFeedConfig | null> {
187
- const settings = loadNewsFeedSettings(configState);
207
+ const settings = requireReadableSettings(configState);
188
208
  const existing = settings.userFeeds.find((feed) => feed.id === feedId);
189
209
  if (!existing) return null;
190
210
  const nextFeed = createUserFeed({
@@ -203,7 +223,7 @@ export async function removeUserNewsFeed(
203
223
  configState: PluginConfigState,
204
224
  feedId: string,
205
225
  ): Promise<boolean> {
206
- const settings = loadNewsFeedSettings(configState);
226
+ const settings = requireReadableSettings(configState);
207
227
  const nextUserFeeds = settings.userFeeds.filter((feed) => feed.id !== feedId);
208
228
  if (nextUserFeeds.length === settings.userFeeds.length) return false;
209
229
  await saveNewsFeedSettings(configState, {
@@ -219,7 +239,7 @@ export async function setDefaultNewsFeedEnabled(
219
239
  enabled: boolean,
220
240
  ): Promise<boolean> {
221
241
  if (!DEFAULT_FEED_IDS.has(feedId)) return false;
222
- const settings = loadNewsFeedSettings(configState);
242
+ const settings = requireReadableSettings(configState);
223
243
  const disabled = new Set(settings.disabledDefaultFeedIds);
224
244
  if (enabled) disabled.delete(feedId);
225
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);
@@ -31,6 +31,7 @@ const isWebUrl = (value: unknown): value is string => {
31
31
  const url = new URL(value);
32
32
  return url.protocol === "https:" || url.protocol === "http:";
33
33
  } catch {
34
+ // Intentional ignore: malformed links fail letter validation like other invalid fields.
34
35
  return false;
35
36
  }
36
37
  };
@@ -8,6 +8,7 @@ import type {
8
8
  GloomPluginContext,
9
9
  } from "../../../../../types/plugin";
10
10
  import { formatFeedTime } from "../../../../../utils/datetime-format";
11
+ import { logFailure } from "../../../../../utils/log-failure";
11
12
  import { openNewsStoryPane } from "./pop-out";
12
13
  import { NEWS_QUERY_PRESETS } from "./query-presets";
13
14
 
@@ -75,9 +76,11 @@ let warming: Promise<unknown> | null = null;
75
76
  */
76
77
  async function warmLatestStories(service: NewsService): Promise<void> {
77
78
  if (service.getQueryState(NEWS_QUERY_PRESETS.feed).articles.length > 0) return;
78
- warming ??= service.load(NEWS_QUERY_PRESETS.feed).catch(() => null).finally(() => {
79
- warming = null;
80
- });
79
+ warming ??= service.load(NEWS_QUERY_PRESETS.feed)
80
+ .catch(logFailure("news.command-bar-search", "Load news for command bar search failed", null))
81
+ .finally(() => {
82
+ warming = null;
83
+ });
81
84
  await warming;
82
85
  }
83
86
 
@@ -14,6 +14,7 @@ import { useInlineTickers } from "../../../../../state/hooks/inline-tickers";
14
14
  import { truncateToDisplayWidth } from "../../../../../utils/format";
15
15
  import { wrapTextLines } from "../../../../../utils/text-wrap";
16
16
  import { formatDetailDate } from "../../../../../utils/datetime-format";
17
+ import { logFailure } from "../../../../../utils/log-failure";
17
18
  import { mergeNewsArticle } from "../../../../../news/news-model";
18
19
  import { usePluginPaneState } from "../../../../runtime";
19
20
  import { formatNewsCategory } from "../categories";
@@ -277,7 +278,7 @@ export function NewsDetailView({ item, focused, width, showTitle = true }: {
277
278
  }).then((choice) => {
278
279
  const url = choice ? coverage[Number(choice)]?.url : undefined;
279
280
  if (url) void rendererHost.openExternal(url);
280
- }).catch(() => {});
281
+ }).catch(logFailure("news.detail-view", "Choose news coverage failed"));
281
282
  }, [coverage, dialog, rendererHost]);
282
283
  usePaneFooter("news-detail:story", () => ({
283
284
  order: -1,
@@ -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,
@@ -15,6 +15,7 @@ import { colors } from "../../../../../theme/colors";
15
15
  import { collectNewsDisplayTickers } from "../../../../../news/ticker-symbols";
16
16
  import { useLoadNewsStory } from "../../../../../news/hooks";
17
17
  import { formatFeedTime } from "../../../../../utils/datetime-format";
18
+ import { logFailure } from "../../../../../utils/log-failure";
18
19
  import { nextHeaderSort } from "../../../../../utils/sort-values";
19
20
  import { formatNewsCategory } from "../categories";
20
21
  import { useOpenTickerChoice } from "../../../shared/ticker-choice";
@@ -270,7 +271,7 @@ export function NewsArticleStackView({
270
271
  const loadNewsStory = useLoadNewsStory();
271
272
  const prefetchStory = useCallback((article: MarketNewsItem) => {
272
273
  if ((article.items?.length ?? 0) > 0) return;
273
- void loadNewsStory(article.id).catch(() => {});
274
+ void loadNewsStory(article.id).catch(logFailure("news.table", "Prefetch news story failed"));
274
275
  }, [loadNewsStory]);
275
276
 
276
277
  // A crosshair pinned in the tab picks the newest story out by then, and
@@ -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(),
@@ -14,7 +14,12 @@ export interface FeedElement {
14
14
 
15
15
  export function resolveFeedUrl(value: string, base: string): string {
16
16
  if (!value.trim()) return "";
17
- try { return new URL(value.trim(), base).href; } catch { return ""; }
17
+ try {
18
+ return new URL(value.trim(), base).href;
19
+ } catch {
20
+ // Intentional ignore: malformed optional feed links are treated as absent.
21
+ return "";
22
+ }
18
23
  }
19
24
 
20
25
  // Read the structure of a syndication document without executing declarations or
@@ -14,6 +14,7 @@ export function isManagedNewsFeedUrl(value: string): boolean {
14
14
  const url = new URL(value);
15
15
  return url.origin === "https://api.gloom.sh" && url.pathname.replace(/\/$/, "") === "/news/rss";
16
16
  } catch {
17
+ // Intentional ignore: malformed URLs cannot identify the managed news feed.
17
18
  return false;
18
19
  }
19
20
  }
@@ -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 { apiClient } from "../../../api-client";
2
2
  import { withCliServices, withConfigData } from "../../../cli/scoped-context";
3
+ import type { CliServicesContext } from "../../../cli/types";
3
4
  import { CLI_COMMAND_GROUPS } from "../../../cli/help";
4
5
  import { dryRunNote } from "../../../cli/helpers";
5
6
  import { requireArg, takeOption } from "../../../cli/commands/command-utils";
@@ -7,9 +8,10 @@ import { EXCHANGE_OPTION, listingReadNotes, requireListingArg } from "../../../c
7
8
  import { createPluginPersistence } from "../../plugin-persistence";
8
9
  import type { CliCommandDef } from "../../../types/plugin";
9
10
  import { cliStyles } from "../../../utils/cli-output";
11
+ import { errorMessage } from "../../../utils/errors";
10
12
  import { logFailure } from "../../../utils/log-failure";
11
13
  import { readChatSessionState } from "../chat/controller/persistence";
12
- import { exportNotesToDirectory, type NotesExportSource } from "./export";
14
+ import { describeExportSkip, exportNotesToDirectory, type NotesExportSource } from "./export";
13
15
  import { NotesFiles } from "./files";
14
16
  import { CloudNotesStore } from "./store";
15
17
 
@@ -42,29 +44,52 @@ export const notesCliCommand: CliCommandDef = {
42
44
  // (personal and every team), one folder each.
43
45
  const dir = args[1] ?? `${config.dataDir}/notes-export-${new Date().toISOString().slice(0, 10)}`;
44
46
  const sources: NotesExportSource[] = [{ label: "local", store: notes }];
45
- await withCliServices(ctx, async (services) => {
47
+ // A Cloud source left out or unreadable is a warning, as `quote` reports
48
+ // a symbol it found nothing for: the local files are always written.
49
+ const warnings: string[] = [];
50
+ const leftOut = (what: string, failure: string) => (error: unknown) => {
51
+ logFailure("notes.cli", failure)(error);
52
+ warnings.push(`${what} (${errorMessage(error)}).`);
53
+ return null;
54
+ };
55
+ const addCloudSources = async (services: CliServicesContext) => {
46
56
  const cloudPersistence = createPluginPersistence(services.persistence.pluginState, services.persistence.resources, "plugin:gloomberb-cloud", "gloomberb-cloud");
47
57
  const session = readChatSessionState(cloudPersistence, null);
48
58
  if (!session?.sessionToken) return;
49
59
  apiClient.setSessionToken(session.sessionToken);
50
60
  const user = await apiClient.ensureVerifiedSession()
51
- .catch(logFailure("notes.cli", "Verify the saved session for the notes export failed", null));
61
+ .catch(leftOut("Skipped Gloom Cloud notes: the saved session could not be checked", "Verify the saved session for the notes export failed"));
52
62
  if (!user) return;
53
63
  const notesPersistence = createPluginPersistence(services.persistence.pluginState, services.persistence.resources, "plugin:notes", "notes");
54
64
  sources.push({ label: "mine", store: new CloudNotesStore({ kind: "user" }, notesPersistence) });
55
- const teams = await apiClient.listTeams().catch(logFailure("notes.cli", "List teams for the notes export failed", []));
65
+ const teams = await apiClient.listTeams()
66
+ .catch(leftOut("Skipped team notes: the team list could not be read", "List teams for the notes export failed")) ?? [];
56
67
  for (const team of teams) {
57
68
  sources.push({ label: team.name, store: new CloudNotesStore({ kind: "team", teamId: team.id }, notesPersistence) });
58
69
  }
70
+ };
71
+ // The Cloud stores read and cache through these services, so the
72
+ // export runs before they close.
73
+ const result = await withCliServices(ctx, async (services) => {
74
+ await addCloudSources(services);
75
+ return ctx.cliOptions.dryRun ? null : await exportNotesToDirectory(dir, sources);
59
76
  });
60
- if (ctx.cliOptions.dryRun) {
61
- ctx.printResult({ data: { dryRun: true, dir, sources: sources.map((source) => source.label) } }, {
77
+ if (!result) {
78
+ ctx.printResult({ data: { dryRun: true, dir, sources: sources.map((source) => source.label) }, ...(warnings.length > 0 ? { warnings } : {}) }, {
62
79
  text: (data) => `Would export ${data.sources.join(", ")} notes to ${data.dir}.${dryRunNote(true)}`,
63
80
  });
64
81
  return;
65
82
  }
66
- const result = await exportNotesToDirectory(dir, sources);
67
- ctx.printResult({ data: { dir: result.dir, files: result.files, sources: sources.map((source) => source.label) } }, {
83
+ warnings.push(...result.skipped.map(describeExportSkip));
84
+ ctx.printResult({
85
+ data: {
86
+ dir: result.dir,
87
+ files: result.files,
88
+ sources: result.exported,
89
+ ...(result.skipped.length > 0 ? { skipped: result.skipped } : {}),
90
+ },
91
+ ...(warnings.length > 0 ? { warnings } : {}),
92
+ }, {
68
93
  text: (data) => `Exported ${data.files} notes from ${data.sources.join(", ")} to ${data.dir}.`,
69
94
  });
70
95
  return;
@@ -1,3 +1,4 @@
1
+ import { errorMessage } from "../../../utils/errors";
1
2
  import type { NoteFileEntry, QuickNoteEntry } from "./files";
2
3
  import type { NotesStore } from "./store";
3
4
  import { noteOwnerKey, splitNoteKey } from "./store";
@@ -7,9 +8,23 @@ export interface NotesExportSource {
7
8
  store: NotesStore;
8
9
  }
9
10
 
11
+ /**
12
+ * What an export left out, and why: a whole source whose notes could not be
13
+ * listed, or with `note`, one note of that source that could not be read.
14
+ */
15
+ export interface NotesExportSkip {
16
+ label: string;
17
+ /** The note's symbol, or a quick note's title. */
18
+ note?: string;
19
+ reason: string;
20
+ }
21
+
10
22
  export interface NotesExportResult {
11
23
  dir: string;
12
24
  files: number;
25
+ /** The sources whose notes were written, in the order given. */
26
+ exported: string[];
27
+ skipped: NotesExportSkip[];
13
28
  }
14
29
 
15
30
  function safeFileName(value: string): string {
@@ -29,16 +44,27 @@ export async function exportNotesToDirectory(
29
44
  const fsModulePath = "fs/promises";
30
45
  const { mkdir, writeFile } = await import(fsModulePath) as typeof import("fs/promises");
31
46
  let files = 0;
47
+ const exported: string[] = [];
48
+ const skipped: NotesExportResult["skipped"] = [];
32
49
  for (const source of sources) {
33
50
  const folder = sources.length > 1 ? `${dir}/${safeFileName(source.label)}` : dir;
34
51
  let entries: NoteFileEntry[];
35
52
  let quick: QuickNoteEntry[];
53
+ const unreadable: Array<{ key: string; error: unknown }> = [];
36
54
  try {
37
- [entries, quick] = await Promise.all([source.store.list(), source.store.loadQuickNotesIndex()]);
38
- } catch {
55
+ [entries, quick] = await Promise.all([
56
+ source.store.list({ strict: true, skipped: (key, error) => unreadable.push({ key, error }) }),
57
+ source.store.loadQuickNotesIndex(),
58
+ ]);
59
+ } catch (error) {
60
+ skipped.push({ label: source.label, reason: errorMessage(error) });
39
61
  continue;
40
62
  }
41
63
  const titles = new Map(quick.map((entry) => [entry.id, entry.title]));
64
+ for (const { key: noteKey, error } of unreadable) {
65
+ const { kind, key } = splitNoteKey(noteKey);
66
+ skipped.push({ label: source.label, note: kind === "quick" ? (titles.get(key) ?? key) : key, reason: errorMessage(error) });
67
+ }
42
68
  for (const entry of entries) {
43
69
  const { kind, key } = splitNoteKey(entry.key);
44
70
  const path = kind === "quick"
@@ -48,8 +74,28 @@ export async function exportNotesToDirectory(
48
74
  await writeFile(path, entry.text, "utf-8");
49
75
  files += 1;
50
76
  }
77
+ exported.push(source.label);
51
78
  }
52
- return { dir, files };
79
+ return { dir, files, exported, skipped };
80
+ }
81
+
82
+ /** One line per skip, as `notes export` warns and the Export Notes command reports. */
83
+ export function describeExportSkip(skip: NotesExportSkip): string {
84
+ return skip.note === undefined
85
+ ? `Skipped ${skip.label}: its notes could not be read (${skip.reason}).`
86
+ : `Skipped the ${skip.label} note ${skip.note}: it could not be read (${skip.reason}).`;
87
+ }
88
+
89
+ /** The Export Notes command's notice: the count, then what was skipped. */
90
+ export function exportNotice(result: NotesExportResult): { body: string; type: "success" | "error" } {
91
+ const exported = `Exported ${result.files} ${result.files === 1 ? "note" : "notes"} to ${result.dir}`;
92
+ if (result.skipped.length === 0) return { body: exported, type: "success" };
93
+ const shown = result.skipped.slice(0, 3).map(describeExportSkip);
94
+ const more = result.skipped.length - shown.length;
95
+ return {
96
+ body: [`${exported}.`, ...shown, ...(more > 0 ? [`${more} more could not be read.`] : [])].join(" "),
97
+ type: "error",
98
+ };
53
99
  }
54
100
 
55
101
  export function exportSourceLabel(store: NotesStore, teamName?: string): string {
@@ -35,6 +35,13 @@ export interface NotesFilesIO {
35
35
  }
36
36
 
37
37
  const QUICK_NOTES_INDEX_FILE = "__quick-notes-index__.json";
38
+
39
+ export interface NotesListOptions {
40
+ /** A notes folder that cannot be listed rejects instead of listing nothing. */
41
+ strict?: boolean;
42
+ /** Hears each note left out because it could not be read, for a caller that says so. */
43
+ skipped?: (key: string, error: unknown) => void;
44
+ }
38
45
  const LOCAL_KEY_PREFIX = "gloomberb:notes:";
39
46
  const LOCAL_TIMESTAMP_KEY = "gloomberb:notes:__updated-at__";
40
47
 
@@ -237,8 +244,8 @@ export class NotesFiles implements NotesStore {
237
244
  }
238
245
 
239
246
  /** Every stored note, ticker notes and quick notes alike, newest first. */
240
- async list(): Promise<NoteFileEntry[]> {
241
- const { entries } = await this.readEach(await this.io.listKeys());
247
+ async list(options: NotesListOptions = {}): Promise<NoteFileEntry[]> {
248
+ const { entries } = await this.readEach(await this.io.listKeys({ strict: options.strict }), options.skipped);
242
249
  return entries.sort((left, right) => right.updatedAt - left.updatedAt);
243
250
  }
244
251
 
@@ -277,7 +284,10 @@ export class NotesFiles implements NotesStore {
277
284
  }
278
285
 
279
286
  /** An unreadable note is left out and counted rather than synced as empty. */
280
- private async readEach(keys: Array<[string, number]>): Promise<{ entries: NoteFileEntry[]; unreadable: number }> {
287
+ private async readEach(
288
+ keys: Array<[string, number]>,
289
+ skipped?: NotesListOptions["skipped"],
290
+ ): Promise<{ entries: NoteFileEntry[]; unreadable: number }> {
281
291
  const entries: NoteFileEntry[] = [];
282
292
  let unreadable = 0;
283
293
  for (const [key, updatedAt] of keys) {
@@ -286,6 +296,7 @@ export class NotesFiles implements NotesStore {
286
296
  } catch (error) {
287
297
  unreadable += 1;
288
298
  logFailure("notes.files", "Read a note while listing notes failed")(error);
299
+ skipped?.(key, error);
289
300
  }
290
301
  }
291
302
  return { entries, unreadable };
@@ -2,7 +2,7 @@ import { apiClient } from "../../../api-client";
2
2
  import type { GloomPlugin } from "../../../types/plugin";
3
3
  import { teamStore } from "../../../team/store";
4
4
  import { notesCliCommand } from "./cli";
5
- import { exportNotesToDirectory, exportSourceLabel } from "./export";
5
+ import { exportNotesToDirectory, exportNotice, exportSourceLabel } from "./export";
6
6
  import { NotesFiles } from "./files";
7
7
  import { migrateLocalNotes, notesMigratedAt } from "./migration";
8
8
  import { createQuickNotesPane } from "./quick-notes-pane";
@@ -114,8 +114,8 @@ export const notesPlugin: GloomPlugin = {
114
114
  ...teams.map((team) => ({ label: exportSourceLabel(registry.forOwner({ kind: "team", teamId: team.id }), team.name), store: registry.forOwner({ kind: "team", teamId: team.id }) })),
115
115
  ];
116
116
  try {
117
- const result = await exportNotesToDirectory(dir, sources);
118
- ctx.notify({ body: `Exported ${result.files} ${result.files === 1 ? "note" : "notes"} to ${result.dir}`, type: "success" });
117
+ // What was skipped is said with the count, as `notes export` warns.
118
+ ctx.notify(exportNotice(await exportNotesToDirectory(dir, sources)));
119
119
  } catch (error) {
120
120
  ctx.notify({ body: error instanceof Error ? error.message : "Could not export notes.", type: "error" });
121
121
  }