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,93 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
3
+ import {
4
+ createEarningsEstimatesHeadless,
5
+ type EarningsEstimateSources,
6
+ } from "./headless";
7
+
8
+ const sources: EarningsEstimateSources = {
9
+ currency: "USD",
10
+ actions: {
11
+ symbol: "AMD",
12
+ currency: "USD",
13
+ dividends: [],
14
+ splits: [],
15
+ earnings: [{
16
+ date: "2026-07-28",
17
+ epsEstimate: 0.45,
18
+ epsActual: 0.48,
19
+ difference: 0.03,
20
+ surprisePercent: 6.67,
21
+ }],
22
+ },
23
+ estimates: {
24
+ symbol: "AMD",
25
+ currency: "USD",
26
+ recommendations: [],
27
+ ratings: [],
28
+ earningsEstimates: [
29
+ { date: "2026-09-30", period: "next_quarter", average: 1.2, analysts: 30 },
30
+ { date: "2026-12-31", period: "current_year", average: 4.8, analysts: 32 },
31
+ ],
32
+ revenueEstimates: [
33
+ { date: "2026-09-30", period: "next_quarter", average: 9_000_000_000, analysts: 28 },
34
+ { date: "2026-12-31", period: "current_year", average: 35_000_000_000, analysts: 29 },
35
+ ],
36
+ },
37
+ financials: {
38
+ annualStatements: [],
39
+ quarterlyStatements: [],
40
+ priceHistory: [],
41
+ },
42
+ };
43
+
44
+ function args(options: Record<string, string | number | boolean>): HeadlessPaneLoadArgs {
45
+ return {
46
+ rawArgument: "AMD",
47
+ argument: "AMD",
48
+ symbols: ["AMD"],
49
+ options,
50
+ };
51
+ }
52
+
53
+ const context = {} as HeadlessPaneContext;
54
+
55
+ describe("earnings estimates headless", () => {
56
+ test("maps the shared event model into estimate rows", async () => {
57
+ const definition = createEarningsEstimatesHeadless({
58
+ loadSources: async () => sources,
59
+ });
60
+
61
+ const result = await definition.load(args({ kind: "all", limit: 50 }), context);
62
+
63
+ expect(result.rows).toEqual(expect.arrayContaining([
64
+ expect.objectContaining({
65
+ status: "Q Est",
66
+ period: "next qtr",
67
+ qEps: 1.2,
68
+ qRevenue: 9_000_000_000,
69
+ detail: "30E/28R",
70
+ }),
71
+ expect.objectContaining({
72
+ status: "FY Est",
73
+ annualEps: 4.8,
74
+ annualRevenue: 35_000_000_000,
75
+ }),
76
+ expect.objectContaining({
77
+ status: "Earnings",
78
+ qEps: 0.48,
79
+ value: "+6.67%",
80
+ }),
81
+ ]));
82
+ });
83
+
84
+ test("applies kind and limit options", async () => {
85
+ const definition = createEarningsEstimatesHeadless({ loadSources: async () => sources });
86
+
87
+ const result = await definition.load(args({ kind: "estimates", limit: 1 }), context);
88
+
89
+ expect(result.rows).toHaveLength(1);
90
+ expect(["Q Est", "FY Est"]).toContain(result.rows[0]?.status);
91
+ expect(result.metadata).toMatchObject({ kind: "estimates", total: 2, truncated: true });
92
+ });
93
+ });
@@ -0,0 +1,161 @@
1
+ import type {
2
+ AnalystResearchData,
3
+ CorporateActionsData,
4
+ TickerFinancials,
5
+ } from "../../../types/financials";
6
+ import type {
7
+ HeadlessPaneContext,
8
+ HeadlessPaneDefinition,
9
+ HeadlessPaneLoadArgs,
10
+ HeadlessRowsResult,
11
+ } from "../../../types/plugin";
12
+ import { buildEventRows, type EventRow } from "./event-model";
13
+
14
+ const ESTIMATE_COLUMNS = [
15
+ { key: "date", header: "Date" },
16
+ { key: "status", header: "Event" },
17
+ { key: "period", header: "Period" },
18
+ { key: "qEps", header: "Q EPS", align: "right" as const },
19
+ { key: "qRevenue", header: "Q revenue", align: "right" as const },
20
+ { key: "annualEps", header: "Annual EPS", align: "right" as const },
21
+ { key: "annualRevenue", header: "Annual revenue", align: "right" as const },
22
+ { key: "value", header: "Value", align: "right" as const },
23
+ { key: "detail", header: "Detail" },
24
+ ];
25
+
26
+ export interface EarningsEstimateSources {
27
+ actions: CorporateActionsData | null;
28
+ estimates: AnalystResearchData | null;
29
+ financials: TickerFinancials | null;
30
+ currency: string;
31
+ errors?: string[];
32
+ }
33
+
34
+ export interface EarningsEstimatesHeadlessDependencies {
35
+ loadSources(
36
+ symbol: string,
37
+ context: HeadlessPaneContext,
38
+ ): Promise<EarningsEstimateSources>;
39
+ }
40
+
41
+ async function settledValue<T>(
42
+ promise: Promise<T> | null,
43
+ errors: string[],
44
+ label: string,
45
+ ): Promise<T | null> {
46
+ if (!promise) return null;
47
+ try {
48
+ return await promise;
49
+ } catch (error) {
50
+ errors.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ const defaultDependencies: EarningsEstimatesHeadlessDependencies = {
56
+ async loadSources(symbol, context) {
57
+ const errors: string[] = [];
58
+ const provider = context.marketData;
59
+ const [actions, estimates, financials] = await Promise.all([
60
+ settledValue(
61
+ provider.getCorporateActions?.(symbol, "") ?? null,
62
+ errors,
63
+ "corporate actions",
64
+ ),
65
+ settledValue(
66
+ provider.getAnalystResearch?.(symbol, "") ?? null,
67
+ errors,
68
+ "analyst estimates",
69
+ ),
70
+ settledValue(provider.getTickerFinancials(symbol, ""), errors, "financials"),
71
+ ]);
72
+ if (!actions && !estimates && !financials && errors.length > 0) {
73
+ throw new Error(errors.join("; "));
74
+ }
75
+ return {
76
+ actions,
77
+ estimates,
78
+ financials,
79
+ currency: actions?.currency ?? estimates?.currency ?? financials?.quote?.currency ?? "USD",
80
+ errors,
81
+ };
82
+ },
83
+ };
84
+
85
+ function matchesKind(row: EventRow, kind: string): boolean {
86
+ if (kind === "all") return true;
87
+ if (kind === "estimates") return row.status === "Q Est" || row.status === "FY Est";
88
+ return row.status === "Earnings" || row.status === "TTM";
89
+ }
90
+
91
+ export function projectEarningsEstimatesHeadless(
92
+ sources: EarningsEstimateSources,
93
+ args: HeadlessPaneLoadArgs,
94
+ ): HeadlessRowsResult {
95
+ const kind = String(args.options.kind ?? "all");
96
+ const limit = Number(args.options.limit ?? 50);
97
+ const matching = buildEventRows(
98
+ sources.actions,
99
+ sources.estimates,
100
+ sources.financials,
101
+ sources.currency,
102
+ ).filter((row) => matchesKind(row, kind));
103
+ const rows = matching.slice(0, limit).map((row) => ({ ...row }));
104
+
105
+ return {
106
+ columns: ESTIMATE_COLUMNS,
107
+ rows,
108
+ errors: sources.errors?.length ? sources.errors : undefined,
109
+ metadata: {
110
+ currency: sources.currency,
111
+ kind,
112
+ total: matching.length,
113
+ returned: rows.length,
114
+ truncated: rows.length < matching.length,
115
+ },
116
+ };
117
+ }
118
+
119
+ export function createEarningsEstimatesHeadless(
120
+ dependencies: EarningsEstimatesHeadlessDependencies = defaultDependencies,
121
+ ): HeadlessPaneDefinition<"rows"> {
122
+ return {
123
+ shape: "rows",
124
+ argument: {
125
+ kind: "ticker",
126
+ placeholder: "ticker",
127
+ description: "Company symbol.",
128
+ },
129
+ options: [
130
+ {
131
+ key: "kind",
132
+ description: "Estimate or reported rows to include.",
133
+ type: "enum",
134
+ values: [
135
+ { value: "all" },
136
+ { value: "estimates" },
137
+ { value: "reported" },
138
+ ],
139
+ defaultValue: "all",
140
+ },
141
+ {
142
+ key: "limit",
143
+ description: "Maximum rows to return.",
144
+ type: "integer",
145
+ defaultValue: 50,
146
+ minimum: 1,
147
+ maximum: 200,
148
+ },
149
+ ],
150
+ columns: ESTIMATE_COLUMNS,
151
+ describe: (args) => `Earnings Estimates | ${String(args.argument)}`,
152
+ async load(args, context) {
153
+ return projectEarningsEstimatesHeadless(
154
+ await dependencies.loadSources(String(args.argument), context),
155
+ args,
156
+ );
157
+ },
158
+ };
159
+ }
160
+
161
+ export const earningsEstimatesHeadless = createEarningsEstimatesHeadless();
@@ -5,6 +5,13 @@ import { AnalystResearchView } from "./analyst-pane";
5
5
  import { CorporateActionsView } from "./corporate-actions-pane";
6
6
  import { EquityDiagnosticView } from "./equity-diagnostic-pane";
7
7
  import { RelativeValuationPane } from "./relative-valuation-pane";
8
+ import { analystResearchHeadless } from "./analyst-headless";
9
+ import { eventsHeadless } from "./events-headless";
10
+ import { earningsEstimatesHeadless } from "./headless";
11
+
12
+ export { analystResearchHeadless } from "./analyst-headless";
13
+ export { eventsHeadless } from "./events-headless";
14
+ export { earningsEstimatesHeadless } from "./headless";
8
15
 
9
16
  function EarningsEstimatesPane(props: { focused: boolean; width: number; height: number }) {
10
17
  return (
@@ -70,6 +77,7 @@ export const researchModule: PluginModule = {
70
77
  defaultMode: "floating",
71
78
  defaultFloatingSize: { width: 104, height: 24 },
72
79
  tableExport: true,
80
+ headless: eventsHeadless,
73
81
  },
74
82
  {
75
83
  id: "relative-valuation",
@@ -94,15 +102,18 @@ export const researchModule: PluginModule = {
94
102
  ],
95
103
 
96
104
  paneTemplates: [
97
- createTickerSurfacePaneTemplate({
98
- id: "analyst-research-pane",
99
- paneId: "analyst-research",
100
- label: "Analyst Research",
101
- description: "Price targets, recommendations, and recent analyst actions.",
102
- keywords: ["analyst", "research", "ratings", "target", "anr"],
103
- shortcut: "ANR",
104
- publicShare: true,
105
- }),
105
+ {
106
+ ...createTickerSurfacePaneTemplate({
107
+ id: "analyst-research-pane",
108
+ paneId: "analyst-research",
109
+ label: "Analyst Research",
110
+ description: "Price targets, recommendations, and recent analyst actions.",
111
+ keywords: ["analyst", "research", "ratings", "target", "anr"],
112
+ shortcut: "ANR",
113
+ publicShare: true,
114
+ }),
115
+ headless: analystResearchHeadless,
116
+ },
106
117
  createTickerSurfacePaneTemplate({
107
118
  id: "equity-diagnostic-pane",
108
119
  paneId: "equity-diagnostic",
@@ -121,15 +132,18 @@ export const researchModule: PluginModule = {
121
132
  shortcut: "EVT",
122
133
  publicShare: true,
123
134
  }),
124
- createTickerSurfacePaneTemplate({
125
- id: "earnings-estimates-pane",
126
- paneId: "earnings-estimates",
127
- label: "Earnings Estimates",
128
- description: "EPS and revenue estimates with reported earnings.",
129
- keywords: ["earnings", "estimates", "ee", "analyst", "eps", "revenue", "events"],
130
- shortcut: "EE",
131
- publicShare: true,
132
- }),
135
+ {
136
+ ...createTickerSurfacePaneTemplate({
137
+ id: "earnings-estimates-pane",
138
+ paneId: "earnings-estimates",
139
+ label: "Earnings Estimates",
140
+ description: "EPS and revenue estimates with reported earnings.",
141
+ keywords: ["earnings", "estimates", "ee", "analyst", "eps", "revenue", "events"],
142
+ shortcut: "EE",
143
+ publicShare: true,
144
+ }),
145
+ headless: earningsEstimatesHeadless,
146
+ },
133
147
  {
134
148
  id: "relative-valuation-pane",
135
149
  paneId: "relative-valuation",
@@ -1,3 +1,4 @@
1
+ import { recordResearchActivity } from "../../../api-client/research-activity";
1
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
3
  import {
3
4
  Button,
@@ -416,6 +417,9 @@ export function ResearchSearchPane({ focused, paneId, width, height }: PaneProps
416
417
  // Search is free and uncapped, so nothing is gated up front: the upsell only
417
418
  // appears if the server itself refuses the query.
418
419
  const proRequired = failure?.status === 402;
420
+ useEffect(() => {
421
+ if (focused && status === "loaded") recordResearchActivity("research_viewed", "search");
422
+ }, [focused, status]);
419
423
  const signInRequired = !access.signedIn || failure?.status === 401 || savedFailure?.status === 401;
420
424
  const verificationRequired = !signInRequired
421
425
  && (!access.emailVerified || failure?.status === 403);
@@ -0,0 +1,59 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ScannerHiloPayload } from "../../../api-client";
3
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
4
+ import { createHiloHeadless, loadHiloSnapshot } from "./headless";
5
+
6
+ const payload: ScannerHiloPayload = {
7
+ status: "live",
8
+ asOf: 123,
9
+ access: "realtime",
10
+ delayMinutes: 0,
11
+ windows: { s30: { highs: 1, lows: 1 }, m1: { highs: 2, lows: 2 }, m5: { highs: 3, lows: 3 } },
12
+ lows: [
13
+ { symbol: "PENNY", price: 0.5, count: 10, at: 10 },
14
+ { symbol: "LOW", price: 10, count: 2, at: 20 },
15
+ ],
16
+ highs: [{ symbol: "HIGH", price: 20, count: 5, at: 30 }],
17
+ };
18
+
19
+ function args(minPrice: string, sort: string): HeadlessPaneLoadArgs {
20
+ return { rawArgument: "", argument: null, symbols: [], options: { minPrice, sort } };
21
+ }
22
+
23
+ describe("highs and lows headless model", () => {
24
+ test("waits for scanner data beyond the connecting snapshot", async () => {
25
+ let unsubscribed = 0;
26
+ const result = await loadHiloSnapshot({
27
+ subscribeScanner(_scanner, listener) {
28
+ listener({ type: "data", payload: { ...payload, status: "starting" } });
29
+ listener({ type: "data", payload });
30
+ return () => { unsubscribed += 1; };
31
+ },
32
+ }, new AbortController().signal);
33
+
34
+ expect(result.status).toBe("live");
35
+ expect(unsubscribed).toBe(1);
36
+ });
37
+
38
+ test("reports a degraded stream as partial data", async () => {
39
+ const headless = createHiloHeadless({
40
+ load: async () => ({ ...payload, status: "degraded" }),
41
+ });
42
+
43
+ const result = await headless.load(args("1", "recent"), {} as HeadlessPaneContext);
44
+
45
+ expect(result.errors).toEqual(["Scanner feed degraded"]);
46
+ });
47
+
48
+ test("applies the pane price and sort options to a stream snapshot", async () => {
49
+ const headless = createHiloHeadless({ load: async () => payload });
50
+ const filtered = await headless.load(args("1", "recent"), {} as HeadlessPaneContext);
51
+ const unfiltered = await headless.load(args("off", "count"), {} as HeadlessPaneContext);
52
+
53
+ expect(filtered.items.map((row) => row.symbol)).toEqual(["LOW", "HIGH"]);
54
+ expect(unfiltered.items.map((row) => row.symbol)).toEqual(["PENNY", "LOW", "HIGH"]);
55
+ expect((filtered.metadata?.windows as Array<Record<string, unknown>>)[0]).toMatchObject({
56
+ key: "m5", highs: 3, lows: 3,
57
+ });
58
+ });
59
+ });
@@ -0,0 +1,125 @@
1
+ import type { ScannerFeedEvent, ScannerHiloPayload } from "../../../api-client";
2
+ import type {
3
+ HeadlessPaneApiClient,
4
+ HeadlessPaneDefinition,
5
+ HeadlessPaneLoadArgs,
6
+ HeadlessSnapshotResult,
7
+ } from "../../../types/plugin";
8
+ import { buildHiloBarRows, filterHiloRows, type HiloMinPrice, type HiloSort } from "./hilo-model";
9
+
10
+ export function loadHiloSnapshot(
11
+ client: Pick<HeadlessPaneApiClient, "subscribeScanner">,
12
+ signal: AbortSignal,
13
+ timeoutMs = 10_000,
14
+ ): Promise<ScannerHiloPayload> {
15
+ return new Promise((resolve, reject) => {
16
+ let settled = false;
17
+ let unsubscribe: (() => void) | null = null;
18
+ const finish = (callback: () => void) => {
19
+ if (settled) return;
20
+ settled = true;
21
+ clearTimeout(timer);
22
+ signal.removeEventListener("abort", onAbort);
23
+ unsubscribe?.();
24
+ callback();
25
+ };
26
+ const onAbort = () => finish(() => reject(signal.reason instanceof Error
27
+ ? signal.reason
28
+ : new Error("Highs and lows snapshot aborted")));
29
+ const timer = setTimeout(() => finish(() => reject(new Error("Highs and lows snapshot timed out"))), timeoutMs);
30
+ signal.addEventListener("abort", onAbort, { once: true });
31
+ unsubscribe = client.subscribeScanner("hilo", (event: ScannerFeedEvent) => {
32
+ if (event.type === "denied") {
33
+ finish(() => reject(new Error(event.reason === "pro_required" ? "Pro access required" : "Sign in required")));
34
+ return;
35
+ }
36
+ if (event.payload.status === "starting") return;
37
+ finish(() => resolve(event.payload as ScannerHiloPayload));
38
+ });
39
+ if (settled) unsubscribe();
40
+ });
41
+ }
42
+
43
+ export function projectHiloSnapshot(
44
+ payload: ScannerHiloPayload,
45
+ minPrice: HiloMinPrice,
46
+ sort: HiloSort,
47
+ ): HeadlessSnapshotResult {
48
+ const lows = filterHiloRows(payload.lows, minPrice, sort).map((row) => ({ side: "low", ...row }));
49
+ const highs = filterHiloRows(payload.highs, minPrice, sort).map((row) => ({ side: "high", ...row }));
50
+ return {
51
+ asOf: new Date(payload.asOf).toISOString(),
52
+ items: [...lows, ...highs],
53
+ ...(payload.status === "degraded" ? { errors: ["Scanner feed degraded"] } : {}),
54
+ metadata: {
55
+ status: payload.status,
56
+ access: payload.access,
57
+ delayMinutes: payload.delayMinutes,
58
+ windows: buildHiloBarRows(payload.windows),
59
+ minPrice,
60
+ sort,
61
+ },
62
+ };
63
+ }
64
+
65
+ export interface HiloHeadlessDependencies {
66
+ load(
67
+ args: HeadlessPaneLoadArgs,
68
+ client: Pick<HeadlessPaneApiClient, "subscribeScanner">,
69
+ signal: AbortSignal,
70
+ ): Promise<ScannerHiloPayload>;
71
+ }
72
+
73
+ const defaultDependencies: HiloHeadlessDependencies = {
74
+ load: (_args, client, signal) => loadHiloSnapshot(client, signal),
75
+ };
76
+
77
+ export function createHiloHeadless(
78
+ dependencies: HiloHeadlessDependencies = defaultDependencies,
79
+ ): HeadlessPaneDefinition<"snapshot"> {
80
+ return {
81
+ shape: "snapshot",
82
+ argument: { kind: "none" },
83
+ options: [
84
+ {
85
+ key: "minPrice",
86
+ aliases: ["min-price"],
87
+ settingKey: "minPrice",
88
+ description: "Minimum stock price.",
89
+ type: "enum",
90
+ values: [{ value: "off" }, { value: "1" }, { value: "5" }],
91
+ defaultValue: "1",
92
+ },
93
+ {
94
+ key: "sort",
95
+ settingKey: "sort",
96
+ description: "Extreme ordering.",
97
+ type: "enum",
98
+ values: [{ value: "recent" }, { value: "count" }],
99
+ defaultValue: "recent",
100
+ },
101
+ ],
102
+ columns: [
103
+ { key: "side", header: "Side" },
104
+ { key: "symbol", header: "Symbol" },
105
+ { key: "price", header: "Price", align: "right" },
106
+ { key: "count", header: "Count", align: "right" },
107
+ {
108
+ key: "at",
109
+ header: "Observed at",
110
+ format: (value: unknown) => new Date(Number(value)).toISOString(),
111
+ },
112
+ ],
113
+ describe: "New Highs / Lows",
114
+ async load(args, ctx) {
115
+ const payload = await dependencies.load(args, ctx.apiClient, ctx.signal);
116
+ return projectHiloSnapshot(
117
+ payload,
118
+ args.options.minPrice as HiloMinPrice,
119
+ args.options.sort as HiloSort,
120
+ );
121
+ },
122
+ };
123
+ }
124
+
125
+ export const hiloHeadless = createHiloHeadless();
@@ -3,6 +3,9 @@ import type { PluginModule } from "../plugin-module";
3
3
  import { DEFAULT_FLOW_FILTERS, FLOW_FILTER_OPTIONS } from "./flow-model";
4
4
  import FlowPane from "./flow-pane";
5
5
  import HiloPane from "./hilo-pane";
6
+ import { hiloHeadless } from "./headless";
7
+
8
+ export { hiloHeadless } from "./headless";
6
9
 
7
10
  export const HILO_PANE_ID = "scanner-hilo";
8
11
  export const FLOW_PANE_ID = "scanner-flow";
@@ -96,6 +99,7 @@ export const scannerModule: PluginModule = {
96
99
  description: "Session new-high and new-low momentum with 30s/1m/5m counts.",
97
100
  keywords: ["hilo", "highs", "lows", "new", "momentum", "breakout", "scanner"],
98
101
  shortcut: { prefix: "HILO" },
102
+ headless: hiloHeadless,
99
103
  createInstance: () => ({ settings: { minPrice: "1", sort: "recent" } }),
100
104
  },
101
105
  {
@@ -0,0 +1,16 @@
1
+ import type {
2
+ DataProvider,
3
+ MarketDataRequestContext,
4
+ SecFilingItem,
5
+ } from "../../../types/data-provider";
6
+
7
+ export async function loadSecFilings(
8
+ provider: DataProvider,
9
+ symbol: string,
10
+ count: number,
11
+ exchange = "",
12
+ context?: MarketDataRequestContext,
13
+ ): Promise<SecFilingItem[]> {
14
+ if (!provider.getSecFilings) throw new Error("SEC filing data unavailable");
15
+ return provider.getSecFilings(symbol, count, exchange, context);
16
+ }
@@ -0,0 +1,72 @@
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 { SecFilingItem } from "../../../types/data-provider";
5
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
6
+ import { createSecHeadless } from "./headless";
7
+
8
+ const filings: SecFilingItem[] = [
9
+ {
10
+ accessionNumber: "0000320193-26-000100",
11
+ form: "8-K",
12
+ filingDate: new Date("2026-08-25T00:00:00.000Z"),
13
+ primaryDocDescription: "8-K Results of Operations",
14
+ items: "2.02,9.01",
15
+ cik: "0000320193",
16
+ filingUrl: "https://www.sec.gov/filing/one",
17
+ },
18
+ {
19
+ accessionNumber: "0000320193-26-000099",
20
+ form: "10-Q",
21
+ filingDate: new Date("2026-08-20T00:00:00.000Z"),
22
+ cik: "0000320193",
23
+ filingUrl: "https://www.sec.gov/filing/two",
24
+ },
25
+ ];
26
+
27
+ function context(): HeadlessPaneContext {
28
+ return {
29
+ marketData: createTestDataProvider(),
30
+ apiClient: {} as HeadlessPaneContext["apiClient"],
31
+ config: createDefaultConfig("/tmp/gloomberb-headless-sec"),
32
+ signal: new AbortController().signal,
33
+ };
34
+ }
35
+
36
+ function args(limit: number): HeadlessPaneLoadArgs {
37
+ return {
38
+ rawArgument: "AAPL",
39
+ argument: "AAPL",
40
+ symbols: ["AAPL"],
41
+ options: { limit },
42
+ };
43
+ }
44
+
45
+ describe("SEC headless model", () => {
46
+ test("projects filing rows and honors the page limit", async () => {
47
+ const requestedLimits: number[] = [];
48
+ const headless = createSecHeadless({
49
+ loadFilings: async (_symbol, limit) => {
50
+ requestedLimits.push(limit);
51
+ return filings;
52
+ },
53
+ });
54
+
55
+ const first = await headless.load(args(1), context());
56
+ expect(first.rows).toEqual([{
57
+ filedAt: "2026-08-25T00:00:00.000Z",
58
+ acceptedAt: null,
59
+ form: "8-K",
60
+ filing: "8-K | Results of Operations | Current Report",
61
+ items: "2.02,9.01",
62
+ accessionNumber: "0000320193-26-000100",
63
+ primaryDocument: null,
64
+ cik: "0000320193",
65
+ url: "https://www.sec.gov/filing/one",
66
+ }]);
67
+
68
+ const both = await headless.load(args(2), context());
69
+ expect(both.rows).toHaveLength(2);
70
+ expect(requestedLimits).toEqual([1, 2]);
71
+ });
72
+ });