gloomberb 0.9.8 → 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 (177) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/index.test.ts +3 -3
  3. package/src/api-client/index.ts +0 -6
  4. package/src/api-client/request.test.ts +63 -0
  5. package/src/api-client/request.ts +53 -5
  6. package/src/api-client/types.ts +0 -2
  7. package/src/app/pane-runtime/layout-placement.ts +0 -1
  8. package/src/brokers/account-cache.ts +2 -10
  9. package/src/brokers/broker-portfolio-sync.ts +0 -1
  10. package/src/brokers/broker-ticker-sync.ts +20 -16
  11. package/src/brokers/remote-broker-adapter.ts +3 -0
  12. package/src/capabilities/registry.test.ts +25 -0
  13. package/src/capabilities/registry.ts +22 -5
  14. package/src/cli/commands/automation.ts +1 -3
  15. package/src/cli/commands/command-utils.ts +0 -7
  16. package/src/cli/commands/overview.ts +2 -2
  17. package/src/cli/commands/plugins.ts +34 -17
  18. package/src/cli/index.ts +2 -2
  19. package/src/cli.test.ts +0 -157
  20. package/src/components/chart/comparison/renderer.ts +0 -1
  21. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  22. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  23. package/src/components/chart/core/index.test.ts +0 -24
  24. package/src/components/chart/core/renderer.ts +0 -4
  25. package/src/components/chart/core/resolution.test.ts +0 -17
  26. package/src/components/chart/core/resolution.ts +0 -26
  27. package/src/components/chart/core/scroll.test.ts +0 -6
  28. package/src/components/chart/core/scroll.ts +0 -22
  29. package/src/components/chart/core/viewport.test.ts +2 -23
  30. package/src/components/chart/core/viewport.ts +0 -27
  31. package/src/components/chart/market-session.ts +1 -1
  32. package/src/components/chart/native/surface/hooks.ts +10 -3
  33. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  34. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  35. package/src/components/command-bar/helpers.test.ts +1 -80
  36. package/src/components/command-bar/helpers.ts +1 -36
  37. package/src/components/command-bar/pane-settings.ts +1 -1
  38. package/src/components/command-bar/panel/runtime.ts +0 -2
  39. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  40. package/src/components/command-bar/surface/index.test.tsx +0 -16
  41. package/src/components/command-bar/surface/index.tsx +0 -1
  42. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  43. package/src/components/feed-data-table/stack-view.tsx +0 -1
  44. package/src/components/layout/shell/index.test.tsx +2 -113
  45. package/src/components/layout/shell/index.tsx +0 -1
  46. package/src/components/layout/shell/menu.ts +0 -1
  47. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  48. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  49. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  50. package/src/components/price-selector-dialog.tsx +3 -3
  51. package/src/components/ticker/badge/text.tsx +0 -1
  52. package/src/components/ui/page-stack-view.tsx +1 -1
  53. package/src/components/ui/ui.test.tsx +0 -78
  54. package/src/core/state/app/layout-reducer.ts +0 -1
  55. package/src/data/config/layout.ts +3 -3
  56. package/src/data/resource-store.ts +6 -0
  57. package/src/data/sqlite/cache.test.ts +5 -0
  58. package/src/data/ticker-store.ts +0 -7
  59. package/src/market-data/coordinator/auxiliary.ts +1 -44
  60. package/src/market-data/coordinator/entries.ts +0 -1
  61. package/src/market-data/coordinator/index.test.ts +0 -10
  62. package/src/market-data/coordinator/index.ts +1 -18
  63. package/src/market-data/coordinator/quotes.ts +0 -6
  64. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  65. package/src/market-data/market/status.ts +19 -1
  66. package/src/market-data/query-store.ts +0 -4
  67. package/src/market-data/quotes/resolution.ts +2 -2
  68. package/src/market-data/request-types.ts +0 -5
  69. package/src/market-data/result-types.ts +0 -11
  70. package/src/market-data/selectors.ts +1 -5
  71. package/src/news/aggregator.ts +5 -1
  72. package/src/pane-settings.test.ts +0 -18
  73. package/src/plugins/builtin/account-management/form-components.tsx +0 -120
  74. package/src/plugins/builtin/account-management/model.ts +3 -27
  75. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  76. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  77. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  78. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  79. package/src/plugins/builtin/buildout/format.ts +2 -11
  80. package/src/plugins/builtin/chat/chat.test.tsx +2 -1
  81. package/src/plugins/builtin/chat/content/index.tsx +1 -2
  82. package/src/plugins/builtin/chat/content/transcript.tsx +0 -1
  83. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  84. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  85. package/src/plugins/builtin/chat/sidebar.test.tsx +15 -5
  86. package/src/plugins/builtin/chat/test-harness.tsx +2 -2
  87. package/src/plugins/builtin/cloud/plugin.tsx +1 -1
  88. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  89. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  90. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  91. package/src/plugins/builtin/earnings/index.tsx +0 -9
  92. package/src/plugins/builtin/econ/index.tsx +1 -5
  93. package/src/plugins/builtin/help/index.tsx +2 -2
  94. package/src/plugins/builtin/holders/pane.tsx +0 -5
  95. package/src/plugins/builtin/insider/index.tsx +1 -8
  96. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  97. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  98. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  99. package/src/plugins/builtin/news/index.tsx +0 -1
  100. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  101. package/src/plugins/builtin/options/view.test.tsx +1 -38
  102. package/src/plugins/builtin/plugin-groups.ts +17 -3
  103. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  104. package/src/plugins/builtin/portfolio-list/index.test.ts +1 -24
  105. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  106. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  107. package/src/plugins/builtin/sec/index.tsx +0 -2
  108. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  109. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  110. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  111. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +1 -1
  112. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  113. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  114. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  115. package/src/plugins/builtin/world-indices/model.ts +0 -7
  116. package/src/plugins/ibkr/account-cache.ts +1 -1
  117. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  118. package/src/plugins/ibkr/trade/utils.ts +1 -7
  119. package/src/plugins/loader.ts +2 -1
  120. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  121. package/src/plugins/registry/contributions.ts +22 -11
  122. package/src/plugins/registry/index.test.ts +44 -0
  123. package/src/plugins/registry/index.ts +44 -26
  124. package/src/remote/controller.ts +1 -7
  125. package/src/remote/json-patch.ts +15 -1
  126. package/src/remote/server.ts +16 -6
  127. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  128. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  129. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  130. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  131. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  132. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  133. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  134. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  135. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  136. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  137. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  138. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  139. package/src/renderers/opentui/host.tsx +11 -3
  140. package/src/renderers/opentui/input-host.tsx +5 -1
  141. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  142. package/src/sources/gloomberb-cloud/index.ts +1 -30
  143. package/src/sources/gloomberb-cloud/news.ts +0 -12
  144. package/src/sources/provider-router/financial-routes.ts +25 -3
  145. package/src/sources/provider-router/index.test.ts +167 -0
  146. package/src/sources/provider-router/search.ts +5 -0
  147. package/src/sources/provider-router/supplemental.ts +4 -3
  148. package/src/sources/yahoo-finance/history.ts +5 -1
  149. package/src/sources/yahoo-finance/options.ts +59 -14
  150. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  151. package/src/sources/yahoo-finance.test.ts +40 -21
  152. package/src/sources/yahoo-finance.ts +13 -8
  153. package/src/state/app/context/index.test.tsx +0 -81
  154. package/src/state/app/context/selector.test.tsx +6 -2
  155. package/src/sync/controller.test.ts +31 -0
  156. package/src/sync/controller.ts +21 -17
  157. package/src/test-support/data-provider.ts +0 -1
  158. package/src/ui/host.tsx +2 -1
  159. package/src/ui/index.tsx +1 -0
  160. package/src/updater.test.ts +0 -21
  161. package/src/updater.ts +20 -16
  162. package/src/utils/async-deadline.ts +26 -0
  163. package/src/utils/selection-clipboard.ts +2 -2
  164. package/src/version.ts +1 -1
  165. package/src/components/layout/pane/header.test.tsx +0 -39
  166. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  167. package/src/plugins/builtin/chat/index.tsx +0 -4
  168. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  169. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  170. package/src/plugins/builtin/help/index.test.tsx +0 -242
  171. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  172. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  173. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  174. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  175. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  176. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  177. /package/src/{plugins/ibkr → utils}/hash.ts +0 -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,7 +5,7 @@ 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 ".";
8
+ import { shouldToggleCashMarginDrawer } from ".";
9
9
  import { needsVisibleQuoteWatchdogRefresh, selectQuoteWarmupTickers, selectStreamTickers } from "./pane/data";
10
10
  import { buildPortfolioPaneSettingsDef, getPortfolioPaneSettings } from "./settings";
11
11
 
@@ -221,29 +221,6 @@ describe("visible quote refresh predicates", () => {
221
221
  });
222
222
  });
223
223
 
224
- describe("portfolio list pane templates", () => {
225
- test("create panes with default all-collection settings", async () => {
226
- const config = createDefaultConfig("/tmp/gloomberb-portfolio-list-template");
227
- const context = {
228
- config,
229
- layout: config.layout,
230
- focusedPaneId: "portfolio-list:main",
231
- activeTicker: null,
232
- activeCollectionId: "watchlist",
233
- };
234
-
235
- for (const templateId of ["new-collection-pane", "new-watchlist-pane"]) {
236
- const template = portfolioListPlugin.paneTemplates?.find((entry) => entry.id === templateId);
237
- const instance = await template?.createInstance?.(context);
238
- expect(instance).toEqual({ params: { collectionId: "watchlist" } });
239
- }
240
-
241
- const portfolioTemplate = portfolioListPlugin.paneTemplates?.find((entry) => entry.id === "new-portfolio-pane");
242
- const portfolioInstance = await portfolioTemplate?.createInstance?.(context);
243
- expect(portfolioInstance).toEqual({ params: { collectionId: "main" } });
244
- });
245
- });
246
-
247
224
  describe("portfolio list pane settings", () => {
248
225
  test("exposes view mode only for portfolio collections", () => {
249
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" }],
@@ -21,17 +21,6 @@ export function documentContentTarget(filing: SecFilingItem, document: SecFiling
21
21
  };
22
22
  }
23
23
 
24
- export function formatDocumentLabel(document: SecFilingDocument): string {
25
- const label = document.isPrimary ? "PRIMARY" : document.type || "DOCUMENT";
26
- const description = document.description
27
- && document.description !== document.document
28
- && document.description !== document.type
29
- ? ` | ${document.description}`
30
- : "";
31
- const size = document.size ? ` | ${document.size}` : "";
32
- return `${label.padEnd(8)} ${document.document}${description}${size}`;
33
- }
34
-
35
24
  export function formatCompactDocumentLabel(document: SecFilingDocument): string {
36
25
  const label = document.isPrimary ? "PRIMARY" : document.type || "DOCUMENT";
37
26
  const description = document.description
@@ -229,8 +229,6 @@ function toFeedItems(
229
229
  eyebrow: filing.form,
230
230
  title: form4Preview ? `${displayTitle} | ${form4Preview}` : enrichedTitle,
231
231
  timestamp: filing.filingDate,
232
- preview: form4Preview
233
- ?? (filing.items ? `Items ${filing.items}` : (formDesc || filing.primaryDocument)),
234
232
  detailTitle: enrichedTitle,
235
233
  detailMeta: [
236
234
  `Filed ${formatFiledAt(filing)}`,
@@ -43,7 +43,7 @@ const ARTICLE_IMAGE_MAX_HEIGHT = 26;
43
43
  const ARTICLE_IMAGE_MIN_HEIGHT = 6;
44
44
  const ARTICLE_IMAGE_LEGACY_HEIGHT_CAP = 14;
45
45
 
46
- export function resolveArticleImageSize(lineWidth: number) {
46
+ function resolveArticleImageSize(lineWidth: number) {
47
47
  const safeLineWidth = Math.max(1, Math.floor(lineWidth));
48
48
  const width = Math.min(safeLineWidth, ARTICLE_IMAGE_MAX_WIDTH);
49
49
  const legacyHeight = Math.min(ARTICLE_IMAGE_LEGACY_HEIGHT_CAP, Math.floor(width * 0.32));
@@ -226,21 +226,6 @@ export async function searchThirteenFFunds(
226
226
  return arrayResponse(raw).map(mapFund).filter((fund): fund is ThirteenFFund => !!fund);
227
227
  }
228
228
 
229
- export async function listThirteenFFilers(
230
- limit = 50,
231
- signal?: AbortSignal,
232
- options: { forceRefresh?: boolean; offset?: number } = {},
233
- ): Promise<ThirteenFFund[]> {
234
- const raw = await fetchForms13F<unknown>("/filers", {
235
- offset: options.offset ?? 0,
236
- limit,
237
- }, { signal, forceRefresh: options.forceRefresh });
238
- return arrayResponse(raw).map((entry) => mapFund({
239
- cik: entry.cik,
240
- name: Array.isArray(entry.company_names) ? entry.company_names[0] : entry.company_name,
241
- })).filter((fund): fund is ThirteenFFund => !!fund);
242
- }
243
-
244
229
  export async function listTopThirteenFFunds(
245
230
  quarter: string,
246
231
  limit = 50,
@@ -235,7 +235,6 @@ export function ThirteenFPane({ focused, width, height }: PaneProps) {
235
235
  }, { allowEditable: true });
236
236
 
237
237
  const sortedRows = useMemo(() => sortBrowserRows(rows, sortPreference), [rows, sortPreference]);
238
- const selectedIndex = selectedIndexById(sortedRows, selectedId);
239
238
  const columns = useMemo(() => buildBrowserColumns(width), [width]);
240
239
 
241
240
  useEffect(() => {
@@ -643,7 +642,7 @@ function FundDetailView({
643
642
  getId: (row) => row.id,
644
643
  onChange: (id) => setFilingSelectedId(id),
645
644
  }}
646
- onActivate={openFilingInPane}
645
+ onActivate={(row) => openFilingInPane(row)}
647
646
  onDetailKeyDown={(event) => {
648
647
  if (event.name !== "o" || !openFiling?.url) return false;
649
648
  event.preventDefault?.();
@@ -755,7 +754,6 @@ function FilingDetailView({
755
754
  sortPreference,
756
755
  )
757
756
  ), [filing.tableValueTotal, holdings, sortPreference]);
758
- const selectedPositionIndex = selectedIndexById(positionRows, selectedPositionId);
759
757
  const columns = useMemo(() => buildFilingPositionColumns(width), [width]);
760
758
 
761
759
  useEffect(() => {
@@ -267,7 +267,7 @@ export function FundamentalGraphContent({
267
267
  header={metricHeader}
268
268
  formatValue={definition.format}
269
269
  onHoverChange={setHoveredBar}
270
- onMouseDown={focusPaneForMouseInteraction}
270
+ onMouseDown={focusMetricTabs}
271
271
  />
272
272
  {tableHeight > 0 ? (
273
273
  <DataTableView<FundamentalGraphRow, FundamentalColumn>
@@ -274,13 +274,6 @@ export function ResolvedFinancialsTab({
274
274
  })),
275
275
  ];
276
276
  const rows = buildFinancialRows(subTab.rows, displayStatements, collapsedGroups);
277
- const selectedIndex = selectedRowId
278
- ? rows.findIndex((row) => row.id === selectedRowId)
279
- : rows.length > 0 ? 0 : -1;
280
- const effectiveSelectedIndex = selectedIndex >= 0
281
- ? selectedIndex
282
- : rows.length > 0 ? 0 : -1;
283
-
284
277
  useEffect(() => {
285
278
  if (rows.length === 0) {
286
279
  if (selectedRowId !== null) setSelectedRowId(null);
@@ -13,10 +13,12 @@ import type { PinTickerOptions } from "../../../../types/plugin";
13
13
 
14
14
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
15
15
 
16
- afterEach(() => {
16
+ afterEach(async () => {
17
17
  setSharedMarketDataCoordinator(null);
18
18
  if (testSetup) {
19
- testSetup.renderer.destroy();
19
+ await act(async () => {
20
+ testSetup!.renderer.destroy();
21
+ });
20
22
  testSetup = undefined;
21
23
  }
22
24
  });
@@ -181,44 +183,13 @@ async function renderOnce() {
181
183
  async function flushFrames(count: number) {
182
184
  for (let index = 0; index < count; index += 1) {
183
185
  await act(async () => {
184
- await Promise.resolve();
186
+ await new Promise((resolve) => setTimeout(resolve, 0));
185
187
  await testSetup!.renderOnce();
186
188
  });
187
189
  }
188
190
  }
189
191
 
190
192
  describe("QuoteMonitorPane", () => {
191
- test("renders a compact quote card for the bound ticker", async () => {
192
- await renderHarness(createQuoteMonitorHarness(), {
193
- width: 72,
194
- height: 7,
195
- });
196
-
197
- await renderOnce();
198
-
199
- const frame = testSetup.captureCharFrame();
200
- expect(frame).toContain("MSFT");
201
- expect(frame).toContain("$356.15");
202
- expect(frame).toContain("-2.68%");
203
- expect(frame).toContain("-9.82");
204
- expect(frame).toMatch(/[⠁-⣿]/);
205
- });
206
-
207
- test("renders multiple configured tickers", async () => {
208
- await renderHarness(createQuoteMonitorHarness({ symbols: ["MSFT", "AAPL"] }), {
209
- width: 72,
210
- height: 8,
211
- });
212
-
213
- await renderOnce();
214
-
215
- const frame = testSetup.captureCharFrame();
216
- expect(frame).toContain("MSFT");
217
- expect(frame).toContain("AAPL");
218
- expect(frame).toContain("$356.15");
219
- expect(frame).toContain("$202.12");
220
- });
221
-
222
193
  test("opens a Ticker Research pane on the second card click", async () => {
223
194
  const pinCalls: PinTickerCall[] = [];
224
195
  await renderHarness(createQuoteMonitorHarness({ pinCalls }), {
@@ -9,7 +9,6 @@ import {
9
9
  buildFlatRows,
10
10
  DEFAULT_SORT_PREFERENCE,
11
11
  nextSortPreference,
12
- rowIndicesOf,
13
12
  type IndexQuoteState,
14
13
  type QuoteMap,
15
14
  type WorldIndexSortPreference,
@@ -36,12 +35,9 @@ function WorldIndicesPane({ focused, width, height }: PaneProps) {
36
35
  () => buildFlatRows(indicesByRegion, sortPreference, quotes),
37
36
  [indicesByRegion, quotes, sortPreference],
38
37
  );
39
- const navigableIndices = useMemo(() => rowIndicesOf(flatRows), [flatRows]);
40
38
  const selectedFlatIdx = selectedSymbol
41
39
  ? flatRows.findIndex((row) => row.type === "row" && row.entry.symbol === selectedSymbol)
42
40
  : -1;
43
- const activeFlatIdx = selectedFlatIdx >= 0 ? selectedFlatIdx : (navigableIndices[0] ?? -1);
44
-
45
41
  useEffect(() => {
46
42
  if (selectedSymbol && selectedFlatIdx >= 0) return;
47
43
  const firstRow = flatRows.find((row) => row.type === "row");
@@ -87,13 +87,6 @@ export function buildFlatRows(
87
87
  return rows;
88
88
  }
89
89
 
90
- export function rowIndicesOf(flatRows: WorldIndexTableRow[]): number[] {
91
- return flatRows.reduce<number[]>((acc, row, i) => {
92
- if (row.type === "row") acc.push(i);
93
- return acc;
94
- }, []);
95
- }
96
-
97
90
  export function nextSortPreference(
98
91
  current: WorldIndexSortPreference,
99
92
  columnId: string,
@@ -1,7 +1,7 @@
1
1
  import type { BrokerInstanceConfig } from "../../types/config";
2
2
  import type { CachePolicy } from "../../types/persistence";
3
3
  import { normalizeIbkrConfig } from "./config";
4
- import { fnv1aHashString } from "./hash";
4
+ import { fnv1aHashString } from "../../utils/hash";
5
5
 
6
6
  const FLEX_ACCOUNT_CACHE_POLICY = {
7
7
  staleMs: 6 * 60 * 60 * 1000,
@@ -5,7 +5,7 @@ import type { BrokerPortfolioPerformance } from "../../types/trading";
5
5
  import { debugLog } from "../../utils/debug-log";
6
6
  import { loadFlexStatement, parseFlexPortfolioPerformance } from "./flex";
7
7
  import { normalizeIbkrConfig } from "./config";
8
- import { fnv1aHashString } from "./hash";
8
+ import { fnv1aHashString } from "../../utils/hash";
9
9
 
10
10
  const PERFORMANCE_LOG = debugLog.createLogger("ibkr-performance");
11
11
  const PERFORMANCE_CACHE_KIND = "portfolio-performance";
@@ -5,6 +5,7 @@ import type { TickerRecord } from "../../../types/ticker";
5
5
  import type { BrokerAccount, BrokerOrderPreview, BrokerOrderType } from "../../../types/trading";
6
6
  import { colors } from "../../../theme/colors";
7
7
  import { formatCompact, formatCurrency } from "../../../utils/format";
8
+ export { truncateWithEllipsis as truncateTradeText } from "../../../utils/text-wrap";
8
9
  import { formatMarketPrice, formatMarketPriceWithCurrency, formatSignedMarketPrice, type AssetDisplayContext } from "../../../market-data/market/format";
9
10
  import { getConfiguredIbkrGatewayInstances } from "../instance-selection";
10
11
 
@@ -108,13 +109,6 @@ export function formatPreviewMetric(before?: number, after?: number): string {
108
109
  return `${formatCompact(before || 0)} → ${formatCompact(after || 0)}`;
109
110
  }
110
111
 
111
- export function truncateTradeText(value: string, maxWidth: number): string {
112
- if (maxWidth <= 0) return "";
113
- if (value.length <= maxWidth) return value;
114
- if (maxWidth <= 3) return value.slice(0, maxWidth);
115
- return `${value.slice(0, maxWidth - 3)}...`;
116
- }
117
-
118
112
  export function findTickerForOrder(
119
113
  order: { contract: BrokerContractRef },
120
114
  tickers: Map<string, TickerRecord>,
@@ -1,12 +1,13 @@
1
1
  import { readdir } from "fs/promises";
2
2
  import { join } from "path";
3
3
  import { existsSync } from "fs";
4
+ import { homedir } from "os";
4
5
  import type { GloomPlugin } from "../types/plugin";
5
6
  import { debugLog } from "../utils/debug-log";
6
7
 
7
8
  const loaderLog = debugLog.createLogger("plugin-loader");
8
9
 
9
- const PLUGINS_DIR = join(process.env.HOME || "~", ".gloomberb", "plugins");
10
+ const PLUGINS_DIR = join(process.env.HOME || homedir(), ".gloomberb", "plugins");
10
11
 
11
12
  export interface LoadedExternalPlugin {
12
13
  plugin: GloomPlugin;
@@ -64,38 +64,4 @@ describe("prediction markets detail views", () => {
64
64
  expect(frame).not.toContain("Loading market detail...");
65
65
  });
66
66
 
67
- test("renders book levels in the shared data table", async () => {
68
- testSetup = await testRender(<GroupedDetailHarness detailTab="book" />, {
69
- width: 64,
70
- height: 24,
71
- });
72
- await flushFrames(testSetup);
73
-
74
- const frame = testSetup.captureCharFrame();
75
- expect(frame).toContain("OUT");
76
- expect(frame).toContain("SIDE");
77
- expect(frame).toContain("PRICE");
78
- expect(frame).toContain("SIZE");
79
- expect(frame).toContain("YES");
80
- expect(frame).toContain("BID");
81
- expect(frame).toContain("47c");
82
- });
83
-
84
- test("renders recent trades in the shared data table", async () => {
85
- testSetup = await testRender(<GroupedDetailHarness detailTab="trades" />, {
86
- width: 64,
87
- height: 24,
88
- });
89
- await flushFrames(testSetup);
90
-
91
- const frame = testSetup.captureCharFrame();
92
- expect(frame).toContain("TIME");
93
- expect(frame).toContain("SIDE");
94
- expect(frame).toContain("OUT");
95
- expect(frame).toContain("PRICE");
96
- expect(frame).toContain("SIZE");
97
- expect(frame).toContain("BUY");
98
- expect(frame).toContain("YES");
99
- expect(frame).toContain("48c");
100
- });
101
67
  });