gloomberb 0.19.2 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (472) hide show
  1. package/README.md +7 -1
  2. package/package.json +1 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +20 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app.tsx +23 -1
  20. package/src/architecture/core-builtin-allowlist.json +2 -1
  21. package/src/capabilities/factories.ts +1 -0
  22. package/src/cli/ascii-output.ts +42 -0
  23. package/src/cli/base-converter.ts +10 -2
  24. package/src/cli/command-options.ts +16 -1
  25. package/src/cli/commands/crypto-hints.ts +23 -4
  26. package/src/cli/commands/market.ts +92 -23
  27. package/src/cli/commands/news-search.ts +24 -5
  28. package/src/cli/commands/overview.ts +9 -4
  29. package/src/cli/commands/search.ts +71 -12
  30. package/src/cli/commands/system.ts +22 -30
  31. package/src/cli/commands/ticker.ts +49 -9
  32. package/src/cli/commands/version.ts +33 -0
  33. package/src/cli/context.ts +5 -9
  34. package/src/cli/data-dir.ts +7 -0
  35. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  36. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  37. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  38. package/src/cli/desktop-pane-shot.ts +196 -256
  39. package/src/cli/early-command.ts +42 -0
  40. package/src/cli/earnings-calendar.ts +9 -0
  41. package/src/cli/entry.ts +19 -13
  42. package/src/cli/errors.ts +12 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/history-export.ts +22 -2
  45. package/src/cli/history-rows.ts +154 -4
  46. package/src/cli/index.ts +18 -1
  47. package/src/cli/known-gaps.ts +40 -5
  48. package/src/cli/listing-arg.ts +46 -9
  49. package/src/cli/options.ts +1 -1
  50. package/src/cli/output-file.ts +60 -13
  51. package/src/cli/pane-functions/catalog.ts +14 -3
  52. package/src/cli/pane-functions/data.ts +1 -1
  53. package/src/cli/pane-functions/dom.ts +45 -0
  54. package/src/cli/pane-functions/freshness.ts +46 -9
  55. package/src/cli/pane-functions/function-help.ts +17 -9
  56. package/src/cli/pane-functions/headless.ts +22 -14
  57. package/src/cli/pane-functions/index.ts +6 -2
  58. package/src/cli/pane-functions/options.ts +6 -2
  59. package/src/cli/pane-functions/resolver.ts +27 -5
  60. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  61. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  62. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  63. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  64. package/src/cli/quote-answer.ts +20 -0
  65. package/src/cli/registry.ts +41 -13
  66. package/src/cli/result.ts +12 -6
  67. package/src/cli/ticker-resolution.ts +2 -1
  68. package/src/cli/types.ts +2 -0
  69. package/src/components/chart/composite/format.ts +3 -0
  70. package/src/components/chart-table/header.tsx +1 -1
  71. package/src/components/chart-table/layout.ts +28 -11
  72. package/src/components/command-bar/action-runtime.ts +4 -0
  73. package/src/components/command-bar/commands/direct/index.ts +18 -33
  74. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  75. package/src/components/command-bar/commands/registry.ts +10 -1
  76. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  77. package/src/components/command-bar/routes/root/runtime.ts +24 -1
  78. package/src/components/command-bar/surface/index.tsx +9 -3
  79. package/src/components/company-logo.tsx +2 -1
  80. package/src/components/layout/header.tsx +8 -3
  81. package/src/components/layout/market-summary.ts +25 -7
  82. package/src/components/layout/pane/footer/index.tsx +7 -2
  83. package/src/components/layout/pane/footer/model.ts +40 -5
  84. package/src/components/market-board/index.tsx +3 -2
  85. package/src/components/onboarding/desks-step.tsx +12 -4
  86. package/src/components/onboarding/first-run-workspace.ts +81 -4
  87. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  88. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  89. package/src/components/onboarding/onboarding-wizard.tsx +54 -23
  90. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  91. package/src/components/onboarding/run-setup-again.ts +24 -0
  92. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  93. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  94. package/src/components/onboarding/wizard-model.ts +57 -3
  95. package/src/components/onboarding/wizard-positions.ts +1 -1
  96. package/src/components/ui/delayed-mark.tsx +56 -0
  97. package/src/components/ui/external-link.tsx +3 -6
  98. package/src/components/ui/icon.tsx +8 -1
  99. package/src/components/ui/logo-mark.tsx +8 -5
  100. package/src/components/ui/stat-grid.tsx +263 -19
  101. package/src/data/config/store/node.ts +11 -5
  102. package/src/data/config/store/normalize.ts +16 -3
  103. package/src/data/profile-permissions.ts +47 -0
  104. package/src/data/resource-store.ts +34 -31
  105. package/src/data/sqlite/access.ts +19 -8
  106. package/src/data/sqlite/checkpointer.ts +95 -8
  107. package/src/data/sqlite/database.ts +40 -7
  108. package/src/docs/snapshot.json +191 -78
  109. package/src/i18n/es.ts +18 -2
  110. package/src/i18n/ja.ts +18 -2
  111. package/src/i18n/ko.ts +18 -2
  112. package/src/i18n/zh-cn.ts +18 -2
  113. package/src/i18n/zh-tw.ts +18 -2
  114. package/src/layout/desk-keys.ts +4 -1
  115. package/src/layout/desks.ts +38 -5
  116. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  117. package/src/market-data/batched-reads.ts +16 -0
  118. package/src/market-data/coordinator/auxiliary.ts +4 -2
  119. package/src/market-data/coordinator/index.ts +11 -2
  120. package/src/market-data/futures-board.ts +1 -1
  121. package/src/market-data/futures-contract.ts +3 -1
  122. package/src/market-data/market/index-level.ts +6 -4
  123. package/src/market-data/published-apac-sessions.ts +6 -0
  124. package/src/news/aggregator.ts +7 -4
  125. package/src/news/news-model.ts +11 -0
  126. package/src/news/story-id.ts +22 -0
  127. package/src/news/ticker-symbols.ts +17 -1
  128. package/src/notifications/app-notifier.ts +35 -14
  129. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  130. package/src/plugins/builtin/account-management/index.tsx +2 -3
  131. package/src/plugins/builtin/account-management/model.ts +1 -1
  132. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  133. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  134. package/src/plugins/builtin/alerts/events.ts +10 -2
  135. package/src/plugins/builtin/alerts/index.tsx +5 -5
  136. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  137. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  138. package/src/plugins/builtin/alerts/targets.ts +4 -1
  139. package/src/plugins/builtin/attention/headless.ts +1 -1
  140. package/src/plugins/builtin/attention/index.tsx +2 -4
  141. package/src/plugins/builtin/attention/listing.ts +1 -1
  142. package/src/plugins/builtin/attention/series.ts +1 -1
  143. package/src/plugins/builtin/awards/index.tsx +1 -2
  144. package/src/plugins/builtin/backtest/client.ts +1 -0
  145. package/src/plugins/builtin/backtest/index.tsx +1 -2
  146. package/src/plugins/builtin/backtest/rules.ts +3 -1
  147. package/src/plugins/builtin/backtest/window.ts +8 -7
  148. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  149. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  150. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  151. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  152. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  153. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  154. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  155. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  156. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  157. package/src/plugins/builtin/buildout/format.ts +9 -0
  158. package/src/plugins/builtin/buildout/index.tsx +51 -5
  159. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  160. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  161. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  162. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  163. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  164. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  165. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  166. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  167. package/src/plugins/builtin/cds/headless.ts +10 -2
  168. package/src/plugins/builtin/cds/model.ts +7 -3
  169. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  170. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  171. package/src/plugins/builtin/changelog/index.tsx +3 -2
  172. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
  173. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  174. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  175. package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
  176. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  177. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  178. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  179. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  180. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  181. package/src/plugins/builtin/chat/index.tsx +5 -5
  182. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  183. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  184. package/src/plugins/builtin/cloud/index.tsx +1 -2
  185. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  186. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  187. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  188. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  189. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  190. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  191. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  192. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  193. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  194. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  195. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  196. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  197. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  198. package/src/plugins/builtin/connections/index.ts +2 -3
  199. package/src/plugins/builtin/cot/index.tsx +1 -2
  200. package/src/plugins/builtin/cpi/index.tsx +1 -2
  201. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  202. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  203. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  204. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  205. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  206. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  207. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  208. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  209. package/src/plugins/builtin/crypto-board/series.ts +2 -0
  210. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  211. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  212. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  213. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  214. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  215. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  216. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  217. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  218. package/src/plugins/builtin/debug/index.tsx +1 -1
  219. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  220. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  221. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  222. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  223. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  224. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  225. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  226. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  227. package/src/plugins/builtin/docs/index.tsx +2 -3
  228. package/src/plugins/builtin/doe/headless.ts +16 -3
  229. package/src/plugins/builtin/doe/index.tsx +5 -4
  230. package/src/plugins/builtin/earnings/board.tsx +2 -1
  231. package/src/plugins/builtin/earnings/history.tsx +2 -1
  232. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  233. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  234. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  235. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  236. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  237. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  238. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  239. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  240. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  241. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  242. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  243. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  244. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  245. package/src/plugins/builtin/exposure/index.tsx +1 -2
  246. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  247. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  248. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  249. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  250. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  251. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  252. package/src/plugins/builtin/futures/index.tsx +15 -0
  253. package/src/plugins/builtin/futures/model.ts +14 -0
  254. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  255. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  256. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  257. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  258. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  259. package/src/plugins/builtin/gpu/board.tsx +51 -12
  260. package/src/plugins/builtin/gpu/client.ts +15 -2
  261. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  262. package/src/plugins/builtin/gpu/headless.ts +14 -2
  263. package/src/plugins/builtin/gpu/index.tsx +1 -2
  264. package/src/plugins/builtin/gpu/model.ts +20 -2
  265. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  266. package/src/plugins/builtin/help/index.tsx +1 -1
  267. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  268. package/src/plugins/builtin/holders/index.tsx +2 -3
  269. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  270. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  271. package/src/plugins/builtin/jodi/index.tsx +1 -2
  272. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  273. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  274. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  275. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  276. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  277. package/src/plugins/builtin/market-movers/index.tsx +7 -1
  278. package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
  279. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  280. package/src/plugins/builtin/members/index.tsx +1 -2
  281. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  282. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  283. package/src/plugins/builtin/news/headless.ts +7 -0
  284. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  285. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  286. package/src/plugins/builtin/notes/index.tsx +3 -6
  287. package/src/plugins/builtin/openfda/index.tsx +1 -2
  288. package/src/plugins/builtin/options/view.tsx +6 -5
  289. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  290. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  291. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  292. package/src/plugins/builtin/perps/index.tsx +1 -2
  293. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  294. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  295. package/src/plugins/builtin/plugin-module.ts +16 -2
  296. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  297. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  298. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  299. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  300. package/src/plugins/builtin/power/index.tsx +1 -2
  301. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  302. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  303. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  304. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  305. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  306. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  307. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  308. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  309. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  310. package/src/plugins/builtin/research/index.tsx +5 -8
  311. package/src/plugins/builtin/research-search/index.tsx +1 -2
  312. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  313. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  314. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  315. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  316. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  317. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  318. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  319. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  320. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  321. package/src/plugins/builtin/sec/headless.ts +5 -1
  322. package/src/plugins/builtin/sec/index.tsx +4 -2
  323. package/src/plugins/builtin/sec/model.ts +2 -0
  324. package/src/plugins/builtin/sectors/index.tsx +11 -5
  325. package/src/plugins/builtin/series-inventory.ts +2 -0
  326. package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
  327. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  328. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  329. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  330. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  331. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  332. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  333. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  334. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  335. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  336. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  337. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  338. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  339. package/src/plugins/builtin/thirteenf/api.ts +116 -13
  340. package/src/plugins/builtin/thirteenf/data.ts +132 -30
  341. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  342. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  343. package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
  344. package/src/plugins/builtin/thirteenf/types.ts +12 -0
  345. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
  346. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  347. package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
  348. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  349. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  350. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  351. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  352. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  353. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  354. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  355. package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  357. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  358. package/src/plugins/builtin/volatility/index.tsx +1 -2
  359. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  360. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  361. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  362. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  363. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  364. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  365. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  366. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  367. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  368. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  369. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  370. package/src/plugins/bundle.ts +52 -2
  371. package/src/plugins/catalog-browser.ts +2 -2
  372. package/src/plugins/catalog-cli.ts +20 -0
  373. package/src/plugins/host-link.ts +13 -1
  374. package/src/plugins/installer.ts +15 -4
  375. package/src/plugins/loader.ts +43 -11
  376. package/src/plugins/peer-imports.ts +201 -0
  377. package/src/plugins/peer-retry.ts +26 -29
  378. package/src/plugins/plugin-process.ts +128 -0
  379. package/src/plugins/redirection-guard.ts +45 -0
  380. package/src/remote/endpoint.ts +16 -3
  381. package/src/renderers/browser/main.tsx +3 -2
  382. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  383. package/src/renderers/dom/desktop/icons.tsx +2 -0
  384. package/src/renderers/dom/host/image-surface.tsx +46 -21
  385. package/src/renderers/dom/styles.css +7 -0
  386. package/src/renderers/dom/toast-host.tsx +2 -0
  387. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  388. package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
  389. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  390. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  391. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  392. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  393. package/src/renderers/electrobun/bun/window/create.ts +4 -2
  394. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  395. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  396. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  397. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  398. package/src/renderers/electrobun/view/main.tsx +7 -2
  399. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  400. package/src/renderers/opentui/browser-open.ts +10 -4
  401. package/src/renderers/opentui/image/surface.tsx +1 -1
  402. package/src/renderers/opentui/start.tsx +12 -6
  403. package/src/renderers/opentui/toast-host.tsx +2 -1
  404. package/src/series/registry.ts +18 -0
  405. package/src/series/standalone.ts +10 -1
  406. package/src/series/types.ts +7 -0
  407. package/src/series/validate.ts +11 -2
  408. package/src/sources/gloomberb-cloud/index.ts +28 -5
  409. package/src/sources/gloomberb-cloud/news.ts +24 -3
  410. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  411. package/src/sources/provider-errors.ts +26 -3
  412. package/src/sources/provider-router/history.ts +5 -0
  413. package/src/sources/provider-router/index.ts +13 -0
  414. package/src/sync/core-contributors.ts +32 -0
  415. package/src/telemetry/attention-counts.ts +64 -6
  416. package/src/telemetry/attention-setting.ts +33 -0
  417. package/src/telemetry/crash-reports-node.ts +3 -1
  418. package/src/telemetry/stall-clocks.ts +93 -0
  419. package/src/telemetry/stall-reports-node.ts +12 -7
  420. package/src/telemetry/stall-reports.ts +34 -5
  421. package/src/test-support/preload.ts +9 -0
  422. package/src/tickers/company-name.ts +19 -0
  423. package/src/tickers/listing-fate.ts +14 -3
  424. package/src/tickers/saved-instrument.ts +17 -0
  425. package/src/tickers/search/home-listing.ts +13 -2
  426. package/src/tickers/search/index.ts +4 -1
  427. package/src/tickers/search/types.ts +6 -1
  428. package/src/time-series/bar-aggregation.ts +7 -1
  429. package/src/time-series/base-series.ts +3 -3
  430. package/src/time-series/chart-bounds.ts +1 -1
  431. package/src/time-series/chart-data.ts +5 -2
  432. package/src/time-series/chart-seed.ts +1 -1
  433. package/src/time-series/coin-session-close.ts +211 -0
  434. package/src/time-series/live-bars.ts +13 -2
  435. package/src/time-series/live-quotes.ts +1 -1
  436. package/src/time-series/official-close.ts +159 -0
  437. package/src/time-series/price-comparison.ts +55 -6
  438. package/src/time-series/resolve.ts +12 -3
  439. package/src/time-series/series-presentation.ts +1 -1
  440. package/src/time-series/studies/formula.ts +205 -0
  441. package/src/time-series/studies/indicators.ts +299 -0
  442. package/src/time-series/studies/leverage.ts +75 -0
  443. package/src/time-series/studies/pair.ts +138 -0
  444. package/src/time-series/studies/resolve.ts +333 -0
  445. package/src/time-series/studies/shared.ts +141 -0
  446. package/src/time-series/studies/units.ts +243 -0
  447. package/src/time-series/studies/vwap.ts +274 -0
  448. package/src/types/config.ts +18 -2
  449. package/src/types/data-provider.ts +23 -0
  450. package/src/types/financials.ts +22 -0
  451. package/src/types/headless.ts +42 -3
  452. package/src/types/plugin.ts +32 -3
  453. package/src/ui/host.tsx +11 -0
  454. package/src/ui/toast.tsx +26 -1
  455. package/src/utils/ascii-text.ts +140 -0
  456. package/src/utils/cli-output.ts +91 -18
  457. package/src/utils/concurrency.ts +24 -0
  458. package/src/utils/exchanges.ts +6 -0
  459. package/src/utils/external-url.ts +20 -7
  460. package/src/utils/futures-generic.ts +4 -4
  461. package/src/utils/http-proxy-upstream.ts +124 -0
  462. package/src/utils/main-thread-stall.ts +75 -3
  463. package/src/utils/perf-marks.ts +36 -2
  464. package/src/utils/private-files.ts +141 -0
  465. package/src/utils/public-address.ts +99 -0
  466. package/src/account/upgrade-personalization.ts +0 -57
  467. package/src/account/upgrade-tickers.ts +0 -124
  468. package/src/api-client/upgrade-experiment.ts +0 -91
  469. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  470. package/src/renderers/electrobun/launcher/preload.ts +0 -13
  471. package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
  472. package/src/time-series/studies.ts +0 -1628
@@ -2,7 +2,7 @@ import { t } from "../../i18n";
2
2
  import type { ThemeColors } from "../../theme/colors";
3
3
  import { useThemeColors } from "../../theme/theme-context";
4
4
  import { Box, Text, TextAttributes, useUiCapabilities } from "../../ui";
5
- import { displayWidth, truncateToDisplayWidth } from "../../utils/format";
5
+ import { clipToDisplayWidth, displayWidth, padTo, truncateToDisplayWidth } from "../../utils/format";
6
6
  import { SplitBar, type SplitBarPart } from "./split-bar";
7
7
 
8
8
  export type StatTone = "neutral" | "positive" | "negative" | "warning" | "accent" | "muted";
@@ -49,6 +49,13 @@ export interface StatGridProps {
49
49
  width: number;
50
50
  /** Fixed column count; by default as many as the widest item allows, up to 4, balanced over the rows. */
51
51
  columns?: number;
52
+ /**
53
+ * The most lines the grid takes. A whole-row cell too narrow for its value
54
+ * and detail wraps onto a second line, the detail under the value; with
55
+ * `maxLines` only the leading such cells the lines leave room for do, and
56
+ * the rest keep one line, cut (`statGridRows` counts the same).
57
+ */
58
+ maxLines?: number;
52
59
  }
53
60
 
54
61
  const MAX_COLUMNS = 4;
@@ -102,9 +109,207 @@ function layoutRows(items: StatItem[], columns: number): StatItem[][] {
102
109
  return rows;
103
110
  }
104
111
 
105
- /** Rows a StatGrid takes, so panes can size what sits below it. */
106
- export function statGridRows(items: StatItem[], width: number, columns?: number): number {
107
- return layoutRows(items, Math.max(1, columns ?? statGridColumns(items, width))).length;
112
+ /** What a grid's cells share: the label column, the inner width and one cell's width, in terminal cells. */
113
+ function gridGeometry(items: StatItem[], width: number, columns: number, nativePaneChrome: boolean) {
114
+ const labels = labelChars(items);
115
+ const innerWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
116
+ const cellWidth = Math.max(8, Math.floor((innerWidth - CELL_GAP * (columns - 1)) / columns));
117
+ const widestValue = Math.max(0, ...items.filter((item) => !item.wide).map((item) => displayWidth(item.value)));
118
+ return { labels, innerWidth, cellWidth, widestValue };
119
+ }
120
+
121
+ /**
122
+ * The label's column in a cell `width` wide: it grows into what the grid's
123
+ * widest value leaves, and never gets less than half the cell, so one long
124
+ * value cannot cut every label in a grid with a fixed column count.
125
+ */
126
+ function cellLabelWidth(width: number, labels: number, widestValue: number): number {
127
+ return Math.min(labels + 1, Math.max(4, Math.floor(width * 0.5), width - widestValue - 1));
128
+ }
129
+
130
+ /** Cells a split bar takes after a value that leaves `room` in its column; none when too few to read. */
131
+ function splitBarCells(room: number): number {
132
+ return room >= 4 ? Math.min(SPLIT_BAR_CELLS, room) : 0;
133
+ }
134
+
135
+ const MONTH = "(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|June?|July?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\\.?";
136
+ /** A date, time or day count in a cell: `2026-10-29`, `18:00`, `Dec 9`, `30 Sep`, `Dec '26`, `2026`, `in 40 days`, `3d ago`. */
137
+ const DATED = new RegExp([
138
+ "\\d{4}-\\d{2}",
139
+ "\\b\\d{1,2}:\\d{2}\\b",
140
+ `\\b${MONTH} (?:'\\d{2}|\\d{1,4})\\b`,
141
+ `\\b\\d{1,2} ${MONTH}(?![a-z])`,
142
+ "\\b(?:19|20)\\d{2}\\b",
143
+ "\\bin \\d+ ?(?:d|days?|wks?|weeks?|mo|months?)\\b",
144
+ "\\b\\d+ ?(?:d|h|m|min|days?|hours?) ago\\b",
145
+ ].join("|"));
146
+
147
+ /** `text` cut to `room` after its last whole word that fits, behind a `…`; mid-word only when its first word does not fit. */
148
+ function clipWords(text: string, room: number): string {
149
+ if (displayWidth(text) <= room) return text;
150
+ const words = text.split(" ");
151
+ for (let count = words.length - 1; count > 0; count--) {
152
+ const kept = `${words.slice(0, count).join(" ")}…`;
153
+ if (displayWidth(kept) <= room) return kept;
154
+ }
155
+ return clipToDisplayWidth(text, room);
156
+ }
157
+
158
+ /**
159
+ * `text` cut to `width` without cutting a date: its ` · ` parts that have none
160
+ * give way first, from the last, cut short at a word and then to a `…` that
161
+ * says something is left out; then the words before a part's date
162
+ * (`1,561,837 shares… 16:02:15.123`). Only dates too wide for the width are
163
+ * cut at their end.
164
+ */
165
+ function cutKeepingDates(text: string, width: number): string {
166
+ const parts = text.split(" · ");
167
+ const joined = () => parts.join(" · ");
168
+ const roomFor = (index: number) => width - (displayWidth(joined()) - displayWidth(parts[index]!));
169
+ for (let index = parts.length - 1; index >= 0 && displayWidth(joined()) > width; index--) {
170
+ const part = parts[index]!;
171
+ if (DATED.test(part)) continue;
172
+ const room = roomFor(index);
173
+ if (room >= 4) parts[index] = clipWords(part, room);
174
+ else if (parts[index + 1] !== "…" && part !== "…") parts[index] = "…";
175
+ else parts.splice(index, 1);
176
+ }
177
+ for (let index = parts.length - 1; index >= 0 && displayWidth(joined()) > width; index--) {
178
+ const part = parts[index]!;
179
+ const at = part.search(DATED);
180
+ if (at <= 0) continue;
181
+ const date = part.slice(at);
182
+ const room = roomFor(index) - displayWidth(date) - 1;
183
+ parts[index] = room >= 4 ? `${clipWords(part.slice(0, at).trimEnd(), room)} ${date}` : date;
184
+ }
185
+ return truncateToDisplayWidth(joined(), width);
186
+ }
187
+
188
+ /**
189
+ * `text` on at most two lines no wider than `width`: broken after the ` · `
190
+ * parts that fit the first, else at its last space that does; the second line
191
+ * is cut where it has no date (cutKeepingDates).
192
+ */
193
+ function twoLines(text: string, width: number): string[] {
194
+ const parts = text.split(" · ");
195
+ let first = "";
196
+ let taken = 0;
197
+ for (; taken < parts.length; taken++) {
198
+ const next = first ? `${first} · ${parts[taken]}` : parts[taken]!;
199
+ if (displayWidth(next) > width) break;
200
+ first = next;
201
+ }
202
+ let rest = parts.slice(taken).join(" · ");
203
+ if (!first) {
204
+ // The first part alone is wider than a line: it breaks at a space, or is cut.
205
+ const words = parts[0]!.split(" ");
206
+ let count = 0;
207
+ while (count < words.length && displayWidth(words.slice(0, count + 1).join(" ")) <= width) count++;
208
+ first = count ? words.slice(0, count).join(" ") : truncateToDisplayWidth(words[0]!, width);
209
+ rest = [words.slice(Math.max(1, count)).join(" "), ...parts.slice(1)].filter(Boolean).join(" · ");
210
+ }
211
+ return rest ? [first, cutKeepingDates(rest, width)] : [first];
212
+ }
213
+
214
+ /** A whole-row cell on its two lines (wrapStatCell). */
215
+ interface WrappedStatCell {
216
+ /** The value: one line, or both when it is wider than its column and the detail gives way. */
217
+ value: string[];
218
+ /** Cells of the split bar after the value on the first line; 0 for none. */
219
+ bar: number;
220
+ /** The detail on the second line, or null. */
221
+ detail: string | null;
222
+ }
223
+
224
+ /** Lines a wrapped cell takes: its value, then its detail. */
225
+ const WRAPPED_CELL_LINES = 2;
226
+
227
+ /**
228
+ * A whole-row cell (a `wide` item, or any cell of a one-column grid) that does
229
+ * not fit one line, on two: the value and its split bar on the first, and the
230
+ * detail on the second, in the longest form that fits, else cut where it has
231
+ * no date. A value wider than its column takes both lines, its bar after the
232
+ * first where it has room, when the detail can go (there is none, the pane
233
+ * offers an empty form, or it has no date); with a dated detail the value is
234
+ * cut instead, as a date is never what gives way.
235
+ * Null when the cell fits one line as it is, with the detail form that fits
236
+ * beside the value (an empty form counts: the pane chose to show none). Worked
237
+ * out in terminal cells for both renderers, so the grid's rows are the same
238
+ * count on each.
239
+ */
240
+ function wrapStatCell(item: StatItem, width: number, labelWidth: number): WrappedStatCell | null {
241
+ const valueWidth = Math.max(1, width - labelWidth);
242
+ const valueCells = displayWidth(item.value);
243
+ const forms = item.detail ? [item.detail, ...(item.detailVariants ?? [])] : [];
244
+ const shown = forms.filter(Boolean);
245
+ const detailLine = () => shown.find((form) => displayWidth(form) <= valueWidth) ?? cutKeepingDates(shown.at(-1)!, valueWidth);
246
+ if (valueCells <= valueWidth) {
247
+ const bar = item.split ? splitBarCells(valueWidth - valueCells - 1) : 0;
248
+ const detailRoom = valueWidth - valueCells - (bar ? bar + 1 : 0) - 2;
249
+ const shortest = sizingDetail(item);
250
+ if (!shortest || (detailRoom > 1 && displayWidth(shortest) <= detailRoom)) return null;
251
+ return { value: [item.value], bar, detail: detailLine() };
252
+ }
253
+ const detailCanGo = shown.length === 0 || shown.length < forms.length || !shown.some((form) => DATED.test(form));
254
+ if (detailCanGo) {
255
+ const value = twoLines(item.value, valueWidth);
256
+ return { value, bar: item.split ? splitBarCells(valueWidth - displayWidth(value[0]!) - 1) : 0, detail: null };
257
+ }
258
+ return { value: [cutKeepingDates(item.value, valueWidth)], bar: 0, detail: detailLine() };
259
+ }
260
+
261
+ /** A laid-out row's one cell, wrapped, when it spans the row and does not fit one line (wrapStatCell). */
262
+ function wrappedRow(row: StatItem[], columns: number, geometry: ReturnType<typeof gridGeometry>): WrappedStatCell | null {
263
+ const item = row[0];
264
+ if (!item || row.length > 1 || (!item.wide && columns > 1)) return null;
265
+ const width = item.wide ? geometry.innerWidth : geometry.cellWidth;
266
+ return wrapStatCell(item, width, cellLabelWidth(width, geometry.labels, geometry.widestValue));
267
+ }
268
+
269
+ /**
270
+ * The grid's rows and which of them wrap: every whole-row cell too narrow for
271
+ * one line, in order, while the grid stays within `maxLines`; past that a cell
272
+ * keeps its one line, cut. Decided in terminal cells on both renderers, so a
273
+ * pane sizes the same rows on each.
274
+ */
275
+ function planStatGrid(items: StatItem[], width: number, columns: number, maxLines = Number.POSITIVE_INFINITY) {
276
+ const rows = layoutRows(items, columns);
277
+ const geometry = gridGeometry(items, width, columns, false);
278
+ let spare = maxLines - rows.length;
279
+ const wrapped = rows.map((row) => {
280
+ if (spare < WRAPPED_CELL_LINES - 1) return null;
281
+ const cell = wrappedRow(row, columns, geometry);
282
+ if (cell) spare -= WRAPPED_CELL_LINES - 1;
283
+ return cell;
284
+ });
285
+ const lines = rows.length + wrapped.filter(Boolean).length * (WRAPPED_CELL_LINES - 1);
286
+ return { rows, wrapped, lines };
287
+ }
288
+
289
+ /**
290
+ * Rows a StatGrid takes, so panes can size what sits below it: a whole-row
291
+ * cell that wraps counts both its lines. With `maxLines`, the grid drawn with
292
+ * the same `maxLines`.
293
+ */
294
+ export function statGridRows(items: StatItem[], width: number, columns?: number, { maxLines }: { maxLines?: number } = {}): number {
295
+ return planStatGrid(items, width, Math.max(1, columns ?? statGridColumns(items, width)), maxLines).lines;
296
+ }
297
+
298
+ /**
299
+ * The leading items that fit a band of `maxLines`, in whole rows: as many rows
300
+ * as there are lines, and the rows they leave for a whole-row cell to wrap
301
+ * into (`maxLines` on the grid, which `lines` counts).
302
+ */
303
+ export function statGridWithin(items: StatItem[], width: number, maxLines: number, columns?: number): { items: StatItem[]; lines: number } {
304
+ if (maxLines < 1 || items.length === 0) return { items: [], lines: 0 };
305
+ const resolved = Math.max(1, columns ?? statGridColumns(items, width));
306
+ const shown = layoutRows(items, resolved).slice(0, maxLines).flat();
307
+ return { items: shown, lines: statGridRows(shown, width, resolved, { maxLines }) };
308
+ }
309
+
310
+ /** A label in its column, cut and padded by terminal cells, so a wide character keeps the values aligned. */
311
+ function labelText(label: string, labelWidth: number): string {
312
+ return padTo(truncateToDisplayWidth(label, labelWidth - 1), labelWidth);
108
313
  }
109
314
 
110
315
  /**
@@ -114,16 +319,13 @@ export function statGridRows(items: StatItem[], width: number, columns?: number)
114
319
  * hairline-split cells, so the top of the pane reads as one piece of chrome;
115
320
  * the terminal draws aligned text columns.
116
321
  */
117
- export function StatGrid({ items, width, columns: columnsProp }: StatGridProps) {
322
+ export function StatGrid({ items, width, columns: columnsProp, maxLines }: StatGridProps) {
118
323
  const colors = useThemeColors();
119
324
  const { nativePaneChrome } = useUiCapabilities();
120
325
  if (items.length === 0) return null;
121
326
  const columns = Math.max(1, columnsProp ?? statGridColumns(items, width));
122
- const rows = layoutRows(items, columns);
123
- const labels = labelChars(items);
124
- const innerWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
125
- const cellWidth = Math.max(8, Math.floor((innerWidth - CELL_GAP * (columns - 1)) / columns));
126
- const widestValue = Math.max(0, ...items.filter((item) => !item.wide).map((item) => displayWidth(item.value)));
327
+ const { rows, wrapped: wrapping, lines: lineCount } = planStatGrid(items, width, columns, maxLines);
328
+ const { labels, innerWidth, cellWidth, widestValue } = gridGeometry(items, width, columns, !!nativePaneChrome);
127
329
 
128
330
  const toneColor = (item: StatItem): string =>
129
331
  item.color || (statToneColor(item.tone, colors) ?? (nativePaneChrome ? colors.textBright : colors.text));
@@ -132,25 +334,66 @@ export function StatGrid({ items, width, columns: columnsProp }: StatGridProps)
132
334
  <Box
133
335
  flexDirection="column"
134
336
  paddingX={nativePaneChrome ? 0 : 1}
135
- height={rows.length}
337
+ height={lineCount}
136
338
  flexShrink={0}
137
339
  data-gloom-role="stat-grid"
138
340
  style={nativePaneChrome ? { "--stat-label-w": `${labels}ch`, "--stat-columns": String(columns) } : undefined}
139
341
  >
140
- {rows.map((row, rowIndex) => (
342
+ {rows.map((row, rowIndex) => {
343
+ const wrapped = wrapping[rowIndex];
344
+ if (wrapped) {
345
+ const item = row[0]!;
346
+ const label = t(item.label);
347
+ const width = item.wide ? innerWidth : cellWidth;
348
+ const labelWidth = cellLabelWidth(width, labels, widestValue);
349
+ const [firstValue, ...moreValue] = wrapped.value;
350
+ return (
351
+ <Box key={rowIndex} height={WRAPPED_CELL_LINES} flexDirection="row" data-gloom-role="stat-grid-row">
352
+ <Box
353
+ key={item.id ?? `${rowIndex}:0`}
354
+ width={nativePaneChrome ? undefined : width}
355
+ height={WRAPPED_CELL_LINES}
356
+ flexDirection="row"
357
+ overflow="hidden"
358
+ data-gloom-role="stat-grid-cell"
359
+ data-wide={item.wide ? "true" : undefined}
360
+ data-wrapped="true"
361
+ data-gloom-stat-id={item.id}
362
+ >
363
+ <Text fg={colors.textDim} data-gloom-role="stat-grid-label">
364
+ {nativePaneChrome ? label : labelText(label, labelWidth)}
365
+ </Text>
366
+ <Box flexDirection="column" flexGrow={1} minWidth={0}>
367
+ <Box height={1} flexDirection="row">
368
+ <Text fg={toneColor(item)} attributes={TextAttributes.BOLD} data-gloom-role="stat-grid-value">{firstValue}</Text>
369
+ {item.split && wrapped.bar ? (
370
+ <Box marginLeft={nativePaneChrome ? 0 : 1} flexShrink={nativePaneChrome ? 1 : 0} style={nativePaneChrome ? { minWidth: 0 } : undefined}>
371
+ <SplitBar parts={item.split} width={nativePaneChrome ? SPLIT_BAR_CELLS : wrapped.bar} />
372
+ </Box>
373
+ ) : null}
374
+ </Box>
375
+ {moreValue.map((line, index) => (
376
+ <Text key={`value:${index}`} fg={toneColor(item)} attributes={TextAttributes.BOLD} data-gloom-role="stat-grid-value">{line}</Text>
377
+ ))}
378
+ {wrapped.detail != null ? (
379
+ <Text fg={colors.textDim} data-gloom-role="stat-grid-detail">{wrapped.detail}</Text>
380
+ ) : null}
381
+ </Box>
382
+ </Box>
383
+ </Box>
384
+ );
385
+ }
386
+ return (
141
387
  <Box key={rowIndex} height={1} flexDirection="row" gap={nativePaneChrome ? 0 : CELL_GAP} data-gloom-role="stat-grid-row">
142
388
  {row.map((item, index) => {
143
389
  const label = t(item.label);
144
390
  const width = item.wide ? innerWidth : cellWidth;
145
- // The label grows into what the grid's widest value leaves, and
146
- // never gets less than half the cell, so one long value cannot cut
147
- // every label in a grid with a fixed column count.
148
- const labelWidth = Math.min(labels + 1, Math.max(4, Math.floor(width * 0.5), width - widestValue - 1));
391
+ const labelWidth = cellLabelWidth(width, labels, widestValue);
149
392
  const valueWidth = Math.max(1, width - labelWidth);
150
393
  const value = nativePaneChrome ? item.value : truncateToDisplayWidth(item.value, valueWidth);
151
394
  // The bar shows only whole enough to read; the value never gives way to it.
152
395
  const barRoom = valueWidth - displayWidth(value) - 1;
153
- const barCells = !item.split ? 0 : nativePaneChrome ? SPLIT_BAR_CELLS : barRoom >= 4 ? Math.min(SPLIT_BAR_CELLS, barRoom) : 0;
396
+ const barCells = !item.split ? 0 : nativePaneChrome ? SPLIT_BAR_CELLS : splitBarCells(barRoom);
154
397
  const detailWidth = valueWidth - displayWidth(value) - (barCells ? barCells + 1 : 0) - 2;
155
398
  // The desktop cell has no gap between cells: its label column, inset and gaps take the room
156
399
  // (measured short, as a cell is wider than a character, so a form that fits is never clipped).
@@ -177,7 +420,7 @@ export function StatGrid({ items, width, columns: columnsProp }: StatGridProps)
177
420
  : undefined}
178
421
  >
179
422
  <Text fg={colors.textDim} data-gloom-role="stat-grid-label">
180
- {nativePaneChrome ? label : truncateToDisplayWidth(label, labelWidth - 1).padEnd(labelWidth)}
423
+ {nativePaneChrome ? label : labelText(label, labelWidth)}
181
424
  </Text>
182
425
  <Text fg={toneColor(item)} attributes={TextAttributes.BOLD} data-gloom-role="stat-grid-value">{value}</Text>
183
426
  {item.split && barCells ? (
@@ -192,7 +435,8 @@ export function StatGrid({ items, width, columns: columnsProp }: StatGridProps)
192
435
  );
193
436
  })}
194
437
  </Box>
195
- ))}
438
+ );
439
+ })}
196
440
  </Box>
197
441
  );
198
442
  }
@@ -1,6 +1,6 @@
1
1
  import { logFailure } from "../../../utils/log-failure";
2
2
  import { existsSync, readdirSync, type Dirent } from "fs";
3
- import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
3
+ import { readFile, rename, rm } from "fs/promises";
4
4
  import { dirname, join, resolve } from "path";
5
5
  import { expandUserHome, getGloomberbDirs } from "../home";
6
6
  import type { AppConfig } from "../../../types/config";
@@ -8,6 +8,8 @@ import { createDefaultConfig } from "../../../types/config";
8
8
  import { debugLog } from "../../../utils/debug-log";
9
9
  import { findAbsorbedCheckout, isDirectoryOrLink, isPluginDirectory } from "../../../plugins/loader";
10
10
  import { EXTRACTED_PLUGINS } from "../../../plugins/extracted-plugins";
11
+ import { makePrivateDir, writePrivateFile } from "../../../utils/private-files";
12
+ import { tightenProfilePermissions } from "../../profile-permissions";
11
13
  import type { ConfigMigrationHost } from "./migrations";
12
14
  import {
13
15
  normalizeConfigForSave,
@@ -113,11 +115,12 @@ const saveQueues = new Map<string, Promise<void>>();
113
115
  let tempFileCounter = 0;
114
116
 
115
117
  async function writeConfigFile(configPath: string, contents: string): Promise<void> {
116
- await mkdir(dirname(configPath), { recursive: true });
118
+ await makePrivateDir(dirname(configPath));
117
119
  // Unique per call: a clock tick is not, and two saves can land in one.
118
120
  const tempPath = `${configPath}.${process.pid}.${Date.now()}.${tempFileCounter++}.tmp`;
119
121
  try {
120
- await writeFile(tempPath, contents, "utf-8");
122
+ // Readable by the user alone: config.json holds broker credentials.
123
+ await writePrivateFile(tempPath, contents);
121
124
  await rename(tempPath, configPath);
122
125
  } catch (error) {
123
126
  await rm(tempPath, { force: true }).catch(() => { /* Intentional ignore: Remove a partial temporary file while preserving the write error rethrown below. */ });
@@ -149,11 +152,12 @@ export async function saveConfig(config: AppConfig): Promise<void> {
149
152
 
150
153
  export async function initDataDir(dataDir: string): Promise<AppConfig> {
151
154
  configLog.info(`Initializing data directory: ${dataDir}`);
152
- await mkdir(dataDir, { recursive: true });
155
+ await makePrivateDir(dataDir);
153
156
  const { config, needsSave } = await loadConfigState(dataDir);
154
157
  if (needsSave) {
155
158
  await saveConfig(config);
156
159
  }
160
+ await tightenProfilePermissions(dataDir, [configFileFor(dataDir), getGloomberbDirs().configFile]);
157
161
  return config;
158
162
  }
159
163
 
@@ -166,7 +170,9 @@ export async function resetAllData(dataDir: string): Promise<void> {
166
170
 
167
171
  export async function exportConfig(config: AppConfig, destPath: string): Promise<void> {
168
172
  const { dataDir, ...rest } = config;
169
- await writeFile(expandHomePath(destPath), JSON.stringify(rest, null, 2), "utf-8");
173
+ // The export carries broker credentials as config.json does: readable by the
174
+ // user alone, an existing file included, and never written through a link.
175
+ await writePrivateFile(expandHomePath(destPath), JSON.stringify(rest, null, 2));
170
176
  }
171
177
 
172
178
  export async function importConfig(dataDir: string, srcPath: string): Promise<AppConfig> {
@@ -30,6 +30,7 @@ import { DEFAULT_THEME, themes } from "../../../theme/themes";
30
30
  import { sanitizeStarPrompt } from "../../../app/star-prompt/model";
31
31
  import { sanitizeCommandBarOpens } from "../../../app/command-bar-intro";
32
32
  import { sanitizeUpdatesConfig } from "../../../updater/channel";
33
+ import { ATTENTION_NOTICE_ACCOUNTS } from "../../../telemetry/attention-setting";
33
34
 
34
35
  const configLog = debugLog.createLogger("config");
35
36
  const reportedUnknownThemes = new Set<string>();
@@ -201,17 +202,28 @@ function withUpdates(updates: AppConfig["updates"]): Pick<AppConfig, "updates">
201
202
  return updates ? { updates } : {};
202
203
  }
203
204
 
204
- /** Only the switches that are set survive; an empty object is the same as none. */
205
+ /**
206
+ * Only the switches that are set survive; an empty object is the same as none.
207
+ * Attention Counts is on by default, so a value that cannot be read turns it
208
+ * off instead: a hand-edited "false" or a broken block never counts as on.
209
+ */
205
210
  function sanitizeTelemetry(value: unknown): TelemetryConfig | undefined {
206
- if (!isRecord(value)) return undefined;
211
+ if (value === undefined) return undefined;
212
+ if (!isRecord(value)) return { attention: false };
207
213
  const telemetry: TelemetryConfig = {
208
214
  ...(typeof value.crashReports === "boolean" ? { crashReports: value.crashReports } : {}),
209
215
  ...(typeof value.usage === "boolean" ? { usage: value.usage } : {}),
210
- ...(typeof value.attention === "boolean" ? { attention: value.attention } : {}),
216
+ ...(value.attention !== undefined ? { attention: value.attention === true } : {}),
217
+ ...sanitizeAttentionNotice(value.attentionNoticeShownTo),
211
218
  };
212
219
  return Object.keys(telemetry).length > 0 ? telemetry : undefined;
213
220
  }
214
221
 
222
+ function sanitizeAttentionNotice(value: unknown): Pick<TelemetryConfig, "attentionNoticeShownTo"> {
223
+ const accounts = sanitizeUniqueStringList(value).slice(-ATTENTION_NOTICE_ACCOUNTS);
224
+ return accounts.length > 0 ? { attentionNoticeShownTo: accounts } : {};
225
+ }
226
+
215
227
  /**
216
228
  * Keeps the shape honest without judging the contents: a chord that does not
217
229
  * parse stays in the file and is reported when the table resolves, so a typo
@@ -286,6 +298,7 @@ function sanitizeOnboardingProgress(value: unknown): OnboardingProgress | undefi
286
298
  accountStatus,
287
299
  checkoutOpenedAt: typeof value.checkoutOpenedAt === "string" ? value.checkoutOpenedAt : undefined,
288
300
  desks: Array.isArray(value.desks) ? sanitizeUniqueStringList(value.desks) : undefined,
301
+ rerun: value.rerun === true ? true : undefined,
289
302
  };
290
303
  }
291
304
 
@@ -0,0 +1,47 @@
1
+ import { readdir } from "fs/promises";
2
+ import { dirname, join } from "path";
3
+ import { logFailure } from "../utils/log-failure";
4
+ import { restrictDir, restrictFile } from "../utils/private-files";
5
+
6
+ /** The data folder's files that hold the Cloud session and cache, or logs of what the user did. */
7
+ const PRIVATE_DATA_FILES = [
8
+ ".gloomberb-cache.db",
9
+ ".gloomberb-cache.db-wal",
10
+ ".gloomberb-cache.db-shm",
11
+ "stall-log.txt",
12
+ "stall-reports.json",
13
+ "crash-reports.json",
14
+ "desktop-view-ids.json",
15
+ ];
16
+ /** `remote-control.json` and one per app (`remote-control.tui.json`): each holds the remote-control token. */
17
+ const REMOTE_ENDPOINT_FILE = /^remote-control(?:\.[\w-]+)?\.json$/;
18
+
19
+ /**
20
+ * Tightens a profile made before its folders and files were created private,
21
+ * or under a umask that let others read them: the data folder and each
22
+ * config.json's folder open to the user alone, config.json (broker
23
+ * credentials) and the files above readable by the user alone. A step that
24
+ * fails (a folder another account owns, a disk without POSIX modes) is logged
25
+ * and the others still run; startup goes on either way. Nothing to do on
26
+ * Windows (see private-files.ts).
27
+ */
28
+ export async function tightenProfilePermissions(dataDir: string, configFiles: readonly string[]): Promise<void> {
29
+ if (process.platform === "win32") return;
30
+ const steps: Array<() => Promise<void>> = [
31
+ ...[...new Set([dataDir, ...configFiles.map((file) => dirname(file))])].map((dir) => () => restrictDir(dir)),
32
+ ...[...new Set(configFiles)].map((file) => () => restrictFile(file)),
33
+ ...PRIVATE_DATA_FILES.map((name) => () => restrictFile(join(dataDir, name))),
34
+ async () => {
35
+ for (const name of await readdir(dataDir)) {
36
+ if (REMOTE_ENDPOINT_FILE.test(name)) await restrictFile(join(dataDir, name));
37
+ }
38
+ },
39
+ ];
40
+ for (const step of steps) {
41
+ try {
42
+ await step();
43
+ } catch (error) {
44
+ logFailure("data.profile-permissions", "Restricting a profile file to its user failed")(error);
45
+ }
46
+ }
47
+ }
@@ -277,22 +277,18 @@ export class ResourceStore {
277
277
  // Eviction goes by the order on disk, which has to hold the latest reads.
278
278
  if (!this.flushTouches()) return false;
279
279
 
280
- const candidates = this.db.query<{
281
- namespace: string;
282
- kind: string;
283
- entity_key: string;
284
- variant_key: string;
285
- source_key: string;
286
- size_bytes: number;
287
- }, [number]>(
288
- `SELECT namespace, kind, entity_key, variant_key, source_key, size_bytes
280
+ // Only the row ids, which the recency index holds: a row's size sits
281
+ // after its payload, and reading it walks the whole payload from disk.
282
+ // Asking it of the oldest 512 rows read 10 MB to pick what to evict.
283
+ const candidates = this.db.query<{ rowid: number }, [number]>(
284
+ `SELECT rowid
289
285
  FROM resource_cache
290
286
  ORDER BY last_accessed_at ASC, fetched_at ASC
291
287
  LIMIT ?`,
292
288
  );
293
- const remove = this.db.query(
294
- `DELETE FROM resource_cache
295
- WHERE namespace = ? AND kind = ? AND entity_key = ? AND variant_key = ? AND source_key = ?`,
289
+ // Deleting a row reads its payload anyway, to free it: the size comes along.
290
+ const remove = this.db.query<{ size_bytes: number }, [number]>(
291
+ "DELETE FROM resource_cache WHERE rowid = ? RETURNING size_bytes",
296
292
  );
297
293
 
298
294
  // A cache at its limit holds tens of thousands of rows, so eviction reads
@@ -306,24 +302,23 @@ export class ResourceStore {
306
302
  if (!batch) return false;
307
303
  if (batch.length === 0) return true;
308
304
 
309
- const rowsToDelete = [] as typeof batch;
310
- for (const row of batch) {
311
- if (this.withinSoftLimits(remainingCount, remainingSize)) break;
312
- rowsToDelete.push(row);
313
- remainingCount -= 1;
314
- remainingSize -= row.size_bytes;
315
- }
316
- if (rowsToDelete.length === 0) return true;
317
-
305
+ let left = { count: remainingCount, size: remainingSize };
318
306
  const pruned = this.access.writeCache("prune least recently used cached resources", () => {
307
+ left = { count: remainingCount, size: remainingSize };
319
308
  const tx = this.db.transaction(() => {
320
- for (const row of rowsToDelete) {
321
- remove.run(row.namespace, row.kind, row.entity_key, row.variant_key, row.source_key);
309
+ for (const { rowid } of batch) {
310
+ if (this.withinSoftLimits(left.count, left.size)) break;
311
+ const removed = remove.get(rowid);
312
+ if (!removed) continue;
313
+ left.count -= 1;
314
+ left.size -= removed.size_bytes;
322
315
  }
323
316
  });
324
317
  tx();
325
318
  });
326
319
  if (!pruned) return false;
320
+ remainingCount = left.count;
321
+ remainingSize = left.size;
327
322
  }
328
323
  return true;
329
324
  }
@@ -382,25 +377,33 @@ export class ResourceStore {
382
377
  key: Pick<ResourceCacheKey, "namespace" | "kind" | "entityKey">,
383
378
  options: ListResourceOptions = {},
384
379
  ): CachedResourceRecord<T>[] {
380
+ const variantKeys = options.variantKeys ? [...new Set(options.variantKeys.map(normalizeVariantKey))] : null;
381
+ const sourceKeys = options.sourceKeys ? [...new Set(options.sourceKeys.map(normalizeSourceKey))] : null;
382
+ if (variantKeys?.length === 0 || sourceKeys?.length === 0) return [];
383
+ // The rows asked for only: an entity can hold dozens of variants (every
384
+ // range of a price history, from every source), megabytes of payload
385
+ // that a filter after the read threw away. The keys come from the
386
+ // primary key's index. Expiry stays below: the column sits after the
387
+ // payload, and checking it here walked a 10 MB payload twice.
388
+ const filters = [
389
+ ...(variantKeys ? [`variant_key IN (${variantKeys.map(() => "?").join(", ")})`] : []),
390
+ ...(sourceKeys ? [`source_key IN (${sourceKeys.map(() => "?").join(", ")})`] : []),
391
+ ];
392
+ const params: string[] = [key.namespace, key.kind, key.entityKey, ...(variantKeys ?? []), ...(sourceKeys ?? [])];
385
393
  const rows = this.access.readCache("list cached resources", () => (
386
394
  this.db
387
- .query<ResourceCacheRow, [string, string, string]>(
395
+ .query<ResourceCacheRow, string[]>(
388
396
  `SELECT namespace, kind, entity_key, variant_key, source_key, schema_version, payload, provenance,
389
397
  fetched_at, stale_at, expires_at, last_accessed_at, size_bytes
390
398
  FROM resource_cache
391
- WHERE namespace = ? AND kind = ? AND entity_key = ?`,
399
+ WHERE ${["namespace = ?", "kind = ?", "entity_key = ?", ...filters].join(" AND ")}`,
392
400
  )
393
- .all(key.namespace, key.kind, key.entityKey)
401
+ .all(...params)
394
402
  )) ?? [];
395
403
 
396
- const allowedVariantKeys = options.variantKeys ? new Set(options.variantKeys.map(normalizeVariantKey)) : null;
397
- const allowedSourceKeys = options.sourceKeys ? new Set(options.sourceKeys.map(normalizeSourceKey)) : null;
398
-
399
404
  const records: CachedResourceRecord<T>[] = [];
400
405
  for (const row of sortRows(rows)) {
401
406
  if (!options.allowExpired && isExpired(row.expires_at)) continue;
402
- if (allowedVariantKeys && !allowedVariantKeys.has(row.variant_key)) continue;
403
- if (allowedSourceKeys && !allowedSourceKeys.has(row.source_key)) continue;
404
407
  if (options.schemaVersion != null && row.schema_version !== options.schemaVersion) {
405
408
  this.deleteExact({
406
409
  namespace: row.namespace,
@@ -43,9 +43,13 @@ const sqliteLog = debugLog.createLogger("sqlite");
43
43
  *
44
44
  * In WAL mode the connection commits with `synchronous=NORMAL`: a commit no
45
45
  * longer flushes the disk, and a power cut can cost the last few, never the
46
- * database. A write of user data flushes as before (`FULL`), which also
47
- * flushes everything committed ahead of it. Where WAL does not take (a
48
- * network share), every commit flushes.
46
+ * database. A write of user data is flushed within a second or two by the
47
+ * checkpointer's thread, which flushes the WAL before copying it (see
48
+ * checkpointer.ts): on Windows the flush took the app's thread for seconds
49
+ * at a time. Where nothing else flushes (a command, a checkpointer that
50
+ * stopped), a write of user data flushes itself (`FULL`), and with it
51
+ * everything committed ahead of it. Where WAL does not take (a network
52
+ * share), every commit flushes.
49
53
  *
50
54
  * On the app's connection the cache writes of one turn of the event loop
51
55
  * share a transaction and commit together once the turn is over: a batch of
@@ -99,6 +103,12 @@ export interface SqliteAccessOptions {
99
103
  * connection's own level is left alone.
100
104
  */
101
105
  relaxedCommits?: boolean;
106
+ /**
107
+ * Whether another thread flushes the WAL soon after each commit (the
108
+ * checkpointer, while it runs). A write of user data then commits at
109
+ * `NORMAL` like the cache's.
110
+ */
111
+ flushedElsewhere?: () => boolean;
102
112
  /** After each commit of the app's connection (see checkpointer.ts). */
103
113
  onCommit?: () => void;
104
114
  }
@@ -152,16 +162,17 @@ export function createSqliteAccess(db: Database, options: SqliteAccessOptions =
152
162
  };
153
163
 
154
164
  /**
155
- * Commits `write` with a full flush when the connection's own level is
156
- * NORMAL. The cache writes of the turn commit first, wherever they came
157
- * from (a queued write's `settled`, the write before), so the user write
158
- * never lands inside their batch at NORMAL.
165
+ * Commits `write` on its own, with a full flush when the connection's own
166
+ * level is NORMAL and nothing else flushes it. The cache writes of the turn
167
+ * commit first, wherever they came from (a queued write's `settled`, the
168
+ * write before), so the user write never lands inside their batch, which
169
+ * a failed commit rolls back whole.
159
170
  */
160
171
  const durable = <T>(write: () => T): T => {
161
172
  commitCacheBatch();
162
173
  durableWrites += 1;
163
174
  try {
164
- if (!options.relaxedCommits || db.inTransaction) return write();
175
+ if (!options.relaxedCommits || db.inTransaction || options.flushedElsewhere?.()) return write();
165
176
  db.exec("PRAGMA synchronous=FULL");
166
177
  try {
167
178
  return write();