gloomberb 0.19.2 → 0.19.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 (472) hide show
  1. package/README.md +7 -1
  2. package/package.json +1 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +20 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app.tsx +23 -1
  20. package/src/architecture/core-builtin-allowlist.json +2 -1
  21. package/src/capabilities/factories.ts +1 -0
  22. package/src/cli/ascii-output.ts +42 -0
  23. package/src/cli/base-converter.ts +10 -2
  24. package/src/cli/command-options.ts +16 -1
  25. package/src/cli/commands/crypto-hints.ts +23 -4
  26. package/src/cli/commands/market.ts +92 -23
  27. package/src/cli/commands/news-search.ts +24 -5
  28. package/src/cli/commands/overview.ts +9 -4
  29. package/src/cli/commands/search.ts +71 -12
  30. package/src/cli/commands/system.ts +22 -30
  31. package/src/cli/commands/ticker.ts +49 -9
  32. package/src/cli/commands/version.ts +33 -0
  33. package/src/cli/context.ts +5 -9
  34. package/src/cli/data-dir.ts +7 -0
  35. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  36. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  37. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  38. package/src/cli/desktop-pane-shot.ts +196 -256
  39. package/src/cli/early-command.ts +42 -0
  40. package/src/cli/earnings-calendar.ts +9 -0
  41. package/src/cli/entry.ts +19 -13
  42. package/src/cli/errors.ts +12 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/history-export.ts +22 -2
  45. package/src/cli/history-rows.ts +154 -4
  46. package/src/cli/index.ts +18 -1
  47. package/src/cli/known-gaps.ts +40 -5
  48. package/src/cli/listing-arg.ts +46 -9
  49. package/src/cli/options.ts +1 -1
  50. package/src/cli/output-file.ts +60 -13
  51. package/src/cli/pane-functions/catalog.ts +14 -3
  52. package/src/cli/pane-functions/data.ts +1 -1
  53. package/src/cli/pane-functions/dom.ts +45 -0
  54. package/src/cli/pane-functions/freshness.ts +46 -9
  55. package/src/cli/pane-functions/function-help.ts +17 -9
  56. package/src/cli/pane-functions/headless.ts +22 -14
  57. package/src/cli/pane-functions/index.ts +6 -2
  58. package/src/cli/pane-functions/options.ts +6 -2
  59. package/src/cli/pane-functions/resolver.ts +27 -5
  60. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  61. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  62. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  63. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  64. package/src/cli/quote-answer.ts +20 -0
  65. package/src/cli/registry.ts +41 -13
  66. package/src/cli/result.ts +12 -6
  67. package/src/cli/ticker-resolution.ts +2 -1
  68. package/src/cli/types.ts +2 -0
  69. package/src/components/chart/composite/format.ts +3 -0
  70. package/src/components/chart-table/header.tsx +1 -1
  71. package/src/components/chart-table/layout.ts +28 -11
  72. package/src/components/command-bar/action-runtime.ts +4 -0
  73. package/src/components/command-bar/commands/direct/index.ts +18 -33
  74. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  75. package/src/components/command-bar/commands/registry.ts +10 -1
  76. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  77. package/src/components/command-bar/routes/root/runtime.ts +24 -1
  78. package/src/components/command-bar/surface/index.tsx +9 -3
  79. package/src/components/company-logo.tsx +2 -1
  80. package/src/components/layout/header.tsx +8 -3
  81. package/src/components/layout/market-summary.ts +25 -7
  82. package/src/components/layout/pane/footer/index.tsx +7 -2
  83. package/src/components/layout/pane/footer/model.ts +40 -5
  84. package/src/components/market-board/index.tsx +3 -2
  85. package/src/components/onboarding/desks-step.tsx +12 -4
  86. package/src/components/onboarding/first-run-workspace.ts +81 -4
  87. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  88. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  89. package/src/components/onboarding/onboarding-wizard.tsx +54 -23
  90. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  91. package/src/components/onboarding/run-setup-again.ts +24 -0
  92. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  93. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  94. package/src/components/onboarding/wizard-model.ts +57 -3
  95. package/src/components/onboarding/wizard-positions.ts +1 -1
  96. package/src/components/ui/delayed-mark.tsx +56 -0
  97. package/src/components/ui/external-link.tsx +3 -6
  98. package/src/components/ui/icon.tsx +8 -1
  99. package/src/components/ui/logo-mark.tsx +8 -5
  100. package/src/components/ui/stat-grid.tsx +263 -19
  101. package/src/data/config/store/node.ts +11 -5
  102. package/src/data/config/store/normalize.ts +16 -3
  103. package/src/data/profile-permissions.ts +47 -0
  104. package/src/data/resource-store.ts +34 -31
  105. package/src/data/sqlite/access.ts +19 -8
  106. package/src/data/sqlite/checkpointer.ts +95 -8
  107. package/src/data/sqlite/database.ts +40 -7
  108. package/src/docs/snapshot.json +191 -78
  109. package/src/i18n/es.ts +18 -2
  110. package/src/i18n/ja.ts +18 -2
  111. package/src/i18n/ko.ts +18 -2
  112. package/src/i18n/zh-cn.ts +18 -2
  113. package/src/i18n/zh-tw.ts +18 -2
  114. package/src/layout/desk-keys.ts +4 -1
  115. package/src/layout/desks.ts +38 -5
  116. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  117. package/src/market-data/batched-reads.ts +16 -0
  118. package/src/market-data/coordinator/auxiliary.ts +4 -2
  119. package/src/market-data/coordinator/index.ts +11 -2
  120. package/src/market-data/futures-board.ts +1 -1
  121. package/src/market-data/futures-contract.ts +3 -1
  122. package/src/market-data/market/index-level.ts +6 -4
  123. package/src/market-data/published-apac-sessions.ts +6 -0
  124. package/src/news/aggregator.ts +7 -4
  125. package/src/news/news-model.ts +11 -0
  126. package/src/news/story-id.ts +22 -0
  127. package/src/news/ticker-symbols.ts +17 -1
  128. package/src/notifications/app-notifier.ts +35 -14
  129. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  130. package/src/plugins/builtin/account-management/index.tsx +2 -3
  131. package/src/plugins/builtin/account-management/model.ts +1 -1
  132. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  133. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  134. package/src/plugins/builtin/alerts/events.ts +10 -2
  135. package/src/plugins/builtin/alerts/index.tsx +5 -5
  136. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  137. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  138. package/src/plugins/builtin/alerts/targets.ts +4 -1
  139. package/src/plugins/builtin/attention/headless.ts +1 -1
  140. package/src/plugins/builtin/attention/index.tsx +2 -4
  141. package/src/plugins/builtin/attention/listing.ts +1 -1
  142. package/src/plugins/builtin/attention/series.ts +1 -1
  143. package/src/plugins/builtin/awards/index.tsx +1 -2
  144. package/src/plugins/builtin/backtest/client.ts +1 -0
  145. package/src/plugins/builtin/backtest/index.tsx +1 -2
  146. package/src/plugins/builtin/backtest/rules.ts +3 -1
  147. package/src/plugins/builtin/backtest/window.ts +8 -7
  148. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  149. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  150. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  151. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  152. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  153. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  154. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  155. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  156. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  157. package/src/plugins/builtin/buildout/format.ts +9 -0
  158. package/src/plugins/builtin/buildout/index.tsx +51 -5
  159. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  160. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  161. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  162. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  163. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  164. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  165. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  166. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  167. package/src/plugins/builtin/cds/headless.ts +10 -2
  168. package/src/plugins/builtin/cds/model.ts +7 -3
  169. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  170. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  171. package/src/plugins/builtin/changelog/index.tsx +3 -2
  172. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
  173. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  174. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  175. package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
  176. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  177. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  178. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  179. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  180. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  181. package/src/plugins/builtin/chat/index.tsx +5 -5
  182. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  183. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  184. package/src/plugins/builtin/cloud/index.tsx +1 -2
  185. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  186. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  187. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  188. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  189. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  190. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  191. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  192. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  193. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  194. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  195. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  196. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  197. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  198. package/src/plugins/builtin/connections/index.ts +2 -3
  199. package/src/plugins/builtin/cot/index.tsx +1 -2
  200. package/src/plugins/builtin/cpi/index.tsx +1 -2
  201. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  202. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  203. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  204. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  205. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  206. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  207. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  208. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  209. package/src/plugins/builtin/crypto-board/series.ts +2 -0
  210. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  211. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  212. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  213. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  214. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  215. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  216. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  217. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  218. package/src/plugins/builtin/debug/index.tsx +1 -1
  219. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  220. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  221. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  222. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  223. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  224. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  225. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  226. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  227. package/src/plugins/builtin/docs/index.tsx +2 -3
  228. package/src/plugins/builtin/doe/headless.ts +16 -3
  229. package/src/plugins/builtin/doe/index.tsx +5 -4
  230. package/src/plugins/builtin/earnings/board.tsx +2 -1
  231. package/src/plugins/builtin/earnings/history.tsx +2 -1
  232. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  233. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  234. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  235. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  236. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  237. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  238. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  239. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  240. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  241. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  242. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  243. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  244. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  245. package/src/plugins/builtin/exposure/index.tsx +1 -2
  246. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  247. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  248. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  249. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  250. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  251. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  252. package/src/plugins/builtin/futures/index.tsx +15 -0
  253. package/src/plugins/builtin/futures/model.ts +14 -0
  254. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  255. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  256. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  257. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  258. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  259. package/src/plugins/builtin/gpu/board.tsx +51 -12
  260. package/src/plugins/builtin/gpu/client.ts +15 -2
  261. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  262. package/src/plugins/builtin/gpu/headless.ts +14 -2
  263. package/src/plugins/builtin/gpu/index.tsx +1 -2
  264. package/src/plugins/builtin/gpu/model.ts +20 -2
  265. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  266. package/src/plugins/builtin/help/index.tsx +1 -1
  267. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  268. package/src/plugins/builtin/holders/index.tsx +2 -3
  269. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  270. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  271. package/src/plugins/builtin/jodi/index.tsx +1 -2
  272. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  273. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  274. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  275. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  276. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  277. package/src/plugins/builtin/market-movers/index.tsx +7 -1
  278. package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
  279. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  280. package/src/plugins/builtin/members/index.tsx +1 -2
  281. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  282. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  283. package/src/plugins/builtin/news/headless.ts +7 -0
  284. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  285. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  286. package/src/plugins/builtin/notes/index.tsx +3 -6
  287. package/src/plugins/builtin/openfda/index.tsx +1 -2
  288. package/src/plugins/builtin/options/view.tsx +6 -5
  289. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  290. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  291. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  292. package/src/plugins/builtin/perps/index.tsx +1 -2
  293. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  294. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  295. package/src/plugins/builtin/plugin-module.ts +16 -2
  296. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  297. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  298. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  299. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  300. package/src/plugins/builtin/power/index.tsx +1 -2
  301. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  302. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  303. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  304. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  305. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  306. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  307. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  308. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  309. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  310. package/src/plugins/builtin/research/index.tsx +5 -8
  311. package/src/plugins/builtin/research-search/index.tsx +1 -2
  312. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  313. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  314. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  315. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  316. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  317. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  318. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  319. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  320. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  321. package/src/plugins/builtin/sec/headless.ts +5 -1
  322. package/src/plugins/builtin/sec/index.tsx +4 -2
  323. package/src/plugins/builtin/sec/model.ts +2 -0
  324. package/src/plugins/builtin/sectors/index.tsx +11 -5
  325. package/src/plugins/builtin/series-inventory.ts +2 -0
  326. package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
  327. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  328. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  329. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  330. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  331. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  332. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  333. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  334. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  335. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  336. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  337. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  338. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  339. package/src/plugins/builtin/thirteenf/api.ts +116 -13
  340. package/src/plugins/builtin/thirteenf/data.ts +132 -30
  341. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  342. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  343. package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
  344. package/src/plugins/builtin/thirteenf/types.ts +12 -0
  345. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
  346. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  347. package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
  348. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  349. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  350. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  351. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  352. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  353. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  354. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  355. package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  357. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  358. package/src/plugins/builtin/volatility/index.tsx +1 -2
  359. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  360. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  361. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  362. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  363. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  364. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  365. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  366. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  367. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  368. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  369. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  370. package/src/plugins/bundle.ts +52 -2
  371. package/src/plugins/catalog-browser.ts +2 -2
  372. package/src/plugins/catalog-cli.ts +20 -0
  373. package/src/plugins/host-link.ts +13 -1
  374. package/src/plugins/installer.ts +15 -4
  375. package/src/plugins/loader.ts +43 -11
  376. package/src/plugins/peer-imports.ts +201 -0
  377. package/src/plugins/peer-retry.ts +26 -29
  378. package/src/plugins/plugin-process.ts +128 -0
  379. package/src/plugins/redirection-guard.ts +45 -0
  380. package/src/remote/endpoint.ts +16 -3
  381. package/src/renderers/browser/main.tsx +3 -2
  382. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  383. package/src/renderers/dom/desktop/icons.tsx +2 -0
  384. package/src/renderers/dom/host/image-surface.tsx +46 -21
  385. package/src/renderers/dom/styles.css +7 -0
  386. package/src/renderers/dom/toast-host.tsx +2 -0
  387. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  388. package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
  389. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  390. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  391. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  392. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  393. package/src/renderers/electrobun/bun/window/create.ts +4 -2
  394. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  395. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  396. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  397. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  398. package/src/renderers/electrobun/view/main.tsx +7 -2
  399. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  400. package/src/renderers/opentui/browser-open.ts +10 -4
  401. package/src/renderers/opentui/image/surface.tsx +1 -1
  402. package/src/renderers/opentui/start.tsx +12 -6
  403. package/src/renderers/opentui/toast-host.tsx +2 -1
  404. package/src/series/registry.ts +18 -0
  405. package/src/series/standalone.ts +10 -1
  406. package/src/series/types.ts +7 -0
  407. package/src/series/validate.ts +11 -2
  408. package/src/sources/gloomberb-cloud/index.ts +28 -5
  409. package/src/sources/gloomberb-cloud/news.ts +24 -3
  410. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  411. package/src/sources/provider-errors.ts +26 -3
  412. package/src/sources/provider-router/history.ts +5 -0
  413. package/src/sources/provider-router/index.ts +13 -0
  414. package/src/sync/core-contributors.ts +32 -0
  415. package/src/telemetry/attention-counts.ts +64 -6
  416. package/src/telemetry/attention-setting.ts +33 -0
  417. package/src/telemetry/crash-reports-node.ts +3 -1
  418. package/src/telemetry/stall-clocks.ts +93 -0
  419. package/src/telemetry/stall-reports-node.ts +12 -7
  420. package/src/telemetry/stall-reports.ts +34 -5
  421. package/src/test-support/preload.ts +9 -0
  422. package/src/tickers/company-name.ts +19 -0
  423. package/src/tickers/listing-fate.ts +14 -3
  424. package/src/tickers/saved-instrument.ts +17 -0
  425. package/src/tickers/search/home-listing.ts +13 -2
  426. package/src/tickers/search/index.ts +4 -1
  427. package/src/tickers/search/types.ts +6 -1
  428. package/src/time-series/bar-aggregation.ts +7 -1
  429. package/src/time-series/base-series.ts +3 -3
  430. package/src/time-series/chart-bounds.ts +1 -1
  431. package/src/time-series/chart-data.ts +5 -2
  432. package/src/time-series/chart-seed.ts +1 -1
  433. package/src/time-series/coin-session-close.ts +211 -0
  434. package/src/time-series/live-bars.ts +13 -2
  435. package/src/time-series/live-quotes.ts +1 -1
  436. package/src/time-series/official-close.ts +159 -0
  437. package/src/time-series/price-comparison.ts +55 -6
  438. package/src/time-series/resolve.ts +12 -3
  439. package/src/time-series/series-presentation.ts +1 -1
  440. package/src/time-series/studies/formula.ts +205 -0
  441. package/src/time-series/studies/indicators.ts +299 -0
  442. package/src/time-series/studies/leverage.ts +75 -0
  443. package/src/time-series/studies/pair.ts +138 -0
  444. package/src/time-series/studies/resolve.ts +333 -0
  445. package/src/time-series/studies/shared.ts +141 -0
  446. package/src/time-series/studies/units.ts +243 -0
  447. package/src/time-series/studies/vwap.ts +274 -0
  448. package/src/types/config.ts +18 -2
  449. package/src/types/data-provider.ts +23 -0
  450. package/src/types/financials.ts +22 -0
  451. package/src/types/headless.ts +42 -3
  452. package/src/types/plugin.ts +32 -3
  453. package/src/ui/host.tsx +11 -0
  454. package/src/ui/toast.tsx +26 -1
  455. package/src/utils/ascii-text.ts +140 -0
  456. package/src/utils/cli-output.ts +91 -18
  457. package/src/utils/concurrency.ts +24 -0
  458. package/src/utils/exchanges.ts +6 -0
  459. package/src/utils/external-url.ts +20 -7
  460. package/src/utils/futures-generic.ts +4 -4
  461. package/src/utils/http-proxy-upstream.ts +124 -0
  462. package/src/utils/main-thread-stall.ts +75 -3
  463. package/src/utils/perf-marks.ts +36 -2
  464. package/src/utils/private-files.ts +141 -0
  465. package/src/utils/public-address.ts +99 -0
  466. package/src/account/upgrade-personalization.ts +0 -57
  467. package/src/account/upgrade-tickers.ts +0 -124
  468. package/src/api-client/upgrade-experiment.ts +0 -91
  469. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  470. package/src/renderers/electrobun/launcher/preload.ts +0 -13
  471. package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
  472. package/src/time-series/studies.ts +0 -1628
@@ -1,5 +1,6 @@
1
- import { readFile, rm, writeFile, mkdir } from "fs/promises";
1
+ import { readFile, rename, rm } from "fs/promises";
2
2
  import { dirname, join } from "path";
3
+ import { makePrivateDir, writePrivateFile } from "../utils/private-files";
3
4
  import type { RemoteAppKind, RemoteEndpoint } from "./types";
4
5
 
5
6
  const DEFAULT_ENDPOINT_FILE = "remote-control.json";
@@ -25,9 +26,21 @@ export async function removeRemoteEndpointFiles(dataDir: string, endpoint: Remot
25
26
  ]);
26
27
  }
27
28
 
29
+ /**
30
+ * Readable by the user alone, an older copy replaced: it holds the token that
31
+ * controls the app. Written beside it and moved into place, so a client that
32
+ * finds the file never reads it half written.
33
+ */
28
34
  async function writeRemoteEndpoint(filePath: string, endpoint: RemoteEndpoint): Promise<void> {
29
- await mkdir(dirname(filePath), { recursive: true });
30
- await writeFile(filePath, JSON.stringify(endpoint, null, 2), { encoding: "utf-8", mode: 0o600 });
35
+ await makePrivateDir(dirname(filePath));
36
+ const temporary = `${filePath}.${process.pid}.tmp`;
37
+ try {
38
+ await writePrivateFile(temporary, JSON.stringify(endpoint, null, 2));
39
+ await rename(temporary, filePath);
40
+ } catch (error) {
41
+ await rm(temporary, { force: true });
42
+ throw error;
43
+ }
31
44
  }
32
45
 
33
46
  async function removeRemoteEndpointIfOwned(filePath: string, endpoint: RemoteEndpoint): Promise<void> {
@@ -31,7 +31,7 @@ import {
31
31
  readOrCreateBrowserInstallId,
32
32
  } from "../../telemetry/crash-reports-dom";
33
33
  import { currentTelemetryConfig } from "../../telemetry/live-config";
34
- import { attentionCountsEnabled, installAttentionCounter } from "../../telemetry/attention-counts";
34
+ import { installAttentionCounter } from "../../telemetry/attention-counts";
35
35
  import { installUsageCounter, recordRestoredFunctions, usageCountsEnabled } from "../../telemetry/usage-counts";
36
36
  import { installWindowUsageFlush } from "../../telemetry/usage-counts-dom";
37
37
  import type { AppConfig } from "../../types/config";
@@ -71,7 +71,7 @@ async function boot(): Promise<void> {
71
71
  getInstallId: () => readOrCreateBrowserInstallId(),
72
72
  officialPluginIds: loadOfficialPluginIds,
73
73
  });
74
- installAttentionCounter(() => !browserDoNotTrack() && attentionCountsEnabled(currentTelemetryConfig(loadedConfig)));
74
+ installAttentionCounter({ allowed: () => !browserDoNotTrack(), launchConfig: () => loadedConfig });
75
75
  // A document reload does not unmount React. Flush both config and session
76
76
  // timers while localStorage is still available, including background tabs.
77
77
  window.addEventListener("pagehide", () => { void flushPendingPersistence(); });
@@ -114,6 +114,7 @@ async function boot(): Promise<void> {
114
114
  plugins={getBrowserPlugins(externalPlugins)}
115
115
  desktopDeepLinkBridge={deepLinkBridge}
116
116
  updatesEnabled={false}
117
+ setupEnabled={false}
117
118
  />
118
119
  </DomHostProviders>
119
120
  </DomErrorBoundary>,
@@ -19,30 +19,12 @@ import {
19
19
  readRequestInit,
20
20
  toResponseHead,
21
21
  type HttpProxyResponseEnvelope,
22
- type ProxiedRequestInit,
23
22
  } from "../../utils/http-proxy-response";
24
23
  import { isProxiedHost, PROXY_ALLOWED_HOSTS } from "../../utils/plugin-proxy-hosts";
24
+ import { fetchCheckingRedirects, readBytesWithin, type ProxyUpstreamResult } from "../../utils/http-proxy-upstream";
25
25
  import type { ProxySessionGate } from "./proxy-session";
26
26
 
27
27
  const PROXY_METHODS = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"]);
28
- /** Set by the caller's browser or meaningful only to the hop it came from. */
29
- const STRIPPED_REQUEST_HEADERS = new Set([
30
- "connection",
31
- "content-length",
32
- "host",
33
- "keep-alive",
34
- "proxy-authorization",
35
- "te",
36
- "trailer",
37
- "transfer-encoding",
38
- "upgrade",
39
- ]);
40
- /** Dropped when a redirect leaves the origin the plugin set them for. */
41
- const CROSS_ORIGIN_STRIPPED_HEADERS = ["authorization", "cookie", "proxy-authorization"];
42
- /** Dropped when a redirect turns the request into a bodiless GET. */
43
- const REQUEST_BODY_HEADERS = ["content-encoding", "content-language", "content-location", "content-type"];
44
- const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
45
- const MAX_REDIRECTS = 5;
46
28
  const MAX_BODY_BYTES = 5 * 1024 * 1024;
47
29
  /** Plugin request bodies are small JSON; anything near this is not one. */
48
30
  const MAX_REQUEST_BYTES = 1024 * 1024;
@@ -88,112 +70,10 @@ export function validateProxyTarget(
88
70
  return { url };
89
71
  }
90
72
 
91
- function upstreamHeaders(raw: Record<string, string>): Headers {
92
- const headers = new Headers();
93
- for (const [name, value] of Object.entries(raw)) {
94
- if (STRIPPED_REQUEST_HEADERS.has(name.toLowerCase())) continue;
95
- try {
96
- headers.set(name, value);
97
- } catch {
98
- // A header name the runtime refuses is dropped rather than failing the
99
- // whole request, matching how a browser treats an unsettable header.
100
- }
101
- }
102
- return headers;
103
- }
104
-
105
73
  function proxyError(message: string, status: number, headers: Record<string, string> = {}): Response {
106
74
  return Response.json({ error: message }, { status, headers: { "cache-control": "no-store", ...headers } });
107
75
  }
108
76
 
109
- /**
110
- * Reads a body, giving up once it passes `maxBytes`. Returns null when it was
111
- * too large, after cancelling the stream so the rest is never pulled.
112
- */
113
- async function readBytesWithin(body: ReadableStream<Uint8Array> | null, maxBytes: number): Promise<Uint8Array | null> {
114
- if (!body) return new Uint8Array(0);
115
- const reader = body.getReader();
116
- const chunks: Uint8Array[] = [];
117
- let size = 0;
118
- for (;;) {
119
- const { done, value } = await reader.read();
120
- if (done) break;
121
- size += value.byteLength;
122
- if (size > maxBytes) {
123
- await reader.cancel().catch(() => {});
124
- return null;
125
- }
126
- chunks.push(value);
127
- }
128
- const bytes = new Uint8Array(size);
129
- let offset = 0;
130
- for (const chunk of chunks) {
131
- bytes.set(chunk, offset);
132
- offset += chunk.byteLength;
133
- }
134
- return bytes;
135
- }
136
-
137
- type UpstreamResult = { response: Response } | { error: string; status: number };
138
-
139
- /**
140
- * Follows redirects one hop at a time instead of letting `fetch` do it, so
141
- * every `Location` passes the same allowlist check as the first URL. An
142
- * allowlisted host that redirects elsewhere (an open redirect, a moved API)
143
- * would otherwise carry the request to any host on the internet.
144
- *
145
- * Method and header handling mirror the Fetch spec: 303, and 301/302 after a
146
- * POST, become a bodiless GET; credentials the plugin set are dropped when the
147
- * redirect leaves the origin they were meant for.
148
- */
149
- async function fetchWithinAllowlist(
150
- first: URL,
151
- init: ProxiedRequestInit,
152
- fetchUpstream: typeof fetch,
153
- hosts: readonly string[],
154
- signal: AbortSignal,
155
- ): Promise<UpstreamResult> {
156
- const headers = upstreamHeaders(init.headers);
157
- const mode = init.redirect ?? "follow";
158
- let url = first;
159
- let method = init.method;
160
- let body = init.body;
161
-
162
- for (let followed = 0; ; followed += 1) {
163
- const response = await fetchUpstream(url, { method, headers, body, redirect: "manual", signal });
164
- const location = REDIRECT_STATUSES.has(response.status) ? response.headers.get("location") : null;
165
- // "manual" hands the redirect back to the plugin. If it follows it, that
166
- // comes back through here as a new request and is checked like one.
167
- if (location === null || mode === "manual") return { response };
168
-
169
- await response.body?.cancel().catch(() => {});
170
- if (mode === "error") return { error: "The upstream response was a redirect.", status: 502 };
171
- if (followed >= MAX_REDIRECTS) return { error: "The upstream redirected too many times.", status: 502 };
172
-
173
- let next: string;
174
- try {
175
- next = new URL(location, url).href;
176
- } catch {
177
- return { error: "The upstream sent an invalid redirect.", status: 502 };
178
- }
179
- const target = validateProxyTarget(next, hosts);
180
- if ("error" in target) return { error: `The upstream redirect was refused. ${target.error}`, status: 502 };
181
-
182
- if (target.url.origin !== url.origin) {
183
- for (const name of CROSS_ORIGIN_STRIPPED_HEADERS) headers.delete(name);
184
- }
185
- if (
186
- (response.status === 303 && method !== "GET" && method !== "HEAD")
187
- || ((response.status === 301 || response.status === 302) && method === "POST")
188
- ) {
189
- method = "GET";
190
- body = undefined;
191
- for (const name of REQUEST_BODY_HEADERS) headers.delete(name);
192
- }
193
- url = target.url;
194
- }
195
- }
196
-
197
77
  function upstreamFailure(error: unknown): Response {
198
78
  const timedOut = error instanceof Error && error.name === "TimeoutError";
199
79
  return proxyError(timedOut ? "The upstream request timed out." : "The upstream request failed.", 504);
@@ -256,9 +136,13 @@ export async function handleHttpProxy(request: Request, options: HttpProxyOption
256
136
  }
257
137
 
258
138
  const signal = AbortSignal.timeout(Math.min(init.timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS));
259
- let result: UpstreamResult;
139
+ let result: ProxyUpstreamResult;
260
140
  try {
261
- result = await fetchWithinAllowlist(target.url, init, fetchUpstream, hosts, signal);
141
+ result = await fetchCheckingRedirects(target.url, init, {
142
+ fetch: fetchUpstream,
143
+ check: (url) => validateProxyTarget(url, hosts),
144
+ signal,
145
+ });
262
146
  } catch (error) {
263
147
  return upstreamFailure(error);
264
148
  }
@@ -225,6 +225,7 @@ export function WebIconButton({
225
225
  disabled = false,
226
226
  size = 12,
227
227
  stopPropagation = true,
228
+ text,
228
229
  }: IconButtonProps) {
229
230
  return (
230
231
  <button
@@ -261,6 +262,7 @@ export function WebIconButton({
261
262
  }}
262
263
  >
263
264
  <WebIcon name={icon} size={size} />
265
+ {text ? <span className="gloom-icon-button-text">{text}</span> : null}
264
266
  </button>
265
267
  );
266
268
  }
@@ -1,48 +1,73 @@
1
1
  /** @jsxImportSource react */
2
- import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
2
+ import { useState, type CSSProperties, type ReactNode } from "react";
3
3
  import type { ImageSurfaceProps } from "../../../ui/host";
4
4
  import { cleanDomProps, commonStyle } from "./style";
5
5
 
6
+ type ImageStatus = "loading" | "loaded" | "failed";
7
+
6
8
  /**
7
9
  * Images that failed to load this session, such as a company with no logo:
8
10
  * a row scrolled back into view draws its fallback at once rather than an
9
11
  * empty box while it asks again.
10
12
  */
11
13
  const failedImages = new Set<string>();
12
- const FAILED_IMAGES_KEPT = 2_000;
14
+ /** Images that loaded this session: a row scrolled back into view draws its picture on its first frame, not its stand-in. */
15
+ const loadedImages = new Set<string>();
16
+ const IMAGES_KEPT = 2_000;
17
+
18
+ function remember(images: Set<string>, src: string): void {
19
+ if (images.size >= IMAGES_KEPT) images.delete(images.values().next().value!);
20
+ images.add(src);
21
+ }
13
22
 
14
- function rememberFailedImage(src: string): void {
15
- if (failedImages.size >= FAILED_IMAGES_KEPT) failedImages.delete(failedImages.values().next().value!);
16
- failedImages.add(src);
23
+ function initialStatus(src: string): ImageStatus {
24
+ if (failedImages.has(src)) return "failed";
25
+ return loadedImages.has(src) ? "loaded" : "loading";
17
26
  }
18
27
 
19
- /** An image filling its box; `children` stand in without a `src`, or once it fails to load. */
20
- export function WebImageSurface({ children, src, alt = "", objectFit = "contain", ...props }: ImageSurfaceProps) {
28
+ /**
29
+ * An image still being fetched, kept out of the layout and the paint so the
30
+ * box shows its `children` until the picture can replace them whole.
31
+ */
32
+ const PENDING_IMAGE_STYLE: CSSProperties = { position: "absolute", width: 0, height: 0, opacity: 0, pointerEvents: "none" };
33
+
34
+ /** An image filling its box; `children` stand in without a `src`, or once it fails to load (and while it loads with `fallbackWhileLoading`). */
35
+ export function WebImageSurface({ children, src, alt = "", objectFit = "contain", fallbackWhileLoading = false, imageBackground, ...props }: ImageSurfaceProps) {
21
36
  const imageSrc = typeof src === "string" ? src.trim() : "";
22
- const [failed, setFailed] = useState(() => failedImages.has(imageSrc));
23
- useEffect(() => setFailed(failedImages.has(imageSrc)), [imageSrc]);
37
+ // Keyed by the address it describes, so a row that takes another logo starts that one's own load.
38
+ const [known, setKnown] = useState<{ src: string; status: ImageStatus }>(() => ({ src: imageSrc, status: initialStatus(imageSrc) }));
39
+ const status = known.src === imageSrc ? known.status : initialStatus(imageSrc);
24
40
  const baseStyle = commonStyle(props);
41
+ const waiting = fallbackWhileLoading && status === "loading";
25
42
  return (
26
43
  <div
27
44
  {...cleanDomProps(props)}
28
45
  style={{
29
46
  ...baseStyle,
30
47
  overflow: baseStyle.overflow ?? "hidden",
48
+ ...(imageBackground && status === "loaded" ? { background: imageBackground } : {}),
31
49
  ...(props.style as CSSProperties | undefined),
32
50
  }}
33
51
  >
34
- {imageSrc && !failed ? (
35
- <img
36
- key={imageSrc}
37
- src={imageSrc}
38
- alt={alt}
39
- draggable={false}
40
- onError={() => {
41
- rememberFailedImage(imageSrc);
42
- setFailed(true);
43
- }}
44
- style={{ width: "100%", height: "100%", display: "block", objectFit }}
45
- />
52
+ {imageSrc && status !== "failed" ? (
53
+ <>
54
+ <img
55
+ key={imageSrc}
56
+ src={imageSrc}
57
+ alt={alt}
58
+ draggable={false}
59
+ onLoad={() => {
60
+ remember(loadedImages, imageSrc);
61
+ setKnown({ src: imageSrc, status: "loaded" });
62
+ }}
63
+ onError={() => {
64
+ remember(failedImages, imageSrc);
65
+ setKnown({ src: imageSrc, status: "failed" });
66
+ }}
67
+ style={waiting ? PENDING_IMAGE_STYLE : { width: "100%", height: "100%", display: "block", objectFit }}
68
+ />
69
+ {waiting ? children as ReactNode : null}
70
+ </>
46
71
  ) : children as ReactNode}
47
72
  </div>
48
73
  );
@@ -1441,6 +1441,10 @@ html[data-gloom-screenshot="true"] [data-gloom-role="chart-watermark"] {
1441
1441
  box-shadow: inset -1px 0 0 var(--sg-hairline);
1442
1442
  }
1443
1443
  [data-gloom-role="stat-grid-cell"][data-wide="true"] { grid-column: 1 / -1; box-shadow: none; }
1444
+ /* A whole-row cell too narrow for its value and detail on one line: the label
1445
+ stays on the first line, the value and the detail run down beside it. */
1446
+ [data-gloom-role="stat-grid-cell"][data-wrapped="true"] { align-items: flex-start; }
1447
+ [data-gloom-role="stat-grid-cell"][data-wrapped="true"] [data-gloom-role="stat-grid-detail"] { flex: none; }
1444
1448
  /* Column grid text whose track can be narrower than it, and the grid itself: clipped
1445
1449
  without a scroll container where the engine has `overflow: clip`. A text box sets its
1446
1450
  own overflow inline, hence !important. The margin keeps the antialiased edge of a glyph
@@ -1497,6 +1501,9 @@ html[data-gloom-screenshot="true"] [data-gloom-role="chart-watermark"] {
1497
1501
  .gloom-icon-button:disabled, [data-gloom-role="pane-footer-icon"] { cursor: default; }
1498
1502
  .gloom-icon-button:not(:disabled):hover { background: var(--gloom-hover-fill); color: var(--gloom-text-bright); }
1499
1503
  .gloom-icon-button[aria-pressed="true"] { color: var(--gloom-warning); }
1504
+ /* A status's few characters after its icon (a delay's 15m): read without hovering, in the muted tone of the meta text around it. */
1505
+ .gloom-icon-button-text, [data-gloom-role="pane-footer-icon-text"] { margin-left: var(--gloom-space-xs); color: var(--gloom-text-muted); }
1506
+ .gloom-icon-button:not(:disabled):hover .gloom-icon-button-text { color: inherit; }
1500
1507
  .gloom-qb-view button:disabled { color: var(--gloom-text-muted); opacity: 0.55; cursor: default; }
1501
1508
  [data-gloom-role="field-grid-value"] button,
1502
1509
  [data-gloom-role="field-grid-value"] button > span { justify-content: flex-end !important; text-align: right; padding-inline: 0 !important; }
@@ -6,12 +6,14 @@ import {
6
6
  ToastHostProvider,
7
7
  type ToastHost,
8
8
  type ToastStore,
9
+ useToastsShown,
9
10
  } from "../../ui/toast";
10
11
  import { WebButton } from "./desktop/controls";
11
12
  import { WebIconButton } from "./desktop/icons";
12
13
 
13
14
  function ToastViewport({ store }: { store: ToastStore }) {
14
15
  const toasts = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
16
+ useToastsShown(toasts);
15
17
  const { dismiss } = store;
16
18
  const actionShortcut = useActionShortcut("notification-action");
17
19
  const dismissShortcut = useActionShortcut("notification-dismiss");
@@ -16,6 +16,7 @@ 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 { createStallClocks } from "../../../../telemetry/stall-clocks";
19
20
  import { installStallReporter, parseStallEvent, recordStall } from "../../../../telemetry/stall-reports";
20
21
  import { createNodeStallReporterHost } from "../../../../telemetry/stall-reports-node";
21
22
  import { installUpdateFailureReporter, reviewLastUpdateAttempt } from "../../../../telemetry/update-failures";
@@ -23,7 +24,7 @@ import { createNodeUpdateFailureHost } from "../../../../telemetry/update-failur
23
24
  import type { AppConfig } from "../../../../types/config";
24
25
  import { linkDesktopCommands } from "../../../../updater/command-names";
25
26
  import { debugLog } from "../../../../utils/debug-log";
26
- import { setStallSink, startMainThreadStallMonitor } from "../../../../utils/main-thread-stall";
27
+ import { setStallClocks, setStallSink, startMainThreadStallMonitor } from "../../../../utils/main-thread-stall";
27
28
  import type { DesktopSharedStateSnapshot } from "../../../../types/desktop-window";
28
29
  import type {
29
30
  ContextMenuSelectMessage,
@@ -45,6 +46,7 @@ import {
45
46
  takeExternalPluginBundles,
46
47
  } from "../external-plugins";
47
48
  import { MAIN_WINDOW_RPC_KEY } from "../window/focus";
49
+ import { measureNativeWindowCalls } from "../window/native-timing";
48
50
  import { handleDesktopBackendRequest } from "./backend-requests";
49
51
  import { detectDesktopInstallKind, removeUpdateAsides } from "./update";
50
52
  import { removeLegacyMacApp } from "./app-name";
@@ -58,6 +60,7 @@ import { applyMacosDockIcon } from "./macos-dock-icon";
58
60
  import { handleDesktopPluginStateRequest } from "./plugin-state";
59
61
  import { scheduleDesktopRelaunch } from "./relaunch";
60
62
  import { createDesktopRpcRegistry } from "./rpc-registry";
63
+ import { measureRpcTraffic } from "./rpc-timing";
61
64
  import { DesktopStateBroadcaster } from "./state-broadcaster";
62
65
  import { createDesktopWorkspace, type DesktopWorkspace } from "./workspace";
63
66
  import { handleDesktopWorkspaceRequest } from "./workspace/requests";
@@ -162,6 +165,8 @@ export class DesktopBackend {
162
165
  // Desktop has no Debug Log: freezes of this process and of its windows go to the stall log in the data folder.
163
166
  installStallReporter(createNodeStallReporterHost({ getDataDir: () => this.config?.dataDir ?? null }));
164
167
  setStallSink((event) => recordStall("app", event));
168
+ setStallClocks(createStallClocks());
169
+ measureNativeWindowCalls();
165
170
  startMainThreadStallMonitor();
166
171
  }
167
172
  // The native menu shows the same accelerators the keys use, so a rebind
@@ -273,6 +278,7 @@ export class DesktopBackend {
273
278
  },
274
279
  },
275
280
  });
281
+ measureRpcTraffic(rpc);
276
282
  this.rpcs.registerWindowRpc(key, rpc);
277
283
  return rpc;
278
284
  }
@@ -1,4 +1,5 @@
1
- import { existsSync, mkdirSync } from "fs";
1
+ import { existsSync } from "fs";
2
+ import { makePrivateDirSync } from "../../../../utils/private-files";
2
3
  import { homedir, hostname } from "os";
3
4
  import { getGloomberbDirs } from "../../../../data/config/home";
4
5
  import type { AppServices } from "../../../../core/app-services";
@@ -103,7 +104,7 @@ function buildInitializationPayload(
103
104
  async function resolveDesktopDataDir(): Promise<string> {
104
105
  const dataDir = await getDataDir() ?? getGloomberbDirs().data;
105
106
  if (!existsSync(dataDir)) {
106
- mkdirSync(dataDir, { recursive: true });
107
+ makePrivateDirSync(dataDir);
107
108
  }
108
109
  return dataDir;
109
110
  }
@@ -0,0 +1,88 @@
1
+ import { measurePerf } from "../../../../utils/perf-marks";
2
+
3
+ interface RpcPacket {
4
+ type?: unknown;
5
+ id?: unknown;
6
+ method?: unknown;
7
+ params?: unknown;
8
+ }
9
+
10
+ interface RpcTransport {
11
+ send?(packet: RpcPacket): void;
12
+ registerHandler?(handler: (packet: RpcPacket) => unknown): void;
13
+ unregisterHandler?(): void;
14
+ }
15
+
16
+ /** Answers waiting for their request's name; past this the oldest is forgotten. */
17
+ const MAX_OPEN_REQUESTS = 500;
18
+
19
+ /** A request or message name as a stall report keeps it: lower-case words, dotted. */
20
+ function sectionPart(name: unknown): string {
21
+ if (typeof name !== "string") return "unknown";
22
+ const part = name
23
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9.]+/g, "-")
26
+ .replace(/^[-.]+|[-.]+$/g, "");
27
+ return part || "unknown";
28
+ }
29
+
30
+ /** What a window asked for: the backend's own method inside the one Electrobun request. */
31
+ function requestName(packet: RpcPacket): string {
32
+ const params = packet.params as { method?: unknown } | null | undefined;
33
+ return sectionPart(packet.method === "backend.request" ? params?.method : packet.method);
34
+ }
35
+
36
+ /**
37
+ * Names every turn a window's channel takes on this process: the part of a
38
+ * request answered before its first wait (`rpc.request.<method>`), a message
39
+ * from the window (`rpc.message.<name>`), and the encoding, encryption and
40
+ * sending of an answer (`rpc.respond.<method>`) or a message to the window
41
+ * (`rpc.send.<name>`). A stall that follows one then says which, where the
42
+ * report would otherwise name whatever was last measured minutes before.
43
+ * Call before the channel's window is created: Electrobun hands it the
44
+ * transport then.
45
+ */
46
+ export function measureRpcTraffic(rpc: { setTransport(transport: RpcTransport): void }): void {
47
+ const setTransport = rpc.setTransport;
48
+ const openRequests = new Map<unknown, string>();
49
+
50
+ const incoming = (packet: RpcPacket): string => {
51
+ if (!packet || typeof packet !== "object") return "rpc.unknown";
52
+ if (packet.type === "request") {
53
+ const name = requestName(packet);
54
+ openRequests.set(packet.id, name);
55
+ if (openRequests.size > MAX_OPEN_REQUESTS) openRequests.delete(openRequests.keys().next().value);
56
+ return `rpc.request.${name}`;
57
+ }
58
+ if (packet.type === "message") return `rpc.message.${sectionPart(packet.id)}`;
59
+ return "rpc.response";
60
+ };
61
+
62
+ const outgoing = (packet: RpcPacket): string => {
63
+ if (!packet || typeof packet !== "object") return "rpc.unknown";
64
+ if (packet.type === "response") {
65
+ const name = openRequests.get(packet.id) ?? "unknown";
66
+ openRequests.delete(packet.id);
67
+ return `rpc.respond.${name}`;
68
+ }
69
+ if (packet.type === "message") return `rpc.send.${sectionPart(packet.id)}`;
70
+ return `rpc.ask.${sectionPart(packet.method)}`;
71
+ };
72
+
73
+ rpc.setTransport = (transport) => {
74
+ const { send, registerHandler } = transport;
75
+ setTransport.call(rpc, {
76
+ ...transport,
77
+ ...(send ? { send: (packet: RpcPacket) => measurePerf(outgoing(packet), () => send.call(transport, packet)) } : {}),
78
+ ...(registerHandler
79
+ ? {
80
+ registerHandler: (handler: (packet: RpcPacket) => unknown) => registerHandler.call(
81
+ transport,
82
+ (packet: RpcPacket) => measurePerf(incoming(packet), () => handler(packet)),
83
+ ),
84
+ }
85
+ : {}),
86
+ });
87
+ };
88
+ }
@@ -111,10 +111,20 @@ export async function removeUpdateAsides(
111
111
 
112
112
  const TAR_BLOCK = 512;
113
113
 
114
+ /**
115
+ * A macOS extended-attributes sidecar (`._name`), which an archive lists just
116
+ * before the file it describes.
117
+ */
118
+ function isAppleDoublePath(name: string): boolean {
119
+ return name.split("/").some((segment) => segment.startsWith("._"));
120
+ }
121
+
114
122
  /**
115
123
  * The first path in a tar, read from its leading headers: a ustar name and
116
- * prefix, or the long name a pax (`x`) or GNU (`L`) header gives it. Null when
117
- * `bytes` ends before one.
124
+ * prefix, or the long name a pax (`x`) or GNU (`L`) header gives it. A macOS
125
+ * extended-attributes sidecar (`._Gloomberb`) is skipped, so the path is the
126
+ * entry the archive carries, not the metadata beside it. Null when `bytes`
127
+ * ends before one.
118
128
  */
119
129
  export function firstTarPath(bytes: Uint8Array): string | null {
120
130
  const decoder = new TextDecoder();
@@ -136,8 +146,14 @@ export function firstTarPath(bytes: Uint8Array): string | null {
136
146
  header = data + Math.ceil(size / TAR_BLOCK) * TAR_BLOCK;
137
147
  continue;
138
148
  }
139
- if (longName) return longName;
140
- const name = field(header, 100);
149
+ const fromHeader = longName == null;
150
+ const name = longName ?? field(header, 100);
151
+ longName = null;
152
+ if (isAppleDoublePath(name)) {
153
+ header = data + Math.ceil(size / TAR_BLOCK) * TAR_BLOCK;
154
+ continue;
155
+ }
156
+ if (!fromHeader) return name;
141
157
  // POSIX ustar ("ustar\0") only: GNU tar keeps other fields where the prefix would be.
142
158
  const prefix = field(header + 257, 6) === "ustar" ? field(header + 345, 155) : "";
143
159
  return prefix ? `${prefix}/${name}` : name;
@@ -1,5 +1,6 @@
1
- import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
1
+ import { readFileSync, renameSync, rmSync } from "fs";
2
2
  import { join } from "path";
3
+ import { makePrivateDirSync, writePrivateFileSync } from "../../../../utils/private-files";
3
4
  import type { DesktopViewIds } from "../../shared/protocol";
4
5
 
5
6
  /**
@@ -37,13 +38,16 @@ export function readDesktopViewIds(dataDir: string): DesktopViewIds {
37
38
  }
38
39
  }
39
40
 
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
+ /**
42
+ * Replaces the copy with what the view sent, written whole or not at all and
43
+ * readable by the user alone. Throws when it cannot, so the view tries again.
44
+ */
41
45
  export function saveDesktopViewIds(dataDir: string, ids: unknown): void {
42
46
  const path = viewIdsPath(dataDir);
43
47
  const temporary = `${path}.${process.pid}.tmp`;
44
48
  try {
45
- mkdirSync(dataDir, { recursive: true });
46
- writeFileSync(temporary, JSON.stringify(parseViewIds(ids)));
49
+ makePrivateDirSync(dataDir);
50
+ writePrivateFileSync(temporary, JSON.stringify(parseViewIds(ids)));
47
51
  renameSync(temporary, path);
48
52
  } catch (error) {
49
53
  rmSync(temporary, { force: true });
@@ -110,9 +110,13 @@ async function bundlePluginDirectory(
110
110
  // production JSX runtime. A bundle built from a process without
111
111
  // NODE_ENV set targets jsx-dev-runtime instead and fails on first
112
112
  // render with "jsxDEV is not a function".
113
+ //
114
+ // The view has no `process`, so the bundler also gives a plugin that reads
115
+ // one a stand-in (see plugins/plugin-process.ts), named after the plugin.
113
116
  const outDir = pluginBundleCacheDir(getPluginCacheDir());
114
117
  const result = await bundleExternalPlugin(pluginDir, join(outDir, directory), {
115
118
  define: { "process.env.NODE_ENV": "\"production\"" },
119
+ pluginName: plugin.name || directory,
116
120
  });
117
121
  const code = await Bun.file(result.outputPath).text();
118
122
  bundleCache.set(pluginDir, { mtimeMs, code });
@@ -1,4 +1,5 @@
1
1
  import { BrowserWindow } from "electrobun/bun";
2
+ import { measurePerf } from "../../../../utils/perf-marks";
2
3
  import { trackWindowAttention } from "./attention";
3
4
  import {
4
5
  applyNativeWindowFrame,
@@ -84,7 +85,8 @@ interface AppWindowOptions {
84
85
  * its cached frame current as it moves, and holds it to `minSize` on resize.
85
86
  */
86
87
  export function createAppWindow({ title, frame, rpc, minSize, onFrameChange, preload }: AppWindowOptions): BrowserWindow {
87
- const window = new BrowserWindow({
88
+ // Opening a window waits on the native layer, the webview's creation included.
89
+ const window = measurePerf("native.window.create", () => new BrowserWindow({
88
90
  title,
89
91
  frame,
90
92
  url: APP_VIEW_URL,
@@ -95,7 +97,7 @@ export function createAppWindow({ title, frame, rpc, minSize, onFrameChange, pre
95
97
  titleBarStyle: desktopTitleBarStyle(),
96
98
  navigationRules: JSON.stringify(["views://*"]),
97
99
  sandbox: false,
98
- });
100
+ }));
99
101
  applyWindowsWindowIcon(title);
100
102
  applyWindowsCustomChrome(title);
101
103
  if (process.platform === "win32") {