gloomberb 0.16.2 → 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 (527) 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/account-cache.ts +45 -0
  30. package/src/brokers/signed-in/adapter.ts +33 -2
  31. package/src/brokers/signed-in/client.ts +10 -0
  32. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  33. package/src/brokers/signed-in/sign-in.ts +62 -5
  34. package/src/capabilities/registry.ts +8 -1
  35. package/src/capabilities/types.ts +2 -0
  36. package/src/cli/command-options.ts +74 -0
  37. package/src/cli/commands/command-utils.ts +29 -0
  38. package/src/cli/commands/crypto-hints.ts +12 -4
  39. package/src/cli/commands/market.ts +524 -148
  40. package/src/cli/commands/options-chain.ts +172 -0
  41. package/src/cli/commands/options-leaps.ts +422 -0
  42. package/src/cli/commands/overview.ts +223 -42
  43. package/src/cli/commands/search.ts +29 -8
  44. package/src/cli/commands/system.ts +99 -19
  45. package/src/cli/commands/themes.ts +64 -0
  46. package/src/cli/commands/ticker.ts +200 -73
  47. package/src/cli/desktop-pane-shot.ts +100 -16
  48. package/src/cli/errors.ts +25 -2
  49. package/src/cli/freshness.ts +7 -4
  50. package/src/cli/fx-pair.ts +57 -0
  51. package/src/cli/help.ts +40 -7
  52. package/src/cli/history-rows.ts +187 -15
  53. package/src/cli/index-roots.ts +51 -0
  54. package/src/cli/index.ts +21 -13
  55. package/src/cli/listing-arg.ts +211 -3
  56. package/src/cli/not-a-ticker.ts +37 -0
  57. package/src/cli/options.ts +55 -7
  58. package/src/cli/pane-functions/capabilities.ts +13 -2
  59. package/src/cli/pane-functions/catalog.ts +13 -2
  60. package/src/cli/pane-functions/data.ts +36 -5
  61. package/src/cli/pane-functions/dom.ts +89 -28
  62. package/src/cli/pane-functions/freshness.ts +205 -26
  63. package/src/cli/pane-functions/function-help.ts +71 -24
  64. package/src/cli/pane-functions/headless.ts +168 -35
  65. package/src/cli/pane-functions/index.ts +12 -2
  66. package/src/cli/pane-functions/market-session.ts +133 -0
  67. package/src/cli/pane-functions/options.ts +15 -4
  68. package/src/cli/pane-functions/report-notices.ts +63 -0
  69. package/src/cli/pane-functions/report.ts +12 -1
  70. package/src/cli/pane-functions/resolver.ts +32 -8
  71. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  72. package/src/cli/pane-functions/screenshot.ts +109 -23
  73. package/src/cli/registry.ts +8 -4
  74. package/src/cli/report-tables.ts +460 -0
  75. package/src/cli/result.ts +63 -16
  76. package/src/cli/row-window.ts +76 -0
  77. package/src/cli/scroll-truncation.ts +34 -0
  78. package/src/components/chart/composite/composite-chart.tsx +43 -4
  79. package/src/components/chart/composite/legend.tsx +39 -6
  80. package/src/components/chart/composite/rasterizer.ts +64 -4
  81. package/src/components/chart/composite/series-cues.ts +71 -0
  82. package/src/components/chart/composite/text-renderer.ts +16 -8
  83. package/src/components/chart/composite/types.ts +3 -0
  84. package/src/components/command-bar/commands/direct/index.ts +9 -1
  85. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  86. package/src/components/command-bar/commands/registry.ts +7 -0
  87. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  88. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  89. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  90. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  91. package/src/components/command-bar/routes/root/results.ts +40 -10
  92. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  93. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  94. package/src/components/command-bar/surface/index.tsx +18 -4
  95. package/src/components/command-bar/theme-picker.tsx +48 -13
  96. package/src/components/command-bar/workflow/ops.ts +5 -3
  97. package/src/components/data-table/export.ts +6 -51
  98. package/src/components/data-table/view.tsx +31 -10
  99. package/src/components/layout/floating-pane.tsx +6 -2
  100. package/src/components/layout/market-summary.ts +23 -9
  101. package/src/components/layout/pane/content.tsx +46 -29
  102. package/src/components/layout/pane/cycle-order.ts +2 -11
  103. package/src/components/layout/pane/header.tsx +2 -1
  104. package/src/components/layout/pane/title.ts +1 -1
  105. package/src/components/layout/presentation.ts +22 -0
  106. package/src/components/layout/shell/active-drag.ts +67 -35
  107. package/src/components/layout/shell/cursor-occlusion.ts +6 -5
  108. package/src/components/layout/shell/drag/index.ts +16 -7
  109. package/src/components/layout/shell/drag/live.ts +193 -0
  110. package/src/components/layout/shell/drag/overlays.tsx +179 -35
  111. package/src/components/layout/shell/drag/runtime.ts +62 -50
  112. package/src/components/layout/shell/drag/slide.ts +10 -0
  113. package/src/components/layout/shell/index.tsx +84 -57
  114. package/src/components/layout/shell/layout-state.ts +5 -11
  115. package/src/components/layout/shell/menu.ts +28 -1
  116. package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
  117. package/src/components/layout/shell/native/surfaces.ts +30 -14
  118. package/src/components/layout/shell/pane/actions.ts +2 -1
  119. package/src/components/layout/shell/pane/layers.tsx +461 -180
  120. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  121. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  122. package/src/components/layout/shell/shortcuts.ts +5 -2
  123. package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
  124. package/src/components/layout/shell/visible-layout.ts +44 -1
  125. package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
  126. package/src/components/layout/status-bar.tsx +4 -1
  127. package/src/components/markdown-text.tsx +9 -5
  128. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  129. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  130. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  131. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  132. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  133. package/src/components/onboarding/pro-step-layout.ts +86 -0
  134. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  135. package/src/components/ui/button.tsx +10 -0
  136. package/src/components/ui/data-table/index.tsx +38 -0
  137. package/src/components/ui/data-table/types.ts +8 -1
  138. package/src/components/ui/data-table/visible-range.ts +2 -0
  139. package/src/components/ui/icon.tsx +11 -2
  140. package/src/components/ui/index.ts +1 -1
  141. package/src/core/connection-health.ts +1 -1
  142. package/src/core/state/app/layout-reducer.ts +89 -3
  143. package/src/core/state/app/state.ts +8 -5
  144. package/src/core/state/app/types.ts +13 -2
  145. package/src/data/config/layout.ts +55 -21
  146. package/src/data/config/store/fold-panes.ts +149 -0
  147. package/src/data/config/store/migrations.ts +7 -0
  148. package/src/data/config/store/node.ts +27 -6
  149. package/src/data/config/store/normalize.ts +37 -3
  150. package/src/data/json-persistence.ts +17 -3
  151. package/src/data/json-storage.ts +2 -0
  152. package/src/data/memory-resource-store.ts +46 -0
  153. package/src/i18n/es.ts +14 -1
  154. package/src/i18n/ja.ts +14 -1
  155. package/src/i18n/ko.ts +14 -1
  156. package/src/i18n/zh-cn.ts +14 -1
  157. package/src/i18n/zh-tw.ts +14 -1
  158. package/src/layout/desks.ts +1 -2
  159. package/src/layout/pane-layout-move.ts +264 -0
  160. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  161. package/src/layout/pane-manager/index.ts +1 -0
  162. package/src/market-data/coordinator/entries.ts +7 -1
  163. package/src/market-data/coordinator/quotes.ts +2 -2
  164. package/src/market-data/market/freshness.ts +156 -11
  165. package/src/market-data/market/status.ts +40 -0
  166. package/src/market-data/options-alternatives.ts +69 -0
  167. package/src/market-data/quotes/contributions.ts +19 -2
  168. package/src/market-data/quotes/freshness.ts +5 -0
  169. package/src/market-data/quotes/metadata.ts +16 -0
  170. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  171. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  172. package/src/plugins/builtin/account-management/model.ts +1 -0
  173. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  174. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  175. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  176. package/src/plugins/builtin/analytics/pane-model.ts +10 -6
  177. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  178. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  179. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  180. package/src/plugins/builtin/attention/pane.tsx +1 -1
  181. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  182. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  183. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  184. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  185. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  186. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  187. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  188. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  189. package/src/plugins/builtin/catalysts/detail.tsx +1 -1
  190. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  191. package/src/plugins/builtin/chat/channels.ts +34 -36
  192. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  193. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  194. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  195. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  196. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  197. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  198. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  199. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  200. package/src/plugins/builtin/chat/conversations.ts +256 -0
  201. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  202. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  203. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  204. package/src/plugins/builtin/chat/index.tsx +40 -2
  205. package/src/plugins/builtin/chat/layout.ts +2 -0
  206. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  207. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  208. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  209. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  210. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  211. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  212. package/src/plugins/builtin/chat/pane.tsx +8 -0
  213. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  214. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  215. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  216. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  217. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  218. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  219. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  220. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  221. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  222. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  223. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  224. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  225. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  226. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  227. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  228. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  229. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  230. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  231. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  232. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  233. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  234. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  235. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  236. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  237. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  238. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  239. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  240. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  241. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  242. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  243. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  244. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  245. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  246. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  247. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  248. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  249. package/src/plugins/builtin/composite-plugins.ts +107 -64
  250. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  251. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  252. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  253. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  254. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  255. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  256. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  257. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  258. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  259. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  260. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  261. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  262. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  263. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  264. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  265. package/src/plugins/builtin/econ/index.tsx +68 -22
  266. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  267. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  268. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  269. package/src/plugins/builtin/executives/data.ts +17 -6
  270. package/src/plugins/builtin/executives/pane.tsx +16 -7
  271. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  272. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  273. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  274. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  275. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  276. package/src/plugins/builtin/gpu/headless.ts +9 -3
  277. package/src/plugins/builtin/gpu/model.ts +14 -5
  278. package/src/plugins/builtin/help/index.tsx +1 -0
  279. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  280. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  281. package/src/plugins/builtin/holders/client.ts +3 -6
  282. package/src/plugins/builtin/holders/format.ts +31 -1
  283. package/src/plugins/builtin/holders/headless.ts +32 -6
  284. package/src/plugins/builtin/holders/pane.tsx +25 -13
  285. package/src/plugins/builtin/holders/report-header.ts +46 -5
  286. package/src/plugins/builtin/insider/index.tsx +4 -4
  287. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  288. package/src/plugins/builtin/iv-history/client.ts +12 -2
  289. package/src/plugins/builtin/iv-history/format.ts +33 -2
  290. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  291. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  292. package/src/plugins/builtin/iv-history/model.ts +61 -13
  293. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  294. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  295. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  296. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  297. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  298. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  299. package/src/plugins/builtin/kelly-sizer/pane.tsx +121 -45
  300. package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
  301. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  302. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  303. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  304. package/src/plugins/builtin/macro-day/client.ts +24 -0
  305. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  306. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  307. package/src/plugins/builtin/macro-day/model.ts +11 -6
  308. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  309. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  310. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  311. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  312. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  313. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  314. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  315. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  316. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  317. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  318. package/src/plugins/builtin/market-movers/model.ts +71 -17
  319. package/src/plugins/builtin/market-movers/table.ts +50 -23
  320. package/src/plugins/builtin/members/pane.tsx +10 -0
  321. package/src/plugins/builtin/news/headless.ts +9 -2
  322. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  323. package/src/plugins/builtin/openfda/client.ts +1 -1
  324. package/src/plugins/builtin/options/carry.ts +95 -0
  325. package/src/plugins/builtin/options/index.tsx +58 -5
  326. package/src/plugins/builtin/options/strike-window.ts +183 -0
  327. package/src/plugins/builtin/options/table.ts +55 -3
  328. package/src/plugins/builtin/options/types.ts +8 -1
  329. package/src/plugins/builtin/options/view.tsx +161 -12
  330. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  331. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  332. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  333. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  334. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  335. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  336. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  337. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  338. package/src/plugins/builtin/pe-band/model.ts +2 -2
  339. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  340. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  341. package/src/plugins/builtin/perps/model.ts +16 -5
  342. package/src/plugins/builtin/perps/pane.tsx +1 -1
  343. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  344. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  345. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  346. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  347. package/src/plugins/builtin/plugin-module.ts +87 -4
  348. package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
  349. package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
  350. package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
  351. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  352. package/src/plugins/builtin/portfolio-list/cli/render.ts +89 -18
  353. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  354. package/src/plugins/builtin/portfolio-list/headless.ts +40 -7
  355. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
  356. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
  357. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
  358. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  359. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  360. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  361. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  362. package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
  363. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  364. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  365. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  366. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  367. package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
  368. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  369. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  370. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  371. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  372. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  373. package/src/plugins/builtin/sec/index.tsx +5 -5
  374. package/src/plugins/builtin/sectors/client.ts +10 -2
  375. package/src/plugins/builtin/sectors/headless.ts +35 -6
  376. package/src/plugins/builtin/sectors/index.tsx +18 -1
  377. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  378. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  379. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  380. package/src/plugins/builtin/shared/members-menu.tsx +260 -0
  381. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  382. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  383. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  384. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  385. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  386. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  387. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  388. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  389. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  390. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  391. package/src/plugins/builtin/themes/pane.tsx +3 -0
  392. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
  393. package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
  394. package/src/plugins/builtin/ticker-detail/headless.ts +63 -25
  395. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
  396. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
  397. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  398. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  399. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  400. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  401. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  402. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  403. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  404. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  405. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  406. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  407. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  408. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  409. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  410. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  411. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  412. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  413. package/src/plugins/builtin/world-indices/client.ts +22 -4
  414. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  415. package/src/plugins/builtin/world-indices/model.ts +4 -2
  416. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  417. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
  418. package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
  419. package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
  420. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  421. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
  422. package/src/plugins/builtin/world-venue-map/index.ts +6 -6
  423. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
  424. package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
  425. package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
  426. package/src/plugins/builtin/world-venue-map/model.ts +16 -0
  427. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
  428. package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
  429. package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
  430. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  431. package/src/plugins/builtin-editorial.ts +126 -0
  432. package/src/plugins/catalog-backend.ts +16 -9
  433. package/src/plugins/catalog-browser.ts +127 -63
  434. package/src/plugins/catalog-manifest.ts +73 -40
  435. package/src/plugins/catalog-ui.ts +26 -6
  436. package/src/plugins/extracted-plugins.ts +7 -1
  437. package/src/plugins/ownership.ts +150 -7
  438. package/src/plugins/plugin-functions.ts +54 -0
  439. package/src/plugins/plugin-off.ts +36 -0
  440. package/src/plugins/registry/host-actions.ts +16 -0
  441. package/src/plugins/registry/index.ts +84 -24
  442. package/src/plugins/registry/plugin-state.ts +25 -1
  443. package/src/plugins/runtime/context.tsx +29 -0
  444. package/src/react/input.ts +48 -1
  445. package/src/remote/app-host.tsx +5 -0
  446. package/src/remote/controller.ts +3 -53
  447. package/src/remote/matching.ts +69 -0
  448. package/src/remote/schema.ts +1 -1
  449. package/src/remote/semantic-tree.tsx +14 -0
  450. package/src/remote/types.ts +2 -0
  451. package/src/renderers/browser/app-services.ts +3 -1
  452. package/src/renderers/dom/data-table/dom.ts +31 -0
  453. package/src/renderers/dom/data-table/index.tsx +5 -0
  454. package/src/renderers/dom/desktop/icons.tsx +14 -0
  455. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  456. package/src/renderers/dom/host/box.tsx +113 -17
  457. package/src/renderers/dom/host/drag-shield.ts +35 -0
  458. package/src/renderers/dom/host/live-frame.ts +62 -0
  459. package/src/renderers/dom/host/mouse.ts +8 -1
  460. package/src/renderers/dom/host/scroll-box.tsx +6 -5
  461. package/src/renderers/dom/host/tabs.tsx +3 -2
  462. package/src/renderers/dom/input-host.tsx +6 -1
  463. package/src/renderers/dom/styles.css +26 -5
  464. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
  465. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  466. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  467. package/src/renderers/electrobun/bun/desktop/initialization.ts +15 -1
  468. package/src/renderers/electrobun/shared/protocol.ts +12 -1
  469. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +4 -1
  470. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  471. package/src/renderers/electrobun/view/host/native.ts +0 -10
  472. package/src/renderers/electrobun/view/host/window-move.ts +28 -0
  473. package/src/renderers/electrobun/view/main.tsx +4 -1
  474. package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
  475. package/src/renderers/electrobun/view/ui-host.tsx +2 -5
  476. package/src/renderers/opentui/input-host.tsx +6 -1
  477. package/src/renderers/opentui/start.tsx +9 -0
  478. package/src/renderers/opentui/ui-host.tsx +7 -1
  479. package/src/shares/portable-layout.ts +16 -6
  480. package/src/sources/gloomberb-cloud/index.ts +19 -10
  481. package/src/sources/gloomberb-cloud/normalizers.ts +20 -21
  482. package/src/sources/listing-symbols.ts +11 -5
  483. package/src/sources/provider-errors.ts +88 -3
  484. package/src/sources/provider-router/batches.ts +3 -3
  485. package/src/sources/provider-router/cached-routes.ts +8 -4
  486. package/src/sources/provider-router/financial-routes.ts +13 -7
  487. package/src/sources/provider-router/financials.ts +106 -16
  488. package/src/sources/provider-router/history.ts +27 -11
  489. package/src/sources/provider-router/index.ts +1 -1
  490. package/src/sources/provider-router/primary.ts +24 -6
  491. package/src/sources/sec-edgar.ts +68 -8
  492. package/src/state/pane-scroll-registry.tsx +67 -35
  493. package/src/sync/core-contributors.ts +4 -3
  494. package/src/test-support/cli-context.ts +2 -1
  495. package/src/test-support/data-provider.ts +34 -0
  496. package/src/test-support/margin-account.ts +108 -0
  497. package/src/test-support/shell-pane-keys.tsx +1 -0
  498. package/src/theme/series-colors.ts +46 -0
  499. package/src/theme/themes.ts +149 -0
  500. package/src/tickers/search/index.ts +22 -2
  501. package/src/tickers/search/ranking.ts +45 -1
  502. package/src/tickers/search/types.ts +3 -1
  503. package/src/time-series/resolution.ts +15 -0
  504. package/src/time-series/resolve.ts +1 -14
  505. package/src/types/config.ts +61 -1
  506. package/src/types/financials.ts +32 -3
  507. package/src/types/headless.ts +11 -0
  508. package/src/types/instrument.ts +8 -0
  509. package/src/types/plugin.ts +49 -3
  510. package/src/ui/host.tsx +16 -0
  511. package/src/ui/index.tsx +10 -3
  512. package/src/ui/scroll-box-axis.ts +26 -0
  513. package/src/utils/cli-output.ts +14 -1
  514. package/src/utils/currency-units.ts +8 -3
  515. package/src/utils/depositary-receipt.ts +54 -10
  516. package/src/utils/display-number.ts +63 -0
  517. package/src/utils/exchanges.ts +13 -0
  518. package/src/utils/format.ts +2 -1
  519. package/src/utils/fundamentals.ts +17 -1
  520. package/src/utils/fx-market-hours.ts +34 -0
  521. package/src/utils/option-expiry.ts +126 -0
  522. package/src/utils/price-history-integrity.ts +19 -6
  523. package/src/utils/price-history.ts +39 -6
  524. package/src/utils/rpc-timeout-error.ts +6 -2
  525. package/src/utils/sec-eps-basis.ts +159 -30
  526. package/src/utils/sec.ts +38 -34
  527. package/src/utils/throttled-fetch.ts +51 -22
@@ -1,6 +1,7 @@
1
1
  import type { HeadlessBundleSection, HeadlessPaneDefinition } from "../../../types/plugin";
2
2
  import { loadRiskReportWithClient, loadRiskReportsWithClient, type RiskReportsResult } from "./data";
3
3
  import { SEC_FILINGS } from "../shared/report-freshness";
4
+ import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
4
5
 
5
6
  export const riskFactorsHeadless: HeadlessPaneDefinition<"bundle"> = {
6
7
  shape: "bundle",
@@ -16,9 +17,11 @@ export const riskFactorsHeadless: HeadlessPaneDefinition<"bundle"> = {
16
17
  const selection = String(args.options.year ?? "latest").trim().toLowerCase();
17
18
  if (selection !== "latest" && !/^\d{4}$/.test(selection)) throw new Error("Risk report year must be a four-digit filing year or latest.");
18
19
  const force = args.options.refresh === true;
20
+ // A listing abroad names its venue and company, so it never reads a US namesake's reports.
21
+ const listing = await resolveHeadlessIssuerListing(context, ticker);
19
22
  // A specific immutable filing does not depend on the latest discovery list.
20
23
  let list: RiskReportsResult | null = null;
21
- if (selection === "latest") list = await loadRiskReportsWithClient(context.apiClient, ticker, { force });
24
+ if (selection === "latest") list = await loadRiskReportsWithClient(context.apiClient, ticker, { force, listing });
22
25
  const year = list
23
26
  ? [...list.reports].sort((a, b) => b.reportYear - a.reportYear)[0]?.reportYear
24
27
  : Number(selection);
@@ -27,7 +30,7 @@ export const riskFactorsHeadless: HeadlessPaneDefinition<"bundle"> = {
27
30
  ? `No cached risk report available for ${ticker}; report discovery failed: ${list.refreshError}`
28
31
  : `No 10-K risk reports on file for ${ticker}.`);
29
32
  }
30
- const report = await loadRiskReportWithClient(context.apiClient, ticker, year, { force });
33
+ const report = await loadRiskReportWithClient(context.apiClient, ticker, year, { force, listing });
31
34
  const sections: HeadlessBundleSection[] = [
32
35
  { title: "Filing", entries: [
33
36
  { label: "Ticker", value: report.ticker },
@@ -21,7 +21,7 @@ import { isPermanentClientError } from "../../../api-client/errors";
21
21
  import { usePluginPaneState } from "../../runtime";
22
22
  import { useFilingYearReader } from "../shared/filing-year-reader";
23
23
  import { useProFeatureWall, type ProFeatureWallCopy, type ReadGuard } from "../shared/pro-feature-wall";
24
- import { useBoundTicker } from "../shared/ticker-request";
24
+ import { useBoundTicker, useIssuerListing } from "../shared/ticker-request";
25
25
  import { loadRiskReport, loadRiskReports } from "./data";
26
26
 
27
27
  export const RISK_FACTORS_PANE_ID = "risk-factors";
@@ -100,8 +100,9 @@ function RiskFactorsReader({
100
100
  nested = false,
101
101
  guard,
102
102
  }: RiskFactorsPaneProps & { guard: ReadGuard }) {
103
- const { symbol } = useBoundTicker();
103
+ const { symbol, ticker: boundTicker } = useBoundTicker();
104
104
  const ticker = symbol ? symbol.toUpperCase() : null;
105
+ const listing = useIssuerListing(boundTicker);
105
106
  const nativePaneChrome = useUiCapabilities().nativePaneChrome === true;
106
107
  const rendererHost = useRendererHost();
107
108
 
@@ -109,13 +110,13 @@ function RiskFactorsReader({
109
110
  // one the pane opened with, so a restored year applies to it.
110
111
  const [selectedYear, setSelectedYear] = usePluginPaneState<number | null>("filingYear", null);
111
112
  const [selectedTicker, setSelectedTicker] = useState<string | null>(ticker);
112
- const listLoader = useCallback((force: boolean) => guard(loadRiskReports(ticker!, { force })), [guard, ticker]);
113
+ const listLoader = useCallback((force: boolean) => guard(loadRiskReports(ticker!, { force, listing })), [guard, listing, ticker]);
113
114
  const list = useAsyncResource(ticker ? listLoader : null, { clearOnError: isPermanentClientError });
114
115
  const years = useMemo(() => (list.data?.reports ?? []).map((entry) => entry.reportYear).sort((a, b) => b - a), [list.data]);
115
116
  // Null follows the newest discovered filing; an explicit choice stays on that year.
116
117
  const year = selectedTicker === ticker && selectedYear !== null
117
118
  ? selectedYear : years[0] ?? null;
118
- const reportLoader = useCallback((force: boolean) => guard(loadRiskReport(ticker!, year!, { force })), [guard, ticker, year]);
119
+ const reportLoader = useCallback((force: boolean) => guard(loadRiskReport(ticker!, year!, { force, listing })), [guard, listing, ticker, year]);
119
120
  const detail = useAsyncResource(ticker && year !== null ? reportLoader : null, { clearOnError: isPermanentClientError });
120
121
  const report = detail.data;
121
122
  const listError = list.error ?? list.data?.refreshError ?? null;
@@ -166,7 +167,7 @@ function RiskFactorsReader({
166
167
  const bodyWidth = Math.max(12, width - 2);
167
168
  const proseWidth = Math.min(bodyWidth, READING_WIDTH);
168
169
 
169
- if (!ticker) return <EmptyState title="Pick a ticker to see its risk factors." />;
170
+ if (!ticker) return <EmptyState title="Select a ticker." />;
170
171
  if (!list.data && list.loading) return <PaneStatusBody loading align="center" loadingLabel="Loading risk factors..." />;
171
172
  if (!list.data && listError && year === null) return <PaneStatusBody error={listError} errorTitle="Could not load risk reports." />;
172
173
  if (year === null) return <EmptyState title={`No 10-K risk factors on file for ${ticker}.`} />;
@@ -133,7 +133,7 @@ export function SeasonalityPane({ width, height, focused }: PaneProps) {
133
133
  return <Box width={width} height={height} flexDirection="column" overflow="hidden">
134
134
  {strip}
135
135
  <QueryBar width={width} filters={[{ id: "lookback", label: "Lookback", value: String(lookback), options: LOOKBACK_OPTIONS, onChange: setLookback }]} />
136
- {!symbol ? <EmptyState title="Choose a ticker." /> : <PaneStatusBody subject="seasonality" loading={history.loading && !model}
136
+ {!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="seasonality" loading={history.loading && !model}
137
137
  error={!model ? history.error ?? identityError ?? null : null} empty={!!model && !model.years.length}>
138
138
  {model ? <>
139
139
  <StatGrid items={stats} width={width} />
@@ -10,7 +10,7 @@ import { useDebouncedPluginPaneState } from "../../runtime";
10
10
  import { usePaneSettingValue } from "../../../state/app/context";
11
11
  import type { ScrollBoxRenderable } from "../../../ui";
12
12
  import { EmptyState, FeedDataTableStackView, Spinner, StatGrid, usePaneNoticeFooter, useTableLoadMore, type FeedDataTableItem, type StatItem } from "../../../components";
13
- import { isUsEquityOrFundTicker, isUsEquityTicker, secFilingItemCodes } from "../../../utils/sec";
13
+ import { mayBeUsEquityOrFundTicker, mayBeUsEquityTicker, secFilingItemCodes } from "../../../utils/sec";
14
14
  import { parseForm4Xml, transactionTypeLabel } from "../insider/insider-data";
15
15
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
16
16
  import {
@@ -241,7 +241,7 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
241
241
  // ticker. The fund filings view (ETF) lists other forms, so it leaves it.
242
242
  useSecFilingFocusRequest(forms ? null : ticker?.metadata.ticker, setOpenItemId);
243
243
  // A form list is the fund filings view (ETF), which US-listed funds open too.
244
- const eligibleTicker = forms ? isUsEquityOrFundTicker(ticker) : isUsEquityTicker(ticker);
244
+ const eligibleTicker = forms ? mayBeUsEquityOrFundTicker(ticker) : mayBeUsEquityTicker(ticker);
245
245
  const instrument = instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null);
246
246
  const filingsEntry = useSecFilingsQuery(
247
247
  instrument && eligibleTicker
@@ -449,7 +449,7 @@ export const secModule: PluginModule = {
449
449
  keywords: ["sec", "filings", "10-k", "10-q", "8-k"],
450
450
  shortcut: "SEC",
451
451
  shortcutAliases: ["CF"],
452
- canCreate: (_context, options) => !options?.ticker || isUsEquityTicker(options.ticker),
452
+ canCreate: (_context, options) => !options?.ticker || mayBeUsEquityTicker(options.ticker),
453
453
  }),
454
454
  headless: secHeadless,
455
455
  },
@@ -462,7 +462,7 @@ export const secModule: PluginModule = {
462
462
  keywords: ["etf", "fund", "n-1a", "485bpos", "497", "n-csr", "n-cen", "n-port", "prospectus"],
463
463
  shortcut: "ETF",
464
464
  viewKey: "etf",
465
- canCreate: (_context, options) => !options?.ticker || isUsEquityOrFundTicker(options.ticker),
465
+ canCreate: (_context, options) => !options?.ticker || mayBeUsEquityOrFundTicker(options.ticker),
466
466
  settings: () => ({ forms: ETF_FORMS_SETTING }),
467
467
  }),
468
468
  headless: createSecHeadless(undefined, {
@@ -481,7 +481,7 @@ export const secModule: PluginModule = {
481
481
  order: 45,
482
482
  component: SecView,
483
483
  instruments: ["equity"],
484
- isVisible: ({ ticker }) => isUsEquityTicker(ticker),
484
+ isVisible: ({ ticker }) => mayBeUsEquityTicker(ticker),
485
485
  });
486
486
  },
487
487
  dispose() {
@@ -1,10 +1,12 @@
1
1
  import type { DataProvider } from "../../../types/data-provider";
2
2
  import type { PricePoint, Quote } from "../../../types/financials";
3
3
  import type { SectorDef } from "./sector-data";
4
+ import { getRegularSessionDisplay } from "../../../market-data/market/status";
4
5
  import {
5
6
  computeTrailingReturn,
6
7
  historySessionBefore,
7
8
  latestHistoryDate,
9
+ sectorExtendedFields,
8
10
  sectorReturnTargetDate,
9
11
  sectorQuoteSessionDate,
10
12
  sectorReturnStartDate,
@@ -41,6 +43,8 @@ export async function loadSectorRows(
41
43
  sector.etf,
42
44
  await provider.getPriceHistory(sector.etf, "", "1Y").catch(() => []),
43
45
  ] as const)));
46
+ // The quotes as sent carry any pre-market or after-hours print, for its own column.
47
+ const sentQuotes = new Map(quotes);
44
48
  for (const [symbol, quote] of quotes) {
45
49
  if (quote) quotes.set(symbol, completedSessionQuote(quote, histories.get(symbol) ?? []));
46
50
  }
@@ -52,7 +56,10 @@ export async function loadSectorRows(
52
56
  let history: PricePoint[] = histories.get(sector.etf) ?? [];
53
57
  const quote = quotes.get(sector.etf) ?? null;
54
58
  if (!quote && history.length === 0) return { etf: sector.etf, row: null };
55
- const lastReportedPrice = quote && Number.isFinite(quote.price) && quote.price > 0 ? quote.price : null;
59
+ // Price and 1D are the regular session, as `ticker` reads them: after the close
60
+ // its close and move, with an extended print in its own column.
61
+ const headline = getRegularSessionDisplay(quote);
62
+ const lastReportedPrice = headline && Number.isFinite(headline.price) && headline.price > 0 ? headline.price : null;
56
63
  const sessionDate = quoteDates.get(sector.etf) ?? null;
57
64
  const priceIssue = !quote || lastReportedPrice == null ? "quote unavailable"
58
65
  : !sessionDate ? "quote session unknown"
@@ -60,7 +67,7 @@ export async function loadSectorRows(
60
67
  : sessionDate !== asOfDate ? `quote from ${sessionDate}; shared session is ${asOfDate}`
61
68
  : null;
62
69
  const price = priceIssue ? null : lastReportedPrice;
63
- const changePercent = price != null && Number.isFinite(quote?.changePercent) ? quote!.changePercent : null;
70
+ const changePercent = price != null && Number.isFinite(headline?.changePercent) ? headline!.changePercent! : null;
64
71
  const quoteIssue = priceIssue ?? (changePercent == null ? "1D change unavailable" : null);
65
72
  // A strict trailing request may begin after the prior year's weekend or
66
73
  // holiday. Ask for a small boundary buffer when the provider supports it.
@@ -89,6 +96,7 @@ export async function loadSectorRows(
89
96
  quoteUpdatedAt: quote && Number.isFinite(quote.lastUpdated) ? quote.lastUpdated : null,
90
97
  quoteDataSource: quote?.dataSource,
91
98
  changePercent,
99
+ ...sectorExtendedFields(price == null ? null : sentQuotes.get(sector.etf)),
92
100
  return1M: month?.value ?? null,
93
101
  return1Y: year?.value ?? null,
94
102
  returnAsOfDate: asOfDate,
@@ -3,19 +3,39 @@ import type {
3
3
  HeadlessPaneDefinition,
4
4
  HeadlessPaneLoadArgs,
5
5
  } from "../../../types/plugin";
6
+ import type { HeadlessPaneColumn } from "../../../types/headless";
6
7
  import { formatCurrency, formatPercentRaw } from "../../../utils/format";
8
+ import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
7
9
  import { loadSectorRows, type SectorRowOutcome } from "./client";
8
10
  import { getSectorCollection, type SectorCollectionId, type SectorDef } from "./sector-data";
9
- import { DEFAULT_SORT_PREFERENCE, sectorRowIssues, sortRows, type SectorRow } from "./sector-model";
11
+ import { DEFAULT_SORT_PREFERENCE, sectorExtendedSessions, sectorRowIssues, sortRows, type SectorRow } from "./sector-model";
10
12
 
11
- const COLUMNS = [
13
+ const price = (value: unknown, row: Record<string, unknown>) => value == null ? "-" : formatCurrency(Number(value), String(row.currency));
14
+ const percent = (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value));
15
+
16
+ const LEAD_COLUMNS: HeadlessPaneColumn[] = [
12
17
  { key: "name", header: "Sector" },
13
18
  { key: "etf", header: "ETF" },
14
- { key: "price", header: "Last", align: "right" as const, format: (value: unknown, row: Record<string, unknown>) => value == null ? "-" : formatCurrency(Number(value), String(row.currency)) },
15
- { key: "changePercent", header: "1D", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
16
- { key: "return1M", header: "1M", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
17
- { key: "return1Y", header: "1Y", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
19
+ { key: "price", header: "Last", align: "right", format: price },
20
+ { key: "changePercent", header: "1D", align: "right", format: percent },
21
+ ];
22
+ const RETURN_COLUMNS: HeadlessPaneColumn[] = [
23
+ { key: "return1M", header: "1M", align: "right", format: percent },
24
+ { key: "return1Y", header: "1Y", align: "right", format: percent },
18
25
  ];
26
+ const COLUMNS = [...LEAD_COLUMNS, ...RETURN_COLUMNS];
27
+
28
+ /** The pre-market or after-hours print and its move from the regular close, for the funds that have one. */
29
+ function extendedColumn(session: ExtendedSession): HeadlessPaneColumn {
30
+ return {
31
+ key: "extendedPrice",
32
+ header: EXTENDED_SESSION_LABELS[session],
33
+ align: "right",
34
+ format: (value, row) => row.extendedSession === session && typeof value === "number"
35
+ ? `${price(value, row)} ${percent(row.extendedChangePercent)}`
36
+ : "-",
37
+ };
38
+ }
19
39
 
20
40
  export function projectSectorRows(
21
41
  definitions: readonly SectorDef[],
@@ -36,6 +56,10 @@ export function projectSectorRows(
36
56
  quoteDataSource: byEtf.get(definition.etf)?.quoteDataSource,
37
57
  quoteIssue: byEtf.get(definition.etf)?.quoteIssue ?? null,
38
58
  lastReportedPrice: byEtf.get(definition.etf)?.lastReportedPrice ?? null,
59
+ extendedSession: byEtf.get(definition.etf)?.extendedSession ?? null,
60
+ extendedPrice: byEtf.get(definition.etf)?.extendedPrice ?? null,
61
+ extendedChange: byEtf.get(definition.etf)?.extendedChange ?? null,
62
+ extendedChangePercent: byEtf.get(definition.etf)?.extendedChangePercent ?? null,
39
63
  returnIntegrity: byEtf.get(definition.etf)?.returnIntegrity ?? {},
40
64
  returnAsOfDate: byEtf.get(definition.etf)?.returnAsOfDate ?? null,
41
65
  return1MStartDate: byEtf.get(definition.etf)?.return1MStartDate ?? null,
@@ -82,7 +106,12 @@ export function createSectorsHeadless(
82
106
  const unavailableReturns = rows.filter((row) => row.return1M == null || row.return1Y == null).map((row) => row.etf);
83
107
  const unavailableDailyChanges = rows.filter((row) => row.changePercent == null).map((row) => row.etf);
84
108
  const unavailableSymbols = [...new Set([...unavailableQuotes, ...unavailableReturns, ...unavailableDailyChanges])];
109
+ // Last and 1D are the regular session; an extended print gets a column only when a fund has one.
110
+ const extendedSessions = sectorExtendedSessions(rows);
85
111
  return {
112
+ ...(extendedSessions.length > 0
113
+ ? { columns: [...LEAD_COLUMNS, ...extendedSessions.map(extendedColumn), ...RETURN_COLUMNS] }
114
+ : {}),
86
115
  unavailableSymbols: unavailableSymbols.length > 0 ? unavailableSymbols : undefined,
87
116
  errors: rows.flatMap((row) => sectorRowIssues(row).map((issue) => `${row.etf}: ${issue}.`)),
88
117
  // The quote behind each row says whether it is real-time or delayed, and how current it is.
@@ -12,6 +12,7 @@ import { useLiveQuoteEntries } from "../../../state/hooks/quote-streaming";
12
12
  import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors";
13
13
  import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
14
14
  import type { Quote } from "../../../types/financials";
15
+ import type { ExtendedSession } from "../../../market-data/market/status";
15
16
  import { useAutoRefresh, useUpdatedAgo } from "../../../react/auto-refresh";
16
17
  import { useLiveStreamingSetting } from "../../../state/hooks/live-streaming";
17
18
  import { isStreamCarryingQuote, useVisibleBoardSymbols } from "../shared/use-quote-board";
@@ -33,6 +34,7 @@ import {
33
34
  nextSortPreference,
34
35
  normalizeRowsForCollection,
35
36
  overlayLiveSectorQuote,
37
+ sectorExtendedSessions,
36
38
  sectorRowFollowsQuote,
37
39
  sectorRowIssues,
38
40
  sortRows,
@@ -149,7 +151,6 @@ function EtfBoard({ collectionId, focused, width, height, rootBefore }: EtfBoard
149
151
  const fetchGenRef = useRef(0);
150
152
  const [loadError, setLoadError] = useState<string | null>(null);
151
153
 
152
- const columns = useMemo(() => buildSectorColumns(width), [width]);
153
154
  const rows = useMemo(
154
155
  () => normalizeRowsForCollection(rowsByCollection, activeCollection.id, activeItems),
155
156
  [activeCollection.id, activeItems, rowsByCollection],
@@ -203,6 +204,12 @@ function EtfBoard({ collectionId, focused, width, height, rootBefore }: EtfBoard
203
204
  return carried && !!quote && sectorRowFollowsQuote(row, quote);
204
205
  }), [freshnessNow, liveEntries, liveStreaming, rows, subscriptionStartedAt]);
205
206
  const sortedRows = useMemo(() => sortRows(liveRows, sortPreference), [liveRows, sortPreference]);
207
+ // A pre-market or after-hours column only while a fund has such a print.
208
+ const extendedSessionsKey = sectorExtendedSessions(liveRows).join(",");
209
+ const columns = useMemo(
210
+ () => buildSectorColumns(width, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : []),
211
+ [extendedSessionsKey, width],
212
+ );
206
213
  const lastRefreshMs = lastRefreshByCollection[activeCollection.id] ?? null;
207
214
  const loading = rows.some((row) => row.loading);
208
215
  /**
@@ -296,6 +303,16 @@ function EtfBoard({ collectionId, focused, width, height, rootBefore }: EtfBoard
296
303
  text: row.changePercent !== null ? formatPercentRaw(shownPercent(row.changePercent)) : "—",
297
304
  color: row.changePercent !== null ? priceColor(shownPercent(row.changePercent)) : colors.textDim,
298
305
  };
306
+ case "preMarket":
307
+ case "afterHours": {
308
+ // The extended print's move from the close in LAST.
309
+ const move = row.extendedSession === (column.id === "preMarket" ? "PRE" : "POST") ? row.extendedChangePercent ?? null : null;
310
+ return {
311
+ text: move !== null ? formatPercentRaw(shownPercent(move)) : "—",
312
+ value: move,
313
+ color: move !== null ? priceColor(shownPercent(move)) : colors.textDim,
314
+ };
315
+ }
299
316
  case "return1M":
300
317
  return {
301
318
  text: row.loading && row.return1M === null ? "…" : row.return1M !== null ? formatPercentRaw(shownPercent(row.return1M)) : "—",
@@ -1,8 +1,15 @@
1
1
  import type { DataTableColumn } from "../../../components";
2
+ import { getTableWidth, tableColumnWidth } from "../../../components/ui/table-layout";
2
3
  import type { PricePoint, Quote } from "../../../types/financials";
3
4
  import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
4
5
  import { getPricePointTimestamp } from "../../../utils/price-history";
5
6
  import { mergePriceHistoryIntegrity, pricePointIntegrity, type PriceHistoryIntegrity } from "../../../utils/price-history-integrity";
7
+ import {
8
+ getExtendedSessionDisplay,
9
+ getRegularSessionDisplay,
10
+ marketStateLabel,
11
+ type ExtendedSession,
12
+ } from "../../../market-data/market/status";
6
13
  import {
7
14
  getSectorCollection,
8
15
  type SectorCollectionId,
@@ -12,8 +19,21 @@ import {
12
19
  const DAY_MS = 24 * 60 * 60 * 1000;
13
20
  export const DEFAULT_COLLECTION_ID: SectorCollectionId = "sectors";
14
21
 
15
- export interface SectorRow extends SectorDef {
22
+ /**
23
+ * The pre-market or after-hours print beside a row's regular close, measured
24
+ * from that close (getExtendedSessionDisplay); all null without one.
25
+ */
26
+ export interface SectorExtendedFields {
27
+ extendedSession?: ExtendedSession | null;
28
+ extendedPrice?: number | null;
29
+ extendedChange?: number | null;
30
+ extendedChangePercent?: number | null;
31
+ }
32
+
33
+ export interface SectorRow extends SectorDef, SectorExtendedFields {
34
+ /** The regular session's price: its close once it is over, never an extended print. */
16
35
  price: number | null;
36
+ /** The regular session's move, as `ticker` reports it. */
17
37
  changePercent: number | null;
18
38
  return1M: number | null;
19
39
  return1Y: number | null;
@@ -33,7 +53,7 @@ export interface SectorRow extends SectorDef {
33
53
  return1YStartDate?: string | null;
34
54
  }
35
55
 
36
- type SectorColumnId = "name" | "etf" | "price" | "changePercent" | "return1M" | "return1Y" | "bar";
56
+ type SectorColumnId = "name" | "etf" | "price" | "changePercent" | "preMarket" | "afterHours" | "return1M" | "return1Y" | "bar";
37
57
  export type SectorColumn = DataTableColumn & { id: SectorColumnId };
38
58
  export type SectorRowsByCollection = Record<SectorCollectionId, SectorRow[]>;
39
59
  export type SectorRefreshByCollection = Partial<Record<SectorCollectionId, number>>;
@@ -48,6 +68,45 @@ export const DEFAULT_SORT_PREFERENCE: SectorSortPreference = {
48
68
  direction: "desc",
49
69
  };
50
70
 
71
+ const NO_EXTENDED_PRINT: Required<SectorExtendedFields> = {
72
+ extendedSession: null,
73
+ extendedPrice: null,
74
+ extendedChange: null,
75
+ extendedChangePercent: null,
76
+ };
77
+
78
+ /** A quote's pre-market or after-hours print, from the regular close a row shows, as row fields. */
79
+ export function sectorExtendedFields(quote: Quote | null | undefined): Required<SectorExtendedFields> {
80
+ const extended = getExtendedSessionDisplay(quote);
81
+ if (!extended || !Number.isFinite(extended.price)) return NO_EXTENDED_PRINT;
82
+ return {
83
+ extendedSession: extended.session,
84
+ extendedPrice: extended.price,
85
+ extendedChange: Number.isFinite(extended.change) ? extended.change! : null,
86
+ extendedChangePercent: Number.isFinite(extended.changePercent) ? extended.changePercent! : null,
87
+ };
88
+ }
89
+
90
+ function extendedFieldsOf(row: SectorExtendedFields | undefined): Required<SectorExtendedFields> {
91
+ return {
92
+ extendedSession: row?.extendedSession ?? null,
93
+ extendedPrice: row?.extendedPrice ?? null,
94
+ extendedChange: row?.extendedChange ?? null,
95
+ extendedChangePercent: row?.extendedChangePercent ?? null,
96
+ };
97
+ }
98
+
99
+ function sameExtended(left: SectorExtendedFields, right: SectorExtendedFields): boolean {
100
+ return (left.extendedSession ?? null) === (right.extendedSession ?? null)
101
+ && (left.extendedPrice ?? null) === (right.extendedPrice ?? null)
102
+ && (left.extendedChangePercent ?? null) === (right.extendedChangePercent ?? null);
103
+ }
104
+
105
+ /** The extended sessions some row has a print for, in the order their columns go. */
106
+ export function sectorExtendedSessions(rows: readonly SectorExtendedFields[]): ExtendedSession[] {
107
+ return (["PRE", "POST"] as const).filter((session) => rows.some((row) => row.extendedSession === session));
108
+ }
109
+
51
110
  /** Full length at a 5% session move, which covers all but crash days. */
52
111
  const MOVE_BAR_FULL_SCALE_PERCENT = 5;
53
112
 
@@ -99,6 +158,7 @@ export function normalizeRowsForCollection(
99
158
  quoteIssue: existing?.quoteIssue ?? null,
100
159
  lastReportedPrice: existing?.lastReportedPrice ?? null,
101
160
  quoteUpdatedAt: existing?.quoteUpdatedAt ?? null,
161
+ ...extendedFieldsOf(existing),
102
162
  returnIntegrity: existing?.returnIntegrity ?? {},
103
163
  returnAsOfDate: existing?.returnAsOfDate ?? null,
104
164
  return1MStartDate: existing?.return1MStartDate ?? null,
@@ -138,6 +198,7 @@ const UNAVAILABLE_SECTOR_ROW: Partial<SectorRow> = {
138
198
  lastReportedPrice: null,
139
199
  quoteUpdatedAt: null,
140
200
  returnIntegrity: {},
201
+ ...NO_EXTENDED_PRINT,
141
202
  };
142
203
 
143
204
  function sameSectorRow(left: SectorRow, right: SectorRow): boolean {
@@ -276,33 +337,49 @@ export function sectorRowIssues(row: SectorRow): string[] {
276
337
  return issues;
277
338
  }
278
339
 
279
- export function buildSectorColumns(width: number): SectorColumn[] {
280
- const etfWidth = 4;
281
- const priceWidth = 8;
282
- const changeWidth = 8;
283
- const returnWidth = 8;
284
- const showBar = width >= 67;
285
- const compactBar = width < 82;
286
- const barWidth = showBar
287
- ? compactBar ? 6 : Math.max(8, Math.min(18, Math.floor(width * 0.16)))
288
- : 0;
289
- const columnCount = showBar ? 7 : 6;
290
- const fixedWidth = etfWidth + priceWidth + changeWidth + returnWidth * 2 + barWidth;
291
- const nameWidth = Math.max(10, Math.min(22, width - 2 - columnCount - fixedWidth));
292
-
293
- const columns: SectorColumn[] = [
294
- { id: "name", label: "SECTOR", width: nameWidth, align: "left", flexGrow: 1 },
295
- { id: "etf", label: "ETF", width: etfWidth, align: "left" },
296
- { id: "price", label: "LAST", width: priceWidth, align: "right" },
297
- { id: "changePercent", label: "1D", width: changeWidth, align: "right" },
298
- { id: "return1M", label: "1M", width: returnWidth, align: "right" },
299
- { id: "return1Y", label: "1Y", width: returnWidth, align: "right" },
340
+ const EXTENDED_COLUMN_WIDTH = 9;
341
+ /** The longest name in either collection ("Consumer Staples"), which a narrow pane keeps whole as long as it can. */
342
+ const FULL_NAME_WIDTH = 16;
343
+
344
+ /**
345
+ * The board's columns at `width`. A pre-market or after-hours column follows
346
+ * 1D for each session in `extendedSessions`. A narrow pane gives up the move
347
+ * bar first, then that column, before it cuts a sector's name.
348
+ */
349
+ export function buildSectorColumns(width: number, extendedSessions: readonly ExtendedSession[] = []): SectorColumn[] {
350
+ const lead: SectorColumn[] = [
351
+ { id: "etf", label: "ETF", width: 4, align: "left" },
352
+ { id: "price", label: "LAST", width: 8, align: "right" },
353
+ { id: "changePercent", label: "1D", width: 8, align: "right" },
354
+ ];
355
+ const returns: SectorColumn[] = [
356
+ { id: "return1M", label: "1M", width: 8, align: "right" },
357
+ { id: "return1Y", label: "1Y", width: 8, align: "right" },
358
+ ];
359
+ const extendedColumns = extendedSessions.map((session): SectorColumn => ({
360
+ id: session === "PRE" ? "preMarket" : "afterHours",
361
+ // Named for the session it trades in, as the header labels it.
362
+ label: marketStateLabel(session),
363
+ width: EXTENDED_COLUMN_WIDTH,
364
+ align: "right",
365
+ }));
366
+ // Measured the way the table draws them (a header and its sort mark widen a
367
+ // column, gaps between them); the name adds its own width and a gap.
368
+ const nameRoom = (columns: SectorColumn[]) => width - getTableWidth(columns) - 1;
369
+ const shownExtended = extendedColumns.length > 0 && nameRoom([...lead, ...extendedColumns, ...returns]) >= FULL_NAME_WIDTH
370
+ ? extendedColumns
371
+ : [];
372
+ const extendedWidth = shownExtended.reduce((total, column) => total + tableColumnWidth(column) + 1, 0);
373
+ const barWidth = width - extendedWidth < 82 ? 6 : Math.max(8, Math.min(18, Math.floor(width * 0.16)));
374
+ // Labelled with the window it encodes: it sits after 1Y but tracks 1D.
375
+ const bar: SectorColumn = { id: "bar", label: "1D MOVE", width: barWidth, align: "left" };
376
+ const withBar = [...lead, ...shownExtended, ...returns, bar];
377
+ const showBar = width - extendedWidth >= 67 && nameRoom(withBar) >= FULL_NAME_WIDTH;
378
+ const columns = showBar ? withBar : [...lead, ...shownExtended, ...returns];
379
+ return [
380
+ { id: "name", label: "SECTOR", width: Math.max(10, Math.min(22, nameRoom(columns))), align: "left", flexGrow: 1 },
381
+ ...columns,
300
382
  ];
301
- if (showBar) {
302
- // Labelled with the window it encodes: it sits after 1Y but tracks 1D.
303
- columns.push({ id: "bar", label: "1D MOVE", width: barWidth, align: "left" });
304
- }
305
- return columns;
306
383
  }
307
384
 
308
385
  function getSortValue(columnId: SectorColumnId, row: SectorRow): string | number | null {
@@ -315,6 +392,9 @@ function getSortValue(columnId: SectorColumnId, row: SectorRow): string | number
315
392
  return row.price;
316
393
  case "changePercent":
317
394
  return row.changePercent;
395
+ case "preMarket":
396
+ case "afterHours":
397
+ return row.extendedSession === (columnId === "preMarket" ? "PRE" : "POST") ? row.extendedChangePercent ?? null : null;
318
398
  case "return1M":
319
399
  return row.return1M;
320
400
  case "return1Y":
@@ -338,6 +418,8 @@ export function nextSortPreference(current: SectorSortPreference, columnId: stri
338
418
  return {
339
419
  columnId: typedColumnId,
340
420
  direction: typedColumnId === "changePercent"
421
+ || typedColumnId === "preMarket"
422
+ || typedColumnId === "afterHours"
341
423
  || typedColumnId === "return1M"
342
424
  || typedColumnId === "return1Y"
343
425
  || typedColumnId === "bar"
@@ -373,17 +455,30 @@ export function sectorQuoteSessionDate(quote: Quote): string | null {
373
455
  /**
374
456
  * A live quote extends a loaded row only inside the session its returns are
375
457
  * measured to. Every fund shares that session, so a pre-market print (the
376
- * next session) or a quote older than the snapshot leaves the row alone and
377
- * the board keeps ranking on one completed session. The 1M and 1Y returns
378
- * keep their baselines: the row's own return and price imply the start close.
458
+ * next session) or a quote older than the snapshot leaves the row's price
459
+ * alone and the board keeps ranking on one completed session. The 1M and 1Y
460
+ * returns keep their baselines: the row's own return and price imply the
461
+ * start close.
379
462
  */
380
463
  function isLiveSectorQuote(row: SectorRow, quote: Quote | null | undefined): quote is Quote {
381
- if (!quote || quote.stale === true || !finitePositive(quote.price)) return false;
382
- if (!finitePositive(row.price) || !row.returnAsOfDate) return false;
383
- if (row.quoteUpdatedAt != null && quote.lastUpdated < row.quoteUpdatedAt) return false;
464
+ if (!isCurrentSectorQuote(row, quote) || !finitePositive(getRegularSessionDisplay(quote)?.price)) return false;
384
465
  return sectorQuoteSessionDate(quote) === row.returnAsOfDate;
385
466
  }
386
467
 
468
+ /** A usable quote no older than the snapshot behind a loaded row. */
469
+ function isCurrentSectorQuote(row: SectorRow, quote: Quote | null | undefined): quote is Quote {
470
+ if (!quote || quote.stale === true) return false;
471
+ if (!finitePositive(row.price) || !row.returnAsOfDate) return false;
472
+ return row.quoteUpdatedAt == null || quote.lastUpdated >= row.quoteUpdatedAt;
473
+ }
474
+
475
+ /** A pre-market print of the session after the row's, which moves only the extended column. */
476
+ function isNextPreMarketQuote(row: SectorRow, quote: Quote): boolean {
477
+ if (quote.marketState !== "PRE" && quote.marketState !== "PREPRE") return false;
478
+ const session = sectorQuoteSessionDate(quote);
479
+ return !!session && !!row.returnAsOfDate && session > row.returnAsOfDate;
480
+ }
481
+
387
482
  /**
388
483
  * Whether the feed keeps a loaded row as current as a snapshot reload would,
389
484
  * given a quote the feed is carrying. It does when the quote belongs to the
@@ -402,18 +497,32 @@ export function sectorRowFollowsQuote(row: SectorRow, quote: Quote): boolean {
402
497
  return quote.marketState === "PRE" || quote.marketState === "PREPRE";
403
498
  }
404
499
 
500
+ /**
501
+ * A row with the feed's quote on it. Price and 1D are the regular session,
502
+ * as `ticker` reads them, so they hold at the close; a pre-market or
503
+ * after-hours print moves only the extended column, measured from that close.
504
+ */
405
505
  export function overlayLiveSectorQuote(row: SectorRow, quote: Quote | null | undefined): SectorRow {
406
- if (!isLiveSectorQuote(row, quote) || row.price == null) return row;
407
- const changePercent = Number.isFinite(quote.changePercent) ? quote.changePercent : row.changePercent;
408
- if (quote.price === row.price && changePercent === row.changePercent) return row;
409
- const scale = quote.price / row.price;
410
- const rescale = (value: number | null) => (value == null ? null : ((1 + value / 100) * scale - 1) * 100);
506
+ if (!isCurrentSectorQuote(row, quote) || row.price == null) return row;
507
+ if (!isLiveSectorQuote(row, quote)) {
508
+ if (!isNextPreMarketQuote(row, quote)) return row;
509
+ const extended = sectorExtendedFields(quote);
510
+ return sameExtended(extended, row) ? row : { ...row, ...extended };
511
+ }
512
+ const headline = getRegularSessionDisplay(quote)!;
513
+ const extended = sectorExtendedFields(quote);
514
+ const changePercent = Number.isFinite(headline.changePercent) ? headline.changePercent! : row.changePercent;
515
+ if (headline.price === row.price && changePercent === row.changePercent && sameExtended(extended, row)) return row;
516
+ const scale = headline.price / row.price;
517
+ // A held close leaves the returns exactly as they were.
518
+ const rescale = (value: number | null) => (value == null || scale === 1 ? value : ((1 + value / 100) * scale - 1) * 100);
411
519
  return {
412
520
  ...row,
413
- price: quote.price,
414
- lastReportedPrice: quote.price,
521
+ price: headline.price,
522
+ lastReportedPrice: headline.price,
415
523
  changePercent,
416
524
  return1M: rescale(row.return1M),
417
525
  return1Y: rescale(row.return1Y),
526
+ ...extended,
418
527
  };
419
528
  }
@@ -0,0 +1,51 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { usePluginPaneState } from "../../../public/react";
3
+ import { usePaneInstance } from "../../../state/app/context";
4
+ import type { PaneTemplateDef, PaneTemplateInstanceConfig } from "../../../types/plugin";
5
+
6
+ /** A command's request to show one tab, stamped so a later command can ask again. */
7
+ export function commandTabParams(tab: string, params?: Record<string, string>): Record<string, string> {
8
+ return { ...(params ?? {}), tab, tabAt: String(Date.now()) };
9
+ }
10
+
11
+ /** The template's pane, with the command's tab written onto the instance. */
12
+ function stampCommandTab(instance: PaneTemplateInstanceConfig | null, tab: string): PaneTemplateInstanceConfig | null {
13
+ return instance ? { ...instance, params: commandTabParams(tab, instance.params) } : null;
14
+ }
15
+
16
+ export function withCommandTab(template: PaneTemplateDef, tab: string): PaneTemplateDef {
17
+ const create = template.createInstance;
18
+ return {
19
+ ...template,
20
+ createInstance: (context, options) => {
21
+ const created = create?.(context, options);
22
+ if (!created) return null;
23
+ return created instanceof Promise ? created.then((instance) => stampCommandTab(instance, tab)) : stampCommandTab(created, tab);
24
+ },
25
+ };
26
+ }
27
+
28
+ /**
29
+ * The tab a command asked this pane to show. A stored choice wins until a
30
+ * newer command arrives. Another pane's params, such as Ticker Research
31
+ * around a nested tab, are ignored.
32
+ */
33
+ export function useCommandTab<T extends string>(
34
+ paneId: string,
35
+ stateKey: string,
36
+ tabs: readonly T[],
37
+ fallback: T,
38
+ ): [T, (next: T) => void] {
39
+ const instance = usePaneInstance();
40
+ const params = instance?.paneId === paneId ? instance.params : undefined;
41
+ const requested = params && (tabs as readonly string[]).includes(params.tab ?? "") ? params.tab as T : null;
42
+ const requestAt = params?.tabAt ?? "";
43
+ const [tab, setTab] = usePluginPaneState<T>(stateKey, requested ?? fallback);
44
+ const seen = useRef(requestAt);
45
+ useEffect(() => {
46
+ if (!requested || requestAt === seen.current) return;
47
+ seen.current = requestAt;
48
+ setTab(requested);
49
+ }, [requestAt, requested, setTab]);
50
+ return [tab, setTab];
51
+ }