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
@@ -19,6 +19,7 @@ import {
19
19
  usePaneStatusFooter,
20
20
  type MetricTreemapDirection,
21
21
  type MetricTreemapItem,
22
+ type PaneFooterSegment,
22
23
  } from "../../../components";
23
24
  import { handleRefreshKey } from "../../../components/data-table/table-pane";
24
25
  import { useShortcut } from "../../../react/input";
@@ -27,7 +28,7 @@ import type { GloomPlugin, PaneProps, PaneSettingField } from "../../../types/pl
27
28
  import type { TickerRecord } from "../../../types/ticker";
28
29
  import { t, tf } from "../../../i18n";
29
30
  import { priceColor } from "../../../theme/colors";
30
- import { clipToDisplayWidth, formatCompact, formatCurrency, formatPercentRaw } from "../../../utils/format";
31
+ import { clipToDisplayWidth, displayWidth, formatCompact, formatCurrency, formatPercentRaw } from "../../../utils/format";
31
32
  import { isPlainKey } from "../../../utils/keyboard";
32
33
  import { useAppSelector, usePaneAppConfig, usePaneSettingValue, usePaneStateValue } from "../../../state/app/context";
33
34
  import { usePluginTickerActions } from "../../runtime";
@@ -48,6 +49,7 @@ import {
48
49
  MARKET_HEATMAP_UNIVERSES,
49
50
  fetchMarketHeatmap,
50
51
  resetMarketHeatmapCache,
52
+ type MarketHeatmapBoard,
51
53
  type MarketHeatmapUniverseId,
52
54
  } from "./data";
53
55
  import {
@@ -79,8 +81,20 @@ import {
79
81
  rankHeatmapQuoteWeights,
80
82
  useSettledValue,
81
83
  } from "./live";
84
+ import { useMarketHeatmapEvidence } from "./evidence";
82
85
  import { marketHeatmapHeadless } from "./headless";
83
- import { heatmapMove, resolveHeatmapGrouping, sizeWeightedMove, type HeatmapGrouping } from "./model";
86
+ import {
87
+ HEATMAP_NAME_COUNTS,
88
+ heatmapBoardCaption,
89
+ heatmapMove,
90
+ heatmapNameCount,
91
+ heatmapSessionBasis,
92
+ regularSessionHeatmapAssets,
93
+ resolveHeatmapGrouping,
94
+ sizeWeightedMove,
95
+ type HeatmapGrouping,
96
+ type HeatmapSessionBasis,
97
+ } from "./model";
84
98
  import {
85
99
  overlayScreenerQuoteEntries,
86
100
  resolveScreenerQuoteFeedStatus,
@@ -93,10 +107,15 @@ const LAYOUT_THROTTLE_MS = 5_000;
93
107
  /** A size change below this share of a tile's area is not worth moving it. */
94
108
  const LAYOUT_TOLERANCE = 0.005;
95
109
  const TERMINAL_PAINT_THROTTLE_MS = 250;
110
+ /** "loading", which comes and goes with each refresh. */
111
+ const LOADING_FOOTER_WIDTH = 7;
96
112
  const SELECTION_STREAM_SETTLE_MS = 1_500;
97
113
  const TOOLTIP_NAME_WIDTH = 28;
98
114
  const SIZE_BY_SETTING_KEY = "sizeBy";
99
115
  const SQRT_SIZE_VALUE: HeatmapSizeBy = "sqrt-market-cap";
116
+ const SESSION_BASIS_SETTING_KEY = "sessionBasis";
117
+ const REGULAR_SESSION_BASIS: HeatmapSessionBasis = "regular-session";
118
+ const NAME_COUNT_SETTING_KEY = "maxNames";
100
119
  const NO_BOARD = { assets: NO_ASSETS, omitted: 0 };
101
120
  const EMPTY_TICKERS: TickerRecord[] = [];
102
121
  const NO_CURRENCIES: string[] = [];
@@ -125,6 +144,10 @@ function formatMoneyCompact(value: number | null | undefined, currency: string):
125
144
  return `${formatCompact(value)} ${currency}`;
126
145
  }
127
146
 
147
+ function footerSegmentWidth(segment: PaneFooterSegment): number {
148
+ return segment.parts.reduce((total, part, index) => total + (index > 0 ? 1 : 0) + displayWidth(part.text), 0);
149
+ }
150
+
128
151
  function sizeLabel(asset: HeatmapBoardAsset): string | null {
129
152
  if (asset.showSize === false || asset.size == null) return null;
130
153
  const label = asset.sizeCaption ?? (asset.sizeKind === "net-assets" ? "Assets" : "Mkt");
@@ -233,6 +256,10 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
233
256
  const [linkPortfolio] = usePaneSettingValue<boolean>("linkPortfolio", false);
234
257
  const [sizeBySetting] = usePaneSettingValue<string>(SIZE_BY_SETTING_KEY, SQRT_SIZE_VALUE);
235
258
  const sizeBy = heatmapSizeBy(sizeBySetting);
259
+ const [sessionBasisSetting] = usePaneSettingValue<unknown>(SESSION_BASIS_SETTING_KEY, null);
260
+ const regularSession = heatmapSessionBasis(sessionBasisSetting) === REGULAR_SESSION_BASIS;
261
+ const [nameCountSetting] = usePaneSettingValue<unknown>(NAME_COUNT_SETTING_KEY, null);
262
+ const nameCount = heatmapNameCount(nameCountSetting);
236
263
  const activeUniverse = heatmapTabId(universeSetting);
237
264
  const portfolioTab = activeUniverse === PORTFOLIO_HEATMAP_TAB;
238
265
  const config = usePaneAppConfig();
@@ -281,26 +308,38 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
281
308
  currency: boardCurrency,
282
309
  exchangeRates,
283
310
  sizeBy,
311
+ regularSession,
284
312
  })
285
313
  : NO_BOARD),
286
314
  [financials],
287
- [boardCurrency, collectionId, collectionKind, collectionTickers, exchangeRates, financials.size, sizeBy],
315
+ [boardCurrency, collectionId, collectionKind, collectionTickers, exchangeRates, financials.size, regularSession, sizeBy],
288
316
  PORTFOLIO_REORDER_THROTTLE_MS,
289
317
  );
290
318
  const portfolioAssets = portfolioBoard.assets;
291
- const [assets, setAssets] = useState<HeatmapBoardAsset[]>([]);
292
- // The universe `assets` came from. A board is drawn only under its own tab,
293
- // so a switch never paints the previous universe's tiles.
294
- const [loadedUniverse, setLoadedUniverse] = useState<MarketHeatmapUniverseId | null>(null);
295
- const [lastUpdated, setLastUpdated] = useState<number | null>(null);
319
+ // The last market board that loaded, with the universe and count it was
320
+ // asked for. It is drawn only under its own tab, so a switch never paints the
321
+ // previous universe's tiles, and its footer names its own count while a new
322
+ // one is on its way.
323
+ const [board, setBoard] = useState<MarketHeatmapBoard | null>(null);
296
324
  // The first load starts before the effect runs; an empty board is not "no data".
297
325
  const [loading, setLoading] = useState(true);
298
326
  const [loadError, setLoadError] = useState<string | null>(null);
299
- const [stale, setStale] = useState(false);
300
327
  const [selectedSymbol, setSelectedSymbol] = useState<string | null>(null);
301
328
  const fetchGenRef = useRef(0);
302
329
  const seenCollection = useRef<string | null>(null);
303
- const boardAssets = portfolioTab ? portfolioAssets : (loadedUniverse === activeUniverse ? assets : NO_ASSETS);
330
+ const shownBoard = !portfolioTab && board?.universe === activeUniverse ? board : null;
331
+ const stale = board?.stale === true;
332
+ const requestCount = isRemoteHeatmapUniverse(activeUniverse)
333
+ ? Math.min(MARKET_HEATMAP_REQUEST_COUNT[activeUniverse], nameCount)
334
+ : 0;
335
+ const marketAssets = useMemo(
336
+ () => {
337
+ const assets: readonly HeatmapBoardAsset[] = shownBoard?.assets ?? NO_ASSETS;
338
+ return regularSession ? regularSessionHeatmapAssets(assets, shownBoard?.session) : assets;
339
+ },
340
+ [regularSession, shownBoard],
341
+ );
342
+ const boardAssets = portfolioTab ? portfolioAssets : marketAssets;
304
343
  const hasBoard = boardAssets.length > 0;
305
344
 
306
345
  const selectUniverse = useCallback((value: string) => {
@@ -345,9 +384,12 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
345
384
  freshnessScopeKey: `market-heatmap:${activeUniverse}`,
346
385
  liveStreaming,
347
386
  });
387
+ const completedSessionDate = shownBoard?.regularSessionDate ?? null;
348
388
  const resolvedAssets = useMemo(
349
- () => overlayScreenerQuoteEntries(boardAssets, liveQuoteEntries, { extendedSessions: true }),
350
- [boardAssets, liveQuoteEntries],
389
+ () => overlayScreenerQuoteEntries(boardAssets, liveQuoteEntries, regularSession
390
+ ? { regularSession: true, completedSessionDate }
391
+ : { extendedSessions: true }),
392
+ [boardAssets, completedSessionDate, liveQuoteEntries, regularSession],
351
393
  );
352
394
  const feedStatus = useMemo(
353
395
  () => resolveScreenerQuoteFeedStatus(quoteTargets, liveQuoteEntries, {
@@ -363,10 +405,11 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
363
405
  () => resolveHeatmapGrouping(boardAssets, activeUniverse === "us-equity"),
364
406
  [activeUniverse, boardAssets],
365
407
  );
408
+ // A new board, or a new Colour by, applies at once rather than on the next tick.
366
409
  const layoutAssets = useThrottledMemo(
367
410
  () => resolvedAssets,
368
411
  [resolvedAssets],
369
- [boardAssets],
412
+ [boardAssets, regularSession],
370
413
  LAYOUT_THROTTLE_MS,
371
414
  );
372
415
  const layoutItems = useSettledLayoutItems(
@@ -384,11 +427,19 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
384
427
  const paintAssets = useThrottledMemo(
385
428
  () => resolvedAssets,
386
429
  [resolvedAssets],
387
- [boardAssets],
430
+ [boardAssets, regularSession],
388
431
  nativePaneChrome ? 0 : TERMINAL_PAINT_THROTTLE_MS,
389
432
  );
390
433
  const paintItems = useMemo(() => buildPaintItems(paintAssets), [paintAssets]);
391
434
  const visibleTiles = scene.tiles;
435
+ useMarketHeatmapEvidence({
436
+ tab: activeUniverse,
437
+ tiles: visibleTiles,
438
+ assets: paintAssets,
439
+ grouped: grouping !== "flat",
440
+ loading: !portfolioTab && loading,
441
+ retained: !portfolioTab && (stale || loadError != null),
442
+ });
392
443
  const selectedTileIndex = selectedSymbol ? visibleTiles.findIndex((tile) => tile.item.id === selectedSymbol) : -1;
393
444
  const activeIdx = selectedTileIndex >= 0 ? selectedTileIndex : (visibleTiles.length > 0 ? 0 : -1);
394
445
  const activeSymbol = activeIdx >= 0 ? visibleTiles[activeIdx]!.item.id : null;
@@ -396,7 +447,7 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
396
447
  ? resolvedAssets.find((asset) => asset.symbol === activeSymbol) ?? null
397
448
  : null;
398
449
 
399
- const loadUniverse = useCallback(async (universe: MarketHeatmapUniverseId, options?: { forceRefresh?: boolean }) => {
450
+ const loadUniverse = useCallback(async (universe: MarketHeatmapUniverseId, count: number, options?: { forceRefresh?: boolean }) => {
400
451
  fetchGenRef.current += 1;
401
452
  const gen = fetchGenRef.current;
402
453
  setLoading(true);
@@ -404,14 +455,11 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
404
455
 
405
456
  try {
406
457
  const result = await fetchMarketHeatmap(universe, {
407
- count: MARKET_HEATMAP_REQUEST_COUNT[universe],
458
+ count,
408
459
  forceRefresh: options?.forceRefresh,
409
460
  });
410
461
  if (fetchGenRef.current !== gen) return;
411
- setAssets(result.assets);
412
- setLoadedUniverse(universe);
413
- setLastUpdated(result.fetchedAt);
414
- setStale(result.stale === true);
462
+ setBoard(result);
415
463
  // Selection is the user's; the effect below only fills it when it is gone.
416
464
  } catch {
417
465
  if (fetchGenRef.current !== gen) return;
@@ -424,8 +472,8 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
424
472
 
425
473
  useEffect(() => {
426
474
  if (!isRemoteHeatmapUniverse(activeUniverse)) return;
427
- void loadUniverse(activeUniverse);
428
- }, [activeUniverse, loadUniverse]);
475
+ void loadUniverse(activeUniverse, requestCount);
476
+ }, [activeUniverse, loadUniverse, requestCount]);
429
477
 
430
478
  useEffect(() => {
431
479
  const previous = seenCollection.current;
@@ -460,12 +508,12 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
460
508
  return;
461
509
  }
462
510
  if (!isRemoteHeatmapUniverse(activeUniverse)) return;
463
- void loadUniverse(activeUniverse, { forceRefresh: true });
464
- }, [activeUniverse, collectionId, collectionKind, collectionTickers, loadUniverse, portfolioTab]);
511
+ void loadUniverse(activeUniverse, requestCount, { forceRefresh: true });
512
+ }, [activeUniverse, collectionId, collectionKind, collectionTickers, loadUniverse, portfolioTab, requestCount]);
465
513
  // The list's quotes stream; on a timer only the market boards reload.
466
514
  const autoRefresh = useCallback(() => {
467
- if (isRemoteHeatmapUniverse(activeUniverse)) void loadUniverse(activeUniverse, { forceRefresh: true });
468
- }, [activeUniverse, loadUniverse]);
515
+ if (isRemoteHeatmapUniverse(activeUniverse)) void loadUniverse(activeUniverse, requestCount, { forceRefresh: true });
516
+ }, [activeUniverse, loadUniverse, requestCount]);
469
517
 
470
518
  const selectAdjacentUniverse = useCallback((direction: -1 | 1) => {
471
519
  const index = universeTabs.findIndex((tab) => tab.value === activeUniverse);
@@ -574,9 +622,11 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
574
622
  }
575
623
  });
576
624
 
577
- const updated = useUpdatedAgo(!portfolioTab && loadedUniverse === activeUniverse ? lastUpdated : null);
625
+ // Ages the last check that succeeded, not the snapshot, which the server can
626
+ // hand back unchanged long after it was put together.
627
+ const checked = useUpdatedAgo(shownBoard?.checkedAt ?? null);
578
628
  // The snapshot carries sizes and every name the stream does not; the server caches it for a minute.
579
- useAutoRefresh(lastUpdated, autoRefresh, { intervalMs: SNAPSHOT_REFRESH_MS });
629
+ useAutoRefresh(board?.checkedAt ?? null, autoRefresh, { intervalMs: SNAPSHOT_REFRESH_MS });
580
630
 
581
631
  usePaneStatusFooter({ registrationId: "market-heatmap-retained", stale: hasBoard && stale && !portfolioTab });
582
632
  usePaneNoticeFooter({
@@ -587,41 +637,53 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
587
637
  focused,
588
638
  });
589
639
 
590
- usePaneFooter("market-heatmap", () => ({
591
- info: [
592
- ...(selectedAsset ? [{
593
- id: "selected",
594
- parts: [
595
- { text: selectedAsset.symbol, tone: "label" as const },
596
- {
597
- text: heatmapMove(selectedAsset) == null ? "—" : formatPercentRaw(heatmapMove(selectedAsset)!),
598
- tone: "value" as const,
599
- color: heatmapMove(selectedAsset) == null ? undefined : priceColor(heatmapMove(selectedAsset)!),
600
- bold: true,
601
- },
602
- ...(sessionMark(selectedAsset) ? [{ text: sessionMark(selectedAsset)!, tone: "muted" as const }] : []),
603
- ...(selectedAsset.price > 0
604
- ? [{ text: formatCurrency(selectedAsset.price, selectedAsset.currency), tone: "value" as const }]
605
- : []),
606
- ...(sizeLabel(selectedAsset) ? [{ text: sizeLabel(selectedAsset)!, tone: "muted" as const }] : []),
607
- ...(selectedAsset.volume != null
608
- ? [{ text: `Vol ${formatCompact(selectedAsset.volume)}`, tone: "muted" as const }]
609
- : []),
610
- ],
611
- }] : []),
612
- ...(updated ? [{
613
- id: "updated",
614
- parts: [{ text: `updated ${updated}`, tone: "muted" as const }],
615
- }] : []),
616
- ...(!portfolioTab && loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
617
- // Without a board the body carries the failure.
618
- ...(!portfolioTab && loadError && hasBoard ? [{ id: "error", parts: [{ text: "refresh failed", tone: "warning" as const }] }] : []),
619
- ...(feedStatus ? [{
620
- id: "feed",
621
- parts: [{ text: feedStatus, tone: feedStatus === "live" ? "value" as const : "muted" as const }],
622
- }] : []),
623
- ],
624
- }), [feedStatus, hasBoard, loadError, loading, portfolioTab, selectedAsset, updated]);
640
+ usePaneFooter("market-heatmap", () => {
641
+ const selected: PaneFooterSegment[] = selectedAsset ? [{
642
+ id: "selected",
643
+ parts: [
644
+ { text: selectedAsset.symbol, tone: "label" as const },
645
+ {
646
+ text: heatmapMove(selectedAsset) == null ? "—" : formatPercentRaw(heatmapMove(selectedAsset)!),
647
+ tone: "value" as const,
648
+ color: heatmapMove(selectedAsset) == null ? undefined : priceColor(heatmapMove(selectedAsset)!),
649
+ bold: true,
650
+ },
651
+ ...(sessionMark(selectedAsset) ? [{ text: sessionMark(selectedAsset)!, tone: "muted" as const }] : []),
652
+ ...(selectedAsset.price > 0
653
+ ? [{ text: formatCurrency(selectedAsset.price, selectedAsset.currency), tone: "value" as const }]
654
+ : []),
655
+ ...(sizeLabel(selectedAsset) ? [{ text: sizeLabel(selectedAsset)!, tone: "muted" as const }] : []),
656
+ ...(selectedAsset.volume != null
657
+ ? [{ text: `Vol ${formatCompact(selectedAsset.volume)}`, tone: "muted" as const }]
658
+ : []),
659
+ ],
660
+ }] : [];
661
+ // Without a board the body carries the failure.
662
+ const failed: PaneFooterSegment[] = !portfolioTab && loadError && hasBoard
663
+ ? [{ id: "error", parts: [{ text: "refresh failed", tone: "warning" as const }] }]
664
+ : [];
665
+ const feed: PaneFooterSegment[] = feedStatus
666
+ ? [{ id: "feed", parts: [{ text: feedStatus, tone: feedStatus === "live" ? "value" as const : "muted" as const }] }]
667
+ : [];
668
+ const loadingSegment: PaneFooterSegment[] = !portfolioTab && loading
669
+ ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }]
670
+ : [];
671
+ // The caption takes the room the rest leaves, with "loading" counted in
672
+ // always, so a refresh never squeezes the row; a narrow pane drops its
673
+ // least important parts rather than truncating every segment.
674
+ const others = [...selected, ...failed, ...feed];
675
+ const room = width - others.reduce((total, segment) => total + footerSegmentWidth(segment) + 1, 0) - LOADING_FOOTER_WIDTH - 1;
676
+ const caption = shownBoard ? heatmapBoardCaption(shownBoard, { now: Date.now(), checked, failed: failed.length > 0, maxWidth: room }) : null;
677
+ return {
678
+ info: [
679
+ ...selected,
680
+ ...failed,
681
+ ...(caption ? [{ id: "board", parts: [{ text: caption, tone: "muted" as const }] }] : []),
682
+ ...feed,
683
+ ...loadingSegment,
684
+ ],
685
+ };
686
+ }, [checked, feedStatus, hasBoard, loadError, loading, portfolioTab, selectedAsset, shownBoard, width]);
625
687
 
626
688
  return (
627
689
  <Box flexDirection="column" width={width} height={height}>
@@ -701,6 +763,8 @@ export const marketHeatmapPlugin: GloomPlugin = {
701
763
  values: {
702
764
  linkPortfolio: context.settings.linkPortfolio === true,
703
765
  [SIZE_BY_SETTING_KEY]: heatmapSizeBy(context.settings[SIZE_BY_SETTING_KEY]),
766
+ [SESSION_BASIS_SETTING_KEY]: heatmapSessionBasis(context.settings[SESSION_BASIS_SETTING_KEY]),
767
+ [NAME_COUNT_SETTING_KEY]: String(heatmapNameCount(context.settings[NAME_COUNT_SETTING_KEY])),
704
768
  },
705
769
  fields: [
706
770
  {
@@ -725,6 +789,23 @@ export const marketHeatmapPlugin: GloomPlugin = {
725
789
  ],
726
790
  description: "Square root gives mid-size names room next to the largest. A portfolio is always sized by position value.",
727
791
  },
792
+ {
793
+ key: SESSION_BASIS_SETTING_KEY,
794
+ label: "Colour by",
795
+ type: "select",
796
+ options: [
797
+ { value: REGULAR_SESSION_BASIS, label: "Regular session" },
798
+ { value: "active-session", label: "Active session" },
799
+ ],
800
+ description: "Active session follows pre-market and after-hours trading. Regular session shows the regular session alone: its move while it trades, then its close and move until the next one opens.",
801
+ },
802
+ {
803
+ key: NAME_COUNT_SETTING_KEY,
804
+ label: "Names",
805
+ type: "select",
806
+ options: HEATMAP_NAME_COUNTS.map((count) => ({ value: String(count), label: `Top ${count}` })),
807
+ description: "How many of the largest names US Stocks draws, and US ETFs up to 160. Fewer names leave fewer in Other.",
808
+ },
728
809
  {
729
810
  key: "linkPortfolio",
730
811
  label: "Link to portfolio",
@@ -1,4 +1,7 @@
1
1
  import type { MarketHeatmapAsset } from "../../../api-client/market-discovery";
2
+ import { formatShortDate } from "../../../utils/datetime-format";
3
+ import { displayWidth } from "../../../utils/format";
4
+ import { zonedDateKey, zonedDateTimeParts } from "../../../utils/zoned-date-time";
2
5
 
3
6
  /**
4
7
  * The board's grouping and move arithmetic, shared by the pane and the `fn HM`
@@ -13,14 +16,102 @@ type HeatmapMoveAsset = Pick<MarketHeatmapAsset, "symbol" | "hasChange" | "chang
13
16
  /**
14
17
  * The move, or null when there is none: no change data is not a flat session.
15
18
  * A name without the last session's close still has the move of the open
16
- * pre-market or after-hours session it streams (`extendedSession`).
19
+ * pre-market or after-hours session it streams (`extendedSession`), or of the
20
+ * completed regular session its quote reports (`regularChangePercent`).
17
21
  */
18
- export function heatmapMove(asset: Pick<MarketHeatmapAsset, "hasChange" | "changePercent"> & { extendedSession?: string }): number | null {
19
- return (asset.hasChange || asset.extendedSession != null) && typeof asset.changePercent === "number" && Number.isFinite(asset.changePercent)
22
+ export function heatmapMove(
23
+ asset: Pick<MarketHeatmapAsset, "hasChange" | "changePercent" | "regularChangePercent"> & { extendedSession?: string },
24
+ ): number | null {
25
+ const moved = asset.hasChange || asset.extendedSession != null
26
+ || (typeof asset.regularChangePercent === "number" && Number.isFinite(asset.regularChangePercent));
27
+ return moved && typeof asset.changePercent === "number" && Number.isFinite(asset.changePercent)
20
28
  ? asset.changePercent
21
29
  : null;
22
30
  }
23
31
 
32
+ /**
33
+ * What a market board's tiles are colored by. The active session, the
34
+ * default, follows an open pre-market or after-hours session; the regular
35
+ * session keeps the regular session's move through extended trading.
36
+ */
37
+ export type HeatmapSessionBasis = "active-session" | "regular-session";
38
+
39
+ /** A saved choice wins; a pane that never chose follows the active session. */
40
+ export function heatmapSessionBasis(value: unknown): HeatmapSessionBasis {
41
+ return value === "regular-session" ? "regular-session" : "active-session";
42
+ }
43
+
44
+ /** How many of the largest names a market board asks for. */
45
+ export const HEATMAP_NAME_COUNTS = [100, 150, 500] as const;
46
+ export type HeatmapNameCount = typeof HEATMAP_NAME_COUNTS[number];
47
+
48
+ /** A saved count, as the settings dialog stores it ("150") or as a number; anything else is the full 500. */
49
+ export function heatmapNameCount(value: unknown): HeatmapNameCount {
50
+ const count = typeof value === "string" && value.trim() ? Number(value) : value;
51
+ return HEATMAP_NAME_COUNTS.find((option) => option === count) ?? 500;
52
+ }
53
+
54
+ /**
55
+ * A market board read by its regular session alone. Taken outside the regular
56
+ * session, a tile's move is that session's only when the server dates it as
57
+ * the completed session's (`regularChangePercent`); any other move there is
58
+ * from a session it does not name, so the tile shows none, and no price.
59
+ * Returns the same array when nothing changes.
60
+ */
61
+ export function regularSessionHeatmapAssets<T extends Pick<MarketHeatmapAsset, "hasChange" | "price" | "regularChangePercent">>(
62
+ assets: readonly T[],
63
+ session: string | null | undefined,
64
+ ): readonly T[] {
65
+ if (session == null || session === "REGULAR") return assets;
66
+ let changed = false;
67
+ const next = assets.map((asset) => {
68
+ if (!asset.hasChange || (typeof asset.regularChangePercent === "number" && Number.isFinite(asset.regularChangePercent))) return asset;
69
+ changed = true;
70
+ return { ...asset, hasChange: false, price: 0 };
71
+ });
72
+ return changed ? next : assets;
73
+ }
74
+
75
+ const NEW_YORK = "America/New_York";
76
+ const twoDigits = (value: number) => String(value).padStart(2, "0");
77
+ /** A New York calendar day ("2026-10-09") as the footer names it: "Oct 9". */
78
+ const dayLabel = (day: string) => formatShortDate(day, { year: false, utc: true, fallback: day });
79
+
80
+ /**
81
+ * What a market board covers and when it is from, for the footer: how many of
82
+ * the largest names it holds, the completed regular session it shows outside
83
+ * that session, when the server put the snapshot together, in New York time,
84
+ * dated when that day is not the session's (or, while the session trades,
85
+ * today's), and when this client last checked, which is a separate figure.
86
+ * A footer narrower than `maxWidth` loses the check first, then the snapshot,
87
+ * then the session; after a failed refresh (`failed`) the age of the last good
88
+ * check outlasts the snapshot. Null when not even the count fits.
89
+ */
90
+ export function heatmapBoardCaption(
91
+ board: { topCount: number; fetchedAt: number; regularSessionDate?: string | null },
92
+ { now, checked = null, failed = false, maxWidth = Number.POSITIVE_INFINITY }:
93
+ { now: number; checked?: string | null; failed?: boolean; maxWidth?: number },
94
+ ): string | null {
95
+ const sessionDate = board.regularSessionDate || null;
96
+ // Display order; `rank` is the order they give way in, highest first.
97
+ const parts: Array<{ text: string; rank: number }> = [{ text: `top ${board.topCount}`, rank: 0 }];
98
+ if (sessionDate) parts.push({ text: `${dayLabel(sessionDate)} session`, rank: 1 });
99
+ if (Number.isFinite(board.fetchedAt) && board.fetchedAt > 0) {
100
+ const { hour, minute } = zonedDateTimeParts(board.fetchedAt, NEW_YORK);
101
+ const clock = `${twoDigits(hour)}:${twoDigits(minute)} ET`;
102
+ const day = zonedDateKey(board.fetchedAt, NEW_YORK);
103
+ const text = `snapshot ${day === (sessionDate ?? zonedDateKey(now, NEW_YORK)) ? clock : `${dayLabel(day)} ${clock}`}`;
104
+ parts.push({ text, rank: failed ? 3 : 2 });
105
+ }
106
+ if (checked) parts.push({ text: `checked ${checked}`, rank: failed ? 2 : 3 });
107
+ const caption = () => parts.map((part) => part.text).join(" · ");
108
+ while (parts.length > 0 && displayWidth(caption()) > maxWidth) {
109
+ const weakest = parts.reduce((found, part, index) => (part.rank > parts[found]!.rank ? index : found), 0);
110
+ parts.splice(weakest, 1);
111
+ }
112
+ return parts.length > 0 ? caption() : null;
113
+ }
114
+
24
115
  export type HeatmapGrouping = "sector-industry" | "sector" | "flat";
25
116
 
26
117
  /**
@@ -3,7 +3,12 @@ import type { MarketHeatmapAsset, MarketHeatmapUniverseId } from "../../../api-c
3
3
  import { useAppSelector, usePaneStateValue } from "../../../state/app/context";
4
4
  import type { ColumnConfig } from "../../../types/config";
5
5
  import type { TickerFinancials } from "../../../types/financials";
6
- import { getSessionMoveDisplay, type ExtendedSession } from "../../../market-data/market/status";
6
+ import {
7
+ getRegularSessionMoveDisplay,
8
+ getSessionMoveDisplay,
9
+ type ActiveQuoteDisplay,
10
+ type ExtendedSession,
11
+ } from "../../../market-data/market/status";
7
12
  import type { TickerRecord } from "../../../types/ticker";
8
13
  import { getSortValue, type ColumnContext } from "../portfolio-list/metrics";
9
14
 
@@ -195,6 +200,7 @@ export function buildPortfolioHeatmapAssets({
195
200
  currency,
196
201
  exchangeRates,
197
202
  sizeBy = "sqrt-market-cap",
203
+ regularSession = false,
198
204
  }: {
199
205
  tickers: readonly TickerRecord[];
200
206
  financials: ReadonlyMap<string, TickerFinancials>;
@@ -204,6 +210,8 @@ export function buildPortfolioHeatmapAssets({
204
210
  currency: string;
205
211
  exchangeRates: Map<string, number>;
206
212
  sizeBy?: HeatmapSizeBy;
213
+ /** Color by the regular session alone, never an extended print (`getRegularSessionMoveDisplay`). */
214
+ regularSession?: boolean;
207
215
  }): { assets: HeatmapBoardAsset[]; omitted: number } {
208
216
  const context: ColumnContext = {
209
217
  activeTab: kind === "portfolio" ? collectionId : undefined,
@@ -217,7 +225,9 @@ export function buildPortfolioHeatmapAssets({
217
225
  const snapshot = financials.get(symbol);
218
226
  const quote = snapshot?.quote;
219
227
  // Colored like the market boards: an open extended session's move, else the regular session's.
220
- const display = getSessionMoveDisplay(quote);
228
+ const display: (ActiveQuoteDisplay & { session?: ExtendedSession }) | null = regularSession
229
+ ? getRegularSessionMoveDisplay(quote)
230
+ : getSessionMoveDisplay(quote);
221
231
  const price = display != null && Number.isFinite(display.price) ? display.price : null;
222
232
  const size = positiveNumber(getSortValue(sizeColumn, ticker, snapshot, context));
223
233
  const hasChange = display?.changePercent != null && Number.isFinite(display.changePercent);
@@ -10,14 +10,18 @@ import type {
10
10
  CloudSessionMoversSide,
11
11
  } from "../../../api-client/market-movers";
12
12
  import { loadMarketMoverTab, loadSessionMovers, type MarketMoverTabResult } from "./client";
13
- import { createRows, fiftyTwoWeekPositionPercent, formatMoverPrice, moverReferencePrice, type MarketMoverRow, type ScreenerTabId, type TabId } from "./model";
13
+ import { createRows, fiftyTwoWeekPositionPercent, formatMoverPrice, moverExtendedSessions, moverReferencePrice, type MarketMoverRow, type ScreenerTabId, type TabId } from "./model";
14
+ import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
15
+ import type { HeadlessPaneColumn } from "../../../types/headless";
14
16
  import { isSessionTab, resolveSide } from "./session";
15
17
 
16
- const COLUMNS = [
18
+ const rowPrice = (value: unknown, row: Record<string, unknown>) => formatMoverPrice(typeof value === "number" ? value : null, typeof row.currency === "string" ? row.currency : "", moverReferencePrice(row as unknown as MarketMoverRow));
19
+
20
+ const COLUMNS: HeadlessPaneColumn[] = [
17
21
  { key: "rank", header: "Rank", align: "right" as const },
18
22
  { key: "symbol", header: "Symbol" },
19
23
  { key: "name", header: "Name" },
20
- { key: "price", header: "Last", align: "right" as const, format: (value: unknown, row: Record<string, unknown>) => formatMoverPrice(typeof value === "number" ? value : null, typeof row.currency === "string" ? row.currency : "", moverReferencePrice(row as unknown as MarketMoverRow)) },
24
+ { key: "price", header: "Last", align: "right" as const, format: rowPrice },
21
25
  { key: "changePercent", header: "Change %", align: "right" as const, format: (value: unknown) => value == null ? "—" : formatPercentRaw(Number(value)) },
22
26
  { key: "volume", header: "Volume", align: "right" as const, format: (value: unknown) => value == null ? "—" : formatCompact(Number(value), { fixedDecimals: true }) },
23
27
  { key: "volumeRatio", header: "Vol / Avg", align: "right" as const, format: (value: unknown) => value == null ? "—" : formatNumber(Number(value), 1) },
@@ -30,6 +34,19 @@ const percent = (value: unknown) => value == null ? "—" : formatPercentRaw(Num
30
34
  const ratio = (value: unknown) => value == null ? "—" : `${formatNumber(Number(value), 1)}x`;
31
35
  const compact = (value: unknown) => value == null ? "—" : formatCompact(Number(value), { fixedDecimals: true });
32
36
  const CATALYST_LABEL: Record<string, string> = { halt: "Halt", filing: "8-K", news: "News" };
37
+
38
+ /** The pre-market or after-hours print and its move from the regular close, for the rows that have one. */
39
+ function extendedColumn(session: ExtendedSession): HeadlessPaneColumn {
40
+ return {
41
+ key: "extendedPrice",
42
+ header: EXTENDED_SESSION_LABELS[session],
43
+ align: "right",
44
+ format: (value, row) => row.extendedSession === session && typeof value === "number"
45
+ ? `${rowPrice(value, row)} ${percent(row.extendedChangePercent)}`
46
+ : "—",
47
+ };
48
+ }
49
+
33
50
  const SESSION_VOLUME: Record<CloudSessionMoversCategory, string> = {
34
51
  premarket: "Pre-market volume",
35
52
  afterhours: "After-hours volume",
@@ -131,8 +148,15 @@ export function createMarketMoversHeadless(
131
148
  };
132
149
  }
133
150
  const result = await dependencies.load(args, tab, ctx.marketData);
134
- const rows = createRows(result.quotes).map((row) => ({
151
+ const ranked = createRows(result.quotes);
152
+ // Last and Change % are the regular session; an extended print is a column of its own.
153
+ const extendedSessions = moverExtendedSessions(ranked);
154
+ const rows = ranked.map(({ extended, ...row }) => ({
135
155
  ...row,
156
+ extendedSession: extended?.session ?? null,
157
+ extendedPrice: extended?.price ?? null,
158
+ extendedChange: extended?.change ?? null,
159
+ extendedChangePercent: extended?.changePercent ?? null,
136
160
  rangePositionPercent: fiftyTwoWeekPositionPercent(
137
161
  row.price,
138
162
  row.fiftyTwoWeekLow,
@@ -140,6 +164,9 @@ export function createMarketMoversHeadless(
140
164
  ),
141
165
  }));
142
166
  return {
167
+ ...(extendedSessions.length > 0
168
+ ? { columns: [...COLUMNS.slice(0, 5), ...extendedSessions.map(extendedColumn), ...COLUMNS.slice(5)] }
169
+ : {}),
143
170
  rows,
144
171
  metadata: { list: tab, source: result.source, stale: result.stale },
145
172
  };
@@ -25,6 +25,7 @@ import {
25
25
  INDEX_SHORT,
26
26
  TABS,
27
27
  createRows,
28
+ moverExtendedSessions,
28
29
  overlayMarketMoverQuotes,
29
30
  resolveSummarySymbols,
30
31
  resolveTabs,
@@ -42,6 +43,7 @@ import { isSessionTab, resolveActiveTab, usSessionAt, type UsSession } from "./s
42
43
  import { SessionMoversBody } from "./session-body";
43
44
  import { marketMoversHeadless } from "./headless";
44
45
  import { buildMarketMoverColumns, renderMarketMoverCell } from "./table";
46
+ import type { ExtendedSession } from "../../../market-data/market/status";
45
47
  import {
46
48
  LIVE_STREAMING_QUICK_SETTING,
47
49
  useLiveStreamingSetting,
@@ -188,8 +190,13 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
188
190
  }),
189
191
  [freshnessNow, liveQuoteEntries, quoteTargets, subscriptionStartedAt],
190
192
  );
191
- const columns = useMemo(() => buildMarketMoverColumns(width), [width]);
192
193
  const rankedRows = useMemo(() => createRows(resolvedQuotes), [resolvedQuotes]);
194
+ // A pre-market or after-hours column only while a row has such a print.
195
+ const extendedSessionsKey = moverExtendedSessions(rankedRows).join(",");
196
+ const columns = useMemo(
197
+ () => buildMarketMoverColumns(width, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : []),
198
+ [extendedSessionsKey, width],
199
+ );
193
200
  const rows = useMemo(() => sortRows(rankedRows, sortPreference), [rankedRows, sortPreference]);
194
201
  const selectedIdx = selectedSymbol
195
202
  ? rows.findIndex((row) => moverKey(row) === selectedSymbol)