gloomberb 0.7.3 → 0.7.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.
Files changed (80) hide show
  1. package/package.json +1 -1
  2. package/src/app.tsx +59 -30
  3. package/src/components/chart/chart-renderer.ts +109 -32
  4. package/src/components/chart/chart-types.ts +18 -0
  5. package/src/components/chart/chart.test.ts +152 -5
  6. package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
  7. package/src/components/chart/comparison-chart-renderer.ts +50 -11
  8. package/src/components/chart/comparison-stock-chart.tsx +45 -8
  9. package/src/components/chart/market-session.ts +187 -0
  10. package/src/components/chart/native/chart-rasterizer.ts +65 -2
  11. package/src/components/chart/stock-chart-renderer-switch.test.tsx +96 -0
  12. package/src/components/chart/stock-chart.tsx +36 -8
  13. package/src/components/chart/time-axis-label.tsx +56 -2
  14. package/src/components/command-bar/command-bar.test.tsx +1 -1
  15. package/src/components/command-bar/command-bar.tsx +26 -2
  16. package/src/components/command-bar/list-model.ts +8 -4
  17. package/src/components/command-bar/view-model.test.ts +20 -0
  18. package/src/components/command-bar/view-model.ts +15 -3
  19. package/src/components/layout/shell.test.tsx +26 -0
  20. package/src/components/layout/shell.tsx +69 -21
  21. package/src/components/ticker-list-table.tsx +4 -15
  22. package/src/core/state/session-persistence.ts +25 -0
  23. package/src/market-data/coordinator-subscriptions.test.ts +102 -18
  24. package/src/market-data/coordinator.test.ts +122 -2
  25. package/src/market-data/coordinator.ts +192 -55
  26. package/src/market-data/hooks.test.tsx +31 -0
  27. package/src/market-data/hooks.tsx +1 -0
  28. package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
  29. package/src/plugins/builtin/chat-controller.ts +8 -7
  30. package/src/plugins/builtin/chat.test.tsx +55 -0
  31. package/src/plugins/builtin/chat.tsx +20 -6
  32. package/src/plugins/builtin/holders/index.test.ts +30 -1
  33. package/src/plugins/builtin/holders/index.tsx +7 -3
  34. package/src/plugins/builtin/market-movers/index.tsx +72 -40
  35. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
  36. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
  37. package/src/plugins/builtin/news-wire/index.tsx +0 -2
  38. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  39. package/src/plugins/builtin/portfolio-list/pane.tsx +26 -9
  40. package/src/plugins/builtin/portfolio-list.test.tsx +29 -7
  41. package/src/plugins/builtin/sectors/index.tsx +30 -9
  42. package/src/plugins/builtin/ticker-detail/pane.tsx +18 -1
  43. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -1
  44. package/src/plugins/builtin/world-indices/index.tsx +58 -19
  45. package/src/plugins/pane-manager.test.ts +31 -0
  46. package/src/plugins/pane-manager.ts +32 -6
  47. package/src/renderers/electrobun/bun/index.ts +83 -3
  48. package/src/renderers/electrobun/shared/protocol.ts +8 -1
  49. package/src/renderers/electrobun/view/backend-rpc.ts +5 -0
  50. package/src/renderers/electrobun/view/fatal-screen.tsx +118 -0
  51. package/src/renderers/electrobun/view/main.tsx +38 -24
  52. package/src/renderers/electrobun/view/styles.css +48 -4
  53. package/src/sources/gloomberb-cloud.test.ts +55 -2
  54. package/src/sources/gloomberb-cloud.ts +180 -29
  55. package/src/sources/provider-router.test.ts +567 -10
  56. package/src/sources/provider-router.ts +323 -93
  57. package/src/sources/yahoo-finance.test.ts +37 -0
  58. package/src/sources/yahoo-finance.ts +48 -2
  59. package/src/state/app-bootstrap.test.ts +50 -23
  60. package/src/state/app-bootstrap.ts +26 -10
  61. package/src/state/session-persistence.test.ts +5 -3
  62. package/src/state/use-inline-tickers.ts +3 -0
  63. package/src/state/use-quote-streaming.ts +11 -0
  64. package/src/types/data-provider.ts +5 -1
  65. package/src/types/financials.ts +1 -0
  66. package/src/utils/api-client.test.ts +120 -0
  67. package/src/utils/api-client.ts +66 -19
  68. package/src/utils/exchanges.test.ts +16 -0
  69. package/src/utils/exchanges.ts +33 -0
  70. package/src/utils/options.ts +1 -1
  71. package/src/utils/price-history.test.ts +28 -1
  72. package/src/utils/price-history.ts +16 -0
  73. package/src/utils/quote-freshness.test.ts +150 -0
  74. package/src/utils/quote-freshness.ts +42 -1
  75. package/src/utils/quote-resolution.ts +2 -0
  76. package/src/utils/quote-time.test.ts +34 -0
  77. package/src/utils/quote-time.ts +18 -5
  78. package/src/utils/ticker-search.ts +72 -1
  79. package/src/version.ts +1 -1
  80. package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
@@ -11,20 +11,33 @@ export function formatQuoteAge(lastUpdated: number | undefined, now = Date.now()
11
11
  const clamped = clampQuoteTimestamp(lastUpdated, now);
12
12
  if (clamped == null) return "—";
13
13
 
14
- const ageSeconds = Math.max(0, (now - clamped) / 1000);
14
+ const ageMs = Math.max(0, now - clamped);
15
+ if (ageMs < 1000) return `${Math.floor(ageMs)}ms`;
16
+
17
+ const ageSeconds = ageMs / 1000;
15
18
  if (ageSeconds < 60) return `${Math.floor(ageSeconds)}s`;
16
19
  if (ageSeconds < 3600) return `${Math.floor(ageSeconds / 60)}m`;
17
20
  if (ageSeconds < 86400) return `${Math.floor(ageSeconds / 3600)}h`;
18
21
  return `${Math.floor(ageSeconds / 86400)}d`;
19
22
  }
20
23
 
24
+ export function resolveQuoteAgeTimestamp(
25
+ quote: Pick<Quote, "lastUpdated" | "receivedAt"> | null | undefined,
26
+ now = Date.now(),
27
+ ): number | null {
28
+ if (!quote) return null;
29
+ const receivedAt = clampQuoteTimestamp(quote.receivedAt, now);
30
+ return receivedAt ?? clampQuoteTimestamp(quote.lastUpdated, now);
31
+ }
32
+
21
33
  export function formatQuoteAgeWithSource(
22
- quote: Pick<Quote, "lastUpdated" | "dataSource"> | null | undefined,
34
+ quote: Pick<Quote, "lastUpdated" | "receivedAt" | "dataSource"> | null | undefined,
23
35
  now = Date.now(),
24
36
  ): string {
25
37
  if (!quote) return "—";
26
38
 
27
- const age = formatQuoteAge(quote.lastUpdated, now);
39
+ const timestamp = resolveQuoteAgeTimestamp(quote, now);
40
+ const age = timestamp == null ? "—" : formatQuoteAge(timestamp, now);
28
41
  if (age === "—") return age;
29
42
 
30
43
  const prefix = quote.dataSource === "delayed" ? "◷" : "";
@@ -32,12 +45,12 @@ export function formatQuoteAgeWithSource(
32
45
  }
33
46
 
34
47
  export function getMostRecentQuoteUpdate(
35
- quotes: Iterable<Pick<Quote, "lastUpdated"> | null | undefined>,
48
+ quotes: Iterable<Pick<Quote, "lastUpdated" | "receivedAt"> | null | undefined>,
36
49
  now = Date.now(),
37
50
  ): number | null {
38
51
  let latest: number | null = null;
39
52
  for (const quote of quotes) {
40
- const timestamp = clampQuoteTimestamp(quote?.lastUpdated, now);
53
+ const timestamp = resolveQuoteAgeTimestamp(quote, now);
41
54
  if (timestamp != null && (latest == null || timestamp > latest)) {
42
55
  latest = timestamp;
43
56
  }
@@ -84,6 +84,12 @@ export type ResolvedTickerSearch =
84
84
  | { kind: "local"; symbol: string; ticker: TickerRecord }
85
85
  | { kind: "provider"; symbol: string; result: InstrumentSearchResult };
86
86
 
87
+ export interface TickerOpenTarget {
88
+ symbol: string;
89
+ ticker: TickerRecord;
90
+ created: boolean;
91
+ }
92
+
87
93
  interface SearchQueryIntent {
88
94
  normalizedQuery: string;
89
95
  compactQuery: string;
@@ -297,6 +303,71 @@ export async function upsertTickerFromSearchResult(
297
303
  return { ticker, created };
298
304
  }
299
305
 
306
+ export async function resolveTickerOpenTarget({
307
+ query,
308
+ tickers,
309
+ dataProvider,
310
+ tickerRepository,
311
+ searchContext,
312
+ }: {
313
+ query: string;
314
+ tickers: ReadonlyMap<string, TickerRecord>;
315
+ dataProvider: DataProvider;
316
+ tickerRepository: TickerRepository;
317
+ searchContext?: SearchRequestContext;
318
+ }): Promise<TickerOpenTarget | null> {
319
+ const symbol = normalizeTickerInput(null, query);
320
+ if (!symbol) return null;
321
+
322
+ let resolved: ResolvedTickerSearch | null = null;
323
+ try {
324
+ resolved = await resolveTickerSearch({
325
+ query: symbol,
326
+ activeTicker: null,
327
+ tickers,
328
+ dataProvider,
329
+ searchContext,
330
+ });
331
+ } catch {
332
+ resolved = null;
333
+ }
334
+
335
+ if (resolved?.kind === "local") {
336
+ return { symbol: resolved.symbol, ticker: resolved.ticker, created: false };
337
+ }
338
+
339
+ if (resolved?.kind === "provider") {
340
+ const { ticker, created } = await upsertTickerFromSearchResult(tickerRepository, resolved.result);
341
+ return { symbol: ticker.metadata.ticker, ticker, created };
342
+ }
343
+
344
+ try {
345
+ const quote = await dataProvider.getQuote(symbol, "");
346
+ const quoteSymbol = normalizeTickerSymbol(quote.symbol || symbol);
347
+ const existing = await tickerRepository.loadTicker(quoteSymbol);
348
+ if (existing) {
349
+ return { symbol: existing.metadata.ticker, ticker: existing, created: false };
350
+ }
351
+
352
+ const ticker = await tickerRepository.createTicker({
353
+ ticker: quoteSymbol,
354
+ exchange: quote.listingExchangeName ?? quote.exchangeName ?? "",
355
+ currency: quote.currency || "USD",
356
+ name: quote.name || quoteSymbol,
357
+ assetCategory: undefined,
358
+ portfolios: [],
359
+ watchlists: [],
360
+ positions: [],
361
+ custom: {},
362
+ tags: [],
363
+ });
364
+
365
+ return { symbol: ticker.metadata.ticker, ticker, created: true };
366
+ } catch {
367
+ return null;
368
+ }
369
+ }
370
+
300
371
  export function findExactTickerSearchMatch<T extends Pick<TickerSearchRankableItem, "label"> & Partial<TickerSearchRankableItem>>(
301
372
  items: T[],
302
373
  query: string,
@@ -618,7 +689,7 @@ function analyzeSearchQuery(query: string): SearchQueryIntent {
618
689
  .map((token) => ASSET_HINT_MAP[token])
619
690
  .filter((token): token is TickerSearchInstrumentClass => token != null),
620
691
  ));
621
- const assetPreference = assetHints.length === 1 ? assetHints[0] : null;
692
+ const assetPreference = assetHints.length === 1 ? assetHints[0]! : null;
622
693
 
623
694
  const companyTokens = tokens.filter((token) => !(token in EXCHANGE_HINT_ALIASES) && !(token in ASSET_HINT_MAP));
624
695
  const companyQuery = companyTokens.join(" ");
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.7.3";
1
+ export const VERSION = "0.7.5";
@@ -1,44 +0,0 @@
1
- import type { PluginPersistence } from "../../../types/plugin";
2
-
3
- const CACHE_POLICY = {
4
- staleMs: 7 * 86_400_000, // 7 days
5
- expireMs: 30 * 86_400_000, // 30 days
6
- };
7
-
8
- const cache = new Map<string, string>();
9
- const inFlight = new Set<string>();
10
- let persist: PluginPersistence | null = null;
11
-
12
- export function setDigestPersistence(p: PluginPersistence): void {
13
- persist = p;
14
- }
15
-
16
- export function getDigest(articleId: string): string | null {
17
- const cached = cache.get(articleId);
18
- if (cached !== undefined) return cached;
19
-
20
- if (!persist) return null;
21
- const res = persist.getResource<string>("ai-digest", articleId, { allowExpired: false });
22
- if (res) {
23
- cache.set(articleId, res.value);
24
- return res.value;
25
- }
26
- return null;
27
- }
28
-
29
- export function setDigest(articleId: string, digest: string): void {
30
- cache.set(articleId, digest);
31
- persist?.setResource("ai-digest", articleId, digest, { cachePolicy: CACHE_POLICY });
32
- }
33
-
34
- export function isDigestInFlight(articleId: string): boolean {
35
- return inFlight.has(articleId);
36
- }
37
-
38
- export function markDigestInFlight(articleId: string): void {
39
- inFlight.add(articleId);
40
- }
41
-
42
- export function clearDigestInFlight(articleId: string): void {
43
- inFlight.delete(articleId);
44
- }