@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
@@ -19,9 +19,63 @@ const LEGACY_STAGES: Partial<Record<OnboardingStage, OnboardingStage>> = {
19
19
  };
20
20
 
21
21
  export function getOnboardingProgress(config: AppConfig): OnboardingProgress {
22
- const progress = config.onboardingProgress ?? { version: 1, stage: "portfolio" };
23
- const stage = LEGACY_STAGES[progress.stage];
24
- return stage ? { ...progress, stage } : progress;
22
+ const saved = config.onboardingProgress ?? { version: 1, stage: "portfolio" };
23
+ const legacy = LEGACY_STAGES[saved.stage];
24
+ const progress = legacy ? { ...saved, stage: legacy } : saved;
25
+ // Older builds sent Back from Gloom Cloud to the portfolio after the
26
+ // workspace was built; such a profile resumes on the workspace.
27
+ return isOnboardingStageFinished(progress, progress.stage) ? { ...progress, stage: "research" } : progress;
28
+ }
29
+
30
+ /** Stages the wizard reaches only once the workspace is built. */
31
+ const WORKSPACE_STAGES: ReadonlySet<OnboardingStage> = new Set(["research", "account", "upgrade", "ready"]);
32
+
33
+ /**
34
+ * Whether the workspace is built around a holding: the portfolio and desk
35
+ * steps are finished. Older builds saved no desks, so a later stage with a
36
+ * holding counts too.
37
+ */
38
+ export function isOnboardingWorkspaceBuilt(progress: OnboardingProgress): boolean {
39
+ return !!progress.tickerSymbol && (Array.isArray(progress.desks) || WORKSPACE_STAGES.has(progress.stage));
40
+ }
41
+
42
+ /**
43
+ * A step that is done for good: the portfolio and desk steps once the
44
+ * workspace is built (building twice would add every desk twice), and Pro
45
+ * for an account that has it. The wizard shows them as done and never asks
46
+ * them again.
47
+ */
48
+ function isOnboardingStageFinished(progress: OnboardingProgress, stage: OnboardingStage, pro = false): boolean {
49
+ if (stage === "upgrade") return pro;
50
+ return (stage === "portfolio" || stage === "desks") && isOnboardingWorkspaceBuilt(progress);
51
+ }
52
+
53
+ /**
54
+ * Where Back (and Esc) goes: the step the user came from, passing over any
55
+ * that is finished for good (`pro`: the account has Pro). With no way back
56
+ * recorded (the app restarted on this step), the step before it in the
57
+ * flow. `visited` is the stages the user came through, oldest first; what
58
+ * comes back is what is left of it. Null when the step has no way back.
59
+ */
60
+ export function previousOnboardingStage(
61
+ progress: OnboardingProgress,
62
+ visited: readonly OnboardingStage[],
63
+ { pro = false }: { pro?: boolean } = {},
64
+ ): { stage: OnboardingStage; visited: OnboardingStage[] } | null {
65
+ for (let index = visited.length - 1; index >= 0; index -= 1) {
66
+ const stage = visited[index]!;
67
+ if (stage !== progress.stage && !isOnboardingStageFinished(progress, stage, pro)) {
68
+ return { stage, visited: visited.slice(0, index) };
69
+ }
70
+ }
71
+ const stage: OnboardingStage | null = progress.stage === "account"
72
+ ? isOnboardingWorkspaceBuilt(progress) ? "research" : "portfolio"
73
+ : progress.stage === "upgrade"
74
+ ? "account"
75
+ : progress.stage === "ready"
76
+ ? progress.accountStatus === "signed-in" && !pro ? "upgrade" : "account"
77
+ : null;
78
+ return stage ? { stage, visited: [] } : null;
25
79
  }
26
80
 
27
81
  export function withOnboardingProgress(
@@ -302,7 +302,7 @@ export function useOnboardingPositions({
302
302
  const costBasis = avgCost ?? result.quote?.price;
303
303
  if (costBasis === undefined || costBasis === null || !Number.isFinite(costBasis)) {
304
304
  setFieldIdx(2);
305
- setError(t("No live price yet. Enter the average cost."));
305
+ setError(t("No price yet. Enter the average cost."));
306
306
  return false;
307
307
  }
308
308
  // A future is entered in contracts, sized by its terms; a cost typed as corn is quoted counts cents.
@@ -0,0 +1,56 @@
1
+ import { Box, Text, useUiCapabilities } from "../../ui";
2
+ import { useThemeColors } from "../../theme/theme-context";
3
+ import { tf } from "../../i18n";
4
+ import { displayWidth } from "../../utils/format";
5
+ import { ICON_GLYPHS, Icon } from "./icon";
6
+
7
+ /** The terminal's form of the mark: the clock and the delay, as the pane footer draws it (`◷15m`). */
8
+ function delayedMarkText(minutes: number): string {
9
+ return `${ICON_GLYPHS.clock}${minutes}m`;
10
+ }
11
+
12
+ /** The delay the mark spells out after its clock: `15m`. */
13
+ function delayedMarkDelay(minutes: number): string {
14
+ return `${minutes}m`;
15
+ }
16
+
17
+ /**
18
+ * Cells the mark takes: `◷15m` in the terminal; on the desktop and the web the
19
+ * clock icon and its gap (12px and 4px, three cells at the narrowest cell) and `15m`.
20
+ */
21
+ export function delayedMarkWidth(minutes: number, nativePaneChrome: boolean): number {
22
+ return nativePaneChrome ? 3 + displayWidth(delayedMarkDelay(minutes)) : displayWidth(delayedMarkText(minutes));
23
+ }
24
+
25
+ /**
26
+ * The one mark for a price shown late, beside the price: what the pane
27
+ * footer's clock is for a pane's data (`delayedFooterSegment`), for a price
28
+ * outside a footer (the header's SPY, the status bar, a quote's headline).
29
+ * It reads without hovering: a yellow clock and the delay (`15m`) in the dim
30
+ * tone of the meta text around it on the desktop and the web, `◷15m` in the
31
+ * terminal, with `15m delayed` in its tooltip. Callers decide when a price is
32
+ * late with `freeQuoteDelayMinutes` (or `useFreeQuoteDelay`); it draws nothing else.
33
+ */
34
+ export function DelayedMark({ minutes }: { minutes: number }) {
35
+ const colors = useThemeColors();
36
+ const { nativePaneChrome } = useUiCapabilities();
37
+ const detail = tf("{delay} delayed", { delay: `${minutes}m` });
38
+ if (!nativePaneChrome) {
39
+ return <Text fg={colors.warning} flexShrink={0} selectable={false} aria-label={detail}>{delayedMarkText(minutes)}</Text>;
40
+ }
41
+ return (
42
+ <Box
43
+ role="img"
44
+ aria-label={detail}
45
+ title={detail}
46
+ flexShrink={0}
47
+ flexDirection="row"
48
+ alignItems="center"
49
+ data-gloom-role="delayed-mark"
50
+ style={{ gap: "var(--gloom-space-xs)" }}
51
+ >
52
+ <Icon name="clock" color={colors.warning} />
53
+ <Text fg={colors.textDim} selectable={false}>{delayedMarkDelay(minutes)}</Text>
54
+ </Box>
55
+ );
56
+ }
@@ -214,8 +214,18 @@ export interface BadgeProps {
214
214
  export const NATIVE_BADGE_STYLE = { height: "calc(100% - 2px)", borderRadius: "var(--gloom-radius-sm)" };
215
215
  /** How far a subtle chip's fill is tinted toward its tone. */
216
216
  export const BADGE_TINT = 0.28;
217
- const NATIVE_BADGE_TEXT_STYLE = { fontWeight: "var(--gloom-weight-regular)" };
218
217
  const NATIVE_ICON_BADGE_STYLE = { ...NATIVE_BADGE_STYLE, gap: "var(--gloom-space-xs)" };
218
+ const NATIVE_BADGE_TEXT_STYLE = { fontWeight: "var(--gloom-weight-regular)" };
219
+ /**
220
+ * A label with lowercase letters has them in the middle of the chip,
221
+ * wherever the font puts them in its line: the line is cut to x-height and
222
+ * baseline, and that is what the chip centres. Centred whole, SF Mono's
223
+ * lowercase sit a pixel and a half low. An icon centres in the chip, so it
224
+ * reads on the letters' line. A label in capitals and digits keeps the
225
+ * whole line, as ticker chips do; a browser without `text-box` keeps it too.
226
+ */
227
+ const NATIVE_LOWERCASE_BADGE_TEXT_STYLE = { ...NATIVE_BADGE_TEXT_STYLE, textBox: "trim-both ex alphabetic" };
228
+ const LOWERCASE = /\p{Ll}/u;
219
229
 
220
230
  export function Badge({ label, tone = "neutral", color, variant = "subtle", icon, translate = true }: BadgeProps) {
221
231
  const colors = useThemeColors();
@@ -233,11 +243,12 @@ export function Badge({ label, tone = "neutral", color, variant = "subtle", icon
233
243
  const chip = (
234
244
  <Box height={nativePaneChrome ? undefined : 1} paddingX={1} backgroundColor={background}
235
245
  data-gloom-ui="badge" {...(nativeIcon ? { flexDirection: "row", alignItems: "center" } as const : {})}
236
- justifyContent={nativePaneChrome && !icon ? "center" : undefined}
237
- style={nativePaneChrome ? icon ? NATIVE_ICON_BADGE_STYLE : NATIVE_BADGE_STYLE : undefined}>
238
- {nativeIcon ? <Icon name={nativeIcon} color={foreground} /> : null}
246
+ justifyContent={nativePaneChrome && !nativeIcon ? "center" : undefined}
247
+ style={nativePaneChrome ? nativeIcon ? NATIVE_ICON_BADGE_STYLE : NATIVE_BADGE_STYLE : undefined}>
248
+ {nativeIcon ? <Icon name={nativeIcon} color={foreground} fullHeight /> : null}
239
249
  {shown || !nativeIcon ? (
240
- <Text fg={foreground} attributes={TextAttributes.BOLD} style={nativePaneChrome ? NATIVE_BADGE_TEXT_STYLE : undefined}>{shown}</Text>
250
+ <Text fg={foreground} attributes={TextAttributes.BOLD}
251
+ style={nativePaneChrome ? LOWERCASE.test(text) ? NATIVE_LOWERCASE_BADGE_TEXT_STYLE : NATIVE_BADGE_TEXT_STYLE : undefined}>{shown}</Text>
241
252
  ) : null}
242
253
  </Box>
243
254
  );
@@ -3,7 +3,7 @@ import { Box, Text } from "../../ui";
3
3
  import { TextAttributes } from "../../ui";
4
4
  import { tf } from "../../i18n";
5
5
  import { useThemeColors } from "../../theme/theme-context";
6
- import { safeExternalUrl } from "../../utils/external-url";
6
+ import { safeExternalUrl, systemOpenCommand } from "../../utils/external-url";
7
7
  import { linkContextMenuItems, useContextMenu, useRendererHost, useUiCapabilities } from "../../ui";
8
8
  import type { ContextMenuItem } from "../../types/context-menu";
9
9
  import { usePaneMenuItems } from "../layout/pane/footer/registration";
@@ -20,11 +20,8 @@ export function openUrl(rawUrl: string) {
20
20
 
21
21
  if (typeof Bun !== "undefined" && typeof Bun.spawn === "function") {
22
22
  const platform = typeof process !== "undefined" ? process.platform : "linux";
23
- const command = platform === "darwin"
24
- ? ["open", url]
25
- : platform === "win32"
26
- ? ["cmd", "/c", "start", "", url]
27
- : ["xdg-open", url];
23
+ const command = systemOpenCommand(url, platform);
24
+ if (!command) return;
28
25
  // Spawning throws when the opener is not installed (no xdg-open on
29
26
  // minimal Linux and WSL); a link that does nothing beats a crash.
30
27
  try {
@@ -94,13 +94,19 @@ export interface IconProps {
94
94
  /** Pixel size on the desktop; the terminal draws one glyph. */
95
95
  size?: number;
96
96
  color?: string;
97
+ /**
98
+ * Desktop: takes its row's full height and draws in the middle of it. A
99
+ * box centred in a row an odd number of pixels taller lands on a whole
100
+ * pixel, half a pixel off; the drawing inside the box does not.
101
+ */
102
+ fullHeight?: boolean;
97
103
  }
98
104
 
99
105
  /** One glyph from the app's icon set. */
100
- export function Icon({ name, size = 12, color }: IconProps) {
106
+ export function Icon({ name, size = 12, color, fullHeight }: IconProps) {
101
107
  const colors = useThemeColors();
102
108
  const HostIcon = useUiHost().Icon as ComponentType<IconProps> | undefined;
103
- if (HostIcon) return <HostIcon name={name} size={size} color={color} />;
109
+ if (HostIcon) return <HostIcon name={name} size={size} color={color} fullHeight={fullHeight} />;
104
110
  return <Text fg={color ?? colors.textDim} selectable={false}>{ICON_GLYPHS[name]}</Text>;
105
111
  }
106
112
 
@@ -132,6 +138,11 @@ export interface IconButtonProps {
132
138
  size?: number;
133
139
  /** Keep the press from also reaching a row or header behind the button. */
134
140
  stopPropagation?: boolean;
141
+ /**
142
+ * A few characters after the icon, in a muted tone, for a status that must
143
+ * read without hovering (a delay's `15m`). The label stays the accessible name.
144
+ */
145
+ text?: string;
135
146
  }
136
147
 
137
148
  /**
@@ -151,6 +162,7 @@ export function IconButton({
151
162
  disabled = false,
152
163
  size = 12,
153
164
  stopPropagation = true,
165
+ text,
154
166
  }: IconButtonProps) {
155
167
  const colors = useThemeColors();
156
168
  const label = t(rawLabel);
@@ -176,6 +188,7 @@ export function IconButton({
176
188
  disabled={disabled}
177
189
  size={size}
178
190
  stopPropagation={stopPropagation}
191
+ text={text}
179
192
  />
180
193
  );
181
194
  }
@@ -195,7 +208,7 @@ export function IconButton({
195
208
  >
196
209
  {/* Pressed takes the colour the desktop and the terminal pane header give an active toggle. */}
197
210
  <Text fg={disabled ? colors.textMuted : color ?? (pressed ? colors.warning : colors.textDim)} selectable={false}>
198
- {` ${ICON_GLYPHS[icon]} `}
211
+ {` ${ICON_GLYPHS[icon]}${text ?? ""} `}
199
212
  </Text>
200
213
  </Box>
201
214
  );
@@ -23,15 +23,16 @@ const LETTER_STYLE: Record<LogoMarkSize, CSSProperties> = {
23
23
  };
24
24
 
25
25
  export interface LogoMarkProps {
26
- /** The logo, an image URL; without one, or when it fails to load, the tile shows `name`'s initial. */
26
+ /** The logo, an image URL; without one, while it loads and when it fails to load, the tile shows `name`'s initial. */
27
27
  src?: string | null;
28
28
  /** What the logo stands for, such as a broker. Only its initial is drawn; the label beside it names it. */
29
29
  name: string;
30
30
  size?: LogoMarkSize;
31
31
  /**
32
- * Draws the logo on a white plate. Company and coin logos are made for a
33
- * light page: a black mark on a clear field (XRP's) would vanish on a dark
34
- * theme, while a logo that fills its square covers the plate.
32
+ * Draws the logo on a white plate once it has loaded, never behind the
33
+ * initial. Company and coin logos are made for a light page: a black mark
34
+ * on a clear field (XRP's) would vanish on a dark theme, while a logo that
35
+ * fills its square covers the plate.
35
36
  */
36
37
  plate?: boolean;
37
38
  }
@@ -59,9 +60,11 @@ export function LogoMark({ src, name, size = "row", plate = false }: LogoMarkPro
59
60
  src={src ?? undefined}
60
61
  alt=""
61
62
  objectFit="cover"
63
+ fallbackWhileLoading
64
+ imageBackground={plate ? LOGO_PLATE : undefined}
62
65
  aria-hidden="true"
63
66
  data-gloom-role="logo-mark"
64
- style={{ width: tile.side, height: tile.side, flexShrink: 0, borderRadius: tile.radius, ...(plate ? { background: LOGO_PLATE } : {}) }}
67
+ style={{ width: tile.side, height: tile.side, flexShrink: 0, borderRadius: tile.radius }}
65
68
  >
66
69
  <Box width="100%" height="100%" alignItems="center" justifyContent="center" backgroundColor={colors.border}>
67
70
  <Text fg={colors.textBright} attributes={TextAttributes.BOLD} style={LETTER_STYLE[size]}>
@@ -2,7 +2,7 @@ import { t } from "../../i18n";
2
2
  import type { ThemeColors } from "../../theme/colors";
3
3
  import { useThemeColors } from "../../theme/theme-context";
4
4
  import { Box, Text, TextAttributes, useUiCapabilities } from "../../ui";
5
- import { displayWidth, truncateToDisplayWidth } from "../../utils/format";
5
+ import { clipToDisplayWidth, displayWidth, padTo, truncateToDisplayWidth } from "../../utils/format";
6
6
  import { SplitBar, type SplitBarPart } from "./split-bar";
7
7
 
8
8
  export type StatTone = "neutral" | "positive" | "negative" | "warning" | "accent" | "muted";
@@ -49,6 +49,13 @@ export interface StatGridProps {
49
49
  width: number;
50
50
  /** Fixed column count; by default as many as the widest item allows, up to 4, balanced over the rows. */
51
51
  columns?: number;
52
+ /**
53
+ * The most lines the grid takes. A whole-row cell too narrow for its value
54
+ * and detail wraps onto a second line, the detail under the value; with
55
+ * `maxLines` only the leading such cells the lines leave room for do, and
56
+ * the rest keep one line, cut (`statGridRows` counts the same).
57
+ */
58
+ maxLines?: number;
52
59
  }
53
60
 
54
61
  const MAX_COLUMNS = 4;
@@ -102,9 +109,207 @@ function layoutRows(items: StatItem[], columns: number): StatItem[][] {
102
109
  return rows;
103
110
  }
104
111
 
105
- /** Rows a StatGrid takes, so panes can size what sits below it. */
106
- export function statGridRows(items: StatItem[], width: number, columns?: number): number {
107
- return layoutRows(items, Math.max(1, columns ?? statGridColumns(items, width))).length;
112
+ /** What a grid's cells share: the label column, the inner width and one cell's width, in terminal cells. */
113
+ function gridGeometry(items: StatItem[], width: number, columns: number, nativePaneChrome: boolean) {
114
+ const labels = labelChars(items);
115
+ const innerWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
116
+ const cellWidth = Math.max(8, Math.floor((innerWidth - CELL_GAP * (columns - 1)) / columns));
117
+ const widestValue = Math.max(0, ...items.filter((item) => !item.wide).map((item) => displayWidth(item.value)));
118
+ return { labels, innerWidth, cellWidth, widestValue };
119
+ }
120
+
121
+ /**
122
+ * The label's column in a cell `width` wide: it grows into what the grid's
123
+ * widest value leaves, and never gets less than half the cell, so one long
124
+ * value cannot cut every label in a grid with a fixed column count.
125
+ */
126
+ function cellLabelWidth(width: number, labels: number, widestValue: number): number {
127
+ return Math.min(labels + 1, Math.max(4, Math.floor(width * 0.5), width - widestValue - 1));
128
+ }
129
+
130
+ /** Cells a split bar takes after a value that leaves `room` in its column; none when too few to read. */
131
+ function splitBarCells(room: number): number {
132
+ return room >= 4 ? Math.min(SPLIT_BAR_CELLS, room) : 0;
133
+ }
134
+
135
+ const MONTH = "(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|June?|July?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\\.?";
136
+ /** A date, time or day count in a cell: `2026-10-29`, `18:00`, `Dec 9`, `30 Sep`, `Dec '26`, `2026`, `in 40 days`, `3d ago`. */
137
+ const DATED = new RegExp([
138
+ "\\d{4}-\\d{2}",
139
+ "\\b\\d{1,2}:\\d{2}\\b",
140
+ `\\b${MONTH} (?:'\\d{2}|\\d{1,4})\\b`,
141
+ `\\b\\d{1,2} ${MONTH}(?![a-z])`,
142
+ "\\b(?:19|20)\\d{2}\\b",
143
+ "\\bin \\d+ ?(?:d|days?|wks?|weeks?|mo|months?)\\b",
144
+ "\\b\\d+ ?(?:d|h|m|min|days?|hours?) ago\\b",
145
+ ].join("|"));
146
+
147
+ /** `text` cut to `room` after its last whole word that fits, behind a `…`; mid-word only when its first word does not fit. */
148
+ function clipWords(text: string, room: number): string {
149
+ if (displayWidth(text) <= room) return text;
150
+ const words = text.split(" ");
151
+ for (let count = words.length - 1; count > 0; count--) {
152
+ const kept = `${words.slice(0, count).join(" ")}…`;
153
+ if (displayWidth(kept) <= room) return kept;
154
+ }
155
+ return clipToDisplayWidth(text, room);
156
+ }
157
+
158
+ /**
159
+ * `text` cut to `width` without cutting a date: its ` · ` parts that have none
160
+ * give way first, from the last, cut short at a word and then to a `…` that
161
+ * says something is left out; then the words before a part's date
162
+ * (`1,561,837 shares… 16:02:15.123`). Only dates too wide for the width are
163
+ * cut at their end.
164
+ */
165
+ function cutKeepingDates(text: string, width: number): string {
166
+ const parts = text.split(" · ");
167
+ const joined = () => parts.join(" · ");
168
+ const roomFor = (index: number) => width - (displayWidth(joined()) - displayWidth(parts[index]!));
169
+ for (let index = parts.length - 1; index >= 0 && displayWidth(joined()) > width; index--) {
170
+ const part = parts[index]!;
171
+ if (DATED.test(part)) continue;
172
+ const room = roomFor(index);
173
+ if (room >= 4) parts[index] = clipWords(part, room);
174
+ else if (parts[index + 1] !== "…" && part !== "…") parts[index] = "…";
175
+ else parts.splice(index, 1);
176
+ }
177
+ for (let index = parts.length - 1; index >= 0 && displayWidth(joined()) > width; index--) {
178
+ const part = parts[index]!;
179
+ const at = part.search(DATED);
180
+ if (at <= 0) continue;
181
+ const date = part.slice(at);
182
+ const room = roomFor(index) - displayWidth(date) - 1;
183
+ parts[index] = room >= 4 ? `${clipWords(part.slice(0, at).trimEnd(), room)} ${date}` : date;
184
+ }
185
+ return truncateToDisplayWidth(joined(), width);
186
+ }
187
+
188
+ /**
189
+ * `text` on at most two lines no wider than `width`: broken after the ` · `
190
+ * parts that fit the first, else at its last space that does; the second line
191
+ * is cut where it has no date (cutKeepingDates).
192
+ */
193
+ function twoLines(text: string, width: number): string[] {
194
+ const parts = text.split(" · ");
195
+ let first = "";
196
+ let taken = 0;
197
+ for (; taken < parts.length; taken++) {
198
+ const next = first ? `${first} · ${parts[taken]}` : parts[taken]!;
199
+ if (displayWidth(next) > width) break;
200
+ first = next;
201
+ }
202
+ let rest = parts.slice(taken).join(" · ");
203
+ if (!first) {
204
+ // The first part alone is wider than a line: it breaks at a space, or is cut.
205
+ const words = parts[0]!.split(" ");
206
+ let count = 0;
207
+ while (count < words.length && displayWidth(words.slice(0, count + 1).join(" ")) <= width) count++;
208
+ first = count ? words.slice(0, count).join(" ") : truncateToDisplayWidth(words[0]!, width);
209
+ rest = [words.slice(Math.max(1, count)).join(" "), ...parts.slice(1)].filter(Boolean).join(" · ");
210
+ }
211
+ return rest ? [first, cutKeepingDates(rest, width)] : [first];
212
+ }
213
+
214
+ /** A whole-row cell on its two lines (wrapStatCell). */
215
+ interface WrappedStatCell {
216
+ /** The value: one line, or both when it is wider than its column and the detail gives way. */
217
+ value: string[];
218
+ /** Cells of the split bar after the value on the first line; 0 for none. */
219
+ bar: number;
220
+ /** The detail on the second line, or null. */
221
+ detail: string | null;
222
+ }
223
+
224
+ /** Lines a wrapped cell takes: its value, then its detail. */
225
+ const WRAPPED_CELL_LINES = 2;
226
+
227
+ /**
228
+ * A whole-row cell (a `wide` item, or any cell of a one-column grid) that does
229
+ * not fit one line, on two: the value and its split bar on the first, and the
230
+ * detail on the second, in the longest form that fits, else cut where it has
231
+ * no date. A value wider than its column takes both lines, its bar after the
232
+ * first where it has room, when the detail can go (there is none, the pane
233
+ * offers an empty form, or it has no date); with a dated detail the value is
234
+ * cut instead, as a date is never what gives way.
235
+ * Null when the cell fits one line as it is, with the detail form that fits
236
+ * beside the value (an empty form counts: the pane chose to show none). Worked
237
+ * out in terminal cells for both renderers, so the grid's rows are the same
238
+ * count on each.
239
+ */
240
+ function wrapStatCell(item: StatItem, width: number, labelWidth: number): WrappedStatCell | null {
241
+ const valueWidth = Math.max(1, width - labelWidth);
242
+ const valueCells = displayWidth(item.value);
243
+ const forms = item.detail ? [item.detail, ...(item.detailVariants ?? [])] : [];
244
+ const shown = forms.filter(Boolean);
245
+ const detailLine = () => shown.find((form) => displayWidth(form) <= valueWidth) ?? cutKeepingDates(shown.at(-1)!, valueWidth);
246
+ if (valueCells <= valueWidth) {
247
+ const bar = item.split ? splitBarCells(valueWidth - valueCells - 1) : 0;
248
+ const detailRoom = valueWidth - valueCells - (bar ? bar + 1 : 0) - 2;
249
+ const shortest = sizingDetail(item);
250
+ if (!shortest || (detailRoom > 1 && displayWidth(shortest) <= detailRoom)) return null;
251
+ return { value: [item.value], bar, detail: detailLine() };
252
+ }
253
+ const detailCanGo = shown.length === 0 || shown.length < forms.length || !shown.some((form) => DATED.test(form));
254
+ if (detailCanGo) {
255
+ const value = twoLines(item.value, valueWidth);
256
+ return { value, bar: item.split ? splitBarCells(valueWidth - displayWidth(value[0]!) - 1) : 0, detail: null };
257
+ }
258
+ return { value: [cutKeepingDates(item.value, valueWidth)], bar: 0, detail: detailLine() };
259
+ }
260
+
261
+ /** A laid-out row's one cell, wrapped, when it spans the row and does not fit one line (wrapStatCell). */
262
+ function wrappedRow(row: StatItem[], columns: number, geometry: ReturnType<typeof gridGeometry>): WrappedStatCell | null {
263
+ const item = row[0];
264
+ if (!item || row.length > 1 || (!item.wide && columns > 1)) return null;
265
+ const width = item.wide ? geometry.innerWidth : geometry.cellWidth;
266
+ return wrapStatCell(item, width, cellLabelWidth(width, geometry.labels, geometry.widestValue));
267
+ }
268
+
269
+ /**
270
+ * The grid's rows and which of them wrap: every whole-row cell too narrow for
271
+ * one line, in order, while the grid stays within `maxLines`; past that a cell
272
+ * keeps its one line, cut. Decided in terminal cells on both renderers, so a
273
+ * pane sizes the same rows on each.
274
+ */
275
+ function planStatGrid(items: StatItem[], width: number, columns: number, maxLines = Number.POSITIVE_INFINITY) {
276
+ const rows = layoutRows(items, columns);
277
+ const geometry = gridGeometry(items, width, columns, false);
278
+ let spare = maxLines - rows.length;
279
+ const wrapped = rows.map((row) => {
280
+ if (spare < WRAPPED_CELL_LINES - 1) return null;
281
+ const cell = wrappedRow(row, columns, geometry);
282
+ if (cell) spare -= WRAPPED_CELL_LINES - 1;
283
+ return cell;
284
+ });
285
+ const lines = rows.length + wrapped.filter(Boolean).length * (WRAPPED_CELL_LINES - 1);
286
+ return { rows, wrapped, lines };
287
+ }
288
+
289
+ /**
290
+ * Rows a StatGrid takes, so panes can size what sits below it: a whole-row
291
+ * cell that wraps counts both its lines. With `maxLines`, the grid drawn with
292
+ * the same `maxLines`.
293
+ */
294
+ export function statGridRows(items: StatItem[], width: number, columns?: number, { maxLines }: { maxLines?: number } = {}): number {
295
+ return planStatGrid(items, width, Math.max(1, columns ?? statGridColumns(items, width)), maxLines).lines;
296
+ }
297
+
298
+ /**
299
+ * The leading items that fit a band of `maxLines`, in whole rows: as many rows
300
+ * as there are lines, and the rows they leave for a whole-row cell to wrap
301
+ * into (`maxLines` on the grid, which `lines` counts).
302
+ */
303
+ export function statGridWithin(items: StatItem[], width: number, maxLines: number, columns?: number): { items: StatItem[]; lines: number } {
304
+ if (maxLines < 1 || items.length === 0) return { items: [], lines: 0 };
305
+ const resolved = Math.max(1, columns ?? statGridColumns(items, width));
306
+ const shown = layoutRows(items, resolved).slice(0, maxLines).flat();
307
+ return { items: shown, lines: statGridRows(shown, width, resolved, { maxLines }) };
308
+ }
309
+
310
+ /** A label in its column, cut and padded by terminal cells, so a wide character keeps the values aligned. */
311
+ function labelText(label: string, labelWidth: number): string {
312
+ return padTo(truncateToDisplayWidth(label, labelWidth - 1), labelWidth);
108
313
  }
109
314
 
110
315
  /**
@@ -114,16 +319,13 @@ export function statGridRows(items: StatItem[], width: number, columns?: number)
114
319
  * hairline-split cells, so the top of the pane reads as one piece of chrome;
115
320
  * the terminal draws aligned text columns.
116
321
  */
117
- export function StatGrid({ items, width, columns: columnsProp }: StatGridProps) {
322
+ export function StatGrid({ items, width, columns: columnsProp, maxLines }: StatGridProps) {
118
323
  const colors = useThemeColors();
119
324
  const { nativePaneChrome } = useUiCapabilities();
120
325
  if (items.length === 0) return null;
121
326
  const columns = Math.max(1, columnsProp ?? statGridColumns(items, width));
122
- const rows = layoutRows(items, columns);
123
- const labels = labelChars(items);
124
- const innerWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
125
- const cellWidth = Math.max(8, Math.floor((innerWidth - CELL_GAP * (columns - 1)) / columns));
126
- const widestValue = Math.max(0, ...items.filter((item) => !item.wide).map((item) => displayWidth(item.value)));
327
+ const { rows, wrapped: wrapping, lines: lineCount } = planStatGrid(items, width, columns, maxLines);
328
+ const { labels, innerWidth, cellWidth, widestValue } = gridGeometry(items, width, columns, !!nativePaneChrome);
127
329
 
128
330
  const toneColor = (item: StatItem): string =>
129
331
  item.color || (statToneColor(item.tone, colors) ?? (nativePaneChrome ? colors.textBright : colors.text));
@@ -132,25 +334,66 @@ export function StatGrid({ items, width, columns: columnsProp }: StatGridProps)
132
334
  <Box
133
335
  flexDirection="column"
134
336
  paddingX={nativePaneChrome ? 0 : 1}
135
- height={rows.length}
337
+ height={lineCount}
136
338
  flexShrink={0}
137
339
  data-gloom-role="stat-grid"
138
340
  style={nativePaneChrome ? { "--stat-label-w": `${labels}ch`, "--stat-columns": String(columns) } : undefined}
139
341
  >
140
- {rows.map((row, rowIndex) => (
342
+ {rows.map((row, rowIndex) => {
343
+ const wrapped = wrapping[rowIndex];
344
+ if (wrapped) {
345
+ const item = row[0]!;
346
+ const label = t(item.label);
347
+ const width = item.wide ? innerWidth : cellWidth;
348
+ const labelWidth = cellLabelWidth(width, labels, widestValue);
349
+ const [firstValue, ...moreValue] = wrapped.value;
350
+ return (
351
+ <Box key={rowIndex} height={WRAPPED_CELL_LINES} flexDirection="row" data-gloom-role="stat-grid-row">
352
+ <Box
353
+ key={item.id ?? `${rowIndex}:0`}
354
+ width={nativePaneChrome ? undefined : width}
355
+ height={WRAPPED_CELL_LINES}
356
+ flexDirection="row"
357
+ overflow="hidden"
358
+ data-gloom-role="stat-grid-cell"
359
+ data-wide={item.wide ? "true" : undefined}
360
+ data-wrapped="true"
361
+ data-gloom-stat-id={item.id}
362
+ >
363
+ <Text fg={colors.textDim} data-gloom-role="stat-grid-label">
364
+ {nativePaneChrome ? label : labelText(label, labelWidth)}
365
+ </Text>
366
+ <Box flexDirection="column" flexGrow={1} minWidth={0}>
367
+ <Box height={1} flexDirection="row">
368
+ <Text fg={toneColor(item)} attributes={TextAttributes.BOLD} data-gloom-role="stat-grid-value">{firstValue}</Text>
369
+ {item.split && wrapped.bar ? (
370
+ <Box marginLeft={nativePaneChrome ? 0 : 1} flexShrink={nativePaneChrome ? 1 : 0} style={nativePaneChrome ? { minWidth: 0 } : undefined}>
371
+ <SplitBar parts={item.split} width={nativePaneChrome ? SPLIT_BAR_CELLS : wrapped.bar} />
372
+ </Box>
373
+ ) : null}
374
+ </Box>
375
+ {moreValue.map((line, index) => (
376
+ <Text key={`value:${index}`} fg={toneColor(item)} attributes={TextAttributes.BOLD} data-gloom-role="stat-grid-value">{line}</Text>
377
+ ))}
378
+ {wrapped.detail != null ? (
379
+ <Text fg={colors.textDim} data-gloom-role="stat-grid-detail">{wrapped.detail}</Text>
380
+ ) : null}
381
+ </Box>
382
+ </Box>
383
+ </Box>
384
+ );
385
+ }
386
+ return (
141
387
  <Box key={rowIndex} height={1} flexDirection="row" gap={nativePaneChrome ? 0 : CELL_GAP} data-gloom-role="stat-grid-row">
142
388
  {row.map((item, index) => {
143
389
  const label = t(item.label);
144
390
  const width = item.wide ? innerWidth : cellWidth;
145
- // The label grows into what the grid's widest value leaves, and
146
- // never gets less than half the cell, so one long value cannot cut
147
- // every label in a grid with a fixed column count.
148
- const labelWidth = Math.min(labels + 1, Math.max(4, Math.floor(width * 0.5), width - widestValue - 1));
391
+ const labelWidth = cellLabelWidth(width, labels, widestValue);
149
392
  const valueWidth = Math.max(1, width - labelWidth);
150
393
  const value = nativePaneChrome ? item.value : truncateToDisplayWidth(item.value, valueWidth);
151
394
  // The bar shows only whole enough to read; the value never gives way to it.
152
395
  const barRoom = valueWidth - displayWidth(value) - 1;
153
- const barCells = !item.split ? 0 : nativePaneChrome ? SPLIT_BAR_CELLS : barRoom >= 4 ? Math.min(SPLIT_BAR_CELLS, barRoom) : 0;
396
+ const barCells = !item.split ? 0 : nativePaneChrome ? SPLIT_BAR_CELLS : splitBarCells(barRoom);
154
397
  const detailWidth = valueWidth - displayWidth(value) - (barCells ? barCells + 1 : 0) - 2;
155
398
  // The desktop cell has no gap between cells: its label column, inset and gaps take the room
156
399
  // (measured short, as a cell is wider than a character, so a form that fits is never clipped).
@@ -177,7 +420,7 @@ export function StatGrid({ items, width, columns: columnsProp }: StatGridProps)
177
420
  : undefined}
178
421
  >
179
422
  <Text fg={colors.textDim} data-gloom-role="stat-grid-label">
180
- {nativePaneChrome ? label : truncateToDisplayWidth(label, labelWidth - 1).padEnd(labelWidth)}
423
+ {nativePaneChrome ? label : labelText(label, labelWidth)}
181
424
  </Text>
182
425
  <Text fg={toneColor(item)} attributes={TextAttributes.BOLD} data-gloom-role="stat-grid-value">{value}</Text>
183
426
  {item.split && barCells ? (
@@ -192,7 +435,8 @@ export function StatGrid({ items, width, columns: columnsProp }: StatGridProps)
192
435
  );
193
436
  })}
194
437
  </Box>
195
- ))}
438
+ );
439
+ })}
196
440
  </Box>
197
441
  );
198
442
  }
@@ -10,8 +10,17 @@ import { createAppRuntime } from "./app-runtime";
10
10
 
11
11
  export type AppServices = ReturnType<typeof createAppServices>;
12
12
 
13
- export function createAppServices({ config, plugins, externalPlugins }: AppServicesFactoryOptions) {
14
- const persistence = new AppPersistence(join(config.dataDir, ".gloomberb-cache.db"));
13
+ interface AppServicesOptions extends AppServicesFactoryOptions {
14
+ /**
15
+ * Runs where the user waits on it (the terminal, the desktop app), so no
16
+ * database statement waits on another program's lock (data/sqlite/access.ts).
17
+ * A command sets it to false and waits instead.
18
+ */
19
+ interactive?: boolean;
20
+ }
21
+
22
+ export function createAppServices({ config, plugins, externalPlugins, interactive = true }: AppServicesOptions) {
23
+ const persistence = new AppPersistence(join(config.dataDir, ".gloomberb-cache.db"), { interactive });
15
24
  const tickerRepository = new TickerRepository(persistence.tickers);
16
25
  const providerRouter = new AssetDataRouter(null, [], persistence.resources, connectionHealth);
17
26
  const runtime = createAppRuntime({