@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
@@ -3,7 +3,7 @@
3
3
  * search, the plugin directory and chat suggestions, and its two screener
4
4
  * fields (ATTN HRS, ATTN Z) in the EQS field and metric pickers.
5
5
  *
6
- * Research attention publishes an hour only once enough opted-in users
6
+ * Research attention publishes an hour only once enough contributing users
7
7
  * researched in it, and it has published nothing for weeks, so ATTN stays
8
8
  * unlisted until it does. Typing ATTN still opens it, a screen that already
9
9
  * uses its fields keeps them, and the Attention Counts setting and its
@@ -14,7 +14,7 @@ export const ATTENTION_SERIES: readonly SeriesDefinition[] = [{
14
14
  frequency: "intraday",
15
15
  group: "Alternative data",
16
16
  tags: ["attention", "research", "trending", "researcher-hours"],
17
- description: "Rounded researcher-hours opt-in users spent on the ticker in each published hour, as ATTN charts them; an hour below the privacy threshold is a gap. Needs Pro.",
17
+ description: "Rounded researcher-hours Gloom users spent on the ticker in each published hour, as ATTN charts them; an hour below the privacy threshold is a gap. Needs Pro.",
18
18
  input: "ticker",
19
19
  load: async ({ ticker }) => {
20
20
  const data = await fetchAttention("now", ticker!);
@@ -1,14 +1,13 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
2
  import { awardDetailCache, awardsCache } from "./client";
3
3
  import { awardsHeadless } from "./headless";
4
- import { AwardsPane } from "./pane";
5
4
  import { AWARD_SERIES } from "./series";
6
5
 
7
6
  export const awardsModule: PluginModule = {
8
7
  series: AWARD_SERIES,
9
8
  setup(ctx) { awardsCache.attach(ctx.persistence); awardDetailCache.attach(ctx.persistence); },
10
9
  dispose() { awardsCache.reset(); awardDetailCache.reset(); },
11
- panes: [{ id: "awards", name: "Government Awards", icon: "A", component: AwardsPane, defaultPosition: "right", defaultMode: "floating",
10
+ panes: [{ id: "awards", name: "Government Awards", icon: "A", get component(): typeof import("./pane").AwardsPane { const { AwardsPane } = require("./pane"); return AwardsPane; }, defaultPosition: "right", defaultMode: "floating",
12
11
  defaultFloatingSize: { width: 132, height: 34 }, tickerFollower: true, instruments: ["equity"], tableExport: true, headless: awardsHeadless,
13
12
  quickSettings: [{ type: "toggle", key: "awardAlerts", icon: "zap", label: "New awards above 1% of annual revenue" }],
14
13
  settings: { title: "Government awards", fields: [{ type: "toggle", key: "awardAlerts", label: "New awards above 1% of annual revenue", description: "Notify while this pane is visible. Starts with the next refresh; historical backfills are excluded." }] } }],
@@ -62,6 +62,7 @@ export function toBacktestBars(
62
62
  low: isFiniteNumber(point.low) && point.low > 0 ? point.low : null,
63
63
  close: point.close,
64
64
  ...(point.partial === true ? { partial: true as const } : {}),
65
+ ...(point.unsettled === true ? { unsettled: true as const } : {}),
65
66
  });
66
67
  }
67
68
  const bars = [...byDate.values()].sort((a, b) => a.date.localeCompare(b.date));
@@ -1,13 +1,12 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
2
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
3
3
  import { backtestHeadless } from "./headless";
4
- import { BacktestPane } from "./pane";
5
4
  import { BACKTEST_PRESETS, LOOKBACK_OPTIONS } from "./presets";
6
5
  import { BACKTEST_FRESHNESS } from "./report";
7
6
 
8
7
  export const backtestModule: PluginModule = {
9
8
  panes: [{
10
- id: "backtest", name: "Backtest", icon: "B", component: BacktestPane,
9
+ id: "backtest", name: "Backtest", icon: "B", get component(): typeof import("./pane").BacktestPane { const { BacktestPane } = require("./pane"); return BacktestPane; },
11
10
  defaultPosition: "right", tickerFollower: true, defaultMode: "floating", defaultFloatingSize: { width: 120, height: 34 },
12
11
  tableExport: true,
13
12
  // A capture is dated as `fn BT` dates the run, by its last session, never by the ticker's live quote.
@@ -1,4 +1,4 @@
1
- import { ema, rsi, sma, type IndexedValue } from "../../../time-series/studies";
1
+ import { ema, rsi, sma, type IndexedValue } from "../../../time-series/studies/indicators";
2
2
 
3
3
  /** One daily bar as the rules see it. Open is optional; fills fall back to the close. */
4
4
  export interface BacktestBar {
@@ -9,6 +9,8 @@ export interface BacktestBar {
9
9
  close: number;
10
10
  /** The bar's day had not ended when it was served: its close is the latest price. */
11
11
  partial?: true;
12
+ /** A future's session that closed before its settlement was in: its close is the last trade. */
13
+ unsettled?: true;
12
14
  }
13
15
 
14
16
  export type Operand =
@@ -4,6 +4,7 @@ import type { BacktestHistory } from "./client";
4
4
  import { runBacktest, type BacktestOptions, type BacktestResult } from "./engine";
5
5
  import { delistingLabel } from "../../../market-data/quotes/delisting";
6
6
  import type { BacktestBar, Rule } from "./rules";
7
+ import { formingBarsState, isFormingBar } from "../shared/forming-bars";
7
8
 
8
9
  /** Which served sessions a run tests. */
9
10
  export interface BacktestWindowOptions {
@@ -62,17 +63,17 @@ export function backtestWindow(
62
63
  if (history.undatedSkipped > 0) {
63
64
  warnings.push(`${history.undatedSkipped} price ${history.undatedSkipped === 1 ? "point" : "points"} without a date ${history.undatedSkipped === 1 ? "was" : "were"} left out.`);
64
65
  }
65
- const forming = inWindow.filter((bar) => bar.partial);
66
- const bars = forming.length === 0 || options.includePartial ? inWindow : inWindow.filter((bar) => !bar.partial);
66
+ // A future's session that has not settled moves too: its close becomes the settlement.
67
+ const forming = inWindow.filter(isFormingBar);
68
+ const bars = forming.length === 0 || options.includePartial ? inWindow : inWindow.filter((bar) => !isFormingBar(bar));
67
69
  const through = bars.at(-1)?.date;
68
70
  if (forming.length > 0) {
69
- const dates = listDates(forming.map((bar) => bar.date));
71
+ const dates = `${listDates(forming.map((bar) => bar.date))} ${formingBarsState(forming)}`;
70
72
  warnings.push(options.includePartial
71
- ? `${dates} ${forming.length === 1 ? "is" : "are"} still forming (partial) and kept: the last close is the latest price, so the results move until the session ends.`
72
- : `${dates} ${forming.length === 1 ? "is" : "are"} still forming (partial) and left out`
73
- + `${through ? `: the test runs through ${through}, the last complete session` : ""}.`);
73
+ ? `${dates} and kept: the last close is ${forming.every((bar) => bar.partial) ? "the latest price" : "not final"}, so the results move until the session ends.`
74
+ : `${dates} and left out${through ? `: the test runs through ${through}, the last complete session` : ""}.`);
74
75
  }
75
- const newestComplete = history.bars.findLast((bar) => !bar.partial)?.date;
76
+ const newestComplete = history.bars.findLast((bar) => !isFormingBar(bar))?.date;
76
77
  const delisting = history.delisting;
77
78
  if (delisting) {
78
79
  const listing = `${history.symbol ? `${parsePublicTickerKey(history.symbol).symbol}: ` : ""}${delistingLabel(delisting)}`;
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef, type Ref } from "react";
2
- import { Box, ScrollBox, Text, TextAttributes, type BoxRenderable, type ScrollBoxRenderable } from "../../../ui";
2
+ import { Box, ScrollBox, Text, TextAttributes, useNativeRenderer, useUiCapabilities, type BoxRenderable, type ScrollBoxRenderable } from "../../../ui";
3
3
  import { Button, FieldLabel, NumberField, SectionHeading, SegmentedControl, TextField, useFieldRing } from "../../../components";
4
4
  import { afterLayout, revealInScrollBox } from "../../../components/ui/reveal-in-scroll-box";
5
5
  import {
@@ -294,10 +294,23 @@ export function BrokerDetailContent({
294
294
  // Enter saves from any field, so what stopped the save can be below the
295
295
  // fold; bring it and the buttons into view.
296
296
  const actionsRef = useRef<BoxRenderable | null>(null);
297
+ const desktop = useUiCapabilities().nativePaneChrome === true;
298
+ const nativeRenderer = useNativeRenderer();
297
299
  useEffect(() => {
298
300
  if (!editError) return;
299
- return afterLayout(() => revealInScrollBox(scrollRef.current, actionsRef.current));
300
- }, [editError]);
301
+ const reveal = () => revealInScrollBox(scrollRef.current, actionsRef.current);
302
+ if (desktop) return afterLayout(reveal);
303
+ // The terminal lays the error line out only when it draws a frame, which
304
+ // can come after a 0 ms timer: measured before it, the buttons are one row
305
+ // short and the error pushes Save back below the fold.
306
+ const onFrame = () => {
307
+ nativeRenderer.off("frame", onFrame);
308
+ reveal();
309
+ };
310
+ nativeRenderer.on("frame", onFrame);
311
+ nativeRenderer.requestRender();
312
+ return () => nativeRenderer.off("frame", onFrame);
313
+ }, [desktop, editError, nativeRenderer]);
301
314
 
302
315
  if (!row) return <Box flexGrow={1} />;
303
316
 
@@ -365,7 +365,7 @@ export const brokerManagerModule: PluginModule = {
365
365
  personalData: "account",
366
366
  icon: "B",
367
367
  component: BrokersPane,
368
- reportFreshness: { status: "not-a-feed", basis: "broker connections" },
368
+ reportFreshness: { status: "not-a-feed", basis: "broker connections", local: true },
369
369
  defaultPosition: "right",
370
370
  defaultMode: "floating",
371
371
  defaultFloatingSize: { width: 92, height: 24 },
@@ -11,6 +11,7 @@ import {
11
11
  activityColor,
12
12
  activityLabel,
13
13
  criticalityColor,
14
+ listWord,
14
15
  metricColor,
15
16
  moneyAmount,
16
17
  moneyText,
@@ -115,6 +116,10 @@ export function renderBuildoutCell(
115
116
  return { text: text(company.return1y), color: metricColor(company.return1y) };
116
117
  case "employees":
117
118
  return { text: text(company.employeeCount), color: colors.textDim };
119
+ case "listRole":
120
+ return { text: text(listWord(company.listRole)), color: colors.textDim };
121
+ case "listTier":
122
+ return { text: text(listWord(company.listTier)), color: company.listTier === "leader" ? colors.text : colors.textDim };
118
123
  }
119
124
  }
120
125
 
@@ -0,0 +1,367 @@
1
+ import { useMemo, useRef, useState } from "react";
2
+ import type { ChartVectorShape } from "../../../../ui/host";
3
+ import { Box, ChartSurface, Span, Text, TextAttributes, useUiCapabilities } from "../../../../ui";
4
+ import { BADGE_TINT, ClippedText, NATIVE_BADGE_STYLE } from "../../../../components/ui/display";
5
+ import { cardSurfaceStyle } from "../../../../components/ui/card";
6
+ import { useShortcut } from "../../../../react/input";
7
+ import { blendHex } from "../../../../theme/colors";
8
+ import { SERIES_COLORS } from "../../../../theme/series-colors";
9
+ import { useThemeColors } from "../../../../theme/theme-context";
10
+ import { displayWidth } from "../../../../utils/format";
11
+ import { isPlainKey } from "../../../../utils/keyboard";
12
+ import { ribbonY } from "../../supply-chain/flow-layout";
13
+ import type { BuildoutCompanyProfile, BuildoutRelation, RelationRole } from "./model";
14
+
15
+ /** Power and hosting gold, then the supply chain's own supplier, customer and competitor colours. */
16
+ const RELATION_COLORS: Record<RelationRole, string> = {
17
+ power: SERIES_COLORS[2]!,
18
+ supplier: SERIES_COLORS[0]!,
19
+ customer: SERIES_COLORS[1]!,
20
+ competitor: SERIES_COLORS[5]!,
21
+ };
22
+
23
+ const GROUP_LABELS: Record<RelationRole, string> = { power: "Power", supplier: "Suppliers", customer: "Customers", competitor: "Competitors" };
24
+ const CARD_ROWS = 2;
25
+ const LINK_STEPS = 40;
26
+ const CHIP_LABEL_CELLS = 13;
27
+ /** Below this the columns and the ribbons between them do not fit; the flow reads as a list. */
28
+ export const FLOW_MIN_WIDTH = 64;
29
+
30
+ interface Rect { x: number; y: number; width: number; height: number }
31
+ interface FlowCard extends Rect { relation: BuildoutRelation }
32
+ interface FlowHeader { role: RelationRole; x: number; y: number }
33
+ interface FlowMore { role: RelationRole; x: number; y: number; hidden: number }
34
+ interface FlowLink { id: string; role: RelationRole; x0: number; y0: number; x1: number; y1: number }
35
+ interface FlowChip { relation: BuildoutRelation | null; label: string; x: number; width: number }
36
+
37
+ export interface FlowGeometry {
38
+ center: Rect;
39
+ cards: FlowCard[];
40
+ headers: FlowHeader[];
41
+ more: FlowMore[];
42
+ links: FlowLink[];
43
+ chips: FlowChip[];
44
+ chipRow: number | null;
45
+ }
46
+
47
+ interface Group { role: RelationRole; items: BuildoutRelation[]; page: number }
48
+
49
+ /** Rows a column of groups takes with `counts` cards each. */
50
+ function columnRows(groups: readonly Group[], counts: readonly number[], gap: number): number {
51
+ return groups.reduce((sum, group, index) => {
52
+ if (!group.items.length) return sum;
53
+ const shown = counts[index]!;
54
+ return sum + (sum ? 1 : 0) + 1 + shown * CARD_ROWS + Math.max(0, shown - 1) * gap + (shown < group.items.length ? 1 : 0);
55
+ }, 0);
56
+ }
57
+
58
+ /** Places a column's groups in `rows`, the last group giving up cards first, centred when there is room. */
59
+ function placeColumn(groups: readonly Group[], x: number, width: number, rows: number) {
60
+ const cards: FlowCard[] = [];
61
+ const headers: FlowHeader[] = [];
62
+ const more: FlowMore[] = [];
63
+ for (const gap of [1, 0]) {
64
+ const counts = groups.map((group) => group.items.length);
65
+ while (columnRows(groups, counts, gap) > rows) {
66
+ let index = -1;
67
+ for (let i = counts.length - 1; i >= 0; i--) if (counts[i]! > 1) { index = i; break; }
68
+ if (index < 0) break;
69
+ counts[index]!--;
70
+ }
71
+ const used = columnRows(groups, counts, gap);
72
+ if (used > rows && gap === 1) continue;
73
+ let y = Math.max(0, Math.floor((rows - used) / 2));
74
+ let first = true;
75
+ groups.forEach((group, index) => {
76
+ if (!group.items.length) return;
77
+ if (!first) y += 1;
78
+ first = false;
79
+ headers.push({ role: group.role, x, y });
80
+ y += 1;
81
+ const shown = counts[index]!;
82
+ const pages = Math.max(1, Math.ceil(group.items.length / Math.max(1, shown)));
83
+ const start = shown < group.items.length ? (group.page % pages) * shown : 0;
84
+ group.items.slice(start, start + shown).forEach((relation, offset) => {
85
+ cards.push({ relation, x, y, width, height: CARD_ROWS });
86
+ y += CARD_ROWS + (offset < shown - 1 ? gap : 0);
87
+ });
88
+ if (shown < group.items.length) {
89
+ more.push({ role: group.role, x, y, hidden: group.items.length - shown });
90
+ y += 1;
91
+ }
92
+ });
93
+ break;
94
+ }
95
+ return { cards, headers, more };
96
+ }
97
+
98
+ /**
99
+ * Power and hosting deals and suppliers on the left, customers on the right,
100
+ * the company between them with a line from every card to its edge, in card
101
+ * order so no two cross; competitors as chips on the last row.
102
+ */
103
+ function flowGeometry(profile: BuildoutCompanyProfile, width: number, height: number, focusLabel: string, pages: Partial<Record<RelationRole, number>>): FlowGeometry {
104
+ const of = (role: RelationRole) => profile.relations.filter((relation) => relation.role === role);
105
+ const competitors = of("competitor");
106
+ const chipRow = competitors.length ? height - 1 : null;
107
+ const rows = Math.max(CARD_ROWS + 1, (chipRow ?? height) - (chipRow === null ? 0 : 1));
108
+ const cardWidth = Math.max(18, Math.min(38, Math.floor(width * (width < 110 ? 0.31 : 0.27))));
109
+ const left = placeColumn([{ role: "power", items: of("power"), page: pages.power ?? 0 }, { role: "supplier", items: of("supplier"), page: pages.supplier ?? 0 }], 1, cardWidth, rows);
110
+ const right = placeColumn([{ role: "customer", items: of("customer"), page: pages.customer ?? 0 }], width - cardWidth - 1, cardWidth, rows);
111
+ const centerWidth = Math.max(12, Math.min(26, displayWidth(focusLabel) + 6));
112
+ const centerHeight = Math.max(3, Math.min(9, Math.floor(rows * 0.4)));
113
+ const center = { x: Math.floor((width - centerWidth) / 2), y: Math.floor((rows - centerHeight) / 2), width: centerWidth, height: centerHeight };
114
+ const links: FlowLink[] = [];
115
+ const leftCards = left.cards;
116
+ leftCards.forEach((card, index) => links.push({
117
+ id: `${card.relation.role}:${card.relation.id}`, role: card.relation.role,
118
+ x0: card.x + card.width, y0: card.y + 1, x1: center.x, y1: center.y + (center.height * (index + 0.5)) / leftCards.length,
119
+ }));
120
+ right.cards.forEach((card, index) => links.push({
121
+ id: `${card.relation.role}:${card.relation.id}`, role: card.relation.role,
122
+ x0: center.x + center.width, y0: center.y + (center.height * (index + 0.5)) / right.cards.length, x1: card.x, y1: card.y + 1,
123
+ }));
124
+ // Competitors as chips while they fit, then "+N more".
125
+ const chips: FlowChip[] = [];
126
+ let x = CHIP_LABEL_CELLS + 1;
127
+ for (const [index, relation] of competitors.entries()) {
128
+ const label = relation.ticker ?? relation.name;
129
+ const chipWidth = Math.min(22, displayWidth(label) + 2);
130
+ const rest = competitors.length - index - 1;
131
+ if (x + chipWidth + (rest ? 11 : 0) > width - 1) {
132
+ chips.push({ relation: null, label: `+${competitors.length - index} more`, x, width: displayWidth(`+${competitors.length - index} more`) + 2 });
133
+ break;
134
+ }
135
+ chips.push({ relation, label, x, width: chipWidth });
136
+ x += chipWidth + 1;
137
+ }
138
+ return {
139
+ center,
140
+ cards: [...left.cards, ...right.cards],
141
+ headers: [...left.headers, ...right.headers],
142
+ more: [...left.more, ...right.more],
143
+ links,
144
+ chips,
145
+ chipRow,
146
+ };
147
+ }
148
+
149
+ const linkId = (relation: BuildoutRelation) => `${relation.role}:${relation.id}`;
150
+
151
+ export function CompanyFlow({ profile, width, height, focused, onOpen }: {
152
+ profile: BuildoutCompanyProfile;
153
+ width: number;
154
+ height: number;
155
+ focused: boolean;
156
+ onOpen: (relation: BuildoutRelation) => void;
157
+ }) {
158
+ const colors = useThemeColors();
159
+ const desktop = !!useUiCapabilities().nativePaneChrome;
160
+ const [pages, setPages] = useState<Partial<Record<RelationRole, number>>>({});
161
+ const [selectedId, setSelectedId] = useState<string | null>(null);
162
+ const [hovered, setHovered] = useState<string | null>(null);
163
+ const surfaceRef = useRef<{ x: number; y: number; absoluteX?: number; absoluteY?: number } | null>(null);
164
+ const focusLabel = profile.ticker ?? profile.name;
165
+ const geometry = useMemo(() => flowGeometry(profile, width, height, focusLabel, pages), [focusLabel, height, pages, profile, width]);
166
+ const { cards, links, center } = geometry;
167
+ const selectable = useMemo(() => [...cards.map((card) => card.relation), ...geometry.chips.flatMap((chip) => chip.relation ? [chip.relation] : [])], [cards, geometry.chips]);
168
+ const selectedIndex = Math.max(0, selectable.findIndex((relation) => linkId(relation) === selectedId));
169
+ const selected = selectable[selectedIndex] ? linkId(selectable[selectedIndex]!) : null;
170
+ const active = hovered ?? (focused ? selected : null);
171
+ const focusTint = blendHex(colors.bg, colors.textBright, 0.55);
172
+
173
+ useShortcut((event) => {
174
+ if (!focused || event.defaultPrevented || selectable.length === 0) return;
175
+ if (isPlainKey(event, "j", "down") || isPlainKey(event, "k", "up")) {
176
+ const step = isPlainKey(event, "j", "down") ? 1 : -1;
177
+ setSelectedId(linkId(selectable[(selectedIndex + step + selectable.length) % selectable.length]!));
178
+ } else if (isPlainKey(event, "return", "enter")) onOpen(selectable[selectedIndex]!);
179
+ else return;
180
+ event.preventDefault();
181
+ event.stopPropagation();
182
+ });
183
+
184
+ const linkColors = (link: FlowLink) => link.x0 < center.x ? [RELATION_COLORS[link.role], focusTint] : [focusTint, RELATION_COLORS[link.role]];
185
+ const vectors = useMemo<ChartVectorShape[]>(() => links.map((link) => ({
186
+ id: link.id,
187
+ points: Array.from({ length: LINK_STEPS + 1 }, (_, step) => {
188
+ const x = link.x0 + (link.x1 - link.x0) * step / LINK_STEPS;
189
+ return { x: x / width, y: ribbonY(link, x) / height };
190
+ }),
191
+ color: RELATION_COLORS[link.role],
192
+ gradient: linkColors(link),
193
+ strokeWidth: active === link.id ? 2.4 : 1.4,
194
+ opacity: active && active !== link.id ? 0.3 : active === link.id ? 1 : 0.75,
195
+ })), [active, height, links, width, focusTint, center.x]);
196
+
197
+ // The terminal draws the same curves in braille, each coloured from its card to the company.
198
+ const braille = useMemo(() => {
199
+ if (desktop) return null;
200
+ const grid = Array.from({ length: height }, () => Array.from({ length: width }, () => ({ mask: 0, color: colors.textDim })));
201
+ const bits = [[1, 8], [2, 16], [4, 32], [64, 128]];
202
+ for (const link of links) {
203
+ const [from, to] = linkColors(link);
204
+ for (let dx = Math.ceil(link.x0 * 2); dx < Math.floor(link.x1 * 2); dx++) {
205
+ const x = (dx + 0.5) / 2;
206
+ const t = (x - link.x0) / Math.max(1e-6, link.x1 - link.x0);
207
+ const dy = Math.round(ribbonY(link, x) * 4 - 0.5);
208
+ const cell = grid[Math.floor(dy / 4)]?.[Math.floor(dx / 2)];
209
+ if (!cell || dy < 0) continue;
210
+ cell.mask |= bits[dy % 4]![dx % 2]!;
211
+ const color = blendHex(from!, to!, t);
212
+ cell.color = active === link.id ? color : blendHex(colors.bg, color, 0.75);
213
+ }
214
+ }
215
+ return grid.map((line) => {
216
+ const spans: { text: string; color: string }[] = [];
217
+ for (const cell of line) {
218
+ const char = cell.mask ? String.fromCharCode(0x2800 + cell.mask) : " ";
219
+ if (spans.at(-1)?.color === cell.color) spans.at(-1)!.text += char;
220
+ else spans.push({ text: char, color: cell.color });
221
+ }
222
+ return spans;
223
+ });
224
+ }, [active, colors.bg, colors.textDim, desktop, height, links, width, focusTint, center.x]);
225
+
226
+ /** The line under the pointer comes forward. */
227
+ const hoverAt = (event: { preciseX?: number; preciseY?: number; x: number; y: number }) => {
228
+ const origin = surfaceRef.current;
229
+ if (!origin) return;
230
+ const x = (event.preciseX ?? event.x) - (origin.absoluteX ?? origin.x);
231
+ const y = (event.preciseY ?? event.y) - (origin.absoluteY ?? origin.y);
232
+ let best: { id: string; distance: number } | null = null;
233
+ for (const link of links) {
234
+ if (x < link.x0 || x > link.x1) continue;
235
+ const distance = Math.abs(ribbonY(link, x) - y);
236
+ if (distance < 0.7 && (!best || distance < best.distance)) best = { id: link.id, distance };
237
+ }
238
+ const next = best?.id ?? null;
239
+ if (next !== hovered) setHovered(next);
240
+ };
241
+
242
+ return (
243
+ <Box width={width} height={height} flexGrow={1} flexBasis={0} minHeight={0} position="relative" overflow="hidden">
244
+ <ChartSurface ref={surfaceRef} width={width} height={height} position="absolute" left={0} top={0}
245
+ vectors={desktop ? vectors : undefined} flexDirection="column"
246
+ onMouseMove={hoverAt} onMouseOut={() => setHovered(null)}
247
+ onMouseDown={() => { const card = hovered ? cards.find((entry) => linkId(entry.relation) === hovered) : null; if (card) onOpen(card.relation); }}
248
+ aria-label={`${profile.name} power, suppliers and customers`}>
249
+ {braille ? braille.map((spans, y) => <Text key={y}>{spans.map((span, i) => <Span key={i} fg={span.color}>{span.text}</Span>)}</Text>) : null}
250
+ </ChartSurface>
251
+ {geometry.headers.map((header) => (
252
+ <Box key={header.role} position="absolute" left={header.x} top={header.y} width={20} height={1}>
253
+ <Text fg={RELATION_COLORS[header.role]} attributes={TextAttributes.BOLD}>{GROUP_LABELS[header.role]}</Text>
254
+ </Box>
255
+ ))}
256
+ <FocusCard profile={profile} label={focusLabel} rect={center} desktop={desktop} />
257
+ {cards.map((card) => (
258
+ <RelationCard key={linkId(card.relation)} card={card} desktop={desktop} active={active === linkId(card.relation)}
259
+ onPress={() => { setSelectedId(linkId(card.relation)); onOpen(card.relation); }}
260
+ onHover={(on) => setHovered(on ? linkId(card.relation) : null)} />
261
+ ))}
262
+ {geometry.more.map((entry) => (
263
+ <Box key={`more:${entry.role}`} position="absolute" left={entry.x} top={entry.y} width={20} height={1}
264
+ onMouseDown={() => setPages((current) => ({ ...current, [entry.role]: (current[entry.role] ?? 0) + 1 }))}
265
+ style={desktop ? { cursor: "pointer" } : undefined} role="button" aria-label={`Show ${entry.hidden} more ${GROUP_LABELS[entry.role].toLowerCase()}`}>
266
+ <Text fg={colors.textDim}>{`+${entry.hidden} more`}</Text>
267
+ </Box>
268
+ ))}
269
+ {geometry.chipRow !== null ? (
270
+ <Box position="absolute" left={1} top={geometry.chipRow} width={CHIP_LABEL_CELLS} height={1}>
271
+ <Text fg={RELATION_COLORS.competitor}>Competitors</Text>
272
+ </Box>
273
+ ) : null}
274
+ {geometry.chipRow !== null ? geometry.chips.map((chip) => (
275
+ <CompetitorChip key={chip.label} chip={chip} row={geometry.chipRow!} desktop={desktop}
276
+ active={!!chip.relation && active === linkId(chip.relation)} onPress={() => { if (chip.relation) onOpen(chip.relation); }} />
277
+ )) : null}
278
+ </Box>
279
+ );
280
+ }
281
+
282
+ function FocusCard({ profile, label, rect, desktop }: { profile: BuildoutCompanyProfile; label: string; rect: Rect; desktop: boolean }) {
283
+ const colors = useThemeColors();
284
+ const showName = profile.name && profile.name.toUpperCase() !== label.toUpperCase() && rect.height >= (desktop ? 3 : 4);
285
+ return (
286
+ <Box position="absolute" left={rect.x} top={rect.y} width={rect.width} height={rect.height} flexDirection="column" alignItems="center" justifyContent="center"
287
+ border={!desktop} borderStyle="rounded" borderColor={colors.textMuted}
288
+ style={desktop ? cardSurfaceStyle({ padding: "0" }) : undefined}>
289
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{label}</Text>
290
+ {showName ? <ClippedText fg={colors.textDim} text={profile.name} width={rect.width - 4} /> : null}
291
+ </Box>
292
+ );
293
+ }
294
+
295
+ function RelationCard({ card, desktop, active, onPress, onHover }: {
296
+ card: FlowCard; desktop: boolean; active: boolean; onPress: () => void; onHover: (on: boolean) => void;
297
+ }) {
298
+ const colors = useThemeColors();
299
+ const { relation } = card;
300
+ const accent = RELATION_COLORS[relation.role];
301
+ const inner = Math.max(4, card.width - 2);
302
+ const nameWidth = Math.max(4, inner - (relation.ticker ? displayWidth(relation.ticker) + 1 : 0));
303
+ const listed = !!relation.ticker;
304
+ return (
305
+ <Box position="absolute" left={card.x} top={card.y} width={card.width} height={card.height} flexDirection="column"
306
+ backgroundColor={desktop ? undefined : active ? colors.selected : blendHex(colors.bg, accent, 0.12)}
307
+ style={desktop ? { ...cardSurfaceStyle({ selected: active, accent, padding: "0 var(--gloom-space-sm)" }), cursor: "pointer" } : undefined}
308
+ onMouseDown={onPress} onMouseOver={() => onHover(true)} onMouseOut={() => onHover(false)}
309
+ data-gloom-role="buildout-flow-card" role="button" aria-label={`Open ${relation.name}`}>
310
+ <Box flexDirection="row" height={1} overflow="hidden" style={desktop ? { flexWrap: "wrap" } : undefined}>
311
+ {!desktop ? <Text fg={accent}>▍</Text> : null}
312
+ <ClippedText fg={active && !desktop ? colors.selectedText : listed ? colors.textBright : colors.text}
313
+ attributes={listed ? TextAttributes.BOLD : 0} text={relation.name} width={nameWidth} />
314
+ {relation.ticker && (desktop || displayWidth(relation.name) + displayWidth(relation.ticker) + 1 <= inner)
315
+ ? <Text fg={colors.textDim} style={desktop ? { flex: "0 0 auto" } : undefined}>{` ${relation.ticker}`}</Text> : null}
316
+ </Box>
317
+ {relation.figure ? (
318
+ <Box height={1} paddingLeft={desktop ? 0 : 1} overflow="hidden">
319
+ <ClippedText fg={active && !desktop ? colors.selectedText : colors.textDim} text={relation.figure} width={inner - (desktop ? 0 : 1)} />
320
+ </Box>
321
+ ) : null}
322
+ </Box>
323
+ );
324
+ }
325
+
326
+ function CompetitorChip({ chip, row, desktop, active, onPress }: { chip: FlowChip; row: number; desktop: boolean; active: boolean; onPress: () => void }) {
327
+ const colors = useThemeColors();
328
+ const accent = RELATION_COLORS.competitor;
329
+ const more = !chip.relation;
330
+ const background = desktop
331
+ ? active ? accent : more ? undefined : blendHex(colors.bg, accent, BADGE_TINT)
332
+ : active ? blendHex(colors.bg, accent, 0.45) : more ? undefined : blendHex(colors.bg, accent, 0.14);
333
+ const text = <ClippedText fg={more ? colors.textDim : active ? desktop ? colors.bg : colors.textBright : accent} text={chip.label} width={chip.width - 2} />;
334
+ return (
335
+ <Box position="absolute" left={chip.x} top={row} width={chip.width} height={1} justifyContent={desktop ? "center" : undefined}
336
+ onMouseDown={onPress} role="button" aria-label={chip.relation ? `Open ${chip.relation.name}` : chip.label}>
337
+ {desktop
338
+ ? <Box paddingX={1} minWidth={0} backgroundColor={background} justifyContent="center" style={{ ...NATIVE_BADGE_STYLE, cursor: more ? undefined : "pointer" }}>{text}</Box>
339
+ : <Box paddingX={1} height={1} backgroundColor={background}>{text}</Box>}
340
+ </Box>
341
+ );
342
+ }
343
+
344
+ /** The flow as a list, for a pane too narrow for columns: each group under its name. */
345
+ export function CompanyFlowList({ profile, width, onOpen }: { profile: BuildoutCompanyProfile; width: number; onOpen: (relation: BuildoutRelation) => void }) {
346
+ const colors = useThemeColors();
347
+ const roles: RelationRole[] = ["power", "customer", "supplier", "competitor"];
348
+ return (
349
+ <Box flexDirection="column" width={width} paddingX={1}>
350
+ {roles.map((role) => {
351
+ const items = profile.relations.filter((relation) => relation.role === role);
352
+ if (!items.length) return null;
353
+ return (
354
+ <Box key={role} flexDirection="column" marginBottom={1}>
355
+ <Text fg={RELATION_COLORS[role]} attributes={TextAttributes.BOLD}>{GROUP_LABELS[role]}</Text>
356
+ {items.map((relation) => (
357
+ <Box key={linkId(relation)} flexDirection="column" onMouseDown={() => onOpen(relation)}>
358
+ <ClippedText fg={relation.ticker ? colors.textBright : colors.text} text={`${relation.name}${relation.ticker ? ` ${relation.ticker}` : ""}`} width={width - 2} />
359
+ {relation.figure ? <ClippedText fg={colors.textDim} text={relation.figure} width={width - 2} /> : null}
360
+ </Box>
361
+ ))}
362
+ </Box>
363
+ );
364
+ })}
365
+ </Box>
366
+ );
367
+ }
@@ -0,0 +1,2 @@
1
+ /** The company pane's id, apart from its component so the plugin can name it without loading the pane. */
2
+ export const BUILDOUT_COMPANY_PANE_ID = "buildout-company";