gloomberb 0.10.4 → 0.10.5

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.
package/LICENSE CHANGED
@@ -13,7 +13,7 @@ The above copyright notice and this permission notice shall be included in all
13
13
  copies or substantial portions of the Software.
14
14
 
15
15
  When using this software in a product or service, attribution to the Gloomberb
16
- project (https://github.com/vincelwt/gloomberb) is appreciated and encouraged,
16
+ project (https://github.com/gloom-sh/gloomberb) is appreciated and encouraged,
17
17
  though not legally required beyond preserving the above copyright notice.
18
18
 
19
19
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
package/README.md CHANGED
@@ -64,7 +64,7 @@ This installs `gloomberb` to `~/.local/bin` by default. A Linux desktop package
64
64
 
65
65
  Install the desktop app:
66
66
 
67
- - [Download GloomberbSetup.exe for Windows](https://github.com/vincelwt/gloomberb/releases/latest/download/stable-win-x64-GloomberbSetup.exe)
67
+ - [Download GloomberbSetup.exe for Windows](https://github.com/gloom-sh/gloomberb/releases/latest/download/stable-win-x64-GloomberbSetup.exe)
68
68
 
69
69
  The installer supports Windows 11 on x64 and ARM64. On ARM64, the desktop app and its bundled `gloomberb` terminal command use Windows' built-in x64 emulation.
70
70
 
package/README.zh-CN.md CHANGED
@@ -66,7 +66,7 @@ curl -fsSL gloomberb.com/install | bash
66
66
 
67
67
  安装桌面应用:
68
68
 
69
- - [下载 GloomberbSetup.exe(Windows x64)](https://github.com/vincelwt/gloomberb/releases/latest/download/stable-win-x64-GloomberbSetup.exe)
69
+ - [下载 GloomberbSetup.exe(Windows x64)](https://github.com/gloom-sh/gloomberb/releases/latest/download/stable-win-x64-GloomberbSetup.exe)
70
70
 
71
71
  安装程序会添加应用本体和 `gloomberb` 终端命令。如果只需要终端版本,安装 Bun 后使用该包即可:
72
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "packageManager": "bun@1.3.11",
5
5
  "description": "Market research and portfolio tracker for the terminal",
6
6
  "module": "src/index.tsx",
@@ -30,12 +30,13 @@
30
30
  "typecheck:electrobun-view": "tsc --project tsconfig.electrobun-view.json",
31
31
  "typecheck:scripts": "tsc --project tsconfig.scripts.json",
32
32
  "test": "bun test",
33
+ "test:global-upgrade": "bun run scripts/smoke-global-upgrade.ts",
33
34
  "release": "./scripts/release.sh"
34
35
  },
35
36
  "license": "MIT",
36
37
  "repository": {
37
38
  "type": "git",
38
- "url": "https://github.com/vincelwt/gloomberb"
39
+ "url": "https://github.com/gloom-sh/gloomberb"
39
40
  },
40
41
  "keywords": [
41
42
  "terminal",
@@ -59,15 +60,12 @@
59
60
  "dependencies": {
60
61
  "@earendil-works/pi-agent-core": "0.81.1",
61
62
  "@earendil-works/pi-ai": "0.81.1",
62
- "@opentui-ui/dialog": "^0.1.2",
63
- "@opentui-ui/toast": "^0.0.5",
64
63
  "@opentui/core": "^0.3.2",
65
64
  "@opentui/react": "^0.3.2",
66
65
  "@stoqey/ib": "^1.5.6",
67
66
  "@tanstack/react-virtual": "^3.14.2",
68
67
  "hls.js": "1.6.13",
69
68
  "jimp": "1.6.1",
70
- "opentui-spinner": "^0.0.6",
71
69
  "react": "19.2.7",
72
70
  "react-dom": "19.2.7",
73
71
  "rxjs": "^7.8.2",
package/src/cli/entry.ts CHANGED
@@ -2,7 +2,12 @@ import { dispatchCli } from "./index";
2
2
  import { fail, inferCliErrorOptions, printCliError } from "./errors";
3
3
  import { loadExternalPlugins } from "../plugins/loader";
4
4
  import type { CliLaunchRequest } from "../types/plugin";
5
- import { OPEN_TUI_NATIVE_SMOKE_COMMAND, smokeOpenTuiNative } from "./native-smoke";
5
+ import {
6
+ OPEN_TUI_NATIVE_SMOKE_COMMAND,
7
+ OPEN_TUI_RUNTIME_SMOKE_COMMAND,
8
+ smokeOpenTuiNative,
9
+ smokeOpenTuiRuntime,
10
+ } from "./native-smoke";
6
11
 
7
12
  async function launchOpenTuiApp(options: {
8
13
  externalPlugins: Awaited<ReturnType<typeof loadExternalPlugins>>;
@@ -26,6 +31,11 @@ export async function runCliEntrypoint(rawArgs = process.argv.slice(2)): Promise
26
31
  process.exit(0);
27
32
  }
28
33
 
34
+ if (command === OPEN_TUI_RUNTIME_SMOKE_COMMAND) {
35
+ await smokeOpenTuiRuntime();
36
+ process.exit(0);
37
+ }
38
+
29
39
  const externalPlugins = await loadExternalPlugins();
30
40
 
31
41
  if (!command) {
@@ -1,5 +1,10 @@
1
1
  export const OPEN_TUI_NATIVE_SMOKE_COMMAND = "__gloomberb-smoke-opentui-native";
2
+ export const OPEN_TUI_RUNTIME_SMOKE_COMMAND = "__gloomberb-smoke-opentui-runtime";
2
3
 
3
4
  export async function smokeOpenTuiNative(): Promise<void> {
4
5
  await import("../renderers/opentui/native-smoke");
5
6
  }
7
+
8
+ export async function smokeOpenTuiRuntime(): Promise<void> {
9
+ await import("../renderers/opentui/start");
10
+ }
@@ -98,7 +98,7 @@ export function useRootProviderSearch(options: {
98
98
  );
99
99
  const localItems = localTickerSearchResultItems(searchQuery, { limit: 6 });
100
100
  setRootProviderResults(cachedCandidates
101
- ? mergeTickerSearchResultItems(searchQuery, localItems, buildTickerSearchResultItems(cachedCandidates, searchQuery))
101
+ ? mergeTickerSearchResultItems(searchQuery, buildTickerSearchResultItems(cachedCandidates, searchQuery), localItems)
102
102
  : null);
103
103
  setRootProviderResultsQuery(cachedCandidates ? searchQuery : null);
104
104
 
@@ -125,8 +125,8 @@ export function useRootProviderSearch(options: {
125
125
  );
126
126
  setRootProviderResults(mergeTickerSearchResultItems(
127
127
  searchQuery,
128
- localTickerSearchResultItems(searchQuery, { limit: 6 }),
129
128
  buildTickerSearchResultItems(combined, searchQuery),
129
+ localTickerSearchResultItems(searchQuery, { limit: 6 }),
130
130
  ));
131
131
  setRootProviderResultsQuery(searchQuery);
132
132
  } catch {
@@ -178,7 +178,11 @@ export function useRootProviderSearch(options: {
178
178
  rootResultItems,
179
179
  rootTickerSearchArg,
180
180
  ]);
181
- const rootSectionOrder: CommandBarSectionOrder = rootPlainTickerSearchArg ? "app-first" : "default";
181
+ const rootSectionOrder: CommandBarSectionOrder = rootPlainTickerSearchArg
182
+ ? "app-first"
183
+ : rootTickerSearchArg
184
+ ? "ranked"
185
+ : "default";
182
186
  const orderedRootResults = useMemo(
183
187
  () => orderListResults(rootResults, { sectionOrder: rootSectionOrder }),
184
188
  [rootResults, rootSectionOrder],
@@ -0,0 +1,34 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { ResultItem } from "../../list/model";
3
+ import { mergeTickerSearchResultItems } from "./results";
4
+
5
+ function resultItem(id: string, label: string, right: string, kind: ResultItem["kind"] = "ticker"): ResultItem {
6
+ return {
7
+ id,
8
+ label,
9
+ detail: "Apple Inc.",
10
+ category: kind === "info" ? "Search" : "Saved",
11
+ kind,
12
+ right,
13
+ action: () => {},
14
+ };
15
+ }
16
+
17
+ test("keeps completed ticker-search results authoritative over provisional rows", () => {
18
+ const authoritative = [
19
+ resultItem("ranked:AAPL", "AAPL", "Equity NASDAQ"),
20
+ resultItem("ranked:APC", "APC", "Equity XETRA"),
21
+ ];
22
+ const provisional = [
23
+ resultItem("local:APC", "APC", "Equity XETRA"),
24
+ resultItem("local:AAPL", "AAPL", "Equity NASDAQ"),
25
+ ];
26
+
27
+ expect(mergeTickerSearchResultItems("Apple", authoritative, provisional).map((item) => item.id)).toEqual([
28
+ "ranked:AAPL",
29
+ "ranked:APC",
30
+ ]);
31
+
32
+ const noResults = resultItem("no-results", "No matches for Apple", "", "info");
33
+ expect(mergeTickerSearchResultItems("Apple", [noResults], [])).toEqual([noResults]);
34
+ });
@@ -1,7 +1,6 @@
1
1
  import type { TickerRecord } from "../../../../types/ticker";
2
2
  import {
3
3
  createLocalTickerSearchCandidates,
4
- rankTickerSearchItems,
5
4
  type TickerSearchCandidate,
6
5
  } from "../../../../tickers/search";
7
6
  import type { ResultItem } from "../../list/model";
@@ -33,7 +32,7 @@ function isExactTickerResultMatch(item: ResultItem, query: string): boolean {
33
32
 
34
33
  export function mergeTickerSearchResultItems(
35
34
  query: string,
36
- preferredItems: ResultItem[],
35
+ rankedItems: ResultItem[],
37
36
  fallbackItems: ResultItem[],
38
37
  ): ResultItem[] {
39
38
  const merged: ResultItem[] = [];
@@ -45,12 +44,11 @@ export function mergeTickerSearchResultItems(
45
44
  seen.add(key);
46
45
  merged.push(item);
47
46
  };
48
- preferredItems.forEach(addItem);
47
+ rankedItems.forEach(addItem);
49
48
  fallbackItems.forEach(addItem);
50
- if (merged.length === 0) return fallbackItems;
49
+ if (merged.length === 0) return rankedItems.length > 0 ? rankedItems : fallbackItems;
51
50
 
52
- return rankTickerSearchItems(merged, query)
53
- .map((item) => isExactTickerResultMatch(item, query) && item.category !== "Saved"
51
+ return merged.map((item) => isExactTickerResultMatch(item, query) && item.category !== "Saved"
54
52
  ? { ...item, category: "Exact Match" }
55
53
  : item);
56
54
  }
@@ -80,7 +80,7 @@ export function useTickerSearchRouteResults(options: {
80
80
  brokerInstanceId,
81
81
  );
82
82
  setTickerSearchResults(cachedCandidates
83
- ? mergeTickerSearchResultItems(searchQuery, localItems, buildTickerSearchResultItems(cachedCandidates, searchQuery))
83
+ ? mergeTickerSearchResultItems(searchQuery, buildTickerSearchResultItems(cachedCandidates, searchQuery), localItems)
84
84
  : localItems);
85
85
  const requestId = ++searchRequestIdRef.current;
86
86
  const searchDelay = skipTickerSearchDebounceRef.current ? 0 : 200;
@@ -109,8 +109,8 @@ export function useTickerSearchRouteResults(options: {
109
109
  );
110
110
  setTickerSearchResults(mergeTickerSearchResultItems(
111
111
  searchQuery,
112
- localTickerSearchResultItems(searchQuery, { limit: 6 }),
113
112
  buildTickerSearchResultItems(combined, searchQuery),
113
+ localTickerSearchResultItems(searchQuery, { limit: 6 }),
114
114
  ));
115
115
  } catch {
116
116
  if (requestId !== searchRequestIdRef.current) return;
@@ -135,12 +135,13 @@ function buildRouteListState(options: BuildRouteListStateOptions): ListScreenSta
135
135
  query: currentRoute.query,
136
136
  selectedIdx: currentRoute.selectedIdx,
137
137
  hoveredIdx: currentRoute.hoveredIdx,
138
- results: orderListResults(results),
138
+ results: orderListResults(results, { sectionOrder: "ranked" }),
139
139
  searching: tickerSearchPending,
140
140
  emptyLabel: emptyState.label,
141
141
  emptyDetail: emptyState.detail,
142
142
  footerLeft: getScreenFooterLeft(currentRoute),
143
143
  footerRight: getScreenFooterRight(currentRoute),
144
+ sectionOrder: "ranked",
144
145
  };
145
146
  }
146
147
  default:
@@ -926,6 +926,71 @@ describe("CommandBar", () => {
926
926
  expect(aaplRow).toBeLessThan(appRow);
927
927
  });
928
928
 
929
+ test("keeps the provider-ranked canonical listing ahead of provisional saved order", async () => {
930
+ testSetup = await testRender(
931
+ <CommandBarHarness
932
+ query=""
933
+ extraTickers={[makeTicker("APC", "Apple Inc.", {
934
+ exchange: "XETRA",
935
+ currency: "EUR",
936
+ assetCategory: "STK",
937
+ })]}
938
+ configureState={(state) => ({
939
+ ...state,
940
+ commandBarLaunchRequest: {
941
+ kind: "ticker-search",
942
+ query: "Apple",
943
+ sequence: 1,
944
+ },
945
+ tickers: new Map([
946
+ ["APC", state.tickers.get("APC")!],
947
+ ["AAPL", state.tickers.get("AAPL")!],
948
+ ["MSFT", state.tickers.get("MSFT")!],
949
+ ]),
950
+ })}
951
+ dataProvider={makeDataProvider(async () => [
952
+ {
953
+ providerId: "yahoo",
954
+ symbol: "AAPL",
955
+ name: "Apple Inc.",
956
+ exchange: "NASDAQ",
957
+ primaryExchange: "NASDAQ",
958
+ type: "EQUITY",
959
+ currency: "USD",
960
+ },
961
+ {
962
+ providerId: "broker",
963
+ symbol: "APC",
964
+ name: "Apple Inc.",
965
+ exchange: "XETRA",
966
+ primaryExchange: "XETRA",
967
+ type: "EQUITY",
968
+ currency: "EUR",
969
+ },
970
+ {
971
+ providerId: "yahoo",
972
+ symbol: "APLY",
973
+ name: "Apple Yield Shares ETF",
974
+ exchange: "NYSE Arca",
975
+ type: "ETF",
976
+ currency: "USD",
977
+ },
978
+ ])}
979
+ />,
980
+ { width: 100, height: 24 },
981
+ );
982
+
983
+ await testSetup.renderOnce();
984
+ const frame = await waitForFrameToContain("APLY");
985
+ const rows = frame.split("\n");
986
+ const aaplRow = rows.findIndex((line) => line.trimStart().startsWith("AAPL"));
987
+ const apcRow = rows.findIndex((line) => line.trimStart().startsWith("APC"));
988
+
989
+ expect(aaplRow).toBeGreaterThanOrEqual(0);
990
+ expect(apcRow).toBeGreaterThanOrEqual(0);
991
+ expect(aaplRow).toBeLessThan(apcRow);
992
+ });
993
+
929
994
  test("renders form-layout wizard fields together on one screen", async () => {
930
995
  testSetup = await testRender(
931
996
  <CommandBarHarness
@@ -67,6 +67,30 @@ describe("command bar view model helpers", () => {
67
67
  ]);
68
68
  });
69
69
 
70
+ test("orders ticker sections by their best-ranked candidate", () => {
71
+ const sections = buildSections([
72
+ { id: "primary", category: "Primary Listing" },
73
+ { id: "saved-alternate", category: "Saved" },
74
+ { id: "other", category: "Other Listings" },
75
+ { id: "saved-fallback", category: "Saved" },
76
+ { id: "fund", category: "Funds & Derivatives" },
77
+ ], { sectionOrder: "ranked" });
78
+
79
+ expect(sections.map((section) => section.category)).toEqual([
80
+ "Primary Listing",
81
+ "Saved",
82
+ "Other Listings",
83
+ "Funds & Derivatives",
84
+ ]);
85
+ expect(sections.flatMap((section) => section.items.map((item) => item.id))).toEqual([
86
+ "primary",
87
+ "saved-alternate",
88
+ "saved-fallback",
89
+ "other",
90
+ "fund",
91
+ ]);
92
+ });
93
+
70
94
  test("derives row presentation for toggles and current rows", () => {
71
95
  expect(getRowPresentation({
72
96
  id: "plugin:news",
@@ -17,7 +17,7 @@ export interface CommandBarSection<T> {
17
17
  items: T[];
18
18
  }
19
19
 
20
- export type CommandBarSectionOrder = "default" | "app-first";
20
+ export type CommandBarSectionOrder = "default" | "app-first" | "ranked";
21
21
 
22
22
  export interface CommandBarItemView {
23
23
  id: string;
@@ -138,6 +138,7 @@ export function truncateText(text: string, width: number): string {
138
138
 
139
139
  function getCategoryPriority(category: string, sectionOrder: CommandBarSectionOrder = "default"): number {
140
140
  const normalized = category.trim().toLowerCase();
141
+ if (sectionOrder === "ranked") return 0;
141
142
  // The AI answers the question the user actually typed, so it leads the list.
142
143
  if (normalized === "ask ai") return -100;
143
144
  if (normalized === "exact match") return -50;
@@ -1,4 +1,4 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { MemoryPluginPersistence } from "../../../test-support/plugin-persistence";
3
3
  import {
4
4
  attachFredSeriesPersistence,
@@ -32,6 +32,13 @@ function makeSeries(value: number): FredSeriesData {
32
32
  };
33
33
  }
34
34
 
35
+ // Plugin activation in other test files attaches a live persistence to this
36
+ // module and never detaches it, so the cache these tests exercise depends on
37
+ // which file ran first. Establish the state instead of inheriting it.
38
+ beforeEach(() => {
39
+ resetFredSeriesPersistence();
40
+ });
41
+
35
42
  afterEach(() => {
36
43
  resetFredSeriesPersistence();
37
44
  });
@@ -31,7 +31,7 @@ const HELP_TABS = [
31
31
  ] as const;
32
32
 
33
33
  type HelpTabId = typeof HELP_TABS[number]["value"];
34
- const GLOOMBERB_ISSUES_URL = "https://github.com/vincelwt/gloomberb/issues";
34
+ const GLOOMBERB_ISSUES_URL = "https://github.com/gloom-sh/gloomberb/issues";
35
35
 
36
36
  function HelpPane({ focused, width, height }: PaneProps) {
37
37
  const registry = getSharedRegistry();
@@ -135,7 +135,7 @@ export async function loadIbkrPositions({
135
135
  ticker: position.contract.localSymbol || position.contract.symbol,
136
136
  exchange: position.contract.primaryExch || position.contract.exchange || "",
137
137
  shares: Math.abs(position.pos),
138
- avgCost: normalizeIbkrPriceValue(portfolioSnapshot?.avgCost ?? position.avgCost, priceDivisor),
138
+ avgCost: portfolioSnapshot?.avgCost ?? position.avgCost,
139
139
  currency: position.contract.currency || "USD",
140
140
  accountId,
141
141
  name: position.contract.description || position.contract.localSymbol || position.contract.symbol,
@@ -2,7 +2,7 @@ import type { ApplicationMenuItemConfig } from "electrobun/bun";
2
2
  import type { DesktopApplicationMenuCommand } from "../../../../types/desktop-menu";
3
3
 
4
4
  export const ELECTROBUN_APPLICATION_MENU_ACTION = "gloom.application-menu.select";
5
- const GITHUB_ISSUE_URL = "https://github.com/vincelwt/gloomberb/issues/new/choose";
5
+ const GITHUB_ISSUE_URL = "https://github.com/gloom-sh/gloomberb/issues/new/choose";
6
6
 
7
7
  export type ElectrobunApplicationMenuCommand =
8
8
  | DesktopApplicationMenuCommand