@gloom-sh/terminal 0.18.5 → 0.19.0

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 (436) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +140 -7
  3. package/src/api-client/cot.ts +6 -2
  4. package/src/api-client/credit-documents.ts +6 -0
  5. package/src/api-client/data.ts +17 -6
  6. package/src/api-client/equity-screener.ts +24 -0
  7. package/src/api-client/fund-facts.ts +45 -4
  8. package/src/api-client/index.ts +11 -0
  9. package/src/api-client/market-discovery.ts +10 -0
  10. package/src/api-client/market-movers.ts +7 -0
  11. package/src/api-client/members.ts +1 -1
  12. package/src/api-client/perps.ts +43 -0
  13. package/src/api-client/research-activity.ts +3 -2
  14. package/src/api-client/types.ts +70 -2
  15. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  16. package/src/app/runtime/broker-import.ts +0 -2
  17. package/src/app/runtime/plugin-bindings.ts +73 -56
  18. package/src/architecture/pane-tabs-allowlist.json +3 -0
  19. package/src/architecture/ttl-caches-allowlist.json +19 -0
  20. package/src/brokers/sync-broker-instance.ts +2 -8
  21. package/src/capabilities/chart-series.ts +1 -1
  22. package/src/cli/commands/crypto-hints.ts +40 -24
  23. package/src/cli/commands/market.ts +72 -26
  24. package/src/cli/commands/options-chain.ts +48 -7
  25. package/src/cli/commands/options-leaps.ts +9 -3
  26. package/src/cli/commands/overview.ts +61 -12
  27. package/src/cli/commands/search.ts +21 -1
  28. package/src/cli/commands/themes.ts +11 -2
  29. package/src/cli/commands/ticker.ts +55 -34
  30. package/src/cli/company-name-hint.ts +41 -0
  31. package/src/cli/earnings-calendar.ts +1 -1
  32. package/src/cli/errors.ts +8 -0
  33. package/src/cli/glossary.ts +25 -2
  34. package/src/cli/history-rows.ts +57 -9
  35. package/src/cli/index.ts +2 -0
  36. package/src/cli/listing-arg.ts +180 -52
  37. package/src/cli/output-file.ts +68 -0
  38. package/src/cli/pane-functions/capabilities.ts +3 -0
  39. package/src/cli/pane-functions/catalog.ts +80 -7
  40. package/src/cli/pane-functions/dom.ts +13 -3
  41. package/src/cli/pane-functions/freshness.ts +86 -3
  42. package/src/cli/pane-functions/function-help.ts +13 -10
  43. package/src/cli/pane-functions/headless.ts +17 -7
  44. package/src/cli/pane-functions/index.ts +49 -14
  45. package/src/cli/pane-functions/market-session.ts +66 -4
  46. package/src/cli/pane-functions/options.ts +19 -1
  47. package/src/cli/pane-functions/resolver.ts +19 -8
  48. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  49. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  50. package/src/cli/quote-answer.ts +22 -11
  51. package/src/cli/ticker-resolution.ts +3 -2
  52. package/src/components/chart/composite/composite-chart.tsx +4 -0
  53. package/src/components/chart/composite/interactions.ts +6 -0
  54. package/src/components/chart/composite/legend.tsx +45 -7
  55. package/src/components/chart/composite/panel-gestures.ts +33 -0
  56. package/src/components/chart/composite/panel-surface.tsx +52 -3
  57. package/src/components/chart/composite/rasterizer.ts +24 -13
  58. package/src/components/chart/composite/scene.ts +39 -4
  59. package/src/components/chart/composite/series-cues.ts +14 -0
  60. package/src/components/chart/composite/text-renderer.ts +9 -2
  61. package/src/components/chart/composite/tool-catalog.ts +36 -0
  62. package/src/components/chart/composite/tools.ts +203 -44
  63. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  64. package/src/components/command-bar/assist/inventory.ts +2 -1
  65. package/src/components/command-bar/commands/direct/index.ts +28 -13
  66. package/src/components/command-bar/pane-templates/items.ts +3 -2
  67. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  68. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  69. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  70. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  71. package/src/components/onboarding/wizard-positions.ts +3 -3
  72. package/src/components/ticker/list-table-view.tsx +4 -1
  73. package/src/components/ui/choice-dialog.tsx +23 -18
  74. package/src/components/ui/choice-groups.ts +18 -0
  75. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  76. package/src/components/ui/data-table/opentui/model.ts +26 -1
  77. package/src/components/ui/data-table/types.ts +2 -0
  78. package/src/components/ui/list-view.tsx +8 -4
  79. package/src/components/ui/query-bar-keys.tsx +2 -0
  80. package/src/components/ui/query-bar.tsx +1 -0
  81. package/src/components/ui/select-button.tsx +5 -1
  82. package/src/components/ui/select-field.tsx +1 -0
  83. package/src/components/ui/table-layout.ts +8 -5
  84. package/src/core/app-runtime.ts +2 -0
  85. package/src/core/app-services.ts +2 -2
  86. package/src/data/config/chart-settings.ts +1 -1
  87. package/src/data/json-ticker-repository.ts +17 -8
  88. package/src/data/ticker-repository.ts +16 -9
  89. package/src/data/ticker-writes.ts +81 -0
  90. package/src/data/ttl-cache.ts +165 -0
  91. package/src/docs/snapshot.json +71 -137
  92. package/src/i18n/es.ts +18 -2
  93. package/src/i18n/ja.ts +18 -2
  94. package/src/i18n/ko.ts +18 -2
  95. package/src/i18n/zh-cn.ts +18 -2
  96. package/src/i18n/zh-tw.ts +18 -2
  97. package/src/market-data/depositary-pair.ts +2 -2
  98. package/src/market-data/fund-facts.ts +50 -0
  99. package/src/market-data/market/change-basis.ts +16 -3
  100. package/src/market-data/market/format.ts +25 -0
  101. package/src/market-data/market/freshness.ts +2 -1
  102. package/src/market-data/options-alternatives.ts +81 -4
  103. package/src/news/aggregator.ts +113 -9
  104. package/src/news/cloud-session.ts +21 -0
  105. package/src/news/hooks.ts +1 -0
  106. package/src/news/news-model.ts +1 -0
  107. package/src/news/types.ts +11 -0
  108. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  109. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  110. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  111. package/src/plugins/builtin/alerts/cli.ts +336 -77
  112. package/src/plugins/builtin/alerts/command.ts +25 -1
  113. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  114. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  115. package/src/plugins/builtin/alerts/format.ts +75 -5
  116. package/src/plugins/builtin/alerts/headless.ts +24 -6
  117. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  118. package/src/plugins/builtin/alerts/history.ts +1 -1
  119. package/src/plugins/builtin/alerts/index.tsx +79 -16
  120. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  121. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  122. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  123. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  124. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  125. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  126. package/src/plugins/builtin/alerts/storage.ts +12 -8
  127. package/src/plugins/builtin/alerts/types.ts +4 -0
  128. package/src/plugins/builtin/analytics/index.tsx +2 -1
  129. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  130. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  131. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  132. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  134. package/src/plugins/builtin/attention/index.tsx +2 -1
  135. package/src/plugins/builtin/attention/listing.ts +14 -0
  136. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  137. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  138. package/src/plugins/builtin/buildout/format.ts +38 -0
  139. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  140. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  141. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  142. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  143. package/src/plugins/builtin/changelog/index.tsx +9 -6
  144. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  147. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  148. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  149. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  150. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  151. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  152. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  153. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  154. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  155. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  156. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  157. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  158. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  159. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  160. package/src/plugins/builtin/company-attention/client.ts +8 -9
  161. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  162. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  163. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  164. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  165. package/src/plugins/builtin/correlation/headless.ts +13 -5
  166. package/src/plugins/builtin/correlation/history.ts +22 -0
  167. package/src/plugins/builtin/cot/client.ts +22 -18
  168. package/src/plugins/builtin/cot/headless.ts +15 -10
  169. package/src/plugins/builtin/cot/model.ts +14 -1
  170. package/src/plugins/builtin/cot/pane.tsx +22 -14
  171. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  172. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  173. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  174. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  175. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  176. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  177. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  178. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  179. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  180. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  181. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  182. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  183. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  184. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  185. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  186. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  187. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  188. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  189. package/src/plugins/builtin/docs/targets.ts +5 -1
  190. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  191. package/src/plugins/builtin/earnings/board.tsx +62 -71
  192. package/src/plugins/builtin/earnings/headless.ts +40 -4
  193. package/src/plugins/builtin/earnings/scope.ts +31 -43
  194. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  195. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  196. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  197. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  198. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  199. package/src/plugins/builtin/econ/types.ts +2 -0
  200. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  201. package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
  202. package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
  203. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  204. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  205. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  206. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  207. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  208. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  209. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  210. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  211. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  212. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  213. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  214. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  215. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  216. package/src/plugins/builtin/gpu/headless.ts +8 -2
  217. package/src/plugins/builtin/help/function-index.ts +4 -1
  218. package/src/plugins/builtin/help/index.tsx +1 -0
  219. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  220. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  221. package/src/plugins/builtin/holders/headless.ts +18 -1
  222. package/src/plugins/builtin/iv-history/client.ts +52 -11
  223. package/src/plugins/builtin/iv-history/format.ts +36 -23
  224. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  225. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  226. package/src/plugins/builtin/iv-history/model.ts +163 -27
  227. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  228. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  229. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  230. package/src/plugins/builtin/jobs/client.ts +25 -29
  231. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  232. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  233. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  234. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  235. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  236. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  237. package/src/plugins/builtin/market-movers/client.ts +25 -7
  238. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  239. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  240. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  241. package/src/plugins/builtin/market-movers/model.ts +4 -1
  242. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  243. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  244. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  245. package/src/plugins/builtin/market-movers/table.ts +17 -5
  246. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  247. package/src/plugins/builtin/news/index.tsx +1 -0
  248. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  249. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  250. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  251. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  252. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  253. package/src/plugins/builtin/openfda/client.ts +14 -2
  254. package/src/plugins/builtin/openfda/index.tsx +3 -2
  255. package/src/plugins/builtin/openfda/model.ts +9 -0
  256. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  257. package/src/plugins/builtin/options/analytics.ts +29 -5
  258. package/src/plugins/builtin/options/enrichment.ts +17 -5
  259. package/src/plugins/builtin/options/table.ts +5 -2
  260. package/src/plugins/builtin/options/types.ts +2 -0
  261. package/src/plugins/builtin/options/view.tsx +17 -4
  262. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  263. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  264. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  265. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  266. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  267. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  268. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  269. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  270. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  271. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  272. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  273. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  274. package/src/plugins/builtin/pe-band/model.ts +12 -49
  275. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  276. package/src/plugins/builtin/perps/cascade.ts +236 -0
  277. package/src/plugins/builtin/perps/client.ts +56 -2
  278. package/src/plugins/builtin/perps/headless.ts +65 -9
  279. package/src/plugins/builtin/perps/index.tsx +7 -5
  280. package/src/plugins/builtin/perps/model.ts +18 -4
  281. package/src/plugins/builtin/perps/pane.tsx +19 -22
  282. package/src/plugins/builtin/perps/status-line.ts +25 -0
  283. package/src/plugins/builtin/plugin-module.ts +5 -1
  284. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  285. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  286. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  287. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  288. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  289. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  290. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  291. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  292. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  293. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  294. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  295. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  296. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  297. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  298. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  299. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  300. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  301. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  302. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  303. package/src/plugins/builtin/research/event-model.ts +10 -8
  304. package/src/plugins/builtin/research/headless.ts +1 -1
  305. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  306. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  307. package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
  308. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  309. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  310. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  311. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  312. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  313. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  314. package/src/plugins/builtin/scanner/headless.ts +204 -35
  315. package/src/plugins/builtin/scanner/index.ts +16 -5
  316. package/src/plugins/builtin/scanner/live-headless.ts +27 -3
  317. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  318. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  319. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  320. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  321. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  322. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  323. package/src/plugins/builtin/sec/headless.ts +2 -1
  324. package/src/plugins/builtin/series-inventory.ts +2 -0
  325. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  326. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  327. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  328. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  329. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  330. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  331. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  332. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  333. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  334. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  335. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  336. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  337. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  338. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  339. package/src/plugins/builtin/themes/model.ts +16 -7
  340. package/src/plugins/builtin/themes/pane.tsx +5 -5
  341. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  342. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  343. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  344. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  345. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  346. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  347. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  348. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  349. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  350. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  351. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  352. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  353. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  354. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  355. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  357. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  358. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  359. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  360. package/src/plugins/builtin/volatility/index.tsx +2 -2
  361. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  362. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  363. package/src/plugins/catalog-manifest.ts +2 -1
  364. package/src/plugins/marketplace/model.ts +18 -5
  365. package/src/portfolio/futures-positions.ts +6 -3
  366. package/src/portfolio/position-quantity.ts +15 -9
  367. package/src/remote/resources.ts +2 -1
  368. package/src/renderers/dom/data-table/row.tsx +26 -0
  369. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  370. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  371. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  372. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  373. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  374. package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
  375. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  376. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  377. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  378. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  379. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  380. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  381. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  382. package/src/renderers/electrobun/view/main.tsx +24 -1
  383. package/src/renderers/electrobun/view/news-source.ts +3 -1
  384. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  385. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  386. package/src/renderers/opentui/start.tsx +5 -0
  387. package/src/series/registry.ts +1 -0
  388. package/src/series/types.ts +6 -0
  389. package/src/series/validate.ts +1 -0
  390. package/src/sources/gloomberb-cloud/index.ts +1 -0
  391. package/src/sources/gloomberb-cloud/news.ts +47 -7
  392. package/src/sources/provider-router/financials.ts +15 -3
  393. package/src/sources/provider-router/index.ts +8 -5
  394. package/src/state/ticker-cleanup.ts +50 -0
  395. package/src/sync/controller.ts +4 -3
  396. package/src/sync/core-contributors.ts +90 -31
  397. package/src/telemetry/stall-reports-dom.ts +38 -0
  398. package/src/telemetry/stall-reports-node.ts +75 -0
  399. package/src/telemetry/stall-reports.ts +214 -0
  400. package/src/test-support/preload.ts +14 -0
  401. package/src/theme/themes.ts +10 -0
  402. package/src/tickers/company-name.ts +44 -3
  403. package/src/tickers/listing-venues.ts +15 -3
  404. package/src/tickers/terminal-codes.ts +19 -0
  405. package/src/time-series/alignment.ts +1 -1
  406. package/src/time-series/economic.ts +18 -4
  407. package/src/time-series/field-catalog.ts +1 -1
  408. package/src/time-series/filed-trailing-eps.ts +197 -0
  409. package/src/time-series/fundamentals.ts +44 -1
  410. package/src/time-series/ice-credit.ts +3 -0
  411. package/src/time-series/price-comparison.ts +30 -2
  412. package/src/time-series/resolve-context.ts +3 -1
  413. package/src/time-series/resolve.ts +56 -4
  414. package/src/time-series/session-bar.ts +11 -0
  415. package/src/time-series/spec.ts +3 -2
  416. package/src/time-series/studies.ts +8 -2
  417. package/src/time-series/total-return.ts +160 -0
  418. package/src/time-series/transforms.ts +2 -3
  419. package/src/time-series/types.ts +22 -1
  420. package/src/types/financials.ts +32 -1
  421. package/src/types/headless.ts +21 -3
  422. package/src/types/news-source.ts +1 -0
  423. package/src/types/plugin.ts +9 -0
  424. package/src/ui/host.tsx +3 -0
  425. package/src/updater/failure.ts +4 -1
  426. package/src/utils/cli-output.ts +42 -1
  427. package/src/utils/crypto-pair.ts +44 -0
  428. package/src/utils/forward-dividend.ts +2 -0
  429. package/src/utils/fundamentals.ts +24 -3
  430. package/src/utils/fx-market-hours.ts +26 -3
  431. package/src/utils/main-thread-stall.ts +35 -2
  432. package/src/utils/market-capitalization.ts +24 -1
  433. package/src/utils/price-history.ts +7 -1
  434. package/src/utils/shell-word.ts +8 -0
  435. package/src/utils/trailing-dividend.ts +53 -4
  436. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -0,0 +1,68 @@
1
+ import { mkdir, stat, writeFile } from "fs/promises";
2
+ import { homedir } from "os";
3
+ import { posix, win32 } from "path";
4
+
5
+ interface OutputPathContext {
6
+ cwd?: string;
7
+ home?: string;
8
+ platform?: NodeJS.Platform;
9
+ }
10
+
11
+ /** `C:\Users\me\out.csv`, `C:/out.csv`, `\\server\share\out.csv`: a path only Windows reads. */
12
+ const WINDOWS_PATH = /^(?:[A-Za-z]:[\\/]|\\\\)/;
13
+
14
+ /**
15
+ * The absolute path `--output <path>` names, in any form a shell passes it:
16
+ * relative to the folder the command runs in, absolute, or under the home
17
+ * folder (`~/Desktop/rv.csv`, which a shell leaves as typed after
18
+ * `--output=`). A Windows path is read as one on Windows; anywhere else it
19
+ * is refused, since it would write a file named after the whole path.
20
+ */
21
+ export function resolveOutputPath(raw: string, context: OutputPathContext = {}): string {
22
+ const platform = context.platform ?? process.platform;
23
+ const paths = platform === "win32" ? win32 : posix;
24
+ const typed = raw.trim();
25
+ if (!typed) throw new Error("--output needs a file to write, such as --output report.csv.");
26
+ if (platform !== "win32" && WINDOWS_PATH.test(typed)) {
27
+ throw new Error(`${typed} is a Windows path. Write a path on this computer, such as --output ~/Desktop/report.csv.`);
28
+ }
29
+ const home = context.home ?? homedir();
30
+ const homeRelative = /^~(?:$|[\\/])/.test(typed);
31
+ const expanded = homeRelative ? paths.join(home, typed.slice(1)) : typed;
32
+ return paths.resolve(context.cwd ?? process.cwd(), expanded);
33
+ }
34
+
35
+ function errorCode(error: unknown): string | undefined {
36
+ return error && typeof error === "object" && "code" in error ? String((error as { code: unknown }).code) : undefined;
37
+ }
38
+
39
+ function isFolder(path: string, paths: typeof posix): Error {
40
+ return new Error(`Cannot write ${path}: it is a folder. Name a file in it, such as ${paths.join(path, "report.csv")}.`);
41
+ }
42
+
43
+ /** Fails when `path` names a folder: checked before a report loads, so it stops at once. */
44
+ export async function checkOutputPath(path: string, platform: NodeJS.Platform = process.platform): Promise<void> {
45
+ const existing = await stat(path).catch(() => {
46
+ // Intentional ignore: a path that does not exist yet is the usual case, and writing reports any real failure.
47
+ return null;
48
+ });
49
+ if (existing?.isDirectory()) throw isFolder(path, platform === "win32" ? win32 : posix);
50
+ }
51
+
52
+ /**
53
+ * Writes a command's output to `path`, creating the folders it names as
54
+ * `shot --output` does, and failing with what to do when it cannot.
55
+ */
56
+ export async function writeOutputFile(path: string, contents: string, platform: NodeJS.Platform = process.platform): Promise<void> {
57
+ const paths = platform === "win32" ? win32 : posix;
58
+ try {
59
+ await mkdir(paths.dirname(path), { recursive: true });
60
+ await writeFile(path, contents);
61
+ } catch (error) {
62
+ const code = errorCode(error);
63
+ if (code === "EISDIR") throw isFolder(path, paths);
64
+ if (code === "EACCES" || code === "EPERM") throw new Error(`Cannot write ${path}: permission denied.`);
65
+ if (code === "ENOTDIR" || code === "EEXIST") throw new Error(`Cannot write ${path}: a file stands where one of its folders would be.`);
66
+ throw new Error(`Cannot write ${path}: ${error instanceof Error ? error.message : String(error)}`);
67
+ }
68
+ }
@@ -2,6 +2,7 @@ import type {
2
2
  HeadlessPaneArgumentDef,
3
3
  HeadlessPaneDefinition,
4
4
  HeadlessPaneOptionDef,
5
+ HeadlessPaneReference,
5
6
  PaneDef,
6
7
  PaneTemplateDef,
7
8
  } from "../../types/plugin";
@@ -26,6 +27,8 @@ export interface PaneFunctionCapability {
26
27
  screenshotReadiness: PaneFunctionScreenshotReadiness;
27
28
  dataRequirements: string[];
28
29
  limitations: string[];
30
+ /** Lists the catalog prints under the options (EQS's sector and industry names). */
31
+ reference?: HeadlessPaneReference[];
29
32
  options: PaneFunctionOptionDef[];
30
33
  /** A rendered view that refuses any option it does not declare (PaneDef.reportOptionsOnly), as a headless report does. */
31
34
  strictOptions?: boolean;
@@ -79,6 +79,14 @@ function builtInPanePrefixes(paneId: string): string[] {
79
79
  return command ? getCommandPrefixes(command) : [];
80
80
  }
81
81
 
82
+ /**
83
+ * What a user types for a function, as the catalog lists it and every error names it: a template's
84
+ * shortcut or id, a pane opened by a built-in command (LAY) by that command's mnemonic, else the pane's id.
85
+ */
86
+ export function paneFunctionCode(template: PaneTemplateDef | undefined, pane: PaneDef): string {
87
+ return template?.shortcut?.prefix ?? template?.id ?? builtInPanePrefixes(pane.id)[0] ?? pane.id;
88
+ }
89
+
82
90
  function registerResolverToken(
83
91
  lookup: Map<string, PaneTemplateDef | PaneDef>,
84
92
  token: string | undefined,
@@ -195,7 +203,8 @@ export function nearFunctionCodes(
195
203
  const near = new Map<PaneTemplateDef, string>();
196
204
  for (const template of registry.paneTemplates.values()) {
197
205
  const prefix = template.shortcut?.prefix;
198
- if (!prefix) continue;
206
+ // An unlisted function is never offered for a typo.
207
+ if (!prefix || template.listed === false) continue;
199
208
  const code = [prefix, ...template.shortcut?.aliases ?? []].find((candidate) => {
200
209
  const known = normalizeLookupToken(candidate);
201
210
  return (!commandWord || known[0] === wanted[0]) && typoDistance(wanted, known) === 1;
@@ -215,6 +224,7 @@ export function suggestedFunctionCodes(registry: PaneFunctionCatalog, name: stri
215
224
  const wanted = normalizeLookupToken(name);
216
225
  if (!wanted) return [];
217
226
  return [...registry.paneTemplates.values()].flatMap((template) => {
227
+ if (template.listed === false) return [];
218
228
  const pane = registry.panes.get(template.paneId);
219
229
  const words = pane ? getHeadlessPaneDefinition(template, pane)?.discovery?.suggestFor ?? [] : [];
220
230
  const code = template.shortcut?.prefix;
@@ -277,9 +287,15 @@ async function buildTemplateCatalogEntry(
277
287
  };
278
288
  }
279
289
 
290
+ /**
291
+ * The functions `gloom catalog` and search list. An unlisted one (`listed:
292
+ * false`) is left out, and its pane is not listed in its place; `unlisted`
293
+ * keeps it, for its own help (`fn ATTN --help`).
294
+ */
280
295
  export async function buildPaneCatalogEntries(
281
296
  registry: PaneFunctionCatalog,
282
297
  context: MarketContext,
298
+ { unlisted = false }: { unlisted?: boolean } = {},
283
299
  ): Promise<PaneCatalogEntry[]> {
284
300
  const entries: PaneCatalogEntry[] = [];
285
301
  const templatedPaneIds = new Set<string>();
@@ -288,6 +304,7 @@ export async function buildPaneCatalogEntries(
288
304
  const pane = registry.panes.get(template.paneId);
289
305
  if (!pane) continue;
290
306
  templatedPaneIds.add(pane.id);
307
+ if (template.listed === false && !unlisted) continue;
291
308
  entries.push(await buildTemplateCatalogEntry(template, pane, context));
292
309
  }
293
310
 
@@ -296,7 +313,7 @@ export async function buildPaneCatalogEntries(
296
313
  const [shortcut, ...aliases] = builtInPanePrefixes(pane.id);
297
314
  const argument = paneFunctionArgument(undefined, pane);
298
315
  entries.push({
299
- token: shortcut ?? pane.id,
316
+ token: paneFunctionCode(undefined, pane),
300
317
  label: pane.name,
301
318
  description: `Open the ${pane.name} pane.`,
302
319
  paneId: pane.id,
@@ -333,6 +350,14 @@ const ALL_WORDS_WITH_FLAGS_BAND = 1000;
333
350
  const PER_MATCHED_WORD = 100;
334
351
  /** Loose matches (no function carries every word) listed before the rest are left to a refined query. */
335
352
  const LOOSE_MATCH_LIMIT = 5;
353
+ /** Loose matches a word of the query names (in the function's code, name or aliases) are all listed, up to this many. */
354
+ const LOOSE_NAMED_LIMIT = 8;
355
+ /** A query word found in what the function is called (its code, name or aliases) outweighs one found in its keywords or description. */
356
+ const PER_WORD_IN_NAME = 3;
357
+ /** A query word that begins a function's code (`vol` for VOLS, `fx` for FXC) is the start of typing it. */
358
+ const PER_WORD_STARTING_CODE = 3;
359
+ /** A query word that is one of the function's own keywords (`vol` for the volatility functions, which also say volume's) names it. */
360
+ const PER_WORD_IS_KEYWORD = 3;
336
361
 
337
362
  function phraseBand(entry: PaneCatalogEntry, phrase: string): number {
338
363
  const lower = (values: Array<string | undefined>) => values
@@ -375,6 +400,12 @@ function nameAndDescription(entry: PaneCatalogEntry): string {
375
400
  return nameAndDescriptionFields(entry).join(" ");
376
401
  }
377
402
 
403
+ /** What a function is called: its code, name and aliases, apart from the keywords and description that say what it covers. */
404
+ function identityFields(entry: PaneCatalogEntry): string[] {
405
+ return [entry.token, entry.label, entry.paneName, entry.shortcut, ...entry.aliases]
406
+ .filter((value): value is string => !!value).map((value) => value.toLowerCase().replace(/\s+/g, " "));
407
+ }
408
+
378
409
  function flagText(entry: PaneCatalogEntry): string {
379
410
  return [
380
411
  entry.argKind,
@@ -394,12 +425,14 @@ interface CatalogMatchScore {
394
425
  terms: number;
395
426
  /** Those found in the function's name, keywords or description. */
396
427
  named: number;
428
+ /** Those found in what the function is called: its code, name or aliases. */
429
+ called: number;
397
430
  }
398
431
 
399
432
  function scorePaneCatalogEntry(entry: PaneCatalogEntry, query: string): CatalogMatchScore {
400
433
  const terms = queryTerms(query);
401
- if (terms.length === 0) return { score: 1, terms: 0, named: 0 };
402
- const none = { score: 0, terms: terms.length, named: 0 };
434
+ if (terms.length === 0) return { score: 1, terms: 0, named: 0, called: 0 };
435
+ const none = { score: 0, terms: terms.length, named: 0, called: 0 };
403
436
 
404
437
  const exactTokens = [
405
438
  entry.token,
@@ -414,6 +447,8 @@ function scorePaneCatalogEntry(entry: PaneCatalogEntry, query: string): CatalogM
414
447
  const named = nameAndDescription(entry);
415
448
  const flags = flagText(entry);
416
449
  const wholeQuery = query.toLowerCase().trim().replace(/\s+/g, " ");
450
+ const called = identityFields(entry);
451
+ const codes = [entry.token, entry.shortcut, ...entry.aliases].filter((value): value is string => !!value).map((value) => normalizeLookupToken(value));
417
452
 
418
453
  let score = containsPhrase(`${named} ${flags}`, wholeQuery) ? 12 : 0;
419
454
  // A query that is one of the function's own keywords ("hedge" for OSA) beats one found inside a
@@ -423,9 +458,16 @@ function scorePaneCatalogEntry(entry: PaneCatalogEntry, query: string): CatalogM
423
458
  if (keywords.includes(wholeQuery)) score += 4;
424
459
  let matchedTerms = 0;
425
460
  let matchedNamed = 0;
461
+ let matchedCalled = 0;
426
462
  let matchedFlagKeys = 0;
427
463
  for (const term of terms) {
428
464
  const normalized = normalizeLookupToken(term);
465
+ if (called.some((field) => containsPhrase(field, term))) {
466
+ score += PER_WORD_IN_NAME;
467
+ matchedCalled += 1;
468
+ }
469
+ if (codes.some((code) => normalized.length >= 2 && code.startsWith(normalized))) score += PER_WORD_STARTING_CODE;
470
+ if (keywords.includes(term)) score += PER_WORD_IS_KEYWORD;
429
471
  // A term starts a word: "rate" is found in "rates" but not in "separate", nor "budget" in "budgetBps".
430
472
  if (exactTokens.includes(normalized)) {
431
473
  score += 8;
@@ -455,12 +497,13 @@ function scorePaneCatalogEntry(entry: PaneCatalogEntry, query: string): CatalogM
455
497
  score += Math.round((matchedTerms / terms.length) * 8);
456
498
  if (entry.capability.botSafe) score += 2;
457
499
  // One word has no phrase to prefer: it keeps the plain term scores.
458
- if (terms.length === 1) return { score, terms: 1, named: matchedNamed };
500
+ if (terms.length === 1) return { score, terms: 1, named: matchedNamed, called: matchedCalled };
459
501
  const allWords = matchedNamed === terms.length ? ALL_WORDS_BAND : matchedTerms === terms.length ? ALL_WORDS_WITH_FLAGS_BAND : 0;
460
502
  return {
461
503
  score: score + matchedTerms * PER_MATCHED_WORD + (phraseBand(entry, wholeQuery) || allWords),
462
504
  terms: terms.length,
463
505
  named: matchedNamed,
506
+ called: matchedCalled,
464
507
  };
465
508
  }
466
509
 
@@ -535,8 +578,14 @@ export function selectPaneCatalogMatches(ranked: PaneCatalogEntry[], query: stri
535
578
  const shown = every(complete).slice(0, limit);
536
579
  return { shown, hidden: ranked.length - shown.length, loose: false };
537
580
  }
581
+ // No function carries every word: the ones a word names come first, the code or name of a function being what a
582
+ // reader types ("fx vol" is FXC, whose name has fx, and VOLS, whose name has vol), ahead of one that mentions a word
583
+ // in its keywords ("implied fx" in ADR) or its flags, and however many of the words a weaker match has.
538
584
  const named = scored.filter(({ named }) => named > 0);
539
- const shown = every(named.length > 0 ? named : scored).slice(0, Math.min(limit, LOOSE_MATCH_LIMIT));
585
+ const pool = named.length > 0 ? named : scored;
586
+ const leading = pool.filter((item) => item.called > 0).slice(0, LOOSE_NAMED_LIMIT);
587
+ const rest = pool.filter((item) => !leading.includes(item)).slice(0, Math.max(0, LOOSE_MATCH_LIMIT - leading.length));
588
+ const shown = every([...leading, ...rest]).slice(0, limit);
540
589
  return { shown, hidden: ranked.length - shown.length, loose: true };
541
590
  }
542
591
 
@@ -576,7 +625,7 @@ function optionDescription(option: PaneFunctionOptionDef): string {
576
625
  const choices = optionChoices(option);
577
626
  const parts = [option.description.replace(/\.$/, "")];
578
627
  // An enum that also reads other values (a bar size of any length) lists its values as examples.
579
- if (choices.join("|").length > MAX_INLINE_CHOICES) parts.push(`${option.normalize ? "Such as" : "One of"} ${choices.join(", ")}`);
628
+ if (choices.join("|").length > MAX_INLINE_CHOICES) parts.push(`${option.list ? "Each one of" : option.normalize ? "Such as" : "One of"} ${choices.join(", ")}`);
580
629
  if (option.defaultValue !== undefined && option.defaultValue !== "") parts.push(`Default ${String(option.defaultValue)}`);
581
630
  return `${parts.join(". ")}.`;
582
631
  }
@@ -587,6 +636,26 @@ export function limitationSentence(text: string): string {
587
636
  return !trimmed || /[.!?]["')\]]*$/.test(trimmed) ? trimmed : `${trimmed}.`;
588
637
  }
589
638
 
639
+ /**
640
+ * Names as lines of at most `width`, each name whole and the names apart by
641
+ * semicolons, since a name may hold a comma (Furnishings, Fixtures & Appliances).
642
+ */
643
+ function packNames(names: readonly string[], width: number): string[] {
644
+ const lines: string[] = [];
645
+ let line = "";
646
+ names.forEach((name, index) => {
647
+ const word = index < names.length - 1 ? `${name};` : name;
648
+ if (line && line.length + 1 + word.length > width) {
649
+ lines.push(line);
650
+ line = word;
651
+ } else {
652
+ line = line ? `${line} ${word}` : word;
653
+ }
654
+ });
655
+ if (line) lines.push(line);
656
+ return lines;
657
+ }
658
+
590
659
  function renderCatalogEntry(entry: PaneCatalogEntry): string {
591
660
  const width = Math.min(cliTerminalWidth() ?? CATALOG_TEXT_WIDTH, CATALOG_TEXT_WIDTH);
592
661
  const capability = entry.capability;
@@ -620,6 +689,10 @@ function renderCatalogEntry(entry: PaneCatalogEntry): string {
620
689
  ));
621
690
  }
622
691
 
692
+ for (const { title, items } of capability.reference ?? []) {
693
+ lines.push("", renderSection(title), ...packNames(items, width - 2).map((line) => ` ${line}`));
694
+ }
695
+
623
696
  lines.push("", renderSection("Examples"));
624
697
  if (capability.reportReadiness !== "unsupported") {
625
698
  lines.push(...wrapCommandLine(`gloom fn ${invocation}`, width));
@@ -1,6 +1,6 @@
1
1
  import type { DesktopPaneShotRenderedRow } from "../desktop-pane-shot";
2
2
  import { cliStyles, cliTerminalWidth, renderSection, renderTable, wrapText } from "../../utils/cli-output";
3
- import { formatUtcTime } from "../../utils/utc-time";
3
+ import { formatUtcTime, parseReportTime } from "../../utils/utc-time";
4
4
  import type { MarketContext } from "../types";
5
5
  import type { PaneFunctionReport } from "./report";
6
6
  import type { ResolvedPaneFunction } from "./resolver";
@@ -83,12 +83,22 @@ function domCellKeys(row: DesktopPaneShotRenderedRow): string[] {
83
83
  });
84
84
  }
85
85
 
86
+ const DAY_MS = 86_400_000;
87
+ /** A time of day in what a cell draws: `11:27`, `9am`. */
88
+ const DRAWN_CLOCK = /\d{1,2}:\d{2}|\b\d{1,2}\s?[ap]m\b/i;
89
+
86
90
  /**
87
91
  * A pane may shorten a time to fit ("Wed 11:27", "Oct 2"); the report prints
88
- * the instant behind it with its date and zone instead.
92
+ * the instant behind it with its date and zone instead. An instant at exactly
93
+ * 00:00 UTC that the cell draws without a time of day (a filing's date, an
94
+ * all-day event) is a day, not a midnight: it prints as the date, as a
95
+ * date-only value does. The CSV and JSON keep the instant.
89
96
  */
90
97
  function cellText(cell: DesktopPaneShotRenderedRow["cells"][number]): string {
91
- return cell.instant ? formatUtcTime(cell.instant) || cell.text : cell.text;
98
+ if (!cell.instant) return cell.text;
99
+ const time = parseReportTime(cell.instant);
100
+ if (time && !time.dateOnly && time.time % DAY_MS === 0 && !DRAWN_CLOCK.test(cell.text)) return formatUtcTime({ ...time, dateOnly: true });
101
+ return formatUtcTime(cell.instant) || cell.text;
92
102
  }
93
103
 
94
104
  interface DomTable {
@@ -11,7 +11,15 @@ import type {
11
11
  import { localTimeSuffix, parseReportTime, type ReportTime } from "../../utils/utc-time";
12
12
  import { isRecord } from "../../utils/guards";
13
13
  import { latestRegularSessionClose, readIndexSession } from "../../market-data/market/freshness";
14
- import { reportMarketSession, reportVenueSessionDates, type ReportMarket, type SessionObservation, type VenueSessionDate } from "./market-session";
14
+ import {
15
+ reportMarketSession,
16
+ reportVenueMarkets,
17
+ reportVenueSessionDates,
18
+ type ReportMarket,
19
+ type SessionObservation,
20
+ type VenueMarket,
21
+ type VenueSessionDate,
22
+ } from "./market-session";
15
23
  import { savedCopyShownAt } from "../../data/saved-copies";
16
24
  import { releasedText } from "../../utils/release-date";
17
25
 
@@ -77,6 +85,13 @@ export interface ReportFreshness {
77
85
  tradingDayMarket?: string;
78
86
  /** Where the venues of a quote report stand now, from their session calendars. */
79
87
  market?: ReportMarket;
88
+ /**
89
+ * With a declared `datesEachVenue`, when the rows trade on two or three
90
+ * markets that do not all stand alike: where each stands and when it opens,
91
+ * which the status line gives in place of `market` (`LSE opens 08:00 UTC ·
92
+ * US 13:30 UTC`).
93
+ */
94
+ markets?: VenueMarket[];
80
95
  /**
81
96
  * With a declared `datesEachVenue`, when the rows trade on more than one
82
97
  * market: each market's session date, which the status line gives in place
@@ -534,6 +549,9 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
534
549
  }
535
550
  const { market, closeDate, closeMarket } = reportMarketSession(signals.sessions, now);
536
551
  const venueSessions = declared.datesEachVenue && !declaredAsOf ? reportVenueSessionDates(signals.sessions, now) : [];
552
+ // Each market's own state and open, when they are not all one: `LSE opens 08:00 UTC · US 13:30 UTC`.
553
+ const venueMarkets = declared.datesEachVenue ? reportVenueMarkets(signals.sessions, now) : [];
554
+ const markets = venueMarkets.length > 0 && !sameMarketText(venueMarkets, now) ? venueMarkets : [];
537
555
  // The close wording dates the newest observation, so only when that is what `asOf` is.
538
556
  const asOfClose = closeDate && asOf && !declaredAsOf ? closeDate : null;
539
557
  const tradingDayMarket = asOfClose ? closeMarket
@@ -584,6 +602,7 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
584
602
  ...(asOfClose ? { asOfClose } : {}),
585
603
  ...(tradingDayMarket ? { tradingDayMarket } : {}),
586
604
  ...(market ? { market } : {}),
605
+ ...(markets.length > 0 ? { markets } : {}),
587
606
  ...(venueSessions.length > 0 ? { venueSessions } : {}),
588
607
  ...(lagDays != null ? { lagDays } : {}),
589
608
  ...(observation ? { observation } : {}),
@@ -857,6 +876,21 @@ function venueSessionsText(sessions: readonly VenueSessionDate[]): string {
857
876
  return groups.map(({ markets, date, close }) => `${listText(markets)} ${dayText(date)} ${close ? "close" : "session"}`).join(", ");
858
877
  }
859
878
 
879
+ /**
880
+ * An as-of the feed stamped at exactly 00:00:00.000 UTC reads as the day it
881
+ * names, as a date-only value does: a daily close, a filing's date or a
882
+ * published report's has no time of day, so `Mon 26 Oct 2026 00:00 UTC` would
883
+ * claim one. Quotes keep their time, as does anything a feed or a venue's
884
+ * sessions date, and bars of a size under a day, whose midnight bar opens then.
885
+ */
886
+ function asOfAsDay(freshness: ReportFreshness, time: ReportTime | null): ReportTime | null {
887
+ if (!time || time.dateOnly || time.time % DAY_MS !== 0) return time;
888
+ const feed = freshness.status === "live" || freshness.status === "delayed" || freshness.feed != null;
889
+ const intradayBars = freshness.basis === "intraday bars";
890
+ return feed || intradayBars || freshness.market || freshness.sources?.length || freshness.oldestAgeMinutes != null
891
+ ? time : { ...time, dateOnly: true };
892
+ }
893
+
860
894
  /**
861
895
  * The dated as-of: a session close, a UTC time, a date, or when the report was
862
896
  * retrieved; with the oldest observation unless `short`, or when the line says
@@ -868,7 +902,7 @@ function asOfText(freshness: ReportFreshness, short: boolean, local: boolean, un
868
902
  if (freshness.venueSessions?.length) return venueSessionsText(freshness.venueSessions);
869
903
  const market = freshness.tradingDayMarket;
870
904
  const period = freshness.periodicity;
871
- const time = parseReportTime(freshness.asOf);
905
+ const time = asOfAsDay(freshness, parseReportTime(freshness.asOf));
872
906
  const reference = freshness.referencePeriod;
873
907
  // `Sep 2026, released Tue 20 Oct`: the period the figure covers, then when it was released.
874
908
  const released = reference ? releasedText(freshness.asOf, reference) : null;
@@ -949,6 +983,53 @@ function marketText(market: ReportMarket, now: number, short: boolean, local: bo
949
983
  }
950
984
  }
951
985
 
986
+ /** Whether every market of a mixed report reads the same (`markets closed until Mon`): one line then says it for all. */
987
+ function sameMarketText(markets: readonly VenueMarket[], now: number): boolean {
988
+ return new Set(markets.map((market) => marketText(market, now, false, false))).size === 1;
989
+ }
990
+
991
+ /**
992
+ * Where each market of a report that mixes them stands, one after another:
993
+ * the open ones first, then those in an extended session, then the closed ones
994
+ * by when they open. `markets closed · LSE opens 08:00 UTC · US 13:30 UTC`
995
+ * while all are closed, else each its own (`LSE open · US pre-market, opens
996
+ * 13:30 UTC`). A time is the regular session's open, as `marketText` says it;
997
+ * `short` leaves out `markets closed`.
998
+ */
999
+ function marketsText(markets: readonly VenueMarket[], now: number, short: boolean, local: boolean): string {
1000
+ const opensAt = (market: VenueMarket) => market.reopensAt ? Date.parse(market.reopensAt) : Number.POSITIVE_INFINITY;
1001
+ const rank = (market: VenueMarket) => market.state === "open" ? 0 : market.state === "closed" ? 2 : 1;
1002
+ const ordered = [...markets].sort((left, right) => rank(left) - rank(right) || opensAt(left) - opensAt(right));
1003
+ // `LSE opens 08:00 UTC · US 13:30 UTC`: a verb is said once for the timed opens that follow each other
1004
+ // with it; a week's first trade after a session's open says its own (`reopens 22:00 UTC`).
1005
+ let previousVerb: string | null = null;
1006
+ const parts = ordered.map((market) => {
1007
+ const reopen = reopenText(market, now, local);
1008
+ const verb = market.state === "closed" && reopen?.time ? market.firstTrade ? "reopens" : "opens" : null;
1009
+ const said = (() => {
1010
+ switch (market.state) {
1011
+ case "open":
1012
+ return `${market.market} open`;
1013
+ case "after-hours":
1014
+ return `${market.market} after hours`;
1015
+ case "pre-market":
1016
+ return reopen?.time ? `${market.market} pre-market, opens ${reopen.text}` : `${market.market} pre-market`;
1017
+ case "closed":
1018
+ if (!reopen) return `${market.market} closed`;
1019
+ if (!reopen.time) return `${market.market} closed until ${reopen.text}`;
1020
+ return `${market.market} ${verb === previousVerb ? "" : `${verb} `}${reopen.text}`;
1021
+ default: {
1022
+ const _exhaustive: never = market.state;
1023
+ return _exhaustive;
1024
+ }
1025
+ }
1026
+ })();
1027
+ previousVerb = verb;
1028
+ return said;
1029
+ });
1030
+ return ordered.every((market) => market.state === "closed") && !short ? ["markets closed", ...parts].join(" · ") : parts.join(" · ");
1031
+ }
1032
+
952
1033
  interface StatusLineDetail {
953
1034
  /** Leave out the oldest observation. */
954
1035
  asOf?: "short";
@@ -987,7 +1068,9 @@ function statusLineParts(freshness: ReportFreshness, detail: StatusLineDetail =
987
1068
  ...(freshness.oldestAgeMinutes != null ? [`oldest ${ageText(freshness.oldestAgeMinutes)}`] : []),
988
1069
  ...(freshness.savedAt ? [savedCopyText(freshness.savedAt, freshness.retrievedAt, local)]
989
1070
  : [statusText(detail.basis === "none" ? { ...freshness, basis: undefined } : freshness, detail.status === "short")].filter(Boolean)),
990
- ...(freshness.market && detail.market !== "none" ? [marketText(freshness.market, now, detail.market === "short", local)] : []),
1071
+ ...(detail.market === "none" ? []
1072
+ : freshness.markets?.length ? [marketsText(freshness.markets, now, detail.market === "short", local)]
1073
+ : freshness.market ? [marketText(freshness.market, now, detail.market === "short", local)] : []),
991
1074
  ...(freshness.note && detail.note !== "none" ? [typeof detail.note === "number" ? freshness.noteVariants?.[detail.note] ?? freshness.note : freshness.note] : []),
992
1075
  ];
993
1076
  }
@@ -234,9 +234,10 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
234
234
  bloomberg: ["RV"],
235
235
  },
236
236
  EQS: {
237
- summary: "Screen covered stocks on valuation, growth, margins, liquidity, volatility, short interest, insider activity and 13F holders, then rank the matches by any metric. "
238
- + "ATR% is the 14-session average true range as a percent of the last close. IV rank is a rank, not an IV percentile: 0 is the lowest 30-day implied volatility of the past 52 weeks and 100 the highest.",
239
- usage: ["EQS"],
237
+ summary: "Screen covered stocks on valuation, growth, margins, liquidity, volatility, technicals, float and ownership, short interest, insider activity and 13F holders, then rank the matches by any metric. "
238
+ + "ATR% is the 14-session average true range as a percent of the last close. IV rank is a rank, not an IV percentile: 0 is the lowest 30-day implied volatility of the past 52 weeks and 100 the highest. IV percentile, IV30 change in vol points, HV20 and HV rank screen premium to sell. "
239
+ + "Technical fields read the last completed session: RSI 14, price vs SMA 20, 50 and 200, trading days since a MACD or golden cross and since a new 20-day or 52-week high or low, and Bollinger width percentile (0 is a six-month squeeze). Ideas holds ready technical screens.",
240
+ usage: ["EQS", "gloom fn EQS --preset golden-cross"],
240
241
  keys: [key("a", "dd criterion"), key("m", "etric"), key("t", "check ticker"), key("s", "ave screen"), key("o", "pen research")],
241
242
  data: same("Daily snapshot, statements as filed"),
242
243
  bloomberg: ["EQS"],
@@ -285,8 +286,10 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
285
286
  },
286
287
  CSP: {
287
288
  summary: "Which names pay most to sell a put? For each, the put nearest a delta (0.30) on the expiry nearest a date or a number of days out (30), "
288
- + "ranked by annualised yield on cash at the bid, with % out of the money, delta, IV rank, the spread (marked wide, never left out) and an earnings flag.",
289
- usage: ["CSP JPM, GS, KO", "gloom fn CSP JPM GS KO AAPL --delta 0.20 --days 45"],
289
+ + "ranked by annualised yield on cash at the bid, with % out of the money, delta, IV rank, the spread (marked wide, never left out) and an earnings flag. "
290
+ + "--strategy screens covered calls, verticals, iron condors, straddles, strangles and calendars the same way, with the trade, credit or debit, "
291
+ + "max gain and loss per contract, break-evens, probability of profit, and an income trade's annualised return or a debit trade's reward to risk.",
292
+ usage: ["CSP JPM, GS, KO", "gloom fn CSP JPM GS KO AAPL --delta 0.20 --days 45", "gloom fn CSP JPM GS KO --strategy iron-condor"],
290
293
  keys: [key("Enter", " open in OSA")],
291
294
  data: OPTIONS,
292
295
  bloomberg: [],
@@ -713,8 +716,8 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
713
716
 
714
717
  // Watch markets
715
718
  MOST: {
716
- summary: "Today's top gainers, losers, most active and trending tickers with price, change, volume against average and market cap, and the pre-market and after-hours movers. On Pro, the gaps at the open.",
717
- usage: ["MOST"],
719
+ summary: "Today's top gainers, losers, most active and trending tickers with price, change, volume against average and market cap, and the pre-market and after-hours movers. On Pro, the gaps at the open. Min float keeps the names with at least that many shares in the float.",
720
+ usage: ["MOST", "gloom fn MOST --list gainers --min-float 10M"],
718
721
  keys: [TABS, OPEN],
719
722
  data: { free: DELAYED, pro: REAL_TIME },
720
723
  bloomberg: ["MOST"],
@@ -902,9 +905,9 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
902
905
  bloomberg: [],
903
906
  },
904
907
  FLOW: {
905
- summary: "Sweeps, blocks and large premium prints as they hit the tape, with ticker, strike, expiry and premium.",
906
- usage: ["FLOW"],
907
- keys: [OPEN],
908
+ summary: "Sweeps, blocks and large premium prints, and the session's net premium tide.",
909
+ usage: ["FLOW", "FLOW NVDA"],
910
+ keys: [TABS, OPEN],
908
911
  data: pro(REAL_TIME),
909
912
  access: "pro",
910
913
  bloomberg: ["FLOW"],
@@ -42,6 +42,7 @@ import type { ResolvedPaneFunction } from "./resolver";
42
42
  import { isRecord } from "../../utils/guards";
43
43
  import { splitSymbolWords } from "../symbol-args";
44
44
  import { formatUtcTime, localTimeSuffix } from "../../utils/utc-time";
45
+ import { formatCompact } from "../../utils/format";
45
46
  import { deriveHeadlessFreshness, formatFreshnessLine, staleRowsText, withSavedCopy, type ReportFreshness } from "./freshness";
46
47
  import { DATA_SERVICE_UNREACHABLE_MESSAGE } from "../../utils/network-failure";
47
48
  import { accessGateStatus, errorsAccessGate, isProLock, reportGateText } from "./access-gate";
@@ -235,7 +236,12 @@ export function createHeadlessPaneContext(
235
236
  command, noun: "a quote", retry: (key) => `gloom ${command} ${argumentWith(key)}`,
236
237
  }, { refresh: !!context.refresh });
237
238
  if (!read) return null;
238
- return { symbol: read.symbol, ...read.exchange ? { exchange: read.exchange } : {}, ...read.name ? { name: read.name } : {} };
239
+ return {
240
+ symbol: read.symbol,
241
+ ...read.exchange ? { exchange: read.exchange } : {},
242
+ ...read.name ? { name: read.name } : {},
243
+ ...read.coinNote ? { coinNote: read.coinNote } : {},
244
+ };
239
245
  },
240
246
  };
241
247
  }
@@ -369,8 +375,8 @@ export function serializeHeadlessPaneResult(
369
375
  }
370
376
  case "series": {
371
377
  const series = result as HeadlessSeriesResult;
372
- // `textDecimals` only shapes the text report; JSON keeps every digit and does not carry it.
373
- return { ...common, series: series.series.map(({ textDecimals: _text, ...kept }) => kept), ...(series.stats ? { stats: series.stats } : {}) };
378
+ // `textDecimals` and `textCompact` only shape the text report; JSON keeps every digit and does not carry them.
379
+ return { ...common, series: series.series.map(({ textDecimals: _decimals, textCompact: _compact, ...kept }) => kept), ...(series.stats ? { stats: series.stats } : {}) };
374
380
  }
375
381
  case "snapshot": {
376
382
  const snapshot = result as HeadlessSnapshotResult;
@@ -380,6 +386,7 @@ export function serializeHeadlessPaneResult(
380
386
  asOf: snapshot.asOf,
381
387
  columns: serializableColumns(columns),
382
388
  items: snapshot.items,
389
+ ...(snapshot.empty && snapshot.items.length === 0 ? { empty: snapshot.empty } : {}),
383
390
  };
384
391
  }
385
392
  default: {
@@ -495,14 +502,16 @@ function seriesPointExportDate(series: HeadlessSeries, date: SeriesPointDate, da
495
502
  * points, multiples and
496
503
  * ratios (P/E, EV/EBITDA, one series over another, VIX 3M / VIX) to two
497
504
  * decimals, which a ratio under 0.1 trades for three significant digits so it
498
- * is not flattened to 0.03, and a 0 to 100 score to a tenth of a point. The
499
- * exports keep every digit. Any other unit prints as loaded, without float
500
- * noise.
505
+ * is not flattened to 0.03, and a 0 to 100 score to a tenth of a point. A
506
+ * company's reported total (`textCompact`) reads as the tables print one:
507
+ * 63.33B. The exports keep every digit. Any other unit prints as loaded,
508
+ * without float noise.
501
509
  */
502
510
  function seriesValue(series: HeadlessSeries, value: number | null): string {
503
511
  if (value == null) return "-";
504
512
  if (!Number.isFinite(value)) return displayNumber(value);
505
513
  if (series.textDecimals != null) return value.toFixed(series.textDecimals);
514
+ if (series.textCompact) return formatCompact(value);
506
515
  const unit = series.unit?.trim().toLowerCase();
507
516
  if (unit === "%" || unit === "pp") return value.toFixed(2);
508
517
  if (unit === "x" || unit === "ratio") return value !== 0 && Math.abs(value) < 0.1 ? value.toPrecision(3) : value.toFixed(2);
@@ -701,6 +710,7 @@ function emptyReportReason(
701
710
  const bundleReason = definition.shape === "bundle" ? (result as HeadlessBundleResult).empty : undefined;
702
711
  if (bundleReason) return bundleReason;
703
712
  const ownReason = definition.shape === "rows" ? !!(result as HeadlessRowsResult).empty
713
+ : definition.shape === "snapshot" ? !!(result as HeadlessSnapshotResult).empty
704
714
  : definition.shape === "bundle" && (result as HeadlessBundleResult).sections.some((section) => "empty" in section && !!section.empty && !isLockedSection(section));
705
715
  const { failed } = splitErrors(result);
706
716
  if (ownReason || failed.length) return null;
@@ -909,7 +919,7 @@ export function renderHeadlessPaneText(
909
919
  case "snapshot": {
910
920
  const snapshot = result as HeadlessSnapshotResult;
911
921
  // The closing source line carries the snapshot's as-of, so it is not printed twice.
912
- lines.push(renderRows(snapshot.items, undefined, definition.columns));
922
+ lines.push(renderRows(snapshot.items, undefined, definition.columns, snapshot.empty));
913
923
  break;
914
924
  }
915
925
  default: {