@gloom-sh/terminal 0.19.1 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -2,8 +2,8 @@ import { useCallback, useEffect, useRef, type MutableRefObject } from "react";
2
2
  import type { AppState } from "../../state/app/context";
3
3
  import type { TickerRecord } from "../../types/ticker";
4
4
  import { t } from "../../i18n";
5
- import { findExactTickerSearchMatch, listingChoiceQuery } from "../../tickers/search";
6
- import { isKnownExchangeCode, parsePublicTickerKey } from "../../utils/exchanges";
5
+ import { listingChoiceQuery } from "../../tickers/search";
6
+ import { isKnownExchangeCode } from "../../utils/exchanges";
7
7
  import type { PluginRegistry } from "../../plugins/registry";
8
8
  import type { Command } from "./commands/registry";
9
9
  import type {
@@ -294,37 +294,21 @@ export function useCommandBarSelectionRuntime({
294
294
  const activateListSelection = useCallback((options?: { secondary?: boolean; item?: ResultItem }) => {
295
295
  const listState = visibleListStateRef.current;
296
296
  if (!listState) return;
297
- // Enter submits the typed command until a row is deliberately chosen.
298
- // Search ranks suggestions by relevance; its first namesake must not
299
- // replace the listing the shared ticker resolver would open.
297
+ // Enter opens exactly the highlighted row. The typed command runs as typed
298
+ // only while no listing for it is highlighted: before its search answers
299
+ // (`DES HFC` then opens the listing `quote HFC` reads), or while the rows
300
+ // on screen are an earlier query's.
300
301
  const queryChanged = rootQueryRef.current !== rootQuery;
301
302
  const tickerArgument = rootShortcutIntent.kind !== "none" && rootShortcutIntent.argKind === "ticker"
302
303
  ? rootShortcutIntent.argText : null;
303
304
  const listingQuery = listingChoiceQuery(tickerArgument ?? rootQueryRef.current);
304
305
  const choosingVenue = listingQuery && !isKnownExchangeCode(listingQuery.suffix);
305
- const submitTyped = !options?.item && !currentRoute
306
+ const highlighted = listState.results[listState.selectedIdx];
307
+ const listingHighlighted = !queryChanged && (highlighted?.kind === "ticker" || highlighted?.kind === "search");
308
+ const submitTyped = !options?.item && !currentRoute && !listingHighlighted
306
309
  && (queryChanged || (!rootSelectionNavigatedRef.current && !choosingVenue));
307
- let typed = submitTyped && (queryChanged || tickerArgument !== null)
310
+ const typed = submitTyped && (queryChanged || tickerArgument !== null)
308
311
  ? resolveImmediateRootSelection(rootQueryRef.current) : null;
309
- const first = listState.results[listState.selectedIdx];
310
- if (typed && !queryChanged && rootShortcutIntent.kind !== "none" && rootShortcutIntent.argKind === "ticker"
311
- && first && (first.kind === "ticker" || first.kind === "search")) {
312
- const argument = rootShortcutIntent.argText;
313
- // Company names and partial symbols still accept the suggested match.
314
- // An exact symbol or named venue goes through listing resolution.
315
- if (!parsePublicTickerKey(argument).exchange && !findExactTickerSearchMatch(
316
- listState.results.filter((item) => item.kind === "ticker" || item.kind === "search"), argument,
317
- )) typed = null;
318
- }
319
- // A plain symbol follows the same path once it is the default ticker row.
320
- // Commands and company-name searches keep their own default actions.
321
- const plainSymbol = submitTyped && !typed && first && (first.kind === "ticker" || first.kind === "search")
322
- && findExactTickerSearchMatch([first], rootQueryRef.current);
323
- if (plainSymbol && first) {
324
- const symbol = rootQueryRef.current;
325
- typed = { ...first, action: () => runSecurityDescriptionShortcut(symbol),
326
- secondaryAction: () => runSecurityDescriptionShortcut(symbol, { forceNewPane: true }) };
327
- }
328
312
  const selected = options?.item ?? typed ?? listState.results[listState.selectedIdx];
329
313
  if (!selected) {
330
314
  if (searchPending && !currentRoute && !options?.item && !rootSelectionNavigatedRef.current) {
@@ -399,8 +383,8 @@ export function useCommandBarSelectionRuntime({
399
383
  item: selected,
400
384
  query,
401
385
  rank: selected === typed ? 0 : Math.max(0, listState.results.indexOf(selected)),
402
- // A plain symbol stays a ticker choice; a submitted command is the shortcut itself.
403
- isShortcut: () => !plainSymbol && (selected === typed || resolveImmediateRootSelection(query)?.id === selected.id),
386
+ // A listing row stays a ticker choice; a submitted command is the shortcut itself.
387
+ isShortcut: () => !listingHighlighted && (selected === typed || resolveImmediateRootSelection(query)?.id === selected.id),
404
388
  }, run);
405
389
  }, [
406
390
  closeAll,
@@ -416,7 +400,6 @@ export function useCommandBarSelectionRuntime({
416
400
  rootSelectionNavigatedRef,
417
401
  rootShortcutIntent,
418
402
  runRootRow,
419
- runSecurityDescriptionShortcut,
420
403
  searchPending,
421
404
  stateConfigLayout,
422
405
  stateRef,
@@ -44,6 +44,8 @@ interface CommandBarProps {
44
44
  pluginRegistry: PluginRegistry;
45
45
  quitApp: () => void;
46
46
  onCheckForUpdates?: () => void | Promise<void>;
47
+ /** Settings' Run Setup Again; without it (the browser, or setup already showing) the command is left out. */
48
+ onRunSetupAgain?: () => Promise<void>;
47
49
  onNativeOccluderChange?: (rect: LayoutBounds | null) => void;
48
50
  }
49
51
 
@@ -53,6 +55,7 @@ export function CommandBar({
53
55
  pluginRegistry,
54
56
  quitApp,
55
57
  onCheckForUpdates,
58
+ onRunSetupAgain,
56
59
  onNativeOccluderChange,
57
60
  }: CommandBarProps) {
58
61
  const {
@@ -80,9 +83,11 @@ export function CommandBar({
80
83
  visibleListStateRef,
81
84
  } = useCommandBarEnvironment();
82
85
  // Without an updater (the web terminal) neither checking nor a channel means anything.
83
- const availableCommands = useMemo(() => onCheckForUpdates
84
- ? allAvailableCommands
85
- : allAvailableCommands.filter((command) => command.id !== "check-for-updates" && command.id !== "release-channel"), [allAvailableCommands, onCheckForUpdates]);
86
+ const availableCommands = useMemo(() => allAvailableCommands.filter((command) => {
87
+ if (command.id === "check-for-updates" || command.id === "release-channel") return !!onCheckForUpdates;
88
+ if (command.id === "run-setup-again") return !!onRunSetupAgain;
89
+ return true;
90
+ }), [allAvailableCommands, onCheckForUpdates, onRunSetupAgain]);
86
91
  const {
87
92
  applyThemePreview,
88
93
  clearThemePreview,
@@ -200,6 +205,7 @@ export function CommandBar({
200
205
  dispatch,
201
206
  focusedPaneId: state.focusedPaneId,
202
207
  onCheckForUpdates,
208
+ onRunSetupAgain,
203
209
  persistConfig,
204
210
  pluginRegistry,
205
211
  pushRoute,
@@ -84,6 +84,7 @@ export function CompanyLogo({
84
84
  alignItems="center"
85
85
  justifyContent="center"
86
86
  objectFit="contain"
87
+ fallbackWhileLoading
87
88
  style={{
88
89
  width: cellWidthPx * width,
89
90
  height: cellHeightPx * height,
@@ -91,7 +92,7 @@ export function CompanyLogo({
91
92
  ...style,
92
93
  }}
93
94
  >
94
- {/* Without a logo the initial fills the same tile, so the header keeps its shape. */}
95
+ {/* Without a logo, or until it loads, the initial fills the same tile, so the header keeps its shape. */}
95
96
  <Box width="100%" height="100%" bg={colors.border} alignItems="center" justifyContent="center">
96
97
  <Text
97
98
  fg={colors.text}
@@ -170,9 +170,9 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
170
170
  fireAndForget(desktopWindowBridge.closeDetachedPane?.(desktopWindowBridge.paneId), "desktop.closeDetachedPane");
171
171
  }, [desktopWindowBridge, locked, pluginRegistry]);
172
172
 
173
- const startWindowDrag = useCallback(() => {
173
+ const startWindowDrag = useCallback((press?: { detail?: number }) => {
174
174
  focusPane();
175
- if (nativeWindowChrome) void rendererHost.startWindowDrag?.();
175
+ if (nativeWindowChrome) void rendererHost.startWindowDrag?.(press);
176
176
  }, [focusPane, nativeWindowChrome, rendererHost]);
177
177
 
178
178
  const sharePane = useCallback(() => {
@@ -36,6 +36,7 @@ import { resolveHeaderPromptGeometry } from "./shell/chrome";
36
36
  import { useWindowFullscreen } from "./window-fullscreen";
37
37
  import { WindowControls, WINDOWS_CONTROL_GROUP_WIDTH_PX } from "./window-controls";
38
38
  import { Button, terminalButtonColumns } from "../ui/button";
39
+ import { DelayedMark, delayedMarkWidth } from "../ui/delayed-mark";
39
40
 
40
41
  const UPDATE_NOTICE_DURATION_MS = 5_000;
41
42
  /** Kept clear after the terminal update status, so its last character never touches the Help pill. */
@@ -768,7 +769,10 @@ function HeaderMarketSummary({ nativePaneChrome, width }: { nativePaneChrome: bo
768
769
  available: width,
769
770
  baseCurrencyWidth: displayWidth(summary.baseCurrencyLabel) + 1,
770
771
  countdownWidth: summary.marketLabel.length - summary.marketLabelShort.length,
771
- spyWidth: summary.spyText.length + 1,
772
+ // A delayed SPY keeps its mark: the two fit, or give way, together. Once the
773
+ // rest of the cluster is gone, it gives up its move rather than give way.
774
+ spyWidth: summary.spyText.length + 1 + (summary.spyDelayMinutes != null ? delayedMarkWidth(summary.spyDelayMinutes, nativePaneChrome) + 1 : 0),
775
+ spyMoveWidth: summary.spyDelayMinutes != null ? summary.spyText.length - summary.spyPriceText.length : 0,
772
776
  stateWidth: summary.marketLabelShort ? summary.marketLabelShort.length + 1 : 0,
773
777
  });
774
778
  const marketLabel = fit.showCountdown ? summary.marketLabel : summary.marketLabelShort;
@@ -783,8 +787,9 @@ function HeaderMarketSummary({ nativePaneChrome, width }: { nativePaneChrome: bo
783
787
  </Box>
784
788
  ) : null}
785
789
  {fit.showSpy ? (
786
- <Box paddingRight={1} flexShrink={0} data-gloom-role="header-market">
787
- <Text fg={summary.spyColor}>{summary.spyText}</Text>
790
+ <Box paddingRight={1} flexShrink={0} flexDirection="row" alignItems="center" gap={1} data-gloom-role="header-market">
791
+ <Text fg={summary.spyColor}>{fit.showSpyMove ? summary.spyText : summary.spyPriceText}</Text>
792
+ {summary.spyDelayMinutes != null ? <DelayedMark minutes={summary.spyDelayMinutes} /> : null}
788
793
  </Box>
789
794
  ) : null}
790
795
  {fit.showBaseCurrency ? (
@@ -870,9 +875,9 @@ export function Header({
870
875
  // bar itself wears the prompt's fill and reads as a single strip.
871
876
  const headerBg = nativePaneChrome ? colors.header : headerSurface(colors);
872
877
 
873
- const startWindowDrag = useCallback(() => {
878
+ const startWindowDrag = useCallback((press?: { detail?: number }) => {
874
879
  if (!titleBarOverlay || !nativeWindowChrome) return;
875
- void rendererHost.startWindowDrag?.();
880
+ void rendererHost.startWindowDrag?.(press);
876
881
  }, [nativeWindowChrome, rendererHost, titleBarOverlay]);
877
882
 
878
883
  const openCommandBar = useCallback((event?: HeaderActionEvent) => {
@@ -12,6 +12,7 @@ import { formatPercentRaw } from "../../utils/format";
12
12
  import { formatMarketPriceWithCurrency, liveQuoteFormatOptions } from "../../market-data/market/format";
13
13
  import { getSessionMoveDisplay, marketStateColor, marketStateCountdown, marketStateLabel } from "../../market-data/market/status";
14
14
  import type { Quote } from "../../types/financials";
15
+ import { useFreeQuoteDelay } from "../../api-client/plan-access";
15
16
 
16
17
  /**
17
18
  * SPY rides the shared feed. The snapshot poll only runs while the feed has
@@ -42,6 +43,10 @@ export interface MarketSummary {
42
43
  marketLabelShort: string;
43
44
  spyColor: string;
44
45
  spyText: string;
46
+ /** `spyText` without its move (`SPY $762.43`), for a delayed SPY in a header too narrow for both and the mark. */
47
+ spyPriceText: string;
48
+ /** Minutes SPY's price is late for this account (signed out or free), drawn as the delayed mark beside it; null when live. */
49
+ spyDelayMinutes: number | null;
45
50
  }
46
51
 
47
52
  export interface MarketSummaryFit {
@@ -49,6 +54,8 @@ export interface MarketSummaryFit {
49
54
  showCountdown: boolean;
50
55
  showState: boolean;
51
56
  showSpy: boolean;
57
+ /** SPY keeps its move; false when it gave the move up to stay (`spyMoveWidth`). */
58
+ showSpyMove: boolean;
52
59
  }
53
60
 
54
61
  /**
@@ -57,12 +64,16 @@ export interface MarketSummaryFit {
57
64
  * base currency, then the countdown suffix that widens the label. A narrowing
58
65
  * header therefore sheds the countdown, then the currency, then the state, and
59
66
  * keeps SPY longest. `countdownWidth` is what the suffix adds to the label.
67
+ * `spyMoveWidth` is what SPY's move adds to `spyWidth`, for a SPY that gives
68
+ * its move up before it gives way: a delayed SPY, whose mark makes it wider,
69
+ * so a narrow header still shows it and says it is late.
60
70
  */
61
71
  export function resolveMarketSummaryFit(options: {
62
72
  available: number;
63
73
  baseCurrencyWidth: number;
64
74
  countdownWidth: number;
65
75
  spyWidth: number;
76
+ spyMoveWidth?: number;
66
77
  stateWidth: number;
67
78
  }): MarketSummaryFit {
68
79
  let remaining = options.available;
@@ -71,11 +82,13 @@ export function resolveMarketSummaryFit(options: {
71
82
  remaining -= width;
72
83
  return true;
73
84
  };
74
- const showSpy = take(options.spyWidth);
85
+ const spyMoveWidth = options.spyMoveWidth ?? 0;
86
+ const showSpyMove = take(options.spyWidth);
87
+ const showSpy = showSpyMove || (spyMoveWidth > 0 && take(options.spyWidth - spyMoveWidth));
75
88
  const showState = take(options.stateWidth);
76
89
  const showBaseCurrency = take(options.baseCurrencyWidth);
77
90
  const showCountdown = showState && take(options.countdownWidth);
78
- return { showBaseCurrency, showCountdown, showState, showSpy };
91
+ return { showBaseCurrency, showCountdown, showState, showSpy, showSpyMove: showSpy && showSpyMove };
79
92
  }
80
93
 
81
94
  /**
@@ -87,15 +100,17 @@ export function resolveMarketSummaryFit(options: {
87
100
  * The price carries its currency's sign ($), so the base currency label
88
101
  * after it is never read as its unit.
89
102
  */
90
- export function spySummary(quote: Quote | null | undefined, colors: ThemeColors): Pick<MarketSummary, "spyColor" | "spyText"> {
103
+ export function spySummary(quote: Quote | null | undefined, colors: ThemeColors): Pick<MarketSummary, "spyColor" | "spyPriceText" | "spyText"> {
91
104
  const display = getSessionMoveDisplay(quote);
92
105
  // Fixed decimals and a percent slot as wide as "+0.53%", so a streamed tick
93
106
  // never slides the market-state label or re-fits the cluster.
107
+ const spyPriceText = display
108
+ ? `SPY ${formatMarketPriceWithCurrency(display.price, quote?.currency || "USD", liveQuoteFormatOptions(quote, quote?.currency, "ETF"))}`
109
+ : "SPY —";
94
110
  return {
95
111
  spyColor: display?.change != null ? priceColor(display.change, colors) : colors.textDim,
96
- spyText: display
97
- ? `SPY ${formatMarketPriceWithCurrency(display.price, quote?.currency || "USD", liveQuoteFormatOptions(quote, quote?.currency, "ETF"))} ${formatPercentRaw(display.changePercent).padStart(6)}`
98
- : "SPY —",
112
+ spyPriceText,
113
+ spyText: display ? `${spyPriceText} ${formatPercentRaw(display.changePercent).padStart(6)}` : spyPriceText,
99
114
  };
100
115
  }
101
116
 
@@ -135,7 +150,8 @@ export function useMarketSummary(): MarketSummary {
135
150
  return () => { clearInterval(id); };
136
151
  }, [appActive]);
137
152
 
138
- const { spyColor, spyText } = spySummary(spyQuote, colors);
153
+ const { spyColor, spyPriceText, spyText } = spySummary(spyQuote, colors);
154
+ const spyDelayMinutes = useFreeQuoteDelay(spyQuote);
139
155
 
140
156
  const mktCountdown = mktState ? marketStateCountdown(mktState, now) : null;
141
157
  const marketLabelShort = mktState ? t(marketStateLabel(mktState)) : "";
@@ -149,6 +165,8 @@ export function useMarketSummary(): MarketSummary {
149
165
  marketLabel,
150
166
  marketLabelShort,
151
167
  spyColor,
168
+ spyPriceText,
152
169
  spyText,
170
+ spyDelayMinutes,
153
171
  };
154
172
  }
@@ -24,11 +24,13 @@ import {
24
24
  } from "./model";
25
25
 
26
26
  export {
27
+ ageFooterSegment,
27
28
  companyDataFooterSegment,
28
29
  datesPaneData,
29
30
  DELAYED_SEGMENT,
30
31
  delayedFooterSegment,
31
32
  hasPaneFooterContent,
33
+ savedCopyFooterInfo,
32
34
  savedCopyFooterSegment,
33
35
  splitDelayedStatus,
34
36
  type CombinedPaneFooter,
@@ -79,10 +81,12 @@ function terminalSegmentKey(segment: PaneFooterSegment, focused: boolean): strin
79
81
  : "";
80
82
  }
81
83
 
82
- /** Columns an icon segment keeps when hints crowd the footer. */
84
+ /** Columns an icon segment keeps when hints crowd the footer: the icon, and on the desktop its `iconText` after it. */
83
85
  function iconSegmentReserve(segment: PaneFooterSegment, focused: boolean, nativePaneChrome: boolean): number {
84
86
  if (!segment.icon) return 0;
85
- return nativePaneChrome ? 3 : Math.max(3, 1 + terminalSegmentKey(segment, focused).length);
87
+ return nativePaneChrome
88
+ ? 3 + (segment.iconText ? segment.iconText.length + 1 : 0)
89
+ : Math.max(3, 1 + terminalSegmentKey(segment, focused).length);
86
90
  }
87
91
 
88
92
  function SegmentView({ segment, focused }: { segment: PaneFooterSegment; focused: boolean }) {
@@ -123,6 +127,7 @@ function SegmentView({ segment, focused }: { segment: PaneFooterSegment; focused
123
127
  data-icon={segment.icon}
124
128
  >
125
129
  <Icon name={segment.icon} size={14} color={segment.disabled ? colors.textMuted : footerToneColor(segment.parts[0] ?? { text: "" })} />
130
+ {segment.iconText ? <Text fg={colors.textMuted} selectable={false} data-gloom-role="pane-footer-icon-text">{segment.iconText}</Text> : null}
126
131
  </Box>
127
132
  );
128
133
  }
@@ -139,6 +144,7 @@ function SegmentView({ segment, focused }: { segment: PaneFooterSegment; focused
139
144
  disabled={!interactive}
140
145
  color={segment.disabled ? colors.textMuted : footerToneColor(segment.parts[0] ?? { text: "" })}
141
146
  onPress={() => segment.onPress?.()}
147
+ text={segment.iconText}
142
148
  />
143
149
  );
144
150
  }
@@ -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,21 @@ 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;
68
+ /**
69
+ * When the copy the pane shows was saved, on the saved-copy segment
70
+ * ({@link savedCopyFooterSegment}). The footer draws that segment first,
71
+ * wherever a pane registers it, and once: the oldest when several parts of
72
+ * the pane show a saved copy.
73
+ */
74
+ savedCopyAt?: number;
54
75
  /**
55
76
  * Shorter forms of `parts`, longest first. When the row is too narrow for
56
77
  * `parts` beside the hints, it shows the first that fits, or the last, and
@@ -68,10 +89,11 @@ export interface PaneFooterSegment {
68
89
  const DELAYED_FOOTER_GLYPH = "◷";
69
90
 
70
91
  /**
71
- * The footer's one form for data a source delivers late: a yellow clock with
72
- * `detail` in its tooltip on the desktop, and the clock with the delay
73
- * (`◷15m`) in the terminal, which has no tooltip, so it takes a few cells
74
- * 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.
75
97
  */
76
98
  export function delayedFooterSegment({
77
99
  id,
@@ -94,6 +116,7 @@ export function delayedFooterSegment({
94
116
  label: detail,
95
117
  title: detail,
96
118
  parts: [{ text: `${DELAYED_FOOTER_GLYPH}${delay ?? ""}`, tone: "warning" }],
119
+ ...(delay ? { iconText: delay } : {}),
97
120
  ...(onPress ? { onPress } : {}),
98
121
  ...(quoteStatus ? { quoteStatus } : {}),
99
122
  };
@@ -113,12 +136,36 @@ export function savedCopyFooterSegment(id: string, savedAt: number, now = Date.n
113
136
  return {
114
137
  id,
115
138
  quoteStatus: true,
139
+ savedCopyAt: savedAt,
116
140
  title: `${t("Couldn't refresh")}: ${text}`,
117
141
  parts: [{ text, tone: "warning" }],
118
142
  variants: [[{ text: t("saved copy"), tone: "warning" }]],
119
143
  };
120
144
  }
121
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
+
159
+ /**
160
+ * The saved-copy segment of a pane showing several answers (a list and the
161
+ * report it opens): the oldest of the copies among `savedAts` (ms), or no
162
+ * segment when none of them is one.
163
+ */
164
+ export function savedCopyFooterInfo(savedAts: readonly (number | null | undefined)[], id = "saved-copy"): PaneFooterSegment[] {
165
+ const times = savedAts.filter((at): at is number => typeof at === "number" && Number.isFinite(at));
166
+ return times.length > 0 ? [savedCopyFooterSegment(id, Math.min(...times))] : [];
167
+ }
168
+
122
169
  /**
123
170
  * The footer's one form for company data a pane shows from another listing
124
171
  * of the company: a receipt or a foreign line (SKHY, HY9H in Frankfurt) reads
@@ -299,7 +346,8 @@ function trimTrailing(info: PaneFooterSegment[], width: number, keep: number): P
299
346
  }
300
347
 
301
348
  /**
302
- * 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
303
351
  * together, one variant at a time, until the row fits or none is left; beside
304
352
  * the hints, the trailing status that still does not fit is then cut or left
305
353
  * out (trimTrailing). A status after every segment with shorter forms gives
@@ -310,6 +358,10 @@ function trimTrailing(info: PaneFooterSegment[], width: number, keep: number): P
310
358
  * so a frame that fits its own status line sees it overflow.
311
359
  */
312
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
+ }
313
365
  if (!besideHints) return stepDownTogether(info, width);
314
366
  const lastWithVariants = info.findLastIndex((segment) => (segment.variants?.length ?? 0) > 0);
315
367
  if (lastWithVariants >= 0 && lastWithVariants < info.length - 1 && infoTextWidth(info) > width) {
@@ -450,7 +502,20 @@ export function combinePaneFooterRegistrations(registrations: Map<string, PaneFo
450
502
  const shown = yielded.length > 0 ? hints.filter((hint) => !yielded.includes(hint)) : hints;
451
503
 
452
504
  if (info.length === 0 && shown.length === 0 && menu.length === 0 && keys.length === 0) return EMPTY_FOOTER;
453
- return { info, hints: shown, menu, keys };
505
+ return { info: savedCopyFirst(info), hints: shown, menu, keys };
506
+ }
507
+
508
+ /**
509
+ * The saved-copy segment in its one place: first, so it reads the same in
510
+ * every pane and is the last a narrow row cuts, and once, the oldest copy,
511
+ * when a list and the report it opens each register one.
512
+ */
513
+ function savedCopyFirst(info: PaneFooterSegment[]): PaneFooterSegment[] {
514
+ let oldest: PaneFooterSegment | undefined;
515
+ for (const segment of info) {
516
+ if (segment.savedCopyAt != null && (oldest?.savedCopyAt == null || segment.savedCopyAt < oldest.savedCopyAt)) oldest = segment;
517
+ }
518
+ return oldest ? [oldest, ...info.filter((segment) => segment.savedCopyAt == null)] : info;
454
519
  }
455
520
 
456
521
  function sameFooterParts(left: PaneFooterPart[], right: PaneFooterPart[]): boolean {
@@ -507,6 +572,8 @@ export function samePaneFooterRegistration(
507
572
  && segment.shortcut === other.shortcut
508
573
  && !!segment.onPress === !!other.onPress
509
574
  && segment.disabled === other.disabled
575
+ && segment.savedCopyAt === other.savedCopyAt
576
+ && segment.age === other.age
510
577
  && sameFooterParts(segment.parts, other.parts)
511
578
  && (segment.variants?.length ?? 0) === (other.variants?.length ?? 0)
512
579
  && (segment.variants ?? []).every((variant, variantIndex) => sameFooterParts(variant, other.variants![variantIndex]!));
@@ -178,13 +178,13 @@ export function PaneHeader({
178
178
  event.stopPropagation?.();
179
179
  onHeaderMouseDown?.(event);
180
180
  };
181
- const beginWindowDrag = (event?: { stopPropagation?: () => void }) => {
181
+ const beginWindowDrag = (event?: { stopPropagation?: () => void; detail?: number }) => {
182
182
  event?.stopPropagation?.();
183
183
  if (titleBar) {
184
184
  onHeaderMouseDown?.(event);
185
185
  return;
186
186
  }
187
- void rendererHost.startWindowDrag?.();
187
+ void rendererHost.startWindowDrag?.(event);
188
188
  };
189
189
  const barHandlers = windowDrag
190
190
  ? { onMouseDown: beginWindowDrag }
@@ -11,7 +11,8 @@ const EMPTY_STATUS_INFO: PaneFooterSegment[] = [];
11
11
  // has no per-pane footer hint. Do not reintroduce one. See PR #589. Panes bind
12
12
  // it with `usePaneRefreshKey`.
13
13
 
14
- const STALE_SEGMENT: PaneFooterSegment = { id: "stale", parts: [{ text: "stale", tone: "warning" }] };
14
+ /** The shared warning for data on screen older than it should be, for a pane composing its own footer. */
15
+ export const STALE_SEGMENT: PaneFooterSegment = { id: "stale", parts: [{ text: "stale", tone: "warning" }] };
15
16
 
16
17
  interface PaneStatusInfoOptions {
17
18
  loading?: boolean;
@@ -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}