@gloom-sh/terminal 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
@@ -1,9 +1,10 @@
1
- import { logFailure } from "../utils/log-failure";
2
1
  import type { ScannerCapture } from "../plugins/builtin/shared/scanner-capture";
3
2
  import type { TapeCapture } from "../plugins/builtin/time-sales/snapshot-client";
4
- import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
3
+ import { mkdtemp, rm } from "fs/promises";
4
+ import { randomBytes, timingSafeEqual } from "crypto";
5
5
  import { tmpdir } from "os";
6
6
  import { join } from "path";
7
+ import { writeOutputFile } from "./output-file";
7
8
  import { encodeRpcValue } from "../renderers/electrobun/shared/rpc-codec";
8
9
  import type { AppConfig } from "../types/config";
9
10
  import type { ChartResolutionResult } from "../time-series/types";
@@ -21,11 +22,20 @@ import type { HttpProxyRequestEnvelope, HttpProxyResponseEnvelope } from "../uti
21
22
  import type { PaneAccessGate } from "./pane-functions/access-gate";
22
23
  import { readVisibleKeyValues } from "./visible-key-values";
23
24
  import { scrollTruncationReasons } from "./scroll-truncation";
24
- import { SHOT_API_PROXY_PREFIX, SHOT_HTTP_BRIDGE_PATH, SHOT_MARKET_BRIDGE_PATH } from "./desktop-pane-shot-routes";
25
+ import {
26
+ SHOT_API_PROXY_PREFIX,
27
+ SHOT_API_SOCKET_PATH,
28
+ SHOT_HTTP_BRIDGE_PATH,
29
+ SHOT_MARKET_BRIDGE_PATH,
30
+ type ShotApiSocketCall,
31
+ type ShotApiSocketReply,
32
+ } from "./desktop-pane-shot-routes";
25
33
  import { SESSION_COOKIE_NAMES } from "../api-client/session-cookie";
34
+ import { isShotApiRequestAllowed, isShotSessionPath } from "./desktop-pane-shot-api-policy";
26
35
  import { renderElectrobunViewHtml } from "../renderers/dom/build-assets";
27
36
  import { loadShotView, SHOT_VIEW_SCRIPT, SHOT_VIEW_STYLESHEET, type ShotViewFiles } from "./desktop-pane-shot-view";
28
37
  import { errorMessage } from "../utils/errors";
38
+ import { findChromeExecutable, launchShotBrowser, type CdpPage } from "./desktop-pane-shot-browser";
29
39
 
30
40
  export interface DesktopPaneShotIntradayHistory {
31
41
  target?: InstrumentRef;
@@ -172,7 +182,8 @@ export interface DesktopPaneShotRenderResult {
172
182
 
173
183
  /**
174
184
  * The status line variants for a capture, longest first, worked out from what
175
- * the pane rendered; null leaves the footer row empty. `dates` says what the
185
+ * the pane rendered; null gives the footer row back to the pane, for a capture
186
+ * with nothing loaded to date (a wall, an empty state). `dates` says what the
176
187
  * line dates (`CaptureStatusLine`): the pane's own data, whose `as of` text is
177
188
  * then left out rather than repeat the date, or the quotes on screen.
178
189
  */
@@ -182,17 +193,6 @@ interface ShotStatusLineVariants {
182
193
  }
183
194
  type ShotStatusLine = (rendered: DesktopPaneShotRenderResult) => ShotStatusLineVariants | null | Promise<ShotStatusLineVariants | null>;
184
195
 
185
- type CdpResponse = {
186
- id?: number;
187
- result?: unknown;
188
- error?: { message?: string; data?: string };
189
- };
190
-
191
- type PendingCdpCall = {
192
- resolve: (value: unknown) => void;
193
- reject: (error: Error) => void;
194
- };
195
-
196
196
  const SHOT_MODE_CSS = [
197
197
  "[data-gloom-role='composite-chart-toolbar']",
198
198
  "[data-gloom-role='chart-series-quick-add']",
@@ -209,10 +209,7 @@ const SHOT_MODE_CSS = [
209
209
  + " line-height: var(--cell-h); font-size: 12px; letter-spacing: 0.02em; color: var(--gloom-text-dim, #888);"
210
210
  + " pointer-events: none; z-index: 1000; }";
211
211
 
212
- // Chrome gets 30 s (300 polls x 100 ms) to open its DevTools port: 8 s was not enough on a busy machine.
213
- const CHROME_POLL_ATTEMPTS = 300;
214
212
  const SHOT_READY_TIMEOUT_MS = 45_000;
215
- const CDP_CALL_TIMEOUT_MS = 10_000;
216
213
  const DEFAULT_DEVICE_SCALE_FACTOR = 2;
217
214
 
218
215
  export async function renderDesktopPaneScreenshot(
@@ -224,12 +221,12 @@ export async function renderDesktopPaneScreenshot(
224
221
  const chrome = await findChromeExecutable();
225
222
  const view = await loadShotView();
226
223
  const tempDir = await mkdtemp(join(tmpdir(), "gloom-pane-shot-"));
227
- let server: ReturnType<typeof Bun.serve> | null = null;
224
+ let server: ShotServer | null = null;
228
225
  try {
229
226
  server = serveShotPage(view, shotPageHtml(view, payload), apiProxy, options.bridge ?? null);
230
227
  return await capturePageScreenshot({
231
228
  chrome,
232
- url: new URL("/", server.url).href,
229
+ url: server.url,
233
230
  outputPath,
234
231
  widthPx: payload.widthPx,
235
232
  heightPx: payload.heightPx,
@@ -246,7 +243,7 @@ export async function renderDesktopPaneScreenshot(
246
243
  } : {}),
247
244
  });
248
245
  } finally {
249
- server?.stop(true);
246
+ server?.stop();
250
247
  await rm(tempDir, { recursive: true, force: true });
251
248
  }
252
249
  }
@@ -287,30 +284,107 @@ function shotPageHtml(view: ShotViewFiles, payload: DesktopPaneShotPayload): str
287
284
  });
288
285
  }
289
286
 
290
- function serveShotPage(
287
+ /** A capture's page server: `url` is the page Chrome opens, the only way in. */
288
+ export interface ShotServer {
289
+ url: string;
290
+ stop(): void;
291
+ }
292
+
293
+ /**
294
+ * The route a request to the shot server asks for, or null when it is not
295
+ * the capture's own page asking. Any program on this machine and any website
296
+ * open in the user's browser can reach the port, and behind it are the
297
+ * capture's data, reads made with the user's session and fetches made from
298
+ * this machine. So every request must name the server as `127.0.0.1:<port>`
299
+ * (a website that rebinds its own name to this address sends its own name),
300
+ * come from no other origin, and start its path with the secret folder the
301
+ * page was opened at, which nothing but Chrome is given.
302
+ */
303
+ function shotRequestRoute(request: Request, port: number | undefined, secret: string): URL | null {
304
+ if (port === undefined) return null;
305
+ const origin = `http://127.0.0.1:${port}`;
306
+ if (request.headers.get("host") !== `127.0.0.1:${port}`) return null;
307
+ const sentFrom = request.headers.get("origin");
308
+ if (sentFrom !== null && sentFrom !== origin) return null;
309
+ const url = new URL(request.url);
310
+ const folder = `/${secret}/`;
311
+ const given = Buffer.from(url.pathname.slice(0, folder.length));
312
+ const expected = Buffer.from(folder);
313
+ if (given.length !== expected.length || !timingSafeEqual(given, expected)) return null;
314
+ const route = new URL(url);
315
+ route.pathname = url.pathname.slice(folder.length - 1);
316
+ return route;
317
+ }
318
+
319
+ /**
320
+ * Serves one capture's page under a secret folder made for this run (see
321
+ * `shotRequestRoute`). The page reaches its API proxy, socket and bridges by
322
+ * paths relative to that folder.
323
+ */
324
+ export function serveShotPage(
291
325
  view: ShotViewFiles,
292
326
  html: string,
293
327
  apiProxy: DesktopPaneShotApiProxy,
294
328
  bridge: DesktopPaneShotBridge | null,
295
- ): ReturnType<typeof Bun.serve> {
296
- return Bun.serve({
329
+ ): ShotServer {
330
+ const secret = randomBytes(32).toString("hex");
331
+ const server = Bun.serve({
297
332
  hostname: "127.0.0.1",
298
333
  port: 0,
299
334
  idleTimeout: 60,
300
- fetch(request) {
301
- const url = new URL(request.url);
335
+ fetch(request, server) {
336
+ const url = shotRequestRoute(request, server.port, secret);
337
+ if (!url) return new Response("Forbidden", { status: 403 });
338
+ if (url.pathname === SHOT_API_SOCKET_PATH) {
339
+ // A browser always sends a socket's origin; only the page's own gets here.
340
+ if (request.headers.get("origin") === null) return new Response("Forbidden", { status: 403 });
341
+ return server.upgrade(request, { data: undefined }) ? undefined : new Response("Expected a WebSocket upgrade", { status: 426 });
342
+ }
302
343
  if (url.pathname.startsWith(`${SHOT_API_PROXY_PREFIX}/`)) {
303
344
  return proxyShotApiRequest(request, url, apiProxy);
304
345
  }
305
- if (url.pathname === SHOT_MARKET_BRIDGE_PATH) {
306
- return runShotMarketBridge(request, bridge);
307
- }
308
- if (url.pathname === SHOT_HTTP_BRIDGE_PATH) {
309
- return runShotHttpBridge(request, bridge);
346
+ if (url.pathname === SHOT_MARKET_BRIDGE_PATH || url.pathname === SHOT_HTTP_BRIDGE_PATH) {
347
+ if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
348
+ return url.pathname === SHOT_MARKET_BRIDGE_PATH ? runShotMarketBridge(request, bridge) : runShotHttpBridge(request, bridge);
310
349
  }
350
+ if (request.method !== "GET" && request.method !== "HEAD") return new Response("Method not allowed", { status: 405 });
311
351
  return serveShotAsset(url, view, html);
312
352
  },
353
+ websocket: {
354
+ message(socket, message) {
355
+ void answerShotApiCall(String(message), apiProxy).then((reply) => socket.send(reply));
356
+ },
357
+ },
313
358
  });
359
+ return {
360
+ url: `http://127.0.0.1:${server.port}/${secret}/`,
361
+ stop: () => server.stop(true),
362
+ };
363
+ }
364
+
365
+ /**
366
+ * One read the page sent over its API socket (`SHOT_API_SOCKET_PATH`), made
367
+ * as the proxy makes it, with the session the page never holds: the reply's
368
+ * JSON, its body in base64. A call that is not an API path answers why.
369
+ */
370
+ export async function answerShotApiCall(message: string, apiProxy: DesktopPaneShotApiProxy): Promise<string> {
371
+ let id = 0;
372
+ try {
373
+ const call = JSON.parse(message) as Partial<ShotApiSocketCall>;
374
+ id = typeof call.id === "number" ? call.id : 0;
375
+ if (typeof call.path !== "string" || !call.path.startsWith("/") || call.path.startsWith("//")) throw new Error("Not an API path.");
376
+ const url = new URL(`${SHOT_API_PROXY_PREFIX}${call.path}`, "http://127.0.0.1");
377
+ const sent = new Headers(Array.isArray(call.headers) ? call.headers : []);
378
+ const response = await proxyShotApiRequest(new Request(url, { headers: sent }), url, apiProxy);
379
+ const body = Buffer.from(await response.arrayBuffer()).toString("base64");
380
+ const headers: Array<[string, string]> = [];
381
+ response.headers.forEach((value, name) => headers.push([name, value]));
382
+ const reply: ShotApiSocketReply = { id, status: response.status, statusText: response.statusText, headers, body };
383
+ return JSON.stringify(reply);
384
+ } catch (error) {
385
+ const reply: ShotApiSocketReply = { id, error: errorMessage(error) };
386
+ return JSON.stringify(reply);
387
+ }
314
388
  }
315
389
 
316
390
  async function runShotMarketBridge(
@@ -361,43 +435,49 @@ function serveShotAsset(url: URL, view: ShotViewFiles, html: string): Response {
361
435
  return new Response(body, { headers: { "Content-Type": `${type}; charset=utf-8` } });
362
436
  }
363
437
 
438
+ /**
439
+ * The request headers the proxy passes on: what the page accepts and sends, a
440
+ * cached copy's validators and the client's feature list. Everything else is
441
+ * the proxy's to set (the session cookie, the API's origin) or nobody's: a
442
+ * client address, a method override, a key.
443
+ */
444
+ const SHOT_API_REQUEST_HEADERS = ["accept", "accept-language", "content-type", "if-modified-since", "if-none-match", "x-gloom-features"];
445
+ /** Response headers that belong to the API's own origin, not the page's. */
446
+ const SHOT_API_DROPPED_RESPONSE_HEADERS = [
447
+ "access-control-allow-credentials",
448
+ "access-control-allow-origin",
449
+ "content-encoding",
450
+ "content-length",
451
+ "set-cookie",
452
+ "transfer-encoding",
453
+ ];
454
+ const SHOT_API_REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
455
+ const SHOT_API_MAX_REDIRECTS = 5;
456
+
457
+ function shotApiError(message: string, status: number): Response {
458
+ return Response.json({ error: message }, { status, headers: { "Cache-Control": "no-store" } });
459
+ }
460
+
461
+ /**
462
+ * Sends one of the page's API requests with the user's session, if the page
463
+ * may send it (`isShotApiRequestAllowed`). A redirect is followed only within
464
+ * the API, to a path the page may ask for too.
465
+ */
364
466
  async function proxyShotApiRequest(
365
467
  request: Request,
366
468
  requestUrl: URL,
367
469
  apiProxy: DesktopPaneShotApiProxy,
368
470
  ): Promise<Response> {
369
471
  const baseUrl = new URL(apiProxy.baseUrl);
370
- const proxiedPath = requestUrl.pathname.slice(SHOT_API_PROXY_PREFIX.length) || "/";
371
- const target = new URL(baseUrl);
372
472
  const basePath = baseUrl.pathname.replace(/\/+$/, "");
373
- target.pathname = `${basePath}${proxiedPath.startsWith("/") ? proxiedPath : `/${proxiedPath}`}`;
374
- target.search = requestUrl.search;
375
- target.hash = "";
376
- const headers = new Headers(request.headers);
377
- for (const name of [
378
- "authorization",
379
- "connection",
380
- "content-length",
381
- "cookie",
382
- "host",
383
- "origin",
384
- "proxy-authorization",
385
- "referer",
386
- "te",
387
- "trailer",
388
- "transfer-encoding",
389
- "upgrade",
390
- "x-forwarded-for",
391
- "x-forwarded-host",
392
- "x-forwarded-proto",
393
- ]) {
394
- headers.delete(name);
473
+ let path = requestUrl.pathname.slice(SHOT_API_PROXY_PREFIX.length) || "/";
474
+ let method = request.method.toUpperCase();
475
+ if (!isShotApiRequestAllowed(method, path)) return shotApiError("The pane screenshot does not send this request.", 403);
476
+ const headers = new Headers();
477
+ for (const name of SHOT_API_REQUEST_HEADERS) {
478
+ const value = request.headers.get(name);
479
+ if (value !== null) headers.set(name, value);
395
480
  }
396
- const browserOwnedHeaders: string[] = [];
397
- headers.forEach((_value, name) => {
398
- if (name.startsWith("sec-") || name === "accept-encoding") browserOwnedHeaders.push(name);
399
- });
400
- for (const name of browserOwnedHeaders) headers.delete(name);
401
481
  headers.set("Origin", baseUrl.origin);
402
482
  if (apiProxy.sessionToken) {
403
483
  headers.set(
@@ -405,57 +485,60 @@ async function proxyShotApiRequest(
405
485
  SESSION_COOKIE_NAMES.map((name) => `${name}=${apiProxy.sessionToken}`).join("; "),
406
486
  );
407
487
  }
488
+ let target = new URL(baseUrl);
489
+ target.pathname = `${basePath}${path}`;
490
+ target.search = requestUrl.search;
491
+ target.hash = "";
408
492
 
409
493
  try {
410
- const response = await fetch(target, {
411
- method: request.method,
412
- headers,
413
- body: request.method === "GET" || request.method === "HEAD" ? undefined : request.body,
414
- redirect: "follow",
415
- });
416
- const responseHeaders = new Headers(response.headers);
417
- for (const name of [
418
- "access-control-allow-credentials",
419
- "access-control-allow-origin",
420
- "content-encoding",
421
- "content-length",
422
- "set-cookie",
423
- "transfer-encoding",
424
- ]) {
425
- responseHeaders.delete(name);
494
+ let body = method === "GET" || method === "HEAD" ? undefined : new Uint8Array(await request.arrayBuffer());
495
+ for (let followed = 0; ; followed += 1) {
496
+ const response = await fetch(target, { method, headers, body, redirect: "manual" });
497
+ const location = SHOT_API_REDIRECT_STATUSES.has(response.status) ? response.headers.get("location") : null;
498
+ if (location === null) return await shotApiResponse(response, path);
499
+ await response.body?.cancel().catch(() => { /* Intentional ignore: a redirect's own body is never read, only dropped. */ });
500
+ if (followed >= SHOT_API_MAX_REDIRECTS) return shotApiError("Cloud API redirected too many times.", 502);
501
+ const next = new URL(location, target);
502
+ if (
503
+ (response.status === 303 && method !== "GET" && method !== "HEAD")
504
+ || ((response.status === 301 || response.status === 302) && method === "POST")
505
+ ) {
506
+ method = "GET";
507
+ body = undefined;
508
+ headers.delete("content-type");
509
+ }
510
+ const inApi = next.origin === baseUrl.origin && (next.pathname === basePath || next.pathname.startsWith(`${basePath}/`));
511
+ path = next.pathname.slice(basePath.length) || "/";
512
+ if (!inApi || !isShotApiRequestAllowed(method, path)) return shotApiError("Cloud API redirected the request elsewhere.", 502);
513
+ target = next;
426
514
  }
427
- responseHeaders.set("Cache-Control", "no-store");
428
- return new Response(response.body, {
429
- status: response.status,
430
- statusText: response.statusText,
431
- headers: responseHeaders,
432
- });
433
515
  } catch {
434
- return Response.json({ error: "Cloud API request failed." }, { status: 502 });
516
+ // Intentional ignore: the page is told the read failed, as a dropped connection tells fetch.
517
+ return shotApiError("Cloud API request failed.", 502);
435
518
  }
436
519
  }
437
520
 
438
- async function findChromeExecutable(): Promise<string> {
439
- const candidates = [
440
- process.env.CHROME_PATH,
441
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
442
- "/Applications/Chromium.app/Contents/MacOS/Chromium",
443
- Bun.which("google-chrome"),
444
- Bun.which("google-chrome-stable"),
445
- Bun.which("chromium"),
446
- Bun.which("chromium-browser"),
447
- // Windows installs Chrome outside PATH, per machine or per user.
448
- ...["PROGRAMFILES", "PROGRAMFILES(X86)", "LOCALAPPDATA"].map((name) => {
449
- const dir = process.env[name];
450
- return dir ? join(dir, "Google", "Chrome", "Application", "chrome.exe") : null;
451
- }),
452
- ].filter((value): value is string => !!value);
453
-
454
- for (const candidate of candidates) {
455
- const file = Bun.file(candidate);
456
- if (await file.exists()) return candidate;
521
+ /**
522
+ * The API's answer as the page gets it: without the API origin's headers, and
523
+ * for the session read only its user, since the session's token rides along
524
+ * in that answer and the page never holds the session.
525
+ */
526
+ async function shotApiResponse(response: Response, path: string): Promise<Response> {
527
+ const headers = new Headers(response.headers);
528
+ for (const name of SHOT_API_DROPPED_RESPONSE_HEADERS) headers.delete(name);
529
+ headers.set("Cache-Control", "no-store");
530
+ if (!isShotSessionPath(path)) {
531
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
457
532
  }
458
- throw new Error("Drawing a pane needs Google Chrome or Chromium, and neither was found. Install one, or set CHROME_PATH to its executable.");
533
+ let user: unknown = null;
534
+ try {
535
+ const answer = await response.json() as { user?: unknown } | null;
536
+ user = answer?.user ?? null;
537
+ } catch {
538
+ // Intentional ignore: an answer that is not JSON names no user, which reads as signed out.
539
+ }
540
+ headers.set("Content-Type", "application/json");
541
+ return new Response(JSON.stringify({ user }), { status: response.status, statusText: response.statusText, headers });
459
542
  }
460
543
 
461
544
  async function capturePageScreenshot({
@@ -481,39 +564,9 @@ async function capturePageScreenshot({
481
564
  statusLine?: ShotStatusLine;
482
565
  fitRows?: { heightCells: number; maxHeightCells: number; roles: readonly string[] };
483
566
  }): Promise<DesktopPaneShotRenderResult> {
484
- await mkdir(userDataDir, { recursive: true });
485
- const port = 43000 + Math.floor(Math.random() * 10000);
486
- const proc = Bun.spawn([
487
- chrome,
488
- "--headless=new",
489
- "--disable-gpu",
490
- // Software WebGL for GPU-drawn panes (the 3D volatility surface).
491
- "--use-angle=swiftshader",
492
- "--enable-unsafe-swiftshader",
493
- "--no-sandbox",
494
- "--disable-dev-shm-usage",
495
- "--disable-background-networking",
496
- "--disable-default-apps",
497
- "--disable-extensions",
498
- "--disable-popup-blocking",
499
- "--hide-scrollbars",
500
- "--no-first-run",
501
- "--no-default-browser-check",
502
- "--allow-file-access-from-files",
503
- `--remote-debugging-port=${port}`,
504
- `--user-data-dir=${userDataDir}`,
505
- `--window-size=${widthPx},${heightPx}`,
506
- url,
507
- ], {
508
- stdout: "pipe",
509
- stderr: "pipe",
510
- });
511
-
512
- let session: CdpSession | null = null;
567
+ const browser = await launchShotBrowser(chrome, { widthPx, heightPx, userDataDir });
568
+ const session = browser.page;
513
569
  try {
514
- const wsUrl = await waitForPageWebSocket(port, url);
515
- session = await CdpSession.connect(wsUrl);
516
- await session.send("Page.enable");
517
570
  await session.send("Runtime.enable");
518
571
  await session.send("Emulation.setDeviceMetricsOverride", {
519
572
  width: widthPx,
@@ -524,12 +577,13 @@ async function capturePageScreenshot({
524
577
  await session.send("Emulation.setDefaultBackgroundColorOverride", {
525
578
  color: { r: 0, g: 0, b: 0, a: 0 },
526
579
  });
580
+ await browser.open(url);
527
581
  await waitForShotReady(session);
528
582
  if (fitRows) await growToFitRows(session, { widthPx, heightPx, deviceScaleFactor, ...fitRows });
529
583
  const rendered = await readRenderedPaneState(session);
530
584
  if (captureImage && statusLine) {
531
585
  const line = await statusLine(rendered);
532
- rendered.statusLine = line?.variants.length ? await fitShotStatusLine(session, line) : null;
586
+ rendered.statusLine = (await fitShotStatusLine(session, line ?? { variants: [] })) || null;
533
587
  }
534
588
  if (captureImage) {
535
589
  const screenshot = await session.send("Page.captureScreenshot", {
@@ -538,16 +592,14 @@ async function capturePageScreenshot({
538
592
  captureBeyondViewport: false,
539
593
  }) as { data?: string };
540
594
  if (!screenshot.data) throw new Error("Chrome did not return screenshot data.");
541
- await writeFile(outputPath, Uint8Array.from(Buffer.from(screenshot.data, "base64")));
595
+ await writeOutputFile(outputPath, Buffer.from(screenshot.data, "base64"));
542
596
  }
543
597
  return rendered;
544
598
  } catch (error) {
545
599
  const message = error instanceof Error ? error.message : String(error);
546
600
  throw new Error(message);
547
601
  } finally {
548
- session?.close();
549
- proc.kill();
550
- await proc.exited.catch(() => { /* Intentional ignore: The screenshot browser was killed during cleanup; preserve the capture result. */ });
602
+ await browser.close();
551
603
  }
552
604
  }
553
605
 
@@ -557,7 +609,7 @@ async function capturePageScreenshot({
557
609
  * taller pane to settle: a board a default capture would cut (FUT's last
558
610
  * sectors) shows every row, and a docs page its whole text.
559
611
  */
560
- async function growToFitRows(session: CdpSession, size: {
612
+ async function growToFitRows(session: CdpPage, size: {
561
613
  widthPx: number;
562
614
  heightPx: number;
563
615
  deviceScaleFactor: number;
@@ -638,7 +690,7 @@ function stateMarkers(text: string, patterns: RegExp[]): string[] {
638
690
  )))];
639
691
  }
640
692
 
641
- async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneShotRenderResult> {
693
+ async function readRenderedPaneState(session: CdpPage): Promise<DesktopPaneShotRenderResult> {
642
694
  const result = await session.send("Runtime.evaluate", {
643
695
  expression: `(() => {
644
696
  const root = document.getElementById("root") || document.body;
@@ -865,7 +917,7 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
865
917
  }
866
918
 
867
919
  /** Draws the first variant that fits the pane footer, and returns what it drew. */
868
- async function fitShotStatusLine(session: CdpSession, { variants, dates }: ShotStatusLineVariants): Promise<string | null> {
920
+ async function fitShotStatusLine(session: CdpPage, { variants, dates }: ShotStatusLineVariants): Promise<string | null> {
869
921
  const result = await session.send("Runtime.evaluate", {
870
922
  expression: `(() => {
871
923
  const fit = window.__GLOOM_CLI_SHOT_FIT_STATUS__;
@@ -878,28 +930,7 @@ async function fitShotStatusLine(session: CdpSession, { variants, dates }: ShotS
878
930
  return typeof value === "string" ? value : null;
879
931
  }
880
932
 
881
- async function waitForPageWebSocket(port: number, targetUrl: string): Promise<string> {
882
- for (let attempt = 0; attempt < CHROME_POLL_ATTEMPTS; attempt += 1) {
883
- try {
884
- const response = await fetch(`http://127.0.0.1:${port}/json/list`);
885
- if (response.ok) {
886
- const targets = await response.json() as Array<{ url?: string; type?: string; webSocketDebuggerUrl?: string }>;
887
- const page = targets.find((target) => (
888
- target.type === "page"
889
- && target.webSocketDebuggerUrl
890
- && (target.url === targetUrl || target.url?.startsWith("file:"))
891
- ));
892
- if (page?.webSocketDebuggerUrl) return page.webSocketDebuggerUrl;
893
- }
894
- } catch {
895
- // Intentional ignore: Chrome may still be starting; the polling deadline reports failure.
896
- }
897
- await sleep(100);
898
- }
899
- throw new Error("Timed out waiting for Chrome DevTools.");
900
- }
901
-
902
- async function waitForShotReady(session: CdpSession): Promise<void> {
933
+ async function waitForShotReady(session: CdpPage): Promise<void> {
903
934
  const startedAt = Date.now();
904
935
  while (Date.now() - startedAt < SHOT_READY_TIMEOUT_MS) {
905
936
  const result = await session.send("Runtime.evaluate", {
@@ -917,94 +948,3 @@ async function waitForShotReady(session: CdpSession): Promise<void> {
917
948
  function sleep(ms: number): Promise<void> {
918
949
  return new Promise((resolve) => setTimeout(resolve, ms));
919
950
  }
920
-
921
- class CdpSession {
922
- private nextId = 1;
923
- private readonly pending = new Map<number, PendingCdpCall>();
924
-
925
- private constructor(private readonly ws: WebSocket) {
926
- ws.addEventListener("message", (event) => {
927
- void this.handleMessage(event.data);
928
- });
929
- ws.addEventListener("close", () => {
930
- for (const pending of this.pending.values()) {
931
- pending.reject(new Error("Chrome DevTools connection closed."));
932
- }
933
- this.pending.clear();
934
- });
935
- }
936
-
937
- static connect(url: string): Promise<CdpSession> {
938
- return new Promise((resolve, reject) => {
939
- const ws = new WebSocket(url);
940
- ws.addEventListener("open", () => resolve(new CdpSession(ws)), { once: true });
941
- ws.addEventListener("error", () => reject(new Error("Could not connect to Chrome DevTools.")), { once: true });
942
- });
943
- }
944
-
945
- send(method: string, params?: Record<string, unknown>): Promise<unknown> {
946
- const id = this.nextId++;
947
- return new Promise((resolve, reject) => {
948
- if (this.ws.readyState !== WebSocket.OPEN) {
949
- reject(new Error("Chrome DevTools connection is not open."));
950
- return;
951
- }
952
-
953
- const timer = setTimeout(() => {
954
- this.pending.delete(id);
955
- reject(new Error(`Timed out waiting for Chrome DevTools method ${method}.`));
956
- }, CDP_CALL_TIMEOUT_MS);
957
- const settle = {
958
- resolve: (value: unknown) => {
959
- clearTimeout(timer);
960
- resolve(value);
961
- },
962
- reject: (error: Error) => {
963
- clearTimeout(timer);
964
- reject(error);
965
- },
966
- };
967
-
968
- this.pending.set(id, settle);
969
- try {
970
- this.ws.send(JSON.stringify({ id, method, params }));
971
- } catch (error) {
972
- this.pending.delete(id);
973
- settle.reject(error instanceof Error ? error : new Error(String(error)));
974
- }
975
- });
976
- }
977
-
978
- close(): void {
979
- this.ws.close();
980
- }
981
-
982
- private async handleMessage(data: unknown): Promise<void> {
983
- let response: CdpResponse;
984
- try {
985
- response = JSON.parse(await stringifyWebSocketMessage(data)) as CdpResponse;
986
- } catch (error) {
987
- logFailure("cli.screenshot", "Decode screenshot browser response failed")(error);
988
- return;
989
- }
990
- if (!response.id) return;
991
- const pending = this.pending.get(response.id);
992
- if (!pending) return;
993
- this.pending.delete(response.id);
994
- if (response.error) {
995
- pending.reject(new Error([response.error.message, response.error.data].filter(Boolean).join("\n")));
996
- } else {
997
- pending.resolve(response.result);
998
- }
999
- }
1000
- }
1001
-
1002
- async function stringifyWebSocketMessage(data: unknown): Promise<string> {
1003
- if (typeof data === "string") return data;
1004
- if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
1005
- if (ArrayBuffer.isView(data)) {
1006
- return new TextDecoder().decode(data as Uint8Array);
1007
- }
1008
- if (typeof Blob !== "undefined" && data instanceof Blob) return data.text();
1009
- return String(data);
1010
- }
@@ -0,0 +1,42 @@
1
+ import { parseCliGlobalArgs } from "./options";
2
+ import { printVersion, versionCommand } from "./commands/version";
3
+ import { checkCliCommandOptions, foldCommandArgs } from "./command-options";
4
+ import { describeCliCommand, renderCommandHelp } from "./help";
5
+ import { printCliResult } from "./result";
6
+ import { printCliError } from "./errors";
7
+ import { setCliColorEnabledOverride, setCliPlainOverride, setCliWidthOverride } from "../utils/cli-output";
8
+ import { foldTypedText } from "../utils/typed-text";
9
+
10
+ /** Version and its help need neither a plugin registry nor application services. */
11
+ export async function dispatchEarlyCommand(args: string[]): Promise<boolean> {
12
+ let parsed;
13
+ try {
14
+ parsed = parseCliGlobalArgs(args);
15
+ } catch {
16
+ // Intentional ignore: The regular dispatcher reports malformed global options.
17
+ return false;
18
+ }
19
+ const token = foldTypedText(parsed.args[0] ?? "").trim().toLowerCase();
20
+ if (token !== versionCommand.name && !versionCommand.aliases?.includes(token)) return false;
21
+ const options = parsed.options;
22
+ setCliColorEnabledOverride(options.color);
23
+ setCliWidthOverride(options.width ?? null);
24
+ if (options.plain != null) setCliPlainOverride(options.plain);
25
+ const commandArgs = foldCommandArgs(versionCommand, parsed.args.slice(1), parsed.literalStart - 1);
26
+ try {
27
+ if (parsed.help) {
28
+ if (options.format === "text") {
29
+ if (!options.quiet) process.stdout.write(`${renderCommandHelp(versionCommand)}\n`);
30
+ } else {
31
+ printCliResult({ data: { ...describeCliCommand(versionCommand), sections: versionCommand.help?.sections ?? [] } }, options);
32
+ }
33
+ } else {
34
+ checkCliCommandOptions(versionCommand, commandArgs, parsed.literalStart - 1);
35
+ await printVersion((result, renderOptions) => printCliResult(result, options, renderOptions, commandArgs));
36
+ }
37
+ } catch (error) {
38
+ printCliError(error, options, { command: versionCommand.name, args: commandArgs });
39
+ process.exitCode = 1;
40
+ }
41
+ return true;
42
+ }