gloomberb 0.19.1 → 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 (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -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 +58 -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/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -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
  }
@@ -180,7 +180,7 @@ const ICONS: Record<IconName, ReactNode> = {
180
180
  ),
181
181
  };
182
182
 
183
- export function WebIcon({ name, size = 12, color }: IconProps) {
183
+ export function WebIcon({ name, size = 12, color, fullHeight }: IconProps) {
184
184
  return (
185
185
  <svg
186
186
  viewBox="0 0 12 12"
@@ -189,7 +189,8 @@ export function WebIcon({ name, size = 12, color }: IconProps) {
189
189
  fill="none"
190
190
  aria-hidden="true"
191
191
  data-gloom-icon={name}
192
- style={{ flex: "none", display: "block", color }}
192
+ // The viewBox keeps its size and sits in the middle of the taller box (preserveAspectRatio's default).
193
+ style={{ flex: "none", display: "block", color, ...(fullHeight ? { height: "100%" } : {}) }}
193
194
  >
194
195
  {ICONS[name]}
195
196
  </svg>
@@ -224,6 +225,7 @@ export function WebIconButton({
224
225
  disabled = false,
225
226
  size = 12,
226
227
  stopPropagation = true,
228
+ text,
227
229
  }: IconButtonProps) {
228
230
  return (
229
231
  <button
@@ -260,6 +262,7 @@ export function WebIconButton({
260
262
  }}
261
263
  >
262
264
  <WebIcon name={icon} size={size} />
265
+ {text ? <span className="gloom-icon-button-text">{text}</span> : null}
263
266
  </button>
264
267
  );
265
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
  }
@@ -18,6 +18,8 @@ import type { DesktopBackend } from "./backend";
18
18
 
19
19
  const INITIAL_DOCK_SUPPRESSION_MS = 800;
20
20
  const WINDOW_CONTROL_DOCK_SUPPRESSION_MS = 5_000;
21
+ /** How long after the main window has its data the popped-out windows of the last session open. */
22
+ const LAUNCH_WINDOWS_DELAY_MS = 300;
21
23
 
22
24
  export class DesktopDetachedWindowManager {
23
25
  private readonly windows = new Map<string, BrowserWindow>();
@@ -27,9 +29,29 @@ export class DesktopDetachedWindowManager {
27
29
  private readonly pendingMoveFlush = new Set<string>();
28
30
  private readonly suppressDockUntil = new Map<string, number>();
29
31
  private readonly windowControlDockSuppressed = new Set<string>();
32
+ /**
33
+ * At launch, popped-out windows wait for the main window. Opening a window
34
+ * holds this process until the system has made it (on Windows, its
35
+ * browser too), and the main window's `init` used to wait behind each one.
36
+ */
37
+ private windowsAllowed = false;
38
+ private windowsTimer: ReturnType<typeof setTimeout> | null = null;
30
39
 
31
40
  constructor(private readonly backend: DesktopBackend) {}
32
41
 
42
+ /**
43
+ * The main window has its data: the popped-out windows open shortly
44
+ * after, once it has had a moment to draw and ask for what it shows.
45
+ */
46
+ openAfterMainWindow(): void {
47
+ if (this.windowsAllowed || this.windowsTimer) return;
48
+ this.windowsTimer = setTimeout(() => {
49
+ this.windowsTimer = null;
50
+ this.windowsAllowed = true;
51
+ this.reconcile();
52
+ }, LAUNCH_WINDOWS_DELAY_MS);
53
+ }
54
+
33
55
  focusDetachedPane(instanceId: string): boolean {
34
56
  return this.focusWindowForRpcKey(detachedRpcKey(instanceId));
35
57
  }
@@ -88,7 +110,7 @@ export class DesktopDetachedWindowManager {
88
110
  for (const [instanceId, entry] of desiredEntries) {
89
111
  const existingWindow = this.windows.get(instanceId);
90
112
  if (!existingWindow) {
91
- this.createWindow(instanceId, entry);
113
+ if (this.windowsAllowed) this.createWindow(instanceId, entry);
92
114
  continue;
93
115
  }
94
116
 
@@ -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";
@@ -25,6 +26,7 @@ import { telemetryOptedOut } from "../../../../telemetry/crash-reports";
25
26
  import { describeNodeOs, readOrCreateInstallId } from "../../../../telemetry/crash-reports-node";
26
27
  import type { DesktopBackend, DesktopRpc } from "./backend";
27
28
  import { readDesktopViewIds } from "./view-ids";
29
+ import { markStartupComplete } from "../../../../utils/main-thread-stall";
28
30
 
29
31
  interface DesktopWindowTarget {
30
32
  kind: "main" | "detached";
@@ -102,7 +104,7 @@ function buildInitializationPayload(
102
104
  async function resolveDesktopDataDir(): Promise<string> {
103
105
  const dataDir = await getDataDir() ?? getGloomberbDirs().data;
104
106
  if (!existsSync(dataDir)) {
105
- mkdirSync(dataDir, { recursive: true });
107
+ makePrivateDirSync(dataDir);
106
108
  }
107
109
  return dataDir;
108
110
  }
@@ -194,5 +196,12 @@ export async function initializeDesktopBackend(
194
196
  backend.workspace = createDesktopWorkspace(config, backend.getSessionSnapshot());
195
197
  backend.detachedWindows.reconcile();
196
198
  }
197
- return buildInitializationPayload(backend, config, services, windowTarget);
199
+ const init = buildInitializationPayload(backend, config, services, windowTarget);
200
+ // The main window has what it needs to draw: this process is up, and the
201
+ // popped-out windows open next.
202
+ if (windowTarget.kind === "main") {
203
+ markStartupComplete();
204
+ backend.detachedWindows.openAfterMainWindow();
205
+ }
206
+ return init;
198
207
  }
@@ -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 });