gloomberb 0.12.0 → 0.13.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 (219) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -2
  3. package/src/api-client/auth.ts +2 -1
  4. package/src/api-client/index.test.ts +34 -0
  5. package/src/api-client/index.ts +10 -2
  6. package/src/api-client/request.ts +4 -1
  7. package/src/api-client/research-activity.ts +61 -0
  8. package/src/api-client/types.ts +2 -1
  9. package/src/cli/commands/plugins.ts +4 -3
  10. package/src/cli/desktop-pane-shot.ts +79 -10
  11. package/src/cli/pane-functions/capabilities.ts +76 -23
  12. package/src/cli/pane-functions/cloud-session.test.ts +54 -0
  13. package/src/cli/pane-functions/cloud-session.ts +38 -0
  14. package/src/cli/pane-functions/dom.test.ts +41 -0
  15. package/src/cli/pane-functions/dom.ts +199 -0
  16. package/src/cli/pane-functions/index.test.ts +13 -2
  17. package/src/cli/pane-functions/index.ts +9 -5
  18. package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
  19. package/src/cli/pane-functions/intraday-shot.ts +334 -0
  20. package/src/cli/pane-functions/report.test.ts +29 -1
  21. package/src/cli/pane-functions/report.ts +68 -7
  22. package/src/cli/pane-functions/screenshot.ts +195 -42
  23. package/src/cli/restore-plugins.ts +10 -3
  24. package/src/components/chart/composite/composite-chart.test.tsx +68 -1
  25. package/src/components/chart/composite/composite-chart.tsx +87 -21
  26. package/src/components/chart/composite/rasterizer.ts +33 -0
  27. package/src/components/chart/composite/renderers.test.ts +51 -4
  28. package/src/components/chart/composite/scene.test.ts +128 -2
  29. package/src/components/chart/composite/scene.ts +92 -16
  30. package/src/components/chart/composite/text-renderer.ts +13 -0
  31. package/src/components/chart/composite/time-scale.ts +30 -10
  32. package/src/components/chart/composite/types.ts +26 -0
  33. package/src/components/command-bar/assist/runtime.ts +38 -30
  34. package/src/components/command-bar/surface/assist.test.tsx +63 -9
  35. package/src/components/command-bar/surface/test-harness.tsx +23 -4
  36. package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
  37. package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
  38. package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
  39. package/src/components/onboarding/onboarding-wizard.tsx +49 -14
  40. package/src/data/config/store/index.test.ts +23 -0
  41. package/src/data/config/store/node.ts +7 -1
  42. package/src/data/config/store/normalize.ts +4 -0
  43. package/src/news/types.ts +1 -0
  44. package/src/plugins/builtin/cds/client.test.ts +20 -0
  45. package/src/plugins/builtin/cds/client.ts +13 -3
  46. package/src/plugins/builtin/cds/headless.test.ts +30 -0
  47. package/src/plugins/builtin/cds/headless.ts +113 -0
  48. package/src/plugins/builtin/cds/index.tsx +4 -0
  49. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
  50. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  51. package/src/plugins/builtin/chat/controller/channels.ts +2 -1
  52. package/src/plugins/builtin/chat/controller/index.ts +13 -5
  53. package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
  54. package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
  55. package/src/plugins/builtin/chat/controller/state.ts +1 -0
  56. package/src/plugins/builtin/chat/controller.test.ts +260 -4
  57. package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
  58. package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
  59. package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
  60. package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
  61. package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
  62. package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
  63. package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
  64. package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
  65. package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
  66. package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
  67. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
  68. package/src/plugins/builtin/cloud/plugin.tsx +2 -0
  69. package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
  70. package/src/plugins/builtin/congress-trades/client.ts +11 -0
  71. package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
  72. package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
  73. package/src/plugins/builtin/congress-trades/headless.ts +145 -0
  74. package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
  75. package/src/plugins/builtin/credit-conditions/client.ts +1 -1
  76. package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
  77. package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
  78. package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
  79. package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
  80. package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
  81. package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
  82. package/src/plugins/builtin/dividend-yield/model.ts +4 -22
  83. package/src/plugins/builtin/dividend-yield/view.ts +17 -0
  84. package/src/plugins/builtin/earnings/headless.test.ts +82 -0
  85. package/src/plugins/builtin/earnings/headless.ts +162 -0
  86. package/src/plugins/builtin/earnings/index.tsx +2 -0
  87. package/src/plugins/builtin/earnings-calls/data.ts +29 -8
  88. package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
  89. package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
  90. package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
  91. package/src/plugins/builtin/earnings-calls/model.ts +238 -0
  92. package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
  93. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
  94. package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
  95. package/src/plugins/builtin/fear-greed/headless.ts +84 -0
  96. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  97. package/src/plugins/builtin/holders/client.ts +29 -0
  98. package/src/plugins/builtin/holders/headless.test.ts +58 -0
  99. package/src/plugins/builtin/holders/headless.ts +162 -0
  100. package/src/plugins/builtin/holders/index.tsx +14 -8
  101. package/src/plugins/builtin/holders/pane.tsx +7 -1
  102. package/src/plugins/builtin/insider/client.ts +50 -0
  103. package/src/plugins/builtin/insider/headless.test.ts +122 -0
  104. package/src/plugins/builtin/insider/headless.ts +117 -0
  105. package/src/plugins/builtin/insider/index.tsx +21 -50
  106. package/src/plugins/builtin/insider/insider-data.ts +1 -1
  107. package/src/plugins/builtin/insider/model.ts +72 -0
  108. package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
  109. package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
  110. package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
  111. package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
  112. package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
  113. package/src/plugins/builtin/market-movers/client.ts +76 -0
  114. package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
  115. package/src/plugins/builtin/market-movers/headless.ts +73 -0
  116. package/src/plugins/builtin/market-movers/index.tsx +11 -51
  117. package/src/plugins/builtin/news/headless.test.ts +138 -0
  118. package/src/plugins/builtin/news/headless.ts +190 -0
  119. package/src/plugins/builtin/news/index.tsx +13 -9
  120. package/src/plugins/builtin/news/wire/index.tsx +2 -1
  121. package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
  122. package/src/plugins/builtin/research/analyst-headless.ts +172 -0
  123. package/src/plugins/builtin/research/analyst-model.ts +219 -0
  124. package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
  125. package/src/plugins/builtin/research/client.ts +12 -0
  126. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
  127. package/src/plugins/builtin/research/event-model.ts +245 -0
  128. package/src/plugins/builtin/research/events-headless.test.ts +32 -0
  129. package/src/plugins/builtin/research/events-headless.ts +87 -0
  130. package/src/plugins/builtin/research/headless.test.ts +93 -0
  131. package/src/plugins/builtin/research/headless.ts +161 -0
  132. package/src/plugins/builtin/research/index.tsx +32 -18
  133. package/src/plugins/builtin/research-search/pane.tsx +4 -0
  134. package/src/plugins/builtin/scanner/headless.test.ts +59 -0
  135. package/src/plugins/builtin/scanner/headless.ts +125 -0
  136. package/src/plugins/builtin/scanner/index.ts +4 -0
  137. package/src/plugins/builtin/sec/client.ts +16 -0
  138. package/src/plugins/builtin/sec/headless.test.ts +72 -0
  139. package/src/plugins/builtin/sec/headless.ts +69 -0
  140. package/src/plugins/builtin/sec/index.tsx +20 -75
  141. package/src/plugins/builtin/sec/model.ts +89 -0
  142. package/src/plugins/builtin/sectors/client.ts +54 -0
  143. package/src/plugins/builtin/sectors/headless.test.ts +28 -0
  144. package/src/plugins/builtin/sectors/headless.ts +80 -0
  145. package/src/plugins/builtin/sectors/index.tsx +6 -51
  146. package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
  147. package/src/plugins/builtin/short-interest/client.ts +5 -2
  148. package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
  149. package/src/plugins/builtin/short-interest/headless.ts +111 -0
  150. package/src/plugins/builtin/short-interest/index.tsx +15 -9
  151. package/src/plugins/builtin/thirteenf/data.ts +13 -11
  152. package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
  153. package/src/plugins/builtin/thirteenf/headless.ts +286 -0
  154. package/src/plugins/builtin/thirteenf/index.tsx +4 -0
  155. package/src/plugins/builtin/thirteenf/model.ts +13 -0
  156. package/src/plugins/builtin/thirteenf/table.ts +3 -15
  157. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
  158. package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
  159. package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
  160. package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
  161. package/src/plugins/builtin/volatility/client.ts +16 -3
  162. package/src/plugins/builtin/volatility/headless.test.ts +35 -0
  163. package/src/plugins/builtin/volatility/headless.ts +75 -0
  164. package/src/plugins/builtin/volatility/index.tsx +4 -0
  165. package/src/plugins/builtin/world-indices/client.ts +43 -0
  166. package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
  167. package/src/plugins/builtin/world-indices/headless.ts +109 -0
  168. package/src/plugins/builtin/world-indices/index.tsx +4 -0
  169. package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
  170. package/src/plugins/builtin/yield-curve/headless.ts +67 -0
  171. package/src/plugins/builtin/yield-curve/index.tsx +6 -8
  172. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
  173. package/src/plugins/bundle.test.ts +58 -1
  174. package/src/plugins/bundle.ts +4 -2
  175. package/src/plugins/catalog-backend.test.ts +26 -0
  176. package/src/plugins/catalog-backend.ts +9 -2
  177. package/src/plugins/catalog-browser.test.ts +0 -13
  178. package/src/plugins/catalog-browser.ts +4 -0
  179. package/src/plugins/catalog-manifest.test.ts +15 -0
  180. package/src/plugins/catalog-manifest.ts +12 -5
  181. package/src/plugins/catalog-ui.ts +0 -6
  182. package/src/plugins/host-contract.ts +9 -0
  183. package/src/plugins/host-modules.ts +12 -0
  184. package/src/plugins/loader.ts +63 -15
  185. package/src/plugins/prediction-markets/headless.test.ts +119 -0
  186. package/src/plugins/prediction-markets/headless.ts +255 -0
  187. package/src/plugins/prediction-markets/index.tsx +2 -0
  188. package/src/plugins/seed.ts +3 -0
  189. package/src/public/broker.ts +6 -0
  190. package/src/remote/schema.test.ts +65 -0
  191. package/src/remote/schema.ts +322 -39
  192. package/src/remote/types.ts +49 -2
  193. package/src/renderers/browser/app-services.ts +3 -0
  194. package/src/renderers/browser/config-host.ts +24 -8
  195. package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
  196. package/src/renderers/browser/deeplink-bridge.ts +15 -3
  197. package/src/renderers/browser/main.tsx +3 -1
  198. package/src/renderers/browser/research-entry.test.ts +8 -0
  199. package/src/renderers/browser/research-entry.ts +7 -0
  200. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
  201. package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
  202. package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
  203. package/src/renderers/electrobun/view/build-assets.ts +0 -2
  204. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
  205. package/src/sources/gloomberb-cloud/news.ts +1 -0
  206. package/src/sync/core-contributors.test.ts +111 -0
  207. package/src/sync/core-contributors.ts +155 -1
  208. package/src/time-series/resolve.test.ts +118 -1
  209. package/src/time-series/resolve.ts +14 -16
  210. package/src/types/config.ts +2 -0
  211. package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
  212. package/src/plugins/broker-sync/native-loader.ts +0 -10
  213. package/src/plugins/broker-sync/normalize.ts +0 -308
  214. package/src/plugins/broker-sync/public.ts +0 -122
  215. package/src/plugins/broker-sync/robinhood-native.ts +0 -367
  216. package/src/plugins/broker-sync/robinhood.ts +0 -114
  217. package/src/plugins/broker-sync/simplefin-native.ts +0 -201
  218. package/src/plugins/broker-sync/simplefin.ts +0 -70
  219. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
@@ -0,0 +1,138 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { CloudNewsPayload } from "../../../api-client";
3
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
4
+ import type { NewsQuery } from "../../../news/types";
5
+ import {
6
+ createNewsFeedHeadless,
7
+ createTickerNewsHeadless,
8
+ type NewsHeadlessDependencies,
9
+ } from "./headless";
10
+
11
+ function story(
12
+ id: string,
13
+ sentiment: CloudNewsPayload["sentiment"],
14
+ importance: number,
15
+ publishedAt: string,
16
+ ): CloudNewsPayload {
17
+ return {
18
+ id,
19
+ headline: `${id} headline`,
20
+ summary: `${id} summary`,
21
+ category: "markets",
22
+ sentiment,
23
+ sectors: ["technology"],
24
+ firstPublishedAt: publishedAt,
25
+ lastPublishedAt: publishedAt,
26
+ firstSeenAt: publishedAt,
27
+ lastSeenAt: publishedAt,
28
+ primaryUrl: `https://example.com/${id}`,
29
+ primarySource: "Example Wire",
30
+ scores: {
31
+ importance,
32
+ urgency: importance - 1,
33
+ marketImpact: importance - 2,
34
+ novelty: 7,
35
+ confidence: 90,
36
+ },
37
+ flags: { breaking: importance > 80 },
38
+ variantCount: 1,
39
+ sourceCount: 1,
40
+ sources: ["Example Wire"],
41
+ entities: [],
42
+ tickerLinks: [{
43
+ symbol: "AMD",
44
+ exchange: "NASDAQ",
45
+ canonicalTicker: "AMD",
46
+ relationType: "issuer",
47
+ displayTier: "primary",
48
+ confidence: 1,
49
+ relevanceScore: 1,
50
+ }],
51
+ };
52
+ }
53
+
54
+ const stories = [
55
+ story("newer", "positive", 95, "2026-09-04T20:00:00.000Z"),
56
+ story("older", "negative", 70, "2026-09-04T18:00:00.000Z"),
57
+ ];
58
+
59
+ function args(
60
+ symbol: string | null,
61
+ options: Record<string, string | number | boolean>,
62
+ ): HeadlessPaneLoadArgs {
63
+ return {
64
+ rawArgument: symbol ?? "",
65
+ argument: symbol,
66
+ symbols: symbol ? [symbol] : [],
67
+ options,
68
+ };
69
+ }
70
+
71
+ const context = {} as HeadlessPaneContext;
72
+
73
+ function dependencies(seen: NewsQuery[]): NewsHeadlessDependencies {
74
+ return {
75
+ loadNews: async (query) => {
76
+ seen.push(query);
77
+ return { items: stories, nextCursor: "page-2" };
78
+ },
79
+ now: () => new Date("2026-09-04T21:00:00.000Z"),
80
+ };
81
+ }
82
+
83
+ describe("news feed headless", () => {
84
+ test("maps cloud stories into a point-in-time snapshot", async () => {
85
+ const seen: NewsQuery[] = [];
86
+ const definition = createNewsFeedHeadless(dependencies(seen));
87
+
88
+ const result = await definition.load(
89
+ args(null, { sentiment: "any", minImportance: 0, limit: 50 }),
90
+ context,
91
+ );
92
+
93
+ expect(result.asOf).toBe("2026-09-04T21:00:00.000Z");
94
+ expect(result.items[0]).toMatchObject({
95
+ id: "newer",
96
+ headline: "newer headline",
97
+ tickers: ["AMD"],
98
+ sentiment: "positive",
99
+ importance: 95,
100
+ breaking: true,
101
+ sourceCount: 1,
102
+ });
103
+ expect(seen[0]).toMatchObject({ feed: "latest", limit: 50 });
104
+ });
105
+
106
+ test("applies sentiment and importance options", async () => {
107
+ const definition = createNewsFeedHeadless(dependencies([]));
108
+
109
+ const result = await definition.load(
110
+ args(null, { sentiment: "positive", minImportance: 90, limit: 50 }),
111
+ context,
112
+ );
113
+
114
+ expect(result.items.map((item) => item.id)).toEqual(["newer"]);
115
+ });
116
+ });
117
+
118
+ describe("ticker news headless", () => {
119
+ test("requests the ticker feed and applies the row limit", async () => {
120
+ const seen: NewsQuery[] = [];
121
+ const definition = createTickerNewsHeadless(dependencies(seen));
122
+
123
+ const result = await definition.load(
124
+ args("AMD:XNAS", { sentiment: "any", minImportance: 0, limit: 1 }),
125
+ context,
126
+ );
127
+
128
+ expect(seen[0]).toMatchObject({
129
+ feed: "ticker",
130
+ ticker: "AMD",
131
+ exchange: "NASDAQ",
132
+ tickerTier: "primary",
133
+ limit: 1,
134
+ });
135
+ expect(result.items).toHaveLength(1);
136
+ expect(result.metadata).toMatchObject({ truncated: true });
137
+ });
138
+ });
@@ -0,0 +1,190 @@
1
+ import type { CloudNewsListResponse } from "../../../api-client";
2
+ import type {
3
+ HeadlessPaneContext,
4
+ HeadlessPaneDefinition,
5
+ HeadlessPaneLoadArgs,
6
+ HeadlessSnapshotResult,
7
+ } from "../../../types/plugin";
8
+ import type { NewsArticle, NewsQuery } from "../../../news/types";
9
+ import {
10
+ cloudNewsParams,
11
+ mapCloudNewsArticle,
12
+ } from "../../../sources/gloomberb-cloud/news";
13
+ import { parsePublicTickerKey } from "../../../utils/exchanges";
14
+
15
+ const NEWS_COLUMNS = [
16
+ { key: "publishedAt", header: "Published" },
17
+ { key: "source", header: "Source" },
18
+ { key: "headline", header: "Headline" },
19
+ { key: "tickers", header: "Tickers" },
20
+ { key: "sentiment", header: "Sentiment" },
21
+ { key: "importance", header: "Score", align: "right" as const },
22
+ ];
23
+
24
+ export interface NewsHeadlessDependencies {
25
+ loadNews(
26
+ query: NewsQuery,
27
+ context: HeadlessPaneContext,
28
+ ): Promise<CloudNewsListResponse>;
29
+ now(): Date;
30
+ }
31
+
32
+ const defaultDependencies: NewsHeadlessDependencies = {
33
+ loadNews: (query, context) => context.apiClient.getCloudNews(cloudNewsParams(query)),
34
+ now: () => new Date(),
35
+ };
36
+
37
+ function newsRow(article: NewsArticle) {
38
+ return {
39
+ id: article.id,
40
+ publishedAt: article.publishedAt.toISOString(),
41
+ source: article.source,
42
+ headline: article.title,
43
+ summary: article.summary ?? null,
44
+ topic: article.topic,
45
+ topics: article.topics,
46
+ sectors: article.sectors,
47
+ categories: article.categories,
48
+ tickers: article.tickers,
49
+ sentiment: article.sentiment ?? null,
50
+ importance: article.scores.importance,
51
+ urgency: article.scores.urgency,
52
+ marketImpact: article.scores.marketImpact,
53
+ novelty: article.scores.novelty,
54
+ confidence: article.scores.confidence,
55
+ breaking: article.isBreaking,
56
+ developing: article.isDeveloping,
57
+ sourceCount: article.sourceCount ?? article.items?.length ?? 0,
58
+ url: article.url,
59
+ };
60
+ }
61
+
62
+ function selectedSentiment(args: HeadlessPaneLoadArgs): string {
63
+ return String(args.options.sentiment ?? "any");
64
+ }
65
+
66
+ export function projectNewsHeadless(
67
+ response: CloudNewsListResponse,
68
+ args: HeadlessPaneLoadArgs,
69
+ now: Date,
70
+ ): HeadlessSnapshotResult {
71
+ const sentiment = selectedSentiment(args);
72
+ const minImportance = Number(args.options.minImportance ?? 0);
73
+ const limit = Number(args.options.limit ?? 50);
74
+ const matching = response.items
75
+ .map((item) => mapCloudNewsArticle(item, args.symbols[0]))
76
+ .filter((article) => sentiment === "any" || article.sentiment === sentiment)
77
+ .filter((article) => article.importance >= minImportance)
78
+ .sort((left, right) => right.publishedAt.getTime() - left.publishedAt.getTime());
79
+ const items = matching.slice(0, limit).map(newsRow);
80
+
81
+ return {
82
+ asOf: now.toISOString(),
83
+ items,
84
+ metadata: {
85
+ nextCursor: response.nextCursor,
86
+ sentiment,
87
+ minImportance,
88
+ total: matching.length,
89
+ returned: items.length,
90
+ truncated: items.length < matching.length || response.nextCursor != null,
91
+ },
92
+ };
93
+ }
94
+
95
+ function newsOptions() {
96
+ return [
97
+ {
98
+ key: "sentiment",
99
+ description: "Story sentiment to include.",
100
+ type: "enum" as const,
101
+ values: [
102
+ { value: "any" },
103
+ { value: "positive" },
104
+ { value: "neutral" },
105
+ { value: "negative" },
106
+ ],
107
+ defaultValue: "any",
108
+ },
109
+ {
110
+ key: "minImportance",
111
+ aliases: ["importance"],
112
+ description: "Minimum importance score.",
113
+ type: "integer" as const,
114
+ defaultValue: 0,
115
+ minimum: 0,
116
+ maximum: 100,
117
+ },
118
+ {
119
+ key: "limit",
120
+ description: "Maximum stories to return.",
121
+ type: "integer" as const,
122
+ defaultValue: 50,
123
+ minimum: 1,
124
+ maximum: 200,
125
+ },
126
+ ];
127
+ }
128
+
129
+ function queryFor(args: HeadlessPaneLoadArgs, feed: "latest" | "ticker"): NewsQuery {
130
+ const sentiment = selectedSentiment(args);
131
+ const ticker = args.symbols[0] ? parsePublicTickerKey(args.symbols[0]) : null;
132
+ return {
133
+ feed,
134
+ ...(feed === "ticker"
135
+ ? {
136
+ ticker: ticker?.symbol,
137
+ exchange: ticker?.exchange,
138
+ tickerTier: "primary" as const,
139
+ }
140
+ : {}),
141
+ ...(sentiment === "any" ? {} : { sentiment: sentiment as "positive" | "neutral" | "negative" }),
142
+ minImportance: Number(args.options.minImportance ?? 0),
143
+ limit: Number(args.options.limit ?? 50),
144
+ };
145
+ }
146
+
147
+ export function createNewsFeedHeadless(
148
+ dependencies: NewsHeadlessDependencies = defaultDependencies,
149
+ ): HeadlessPaneDefinition<"snapshot"> {
150
+ return {
151
+ shape: "snapshot",
152
+ argument: { kind: "none" },
153
+ options: newsOptions(),
154
+ columns: NEWS_COLUMNS,
155
+ describe: "News Feed",
156
+ async load(args, context) {
157
+ return projectNewsHeadless(
158
+ await dependencies.loadNews(queryFor(args, "latest"), context),
159
+ args,
160
+ dependencies.now(),
161
+ );
162
+ },
163
+ };
164
+ }
165
+
166
+ export function createTickerNewsHeadless(
167
+ dependencies: NewsHeadlessDependencies = defaultDependencies,
168
+ ): HeadlessPaneDefinition<"snapshot"> {
169
+ return {
170
+ shape: "snapshot",
171
+ argument: {
172
+ kind: "ticker",
173
+ placeholder: "ticker",
174
+ description: "Company symbol, optionally qualified as SYMBOL:EXCHANGE.",
175
+ },
176
+ options: newsOptions(),
177
+ columns: NEWS_COLUMNS,
178
+ describe: (args) => `Ticker News | ${String(args.argument)}`,
179
+ async load(args, context) {
180
+ return projectNewsHeadless(
181
+ await dependencies.loadNews(queryFor(args, "ticker"), context),
182
+ args,
183
+ dependencies.now(),
184
+ );
185
+ },
186
+ };
187
+ }
188
+
189
+ export const newsFeedHeadless = createNewsFeedHeadless();
190
+ export const tickerNewsHeadless = createTickerNewsHeadless();
@@ -17,6 +17,7 @@ import { useNewsArticleFooter } from "./wire/news/footer";
17
17
  import { usePersistedNewsArticles } from "./wire/persisted-articles";
18
18
  import { useNewsReadState } from "./wire/read-state";
19
19
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
20
+ import { tickerNewsHeadless } from "./headless";
20
21
 
21
22
  const NEWS_ITEM_LIMIT = 50;
22
23
  const DEFAULT_SORT: NewsSortPreference = { columnId: "time", direction: "desc" };
@@ -140,15 +141,18 @@ export const tickerNewsModule: PluginModule = {
140
141
  ],
141
142
 
142
143
  paneTemplates: [
143
- createTickerSurfacePaneTemplate({
144
- id: "ticker-news-pane",
145
- paneId: "ticker-news",
146
- label: "Ticker News",
147
- description: "Company news for the selected ticker.",
148
- keywords: ["company", "ticker", "news", "headlines", "cn"],
149
- shortcut: "CN",
150
- publicShare: true,
151
- }),
144
+ {
145
+ ...createTickerSurfacePaneTemplate({
146
+ id: "ticker-news-pane",
147
+ paneId: "ticker-news",
148
+ label: "Ticker News",
149
+ description: "Company news for the selected ticker.",
150
+ keywords: ["company", "ticker", "news", "headlines", "cn"],
151
+ shortcut: "CN",
152
+ publicShare: true,
153
+ }),
154
+ headless: tickerNewsHeadless,
155
+ },
152
156
  ],
153
157
 
154
158
  setup(ctx) {
@@ -23,6 +23,7 @@ import { NewsPresetPane } from "./news/preset-pane";
23
23
  import { NEWS_QUERY_PRESETS } from "./news/query-presets";
24
24
  import type { NewsColumnId, NewsSortPreference } from "./news/table";
25
25
  import { createRssNewsCapability } from "./rss/source";
26
+ import { newsFeedHeadless } from "../headless";
26
27
 
27
28
  interface NewsPresetPaneConfig {
28
29
  paneKey: string;
@@ -107,7 +108,7 @@ export const newsWireModule: PluginModule = {
107
108
  ],
108
109
  paneTemplates: [
109
110
  { id: "news-top-pane", paneId: "news-top", label: "Top News", description: "Curated top market stories ranked by importance", keywords: ["top", "news", "headlines", "stories"], shortcut: { prefix: "TOP" } },
110
- { id: "news-feed-pane", paneId: "news-feed", label: "News Feed", description: "Chronological market news firehose", keywords: ["news", "feed", "firehose", "wire", "stream"], shortcut: { prefix: "N" } },
111
+ { id: "news-feed-pane", paneId: "news-feed", label: "News Feed", description: "Chronological market news firehose", keywords: ["news", "feed", "firehose", "wire", "stream"], shortcut: { prefix: "N" }, headless: newsFeedHeadless },
111
112
  { id: "news-industry-pane", paneId: "news-industry", label: "Sector News", description: "Market news filtered by sector", keywords: ["news", "industry", "sector", "ni", "filter"], shortcut: { prefix: "NI" } },
112
113
  { id: "news-breaking-pane", paneId: "news-breaking", label: "Breaking News", description: "Breaking and urgent market news", keywords: ["first", "breaking", "urgent", "alert", "flash"], shortcut: { prefix: "FIRST" } },
113
114
  ],
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createTestDataProvider } from "../../../test-support/data-provider";
3
+ import { createDefaultConfig } from "../../../types/config";
4
+ import type { AnalystResearchData } from "../../../types/financials";
5
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
6
+ import { createAnalystResearchHeadless } from "./analyst-headless";
7
+
8
+ const data: AnalystResearchData = {
9
+ symbol: "AMD",
10
+ currency: "USD",
11
+ priceTarget: { current: 150, average: 180, low: 120, median: 175, high: 230 },
12
+ recommendationRating: 8.4,
13
+ recommendations: [{ period: "current month", strongBuy: 10, buy: 8, hold: 4, sell: 1 }],
14
+ ratings: [
15
+ { date: "2026-08-20", firm: "Beta", action: "Maintains", current: "Buy", currentPriceTarget: 190 },
16
+ { date: "2026-08-25", firm: "Alpha", action: "Raises", current: "Buy", prior: "Hold", currentPriceTarget: 210, priorPriceTarget: 180 },
17
+ ],
18
+ earningsEstimates: [],
19
+ revenueEstimates: [],
20
+ };
21
+
22
+ function context(): HeadlessPaneContext {
23
+ return {
24
+ marketData: createTestDataProvider(),
25
+ apiClient: {} as HeadlessPaneContext["apiClient"],
26
+ config: createDefaultConfig("/tmp/gloomberb-headless-analyst"),
27
+ signal: new AbortController().signal,
28
+ };
29
+ }
30
+
31
+ function args(overrides: Partial<HeadlessPaneLoadArgs["options"]> = {}): HeadlessPaneLoadArgs {
32
+ return {
33
+ rawArgument: "AMD",
34
+ argument: "AMD",
35
+ symbols: ["AMD"],
36
+ options: { sort: "date", order: "desc", limit: 25, ...overrides },
37
+ };
38
+ }
39
+
40
+ describe("analyst research headless model", () => {
41
+ test("projects summary and ratings while applying sort and limit", async () => {
42
+ const headless = createAnalystResearchHeadless({ loadData: async () => data });
43
+
44
+ const latest = await headless.load(args({ limit: 1 }), context());
45
+ expect(latest.sections[0]?.title).toBe("Summary");
46
+ const entries = "entries" in latest.sections[0]! ? latest.sections[0]!.entries : [];
47
+ expect(entries.slice(0, 2)).toMatchObject([
48
+ { label: "Average target", value: 180 },
49
+ { label: "Target upside", value: 0.2 },
50
+ ]);
51
+ expect(latest.sections[1]).toMatchObject({
52
+ title: "Recent analyst actions",
53
+ rows: [{ firm: "Alpha", currentPriceTarget: 210, priorPriceTarget: 180 }],
54
+ });
55
+
56
+ const byFirm = await headless.load(args({ sort: "firm", order: "desc", limit: 2 }), context());
57
+ const ratings = "rows" in byFirm.sections[1]! ? byFirm.sections[1]!.rows : [];
58
+ expect(ratings.map((row) => row.firm)).toEqual(["Beta", "Alpha"]);
59
+ });
60
+ });
@@ -0,0 +1,172 @@
1
+ import type {
2
+ HeadlessBundleSection,
3
+ HeadlessPaneColumn,
4
+ HeadlessPaneContext,
5
+ HeadlessPaneDefinition,
6
+ HeadlessPaneEntry,
7
+ HeadlessPaneLoadArgs,
8
+ } from "../../../types/plugin";
9
+ import type { AnalystResearchData } from "../../../types/financials";
10
+ import { formatCurrency, formatPercent } from "../../../utils/format";
11
+ import { loadAnalystResearch } from "./client";
12
+ import {
13
+ formatRatingLabel,
14
+ formatRatingTarget,
15
+ latestRecommendation,
16
+ recommendationTotal,
17
+ sortRatingRows,
18
+ targetUpside,
19
+ type RatingColumnId,
20
+ } from "./analyst-model";
21
+
22
+ const RATING_COLUMNS: HeadlessPaneColumn[] = [
23
+ { key: "date", header: "Date" },
24
+ { key: "firm", header: "Firm" },
25
+ { key: "action", header: "Action" },
26
+ { key: "current", header: "Rating" },
27
+ {
28
+ key: "target",
29
+ header: "Target",
30
+ format: (_value, row) => formatRatingTarget(
31
+ row as unknown as AnalystResearchData["ratings"][number],
32
+ String(row.currency ?? "USD"),
33
+ ).trim(),
34
+ },
35
+ { key: "prior", header: "Prior" },
36
+ ];
37
+
38
+ const SORT_COLUMNS: Record<string, RatingColumnId> = {
39
+ date: "date",
40
+ firm: "firm",
41
+ action: "action",
42
+ rating: "current",
43
+ target: "target",
44
+ };
45
+
46
+ function overviewEntries(data: AnalystResearchData): HeadlessPaneEntry[] {
47
+ const target = data.priceTarget;
48
+ const currency = target?.currency ?? data.currency ?? "USD";
49
+ const recommendation = latestRecommendation(data);
50
+ const upside = targetUpside(target);
51
+ return [
52
+ {
53
+ label: "Average target",
54
+ value: target?.average ?? null,
55
+ formatted: target?.average == null ? "-" : formatCurrency(target.average, currency),
56
+ },
57
+ {
58
+ label: "Target upside",
59
+ value: upside ?? null,
60
+ formatted: upside == null ? "-" : formatPercent(upside),
61
+ },
62
+ {
63
+ label: "Target range",
64
+ value: target ? { low: target.low, median: target.median, high: target.high } : null,
65
+ formatted: target
66
+ ? [target.low, target.median, target.high]
67
+ .map((value) => value == null ? "-" : formatCurrency(value, currency))
68
+ .join(" / ")
69
+ : "-",
70
+ },
71
+ {
72
+ label: "Recommendation rating",
73
+ value: data.recommendationRating ?? null,
74
+ formatted: formatRatingLabel(data.recommendationRating),
75
+ },
76
+ { label: "Analysts", value: recommendationTotal(data) },
77
+ {
78
+ label: "Recommendation mix",
79
+ value: recommendation ? {
80
+ period: recommendation.period,
81
+ strongBuy: recommendation.strongBuy ?? 0,
82
+ buy: recommendation.buy ?? 0,
83
+ hold: recommendation.hold ?? 0,
84
+ sell: recommendation.sell ?? 0,
85
+ strongSell: recommendation.strongSell ?? 0,
86
+ } : null,
87
+ formatted: recommendation
88
+ ? `SB ${recommendation.strongBuy ?? 0} B ${recommendation.buy ?? 0} H ${recommendation.hold ?? 0} S ${(recommendation.sell ?? 0) + (recommendation.strongSell ?? 0)}`
89
+ : "-",
90
+ },
91
+ ];
92
+ }
93
+
94
+ export interface AnalystResearchHeadlessDependencies {
95
+ loadData(
96
+ symbol: string,
97
+ args: HeadlessPaneLoadArgs,
98
+ ctx: HeadlessPaneContext,
99
+ ): Promise<AnalystResearchData>;
100
+ }
101
+
102
+ const defaultDependencies: AnalystResearchHeadlessDependencies = {
103
+ loadData: (symbol, _args, ctx) => loadAnalystResearch(ctx.marketData, symbol),
104
+ };
105
+
106
+ export function createAnalystResearchHeadless(
107
+ dependencies: AnalystResearchHeadlessDependencies = defaultDependencies,
108
+ ): HeadlessPaneDefinition<"bundle"> {
109
+ return {
110
+ shape: "bundle",
111
+ argument: {
112
+ kind: "ticker",
113
+ placeholder: "ticker",
114
+ description: "Ticker whose analyst research should be loaded.",
115
+ },
116
+ options: [
117
+ {
118
+ key: "sort",
119
+ description: "Column used to sort analyst actions.",
120
+ type: "enum",
121
+ values: Object.keys(SORT_COLUMNS).map((value) => ({ value })),
122
+ defaultValue: "date",
123
+ },
124
+ {
125
+ key: "order",
126
+ description: "Sort direction.",
127
+ type: "enum",
128
+ values: [{ value: "desc" }, { value: "asc" }],
129
+ defaultValue: "desc",
130
+ },
131
+ {
132
+ key: "limit",
133
+ aliases: ["count", "rows"],
134
+ description: "Maximum analyst action rows.",
135
+ type: "integer",
136
+ defaultValue: 25,
137
+ minimum: 1,
138
+ maximum: 100,
139
+ },
140
+ ],
141
+ describe: (args) => `Analyst Research | ${args.symbols[0]}`,
142
+ async load(args, ctx) {
143
+ const symbol = args.symbols[0]!;
144
+ const data = await dependencies.loadData(symbol, args, ctx);
145
+ const currency = data.priceTarget?.currency ?? data.currency ?? "USD";
146
+ const ratings = sortRatingRows(data.ratings, {
147
+ columnId: SORT_COLUMNS[String(args.options.sort)] ?? "date",
148
+ direction: args.options.order === "asc" ? "asc" : "desc",
149
+ })
150
+ .slice(0, Number(args.options.limit))
151
+ .map((rating) => ({
152
+ ...rating,
153
+ target: rating.currentPriceTarget ?? rating.priorPriceTarget ?? null,
154
+ currency,
155
+ }));
156
+ const sections: HeadlessBundleSection[] = [
157
+ { title: "Summary", entries: overviewEntries(data) },
158
+ { title: "Recent analyst actions", columns: RATING_COLUMNS, rows: ratings },
159
+ ];
160
+ return {
161
+ sections,
162
+ metadata: {
163
+ symbol: data.symbol || symbol,
164
+ name: data.name ?? null,
165
+ currency,
166
+ },
167
+ };
168
+ },
169
+ };
170
+ }
171
+
172
+ export const analystResearchHeadless = createAnalystResearchHeadless();