@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
@@ -235,7 +235,7 @@ export function ratingTargetDelta(row: AnalystResearchData["ratings"][number]):
235
235
  return row.currentPriceTarget - prior;
236
236
  }
237
237
 
238
- export type RatingColumnId = "date" | "firm" | "action" | "current" | "target" | "prior";
238
+ export type RatingColumnId = "date" | "firm" | "action" | "current" | "target" | "prior" | "reason";
239
239
  export type RatingColumn = DataTableColumn & { id: RatingColumnId } & Partial<RatingTargetColumnSizing>;
240
240
 
241
241
  export interface RatingSortPreference {
@@ -255,6 +255,7 @@ const DEFAULT_RATING_SORT_DIRECTIONS: Record<RatingColumnId, SortDirection> = {
255
255
  current: "asc",
256
256
  target: "desc",
257
257
  prior: "asc",
258
+ reason: "asc",
258
259
  };
259
260
 
260
261
  const BASE_RATING_COLUMNS: RatingColumn[] = [
@@ -266,6 +267,36 @@ const BASE_RATING_COLUMNS: RatingColumn[] = [
266
267
  { id: "prior", label: "PRIOR", width: 13, align: "left" },
267
268
  ];
268
269
 
270
+ /** The reason coverage gives for an action: last, taking the room the pane has left. */
271
+ const REASON_COLUMN: RatingColumn = { id: "reason", label: "REASON", width: 28, align: "left", flexGrow: 1 };
272
+
273
+ /** The line coverage of the action quotes, or null when none gives one. */
274
+ export function ratingReason(row: AnalystRatingRecord): AnalystRatingRecord["reason"] | null {
275
+ const reason = row.reason;
276
+ return reason?.text?.trim() && reason.url?.trim() ? reason : null;
277
+ }
278
+
279
+ /**
280
+ * The status bar room the selected action's article takes: its `source`
281
+ * link, the `o` hint that opens it and their separators. The consensus
282
+ * context gives way to it rather than cut it.
283
+ */
284
+ export function ratingReasonFooterWidth(reason: AnalystRatingRecord["reason"] | null): number {
285
+ if (!reason) return 0;
286
+ return displayWidth(`source ${ratingReasonSource(reason)}`) + displayWidth(" · ") + displayWidth("[o]pen") + 2;
287
+ }
288
+
289
+ /** Who published the reason: the publisher's name, else the article's site. */
290
+ export function ratingReasonSource(reason: NonNullable<AnalystRatingRecord["reason"]>): string {
291
+ const named = reason.source?.trim();
292
+ if (named) return named;
293
+ try {
294
+ return new URL(reason.url).hostname.replace(/^www\./, "");
295
+ } catch {
296
+ return "source";
297
+ }
298
+ }
299
+
269
300
  export function buildRatingColumns(
270
301
  rows: readonly AnalystRatingRecord[],
271
302
  currency: string | undefined,
@@ -288,9 +319,11 @@ export function buildRatingColumns(
288
319
  BASE_RATING_COLUMNS.find((column) => column.id === "target")?.width ?? 13,
289
320
  );
290
321
 
291
- return BASE_RATING_COLUMNS.map((column) => {
322
+ const columns = BASE_RATING_COLUMNS.map((column) => {
292
323
  return column.id === "target" ? { ...column, ...targetSizing, width: targetWidth } : column;
293
324
  });
325
+ // A history with no reason anywhere keeps the table it always had.
326
+ return rows.some((row) => ratingReason(row)) ? [...columns, REASON_COLUMN] : columns;
294
327
  }
295
328
 
296
329
  function normalizedText(value: string | undefined): string | null {
@@ -316,6 +349,8 @@ function ratingSortValue(row: AnalystRatingRecord, columnId: RatingColumnId): st
316
349
  return ratingTargetSortValue(row);
317
350
  case "prior":
318
351
  return normalizedText(row.prior);
352
+ case "reason":
353
+ return normalizedText(ratingReason(row)?.text);
319
354
  }
320
355
  }
321
356
 
@@ -8,7 +8,7 @@ import {
8
8
  spanAxisFormatter,
9
9
  staticSeries,
10
10
  useChartTableSelection,
11
- usePaneFooter,
11
+ useExternalLinkFooter,
12
12
  type ChartTableSelection,
13
13
  type DataTableCell,
14
14
  type DataTableKeyEvent,
@@ -39,6 +39,9 @@ import {
39
39
  buildRatingColumns,
40
40
  firstRatingSortDirection,
41
41
  formatRatingTarget,
42
+ ratingReason,
43
+ ratingReasonFooterWidth,
44
+ ratingReasonSource,
42
45
  ratingSplit,
43
46
  ratingTargetDelta,
44
47
  sortRatingRows,
@@ -142,13 +145,16 @@ function AnalystHeader({ data, history, series, currency, width, height, tableRo
142
145
  /**
143
146
  * Streams the bound symbol and keeps the status bar's reference price on it.
144
147
  * Rendered beside the table so a tick redraws the footer, not the ratings.
148
+ * The selected action's reason, when coverage gave one, is a link there.
145
149
  */
146
- function AnalystFooter({ data, loading, error, width, binding }: {
150
+ function AnalystFooter({ data, loading, error, width, binding, focused, reason }: {
147
151
  data: AnalystResearchData | null;
148
152
  loading: boolean;
149
153
  error: string | null;
150
154
  width: number;
151
155
  binding: AnalystQuoteBinding;
156
+ focused: boolean;
157
+ reason: ReturnType<typeof ratingReason>;
152
158
  }) {
153
159
  // The upside is a percentage; about one update a second is enough for it.
154
160
  const financials = useLiveTickerFinancials(binding.ticker ? binding.symbol : null, binding.ticker, {
@@ -157,9 +163,17 @@ function AnalystFooter({ data, loading, error, width, binding }: {
157
163
  weight: 40,
158
164
  });
159
165
  const reference = analystReferencePrice(data, financials?.quote);
160
- usePaneFooter("analyst-research", () => ({
161
- info: buildAnalystFooterInfo(data, { width, loading, error, reference }),
162
- }), [data, error, loading, reference.freshness, reference.live, reference.price, width]);
166
+ const info = useMemo(
167
+ () => buildAnalystFooterInfo(data, { width: width - ratingReasonFooterWidth(reason), loading, error, reference }),
168
+ [data, error, loading, reason, reference.freshness, reference.live, reference.price, width],
169
+ );
170
+ useExternalLinkFooter({
171
+ registrationId: "analyst-research",
172
+ focused,
173
+ url: reason?.url,
174
+ source: reason ? ratingReasonSource(reason) : null,
175
+ info,
176
+ });
163
177
  return null;
164
178
  }
165
179
 
@@ -246,6 +260,9 @@ export function AnalystResearchView({ focused, width, height }: { focused: boole
246
260
  }
247
261
  case "prior":
248
262
  return { text: row.prior ?? "-", color: colors.textDim };
263
+ case "reason":
264
+ // Blank, not a dash: no coverage gave a reason.
265
+ return { text: ratingReason(row)?.text ?? "", color: colors.textDim };
249
266
  }
250
267
  }, [ratingCurrency]);
251
268
 
@@ -258,6 +275,7 @@ export function AnalystResearchView({ focused, width, height }: { focused: boole
258
275
  }));
259
276
  }, []);
260
277
 
278
+ const selectedRow = rows.length > 0 ? rows[selectedIdx] : undefined;
261
279
  const footer = (
262
280
  <AnalystFooter
263
281
  data={authWall ? null : data}
@@ -265,6 +283,8 @@ export function AnalystResearchView({ focused, width, height }: { focused: boole
265
283
  error={authWall ? null : error}
266
284
  width={width}
267
285
  binding={binding}
286
+ focused={focused}
287
+ reason={authWall || !selectedRow ? null : ratingReason(selectedRow)}
268
288
  />
269
289
  );
270
290
 
@@ -1,10 +1,7 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
2
  import { parseTickerListInput, formatTickerListInput } from "../../../tickers/list";
3
3
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
4
- import { AnalystResearchView } from "./analyst-pane";
5
4
  import { CorporateActionsView } from "./corporate-actions-pane";
6
- import { EquityDiagnosticView } from "./equity-diagnostic-pane";
7
- import { RelativeValuationPane } from "./relative-valuation-pane";
8
5
  import { relativeValuationHeadless } from "./relative-valuation-headless";
9
6
  import { AUTO_PEERS_SETTING, relativeValuationTitle } from "./relative-valuation-peers";
10
7
  import { buildRelativeValuationSettingsDef, MAX_RELATIVE_VALUATION_TICKERS } from "./relative-valuation-settings";
@@ -29,14 +26,14 @@ export const researchModule: PluginModule = {
29
26
  id: "analyst-research",
30
27
  name: "Analyst",
31
28
  order: 32,
32
- component: AnalystResearchView,
29
+ get component(): typeof import("./analyst-pane").AnalystResearchView { const { AnalystResearchView } = require("./analyst-pane"); return AnalystResearchView; },
33
30
  instruments: ["equity"],
34
31
  });
35
32
  ctx.registerTickerResearchTab({
36
33
  id: "equity-diagnostic",
37
34
  name: "Diagnostic",
38
35
  order: 33,
39
- component: EquityDiagnosticView,
36
+ get component(): typeof import("./equity-diagnostic-pane").EquityDiagnosticView { const { EquityDiagnosticView } = require("./equity-diagnostic-pane"); return EquityDiagnosticView; },
40
37
  instruments: ["equity"],
41
38
  });
42
39
  ctx.registerTickerResearchTab({
@@ -53,7 +50,7 @@ export const researchModule: PluginModule = {
53
50
  id: "analyst-research",
54
51
  name: "Analyst Research",
55
52
  icon: "A",
56
- component: AnalystResearchView,
53
+ get component(): typeof import("./analyst-pane").AnalystResearchView { const { AnalystResearchView } = require("./analyst-pane"); return AnalystResearchView; },
57
54
  defaultPosition: "right",
58
55
  tickerFollower: true,
59
56
  instruments: ["equity"],
@@ -68,7 +65,7 @@ export const researchModule: PluginModule = {
68
65
  reportRequirements: ["Free Gloom sign-in"],
69
66
  name: "Equity Diagnostic",
70
67
  icon: "D",
71
- component: EquityDiagnosticView,
68
+ get component(): typeof import("./equity-diagnostic-pane").EquityDiagnosticView { const { EquityDiagnosticView } = require("./equity-diagnostic-pane"); return EquityDiagnosticView; },
72
69
  defaultPosition: "right",
73
70
  tickerFollower: true,
74
71
  instruments: ["equity"],
@@ -91,7 +88,7 @@ export const researchModule: PluginModule = {
91
88
  id: "relative-valuation",
92
89
  name: "Relative Valuation",
93
90
  icon: "R",
94
- component: RelativeValuationPane,
91
+ get component(): typeof import("./relative-valuation-pane").RelativeValuationPane { const { RelativeValuationPane } = require("./relative-valuation-pane"); return RelativeValuationPane; },
95
92
  headless: relativeValuationHeadless,
96
93
  defaultPosition: "right",
97
94
  defaultMode: "floating",
@@ -7,7 +7,6 @@ import {
7
7
  RESEARCH_SEARCH_PANE_ID,
8
8
  RESEARCH_SEARCH_TEMPLATE_ID,
9
9
  } from "./model";
10
- import { ResearchSearchPane } from "./pane";
11
10
 
12
11
  const description =
13
12
  "Full-text search across earnings call transcripts, news wires, and SEC filings, with saved searches and keyword alerts.";
@@ -40,7 +39,7 @@ export const researchSearchPlugin: GloomPlugin = {
40
39
  reportFreshness: { status: "not-a-feed", basis: "search results" },
41
40
  name: "Research Search",
42
41
  icon: "RS",
43
- component: ResearchSearchPane,
42
+ get component(): typeof import("./pane").ResearchSearchPane { const { ResearchSearchPane } = require("./pane"); return ResearchSearchPane; },
44
43
  defaultPosition: "right",
45
44
  defaultMode: "floating",
46
45
  defaultFloatingSize: { width: 118, height: 32 },
@@ -1,13 +1,25 @@
1
1
  import type { RevenueBreakdownView } from "../../../api-client/revenue-breakdown";
2
2
  import type { HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/plugin";
3
3
  import { fetchRevenueBreakdown } from "./client";
4
- import { growthPercent, quarterLabel, reportedSpan, revenueAmount, sharePercent } from "./model";
4
+ import { growthPercent, quarterLabel, reportedSpan, revenueScale, sharePercent } from "./model";
5
5
  import { SEC_FILINGS } from "../shared/report-freshness";
6
6
  import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
7
7
 
8
- /** Amounts compact and shares and growth in percent, as the pane prints them (15.97B, 83.7%, +10.8%); exports keep the whole number. */
8
+ /** Shares and growth in percent, as the pane prints them (83.7%, +10.8%), amounts on the table's scale; exports keep the whole number. */
9
9
  const number = (format: (value: number | null) => string) => (value: unknown) => format(typeof value === "number" ? value : null);
10
- const amountColumn = (key: string, header: string): HeadlessPaneColumn => ({ key, header, align: "right", textFormat: number(revenueAmount) });
10
+
11
+ /**
12
+ * A table's amounts on its one scale (`revenueScale`), named once in the text
13
+ * report's heading ("Total revenue, USD millions"). Exports keep the section
14
+ * name and the whole number.
15
+ */
16
+ function amountScale(values: ReadonlyArray<number | null>, currency: string | null) {
17
+ const scale = revenueScale(values, currency);
18
+ return {
19
+ section: (title: string) => ({ title, ...scale.unit ? { textTitle: `${title}, ${scale.unit}` } : {} }),
20
+ column: (key: string, header: string): HeadlessPaneColumn => ({ key, header, align: "right", textFormat: number(scale.format) }),
21
+ };
22
+ }
11
23
 
12
24
  export const revenueBreakdownHeadless: HeadlessPaneDefinition<"bundle"> = {
13
25
  shape: "bundle",
@@ -46,16 +58,18 @@ export const revenueBreakdownHeadless: HeadlessPaneDefinition<"bundle"> = {
46
58
  ctx.apiClient,
47
59
  await resolveHeadlessIssuerListing(ctx, symbol),
48
60
  ));
61
+ const rowScale = amountScale(data.rows.flatMap((row) => [...row.values, row.ttm]), data.currency);
62
+ const totalScale = amountScale(data.total, data.currency);
49
63
  return {
50
64
  complete: data.access === "full",
51
65
  errors: data.lockedRows > 0 ? [`${data.lockedRows} more rows need Gloom Pro`] : [],
52
66
  sections: [
53
67
  {
54
- title: `Quarterly revenue by ${data.view}`,
68
+ ...rowScale.section(`Quarterly revenue by ${data.view}`),
55
69
  columns: [
56
70
  { key: data.view, header: data.view[0]!.toUpperCase() + data.view.slice(1) },
57
- ...data.periods.map((period) => amountColumn(quarterLabel(period), quarterLabel(period))),
58
- amountColumn("ttm", "TTM"),
71
+ ...data.periods.map((period) => rowScale.column(quarterLabel(period), quarterLabel(period))),
72
+ rowScale.column("ttm", "TTM"),
59
73
  { key: "share", header: "Share", align: "right", textFormat: number(sharePercent) },
60
74
  { key: "yoy", header: "YoY", align: "right", textFormat: number(growthPercent) },
61
75
  ],
@@ -68,8 +82,8 @@ export const revenueBreakdownHeadless: HeadlessPaneDefinition<"bundle"> = {
68
82
  })),
69
83
  },
70
84
  {
71
- title: "Total revenue",
72
- columns: [{ key: "quarter", header: "Quarter" }, { key: "end", header: "End" }, amountColumn("revenue", "Revenue")],
85
+ ...totalScale.section("Total revenue"),
86
+ columns: [{ key: "quarter", header: "Quarter" }, { key: "end", header: "End" }, totalScale.column("revenue", "Revenue")],
73
87
  rows: data.periods.map((period, index) => ({
74
88
  quarter: quarterLabel(period),
75
89
  end: period.end,
@@ -1,4 +1,5 @@
1
1
  import type { DataTableColumn } from "../../../components";
2
+ import { pickUnit } from "../../../utils/format";
2
3
  import type {
3
4
  RevenueBreakdownPayload,
4
5
  RevenueBreakdownPeriod,
@@ -83,6 +84,28 @@ export function revenueAmount(value: number | null): string {
83
84
  return `${value < 0 ? "-" : ""}${scaled.toFixed(digits)}${suffix}`;
84
85
  }
85
86
 
87
+ const SCALE_WORDS: Record<string, string> = { K: "thousands", M: "millions", B: "billions", T: "trillions" };
88
+
89
+ /**
90
+ * One scale for a report table's amounts, its largest one's, so a 0.0 never sits beside a 5.52M: `unit`
91
+ * names it once ("USD millions") and `format` prints each amount in it, with one count of decimals down the
92
+ * table (179.3 and 0.0, or 54.32 and 7.88). An amount too small for that many decimals keeps two significant
93
+ * digits (0.030), so only a true zero reads 0.0. The pane keeps each amount's own suffix (`revenueAmount`).
94
+ */
95
+ export function revenueScale(values: ReadonlyArray<number | null>, currency: string | null): { unit: string; format: (value: number | null) => string } {
96
+ const amounts = values.filter((value): value is number => value !== null && Number.isFinite(value));
97
+ const { suffix, divisor } = pickUnit(amounts);
98
+ const digits = Math.max(0, ...amounts.map((value) => Math.abs(value) / divisor)) >= 100 ? 1 : 2;
99
+ const format = (value: number | null) => {
100
+ if (value === null || !Number.isFinite(value)) return revenueAmount(null);
101
+ const scaled = value / divisor;
102
+ if (scaled === 0 || Number(scaled.toFixed(digits)) !== 0) return scaled.toFixed(digits);
103
+ // Two significant digits: as many decimals as the first one's place, and one more.
104
+ return scaled.toFixed(Math.min(20, 1 - Math.floor(Math.log10(Math.abs(scaled)))));
105
+ };
106
+ return { unit: [currency?.trim(), SCALE_WORDS[suffix]].filter(Boolean).join(" "), format };
107
+ }
108
+
86
109
  export const sharePercent = (value: number | null) =>
87
110
  value === null ? "--" : `${(value * 100).toFixed(1)}%`;
88
111
 
@@ -110,7 +110,9 @@ export function ReverseDcfPane({ width, height, focused }: PaneProps) {
110
110
  }, [model]);
111
111
 
112
112
  const selectedId = String(discountRate);
113
- const contentHeight = Math.max(4, height - 1 - statGridRows(stats, width));
113
+ // A figure wraps only into rows the table's least leaves, so the pane never spills.
114
+ const statMaxLines = height - 1 - 4;
115
+ const contentHeight = Math.max(4, height - 1 - statGridRows(stats, width, undefined, { maxLines: statMaxLines }));
114
116
 
115
117
  return <Box width={width} height={height} flexDirection="column" overflow="hidden">
116
118
  <QueryBar width={width} filters={[
@@ -120,7 +122,7 @@ export function ReverseDcfPane({ width, height, focused }: PaneProps) {
120
122
  {!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="reverse DCF" loading={inputs.loading && !model}
121
123
  error={!model ? inputs.error ?? identityError ?? null : model.error} empty={false}>
122
124
  {model && !model.error ? <>
123
- <StatGrid items={stats} width={width} />
125
+ <StatGrid items={stats} width={width} maxLines={statMaxLines} />
124
126
  <DataTableView<Row> focused={focused} columns={COLUMNS} items={model.sensitivity} rootWidth={width} rootHeight={contentHeight}
125
127
  getItemKey={(row) => String(row.discountRate)} emptyStateTitle="No implied growth." sortColumnId={null} sortDirection="desc"
126
128
  selection={{ kind: "id", selectedId, getId: (row) => String(row.discountRate),
@@ -3,7 +3,7 @@ import {
3
3
  type CloudRiskReportListPayload,
4
4
  type CloudRiskReportPayload,
5
5
  } from "../../../api-client";
6
- import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
6
+ import { ApiRequestError, isPermanentClientError, isTransientFailure } from "../../../api-client/errors";
7
7
  import { listingAbroad, type IssuerListingParams } from "../../../api-client/paths";
8
8
  import { servedFromSavedCopy } from "../../../data/saved-copies";
9
9
  import type { HeadlessPaneApiClient, PluginPersistence } from "../../../types/plugin";
@@ -21,6 +21,8 @@ type RiskApiClient = Pick<HeadlessPaneApiClient, "getRiskReports" | "getRiskRepo
21
21
  interface RiskFreshness {
22
22
  fetchedAt: number;
23
23
  stale: boolean;
24
+ /** Its refresh failed and the copy saved at this time (ms) answered instead. */
25
+ savedAt?: number;
24
26
  refreshError?: string;
25
27
  errorStatus?: number;
26
28
  }
@@ -72,9 +74,10 @@ function loadCached<T extends object>(
72
74
  // A failed forced refresh must be retried when this pane is reopened, even
73
75
  // when the previously cached value has not reached its ordinary TTL yet.
74
76
  if (current()) failedRefreshes.set(failedKey, store);
75
- // A missing or denied resource must not be replaced by previously cached content.
76
- if (isPermanentClientError(error)) {
77
- if (current()) store?.deleteResource(kind, key, { sourceKey: CACHE_SOURCE });
77
+ // A saved copy answers only for a failure asking again may get past: a refusal or a reply that
78
+ // fails validation is the error, and a missing or denied resource also drops its copy.
79
+ if (!isTransientFailure(error)) {
80
+ if (isPermanentClientError(error) && current()) store?.deleteResource(kind, key, { sourceKey: CACHE_SOURCE });
78
81
  throw error;
79
82
  }
80
83
  const fallback = store?.getResource<T>(kind, key, { ...options, allowExpired: true });
@@ -84,6 +87,7 @@ function loadCached<T extends object>(
84
87
  ...fallback.value,
85
88
  fetchedAt: fallback.fetchedAt,
86
89
  stale: true,
90
+ savedAt: fallback.fetchedAt,
87
91
  refreshError: (error instanceof Error ? error.message : String(error)).trim() || "Request failed",
88
92
  errorStatus: error instanceof ApiRequestError ? error.status : undefined,
89
93
  }, fallback.fetchedAt);
@@ -21,7 +21,8 @@ const description =
21
21
  * filing list, which needs no account: `filings` lists them with a link to
22
22
  * each, where Item 1A is the risk factors as written, without RISK's grouping
23
23
  * or its changes since the year before. A foreign issuer files a 20-F instead
24
- * (ASML), whose Item 3.D holds them.
24
+ * (ASML), whose Item 3.D holds them. A company that files neither (IAG on
25
+ * LSE) has nothing in RISK either, and the note says so.
25
26
  */
26
27
  function riskFactorsLockNote(lock: PaneLock): Promise<string> {
27
28
  return proLockNote(lock, {
@@ -35,6 +36,7 @@ function riskFactorsLockNote(lock: PaneLock): Promise<string> {
35
36
  text: (ticker, company) => `gloom filings ${ticker} --form 20-F lists ${company}'s annual reports, each with its filing date and a link to the filing, whose Item 3.D holds the risk factors as written.`,
36
37
  answers: (symbol, context) => filesForm(symbol, "20-F", context),
37
38
  }],
39
+ noneFor: (ticker) => `${ticker}'s company files no 10-K or 20-F with the SEC, so RISK has no risk factors for it.`,
38
40
  });
39
41
  }
40
42
 
@@ -8,6 +8,7 @@ import {
8
8
  type PaneFooterSegment,
9
9
  type PaneHint,
10
10
  } from "../../../components";
11
+ import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
11
12
  import { useAsyncResource } from "../../../react/async-resource";
12
13
  import { colors } from "../../../theme/colors";
13
14
  import {
@@ -119,8 +120,9 @@ function RiskFactorsReader({
119
120
  const reportLoader = useCallback((force: boolean) => guard(loadRiskReport(ticker!, year!, { force, listing })), [guard, listing, ticker, year]);
120
121
  const detail = useAsyncResource(ticker && year !== null ? reportLoader : null, { clearOnError: isPermanentClientError });
121
122
  const report = detail.data;
122
- const listError = list.error ?? list.data?.refreshError ?? null;
123
- const reportError = detail.error ?? report?.refreshError ?? null;
123
+ // A copy saved earlier answering a failed refresh says when it was saved, in the footer every pane uses, not as an error.
124
+ const listError = list.error ?? (list.data?.savedAt == null ? list.data?.refreshError : null) ?? null;
125
+ const reportError = detail.error ?? (report?.savedAt == null ? report?.refreshError : null) ?? null;
124
126
  const refresh = useCallback(() => {
125
127
  void list.reload();
126
128
  if (year !== null) void detail.reload();
@@ -151,12 +153,7 @@ function RiskFactorsReader({
151
153
  info.push({ id: "loading", parts: [{ text: "loading", tone: "muted" }] });
152
154
  if (listError) info.push({ id: "list-error", parts: [{ text: `Report list: ${listError}`, tone: "warning" }] });
153
155
  if (reportError) info.push({ id: "report-error", parts: [{ text: `${year} report: ${reportError}`, tone: "warning" }] });
154
- if ((listError || list.data?.stale) && list.data) {
155
- info.push({ id: "list-stale", parts: [{ text: `List cached ${new Date(list.data.fetchedAt).toISOString()}`, tone: "muted" }] });
156
- }
157
- if ((reportError || report?.stale) && report) {
158
- info.push({ id: "report-stale", parts: [{ text: `Report cached ${new Date(report.fetchedAt).toISOString()}`, tone: "muted" }] });
159
- }
156
+ info.push(...savedCopyFooterInfo([list.data?.savedAt, report?.savedAt]));
160
157
  const hints: PaneHint[] = report?.docUrl
161
158
  ? [{ id: "open", key: "o", label: "pen filing", onPress: openFiling }]
162
159
  : [];
@@ -1,5 +1,4 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
- import { RoadmapPane } from "./pane";
3
2
 
4
3
  export const roadmapModule: PluginModule = {
5
4
  panes: [
@@ -7,8 +6,8 @@ export const roadmapModule: PluginModule = {
7
6
  id: "roadmap",
8
7
  name: "Roadmap",
9
8
  icon: "R",
10
- component: RoadmapPane,
11
- reportFreshness: { status: "not-a-feed", basis: "curated list" },
9
+ get component(): typeof import("./pane").RoadmapPane { const { RoadmapPane } = require("./pane"); return RoadmapPane; },
10
+ reportFreshness: { status: "not-a-feed", basis: "curated list", local: true },
12
11
  defaultPosition: "right",
13
12
  defaultMode: "floating",
14
13
  defaultFloatingSize: { width: 100, height: 30 },
@@ -1,4 +1,4 @@
1
- import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type RefObject } from "react";
1
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react";
2
2
  import { apiClient } from "../../../api-client";
3
3
  import { usePlanAccess } from "../../../api-client/plan-access";
4
4
  import type { RoadmapItem, RoadmapVote } from "../../../api-client/roadmap";
@@ -7,7 +7,9 @@ import {
7
7
  DataTableStackView,
8
8
  EMPTY_TABLE_CELL,
9
9
  isSectionedItemRow,
10
+ PaneFooterScope,
10
11
  renderSectionedRowHeader,
12
+ usePaneTabs,
11
13
  usePaneStatusFooter,
12
14
  useRowNotes,
13
15
  type DataTableCell,
@@ -23,6 +25,7 @@ import { useThemeColors } from "../../../theme/theme-context";
23
25
  import type { PaneProps } from "../../../types/plugin";
24
26
  import { Box, TextAttributes, type ScrollBoxRenderable } from "../../../ui";
25
27
  import { formatShortDate } from "../../../utils/datetime-format";
28
+ import { ChangelogPane } from "../changelog";
26
29
  import {
27
30
  isVotable,
28
31
  optimisticVote,
@@ -97,7 +100,7 @@ function RoadmapDetail({ item, failure, width, scrollRef }: {
97
100
  );
98
101
  }
99
102
 
100
- export function RoadmapPane({ focused, width, height }: PaneProps) {
103
+ function RoadmapView({ focused, width, height }: PaneProps) {
101
104
  const colors = useThemeColors();
102
105
  const client = useContext(RoadmapClientContext) ?? apiClient;
103
106
  const access = usePlanAccess();
@@ -242,3 +245,47 @@ export function RoadmapPane({ focused, width, height }: PaneProps) {
242
245
  />
243
246
  );
244
247
  }
248
+
249
+ /**
250
+ * What is coming and what came: the roadmap's features, and the Changelog as
251
+ * CHG shows it. The first tab is not "Roadmap", which the title already says.
252
+ */
253
+ const VIEWS = [
254
+ { value: "roadmap", label: "Features" },
255
+ { value: "changelog", label: "Changelog" },
256
+ ] as const;
257
+ type ViewId = (typeof VIEWS)[number]["value"];
258
+ const VIEW_PANES: Record<ViewId, (props: PaneProps) => ReactNode> = {
259
+ roadmap: RoadmapView,
260
+ changelog: ChangelogPane,
261
+ };
262
+
263
+ export function RoadmapPane(props: PaneProps) {
264
+ const [storedView, setView] = usePluginPaneState<string>("view", "roadmap");
265
+ const view: ViewId = storedView === "changelog" ? "changelog" : "roadmap";
266
+ // A view stays mounted once shown, so coming back to it keeps its place.
267
+ const [mounted, setMounted] = useState<ReadonlySet<ViewId>>(() => new Set([view]));
268
+ useEffect(() => {
269
+ setMounted((current) => (current.has(view) ? current : new Set([...current, view])));
270
+ }, [view]);
271
+ const { strip, rows } = usePaneTabs({ tabs: [...VIEWS], activeValue: view, onSelect: setView, focused: props.focused, dense: true });
272
+ const bodyHeight = Math.max(1, props.height - rows);
273
+
274
+ return (
275
+ <Box width={props.width} height={props.height} flexDirection="column" overflow="hidden">
276
+ {strip}
277
+ {VIEWS.map(({ value }) => {
278
+ if (!mounted.has(value) && value !== view) return null;
279
+ const View = VIEW_PANES[value];
280
+ const active = value === view;
281
+ return (
282
+ <Box key={value} visible={active} height={bodyHeight} flexGrow={1} flexBasis={0} overflow="hidden" data-gloom-role="nested-pane-content">
283
+ <PaneFooterScope active={active}>
284
+ <View {...props} height={bodyHeight} focused={props.focused && active} />
285
+ </PaneFooterScope>
286
+ </Box>
287
+ );
288
+ })}
289
+ </Box>
290
+ );
291
+ }
@@ -1,4 +1,5 @@
1
- import type { ScannerHiloExtreme, ScannerHiloPayload } from "../../../api-client";
1
+ import type { ScannerHiloExtreme, ScannerHiloPayload, ScannerStatus } from "../../../api-client";
2
+ import { venueMarketLine } from "../../../cli/pane-functions/freshness";
2
3
 
3
4
  type HiloWindowKey = keyof ScannerHiloPayload["windows"];
4
5
  export type HiloMinPrice = "off" | "1" | "5";
@@ -94,3 +95,19 @@ export function filterHiloRows(
94
95
  ? [...filtered].sort((left, right) => right.count - left.count || right.at - left.at)
95
96
  : [...filtered].sort((left, right) => right.at - left.at);
96
97
  }
98
+
99
+ /**
100
+ * What an empty table says. The price filter when rows came in and it hid them all, or when the market is open and
101
+ * nothing has printed a new extreme yet. A closed market says so and when it reopens, in the status line's words
102
+ * (`Markets closed until Mon.`), as SCAN says its own close: its session has no new highs or lows to count.
103
+ */
104
+ export function hiloEmptyState(
105
+ { filtered, status, now }: { filtered: boolean; status: ScannerStatus | undefined; now: number },
106
+ ): { title: string; hint?: string } {
107
+ if (filtered || status !== "closed") return { title: "Nothing above the price filter yet." };
108
+ const line = venueMarketLine("NYSE", now);
109
+ return {
110
+ title: line?.state === "closed" ? `${line.text[0]!.toUpperCase()}${line.text.slice(1)}.` : "Market closed.",
111
+ hint: "New highs and lows resume at the open.",
112
+ };
113
+ }
@@ -18,7 +18,7 @@ import { usePluginTickerActions } from "../../runtime";
18
18
  import { ScannerDeniedState } from "./denied";
19
19
  import { useHiloFeed, useScannerStatusFooter } from "./feed";
20
20
  import { HiloBars } from "./hilo-bars";
21
- import { filterHiloRows, formatHiloPrice, type HiloMinPrice, type HiloSort } from "./hilo-model";
21
+ import { filterHiloRows, formatHiloPrice, hiloEmptyState, type HiloMinPrice, type HiloSort } from "./hilo-model";
22
22
 
23
23
  type Side = "lows" | "highs";
24
24
 
@@ -118,6 +118,15 @@ function HiloPane({ focused, width, height }: PaneProps) {
118
118
  );
119
119
 
120
120
  useScannerStatusFooter("hilo", feed, focused);
121
+ // Read when the feed moves, not each render: the clock only matters once a closed session's text is shown.
122
+ const status = feed.payload?.status;
123
+ const emptyStates = useMemo<Record<Side, { title: string; hint?: string }>>(() => {
124
+ const now = Date.now();
125
+ return {
126
+ lows: hiloEmptyState({ filtered: !!feed.payload?.lows.length, status, now }),
127
+ highs: hiloEmptyState({ filtered: !!feed.payload?.highs.length, status, now }),
128
+ };
129
+ }, [feed.payload?.lows.length, feed.payload?.highs.length, status]);
121
130
 
122
131
  const split = width >= SPLIT_MIN_WIDTH;
123
132
  // Zero counts before the first payload would read as a dead market, not a loading one.
@@ -168,7 +177,8 @@ function HiloPane({ focused, width, height }: PaneProps) {
168
177
  renderCell={RENDER_ROW[side]}
169
178
  selectedTextOverridesCellColor
170
179
  emptyContent={feed.payload ? undefined : <PaneStatusBody loading loadingLabel="Waiting for the scanner..." />}
171
- emptyStateTitle="Nothing above the price filter yet."
180
+ emptyStateTitle={emptyStates[side].title}
181
+ emptyStateHint={emptyStates[side].hint}
172
182
  />
173
183
  );
174
184
 
@@ -100,9 +100,11 @@ const DISPLAY_ORDER = ["symbol", "name", "price", "change", "conditions", "rvol"
100
100
  /**
101
101
  * Symbol, name, price and change, then one column per condition with the
102
102
  * figure it matched on, then context as the width allows: a column is added
103
- * only when it fits whole, the row's status first.
103
+ * only when it fits whole, the row's status first, and only when a row has
104
+ * one (after the close none does, and an empty column would take the room of
105
+ * the figures).
104
106
  */
105
- function liveScanColumns(width: number, conditions: readonly LiveScanCondition[]): DataTableColumn[] {
107
+ export function liveScanColumns(width: number, conditions: readonly LiveScanCondition[], withStatus: boolean): DataTableColumn[] {
106
108
  const own = conditions.map((condition, index): DataTableColumn => {
107
109
  const label = conditionColumnLabel(condition);
108
110
  return { id: `c:${index}`, label, width: Math.max(8, label.length), align: "right" };
@@ -121,7 +123,7 @@ function liveScanColumns(width: number, conditions: readonly LiveScanCondition[]
121
123
  ));
122
124
  let columns = order([...base, ...own]);
123
125
  for (const column of OPTIONAL_COLUMNS) {
124
- if (repeats.has(column.id)) continue;
126
+ if (repeats.has(column.id) || (column.id === "status" && !withStatus)) continue;
125
127
  const next = order([...columns, column]);
126
128
  if (getTableWidth(next) > width) break;
127
129
  columns = next;
@@ -349,7 +351,8 @@ export default function LiveScanPane({ focused, width, height }: PaneProps) {
349
351
  [payload, track],
350
352
  );
351
353
  const matches = useMemo(() => sortMatches(payload?.matches ?? [], sort, noteOf), [noteOf, payload?.matches, sort]);
352
- const columns = useMemo(() => liveScanColumns(width, scan.conditions), [scan.conditions, width]);
354
+ const withStatus = useMemo(() => matches.some((match) => noteOf(match) != null), [matches, noteOf]);
355
+ const columns = useMemo(() => liveScanColumns(width, scan.conditions, withStatus), [scan.conditions, width, withStatus]);
353
356
 
354
357
  const apply = (next: LiveScan) => {
355
358
  setStoredScan(next);