gloomberb 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/package.json +1 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +158 -57
  4. package/src/capabilities/factories.ts +3 -0
  5. package/src/cli/registry.test.ts +0 -1
  6. package/src/components/chart/chart-controller.ts +0 -1
  7. package/src/components/chart/chart-data.ts +1 -31
  8. package/src/components/chart/chart-renderer.ts +94 -12
  9. package/src/components/chart/chart-resolution.test.ts +0 -36
  10. package/src/components/chart/chart-resolution.ts +0 -27
  11. package/src/components/chart/chart.test.ts +60 -45
  12. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  13. package/src/components/chart/comparison-chart-data.ts +1 -4
  14. package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
  15. package/src/components/chart/comparison-chart-renderer.ts +13 -6
  16. package/src/components/chart/comparison-stock-chart.tsx +63 -39
  17. package/src/components/chart/price-axis-labels.tsx +114 -0
  18. package/src/components/chart/static-chart-surface.tsx +30 -4
  19. package/src/components/chart/stock-chart.test.ts +0 -16
  20. package/src/components/chart/stock-chart.tsx +69 -41
  21. package/src/components/chart/time-axis-label.tsx +120 -0
  22. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
  23. package/src/components/command-bar/command-bar.test.tsx +182 -36
  24. package/src/components/command-bar/command-bar.tsx +943 -1016
  25. package/src/components/command-bar/command-registry.ts +0 -10
  26. package/src/components/command-bar/confirm-body.tsx +51 -0
  27. package/src/components/command-bar/helpers.ts +2 -5
  28. package/src/components/command-bar/list-model.ts +76 -0
  29. package/src/components/command-bar/theme-picker.tsx +254 -0
  30. package/src/components/command-bar/view-model.test.ts +4 -4
  31. package/src/components/command-bar/view-model.ts +1 -20
  32. package/src/components/command-bar/workflow-ops.ts +4 -0
  33. package/src/components/command-bar/workflow-types.ts +1 -2
  34. package/src/components/data-table-stack-view.test.tsx +39 -4
  35. package/src/components/data-table-stack-view.tsx +11 -104
  36. package/src/components/data-table-view.tsx +43 -79
  37. package/src/components/feed-data-table-stack-view.tsx +10 -17
  38. package/src/components/feed-stack-controller.ts +39 -0
  39. package/src/components/index.ts +5 -1
  40. package/src/components/layout/detached-pane-shell.tsx +29 -10
  41. package/src/components/layout/floating-pane.tsx +35 -4
  42. package/src/components/layout/header.tsx +12 -7
  43. package/src/components/layout/pane-content.test.tsx +64 -0
  44. package/src/components/layout/pane-content.tsx +2 -0
  45. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  46. package/src/components/layout/pane-cycle-order.ts +32 -0
  47. package/src/components/layout/pane-footer.tsx +11 -4
  48. package/src/components/layout/pane-header.tsx +5 -5
  49. package/src/components/layout/pane-sizing.ts +9 -2
  50. package/src/components/layout/pane-title.ts +4 -1
  51. package/src/components/layout/pane.tsx +11 -5
  52. package/src/components/layout/shell.test.tsx +264 -22
  53. package/src/components/layout/shell.tsx +46 -8
  54. package/src/components/layout/status-bar.tsx +2 -0
  55. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  56. package/src/components/pane-settings-dialog.tsx +12 -4
  57. package/src/components/pane-template-wizard.test.tsx +3 -4
  58. package/src/components/speedometer-gauge.test.tsx +45 -0
  59. package/src/components/speedometer-gauge.tsx +562 -0
  60. package/src/components/table-view-shared.tsx +218 -0
  61. package/src/components/ticker-list-table-view.tsx +57 -73
  62. package/src/components/ticker-list-table.test.tsx +82 -1
  63. package/src/components/ticker-list-table.tsx +22 -9
  64. package/src/components/ui/confirm-dialog.tsx +66 -0
  65. package/src/components/ui/data-table.tsx +30 -19
  66. package/src/components/ui/frame.tsx +2 -0
  67. package/src/components/ui/index.ts +0 -1
  68. package/src/components/ui/page-stack-view.tsx +39 -10
  69. package/src/components/ui/table-layout.test.ts +42 -0
  70. package/src/components/ui/table-layout.ts +49 -4
  71. package/src/components/ui/tabs.tsx +34 -1
  72. package/src/components/ui/ui.test.tsx +168 -21
  73. package/src/core/state/app-state.ts +10 -17
  74. package/src/market-data/coordinator.test.ts +76 -1
  75. package/src/market-data/coordinator.ts +218 -49
  76. package/src/news/aggregator.test.ts +49 -1
  77. package/src/news/aggregator.ts +48 -3
  78. package/src/news/hooks.ts +6 -2
  79. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  80. package/src/plugins/builtin/ai/providers.ts +0 -17
  81. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  82. package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
  83. package/src/plugins/builtin/analytics/index.tsx +1 -18
  84. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  85. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  86. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  87. package/src/plugins/builtin/chat-controller.test.ts +188 -1
  88. package/src/plugins/builtin/chat-controller.ts +537 -220
  89. package/src/plugins/builtin/chat.test.tsx +431 -13
  90. package/src/plugins/builtin/chat.tsx +1134 -227
  91. package/src/plugins/builtin/cloud-tweets.tsx +1 -1
  92. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  93. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  94. package/src/plugins/builtin/correlation/index.tsx +1 -6
  95. package/src/plugins/builtin/debug.tsx +1 -3
  96. package/src/plugins/builtin/econ/index.tsx +1 -2
  97. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  98. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  99. package/src/plugins/builtin/fear-greed/index.tsx +587 -0
  100. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  101. package/src/plugins/builtin/help.test.tsx +2 -2
  102. package/src/plugins/builtin/help.tsx +2 -2
  103. package/src/plugins/builtin/holders/index.test.ts +13 -1
  104. package/src/plugins/builtin/holders/index.tsx +64 -29
  105. package/src/plugins/builtin/insider/index.tsx +22 -89
  106. package/src/plugins/builtin/market-movers/index.tsx +13 -19
  107. package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
  108. package/src/plugins/builtin/market-movers/screener.ts +70 -17
  109. package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
  110. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  111. package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
  112. package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
  113. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
  114. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  115. package/src/plugins/builtin/news.tsx +4 -16
  116. package/src/plugins/builtin/notes.tsx +19 -55
  117. package/src/plugins/builtin/options.test.tsx +58 -2
  118. package/src/plugins/builtin/options.tsx +110 -65
  119. package/src/plugins/builtin/plugin-groups.ts +2 -0
  120. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  121. package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
  122. package/src/plugins/builtin/research/index.test.ts +94 -0
  123. package/src/plugins/builtin/research/index.tsx +164 -33
  124. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  125. package/src/plugins/builtin/sec.tsx +13 -66
  126. package/src/plugins/builtin/sectors/index.tsx +1 -1
  127. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  128. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  129. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  130. package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
  131. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  132. package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
  133. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  134. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  135. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  136. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  137. package/src/plugins/ibkr/gateway-service.ts +1 -1
  138. package/src/plugins/ibkr/index.test.tsx +0 -2
  139. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  140. package/src/plugins/pane-manager.ts +0 -13
  141. package/src/plugins/plugin-runtime.test.tsx +40 -6
  142. package/src/plugins/plugin-runtime.tsx +20 -10
  143. package/src/plugins/prediction-markets/cli.ts +0 -1
  144. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  145. package/src/plugins/prediction-markets/controller.ts +0 -15
  146. package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
  147. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  148. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  149. package/src/plugins/registry.ts +13 -42
  150. package/src/plugins/ticker-navigation.test.ts +66 -0
  151. package/src/plugins/ticker-navigation.ts +44 -0
  152. package/src/react/input.ts +2 -1
  153. package/src/renderers/electrobun/bun/index.ts +32 -11
  154. package/src/renderers/electrobun/bun/window-focus.ts +37 -0
  155. package/src/renderers/electrobun/shared/protocol.ts +7 -1
  156. package/src/renderers/electrobun/view/app-services.ts +88 -31
  157. package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
  158. package/src/renderers/electrobun/view/data-table.tsx +138 -101
  159. package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
  160. package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
  161. package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
  162. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  163. package/src/renderers/electrobun/view/key-event.ts +1 -1
  164. package/src/renderers/electrobun/view/main.tsx +18 -1
  165. package/src/renderers/electrobun/view/styles.css +329 -0
  166. package/src/renderers/electrobun/view/toast-host.tsx +29 -3
  167. package/src/renderers/electrobun/view/ui-host.tsx +42 -0
  168. package/src/renderers/opentui/chart-surface.tsx +179 -0
  169. package/src/renderers/opentui/input-host.tsx +1 -1
  170. package/src/renderers/opentui/start.tsx +48 -30
  171. package/src/renderers/opentui/test-utils.tsx +17 -0
  172. package/src/renderers/opentui/ui-host.tsx +5 -2
  173. package/src/sources/gloomberb-cloud.test.ts +86 -1
  174. package/src/sources/gloomberb-cloud.ts +117 -21
  175. package/src/sources/provider-router.test.ts +122 -0
  176. package/src/sources/provider-router.ts +198 -15
  177. package/src/sources/yahoo-finance.test.ts +44 -0
  178. package/src/sources/yahoo-finance.ts +6 -0
  179. package/src/state/app-bootstrap.ts +20 -7
  180. package/src/state/app-context-selector.test.tsx +189 -4
  181. package/src/state/app-context.test.ts +65 -0
  182. package/src/state/app-context.tsx +48 -16
  183. package/src/state/use-inline-tickers.test.tsx +73 -0
  184. package/src/state/use-inline-tickers.ts +19 -10
  185. package/src/test-support/plugin-runtime.ts +3 -2
  186. package/src/theme/colors.test.ts +75 -0
  187. package/src/theme/colors.ts +76 -6
  188. package/src/theme/theme-context.tsx +26 -0
  189. package/src/theme/themes.test.ts +23 -1
  190. package/src/theme/themes.ts +115 -0
  191. package/src/types/broker.ts +1 -1
  192. package/src/types/capability-route-source.ts +1 -0
  193. package/src/types/data-provider.ts +15 -2
  194. package/src/types/desktop-window.ts +6 -0
  195. package/src/types/financials.ts +2 -0
  196. package/src/types/plugin.ts +1 -3
  197. package/src/ui/host.tsx +7 -0
  198. package/src/utils/api-client.test.ts +32 -0
  199. package/src/utils/api-client.ts +119 -18
  200. package/src/utils/back-navigation.ts +24 -1
  201. package/src/utils/double-escape-close.test.ts +33 -0
  202. package/src/utils/double-escape-close.ts +40 -0
  203. package/src/utils/throttled-fetch.test.ts +22 -0
  204. package/src/utils/throttled-fetch.ts +3 -1
  205. package/src/utils/ticker-search.ts +53 -9
  206. package/src/version.ts +1 -1
  207. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  208. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -1,11 +1,10 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act, useEffect, useRef, useState } from "react";
3
- import { testRender } from "../../renderers/opentui/test-utils";
4
- import { DialogProvider } from "@opentui-ui/dialog/react";
3
+ import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
5
4
  import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
6
5
  import { Button } from "./button";
7
6
  import { ChoiceDialog } from "./choice-dialog";
8
- import { DataTable } from "./data-table";
7
+ import { DataTable, type DataTableColumn } from "./data-table";
9
8
  import { TextField } from "./fields";
10
9
  import { ListView } from "./list-view";
11
10
  import { MultiSelectDialogButton } from "./multi-select-dialog";
@@ -21,12 +20,13 @@ import { Tabs } from "./tabs";
21
20
  import { ToggleList } from "../toggle-list";
22
21
  import { AppContext, PaneInstanceProvider, createInitialState } from "../../state/app-context";
23
22
  import { createDefaultConfig } from "../../types/config";
23
+ import { DataTableView } from "../data-table-view";
24
24
 
25
25
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
26
26
  let setListSelection: ((index: number) => void) | null = null;
27
27
  let selectedTableRow: string | null = null;
28
28
  let activatedTableRow: string | null = null;
29
- let tableHorizontalScrollbarVisible: boolean | null = null;
29
+ let tableScrollBoxForTest: ScrollBoxRenderable | null = null;
30
30
  let closedTab: string | null = null;
31
31
  let addedTab = false;
32
32
  let resolvedChoice: string | null = null;
@@ -125,34 +125,47 @@ function DataTableSectionHarness() {
125
125
  );
126
126
  }
127
127
 
128
- function DataTableNoHorizontalScrollHarness() {
128
+ function DataTableHorizontalScrollHarness({
129
+ columns = [{ id: "name", label: "NAME", width: 12, align: "left" }],
130
+ containerWidth = 32,
131
+ containerHeight = 5,
132
+ rowCount = 1,
133
+ showHorizontalScrollbar,
134
+ }: {
135
+ columns?: DataTableColumn[];
136
+ containerWidth?: number;
137
+ containerHeight?: number;
138
+ rowCount?: number;
139
+ showHorizontalScrollbar?: boolean;
140
+ }) {
129
141
  const headerScrollRef = useRef<ScrollBoxRenderable>(null);
130
142
  const scrollRef = useRef<ScrollBoxRenderable>(null);
131
- const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
143
+ const items = Array.from({ length: rowCount }, (_, index) => ({
144
+ id: `row-${index}`,
145
+ name: index === 0 ? "Alpha" : `Alpha ${index + 1}`,
146
+ }));
132
147
 
133
148
  useEffect(() => {
134
- tableHorizontalScrollbarVisible = scrollRef.current?.horizontalScrollBar.visible ?? null;
149
+ tableScrollBoxForTest = scrollRef.current;
135
150
  });
136
151
 
137
152
  return (
138
- <DataTable
139
- columns={[{ id: "name", label: "NAME", width: 12, align: "left" }]}
140
- items={[{ id: "alpha", name: "Alpha" }]}
153
+ <DataTableView
154
+ rootWidth={containerWidth}
155
+ rootHeight={containerHeight}
156
+ columns={columns}
157
+ items={items}
141
158
  sortColumnId={null}
142
159
  sortDirection="asc"
143
160
  onHeaderClick={() => {}}
144
161
  headerScrollRef={headerScrollRef}
145
162
  scrollRef={scrollRef}
146
- syncHeaderScroll={() => {}}
147
- onBodyScrollActivity={() => {}}
148
- hoveredIdx={hoveredIdx}
149
- setHoveredIdx={setHoveredIdx}
150
163
  getItemKey={(row) => row.id}
151
164
  isSelected={() => false}
152
165
  onSelect={() => {}}
153
166
  renderCell={(row) => ({ text: row.name })}
154
167
  emptyStateTitle="No rows."
155
- showHorizontalScrollbar={false}
168
+ showHorizontalScrollbar={showHorizontalScrollbar}
156
169
  />
157
170
  );
158
171
  }
@@ -166,6 +179,15 @@ function DataTableVirtualizationHarness() {
166
179
  name: `Row ${index}`,
167
180
  }));
168
181
 
182
+ useEffect(() => {
183
+ tableScrollBoxForTest = scrollRef.current;
184
+ return () => {
185
+ if (tableScrollBoxForTest === scrollRef.current) {
186
+ tableScrollBoxForTest = null;
187
+ }
188
+ };
189
+ });
190
+
169
191
  return (
170
192
  <DataTable
171
193
  columns={[{ id: "name", label: "NAME", width: 12, align: "left" }]}
@@ -192,7 +214,7 @@ function MultiSelectDialogButtonHarness() {
192
214
  const [values, setValues] = useState(["sma"]);
193
215
 
194
216
  return (
195
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
217
+ <TestDialogProvider>
196
218
  <MultiSelectDialogButton
197
219
  label="IND"
198
220
  title="Chart Indicators"
@@ -204,7 +226,7 @@ function MultiSelectDialogButtonHarness() {
204
226
  { value: "ema", label: "EMA" },
205
227
  ]}
206
228
  />
207
- </DialogProvider>
229
+ </TestDialogProvider>
208
230
  );
209
231
  }
210
232
 
@@ -253,7 +275,7 @@ afterEach(() => {
253
275
  setListSelection = null;
254
276
  selectedTableRow = null;
255
277
  activatedTableRow = null;
256
- tableHorizontalScrollbarVisible = null;
278
+ tableScrollBoxForTest = null;
257
279
  closedTab = null;
258
280
  addedTab = false;
259
281
  resolvedChoice = null;
@@ -320,6 +342,46 @@ describe("shared UI kit", () => {
320
342
  expect(frame).toContain("Insider");
321
343
  });
322
344
 
345
+ test("moves focused tabs with arrow keys and leaves Tab for pane focus", async () => {
346
+ let lastSelected = "overview";
347
+
348
+ function KeyboardTabsHarness() {
349
+ const [activeValue, setActiveValue] = useState("overview");
350
+ return (
351
+ <Tabs
352
+ tabs={[
353
+ { label: "Overview", value: "overview" },
354
+ { label: "News", value: "news" },
355
+ { label: "Chart", value: "chart" },
356
+ ]}
357
+ activeValue={activeValue}
358
+ onSelect={(value) => {
359
+ lastSelected = value;
360
+ setActiveValue(value);
361
+ }}
362
+ focused
363
+ />
364
+ );
365
+ }
366
+
367
+ testSetup = await testRender(<KeyboardTabsHarness />, { width: 40, height: 4 });
368
+
369
+ await act(async () => {
370
+ await testSetup!.renderOnce();
371
+ testSetup!.mockInput.pressArrow("right");
372
+ await testSetup!.renderOnce();
373
+ });
374
+
375
+ expect(lastSelected).toBe("news");
376
+
377
+ await act(async () => {
378
+ testSetup!.mockInput.pressTab();
379
+ await testSetup!.renderOnce();
380
+ });
381
+
382
+ expect(lastSelected).toBe("news");
383
+ });
384
+
323
385
  test("renders tab actions for editable tab sets", async () => {
324
386
  testSetup = await testRender(
325
387
  <Tabs
@@ -650,10 +712,77 @@ describe("shared UI kit", () => {
650
712
 
651
713
  test("hides data table horizontal scrolling when disabled", async () => {
652
714
  const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
715
+ tableScrollBoxForTest = null;
716
+ testSetup = await testRender(
717
+ <AppContext value={{ state, dispatch: () => {} }}>
718
+ <PaneInstanceProvider paneId="portfolio-list:main">
719
+ <DataTableHorizontalScrollHarness showHorizontalScrollbar={false} />
720
+ </PaneInstanceProvider>
721
+ </AppContext>,
722
+ { width: 32, height: 5 },
723
+ );
724
+
725
+ await act(async () => {
726
+ await testSetup!.renderOnce();
727
+ await testSetup!.renderOnce();
728
+ });
729
+
730
+ expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(false);
731
+ });
732
+
733
+ test("hides data table horizontal scrolling when content fits", async () => {
734
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
735
+ tableScrollBoxForTest = null;
736
+ testSetup = await testRender(
737
+ <AppContext value={{ state, dispatch: () => {} }}>
738
+ <PaneInstanceProvider paneId="portfolio-list:main">
739
+ <DataTableHorizontalScrollHarness />
740
+ </PaneInstanceProvider>
741
+ </AppContext>,
742
+ { width: 32, height: 5 },
743
+ );
744
+
745
+ await act(async () => {
746
+ await testSetup!.renderOnce();
747
+ await testSetup!.renderOnce();
748
+ });
749
+
750
+ expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(false);
751
+ });
752
+
753
+ test("shows data table horizontal scrolling when content overflows", async () => {
754
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
755
+ tableScrollBoxForTest = null;
756
+ testSetup = await testRender(
757
+ <AppContext value={{ state, dispatch: () => {} }}>
758
+ <PaneInstanceProvider paneId="portfolio-list:main">
759
+ <DataTableHorizontalScrollHarness
760
+ containerWidth={24}
761
+ columns={[
762
+ { id: "name", label: "NAME", width: 24, align: "left" },
763
+ { id: "price", label: "PRICE", width: 18, align: "right" },
764
+ ]}
765
+ />
766
+ </PaneInstanceProvider>
767
+ </AppContext>,
768
+ { width: 24, height: 5 },
769
+ );
770
+
771
+ await act(async () => {
772
+ await testSetup!.renderOnce();
773
+ await testSetup!.renderOnce();
774
+ });
775
+
776
+ expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(true);
777
+ });
778
+
779
+ test("shows data table vertical scrolling when rows overflow", async () => {
780
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
781
+ tableScrollBoxForTest = null;
653
782
  testSetup = await testRender(
654
783
  <AppContext value={{ state, dispatch: () => {} }}>
655
784
  <PaneInstanceProvider paneId="portfolio-list:main">
656
- <DataTableNoHorizontalScrollHarness />
785
+ <DataTableHorizontalScrollHarness rowCount={20} containerHeight={5} />
657
786
  </PaneInstanceProvider>
658
787
  </AppContext>,
659
788
  { width: 32, height: 5 },
@@ -664,11 +793,12 @@ describe("shared UI kit", () => {
664
793
  await testSetup!.renderOnce();
665
794
  });
666
795
 
667
- expect(tableHorizontalScrollbarVisible).toBe(false);
796
+ expect(tableScrollBoxForTest?.verticalScrollBar.visible).toBe(true);
668
797
  });
669
798
 
670
- test("virtualizes data table rows by default", async () => {
799
+ test("virtualizes data table rows and refreshes after wheel scrolling", async () => {
671
800
  const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
801
+ tableScrollBoxForTest = null;
672
802
  testSetup = await testRender(
673
803
  <AppContext value={{ state, dispatch: () => {} }}>
674
804
  <PaneInstanceProvider paneId="portfolio-list:main">
@@ -685,5 +815,22 @@ describe("shared UI kit", () => {
685
815
  const frame = testSetup.captureCharFrame();
686
816
  expect(frame).toContain("Row 0");
687
817
  expect(frame).not.toContain("Row 99");
818
+ expect(tableScrollBoxForTest?.scrollTop).toBe(0);
819
+
820
+ await act(async () => {
821
+ for (let index = 0; index < 12; index++) {
822
+ await testSetup!.mockMouse.scroll(2, 2, "down");
823
+ }
824
+ await Promise.resolve();
825
+ await new Promise((resolve) => setTimeout(resolve, 0));
826
+ await testSetup!.renderOnce();
827
+ });
828
+ await testSetup.renderOnce();
829
+
830
+ const scrollTop = tableScrollBoxForTest?.scrollTop ?? 0;
831
+ expect(scrollTop).toBeGreaterThan(0);
832
+
833
+ const scrolledFrame = testSetup.captureCharFrame();
834
+ expect(scrolledFrame).toContain(`Row ${scrollTop}`);
688
835
  });
689
836
  });
@@ -318,23 +318,13 @@ function getTopFloatingPaneId(layout: LayoutConfig): string | null {
318
318
  return topInstanceId;
319
319
  }
320
320
 
321
- function resolveFocusedPaneId(
322
- previousLayout: LayoutConfig,
323
- nextLayout: LayoutConfig,
324
- focusedPaneId: string | null,
325
- ): string | null {
321
+ function resolveFocusedPaneId(nextLayout: LayoutConfig, focusedPaneId: string | null): string | null {
326
322
  const paneOrder = getPaneOrder(nextLayout);
327
323
  if (paneOrder.length === 0) return null;
328
324
  if (focusedPaneId && paneOrder.includes(focusedPaneId)) {
329
325
  return focusedPaneId;
330
326
  }
331
- const previouslyFloating = focusedPaneId
332
- ? previousLayout.floating.some((entry) => entry.instanceId === focusedPaneId)
333
- : false;
334
- if (previouslyFloating) {
335
- return getTopFloatingPaneId(nextLayout) ?? paneOrder[0] ?? null;
336
- }
337
- return paneOrder[0] ?? null;
327
+ return getTopFloatingPaneId(nextLayout) ?? paneOrder[0] ?? null;
338
328
  }
339
329
 
340
330
  function getPanelFocusTarget(layout: LayoutConfig, panel: "left" | "right"): string | null {
@@ -387,10 +377,12 @@ function removeHistoryIndex(layoutHistory: Record<number, LayoutHistoryEntry>, r
387
377
 
388
378
  /** If paneId is a floating pane, bump its zIndex to the top. */
389
379
  function bringFloatingToFront(layout: LayoutConfig, paneId: string): LayoutConfig {
390
- const entry = layout.floating.find((e) => e.instanceId === paneId);
380
+ const entryIndex = layout.floating.findIndex((e) => e.instanceId === paneId);
381
+ const entry = entryIndex >= 0 ? layout.floating[entryIndex] : undefined;
391
382
  if (!entry) return layout;
392
383
  const maxZ = layout.floating.reduce((max, e) => Math.max(max, e.zIndex ?? 50), 0);
393
- if ((entry.zIndex ?? 50) >= maxZ) return layout; // already on top
384
+ const topEqualIndex = layout.floating.findLastIndex((e) => (e.zIndex ?? 50) === maxZ);
385
+ if ((entry.zIndex ?? 50) === maxZ && entryIndex === topEqualIndex) return layout; // already on top
394
386
  return {
395
387
  ...layout,
396
388
  floating: layout.floating.map((e) =>
@@ -444,7 +436,7 @@ function withFocusedPane(state: AppState, config: AppConfig): AppState {
444
436
  layouts: syncLayouts(config.layouts, config.activeLayoutIndex, normalizedLayout),
445
437
  };
446
438
  const nextPaneState = reconcilePaneState(nextConfig, state.paneState);
447
- const focusedPaneId = resolveFocusedPaneId(state.config.layout, nextConfig.layout, state.focusedPaneId);
439
+ const focusedPaneId = resolveFocusedPaneId(nextConfig.layout, state.focusedPaneId);
448
440
  const focusedConfig = bringFocusedFloatingPaneToFront(nextConfig, focusedPaneId);
449
441
  return {
450
442
  ...state,
@@ -792,7 +784,8 @@ export function appReducer(state: AppState, action: AppAction): AppState {
792
784
 
793
785
  case "FOCUS_PREV": {
794
786
  if (action.paneOrder.length === 0) return state;
795
- const currentIndex = state.focusedPaneId ? action.paneOrder.indexOf(state.focusedPaneId) : 0;
787
+ const focusedIndex = state.focusedPaneId ? action.paneOrder.indexOf(state.focusedPaneId) : -1;
788
+ const currentIndex = focusedIndex >= 0 ? focusedIndex : 0;
796
789
  const nextPaneId = action.paneOrder[(currentIndex - 1 + action.paneOrder.length) % action.paneOrder.length]!;
797
790
  return focusPaneState(state, nextPaneId);
798
791
  }
@@ -851,7 +844,7 @@ export function appReducer(state: AppState, action: AppAction): AppState {
851
844
 
852
845
  export function createInitialState(config: AppConfig, sessionSnapshot: AppSessionSnapshot | null = null): AppState {
853
846
  const paneState = reconcilePaneState(config, sessionSnapshot?.paneState ?? {});
854
- const defaultFocusedPaneId = getPaneOrder(config.layout)[0] ?? null;
847
+ const defaultFocusedPaneId = getTopFloatingPaneId(config.layout) ?? getPaneOrder(config.layout)[0] ?? null;
855
848
  const focusedPaneId = sessionSnapshot?.focusedPaneId
856
849
  && config.layout.instances.some((instance) => instance.instanceId === sessionSnapshot.focusedPaneId)
857
850
  ? sessionSnapshot.focusedPaneId
@@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test";
2
2
  import { MarketDataCoordinator } from "./coordinator";
3
3
  import type { DataProvider, QuoteSubscriptionTarget } from "../types/data-provider";
4
4
  import type { InstrumentSearchResult } from "../types/instrument";
5
- import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
5
+ import type { PricePoint, Quote, TickerFinancials } from "../types/financials";
6
6
  import { createTestDataProvider } from "../test-support/data-provider";
7
7
 
8
8
  function createProvider(overrides: Partial<DataProvider> = {}): DataProvider {
@@ -632,4 +632,79 @@ describe("MarketDataCoordinator", () => {
632
632
  expect(financials?.quote?.marketCap).toBe(3_640_775_908_600);
633
633
  expect(financials?.fundamentals?.trailingPE).toBe(31.4);
634
634
  });
635
+
636
+ it("batch loads only missing quotes when fresh quote data is already stored", async () => {
637
+ const batchTargets: QuoteSubscriptionTarget[][] = [];
638
+ const provider = createProvider({
639
+ getQuote: async () => ({
640
+ symbol: "AAPL",
641
+ price: 100,
642
+ currency: "USD",
643
+ change: 0,
644
+ changePercent: 0,
645
+ lastUpdated: 1_700_000_000_000,
646
+ }),
647
+ getQuotesBatch: async (targets) => {
648
+ batchTargets.push(targets);
649
+ return targets.map((target) => ({
650
+ target,
651
+ quote: {
652
+ symbol: target.symbol,
653
+ price: target.symbol === "MSFT" ? 200 : 300,
654
+ currency: "USD",
655
+ change: 0,
656
+ changePercent: 0,
657
+ lastUpdated: 1_700_000_000_000,
658
+ },
659
+ }));
660
+ },
661
+ });
662
+ const coordinator = new MarketDataCoordinator(provider);
663
+ const aapl = { symbol: "AAPL", exchange: "NASDAQ" };
664
+ const msft = { symbol: "MSFT", exchange: "NASDAQ" };
665
+
666
+ await coordinator.loadQuote(aapl);
667
+ await coordinator.loadQuotesBatch([aapl, msft]);
668
+
669
+ expect(batchTargets).toHaveLength(1);
670
+ expect(batchTargets[0]?.map((target) => target.symbol)).toEqual(["MSFT"]);
671
+ expect(coordinator.getQuoteEntry(aapl).data?.price).toBe(100);
672
+ expect(coordinator.getQuoteEntry(msft).data?.price).toBe(200);
673
+ });
674
+
675
+ it("batch loads snapshots through provider batch support", async () => {
676
+ const batchSymbols: string[][] = [];
677
+ const provider = createProvider({
678
+ getTickerFinancialsBatch: async (targets) => {
679
+ batchSymbols.push(targets.map((target) => target.symbol));
680
+ return targets.map((target) => ({
681
+ target,
682
+ financials: {
683
+ quote: {
684
+ symbol: target.symbol,
685
+ price: target.symbol === "AAPL" ? 150 : 250,
686
+ currency: "USD",
687
+ change: 0,
688
+ changePercent: 0,
689
+ lastUpdated: 1_700_000_000_000,
690
+ },
691
+ fundamentals: { trailingPE: target.symbol === "AAPL" ? 20 : 30 },
692
+ annualStatements: [],
693
+ quarterlyStatements: [],
694
+ priceHistory: [],
695
+ },
696
+ }));
697
+ },
698
+ });
699
+ const coordinator = new MarketDataCoordinator(provider);
700
+
701
+ await coordinator.loadSnapshotsBatch([
702
+ { symbol: "AAPL", exchange: "NASDAQ" },
703
+ { symbol: "MSFT", exchange: "NASDAQ" },
704
+ ]);
705
+
706
+ expect(batchSymbols).toEqual([["AAPL", "MSFT"]]);
707
+ expect(coordinator.getTickerFinancialsSync({ symbol: "AAPL", exchange: "NASDAQ" })?.quote?.price).toBe(150);
708
+ expect(coordinator.getTickerFinancialsSync({ symbol: "MSFT", exchange: "NASDAQ" })?.fundamentals?.trailingPE).toBe(30);
709
+ });
635
710
  });