@gloom-sh/terminal 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,14 +1,13 @@
1
1
  /**
2
2
  * Scheme validation for links handed to the operating system.
3
3
  *
4
- * Opening a link natively means spawning `open` / `xdg-open` / `cmd /c start`
5
- * with a string that usually came from an external feed: RSS items, prediction
6
- * markets, news, filings, Substack. Two things make an unvalidated string
7
- * dangerous there — `file://` launches local content, and Windows routes the
8
- * argument through cmd's parser.
4
+ * Opening a link natively means spawning `open` / `xdg-open` / `rundll32` with
5
+ * a string that usually came from an external feed: RSS items, prediction
6
+ * markets, news, filings, Substack. `file://` and the other local schemes
7
+ * launch local content, so only http(s) goes through.
9
8
  *
10
- * Restricting to http(s) removes both. Returns the normalised URL so callers
11
- * spawn the parsed form rather than the raw input.
9
+ * Returns the normalised URL so callers spawn the parsed form rather than the
10
+ * raw input.
12
11
  */
13
12
  export function safeExternalUrl(value: string): string | null {
14
13
  if (!value.trim()) return null;
@@ -22,3 +21,17 @@ export function safeExternalUrl(value: string): string | null {
22
21
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
23
22
  return parsed.toString();
24
23
  }
24
+
25
+ /**
26
+ * The command that hands an http(s) link to the system's own opener, with the
27
+ * link as the last argument, or null for any other link. Windows opens it
28
+ * through `url.dll` rather than `cmd /c start`: `&`, `|`, `^` and `%` are part
29
+ * of a link, and a command line would read them as its own.
30
+ */
31
+ export function systemOpenCommand(rawUrl: string, platform: NodeJS.Platform): string[] | null {
32
+ const url = safeExternalUrl(rawUrl);
33
+ if (!url) return null;
34
+ if (platform === "darwin") return ["open", url];
35
+ if (platform === "win32") return ["rundll32.exe", "url.dll,FileProtocolHandler", url];
36
+ return ["xdg-open", url];
37
+ }
@@ -170,9 +170,9 @@ export function futuresGenericPriceBasis(generic: Pick<FuturesGeneric, "root">):
170
170
 
171
171
  /**
172
172
  * How a futures listing's price reads, whichever form it takes: a generic by
173
- * its root (VX1 points, TY1 32nds), and a continuous (`ES=F`, `SXO=F`) or
174
- * listed contract (`SXOZ26.CME`) of an index, sector or industry root in index
175
- * points, on the root's own venue or with none. The quote keeps its currency
173
+ * its root (VX1 points, TY1 32nds), and a continuous (`ES=F`, `ZN=F`) or
174
+ * listed contract (`ZNZ26.CBT`) in the same units as its root's generic,
175
+ * on the root's own venue or with none. The quote keeps its currency
176
176
  * for what a tick is worth. Null for every other listing, a security that
177
177
  * shares a root's code on another venue (XAU on Toronto, the XAR ETF) included.
178
178
  */
@@ -180,7 +180,7 @@ export function futuresListingPriceBasis(symbol: string, exchange?: string | nul
180
180
  const generic = futuresGenericListing(symbol, exchange);
181
181
  if (generic) return futuresGenericPriceBasis(generic);
182
182
  const listing = futuresListingVenue(symbol, exchange);
183
- return listing?.root && ROOT_VENUES[listing.root] === listing.venue && POINT_ROOTS.has(listing.root) ? "points" : null;
183
+ return listing?.root && ROOT_VENUES[listing.root] === listing.venue ? futuresGenericPriceBasis({ root: listing.root }) : null;
184
184
  }
185
185
 
186
186
  /** Whether a root's date rules count back from first notice or from last trade. */
@@ -0,0 +1,124 @@
1
+ import type { ProxiedRequestInit } from "./http-proxy-response";
2
+
3
+ /** Set by the caller's browser or meaningful only to the hop it came from. */
4
+ const STRIPPED_REQUEST_HEADERS = new Set([
5
+ "connection",
6
+ "content-length",
7
+ "host",
8
+ "keep-alive",
9
+ "proxy-authorization",
10
+ "te",
11
+ "trailer",
12
+ "transfer-encoding",
13
+ "upgrade",
14
+ ]);
15
+ /** Dropped when a redirect leaves the origin the plugin set them for. */
16
+ const CROSS_ORIGIN_STRIPPED_HEADERS = ["authorization", "cookie", "proxy-authorization"];
17
+ /** Dropped when a redirect turns the request into a bodiless GET. */
18
+ const REQUEST_BODY_HEADERS = ["content-encoding", "content-language", "content-location", "content-type"];
19
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
20
+ const MAX_REDIRECTS = 5;
21
+
22
+ /** Whether a proxy may reach a URL: the URL to fetch, or why not and the status to answer with. */
23
+ export type ProxyTargetCheck = (url: string) => ProxyTargetVerdict | Promise<ProxyTargetVerdict>;
24
+ type ProxyTargetVerdict = { url: URL } | { error: string; status: number };
25
+
26
+ export type ProxyUpstreamResult = { response: Response } | { error: string; status: number };
27
+
28
+ function upstreamHeaders(raw: Record<string, string>): Headers {
29
+ const headers = new Headers();
30
+ for (const [name, value] of Object.entries(raw)) {
31
+ if (STRIPPED_REQUEST_HEADERS.has(name.toLowerCase())) continue;
32
+ try {
33
+ headers.set(name, value);
34
+ } catch {
35
+ // Intentional ignore: a header name the runtime refuses is dropped, as a browser drops an unsettable header.
36
+ }
37
+ }
38
+ return headers;
39
+ }
40
+
41
+ /**
42
+ * Fetches a proxied plugin request, following redirects one hop at a time
43
+ * instead of letting `fetch` do it, so every `Location` passes the same
44
+ * `check` as the first URL. A host that redirects elsewhere (an open
45
+ * redirect, a moved API) would otherwise carry the request anywhere.
46
+ *
47
+ * Method and header handling mirror the Fetch spec: 303, and 301/302 after a
48
+ * POST, become a bodiless GET; credentials the plugin set are dropped when the
49
+ * redirect leaves the origin they were meant for.
50
+ */
51
+ export async function fetchCheckingRedirects(
52
+ first: URL,
53
+ init: ProxiedRequestInit,
54
+ options: { fetch: typeof fetch; check: ProxyTargetCheck; signal: AbortSignal },
55
+ ): Promise<ProxyUpstreamResult> {
56
+ const headers = upstreamHeaders(init.headers);
57
+ const mode = init.redirect ?? "follow";
58
+ let url = first;
59
+ let method = init.method;
60
+ let body = init.body;
61
+
62
+ for (let followed = 0; ; followed += 1) {
63
+ const response = await options.fetch(url, { method, headers, body, redirect: "manual", signal: options.signal });
64
+ const location = REDIRECT_STATUSES.has(response.status) ? response.headers.get("location") : null;
65
+ // "manual" hands the redirect back to the plugin. If it follows it, that
66
+ // comes back through here as a new request and is checked like one.
67
+ if (location === null || mode === "manual") return { response };
68
+
69
+ await response.body?.cancel().catch(() => { /* Intentional ignore: the redirect's own body is never read, only dropped. */ });
70
+ if (mode === "error") return { error: "The upstream response was a redirect.", status: 502 };
71
+ if (followed >= MAX_REDIRECTS) return { error: "The upstream redirected too many times.", status: 502 };
72
+
73
+ let next: string;
74
+ try {
75
+ next = new URL(location, url).href;
76
+ } catch {
77
+ // Intentional ignore: a Location that does not parse is reported as an invalid redirect, never followed.
78
+ return { error: "The upstream sent an invalid redirect.", status: 502 };
79
+ }
80
+ const target = await options.check(next);
81
+ if ("error" in target) return { error: `The upstream redirect was refused. ${target.error}`, status: 502 };
82
+
83
+ if (target.url.origin !== url.origin) {
84
+ for (const name of CROSS_ORIGIN_STRIPPED_HEADERS) headers.delete(name);
85
+ }
86
+ if (
87
+ (response.status === 303 && method !== "GET" && method !== "HEAD")
88
+ || ((response.status === 301 || response.status === 302) && method === "POST")
89
+ ) {
90
+ method = "GET";
91
+ body = undefined;
92
+ for (const name of REQUEST_BODY_HEADERS) headers.delete(name);
93
+ }
94
+ url = target.url;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Reads a body, giving up once it passes `maxBytes`. Returns null when it was
100
+ * too large, after cancelling the stream so the rest is never pulled.
101
+ */
102
+ export async function readBytesWithin(body: ReadableStream<Uint8Array> | null, maxBytes: number): Promise<Uint8Array | null> {
103
+ if (!body) return new Uint8Array(0);
104
+ const reader = body.getReader();
105
+ const chunks: Uint8Array[] = [];
106
+ let size = 0;
107
+ for (;;) {
108
+ const { done, value } = await reader.read();
109
+ if (done) break;
110
+ size += value.byteLength;
111
+ if (size > maxBytes) {
112
+ await reader.cancel().catch(() => { /* Intentional ignore: the rest of a body past the cap is dropped unread. */ });
113
+ return null;
114
+ }
115
+ chunks.push(value);
116
+ }
117
+ const bytes = new Uint8Array(size);
118
+ let offset = 0;
119
+ for (const chunk of chunks) {
120
+ bytes.set(chunk, offset);
121
+ offset += chunk.byteLength;
122
+ }
123
+ return bytes;
124
+ }
@@ -1,4 +1,4 @@
1
- import { lastPerfSample, recordPerfSample } from "./perf-marks";
1
+ import { lastPerfSample, recordPerfSample, withoutLastStep } from "./perf-marks";
2
2
 
3
3
  /** How often the monitor checks in. Short enough to date a stall precisely. */
4
4
  const SAMPLE_INTERVAL_MS = 250;
@@ -18,7 +18,21 @@ interface StallSqliteDetail {
18
18
  lockWait: boolean;
19
19
  }
20
20
 
21
+ /**
22
+ * What a platform's own clocks say at one instant (telemetry/stall-clocks.ts),
23
+ * in ms from any origin. Null where the platform cannot say.
24
+ */
25
+ export interface StallClockReading {
26
+ /** Time the computer was awake: sleep and suspend do not count. */
27
+ awakeMs: number | null;
28
+ /** CPU time this thread has used. */
29
+ threadCpuMs: number | null;
30
+ /** CPU time the whole process has used, every thread together. */
31
+ processCpuMs: number | null;
32
+ }
33
+
21
34
  export interface StallEvent {
35
+ /** How long the thread could not run while the computer was awake. */
22
36
  blockedMs: number;
23
37
  /** The last measured section before the stall (perf-marks), if any. */
24
38
  lastMeasured?: string;
@@ -30,9 +44,18 @@ export interface StallEvent {
30
44
  uptimeMs?: number;
31
45
  /** Whether the process had finished starting (`markStartupComplete`). */
32
46
  phase?: "startup" | "steady";
47
+ /**
48
+ * Share of the stall the thread spent on the CPU, 0 to 1: near 1 it was
49
+ * busy (script, a collection), near 0 it waited (the disk, a lock, a
50
+ * native call answered on another thread).
51
+ */
52
+ threadCpu?: number;
53
+ /** CPU the process's other threads used meanwhile, in cores (1: one core throughout). */
54
+ otherCpu?: number;
33
55
  }
34
56
 
35
57
  let stallSink: ((event: StallEvent) => void) | null = null;
58
+ let stallClocks: (() => StallClockReading) | null = null;
36
59
  let startupComplete = false;
37
60
 
38
61
  /**
@@ -47,6 +70,32 @@ export function setStallSink(next: ((event: StallEvent) => void) | null): () =>
47
70
  };
48
71
  }
49
72
 
73
+ /**
74
+ * How this process reads its platform's clocks (telemetry/stall-clocks.ts):
75
+ * a stall then says whether the thread was busy or waiting, and the time
76
+ * the computer slept is not counted as one.
77
+ */
78
+ export function setStallClocks(next: (() => StallClockReading) | null): () => void {
79
+ stallClocks = next;
80
+ return () => {
81
+ if (stallClocks === next) stallClocks = null;
82
+ };
83
+ }
84
+
85
+ function readStallClocks(read: (() => StallClockReading) | null): StallClockReading | null {
86
+ if (!read) return null;
87
+ try {
88
+ return read();
89
+ } catch {
90
+ // Intentional ignore: a clock that cannot be read leaves the stall's cause unknown, never the stall unseen.
91
+ return null;
92
+ }
93
+ }
94
+
95
+ function delta(after: number | null | undefined, before: number | null | undefined): number | null {
96
+ return after == null || before == null ? null : after - before;
97
+ }
98
+
50
99
  /**
51
100
  * The process is up: its first screen has its data, or (the desktop app's
52
101
  * Bun process) the main window has what it asked for. Stalls before this are
@@ -63,7 +112,7 @@ export function resetStartupCompleteForTests(): void {
63
112
 
64
113
  function noteStall(event: StallEvent): void {
65
114
  try {
66
- stallSink?.(event);
115
+ withoutLastStep(() => stallSink?.(event));
67
116
  } catch {
68
117
  // Intentional ignore: reporting a freeze must never become one.
69
118
  }
@@ -84,6 +133,8 @@ export interface MainThreadStallMonitorOptions {
84
133
  schedule?: (callback: () => void, delayMs: number) => unknown;
85
134
  cancel?: (handle: unknown) => void;
86
135
  report?: (blockedMs: number, context: Omit<StallEvent, "blockedMs">) => void;
136
+ /** Defaults to the clocks `setStallClocks` installed. */
137
+ clocks?: () => StallClockReading;
87
138
  }
88
139
 
89
140
  function defaultNow(): number {
@@ -101,6 +152,12 @@ function defaultNow(): number {
101
152
  * did it" in a report that only says the app hung for a few seconds, and how
102
153
  * long the process had been up, so a slow start reads apart from a hang
103
154
  * hours in.
155
+ *
156
+ * Where the platform's clocks can be read, the time the computer slept is
157
+ * taken out (a timer cannot fire then either, and a laptop's lid is not a
158
+ * freeze), and the stall says how much of it the thread spent on the CPU
159
+ * and how busy the process's other threads were: busy is script or a
160
+ * collection, idle on both is a wait on the disk or another program.
104
161
  */
105
162
  export function startMainThreadStallMonitor(
106
163
  options: MainThreadStallMonitorOptions = {},
@@ -117,14 +174,25 @@ export function startMainThreadStallMonitor(
117
174
  noteStall({ blockedMs, ...context });
118
175
  });
119
176
 
177
+ const clocks = () => readStallClocks(options.clocks ?? stallClocks);
120
178
  let previous = now();
179
+ let previousClocks = clocks();
121
180
  const handle = schedule(() => {
122
181
  const current = now();
123
- const blockedMs = current - previous - intervalMs;
182
+ const currentClocks = clocks();
183
+ const elapsedMs = current - previous;
184
+ const before = previousClocks;
124
185
  previous = current;
186
+ previousClocks = currentClocks;
187
+ const awakeMs = delta(currentClocks?.awakeMs, before?.awakeMs);
188
+ // Asleep, the computer runs nothing; only the awake part of the gap was a freeze.
189
+ const asleepMs = awakeMs == null ? 0 : Math.max(0, elapsedMs - awakeMs);
190
+ const blockedMs = elapsedMs - asleepMs - intervalMs;
125
191
  if (blockedMs < thresholdMs) return;
126
192
  const sample = lastPerfSample();
127
193
  const sqlite = sample?.name === "sqlite.blocked" ? sqliteDetail(sample.metadata) : undefined;
194
+ const threadCpuMs = delta(currentClocks?.threadCpuMs, before?.threadCpuMs);
195
+ const processCpuMs = delta(currentClocks?.processCpuMs, before?.processCpuMs);
128
196
  report(blockedMs, {
129
197
  ...(sample
130
198
  ? { lastMeasured: sample.name, lastMeasuredEndedMsAgo: Math.round(current - sample.endedAt) }
@@ -132,6 +200,10 @@ export function startMainThreadStallMonitor(
132
200
  ...(sqlite ? { sqlite } : {}),
133
201
  uptimeMs: Math.round(current),
134
202
  phase: startupComplete ? "steady" : "startup",
203
+ ...(threadCpuMs == null ? {} : { threadCpu: Math.min(1, Math.max(0, threadCpuMs / blockedMs)) }),
204
+ ...(threadCpuMs == null || processCpuMs == null
205
+ ? {}
206
+ : { otherCpu: Math.max(0, (processCpuMs - threadCpuMs) / (elapsedMs - asleepMs)) }),
135
207
  });
136
208
  }, intervalMs);
137
209
  if (handle && typeof (handle as { unref?: () => void }).unref === "function") {
@@ -25,7 +25,10 @@ function now(): number {
25
25
  // object, kept by reference.
26
26
  let lastSampleName: string | null = null;
27
27
  let lastSampleEndedAt = 0;
28
+ let lastSampleDurationMs = 0;
28
29
  let lastSampleMetadata: Record<string, unknown> | undefined;
30
+ /** Inside `withoutLastStep`: samples are logged, and none becomes the last step. */
31
+ let quietDepth = 0;
29
32
 
30
33
  /**
31
34
  * The most recent measured section, when it ended and what it was measured
@@ -37,15 +40,28 @@ export function lastPerfSample(): { name: string; endedAt: number; metadata?: Re
37
40
  return { name: lastSampleName, endedAt: lastSampleEndedAt, ...(lastSampleMetadata ? { metadata: lastSampleMetadata } : {}) };
38
41
  }
39
42
 
43
+ /**
44
+ * Whether the last step ended inside a section that started at `startedAt`
45
+ * and took at least half of its `durationMs`. That step says more about the
46
+ * time than the section around it: a SQLite statement that held the thread
47
+ * through most of a window's request keeps its lock and try facts, where a
48
+ * short one before the request's real work does not.
49
+ */
50
+ function lastStepAccountsForSection(startedAt: number, durationMs: number): boolean {
51
+ return lastSampleName != null && lastSampleEndedAt >= startedAt && lastSampleDurationMs * 2 >= durationMs;
52
+ }
53
+
40
54
  function logSlowPerfSample(
41
55
  name: string,
42
56
  durationMs: number,
43
57
  metadata?: Record<string, unknown>,
44
58
  lastStep = true,
59
+ startedAt?: number,
45
60
  ): void {
46
- if (lastStep) {
61
+ if (lastStep && quietDepth === 0 && (startedAt === undefined || !lastStepAccountsForSection(startedAt, durationMs))) {
47
62
  lastSampleName = name;
48
63
  lastSampleEndedAt = now();
64
+ lastSampleDurationMs = durationMs;
49
65
  lastSampleMetadata = metadata;
50
66
  }
51
67
  const payload = {
@@ -63,6 +79,20 @@ function logSlowPerfSample(
63
79
  }
64
80
  }
65
81
 
82
+ /**
83
+ * Runs `fn` without any section it measures becoming the step a later stall
84
+ * names: reporting a stall reads the windows, and the next stall must still
85
+ * name the work before it, not the report.
86
+ */
87
+ export function withoutLastStep<T>(fn: () => T): T {
88
+ quietDepth += 1;
89
+ try {
90
+ return fn();
91
+ } finally {
92
+ quietDepth -= 1;
93
+ }
94
+ }
95
+
66
96
  export function isPerfTraceEnabled(): boolean {
67
97
  return traceToStderr;
68
98
  }
@@ -81,6 +111,10 @@ export function recordPerfSample(
81
111
  logSlowPerfSample(name, durationMs, metadata, options?.lastStep !== false);
82
112
  }
83
113
 
114
+ /**
115
+ * Measures `fn`. A step measured inside it that took most of its time stays
116
+ * the step a stall names (`lastStepAccountsForSection`).
117
+ */
84
118
  export function measurePerf<T>(
85
119
  name: string,
86
120
  fn: () => T,
@@ -90,7 +124,7 @@ export function measurePerf<T>(
90
124
  try {
91
125
  return fn();
92
126
  } finally {
93
- logSlowPerfSample(name, now() - start, metadata);
127
+ logSlowPerfSample(name, now() - start, metadata, true, start);
94
128
  }
95
129
  }
96
130
 
@@ -0,0 +1,141 @@
1
+ import { chmodSync, closeSync, constants, fchmodSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, writeSync } from "fs";
2
+ import { chmod, lstat, mkdir, open } from "fs/promises";
3
+
4
+ /**
5
+ * The profile keeps broker credentials (config.json), the Cloud session (the
6
+ * database), a remote-control token and logs of what the user looked at. On
7
+ * POSIX its folders are opened only by the user and its files read only by
8
+ * them, whatever the umask. Windows keeps the profile under the user's own
9
+ * folder, whose access list already does this, and has no such modes: there
10
+ * every call here only creates and writes.
11
+ */
12
+ const PRIVATE_DIR_MODE = 0o700;
13
+ const PRIVATE_FILE_MODE = 0o600;
14
+
15
+ function usesPosixModes(): boolean {
16
+ return process.platform !== "win32";
17
+ }
18
+
19
+ function errorCode(error: unknown): unknown {
20
+ return (error as { code?: unknown } | null)?.code;
21
+ }
22
+
23
+ /** Creates `path` and the folders above it as `mkdir -p` does, each one it creates open to the user alone. */
24
+ export function makePrivateDirSync(path: string): void {
25
+ mkdirSync(path, { recursive: true, mode: PRIVATE_DIR_MODE });
26
+ }
27
+
28
+ export async function makePrivateDir(path: string): Promise<void> {
29
+ await mkdir(path, { recursive: true, mode: PRIVATE_DIR_MODE });
30
+ }
31
+
32
+ /** Leaves an existing folder open to the user alone; a missing one stays missing. */
33
+ export async function restrictDir(path: string): Promise<void> {
34
+ if (!usesPosixModes()) return;
35
+ try {
36
+ await chmod(path, PRIVATE_DIR_MODE);
37
+ } catch (error) {
38
+ if (errorCode(error) !== "ENOENT") throw error;
39
+ }
40
+ }
41
+
42
+ /** Leaves an existing file readable by the user alone; a missing one stays missing. */
43
+ export async function restrictFile(path: string): Promise<void> {
44
+ if (!usesPosixModes()) return;
45
+ try {
46
+ await chmod(path, PRIVATE_FILE_MODE);
47
+ } catch (error) {
48
+ if (errorCode(error) !== "ENOENT") throw error;
49
+ }
50
+ }
51
+
52
+ export function restrictFileSync(path: string): void {
53
+ if (!usesPosixModes()) return;
54
+ try {
55
+ chmodSync(path, PRIVATE_FILE_MODE);
56
+ } catch (error) {
57
+ if (errorCode(error) !== "ENOENT") throw error;
58
+ }
59
+ }
60
+
61
+ /** The code of the error a private write throws when its path names a device, a pipe or anything else but a plain file. */
62
+ export const NOT_A_PLAIN_FILE = "ENOTPLAINFILE";
63
+
64
+ function notAPlainFile(path: string): Error {
65
+ return Object.assign(new Error(`${path} is not a plain file.`), { code: NOT_A_PLAIN_FILE });
66
+ }
67
+
68
+ function isALink(path: string): Error {
69
+ return Object.assign(new Error(`${path} is a link.`), { code: "ELOOP" });
70
+ }
71
+
72
+ /**
73
+ * Opened to write without following a link (`O_NOFOLLOW`, which fails with
74
+ * ELOOP) or waiting on a pipe no one reads (`O_NONBLOCK`). Windows has
75
+ * neither flag; a link is refused there by looking first.
76
+ */
77
+ function privateOpenFlags(append: boolean): number {
78
+ return constants.O_WRONLY | constants.O_CREAT | (append ? constants.O_APPEND : 0)
79
+ | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
80
+ }
81
+
82
+ function refuseWindowsLinkSync(path: string): void {
83
+ if (usesPosixModes()) return;
84
+ try {
85
+ if (lstatSync(path).isSymbolicLink()) throw isALink(path);
86
+ } catch (error) {
87
+ if (errorCode(error) !== "ENOENT") throw error;
88
+ }
89
+ }
90
+
91
+ async function refuseWindowsLink(path: string): Promise<void> {
92
+ if (usesPosixModes()) return;
93
+ try {
94
+ if ((await lstat(path)).isSymbolicLink()) throw isALink(path);
95
+ } catch (error) {
96
+ if (errorCode(error) !== "ENOENT") throw error;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Writes `contents` to a plain file only the user can read: created so, and
102
+ * an existing file made so before anything is written to it, so the new
103
+ * contents are never readable by anyone else. A link, a device or a pipe in
104
+ * its place is refused, never written through. `append` adds to the end.
105
+ */
106
+ export function writePrivateFileSync(path: string, contents: string | Uint8Array, options: { append?: boolean } = {}): void {
107
+ refuseWindowsLinkSync(path);
108
+ const fd = openSync(path, privateOpenFlags(options.append === true), PRIVATE_FILE_MODE);
109
+ try {
110
+ if (!fstatSync(fd).isFile()) throw notAPlainFile(path);
111
+ if (usesPosixModes()) {
112
+ try {
113
+ fchmodSync(fd, PRIVATE_FILE_MODE);
114
+ } catch {
115
+ // Intentional ignore: a disk without POSIX modes (FAT, some network shares) cannot restrict the file; the write still goes through.
116
+ }
117
+ }
118
+ if (!options.append) ftruncateSync(fd, 0);
119
+ const bytes = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
120
+ for (let written = 0; written < bytes.length;) written += writeSync(fd, bytes, written);
121
+ } finally {
122
+ closeSync(fd);
123
+ }
124
+ }
125
+
126
+ export async function writePrivateFile(path: string, contents: string | Uint8Array): Promise<void> {
127
+ await refuseWindowsLink(path);
128
+ const handle = await open(path, privateOpenFlags(false), PRIVATE_FILE_MODE);
129
+ try {
130
+ if (!(await handle.stat()).isFile()) throw notAPlainFile(path);
131
+ if (usesPosixModes()) {
132
+ await handle.chmod(PRIVATE_FILE_MODE).catch(() => {
133
+ // Intentional ignore: a disk without POSIX modes (FAT, some network shares) cannot restrict the file; the write still goes through.
134
+ });
135
+ }
136
+ await handle.truncate(0);
137
+ await handle.writeFile(contents);
138
+ } finally {
139
+ await handle.close();
140
+ }
141
+ }
@@ -0,0 +1,99 @@
1
+ import { isIP } from "net";
2
+
3
+ /**
4
+ * Whether an IP address is on the public internet: not this machine, not a
5
+ * private or link-local network, not a carrier-grade NAT (a VPN's own
6
+ * addresses), documentation, multicast or reserved block. A request made for
7
+ * a page that only reads public data (the pane screenshot's HTTP bridge) is
8
+ * refused for anything else, so a URL it is handed cannot reach a service on
9
+ * the user's machine or network.
10
+ */
11
+ export function isPublicAddress(address: string): boolean {
12
+ const bare = address.replace(/^\[|\]$/g, "").replace(/%.*$/, "");
13
+ const family = isIP(bare);
14
+ if (family === 4) return isPublicIpv4(parseIpv4(bare)!);
15
+ if (family === 6) {
16
+ const groups = parseIpv6(bare);
17
+ return groups !== null && isPublicIpv6(groups);
18
+ }
19
+ return false;
20
+ }
21
+
22
+ function parseIpv4(address: string): number[] | null {
23
+ const parts = address.split(".").map(Number);
24
+ return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) ? parts : null;
25
+ }
26
+
27
+ /** [first octet, second octet, prefix bits of the second octet that count] per non-public block. */
28
+ const IPV4_BLOCKS: ReadonlyArray<readonly [number, number, number]> = [
29
+ [0, 0, 0], // 0.0.0.0/8, this network
30
+ [10, 0, 0], // 10.0.0.0/8, private
31
+ [100, 64, 2], // 100.64.0.0/10, carrier-grade NAT
32
+ [127, 0, 0], // 127.0.0.0/8, loopback
33
+ [169, 254, 8], // 169.254.0.0/16, link-local
34
+ [172, 16, 4], // 172.16.0.0/12, private
35
+ [192, 168, 8], // 192.168.0.0/16, private
36
+ // 198.18.0.0/15 (benchmarking) stays public: proxies that answer every name with a stand-in address use it.
37
+ ];
38
+
39
+ function isPublicIpv4([a, b, c]: number[]): boolean {
40
+ for (const [first, second, bits] of IPV4_BLOCKS) {
41
+ if (a !== first) continue;
42
+ if (bits === 0) return false;
43
+ const mask = (0xff << (8 - bits)) & 0xff;
44
+ if ((b! & mask) === second) return false;
45
+ }
46
+ // 192.0.0.0/24 protocol assignments, 192.0.2.0/24, 198.51.100.0/24 and 203.0.113.0/24 documentation.
47
+ if (a === 192 && b === 0 && (c === 0 || c === 2)) return false;
48
+ if (a === 198 && b === 51 && c === 100) return false;
49
+ if (a === 203 && b === 0 && c === 113) return false;
50
+ // 224.0.0.0/4 multicast, 240.0.0.0/4 reserved and the broadcast address.
51
+ return a! < 224;
52
+ }
53
+
54
+ /** Eight 16-bit groups, or null when the text is not one IPv6 address. */
55
+ function parseIpv6(address: string): number[] | null {
56
+ let text = address.toLowerCase();
57
+ const tail: number[] = [];
58
+ const dotted = /:(\d+\.\d+\.\d+\.\d+)$/.exec(text);
59
+ if (dotted) {
60
+ const v4 = parseIpv4(dotted[1]!);
61
+ if (!v4) return null;
62
+ tail.push((v4[0]! << 8) | v4[1]!, (v4[2]! << 8) | v4[3]!);
63
+ // `::1.2.3.4` keeps its `::`; `1:2:3:4:5:6:1.2.3.4` loses the colon before the dotted part.
64
+ const before = text.slice(0, dotted.index);
65
+ text = before.endsWith(":") ? `${before}:` : before;
66
+ }
67
+ const halves = text.split("::");
68
+ if (halves.length > 2) return null;
69
+ const read = (half: string) => (half ? half.split(":").map((group) => Number.parseInt(group, 16)) : []);
70
+ const head = read(halves[0]!);
71
+ const rest = halves.length === 2 ? read(halves[1]!) : [];
72
+ const known = head.length + rest.length + tail.length;
73
+ if (halves.length === 1 && known !== 8) return null;
74
+ if (halves.length === 2 && known > 7) return null;
75
+ const groups = [...head, ...Array(8 - known).fill(0), ...rest, ...tail];
76
+ return groups.every((group) => Number.isInteger(group) && group >= 0 && group <= 0xffff) ? groups : null;
77
+ }
78
+
79
+ function embeddedIpv4(high: number, low: number): number[] {
80
+ return [high >> 8, high & 0xff, low >> 8, low & 0xff];
81
+ }
82
+
83
+ function isPublicIpv6(groups: number[]): boolean {
84
+ const [g0, g1, g2, g3, g4, g5, g6, g7] = groups as [number, number, number, number, number, number, number, number];
85
+ // ::ffff:a.b.c.d, an IPv4 address written as IPv6: as public as the address it carries.
86
+ if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0xffff) return isPublicIpv4(embeddedIpv4(g6, g7));
87
+ // ::, ::1 and the retired IPv4-compatible block.
88
+ if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0) return false;
89
+ // 64:ff9b::/96 NAT64 carries an IPv4 address; 64:ff9b:1::/48 is a local NAT64.
90
+ if (g0 === 0x64 && g1 === 0xff9b) return g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0 && isPublicIpv4(embeddedIpv4(g6, g7));
91
+ // 2002::/16 6to4 carries an IPv4 address in its next 32 bits.
92
+ if (g0 === 0x2002) return isPublicIpv4(embeddedIpv4(g1, g2));
93
+ // 100::/64 discard.
94
+ if (g0 === 0x100 && g1 === 0 && g2 === 0 && g3 === 0) return false;
95
+ // 2001::/23 protocol assignments (Teredo among them) and 2001:db8::/32 documentation.
96
+ if (g0 === 0x2001 && (g1 < 0x200 || g1 === 0xdb8)) return false;
97
+ // fc00::/7 unique local, fe80::/10 link-local, fec0::/10 site-local, ff00::/8 multicast.
98
+ return (g0 & 0xfe00) !== 0xfc00 && (g0 & 0xffc0) !== 0xfe80 && (g0 & 0xffc0) !== 0xfec0 && (g0 & 0xff00) !== 0xff00;
99
+ }