gloomberb 0.4.0 → 0.5.0

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 (218) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/app.tsx +184 -69
  4. package/src/cli/commands/ticker.ts +45 -15
  5. package/src/cli/helpers.ts +4 -2
  6. package/src/cli/registry.test.ts +42 -35
  7. package/src/cli.test.ts +4 -2
  8. package/src/components/chart/chart-control-hint.tsx +39 -0
  9. package/src/components/chart/chart-controller.test.ts +110 -0
  10. package/src/components/chart/chart-controller.ts +364 -2
  11. package/src/components/chart/chart-data.ts +177 -29
  12. package/src/components/chart/chart-indicator-selector.tsx +53 -0
  13. package/src/components/chart/chart-pane-settings.ts +20 -0
  14. package/src/components/chart/chart-renderer.ts +220 -30
  15. package/src/components/chart/chart-resolution.test.ts +67 -0
  16. package/src/components/chart/chart-resolution.ts +74 -4
  17. package/src/components/chart/chart-scroll.test.ts +69 -0
  18. package/src/components/chart/chart-scroll.ts +104 -0
  19. package/src/components/chart/chart-types.ts +12 -0
  20. package/src/components/chart/chart-viewport.test.ts +5 -1
  21. package/src/components/chart/chart-viewport.ts +1 -1
  22. package/src/components/chart/chart.test.ts +249 -10
  23. package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
  24. package/src/components/chart/comparison-chart-renderer.ts +40 -3
  25. package/src/components/chart/comparison-stock-chart.tsx +218 -123
  26. package/src/components/chart/indicators/bands.test.ts +50 -0
  27. package/src/components/chart/indicators/bands.ts +29 -0
  28. package/src/components/chart/indicators/moving-averages.test.ts +54 -0
  29. package/src/components/chart/indicators/moving-averages.ts +42 -0
  30. package/src/components/chart/indicators/options.test.ts +32 -0
  31. package/src/components/chart/indicators/options.ts +107 -0
  32. package/src/components/chart/indicators/oscillators.test.ts +79 -0
  33. package/src/components/chart/indicators/oscillators.ts +92 -0
  34. package/src/components/chart/indicators/types.ts +36 -0
  35. package/src/components/chart/native/chart-rasterizer.ts +51 -0
  36. package/src/components/chart/native/kitty.test.ts +68 -0
  37. package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
  38. package/src/components/chart/stock-chart.test.ts +239 -0
  39. package/src/components/chart/stock-chart.tsx +2257 -372
  40. package/src/components/command-bar/command-bar.test.tsx +337 -5
  41. package/src/components/command-bar/command-bar.tsx +296 -120
  42. package/src/components/command-bar/helpers.ts +1 -1
  43. package/src/components/command-bar/root-shortcuts.ts +33 -4
  44. package/src/components/command-bar/view-model.test.ts +3 -3
  45. package/src/components/command-bar/view-model.ts +4 -4
  46. package/src/components/command-bar/workflow-ops.ts +10 -0
  47. package/src/components/layout/floating-pane.tsx +3 -1
  48. package/src/components/layout/header.tsx +5 -4
  49. package/src/components/layout/pane-sizing.ts +6 -0
  50. package/src/components/layout/pane.tsx +5 -1
  51. package/src/components/layout/shell.test.tsx +263 -32
  52. package/src/components/layout/shell.tsx +10 -6
  53. package/src/components/layout/status-bar.test.tsx +16 -7
  54. package/src/components/layout/status-bar.tsx +4 -1
  55. package/src/components/markdown-editor.tsx +1 -0
  56. package/src/components/pane-settings-dialog.tsx +13 -152
  57. package/src/components/pane-template-wizard.tsx +1 -0
  58. package/src/components/price-selector-dialog.tsx +12 -7
  59. package/src/components/ticker-badge-text.test.tsx +50 -0
  60. package/src/components/ticker-badge-text.tsx +17 -2
  61. package/src/components/ticker-badge.tsx +2 -2
  62. package/src/components/ticker-list-table.tsx +62 -2
  63. package/src/components/toggle-list.tsx +15 -2
  64. package/src/components/ui/data-table.tsx +82 -5
  65. package/src/components/ui/external-link.tsx +48 -11
  66. package/src/components/ui/fields.tsx +2 -1
  67. package/src/components/ui/index.ts +8 -1
  68. package/src/components/ui/multi-select-chips.tsx +69 -0
  69. package/src/components/ui/multi-select-dialog.tsx +267 -0
  70. package/src/components/ui/multi-select.ts +78 -0
  71. package/src/components/ui/ui.test.tsx +343 -1
  72. package/src/data/plugin-state-store.ts +30 -19
  73. package/src/data/resource-store.ts +133 -103
  74. package/src/data/session-store.ts +19 -12
  75. package/src/data/sqlite-cache.test.ts +37 -2
  76. package/src/data/sqlite-database.ts +8 -2
  77. package/src/data/sqlite-retry.test.ts +63 -0
  78. package/src/data/sqlite-retry.ts +66 -0
  79. package/src/data/ticker-store.ts +29 -18
  80. package/src/index.tsx +6 -1
  81. package/src/market-data/coordinator.test.ts +114 -5
  82. package/src/market-data/coordinator.ts +70 -1
  83. package/src/market-data/hooks.test.tsx +73 -2
  84. package/src/market-data/hooks.tsx +16 -4
  85. package/src/notifications/app-notifier.test.ts +177 -0
  86. package/src/notifications/app-notifier.ts +241 -0
  87. package/src/pane-settings.test.ts +2 -0
  88. package/src/plugins/builtin/ai/index.test.tsx +1 -1
  89. package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
  90. package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
  91. package/src/plugins/builtin/ai/ticker-context.ts +3 -2
  92. package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
  93. package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
  94. package/src/plugins/builtin/alerts/index.test.tsx +357 -0
  95. package/src/plugins/builtin/alerts/index.tsx +519 -0
  96. package/src/plugins/builtin/alerts/types.ts +14 -0
  97. package/src/plugins/builtin/analytics/index.test.tsx +300 -0
  98. package/src/plugins/builtin/analytics/index.tsx +775 -0
  99. package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
  100. package/src/plugins/builtin/analytics/metrics.ts +155 -0
  101. package/src/plugins/builtin/ask-ai.test.tsx +2 -0
  102. package/src/plugins/builtin/chat-controller.test.ts +92 -18
  103. package/src/plugins/builtin/chat-controller.ts +40 -11
  104. package/src/plugins/builtin/chat.test.tsx +715 -4
  105. package/src/plugins/builtin/chat.tsx +420 -119
  106. package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
  107. package/src/plugins/builtin/comparison-chart.tsx +1 -2
  108. package/src/plugins/builtin/correlation/compute.test.ts +147 -0
  109. package/src/plugins/builtin/correlation/compute.ts +115 -0
  110. package/src/plugins/builtin/correlation/index.tsx +385 -0
  111. package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
  112. package/src/plugins/builtin/correlation/settings.test.ts +34 -0
  113. package/src/plugins/builtin/correlation/settings.ts +95 -0
  114. package/src/plugins/builtin/debug.tsx +36 -50
  115. package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
  116. package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
  117. package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
  118. package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
  119. package/src/plugins/builtin/earnings/index.tsx +337 -0
  120. package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
  121. package/src/plugins/builtin/econ/calendar-source.ts +121 -0
  122. package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
  123. package/src/plugins/builtin/econ/fred-client.ts +79 -0
  124. package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
  125. package/src/plugins/builtin/econ/index.tsx +999 -0
  126. package/src/plugins/builtin/econ/types.ts +13 -0
  127. package/src/plugins/builtin/fred-settings.ts +10 -0
  128. package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
  129. package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
  130. package/src/plugins/builtin/help.test.tsx +1 -5
  131. package/src/plugins/builtin/help.tsx +1 -16
  132. package/src/plugins/builtin/insider/index.tsx +417 -0
  133. package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
  134. package/src/plugins/builtin/insider/insider-data.ts +49 -0
  135. package/src/plugins/builtin/layout-manager.tsx +33 -16
  136. package/src/plugins/builtin/market-movers/index.tsx +573 -0
  137. package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
  138. package/src/plugins/builtin/market-movers/screener.ts +105 -0
  139. package/src/plugins/builtin/options.tsx +4 -3
  140. package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
  141. package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
  142. package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
  143. package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
  144. package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
  145. package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
  146. package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
  147. package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
  148. package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
  149. package/src/plugins/builtin/sec.tsx +114 -7
  150. package/src/plugins/builtin/sectors/index.tsx +359 -0
  151. package/src/plugins/builtin/sectors/sector-data.ts +18 -0
  152. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
  153. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
  154. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
  155. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
  156. package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
  157. package/src/plugins/builtin/world-indices/index.tsx +410 -0
  158. package/src/plugins/builtin/world-indices/indices.ts +52 -0
  159. package/src/plugins/builtin/yield-curve/index.tsx +242 -0
  160. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
  161. package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
  162. package/src/plugins/catalog.ts +22 -0
  163. package/src/plugins/gridlock-notification.ts +15 -0
  164. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  165. package/src/plugins/ibkr/gateway-service.test.ts +22 -0
  166. package/src/plugins/ibkr/gateway-service.ts +6 -0
  167. package/src/plugins/ibkr/index.test.tsx +1 -1
  168. package/src/plugins/ibkr/index.tsx +8 -2
  169. package/src/plugins/ibkr/trade-tab.tsx +23 -5
  170. package/src/plugins/ibkr/trade-utils.test.ts +1 -1
  171. package/src/plugins/ibkr/trade-utils.ts +7 -6
  172. package/src/plugins/ibkr/trading-pane.tsx +8 -7
  173. package/src/plugins/plugin-runtime.test.tsx +2 -0
  174. package/src/plugins/plugin-runtime.tsx +10 -0
  175. package/src/plugins/prediction-markets/controller-data.ts +129 -14
  176. package/src/plugins/prediction-markets/controller.ts +1 -1
  177. package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
  178. package/src/plugins/prediction-markets/index.tsx +5 -1
  179. package/src/plugins/prediction-markets/pane.test.tsx +78 -0
  180. package/src/plugins/prediction-markets/pane.tsx +7 -3
  181. package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
  182. package/src/plugins/prediction-markets/services/fetch.ts +17 -7
  183. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  184. package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
  185. package/src/plugins/prediction-markets/test-helpers.tsx +22 -5
  186. package/src/plugins/registry.ts +45 -13
  187. package/src/sources/provider-router.test.ts +3 -2
  188. package/src/sources/sec-edgar.ts +13 -1
  189. package/src/sources/yahoo-finance.ts +75 -1
  190. package/src/state/app-context-selector.test.tsx +17 -1
  191. package/src/state/app-context.test.tsx +24 -0
  192. package/src/state/app-context.tsx +29 -6
  193. package/src/theme/colors.ts +5 -0
  194. package/src/theme/themes.ts +25 -0
  195. package/src/types/config.test.ts +107 -0
  196. package/src/types/config.ts +30 -1
  197. package/src/types/data-provider.ts +13 -0
  198. package/src/types/plugin.ts +35 -1
  199. package/src/updater.test.ts +2 -0
  200. package/src/utils/api-client.ts +12 -0
  201. package/src/utils/format.test.ts +11 -1
  202. package/src/utils/format.ts +86 -8
  203. package/src/utils/inline-content-tokenizer.test.ts +22 -0
  204. package/src/utils/inline-content-tokenizer.ts +76 -0
  205. package/src/utils/link-tokenizer.test.ts +27 -0
  206. package/src/utils/link-tokenizer.ts +93 -0
  207. package/src/utils/market-format.test.ts +109 -0
  208. package/src/utils/market-format.ts +299 -0
  209. package/src/utils/market-status.ts +3 -2
  210. package/src/utils/selection-clipboard.test.ts +43 -0
  211. package/src/utils/selection-clipboard.ts +88 -0
  212. package/src/utils/terminal-input-reset.test.ts +50 -0
  213. package/src/utils/terminal-input-reset.ts +24 -0
  214. package/src/utils/throttled-fetch.test.ts +145 -0
  215. package/src/utils/throttled-fetch.ts +212 -0
  216. package/src/utils/ticker-tokenizer.test.ts +31 -0
  217. package/src/utils/ticker-tokenizer.ts +7 -42
  218. package/src/version.ts +1 -1
@@ -1,16 +1,28 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
- import { act, useState } from "react";
2
+ import { act, useEffect, useRef, useState } from "react";
3
3
  import { testRender } from "@opentui/react/test-utils";
4
+ import { DialogProvider } from "@opentui-ui/dialog/react";
5
+ import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
4
6
  import { Button } from "./button";
7
+ import { DataTable } from "./data-table";
5
8
  import { TextField } from "./fields";
6
9
  import { ListView } from "./list-view";
10
+ import { MultiSelectChips } from "./multi-select-chips";
11
+ import { MultiSelectDialogButton } from "./multi-select-dialog";
12
+ import { toggleMultiSelectValue } from "./multi-select";
7
13
  import { ProgressBar } from "./loading";
8
14
  import { Notice } from "./status";
9
15
  import { Tabs } from "./tabs";
10
16
  import { ToggleList } from "../toggle-list";
17
+ import { AppContext, PaneInstanceProvider, createInitialState } from "../../state/app-context";
18
+ import { createDefaultConfig } from "../../types/config";
11
19
 
12
20
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
13
21
  let setListSelection: ((index: number) => void) | null = null;
22
+ let selectedTableRow: string | null = null;
23
+ let activatedTableRow: string | null = null;
24
+ let selectedChips: string[] = [];
25
+ let tableHorizontalScrollbarVisible: boolean | null = null;
14
26
 
15
27
  function ScrollableListHarness() {
16
28
  const [selectedIndex, setSelectedIndex] = useState(0);
@@ -29,12 +41,163 @@ function ScrollableListHarness() {
29
41
  );
30
42
  }
31
43
 
44
+ function DataTableActivationHarness() {
45
+ const headerScrollRef = useRef<ScrollBoxRenderable>(null);
46
+ const scrollRef = useRef<ScrollBoxRenderable>(null);
47
+ const [selectedId, setSelectedId] = useState<string | null>(null);
48
+ const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
49
+
50
+ return (
51
+ <DataTable
52
+ columns={[{ id: "name", label: "NAME", width: 12, align: "left" }]}
53
+ items={[{ id: "alpha", name: "Alpha" }]}
54
+ sortColumnId={null}
55
+ sortDirection="asc"
56
+ onHeaderClick={() => {}}
57
+ headerScrollRef={headerScrollRef}
58
+ scrollRef={scrollRef}
59
+ syncHeaderScroll={() => {}}
60
+ onBodyScrollActivity={() => {}}
61
+ hoveredIdx={hoveredIdx}
62
+ setHoveredIdx={setHoveredIdx}
63
+ getItemKey={(row) => row.id}
64
+ isSelected={(row) => selectedId === row.id}
65
+ onSelect={(row) => {
66
+ selectedTableRow = row.id;
67
+ setSelectedId(row.id);
68
+ }}
69
+ onActivate={(row) => {
70
+ activatedTableRow = row.id;
71
+ }}
72
+ renderCell={(row) => ({ text: row.name })}
73
+ emptyStateTitle="No rows."
74
+ />
75
+ );
76
+ }
77
+
78
+ type SectionTableRow =
79
+ | { kind: "header"; id: string; label: string }
80
+ | { kind: "row"; id: string; name: string };
81
+
82
+ function DataTableSectionHarness() {
83
+ const headerScrollRef = useRef<ScrollBoxRenderable>(null);
84
+ const scrollRef = useRef<ScrollBoxRenderable>(null);
85
+ const [selectedId, setSelectedId] = useState<string | null>(null);
86
+ const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
87
+ const rows: SectionTableRow[] = [
88
+ { kind: "header", id: "macro", label: "Macro Releases" },
89
+ { kind: "row", id: "cpi", name: "CPI" },
90
+ ];
91
+
92
+ return (
93
+ <DataTable
94
+ columns={[{ id: "name", label: "NAME", width: 16, align: "left" }]}
95
+ items={rows}
96
+ sortColumnId={null}
97
+ sortDirection="asc"
98
+ onHeaderClick={() => {}}
99
+ headerScrollRef={headerScrollRef}
100
+ scrollRef={scrollRef}
101
+ syncHeaderScroll={() => {}}
102
+ onBodyScrollActivity={() => {}}
103
+ hoveredIdx={hoveredIdx}
104
+ setHoveredIdx={setHoveredIdx}
105
+ getItemKey={(row) => row.id}
106
+ isSelected={(row) => row.kind === "row" && row.id === selectedId}
107
+ onSelect={(row) => {
108
+ if (row.kind !== "row") return;
109
+ selectedTableRow = row.id;
110
+ setSelectedId(row.id);
111
+ }}
112
+ renderSectionHeader={(row) => (
113
+ row.kind === "header" ? { text: row.label } : null
114
+ )}
115
+ renderCell={(row) => ({ text: row.kind === "row" ? row.name : "" })}
116
+ emptyStateTitle="No rows."
117
+ />
118
+ );
119
+ }
120
+
121
+ function DataTableNoHorizontalScrollHarness() {
122
+ const headerScrollRef = useRef<ScrollBoxRenderable>(null);
123
+ const scrollRef = useRef<ScrollBoxRenderable>(null);
124
+ const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
125
+
126
+ useEffect(() => {
127
+ tableHorizontalScrollbarVisible = scrollRef.current?.horizontalScrollBar.visible ?? null;
128
+ });
129
+
130
+ return (
131
+ <DataTable
132
+ columns={[{ id: "name", label: "NAME", width: 12, align: "left" }]}
133
+ items={[{ id: "alpha", name: "Alpha" }]}
134
+ sortColumnId={null}
135
+ sortDirection="asc"
136
+ onHeaderClick={() => {}}
137
+ headerScrollRef={headerScrollRef}
138
+ scrollRef={scrollRef}
139
+ syncHeaderScroll={() => {}}
140
+ onBodyScrollActivity={() => {}}
141
+ hoveredIdx={hoveredIdx}
142
+ setHoveredIdx={setHoveredIdx}
143
+ getItemKey={(row) => row.id}
144
+ isSelected={() => false}
145
+ onSelect={() => {}}
146
+ renderCell={(row) => ({ text: row.name })}
147
+ emptyStateTitle="No rows."
148
+ showHorizontalScrollbar={false}
149
+ />
150
+ );
151
+ }
152
+
153
+ function MultiSelectChipsHarness() {
154
+ const [values, setValues] = useState(["sma"]);
155
+ selectedChips = values;
156
+
157
+ return (
158
+ <MultiSelectChips
159
+ label="IND"
160
+ selectedValues={values}
161
+ onChange={setValues}
162
+ idPrefix="indicator-chip"
163
+ options={[
164
+ { value: "sma", label: "SMA" },
165
+ { value: "ema", label: "EMA" },
166
+ ]}
167
+ />
168
+ );
169
+ }
170
+
171
+ function MultiSelectDialogButtonHarness() {
172
+ const [values, setValues] = useState(["sma"]);
173
+
174
+ return (
175
+ <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
176
+ <MultiSelectDialogButton
177
+ label="IND"
178
+ title="Chart Indicators"
179
+ selectedValues={values}
180
+ onChange={setValues}
181
+ idPrefix="indicator-dialog"
182
+ options={[
183
+ { value: "sma", label: "SMA" },
184
+ { value: "ema", label: "EMA" },
185
+ ]}
186
+ />
187
+ </DialogProvider>
188
+ );
189
+ }
190
+
32
191
  afterEach(() => {
33
192
  if (testSetup) {
34
193
  testSetup.renderer.destroy();
35
194
  testSetup = undefined;
36
195
  }
37
196
  setListSelection = null;
197
+ selectedTableRow = null;
198
+ activatedTableRow = null;
199
+ selectedChips = [];
200
+ tableHorizontalScrollbarVisible = null;
38
201
  });
39
202
 
40
203
  describe("shared UI kit", () => {
@@ -90,6 +253,79 @@ describe("shared UI kit", () => {
90
253
  expect(frame).toContain("Headlines and previews");
91
254
  });
92
255
 
256
+ test("toggles multi-select chips with the mouse", async () => {
257
+ testSetup = await testRender(<MultiSelectChipsHarness />, { width: 32, height: 3 });
258
+
259
+ await act(async () => {
260
+ await testSetup!.renderOnce();
261
+ });
262
+
263
+ let frame = testSetup.captureCharFrame();
264
+ expect(frame).toContain("[x] SMA");
265
+ expect(frame).toContain("[ ] EMA");
266
+ const emaChip = testSetup.renderer.root.findDescendantById("indicator-chip:ema") as BoxRenderable | undefined;
267
+ expect(emaChip).toBeDefined();
268
+
269
+ await act(async () => {
270
+ await testSetup!.mockMouse.click(emaChip!.x + 1, emaChip!.y);
271
+ });
272
+ await new Promise((resolve) => setTimeout(resolve, 0));
273
+ await act(async () => {
274
+ await testSetup!.renderOnce();
275
+ });
276
+
277
+ frame = testSetup.captureCharFrame();
278
+ expect(frame).toContain("[x] SMA");
279
+ expect(frame).toContain("[x] EMA");
280
+ expect(selectedChips).toEqual(["sma", "ema"]);
281
+ });
282
+
283
+ test("opens compact multi-select dialogs from a button", async () => {
284
+ testSetup = await testRender(<MultiSelectDialogButtonHarness />, { width: 60, height: 18 });
285
+
286
+ await act(async () => {
287
+ await testSetup!.renderOnce();
288
+ });
289
+
290
+ let frame = testSetup.captureCharFrame();
291
+ expect(frame).toContain("IND: SMA");
292
+ const button = testSetup.renderer.root.findDescendantById("indicator-dialog:button") as BoxRenderable | undefined;
293
+ expect(button).toBeDefined();
294
+ expect(button!.width).toBe(" IND: SMA ".length);
295
+
296
+ await act(async () => {
297
+ await testSetup!.mockMouse.click(button!.x + 1, button!.y);
298
+ await Promise.resolve();
299
+ await testSetup!.renderOnce();
300
+ });
301
+
302
+ frame = testSetup.captureCharFrame();
303
+ expect(frame).toContain("Chart Indicators");
304
+ expect(frame).toContain("[✓] SMA");
305
+ expect(frame).toContain("[ ] EMA");
306
+ expect(frame).not.toContain("Toggle");
307
+ expect(frame).not.toContain("space toggle");
308
+
309
+ await act(async () => {
310
+ await testSetup!.mockMouse.click(0, 0);
311
+ await Promise.resolve();
312
+ await testSetup!.renderOnce();
313
+ });
314
+
315
+ frame = testSetup.captureCharFrame();
316
+ expect(frame).not.toContain("Chart Indicators");
317
+ });
318
+
319
+ test("keeps shared multi-select values in option order when toggling", () => {
320
+ const options = [
321
+ { value: "sma", label: "SMA" },
322
+ { value: "ema", label: "EMA" },
323
+ ];
324
+
325
+ expect(toggleMultiSelectValue(options, ["sma"], "ema")).toEqual(["sma", "ema"]);
326
+ expect(toggleMultiSelectValue(options, ["sma", "ema"], "sma")).toEqual(["ema"]);
327
+ });
328
+
93
329
  test("auto-scrolls a scrollable list to keep the selected row visible", async () => {
94
330
  testSetup = await testRender(
95
331
  <ScrollableListHarness />,
@@ -131,4 +367,110 @@ describe("shared UI kit", () => {
131
367
  expect(frame).toContain("******");
132
368
  expect(frame).not.toContain("secret");
133
369
  });
370
+
371
+ test("does not allow selecting masked password fields", async () => {
372
+ testSetup = await testRender(
373
+ <TextField
374
+ type="password"
375
+ value="secret"
376
+ focused
377
+ width={12}
378
+ onChange={() => {}}
379
+ />,
380
+ { width: 16, height: 3 },
381
+ );
382
+
383
+ await testSetup.renderOnce();
384
+
385
+ await act(async () => {
386
+ await testSetup!.mockMouse.drag(1, 0, 6, 0);
387
+ await testSetup!.renderOnce();
388
+ });
389
+
390
+ expect(testSetup.renderer.getSelection()).toBeNull();
391
+ });
392
+
393
+ test("activates data table rows on a second click", async () => {
394
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
395
+ testSetup = await testRender(
396
+ <AppContext value={{ state, dispatch: () => {} }}>
397
+ <PaneInstanceProvider paneId="portfolio-list:main">
398
+ <DataTableActivationHarness />
399
+ </PaneInstanceProvider>
400
+ </AppContext>,
401
+ { width: 32, height: 5 },
402
+ );
403
+
404
+ await act(async () => {
405
+ await testSetup!.renderOnce();
406
+ });
407
+ await act(async () => {
408
+ await testSetup!.mockMouse.click(2, 1);
409
+ await testSetup!.renderOnce();
410
+ });
411
+
412
+ expect(selectedTableRow).toBe("alpha");
413
+ expect(activatedTableRow).toBeNull();
414
+
415
+ await act(async () => {
416
+ await testSetup!.mockMouse.click(2, 1);
417
+ await testSetup!.renderOnce();
418
+ });
419
+
420
+ expect(activatedTableRow).toBe("alpha");
421
+ });
422
+
423
+ test("renders data table section headers as non-selectable rows", async () => {
424
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
425
+ testSetup = await testRender(
426
+ <AppContext value={{ state, dispatch: () => {} }}>
427
+ <PaneInstanceProvider paneId="portfolio-list:main">
428
+ <DataTableSectionHarness />
429
+ </PaneInstanceProvider>
430
+ </AppContext>,
431
+ { width: 32, height: 6 },
432
+ );
433
+
434
+ await act(async () => {
435
+ await testSetup!.renderOnce();
436
+ });
437
+
438
+ let frame = testSetup.captureCharFrame();
439
+ expect(frame).toContain("Macro Releases");
440
+ expect(frame).toContain("CPI");
441
+
442
+ await act(async () => {
443
+ await testSetup!.mockMouse.click(2, 1);
444
+ await testSetup!.renderOnce();
445
+ });
446
+ expect(selectedTableRow).toBeNull();
447
+
448
+ await act(async () => {
449
+ await testSetup!.mockMouse.click(2, 2);
450
+ await testSetup!.renderOnce();
451
+ });
452
+
453
+ frame = testSetup.captureCharFrame();
454
+ expect(selectedTableRow).toBe("cpi");
455
+ expect(frame).toContain("CPI");
456
+ });
457
+
458
+ test("hides data table horizontal scrolling when disabled", async () => {
459
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
460
+ testSetup = await testRender(
461
+ <AppContext value={{ state, dispatch: () => {} }}>
462
+ <PaneInstanceProvider paneId="portfolio-list:main">
463
+ <DataTableNoHorizontalScrollHarness />
464
+ </PaneInstanceProvider>
465
+ </AppContext>,
466
+ { width: 32, height: 5 },
467
+ );
468
+
469
+ await act(async () => {
470
+ await testSetup!.renderOnce();
471
+ await testSetup!.renderOnce();
472
+ });
473
+
474
+ expect(tableHorizontalScrollbarVisible).toBe(false);
475
+ });
134
476
  });
@@ -1,5 +1,6 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import { safeParseJson, serializeJson } from "./sqlite-json";
3
+ import { withSqliteBusyRetry } from "./sqlite-retry";
3
4
 
4
5
  export const DEFAULT_PLUGIN_STATE_SCHEMA_VERSION = 1;
5
6
 
@@ -23,11 +24,13 @@ export class PluginStateStore {
23
24
 
24
25
  get<T>(pluginId: string, key: string, schemaVersion = DEFAULT_PLUGIN_STATE_SCHEMA_VERSION): PluginStateRecord<T> | null {
25
26
  const cacheKey = `${pluginId}:${key}`;
26
- const row = this.db
27
- .query<{ schema_version: number; value: string; updated_at: number }, [string, string]>(
28
- "SELECT schema_version, value, updated_at FROM plugin_state WHERE plugin_id = ? AND key = ?",
29
- )
30
- .get(pluginId, key);
27
+ const row = withSqliteBusyRetry("load plugin state", () => (
28
+ this.db
29
+ .query<{ schema_version: number; value: string; updated_at: number }, [string, string]>(
30
+ "SELECT schema_version, value, updated_at FROM plugin_state WHERE plugin_id = ? AND key = ?",
31
+ )
32
+ .get(pluginId, key)
33
+ ));
31
34
  if (!row) {
32
35
  this.cache.delete(cacheKey);
33
36
  return null;
@@ -63,12 +66,14 @@ export class PluginStateStore {
63
66
  set(pluginId: string, key: string, value: unknown, schemaVersion = DEFAULT_PLUGIN_STATE_SCHEMA_VERSION): void {
64
67
  const rawValue = serializeJson(value);
65
68
  const updatedAt = Date.now();
66
- this.db
67
- .query(
68
- `INSERT OR REPLACE INTO plugin_state (plugin_id, key, schema_version, value, updated_at)
69
- VALUES (?, ?, ?, ?, ?)`,
70
- )
71
- .run(pluginId, key, schemaVersion, rawValue, updatedAt);
69
+ withSqliteBusyRetry("save plugin state", () => {
70
+ this.db
71
+ .query(
72
+ `INSERT OR REPLACE INTO plugin_state (plugin_id, key, schema_version, value, updated_at)
73
+ VALUES (?, ?, ?, ?, ?)`,
74
+ )
75
+ .run(pluginId, key, schemaVersion, rawValue, updatedAt);
76
+ });
72
77
  this.cache.set(`${pluginId}:${key}`, {
73
78
  value,
74
79
  schemaVersion,
@@ -78,21 +83,27 @@ export class PluginStateStore {
78
83
  }
79
84
 
80
85
  delete(pluginId: string, key: string): void {
81
- this.db.query("DELETE FROM plugin_state WHERE plugin_id = ? AND key = ?").run(pluginId, key);
86
+ withSqliteBusyRetry("delete plugin state", () => {
87
+ this.db.query("DELETE FROM plugin_state WHERE plugin_id = ? AND key = ?").run(pluginId, key);
88
+ });
82
89
  this.cache.delete(`${pluginId}:${key}`);
83
90
  }
84
91
 
85
92
  keys(pluginId: string): string[] {
86
- return this.db
87
- .query<{ key: string }, [string]>(
88
- "SELECT key FROM plugin_state WHERE plugin_id = ? ORDER BY key",
89
- )
90
- .all(pluginId)
91
- .map((row) => row.key);
93
+ return withSqliteBusyRetry("list plugin state keys", () => (
94
+ this.db
95
+ .query<{ key: string }, [string]>(
96
+ "SELECT key FROM plugin_state WHERE plugin_id = ? ORDER BY key",
97
+ )
98
+ .all(pluginId)
99
+ .map((row) => row.key)
100
+ ));
92
101
  }
93
102
 
94
103
  clear(pluginId: string): void {
95
- this.db.query("DELETE FROM plugin_state WHERE plugin_id = ?").run(pluginId);
104
+ withSqliteBusyRetry("clear plugin state", () => {
105
+ this.db.query("DELETE FROM plugin_state WHERE plugin_id = ?").run(pluginId);
106
+ });
96
107
  for (const cacheKey of this.cache.keys()) {
97
108
  if (cacheKey.startsWith(`${pluginId}:`)) {
98
109
  this.cache.delete(cacheKey);