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
@@ -70,6 +70,10 @@ button, [data-gloom-interactive="true"] { cursor: pointer; }
70
70
  [data-gloom-role="desktop-button"]:is([data-gloom-variant="plain"], [data-gloom-variant="ghost"], [data-gloom-variant="secondary"]):not(:disabled):not([aria-pressed="true"]):hover {
71
71
  background-color: var(--gloom-hover-fill);
72
72
  }
73
+ /* Quiet quick actions take the same wash, over a chosen one's fill too. */
74
+ [data-gloom-role="desktop-button"][data-gloom-variant="quiet"]:not(:disabled):hover {
75
+ background-color: var(--gloom-hover-fill) !important;
76
+ }
73
77
  [data-gloom-hover-bg="true"]:hover {
74
78
  background-color: var(--gloom-box-hover-bg) !important;
75
79
  }
@@ -503,6 +507,45 @@ button, [data-gloom-interactive="true"] { cursor: pointer; }
503
507
  [data-gloom-role="tab-button"][data-active="true"]::after { opacity: 1; }
504
508
  [data-gloom-role="tab-button"]:disabled { color: var(--gloom-text-muted); cursor: default; }
505
509
  [data-gloom-role="tab-button"]:focus-visible { outline: none; }
510
+ /* Title-bar tabs: the same text, sizes and colours, drawn as raised boxes
511
+ instead of underlines. The active tab is a box with rounded top corners,
512
+ open at the bottom: its top and sides in the header rule's colour (the
513
+ accent while the pane is focused), its fill the surface under it, laid over
514
+ the rule so it opens into the pane. An inactive tab takes the hover fill in
515
+ the same shape. The pane header sets the colours (pane/header.tsx). */
516
+ [data-gloom-role="tab-list"][data-gloom-variant="header"] {
517
+ /* Header colour left above a box, and the box's line: the header rule's width. */
518
+ --pane-tab-top-gap: 2px;
519
+ --pane-tab-rule: 1px;
520
+ }
521
+ [data-gloom-role="tab-list"][data-gloom-variant="header"] > [data-gloom-role="tab-button"] {
522
+ height: calc(100% - var(--pane-tab-top-gap));
523
+ /* The sides take their line from the padding, so a label sits where it
524
+ would without the box. The gap and the top line push the label down; the
525
+ bottom padding lifts it back onto the header's centre line, halfway
526
+ between its top rule (a divider or a floating border, when there is one)
527
+ and its bottom rule, where the title sits. */
528
+ padding: 0 calc(var(--gloom-space-sm) - var(--pane-tab-rule)) calc(var(--pane-tab-top-gap) + 2 * var(--pane-tab-rule) - var(--pane-tab-top-inset, 0px));
529
+ border: var(--pane-tab-rule) solid transparent;
530
+ border-bottom: 0;
531
+ border-radius: var(--gloom-radius-md) var(--gloom-radius-md) 0 0;
532
+ /* The line lies on the bar, as the rule does, so a translucent hairline is
533
+ the same colour on both. */
534
+ background-clip: padding-box;
535
+ }
536
+ [data-gloom-role="tab-list"][data-gloom-variant="header"] > [data-gloom-role="tab-button"]::after { content: none; }
537
+ [data-gloom-role="tab-list"][data-gloom-variant="header"] > [data-gloom-role="tab-button"][data-active="true"] {
538
+ background-color: var(--pane-tab-active-bg, var(--gloom-bg));
539
+ border-color: var(--pane-tab-border, var(--gloom-hairline));
540
+ }
541
+ [data-gloom-role="tab-list"][data-gloom-variant="header"] > [data-gloom-role="tab-button"]:not([data-active="true"]):not(:disabled):is(:hover, :focus-visible, [data-drag-target="true"]) {
542
+ background-color: var(--gloom-hover-fill);
543
+ }
544
+ /* A tab's close mark stays inside the box: in a 20px title bar the box is
545
+ shorter than the icon button, whose fill would cover the box's top line. */
546
+ [data-gloom-role="tab-list"][data-gloom-variant="header"] [data-gloom-role="tab-close"] {
547
+ max-height: 100%;
548
+ }
506
549
  /* A tab's close mark and a strip's plus are the icon button: 20 by 18, the control radius, the hover fill. */
507
550
  [data-gloom-role="tab-add"] {
508
551
  border: 0;
@@ -941,12 +984,14 @@ html[data-gloom-screenshot="true"] [data-gloom-role="chart-watermark"] {
941
984
  }
942
985
  .gloom-qb-inline .gloom-qb-view { margin-left: 0; box-shadow: none; align-self: stretch; }
943
986
  /* A long inline choice (an expiry strip) scrolls by itself, as the terminal's
944
- does, so its label and the controls beside it stay in view. */
987
+ does, so its label and the controls beside it stay in view. It narrows with
988
+ its group and has no floor of its own: a floor wider than the room the bar
989
+ gives it drew its next choice under the control beside it. */
945
990
  .gloom-qb-filters[data-strip="true"],
946
991
  .gloom-qb-inline[data-strip="true"] { flex: 0 1 auto; min-width: 0; }
947
992
  .gloom-qb-inline[data-strip="true"] .gloom-qb-segments {
948
993
  flex: 0 1 auto;
949
- min-width: 12ch;
994
+ min-width: 0;
950
995
  overflow-x: auto;
951
996
  scrollbar-width: none;
952
997
  }
@@ -1354,8 +1399,9 @@ html[data-gloom-screenshot="true"] [data-gloom-role="chart-watermark"] {
1354
1399
  white-space: nowrap;
1355
1400
  }
1356
1401
 
1357
- /* Icon-only button: pane header, footer, dialog close, window controls. */
1358
- .gloom-icon-button {
1402
+ /* Icon-only button: pane header, footer, dialog close, window controls. A
1403
+ footer status icon (a delayed source's clock) takes the same box. */
1404
+ .gloom-icon-button, [data-gloom-role="pane-footer-icon"] {
1359
1405
  flex: none;
1360
1406
  display: inline-flex;
1361
1407
  align-items: center;
@@ -1372,7 +1418,7 @@ html[data-gloom-screenshot="true"] [data-gloom-role="chart-watermark"] {
1372
1418
  cursor: pointer;
1373
1419
  transition: background-color 120ms ease, color 120ms ease;
1374
1420
  }
1375
- .gloom-icon-button:disabled { cursor: default; }
1421
+ .gloom-icon-button:disabled, [data-gloom-role="pane-footer-icon"] { cursor: default; }
1376
1422
  .gloom-icon-button:not(:disabled):hover { background: var(--gloom-hover-fill); color: var(--gloom-text-bright); }
1377
1423
  .gloom-icon-button[aria-pressed="true"] { color: var(--gloom-warning); }
1378
1424
  .gloom-qb-view button:disabled { color: var(--gloom-text-muted); opacity: 0.55; cursor: default; }
@@ -8,6 +8,7 @@ import type { AppServices } from "../../../core/app-services";
8
8
  import { diskNotesFilesIO, isNotesFile } from "../../../plugins/builtin/notes/files";
9
9
  import type { BrokerAdapter } from "../../../types/broker";
10
10
  import type { AppConfig, BrokerInstanceConfig } from "../../../types/config";
11
+ import { errorMessage } from "../../../utils/errors";
11
12
 
12
13
  const DESKTOP_CORE_PLUGIN_ID = "desktop-core";
13
14
 
@@ -30,6 +31,7 @@ const BROKER_INVOKE_OPERATIONS = new Set([
30
31
  "getChartResolutionCapabilities",
31
32
  "getOptionsChain",
32
33
  "listOpenOrders",
34
+ "getOrderStatus",
33
35
  "listExecutions",
34
36
  "previewOrder",
35
37
  "placeOrder",
@@ -116,6 +118,8 @@ function invokeBrokerOperation(
116
118
  case "previewOrder":
117
119
  case "placeOrder":
118
120
  return (broker[operation] as any)?.call(broker, instance, args[0]);
121
+ case "getOrderStatus":
122
+ return broker.getOrderStatus?.(instance, args[0] as number);
119
123
  case "modifyOrder":
120
124
  return broker.modifyOrder?.(instance, args[0] as number, args[1] as never);
121
125
  case "cancelOrder":
@@ -167,6 +171,15 @@ function createBrokerCapability(options: CoreCapabilityOptions): PluginCapabilit
167
171
  },
168
172
  );
169
173
  }),
174
+ orders: stream((input: any, emit) => {
175
+ const instanceId = requireString(input.instanceId, "Broker instance");
176
+ const { instance, broker } = resolveBrokerInstance(options, instanceId);
177
+ if (!broker.subscribeOrders) throw new Error(`Broker "${broker.name}" does not stream orders.`);
178
+ return broker.subscribeOrders(instance, {
179
+ order: (order) => emit({ kind: "order", order }),
180
+ live: (live) => emit({ kind: "orders-live", live }),
181
+ });
182
+ }),
170
183
  removeInstance: op(async (input: any) => {
171
184
  const { instance, broker } = resolveBrokerInstance(options, input.instanceId);
172
185
  await broker.disconnect?.(instance);
@@ -209,7 +222,14 @@ function createNotesFilesCapability(): PluginCapability {
209
222
  await io(input).delete(file(input));
210
223
  return null;
211
224
  }, "action"),
212
- listKeys: op((input: any) => io(input).listKeys({ strict: input.strict === true })),
225
+ listKeys: op(async (input: any) => {
226
+ const skipped: Array<{ key: string; reason: string }> = [];
227
+ const keys = await io(input).listKeys({
228
+ strict: input.strict === true,
229
+ ...(input.reportSkipped === true ? { skipped: (key: string, error: unknown) => skipped.push({ key, reason: errorMessage(error) }) } : {}),
230
+ });
231
+ return input.reportSkipped === true ? { keys, skipped } : keys;
232
+ }),
213
233
  },
214
234
  };
215
235
  }
@@ -15,6 +15,8 @@ import { startRemoteControlServer, type RemoteControlServer } from "../../../../
15
15
  import type { RemoteControlRequest, RemoteControlResponse } from "../../../../remote/types";
16
16
  import { installCrashReporter } from "../../../../telemetry/crash-reports";
17
17
  import { createNodeCrashReporterHost } from "../../../../telemetry/crash-reports-node";
18
+ import { installUpdateFailureReporter, reviewLastUpdateAttempt } from "../../../../telemetry/update-failures";
19
+ import { createNodeUpdateFailureHost } from "../../../../telemetry/update-failures-node";
18
20
  import type { AppConfig } from "../../../../types/config";
19
21
  import type { DesktopSharedStateSnapshot } from "../../../../types/desktop-window";
20
22
  import type {
@@ -37,6 +39,7 @@ import {
37
39
  } from "../external-plugins";
38
40
  import { MAIN_WINDOW_RPC_KEY } from "../window/focus";
39
41
  import { handleDesktopBackendRequest } from "./backend-requests";
42
+ import { detectDesktopInstallKind, removeUpdateAsides } from "./update";
40
43
  import { DesktopCapabilityBridge } from "./capability-bridge";
41
44
  import { DesktopDetachedWindowManager } from "./detached-windows";
42
45
  import { handleDesktopHostRequest } from "./host-requests";
@@ -138,6 +141,13 @@ export class DesktopBackend {
138
141
  if (!this.crashReporterInstalled) {
139
142
  this.crashReporterInstalled = true;
140
143
  installCrashReporter(createNodeCrashReporterHost({ surface: "desktop", getConfig: () => this.config }));
144
+ // This process runs the desktop update, so it reports how the last one ended and any that fail.
145
+ installUpdateFailureReporter(createNodeUpdateFailureHost({
146
+ installKind: detectDesktopInstallKind(),
147
+ getDataDir: () => this.config?.dataDir ?? null,
148
+ }));
149
+ reviewLastUpdateAttempt();
150
+ void removeUpdateAsides();
141
151
  }
142
152
  // The native menu shows the same accelerators the keys use, so a rebind
143
153
  // rebuilds it.
@@ -1,3 +1,5 @@
1
+ import { readdir, rm } from "fs/promises";
2
+ import { basename, dirname, join } from "path";
1
3
  import type Electrobun from "electrobun/bun";
2
4
  import type { UpdateStatusEntry } from "electrobun/bun";
3
5
  import type { UpdateChannel } from "../../../../types/config";
@@ -15,6 +17,20 @@ import {
15
17
  type GitHubReleaseListing,
16
18
  } from "../../../../updater/github-releases";
17
19
  import { stagedUpdateFitsChannel } from "../../../../updater/channel";
20
+ import {
21
+ classifyUpdateError,
22
+ httpStatusFailureCode,
23
+ type UpdateFailure,
24
+ type UpdateFailureCode,
25
+ } from "../../../../updater/failure";
26
+ import {
27
+ forgetStagedUpdate,
28
+ noteUpdateApplying,
29
+ noteUpdateStaged,
30
+ reportUpdateFailure,
31
+ type UpdateInstallKind,
32
+ } from "../../../../telemetry/update-failures";
33
+ import { logFailure } from "../../../../utils/log-failure";
18
34
  import { compareSemver, isPrereleaseVersion } from "../../../../utils/semver";
19
35
  import { VERSION } from "../../../../version";
20
36
 
@@ -27,7 +43,14 @@ type ElectrobunUpdater = typeof Electrobun.Updater;
27
43
  */
28
44
  export type ElectrobunUpdaterApi = Pick<
29
45
  ElectrobunUpdater,
30
- "checkForUpdate" | "downloadUpdate" | "applyUpdate" | "updateInfo" | "clearStatusHistory" | "onStatusChange" | "setUpdateSource"
46
+ | "checkForUpdate"
47
+ | "downloadUpdate"
48
+ | "applyUpdate"
49
+ | "updateInfo"
50
+ | "clearStatusHistory"
51
+ | "getStatusHistory"
52
+ | "onStatusChange"
53
+ | "setUpdateSource"
31
54
  > & {
32
55
  localInfo: Pick<ElectrobunUpdater["localInfo"], "channel" | "baseUrl">;
33
56
  };
@@ -37,12 +60,111 @@ type ListReleases = () => Promise<GitHubReleaseListing[]>;
37
60
 
38
61
  const NIGHTLY_RELEASE_WINDOW = 30;
39
62
 
63
+ /** A step that failed for a reason known where it was thrown. */
64
+ class DesktopUpdateError extends Error {
65
+ constructor(message: string, readonly code: UpdateFailureCode) {
66
+ super(message);
67
+ this.name = "DesktopUpdateError";
68
+ }
69
+ }
70
+
71
+ /** `<app>.previous` and `<app>.previous-<suffix>`: what an install moves the old app to. */
72
+ function isUpdateAside(entry: string, appName: string): boolean {
73
+ if (!entry.startsWith(`${appName}.previous`)) return false;
74
+ const rest = entry.slice(appName.length + ".previous".length);
75
+ return rest === "" || /^-[0-9a-z]+(?:-\d+)?$/.test(rest);
76
+ }
77
+
78
+ /**
79
+ * Removes the copies of the app an install moved aside and could not remove
80
+ * then (the Electrobun patch's `replaceAppBundle`), from beside the app this
81
+ * process runs from: starting from it means the install that left them worked.
82
+ * macOS keeps the app in its bundle, Linux in `<app data>/app` (`bin/bun`
83
+ * inside it); Windows installs through its own script and leaves none.
84
+ * Resolves to the paths it removed.
85
+ */
86
+ export async function removeUpdateAsides(
87
+ execPath: string = process.execPath,
88
+ platform: string = process.platform,
89
+ ): Promise<string[]> {
90
+ const app = platform === "darwin"
91
+ ? /^(.*?\.app)\/Contents\/MacOS\/[^/]+$/.exec(execPath)?.[1]
92
+ : platform === "linux" && basename(dirname(execPath)) === "bin" ? dirname(dirname(execPath)) : undefined;
93
+ if (!app) return [];
94
+ const folder = dirname(app);
95
+ const removed: string[] = [];
96
+ for (const entry of await readdir(folder).catch(() => [] as string[])) {
97
+ if (!isUpdateAside(entry, basename(app))) continue;
98
+ const path = join(folder, entry);
99
+ // One that still cannot be removed (another user's) is tried again at the next start.
100
+ const done = await rm(path, { recursive: true, force: true }).then(() => true, logFailure("desktop-update", "Could not remove an old copy of the app", false));
101
+ if (done) removed.push(path);
102
+ }
103
+ return removed;
104
+ }
105
+
106
+ /**
107
+ * Where the running app sits, for update failure reports and for what an
108
+ * install that cannot replace it means. Electrobun replaces the macOS bundle
109
+ * the app runs from; on Windows and Linux it always installs into its own
110
+ * folder under the user's app data.
111
+ */
112
+ export function detectDesktopInstallKind(
113
+ execPath: string = process.execPath,
114
+ platform: string = process.platform,
115
+ homeDir: string = process.env.HOME ?? "",
116
+ ): UpdateInstallKind {
117
+ if (platform !== "darwin") return platform === "win32" || platform === "linux" ? "desktop-app-data" : "unknown";
118
+ const bundle = /^(.*?\.app)\/Contents\//.exec(execPath)?.[1];
119
+ if (!bundle) return "unknown";
120
+ if (bundle.includes("/AppTranslocation/")) return "desktop-translocated";
121
+ if (bundle.startsWith("/Volumes/")) return "desktop-volume";
122
+ if (bundle.startsWith("/Applications/") || (homeDir && bundle.startsWith(`${homeDir}/Applications/`))) return "desktop-applications";
123
+ return "desktop-elsewhere";
124
+ }
125
+
126
+ /**
127
+ * On macOS a bundle that cannot be replaced where it runs (a disk image, the
128
+ * read-only copy App Translocation runs, another volume) is fixed by moving
129
+ * the app to Applications, which is what the header then says. Anywhere else
130
+ * a move across disks keeps its own reason: from Applications, moving the app
131
+ * again would not help.
132
+ */
133
+ function installFailureCode(code: UpdateFailureCode, installKind: UpdateInstallKind): UpdateFailureCode {
134
+ const movable = installKind === "desktop-translocated" || installKind === "desktop-volume";
135
+ if (movable && (code === "permission_denied" || code === "read_only" || code === "wrong_location" || code === "cross_device")) return "wrong_location";
136
+ return code;
137
+ }
138
+
139
+ /**
140
+ * Why Electrobun's check failed. It returns one message for every failure, so
141
+ * the status it emitted just before says whether the server answered (with
142
+ * which status) or could not be reached.
143
+ */
144
+ function checkFailureCode(error: string, history: UpdateStatusEntry[]): UpdateFailureCode {
145
+ const last = history.at(-1);
146
+ const status = last?.status === "error" ? /HTTP (\d{3})/.exec(last.message)?.[1] : undefined;
147
+ if (status) return httpStatusFailureCode(Number(status));
148
+ if (/Invalid update\.json/i.test(error)) return "bad_response";
149
+ const code = classifyUpdateError(error);
150
+ // "Failed to fetch update info" with no status before it: the request itself threw.
151
+ return code === "unknown" ? "offline" : code;
152
+ }
153
+
154
+ /** Why a download that Electrobun returned from did not leave an update ready. */
155
+ function downloadFailureCode(error: string, lastErrorStatus: UpdateStatusEntry | null): UpdateFailure {
156
+ if (/zstd/i.test(error)) return { phase: "verify", code: "corrupt" };
157
+ if (lastErrorStatus?.message.startsWith("Failed to download:")) return { phase: "download", code: "http_error" };
158
+ const code = classifyUpdateError(error);
159
+ return { phase: "download", code: code === "unknown" ? "interrupted" : code };
160
+ }
161
+
40
162
  async function listGitHubReleases(): Promise<GitHubReleaseListing[]> {
41
163
  const response = await fetch(`${GITHUB_RELEASES_API_URL}?per_page=${NIGHTLY_RELEASE_WINDOW}`, {
42
164
  headers: { Accept: "application/vnd.github+json" },
43
165
  signal: AbortSignal.timeout(10_000),
44
166
  });
45
- if (!response.ok) throw new Error(`GitHub returned ${response.status}`);
167
+ if (!response.ok) throw new DesktopUpdateError(`GitHub returned ${response.status}`, httpStatusFailureCode(response.status));
46
168
  const data: unknown = await response.json();
47
169
  if (!Array.isArray(data)) throw new Error("GitHub returned an unexpected releases payload");
48
170
  return data as GitHubReleaseListing[];
@@ -88,6 +210,12 @@ function hasDesktopManifest(release: GitHubReleaseListing): boolean {
88
210
  return release.assets?.some((asset) => asset.name === manifest) === true;
89
211
  }
90
212
 
213
+ /**
214
+ * The progress an Electrobun status means. An `error` status is left to the
215
+ * caller: during a download Electrobun emits some it recovers from (a patch
216
+ * that does not apply falls back to the full download), so only the outcome
217
+ * says whether the download failed.
218
+ */
91
219
  function mapDesktopUpdateStatus(entry: UpdateStatusEntry): UpdateProgress | null {
92
220
  const progress = entry.details?.progress;
93
221
  switch (entry.status) {
@@ -120,11 +248,6 @@ function mapDesktopUpdateStatus(entry: UpdateStatusEntry): UpdateProgress | null
120
248
  // once `updateInfo().updateReady` confirms it.
121
249
  case "complete":
122
250
  return { phase: "done", message: "Update installed, restarting..." };
123
- case "error":
124
- return {
125
- phase: "error",
126
- error: entry.details?.errorMessage || entry.message,
127
- };
128
251
  default:
129
252
  return null;
130
253
  }
@@ -133,9 +256,12 @@ function mapDesktopUpdateStatus(entry: UpdateStatusEntry): UpdateProgress | null
133
256
  export function createElectrobunDesktopUpdater(
134
257
  updater: ElectrobunUpdaterApi,
135
258
  {
259
+ installKind = detectDesktopInstallKind(),
136
260
  listReleases = listGitHubReleases,
137
261
  runningVersion = VERSION,
138
262
  }: {
263
+ /** Where the app runs from, which an install that cannot replace it is blamed on. */
264
+ installKind?: UpdateInstallKind;
139
265
  listReleases?: ListReleases;
140
266
  /** The version of the installed app, which a download checks as. */
141
267
  runningVersion?: string;
@@ -152,6 +278,15 @@ export function createElectrobunDesktopUpdater(
152
278
  /** An apply is under way, or finished and the app is quitting. */
153
279
  let applyInProgress = false;
154
280
 
281
+ /** The error progress for a failed step, reported to error tracking on the way. */
282
+ function failed(failure: UpdateFailure, error: string, release?: { version?: string; channel?: UpdateChannel } | null): UpdateProgress {
283
+ reportUpdateFailure(failure, {
284
+ targetVersion: release?.version,
285
+ channel: release?.channel === "nightly" ? "nightly" : "stable",
286
+ });
287
+ return { phase: "error", error, failure };
288
+ }
289
+
155
290
  /**
156
291
  * Forgets a downloaded update `updateChannel` does not allow: a nightly
157
292
  * build staged before the switch back to stable. Electrobun's own ready
@@ -159,7 +294,10 @@ export function createElectrobunDesktopUpdater(
159
294
  * a restart cannot install it, and its files go with the next download.
160
295
  */
161
296
  function dropStagedMisfit(updateChannel: UpdateChannel): void {
162
- if (readyRelease && !applyInProgress && !stagedUpdateFitsChannel(readyRelease, updateChannel)) readyRelease = null;
297
+ if (readyRelease && !applyInProgress && !stagedUpdateFitsChannel(readyRelease, updateChannel)) {
298
+ readyRelease = null;
299
+ forgetStagedUpdate();
300
+ }
163
301
  }
164
302
 
165
303
  async function releaseInfo(updateInfo: { version?: string; hash?: string }, currentVersion: string): Promise<ReleaseInfo> {
@@ -177,6 +315,13 @@ export function createElectrobunDesktopUpdater(
177
315
  };
178
316
  }
179
317
 
318
+ /** Electrobun's check, with the failure it ended in named. */
319
+ async function checkElectrobun(): Promise<Awaited<ReturnType<ElectrobunUpdaterApi["checkForUpdate"]>> | { error: string; code: UpdateFailureCode }> {
320
+ const info = await updater.checkForUpdate();
321
+ if (!info.error) return info;
322
+ return { error: info.error, code: checkFailureCode(info.error, updater.getStatusHistory()) };
323
+ }
324
+
180
325
  /**
181
326
  * Nightly: the highest version among the newest releases that has this
182
327
  * platform's desktop manifest, nightly or stable, fetched from that release's
@@ -192,8 +337,8 @@ export function createElectrobunDesktopUpdater(
192
337
  const baseUrl = githubReleaseDownloadBaseUrl(target.tag_name);
193
338
  updater.setUpdateSource({ channel: filesChannel, baseUrl });
194
339
 
195
- const info = await updater.checkForUpdate();
196
- if (info.error) return { kind: "error", error: info.error };
340
+ const info = await checkElectrobun();
341
+ if ("code" in info) throw new DesktopUpdateError(info.error, info.code);
197
342
  // The same build as the one installed.
198
343
  if (!info.updateAvailable) return { kind: "current" };
199
344
 
@@ -210,7 +355,11 @@ export function createElectrobunDesktopUpdater(
210
355
  };
211
356
  }
212
357
 
213
- async function check(currentVersion: string, updateChannel: UpdateChannel = "stable"): Promise<UpdateCheckResult> {
358
+ /**
359
+ * The check, without reporting: a download runs it first and reports what
360
+ * went wrong itself.
361
+ */
362
+ async function checkQuietly(currentVersion: string, updateChannel: UpdateChannel): Promise<UpdateCheckResult> {
214
363
  dropStagedMisfit(updateChannel);
215
364
  // A downloaded update the channel still allows stays the answer until the
216
365
  // user restarts into it, and asking Electrobun again would discard its
@@ -229,9 +378,9 @@ export function createElectrobunDesktopUpdater(
229
378
 
230
379
  // Stable is the release version.json names, as it always was.
231
380
  updater.setUpdateSource(null);
232
- const info = await updater.checkForUpdate();
233
- if (info.error) {
234
- return { kind: "error", error: info.error };
381
+ const info = await checkElectrobun();
382
+ if ("code" in info) {
383
+ return { kind: "error", error: info.error, failure: { phase: "check", code: info.code } };
235
384
  }
236
385
  if (!info.updateAvailable) {
237
386
  return { kind: "current" };
@@ -247,13 +396,21 @@ export function createElectrobunDesktopUpdater(
247
396
  release: await releaseInfo(info, currentVersion),
248
397
  };
249
398
  } catch (error) {
399
+ const code = error instanceof DesktopUpdateError ? error.code : classifyUpdateError(error);
250
400
  return {
251
401
  kind: "error",
252
402
  error: error instanceof Error ? error.message : "Desktop update check failed",
403
+ failure: { phase: "check", code },
253
404
  };
254
405
  }
255
406
  }
256
407
 
408
+ async function check(currentVersion: string, updateChannel: UpdateChannel = "stable"): Promise<UpdateCheckResult> {
409
+ const result = await checkQuietly(currentVersion, updateChannel);
410
+ if (result.kind === "error" && result.failure) reportUpdateFailure(result.failure, { channel: updateChannel });
411
+ return result;
412
+ }
413
+
257
414
  async function download(
258
415
  release: Pick<ReleaseInfo, "channel">,
259
416
  onProgress: (progress: UpdateProgress) => void,
@@ -277,17 +434,25 @@ export function createElectrobunDesktopUpdater(
277
434
  }
278
435
 
279
436
  downloadInProgress = true;
437
+ /** The last error Electrobun emitted on the way, which may or may not have ended the download. */
438
+ let lastErrorStatus: UpdateStatusEntry | null = null;
439
+ /** The release the download is for, once the check has found it. */
440
+ let target: ReleaseInfo | null = null;
280
441
  updater.clearStatusHistory();
281
442
  updater.onStatusChange((entry) => {
443
+ if (entry.status === "error") {
444
+ lastErrorStatus = entry;
445
+ return;
446
+ }
282
447
  const progress = mapDesktopUpdateStatus(entry);
283
448
  if (progress) onProgress(progress);
284
449
  });
285
450
 
286
451
  try {
287
452
  onProgress({ phase: "downloading", percent: 0 });
288
- const result = await check(runningVersion, updateChannel);
453
+ const result = await checkQuietly(runningVersion, updateChannel);
289
454
  if (result.kind === "error") {
290
- onProgress({ phase: "error", error: result.error });
455
+ onProgress(failed(result.failure ?? { phase: "check", code: "unknown" }, result.error, { channel: updateChannel }));
291
456
  return;
292
457
  }
293
458
  if (result.kind !== "available") {
@@ -297,27 +462,31 @@ export function createElectrobunDesktopUpdater(
297
462
  });
298
463
  return;
299
464
  }
465
+ target = result.release;
300
466
 
301
467
  await updater.downloadUpdate();
302
468
  const updateInfo = updater.updateInfo();
303
469
  if (updateInfo?.error) {
304
- onProgress({ phase: "error", error: updateInfo.error });
470
+ onProgress(failed(downloadFailureCode(updateInfo.error, lastErrorStatus), updateInfo.error, result.release));
305
471
  return;
306
472
  }
307
473
  if (!updateInfo?.updateReady) {
308
- onProgress({ phase: "error", error: "Desktop update did not finish downloading." });
474
+ onProgress(failed({ phase: "download", code: "interrupted" }, "Desktop update did not finish downloading.", result.release));
309
475
  return;
310
476
  }
311
477
 
312
478
  // Downloaded, not installed: applying relaunches the app, and that waits
313
479
  // for the user's explicit request (`apply`).
314
480
  readyRelease = result.release;
481
+ noteUpdateStaged(result.release.version, updateChannel);
315
482
  onProgress({ phase: "ready", canRestart: true });
316
483
  } catch (error) {
317
- onProgress({
318
- phase: "error",
319
- error: error instanceof Error ? error.message : "Desktop update failed",
320
- });
484
+ const code = classifyUpdateError(error);
485
+ onProgress(failed(
486
+ { phase: "download", code: code === "unknown" ? "interrupted" : code },
487
+ error instanceof Error ? error.message : "Desktop update failed",
488
+ { version: target?.version, channel: updateChannel },
489
+ ));
321
490
  } finally {
322
491
  downloadInProgress = false;
323
492
  updater.onStatusChange(null);
@@ -327,11 +496,13 @@ export function createElectrobunDesktopUpdater(
327
496
  async function apply(onProgress: (progress: UpdateProgress) => void): Promise<void> {
328
497
  if (applyInProgress || downloadInProgress) return;
329
498
  if (!readyRelease || !updater.updateInfo()?.updateReady) {
499
+ const missing = readyRelease;
330
500
  readyRelease = null;
331
- onProgress({ phase: "error", error: "The downloaded update is no longer available." });
501
+ onProgress(failed({ phase: "apply", code: "staged_missing" }, "The downloaded update is no longer available.", missing));
332
502
  return;
333
503
  }
334
504
 
505
+ const target = readyRelease;
335
506
  applyInProgress = true;
336
507
  let relaunching = false;
337
508
  let reportedError = false;
@@ -339,7 +510,19 @@ export function createElectrobunDesktopUpdater(
339
510
  if (progress.phase === "error") reportedError = true;
340
511
  onProgress(progress);
341
512
  };
513
+ const installFailed = (error: unknown, message: string) => {
514
+ // Electrobun names the system error in its message ("Failed to replace app: EACCES: ...").
515
+ const code = installFailureCode(classifyUpdateError(error), installKind);
516
+ report(failed({ phase: relaunching ? "relaunch" : "apply", code }, message, target));
517
+ };
518
+ // Kept before Electrobun quits the app, so the next launch can tell whether it took.
519
+ noteUpdateApplying(target.version, target.channel === "nightly" ? "nightly" : "stable");
342
520
  updater.onStatusChange((entry) => {
521
+ if (entry.status === "error") {
522
+ const message = entry.details?.errorMessage || entry.message;
523
+ installFailed(message, message);
524
+ return;
525
+ }
343
526
  if (entry.status === "launching-new-version" || entry.status === "complete") relaunching = true;
344
527
  const progress = mapDesktopUpdateStatus(entry);
345
528
  if (progress) report(progress);
@@ -348,16 +531,13 @@ export function createElectrobunDesktopUpdater(
348
531
  try {
349
532
  report({ phase: "replacing" });
350
533
  await updater.applyUpdate();
351
- // Electrobun returns without a status when it cannot install, for example
352
- // when a newer release replaced the one that was downloaded.
534
+ // Electrobun returns without a status when it finds nothing to install:
535
+ // the downloaded files are gone, or hold no app.
353
536
  if (!relaunching && !reportedError) {
354
- report({ phase: "error", error: "Desktop update could not be installed." });
537
+ report(failed({ phase: "apply", code: "staged_missing" }, "Desktop update could not be installed.", target));
355
538
  }
356
539
  } catch (error) {
357
- report({
358
- phase: "error",
359
- error: error instanceof Error ? error.message : "Desktop update failed",
360
- });
540
+ installFailed(error, error instanceof Error ? error.message : "Desktop update failed");
361
541
  } finally {
362
542
  updater.onStatusChange(null);
363
543
  }
@@ -14,6 +14,7 @@ import {
14
14
  loadExternalPlugin,
15
15
  } from "../../../plugins/loader";
16
16
  import { createNodePluginManager } from "../../../plugins/manager-node";
17
+ import { readPluginIcon } from "../../../plugins/plugin-icon";
17
18
  import type { PluginRegistry } from "../../../plugins/registry";
18
19
  import { desktopRendererCapabilityManifests } from "./desktop/initialization";
19
20
  import { reportCrash } from "../../../telemetry/crash-reports";
@@ -70,6 +71,7 @@ async function bundlePluginDirectory(
70
71
 
71
72
  const { plugin } = loaded;
72
73
  const directory = basename(pluginDir);
74
+ const icon = readPluginIcon(pluginDir);
73
75
  const base = {
74
76
  id: plugin.id,
75
77
  name: plugin.name,
@@ -80,6 +82,7 @@ async function bundlePluginDirectory(
80
82
  ...(loaded.commit ? { commit: loaded.commit } : {}),
81
83
  ...(loaded.repo ? { repo: loaded.repo } : {}),
82
84
  ...(loaded.linked ? { linked: true } : {}),
85
+ ...(icon ? { icon } : {}),
83
86
  ...(plugin.targets ? { targets: plugin.targets } : {}),
84
87
  };
85
88
 
@@ -130,6 +130,8 @@ export interface DesktopExternalPluginBundle {
130
130
  /** `owner/repo` of the checkout's GitHub origin. */
131
131
  repo?: string;
132
132
  linked?: boolean;
133
+ /** The plugin's logo as a `data:` URL, read from the checkout by the Bun process. */
134
+ icon?: string;
133
135
  /** ES module source, absent when `error` or `unsupportedTarget` is set. */
134
136
  code?: string;
135
137
  targets?: readonly ("cli" | "tui" | "desktop" | "web")[];
@@ -125,6 +125,22 @@ const client: BrokerRemoteClient = {
125
125
  });
126
126
  },
127
127
 
128
+ subscribeOrders(instanceId, listener) {
129
+ return subscribeBackendCapability({
130
+ subscriptionId: `broker-orders:${instanceId}:${nextSubscriptionId++}`,
131
+ capabilityId: BROKER_CAPABILITY_ID,
132
+ operationId: "orders",
133
+ payload: { instanceId },
134
+ onEvent: (message) => {
135
+ const event = message as BrokerRemoteEvent;
136
+ if (event.kind === "order") listener.order(event.order);
137
+ else if (event.kind === "orders-live") listener.live?.(event.live);
138
+ },
139
+ // A stream that cannot open leaves the host polling open orders.
140
+ onError: (error) => { listener.live?.(false); console.error("Failed to subscribe to broker orders", error); },
141
+ });
142
+ },
143
+
128
144
  async removeInstance(instanceId) {
129
145
  await invokeBrokerCapability("removeInstance", { instanceId });
130
146
  statuses.delete(instanceId);
@@ -28,6 +28,7 @@ export async function loadDesktopExternalPlugin(bundle: DesktopExternalPluginBun
28
28
  ...(bundle.commit ? { commit: bundle.commit } : {}),
29
29
  ...(bundle.repo ? { repo: bundle.repo } : {}),
30
30
  ...(bundle.linked ? { linked: true } : {}),
31
+ ...(bundle.icon ? { icon: bundle.icon } : {}),
31
32
  };
32
33
  const fallback = {
33
34
  ...base,