@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
@@ -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.
@@ -171,8 +171,11 @@ export function VolatilityPane({ focused, width, height }: PaneProps) {
171
171
  usePaneFooter("volatility", () => ({ info: [
172
172
  ...(resource.loading ? [{ id: "loading", parts: [{ text: "loading volatility", tone: "muted" as const }] }] : []),
173
173
  ...(result?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "warning" as const }] }] : []),
174
- ...(data ? [{ id: "basis", parts: [{ text: observationBasis, tone: "muted" as const }] }] : []),
175
- ...(asOf ? [{ id: "date", parts: [{ text: observationTime ? `${observationTime.slice(0, 16).replace("T", " ")} UTC` : asOf, tone: "muted" as const }] }] : []),
174
+ // One fact, "close 2026-09-25": the basis and its date read as one segment.
175
+ ...(data || asOf ? [{ id: "basis", parts: [
176
+ ...(data ? [{ text: observationBasis, tone: "muted" as const }] : []),
177
+ ...(asOf ? [{ text: observationTime ? `${observationTime.slice(0, 16).replace("T", " ")} UTC` : asOf, tone: "muted" as const }] : []),
178
+ ] }] : []),
176
179
  ], hints: [{ id: "view", key: "v", label: "iew", onPress: cycleTab }] }), [resource.loading, result?.stale, data, asOf, observationTime, observationBasis, cycleTab]);
177
180
  const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs: TABS, activeValue: tab, onSelect: setTab, focused, dense: true });
178
181
  const contentHeight = Math.max(5, height - tabRows);
@@ -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
 
@@ -1,8 +1,11 @@
1
1
  import { apiClient } from "../../../api-client";
2
2
  import type { SwapCurvePayload, SwapTenor } from "../../../api-client/swap-curve";
3
- import { createPluginCache } from "../../../data/plugin-cache";
3
+ import { isPermanentClientError } from "../../../api-client/errors";
4
+ import { createPluginCache, type PluginCacheResult } from "../../../data/plugin-cache";
4
5
  import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
5
6
  import { validateSwapCurve } from "./swaps-model";
7
+ import { loadCurveData, type CurveData, type CurveId } from "./curves";
8
+ import { loadWorldRows, type WorldRow } from "./world";
6
9
 
7
10
  export const swapCurveCache = createPluginCache<SwapCurvePayload>({
8
11
  kind: "sofr-swap-curve", source: "gloom-cloud", schemaVersion: 1,
@@ -23,3 +26,60 @@ export async function fetchSwapCurve(tenor: SwapTenor,
23
26
  export const getCachedSwapCurve = (tenor: SwapTenor) => cachedCloudResource(swapCurveCache, tenor, validateSwapCurve);
24
27
  export const loadSwapCurve = (tenor: SwapTenor, force = false) =>
25
28
  loadCloudResource(swapCurveCache, tenor, () => fetchSwapCurve(tenor), { force, validate: validateSwapCurve });
29
+
30
+ const DAY_MS = 24 * 60 * 60_000;
31
+
32
+ /**
33
+ * The Curve and World tabs' curves as last read, so a refresh that fails
34
+ * (offline, a service error) shows the copy an earlier read saved instead of
35
+ * an empty tab. Every load still asks the server; the copy is the fallback.
36
+ */
37
+ export const curveViewCache = createPluginCache<CurveData>({
38
+ kind: "yield-curve-view", source: "gloom-cloud", schemaVersion: 1,
39
+ policy: { staleMs: 15 * 60_000, expireMs: 30 * DAY_MS },
40
+ });
41
+ export const worldCurvesCache = createPluginCache<WorldRow[]>({
42
+ kind: "world-curves", source: "gloom-cloud", schemaVersion: 1,
43
+ policy: { staleMs: 15 * 60_000, expireMs: 30 * DAY_MS },
44
+ });
45
+
46
+ /** What a tab shows, and when the copy shown was saved when it is one, its refresh having failed. */
47
+ export interface SavedCopyAnswer<T> {
48
+ data: T;
49
+ savedAt: number | null;
50
+ }
51
+
52
+ const curveKey = (curve: CurveId, requestedDate: string) => `${curve}:${requestedDate || "latest"}`;
53
+ /**
54
+ * A copy is the saved one by where it came from, whatever its failure said. A
55
+ * refusal asking again will not change (access denied, an unknown curve) is
56
+ * said rather than covered by the copy, as `loadCloudResource` does.
57
+ */
58
+ function answer<T>(result: PluginCacheResult<T>): SavedCopyAnswer<T> {
59
+ const saved = result.source === "stale-fallback";
60
+ if (saved && isPermanentClientError(result.error)) throw result.error;
61
+ return { data: result.data, savedAt: saved ? result.fetchedAt : null };
62
+ }
63
+
64
+ /** The curve on the date (its latest without one), or the copy an earlier read saved when it cannot be read. */
65
+ export async function loadSavedCurve(curve: CurveId, requestedDate: string,
66
+ client?: Parameters<typeof loadCurveData>[2]): Promise<SavedCopyAnswer<CurveData>> {
67
+ return answer(await curveViewCache.load(curveKey(curve, requestedDate), () => loadCurveData(curve, requestedDate, client), { force: true }));
68
+ }
69
+
70
+ /** The saved curve, for the tab's first paint while it is read again. */
71
+ export function cachedCurve(curve: CurveId, requestedDate: string): SavedCopyAnswer<CurveData> | null {
72
+ const cached = curveViewCache.get(curveKey(curve, requestedDate), { allowExpired: true });
73
+ return cached ? { data: cached.data, savedAt: null } : null;
74
+ }
75
+
76
+ /** Each market's latest curve, or the copy an earlier read saved when they cannot be read. */
77
+ export async function loadSavedWorldRows(client?: Parameters<typeof loadWorldRows>[0]): Promise<SavedCopyAnswer<WorldRow[]>> {
78
+ return answer(await worldCurvesCache.load("latest", () => loadWorldRows(client), { force: true }));
79
+ }
80
+
81
+ /** The saved world board, for the tab's first paint while it is read again. */
82
+ export function cachedWorldRows(): SavedCopyAnswer<WorldRow[]> | null {
83
+ const cached = worldCurvesCache.get("latest", { allowExpired: true });
84
+ return cached ? { data: cached.data, savedAt: null } : null;
85
+ }
@@ -4,16 +4,17 @@ import type {
4
4
  } from "../../../types/plugin";
5
5
  import {
6
6
  compareDate, curveCutoff, CURVE_OPTIONS, curveIdOf, curveOption, curveSpreadFigures, curveTenorMove, loadComparePoints,
7
- loadCurveData, parseCompareInput, parseCurveArgument, type CurveData, type CurveId,
7
+ parseCompareInput, parseCurveArgument, type CurveData, type CurveId,
8
8
  } from "./curves";
9
9
  import { yieldTenorRows, type YieldLookbackCurves } from "./chart";
10
10
  import { curveAsOf, type YieldPoint, yieldCurveErrors } from "./treasury-data";
11
11
  import { loadYieldCurveLookbacks, yieldCurveDate } from "./history";
12
- import { loadWorldRows, type WorldRow } from "./world";
12
+ import type { WorldRow } from "./world";
13
13
  import { formatBasisPoints, toBasisPoints } from "../../../utils/basis-points";
14
14
  import { DATE_OPTION_EXAMPLES } from "../../../utils/calendar-date";
15
15
  import { SWAP_TENORS, type SwapCurvePayload, type SwapTenor } from "../../../api-client/swap-curve";
16
- import { fetchSwapCurve } from "./client";
16
+ import { fetchSwapCurve, loadSavedCurve, loadSavedWorldRows } from "./client";
17
+ import { savedCopyMark } from "../../../data/saved-copies";
17
18
  import { formatSwapBp, selectedSwapTenor } from "./swaps-model";
18
19
 
19
20
  const percent = (value: unknown) => value == null ? "-" : `${Number(value).toFixed(2)}%`;
@@ -87,13 +88,14 @@ export interface YieldCurveHeadlessDependencies {
87
88
  }
88
89
 
89
90
  const defaultDependencies: Required<YieldCurveHeadlessDependencies> = {
90
- load: (curve, requestedDate, context) => loadCurveData(curve, requestedDate, context.apiClient),
91
+ // A curve that cannot be read is the copy an earlier read saved, marked so the report says so.
92
+ load: async (curve, requestedDate, context) => (await loadSavedCurve(curve, requestedDate, context.apiClient)).data,
91
93
  compare: (curve, date, context) => loadComparePoints(curve, date, context.apiClient),
92
94
  lookbacks: async (session, context) => Object.fromEntries(
93
95
  (await loadYieldCurveLookbacks(session, (id, options) => context.apiClient.getCloudFredSeries(id, options)))
94
96
  .map((lookback) => [lookback.id, lookback.points]),
95
97
  ),
96
- world: (context) => loadWorldRows(context.apiClient),
98
+ world: async (context) => (await loadSavedWorldRows(context.apiClient)).data,
97
99
  swaps: (tenor, context) => fetchSwapCurve(tenor, context.apiClient),
98
100
  };
99
101
 
@@ -184,7 +186,7 @@ export function createYieldCurveHeadless(
184
186
  return {
185
187
  freshness: { source: "Government bond publishers", status: "not-a-feed", basis: "daily curves" },
186
188
  columns: WORLD_COLUMNS,
187
- rows: rows.map((row) => ({ market: row.market, twoYear: row.twoYear, tenYear: row.tenYear, thirtyYear: row.thirtyYear,
189
+ rows: rows.map((row) => ({ ...savedCopyMark(row), market: row.market, twoYear: row.twoYear, tenYear: row.tenYear, thirtyYear: row.thirtyYear,
188
190
  twosTensBasisPoints: bp(row.twosTens), tenYearChangeBasisPoints: bp(row.tenYearChange), asOf: row.asOf })),
189
191
  metadata: { units: "Percent" },
190
192
  };
@@ -244,6 +246,7 @@ export function createYieldCurveHeadless(
244
246
  const day = ranked ? curveTenorMove(data, point.maturity, "1D") : null;
245
247
  const week = ranked ? curveTenorMove(data, point.maturity, "1W") : null;
246
248
  return {
249
+ ...savedCopyMark(data),
247
250
  ...point,
248
251
  change1dBasisPoints: change == null ? null : toBasisPoints(change, 1),
249
252
  ...(ranked ? {
@@ -8,6 +8,7 @@ import { curveStrip, curveSurfaceMinRows } from "../../../components/chart/curve
8
8
  import { curveLookbackLabel } from "../../../components/chart/curve/model";
9
9
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
10
10
  import { useAsyncResource } from "../../../react/async-resource";
11
+ import { isPermanentClientError } from "../../../api-client/errors";
11
12
  import { usePaneSettingValue } from "../../../state/app/context";
12
13
  import { usePluginPaneState } from "../../../public/react";
13
14
  import { priceColor } from "../../../theme/colors";
@@ -18,6 +19,7 @@ import type { PluginModule } from "../plugin-module";
18
19
  import { GOVERNMENT_BOND_SERIES, TREASURY_SERIES } from "./series";
19
20
  import { useAutoRefresh } from "../../../react/auto-refresh";
20
21
  import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
22
+ import { savedCopyFooterSegment, type PaneFooterSegment } from "../../../components/layout/pane/footer";
21
23
  import { formatPercentileRank } from "../../../utils/format";
22
24
  import { SERIES_COLORS } from "../../../theme/series-colors";
23
25
  import type { CurveSeries } from "../../../components/chart/curve";
@@ -27,20 +29,28 @@ import {
27
29
  type YieldTenorRow,
28
30
  } from "./chart";
29
31
  import {
30
- compareDate, curveCutoff, curveIdOf, CURVE_OPTIONS, curveSpreadFigures, curveTenorMove, loadComparePoints, loadCurveData,
32
+ compareDate, curveCutoff, curveIdOf, CURVE_OPTIONS, curveSpreadFigures, curveTenorMove, loadComparePoints,
31
33
  parseCompareInput, parseCurveArgument, type CurveId,
32
34
  } from "./curves";
33
35
  import { formatMoveRank, MOVE_HORIZONS } from "../shared/move-rank";
34
36
  import { forwardCurve, type CurveBasis } from "./forward";
35
37
  import { loadYieldCurveLookbacks, yieldCurveDate } from "./history";
36
38
  import { curveAsOf, isYieldObservationDate, type YieldPoint, yieldCurveErrors } from "./treasury-data";
37
- import { loadWorldRows, type WorldRow } from "./world";
39
+ import type { WorldRow } from "./world";
38
40
  import { SwapsTab } from "./swaps";
39
- import { swapCurveCache } from "./client";
41
+ import {
42
+ cachedCurve, cachedWorldRows, curveViewCache, loadSavedCurve, loadSavedWorldRows, swapCurveCache, worldCurvesCache,
43
+ } from "./client";
40
44
 
41
45
  type GcTab = "curve" | "world" | "swaps";
42
46
  const TABS = [{ label: "Curve", value: "curve" }, { label: "World", value: "world" }, { label: "Swaps", value: "swaps" }];
43
47
  const EMPTY_POINTS: YieldPoint[] = [];
48
+
49
+ /** A tab showing the copy an earlier read saved, its refresh having failed, says when that copy is from. */
50
+ function useSavedCopyInfo(savedAt: number | null | undefined): PaneFooterSegment[] | undefined {
51
+ return useMemo(() => savedAt != null ? [savedCopyFooterSegment("saved-copy", savedAt)] : undefined, [savedAt]);
52
+ }
53
+
44
54
  /** Axis word for the loaded curve's basis. */
45
55
  const BASIS_LABEL: Record<CurveBasis, string> = {
46
56
  par: "Par yield",
@@ -165,8 +175,14 @@ function CurveTab({ curve, onCurveChange, focused, width, height }: {
165
175
  const compareField = useDraftField(compareInput);
166
176
  const editing = dateField.active || compareField.active;
167
177
 
168
- const loadCurve = useCallback(async () => loadCurveData(curve, requestedDate), [curve, requestedDate]);
169
- const { data, loading, error, updatedAt: lastUpdated, load } = useAsyncResource(loadCurve);
178
+ // The saved copy paints first and answers when a refresh fails; a failed refresh never empties the tab, unless
179
+ // asking again cannot help (access denied), which is said instead.
180
+ const loadCurve = useCallback(async () => loadSavedCurve(curve, requestedDate), [curve, requestedDate]);
181
+ const { data: answer, loading, error, updatedAt: lastUpdated, load } = useAsyncResource(loadCurve, {
182
+ initialData: () => cachedCurve(curve, requestedDate), clearOnError: isPermanentClientError,
183
+ });
184
+ const data = answer?.data ?? null;
185
+ const savedInfo = useSavedCopyInfo(answer?.savedAt);
170
186
  // A pending curve or date change must never relabel the previous curve as the new one.
171
187
  const current = data?.curve === curve && data.requestedDate === requestedDate ? data : null;
172
188
  const points = current?.points ?? EMPTY_POINTS;
@@ -260,6 +276,7 @@ function CurveTab({ curve, onCurveChange, focused, width, height }: {
260
276
  registrationId: "yield-curve",
261
277
  loading: loading || (compareKey != null && compareResource.loading && !comparePoints),
262
278
  error: error || sourceError || null,
279
+ info: savedInfo,
263
280
  hints: [
264
281
  { id: "date", key: "d", label: "ate", onPress: dateField.focus },
265
282
  { id: "compare", key: "v", label: "s", title: "Compare with a date", onPress: compareField.focus },
@@ -413,11 +430,14 @@ function WorldTab({ onOpen, focused, width, height }: {
413
430
  }) {
414
431
  const colors = useThemeColors();
415
432
  const [selected, setSelected] = usePluginPaneState<string | null>("yield-curve:market", null);
416
- const loadWorld = useCallback(() => loadWorldRows(), []);
417
- const { data, loading, error, updatedAt, load } = useAsyncResource(loadWorld);
433
+ const loadWorld = useCallback(() => loadSavedWorldRows(), []);
434
+ const { data: answer, loading, error, updatedAt, load } = useAsyncResource(loadWorld, {
435
+ initialData: cachedWorldRows, clearOnError: isPermanentClientError,
436
+ });
437
+ const data = answer?.data ?? null;
418
438
  useAutoRefresh(updatedAt, load);
419
439
  usePaneRefreshKey(() => void load(), { focused });
420
- usePaneStatusFooter({ registrationId: "yield-curve:world", loading, error: error || null });
440
+ usePaneStatusFooter({ registrationId: "yield-curve:world", loading, error: error || null, info: useSavedCopyInfo(answer?.savedAt) });
421
441
  const rows = data ?? [];
422
442
  const selectedId = rows.some((row) => row.id === selected) ? selected! : rows[0]?.id ?? null;
423
443
  // Every market's latest curve, each in its own colour; the selected row is
@@ -465,8 +485,12 @@ function WorldTab({ onOpen, focused, width, height }: {
465
485
  }
466
486
 
467
487
  export const yieldCurveModule: PluginModule = {
468
- setup(ctx) { swapCurveCache.attach(ctx.persistence); },
469
- dispose() { swapCurveCache.reset(); },
488
+ setup(ctx) {
489
+ for (const cache of [swapCurveCache, curveViewCache, worldCurvesCache]) cache.attach(ctx.persistence);
490
+ },
491
+ dispose() {
492
+ for (const cache of [swapCurveCache, curveViewCache, worldCurvesCache]) cache.reset();
493
+ },
470
494
  series: [...TREASURY_SERIES, ...GOVERNMENT_BOND_SERIES],
471
495
  panes: [{
472
496
  id: "yield-curve",
@@ -7,8 +7,9 @@ import {
7
7
  import { curveStrip, curveSurfaceMinRows } from "../../../components/chart/curve";
8
8
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
9
9
  import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
10
+ import { savedCopyFooterSegment } from "../../../components/layout/pane/footer";
10
11
  import { FILL_REST } from "../../../components/ui/fill";
11
- import { isAccessDenied } from "../../../api-client/errors";
12
+ import { isPermanentClientError } from "../../../api-client/errors";
12
13
  import { usePluginPaneState } from "../../../public/react";
13
14
  import { useAsyncResource } from "../../../react/async-resource";
14
15
  import { useAutoRefresh } from "../../../react/auto-refresh";
@@ -37,17 +38,20 @@ export function SwapsTab({ focused, width, height }: { focused: boolean; width:
37
38
  const tenor = selectedSwapTenor(selected);
38
39
  const loader = useCallback((force: boolean) => loadSwapCurve(tenor, force), [tenor]);
39
40
  const resource = useAsyncResource(loader, {
40
- initialData: () => getCachedSwapCurve(tenor), keepPreviousData: true, clearOnError: isAccessDenied,
41
+ initialData: () => getCachedSwapCurve(tenor), keepPreviousData: true, clearOnError: isPermanentClientError,
41
42
  });
42
43
  const payload = resource.data?.payload;
43
44
  const rows = payload?.asOf ? payload.points : EMPTY_ROWS;
44
45
  // A new tenor keeps the curve/table in place, but never relabels the old tenor's history.
45
46
  const history = payload?.history.tenor === tenor ? payload.history : null;
46
- const stale = !!payload?.asOf && (payload.stale || resource.data?.stale || false);
47
- const error = resource.error || resource.data?.refreshError || null;
47
+ // A refresh that failed shows the copy an earlier read saved, and says when that copy is from.
48
+ const savedAt = resource.data?.savedAt ?? null;
49
+ const savedInfo = useMemo(() => savedAt != null ? [savedCopyFooterSegment("saved-copy", savedAt)] : undefined, [savedAt]);
50
+ const stale = !!payload?.asOf && (payload.stale || (savedAt == null && resource.data?.stale) || false);
51
+ const error = resource.error || (savedAt == null ? resource.data?.refreshError : null) || null;
48
52
  useAutoRefresh(stale ? null : resource.updatedAt, resource.load);
49
53
  usePaneRefreshKey(resource.reload, { focused });
50
- usePaneStatusFooter({ registrationId: "yield-curve:swaps", loading: resource.loading, error, stale });
54
+ usePaneStatusFooter({ registrationId: "yield-curve:swaps", loading: resource.loading, error, stale, info: savedInfo });
51
55
  usePaneNoticeFooter({
52
56
  registrationId: "yield-curve:swaps:notices", focused,
53
57
  notices: payload?.asOf ? [
@@ -45,6 +45,7 @@ import { commentLettersPlugin } from "./builtin/comment-letters";
45
45
  import { openFdaPlugin } from "./builtin/openfda";
46
46
  import { browserGloomberbCloudPlugin } from "./builtin/cloud/browser";
47
47
  import { changelogModule } from "./builtin/changelog";
48
+ import { roadmapModule } from "./builtin/roadmap";
48
49
  import { chartComposerModule } from "./builtin/chart-composer";
49
50
  import { connectionsModule } from "./builtin/connections";
50
51
  import { relativeRotationModule } from "./builtin/relative-rotation";
@@ -126,7 +127,7 @@ import { FIXED_INCOME_HOSTS } from "./builtin/fixed-income/model";
126
127
 
127
128
  const browserApplicationPlugin = composeBuiltinPlugin({
128
129
  ...applicationPluginMeta,
129
- modules: [layoutManagerModule, helpModule, changelogModule, connectionsModule, docsModule],
130
+ modules: [layoutManagerModule, helpModule, changelogModule, connectionsModule, docsModule, roadmapModule],
130
131
  });
131
132
 
132
133
  const runtimeOnly = (module: PluginModule): PluginModule => {
@@ -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,
@@ -32,7 +32,7 @@ const LINKED_PACKAGES = ["gloomberb", "react"] as const;
32
32
 
33
33
  /**
34
34
  * The names the host is published under: `gloomberb`, which plugins import
35
- * as, and `@gloom-sh/terminal` once releases are also published under it.
35
+ * as, and `@gloom-sh/terminal`.
36
36
  */
37
37
  const HOST_PACKAGE_NAMES: ReadonlySet<string> = new Set(["gloomberb", "@gloom-sh/terminal"]);
38
38
 
@@ -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",
@@ -185,15 +185,19 @@ function positionSideLabel(ticker: TickerRecord, activeTab: string | undefined):
185
185
  return "MIX";
186
186
  }
187
187
 
188
- /** The currency a row is listed in: its position's, else its quote's; an index's level is in points and has none. */
188
+ /**
189
+ * The currency a row is listed in: its position's, else its quote's or the one saved with it. An index's level is in
190
+ * points and has none, and a row whose currency no source gave (a coin pair before its quote) shows none, never the
191
+ * dollars it is valued in meanwhile: a row not held has no position currency, only that fallback.
192
+ */
189
193
  function listedCurrency(
190
- positionCurrency: string | undefined,
191
- quoteCurrency: string,
194
+ metrics: Pick<PortfolioPositionMetrics, "positionCount" | "positionCurrency">,
192
195
  ticker: TickerRecord,
193
196
  quote: TickerFinancials["quote"],
194
197
  ): string {
195
- if (!positionCurrency && isIndexLevel(quote?.instrumentType || ticker.metadata.assetCategory)) return "";
196
- return (positionCurrency || quoteCurrency || ticker.metadata.currency || "").toUpperCase();
198
+ const held = metrics.positionCount > 0 ? metrics.positionCurrency : "";
199
+ if (!held && isIndexLevel(quote?.instrumentType || ticker.metadata.assetCategory)) return "";
200
+ return (held || quote?.currency || ticker.metadata.currency || "").toUpperCase();
197
201
  }
198
202
 
199
203
  function compactText(value: string | null | undefined): string {
@@ -458,7 +462,7 @@ export function getColumnValue(
458
462
  ),
459
463
  };
460
464
  case "currency":
461
- return { text: compactText(currencyCodeLabel(listedCurrency(positionCurrency, quoteCurrency, ticker, quote))) };
465
+ return { text: compactText(currencyCodeLabel(listedCurrency(positionMetrics, ticker, quote))) };
462
466
  case "sector":
463
467
  return { text: compactText(ticker.metadata.sector || financials?.profile?.sector) };
464
468
  case "industry":
@@ -713,7 +717,7 @@ export function getSortValue(
713
717
  ?? ticker.metadata.exchange
714
718
  ?? null;
715
719
  case "currency":
716
- return listedCurrency(positionCurrency, quoteCurrency, ticker, quote) || null;
720
+ return listedCurrency(positionMetrics, ticker, quote) || null;
717
721
  case "sector":
718
722
  return ticker.metadata.sector || financials?.profile?.sector || null;
719
723
  case "industry":
@@ -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
  /**