gloomberb 0.7.2 → 0.7.4

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 (167) hide show
  1. package/README.md +79 -132
  2. package/package.json +1 -1
  3. package/src/app.tsx +73 -67
  4. package/src/components/chart/chart-data.ts +7 -2
  5. package/src/components/chart/chart-pane-settings.ts +7 -4
  6. package/src/components/chart/chart-renderer.ts +158 -47
  7. package/src/components/chart/chart-types.ts +20 -2
  8. package/src/components/chart/chart.test.ts +179 -13
  9. package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
  10. package/src/components/chart/comparison-chart-renderer.ts +50 -11
  11. package/src/components/chart/comparison-stock-chart.tsx +129 -129
  12. package/src/components/chart/market-session.ts +187 -0
  13. package/src/components/chart/native/chart-rasterizer.ts +108 -19
  14. package/src/components/chart/price-axis-labels.tsx +25 -9
  15. package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
  16. package/src/components/chart/stock-chart.tsx +95 -49
  17. package/src/components/chart/time-axis-label.tsx +56 -2
  18. package/src/components/command-bar/command-bar.test.tsx +446 -120
  19. package/src/components/command-bar/command-bar.tsx +535 -384
  20. package/src/components/command-bar/command-registry.ts +16 -6
  21. package/src/components/command-bar/list-model.ts +8 -4
  22. package/src/components/command-bar/root-shortcuts.ts +4 -5
  23. package/src/components/command-bar/view-model.test.ts +21 -0
  24. package/src/components/command-bar/view-model.ts +17 -4
  25. package/src/components/command-bar/workflow-ops.ts +11 -32
  26. package/src/components/data-table-view.test.tsx +24 -1
  27. package/src/components/data-table-view.tsx +3 -2
  28. package/src/components/feed-data-table-stack-view.tsx +3 -2
  29. package/src/components/layout/detached-pane-shell.tsx +2 -2
  30. package/src/components/layout/floating-pane.tsx +9 -2
  31. package/src/components/layout/pane-footer.tsx +1 -1
  32. package/src/components/layout/pane-header.tsx +4 -2
  33. package/src/components/layout/pane-sizing.ts +12 -0
  34. package/src/components/layout/pane.tsx +24 -2
  35. package/src/components/layout/shell.tsx +24 -10
  36. package/src/components/layout/status-bar.test.tsx +4 -1
  37. package/src/components/layout/status-bar.tsx +4 -24
  38. package/src/components/markdown-text.tsx +53 -8
  39. package/src/components/onboarding/onboarding-wizard.tsx +7 -6
  40. package/src/components/pane-settings-dialog.tsx +9 -6
  41. package/src/components/pane-template-wizard.tsx +3 -2
  42. package/src/components/price-selector-dialog.tsx +3 -2
  43. package/src/components/price-sparkline-view.tsx +463 -0
  44. package/src/components/price-sparkline.ts +46 -0
  45. package/src/components/table-view-shared.tsx +21 -3
  46. package/src/components/ticker-list-table-view.tsx +3 -2
  47. package/src/components/ticker-list-table.tsx +15 -66
  48. package/src/components/toggle-list.tsx +3 -3
  49. package/src/components/ui/choice-dialog.tsx +3 -2
  50. package/src/components/ui/multi-select-dialog.tsx +3 -2
  51. package/src/components/ui/native-select.tsx +3 -3
  52. package/src/core/state/app-state.ts +210 -80
  53. package/src/core/state/session-persistence.ts +25 -0
  54. package/src/data/config-store-node.ts +113 -25
  55. package/src/data/config-store.test.ts +65 -3
  56. package/src/data/plugin-state-store.ts +39 -13
  57. package/src/data/sqlite-cache.test.ts +13 -0
  58. package/src/market-data/coordinator-subscriptions.test.ts +102 -18
  59. package/src/market-data/coordinator.test.ts +122 -2
  60. package/src/market-data/coordinator.ts +200 -56
  61. package/src/market-data/hooks.test.tsx +31 -0
  62. package/src/market-data/hooks.tsx +1 -0
  63. package/src/news/aggregator.test.ts +41 -0
  64. package/src/news/aggregator.ts +61 -9
  65. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
  66. package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
  67. package/src/plugins/builtin/ai/settings.ts +3 -0
  68. package/src/plugins/builtin/broker-manager/index.tsx +4 -3
  69. package/src/plugins/builtin/chat-controller.test.ts +398 -17
  70. package/src/plugins/builtin/chat-controller.ts +263 -48
  71. package/src/plugins/builtin/chat.test.tsx +312 -6
  72. package/src/plugins/builtin/chat.tsx +245 -66
  73. package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
  74. package/src/plugins/builtin/correlation/colors.test.ts +39 -0
  75. package/src/plugins/builtin/correlation/colors.ts +87 -0
  76. package/src/plugins/builtin/correlation/index.test.tsx +151 -0
  77. package/src/plugins/builtin/correlation/index.tsx +92 -21
  78. package/src/plugins/builtin/debug.tsx +4 -3
  79. package/src/plugins/builtin/econ/index.tsx +3 -2
  80. package/src/plugins/builtin/help.test.tsx +2 -2
  81. package/src/plugins/builtin/holders/index.tsx +25 -13
  82. package/src/plugins/builtin/layout-manager.tsx +1 -7
  83. package/src/plugins/builtin/market-movers/index.tsx +72 -40
  84. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
  85. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
  86. package/src/plugins/builtin/news-wire/index.tsx +1 -3
  87. package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
  88. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
  89. package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
  90. package/src/plugins/builtin/options.tsx +9 -17
  91. package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
  92. package/src/plugins/builtin/portfolio-list/metrics.ts +13 -2
  93. package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
  94. package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
  95. package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
  96. package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
  97. package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
  98. package/src/plugins/builtin/portfolio-list.test.tsx +311 -9
  99. package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
  100. package/src/plugins/builtin/sectors/index.tsx +275 -43
  101. package/src/plugins/builtin/sectors/sector-data.ts +41 -1
  102. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
  103. package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
  104. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
  105. package/src/plugins/builtin/ticker-detail/pane.tsx +18 -7
  106. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
  107. package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
  108. package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
  109. package/src/plugins/builtin/world-indices/index.tsx +58 -19
  110. package/src/plugins/ibkr/trading-pane.tsx +3 -2
  111. package/src/plugins/plugin-runtime.tsx +2 -2
  112. package/src/plugins/prediction-markets/settings.ts +4 -0
  113. package/src/plugins/registry.test.ts +55 -0
  114. package/src/plugins/registry.ts +11 -4
  115. package/src/plugins/ticker-navigation.test.ts +35 -5
  116. package/src/plugins/ticker-navigation.ts +16 -22
  117. package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
  118. package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
  119. package/src/renderers/electrobun/bun/application-menu.ts +6 -1
  120. package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
  121. package/src/renderers/electrobun/bun/index.ts +52 -13
  122. package/src/renderers/electrobun/view/app-services.ts +51 -9
  123. package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
  124. package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
  125. package/src/renderers/electrobun/view/main.tsx +0 -16
  126. package/src/renderers/electrobun/view/styles.css +23 -21
  127. package/src/renderers/electrobun/view/ui-host.tsx +6 -4
  128. package/src/renderers/opentui/host.tsx +2 -1
  129. package/src/renderers/opentui/start.tsx +1 -1
  130. package/src/renderers/opentui/test-utils.tsx +6 -5
  131. package/src/sources/gloomberb-cloud.test.ts +55 -2
  132. package/src/sources/gloomberb-cloud.ts +180 -29
  133. package/src/sources/provider-router.test.ts +567 -10
  134. package/src/sources/provider-router.ts +323 -93
  135. package/src/sources/yahoo-finance.test.ts +37 -0
  136. package/src/sources/yahoo-finance.ts +48 -2
  137. package/src/state/app-bootstrap.test.ts +50 -23
  138. package/src/state/app-bootstrap.ts +29 -13
  139. package/src/state/app-context.test.ts +113 -0
  140. package/src/state/app-context.tsx +8 -4
  141. package/src/state/session-persistence.test.ts +5 -3
  142. package/src/state/use-inline-tickers.ts +3 -0
  143. package/src/state/use-quote-streaming.ts +11 -0
  144. package/src/theme/colors.ts +13 -14
  145. package/src/types/config.ts +9 -4
  146. package/src/types/data-provider.ts +5 -1
  147. package/src/types/financials.ts +1 -0
  148. package/src/types/plugin.ts +8 -1
  149. package/src/ui/context-menu.tsx +4 -4
  150. package/src/utils/api-client.test.ts +248 -0
  151. package/src/utils/api-client.ts +178 -26
  152. package/src/utils/exchanges.test.ts +16 -0
  153. package/src/utils/exchanges.ts +33 -0
  154. package/src/utils/fuzzy-search.ts +12 -1
  155. package/src/utils/keyboard.ts +31 -0
  156. package/src/utils/options.ts +1 -1
  157. package/src/utils/price-history.test.ts +28 -1
  158. package/src/utils/price-history.ts +16 -0
  159. package/src/utils/quote-freshness.test.ts +150 -0
  160. package/src/utils/quote-freshness.ts +42 -1
  161. package/src/utils/quote-resolution.ts +2 -0
  162. package/src/utils/quote-time.test.ts +34 -0
  163. package/src/utils/quote-time.ts +18 -5
  164. package/src/utils/ticker-search.test.ts +2 -2
  165. package/src/utils/ticker-search.ts +48 -1
  166. package/src/version.ts +1 -1
  167. package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
@@ -12,6 +12,9 @@ const MAX_ARTICLES = 500;
12
12
  const DEFAULT_POLL_INTERVAL_MS = 2 * 60 * 1000;
13
13
  const DEFAULT_GLOBAL_QUERY: NewsQuery = { feed: "latest", limit: MAX_ARTICLES };
14
14
  const FEEDS = new Set<NewsFeed>(["latest", "top", "breaking", "ticker", "sector", "topic"]);
15
+ const DETAIL_CAPABLE_ARTICLE = Symbol("detail-capable-news-article");
16
+
17
+ type DetailCapableArticle = NewsArticle & { [DETAIL_CAPABLE_ARTICLE]?: true };
15
18
 
16
19
  function normalizeTicker(ticker: string | undefined): string {
17
20
  return ticker ? normalizeSymbol(ticker) : "";
@@ -101,6 +104,57 @@ function sortByPublishedAt(items: NewsArticle[]): NewsArticle[] {
101
104
  return [...items].sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
102
105
  }
103
106
 
107
+ function hasStoryItems(item: NewsArticle | null | undefined): boolean {
108
+ return (item?.items?.length ?? 0) > 0;
109
+ }
110
+
111
+ function markDetailCapableArticle(source: NewsCapability, item: NewsArticle): NewsArticle {
112
+ if (!source.provider.fetchNewsStory) return item;
113
+ Object.defineProperty(item, DETAIL_CAPABLE_ARTICLE, {
114
+ value: true,
115
+ configurable: true,
116
+ });
117
+ return item;
118
+ }
119
+
120
+ function isDetailCapableArticle(item: NewsArticle): boolean {
121
+ return (item as DetailCapableArticle)[DETAIL_CAPABLE_ARTICLE] === true;
122
+ }
123
+
124
+ function shouldReplaceDuplicate(existing: NewsArticle, item: NewsArticle): boolean {
125
+ return (
126
+ item.importance > existing.importance ||
127
+ (item.importance === existing.importance && item.publishedAt > existing.publishedAt)
128
+ );
129
+ }
130
+
131
+ function selectDetailArticle(
132
+ existing: NewsArticle,
133
+ item: NewsArticle,
134
+ winner: NewsArticle,
135
+ ): NewsArticle | null {
136
+ if (isDetailCapableArticle(winner)) return winner;
137
+ if (isDetailCapableArticle(item)) return item;
138
+ if (isDetailCapableArticle(existing)) return existing;
139
+ if (hasStoryItems(winner)) return winner;
140
+ if (hasStoryItems(item)) return item;
141
+ if (hasStoryItems(existing)) return existing;
142
+ return null;
143
+ }
144
+
145
+ function mergeDuplicateArticle(existing: NewsArticle, item: NewsArticle): NewsArticle {
146
+ const winner = shouldReplaceDuplicate(existing, item)
147
+ ? { ...existing, ...item }
148
+ : existing;
149
+ const detailArticle = selectDetailArticle(existing, item, winner);
150
+ if (!detailArticle || detailArticle.id === winner.id) return winner;
151
+ return {
152
+ ...winner,
153
+ id: detailArticle.id,
154
+ items: hasStoryItems(detailArticle) ? detailArticle.items : winner.items,
155
+ };
156
+ }
157
+
104
158
  function dedupeArticles(items: NewsArticle[]): NewsArticle[] {
105
159
  const byKey = new Map<string, NewsArticle>();
106
160
  for (const item of items) {
@@ -110,12 +164,7 @@ function dedupeArticles(items: NewsArticle[]): NewsArticle[] {
110
164
  byKey.set(key, item);
111
165
  continue;
112
166
  }
113
- if (
114
- item.importance > existing.importance ||
115
- (item.importance === existing.importance && item.publishedAt > existing.publishedAt)
116
- ) {
117
- byKey.set(key, { ...existing, ...item });
118
- }
167
+ byKey.set(key, mergeDuplicateArticle(existing, item));
119
168
  }
120
169
  return sortByPublishedAt([...byKey.values()]).slice(0, MAX_ARTICLES);
121
170
  }
@@ -382,7 +431,8 @@ export class NewsService {
382
431
  let firstEmpty: SourceFetchResult | null = null;
383
432
  for (const source of sources) {
384
433
  try {
385
- const articles = await source.provider.fetchNews(query);
434
+ const articles = (await source.provider.fetchNews(query))
435
+ .map((article) => markDetailCapableArticle(source, article));
386
436
  const result = { articles, sourceIds: [newsCapabilitySourceId(source)] };
387
437
  if (articles.length > 0) return result;
388
438
  firstEmpty ??= result;
@@ -397,7 +447,8 @@ export class NewsService {
397
447
  const settled = await Promise.allSettled(
398
448
  sources.map(async (source) => ({
399
449
  source,
400
- articles: await source.provider.fetchNews(query),
450
+ articles: (await source.provider.fetchNews(query))
451
+ .map((article) => markDetailCapableArticle(source, article)),
401
452
  })),
402
453
  );
403
454
  const articles: NewsArticle[] = [];
@@ -418,7 +469,8 @@ export class NewsService {
418
469
  let changed = false;
419
470
  for (const query of queries) {
420
471
  if (source.isEnabled?.() === false || news.supports?.(query) === false) continue;
421
- const cached = news.getCachedNews?.(query) ?? [];
472
+ const cached = (news.getCachedNews?.(query) ?? [])
473
+ .map((article) => markDetailCapableArticle(source, article));
422
474
  if (cached.length === 0) continue;
423
475
  const key = buildNewsQueryKey(query);
424
476
  const current = this.queryStates.get(key) ?? createIdleState();
@@ -300,13 +300,32 @@ export function AskAiDetailTab({ width, height, focused, onCapture }: DetailTabP
300
300
  nativePaneChrome,
301
301
  terminalBottomInset: terminalFooterClearance,
302
302
  });
303
- const chatHeight = Math.max(height - composerBlockHeight, 0);
303
+ const chatHeight = nativePaneChrome ? undefined : Math.max(height - composerBlockHeight, 0);
304
+ const layoutHeight = nativePaneChrome ? "100%" : height;
305
+ const nativeFillStyle = nativePaneChrome ? { minHeight: 0 } : undefined;
304
306
 
305
307
  return (
306
- <Box flexDirection="column" paddingX={nativePaneChrome ? 0 : 1} height={height} overflow="hidden">
307
- {chatHeight > 0 && (
308
- <ScrollBox ref={scrollRef} height={chatHeight} scrollY>
309
- <Box flexDirection="column" paddingX={nativePaneChrome ? 1 : 0}>
308
+ <Box
309
+ flexDirection="column"
310
+ paddingX={nativePaneChrome ? 0 : 1}
311
+ height={layoutHeight}
312
+ flexGrow={nativePaneChrome ? 1 : undefined}
313
+ overflow="hidden"
314
+ style={nativeFillStyle}
315
+ >
316
+ {(nativePaneChrome || (chatHeight ?? 0) > 0) && (
317
+ <ScrollBox
318
+ ref={scrollRef}
319
+ height={nativePaneChrome ? undefined : chatHeight}
320
+ flexGrow={nativePaneChrome ? 1 : undefined}
321
+ scrollY
322
+ style={nativeFillStyle}
323
+ >
324
+ <Box
325
+ flexDirection="column"
326
+ paddingX={nativePaneChrome ? 1 : 0}
327
+ style={nativeFillStyle}
328
+ >
310
329
  {messages.length === 0 ? (
311
330
  <Box paddingTop={1}>
312
331
  <Text fg={colors.textDim}>
@@ -354,7 +373,7 @@ export function AskAiDetailTab({ width, height, focused, onCapture }: DetailTabP
354
373
  placeholder="Ask a question..."
355
374
  terminalPrefix=" > "
356
375
  terminalBottomInset={terminalFooterClearance}
357
- width={nativePaneChrome ? width : contentWidth}
376
+ width={nativePaneChrome ? "100%" : contentWidth}
358
377
  height={composerHeight}
359
378
  onFocusRequest={focusInput}
360
379
  onInput={(value) => setInputValue(value)}
@@ -432,6 +432,9 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
432
432
  const quoteTargets = useMemo(() => (
433
433
  sortedTickers
434
434
  .map((ticker) => quoteSubscriptionTargetFromTicker(ticker, ticker.metadata.ticker))
435
+ .map((target) => target
436
+ ? { ...target, surface: "screener" as const, visible: true, weight: 70 }
437
+ : null)
435
438
  .filter((target): target is NonNullable<ReturnType<typeof quoteSubscriptionTargetFromTicker>> => target != null)
436
439
  ), [sortedTickers]);
437
440
 
@@ -60,6 +60,9 @@ export function resolveVisibleAiScreenerColumns(columnIds: string[]): ColumnConf
60
60
  export function buildAiScreenerPaneSettingsDef(settings: AiScreenerPaneSettings): PaneSettingsDef {
61
61
  return {
62
62
  title: "AI Screener Settings",
63
+ values: {
64
+ columnIds: [...settings.columnIds],
65
+ },
63
66
  fields: [
64
67
  {
65
68
  key: "columnIds",
@@ -32,6 +32,7 @@ import type { GloomPlugin, PaneProps } from "../../../types/plugin";
32
32
  import type { BrokerAccount } from "../../../types/trading";
33
33
  import { useDialog, type PromptContext } from "../../../ui/dialog";
34
34
  import { formatCurrency } from "../../../utils/format";
35
+ import { isPlainKey } from "../../../utils/keyboard";
35
36
  import { usePluginAppActions, usePluginBrokerActions } from "../../plugin-runtime";
36
37
  import {
37
38
  buildBrokerProfileRows,
@@ -47,7 +48,7 @@ type BrokerColumn = DataTableColumn & { id: BrokerColumnId };
47
48
  function stateColor(state: BrokerDisplayState): string {
48
49
  switch (state) {
49
50
  case "connected": return colors.positive;
50
- case "connecting": return "#e5c07b";
51
+ case "connecting": return colors.warning;
51
52
  case "error": return colors.negative;
52
53
  case "disabled": return colors.textMuted;
53
54
  case "unavailable": return colors.negative;
@@ -442,13 +443,13 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
442
443
  saveEdit().catch(() => {});
443
444
  return;
444
445
  }
445
- if (event.name === "up" || event.name === "k") {
446
+ if (isPlainKey(event, "up", "k")) {
446
447
  event.stopPropagation();
447
448
  const index = editKeys.indexOf(activeEditKey);
448
449
  setActiveEditKey(editKeys[Math.max(0, index - 1)] ?? "label");
449
450
  return;
450
451
  }
451
- if (event.name === "down" || event.name === "j" || event.name === "tab") {
452
+ if (isPlainKey(event, "down", "j", "tab")) {
452
453
  event.stopPropagation();
453
454
  const index = editKeys.indexOf(activeEditKey);
454
455
  setActiveEditKey(editKeys[Math.min(editKeys.length - 1, index + 1)] ?? "label");