gloomberb 0.7.0 → 0.7.2

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 (126) hide show
  1. package/package.json +1 -1
  2. package/src/app.tsx +149 -51
  3. package/src/capabilities/factories.ts +3 -0
  4. package/src/components/chart/chart-renderer.ts +24 -12
  5. package/src/components/chart/chart.test.ts +43 -0
  6. package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
  7. package/src/components/chart/comparison-chart-renderer.ts +13 -6
  8. package/src/components/chart/comparison-stock-chart.tsx +15 -12
  9. package/src/components/chart/price-axis-labels.tsx +114 -0
  10. package/src/components/chart/static-chart-surface.tsx +2 -0
  11. package/src/components/chart/stock-chart.tsx +31 -20
  12. package/src/components/chart/time-axis-label.tsx +78 -1
  13. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
  14. package/src/components/command-bar/command-bar.test.tsx +114 -31
  15. package/src/components/command-bar/command-bar.tsx +587 -335
  16. package/src/components/command-bar/command-registry.ts +0 -10
  17. package/src/components/command-bar/helpers.ts +2 -5
  18. package/src/components/command-bar/list-model.ts +1 -2
  19. package/src/components/command-bar/theme-picker.tsx +254 -0
  20. package/src/components/command-bar/view-model.test.ts +4 -4
  21. package/src/components/command-bar/view-model.ts +1 -1
  22. package/src/components/command-bar/workflow-types.ts +1 -2
  23. package/src/components/layout/detached-pane-shell.tsx +29 -10
  24. package/src/components/layout/floating-pane.tsx +5 -2
  25. package/src/components/layout/header.tsx +2 -0
  26. package/src/components/layout/pane-content.test.tsx +64 -0
  27. package/src/components/layout/pane-content.tsx +2 -0
  28. package/src/components/layout/pane-sizing.ts +9 -2
  29. package/src/components/layout/pane-title.ts +4 -1
  30. package/src/components/layout/pane.tsx +11 -5
  31. package/src/components/layout/shell.test.tsx +198 -0
  32. package/src/components/layout/shell.tsx +33 -2
  33. package/src/components/layout/status-bar.tsx +2 -0
  34. package/src/components/speedometer-gauge.tsx +29 -25
  35. package/src/components/table-view-shared.tsx +85 -3
  36. package/src/components/ticker-list-table-view.tsx +16 -2
  37. package/src/components/ticker-list-table.test.tsx +82 -1
  38. package/src/components/ticker-list-table.tsx +22 -9
  39. package/src/components/ui/data-table.tsx +12 -9
  40. package/src/components/ui/frame.tsx +2 -0
  41. package/src/components/ui/table-layout.test.ts +27 -1
  42. package/src/components/ui/table-layout.ts +39 -1
  43. package/src/components/ui/tabs.tsx +34 -1
  44. package/src/components/ui/ui.test.tsx +68 -1
  45. package/src/core/state/app-state.ts +4 -14
  46. package/src/market-data/coordinator.test.ts +75 -0
  47. package/src/market-data/coordinator.ts +218 -48
  48. package/src/news/aggregator.test.ts +49 -1
  49. package/src/news/aggregator.ts +48 -0
  50. package/src/news/hooks.ts +6 -2
  51. package/src/plugins/builtin/ai/screener-pane.tsx +3 -2
  52. package/src/plugins/builtin/analytics/index.tsx +1 -18
  53. package/src/plugins/builtin/chat-controller.test.ts +83 -1
  54. package/src/plugins/builtin/chat-controller.ts +491 -243
  55. package/src/plugins/builtin/chat.test.tsx +425 -7
  56. package/src/plugins/builtin/chat.tsx +1109 -211
  57. package/src/plugins/builtin/cloud-tweets.tsx +1 -0
  58. package/src/plugins/builtin/fear-greed/index.tsx +148 -39
  59. package/src/plugins/builtin/holders/index.test.ts +13 -1
  60. package/src/plugins/builtin/holders/index.tsx +54 -14
  61. package/src/plugins/builtin/market-movers/index.tsx +12 -18
  62. package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
  63. package/src/plugins/builtin/market-movers/screener.ts +70 -17
  64. package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
  65. package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
  66. package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
  67. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
  68. package/src/plugins/builtin/notes.tsx +1 -0
  69. package/src/plugins/builtin/options.tsx +2 -16
  70. package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
  71. package/src/plugins/builtin/research/index.test.ts +94 -0
  72. package/src/plugins/builtin/research/index.tsx +159 -12
  73. package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
  74. package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
  75. package/src/plugins/plugin-runtime.test.tsx +34 -0
  76. package/src/plugins/plugin-runtime.tsx +7 -2
  77. package/src/plugins/prediction-markets/controller.ts +0 -15
  78. package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
  79. package/src/plugins/registry.ts +3 -3
  80. package/src/plugins/ticker-navigation.test.ts +66 -0
  81. package/src/plugins/ticker-navigation.ts +44 -0
  82. package/src/react/input.ts +1 -1
  83. package/src/renderers/electrobun/bun/index.ts +32 -11
  84. package/src/renderers/electrobun/bun/window-focus.ts +37 -0
  85. package/src/renderers/electrobun/shared/protocol.ts +7 -1
  86. package/src/renderers/electrobun/view/app-services.ts +88 -31
  87. package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
  88. package/src/renderers/electrobun/view/data-table.tsx +111 -96
  89. package/src/renderers/electrobun/view/desktop-controls.tsx +8 -0
  90. package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
  91. package/src/renderers/electrobun/view/dialog-host.tsx +2 -0
  92. package/src/renderers/electrobun/view/input-host.tsx +1 -1
  93. package/src/renderers/electrobun/view/main.tsx +18 -1
  94. package/src/renderers/electrobun/view/styles.css +6 -8
  95. package/src/renderers/electrobun/view/toast-host.tsx +29 -3
  96. package/src/renderers/electrobun/view/ui-host.tsx +101 -5
  97. package/src/renderers/opentui/input-host.tsx +1 -1
  98. package/src/sources/gloomberb-cloud.test.ts +86 -1
  99. package/src/sources/gloomberb-cloud.ts +117 -21
  100. package/src/sources/provider-router.test.ts +122 -0
  101. package/src/sources/provider-router.ts +198 -11
  102. package/src/sources/yahoo-finance.test.ts +44 -0
  103. package/src/sources/yahoo-finance.ts +6 -0
  104. package/src/state/app-bootstrap.ts +20 -7
  105. package/src/state/app-context-selector.test.tsx +189 -4
  106. package/src/state/app-context.test.ts +20 -0
  107. package/src/state/app-context.tsx +48 -16
  108. package/src/state/use-inline-tickers.ts +13 -10
  109. package/src/theme/colors.test.ts +75 -0
  110. package/src/theme/colors.ts +64 -3
  111. package/src/theme/theme-context.tsx +26 -0
  112. package/src/types/capability-route-source.ts +1 -0
  113. package/src/types/data-provider.ts +15 -1
  114. package/src/types/desktop-window.ts +6 -0
  115. package/src/types/financials.ts +2 -0
  116. package/src/types/plugin.ts +1 -1
  117. package/src/ui/host.tsx +14 -2
  118. package/src/ui/index.tsx +1 -0
  119. package/src/utils/api-client.test.ts +32 -0
  120. package/src/utils/api-client.ts +119 -18
  121. package/src/utils/double-escape-close.test.ts +33 -0
  122. package/src/utils/double-escape-close.ts +40 -0
  123. package/src/utils/throttled-fetch.test.ts +22 -0
  124. package/src/utils/throttled-fetch.ts +3 -1
  125. package/src/utils/ticker-search.ts +53 -9
  126. package/src/version.ts +1 -1
@@ -2,12 +2,14 @@ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act, useReducer } from "react";
3
3
  import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
4
4
  import { CommandBar } from "./command-bar";
5
- import { AppContext, type AppState, appReducer, createInitialState, getEffectiveThemeId } from "../../state/app-context";
5
+ import { AppContext, type AppAction, type AppState, appReducer, createInitialState, getEffectiveThemeId } from "../../state/app-context";
6
6
  import { createTestDataProvider } from "../../test-support/data-provider";
7
+ import { ThemeProvider, useThemeId } from "../../theme/theme-context";
7
8
  import { cloneLayout, createDefaultConfig, type AppConfig } from "../../types/config";
8
9
  import type { DataProvider } from "../../types/data-provider";
9
10
  import type { TickerRecord } from "../../types/ticker";
10
11
  import type { PluginRegistry } from "../../plugins/registry";
12
+ import type { PaneTemplateCreateOptions } from "../../types/plugin";
11
13
 
12
14
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
13
15
 
@@ -161,7 +163,7 @@ function makePluginRegistry(hasPaneSettings: (paneId: string) => boolean = () =>
161
163
  paneId: "chat",
162
164
  label: "New Chat Pane",
163
165
  description: "Open another floating chat window",
164
- shortcut: { prefix: "CHAT" },
166
+ shortcut: { prefix: "CHAT", argPlaceholder: "channel", argKind: "text" },
165
167
  }],
166
168
  ["quote-monitor-pane", {
167
169
  id: "quote-monitor-pane",
@@ -230,6 +232,10 @@ function makePluginRegistry(hasPaneSettings: (paneId: string) => boolean = () =>
230
232
  } as unknown as PluginRegistry;
231
233
  }
232
234
 
235
+ function ThemeProbe() {
236
+ return <text>{`theme:${useThemeId()}`}</text>;
237
+ }
238
+
233
239
  function CommandBarHarness({
234
240
  query,
235
241
  disabledPlugins = [],
@@ -244,6 +250,7 @@ function CommandBarHarness({
244
250
  dataProvider = makeDataProvider(),
245
251
  hasPaneSettings,
246
252
  onCheckForUpdates,
253
+ onAction,
247
254
  }: {
248
255
  query: string;
249
256
  disabledPlugins?: string[];
@@ -258,6 +265,7 @@ function CommandBarHarness({
258
265
  dataProvider?: DataProvider;
259
266
  hasPaneSettings?: (paneId: string) => boolean;
260
267
  onCheckForUpdates?: () => void | Promise<void>;
268
+ onAction?: (action: AppAction) => void;
261
269
  }) {
262
270
  let config = {
263
271
  ...createDefaultConfig("/tmp/gloomberb-test"),
@@ -272,6 +280,7 @@ function CommandBarHarness({
272
280
  ...createInitialState(config),
273
281
  commandBarOpen: true,
274
282
  commandBarQuery: query,
283
+ focusedPaneId: "portfolio-list:main",
275
284
  tickers: new Map(tickers.map((ticker) => [ticker.metadata.ticker, ticker])),
276
285
  config: { ...config, disabledPlugins },
277
286
  paneState: selectedTicker
@@ -299,24 +308,31 @@ function CommandBarHarness({
299
308
  configurePluginRegistry?.(pluginRegistry);
300
309
  const [liveState, dispatch] = useReducer(appReducer, state);
301
310
  const currentState = live ? liveState : state;
302
- const currentDispatch = live ? dispatch : () => {};
311
+ const currentDispatch = live
312
+ ? (action: AppAction) => {
313
+ onAction?.(action);
314
+ dispatch(action);
315
+ }
316
+ : (_action: AppAction) => {};
303
317
 
304
318
  return (
305
- <AppContext value={{ state: currentState, dispatch: currentDispatch }}>
306
- <TestDialogProvider>
307
- {live && <text>{`theme:${getEffectiveThemeId(currentState)}`}</text>}
308
- {showQueryState && <text>{`query:${currentState.commandBarQuery}`}</text>}
309
- {currentState.commandBarOpen && (
310
- <CommandBar
311
- dataProvider={dataProvider}
312
- tickerRepository={tickerRepository as any}
313
- pluginRegistry={pluginRegistry}
314
- quitApp={() => {}}
315
- onCheckForUpdates={onCheckForUpdates}
316
- />
317
- )}
318
- </TestDialogProvider>
319
- </AppContext>
319
+ <ThemeProvider themeId={getEffectiveThemeId(currentState)}>
320
+ <AppContext value={{ state: currentState, dispatch: currentDispatch }}>
321
+ <TestDialogProvider>
322
+ {live && <ThemeProbe />}
323
+ {showQueryState && <text>{`query:${currentState.commandBarQuery}`}</text>}
324
+ {currentState.commandBarOpen && (
325
+ <CommandBar
326
+ dataProvider={dataProvider}
327
+ tickerRepository={tickerRepository as any}
328
+ pluginRegistry={pluginRegistry}
329
+ quitApp={() => {}}
330
+ onCheckForUpdates={onCheckForUpdates}
331
+ />
332
+ )}
333
+ </TestDialogProvider>
334
+ </AppContext>
335
+ </ThemeProvider>
320
336
  );
321
337
  }
322
338
 
@@ -333,7 +349,7 @@ describe("CommandBar", () => {
333
349
  });
334
350
 
335
351
  test("runs check for updates from the command bar", async () => {
336
- const calls = [];
352
+ const calls: number[] = [];
337
353
 
338
354
  testSetup = await testRender(<CommandBarHarness query="check for updates" live onCheckForUpdates={() => { calls.push(Date.now()); }} />, {
339
355
  width: 80,
@@ -738,7 +754,6 @@ describe("CommandBar", () => {
738
754
  const frame = testSetup.captureCharFrame();
739
755
  expect(frame).toContain("Commands");
740
756
  expect(frame).toContain("TH");
741
- expect(frame).toContain("Themes");
742
757
  expect(frame).toContain("Amber");
743
758
  expect(frame).toContain("Paper");
744
759
  expect(frame).toContain("GitHub Light");
@@ -797,6 +812,7 @@ describe("CommandBar", () => {
797
812
  await testSetup.renderOnce();
798
813
  await act(async () => {
799
814
  testSetup!.mockInput.pressArrow("down");
815
+ await Bun.sleep(220);
800
816
  await testSetup!.renderOnce();
801
817
  });
802
818
  await testSetup.renderOnce();
@@ -805,6 +821,38 @@ describe("CommandBar", () => {
805
821
  expect(frame).toContain("theme:green");
806
822
  });
807
823
 
824
+ test("debounces rapid keyboard theme previews", async () => {
825
+ const actions: AppAction[] = [];
826
+ testSetup = await testRender(
827
+ <CommandBarHarness query="TH " live onAction={(action) => actions.push(action)} />,
828
+ { width: 80, height: 24 },
829
+ );
830
+
831
+ await testSetup.renderOnce();
832
+ await act(async () => {
833
+ for (let index = 0; index < 6; index++) {
834
+ testSetup!.mockInput.pressArrow("down");
835
+ }
836
+ await testSetup!.renderOnce();
837
+ });
838
+
839
+ expect(actions.filter((action) => action.type === "PREVIEW_THEME")).toHaveLength(0);
840
+
841
+ await act(async () => {
842
+ await Bun.sleep(100);
843
+ await testSetup!.renderOnce();
844
+ });
845
+
846
+ expect(actions.filter((action) => action.type === "PREVIEW_THEME")).toHaveLength(0);
847
+
848
+ await act(async () => {
849
+ await Bun.sleep(150);
850
+ await testSetup!.renderOnce();
851
+ });
852
+
853
+ expect(actions.filter((action) => action.type === "PREVIEW_THEME")).toHaveLength(1);
854
+ });
855
+
808
856
  test("does not preview a theme from mouse hover alone", async () => {
809
857
  testSetup = await testRender(<CommandBarHarness query="TH " live />, {
810
858
  width: 80,
@@ -840,6 +888,7 @@ describe("CommandBar", () => {
840
888
  await testSetup.renderOnce();
841
889
  await act(async () => {
842
890
  testSetup!.mockInput.pressArrow("down");
891
+ await Bun.sleep(220);
843
892
  await testSetup!.renderOnce();
844
893
  });
845
894
  await testSetup.renderOnce();
@@ -887,8 +936,10 @@ describe("CommandBar", () => {
887
936
  await testSetup.renderOnce();
888
937
  await act(async () => {
889
938
  testSetup!.mockInput.pressArrow("down");
939
+ await Bun.sleep(220);
890
940
  await testSetup!.renderOnce();
891
941
  });
942
+ await testSetup.renderOnce();
892
943
  expect(testSetup.captureCharFrame()).toContain("theme:green");
893
944
 
894
945
  await emitKeypress(testSetup, { name: "u", ctrl: true });
@@ -956,7 +1007,7 @@ describe("CommandBar", () => {
956
1007
  });
957
1008
 
958
1009
  test("typing a shorthand and pressing enter executes the inferred quote monitor shortcut", async () => {
959
- const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
1010
+ const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
960
1011
 
961
1012
  testSetup = await testRender(<CommandBarHarness
962
1013
  query=""
@@ -991,6 +1042,39 @@ describe("CommandBar", () => {
991
1042
  }]);
992
1043
  });
993
1044
 
1045
+ test("typing a chat channel shortcut opens that channel directly", async () => {
1046
+ const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
1047
+
1048
+ testSetup = await testRender(<CommandBarHarness
1049
+ query=""
1050
+ live
1051
+ configurePluginRegistry={(pluginRegistry) => {
1052
+ pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
1053
+ created.push({ templateId, options });
1054
+ };
1055
+ }}
1056
+ />, {
1057
+ width: 100,
1058
+ height: 20,
1059
+ });
1060
+
1061
+ await testSetup.renderOnce();
1062
+
1063
+ await act(async () => {
1064
+ await testSetup!.mockInput.typeText("CHAT help");
1065
+ testSetup!.mockInput.pressEnter();
1066
+ await Bun.sleep(0);
1067
+ await testSetup!.renderOnce();
1068
+ });
1069
+
1070
+ expect(created).toEqual([{
1071
+ templateId: "new-chat-pane",
1072
+ options: {
1073
+ arg: "help",
1074
+ },
1075
+ }]);
1076
+ });
1077
+
994
1078
  test("clears the root query with cmd-backspace", async () => {
995
1079
  testSetup = await testRender(<CommandBarHarness query="DES AMD" />, {
996
1080
  width: 80,
@@ -1653,7 +1737,6 @@ describe("CommandBar", () => {
1653
1737
  await testSetup.renderOnce();
1654
1738
  const initialFrame = testSetup.captureCharFrame();
1655
1739
  expect(initialFrame).toContain("Delete Layout");
1656
- expect(initialFrame).toContain("Danger");
1657
1740
 
1658
1741
  await act(async () => {
1659
1742
  testSetup!.mockInput.pressEnter();
@@ -1904,7 +1987,7 @@ describe("CommandBar", () => {
1904
1987
  });
1905
1988
 
1906
1989
  test("does not treat no-argument shortcut prefixes as typed pane names", async () => {
1907
- const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
1990
+ const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
1908
1991
 
1909
1992
  testSetup = await testRender(<CommandBarHarness
1910
1993
  query="Top News"
@@ -2090,7 +2173,7 @@ describe("CommandBar", () => {
2090
2173
  });
2091
2174
 
2092
2175
  test("QQ MSFT executes directly without opening a secondary workflow", async () => {
2093
- const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
2176
+ const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2094
2177
 
2095
2178
  testSetup = await testRender(<CommandBarHarness
2096
2179
  query="QQ MSFT"
@@ -2123,7 +2206,7 @@ describe("CommandBar", () => {
2123
2206
  });
2124
2207
 
2125
2208
  test("CMP AAPL,MSFT creates the comparison chart directly", async () => {
2126
- const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
2209
+ const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2127
2210
 
2128
2211
  testSetup = await testRender(<CommandBarHarness
2129
2212
  query="CMP AAPL,MSFT"
@@ -2202,7 +2285,7 @@ describe("CommandBar", () => {
2202
2285
  });
2203
2286
 
2204
2287
  test("CMP with one resolved ticker opens inline completion instead of creating a one-symbol chart", async () => {
2205
- const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
2288
+ const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2206
2289
 
2207
2290
  testSetup = await testRender(<CommandBarHarness
2208
2291
  query="CMP AMD"
@@ -2246,7 +2329,7 @@ describe("CommandBar", () => {
2246
2329
  });
2247
2330
 
2248
2331
  test("AI <prompt> opens the inline workflow and prefills the textarea prompt", async () => {
2249
- const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
2332
+ const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2250
2333
 
2251
2334
  testSetup = await testRender(<CommandBarHarness
2252
2335
  query="AI quality compounders"
@@ -2302,7 +2385,7 @@ describe("CommandBar", () => {
2302
2385
  });
2303
2386
 
2304
2387
  test("submits typed AI screener prompts from the textarea field", async () => {
2305
- const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
2388
+ const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2306
2389
 
2307
2390
  testSetup = await testRender(<CommandBarHarness
2308
2391
  query="AI"
@@ -2749,7 +2832,7 @@ describe("CommandBar", () => {
2749
2832
  query="auth login"
2750
2833
  live
2751
2834
  configurePluginRegistry={(pluginRegistry) => {
2752
- pluginRegistry.commands.set("auth-login", {
2835
+ (pluginRegistry.commands as Map<string, any>).set("auth-login", {
2753
2836
  id: "auth-login",
2754
2837
  label: "Auth Login",
2755
2838
  description: "Log in to your account",
@@ -2789,7 +2872,7 @@ describe("CommandBar", () => {
2789
2872
  query="workspace"
2790
2873
  live
2791
2874
  configurePluginRegistry={(pluginRegistry) => {
2792
- pluginRegistry.commands.set("new-workspace", {
2875
+ (pluginRegistry.commands as Map<string, any>).set("new-workspace", {
2793
2876
  id: "new-workspace",
2794
2877
  label: "Workspace",
2795
2878
  description: "Create a workspace",
@@ -2799,7 +2882,7 @@ describe("CommandBar", () => {
2799
2882
  wizard: [
2800
2883
  { key: "name", label: "Name", type: "text", placeholder: "Research" },
2801
2884
  ],
2802
- execute: async (values) => {
2885
+ execute: async (values?: Record<string, string>) => {
2803
2886
  submitted.push(values);
2804
2887
  },
2805
2888
  } as any);