gloomberb 0.9.7 → 0.9.9

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 (276) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.test.ts +3 -3
  4. package/src/api-client/index.ts +48 -6
  5. package/src/api-client/request.test.ts +63 -0
  6. package/src/api-client/request.ts +53 -5
  7. package/src/api-client/types.ts +45 -2
  8. package/src/app/pane-runtime/layout-placement.ts +0 -1
  9. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  10. package/src/app/runtime/desktop-deeplink.ts +496 -0
  11. package/src/app.tsx +34 -0
  12. package/src/brokers/account-cache.ts +2 -10
  13. package/src/brokers/broker-portfolio-sync.ts +0 -1
  14. package/src/brokers/broker-ticker-sync.ts +20 -16
  15. package/src/brokers/remote-broker-adapter.ts +3 -0
  16. package/src/capabilities/registry.test.ts +25 -0
  17. package/src/capabilities/registry.ts +22 -5
  18. package/src/cli/commands/automation.ts +1 -3
  19. package/src/cli/commands/command-utils.ts +0 -7
  20. package/src/cli/commands/overview.ts +2 -2
  21. package/src/cli/commands/plugins.ts +34 -17
  22. package/src/cli/index.ts +2 -2
  23. package/src/cli.test.ts +0 -157
  24. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  25. package/src/components/chart/bar-chart-renderer.ts +111 -55
  26. package/src/components/chart/comparison/renderer.ts +0 -1
  27. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  28. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  29. package/src/components/chart/core/index.test.ts +0 -24
  30. package/src/components/chart/core/renderer.ts +0 -4
  31. package/src/components/chart/core/resolution.test.ts +0 -17
  32. package/src/components/chart/core/resolution.ts +0 -26
  33. package/src/components/chart/core/scroll.test.ts +0 -6
  34. package/src/components/chart/core/scroll.ts +0 -22
  35. package/src/components/chart/core/viewport.test.ts +2 -23
  36. package/src/components/chart/core/viewport.ts +0 -27
  37. package/src/components/chart/market-session.ts +1 -1
  38. package/src/components/chart/native/surface/hooks.ts +10 -3
  39. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  40. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  41. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  42. package/src/components/chart/stock-chart/footer.ts +68 -13
  43. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  44. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  45. package/src/components/command-bar/helpers.test.ts +1 -80
  46. package/src/components/command-bar/helpers.ts +1 -36
  47. package/src/components/command-bar/pane-settings.ts +1 -1
  48. package/src/components/command-bar/pane-templates/items.ts +11 -3
  49. package/src/components/command-bar/panel/runtime.ts +0 -2
  50. package/src/components/command-bar/routes/root/results.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  52. package/src/components/command-bar/surface/index.test.tsx +52 -16
  53. package/src/components/command-bar/surface/index.tsx +0 -1
  54. package/src/components/command-bar/workflow/ops.ts +81 -20
  55. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  56. package/src/components/feed-data-table/stack-view.tsx +0 -1
  57. package/src/components/index.ts +1 -0
  58. package/src/components/layout/shell/index.test.tsx +2 -113
  59. package/src/components/layout/shell/index.tsx +0 -1
  60. package/src/components/layout/shell/menu.ts +0 -1
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  62. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  63. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  64. package/src/components/price-selector-dialog.tsx +3 -3
  65. package/src/components/ticker/badge/text.tsx +0 -1
  66. package/src/components/toggle-list.tsx +29 -73
  67. package/src/components/ui/button.tsx +4 -1
  68. package/src/components/ui/checkbox.tsx +76 -0
  69. package/src/components/ui/choice-dialog.tsx +15 -3
  70. package/src/components/ui/frame.tsx +1 -1
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/message-composer.tsx +4 -0
  73. package/src/components/ui/native-select.tsx +4 -2
  74. package/src/components/ui/page-stack-view.tsx +1 -1
  75. package/src/components/ui/ui.test.tsx +0 -78
  76. package/src/core/state/app/layout-reducer.ts +0 -1
  77. package/src/data/config/layout.ts +3 -3
  78. package/src/data/resource-store.ts +6 -0
  79. package/src/data/sqlite/cache.test.ts +5 -0
  80. package/src/data/ticker-store.ts +0 -7
  81. package/src/market-data/coordinator/auxiliary.ts +1 -44
  82. package/src/market-data/coordinator/entries.ts +0 -1
  83. package/src/market-data/coordinator/events.test.ts +49 -0
  84. package/src/market-data/coordinator/events.ts +25 -4
  85. package/src/market-data/coordinator/index.test.ts +223 -10
  86. package/src/market-data/coordinator/index.ts +41 -21
  87. package/src/market-data/coordinator/quotes.ts +13 -11
  88. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  89. package/src/market-data/hooks.tsx +17 -13
  90. package/src/market-data/market/freshness.ts +58 -1
  91. package/src/market-data/market/status.ts +19 -1
  92. package/src/market-data/query-store.ts +0 -4
  93. package/src/market-data/quotes/freshness.test.ts +49 -1
  94. package/src/market-data/quotes/freshness.ts +8 -0
  95. package/src/market-data/quotes/resolution.test.ts +185 -0
  96. package/src/market-data/quotes/resolution.ts +96 -10
  97. package/src/market-data/request-types.test.ts +61 -1
  98. package/src/market-data/request-types.ts +42 -8
  99. package/src/market-data/result-types.ts +0 -11
  100. package/src/market-data/selectors.ts +1 -5
  101. package/src/news/aggregator.ts +5 -1
  102. package/src/pane-settings.test.ts +0 -18
  103. package/src/plugins/builtin/account-management/footer.ts +2 -5
  104. package/src/plugins/builtin/account-management/form-components.tsx +175 -59
  105. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  106. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  107. package/src/plugins/builtin/account-management/model.ts +259 -23
  108. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  109. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  110. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  111. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  112. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  113. package/src/plugins/builtin/buildout/format.ts +2 -11
  114. package/src/plugins/builtin/chat/chat.test.tsx +127 -1
  115. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  116. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  117. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  118. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  119. package/src/plugins/builtin/chat/content/index.tsx +153 -16
  120. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  121. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  122. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  123. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  124. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  125. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  126. package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
  127. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  128. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
  132. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  133. package/src/plugins/builtin/chat/test-harness.tsx +22 -12
  134. package/src/plugins/builtin/cloud/plugin.tsx +8 -1
  135. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  136. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  137. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  138. package/src/plugins/builtin/correlation/index.tsx +4 -1
  139. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  140. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  141. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  142. package/src/plugins/builtin/earnings/index.tsx +0 -9
  143. package/src/plugins/builtin/econ/index.tsx +1 -5
  144. package/src/plugins/builtin/help/index.tsx +2 -2
  145. package/src/plugins/builtin/holders/pane.tsx +0 -5
  146. package/src/plugins/builtin/insider/index.tsx +1 -8
  147. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  148. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  149. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  150. package/src/plugins/builtin/news/index.tsx +0 -1
  151. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  152. package/src/plugins/builtin/options/view.test.tsx +1 -38
  153. package/src/plugins/builtin/plugin-groups.ts +17 -3
  154. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  155. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
  156. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  157. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  158. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  159. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  160. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  161. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  162. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  163. package/src/plugins/builtin/sec/index.tsx +0 -2
  164. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  165. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  166. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  167. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
  168. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  169. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  170. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  171. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  172. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  173. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  174. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  175. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  176. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  177. package/src/plugins/builtin/world-indices/model.ts +0 -7
  178. package/src/plugins/ibkr/account-cache.ts +1 -1
  179. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  180. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  181. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  182. package/src/plugins/ibkr/trade/utils.ts +1 -7
  183. package/src/plugins/loader.ts +2 -1
  184. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  185. package/src/plugins/registry/context.ts +15 -0
  186. package/src/plugins/registry/contributions.ts +22 -11
  187. package/src/plugins/registry/index.test.ts +44 -0
  188. package/src/plugins/registry/index.ts +75 -26
  189. package/src/remote/controller.ts +1 -7
  190. package/src/remote/json-patch.ts +15 -1
  191. package/src/remote/server.ts +16 -6
  192. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  193. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  194. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  195. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  196. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  197. package/src/renderers/electrobun/bun/index.ts +42 -0
  198. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  199. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  201. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  202. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  203. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  204. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  205. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  206. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  207. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  208. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  209. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  210. package/src/renderers/electrobun/view/main.tsx +3 -0
  211. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  212. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  213. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  214. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  215. package/src/renderers/opentui/host.tsx +11 -3
  216. package/src/renderers/opentui/input-host.tsx +5 -1
  217. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  218. package/src/sources/gloomberb-cloud/index.ts +1 -30
  219. package/src/sources/gloomberb-cloud/news.ts +0 -12
  220. package/src/sources/provider-router/batches.ts +11 -3
  221. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  222. package/src/sources/provider-router/financial-routes.ts +77 -5
  223. package/src/sources/provider-router/financials.test.ts +49 -0
  224. package/src/sources/provider-router/financials.ts +40 -3
  225. package/src/sources/provider-router/index.test.ts +314 -0
  226. package/src/sources/provider-router/primary.ts +17 -3
  227. package/src/sources/provider-router/search.ts +5 -0
  228. package/src/sources/provider-router/streaming.ts +6 -1
  229. package/src/sources/provider-router/supplemental.ts +4 -3
  230. package/src/sources/sec-edgar.test.ts +42 -0
  231. package/src/sources/sec-edgar.ts +1 -2
  232. package/src/sources/yahoo-finance/history.ts +5 -1
  233. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  234. package/src/sources/yahoo-finance/mappers.ts +2 -1
  235. package/src/sources/yahoo-finance/options.ts +59 -14
  236. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  237. package/src/sources/yahoo-finance/requests.ts +2 -1
  238. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  239. package/src/sources/yahoo-finance.test.ts +40 -21
  240. package/src/sources/yahoo-finance.ts +19 -10
  241. package/src/state/app/context/index.test.tsx +0 -81
  242. package/src/state/app/context/selector.test.tsx +6 -2
  243. package/src/state/hooks/quote-streaming.test.ts +14 -1
  244. package/src/state/hooks/quote-streaming.ts +5 -4
  245. package/src/sync/controller.test.ts +31 -0
  246. package/src/sync/controller.ts +300 -0
  247. package/src/sync/core-contributors.test.ts +251 -0
  248. package/src/sync/core-contributors.ts +400 -0
  249. package/src/sync/profile-analytics.ts +39 -0
  250. package/src/sync/react.ts +53 -0
  251. package/src/sync/types.ts +77 -0
  252. package/src/test-support/data-provider.ts +0 -1
  253. package/src/types/desktop-deeplink.ts +7 -0
  254. package/src/types/plugin.ts +3 -0
  255. package/src/ui/host.tsx +15 -1
  256. package/src/ui/index.tsx +2 -0
  257. package/src/updater.test.ts +0 -21
  258. package/src/updater.ts +20 -16
  259. package/src/utils/async-deadline.ts +26 -0
  260. package/src/utils/exchanges.test.ts +11 -1
  261. package/src/utils/exchanges.ts +5 -1
  262. package/src/utils/selection-clipboard.ts +2 -2
  263. package/src/version.ts +1 -1
  264. package/src/components/layout/pane/header.test.tsx +0 -39
  265. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  266. package/src/plugins/builtin/chat/index.tsx +0 -4
  267. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  268. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  269. package/src/plugins/builtin/help/index.test.tsx +0 -242
  270. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  271. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  272. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  273. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  274. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  275. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  276. /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
@@ -31,7 +31,7 @@ const HELP_TABS = [
31
31
  type HelpTabId = typeof HELP_TABS[number]["value"];
32
32
  const GLOOMBERB_ISSUES_URL = "https://github.com/vincelwt/gloomberb/issues";
33
33
 
34
- export function HelpPane({ focused, width, height }: PaneProps) {
34
+ function HelpPane({ focused, width, height }: PaneProps) {
35
35
  const registry = getSharedRegistry();
36
36
  const { openCommandBar, showPane } = usePluginAppActions();
37
37
  const [activeTabId, setActiveTabId] = useState<HelpTabId>("basics");
@@ -381,7 +381,7 @@ export function HelpPane({ focused, width, height }: PaneProps) {
381
381
  <Box flexDirection="column" width={width} height={height}>
382
382
  <Box width={width} height={1} flexShrink={0}>
383
383
  <Tabs
384
- tabs={HELP_TABS}
384
+ tabs={[...HELP_TABS]}
385
385
  activeValue={activeTabId}
386
386
  onSelect={(value) => setActiveTabId(value as HelpTabId)}
387
387
  focused={focused}
@@ -127,11 +127,6 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
127
127
  };
128
128
  }, [sortedRows]);
129
129
 
130
- const selectIndex = useCallback((index: number) => {
131
- const row = sortedRows[index];
132
- if (row) setSelectedId(row.id);
133
- }, [sortedRows]);
134
-
135
130
  const selectedRow = activeIdx >= 0 ? sortedRows[activeIdx] ?? null : null;
136
131
  const selectedFundMatch = selectedRow ? fundMatches.get(selectedRow.id) ?? null : null;
137
132
 
@@ -14,6 +14,7 @@ import { FeedDataTableStackView, Spinner, useExternalLinkFooter, type FeedDataTa
14
14
  import { usePluginPaneState } from "../../runtime";
15
15
  import { isUsEquityTicker } from "../../../utils/sec";
16
16
  import { formatCompact, formatCurrency } from "../../../utils/format";
17
+ import { truncateWithEllipsis as truncateText } from "../../../utils/text-wrap";
17
18
  import { parseForm4Xml, type InsiderTransaction } from "./insider-data";
18
19
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
19
20
  import {
@@ -61,13 +62,6 @@ function buildSummary(parsed: ParsedFiling[]): string {
61
62
  return parts.join(" | ");
62
63
  }
63
64
 
64
- function truncateText(text: string, width: number): string {
65
- if (width <= 0) return "";
66
- if (text.length <= width) return text;
67
- if (width <= 3) return text.slice(0, width);
68
- return `${text.slice(0, width - 3)}...`;
69
- }
70
-
71
65
  function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
72
66
  return parsed.map(({ filing, transaction, isLoading }) => {
73
67
  const filingMeta = [
@@ -95,7 +89,6 @@ function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
95
89
  eyebrow: transaction.reportedName,
96
90
  title: buildInsiderTransactionTitle(transaction),
97
91
  timestamp: transaction.filingDate,
98
- preview: transaction.title || undefined,
99
92
  detailTitle: transaction.reportedName,
100
93
  detailMeta: [
101
94
  ...(transaction.title ? [transaction.title] : []),
@@ -2,7 +2,6 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { act } from "react";
3
3
  import { testRender } from "../../../renderers/opentui/test-utils";
4
4
  import { setSharedMarketDataCoordinator } from "../../../market-data/coordinator";
5
- import type { TickerRecord } from "../../../types/ticker";
6
5
  import { KellySizerHarness, createFinancials, createSizerConfig, createTicker } from "./test-support";
7
6
 
8
7
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
@@ -36,31 +35,6 @@ afterEach(async () => {
36
35
  });
37
36
 
38
37
  describe("KellySizerPane", () => {
39
- test("renders binary sizing context, result, and curve", async () => {
40
- await renderPane();
41
- await flushFrame();
42
-
43
- const frame = testSetup!.captureCharFrame();
44
- expect(frame).toContain("AAPL · Main Portfolio");
45
- expect(frame).toContain("Bankroll");
46
- expect(frame).toContain("100000");
47
- expect(frame).toContain("Win p");
48
- expect(frame).toContain("Full Kelly");
49
- expect(frame).toContain("Clipped");
50
- expect(frame).toContain("Target val");
51
- expect(frame).toContain("Current %");
52
- expect(frame).toContain("Kelly Curve");
53
- expect(frame).toContain("4.0% -> 8.0%");
54
- expect(frame).toContain("growth");
55
- expect(frame).toContain("loss cap");
56
- expect(frame).toContain("cap max loss, max name");
57
- expect(frame).toContain("full");
58
- expect(frame).toContain("159%");
59
- expect(frame).toContain("636%");
60
- expect(frame).not.toContain("Mode");
61
- expect(frame).not.toContain("Sensitivity");
62
- });
63
-
64
38
  test("prefills shared caps from plugin config", async () => {
65
39
  const config = {
66
40
  ...createSizerConfig(),
@@ -43,5 +43,3 @@ export const kellySizerPlugin: GloomPlugin = {
43
43
  },
44
44
  ],
45
45
  };
46
-
47
- export default kellySizerPlugin;
@@ -51,8 +51,6 @@ function MarketMoversPane({ focused, width, height }: PaneProps) {
51
51
  const selectedIdx = selectedSymbol
52
52
  ? rows.findIndex((row) => row.symbol === selectedSymbol)
53
53
  : -1;
54
- const activeIdx = selectedIdx >= 0 ? selectedIdx : (rows.length > 0 ? 0 : -1);
55
-
56
54
  useEffect(() => {
57
55
  if (selectedSymbol && selectedIdx >= 0) return;
58
56
  const firstRow = rows[0];
@@ -32,7 +32,6 @@ function getFeedItems(
32
32
  eyebrow: item.source,
33
33
  title: item.title,
34
34
  timestamp: item.publishedAt,
35
- preview,
36
35
  detailTitle: item.title,
37
36
  detailMeta: [
38
37
  item.source,
@@ -3,7 +3,6 @@ import { TextAttributes } from "../../../../../ui";
3
3
  import {
4
4
  DataTableStackView,
5
5
  TickerBadgeList,
6
- activeStackIndex,
7
6
  sortStackItems,
8
7
  type DataTableCell,
9
8
  type DataTableColumn,
@@ -165,23 +164,13 @@ export function NewsArticleStackView({
165
164
  [articles, sortPreference],
166
165
  );
167
166
  const selectedIdx = sortedArticles.findIndex((article) => article.id === selectedArticleId);
168
- const activeIdx = activeStackIndex(sortedArticles.length, selectedIdx);
169
167
  const columns = useMemo(() => buildColumns(width, columnIds), [columnIds, width]);
170
168
 
171
- const selectIndex = useCallback((index: number) => {
172
- setSelectedArticleId(sortedArticles[index]?.id ?? null);
173
- }, [setSelectedArticleId, sortedArticles]);
174
-
175
169
  const openArticle = useCallback((article: MarketNewsItem) => {
176
170
  onArticleRead?.(article.id);
177
171
  onOpenArticle(article);
178
172
  }, [onArticleRead, onOpenArticle]);
179
173
 
180
- const openIndex = useCallback((index: number) => {
181
- const article = sortedArticles[index];
182
- if (article) openArticle(article);
183
- }, [openArticle, sortedArticles]);
184
-
185
174
  useEffect(() => {
186
175
  if (sortedArticles.length === 0) {
187
176
  if (selectedArticleId !== null) setSelectedArticleId(null);
@@ -123,8 +123,8 @@ function OptionsHarness({
123
123
  async function renderSettled() {
124
124
  for (let i = 0; i < 4; i += 1) {
125
125
  await act(async () => {
126
+ await new Promise((resolve) => setTimeout(resolve, 0));
126
127
  await testSetup!.renderOnce();
127
- await Promise.resolve();
128
128
  });
129
129
  }
130
130
  }
@@ -139,43 +139,6 @@ afterEach(async () => {
139
139
  setSharedMarketDataCoordinator(null);
140
140
  });
141
141
 
142
- test("renders the options chain with the shared table columns", async () => {
143
- const provider = createTestDataProvider({
144
- getOptionsChain: async () => makeChain(),
145
- });
146
- setSharedMarketDataCoordinator(new MarketDataCoordinator(provider));
147
-
148
- await act(async () => {
149
- testSetup = await testRender(<OptionsHarness ticker={makeTicker("AAPL")} />, {
150
- width: 124,
151
- height: 16,
152
- });
153
- });
154
-
155
- await renderSettled();
156
-
157
- const frame = testSetup!.captureCharFrame();
158
- expect(frame).toContain("C LAST");
159
- expect(frame).toContain("C IV");
160
- expect(frame).toContain("STRIKE");
161
- expect(frame).toContain("P IV");
162
- expect(frame).toContain("P LAST");
163
- expect(frame).toContain("100");
164
- expect(frame).toContain("101");
165
-
166
- const headerLine = frame.split("\n").find((line) => line.includes("C OI") && line.includes("STRIKE")) ?? "";
167
- expect(headerLine.indexOf("C OI")).toBeLessThan(headerLine.indexOf("C VOL"));
168
- expect(headerLine.indexOf("C VOL")).toBeLessThan(headerLine.indexOf("C LAST"));
169
- expect(headerLine.indexOf("C LAST")).toBeLessThan(headerLine.indexOf("C IV"));
170
- expect(headerLine.indexOf("C IV")).toBeLessThan(headerLine.indexOf("C BID"));
171
- expect(headerLine.indexOf("C BID")).toBeLessThan(headerLine.indexOf("C ASK"));
172
- expect(headerLine.indexOf("P BID")).toBeLessThan(headerLine.indexOf("P ASK"));
173
- expect(headerLine.indexOf("P ASK")).toBeLessThan(headerLine.indexOf("P IV"));
174
- expect(headerLine.indexOf("P IV")).toBeLessThan(headerLine.indexOf("P LAST"));
175
- expect(headerLine.indexOf("P LAST")).toBeLessThan(headerLine.indexOf("P VOL"));
176
- expect(headerLine.indexOf("P VOL")).toBeLessThan(headerLine.indexOf("P OI"));
177
- });
178
-
179
142
  test("defaults the table around the nearest strike to the current quote", async () => {
180
143
  const strikes = Array.from({ length: 25 }, (_, index) => 50 + index * 5);
181
144
  const provider = createTestDataProvider({
@@ -29,6 +29,7 @@ type PluginGroupOptions = Pick<
29
29
  };
30
30
 
31
31
  function createPluginGroup(options: PluginGroupOptions): GloomPlugin {
32
+ let startedPlugins: GloomPlugin[] = [];
32
33
  return {
33
34
  id: options.id,
34
35
  name: options.name,
@@ -42,17 +43,30 @@ function createPluginGroup(options: PluginGroupOptions): GloomPlugin {
42
43
  capabilities: options.plugins.flatMap((plugin) => plugin.capabilities ?? []),
43
44
 
44
45
  async setup(ctx) {
46
+ startedPlugins = [];
45
47
  await options.setup?.(ctx);
46
48
  for (const plugin of options.plugins) {
49
+ startedPlugins.push(plugin);
47
50
  await plugin.setup?.(ctx);
48
51
  }
49
52
  },
50
53
 
51
54
  dispose() {
52
- for (const plugin of [...options.plugins].reverse()) {
53
- plugin.dispose?.();
55
+ let firstError: unknown;
56
+ for (const plugin of startedPlugins.reverse()) {
57
+ try {
58
+ plugin.dispose?.();
59
+ } catch (error) {
60
+ firstError ??= error;
61
+ }
54
62
  }
55
- options.dispose?.();
63
+ startedPlugins = [];
64
+ try {
65
+ options.dispose?.();
66
+ } catch (error) {
67
+ firstError ??= error;
68
+ }
69
+ if (firstError) throw firstError;
56
70
  },
57
71
  };
58
72
  }
@@ -1,5 +1,5 @@
1
1
  import type { ColumnConfig } from "../../../types/config";
2
- import type { AnalystResearchData, CorporateActionsData, TickerFinancials } from "../../../types/financials";
2
+ import type { AnalystResearchData, CorporateActionsData, MarketState, TickerFinancials } from "../../../types/financials";
3
3
  import type { EarningsEvent } from "../../../types/data-provider";
4
4
  import type { TickerRecord } from "../../../types/ticker";
5
5
  import { priceColor } from "../../../theme/colors";
@@ -13,7 +13,13 @@ import {
13
13
  formatSignedMarketPrice,
14
14
  type MarketFormatOptions,
15
15
  } from "../../../market-data/market/format";
16
- import { getActiveQuoteDisplay, marketStateDot, type ActiveQuoteDisplay } from "../../../market-data/market/status";
16
+ import {
17
+ getActiveQuoteDisplay,
18
+ marketChangeColor,
19
+ marketPriceColor,
20
+ marketStateDot,
21
+ type ActiveQuoteDisplay,
22
+ } from "../../../market-data/market/status";
17
23
  import { formatOptionTicker } from "../../../utils/options";
18
24
  import { PRICE_SPARKLINE_COLUMN_ID } from "../../../components/price-sparkline/view";
19
25
  import {
@@ -159,13 +165,14 @@ function getActiveMarketValue(
159
165
  export function resolvePortfolioPriceValue(
160
166
  activeQuote: ActiveQuoteDisplay | null,
161
167
  brokerMarkPrice: number | undefined,
162
- formatOptions: MarketFormatOptions,
168
+ formatOptions: MarketFormatOptions = {},
163
169
  maxWidth?: number,
170
+ marketState?: MarketState,
164
171
  ): { text: string; color?: string } {
165
172
  if (activeQuote) {
166
173
  return {
167
174
  text: formatMarketPrice(activeQuote.price, { ...formatOptions, maxWidth }),
168
- color: priceColor(activeQuote.change),
175
+ color: marketPriceColor(activeQuote.change, marketState),
169
176
  };
170
177
  }
171
178
  if (brokerMarkPrice != null) {
@@ -229,12 +236,12 @@ export function getColumnValue(
229
236
  case "tags":
230
237
  return { text: ticker.metadata.tags.length > 0 ? ticker.metadata.tags.join(",") : "—" };
231
238
  case "price":
232
- return resolvePortfolioPriceValue(activeQuote, brokerMarkPrice, formatOptions, col.width);
239
+ return resolvePortfolioPriceValue(activeQuote, brokerMarkPrice, formatOptions, col.width, quote?.marketState);
233
240
  case "change":
234
241
  if (!activeQuote) return { text: "—" };
235
242
  return {
236
243
  text: formatSignedMarketPrice(activeQuote.change, { ...formatOptions, maxWidth: col.width }),
237
- color: priceColor(activeQuote.change),
244
+ color: marketChangeColor(activeQuote.change, quote?.marketState),
238
245
  };
239
246
  case "bid":
240
247
  return { text: quote?.bid != null ? formatMarketPrice(quote.bid, { ...formatOptions, maxWidth: col.width }) : "—" };
@@ -258,8 +265,8 @@ export function getColumnValue(
258
265
  }
259
266
  case "change_pct":
260
267
  return activeQuote
261
- ? { text: formatPercentRaw(activeQuote.changePercent), color: priceColor(activeQuote.changePercent) }
262
- : { text: quote ? formatPercentRaw(quote.changePercent) : "—", color: quote ? priceColor(quote.changePercent) : undefined };
268
+ ? { text: formatPercentRaw(activeQuote.changePercent), color: marketChangeColor(activeQuote.changePercent, quote?.marketState) }
269
+ : { text: quote ? formatPercentRaw(quote.changePercent) : "—", color: quote ? marketChangeColor(quote.changePercent, quote.marketState) : undefined };
263
270
  case "volume":
264
271
  return { text: finiteNumber(quote?.volume) ? formatCompact(quote.volume) : "—" };
265
272
  case "dollar_volume": {
@@ -5,8 +5,8 @@ import type { BrokerAccount } from "../../../types/trading";
5
5
  import type { TickerRecord } from "../../../types/ticker";
6
6
  import type { PortfolioSummaryTotals } from "./metrics";
7
7
  import { buildPortfolioSummarySegments } from "./summary";
8
- import { portfolioListPlugin, shouldToggleCashMarginDrawer } from ".";
9
- import { needsVisibleQuoteWatchdogRefresh, selectStreamTickers } from "./pane/data";
8
+ import { shouldToggleCashMarginDrawer } from ".";
9
+ import { needsVisibleQuoteWatchdogRefresh, selectQuoteWarmupTickers, selectStreamTickers } from "./pane/data";
10
10
  import { buildPortfolioPaneSettingsDef, getPortfolioPaneSettings } from "./settings";
11
11
 
12
12
  function ticker(symbol: string): TickerRecord {
@@ -134,6 +134,56 @@ describe("selectStreamTickers", () => {
134
134
  });
135
135
  });
136
136
 
137
+ describe("selectQuoteWarmupTickers", () => {
138
+ function financials(quoteValue: Quote | undefined): TickerFinancials {
139
+ return {
140
+ annualStatements: [],
141
+ quarterlyStatements: [],
142
+ priceHistory: [],
143
+ quote: quoteValue,
144
+ };
145
+ }
146
+
147
+ test("includes hidden quote-missing rows when sorting by quote-dependent columns", () => {
148
+ const tickers = Array.from({ length: 30 }, (_, index) => ticker(`T${index}`));
149
+ const financialsMap = new Map<string, TickerFinancials>(
150
+ tickers.slice(0, 29).map((entry, index) => [
151
+ entry.metadata.ticker,
152
+ financials({
153
+ symbol: entry.metadata.ticker,
154
+ price: index + 1,
155
+ currency: "USD",
156
+ change: index,
157
+ changePercent: index,
158
+ lastUpdated: 1_700_000_000_000,
159
+ }),
160
+ ]),
161
+ );
162
+
163
+ const selected = selectQuoteWarmupTickers(
164
+ tickers,
165
+ { start: 0, end: 24 },
166
+ financialsMap,
167
+ { columnId: "change_pct", direction: "asc" },
168
+ 1_700_000_010_000,
169
+ ).map((entry) => entry.metadata.ticker);
170
+
171
+ expect(selected).toContain("T29");
172
+ });
173
+
174
+ test("does not add hidden rows for ticker-only sorting", () => {
175
+ const tickers = Array.from({ length: 30 }, (_, index) => ticker(`T${index}`));
176
+ expect(selectQuoteWarmupTickers(
177
+ tickers,
178
+ { start: 0, end: 24 },
179
+ new Map(),
180
+ { columnId: "ticker", direction: "asc" },
181
+ ).map((entry) => entry.metadata.ticker)).toEqual(
182
+ tickers.slice(0, 24).map((entry) => entry.metadata.ticker),
183
+ );
184
+ });
185
+ });
186
+
137
187
  describe("visible quote refresh predicates", () => {
138
188
  function quote(overrides: Partial<Quote> = {}): Quote {
139
189
  return {
@@ -171,29 +221,6 @@ describe("visible quote refresh predicates", () => {
171
221
  });
172
222
  });
173
223
 
174
- describe("portfolio list pane templates", () => {
175
- test("create panes with default all-collection settings", async () => {
176
- const config = createDefaultConfig("/tmp/gloomberb-portfolio-list-template");
177
- const context = {
178
- config,
179
- layout: config.layout,
180
- focusedPaneId: "portfolio-list:main",
181
- activeTicker: null,
182
- activeCollectionId: "watchlist",
183
- };
184
-
185
- for (const templateId of ["new-collection-pane", "new-watchlist-pane"]) {
186
- const template = portfolioListPlugin.paneTemplates?.find((entry) => entry.id === templateId);
187
- const instance = await template?.createInstance?.(context);
188
- expect(instance).toEqual({ params: { collectionId: "watchlist" } });
189
- }
190
-
191
- const portfolioTemplate = portfolioListPlugin.paneTemplates?.find((entry) => entry.id === "new-portfolio-pane");
192
- const portfolioInstance = await portfolioTemplate?.createInstance?.(context);
193
- expect(portfolioInstance).toEqual({ params: { collectionId: "main" } });
194
- });
195
- });
196
-
197
224
  describe("portfolio list pane settings", () => {
198
225
  test("exposes view mode only for portfolio collections", () => {
199
226
  const config = createDefaultConfig("/tmp/gloomberb-portfolio-list-settings");
@@ -1,8 +1,9 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { CollectionSortPreference } from "../../../state/app/context";
3
3
  import type { ColumnConfig } from "../../../types/config";
4
- import type { TickerFinancials } from "../../../types/financials";
4
+ import type { Quote, TickerFinancials } from "../../../types/financials";
5
5
  import type { TickerRecord } from "../../../types/ticker";
6
+ import { blendHex, colors } from "../../../theme/colors";
6
7
  import {
7
8
  calculatePortfolioSummaryTotals,
8
9
  getColumnValue,
@@ -29,8 +30,15 @@ function createTicker(overrides: Partial<TickerRecord["metadata"]> = {}): Ticker
29
30
  };
30
31
  }
31
32
 
32
- function createFinancials(overrides: Partial<TickerFinancials> = {}): TickerFinancials {
33
+ function createFinancials(
34
+ overrides: Omit<Partial<TickerFinancials>, "quote"> & { quote?: Partial<Quote> } = {},
35
+ ): TickerFinancials {
36
+ const { quote: quoteOverrides, ...financialOverrides } = overrides;
33
37
  return {
38
+ annualStatements: [],
39
+ quarterlyStatements: [],
40
+ priceHistory: [],
41
+ ...financialOverrides,
34
42
  quote: {
35
43
  symbol: "AAPL",
36
44
  price: 120,
@@ -39,12 +47,8 @@ function createFinancials(overrides: Partial<TickerFinancials> = {}): TickerFina
39
47
  changePercent: 4.35,
40
48
  previousClose: 115,
41
49
  lastUpdated: 1_700_000_000_000,
42
- ...overrides.quote,
50
+ ...quoteOverrides,
43
51
  },
44
- annualStatements: [],
45
- quarterlyStatements: [],
46
- priceHistory: [],
47
- ...overrides,
48
52
  };
49
53
  }
50
54
 
@@ -82,6 +86,39 @@ describe("portfolio-metrics", () => {
82
86
  });
83
87
  });
84
88
 
89
+ test("mutes completed-session prices and changes without discarding their direction", () => {
90
+ const ticker = createTicker({
91
+ positions: [{ portfolio: "main", shares: 10, avgCost: 100, broker: "manual" }],
92
+ });
93
+ const priceColumn: ColumnConfig = { id: "price", label: "LAST", width: 10, align: "right" };
94
+ const changeColumn: ColumnConfig = { id: "change", label: "CHG", width: 8, align: "right" };
95
+ const changePctColumn: ColumnConfig = { id: "change_pct", label: "CHG%", width: 8, align: "right" };
96
+ const financials = createFinancials({ quote: { marketState: "CLOSED" } });
97
+ const mutedPositive = blendHex(colors.positive, colors.textDim, 0.55);
98
+
99
+ expect(getColumnValue(priceColumn, ticker, financials, defaultColumnContext)).toEqual({
100
+ text: "120",
101
+ color: colors.textDim,
102
+ });
103
+ expect(getColumnValue(changeColumn, ticker, financials, defaultColumnContext)).toEqual({
104
+ text: "+5",
105
+ color: mutedPositive,
106
+ });
107
+ expect(getColumnValue(changePctColumn, ticker, financials, defaultColumnContext)).toEqual({
108
+ text: "+4.35%",
109
+ color: mutedPositive,
110
+ });
111
+ const unchanged = createFinancials({ quote: { change: 0, changePercent: 0, previousClose: 120, marketState: "CLOSED" } });
112
+ expect(getColumnValue(changePctColumn, ticker, unchanged, defaultColumnContext)).toEqual({
113
+ text: "0.00%",
114
+ color: colors.neutral,
115
+ });
116
+
117
+ const open = createFinancials({ quote: { marketState: "REGULAR" } });
118
+ expect(getColumnValue(priceColumn, ticker, open, defaultColumnContext).color).toBe(colors.positive);
119
+ expect(getColumnValue(changePctColumn, ticker, open, defaultColumnContext).color).toBe(colors.positive);
120
+ });
121
+
85
122
  test("calculates portfolio totals from live quotes", () => {
86
123
  const ticker = createTicker({
87
124
  positions: [{ portfolio: "main", shares: 10, avgCost: 100, broker: "manual" }],
@@ -0,0 +1,54 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { Quote, TickerFinancials } from "../../../../types/financials";
3
+ import {
4
+ needsVisibleQuoteWarmup,
5
+ needsVisibleQuoteWatchdogRefresh,
6
+ VISIBLE_QUOTE_STREAM_MAX_AGE_MS,
7
+ } from "./data";
8
+
9
+ function quote(overrides: Partial<Quote> = {}): Quote {
10
+ return {
11
+ symbol: "AAPL",
12
+ price: 100,
13
+ currency: "USD",
14
+ change: 0,
15
+ changePercent: 0,
16
+ lastUpdated: Date.now(),
17
+ ...overrides,
18
+ };
19
+ }
20
+
21
+ function financials(quoteValue: Quote): TickerFinancials {
22
+ return {
23
+ annualStatements: [],
24
+ quarterlyStatements: [],
25
+ priceHistory: [],
26
+ quote: quoteValue,
27
+ };
28
+ }
29
+
30
+ describe("portfolio visible quote warmup", () => {
31
+ test("refreshes current-session quotes once the visible-row age window expires", () => {
32
+ const now = Date.now();
33
+ const data = financials(quote({
34
+ lastUpdated: now - VISIBLE_QUOTE_STREAM_MAX_AGE_MS,
35
+ listingExchangeName: "FWB2",
36
+ marketState: "REGULAR",
37
+ }));
38
+
39
+ expect(needsVisibleQuoteWarmup(data, now)).toBe(false);
40
+ expect(needsVisibleQuoteWatchdogRefresh(data, now)).toBe(true);
41
+ });
42
+
43
+ test("treats stale active-session quotes as visible warmup misses", () => {
44
+ const now = Date.parse("2026-07-07T12:10:30Z");
45
+ const data = financials(quote({
46
+ lastUpdated: Date.parse("2026-07-07T12:10:00Z"),
47
+ listingExchangeName: "NASDAQ",
48
+ marketState: "PRE",
49
+ }));
50
+
51
+ expect(needsVisibleQuoteWarmup(data, now)).toBe(true);
52
+ expect(needsVisibleQuoteWatchdogRefresh(data, now)).toBe(true);
53
+ });
54
+ });
@@ -10,14 +10,32 @@ import { compareSortValues } from "../../../../utils/sort-values";
10
10
  import { getSortValue, type ColumnContext } from "../metrics";
11
11
  import type { ResolvedPortfolioAccountState } from "../summary";
12
12
 
13
- export const VISIBLE_QUOTE_REFRESH_COOLDOWN_MS = 60_000;
14
- export const VISIBLE_QUOTE_STREAM_MAX_AGE_MS = 60_000;
15
- export const VISIBLE_QUOTE_STREAM_WATCHDOG_MS = 30_000;
13
+ export const VISIBLE_QUOTE_REFRESH_COOLDOWN_MS = 15_000;
14
+ export const VISIBLE_QUOTE_STREAM_MAX_AGE_MS = 15_000;
15
+ export const VISIBLE_QUOTE_STREAM_WATCHDOG_MS = 5_000;
16
16
  export const VISIBLE_SNAPSHOT_REFRESH_COOLDOWN_MS = 5 * 60_000;
17
17
  export const VISIBLE_FINANCIAL_WARMUP_DELAY_MS = 350;
18
18
  export const VISIBLE_SNAPSHOT_WARMUP_BATCH_LIMIT = 3;
19
+ export const SORT_QUOTE_WARMUP_BATCH_LIMIT = 64;
19
20
 
20
21
  const STREAM_OVERSCAN_ROWS = 6;
22
+ const QUOTE_SORT_COLUMN_IDS = new Set([
23
+ "price",
24
+ "change",
25
+ "change_pct",
26
+ "volume",
27
+ "dollar_volume",
28
+ "range_52w",
29
+ "market_cap",
30
+ "latency",
31
+ "ext_hours",
32
+ "mkt_value",
33
+ "weight",
34
+ "day_pnl",
35
+ "pnl",
36
+ "pnl_pct",
37
+ "mark_delta",
38
+ ]);
21
39
  const FUNDAMENTAL_COLUMN_IDS = new Set(["pe", "forward_pe", "dividend_yield"]);
22
40
  const PROFILE_COLUMN_IDS = new Set(["sector", "industry"]);
23
41
  const sortValueCache = createRowValueCache<string, ReturnType<typeof getSortValue>>(5000);
@@ -81,6 +99,35 @@ export function selectStreamTickers(
81
99
  return selectedTicker ? [...visible, selectedTicker] : visible;
82
100
  }
83
101
 
102
+ export function sortPreferenceUsesQuote(sortPreference: CollectionSortPreference): boolean {
103
+ return !!sortPreference.columnId && QUOTE_SORT_COLUMN_IDS.has(sortPreference.columnId);
104
+ }
105
+
106
+ export function selectQuoteWarmupTickers(
107
+ sortedTickers: TickerRecord[],
108
+ visibleRange: { start: number; end: number },
109
+ financialsMap: Map<string, TickerFinancials>,
110
+ sortPreference: CollectionSortPreference,
111
+ now = Date.now(),
112
+ hiddenLimit = SORT_QUOTE_WARMUP_BATCH_LIMIT,
113
+ ): TickerRecord[] {
114
+ const visible = sortedTickers.slice(visibleRange.start, visibleRange.end);
115
+ if (!sortPreferenceUsesQuote(sortPreference)) {
116
+ return visible;
117
+ }
118
+
119
+ const seen = new Set(visible.map((ticker) => ticker.metadata.ticker));
120
+ const hidden: TickerRecord[] = [];
121
+ for (const ticker of sortedTickers) {
122
+ if (seen.has(ticker.metadata.ticker)) continue;
123
+ if (!needsVisibleQuoteWarmup(financialsMap.get(ticker.metadata.ticker), now)) continue;
124
+ hidden.push(ticker);
125
+ seen.add(ticker.metadata.ticker);
126
+ if (hidden.length >= hiddenLimit) break;
127
+ }
128
+ return [...visible, ...hidden];
129
+ }
130
+
84
131
  export function buildTrackedCurrencies(
85
132
  tickers: TickerRecord[],
86
133
  financialsMap: Map<string, TickerFinancials>,