@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
@@ -1,4 +1,5 @@
1
1
  import type { DataTableColumn } from "../../../components";
2
+ import { formatNumber } from "../../../utils/format";
2
3
 
3
4
  export const TREASURY_DAILY_PANE_ID = "treasury-daily";
4
5
 
@@ -23,6 +24,51 @@ export interface OperatingCash {
23
24
  rows: CashAccount[];
24
25
  }
25
26
 
27
+ /** One day's Treasury General Account closing balance, millions of dollars. */
28
+ export interface TgaClose {
29
+ date: string;
30
+ millions: number;
31
+ }
32
+
33
+ /** The longest stretch of closing balances the Cash tab charts, and the ranges it offers. */
34
+ export type TgaRange = "3M" | "6M" | "1Y" | "ALL";
35
+ export const TGA_RANGES: ReadonlyArray<{ value: TgaRange; label: string; hint: string; months: number | null }> = [
36
+ { value: "3M", label: "3M", hint: "1", months: 3 },
37
+ { value: "6M", label: "6M", hint: "2", months: 6 },
38
+ { value: "1Y", label: "1Y", hint: "3", months: 12 },
39
+ { value: "ALL", label: "All", hint: "4", months: null },
40
+ ];
41
+ const DEFAULT_TGA_RANGE: TgaRange = "1Y";
42
+
43
+ export function tgaRange(value: unknown): TgaRange {
44
+ return TGA_RANGES.find((range) => range.value === value)?.value ?? DEFAULT_TGA_RANGE;
45
+ }
46
+
47
+ /** The closes of the last `range`, counted back from the newest one, oldest first. */
48
+ export function tgaClosesInRange(closes: readonly TgaClose[], range: TgaRange): TgaClose[] {
49
+ const months = TGA_RANGES.find((entry) => entry.value === range)?.months ?? null;
50
+ const last = closes.at(-1);
51
+ if (months == null || !last) return [...closes];
52
+ const start = new Date(`${last.date}T00:00:00Z`);
53
+ start.setUTCMonth(start.getUTCMonth() - months);
54
+ const from = start.toISOString().slice(0, 10);
55
+ return closes.filter((close) => close.date >= from);
56
+ }
57
+
58
+ /**
59
+ * What the Treasury's statement calls a line of the Treasury General Account,
60
+ * said plainly: `Treasury General Account (TGA) Opening Balance` is the
61
+ * opening balance, `Total TGA Withdrawals (Table II) (-)` the withdrawals. A
62
+ * line the statement adds later keeps its own words, less the table reference.
63
+ */
64
+ export function cashLineLabel(account: string): string {
65
+ if (/^Treasury General Account \(TGA\) Opening Balance$/i.test(account)) return "Opening balance";
66
+ if (/^Treasury General Account \(TGA\) Closing Balance$/i.test(account)) return "Closing balance";
67
+ if (/^Total TGA Deposits\b/i.test(account)) return "Deposits";
68
+ if (/^Total TGA Withdrawals\b/i.test(account)) return "Withdrawals";
69
+ return account.replace(/\s*\(Table [IVX]+\)/g, "").replace(/\s*\(-\)$/, "").trim();
70
+ }
71
+
26
72
  export interface DebtDay {
27
73
  id: string;
28
74
  date: string;
@@ -40,7 +86,7 @@ export type CashColumn = DataTableColumn & { id: CashColumnId };
40
86
  export type DebtColumn = DataTableColumn & { id: DebtColumnId };
41
87
 
42
88
  export const CASH_COLUMNS: CashColumn[] = [
43
- { id: "account", label: "Account", width: 28, align: "left", flexGrow: 1 },
89
+ { id: "account", label: "Treasury General Account", width: 26, align: "left", flexGrow: 1 },
44
90
  { id: "today", label: "Today $bn", width: 12, align: "right" },
45
91
  { id: "month", label: "This month $bn", width: 15, align: "right" },
46
92
  ];
@@ -52,10 +98,11 @@ export const DEBT_COLUMNS: DebtColumn[] = [
52
98
  { id: "total", label: "Total $tn", width: 12, align: "right" },
53
99
  ];
54
100
 
101
+ /** Two decimals with thousands separators, as every money table writes a large figure: `2,594.63`. */
55
102
  function scaled(value: unknown, divisor: number): string {
56
103
  if (typeof value !== "number" || !Number.isFinite(value)) return "--";
57
- const text = (value / divisor).toFixed(2);
58
- return text === "-0.00" ? "0.00" : text;
104
+ const rounded = Number((value / divisor).toFixed(2));
105
+ return formatNumber(rounded === 0 ? 0 : rounded, 2);
59
106
  }
60
107
 
61
108
  /** Millions of dollars, drawn as billions. */
@@ -1,33 +1,52 @@
1
1
  import { useCallback, useMemo, useState } from "react";
2
2
  import {
3
+ ChartTableHeader,
3
4
  DataTableView,
4
5
  PaneStatusBody,
6
+ QueryBar,
7
+ usePaneNoticeFooter,
5
8
  usePaneStatusFooter,
6
9
  usePaneTabs,
10
+ type ChartTableChart,
7
11
  type DataTableCell,
12
+ type DataTableKeyEvent,
8
13
  } from "../../../components";
9
- import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
14
+ import { spanAxisFormatter } from "../../../components/chart-table";
15
+ import { useChartViewReset } from "../../../components/chart/core/view-reset";
16
+ import { scalarPoint, staticSeries } from "../../../components/chart/static/series";
17
+ import { handleRefreshKey, usePaneRefreshKey } from "../../../components/data-table/table-pane";
10
18
  import { useAsyncResource, useAutoRefresh, usePluginPaneState } from "../../../public/react";
11
19
  import { useThemeColors } from "../../../theme/theme-context";
12
20
  import type { PaneProps } from "../../../types/plugin";
13
21
  import { Box } from "../../../ui";
22
+ import { formatNumber } from "../../../utils/format";
23
+ import { isPlainKeyboardEvent } from "../../../utils/keyboard";
14
24
  import { fetchTreasuryDaily } from "./client";
15
25
  import {
16
26
  CASH_COLUMNS,
17
27
  DEBT_COLUMNS,
28
+ TGA_RANGES,
18
29
  TREASURY_DAILY_PANE_ID,
19
30
  TREASURY_TABS,
20
31
  formatBillions,
21
32
  formatTrillions,
33
+ tgaClosesInRange,
34
+ tgaRange,
22
35
  type CashAccount,
23
36
  type CashColumn,
24
37
  type DebtColumn,
25
38
  type DebtDay,
39
+ type TgaRange,
26
40
  type TreasuryTab,
27
41
  } from "./model";
28
42
 
29
43
  const EMPTY_CASH: CashAccount[] = [];
30
44
  const EMPTY_DEBT: DebtDay[] = [];
45
+ const RANGE_OPTIONS = TGA_RANGES.map(({ value, label, hint }) => ({ value, label, hint }));
46
+ const TGA_SERIES_LABEL = "TGA closing balance $bn";
47
+ /** Ticks in billions, with thousands separators and the decimals the plotted range needs. */
48
+ const formatBillionsAxis = spanAxisFormatter((value, digits) => formatNumber(value, digits));
49
+ const formatBillionsLevel = (value: number) => formatNumber(value, 2);
31
50
 
32
51
  function scaledCell(value: number | null, text: string, muted: string): DataTableCell {
33
52
  return value == null ? { text, value, color: muted } : { text, value };
@@ -44,6 +63,9 @@ export function TreasuryDailyPane({ width, height, focused }: PaneProps) {
44
63
  const debtRows = data?.debt ?? EMPTY_DEBT;
45
64
  const activeRows = tab === "cash" ? cashRows : debtRows;
46
65
  const recordDate = tab === "cash" ? data?.cash.recordDate ?? null : debtRows[0]?.date ?? null;
66
+ const [storedRange, setStoredRange] = usePluginPaneState<string>("range", "1Y");
67
+ const range = tgaRange(storedRange);
68
+ const resetChartView = useChartViewReset();
47
69
  const [selectedId, setSelectedId] = useState<string | null>(null);
48
70
  const resolvedId = activeRows.some((row) => row.id === selectedId) ? selectedId : activeRows[0]?.id ?? null;
49
71
  const info = useMemo(
@@ -58,13 +80,46 @@ export function TreasuryDailyPane({ width, height, focused }: PaneProps) {
58
80
  dense: true,
59
81
  });
60
82
  useAutoRefresh(resource.updatedAt, resource.load);
61
- usePaneRefreshKey(() => void resource.reload(), { focused });
83
+ const refresh = useCallback(() => void resource.reload(), [resource.reload]);
84
+ usePaneRefreshKey(refresh, { focused });
62
85
  usePaneStatusFooter({
63
86
  registrationId: TREASURY_DAILY_PANE_ID,
64
87
  loading: resource.loading,
65
88
  error: resource.error,
66
89
  info,
67
90
  });
91
+ usePaneNoticeFooter({
92
+ registrationId: `${TREASURY_DAILY_PANE_ID}:history`,
93
+ focused: focused && tab === "cash",
94
+ notices: data?.tgaError ? [`Closing balance history is unavailable: ${data.tgaError}`] : [],
95
+ });
96
+
97
+ // The closing balance the table's last line states for the day, drawn over the range chosen.
98
+ const series = useMemo(() => {
99
+ const closes = tgaClosesInRange(data?.tga ?? [], range);
100
+ return closes.length < 2 ? [] : [staticSeries(
101
+ closes.map((close) => scalarPoint(new Date(`${close.date}T00:00:00Z`), close.millions / 1_000)),
102
+ { id: "tga-closing-balance", label: TGA_SERIES_LABEL, color: colors.positive, calendarSpaced: true },
103
+ )];
104
+ }, [colors.positive, data?.tga, range]);
105
+ const chart: ChartTableChart | null = data?.tgaError ? null : {
106
+ series,
107
+ formatValue: formatBillionsLevel,
108
+ formatAxisValue: formatBillionsAxis,
109
+ remoteKind: "treasury-general-account",
110
+ empty: "No closing balance history.",
111
+ };
112
+ // The number keys pick the range, as the bar's hints say; picking the one showing sends the chart back to it.
113
+ const handleKeyDown = useCallback((event: DataTableKeyEvent) => {
114
+ if (handleRefreshKey(event, refresh, { stopPropagation: true })) return true;
115
+ if (!isPlainKeyboardEvent(event)) return false;
116
+ const picked = TGA_RANGES.find((option) => option.hint === event.name);
117
+ if (!picked) return false;
118
+ event.preventDefault?.();
119
+ if (picked.value === range) resetChartView();
120
+ else setStoredRange(picked.value);
121
+ return true;
122
+ }, [range, refresh, resetChartView, setStoredRange]);
68
123
 
69
124
  const bodyHeight = Math.max(1, height - tabRows);
70
125
  const renderCash = useCallback((row: CashAccount, column: CashColumn): DataTableCell => {
@@ -91,6 +146,30 @@ export function TreasuryDailyPane({ width, height, focused }: PaneProps) {
91
146
  focused={focused}
92
147
  rootWidth={width}
93
148
  rootHeight={bodyHeight}
149
+ rootBefore={chart ? (
150
+ <ChartTableHeader
151
+ width={width}
152
+ height={bodyHeight}
153
+ tableRows={cashRows.length}
154
+ tableColumns={CASH_COLUMNS}
155
+ query={(
156
+ <QueryBar
157
+ width={width}
158
+ filters={[{
159
+ id: "range",
160
+ label: "Range",
161
+ inline: true,
162
+ chartRange: true,
163
+ value: range,
164
+ options: RANGE_OPTIONS,
165
+ onChange: (value: string) => setStoredRange(tgaRange(value) satisfies TgaRange),
166
+ }]}
167
+ />
168
+ )}
169
+ chart={chart}
170
+ />
171
+ ) : undefined}
172
+ onRootKeyDown={handleKeyDown}
94
173
  columns={CASH_COLUMNS}
95
174
  items={cashRows}
96
175
  sortColumnId={null}
@@ -1,4 +1,5 @@
1
1
  import { apiClient } from "../../../api-client";
2
+ import { createTtlCache } from "../../../data/ttl-cache";
2
3
  import { getSharedMarketDataCoordinator, MarketDataCoordinator, resolveEntryValue } from "../../../market-data/coordinator";
3
4
  import type { InstrumentRef, OptionsRequest } from "../../../market-data/request-types";
4
5
  import type { QueryEntry } from "../../../market-data/result-types";
@@ -73,19 +74,8 @@ export function withReusedYieldCurve(
73
74
  dependencies: SurfaceLoaderDependencies,
74
75
  reuseMs = TREASURY_CURVE_REUSE_MS,
75
76
  ): SurfaceLoaderDependencies {
76
- let held: { at: number; curve: Promise<YieldPoint[]> } | null = null;
77
- return {
78
- ...dependencies,
79
- loadYieldCurve: () => {
80
- const now = Date.now();
81
- if (held && now - held.at < reuseMs) return held.curve;
82
- const curve = dependencies.loadYieldCurve();
83
- const entry = { at: now, curve };
84
- held = entry;
85
- curve.catch(() => { if (held === entry) held = null; });
86
- return curve;
87
- },
88
- };
77
+ const curves = createTtlCache<YieldPoint[]>({ ttlMs: reuseMs });
78
+ return { ...dependencies, loadYieldCurve: () => curves.load("curve", () => dependencies.loadYieldCurve()) };
89
79
  }
90
80
 
91
81
  export interface SurfaceLoadRequest {
@@ -12,7 +12,7 @@ import { createSurfaceDependencies, loadVolatilitySurface } from "./client";
12
12
  import { buildSurfaceGrid, type SurfaceGridOptions, type SurfaceSettings, type SurfaceSnapshot } from "./model";
13
13
  import { atCapturePrecision, formatCaptureTime, loadDatedSurface, storedSurfaceDate, type DatedSurfaceSnapshot } from "./stored";
14
14
  import { listingFateEmpty, loadListingFate } from "../../../tickers/listing-fate";
15
- import { coinOptionsAlias, coinWithoutOptionsRefusal } from "../../../market-data/options-alternatives";
15
+ import { coinOptionsAlias, coinWithoutOptionsRefusal, fxVolatilityRefusal } from "../../../market-data/options-alternatives";
16
16
  import { formatUtcTime } from "../../../utils/utc-time";
17
17
  import { dvolCurrency, dvolGap, dvolSeriesId, loadDvol, type DvolPayload } from "../volatility/dvol";
18
18
  import {
@@ -118,7 +118,7 @@ function skewSection(snapshot: SurfaceSnapshot): HeadlessBundleSection {
118
118
  return { title: "Skew", columns: [
119
119
  { key: "expiry", header: "Expiry" }, { key: "days", header: "Days", format: days },
120
120
  { key: "put25", header: "25D put IV", format: iv }, { key: "call25", header: "25D call IV", format: iv },
121
- // Each difference names its sign: the risk reversal is call minus put, unlike HIVG and VCA's put-minus-call skew.
121
+ // Each difference names its sign: the risk reversal is call minus put here and in VCA and HIVG's rr25d.
122
122
  { key: "riskReversal", header: skewHeader("RR", "call-put"), format: gap }, { key: "butterfly", header: skewHeader("BF", "wings-ATM"), format: gap },
123
123
  { key: "moneynessSkew", header: skewHeader("90/110", "90%-110%"), format: (value, row) => typeof value === "number" ? points(value)
124
124
  : (row as unknown as SurfaceSkewRow).moneynessGap ?? "--" },
@@ -169,7 +169,7 @@ export const volSurfaceHeadless: HeadlessPaneDefinition<"bundle"> = {
169
169
  shape: "bundle", argument: { kind: "ticker", description: "Underlying ticker" },
170
170
  usListingsOnly: "Options data",
171
171
  symbolAlias: coinOptionsAlias("OVDV"),
172
- refuseSymbol: coinWithoutOptionsRefusal("OVDV"),
172
+ refuseSymbol: (key) => coinWithoutOptionsRefusal("OVDV")(key) ?? fxVolatilityRefusal("OVDV")(key),
173
173
  discovery: { suggestFor: IMPLIED_VOL_WORDS },
174
174
  describe: (args) => `OVDV ${args.symbols[0] ?? ""}`,
175
175
  options: [
@@ -39,7 +39,7 @@ export const volSurfaceModule: PluginModule = {
39
39
  paneTemplates: [createTickerSurfacePaneTemplate({
40
40
  id: "vol-surface-pane", paneId: "vol-surface", label: "Volatility Surface",
41
41
  description: "Implied volatility surface, smiles, term structure, skew (25-delta risk reversal and butterfly) and forwards; DVOL beside BTC-USD and ETH-USD.",
42
- keywords: ["ovdv", "volatility", "surface", "smile", "skew", "options", "risk reversal", "butterfly", "25 delta", "dvol", "deribit", "implied volatility index"],
42
+ keywords: ["ovdv", "volatility", "vol", "surface", "smile", "skew", "options", "risk reversal", "butterfly", "25 delta", "dvol", "deribit", "implied volatility index"],
43
43
  shortcut: "OVDV", publicShare: true,
44
44
  settings: (_symbol, _context, options) => options?.values?.expiration
45
45
  ? { expiration: Number(options.values.expiration) } : {},
@@ -43,7 +43,7 @@ import { expiryLabel, formatIv, formatPrice, formatVolPoints, HIDDEN_SLOPE_TEXT,
43
43
  surfaceSkewRow, surfaceSpotBasisLabel } from "./tables";
44
44
  import { findListedExpiry, unlistedExpiryText } from "../../../utils/option-expiry";
45
45
  import { dvolCurrency, dvolGap, loadDvol, type DvolPayload } from "../volatility/dvol";
46
- import { COIN_OPTIONS_HINT, isCoinWithoutOptions, optionsUnavailableTitle } from "../../../market-data/options-alternatives";
46
+ import { COIN_OPTIONS_HINT, fxVolatilityNote, isCoinWithoutOptions, optionsUnavailableTitle } from "../../../market-data/options-alternatives";
47
47
  import { parsePublicTickerKey } from "../../../utils/exchanges";
48
48
  import { surfaceThirtyDayAtm } from "./tables";
49
49
 
@@ -191,9 +191,10 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
191
191
  const fateExchange = target?.effectiveExchange ?? "";
192
192
  const fateLoader = useCallback(async () => (await getSharedMarketDataCoordinator()
193
193
  ?.loadListingFate({ symbol: underlying, exchange: fateExchange }))?.note ?? null, [underlying, fateExchange]);
194
- // A coin nothing lists options on (SOL-USD) says which coins have them.
194
+ // A coin nothing lists options on (SOL-USD) says which coins have them, and an FX pair which funds stand in.
195
195
  const coinNote = underlying && isCoinWithoutOptions(underlying)
196
- ? `${optionsUnavailableTitle(parsePublicTickerKey(underlying).symbol)} ${COIN_OPTIONS_HINT}` : null;
196
+ ? `${optionsUnavailableTitle(parsePublicTickerKey(underlying).symbol)} ${COIN_OPTIONS_HINT}`
197
+ : underlying ? fxVolatilityNote(underlying, target?.effectiveExchange) : null;
197
198
  const fateNote = useAsyncResource(underlying && noChain && !coinNote ? fateLoader : null).data ?? coinNote;
198
199
  const ivRank = useIvRank(underlying || null);
199
200
  // A coin's options (BTC-USD, ETH-USD) come with Deribit's volatility index.
@@ -224,9 +225,10 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
224
225
  // visible; the upstream chain cache dedupes the requests across users. A
225
226
  // delayed surface keeps the configured refresh.
226
227
  const realtimeSurface = !!settledSnapshot?.expiries.some((entry) => entry.dataSource === "live" || entry.realtimeEligible === true);
227
- const liveActive = useLiveSessionRefresh(reloadLive, SURFACE_LIVE_RELOAD_MS,
228
+ const liveRefresh = useLiveSessionRefresh(reloadLive, SURFACE_LIVE_RELOAD_MS,
228
229
  !!symbol && spotAvailable && !historyDate && !fallbackDate && liveStreaming && realtimeSurface && !resource.loading,
229
230
  resource.updatedAt);
231
+ const liveActive = liveRefresh.live;
230
232
  useAutoRefresh(resource.updatedAt, liveActive ? noRefresh : resource.load);
231
233
  const shownDate = historyDate ?? fallbackDate;
232
234
  const storedLoader = useCallback(async () => storedSurfaceSnapshot(await loadStoredSurface(underlying, shownDate!)), [underlying, shownDate]);
@@ -396,7 +398,7 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
396
398
  ...(pinned != null && snapshot && !resource.loading && !snapshot.catalogue.includes(pinned)
397
399
  && !snapshot.failures.some((failure) => failure.expiration === pinned)
398
400
  ? [`${expiryLabel(pinned)}: ${unlistedExpiryText(pinned, snapshot.catalogue)}`] : []),
399
- ...(active.error ? [active.error] : [])];
401
+ ...(active.error ? [active.error] : []), ...(liveRefresh.error ? [liveRefresh.error] : [])];
400
402
  const spotBasis = snapshot && !snapshot.stored ? surfaceSpotBasisLabel(snapshot) : null;
401
403
  // What the quotes are and when they were observed; a reload moves the time.
402
404
  const freshness = !snapshot ? null : snapshot.stored ? `stored close ${snapshot.stored.sessionDate}`
@@ -14,13 +14,13 @@ export const volatilityModule: PluginModule = {
14
14
  paneTemplates: [{
15
15
  id: "volatility-term-structure-pane", paneId: "volatility-term-structure",
16
16
  label: "VIX Curve", description: "Dated VIX tenor closes, the 3M/30D ratio history and the cross-asset volatility board.",
17
- keywords: ["vix", "volatility", "curve", "contango", "backwardation", "vvix", "skew", "move"],
17
+ keywords: ["vix", "volatility", "vol", "curve", "contango", "backwardation", "vvix", "skew", "move"],
18
18
  shortcut: { prefix: "VIX" }, headless: volatilityHeadless,
19
19
  createInstance: () => ({ instanceId: "volatility:curve", title: "VIX", placement: "floating", settings: { initialTab: "curve" } }),
20
20
  }, {
21
21
  id: "volatility-board-pane", paneId: "volatility-term-structure",
22
22
  label: "Cross-asset Volatility", description: "Volatility index closes, daily changes and one-year percentiles.",
23
- keywords: ["volatility", "cross asset", "vols", "vxn", "ovx", "gvz"],
23
+ keywords: ["volatility", "vol", "cross asset", "vols", "vxn", "ovx", "gvz"],
24
24
  shortcut: { prefix: "VOLS" }, headless: volatilityHeadless,
25
25
  createInstance: () => ({ instanceId: "volatility:board", title: "VOLS", placement: "floating", settings: { initialTab: "board" } }),
26
26
  }],
@@ -107,7 +107,7 @@ export function VolatilityPane({ focused, width, height }: PaneProps) {
107
107
  await coordinator.loadQuotesBatch(quiet.map((target) => ({ symbol: target.symbol, exchange: "" })), { forceRefresh: true });
108
108
  }, [liveTargets]);
109
109
  // With streaming off the quote layer already polls every minute.
110
- useLiveSessionRefresh(refreshIndexLevels, INDEX_LEVEL_REFRESH_MS, liveStreaming && liveTargets.length > 0);
110
+ const levelRefresh = useLiveSessionRefresh(refreshIndexLevels, INDEX_LEVEL_REFRESH_MS, liveStreaming && liveTargets.length > 0);
111
111
  const liveLevels = useThrottledValue(useMemo(() => {
112
112
  const levels = new Map<VolatilityIndexId, VolatilityLiveLevel>();
113
113
  for (const definition of VOLATILITY_INDICES) {
@@ -139,7 +139,7 @@ export function VolatilityPane({ focused, width, height }: PaneProps) {
139
139
  }, [data?.board, sort, loading]);
140
140
  const selected = rows.find((row) => row.id === selectedId) ?? rows[0] ?? null;
141
141
  useVolatilityEvidence(result, tab, selected, resource.loading);
142
- const notices = [resource.error, ...(result?.errors ?? []), ...(data?.warnings ?? []),
142
+ const notices = [resource.error, levelRefresh.error, ...(result?.errors ?? []), ...(data?.warnings ?? []),
143
143
  ...(selected?.warnings ?? [])].filter((notice): notice is string => !!notice);
144
144
  usePaneNoticeFooter({ registrationId: "volatility-notices", notices, focused });
145
145
  // From the tab the press finds, so the footer's hint never holds an old one.
@@ -84,15 +84,15 @@ function layerColumns(layer: GeoLayerInfo): HeadlessPaneColumn[] {
84
84
  /**
85
85
  * What a search that finds nothing says. A search outside a one-country layer
86
86
  * finds nothing because the layer has nothing there; ships are found by name,
87
- * not by the ticker of the company that runs them, so a ticker that links no
88
- * fleet (or a server that links none) gets a name to try.
87
+ * or by the name or ticker of an operator whose fleet is linked, so anything
88
+ * else (or a server that links no fleet) gets a name to try.
89
89
  */
90
90
  function emptySearchNote(layer: GeoLayerInfo, query: string): string | null {
91
91
  const typed = query.trim();
92
92
  if (!typed) return null;
93
93
  const coverage = layerCoverageNote(layer);
94
94
  if (coverage) return `No ${layer.name.toLowerCase()} match "${typed}". ${coverage}`;
95
- if (layer.id === "vessels") return `No ${layer.name.toLowerCase()} match "${typed}"; try a ship-name prefix, such as --query Norwegian.`;
95
+ if (layer.id === "vessels") return `No ${layer.name.toLowerCase()} match "${typed}"; try a ship-name prefix or an operator, such as --query Norwegian or --query "Royal Caribbean".`;
96
96
  return null;
97
97
  }
98
98
 
@@ -75,7 +75,8 @@ const NATIVE_TARGETS: PluginTarget[] = ["cli", "tui", "desktop"];
75
75
  */
76
76
  function functionsOf(plugin: GloomPlugin) {
77
77
  return pluginFunctions({
78
- templates: (plugin.paneTemplates ?? []).map((template) => ({
78
+ // An unlisted function (`listed: false`) is not published.
79
+ templates: (plugin.paneTemplates ?? []).filter((template) => template.listed !== false).map((template) => ({
79
80
  label: template.label,
80
81
  prefix: template.shortcut?.prefix,
81
82
  description: template.description,
@@ -186,6 +186,19 @@ function repoDirectory(repo: string | undefined): string | null {
186
186
  return name ? name.toLowerCase() : null;
187
187
  }
188
188
 
189
+ /**
190
+ * Built-ins write their copy as sentences and registry plugins as labels, so a
191
+ * row's short line is read without its closing period, whoever wrote it, and
192
+ * the detail's paragraph always has one. An ellipsis stays as written.
193
+ */
194
+ function rowLine(text: string | undefined): string {
195
+ return (text ?? "").trim().replace(/(?<!\.)\.$/, "");
196
+ }
197
+ function detailParagraph(text: string | undefined): string | undefined {
198
+ const paragraph = text?.trim();
199
+ return paragraph ? /[\p{L}\p{N}]$/u.test(paragraph) ? `${paragraph}.` : paragraph : undefined;
200
+ }
201
+
189
202
  /** The short line a built-in's row reads: its editorial tagline, else its description. */
190
203
  function builtinTagline(local: InstalledPlugin): string {
191
204
  return BUILTIN_EDITORIAL[local.id]?.tagline ?? local.description ?? "";
@@ -257,8 +270,8 @@ export function mergeCatalog(options: {
257
270
  entries.push({
258
271
  id: plugin.id,
259
272
  name: builtin?.name ?? plugin.name,
260
- tagline: builtin ? builtinTagline(builtin) : plugin.tagline,
261
- description: builtin?.description ?? plugin.description,
273
+ tagline: rowLine(builtin ? builtinTagline(builtin) : plugin.tagline),
274
+ description: detailParagraph(builtin?.description ?? plugin.description),
262
275
  categories: builtin ? BUILTIN_EDITORIAL[plugin.id]?.categories ?? plugin.categories : plugin.categories,
263
276
  tier: plugin.tier,
264
277
  targets: plugin.targets,
@@ -309,10 +322,10 @@ export function mergeCatalog(options: {
309
322
  entries.push({
310
323
  id: local.id,
311
324
  name: local.name,
312
- tagline: local.source === "builtin"
325
+ tagline: rowLine(local.source === "builtin"
313
326
  ? builtinTagline(local)
314
- : local.description ?? (local.linked ? "Linked from a local checkout" : "Installed outside the registry"),
315
- description: local.description,
327
+ : local.description ?? (local.linked ? "Linked from a local checkout" : "Installed outside the registry")),
328
+ description: detailParagraph(local.description),
316
329
  categories: local.source === "builtin" ? [...BUILTIN_EDITORIAL[local.id]?.categories ?? []] : ["unlisted"],
317
330
  // Shipped by Gloom like every built-in, whether or not the feed lists it.
318
331
  tier: local.source === "builtin" ? "official" : "community",
@@ -36,10 +36,12 @@ export interface FuturesSettlement {
36
36
  previous: number | null;
37
37
  }
38
38
 
39
- /** What a portfolio reads from a future's terms: its contract size and its settlement. */
39
+ /** A future's contract size, settlement and front reference for collection rows. */
40
40
  export interface PortfolioFuturesTerms {
41
41
  size: FuturesContractSize | null;
42
42
  settlement: FuturesSettlement | null;
43
+ /** A different contract that is currently front, when the terms name one. */
44
+ front?: Pick<NonNullable<FuturesContractTermsPayload["front"]>, "contract" | "contractSymbol">;
43
45
  }
44
46
 
45
47
  /** A grain or a soft quotes in US cents, so a dollar point value is a hundred of its price units. */
@@ -167,7 +169,7 @@ export function previousFuturesSettlement(
167
169
 
168
170
  /** What a portfolio reads from a future's terms, with the settlement before the latest when the history has it. */
169
171
  export function portfolioFuturesTerms(
170
- terms: Pick<FuturesContractTermsPayload, "spec" | "settlement"> | null | undefined,
172
+ terms: (Pick<FuturesContractTermsPayload, "spec" | "settlement"> & Partial<Pick<FuturesContractTermsPayload, "front" | "contract">>) | null | undefined,
171
173
  history?: readonly PricePoint[] | null,
172
174
  ): PortfolioFuturesTerms {
173
175
  const settlement = terms?.settlement && Number.isFinite(terms.settlement.price) && terms.settlement.price > 0
@@ -177,7 +179,8 @@ export function portfolioFuturesTerms(
177
179
  previous: previousFuturesSettlement(history, terms.settlement.date, terms.settlement.price),
178
180
  }
179
181
  : null;
180
- return { size: futuresContractSize(terms?.spec), settlement };
182
+ const front = terms?.front && terms.front.contract !== terms.contract ? terms.front : undefined;
183
+ return { size: futuresContractSize(terms?.spec), settlement, ...(front ? { front } : {}) };
181
184
  }
182
185
 
183
186
  /**
@@ -23,21 +23,27 @@ function tradesInWholeUnits(ticker: TickerRecord): boolean {
23
23
  }
24
24
 
25
25
  /**
26
- * Why a typed size cannot be stored as read: a dot and three digits (`1.250`)
27
- * on a listing held in whole units, where it is far likelier the thousands
28
- * grouping many locales write (1,250 shares) than a quarter of a share, so the
29
- * command asks which, as it does for `1,250`. Null for any other size, and
30
- * for a listing that may be held in fractions, where `1.250` is 1.25.
26
+ * Why a typed size cannot be stored as read: a dot and three digits (`1.000`,
27
+ * `1.250`), which is as likely the thousands grouping many locales write
28
+ * (1,000 shares, as cash reads `1.000`) as a fraction of a share, so the
29
+ * command asks which, as it does for `1,250`. On a listing held in whole
30
+ * units the thousands reading is the only one that can be held, and the
31
+ * message says so. Null for any other size (`0.500`, `12.25`).
31
32
  */
32
- export function wholeUnitQuantityProblem(
33
+ export function ambiguousQuantityProblem(
33
34
  typed: string,
34
35
  ticker: TickerRecord,
35
36
  options: { label?: PositionQuantityLabel; listing?: string } = {},
36
37
  ): string | null {
37
38
  const reading = thousandsDotReadings(typed);
38
- if (!reading || !tradesInWholeUnits(ticker)) return null;
39
+ if (!reading) return null;
39
40
  const label = options.label ?? positionQuantityLabel(ticker);
40
41
  const listing = options.listing ?? ticker.metadata.ticker;
41
- const because = `${listing} trades in whole ${label === "Contracts" ? "contracts" : "shares"}`;
42
- return `${label} ${ambiguousNumberMessage(typed, reading, { because })}.`;
42
+ const because = tradesInWholeUnits(ticker)
43
+ ? `${listing} trades in whole ${label === "Contracts" ? "contracts" : "shares"}`
44
+ : undefined;
45
+ // The fraction typed so it does not read two ways itself: 1.234 asks again, 1.2340 does not.
46
+ const fraction = String(reading.decimal);
47
+ const decimalAs = thousandsDotReadings(fraction) ? `${fraction}0` : fraction;
48
+ return `${label} ${ambiguousNumberMessage(typed, reading, { because, decimalAs })}.`;
43
49
  }
@@ -110,7 +110,8 @@ export function createRemoteResources({
110
110
  }));
111
111
  }
112
112
  if (resource === "app://pane-templates") {
113
- return [...pluginRegistry.paneTemplates.values()].map((template) => ({
113
+ // An unlisted function (`listed: false`) is left out, as of every list.
114
+ return [...pluginRegistry.paneTemplates.values()].filter((template) => template.listed !== false).map((template) => ({
114
115
  id: template.id,
115
116
  paneId: template.paneId,
116
117
  label: template.label,
@@ -457,6 +457,32 @@ function WebDataTableRowInner<
457
457
 
458
458
  function CellText({ cell, column, color }: { cell: DataTableCell; column: DataTableColumn; color: string }) {
459
459
  const style = clippedCellTextStyle(column, color, cell.attributes ?? TextAttributes.NONE);
460
+ if (cell.note) {
461
+ const text = (
462
+ <div
463
+ style={{
464
+ display: "flex",
465
+ flexWrap: "wrap",
466
+ alignContent: "flex-start",
467
+ justifyContent: contentJustifyForAlign(column.align),
468
+ columnGap: "var(--gloom-space-sm)",
469
+ width: "100%",
470
+ height: "100%",
471
+ minWidth: 0,
472
+ overflow: "hidden",
473
+ }}
474
+ >
475
+ <span title={cell.text} style={{ ...style, width: "auto", flex: "0 1 auto", minWidth: 0 }}>{cell.text}</span>
476
+ <span data-gloom-role="data-table-cell-note" style={{ ...cellTextStyle(CSS_TEXT_DIM, TextAttributes.NONE), flex: "none" }}>{cell.note}</span>
477
+ </div>
478
+ );
479
+ return cell.leading === undefined ? text : (
480
+ <div style={{ display: "flex", alignItems: "center", gap: LABEL_GAP_PX, width: "100%", height: "100%", minWidth: 0, overflow: "hidden" }}>
481
+ <Leading>{cell.leading}</Leading>
482
+ {text}
483
+ </div>
484
+ );
485
+ }
460
486
  if (cell.leading === undefined) return <span title={cell.text} style={style}>{cell.text}</span>;
461
487
  return (
462
488
  <div
@@ -27,6 +27,7 @@ function DefaultDesktopRow({
27
27
  stacked: boolean;
28
28
  }) {
29
29
  const colors = useThemeColors();
30
+ if (item.kind === "heading") return <Text fg={colors.textMuted}>{item.label}</Text>;
30
31
  return (
31
32
  <Box
32
33
  flexDirection="row"
@@ -142,6 +143,7 @@ export function WebListView({
142
143
  const rowHoverBg = hoverBgColor ?? hoverBg(colors);
143
144
  const selectedItem = selectedIndex >= 0 ? items[selectedIndex] : undefined;
144
145
  const activeScrollIndex = scrollIndex ?? selectedIndex;
146
+ const headingAboveSelection = items[activeScrollIndex - 1]?.kind === "heading";
145
147
  const rowStride = rowHeight + rowGap;
146
148
  const tabStopIndex = selectedIndex >= 0 && items[selectedIndex] && !items[selectedIndex].disabled
147
149
  ? selectedIndex : items.findIndex((item) => !item.disabled);
@@ -176,14 +178,14 @@ export function WebListView({
176
178
  if (!scrollBox) return;
177
179
  const safeIndex = Math.min(activeScrollIndex, items.length - 1);
178
180
  const viewportHeight = Math.max(scrollBox.viewport?.height ?? 0, 1);
179
- const rowTop = safeIndex * rowStride;
180
- const rowBottom = rowTop + rowHeight;
181
+ const rowTop = Math.max(0, safeIndex - (headingAboveSelection ? 1 : 0)) * rowStride;
182
+ const rowBottom = safeIndex * rowStride + rowHeight;
181
183
  if (rowTop < scrollBox.scrollTop) {
182
184
  scrollBox.scrollTo(rowTop);
183
185
  } else if (rowBottom > scrollBox.scrollTop + viewportHeight) {
184
186
  scrollBox.scrollTo(rowBottom - viewportHeight);
185
187
  }
186
- }, [activeScrollIndex, autoScrollToIndex, items.length, rowHeight, rowStride, scrolls]);
188
+ }, [activeScrollIndex, autoScrollToIndex, headingAboveSelection, items.length, rowHeight, rowStride, scrolls]);
187
189
 
188
190
  useEffect(() => {
189
191
  if (!scrolls) return;
@@ -202,7 +204,7 @@ export function WebListView({
202
204
  const list = document.getElementById(listDomId);
203
205
  const active = document.activeElement;
204
206
  if (!list || !active || !list.contains(active) || isEditableKeyboardTarget(active)) return;
205
- const row = list.querySelectorAll<HTMLElement>('[role="option"]')[selectedIndex];
207
+ const row = list.querySelector<HTMLElement>(`[data-gloom-list-index="${selectedIndex}"]`);
206
208
  if (row && !row.contains(active)) row.focus({ preventScroll: scrolls });
207
209
  }, [listDomId, scrolls, selectedIndex]);
208
210
 
@@ -246,10 +248,11 @@ export function WebListView({
246
248
  onActivate?.(item, index);
247
249
  }}
248
250
  data-gloom-role="desktop-list-row"
249
- role="option"
250
- aria-selected={selected}
251
- aria-disabled={disabled || undefined}
252
- tabIndex={index === tabStopIndex ? 0 : -1}
251
+ data-gloom-list-index={index}
252
+ role={item.kind === "heading" ? "presentation" : "option"}
253
+ aria-selected={item.kind === "heading" ? undefined : selected}
254
+ aria-disabled={item.kind === "heading" ? undefined : disabled || undefined}
255
+ tabIndex={item.kind === "heading" ? undefined : index === tabStopIndex ? 0 : -1}
253
256
  onKeyDown={(event: React.KeyboardEvent<HTMLElement>) => {
254
257
  if (disabled) return;
255
258
  if (event.key === " " || (event.key === "Enter" && !checkboxRows)) {
@@ -267,8 +270,8 @@ export function WebListView({
267
270
  while (items[nextIndex]?.disabled) nextIndex += direction;
268
271
  if (!items[nextIndex]) return;
269
272
  onSelect?.(nextIndex);
270
- const rows = event.currentTarget.parentElement?.querySelectorAll<HTMLElement>('[role="option"]');
271
- rows?.[nextIndex]?.focus();
273
+ const row = event.currentTarget.parentElement?.querySelector<HTMLElement>(`[data-gloom-list-index="${nextIndex}"]`);
274
+ row?.focus();
272
275
  }}
273
276
  style={listRowStyle(selected, disabled, colors, selection === "frame")}
274
277
  >