@gloom-sh/terminal 0.18.5 → 0.19.1

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 (570) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +163 -8
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +19 -7
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/hiring.ts +3 -0
  14. package/src/api-client/index.ts +15 -0
  15. package/src/api-client/ipo.ts +24 -1
  16. package/src/api-client/market-discovery.ts +10 -0
  17. package/src/api-client/market-movers.ts +7 -0
  18. package/src/api-client/members.ts +1 -1
  19. package/src/api-client/mna.ts +3 -0
  20. package/src/api-client/paths.ts +9 -1
  21. package/src/api-client/perps.ts +43 -0
  22. package/src/api-client/research-activity.ts +3 -2
  23. package/src/api-client/roadmap.ts +85 -0
  24. package/src/api-client/types.ts +82 -2
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +0 -2
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +2 -2
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/market.ts +107 -38
  35. package/src/cli/commands/news-search.ts +22 -0
  36. package/src/cli/commands/options-chain.ts +48 -7
  37. package/src/cli/commands/options-leaps.ts +9 -3
  38. package/src/cli/commands/overview.ts +147 -32
  39. package/src/cli/commands/search.ts +21 -1
  40. package/src/cli/commands/themes.ts +11 -2
  41. package/src/cli/commands/ticker.ts +87 -36
  42. package/src/cli/company-name-hint.ts +41 -0
  43. package/src/cli/earnings-calendar.ts +1 -1
  44. package/src/cli/errors.ts +25 -2
  45. package/src/cli/glossary.ts +31 -2
  46. package/src/cli/history-rows.ts +57 -9
  47. package/src/cli/index-roots.ts +0 -7
  48. package/src/cli/index.ts +2 -0
  49. package/src/cli/listing-arg.ts +235 -60
  50. package/src/cli/output-file.ts +68 -0
  51. package/src/cli/pane-functions/capabilities.ts +4 -0
  52. package/src/cli/pane-functions/catalog.ts +80 -7
  53. package/src/cli/pane-functions/discovery.ts +21 -1
  54. package/src/cli/pane-functions/dom.ts +13 -3
  55. package/src/cli/pane-functions/freshness.ts +93 -4
  56. package/src/cli/pane-functions/function-help.ts +20 -10
  57. package/src/cli/pane-functions/headless.ts +17 -7
  58. package/src/cli/pane-functions/index.ts +54 -16
  59. package/src/cli/pane-functions/market-session.ts +66 -4
  60. package/src/cli/pane-functions/options.ts +19 -1
  61. package/src/cli/pane-functions/resolver.ts +19 -8
  62. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  63. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  64. package/src/cli/quote-answer.ts +22 -11
  65. package/src/cli/saved-copy-persistence.ts +88 -0
  66. package/src/cli/ticker-resolution.ts +3 -2
  67. package/src/components/chart/composite/composite-chart.tsx +4 -0
  68. package/src/components/chart/composite/interactions.ts +6 -0
  69. package/src/components/chart/composite/legend.tsx +45 -7
  70. package/src/components/chart/composite/panel-gestures.ts +33 -0
  71. package/src/components/chart/composite/panel-surface.tsx +52 -3
  72. package/src/components/chart/composite/rasterizer.ts +24 -13
  73. package/src/components/chart/composite/scene.ts +39 -4
  74. package/src/components/chart/composite/series-cues.ts +14 -0
  75. package/src/components/chart/composite/text-renderer.ts +9 -2
  76. package/src/components/chart/composite/tool-catalog.ts +36 -0
  77. package/src/components/chart/composite/tools.ts +203 -44
  78. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  79. package/src/components/command-bar/assist/inventory.ts +2 -1
  80. package/src/components/command-bar/commands/direct/index.ts +28 -13
  81. package/src/components/command-bar/list/model.ts +5 -0
  82. package/src/components/command-bar/pane-templates/items.ts +3 -2
  83. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  84. package/src/components/command-bar/selection-runtime.ts +52 -3
  85. package/src/components/command-bar/surface/index.tsx +2 -0
  86. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  87. package/src/components/layout/pane/capture-stamp.ts +2 -2
  88. package/src/components/layout/pane/footer/index.tsx +22 -6
  89. package/src/components/layout/pane/footer/model.ts +83 -15
  90. package/src/components/layout/pane/status-footer.ts +35 -2
  91. package/src/components/layout/status-widget-space.ts +9 -0
  92. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  93. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  94. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  95. package/src/components/onboarding/wizard-positions.ts +3 -3
  96. package/src/components/ticker/list-table-view.tsx +8 -21
  97. package/src/components/ui/choice-dialog.tsx +23 -18
  98. package/src/components/ui/choice-groups.ts +18 -0
  99. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  100. package/src/components/ui/data-table/opentui/model.ts +26 -1
  101. package/src/components/ui/data-table/types.ts +2 -0
  102. package/src/components/ui/display.tsx +18 -3
  103. package/src/components/ui/icon.tsx +4 -1
  104. package/src/components/ui/list-view.tsx +8 -4
  105. package/src/components/ui/query-bar-keys.tsx +2 -0
  106. package/src/components/ui/query-bar.tsx +1 -0
  107. package/src/components/ui/select-button.tsx +5 -1
  108. package/src/components/ui/select-field.tsx +1 -0
  109. package/src/components/ui/table-layout.ts +8 -5
  110. package/src/core/app-runtime.ts +2 -0
  111. package/src/core/app-services.ts +2 -2
  112. package/src/data/config/chart-settings.ts +1 -1
  113. package/src/data/json-ticker-repository.ts +17 -8
  114. package/src/data/plugin-cache.ts +5 -3
  115. package/src/data/saved-copies.ts +8 -0
  116. package/src/data/ticker-repository.ts +16 -9
  117. package/src/data/ticker-writes.ts +81 -0
  118. package/src/data/ttl-cache.ts +165 -0
  119. package/src/docs/snapshot.json +103 -142
  120. package/src/i18n/es.ts +20 -6
  121. package/src/i18n/ja.ts +20 -6
  122. package/src/i18n/ko.ts +20 -6
  123. package/src/i18n/zh-cn.ts +20 -6
  124. package/src/i18n/zh-tw.ts +20 -6
  125. package/src/market-data/depositary-pair.ts +27 -3
  126. package/src/market-data/fund-facts.ts +50 -0
  127. package/src/market-data/market/change-basis.ts +16 -3
  128. package/src/market-data/market/format.ts +25 -0
  129. package/src/market-data/market/freshness.ts +2 -1
  130. package/src/market-data/options-alternatives.ts +81 -4
  131. package/src/market-data/quotes/metadata.ts +7 -0
  132. package/src/news/aggregator.ts +119 -9
  133. package/src/news/cloud-session.ts +21 -0
  134. package/src/news/hooks.ts +1 -0
  135. package/src/news/news-model.ts +1 -0
  136. package/src/news/types.ts +17 -0
  137. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  138. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  139. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  140. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  141. package/src/plugins/builtin/alerts/cli.ts +362 -80
  142. package/src/plugins/builtin/alerts/command.ts +25 -1
  143. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  144. package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
  145. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  146. package/src/plugins/builtin/alerts/format.ts +75 -5
  147. package/src/plugins/builtin/alerts/headless.ts +34 -12
  148. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  149. package/src/plugins/builtin/alerts/history.ts +1 -1
  150. package/src/plugins/builtin/alerts/index.tsx +79 -16
  151. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  152. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  153. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  154. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  155. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  156. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  157. package/src/plugins/builtin/alerts/storage.ts +12 -8
  158. package/src/plugins/builtin/alerts/types.ts +4 -0
  159. package/src/plugins/builtin/analytics/index.tsx +2 -1
  160. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  161. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  162. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  163. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  164. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  165. package/src/plugins/builtin/attention/index.tsx +2 -1
  166. package/src/plugins/builtin/attention/listing.ts +14 -0
  167. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  168. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  169. package/src/plugins/builtin/buildout/format.ts +38 -0
  170. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  171. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  172. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  173. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  174. package/src/plugins/builtin/cds/client.ts +142 -30
  175. package/src/plugins/builtin/cds/headless.ts +35 -8
  176. package/src/plugins/builtin/cds/model.ts +145 -18
  177. package/src/plugins/builtin/cds/pane.tsx +16 -5
  178. package/src/plugins/builtin/changelog/index.tsx +9 -6
  179. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  180. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  181. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  182. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  183. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  184. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  185. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  186. package/src/plugins/builtin/chart-composer/index.tsx +5 -6
  187. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  188. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  189. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  190. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  191. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  192. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  193. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  194. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  195. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  196. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  197. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  198. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  199. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  200. package/src/plugins/builtin/clinical-trials/model.ts +86 -31
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
  202. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  203. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
  204. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  205. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  206. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  207. package/src/plugins/builtin/company-attention/client.ts +8 -9
  208. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  209. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  210. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  211. package/src/plugins/builtin/company-attention/pane.tsx +12 -2
  212. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  213. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  214. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  215. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  216. package/src/plugins/builtin/composite-plugins.ts +2 -1
  217. package/src/plugins/builtin/correlation/headless.ts +13 -5
  218. package/src/plugins/builtin/correlation/history.ts +22 -0
  219. package/src/plugins/builtin/cot/client.ts +22 -18
  220. package/src/plugins/builtin/cot/headless.ts +15 -10
  221. package/src/plugins/builtin/cot/model.ts +14 -1
  222. package/src/plugins/builtin/cot/pane.tsx +22 -14
  223. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  224. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  225. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  226. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  227. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  228. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  229. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  230. package/src/plugins/builtin/credit-documents/headless.ts +10 -10
  231. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  232. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  233. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  234. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  235. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  236. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  237. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  238. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  239. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  240. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  241. package/src/plugins/builtin/docs/targets.ts +5 -1
  242. package/src/plugins/builtin/doe/headless.ts +3 -2
  243. package/src/plugins/builtin/doe/pane.tsx +13 -3
  244. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  245. package/src/plugins/builtin/earnings/board.tsx +62 -71
  246. package/src/plugins/builtin/earnings/headless.ts +40 -4
  247. package/src/plugins/builtin/earnings/scope.ts +31 -43
  248. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  249. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  250. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  251. package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
  252. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  253. package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
  254. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  255. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  256. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  257. package/src/plugins/builtin/econ/figures.ts +48 -0
  258. package/src/plugins/builtin/econ/index.tsx +14 -5
  259. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  260. package/src/plugins/builtin/econ/types.ts +2 -0
  261. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  262. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  263. package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
  264. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  265. package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
  266. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  267. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  268. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  269. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  270. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  271. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  272. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  273. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  274. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  275. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  276. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  277. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  278. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  279. package/src/plugins/builtin/gpu/headless.ts +8 -2
  280. package/src/plugins/builtin/help/function-index.ts +4 -1
  281. package/src/plugins/builtin/help/index.tsx +62 -59
  282. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  283. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  284. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  285. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  286. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  287. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  288. package/src/plugins/builtin/holders/headless.ts +22 -3
  289. package/src/plugins/builtin/holders/pane.tsx +3 -0
  290. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  291. package/src/plugins/builtin/insider/headless.ts +2 -2
  292. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  293. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  294. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  295. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  296. package/src/plugins/builtin/iv-history/client.ts +52 -11
  297. package/src/plugins/builtin/iv-history/format.ts +36 -23
  298. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  299. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  300. package/src/plugins/builtin/iv-history/model.ts +163 -27
  301. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  302. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  303. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  304. package/src/plugins/builtin/jobs/client.ts +25 -29
  305. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  306. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  307. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  308. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  309. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  310. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  311. package/src/plugins/builtin/macro-day/model.ts +39 -5
  312. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  313. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  314. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  315. package/src/plugins/builtin/market-movers/client.ts +27 -7
  316. package/src/plugins/builtin/market-movers/headless.ts +34 -12
  317. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  318. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  319. package/src/plugins/builtin/market-movers/model.ts +4 -1
  320. package/src/plugins/builtin/market-movers/screener.ts +22 -4
  321. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  322. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  323. package/src/plugins/builtin/market-movers/table.ts +17 -5
  324. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  325. package/src/plugins/builtin/mna/headless.ts +2 -0
  326. package/src/plugins/builtin/mna/pane.tsx +3 -1
  327. package/src/plugins/builtin/news/headless.ts +3 -1
  328. package/src/plugins/builtin/news/index.tsx +4 -0
  329. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  330. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  331. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  332. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  333. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  334. package/src/plugins/builtin/openfda/client.ts +14 -2
  335. package/src/plugins/builtin/openfda/index.tsx +3 -2
  336. package/src/plugins/builtin/openfda/model.ts +9 -0
  337. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  338. package/src/plugins/builtin/options/analytics.ts +29 -5
  339. package/src/plugins/builtin/options/enrichment.ts +17 -5
  340. package/src/plugins/builtin/options/table.ts +5 -2
  341. package/src/plugins/builtin/options/types.ts +2 -0
  342. package/src/plugins/builtin/options/view.tsx +17 -4
  343. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  344. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  345. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  346. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  347. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  348. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  349. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  350. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  351. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  352. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  353. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  354. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  355. package/src/plugins/builtin/pe-band/model.ts +12 -49
  356. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  357. package/src/plugins/builtin/perps/cascade.ts +236 -0
  358. package/src/plugins/builtin/perps/client.ts +56 -2
  359. package/src/plugins/builtin/perps/headless.ts +65 -9
  360. package/src/plugins/builtin/perps/index.tsx +7 -5
  361. package/src/plugins/builtin/perps/model.ts +18 -4
  362. package/src/plugins/builtin/perps/pane.tsx +19 -22
  363. package/src/plugins/builtin/perps/status-line.ts +25 -0
  364. package/src/plugins/builtin/plugin-module.ts +5 -1
  365. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  366. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  367. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  368. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  369. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  370. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
  371. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  372. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  373. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  374. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
  375. package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
  376. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  377. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  378. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  379. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  380. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  381. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  382. package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
  383. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  384. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  385. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  386. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  387. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  388. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  389. package/src/plugins/builtin/research/event-model.ts +40 -12
  390. package/src/plugins/builtin/research/headless.ts +1 -1
  391. package/src/plugins/builtin/research/index.tsx +7 -3
  392. package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
  393. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  394. package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
  395. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  396. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  397. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  398. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  399. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  400. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  401. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  402. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  403. package/src/plugins/builtin/roadmap/model.ts +67 -0
  404. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  405. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  406. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  407. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  408. package/src/plugins/builtin/scanner/headless.ts +204 -35
  409. package/src/plugins/builtin/scanner/index.ts +16 -5
  410. package/src/plugins/builtin/scanner/live-headless.ts +48 -5
  411. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  412. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  413. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  414. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  415. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  416. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  417. package/src/plugins/builtin/sec/headless.ts +4 -2
  418. package/src/plugins/builtin/sec/index.tsx +3 -0
  419. package/src/plugins/builtin/series-inventory.ts +2 -0
  420. package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
  421. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  422. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  423. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  424. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  425. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  426. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  427. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  428. package/src/plugins/builtin/shared/report-freshness.ts +11 -2
  429. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  430. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  431. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  432. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  433. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  434. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  435. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  436. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  437. package/src/plugins/builtin/themes/model.ts +16 -7
  438. package/src/plugins/builtin/themes/pane.tsx +5 -5
  439. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  440. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  441. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  442. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  443. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  444. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  445. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  446. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  447. package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
  448. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  449. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
  450. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  451. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  452. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  453. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  454. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  455. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  456. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  457. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  458. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  459. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  460. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  461. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  462. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  463. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  464. package/src/plugins/builtin/volatility/index.tsx +2 -2
  465. package/src/plugins/builtin/volatility/pane.tsx +7 -4
  466. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  467. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  468. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  469. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  470. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  471. package/src/plugins/catalog-browser.ts +2 -1
  472. package/src/plugins/catalog-manifest.ts +2 -1
  473. package/src/plugins/host-link.ts +1 -1
  474. package/src/plugins/marketplace/model.ts +18 -5
  475. package/src/portfolio/column-values.ts +11 -7
  476. package/src/portfolio/futures-positions.ts +6 -3
  477. package/src/portfolio/position-quantity.ts +15 -9
  478. package/src/remote/resources.ts +2 -1
  479. package/src/renderers/dom/data-table/row.tsx +26 -0
  480. package/src/renderers/dom/desktop/icons.tsx +7 -0
  481. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  482. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  483. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  484. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  485. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  486. package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
  487. package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
  488. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  489. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  490. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  491. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  492. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  493. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  494. package/src/renderers/electrobun/bun/index.ts +26 -2
  495. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  496. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  497. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  498. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  499. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  500. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  501. package/src/renderers/electrobun/shared/protocol.ts +12 -0
  502. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  503. package/src/renderers/electrobun/view/main.tsx +24 -1
  504. package/src/renderers/electrobun/view/news-source.ts +5 -1
  505. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  506. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  507. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  508. package/src/renderers/opentui/start.tsx +5 -0
  509. package/src/series/registry.ts +1 -0
  510. package/src/series/types.ts +6 -0
  511. package/src/series/validate.ts +1 -0
  512. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  513. package/src/sources/gloomberb-cloud/index.ts +43 -8
  514. package/src/sources/gloomberb-cloud/news.ts +47 -7
  515. package/src/sources/provider-router/financials.ts +15 -3
  516. package/src/sources/provider-router/index.ts +8 -5
  517. package/src/state/ticker-cleanup.ts +50 -0
  518. package/src/sync/controller.ts +4 -3
  519. package/src/sync/core-contributors.ts +90 -31
  520. package/src/telemetry/stall-reports-dom.ts +38 -0
  521. package/src/telemetry/stall-reports-node.ts +75 -0
  522. package/src/telemetry/stall-reports.ts +214 -0
  523. package/src/test-support/preload.ts +14 -0
  524. package/src/theme/themes.ts +10 -0
  525. package/src/tickers/company-name-listing.ts +53 -13
  526. package/src/tickers/company-name.ts +50 -4
  527. package/src/tickers/index-roots.ts +7 -0
  528. package/src/tickers/listing-fate.ts +2 -2
  529. package/src/tickers/listing-venues.ts +15 -3
  530. package/src/tickers/search/index.ts +6 -2
  531. package/src/tickers/search/ranking.ts +5 -1
  532. package/src/tickers/search/upsert.ts +13 -0
  533. package/src/tickers/terminal-codes.ts +19 -0
  534. package/src/tickers/ticker-changes.ts +8 -4
  535. package/src/time-series/alignment.ts +1 -1
  536. package/src/time-series/economic.ts +18 -4
  537. package/src/time-series/field-catalog.ts +1 -1
  538. package/src/time-series/filed-trailing-eps.ts +197 -0
  539. package/src/time-series/fundamentals.ts +44 -1
  540. package/src/time-series/ice-credit.ts +3 -0
  541. package/src/time-series/price-comparison.ts +30 -2
  542. package/src/time-series/resolve-context.ts +3 -1
  543. package/src/time-series/resolve.ts +56 -4
  544. package/src/time-series/session-bar.ts +11 -0
  545. package/src/time-series/spec.ts +3 -2
  546. package/src/time-series/studies.ts +8 -2
  547. package/src/time-series/total-return.ts +160 -0
  548. package/src/time-series/transforms.ts +2 -3
  549. package/src/time-series/types.ts +22 -1
  550. package/src/types/company-from.ts +40 -0
  551. package/src/types/data-provider.ts +7 -0
  552. package/src/types/financials.ts +59 -1
  553. package/src/types/headless.ts +27 -3
  554. package/src/types/news-source.ts +1 -0
  555. package/src/types/plugin.ts +9 -0
  556. package/src/ui/host.tsx +3 -0
  557. package/src/updater/failure.ts +4 -1
  558. package/src/utils/cli-output.ts +42 -1
  559. package/src/utils/crypto-pair.ts +165 -7
  560. package/src/utils/declared-dividends.ts +1 -1
  561. package/src/utils/forward-dividend.ts +2 -0
  562. package/src/utils/fundamentals.ts +60 -8
  563. package/src/utils/fx-market-hours.ts +26 -3
  564. package/src/utils/main-thread-stall.ts +35 -2
  565. package/src/utils/market-capitalization.ts +24 -1
  566. package/src/utils/price-history.ts +7 -1
  567. package/src/utils/shell-word.ts +8 -0
  568. package/src/utils/trailing-dividend.ts +53 -4
  569. package/src/version.ts +2 -2
  570. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -3,7 +3,7 @@ import { extname, isAbsolute, relative, resolve } from "path";
3
3
  import type { CliCommandContext, HeadlessArgumentExpansion } from "../../types/plugin";
4
4
  import type { MarketContext } from "../types";
5
5
  import { withMarketData } from "../context";
6
- import { cliStyles, renderStats, type CliStatEntry } from "../../utils/cli-output";
6
+ import { buildForFile, cliStyles, renderForFile, renderStats, type CliStatEntry } from "../../utils/cli-output";
7
7
  import {
8
8
  filterPaneCatalogEntries,
9
9
  findFunctionCode,
@@ -35,13 +35,14 @@ import { withPersistedCloudSession } from "./cloud-session";
35
35
  import {
36
36
  ListingArgError, listingFateLoader, listingReadNotes, loadForListing, loadListingQuoteAnswer, quotedListingChoiceNotes, refuseMovedListing,
37
37
  } from "../listing-arg";
38
- import { isNotATickerMessage } from "../not-a-ticker";
39
- import { isCliFailure, SERVICE_UNREACHABLE_CODE } from "../errors";
40
- import { DATA_SERVICE_UNREACHABLE_MESSAGE } from "../../utils/network-failure";
38
+ import { notATickerStop } from "../company-name-hint";
39
+ import { CLI_ERROR_CODE, failUnreachable, failWithError, isCliFailure, unreachableError } from "../errors";
40
+ import { DATA_SERVICE_UNREACHABLE_MESSAGE, isNetworkFailure } from "../../utils/network-failure";
41
41
  import { isTickerLinkPeer } from "../../layout/ticker-navigation";
42
42
  import { knownGapMessage, commandPointer, commandPointerLeads, topicPointer } from "../known-gaps";
43
43
  import { selectReportTables, type CliReportTables } from "../report-tables";
44
- import { cliOutputWritten, type CliErrorObject } from "../result";
44
+ import { cliOutputWritten, printCliResult, serializeCliResult, type CliErrorObject } from "../result";
45
+ import { checkOutputPath, resolveOutputPath, writeOutputFile } from "../output-file";
45
46
  import type { CatalogCommandHit } from "../unknown-command";
46
47
  import { CLI_EXIT_CODES } from "../exit-codes";
47
48
  import { shotOptionRefusal } from "./shot-options";
@@ -263,7 +264,7 @@ export function reportFailure(
263
264
  const reasons = [...data.errors ?? [], ...typeof data.reason === "string" ? [data.reason] : []];
264
265
  const unreached = reasons.filter((reason) => reason.includes(DATA_SERVICE_UNREACHABLE_MESSAGE));
265
266
  if (unreached.length > 0 && (data.empty || data.unavailableSymbols.length > 0)) {
266
- return { code: SERVICE_UNREACHABLE_CODE, message: `${asked}: ${unreached.join("; ")}.`.replace(/\.\.$/, "."), retryable: true };
267
+ return unreachableError(`${asked}: ${unreached.join("; ")}.`.replace(/\.\.$/, "."));
267
268
  }
268
269
  if (!data.empty && !(noListing && noValues) && !(failsWithoutData && foundNothing(data))) return null;
269
270
  const unavailable = data.unavailableSymbols.length > 0 ? `no data for ${data.unavailableSymbols.join(", ")}` : "no data";
@@ -276,6 +277,16 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
276
277
  if (parsed.strict) {
277
278
  throw new Error("--strict is an option of shot. fn takes --require-bot-safe to fail on a report that is not complete.");
278
279
  }
280
+ // What only a capture draws is refused, never ignored: the report would print as if it had not been given.
281
+ if (parsed.shotOnly?.length) {
282
+ const one = parsed.shotOnly.length === 1;
283
+ const shot = ["gloom shot", resolved.token, parsed.arg.trim() && commandWord(parsed.arg.trim())].filter(Boolean).join(" ");
284
+ throw new Error(`${parsed.shotOnly.join(" and ")} ${one ? "is an option" : "are options"} of shot, which saves the pane as a PNG; fn prints the report. `
285
+ + `Drop ${one ? "it" : "them"} from fn, or capture the pane with ${shot}.`);
286
+ }
287
+ // The file --output names, checked before the report loads so a path that cannot be written fails at once.
288
+ const outputPath = parsed.outputPath === null ? null : resolveOutputPath(parsed.outputPath);
289
+ if (outputPath) await checkOutputPath(outputPath);
279
290
  if (parsed.requireBotSafe && (
280
291
  !resolved.capability.botSafe || resolved.capability.reportReadiness !== "ready"
281
292
  )) {
@@ -305,22 +316,25 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
305
316
  : null;
306
317
  const moved = { command: "fn", retry: (key: string) => `gloom fn ${resolved.token} ${key}` };
307
318
  // A report that fails or comes back empty for an exchange the symbol is not listed on says so.
319
+ // A report written to a file is drawn for it as it is built: no colour and no terminal width, as a redirect gets it.
320
+ const buildReport = () => buildFunctionReport(resolved, context, parsed.arg);
308
321
  const report = await refuseMovedListing(fate, moved, () => withPersistedCloudSession(
309
322
  context,
310
323
  () => loadForListing(
311
324
  parsed.listing ?? [],
312
325
  context,
313
326
  ctx,
314
- () => buildFunctionReport(resolved, context, parsed.arg),
327
+ outputPath ? () => buildForFile(buildReport) : buildReport,
315
328
  (built) => built.data.empty || !built.data.complete,
316
329
  ),
317
330
  ));
318
331
  // A company's name typed where its ticker goes (`fn RDCF Palantir`) found nothing: the answer is `quote`'s,
319
- // "Not a ticker: PALANTIR. Try `gloom search Palantir`.", not an empty report.
320
- if (quoteAnswer && (report.data.empty || report.data.unavailableSymbols.length > 0)) {
332
+ // "Not a ticker: PALANTIR. Did you mean PLTR (Palantir Technologies Inc.)?", not an empty report. A source that
333
+ // could not answer for the word (a timeout) says nothing of it, but when search finds the company it is that.
334
+ if (quoteAnswer && listing && (report.data.empty || report.data.unavailableSymbols.length > 0)) {
321
335
  const { quote, error } = await quoteAnswer();
322
- const message = error instanceof Error ? error.message : null;
323
- if (!quote && isNotATickerMessage(message)) ctx.fail(message!);
336
+ const stop = quote ? null : await notATickerStop(error, listing, context);
337
+ if (stop) ctx.fail(stop);
324
338
  }
325
339
  if (parsed.requireBotSafe && (report.data.empty || !report.data.complete)) {
326
340
  const unavailable = report.data.unavailableSymbols.length > 0
@@ -347,10 +361,24 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
347
361
  const failure = reportFailure(report.data, ["fn", resolved.token, parsed.arg.trim()].filter(Boolean).join(" "), {
348
362
  failsWithoutData: resolved.headless?.failsWithoutData, tables: report.tables });
349
363
  // The report states its own freshness in its data and text; JSON cites it as `source` too.
350
- ctx.printResult({ data, exportFreshness: report.data.freshness, ...(warnings.length > 0 ? { warnings } : {}), ...(failure ? { failure } : {}) }, {
364
+ const result = { data, exportFreshness: report.data.freshness, ...(warnings.length > 0 ? { warnings } : {}), ...(failure ? { failure } : {}) };
365
+ const render = {
351
366
  text: () => glossary ? `${report.text}\n\n${renderReportGlossary(resolved.token, glossary)}` : report.text,
352
367
  ...(tabular ? { tables: selectReportTables(report.tables, resolved.tableSection) } : {}),
353
- });
368
+ };
369
+ if (outputPath) {
370
+ // A report with no data writes no file: a header alone would read as a screen that matched nothing. It fails
371
+ // as plain --json fails, with its code and whether to retry.
372
+ if (failure) failWithError({ ...failure, details: `Nothing was written to ${outputPath}.` });
373
+ await writeOutputFile(outputPath, `${renderForFile(() => serializeCliResult(result, ctx.cliOptions, render))}\n`);
374
+ // What was written and where; warnings go to stderr as they would beside printed rows, and JSON keeps its envelope.
375
+ const saved = { output: outputPath, format: ctx.cliOptions.format, rows: report.data.rowCount };
376
+ printCliResult({ data: saved, ...(warnings.length > 0 ? { warnings } : {}) },
377
+ { ...ctx.cliOptions, format: ctx.cliOptions.format === "json" ? "json" : "text" },
378
+ { text: () => `Saved ${outputPath}` });
379
+ return;
380
+ }
381
+ ctx.printResult(result, render);
354
382
  if (failure) process.exitCode = CLI_EXIT_CODES.error;
355
383
  }, { strictHeadlessOptions: true, tableSection: true });
356
384
  });
@@ -437,6 +465,11 @@ async function capturePaneScreenshot(args: string[], ctx: CliCommandContext, onS
437
465
  fitRows: parsed.heightGiven !== true,
438
466
  options: parsed.options,
439
467
  onStalledReport,
468
+ }).catch(async (error: unknown) => {
469
+ // A company's name typed where its ticker goes fails as `fn` words it, with the company search finds.
470
+ const { quote, error: quoteError } = parsed.listing ? await loadListingQuoteAnswer(context.dataProvider, parsed.listing) : { quote: null, error: undefined };
471
+ const stop = parsed.listing && !quote ? await notATickerStop(quoteError, parsed.listing, context) : null;
472
+ throw stop ? new Error(stop) : error;
440
473
  });
441
474
  if (parsed.requireBotSafe && !result.usable) {
442
475
  throw new Error(
@@ -508,7 +541,8 @@ export async function runPaneFunctionHelp(command: string, args: string[], ctx:
508
541
  const found = findPaneFunction(registry, target);
509
542
  if (!found?.pane) return undefined;
510
543
  const { template, pane } = found;
511
- const entries = await buildPaneCatalogEntries(registry, context);
544
+ // A function typed by its code is helped even while it is unlisted.
545
+ const entries = await buildPaneCatalogEntries(registry, context, { unlisted: true });
512
546
  // A pane named directly, where functions open it, is helped by the first of them.
513
547
  return (template ? entries.find((candidate) => candidate.templateId === template.id) : undefined)
514
548
  ?? entries.find((candidate) => candidate.paneId === pane.id);
@@ -578,11 +612,15 @@ async function runPaneCliCommand(ctx: CliCommandContext, run: () => Promise<void
578
612
  try {
579
613
  await run();
580
614
  } catch (error) {
581
- // A failure with fields for --json (`ticker_moved`, its change and retry) keeps its code and them.
582
- if (isCliFailure(error) && error.fields) throw error;
615
+ // A failure with fields for --json (`ticker_moved`, its change and retry), or a code of its own (`no_data`,
616
+ // `service_unreachable` and whether to retry), keeps them.
617
+ if (isCliFailure(error) && (error.fields || error.code !== CLI_ERROR_CODE)) throw error;
583
618
  // A listing it cannot use says what to type instead; a name that opens nothing, what is close to it.
584
619
  if (error instanceof ListingArgError || error instanceof UnknownPaneFunctionError) ctx.fail(error.message, error.details || undefined);
585
620
  const message = error instanceof Error ? error.message : String(error);
621
+ // Nothing answered: said as every command says it, with what to check.
622
+ if (message.includes(DATA_SERVICE_UNREACHABLE_MESSAGE)) failUnreachable(message);
623
+ if (isNetworkFailure(error)) failUnreachable();
586
624
  // A failure the command already worded keeps its advice line.
587
625
  const stated = error instanceof Error ? (error as Error & { details?: unknown }).details : undefined;
588
626
  const details = typeof stated === "string" ? stated : undefined;
@@ -8,7 +8,7 @@ import {
8
8
  nextRegularSessionOpen,
9
9
  } from "../../market-data/market/freshness";
10
10
  import { futuresWeekendClosure } from "../../market-data/market/futures-week";
11
- import { currentFxClosureStart } from "../../utils/fx-market-hours";
11
+ import { currentFxClosureStart, isFxMarketOpen } from "../../utils/fx-market-hours";
12
12
  import { zonedDateKey } from "../../utils/zoned-date-time";
13
13
  import type { ReportTime } from "../../utils/utc-time";
14
14
  import type { QuoteIndexSession } from "../../types/financials";
@@ -60,6 +60,10 @@ const US_EXCHANGE_TIME_ZONE = "America/New_York";
60
60
  */
61
61
  function venueSession(exchange: string, marketState: string | undefined, now: number, calendar?: QuoteIndexSession): VenueSession | null {
62
62
  if (isAlwaysOpenExchange(exchange)) return { state: "always" };
63
+ // Spot FX trades its week, Sunday 17:00 to Friday 17:00 New York, by the clock as an exchange rate's line has it.
64
+ // A source's own state is not its week: Yahoo stamps a pair CLOSED from that open to London's midnight, which read
65
+ // `markets closed` beside `fx`'s `markets open` for the same pair.
66
+ if (exchange === FX_VENUE) return { state: isFxMarketOpen(now) ? "open" : "closed" };
63
67
  const next = nextRegularSessionOpen(exchange, now);
64
68
  if (next) {
65
69
  const { timeZone } = next;
@@ -125,10 +129,13 @@ function closeDateOf(observation: SessionObservation, session: VenueSession, now
125
129
  const time = observation.time;
126
130
  if (!time || time.dateOnly) return null;
127
131
  // FX closes once a week, Friday 17:00 New York time, and its trading day turns
128
- // at 17:00: a print from that last day, or a quiet tick over the weekend, is
129
- // that Friday's close, not the day it happens to be stamped with.
132
+ // at 17:00: a print from that last day, after Thursday's 17:00 close through
133
+ // Friday's, is that Friday's close. A tick after it, over the weekend, is no
134
+ // price of the week and keeps its own time.
130
135
  const weekClose = canonicalExchange(observation.exchange) === FX_VENUE ? currentFxClosureStart(now) : null;
131
- if (weekClose != null) return time.time >= weekClose - DAY_MS ? zonedDateKey(weekClose - 1, "America/New_York") : null;
136
+ if (weekClose != null) {
137
+ return time.time > weekClose - DAY_MS && time.time <= weekClose ? zonedDateKey(weekClose - 1, "America/New_York") : null;
138
+ }
132
139
  if (session.last && session.timeZone) {
133
140
  if (session.last.open != null && time.time < session.last.open) return null;
134
141
  return zonedDateKey(time.time, session.timeZone) === session.last.date ? session.last.date : null;
@@ -151,6 +158,22 @@ function closeMarketOf(exchange: string, session: VenueSession): string | null {
151
158
  return isUsListingExchange(exchange) ? "US" : exchange;
152
159
  }
153
160
 
161
+ /**
162
+ * The market a venue's sessions belong to, as a report that mixes markets
163
+ * names it: `US` for the US listing venues, which share one calendar (and an
164
+ * index publisher's sessions dated in New York), `FX` for currency pairs, the
165
+ * venue's code for any other the app has a calendar of (`LSE`). Null for one
166
+ * known only by the state its source stamps (an index publisher dated
167
+ * elsewhere, a futures venue), whose open a report cannot name: its sessions
168
+ * are still dated by venue (`reportVenueSessionDates`), as they always were.
169
+ */
170
+ function sessionMarketOf(exchange: string, session: VenueSession): string | null {
171
+ if (exchange === FX_VENUE) return "FX";
172
+ if (session.quoted) return session.timeZone === US_EXCHANGE_TIME_ZONE ? "US" : null;
173
+ if (!session.timeZone) return null;
174
+ return isUsListingExchange(exchange) ? "US" : exchange;
175
+ }
176
+
154
177
  /** One market's session behind a report that mixes markets: its local date, and whether that session has closed. */
155
178
  export interface VenueSessionDate {
156
179
  /** `US` for the US listing venues, which share one calendar, the venue's code elsewhere (`XETRA`, `EPA`). */
@@ -192,6 +215,45 @@ export function reportVenueSessionDates(observations: readonly SessionObservatio
192
215
  });
193
216
  }
194
217
 
218
+ /** One market behind a report that mixes markets, and where it stands now (`reportVenueMarkets`). */
219
+ export interface VenueMarket extends ReportMarket {
220
+ /** The market as `sessionMarketOf` names it: `US`, `FX`, or the venue's code (`LSE`). */
221
+ market: string;
222
+ }
223
+
224
+ /** The fewest and most markets a report names one by one; past that a line of them would not be read. */
225
+ const MIN_NAMED_MARKETS = 2;
226
+ const MAX_NAMED_MARKETS = 3;
227
+
228
+ /**
229
+ * Where each market behind a report that mixes two or three stands now, each
230
+ * with its own next open (`LSE` opens 08:00 UTC, `US` 13:30 UTC), in the
231
+ * order the markets first appear. Empty for one market, more than three, or
232
+ * any that cannot be placed or named (one trading around the clock among them,
233
+ * an index publisher with no calendar).
234
+ */
235
+ export function reportVenueMarkets(observations: readonly SessionObservation[], now: number): VenueMarket[] {
236
+ const markets = new Map<string, VenueSession>();
237
+ for (const observation of observations) {
238
+ const exchange = canonicalExchange(observation.exchange);
239
+ if (!exchange) continue;
240
+ const session = venueSession(exchange, observation.marketState, now, observation.calendar);
241
+ const market = session && session.state !== "always" ? sessionMarketOf(exchange, session) : null;
242
+ if (!session || !market) return [];
243
+ if (!markets.has(market)) markets.set(market, session);
244
+ }
245
+ if (markets.size < MIN_NAMED_MARKETS || markets.size > MAX_NAMED_MARKETS) return [];
246
+ return [...markets].map(([market, session]) => ({
247
+ market,
248
+ state: session.state as ReportMarketState,
249
+ ...(session.next && session.state !== "open" && session.state !== "after-hours" ? {
250
+ reopensAt: new Date(session.next.open).toISOString(),
251
+ reopensOn: session.next.date,
252
+ ...(session.next.firstTrade ? { firstTrade: true as const } : {}),
253
+ } : {}),
254
+ }));
255
+ }
256
+
195
257
  /**
196
258
  * Where the markets behind a quote report stand now, and whether the newest
197
259
  * observation is a close, and whose. Null market when the venues disagree
@@ -53,6 +53,8 @@ export interface ParsedPaneFunctionArgs {
53
53
  requireBotSafe: boolean;
54
54
  /** `shot --strict`: exit with the unusable-shot code when the capture shows no usable data. */
55
55
  strict?: boolean;
56
+ /** The options only `shot` reads (`--theme`, `--height`), as typed, which `fn` refuses rather than ignore. */
57
+ shotOnly?: string[];
56
58
  /** The listing a one-ticker function's argument named, once `applyListingArgument` has resolved it. */
57
59
  listing?: CliListing;
58
60
  /**
@@ -112,10 +114,18 @@ export type OptionValueWords = (key: string) => number;
112
114
  /** Words in the last value of a list (`TRY -20, EUR 5` ends with two); a decimal comma (`TRY -2,5`) separates nothing. */
113
115
  const lastValueWords = (value: string) => value.split(/(?<!\d),|,(?!\d)/).at(-1)!.trim().split(/\s+/).filter(Boolean).length;
114
116
 
117
+ /**
118
+ * What only `shot` draws: the capture's height, theme, text scale, watermark
119
+ * and status line. `--width` is not one: on `fn` it is the CLI's table width.
120
+ */
121
+ const SHOT_ONLY_KEYS = new Set(["height", "theme", "scale", "watermark"]);
122
+
115
123
  export function parsePaneFunctionArgs(
116
124
  args: string[],
117
125
  globalOptions: { limit?: number; tail?: number } = {},
118
126
  valueWords?: OptionValueWords,
127
+ /** Whether an option takes a list, so its flag typed again adds to it rather than replacing it. */
128
+ listOption?: (key: string) => boolean,
119
129
  ): ParsedPaneFunctionArgs {
120
130
  const positionals: string[] = [];
121
131
  const options: Record<string, string | true> = {};
@@ -131,6 +141,7 @@ export function parsePaneFunctionArgs(
131
141
  let strict = false;
132
142
  let requireBotSafe = false;
133
143
  const clamped: ShotSizeClamp[] = [];
144
+ const shotOnly: string[] = [];
134
145
 
135
146
  for (let index = 0; index < args.length; index += 1) {
136
147
  const token = args[index]!;
@@ -151,6 +162,7 @@ export function parsePaneFunctionArgs(
151
162
  // A switch: the token after it is the function's argument, never its value.
152
163
  if (normalizeOptionKey(key) === "noStatus" && inlineValue === undefined) {
153
164
  status = false;
165
+ shotOnly.push(`--${key}`);
154
166
  continue;
155
167
  }
156
168
  if (normalizeOptionKey(key) === "explain" && inlineValue === undefined) {
@@ -165,6 +177,7 @@ export function parsePaneFunctionArgs(
165
177
  const nextValue = inlineValue === undefined && next && !next.startsWith("--") ? args[++index] : true;
166
178
  let value: string | true = inlineValue ?? nextValue ?? true;
167
179
  const normalizedKey = normalizeOptionKey(key);
180
+ if (SHOT_ONLY_KEYS.has(normalizedKey)) shotOnly.push(`--${key}`);
168
181
  // A value that is a pair (`--fx-shift TRY -20`) takes the words after it, and a list of them the words after a comma.
169
182
  const words = value === true ? 1 : valueWords?.(normalizedKey) ?? 1;
170
183
  if (words > 1 && value !== true) {
@@ -172,13 +185,17 @@ export function parsePaneFunctionArgs(
172
185
  && (lastValueWords(value) < words || value.trimEnd().endsWith(","))) {
173
186
  value = `${value} ${args[++index]!}`;
174
187
  }
188
+ }
189
+ // A list's flag typed again adds to it: `--metric trailingPE --metric dividendYieldPercent`.
190
+ if ((words > 1 || listOption?.(normalizedKey)) && value !== true) {
175
191
  const earlier = options[normalizedKey];
176
192
  if (typeof earlier === "string") value = `${earlier}, ${value}`;
177
193
  }
178
194
  if (normalizedKey === "requireBotSafe" || normalizedKey === "botSafe") {
179
195
  requireBotSafe = value === true || String(value).toLowerCase() === "true";
180
196
  } else if (normalizedKey === "output" || normalizedKey === "out" || normalizedKey === "o") {
181
- outputPath = value === true ? null : value;
197
+ // A flag with no path is kept as an empty one, which `fn` refuses rather than print as if it had not been given.
198
+ outputPath = value === true ? "" : value;
182
199
  } else if ((normalizedKey === "width" || normalizedKey === "height") && value !== true) {
183
200
  const size = parseShotSize(normalizedKey, value);
184
201
  if (size.used !== size.requested) clamped.push(size);
@@ -214,6 +231,7 @@ export function parsePaneFunctionArgs(
214
231
  ...(heightGiven ? { heightGiven: true as const } : {}),
215
232
  ...(explain ? { explain } : {}),
216
233
  ...(strict ? { strict } : {}),
234
+ ...(shotOnly.length > 0 ? { shotOnly } : {}),
217
235
  };
218
236
  }
219
237
 
@@ -19,12 +19,15 @@ import { splitSymbolArgs, stripStrayEdges } from "../symbol-args";
19
19
  import { readNameEntries } from "../name-entries";
20
20
  import { bankFigurePointer } from "../known-gaps";
21
21
  import { readTerminalCodeArg } from "../../tickers/company-name-listing";
22
+ import { unknownBloombergMarket } from "../../tickers/terminal-codes";
23
+ import { notATickerWords } from "../company-name-hint";
22
24
  import type { MarketContext } from "../types";
23
25
  import {
24
26
  buildPaneFunctionLookup,
25
27
  buildTemplateContext,
26
28
  findFunctionCode,
27
29
  nearFunctionCodes,
30
+ paneFunctionCode,
28
31
  suggestedFunctionCodes,
29
32
  suggestPaneFunctions,
30
33
  type NearFunctionCode,
@@ -142,8 +145,9 @@ export function parsePaneFunctionArgsFor(
142
145
  const found = parsed.target ? findPaneFunction(registry, parsed.target) : null;
143
146
  if (!found?.pane) return parsed;
144
147
  const capability = getPaneFunctionCapability(found.template, found.pane);
145
- if (!capability.options.some((option) => (option.valueWords ?? 1) > 1)) return parsed;
146
- return parsePaneFunctionArgs(args, globalOptions, (key) => findCapabilityOption(capability, key)?.valueWords ?? 1);
148
+ if (!capability.options.some((option) => (option.valueWords ?? 1) > 1 || option.list)) return parsed;
149
+ return parsePaneFunctionArgs(args, globalOptions, (key) => findCapabilityOption(capability, key)?.valueWords ?? 1,
150
+ (key) => findCapabilityOption(capability, key)?.list === true);
147
151
  }
148
152
 
149
153
  /**
@@ -168,11 +172,17 @@ export async function applyListingArgument(
168
172
  const takesOneTicker = kind === "ticker";
169
173
  const takesList = kind === "tickers" || kind === "symbol-list" || (!headless && kind === "ticker-list");
170
174
  // A function with an exchange option of its own (DIST) keeps it.
171
- // A ticker with spaces is no listing, unless it is a terminal's code for one ("JPM US Equity").
172
- if (!(takesOneTicker || takesList) || (takesOneTicker && /\s/.test(args.arg) && !await readTerminalCodeArg(context.dataProvider, args.arg))
173
- || getPaneFunctionCapability(found.template, found.pane).options.some((option) => option.key === "exchange")) {
175
+ if (!(takesOneTicker || takesList) || getPaneFunctionCapability(found.template, found.pane).options.some((option) => option.key === "exchange")) {
174
176
  return args;
175
177
  }
178
+ // A ticker with spaces is no listing, unless it is a terminal's code for one ("JPM US Equity"). A company's name
179
+ // typed there (`fn RDCF "Palantir Technologies"`) stops before a report of dashes is drawn, as `quote` words a
180
+ // name that is no ticker, with the company search finds for it.
181
+ if (takesOneTicker && /\s/.test(args.arg) && !await readTerminalCodeArg(context.dataProvider, args.arg)) {
182
+ // A line written as a code with a market no venue answers to ("AAPL GP") is refused as the resolver words it.
183
+ if (unknownBloombergMarket(args.arg)) await resolveCliListing(args.arg, undefined, context);
184
+ throw new ListingArgError(await notATickerWords(args.arg, context));
185
+ }
176
186
  const exchange = typeof args.options.exchange === "string" ? args.options.exchange : undefined;
177
187
  const { exchange: _exchange, ...options } = args.options;
178
188
  // A list entry with spaces, or an argument of words, is a company's name search finds, or the symbols it holds.
@@ -225,9 +235,10 @@ export async function applyListingArgument(
225
235
  if (refusals.length > 0) {
226
236
  listings = listings.filter((listing) => !refusals.some((entry) => entry.listing === listing));
227
237
  if (listings.length === 0) {
228
- throw new ListingArgError(refusals.map(({ refusal }) => refusal.message).join("\n"), refusals.map(({ refusal }) => refusal.details).join("\n"));
238
+ const details = refusals.flatMap(({ refusal }) => refusal.details ? [refusal.details] : []).join("\n");
239
+ throw new ListingArgError(refusals.map(({ refusal }) => refusal.message).join("\n"), details || undefined);
229
240
  }
230
- notes.push(...refusals.map(({ refusal }) => `${refusal.message} ${refusal.details}`));
241
+ notes.push(...refusals.map(({ refusal }) => [refusal.message, refusal.details].filter(Boolean).join(" ")));
231
242
  }
232
243
  const usOnly = headless?.usListingsOnly;
233
244
  const abroad = usOnly ? listings.filter((listing) => listingAbroadVenue(listing.key) != null) : [];
@@ -348,7 +359,7 @@ export async function resolvePaneFunction(
348
359
  tableSection = taken.tableSection;
349
360
  }
350
361
  // What the user types for the function, which every error about it names.
351
- const token = template?.shortcut?.prefix ?? template?.id ?? pane.id;
362
+ const token = paneFunctionCode(template, pane);
352
363
  // A word after a function that takes none is most often one of its option values (`fn JODI gas`).
353
364
  // Every function the catalog lists with no argument refuses one, a rendered view (HALT) as a report (CBR).
354
365
  // The catalog lists templates and the panes no template opens (HELP); a pane opened by its id stands for its templates.
@@ -4,6 +4,7 @@ import type { DesktopPaneShotPayload, DesktopPaneShotRenderResult } from "../des
4
4
  import type { ResolvedPaneFunction } from "./resolver";
5
5
  import type { RemoteUiNodeSnapshot } from "../../remote/types";
6
6
  import { accessGateSentence, accessGateStatus } from "./access-gate";
7
+ import { isDataPaneForDomFallback } from "./capabilities";
7
8
  import { paneEvidenceMismatches, paneScreenshotEvidenceHook } from "./screenshot-evidence";
8
9
  import { collectShotSymbols } from "./data";
9
10
  import { renderedReportNotices } from "./report-notices";
@@ -21,10 +22,9 @@ import {
21
22
  import { shotExpectedChart, chartEvidenceMismatchesFor, intradayChartEvidenceMismatchesFor } from "./screenshot-chart-evidence";
22
23
 
23
24
  export function isPaneScreenshotUsable(signals: PaneScreenshotReadinessSignals): boolean {
24
- return signals.rowCount > 0
25
+ return (signals.interactive || (signals.rowCount > 0 && !signals.emptyStateDetected))
25
26
  && !signals.loadingStateDetected
26
27
  && !signals.errorStateDetected
27
- && !signals.emptyStateDetected
28
28
  && signals.complete
29
29
  && !signals.semanticMismatch
30
30
  && (!signals.requiresStructuredDataEvidence || signals.hasStructuredDataEvidence);
@@ -111,12 +111,14 @@ export function assessPaneScreenshot(
111
111
  const semanticMismatch = missingExpectedText.length > 0
112
112
  || missingExpectedSelections.length > 0
113
113
  || chartEvidenceMismatches.length > 0;
114
+ // Layouts and the other panes that are surfaces to operate have no rows to count: a capture of one is not empty for that.
115
+ const interactive = !isDataPaneForDomFallback(resolved.pane);
114
116
  const empty = render.loadingStateDetected
115
117
  || render.errorStateDetected
116
- || render.emptyStateDetected
117
- || rowCount === 0
118
+ || (!interactive && (render.emptyStateDetected || rowCount === 0))
118
119
  || semanticMismatch;
119
120
  const usable = isPaneScreenshotUsable({
121
+ interactive,
120
122
  rowCount,
121
123
  loadingStateDetected: render.loadingStateDetected,
122
124
  errorStateDetected: render.errorStateDetected,
@@ -108,6 +108,11 @@ export type PaneScreenshotDataEvidence =
108
108
  | PaneScreenshotEvidence;
109
109
 
110
110
  export interface PaneScreenshotReadinessSignals {
111
+ /**
112
+ * A surface to operate rather than data to read (Layouts, Brokers): its rows
113
+ * and an empty-state marker inside it say nothing of whether it rendered.
114
+ */
115
+ interactive?: boolean;
111
116
  rowCount: number;
112
117
  loadingStateDetected: boolean;
113
118
  errorStateDetected: boolean;
@@ -14,8 +14,8 @@ import { knownGapForTicker } from "./known-gaps";
14
14
  import {
15
15
  futuresTryLine,
16
16
  isNoProviderError,
17
+ isUnsettledNoProvider,
17
18
  listingChoiceNotes,
18
- listingReadNotes,
19
19
  ListingArgError,
20
20
  loadBareVenues,
21
21
  notTradedMessage,
@@ -29,6 +29,7 @@ import {
29
29
  type ListingVenue,
30
30
  } from "./listing-arg";
31
31
  import { withListingSuggestion } from "./not-a-ticker";
32
+ import { notATickerWords } from "./company-name-hint";
32
33
  import { isNetworkFailure } from "../utils/network-failure";
33
34
  import type { MarketContext } from "./types";
34
35
 
@@ -85,8 +86,8 @@ export async function answerQuote(
85
86
  const also = (quote: Quote) => root && quote.instrumentType?.trim().toUpperCase() !== "INDEX"
86
87
  ? [indexRootAlsoLine(root, request.command)] : [];
87
88
  if (result.quote) {
88
- // "CAR = Avis Budget Group, NASDAQ (also XETRA, ASX)" names the venue too, so it replaces "SXR8 -> XETRA".
89
- const choice = venues ? await listingChoiceNotes(listing, market, result.quote, venues) : listingReadNotes(listing);
89
+ // "CAR = Avis Budget Group, NASDAQ (different companies on XETRA, ASX)" names the venue too, so it replaces "SXR8 -> XETRA".
90
+ const choice = await listingChoiceNotes(listing, market, result.quote, venues);
90
91
  const venue = choice.length > 0 ? null : bareListingNote(listing, result.quote);
91
92
  // A row saved as 247540:KRX is quoted from KOSDAQ, the board it trades on.
92
93
  const board = tradingBoardNote(listing, result.quote);
@@ -132,8 +133,12 @@ export async function answerQuote(
132
133
  * A company's name typed as one word (`Apple`) that the source says is not a
133
134
  * ticker, answered as the listing search names for it with the line saying so
134
135
  * ("Read "Apple" as AAPL:NASDAQ (Apple Inc.)."), as a name of several words
135
- * is. Null when the word names no company, or that listing has no quote
136
- * either: the word keeps its own "Not a ticker".
136
+ * is. A source that could not answer for the word (a timeout, a rate limit)
137
+ * says nothing of it, but when search resolves it to a company, it is read the
138
+ * same way; when that listing has no quote either, the row says the word is not
139
+ * a ticker and names the company. Null when the word names no company, or that
140
+ * listing has no quote though the source did say "not a ticker": the word keeps
141
+ * its own answer.
137
142
  */
138
143
  async function answerCompanyName(
139
144
  result: QuoteBatchResult,
@@ -143,18 +148,24 @@ async function answerCompanyName(
143
148
  refresh: boolean,
144
149
  datesChangeBasis: boolean,
145
150
  ): Promise<QuoteAnswer | null> {
146
- if (!listing.typed || listing.exchange || !notATickerSymbol(errorMessage(result.error) ?? "")) return null;
151
+ if (!listing.typed || listing.exchange) return null;
152
+ const notATicker = !!notATickerSymbol(errorMessage(result.error) ?? "");
153
+ const unsettled = !notATicker && isUnsettledNoProvider(result.error);
154
+ if (!notATicker && !unsettled) return null;
147
155
  const named = await oneWordCompanyName(listing.typed, market);
148
- if (!named) return null;
156
+ // A real ticker a source failed to quote is not a company's name, even when the company carries the word.
157
+ if (!named || (unsettled && named.symbol.toUpperCase() === listing.symbol.toUpperCase())) return null;
149
158
  // A listing on a venue the app cannot read is no answer: the word keeps its own "Not a ticker".
150
159
  const resolved = await resolveCliListing(named.key, null, market).catch((error: unknown) => {
151
160
  if (error instanceof ListingArgError) return null;
152
161
  throw error;
153
162
  });
154
- if (!resolved) return null;
155
- const found = { ...resolved, readAs: named.readAs, ...named.meant ? { meant: named.meant } : {} };
156
- const [quoted] = await market.dataProvider.getQuotesBatch([found.request], { forceRefresh: refresh, revalidate: true, datesChangeBasis });
157
- return quoted?.quote ? answerQuote(quoted, found, market, request, refresh, null, { datesChangeBasis }) : null;
163
+ const found = resolved ? { ...resolved, readAs: named.readAs, ...named.meant ? { meant: named.meant } : {}, ...named.readFromName ? { readFromName: named.readFromName } : {} } : null;
164
+ const [quoted] = found ? await market.dataProvider.getQuotesBatch([found.request], { forceRefresh: refresh, revalidate: true, datesChangeBasis }) : [];
165
+ if (found && quoted?.quote) return answerQuote(quoted, found, market, request, refresh, null, { datesChangeBasis });
166
+ if (!unsettled) return null;
167
+ const sentence = await notATickerWords(listing.typed, market);
168
+ return { result: { ...result, error: new Error(sentence) }, listing, notes: [] };
158
169
  }
159
170
 
160
171
  /** A listing a command found a quote for, with what `quote` says beside it. */
@@ -0,0 +1,88 @@
1
+ import type { AppResourceStorePort } from "../core/app-service-ports";
2
+ import type { JsonValue } from "../data/sqlite/json";
3
+ import type { PersistedResourceValue } from "../types/persistence";
4
+ import type { PluginPersistence } from "../types/plugin";
5
+ import { logFailure } from "../utils/log-failure";
6
+
7
+ type SetResourceOptions = Parameters<PluginPersistence["setResource"]>[3];
8
+
9
+ /**
10
+ * A plugin's persistence in a command: what the command fetches through a
11
+ * plugin's cache is saved in the profile, under the namespace the app keeps
12
+ * it in (`plugin:<state id>`), so a later run whose refresh fails answers
13
+ * from that saved copy as the app does. Only a read that may take an expired
14
+ * copy (`allowExpired`, what a cache falls back to when its refresh fails)
15
+ * reaches the saved copies; any other read sees only what this run fetched,
16
+ * so a command still asks for current data rather than answering from the
17
+ * app's cache. Plugin state is not read or kept. With no resource store,
18
+ * nothing outlives the run.
19
+ *
20
+ * The profile's store closes when the command ends, while a plugin may keep
21
+ * the persistence it was set up with: `release()` leaves the store alone from
22
+ * then on, and a store that fails (closed, full) costs the saved copy only,
23
+ * never the command.
24
+ */
25
+ export function createCommandPluginPersistence(
26
+ resources: AppResourceStorePort | null | undefined,
27
+ namespace: string,
28
+ ): PluginPersistence & { release(): void } {
29
+ const fetched = new Map<string, PersistedResourceValue<unknown>>();
30
+ const keyOf = (kind: string, key: string) => `${kind}:${key}`;
31
+ let store = resources ?? null;
32
+ const saved = <T>(operation: string, run: (store: AppResourceStorePort) => T, fallback: T): T => {
33
+ if (!store) return fallback;
34
+ try {
35
+ return run(store);
36
+ } catch (error) {
37
+ return logFailure("cli.saved-copies", `${operation} a saved copy failed`, fallback)(error);
38
+ }
39
+ };
40
+ return {
41
+ release: () => { store = null; },
42
+ getState: () => null,
43
+ setState: () => {},
44
+ deleteState: () => {},
45
+ getResource: <T = unknown>(kind: string, key: string, options?: { sourceKey?: string; schemaVersion?: number; allowExpired?: boolean }) => {
46
+ const own = fetched.get(keyOf(kind, key)) as PersistedResourceValue<T> | undefined;
47
+ if (own || !options?.allowExpired) return own ?? null;
48
+ const record = saved("Reading", (resources) => resources.get<T>(
49
+ { namespace, kind, entityKey: key, sourceKey: options.sourceKey },
50
+ { schemaVersion: options.schemaVersion, allowExpired: true },
51
+ ), null);
52
+ return record && {
53
+ value: record.value,
54
+ fetchedAt: record.fetchedAt,
55
+ staleAt: record.staleAt,
56
+ expiresAt: record.expiresAt,
57
+ sourceKey: record.sourceKey,
58
+ schemaVersion: record.schemaVersion,
59
+ provenance: record.provenance,
60
+ stale: record.stale,
61
+ expired: record.expired,
62
+ };
63
+ },
64
+ setResource: <T = unknown>(kind: string, key: string, value: T, options: SetResourceOptions) => {
65
+ const fetchedAt = Date.now();
66
+ const entry: PersistedResourceValue<T> = {
67
+ value,
68
+ fetchedAt,
69
+ staleAt: fetchedAt + options.cachePolicy.staleMs,
70
+ expiresAt: fetchedAt + options.cachePolicy.expireMs,
71
+ sourceKey: options.sourceKey ?? "",
72
+ schemaVersion: options.schemaVersion ?? 1,
73
+ provenance: options.provenance ?? null,
74
+ };
75
+ fetched.set(keyOf(kind, key), entry);
76
+ saved("Writing", (resources) => resources.set<T>({ namespace, kind, entityKey: key, sourceKey: options.sourceKey }, value, {
77
+ cachePolicy: options.cachePolicy,
78
+ schemaVersion: options.schemaVersion,
79
+ provenance: options.provenance as JsonValue | null | undefined,
80
+ }), undefined);
81
+ return entry;
82
+ },
83
+ deleteResource: (kind: string, key: string, options?: { sourceKey?: string }) => {
84
+ fetched.delete(keyOf(kind, key));
85
+ saved("Deleting", (resources) => resources.delete({ namespace, kind, entityKey: key, sourceKey: options?.sourceKey }), undefined);
86
+ },
87
+ };
88
+ }
@@ -85,7 +85,8 @@ export interface CliTicker {
85
85
  /**
86
86
  * A bare coin ticker (ETH) read as a listing that is not the coin, such as a
87
87
  * fund named for it: "ETH is Grayscale Ethereum Mini Trust ETF, not ethereum.
88
- * For ethereum use ETH-USD." It is also the last of `notes`.
88
+ * For ethereum use ETH-USD." A live company that only shares the letters gets
89
+ * "Also a coin: LINK-USD" (`notTheCoinNote`). It is also the last of `notes`.
89
90
  */
90
91
  coinNote: string | null;
91
92
  /**
@@ -367,7 +368,7 @@ async function describeResolution(
367
368
  const formerIssuer = describeFormerIssuer(listing.symbol, payload, current);
368
369
  // One line: a company that left the symbol, else one that merged away from it.
369
370
  const line = tickerChangeLine(change) ?? formerIssuer?.note;
370
- const coinNote = listing.exchange ? null : savedListingCoinNote(listing.symbol, resolved.ticker);
371
+ const coinNote = listing.exchange ? null : savedListingCoinNote(listing.symbol, resolved.ticker, { ended: !!resolved.ended });
371
372
  return { ...resolved, notes: [...line ? [line] : [], ...resolved.notes, ...coinNote ? [coinNote] : []], change, formerIssuer, coinNote,
372
373
  readAs: listing.readAs ?? null, bareSymbol: listing.exchange ? null : listing.symbol, otherVenues: resolved.otherVenues ?? null };
373
374
  }