gloomberb 0.16.3 → 0.16.4

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 (451) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/signed-in/adapter.ts +33 -2
  30. package/src/brokers/signed-in/client.ts +10 -0
  31. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  32. package/src/brokers/signed-in/sign-in.ts +62 -5
  33. package/src/capabilities/registry.ts +8 -1
  34. package/src/capabilities/types.ts +2 -0
  35. package/src/cli/commands/crypto-hints.ts +12 -4
  36. package/src/cli/commands/market.ts +359 -99
  37. package/src/cli/commands/options-chain.ts +172 -0
  38. package/src/cli/commands/options-leaps.ts +422 -0
  39. package/src/cli/commands/overview.ts +164 -26
  40. package/src/cli/commands/search.ts +29 -8
  41. package/src/cli/commands/system.ts +99 -19
  42. package/src/cli/commands/themes.ts +64 -0
  43. package/src/cli/commands/ticker.ts +62 -30
  44. package/src/cli/desktop-pane-shot.ts +97 -16
  45. package/src/cli/errors.ts +25 -2
  46. package/src/cli/freshness.ts +7 -4
  47. package/src/cli/fx-pair.ts +57 -0
  48. package/src/cli/help.ts +14 -1
  49. package/src/cli/index-roots.ts +51 -0
  50. package/src/cli/index.ts +9 -6
  51. package/src/cli/listing-arg.ts +211 -3
  52. package/src/cli/not-a-ticker.ts +37 -0
  53. package/src/cli/options.ts +50 -6
  54. package/src/cli/pane-functions/capabilities.ts +8 -2
  55. package/src/cli/pane-functions/catalog.ts +13 -2
  56. package/src/cli/pane-functions/data.ts +36 -5
  57. package/src/cli/pane-functions/dom.ts +21 -13
  58. package/src/cli/pane-functions/freshness.ts +205 -26
  59. package/src/cli/pane-functions/function-help.ts +67 -22
  60. package/src/cli/pane-functions/headless.ts +5 -4
  61. package/src/cli/pane-functions/index.ts +3 -0
  62. package/src/cli/pane-functions/market-session.ts +133 -0
  63. package/src/cli/pane-functions/options.ts +15 -4
  64. package/src/cli/pane-functions/report-notices.ts +63 -0
  65. package/src/cli/pane-functions/report.ts +4 -0
  66. package/src/cli/pane-functions/resolver.ts +4 -6
  67. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  68. package/src/cli/pane-functions/screenshot.ts +105 -23
  69. package/src/cli/registry.ts +2 -1
  70. package/src/cli/result.ts +50 -13
  71. package/src/cli/row-window.ts +76 -0
  72. package/src/cli/scroll-truncation.ts +34 -0
  73. package/src/components/chart/composite/composite-chart.tsx +43 -4
  74. package/src/components/chart/composite/legend.tsx +39 -6
  75. package/src/components/chart/composite/rasterizer.ts +64 -4
  76. package/src/components/chart/composite/series-cues.ts +71 -0
  77. package/src/components/chart/composite/text-renderer.ts +16 -8
  78. package/src/components/chart/composite/types.ts +3 -0
  79. package/src/components/command-bar/commands/direct/index.ts +9 -1
  80. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  81. package/src/components/command-bar/commands/registry.ts +7 -0
  82. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  83. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  84. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  85. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  86. package/src/components/command-bar/routes/root/results.ts +40 -10
  87. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  88. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  89. package/src/components/command-bar/surface/index.tsx +18 -4
  90. package/src/components/command-bar/theme-picker.tsx +48 -13
  91. package/src/components/command-bar/workflow/ops.ts +5 -3
  92. package/src/components/data-table/view.tsx +28 -9
  93. package/src/components/layout/floating-pane.tsx +6 -2
  94. package/src/components/layout/market-summary.ts +23 -9
  95. package/src/components/layout/pane/content.tsx +46 -29
  96. package/src/components/layout/pane/cycle-order.ts +2 -11
  97. package/src/components/layout/pane/header.tsx +2 -1
  98. package/src/components/layout/pane/title.ts +1 -1
  99. package/src/components/layout/presentation.ts +22 -0
  100. package/src/components/layout/shell/active-drag.ts +1 -1
  101. package/src/components/layout/shell/drag/index.ts +4 -0
  102. package/src/components/layout/shell/drag/live.ts +76 -5
  103. package/src/components/layout/shell/drag/runtime.ts +8 -8
  104. package/src/components/layout/shell/index.tsx +47 -6
  105. package/src/components/layout/shell/layout-state.ts +5 -11
  106. package/src/components/layout/shell/menu.ts +28 -1
  107. package/src/components/layout/shell/pane/actions.ts +2 -1
  108. package/src/components/layout/shell/pane/layers.tsx +51 -10
  109. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  110. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  111. package/src/components/layout/shell/shortcuts.ts +5 -2
  112. package/src/components/layout/shell/visible-layout.ts +44 -1
  113. package/src/components/layout/status-bar.tsx +4 -1
  114. package/src/components/markdown-text.tsx +9 -5
  115. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  116. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  117. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  118. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  119. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  120. package/src/components/onboarding/pro-step-layout.ts +86 -0
  121. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  122. package/src/components/ui/button.tsx +10 -0
  123. package/src/components/ui/data-table/index.tsx +38 -0
  124. package/src/components/ui/data-table/types.ts +8 -1
  125. package/src/components/ui/data-table/visible-range.ts +2 -0
  126. package/src/components/ui/icon.tsx +11 -2
  127. package/src/components/ui/index.ts +1 -1
  128. package/src/core/connection-health.ts +1 -1
  129. package/src/core/state/app/layout-reducer.ts +89 -3
  130. package/src/core/state/app/state.ts +8 -5
  131. package/src/core/state/app/types.ts +13 -2
  132. package/src/data/config/layout.ts +55 -21
  133. package/src/data/config/store/fold-panes.ts +149 -0
  134. package/src/data/config/store/migrations.ts +7 -0
  135. package/src/data/config/store/node.ts +27 -6
  136. package/src/data/config/store/normalize.ts +37 -3
  137. package/src/i18n/es.ts +14 -1
  138. package/src/i18n/ja.ts +14 -1
  139. package/src/i18n/ko.ts +14 -1
  140. package/src/i18n/zh-cn.ts +14 -1
  141. package/src/i18n/zh-tw.ts +14 -1
  142. package/src/layout/desks.ts +1 -2
  143. package/src/layout/pane-layout-move.ts +264 -0
  144. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  145. package/src/layout/pane-manager/index.ts +1 -0
  146. package/src/market-data/coordinator/entries.ts +7 -1
  147. package/src/market-data/coordinator/quotes.ts +2 -2
  148. package/src/market-data/market/freshness.ts +118 -18
  149. package/src/market-data/market/status.ts +40 -0
  150. package/src/market-data/options-alternatives.ts +69 -0
  151. package/src/market-data/quotes/contributions.ts +19 -2
  152. package/src/market-data/quotes/metadata.ts +16 -0
  153. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  154. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  155. package/src/plugins/builtin/account-management/model.ts +1 -0
  156. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  157. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  158. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  159. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  160. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  161. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  162. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  163. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  164. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  165. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  166. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  167. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  168. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  169. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  170. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  171. package/src/plugins/builtin/chat/channels.ts +34 -36
  172. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  173. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  174. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  175. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  176. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  177. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  178. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  179. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  180. package/src/plugins/builtin/chat/conversations.ts +256 -0
  181. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  182. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  183. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  184. package/src/plugins/builtin/chat/index.tsx +40 -2
  185. package/src/plugins/builtin/chat/layout.ts +2 -0
  186. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  187. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  188. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  189. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  190. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  191. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  192. package/src/plugins/builtin/chat/pane.tsx +8 -0
  193. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  194. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  195. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  196. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  197. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  198. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  199. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  200. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  201. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  202. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  203. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  204. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  205. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  206. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  207. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  208. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  209. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  210. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  211. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  212. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  213. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  214. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  215. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  216. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  217. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  218. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  219. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  220. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  221. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  222. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  223. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  224. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  225. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  226. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  227. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  228. package/src/plugins/builtin/composite-plugins.ts +107 -64
  229. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  230. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  231. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  232. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  233. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  234. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  235. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  236. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  237. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  238. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  239. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  240. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  241. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  242. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  243. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  244. package/src/plugins/builtin/econ/index.tsx +68 -22
  245. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  246. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  247. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  248. package/src/plugins/builtin/executives/data.ts +17 -6
  249. package/src/plugins/builtin/executives/pane.tsx +16 -7
  250. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  251. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  254. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  255. package/src/plugins/builtin/gpu/headless.ts +9 -3
  256. package/src/plugins/builtin/gpu/model.ts +14 -5
  257. package/src/plugins/builtin/help/index.tsx +1 -0
  258. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  259. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  260. package/src/plugins/builtin/holders/headless.ts +6 -1
  261. package/src/plugins/builtin/holders/pane.tsx +3 -0
  262. package/src/plugins/builtin/insider/index.tsx +4 -4
  263. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  264. package/src/plugins/builtin/iv-history/client.ts +12 -2
  265. package/src/plugins/builtin/iv-history/format.ts +33 -2
  266. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  267. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  268. package/src/plugins/builtin/iv-history/model.ts +61 -13
  269. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  270. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  271. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  272. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  273. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  274. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  275. package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
  276. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  277. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  278. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  279. package/src/plugins/builtin/macro-day/client.ts +24 -0
  280. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  281. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  282. package/src/plugins/builtin/macro-day/model.ts +11 -6
  283. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  284. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  285. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  286. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  287. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  288. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  289. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  290. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  291. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  292. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  293. package/src/plugins/builtin/market-movers/model.ts +71 -17
  294. package/src/plugins/builtin/market-movers/table.ts +50 -23
  295. package/src/plugins/builtin/news/headless.ts +9 -2
  296. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  297. package/src/plugins/builtin/openfda/client.ts +1 -1
  298. package/src/plugins/builtin/options/carry.ts +95 -0
  299. package/src/plugins/builtin/options/index.tsx +58 -5
  300. package/src/plugins/builtin/options/strike-window.ts +183 -0
  301. package/src/plugins/builtin/options/table.ts +55 -3
  302. package/src/plugins/builtin/options/types.ts +8 -1
  303. package/src/plugins/builtin/options/view.tsx +161 -12
  304. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  305. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  306. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  307. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  308. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  309. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  310. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  311. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  312. package/src/plugins/builtin/pe-band/model.ts +2 -2
  313. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  314. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  315. package/src/plugins/builtin/perps/model.ts +16 -5
  316. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  317. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  318. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  319. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  320. package/src/plugins/builtin/plugin-module.ts +87 -4
  321. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  322. package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
  323. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  324. package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
  325. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  326. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  327. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  328. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  329. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  330. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  331. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  332. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  333. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  334. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  335. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  336. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  337. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  338. package/src/plugins/builtin/sec/index.tsx +5 -5
  339. package/src/plugins/builtin/sectors/client.ts +10 -2
  340. package/src/plugins/builtin/sectors/headless.ts +35 -6
  341. package/src/plugins/builtin/sectors/index.tsx +18 -1
  342. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  343. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  344. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  345. package/src/plugins/builtin/shared/members-menu.tsx +4 -4
  346. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  347. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  348. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  349. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  350. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  351. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  352. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  353. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  354. package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
  355. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  356. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  357. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  358. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  359. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  360. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  361. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  362. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  363. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  364. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  365. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  366. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  367. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  368. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  369. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  370. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  371. package/src/plugins/builtin/world-indices/client.ts +22 -4
  372. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  373. package/src/plugins/builtin/world-indices/model.ts +4 -2
  374. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  375. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  376. package/src/plugins/builtin/world-venue-map/index.ts +1 -1
  377. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  378. package/src/plugins/builtin-editorial.ts +126 -0
  379. package/src/plugins/catalog-backend.ts +16 -9
  380. package/src/plugins/catalog-browser.ts +127 -63
  381. package/src/plugins/catalog-manifest.ts +73 -40
  382. package/src/plugins/catalog-ui.ts +26 -6
  383. package/src/plugins/extracted-plugins.ts +7 -1
  384. package/src/plugins/ownership.ts +150 -7
  385. package/src/plugins/plugin-functions.ts +54 -0
  386. package/src/plugins/plugin-off.ts +36 -0
  387. package/src/plugins/registry/host-actions.ts +16 -0
  388. package/src/plugins/registry/index.ts +84 -24
  389. package/src/plugins/registry/plugin-state.ts +25 -1
  390. package/src/plugins/runtime/context.tsx +29 -0
  391. package/src/react/input.ts +48 -1
  392. package/src/remote/app-host.tsx +5 -0
  393. package/src/remote/controller.ts +3 -53
  394. package/src/remote/matching.ts +69 -0
  395. package/src/remote/schema.ts +1 -1
  396. package/src/remote/semantic-tree.tsx +14 -0
  397. package/src/remote/types.ts +2 -0
  398. package/src/renderers/dom/desktop/icons.tsx +14 -0
  399. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  400. package/src/renderers/dom/host/box.tsx +1 -1
  401. package/src/renderers/dom/host/drag-shield.ts +17 -6
  402. package/src/renderers/dom/host/live-frame.ts +10 -2
  403. package/src/renderers/dom/input-host.tsx +6 -1
  404. package/src/renderers/dom/styles.css +13 -0
  405. package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
  406. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  407. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  408. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  409. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  410. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  411. package/src/renderers/electrobun/view/main.tsx +4 -1
  412. package/src/renderers/opentui/input-host.tsx +6 -1
  413. package/src/renderers/opentui/start.tsx +9 -0
  414. package/src/shares/portable-layout.ts +16 -6
  415. package/src/sources/gloomberb-cloud/index.ts +19 -10
  416. package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
  417. package/src/sources/listing-symbols.ts +11 -5
  418. package/src/sources/provider-errors.ts +88 -3
  419. package/src/sources/provider-router/cached-routes.ts +8 -4
  420. package/src/sources/provider-router/financial-routes.ts +11 -5
  421. package/src/sources/provider-router/financials.ts +42 -15
  422. package/src/sources/provider-router/history.ts +27 -11
  423. package/src/sources/provider-router/index.ts +1 -1
  424. package/src/sources/provider-router/primary.ts +23 -5
  425. package/src/sources/sec-edgar.ts +68 -8
  426. package/src/sync/core-contributors.ts +4 -3
  427. package/src/test-support/cli-context.ts +2 -1
  428. package/src/test-support/data-provider.ts +34 -0
  429. package/src/test-support/shell-pane-keys.tsx +1 -0
  430. package/src/theme/series-colors.ts +46 -0
  431. package/src/theme/themes.ts +149 -0
  432. package/src/tickers/search/index.ts +22 -2
  433. package/src/tickers/search/ranking.ts +45 -1
  434. package/src/tickers/search/types.ts +3 -1
  435. package/src/time-series/resolve.ts +1 -14
  436. package/src/types/config.ts +61 -1
  437. package/src/types/headless.ts +11 -0
  438. package/src/types/instrument.ts +8 -0
  439. package/src/types/plugin.ts +49 -3
  440. package/src/ui/host.tsx +8 -4
  441. package/src/ui/index.tsx +2 -0
  442. package/src/utils/cli-output.ts +14 -1
  443. package/src/utils/exchanges.ts +13 -0
  444. package/src/utils/format.ts +2 -1
  445. package/src/utils/fx-market-hours.ts +34 -0
  446. package/src/utils/option-expiry.ts +126 -0
  447. package/src/utils/price-history.ts +39 -6
  448. package/src/utils/rpc-timeout-error.ts +6 -2
  449. package/src/utils/sec-eps-basis.ts +159 -30
  450. package/src/utils/sec.ts +38 -34
  451. package/src/utils/throttled-fetch.ts +51 -22
@@ -9,6 +9,7 @@ import { cleanTickerInput } from "./options";
9
9
  import { parsePublicTickerKey, publicTickerKey } from "../../utils/exchanges";
10
10
  import type { ResolvedPaneFunction } from "./resolver";
11
11
  import { toMarketDataContext } from "../../market-data/selectors";
12
+ import { quoteMetadataFromQuote } from "../../market-data/quotes/metadata";
12
13
  import { loadSeasonalityHistory, SEASONALITY_HISTORY_RESOLUTION } from "../../plugins/builtin/seasonality/client";
13
14
  import type { InstrumentRef } from "../../market-data/request-types";
14
15
  import { CORRELATION_HISTORY_RESOLUTION, loadCorrelationHistory } from "../../plugins/builtin/correlation/history";
@@ -22,6 +23,26 @@ const SHOT_PRICE_HISTORY_RANGE = "5Y" as const;
22
23
  const FINANCIAL_ANALYSIS_PANE_ID = "financial-analysis";
23
24
  const FINANCIAL_ANALYSIS_TEMPLATE_ID = "financial-analysis-pane";
24
25
 
26
+ async function loadStoredShotTicker(context: MarketContext, symbol: string): Promise<TickerRecord | null> {
27
+ const normalized = cleanTickerInput(symbol);
28
+ const { symbol: bare } = parsePublicTickerKey(normalized);
29
+ return await context.store.loadTicker(normalized)
30
+ ?? (bare !== normalized ? await context.store.loadTicker(bare) : null);
31
+ }
32
+
33
+ /**
34
+ * A holding the pane can value without a quote: some position carries the
35
+ * broker's own mark or market value. An option contract no quote covers stays
36
+ * at that mark in the pane, so a failed quote lookup for it is not an error.
37
+ */
38
+ export async function loadBrokerMarkedHolding(context: MarketContext, symbol: string): Promise<TickerRecord | null> {
39
+ const ticker = await loadStoredShotTicker(context, symbol);
40
+ const marked = ticker?.metadata.positions.some(({ shares, markPrice, marketValue }) => (
41
+ shares !== 0 && (Number.isFinite(markPrice) || Number.isFinite(marketValue))
42
+ ));
43
+ return marked ? ticker : null;
44
+ }
45
+
25
46
  export async function fetchTickerFinancials(
26
47
  context: MarketContext,
27
48
  symbol: string,
@@ -32,8 +53,7 @@ export async function fetchTickerFinancials(
32
53
  }> {
33
54
  const normalized = cleanTickerInput(symbol);
34
55
  const instrument = parsePublicTickerKey(normalized);
35
- const tickerFile = await context.store.loadTicker(normalized)
36
- ?? (instrument.symbol !== normalized ? await context.store.loadTicker(instrument.symbol) : null);
56
+ const tickerFile = await loadStoredShotTicker(context, symbol);
37
57
  const exchange = instrument.exchange ?? tickerFile?.metadata.exchange ?? "";
38
58
  const financials = await context.dataProvider.getTickerFinancials(instrument.symbol, exchange);
39
59
  return { tickerFile, financials, instrument: { symbol: instrument.symbol, ...(exchange ? { exchange } : {}) } };
@@ -142,14 +162,25 @@ export function isFinancialAnalysisFunction(resolved: ResolvedPaneFunction): boo
142
162
  && resolved.instance.settings?.lockedTabId === "financials";
143
163
  }
144
164
 
145
- export function createFallbackTicker(symbol: string, financials: TickerFinancials | null, context: MarketContext): TickerRecord {
165
+ /**
166
+ * The ticker a shot shows for a symbol that is not saved. Without a usable
167
+ * quote (a thin US listing after the close), the listing comes from the quote
168
+ * metadata kept when the quote was dropped, or else from asking the source
169
+ * for it, so the pane still knows a US stock from a listing abroad. What no
170
+ * source knows stays empty rather than guessed from the base currency.
171
+ */
172
+ export async function createFallbackTicker(symbol: string, financials: TickerFinancials | null, context: MarketContext): Promise<TickerRecord> {
146
173
  const instrument = parsePublicTickerKey(symbol);
147
174
  const quote = financials?.quote;
175
+ const known = quote ? quoteMetadataFromQuote(quote) : financials?.quoteMetadata;
176
+ const listing = instrument.exchange || known?.listingExchangeName
177
+ ? known
178
+ : await context.dataProvider.getQuoteMetadata?.(instrument.symbol, "").catch(() => null) ?? known;
148
179
  return {
149
180
  metadata: {
150
181
  ticker: instrument.symbol,
151
- exchange: instrument.exchange ?? quote?.listingExchangeName ?? quote?.exchangeName ?? "",
152
- currency: quote?.currency ?? context.config.baseCurrency,
182
+ exchange: instrument.exchange ?? listing?.listingExchangeName ?? "",
183
+ currency: listing?.currency ?? "",
153
184
  name: quote?.name ?? instrument.symbol,
154
185
  portfolios: [],
155
186
  watchlists: [],
@@ -8,6 +8,7 @@ import { collectShotSymbols } from "./data";
8
8
  import { renderDesktopShot, type PaneScreenshotResult } from "./screenshot";
9
9
  import { deriveRenderedFreshness, formatFreshnessLine, type ReportFreshness } from "./freshness";
10
10
  import { exportTextTable, reportFooterLines, type CliReportTables } from "../report-tables";
11
+ import { renderedReportFreshness, renderedReportNotices, renderedReportObservations } from "./report-notices";
11
12
 
12
13
  const DOM_REPORT_WIDTH = 1280;
13
14
  const DOM_REPORT_HEIGHT = 720;
@@ -18,12 +19,6 @@ function rowsContainEllipsis(rows: DesktopPaneShotRenderedRow[]): boolean {
18
19
  return rows.some((row) => row.cells.some((cell) => /\u2026|\.\.\./.test(cell.text)));
19
20
  }
20
21
 
21
- function isDomReportTruncated(
22
- render: Pick<PaneScreenshotResult["render"], "rows" | "truncated">,
23
- ): boolean {
24
- return render.truncated || rowsContainEllipsis(render.rows);
25
- }
26
-
27
22
  /** Column identity per cell; a repeated label within one row stays distinct. */
28
23
  function domCellKeys(row: DesktopPaneShotRenderedRow): string[] {
29
24
  const seen = new Map<string, number>();
@@ -109,6 +104,7 @@ function domReportTables(
109
104
  title: string,
110
105
  freshness: ReportFreshness,
111
106
  incomplete: boolean | string,
107
+ notices: readonly string[],
112
108
  ): CliReportTables {
113
109
  const tables = domTables(rows);
114
110
  return {
@@ -117,7 +113,7 @@ function domReportTables(
117
113
  table.columns.map((column) => column.header),
118
114
  table.rows.map((cells) => table.columns.map((column) => cells.get(column.key))),
119
115
  )),
120
- footer: reportFooterLines({ freshness, incomplete }),
116
+ footer: reportFooterLines({ freshness, incomplete, notes: notices }),
121
117
  };
122
118
  }
123
119
 
@@ -138,8 +134,10 @@ function renderedFailureReason(
138
134
  return null;
139
135
  }
140
136
 
137
+ /** The rows a report reads: every row of a table that publishes them all, else what the viewport shows. */
141
138
  function reportRows(result: PaneScreenshotResult): DesktopPaneShotRenderedRow[] {
142
- if (result.render.rows.length > 0) return result.render.rows;
139
+ const rows = result.render.reportRows ?? result.render.rows;
140
+ if (rows.length > 0) return rows;
143
141
  if (
144
142
  result.render.loadingStateDetected
145
143
  || result.render.errorStateDetected
@@ -161,15 +159,20 @@ export function buildDomPaneReportFromRender(
161
159
  result: PaneScreenshotResult,
162
160
  ): PaneFunctionReport {
163
161
  const rows = reportRows(result);
164
- const hasStructuredRows = result.render.rows.length > 0;
165
- const truncated = isDomReportTruncated({ ...result.render, rows });
166
- const truncationReasons = [...result.render.truncationReasons];
162
+ const hasStructuredRows = (result.render.reportRows ?? result.render.rows).length > 0;
163
+ // A table that reported every row is whole, whatever the viewport cut from the capture.
164
+ const truncationReasons = [...(result.render.reportTruncationReasons ?? result.render.truncationReasons)];
167
165
  if (rowsContainEllipsis(rows) && !truncationReasons.includes("one or more cells are visibly clipped")) {
168
166
  truncationReasons.push("one or more cells are visibly clipped");
169
167
  }
168
+ const truncated = truncationReasons.length > 0
169
+ || (result.render.reportTruncationReasons ? false : result.render.truncated);
170
170
  const failureReason = renderedFailureReason(result, rows);
171
171
  const unavailableSymbols = failureReason && result.symbols.length > 0 ? result.symbols : [];
172
- const textLines = [resolved.label, ""];
172
+ // What the view leaves out and how to see more: FXC's other currencies, OMON's strikes outside its window.
173
+ const rendered = renderedReportNotices(result.render.semanticUi ?? []);
174
+ const notices = [...(resolved.pane.reportNotices?.(resolved.instance.settings ?? {}) ?? []), ...rendered.notices];
175
+ const textLines = [resolved.label, "", ...notices, ...(notices.length > 0 ? [""] : [])];
173
176
  if (hasStructuredRows) {
174
177
  textLines.push(...renderDomTables(rows));
175
178
  } else if (result.render.visibleText) {
@@ -178,9 +181,12 @@ export function buildDomPaneReportFromRender(
178
181
  textLines.push(failureReason ?? "No rendered values were available.");
179
182
  }
180
183
  if (failureReason && rows.length > 0) textLines.push("", failureReason);
181
- const freshness = deriveRenderedFreshness(resolved.pane.reportFreshness, {
184
+ // A pane that knows its feed dates the report; the footer and cell times fill in the rest.
185
+ const declared = { ...resolved.pane.reportFreshness, ...renderedReportFreshness(result.render.semanticUi ?? []) };
186
+ const freshness = deriveRenderedFreshness(declared, {
182
187
  footerText: result.render.footerText ?? "",
183
188
  cellTimes: rows.flatMap((row) => row.cells.flatMap((cell) => cell.instant ?? [])),
189
+ observed: renderedReportObservations(result.render.semanticUi ?? []),
184
190
  });
185
191
 
186
192
  const complete = failureReason === null && !truncated;
@@ -201,6 +207,7 @@ export function buildDomPaneReportFromRender(
201
207
  truncationReasons,
202
208
  limitation: DOM_LIMITATION,
203
209
  ...(failureReason ? { reason: failureReason } : {}),
210
+ ...(notices.length > 0 ? { metadata: { ...rendered.facts, notices } } : {}),
204
211
  freshness,
205
212
  },
206
213
  text: textLines.join("\n").trimEnd(),
@@ -209,6 +216,7 @@ export function buildDomPaneReportFromRender(
209
216
  resolved.label,
210
217
  freshness,
211
218
  !complete && (failureReason ?? (truncationReasons.length > 0 ? truncationReasons.join("; ") : true)),
219
+ notices,
212
220
  ),
213
221
  };
214
222
  }
@@ -8,9 +8,10 @@ import type {
8
8
  HeadlessSeriesResult,
9
9
  HeadlessSnapshotResult,
10
10
  } from "../../types/plugin";
11
- import { formatUtcTime, parseReportTime, type ReportTime } from "../../utils/utc-time";
11
+ import { parseReportTime, type ReportTime } from "../../utils/utc-time";
12
12
  import { isRecord } from "../../utils/guards";
13
13
  import { latestRegularSessionClose } from "../../market-data/market/freshness";
14
+ import { reportMarketSession, type ReportMarket, type SessionObservation } from "./market-session";
14
15
 
15
16
  /**
16
17
  * The four states of docs/usage.md#how-current-a-report-is, plus `unreported`
@@ -27,7 +28,10 @@ export interface ReportFreshness {
27
28
  /** Oldest observation, when it is more than a day older than `asOf`. */
28
29
  oldest?: string;
29
30
  status: ReportFreshnessStatus;
31
+ /** How far the data is held back; the most of its rows when they differ... */
30
32
  delayMinutes?: number;
33
+ /** ...and the least, only when it is less. */
34
+ minDelayMinutes?: number;
31
35
  /** How old the stale data is: the newest observation, or the oldest stale row when only some are. */
32
36
  ageMinutes?: number;
33
37
  /** When only some rows are stale: how many, of the rows that say whether they are... */
@@ -36,6 +40,10 @@ export interface ReportFreshness {
36
40
  /** ...and what the rest are. */
37
41
  feed?: "live" | "delayed";
38
42
  basis?: string;
43
+ /** The local date of the session close `asOf` is, when the newest observation is its venue's latest close. */
44
+ asOfClose?: string;
45
+ /** Where the venues of a quote report stand now, from their session calendars. */
46
+ market?: ReportMarket;
39
47
  /** When this report was built. */
40
48
  retrievedAt: string;
41
49
  }
@@ -59,11 +67,14 @@ interface FeedSignals {
59
67
  live: boolean;
60
68
  delayed: boolean;
61
69
  delayMinutes?: number;
70
+ minDelayMinutes?: number;
62
71
  /** A record-wide stale flag (metadata), read only when no row says whether it is stale. */
63
72
  stale: boolean;
64
73
  /** Rows and series that carry a `stale` flag, and the times of those that are stale. */
65
74
  flagged: number;
66
75
  staleTimes: Array<ReportTime | null>;
76
+ /** Rows that name the venue whose sessions they follow (`sessionExchange`), with their time. */
77
+ sessions: SessionObservation[];
67
78
  }
68
79
 
69
80
  interface Observations {
@@ -74,7 +85,7 @@ interface Observations {
74
85
  }
75
86
 
76
87
  function emptySignals(): FeedSignals {
77
- return { live: false, delayed: false, stale: false, flagged: 0, staleTimes: [] };
88
+ return { live: false, delayed: false, stale: false, flagged: 0, staleTimes: [], sessions: [] };
78
89
  }
79
90
 
80
91
  function readFeedSignals(record: Record<string, unknown>, signals: FeedSignals): void {
@@ -84,9 +95,21 @@ function readFeedSignals(record: Record<string, unknown>, signals: FeedSignals):
84
95
  if (typeof delay === "number" && Number.isFinite(delay) && delay > 0) {
85
96
  signals.delayed = true;
86
97
  signals.delayMinutes = Math.max(signals.delayMinutes ?? 0, delay);
98
+ signals.minDelayMinutes = Math.min(signals.minDelayMinutes ?? delay, delay);
87
99
  }
88
100
  }
89
101
 
102
+ /** A quote row's venue and reported session, so the report can say whether its market is open. */
103
+ function readSession(record: Record<string, unknown>, time: ReportTime | null, signals: FeedSignals): void {
104
+ const exchange = record.sessionExchange;
105
+ if (typeof exchange !== "string" || !exchange.trim()) return;
106
+ signals.sessions.push({
107
+ exchange,
108
+ ...(typeof record.marketState === "string" ? { marketState: record.marketState } : {}),
109
+ time,
110
+ });
111
+ }
112
+
90
113
  /** A row's or series' own stale flag; worst of across them, counted so a partly stale board says how much. */
91
114
  function readUnitStale(record: Record<string, unknown>, time: ReportTime | null, signals: FeedSignals): void {
92
115
  if (typeof record.stale !== "boolean") return;
@@ -147,6 +170,7 @@ function collect(
147
170
  const time = observedTime(row, now, rowKeys)
148
171
  ?? (observedKey ? null : observedTime(row, now, UPDATE_KEYS));
149
172
  if (!ignoreStaleFlags) readUnitStale(row, time, signals);
173
+ readSession(row, time, signals);
150
174
  if (time) observations.units.push(time);
151
175
  }
152
176
  if (definition.shape === "series") {
@@ -247,6 +271,7 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
247
271
 
248
272
  let status: ReportFreshnessStatus;
249
273
  let delayMinutes: number | undefined;
274
+ let minDelayMinutes: number | undefined;
250
275
  let staleAt: ReportTime | null = null;
251
276
  let partial: { staleCount: number; observationCount: number; feed?: "live" | "delayed" } | null = null;
252
277
  if (declared.status === "not-a-feed") {
@@ -288,8 +313,16 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
288
313
  status = feed ?? "unreported";
289
314
  }
290
315
  // A partly stale board still says how far its fresh rows are held back.
291
- if (status === "delayed" || partial?.feed === "delayed") delayMinutes = declared.delayMinutes ?? signals.delayMinutes;
316
+ if (status === "delayed" || partial?.feed === "delayed") {
317
+ delayMinutes = declared.delayMinutes ?? signals.delayMinutes;
318
+ if (declared.delayMinutes == null && signals.minDelayMinutes != null && delayMinutes != null && signals.minDelayMinutes < delayMinutes) {
319
+ minDelayMinutes = signals.minDelayMinutes;
320
+ }
321
+ }
292
322
  }
323
+ const { market, closeDate } = reportMarketSession(signals.sessions, now);
324
+ // The close wording dates the newest observation, so only when that is what `asOf` is.
325
+ const asOfClose = closeDate && asOf && !declaredAsOf ? closeDate : null;
293
326
 
294
327
  return {
295
328
  source: declared.source?.trim() || DEFAULT_REPORT_SOURCE,
@@ -297,9 +330,12 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
297
330
  ...(asOf && oldestTime && asOf.time - oldestTime.time > OLDEST_GAP_MS ? { oldest: isoTime(oldestTime) } : {}),
298
331
  status,
299
332
  ...(delayMinutes != null && delayMinutes > 0 ? { delayMinutes } : {}),
333
+ ...(minDelayMinutes != null && minDelayMinutes > 0 ? { minDelayMinutes } : {}),
300
334
  ...(status === "stale" && staleAt ? { ageMinutes: Math.max(0, Math.floor((now - staleAt.time) / 60_000)) } : {}),
301
335
  ...(partial ?? {}),
302
336
  ...(status === "not-a-feed" && declared.basis ? { basis: declared.basis } : {}),
337
+ ...(asOfClose ? { asOfClose } : {}),
338
+ ...(market ? { market } : {}),
303
339
  retrievedAt: new Date(now).toISOString(),
304
340
  };
305
341
  }
@@ -322,14 +358,20 @@ export function deriveHeadlessFreshness(
322
358
  /**
323
359
  * A rendered view has no structured data: the source is the pane's
324
360
  * declaration, the times are the cells that carry an instant, and the state
325
- * is what the pane's own footer says, or not reported.
361
+ * is what the pane's own footer says, or not reported. A pane that dates its
362
+ * figures itself publishes `observed` records, read as a headless report's
363
+ * rows are: quote or rate times, feed, delay, stale flags and venue sessions.
326
364
  */
327
365
  export function deriveRenderedFreshness(
328
366
  declared: HeadlessPaneFreshness | undefined,
329
- input: { footerText: string; cellTimes: unknown[] },
367
+ input: { footerText: string; cellTimes: unknown[]; observed?: readonly Record<string, unknown>[] },
330
368
  now = Date.now(),
331
369
  ): ReportFreshness {
332
- const signals = emptySignals();
370
+ const merged = mergeDeclarations(declared);
371
+ const observed = input.observed?.length
372
+ ? collect({ shape: "rows" }, { rows: input.observed as HeadlessPaneRow[] }, now, merged)
373
+ : null;
374
+ const signals = observed?.signals ?? emptySignals();
333
375
  const footer = input.footerText;
334
376
  const delayed = /\b(\d+)\s*(?:m|min|minutes?)\s+delayed\b/i.exec(footer);
335
377
  if (delayed) {
@@ -344,14 +386,62 @@ export function deriveRenderedFreshness(
344
386
  .map((value) => parseReportTime(value))
345
387
  .filter((time): time is ReportTime => time != null && time.time <= now + FUTURE_TOLERANCE_MS);
346
388
  return resolveFreshness({
347
- declared: mergeDeclarations(declared),
389
+ declared: merged,
348
390
  signals,
349
391
  // Rendered rows are often a history (a news list, a filing feed), so only the newest is cited.
350
- observations: { units: [], extra: units },
392
+ observations: { units: observed?.observations.units ?? [], extra: [...(observed?.observations.extra ?? []), ...units] },
351
393
  now,
352
394
  });
353
395
  }
354
396
 
397
+ const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
398
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
399
+ const MINUTES_PER_DAY = 24 * 60;
400
+ const DAY_MS = 24 * 60 * 60_000;
401
+ /** A reopen further ahead than this reads with its date, not just its weekday. */
402
+ const WEEKDAY_ONLY_MS = 6 * DAY_MS;
403
+
404
+ /** `Fri 9 Oct` for a `YYYY-MM-DD` date, with the year when it is not the year of `now`. */
405
+ function dayText(date: string, now: number): string {
406
+ const day = new Date(`${date.slice(0, 10)}T00:00:00Z`);
407
+ if (!Number.isFinite(day.getTime())) return date;
408
+ const year = day.getUTCFullYear() === new Date(now).getUTCFullYear() ? "" : ` ${day.getUTCFullYear()}`;
409
+ return `${WEEKDAYS[day.getUTCDay()]} ${day.getUTCDate()} ${MONTHS[day.getUTCMonth()]}${year}`;
410
+ }
411
+
412
+ /** `Fri 9 Oct 23:59 UTC`, or `Fri 9 Oct` for a date. */
413
+ function timeText(value: string, now: number): string {
414
+ const time = parseReportTime(value);
415
+ if (!time) return value;
416
+ const iso = new Date(time.time).toISOString();
417
+ return time.dateOnly ? dayText(iso, now) : `${dayText(iso, now)} ${iso.slice(11, 16)} UTC`;
418
+ }
419
+
420
+ /**
421
+ * How long a delay is, in the unit it is stated in: `15 min`, `12 h`, `3 days`.
422
+ * A delay under two hours that is not a whole hour stays in minutes.
423
+ */
424
+ export function delayLength(minutes: number): string {
425
+ const whole = Math.max(0, Math.round(minutes));
426
+ if (whole >= 2 * MINUTES_PER_DAY && whole % MINUTES_PER_DAY === 0) return `${whole / MINUTES_PER_DAY} days`;
427
+ if (whole >= 60 && whole % 60 === 0) return `${whole / 60} h`;
428
+ if (whole < 120) return `${whole} min`;
429
+ return `${Math.round(whole / 60)} h`;
430
+ }
431
+
432
+ /** `15 min delayed`, `15-20 min delayed` across venues, or `delayed` when the feed does not say. */
433
+ function delayedText(freshness: Pick<ReportFreshness, "delayMinutes" | "minDelayMinutes">): string {
434
+ const most = freshness.delayMinutes;
435
+ if (!most) return "delayed";
436
+ const least = freshness.minDelayMinutes;
437
+ if (least && least < most) {
438
+ const [low, lowUnit] = delayLength(least).split(" ");
439
+ const [high, highUnit] = delayLength(most).split(" ");
440
+ return lowUnit === highUnit ? `${low}-${high} ${highUnit} delayed` : `up to ${delayLength(most)} delayed`;
441
+ }
442
+ return `${delayLength(most)} delayed`;
443
+ }
444
+
355
445
  function ageText(minutes: number): string {
356
446
  if (minutes < 120) return `${minutes} min`;
357
447
  const hours = Math.floor(minutes / 60);
@@ -359,27 +449,25 @@ function ageText(minutes: number): string {
359
449
  return `${Math.floor(hours / 24)} days`;
360
450
  }
361
451
 
362
- function delayedText(minutes: number | undefined): string {
363
- return minutes ? `Delayed ${minutes} min` : "Delayed";
364
- }
365
-
366
- function statusText(freshness: ReportFreshness): string {
452
+ /** `short` counts stale rows without their total (`2 stale`), for a narrow capture. */
453
+ function statusText(freshness: ReportFreshness, short = false): string {
367
454
  switch (freshness.status) {
368
455
  case "live":
369
- return "Live";
456
+ return "live";
370
457
  case "delayed":
371
- return delayedText(freshness.delayMinutes);
458
+ return delayedText(freshness);
372
459
  case "stale": {
373
460
  if (freshness.staleCount != null && freshness.observationCount != null) {
374
- const rest = freshness.feed === "live" ? "Live" : freshness.feed === "delayed" ? delayedText(freshness.delayMinutes) : null;
375
- return `${rest ? `${rest}, ` : ""}${freshness.staleCount} of ${freshness.observationCount} stale`;
461
+ const rest = freshness.feed === "live" ? "live" : freshness.feed === "delayed" ? delayedText(freshness) : null;
462
+ const count = short ? `${freshness.staleCount}` : `${freshness.staleCount} of ${freshness.observationCount}`;
463
+ return `${rest ? `${rest}, ` : ""}${count} stale`;
376
464
  }
377
- return freshness.ageMinutes != null ? `Stale (${ageText(freshness.ageMinutes)} old)` : "Stale";
465
+ return freshness.ageMinutes != null ? `stale (${ageText(freshness.ageMinutes)} old)` : "stale";
378
466
  }
379
467
  case "not-a-feed":
380
- return freshness.basis ? `Not a live feed (${freshness.basis})` : "Not a live feed";
468
+ return freshness.basis ? `not a live feed (${freshness.basis})` : "not a live feed";
381
469
  case "unreported":
382
- return "Status not reported";
470
+ return "status not reported";
383
471
  default: {
384
472
  const _exhaustive: never = freshness.status;
385
473
  return _exhaustive;
@@ -388,13 +476,104 @@ function statusText(freshness: ReportFreshness): string {
388
476
  }
389
477
 
390
478
  /**
391
- * The one line every `fn` text report ends with:
392
- * `Source: Gloom Cloud | As of 2026-10-09 00:08 UTC | Delayed 10 min`.
479
+ * The dated as-of: a session close, a UTC time, a date, or when the report was
480
+ * retrieved; with the oldest observation unless `short`.
481
+ */
482
+ function asOfText(freshness: ReportFreshness, now: number, short = false): string {
483
+ if (!freshness.asOf) return `retrieved ${timeText(freshness.retrievedAt, now)}`;
484
+ const asOf = freshness.asOfClose ? `${dayText(freshness.asOfClose, now)} close` : timeText(freshness.asOf, now);
485
+ return freshness.oldest && !short ? `${asOf} (oldest ${dayText(freshness.oldest, now)})` : asOf;
486
+ }
487
+
488
+ /**
489
+ * When the first venue opens again: its UTC time when that is later the same
490
+ * UTC day, else its weekday at the venue (`Mon`), with the date a week out.
491
+ */
492
+ function reopenText(market: ReportMarket, now: number): string | null {
493
+ if (!market.reopensAt || !market.reopensOn) return null;
494
+ const at = Date.parse(market.reopensAt);
495
+ if (!Number.isFinite(at)) return null;
496
+ const iso = new Date(at).toISOString();
497
+ if (iso.slice(0, 10) === new Date(now).toISOString().slice(0, 10)) return `${iso.slice(11, 16)} UTC`;
498
+ return at - now > WEEKDAY_ONLY_MS ? dayText(market.reopensOn, now) : WEEKDAYS[new Date(`${market.reopensOn}T00:00:00Z`).getUTCDay()]!;
499
+ }
500
+
501
+ /** `markets closed until Mon`; `short` is the compact form a narrow capture falls back to (`reopens Mon`). */
502
+ function marketText(market: ReportMarket, now: number, short = false): string {
503
+ const reopen = reopenText(market, now);
504
+ switch (market.state) {
505
+ case "open":
506
+ return short ? "open" : "markets open";
507
+ case "after-hours":
508
+ return "after hours";
509
+ case "pre-market":
510
+ return reopen ? `pre-market, opens ${reopen}` : "pre-market";
511
+ case "closed":
512
+ if (short) return reopen ? `reopens ${reopen}` : "closed";
513
+ return reopen ? `markets closed until ${reopen}` : "markets closed";
514
+ default: {
515
+ const _exhaustive: never = market.state;
516
+ return _exhaustive;
517
+ }
518
+ }
519
+ }
520
+
521
+ interface StatusLineDetail {
522
+ /** Leave out the oldest observation. */
523
+ asOf?: "short";
524
+ /** Count stale rows without their total. */
525
+ status?: "short";
526
+ /** `reopens Mon` for `markets closed until Mon`; `none` leaves the market out. */
527
+ market?: "short" | "none";
528
+ }
529
+
530
+ /**
531
+ * The parts of a report's status line: the dated as-of, how current the data
532
+ * is (`15 min delayed`, `live`), and where its markets stand, when known.
533
+ */
534
+ function statusLineParts(freshness: ReportFreshness, detail: StatusLineDetail = {}): string[] {
535
+ const now = Date.parse(freshness.retrievedAt);
536
+ return [
537
+ asOfText(freshness, now, detail.asOf === "short"),
538
+ statusText(freshness, detail.status === "short"),
539
+ ...(freshness.market && detail.market !== "none" ? [marketText(freshness.market, now, detail.market === "short")] : []),
540
+ ];
541
+ }
542
+
543
+ function capitalize(text: string): string {
544
+ return text ? `${text[0]!.toUpperCase()}${text.slice(1)}` : text;
545
+ }
546
+
547
+ /**
548
+ * The plain status line of a report or capture, without its source:
549
+ * `Fri 9 Oct close · 15 min delayed · markets closed until Mon`.
550
+ */
551
+ export function formatStatusLine(freshness: ReportFreshness): string {
552
+ return capitalize(statusLineParts(freshness).join(" · "));
553
+ }
554
+
555
+ /**
556
+ * The status line from longest to shortest, for a capture to take the first
557
+ * that fits its width. The date and the delay stay to the last; the market
558
+ * part shortens, then the oldest observation and the stale total go, then
559
+ * the market part.
560
+ */
561
+ export function statusLineVariants(freshness: ReportFreshness): string[] {
562
+ const details: StatusLineDetail[] = [
563
+ {},
564
+ { market: "short" },
565
+ { asOf: "short", market: "short" },
566
+ { asOf: "short", status: "short", market: "short" },
567
+ { asOf: "short", status: "short", market: "none" },
568
+ ];
569
+ return [...new Set(details.map((detail) => capitalize(statusLineParts(freshness, detail).join(" · "))))];
570
+ }
571
+
572
+ /**
573
+ * The one line every text report ends with, its source then its status line:
574
+ * `Source: Gloom Cloud · Fri 9 Oct close · 15 min delayed · markets closed until Mon`.
393
575
  * Without a dated observation it says when the report was retrieved instead.
394
576
  */
395
577
  export function formatFreshnessLine(freshness: ReportFreshness): string {
396
- const time = freshness.asOf
397
- ? `As of ${formatUtcTime(freshness.asOf)}${freshness.oldest ? ` (oldest ${freshness.oldest.slice(0, 10)})` : ""}`
398
- : `Retrieved ${formatUtcTime(freshness.retrievedAt)}`;
399
- return [`Source: ${freshness.source}`, time, statusText(freshness)].join(" | ");
578
+ return [`Source: ${freshness.source}`, ...statusLineParts(freshness)].join(" · ");
400
579
  }