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
@@ -30,6 +30,12 @@ export interface PaneFooterSegment {
30
30
  * data a source delivers late: build it with {@link delayedFooterSegment}.
31
31
  */
32
32
  icon?: "warning" | "clock";
33
+ /**
34
+ * A few characters the desktop draws after `icon`, in the footer's muted
35
+ * tone, so the segment reads without hovering: a delay's `15m`. The
36
+ * terminal's `parts` already carry it (`◷15m`).
37
+ */
38
+ iconText?: string;
33
39
  /**
34
40
  * The segment only dates the pane's data (`as of 2026-10-05`). A `gloomberb
35
41
  * shot` whose status line already dates that data leaves it out, so the
@@ -51,6 +57,14 @@ export interface PaneFooterSegment {
51
57
  * warning.
52
58
  */
53
59
  capture?: boolean;
60
+ /**
61
+ * The segment only says how long ago the pane's data loaded (`5m ago`,
62
+ * `fetched just now`), relative to the moment it is read: build it with
63
+ * {@link ageFooterSegment}. A row too narrow for every segment leaves it out
64
+ * first, whole, before anything else shortens or is cut, so the source and
65
+ * the date stay.
66
+ */
67
+ age?: boolean;
54
68
  /**
55
69
  * When the copy the pane shows was saved, on the saved-copy segment
56
70
  * ({@link savedCopyFooterSegment}). The footer draws that segment first,
@@ -75,10 +89,11 @@ export interface PaneFooterSegment {
75
89
  const DELAYED_FOOTER_GLYPH = "◷";
76
90
 
77
91
  /**
78
- * The footer's one form for data a source delivers late: a yellow clock with
79
- * `detail` in its tooltip on the desktop, and the clock with the delay
80
- * (`◷15m`) in the terminal, which has no tooltip, so it takes a few cells
81
- * rather than a phrase. `onPress` makes it a button, such as an upgrade.
92
+ * The footer's one form for data a source delivers late: a yellow clock and
93
+ * the delay (`15m`) on the desktop, with `detail` in its tooltip, and the
94
+ * clock with the delay (`◷15m`) in the terminal, which has no tooltip, so it
95
+ * takes a few cells rather than a phrase. Without a known delay the desktop
96
+ * draws the clock alone. `onPress` makes it a button, such as an upgrade.
82
97
  */
83
98
  export function delayedFooterSegment({
84
99
  id,
@@ -101,6 +116,7 @@ export function delayedFooterSegment({
101
116
  label: detail,
102
117
  title: detail,
103
118
  parts: [{ text: `${DELAYED_FOOTER_GLYPH}${delay ?? ""}`, tone: "warning" }],
119
+ ...(delay ? { iconText: delay } : {}),
104
120
  ...(onPress ? { onPress } : {}),
105
121
  ...(quoteStatus ? { quoteStatus } : {}),
106
122
  };
@@ -127,6 +143,19 @@ export function savedCopyFooterSegment(id: string, savedAt: number, now = Date.n
127
143
  };
128
144
  }
129
145
 
146
+ /**
147
+ * The footer's one form for how long ago the pane's data loaded: `5m ago`,
148
+ * or `fetched 5m ago` with a `verb`. A narrow row leaves it out first (`age`).
149
+ */
150
+ export function ageFooterSegment(ago: string, { id = "updated", verb, tone = "muted" }: {
151
+ id?: string;
152
+ /** Said before the age: `fetched`, `checked`, `updated`. */
153
+ verb?: string;
154
+ tone?: PaneFooterPart["tone"];
155
+ } = {}): PaneFooterSegment {
156
+ return { id, age: true, parts: [{ text: verb ? `${verb} ${ago}` : ago, tone }] };
157
+ }
158
+
130
159
  /**
131
160
  * The saved-copy segment of a pane showing several answers (a list and the
132
161
  * report it opens): the oldest of the copies among `savedAts` (ms), or no
@@ -317,7 +346,8 @@ function trimTrailing(info: PaneFooterSegment[], width: number, keep: number): P
317
346
  }
318
347
 
319
348
  /**
320
- * The info segments as they fit `width`: segments with `variants` step down
349
+ * The info segments as they fit `width`: an `age` (`5m ago`) is left out
350
+ * first, so the source and the date outlast it; then segments with `variants` step down
321
351
  * together, one variant at a time, until the row fits or none is left; beside
322
352
  * the hints, the trailing status that still does not fit is then cut or left
323
353
  * out (trimTrailing). A status after every segment with shorter forms gives
@@ -328,6 +358,10 @@ function trimTrailing(info: PaneFooterSegment[], width: number, keep: number): P
328
358
  * so a frame that fits its own status line sees it overflow.
329
359
  */
330
360
  export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number, { besideHints = true }: { besideHints?: boolean } = {}): PaneFooterSegment[] {
361
+ // How long ago the data loaded goes first, whole: a cut age (`fetched just`) says nothing.
362
+ if (infoTextWidth(info) > width && info.some((segment) => segment.age)) {
363
+ return fitPaneFooterInfo(info.filter((segment) => !segment.age), width, { besideHints });
364
+ }
331
365
  if (!besideHints) return stepDownTogether(info, width);
332
366
  const lastWithVariants = info.findLastIndex((segment) => (segment.variants?.length ?? 0) > 0);
333
367
  if (lastWithVariants >= 0 && lastWithVariants < info.length - 1 && infoTextWidth(info) > width) {
@@ -539,6 +573,7 @@ export function samePaneFooterRegistration(
539
573
  && !!segment.onPress === !!other.onPress
540
574
  && segment.disabled === other.disabled
541
575
  && segment.savedCopyAt === other.savedCopyAt
576
+ && segment.age === other.age
542
577
  && sameFooterParts(segment.parts, other.parts)
543
578
  && (segment.variants?.length ?? 0) === (other.variants?.length ?? 0)
544
579
  && (segment.variants ?? []).every((variant, variantIndex) => sameFooterParts(variant, other.variants![variantIndex]!));
@@ -215,9 +215,10 @@ export interface StatChartDetailProps {
215
215
  export function StatChartDetail({ items, width, height, empty, emptySubject, emptyTitle, renderChart }: StatChartDetailProps) {
216
216
  return (
217
217
  <Box flexDirection="column" width={width} height={height}>
218
- <StatGrid items={items} width={width} />
218
+ {/* A figure wraps only into rows the chart's least leaves, so the detail never spills. */}
219
+ <StatGrid items={items} width={width} maxLines={height - 3} />
219
220
  <PaneStatusBody empty={empty} subject={emptySubject} emptyTitle={emptyTitle}>
220
- {renderChart(Math.max(3, height - statGridRows(items, width)))}
221
+ {renderChart(Math.max(3, height - statGridRows(items, width, undefined, { maxLines: height - 3 })))}
221
222
  </PaneStatusBody>
222
223
  </Box>
223
224
  );
@@ -1,14 +1,19 @@
1
1
  import { Box, useUiCapabilities } from "../../ui";
2
2
  import { t } from "../../i18n";
3
3
  import { DESKS, deskFunctions, type DeskKey } from "../../layout/desks";
4
+ import { MAX_DESK_PICKS } from "../../layout/desk-keys";
4
5
  import { ToggleList } from "../toggle-list";
5
6
 
6
- /** Ticks or unticks a desk; the ticked ones keep the order they were ticked in. */
7
- export function toggleDeskChoice(chosen: readonly DeskKey[], key: DeskKey): DeskKey[] {
8
- return chosen.includes(key) ? chosen.filter((entry) => entry !== key) : [...chosen, key];
7
+ /**
8
+ * Ticks or unticks a desk; the ticked ones keep the order they were ticked
9
+ * in. A fourth tick is ignored: untick one first.
10
+ */
11
+ export function toggleDeskChoice(chosen: DeskKey[], key: DeskKey): DeskKey[] {
12
+ if (chosen.includes(key)) return chosen.filter((entry) => entry !== key);
13
+ return chosen.length < MAX_DESK_PICKS ? [...chosen, key] : chosen;
9
14
  }
10
15
 
11
- /** "What do you trade?": one checkbox per desk, with the functions it opens. */
16
+ /** "What do you trade most?": one checkbox per desk, with the functions it opens. */
12
17
  export function DesksStep({
13
18
  chosen,
14
19
  cursor,
@@ -21,6 +26,7 @@ export function DesksStep({
21
26
  onToggle: (key: DeskKey) => void;
22
27
  }) {
23
28
  const desktop = useUiCapabilities().nativePaneChrome === true;
29
+ const full = chosen.length >= MAX_DESK_PICKS;
24
30
  return (
25
31
  <Box flexDirection="column" flexShrink={0} paddingX={desktop ? 0 : 2} style={desktop ? { marginTop: 14 } : undefined}>
26
32
  <ToggleList
@@ -28,6 +34,8 @@ export function DesksStep({
28
34
  id: desk.key,
29
35
  label: t(desk.label),
30
36
  enabled: chosen.includes(desk.key),
37
+ // Once three are ticked, the rest wait until one is unticked.
38
+ disabled: full && !chosen.includes(desk.key),
31
39
  detail: deskFunctions(desk).join(" "),
32
40
  }))}
33
41
  selectedIdx={cursor}
@@ -8,6 +8,8 @@ import {
8
8
  type SavedLayout,
9
9
  } from "../../types/config";
10
10
  import type { TickerMetadata, TickerRecord } from "../../types/ticker";
11
+ import { MAX_DESK_PICKS, type DeskKey } from "../../layout/desk-keys";
12
+ import { DESKS, isDeskKey } from "../../layout/desks";
11
13
 
12
14
  /**
13
15
  * The watchlist a first run lands with: the two index ETFs and the names most
@@ -211,13 +213,88 @@ export function buildFirstRunLayout({
211
213
  };
212
214
  }
213
215
 
214
- /** Replaces the Home layout with the first-run workspace and makes it active. */
215
- export function applyFirstRunLayout(config: AppConfig, home: SavedLayout): AppConfig {
216
- const layouts = config.layouts.length > 0 ? [home, ...config.layouts.slice(1)] : [home];
216
+ /**
217
+ * The ids of the layouts onboarding makes. Running it again finds its own
218
+ * layouts by these and replaces them, so it never adds a copy; a layout
219
+ * without one is the user's.
220
+ */
221
+ const ONBOARDING_HOME_LAYOUT_ID = "onboarding:home";
222
+ const ONBOARDING_LAYOUT_ID_PREFIX = "onboarding:";
223
+ const ONBOARDING_DESK_LAYOUT_ID_PREFIX = `${ONBOARDING_LAYOUT_ID_PREFIX}desk:`;
224
+
225
+ export function onboardingDeskLayoutId(desk: DeskKey): string {
226
+ return `${ONBOARDING_DESK_LAYOUT_ID_PREFIX}${desk}`;
227
+ }
228
+
229
+ function isOnboardingLayout(layout: SavedLayout): boolean {
230
+ return layout.id?.startsWith(ONBOARDING_LAYOUT_ID_PREFIX) === true;
231
+ }
232
+
233
+ /**
234
+ * Takes back the layouts setup made before it tagged them, so a run from
235
+ * Settings rebuilds them in place instead of copying them. Such a profile
236
+ * has setup's Home first and its desks right after it, under the desks'
237
+ * names. A first layout not called Home is the user's, and so is
238
+ * everything after the desks.
239
+ */
240
+ function adoptUntaggedSetupLayouts(config: AppConfig): AppConfig {
241
+ const [first, ...rest] = config.layouts;
242
+ if (!first || first.name !== "Home" || config.layouts.some(isOnboardingLayout)) return config;
243
+ const desks: SavedLayout[] = [];
244
+ for (const layout of rest) {
245
+ const key = DESKS.find((desk) => desk.name === layout.name)?.key;
246
+ if (!key || desks.length >= MAX_DESK_PICKS || desks.some((desk) => desk.id === onboardingDeskLayoutId(key))) break;
247
+ desks.push({ ...layout, id: onboardingDeskLayoutId(key) });
248
+ }
249
+ return {
250
+ ...config,
251
+ layouts: [{ ...first, id: ONBOARDING_HOME_LAYOUT_ID }, ...desks, ...rest.slice(desks.length)],
252
+ };
253
+ }
254
+
255
+ /** The desks setup made that are still tabs, in tab order: what a run from Settings starts with ticked. */
256
+ export function setupDeskPicks(config: AppConfig): DeskKey[] {
257
+ const picks: DeskKey[] = [];
258
+ for (const layout of adoptUntaggedSetupLayouts(config).layouts) {
259
+ const key = layout.id?.startsWith(ONBOARDING_DESK_LAYOUT_ID_PREFIX)
260
+ ? layout.id.slice(ONBOARDING_DESK_LAYOUT_ID_PREFIX.length)
261
+ : null;
262
+ if (isDeskKey(key) && !picks.includes(key)) picks.push(key);
263
+ }
264
+ return picks.slice(0, MAX_DESK_PICKS);
265
+ }
266
+
267
+ /**
268
+ * Puts the first-run workspace in place and makes Home active: Home, then a
269
+ * tab per picked desk. The layouts an earlier run made are replaced where
270
+ * they stood, and a desk not picked this time goes with them; a first run
271
+ * replaces the stock Home it starts with. Every other layout is the user's
272
+ * and keeps its place and content. A run from Settings (`rerun`) also
273
+ * rebuilds what setup made before it tagged its layouts, and with none of
274
+ * setup's left it puts Home first, replacing nothing.
275
+ */
276
+ export function applyFirstRunLayout(
277
+ config: AppConfig,
278
+ home: SavedLayout,
279
+ desks: ReadonlyArray<SavedLayout & { id: string }> = [],
280
+ { rerun = false }: { rerun?: boolean } = {},
281
+ ): AppConfig {
282
+ const { layouts: current } = rerun ? adoptUntaggedSetupLayouts(config) : config;
283
+ const earlier = current.findIndex(isOnboardingLayout);
284
+ const users = current.filter((layout) => !isOnboardingLayout(layout));
285
+ // Everything before the first layout onboarding made is the user's, so its index holds.
286
+ const at = earlier >= 0 ? earlier : 0;
287
+ const kept = earlier >= 0 || rerun ? users : users.slice(1);
288
+ const layouts = [
289
+ ...kept.slice(0, at),
290
+ { ...home, id: ONBOARDING_HOME_LAYOUT_ID },
291
+ ...desks,
292
+ ...kept.slice(at),
293
+ ];
217
294
  return {
218
295
  ...config,
219
296
  layouts,
220
297
  layout: home.layout,
221
- activeLayoutIndex: 0,
298
+ activeLayoutIndex: at,
222
299
  };
223
300
  }
@@ -359,7 +359,8 @@ export function OnboardingHeader({
359
359
  }
360
360
 
361
361
  // Progress, not navigation: three segments, the current one lit, the ones
362
- // behind it filled. A filled segment is still a way back.
362
+ // behind it filled. A filled segment is a way back unless the wizard marks
363
+ // it unavailable: a step done for good stays filled and is not offered again.
363
364
  const activeIndex = sections.findIndex((section) => section.id === active);
364
365
  return (
365
366
  <Box flexDirection="row" alignItems="flex-start" justifyContent="space-between" minWidth={0}>
@@ -58,7 +58,7 @@ export function useOnboardingKeyboard({
58
58
  submitAccountField, backPortfolio, continuePortfolio, setPortfolioOptionIdx, activeBrokerFields,
59
59
  brokerFieldIdx, setBrokerSelectIdx, brokerChoices, openBrokerGuide, chosenDesks, finishDesks,
60
60
  setDeskCursor, deskCursor, setChosenDesks, continueAccount, skipSetup, primaryUpgradeAction,
61
- setBillingInterval, keybindings, dialogOpen, commandBarOpen, saveProgressInBackground,
61
+ setBillingInterval, keybindings, dialogOpen, commandBarOpen, saveProgressInBackground, goBack,
62
62
  }: {
63
63
  stage: OnboardingStage;
64
64
  progress: OnboardingProgress;
@@ -102,6 +102,8 @@ export function useOnboardingKeyboard({
102
102
  dialogOpen: boolean;
103
103
  commandBarOpen: boolean;
104
104
  saveProgressInBackground: (patch: Partial<OnboardingProgress> & Pick<OnboardingProgress, "stage">, baseConfig?: AppConfig | undefined) => void;
105
+ /** Back on the card: to the step the user came from. */
106
+ goBack: () => void;
105
107
  }) {
106
108
  const activeSection: OnboardingSectionId = stage === "portfolio" || stage === "desks"
107
109
  ? "portfolio"
@@ -128,7 +130,8 @@ export function useOnboardingKeyboard({
128
130
 
129
131
  const handled = ((): boolean => {
130
132
  if (isPlainKey(event, "f10")) {
131
- if (stage === "portfolio" || stage === "desks" || isBrokerCommitting) return true;
133
+ // The first run needs a holding; a run from Settings can be left as it was.
134
+ if (isBrokerCommitting || ((stage === "portfolio" || stage === "desks") && !progress.rerun)) return true;
132
135
  if (stage === "upgrade" && !planAccess.hasProAccess) continueFree();
133
136
  else void finish(true);
134
137
  return true;
@@ -234,7 +237,7 @@ export function useOnboardingKeyboard({
234
237
  continueAccount();
235
238
  } else if (escape) {
236
239
  if (account.accountSub === "qr" || account.accountSub === "login") account.returnToAccountForm();
237
- else goToSection("portfolio");
240
+ else goBack();
238
241
  } else if (letter(BROWSER_SIGN_IN_KEY) && accountForm) {
239
242
  account.beginQrSignIn();
240
243
  } else if (letter(SKIP_SETUP_KEY)) {
@@ -251,7 +254,7 @@ export function useOnboardingKeyboard({
251
254
  if (enter) {
252
255
  primaryUpgradeAction();
253
256
  } else if (escape) {
254
- goToSection("cloud");
257
+ goBack();
255
258
  } else if (!planAccess.hasProAccess && letter(KEEP_FREE_KEY)) {
256
259
  continueFree();
257
260
  } else if (!planAccess.hasProAccess && (isPlainKey(event, "left") || letter("h"))) {
@@ -266,7 +269,7 @@ export function useOnboardingKeyboard({
266
269
 
267
270
  if (stage === "ready") {
268
271
  if (enter) void finish();
269
- else if (escape) goToSection(progress.accountStatus === "signed-in" ? "pro" : "cloud");
272
+ else if (escape) goBack();
270
273
  else return false;
271
274
  return true;
272
275
  }
@@ -20,7 +20,7 @@ import { t, tf } from "../../i18n";
20
20
  import { useAppLanguage } from "../../i18n/react";
21
21
  import type { PluginRegistry } from "../../plugins/registry";
22
22
  import { formatCloudPrice, monthsFreeYearly } from "../../account/billing";
23
- import { proStepCopy, proStepRealtimeTitle } from "../../account/upgrade-dialog";
23
+ import { proStepCopy } from "../../account/upgrade-dialog";
24
24
  import { Button, SegmentedControl, terminalButtonColumns } from "../ui";
25
25
  import { AccountStep, PortfolioStep } from "./onboarding-steps";
26
26
  import {
@@ -41,7 +41,7 @@ import { useOnboardingAccount } from "./wizard-account";
41
41
  import { useOnboardingBrokerSync } from "./wizard-broker-sync";
42
42
  import { useOnboardingPositions } from "./wizard-positions";
43
43
  import { DesksStep, toggleDeskChoice } from "./desks-step";
44
- import { getOnboardingProgress } from "./wizard-model";
44
+ import { getOnboardingProgress, isOnboardingWorkspaceBuilt, previousOnboardingStage } from "./wizard-model";
45
45
  import {
46
46
  useOnboardingKeyboard,
47
47
  SKIP_SETUP_KEY,
@@ -86,7 +86,22 @@ function useOnboardingWizard({ pluginRegistry, importBrokerPositions, onComplete
86
86
  const config = useAppSelector((state) => state.config);
87
87
  const progress = getOnboardingProgress(config);
88
88
  const stage = progress.stage;
89
+ const workspaceBuilt = isOnboardingWorkspaceBuilt(progress);
90
+ /** Opened again from Settings: any step can be left, and the first-run funnel is not told. */
91
+ const rerun = progress.rerun === true;
89
92
  const researchOpenedRef = useRef<string | null>(null);
93
+ /** The stages the user came through, oldest first, for Back to retrace. */
94
+ const visitedRef = useRef<OnboardingStage[]>([]);
95
+ const shownStageRef = useRef(stage);
96
+ const backTargetRef = useRef<OnboardingStage | null>(null);
97
+ useEffect(() => {
98
+ const left = shownStageRef.current;
99
+ if (left === stage) return;
100
+ shownStageRef.current = stage;
101
+ const back = backTargetRef.current === stage;
102
+ backTargetRef.current = null;
103
+ if (!back) visitedRef.current = [...visitedRef.current, left];
104
+ }, [stage]);
90
105
  const [editingField, setEditingField] = useState(false);
91
106
  /** The added position the keyboard acts on once no field is being typed in. */
92
107
  const [positionCursorSymbol, setPositionCursorSymbol] = useState<string | null>(null);
@@ -109,7 +124,7 @@ function useOnboardingWizard({ pluginRegistry, importBrokerPositions, onComplete
109
124
  // bar, so an unread inbox never stalls the first session.
110
125
  const account = useOnboardingAccount({
111
126
  nextStep: () => {
112
- recordResearchActivity("onboarding_signed_in");
127
+ if (!rerun) recordResearchActivity("onboarding_signed_in");
113
128
  saveProgressInBackground({ stage: "upgrade", accountStatus: "signed-in" });
114
129
  },
115
130
  setEditingField,
@@ -127,12 +142,12 @@ function useOnboardingWizard({ pluginRegistry, importBrokerPositions, onComplete
127
142
 
128
143
  useEffect(() => {
129
144
  const activity = STAGE_ACTIVITY[stage];
130
- if (activity) recordResearchActivity(activity);
131
- }, [stage]);
145
+ if (activity && !rerun) recordResearchActivity(activity);
146
+ }, [rerun, stage]);
132
147
 
133
148
  useEffect(() => {
134
- if (stage === "portfolio" && positionCount > 0) recordResearchActivity("onboarding_position_added");
135
- }, [positionCount, stage]);
149
+ if (stage === "portfolio" && positionCount > 0 && !rerun) recordResearchActivity("onboarding_position_added");
150
+ }, [positionCount, rerun, stage]);
136
151
 
137
152
  // The portfolio step opens straight into the ticker field.
138
153
  useEffect(() => {
@@ -197,8 +212,8 @@ function useOnboardingWizard({ pluginRegistry, importBrokerPositions, onComplete
197
212
  useEffect(() => {
198
213
  if (stage !== "research" || !progress.tickerSymbol || researchOpenedRef.current === progress.tickerSymbol) return;
199
214
  researchOpenedRef.current = progress.tickerSymbol;
200
- recordResearchActivity("ticker_saved");
201
- }, [stage, progress.tickerSymbol]);
215
+ if (!rerun) recordResearchActivity("ticker_saved");
216
+ }, [rerun, stage, progress.tickerSymbol]);
202
217
  const movePositionCursor = useCallback((delta: number) => {
203
218
  if (positionCursorIndex < 0) return;
204
219
  const next = Math.max(0, Math.min(positionCount - 1, positionCursorIndex + delta));
@@ -239,16 +254,17 @@ function useOnboardingWizard({ pluginRegistry, importBrokerPositions, onComplete
239
254
  : null;
240
255
  const helpFocused = focusedInstance?.paneId === "help";
241
256
  const {
242
- offer, pricing, billingInterval, setBillingInterval, planAccess, primaryUpgradeAction, continueFree, personalTickers,
257
+ offer, pricing, billingInterval, setBillingInterval, planAccess, primaryUpgradeAction, continueFree,
243
258
  } = useOnboardingUpgrade({
244
259
  stage, progress, saveProgressInBackground, persistProgress,
245
- shown: !helpFocused && !dialogOpen && !commandBarOpen,
246
260
  });
247
261
 
248
262
  const goToSection = useCallback((section: OnboardingSectionId) => {
249
263
  if (finishingRef.current || isBrokerCommitting) return;
250
264
  setEditingField(false);
251
265
  if (section === "portfolio") {
266
+ // Done for good once the workspace is built: picking it again would build it twice.
267
+ if (workspaceBuilt) return;
252
268
  setPortfolioSub("positions");
253
269
  saveProgressInBackground({ stage: "portfolio" });
254
270
  return;
@@ -260,10 +276,22 @@ function useOnboardingWizard({ pluginRegistry, importBrokerPositions, onComplete
260
276
  if (planAccess.signedIn) {
261
277
  saveProgressInBackground({ stage: "upgrade", accountStatus: "signed-in" });
262
278
  }
263
- }, [isBrokerCommitting, planAccess.signedIn, saveProgressInBackground]);
279
+ }, [isBrokerCommitting, planAccess.signedIn, saveProgressInBackground, workspaceBuilt]);
280
+
281
+ /** Back and Esc: to the step the user came from, never one that is done for good. */
282
+ const goBack = useCallback(() => {
283
+ if (finishingRef.current || isBrokerCommitting) return;
284
+ const back = previousOnboardingStage(progress, visitedRef.current, { pro: planAccess.hasProAccess });
285
+ if (!back) return;
286
+ visitedRef.current = back.visited;
287
+ backTargetRef.current = back.stage;
288
+ setEditingField(false);
289
+ if (back.stage === "portfolio") setPortfolioSub("positions");
290
+ saveProgressInBackground({ stage: back.stage });
291
+ }, [isBrokerCommitting, planAccess.hasProAccess, progress, saveProgressInBackground]);
264
292
 
265
293
  const sectionAvailability: Partial<Record<OnboardingSectionId, boolean>> = {
266
- portfolio: !isBrokerCommitting,
294
+ portfolio: !isBrokerCommitting && !workspaceBuilt,
267
295
  // The workspace is built when the desks are picked, so that step comes first.
268
296
  cloud: !isBrokerCommitting && stage !== "desks" && (stage === "account" || stage === "upgrade" || stage === "ready" || !!progress.tickerSymbol),
269
297
  pro: !isBrokerCommitting && planAccess.signedIn && (
@@ -282,7 +310,7 @@ function useOnboardingWizard({ pluginRegistry, importBrokerPositions, onComplete
282
310
  submitAccountField, backPortfolio, continuePortfolio, setPortfolioOptionIdx, activeBrokerFields,
283
311
  brokerFieldIdx, setBrokerSelectIdx, brokerChoices, openBrokerGuide, chosenDesks, finishDesks,
284
312
  setDeskCursor, deskCursor, setChosenDesks, continueAccount, skipSetup, primaryUpgradeAction,
285
- setBillingInterval, keybindings, dialogOpen, commandBarOpen, saveProgressInBackground,
313
+ setBillingInterval, keybindings, dialogOpen, commandBarOpen, saveProgressInBackground, goBack,
286
314
  });
287
315
 
288
316
  return {
@@ -295,7 +323,7 @@ function useOnboardingWizard({ pluginRegistry, importBrokerPositions, onComplete
295
323
  openBrokerConnect, continueFromPositions, backPortfolio, continuePortfolio, chosenDesks, deskCursor,
296
324
  setDeskCursor, setChosenDesks, buildingDesks, finishDesks, account, viewportWidth, viewportHeight, continueAccount,
297
325
  offer, planAccess, pricing, billingInterval, setBillingInterval, continueFree, primaryUpgradeAction,
298
- personalTickers,
326
+ goBack, rerun,
299
327
  };
300
328
  }
301
329
 
@@ -310,7 +338,7 @@ export function OnboardingWizard(props: OnboardingWizardProps) {
310
338
  openBrokerConnect, continueFromPositions, backPortfolio, continuePortfolio, chosenDesks, deskCursor,
311
339
  setDeskCursor, setChosenDesks, buildingDesks, finishDesks, account, viewportWidth, viewportHeight, continueAccount,
312
340
  offer, planAccess, pricing, billingInterval, setBillingInterval, continueFree, primaryUpgradeAction,
313
- personalTickers,
341
+ goBack, rerun,
314
342
  } = useOnboardingWizard(props);
315
343
 
316
344
  if (helpFocused) {
@@ -387,7 +415,8 @@ export function OnboardingWizard(props: OnboardingWizardProps) {
387
415
  onDismiss={skipSetup}
388
416
  dismissing={isFinishing}
389
417
  dismissDisabled={isBrokerCommitting}
390
- showDismiss={false}
418
+ // The first run needs a holding; a run from Settings can be left as it was.
419
+ showDismiss={rerun}
391
420
  />
392
421
  <OnboardingTitle
393
422
  step={desktop ? undefined : t("PORTFOLIO")}
@@ -467,12 +496,14 @@ export function OnboardingWizard(props: OnboardingWizardProps) {
467
496
  onNavigate={goToSection}
468
497
  onDismiss={skipSetup}
469
498
  dismissing={isFinishing}
470
- showDismiss={false}
499
+ dismissDisabled={buildingDesks}
500
+ showDismiss={rerun}
471
501
  />
472
502
  <OnboardingTitle
473
503
  step={desktop ? undefined : t("DESKS")}
474
- title={t("What do you trade?")}
475
- description={t("Each one adds a ready-made desk as a tab.")}
504
+ title={t("What do you trade most?")}
505
+ // A line of its own: the picks guide the workspace, they do not limit it.
506
+ description={`${t("Pick up to 3; each adds a ready-made tab.")}\n${t("Everything else is one search away.")}`}
476
507
  />
477
508
  {/* Not allowed to shrink: on a short window the card scrolls instead of the list running under the buttons. */}
478
509
  <DesksStep
@@ -594,7 +625,7 @@ export function OnboardingWizard(props: OnboardingWizardProps) {
594
625
  <OnboardingButton
595
626
  label="Back"
596
627
  variant="ghost"
597
- onPress={account.accountSub === "qr" || account.accountSub === "login" ? account.returnToAccountForm : () => goToSection("portfolio")}
628
+ onPress={account.accountSub === "qr" || account.accountSub === "login" ? account.returnToAccountForm : goBack}
598
629
  />
599
630
  {account.accountSub !== "qr" ? (
600
631
  <OnboardingButton
@@ -621,7 +652,7 @@ export function OnboardingWizard(props: OnboardingWizardProps) {
621
652
  : proCopy.note;
622
653
  // Ranked: the data itself first, then what reads it.
623
654
  const features = [
624
- { title: proStepRealtimeTitle(personalTickers), description: t("Free is 15 minutes behind on quotes and 12 hours on news.") },
655
+ { title: t("Real-time market data"), description: t("Free is 15 minutes behind on quotes and 12 hours on news.") },
625
656
  { title: t("MCP server"), description: t("Claude Code, Codex or Cursor call Gloom's research tools.") },
626
657
  { title: t("Ask Gloom"), description: t("Answers cite filings, calls and news.") },
627
658
  { title: t("Earnings calls"), description: t("Transcripts, summaries, guidance and scores.") },
@@ -734,7 +765,7 @@ export function OnboardingWizard(props: OnboardingWizardProps) {
734
765
  <OnboardingButton
735
766
  label="Back"
736
767
  variant="ghost"
737
- onPress={() => goToSection(progress.accountStatus === "signed-in" ? "pro" : "cloud")}
768
+ onPress={goBack}
738
769
  />
739
770
  <OnboardingButton
740
771
  label={isFinishing ? "Opening workspace..." : "Start exploring"}
@@ -7,6 +7,8 @@ import { formatMarketPrice } from "../../../market-data/market/format";
7
7
  import { getRegularSessionDisplay } from "../../../market-data/market/status";
8
8
  import { formatPercentRaw } from "../../../utils/format";
9
9
  import { Button, NumberField, TextField } from "../../ui";
10
+ import { DelayedMark } from "../../ui/delayed-mark";
11
+ import { useFreeQuoteDelay } from "../../../api-client/plan-access";
10
12
  import { ONBOARDING_DESKTOP } from "../onboarding-frame";
11
13
  import {
12
14
  POSITION_FIELDS,
@@ -31,7 +33,7 @@ const FIELD_LABELS: Record<PositionFieldId, string> = {
31
33
  const FIELD_PLACEHOLDERS: Record<Exclude<PositionFieldId, "currency">, string> = {
32
34
  ticker: "e.g. AAPL",
33
35
  shares: "optional",
34
- avgCost: "current price",
36
+ avgCost: "last price",
35
37
  };
36
38
 
37
39
  /** A field's label; the size counts contracts once the symbol is a future. */
@@ -47,6 +49,8 @@ function fieldPlaceholder(field: PositionFieldId, state: OnboardingPositionsStat
47
49
  /** Company name and last price for the symbol being typed, or why it will not resolve. */
48
50
  function PreviewLine({ preview, error }: Pick<OnboardingPositionsState, "preview" | "error">) {
49
51
  const colors = useThemeColors();
52
+ // Signed out or free, the price is the delayed one, and says so.
53
+ const delay = useFreeQuoteDelay(preview.status === "ready" ? preview.quote : null);
50
54
  if (error) return <Text fg={colors.negative} wrapText>{error}</Text>;
51
55
  if (preview.status === "idle") return null;
52
56
  if (preview.status === "checking") {
@@ -58,11 +62,12 @@ function PreviewLine({ preview, error }: Pick<OnboardingPositionsState, "preview
58
62
  const price = headline?.price;
59
63
  const change = headline?.changePercent;
60
64
  return (
61
- <Box flexDirection="row" minWidth={0} overflow="hidden">
65
+ <Box flexDirection="row" alignItems="center" minWidth={0} overflow="hidden">
62
66
  <Text fg={colors.text} attributes={TextAttributes.BOLD}>{preview.symbol}</Text>
63
67
  {preview.name ? <Text fg={colors.textDim}>{` ${preview.name}`}</Text> : null}
64
68
  {price != null ? <Text fg={colors.text}>{` ${formatMarketPrice(price, { maxWidth: 12 })}`}</Text> : null}
65
69
  {change != null ? <Text fg={shownPriceColor(change, 2, colors)}>{` ${formatPercentRaw(change)}`}</Text> : null}
70
+ {price != null && delay != null ? <Box flexShrink={0} paddingLeft={1}><DelayedMark minutes={delay} /></Box> : null}
66
71
  {preview.duplicate ? <Text fg={colors.textMuted}>{t(" already added")}</Text> : null}
67
72
  </Box>
68
73
  );
@@ -0,0 +1,24 @@
1
+ import { useCallback } from "react";
2
+ import { useAppDispatch, useAppStateRef } from "../../state/app/context";
3
+ import { saveConfigImmediately } from "../../state/config-save-scheduler";
4
+ import type { OnboardingProgress } from "../../types/config";
5
+
6
+ /**
7
+ * Settings' Run Setup Again: setup from its first step, on what the user has.
8
+ * The holdings are the portfolio's own, and the desks setup made start
9
+ * ticked; finishing rebuilds setup's layouts in place and leaves the user's
10
+ * alone. `open` shows the wizard once the progress is saved; without it
11
+ * (a host that cannot run setup) there is nothing to run.
12
+ */
13
+ export function useRunSetupAgain(open: (() => void) | undefined): (() => Promise<void>) | undefined {
14
+ const dispatch = useAppDispatch();
15
+ const stateRef = useAppStateRef();
16
+ const run = useCallback(async () => {
17
+ if (!open) return;
18
+ const progress: OnboardingProgress = { version: 1, stage: "portfolio", rerun: true };
19
+ await saveConfigImmediately({ ...stateRef.current.config, onboardingComplete: false, onboardingProgress: progress });
20
+ dispatch({ type: "SET_ONBOARDING_STATE", complete: false, progress });
21
+ open();
22
+ }, [dispatch, open, stateRef]);
23
+ return open ? run : undefined;
24
+ }