@gloom-sh/terminal 0.18.5 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +140 -7
  3. package/src/api-client/cot.ts +6 -2
  4. package/src/api-client/credit-documents.ts +6 -0
  5. package/src/api-client/data.ts +17 -6
  6. package/src/api-client/equity-screener.ts +24 -0
  7. package/src/api-client/fund-facts.ts +45 -4
  8. package/src/api-client/index.ts +11 -0
  9. package/src/api-client/market-discovery.ts +10 -0
  10. package/src/api-client/market-movers.ts +7 -0
  11. package/src/api-client/members.ts +1 -1
  12. package/src/api-client/perps.ts +43 -0
  13. package/src/api-client/research-activity.ts +3 -2
  14. package/src/api-client/types.ts +70 -2
  15. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  16. package/src/app/runtime/broker-import.ts +0 -2
  17. package/src/app/runtime/plugin-bindings.ts +73 -56
  18. package/src/architecture/pane-tabs-allowlist.json +3 -0
  19. package/src/architecture/ttl-caches-allowlist.json +19 -0
  20. package/src/brokers/sync-broker-instance.ts +2 -8
  21. package/src/capabilities/chart-series.ts +1 -1
  22. package/src/cli/commands/crypto-hints.ts +40 -24
  23. package/src/cli/commands/market.ts +72 -26
  24. package/src/cli/commands/options-chain.ts +48 -7
  25. package/src/cli/commands/options-leaps.ts +9 -3
  26. package/src/cli/commands/overview.ts +61 -12
  27. package/src/cli/commands/search.ts +21 -1
  28. package/src/cli/commands/themes.ts +11 -2
  29. package/src/cli/commands/ticker.ts +55 -34
  30. package/src/cli/company-name-hint.ts +41 -0
  31. package/src/cli/earnings-calendar.ts +1 -1
  32. package/src/cli/errors.ts +8 -0
  33. package/src/cli/glossary.ts +25 -2
  34. package/src/cli/history-rows.ts +57 -9
  35. package/src/cli/index.ts +2 -0
  36. package/src/cli/listing-arg.ts +180 -52
  37. package/src/cli/output-file.ts +68 -0
  38. package/src/cli/pane-functions/capabilities.ts +3 -0
  39. package/src/cli/pane-functions/catalog.ts +80 -7
  40. package/src/cli/pane-functions/dom.ts +13 -3
  41. package/src/cli/pane-functions/freshness.ts +86 -3
  42. package/src/cli/pane-functions/function-help.ts +13 -10
  43. package/src/cli/pane-functions/headless.ts +17 -7
  44. package/src/cli/pane-functions/index.ts +49 -14
  45. package/src/cli/pane-functions/market-session.ts +66 -4
  46. package/src/cli/pane-functions/options.ts +19 -1
  47. package/src/cli/pane-functions/resolver.ts +19 -8
  48. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  49. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  50. package/src/cli/quote-answer.ts +22 -11
  51. package/src/cli/ticker-resolution.ts +3 -2
  52. package/src/components/chart/composite/composite-chart.tsx +4 -0
  53. package/src/components/chart/composite/interactions.ts +6 -0
  54. package/src/components/chart/composite/legend.tsx +45 -7
  55. package/src/components/chart/composite/panel-gestures.ts +33 -0
  56. package/src/components/chart/composite/panel-surface.tsx +52 -3
  57. package/src/components/chart/composite/rasterizer.ts +24 -13
  58. package/src/components/chart/composite/scene.ts +39 -4
  59. package/src/components/chart/composite/series-cues.ts +14 -0
  60. package/src/components/chart/composite/text-renderer.ts +9 -2
  61. package/src/components/chart/composite/tool-catalog.ts +36 -0
  62. package/src/components/chart/composite/tools.ts +203 -44
  63. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  64. package/src/components/command-bar/assist/inventory.ts +2 -1
  65. package/src/components/command-bar/commands/direct/index.ts +28 -13
  66. package/src/components/command-bar/pane-templates/items.ts +3 -2
  67. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  68. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  69. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  70. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  71. package/src/components/onboarding/wizard-positions.ts +3 -3
  72. package/src/components/ticker/list-table-view.tsx +4 -1
  73. package/src/components/ui/choice-dialog.tsx +23 -18
  74. package/src/components/ui/choice-groups.ts +18 -0
  75. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  76. package/src/components/ui/data-table/opentui/model.ts +26 -1
  77. package/src/components/ui/data-table/types.ts +2 -0
  78. package/src/components/ui/list-view.tsx +8 -4
  79. package/src/components/ui/query-bar-keys.tsx +2 -0
  80. package/src/components/ui/query-bar.tsx +1 -0
  81. package/src/components/ui/select-button.tsx +5 -1
  82. package/src/components/ui/select-field.tsx +1 -0
  83. package/src/components/ui/table-layout.ts +8 -5
  84. package/src/core/app-runtime.ts +2 -0
  85. package/src/core/app-services.ts +2 -2
  86. package/src/data/config/chart-settings.ts +1 -1
  87. package/src/data/json-ticker-repository.ts +17 -8
  88. package/src/data/ticker-repository.ts +16 -9
  89. package/src/data/ticker-writes.ts +81 -0
  90. package/src/data/ttl-cache.ts +165 -0
  91. package/src/docs/snapshot.json +71 -137
  92. package/src/i18n/es.ts +18 -2
  93. package/src/i18n/ja.ts +18 -2
  94. package/src/i18n/ko.ts +18 -2
  95. package/src/i18n/zh-cn.ts +18 -2
  96. package/src/i18n/zh-tw.ts +18 -2
  97. package/src/market-data/depositary-pair.ts +2 -2
  98. package/src/market-data/fund-facts.ts +50 -0
  99. package/src/market-data/market/change-basis.ts +16 -3
  100. package/src/market-data/market/format.ts +25 -0
  101. package/src/market-data/market/freshness.ts +2 -1
  102. package/src/market-data/options-alternatives.ts +81 -4
  103. package/src/news/aggregator.ts +113 -9
  104. package/src/news/cloud-session.ts +21 -0
  105. package/src/news/hooks.ts +1 -0
  106. package/src/news/news-model.ts +1 -0
  107. package/src/news/types.ts +11 -0
  108. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  109. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  110. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  111. package/src/plugins/builtin/alerts/cli.ts +336 -77
  112. package/src/plugins/builtin/alerts/command.ts +25 -1
  113. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  114. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  115. package/src/plugins/builtin/alerts/format.ts +75 -5
  116. package/src/plugins/builtin/alerts/headless.ts +24 -6
  117. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  118. package/src/plugins/builtin/alerts/history.ts +1 -1
  119. package/src/plugins/builtin/alerts/index.tsx +79 -16
  120. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  121. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  122. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  123. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  124. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  125. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  126. package/src/plugins/builtin/alerts/storage.ts +12 -8
  127. package/src/plugins/builtin/alerts/types.ts +4 -0
  128. package/src/plugins/builtin/analytics/index.tsx +2 -1
  129. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  130. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  131. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  132. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  134. package/src/plugins/builtin/attention/index.tsx +2 -1
  135. package/src/plugins/builtin/attention/listing.ts +14 -0
  136. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  137. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  138. package/src/plugins/builtin/buildout/format.ts +38 -0
  139. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  140. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  141. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  142. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  143. package/src/plugins/builtin/changelog/index.tsx +9 -6
  144. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  147. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  148. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  149. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  150. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  151. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  152. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  153. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  154. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  155. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  156. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  157. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  158. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  159. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  160. package/src/plugins/builtin/company-attention/client.ts +8 -9
  161. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  162. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  163. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  164. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  165. package/src/plugins/builtin/correlation/headless.ts +13 -5
  166. package/src/plugins/builtin/correlation/history.ts +22 -0
  167. package/src/plugins/builtin/cot/client.ts +22 -18
  168. package/src/plugins/builtin/cot/headless.ts +15 -10
  169. package/src/plugins/builtin/cot/model.ts +14 -1
  170. package/src/plugins/builtin/cot/pane.tsx +22 -14
  171. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  172. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  173. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  174. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  175. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  176. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  177. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  178. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  179. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  180. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  181. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  182. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  183. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  184. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  185. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  186. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  187. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  188. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  189. package/src/plugins/builtin/docs/targets.ts +5 -1
  190. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  191. package/src/plugins/builtin/earnings/board.tsx +62 -71
  192. package/src/plugins/builtin/earnings/headless.ts +40 -4
  193. package/src/plugins/builtin/earnings/scope.ts +31 -43
  194. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  195. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  196. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  197. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  198. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  199. package/src/plugins/builtin/econ/types.ts +2 -0
  200. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  201. package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
  202. package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
  203. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  204. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  205. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  206. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  207. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  208. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  209. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  210. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  211. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  212. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  213. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  214. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  215. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  216. package/src/plugins/builtin/gpu/headless.ts +8 -2
  217. package/src/plugins/builtin/help/function-index.ts +4 -1
  218. package/src/plugins/builtin/help/index.tsx +1 -0
  219. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  220. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  221. package/src/plugins/builtin/holders/headless.ts +18 -1
  222. package/src/plugins/builtin/iv-history/client.ts +52 -11
  223. package/src/plugins/builtin/iv-history/format.ts +36 -23
  224. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  225. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  226. package/src/plugins/builtin/iv-history/model.ts +163 -27
  227. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  228. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  229. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  230. package/src/plugins/builtin/jobs/client.ts +25 -29
  231. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  232. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  233. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  234. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  235. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  236. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  237. package/src/plugins/builtin/market-movers/client.ts +25 -7
  238. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  239. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  240. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  241. package/src/plugins/builtin/market-movers/model.ts +4 -1
  242. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  243. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  244. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  245. package/src/plugins/builtin/market-movers/table.ts +17 -5
  246. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  247. package/src/plugins/builtin/news/index.tsx +1 -0
  248. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  249. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  250. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  251. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  252. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  253. package/src/plugins/builtin/openfda/client.ts +14 -2
  254. package/src/plugins/builtin/openfda/index.tsx +3 -2
  255. package/src/plugins/builtin/openfda/model.ts +9 -0
  256. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  257. package/src/plugins/builtin/options/analytics.ts +29 -5
  258. package/src/plugins/builtin/options/enrichment.ts +17 -5
  259. package/src/plugins/builtin/options/table.ts +5 -2
  260. package/src/plugins/builtin/options/types.ts +2 -0
  261. package/src/plugins/builtin/options/view.tsx +17 -4
  262. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  263. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  264. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  265. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  266. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  267. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  268. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  269. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  270. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  271. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  272. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  273. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  274. package/src/plugins/builtin/pe-band/model.ts +12 -49
  275. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  276. package/src/plugins/builtin/perps/cascade.ts +236 -0
  277. package/src/plugins/builtin/perps/client.ts +56 -2
  278. package/src/plugins/builtin/perps/headless.ts +65 -9
  279. package/src/plugins/builtin/perps/index.tsx +7 -5
  280. package/src/plugins/builtin/perps/model.ts +18 -4
  281. package/src/plugins/builtin/perps/pane.tsx +19 -22
  282. package/src/plugins/builtin/perps/status-line.ts +25 -0
  283. package/src/plugins/builtin/plugin-module.ts +5 -1
  284. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  285. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  286. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  287. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  288. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  289. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  290. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  291. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  292. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  293. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  294. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  295. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  296. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  297. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  298. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  299. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  300. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  301. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  302. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  303. package/src/plugins/builtin/research/event-model.ts +10 -8
  304. package/src/plugins/builtin/research/headless.ts +1 -1
  305. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  306. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  307. package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
  308. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  309. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  310. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  311. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  312. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  313. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  314. package/src/plugins/builtin/scanner/headless.ts +204 -35
  315. package/src/plugins/builtin/scanner/index.ts +16 -5
  316. package/src/plugins/builtin/scanner/live-headless.ts +27 -3
  317. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  318. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  319. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  320. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  321. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  322. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  323. package/src/plugins/builtin/sec/headless.ts +2 -1
  324. package/src/plugins/builtin/series-inventory.ts +2 -0
  325. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  326. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  327. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  328. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  329. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  330. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  331. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  332. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  333. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  334. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  335. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  336. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  337. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  338. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  339. package/src/plugins/builtin/themes/model.ts +16 -7
  340. package/src/plugins/builtin/themes/pane.tsx +5 -5
  341. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  342. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  343. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  344. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  345. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  346. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  347. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  348. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  349. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  350. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  351. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  352. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  353. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  354. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  355. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  357. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  358. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  359. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  360. package/src/plugins/builtin/volatility/index.tsx +2 -2
  361. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  362. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  363. package/src/plugins/catalog-manifest.ts +2 -1
  364. package/src/plugins/marketplace/model.ts +18 -5
  365. package/src/portfolio/futures-positions.ts +6 -3
  366. package/src/portfolio/position-quantity.ts +15 -9
  367. package/src/remote/resources.ts +2 -1
  368. package/src/renderers/dom/data-table/row.tsx +26 -0
  369. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  370. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  371. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  372. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  373. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  374. package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
  375. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  376. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  377. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  378. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  379. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  380. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  381. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  382. package/src/renderers/electrobun/view/main.tsx +24 -1
  383. package/src/renderers/electrobun/view/news-source.ts +3 -1
  384. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  385. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  386. package/src/renderers/opentui/start.tsx +5 -0
  387. package/src/series/registry.ts +1 -0
  388. package/src/series/types.ts +6 -0
  389. package/src/series/validate.ts +1 -0
  390. package/src/sources/gloomberb-cloud/index.ts +1 -0
  391. package/src/sources/gloomberb-cloud/news.ts +47 -7
  392. package/src/sources/provider-router/financials.ts +15 -3
  393. package/src/sources/provider-router/index.ts +8 -5
  394. package/src/state/ticker-cleanup.ts +50 -0
  395. package/src/sync/controller.ts +4 -3
  396. package/src/sync/core-contributors.ts +90 -31
  397. package/src/telemetry/stall-reports-dom.ts +38 -0
  398. package/src/telemetry/stall-reports-node.ts +75 -0
  399. package/src/telemetry/stall-reports.ts +214 -0
  400. package/src/test-support/preload.ts +14 -0
  401. package/src/theme/themes.ts +10 -0
  402. package/src/tickers/company-name.ts +44 -3
  403. package/src/tickers/listing-venues.ts +15 -3
  404. package/src/tickers/terminal-codes.ts +19 -0
  405. package/src/time-series/alignment.ts +1 -1
  406. package/src/time-series/economic.ts +18 -4
  407. package/src/time-series/field-catalog.ts +1 -1
  408. package/src/time-series/filed-trailing-eps.ts +197 -0
  409. package/src/time-series/fundamentals.ts +44 -1
  410. package/src/time-series/ice-credit.ts +3 -0
  411. package/src/time-series/price-comparison.ts +30 -2
  412. package/src/time-series/resolve-context.ts +3 -1
  413. package/src/time-series/resolve.ts +56 -4
  414. package/src/time-series/session-bar.ts +11 -0
  415. package/src/time-series/spec.ts +3 -2
  416. package/src/time-series/studies.ts +8 -2
  417. package/src/time-series/total-return.ts +160 -0
  418. package/src/time-series/transforms.ts +2 -3
  419. package/src/time-series/types.ts +22 -1
  420. package/src/types/financials.ts +32 -1
  421. package/src/types/headless.ts +21 -3
  422. package/src/types/news-source.ts +1 -0
  423. package/src/types/plugin.ts +9 -0
  424. package/src/ui/host.tsx +3 -0
  425. package/src/updater/failure.ts +4 -1
  426. package/src/utils/cli-output.ts +42 -1
  427. package/src/utils/crypto-pair.ts +44 -0
  428. package/src/utils/forward-dividend.ts +2 -0
  429. package/src/utils/fundamentals.ts +24 -3
  430. package/src/utils/fx-market-hours.ts +26 -3
  431. package/src/utils/main-thread-stall.ts +35 -2
  432. package/src/utils/market-capitalization.ts +24 -1
  433. package/src/utils/price-history.ts +7 -1
  434. package/src/utils/shell-word.ts +8 -0
  435. package/src/utils/trailing-dividend.ts +53 -4
  436. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -1,6 +1,7 @@
1
1
  /** @jsxImportSource react */
2
2
  import { useCallback, useContext, useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
3
3
  import type { HostQueryBarItem, HostQueryBarProps } from "../../../ui/host";
4
+ import { withChoiceGroupHeadings } from "../../../components/ui/choice-groups";
4
5
  import { WebPopover } from "./popover";
5
6
  import { WebMenu } from "./menu";
6
7
  import { useHorizontalOverflow } from "../host/overflow-fade";
@@ -75,15 +76,16 @@ function QueryMenu({ item, onClose }: { item: HostQueryBarItem; onClose: () => v
75
76
  <WebMenu
76
77
  label={item.label}
77
78
  selection={item.kind === "multi" ? "multi" : "single"}
78
- items={item.options.map((option) => ({
79
+ items={withChoiceGroupHeadings(item.options.map((option) => ({
79
80
  id: option.value,
80
81
  label: option.label,
82
+ group: option.group,
81
83
  description: option.description,
82
84
  disabled: option.disabled,
83
85
  selected: item.kind === "select" ? option.selected : undefined,
84
86
  checked: item.kind === "multi" ? option.selected : undefined,
85
87
  color: option.color,
86
- }))}
88
+ })))}
87
89
  onSelect={(value) => {
88
90
  item.onSelect(value);
89
91
  if (item.kind === "select") onClose();
@@ -5,6 +5,7 @@ import { WEB_CELL_HEIGHT } from "../../../theme/font-scale";
5
5
  import { Box } from "../../../ui";
6
6
  import { MenuPopover } from "../../../components/ui/menu";
7
7
  import type { SelectFieldHandle, SelectFieldProps } from "../../../components/ui/select-field";
8
+ import { withChoiceGroupHeadings } from "../../../components/ui/choice-groups";
8
9
  import { WebIcon } from "./icons";
9
10
  import { isInsideDialogSurface } from "../host/focus-scope";
10
11
 
@@ -136,13 +137,14 @@ export function WebSelectField({
136
137
  minWidth={typeof resolvedWidth === "number" ? Math.max(160, resolvedWidth) : 170}
137
138
  items={[
138
139
  ...(showUnset ? [{ id: UNSET_VALUE, label: "Unset", selected: true, disabled: true }] : []),
139
- ...options.map((option) => ({
140
+ ...withChoiceGroupHeadings(options.map((option) => ({
140
141
  id: option.value,
141
142
  label: option.label,
143
+ group: option.group,
142
144
  description: option.description,
143
145
  disabled: option.disabled,
144
146
  selected: option.value === value,
145
- })),
147
+ }))),
146
148
  ]}
147
149
  onSelect={(id) => {
148
150
  if (id !== UNSET_VALUE && id !== value) onChange(id);
@@ -168,6 +168,27 @@ const ChartVectors = memo(function ChartVectors({ vectors }: { vectors: readonly
168
168
  />
169
169
  ))
170
170
  : [])}
171
+ {vectors.map((shape) => shape.label && shape.points[0]
172
+ ? (
173
+ <div
174
+ key={`${shape.id}:label`}
175
+ style={{
176
+ position: "absolute",
177
+ left: `${shape.points[0].x * 100}%`,
178
+ top: `${shape.points[0].y * 100}%`,
179
+ transform: "translate(2px, -100%)",
180
+ color: shape.color,
181
+ fontSize: "var(--gloom-type-caption)",
182
+ lineHeight: 1.2,
183
+ whiteSpace: "nowrap",
184
+ pointerEvents: "none",
185
+ zIndex: 10,
186
+ }}
187
+ >
188
+ {shape.label}
189
+ </div>
190
+ )
191
+ : null)}
171
192
  </>
172
193
  );
173
194
  });
@@ -3,7 +3,7 @@ import { access, lstat, readFile, readlink, rename, rm, symlink } from "fs/promi
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
4
4
  import { HOMEBREW_PREFIXES, macCommandFolders } from "../../../../updater/command-names";
5
5
  import { logFailure } from "../../../../utils/log-failure";
6
- import { DESKTOP_APP_IDENTIFIER, DESKTOP_APP_NAME, LEGACY_DESKTOP_APP_NAME } from "../../shared/app-name";
6
+ import { DESKTOP_APP_NAME, isDesktopAppIdentifier, LEGACY_DESKTOP_APP_NAME } from "../../shared/app-name";
7
7
 
8
8
  /**
9
9
  * A Mac install from before the rename is `Gloomberb.app`, and its updater
@@ -171,7 +171,7 @@ export async function renameLegacyMacApp(options: MacAppNameOptions): Promise<Ma
171
171
 
172
172
  const renamed = join(dirname(bundle), APP_BUNDLE);
173
173
  const occupied = await exists(renamed);
174
- if (occupied && await bundleIdentifier(renamed) !== DESKTOP_APP_IDENTIFIER) return { kind: "skipped", reason: "name-taken" };
174
+ if (occupied && !isDesktopAppIdentifier(await bundleIdentifier(renamed))) return { kind: "skipped", reason: "name-taken" };
175
175
  const links = await movableLinks(bundle, options);
176
176
  if (!links) return { kind: "skipped", reason: "command-folder-not-writable" };
177
177
 
@@ -209,7 +209,7 @@ export async function removeLegacyMacApp(options: MacAppNameOptions): Promise<st
209
209
  const bundle = runningBundle(options.execPath);
210
210
  if (!bundle || basename(bundle) !== APP_BUNDLE) return null;
211
211
  const legacy = join(dirname(bundle), LEGACY_APP_BUNDLE);
212
- if (!(await exists(legacy)) || await bundleIdentifier(legacy) !== DESKTOP_APP_IDENTIFIER) return null;
212
+ if (!(await exists(legacy)) || !isDesktopAppIdentifier(await bundleIdentifier(legacy))) return null;
213
213
  if (await managedByHomebrew(options.homebrewPrefixes ?? HOMEBREW_PREFIXES)) return null;
214
214
  const links = await movableLinks(legacy, options);
215
215
  if (!links) return null;
@@ -16,11 +16,14 @@ import { startRemoteControlServer, type RemoteControlServer } from "../../../../
16
16
  import type { RemoteControlRequest, RemoteControlResponse } from "../../../../remote/types";
17
17
  import { installCrashReporter } from "../../../../telemetry/crash-reports";
18
18
  import { createNodeCrashReporterHost } from "../../../../telemetry/crash-reports-node";
19
+ import { installStallReporter, parseStallEvent, recordStall } from "../../../../telemetry/stall-reports";
20
+ import { createNodeStallReporterHost } from "../../../../telemetry/stall-reports-node";
19
21
  import { installUpdateFailureReporter, reviewLastUpdateAttempt } from "../../../../telemetry/update-failures";
20
22
  import { createNodeUpdateFailureHost } from "../../../../telemetry/update-failures-node";
21
23
  import type { AppConfig } from "../../../../types/config";
22
24
  import { linkDesktopCommands } from "../../../../updater/command-names";
23
25
  import { debugLog } from "../../../../utils/debug-log";
26
+ import { setStallSink, startMainThreadStallMonitor } from "../../../../utils/main-thread-stall";
24
27
  import type { DesktopSharedStateSnapshot } from "../../../../types/desktop-window";
25
28
  import type {
26
29
  ContextMenuSelectMessage,
@@ -156,6 +159,10 @@ export class DesktopBackend {
156
159
  }));
157
160
  reviewLastUpdateAttempt();
158
161
  void removeUpdateAsides();
162
+ // Desktop has no Debug Log: freezes of this process and of its windows go to the stall log in the data folder.
163
+ installStallReporter(createNodeStallReporterHost({ getDataDir: () => this.config?.dataDir ?? null }));
164
+ setStallSink((event) => recordStall("app", event));
165
+ startMainThreadStallMonitor();
159
166
  }
160
167
  // The native menu shows the same accelerators the keys use, so a rebind
161
168
  // rebuilds it.
@@ -259,6 +266,10 @@ export class DesktopBackend {
259
266
  "host.restart": (message) => {
260
267
  this.restart(message);
261
268
  },
269
+ "runtime.stall": (message) => {
270
+ const event = parseStallEvent(message);
271
+ if (event) recordStall("window", event);
272
+ },
262
273
  },
263
274
  },
264
275
  });
@@ -564,6 +575,7 @@ export class DesktopBackend {
564
575
  case "host.notify":
565
576
  case "host.showContextMenu":
566
577
  case "host.saveBootFrame":
578
+ case "host.saveViewIds":
567
579
  case "host.setAppBadge":
568
580
  return handleDesktopHostRequest(this, rpc, request);
569
581
  case "update.check":
@@ -13,6 +13,7 @@ import { applyDesktopWindowControl } from "../window/controls";
13
13
  import { saveTextFileToDownloads } from "../../../../utils/save-text-file";
14
14
  import { setAppBadge } from "./app-badge";
15
15
  import { desktopBootFramePath, saveDesktopBootFrame } from "../window/boot-frame";
16
+ import { saveDesktopViewIds } from "./view-ids";
16
17
  import type { DesktopBackend, DesktopRpc } from "./backend";
17
18
 
18
19
  function normalizeText(value: unknown): string | undefined {
@@ -162,6 +163,12 @@ export async function handleDesktopHostRequest(
162
163
  session: backend.config !== null,
163
164
  });
164
165
  return null;
166
+ case "host.saveViewIds":
167
+ // The main window's view keeps them; its popped-out panes share its storage.
168
+ if (backend.rpcs.getRpcWindowKey(rpc) === MAIN_WINDOW_RPC_KEY && backend.config) {
169
+ saveDesktopViewIds(backend.config.dataDir, request.payload.ids);
170
+ }
171
+ return null;
165
172
  case "host.setAppBadge":
166
173
  // One badge for the app, from the main window; a popped-out pane's view
167
174
  // has its own chat state and must not overwrite it.
@@ -24,6 +24,7 @@ import type { CapabilityRegistry } from "../../../../capabilities";
24
24
  import { telemetryOptedOut } from "../../../../telemetry/crash-reports";
25
25
  import { describeNodeOs, readOrCreateInstallId } from "../../../../telemetry/crash-reports-node";
26
26
  import type { DesktopBackend, DesktopRpc } from "./backend";
27
+ import { readDesktopViewIds } from "./view-ids";
27
28
 
28
29
  interface DesktopWindowTarget {
29
30
  kind: "main" | "detached";
@@ -94,6 +95,7 @@ function buildInitializationPayload(
94
95
  homeDir: homedir(),
95
96
  optedOut: telemetryOptedOut(process.env),
96
97
  },
98
+ viewIds: readDesktopViewIds(config.dataDir),
97
99
  };
98
100
  }
99
101
 
@@ -1,4 +1,4 @@
1
- import { readdir, rm } from "fs/promises";
1
+ import { open, readdir, rm } from "fs/promises";
2
2
  import { basename, dirname, join } from "path";
3
3
  import type Electrobun from "electrobun/bun";
4
4
  import type { UpdateStatusEntry } from "electrobun/bun";
@@ -145,24 +145,101 @@ export function firstTarPath(bytes: Uint8Array): string | null {
145
145
  return null;
146
146
  }
147
147
 
148
+ /**
149
+ * An octal number field of a tar header, read as tar readers do: the digits
150
+ * after any leading spaces, up to the first other character. The checksum is
151
+ * what tells a header from noise.
152
+ */
153
+ function tarOctal(header: Uint8Array, start: number, length: number): number {
154
+ const digits = /^[\0 ]*([0-7]*)/.exec(new TextDecoder().decode(header.subarray(start, start + length)))?.[1];
155
+ return digits ? Number.parseInt(digits, 8) : 0;
156
+ }
157
+
158
+ /** Whether a header's checksum matches it: its bytes summed with the checksum field as spaces. */
159
+ function tarHeaderChecksumMatches(header: Uint8Array): boolean {
160
+ const stored = tarOctal(header, 148, 8);
161
+ let unsigned = 0;
162
+ let signed = 0;
163
+ for (let index = 0; index < TAR_BLOCK; index += 1) {
164
+ const byte = index >= 148 && index < 156 ? 0x20 : header[index]!;
165
+ unsigned += byte;
166
+ signed += byte > 127 ? byte - 256 : byte;
167
+ }
168
+ return stored === unsigned || stored === signed;
169
+ }
170
+
171
+ /**
172
+ * Whether the file at `path` is a whole tar: every header from the first
173
+ * matches its checksum, and the entries reach the end-of-archive block before
174
+ * the file ends. A tar cut off partway (a decompress that failed, an app that
175
+ * quit while writing it) or no tar at all is not. It reads the headers only,
176
+ * so it says nothing about the files' contents.
177
+ */
178
+ export async function isWholeTar(path: string): Promise<boolean> {
179
+ const file = await open(path, "r").catch(() => null);
180
+ if (!file) return false;
181
+ try {
182
+ const { size } = await file.stat();
183
+ const header = new Uint8Array(TAR_BLOCK);
184
+ for (let offset = 0; offset + TAR_BLOCK <= size;) {
185
+ const { bytesRead } = await file.read(header, 0, TAR_BLOCK, offset);
186
+ if (bytesRead < TAR_BLOCK) return false;
187
+ if (header.every((byte) => byte === 0)) return offset > 0;
188
+ if (!tarHeaderChecksumMatches(header)) return false;
189
+ // Octal: the base-256 sizes past 8 GB are not in an app's archive.
190
+ const entrySize = tarOctal(header, 124, 12);
191
+ offset += TAR_BLOCK + Math.ceil(entrySize / TAR_BLOCK) * TAR_BLOCK;
192
+ }
193
+ return false;
194
+ } catch {
195
+ return false;
196
+ } finally {
197
+ await file.close().catch(() => {});
198
+ }
199
+ }
200
+
201
+ /** Whether `path` holds a file that is not a whole tar. A missing one is not damaged. */
202
+ async function isDamagedTar(path: string): Promise<boolean> {
203
+ return await Bun.file(path).exists() && !(await isWholeTar(path));
204
+ }
205
+
206
+ /** Where Electrobun keeps the update with build hash `hash` once it is downloaded. */
207
+ function stagedArchivePath(extractionFolder: string, hash: string | undefined): string | null {
208
+ return hash ? join(extractionFolder, `${hash}.tar`) : null;
209
+ }
210
+
211
+ async function removeStagedArchive(path: string): Promise<void> {
212
+ await rm(path, { force: true }).catch(logFailure("desktop-update", "Could not remove a damaged update archive"));
213
+ }
214
+
148
215
  /**
149
216
  * Removes the unpacked update archives in Electrobun's `self-extraction`
150
- * folder whose app carries its name from before the rename. The one that
151
- * matters is the Windows archive an install from then updated from: it is
152
- * packed again with the folder renamed (scripts/write-legacy-desktop-files.ts),
153
- * so its bytes are not those of the archive the next release's patch is made
154
- * from, and bspatch would turn it into a broken build. Without it the updater
217
+ * folder it must not build on. Electrobun takes `<hash>.tar` there as the
218
+ * update already downloaded without reading it, and a damaged one (part of a
219
+ * tar, from a decompress that failed) then failed to unpack at every install:
220
+ * without it, the download fetches a fresh copy. Also removed are the ones
221
+ * whose app carries its name from before the rename. The one that matters is
222
+ * the Windows archive an install from then updated from: it is packed again
223
+ * with the folder renamed (scripts/write-legacy-desktop-files.ts), so its
224
+ * bytes are not those of the archive the next release's patch is made from,
225
+ * and bspatch would turn it into a broken build. Without it the updater
155
226
  * downloads the full archive instead. Resolves to the paths it removed.
156
227
  */
157
- async function removeLegacyUpdateArchives(folder: string): Promise<string[]> {
228
+ async function removeUnusableUpdateArchives(folder: string): Promise<string[]> {
158
229
  const removed: string[] = [];
159
230
  for (const entry of await readdir(folder).catch(() => [] as string[])) {
160
231
  if (!entry.endsWith(".tar")) continue;
161
232
  const path = join(folder, entry);
162
- const head = await Bun.file(path).slice(0, 64 * 1024).bytes().catch(() => null);
163
- const top = head ? firstTarPath(head)?.replace(/^\.\//, "").split("/")[0] : undefined;
164
- if (top !== LEGACY_DESKTOP_APP_NAME && top !== `${LEGACY_DESKTOP_APP_NAME}.app`) continue;
165
- const done = await rm(path, { force: true }).then(() => true, logFailure("desktop-update", "Could not remove an update archive from before the rename", false));
233
+ let reason: string;
234
+ if (!(await isWholeTar(path))) {
235
+ reason = "Could not remove a damaged update archive";
236
+ } else {
237
+ const head = await Bun.file(path).slice(0, 64 * 1024).bytes().catch(() => null);
238
+ const top = head ? firstTarPath(head)?.replace(/^\.\//, "").split("/")[0] : undefined;
239
+ if (top !== LEGACY_DESKTOP_APP_NAME && top !== `${LEGACY_DESKTOP_APP_NAME}.app`) continue;
240
+ reason = "Could not remove an update archive from before the rename";
241
+ }
242
+ const done = await rm(path, { force: true }).then(() => true, logFailure("desktop-update", reason, false));
166
243
  if (done) removed.push(path);
167
244
  }
168
245
  return removed;
@@ -343,6 +420,12 @@ export function createElectrobunDesktopUpdater(
343
420
  /** An apply is under way, or finished and the app is quitting. */
344
421
  let applyInProgress = false;
345
422
 
423
+ /** Electrobun's `self-extraction` folder, where the download leaves the update as `<hash>.tar`. */
424
+ async function extractionFolder(): Promise<string | null> {
425
+ const appData = await updater.appDataFolder?.().catch(() => null);
426
+ return appData ? join(appData, "self-extraction") : null;
427
+ }
428
+
346
429
  /** The error progress for a failed step, reported to error tracking on the way. */
347
430
  function failed(failure: UpdateFailure, error: string, release?: { version?: string; channel?: UpdateChannel } | null): UpdateProgress {
348
431
  reportUpdateFailure(failure, {
@@ -529,8 +612,8 @@ export function createElectrobunDesktopUpdater(
529
612
  }
530
613
  target = result.release;
531
614
 
532
- const appData = await updater.appDataFolder?.().catch(() => null);
533
- if (appData) await removeLegacyUpdateArchives(join(appData, "self-extraction"));
615
+ const extraction = await extractionFolder();
616
+ if (extraction) await removeUnusableUpdateArchives(extraction);
534
617
  await updater.downloadUpdate();
535
618
  const updateInfo = updater.updateInfo();
536
619
  if (updateInfo?.error) {
@@ -541,6 +624,15 @@ export function createElectrobunDesktopUpdater(
541
624
  onProgress(failed({ phase: "download", code: "interrupted" }, "Desktop update did not finish downloading.", result.release));
542
625
  return;
543
626
  }
627
+ // However Electrobun came by it (a patch, the full download), an archive
628
+ // that is not whole is never offered for a restart. It goes, so the retry
629
+ // downloads it again.
630
+ const staged = extraction ? stagedArchivePath(extraction, updateInfo.hash) : null;
631
+ if (staged && await isDamagedTar(staged)) {
632
+ await removeStagedArchive(staged);
633
+ onProgress(failed({ phase: "verify", code: "corrupt" }, "The downloaded update is damaged.", result.release));
634
+ return;
635
+ }
544
636
 
545
637
  // Downloaded, not installed: applying relaunches the app, and that waits
546
638
  // for the user's explicit request (`apply`).
@@ -570,9 +662,12 @@ export function createElectrobunDesktopUpdater(
570
662
  }
571
663
 
572
664
  const target = readyRelease;
665
+ const stagedHash = updater.updateInfo()?.hash;
573
666
  applyInProgress = true;
574
667
  let relaunching = false;
575
668
  let reportedError = false;
669
+ /** The install failed on the archive itself: it does not unpack. */
670
+ let damaged = false;
576
671
  const report = (progress: UpdateProgress) => {
577
672
  if (progress.phase === "error") reportedError = true;
578
673
  onProgress(progress);
@@ -580,6 +675,7 @@ export function createElectrobunDesktopUpdater(
580
675
  const installFailed = (error: unknown, message: string) => {
581
676
  // Electrobun names the system error in its message ("Failed to replace app: EACCES: ...").
582
677
  const code = installFailureCode(classifyUpdateError(error), installKind);
678
+ if (code === "corrupt") damaged = true;
583
679
  report(failed({ phase: relaunching ? "relaunch" : "apply", code }, message, target));
584
680
  };
585
681
  // Kept before Electrobun quits the app, so the next launch can tell whether it took.
@@ -610,9 +706,14 @@ export function createElectrobunDesktopUpdater(
610
706
  }
611
707
 
612
708
  // Past this point the app is quitting, so the guard stays up. A failed apply
613
- // drops the staged release: the retry starts from the download again.
709
+ // drops the staged release: the retry starts from the download again. An
710
+ // archive that does not unpack goes too, or that download would take it as
711
+ // done once more and the install would fail on it again.
614
712
  if (!relaunching) {
615
713
  readyRelease = null;
714
+ const extraction = damaged ? await extractionFolder() : null;
715
+ const staged = extraction ? stagedArchivePath(extraction, stagedHash) : null;
716
+ if (staged) await removeStagedArchive(staged);
616
717
  applyInProgress = false;
617
718
  }
618
719
  }
@@ -0,0 +1,52 @@
1
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import type { DesktopViewIds } from "../../shared/protocol";
4
+
5
+ /**
6
+ * The copy on disk of the ids the desktop view keeps in its WebView storage
7
+ * (view/view-ids.ts), in the data folder beside the install id. That storage
8
+ * belongs to the app's bundle identifier, and this copy is what a build under
9
+ * another identifier starts from.
10
+ */
11
+ const FILE_NAME = "desktop-view-ids.json";
12
+ const KEY = /^gloomberb\.[a-z0-9.-]{1,64}$/i;
13
+ const MAX_IDS = 16;
14
+ const MAX_VALUE_LENGTH = 4096;
15
+
16
+ function viewIdsPath(dataDir: string): string {
17
+ return join(dataDir, FILE_NAME);
18
+ }
19
+
20
+ /** The entries of `raw` shaped like view ids: a storage key of this app's and a short string. */
21
+ function parseViewIds(raw: unknown): DesktopViewIds {
22
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
23
+ const ids: DesktopViewIds = {};
24
+ for (const [key, value] of Object.entries(raw).slice(0, MAX_IDS)) {
25
+ if (KEY.test(key) && typeof value === "string" && value.length > 0 && value.length <= MAX_VALUE_LENGTH) ids[key] = value;
26
+ }
27
+ return ids;
28
+ }
29
+
30
+ /** What the data folder keeps; nothing when the file is missing or unreadable. */
31
+ export function readDesktopViewIds(dataDir: string): DesktopViewIds {
32
+ try {
33
+ return parseViewIds(JSON.parse(readFileSync(viewIdsPath(dataDir), "utf8")));
34
+ } catch {
35
+ // Intentional ignore: no copy yet (a first launch, or a build from before it was kept), so the view keeps what it has.
36
+ return {};
37
+ }
38
+ }
39
+
40
+ /** Replaces the copy with what the view sent, written whole or not at all. Throws when it cannot, so the view tries again. */
41
+ export function saveDesktopViewIds(dataDir: string, ids: unknown): void {
42
+ const path = viewIdsPath(dataDir);
43
+ const temporary = `${path}.${process.pid}.tmp`;
44
+ try {
45
+ mkdirSync(dataDir, { recursive: true });
46
+ writeFileSync(temporary, JSON.stringify(parseViewIds(ids)));
47
+ renameSync(temporary, path);
48
+ } catch (error) {
49
+ rmSync(temporary, { force: true });
50
+ throw error;
51
+ }
52
+ }
@@ -15,3 +15,16 @@ export const LEGACY_DESKTOP_APP_NAME = "Gloomberb";
15
15
  * to it.
16
16
  */
17
17
  export const DESKTOP_APP_IDENTIFIER = "com.vincelwt.gloomberb";
18
+
19
+ /**
20
+ * The identifiers a build of this app carries: the one above, and the one a
21
+ * later release moves to. Until every install has moved, either one can be
22
+ * on disk (an app bundle, an install the script or Homebrew replaces), so
23
+ * anything that asks whether a copy is this app accepts both.
24
+ */
25
+ export const DESKTOP_APP_IDENTIFIERS: readonly string[] = [DESKTOP_APP_IDENTIFIER, "sh.gloom.desktop"];
26
+
27
+ /** Whether `identifier` is one a build of this app carries. */
28
+ export function isDesktopAppIdentifier(identifier: string | null | undefined): boolean {
29
+ return !!identifier && DESKTOP_APP_IDENTIFIERS.includes(identifier);
30
+ }
@@ -10,6 +10,7 @@ import type { CapabilityManifest } from "../../../capabilities";
10
10
  import type { PluginOperationResult, PluginPin } from "../../../plugins/marketplace/store";
11
11
  import type { RemoteControlRequest, RemoteControlResponse } from "../../../remote/types";
12
12
  import type { HttpProxyRequestEnvelope, HttpProxyResponseEnvelope } from "../../../utils/http-proxy-response";
13
+ import type { StallEvent } from "../../../utils/main-thread-stall";
13
14
  import type { BootFrame } from "../../dom/boot-frame-page";
14
15
 
15
16
  export const ELECTROBUN_CONTEXT_MENU_ACTION = "gloom.context-menu.select";
@@ -32,8 +33,13 @@ export interface ElectrobunBackendInit {
32
33
  pluginSeedNotice?: { body: string; type: "error" | "info" };
33
34
  /** What the view needs to send crash reports and usage counts the same way the Bun process would. */
34
35
  telemetry: ElectrobunTelemetryInit;
36
+ /** The copy on disk of the ids the view keeps in its own storage (view/view-ids.ts). */
37
+ viewIds: DesktopViewIds;
35
38
  }
36
39
 
40
+ /** Ids the view keeps in its WebView storage, by storage key. */
41
+ export type DesktopViewIds = Record<string, string>;
42
+
37
43
  interface ElectrobunTelemetryInit {
38
44
  /** Random per-install id from `<dataDir>/install-id`; null when the file could not be written. */
39
45
  installId: string | null;
@@ -222,6 +228,8 @@ interface DesktopBackendRequestMap {
222
228
  "host.showContextMenu": { request: { menu: DesktopContextMenuItem[] }; response: boolean };
223
229
  /** The layout the main window draws at the next launch before its script loads (dom/boot-frame.ts). Only the main window's is kept. */
224
230
  "host.saveBootFrame": { request: { frame: BootFrame }; response: null };
231
+ /** The ids the view keeps in its storage, for the copy on disk. Only the main window's are kept. */
232
+ "host.saveViewIds": { request: { ids: DesktopViewIds }; response: null };
225
233
  /** The unread count for the Dock icon badge: `""` (none), `"1"` to `"99"` or `"99+"`. Only the main window's is applied. */
226
234
  "host.setAppBadge": { request: { label: string }; response: null };
227
235
  /** `channel` absent (an older view) means stable. */
@@ -337,6 +345,8 @@ export interface ElectrobunDesktopRpcSchema {
337
345
  };
338
346
  messages: {
339
347
  "host.restart": DesktopRestartMessage;
348
+ /** A window froze; the Bun process keeps the stall log and its report. */
349
+ "runtime.stall": StallEvent;
340
350
  };
341
351
  };
342
352
  webview: {
@@ -17,6 +17,7 @@ import { subscribeCapability, type CapabilitySubscriptionOptions } from "./capab
17
17
  import { RpcTimeoutError } from "../../../utils/rpc-timeout-error";
18
18
  import { nameRpcTimeout, RPC_MAX_REQUEST_TIME_MS } from "./rpc-timeout";
19
19
  import type { RemoteControlRequest, RemoteControlResponse } from "../../../remote/types";
20
+ import type { StallEvent } from "../../../utils/main-thread-stall";
20
21
 
21
22
  type BackendMessages = ElectrobunDesktopRpcSchema["webview"]["messages"];
22
23
  type BackendMessageName = keyof BackendMessages;
@@ -153,6 +154,11 @@ export function requestElectrobunRestart(message: DesktopRestartMessage = {}): v
153
154
  rpc.send["host.restart"](message);
154
155
  }
155
156
 
157
+ /** The Bun process keeps the stall log and sends its report. */
158
+ export function sendElectrobunStall(event: StallEvent): void {
159
+ rpc.send["runtime.stall"](event);
160
+ }
161
+
156
162
  export function getElectrobunBackendInitSnapshot(): ElectrobunBackendInit | null {
157
163
  return initSnapshot;
158
164
  }
@@ -5,12 +5,14 @@ import { App } from "../../../app";
5
5
  import { applyLanguageFromConfig } from "../../../i18n";
6
6
  import { debugLog } from "../../../utils/debug-log";
7
7
  import { capturePaneImage } from "../../../utils/dom-screenshot";
8
+ import { setStallSink } from "../../../utils/main-thread-stall";
8
9
  import { measurePerfAsync } from "../../../utils/perf-marks";
9
10
  import {
10
11
  backendRequest,
11
12
  initElectrobunBackend,
12
13
  onBackendMessage,
13
14
  replaceElectrobunCapabilityManifests,
15
+ sendElectrobunStall,
14
16
  setElectrobunRemoteRequestHandler,
15
17
  } from "./backend-rpc";
16
18
  import { installElectrobunCapabilityStreamClient } from "./capability-stream-client";
@@ -49,10 +51,13 @@ import { loadOfficialPluginIds } from "../../../plugins/marketplace/feed";
49
51
  import { setPluginSeedNotice } from "../../../plugins/seed-notice";
50
52
  import { crashReportsEnabled, installCrashReporter, reportCrash } from "../../../telemetry/crash-reports";
51
53
  import { browserDoNotTrack, installWindowCrashListeners } from "../../../telemetry/crash-reports-dom";
54
+ import { forwardAttendedStalls } from "../../../telemetry/stall-reports-dom";
52
55
  import { currentTelemetryConfig } from "../../../telemetry/live-config";
53
56
  import { attentionCountsEnabled, installAttentionCounter } from "../../../telemetry/attention-counts";
54
57
  import { installUsageCounter, usageCountsEnabled } from "../../../telemetry/usage-counts";
55
58
  import { installWindowUsageFlush } from "../../../telemetry/usage-counts-dom";
59
+ import { cloudSyncController } from "../../../sync/controller";
60
+ import { createViewIdMirror, restoreViewIds, viewIdStorage } from "./view-ids";
56
61
 
57
62
  // Declared here rather than sniffed: the desktop view and the hosted browser
58
63
  // app are both browser contexts but differ in what plugins may do.
@@ -156,6 +161,8 @@ async function boot() {
156
161
  officialPluginIds: loadOfficialPluginIds,
157
162
  });
158
163
  installAttentionCounter(() => !init.telemetry.optedOut && !browserDoNotTrack() && attentionCountsEnabled(currentTelemetryConfig(init.config)));
164
+ // Desktop has no Debug Log: this window's freezes go to the Bun process, which keeps the stall log.
165
+ setStallSink(forwardAttendedStalls(sendElectrobunStall).sink);
159
166
  installElectrobunCapabilityStreamClient();
160
167
  installFocusScopeRelease();
161
168
  installElectrobunWindowFullscreenTracking();
@@ -173,10 +180,26 @@ async function boot() {
173
180
  const desktopApplicationMenuBridge: DesktopApplicationMenuBridge = {
174
181
  subscribe: (listener) => onBackendMessage("application-menu.select", ({ command }) => listener(command)),
175
182
  };
183
+ // Before anything reads them: ids this view's storage lacks come back from
184
+ // the copy on disk, and the main window keeps that copy up to date. The sync
185
+ // id is made at the first push, and a gloom.sh link can bring a visitor id.
186
+ const viewStorage = viewIdStorage();
187
+ if (viewStorage) restoreViewIds(viewStorage, init.viewIds);
188
+ const mirrorViewIds = viewStorage && init.windowKind === "main"
189
+ ? createViewIdMirror(viewStorage, init.viewIds, (ids) => backendRequest("host.saveViewIds", { ids }))
190
+ : () => {};
191
+ mirrorViewIds();
192
+ cloudSyncController.subscribe(mirrorViewIds);
176
193
  initializeDesktopResearchActivity();
177
- const desktopDeepLinkBridge = observeDesktopDeepLinks({
194
+ const observedDeepLinks = observeDesktopDeepLinks({
178
195
  subscribe: (listener) => onBackendMessage("desktop.deepLink", listener),
179
196
  });
197
+ const desktopDeepLinkBridge: typeof observedDeepLinks = {
198
+ subscribe: (listener) => observedDeepLinks.subscribe((deeplink) => {
199
+ listener(deeplink);
200
+ mirrorViewIds();
201
+ }),
202
+ };
180
203
  const webUiHost = createWebUiHost(init.desktopPlatform);
181
204
  // Compiled by the Bun process, which owns the filesystem. A failure here must
182
205
  // not stop the app from starting: the marketplace reports broken plugins, and
@@ -10,7 +10,8 @@ interface BackendNews {
10
10
  getNewsPage(query: NewsQuery): Promise<NewsPage>;
11
11
  }
12
12
 
13
- type CloudNewsClient = Pick<typeof apiClient, "getCloudNews" | "getCloudNewsStory" | "searchCloudNews">;
13
+ type CloudNewsClient = Pick<typeof apiClient, "getCloudNews" | "getCloudNewsStory" | "searchCloudNews">
14
+ & Partial<Pick<typeof apiClient, "hasSessionCredential">>;
14
15
 
15
16
  /**
16
17
  * The view's only news source: the desktop view runs without capability
@@ -32,6 +33,7 @@ export function createDesktopNewsSource(backend: BackendNews, client: CloudNewsC
32
33
  return {
33
34
  articles: cloudNewsReadArticles(read, query.ticker),
34
35
  nextCursor: read.nextCursor ?? null,
36
+ access: read.access ?? null,
35
37
  };
36
38
  } catch {
37
39
  // The aggregate page keeps RSS failures beside any usable stories.
@@ -1,5 +1,6 @@
1
1
  import type { TickerMetadata, TickerRecord } from "../../../../types/ticker";
2
2
  import { backendRequest } from "../backend-rpc";
3
+ import { runTickerWrite } from "../../../../data/ticker-writes";
3
4
 
4
5
  export class RemoteTickerRepository {
5
6
  async loadAllTickers(): Promise<TickerRecord[]> {
@@ -10,17 +11,23 @@ export class RemoteTickerRepository {
10
11
  return backendRequest("ticker.load", { symbol });
11
12
  }
12
13
 
13
- async saveTicker(ticker: TickerRecord): Promise<void> {
14
- await backendRequest("ticker.save", { ticker });
14
+ saveTicker(ticker: TickerRecord): Promise<void> {
15
+ return runTickerWrite(this, ticker.metadata.ticker, async () => {
16
+ await backendRequest("ticker.save", { ticker });
17
+ });
15
18
  }
16
19
 
17
- async createTicker(metadata: TickerMetadata): Promise<TickerRecord> {
20
+ createTicker(metadata: TickerMetadata): Promise<TickerRecord> {
18
21
  const ticker: TickerRecord = { metadata };
19
- await this.saveTicker(ticker);
20
- return ticker;
22
+ return runTickerWrite(this, metadata.ticker, async () => {
23
+ await backendRequest("ticker.save", { ticker });
24
+ return ticker;
25
+ });
21
26
  }
22
27
 
23
- async deleteTicker(symbol: string): Promise<void> {
24
- await backendRequest("ticker.delete", { symbol });
28
+ deleteTicker(symbol: string): Promise<void> {
29
+ return runTickerWrite(this, symbol, async () => {
30
+ await backendRequest("ticker.delete", { symbol });
31
+ });
25
32
  }
26
33
  }