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
@@ -4,7 +4,12 @@ import {
4
4
  type CloudEarningsTranscriptPayload,
5
5
  } from "../../../api-client";
6
6
  import { ApiRequestError } from "../../../api-client/errors";
7
- import type { PluginPersistence } from "../../../types/plugin";
7
+ import type { HeadlessPaneApiClient, PluginPersistence } from "../../../types/plugin";
8
+
9
+ type EarningsCallsApiClient = Pick<
10
+ HeadlessPaneApiClient,
11
+ "getCloudEarningsCalls" | "getCloudEarningsTranscript"
12
+ >;
8
13
 
9
14
  const LIST_KIND = "calls";
10
15
  const TRANSCRIPT_KIND = "transcript";
@@ -83,7 +88,8 @@ function listKey(ticker: string | null): string {
83
88
  return ticker ? ticker.toUpperCase() : "__all__";
84
89
  }
85
90
 
86
- export async function loadEarningsCalls(
91
+ export async function loadEarningsCallsWithClient(
92
+ client: EarningsCallsApiClient,
87
93
  ticker: string | null,
88
94
  options?: { force?: boolean; limit?: number },
89
95
  ): Promise<EarningsCallsResult> {
@@ -101,7 +107,7 @@ export async function loadEarningsCalls(
101
107
  const active = activeListFetches.get(key);
102
108
  if (active && !force) return active;
103
109
 
104
- const request = apiClient
110
+ const request = client
105
111
  .getCloudEarningsCalls({
106
112
  ticker: ticker ?? undefined,
107
113
  limit: options?.limit ?? 50,
@@ -155,6 +161,13 @@ export async function loadEarningsCalls(
155
161
  return request;
156
162
  }
157
163
 
164
+ export function loadEarningsCalls(
165
+ ticker: string | null,
166
+ options?: { force?: boolean; limit?: number },
167
+ ): Promise<EarningsCallsResult> {
168
+ return loadEarningsCallsWithClient(apiClient, ticker, options);
169
+ }
170
+
158
171
  /**
159
172
  * The server answers 202 with a pending marker when a call is known but not
160
173
  * transcribed yet, having just queued it. That is not a transcript and must
@@ -163,12 +176,13 @@ export async function loadEarningsCalls(
163
176
  export function isPendingTranscript(
164
177
  value: CloudEarningsTranscriptPayload | { status?: string } | null,
165
178
  ): boolean {
166
- if (!value) return false
167
- const record = value as { status?: string; turns?: unknown }
168
- return record.status === "pending" || !Array.isArray(record.turns)
179
+ if (!value) return false;
180
+ const record = value as { status?: string; turns?: unknown };
181
+ return record.status === "pending" || !Array.isArray(record.turns);
169
182
  }
170
183
 
171
- export async function loadTranscript(
184
+ export async function loadTranscriptWithClient(
185
+ client: EarningsCallsApiClient,
172
186
  callId: string,
173
187
  options?: { force?: boolean },
174
188
  ): Promise<CloudEarningsTranscriptPayload> {
@@ -184,7 +198,7 @@ export async function loadTranscript(
184
198
  const active = activeTranscriptFetches.get(callId);
185
199
  if (active && !force) return active;
186
200
 
187
- const request = apiClient
201
+ const request = client
188
202
  .getCloudEarningsTranscript(callId)
189
203
  .then((transcript) => {
190
204
  if (isPendingTranscript(transcript)) return transcript;
@@ -221,3 +235,10 @@ export async function loadTranscript(
221
235
  activeTranscriptFetches.set(callId, request);
222
236
  return request;
223
237
  }
238
+
239
+ export function loadTranscript(
240
+ callId: string,
241
+ options?: { force?: boolean },
242
+ ): Promise<CloudEarningsTranscriptPayload> {
243
+ return loadTranscriptWithClient(apiClient, callId, options);
244
+ }
@@ -0,0 +1,192 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type {
3
+ CloudEarningsCallPayload,
4
+ CloudEarningsTranscriptPayload,
5
+ } from "../../../api-client";
6
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
7
+ import {
8
+ createEarningsCallsHeadless,
9
+ createEarningsTranscriptHeadless,
10
+ type EarningsCallsHeadlessDependencies,
11
+ } from "./headless";
12
+
13
+ const calls: CloudEarningsCallPayload[] = [
14
+ {
15
+ id: "amd-fq2-2026",
16
+ ticker: "AMD",
17
+ companyName: "Advanced Micro Devices",
18
+ fiscalYear: 2026,
19
+ fiscalQuarter: 2,
20
+ callAt: "2026-07-29T21:00:00.000Z",
21
+ status: "published",
22
+ durationSeconds: 3_600,
23
+ wordCount: 12_000,
24
+ hasTranscript: true,
25
+ sentiment: 0.2,
26
+ },
27
+ {
28
+ id: "amd-fq1-2026",
29
+ ticker: "AMD",
30
+ companyName: "Advanced Micro Devices",
31
+ fiscalYear: 2026,
32
+ fiscalQuarter: 1,
33
+ callAt: "2026-04-29T21:00:00.000Z",
34
+ status: "discovered",
35
+ durationSeconds: null,
36
+ wordCount: null,
37
+ hasTranscript: false,
38
+ sentiment: null,
39
+ },
40
+ ];
41
+
42
+ const transcript: CloudEarningsTranscriptPayload = {
43
+ id: "amd-fq2-2026",
44
+ ticker: "AMD",
45
+ companyName: "Advanced Micro Devices",
46
+ fiscalYear: 2026,
47
+ fiscalQuarter: 2,
48
+ callAt: "2026-07-29T21:00:00.000Z",
49
+ timing: "AMC",
50
+ webcastUrl: "https://example.com/call",
51
+ durationSeconds: 3_600,
52
+ status: "published",
53
+ fullText: "",
54
+ turns: [
55
+ {
56
+ speaker: "Jean Hu",
57
+ role: "Chief Financial Officer",
58
+ company: "AMD",
59
+ text: `Gross margin expanded. ${"Margin discipline remained strong. ".repeat(80)}`,
60
+ isQa: false,
61
+ startSeconds: 120,
62
+ },
63
+ {
64
+ speaker: "Analyst One",
65
+ role: "Analyst",
66
+ company: "Research Firm",
67
+ text: "What should investors expect from the CFO on data center margins?",
68
+ isQa: true,
69
+ startSeconds: 2_400,
70
+ },
71
+ ],
72
+ participants: [
73
+ { name: "Jean Hu", role: "Chief Financial Officer", company: "AMD" },
74
+ ],
75
+ summary: "Revenue grew while gross margin expanded.",
76
+ guidance: "Management expects sequential growth.",
77
+ riskFactors: "Supply remains constrained.",
78
+ analystFocus: "Analysts focused on data center margins.",
79
+ notable: "Management raised its annual outlook.",
80
+ sentiment: 0.2,
81
+ sentimentRationale: "Constructive outlook.",
82
+ qaStartTurn: 1,
83
+ wordCount: 12_000,
84
+ asrModel: "test",
85
+ updatedAt: "2026-07-30T00:00:00.000Z",
86
+ };
87
+
88
+ function callArgs(options: Record<string, string | number | boolean>): HeadlessPaneLoadArgs {
89
+ return {
90
+ rawArgument: "",
91
+ argument: null,
92
+ symbols: [],
93
+ options,
94
+ };
95
+ }
96
+
97
+ function transcriptArgs(options: Record<string, string | number | boolean>): HeadlessPaneLoadArgs {
98
+ return {
99
+ rawArgument: "AMD",
100
+ argument: "AMD",
101
+ symbols: ["AMD"],
102
+ options,
103
+ };
104
+ }
105
+
106
+ function dependencies(): EarningsCallsHeadlessDependencies {
107
+ return {
108
+ loadCalls: async () => ({
109
+ calls,
110
+ fetchedAt: 123,
111
+ stale: false,
112
+ }),
113
+ loadTranscript: async () => transcript,
114
+ };
115
+ }
116
+
117
+ const context = {} as HeadlessPaneContext;
118
+
119
+ describe("earnings calls headless", () => {
120
+ test("maps call shelf rows and applies availability", async () => {
121
+ const definition = createEarningsCallsHeadless(dependencies());
122
+
123
+ const result = await definition.load(
124
+ callArgs({ availability: "pending", limit: 1 }),
125
+ context,
126
+ );
127
+
128
+ expect(result.rows).toEqual([
129
+ expect.objectContaining({
130
+ id: "amd-fq1-2026",
131
+ ticker: "AMD",
132
+ status: "queued",
133
+ hasTranscript: false,
134
+ }),
135
+ ]);
136
+ expect(result.metadata).toMatchObject({ availability: "pending", returned: 1 });
137
+ });
138
+ });
139
+
140
+ describe("earnings transcript headless", () => {
141
+ test("selects a quarter and filters speakers by role acronym", async () => {
142
+ const definition = createEarningsTranscriptHeadless(dependencies());
143
+
144
+ const result = await definition.load(transcriptArgs({
145
+ section: "transcript",
146
+ quarter: "FQ2-2026",
147
+ speaker: "CFO",
148
+ search: "margin",
149
+ offset: 0,
150
+ limit: 20,
151
+ }), context);
152
+
153
+ expect(result.rows.length).toBeGreaterThan(0);
154
+ expect(result.rows.every((row) => row.speaker === "Jean Hu")).toBe(true);
155
+ expect(result.metadata).toMatchObject({
156
+ fiscalYear: 2026,
157
+ fiscalQuarter: 2,
158
+ section: "transcript",
159
+ speaker: "CFO",
160
+ });
161
+ });
162
+
163
+ test("pages bounded transcript segments and reports truncation", async () => {
164
+ const definition = createEarningsTranscriptHeadless(dependencies());
165
+ const first = await definition.load(transcriptArgs({
166
+ section: "transcript",
167
+ quarter: "latest",
168
+ speaker: "CFO",
169
+ search: "",
170
+ offset: 0,
171
+ limit: 1,
172
+ }), context);
173
+ const second = await definition.load(transcriptArgs({
174
+ section: "transcript",
175
+ quarter: "latest",
176
+ speaker: "CFO",
177
+ search: "",
178
+ offset: 1,
179
+ limit: 1,
180
+ }), context);
181
+
182
+ expect(first.rows).toHaveLength(1);
183
+ expect(first.metadata).toMatchObject({
184
+ returnedSegments: 1,
185
+ truncated: true,
186
+ hasMore: true,
187
+ nextOffset: 1,
188
+ });
189
+ expect(second.rows).toHaveLength(1);
190
+ expect(second.rows[0]?.index).toBe(1);
191
+ });
192
+ });
@@ -0,0 +1,300 @@
1
+ import type {
2
+ CloudEarningsCallPayload,
3
+ CloudEarningsTranscriptPayload,
4
+ } from "../../../api-client";
5
+ import type {
6
+ HeadlessPaneContext,
7
+ HeadlessPaneDefinition,
8
+ HeadlessPaneLoadArgs,
9
+ HeadlessRowsResult,
10
+ } from "../../../types/plugin";
11
+ import {
12
+ callStatusLabel,
13
+ isPendingTranscript,
14
+ loadEarningsCallsWithClient,
15
+ loadTranscriptWithClient,
16
+ type EarningsCallsResult,
17
+ } from "./data";
18
+ import {
19
+ buildTranscriptSegments,
20
+ findCallForQuarter,
21
+ sortCallsNewest,
22
+ type TranscriptSection,
23
+ } from "./model";
24
+
25
+ const CALL_COLUMNS = [
26
+ { key: "ticker", header: "Ticker" },
27
+ { key: "company", header: "Company" },
28
+ { key: "callAt", header: "Call date" },
29
+ { key: "fiscalYear", header: "FY", align: "right" as const },
30
+ { key: "fiscalQuarter", header: "FQ", align: "right" as const },
31
+ { key: "status", header: "Status" },
32
+ { key: "durationSeconds", header: "Seconds", align: "right" as const },
33
+ { key: "wordCount", header: "Words", align: "right" as const },
34
+ { key: "sentiment", header: "Tone", align: "right" as const },
35
+ ];
36
+
37
+ const TRANSCRIPT_COLUMNS = [
38
+ { key: "index", header: "#", align: "right" as const },
39
+ { key: "section", header: "Section" },
40
+ { key: "startSeconds", header: "Start", align: "right" as const },
41
+ { key: "speaker", header: "Speaker" },
42
+ { key: "role", header: "Role" },
43
+ { key: "isQa", header: "Q&A" },
44
+ { key: "text", header: "Text" },
45
+ ];
46
+
47
+ export interface EarningsCallsHeadlessDependencies {
48
+ loadCalls(
49
+ ticker: string | null,
50
+ limit: number,
51
+ context: HeadlessPaneContext,
52
+ ): Promise<EarningsCallsResult>;
53
+ loadTranscript(
54
+ callId: string,
55
+ context: HeadlessPaneContext,
56
+ ): Promise<CloudEarningsTranscriptPayload>;
57
+ }
58
+
59
+ const defaultDependencies: EarningsCallsHeadlessDependencies = {
60
+ loadCalls: (ticker, limit, context) => loadEarningsCallsWithClient(
61
+ context.apiClient,
62
+ ticker,
63
+ { limit },
64
+ ),
65
+ loadTranscript: (callId, context) => loadTranscriptWithClient(context.apiClient, callId),
66
+ };
67
+
68
+ function callRow(call: CloudEarningsCallPayload) {
69
+ return {
70
+ id: call.id,
71
+ ticker: call.ticker,
72
+ company: call.companyName,
73
+ callAt: call.callAt,
74
+ fiscalYear: call.fiscalYear,
75
+ fiscalQuarter: call.fiscalQuarter,
76
+ status: call.hasTranscript ? "transcribed" : callStatusLabel(call) || call.status,
77
+ durationSeconds: call.durationSeconds,
78
+ wordCount: call.wordCount,
79
+ hasTranscript: call.hasTranscript,
80
+ sentiment: call.sentiment,
81
+ webcastUrl: call.webcastUrl ?? null,
82
+ };
83
+ }
84
+
85
+ function availabilityMatches(call: CloudEarningsCallPayload, availability: string): boolean {
86
+ if (availability === "all") return true;
87
+ return availability === "transcribed" ? call.hasTranscript : !call.hasTranscript;
88
+ }
89
+
90
+ export function projectEarningsCallsHeadless(
91
+ result: EarningsCallsResult,
92
+ args: HeadlessPaneLoadArgs,
93
+ ): HeadlessRowsResult {
94
+ const availability = String(args.options.availability ?? "all");
95
+ const limit = Number(args.options.limit ?? 50);
96
+ const matching = sortCallsNewest(result.calls)
97
+ .filter((call) => availabilityMatches(call, availability));
98
+ const rows = matching.slice(0, limit).map(callRow);
99
+
100
+ return {
101
+ columns: CALL_COLUMNS,
102
+ rows,
103
+ errors: result.refreshError ? [result.refreshError] : undefined,
104
+ metadata: {
105
+ fetchedAt: result.fetchedAt,
106
+ stale: result.stale,
107
+ pending: result.pending ?? false,
108
+ unknownTicker: result.unknownTicker ?? false,
109
+ availability,
110
+ total: matching.length,
111
+ returned: rows.length,
112
+ truncated: rows.length < matching.length,
113
+ },
114
+ };
115
+ }
116
+
117
+ function selectedSection(args: HeadlessPaneLoadArgs): TranscriptSection {
118
+ return String(args.options.section ?? "overview") as TranscriptSection;
119
+ }
120
+
121
+ function transcriptResult(
122
+ transcript: CloudEarningsTranscriptPayload,
123
+ call: CloudEarningsCallPayload,
124
+ args: HeadlessPaneLoadArgs,
125
+ ): HeadlessRowsResult {
126
+ const section = selectedSection(args);
127
+ const speaker = String(args.options.speaker ?? "").trim();
128
+ const search = String(args.options.search ?? "").trim();
129
+ const offset = Number(args.options.offset ?? 0);
130
+ const limit = Number(args.options.limit ?? 20);
131
+ const segments = buildTranscriptSegments(transcript, section, { speaker, search });
132
+ const rows = segments.slice(offset, offset + limit).map((segment) => ({ ...segment }));
133
+ const hasMore = offset + rows.length < segments.length;
134
+
135
+ return {
136
+ columns: TRANSCRIPT_COLUMNS,
137
+ rows,
138
+ metadata: {
139
+ callId: call.id,
140
+ ticker: transcript.ticker,
141
+ company: transcript.companyName,
142
+ fiscalYear: transcript.fiscalYear,
143
+ fiscalQuarter: transcript.fiscalQuarter,
144
+ callAt: transcript.callAt,
145
+ durationSeconds: transcript.durationSeconds,
146
+ wordCount: transcript.wordCount,
147
+ sentiment: transcript.sentiment,
148
+ updatedAt: transcript.updatedAt,
149
+ section,
150
+ speaker: speaker || null,
151
+ search: search || null,
152
+ offset,
153
+ limit,
154
+ totalSegments: segments.length,
155
+ returnedSegments: rows.length,
156
+ truncated: offset > 0 || hasMore,
157
+ hasMore,
158
+ nextOffset: hasMore ? offset + rows.length : null,
159
+ },
160
+ };
161
+ }
162
+
163
+ function transcriptOptions() {
164
+ return [
165
+ {
166
+ key: "section",
167
+ description: "Transcript section to return.",
168
+ type: "enum" as const,
169
+ values: [
170
+ { value: "overview" },
171
+ { value: "summary" },
172
+ { value: "transcript" },
173
+ { value: "qa", aliases: ["q&a"] },
174
+ { value: "guidance" },
175
+ { value: "risks" },
176
+ { value: "notable" },
177
+ { value: "analyst-focus", aliases: ["analyst"] },
178
+ { value: "participants", aliases: ["speakers"] },
179
+ ],
180
+ defaultValue: "overview",
181
+ },
182
+ {
183
+ key: "quarter",
184
+ description: "Fiscal quarter such as latest, FQ2-2026, or FY2026.",
185
+ type: "string" as const,
186
+ defaultValue: "latest",
187
+ },
188
+ {
189
+ key: "speaker",
190
+ description: "Speaker name, role, or role acronym such as CFO.",
191
+ type: "string" as const,
192
+ defaultValue: "",
193
+ },
194
+ {
195
+ key: "search",
196
+ description: "Text to find in transcript turns.",
197
+ type: "string" as const,
198
+ defaultValue: "",
199
+ },
200
+ {
201
+ key: "offset",
202
+ description: "Transcript segment offset for paging.",
203
+ type: "integer" as const,
204
+ defaultValue: 0,
205
+ minimum: 0,
206
+ maximum: 100_000,
207
+ },
208
+ {
209
+ key: "limit",
210
+ description: "Maximum transcript segments to return.",
211
+ type: "integer" as const,
212
+ defaultValue: 20,
213
+ minimum: 1,
214
+ maximum: 200,
215
+ },
216
+ ];
217
+ }
218
+
219
+ export function createEarningsCallsHeadless(
220
+ dependencies: EarningsCallsHeadlessDependencies = defaultDependencies,
221
+ ): HeadlessPaneDefinition<"rows"> {
222
+ return {
223
+ shape: "rows",
224
+ argument: {
225
+ kind: "ticker",
226
+ placeholder: "ticker",
227
+ description: "Optional company symbol. Omit it to browse all calls.",
228
+ optional: true,
229
+ },
230
+ options: [
231
+ {
232
+ key: "availability",
233
+ description: "Transcript availability to include.",
234
+ type: "enum",
235
+ values: [
236
+ { value: "all" },
237
+ { value: "transcribed" },
238
+ { value: "pending" },
239
+ ],
240
+ defaultValue: "all",
241
+ },
242
+ {
243
+ key: "limit",
244
+ description: "Maximum calls to return.",
245
+ type: "integer",
246
+ defaultValue: 50,
247
+ minimum: 1,
248
+ maximum: 200,
249
+ },
250
+ ],
251
+ columns: CALL_COLUMNS,
252
+ describe: (args) => args.argument
253
+ ? `Earnings Calls | ${String(args.argument)}`
254
+ : "Earnings Calls",
255
+ async load(args, context) {
256
+ const ticker = typeof args.argument === "string" ? args.argument : null;
257
+ return projectEarningsCallsHeadless(
258
+ await dependencies.loadCalls(ticker, 200, context),
259
+ args,
260
+ );
261
+ },
262
+ };
263
+ }
264
+
265
+ export function createEarningsTranscriptHeadless(
266
+ dependencies: EarningsCallsHeadlessDependencies = defaultDependencies,
267
+ ): HeadlessPaneDefinition<"rows"> {
268
+ return {
269
+ shape: "rows",
270
+ argument: {
271
+ kind: "ticker",
272
+ placeholder: "ticker",
273
+ description: "Company symbol.",
274
+ },
275
+ options: transcriptOptions(),
276
+ columns: TRANSCRIPT_COLUMNS,
277
+ describe: (args) => (
278
+ `Earnings Call Transcript | ${String(args.argument)} | ${String(args.options.quarter)}`
279
+ ),
280
+ async load(args, context) {
281
+ const ticker = String(args.argument);
282
+ const calls = await dependencies.loadCalls(ticker, 200, context);
283
+ const quarter = String(args.options.quarter ?? "latest");
284
+ const selected = findCallForQuarter(calls.calls, quarter);
285
+ if (!selected) {
286
+ if (calls.unknownTicker) throw new Error(`${ticker} is not a known listed company.`);
287
+ if (calls.pending) throw new Error(`Earnings call discovery for ${ticker} is still pending.`);
288
+ throw new Error(`No ${quarter} earnings call found for ${ticker}.`);
289
+ }
290
+ const transcript = await dependencies.loadTranscript(selected.id, context);
291
+ if (isPendingTranscript(transcript)) {
292
+ throw new Error(`Transcript for ${ticker} ${quarter} is still being produced.`);
293
+ }
294
+ return transcriptResult(transcript, selected, args);
295
+ },
296
+ };
297
+ }
298
+
299
+ export const earningsCallsHeadless = createEarningsCallsHeadless();
300
+ export const earningsTranscriptHeadless = createEarningsTranscriptHeadless();
@@ -2,6 +2,12 @@ import type { PluginModule } from "../plugin-module";
2
2
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
3
3
  import { attachEarningsCallsPersistence, resetEarningsCallsPersistence } from "./data";
4
4
  import { EarningsCallsPane, EARNINGS_CALLS_PANE_ID } from "./pane";
5
+ import {
6
+ earningsCallsHeadless,
7
+ earningsTranscriptHeadless,
8
+ } from "./headless";
9
+
10
+ export { earningsCallsHeadless, earningsTranscriptHeadless } from "./headless";
5
11
 
6
12
  const description =
7
13
  "Earnings call transcripts with speaker attribution, analyst Q&A, and extracted guidance.";
@@ -54,16 +60,20 @@ export const earningsCallsModule: PluginModule = {
54
60
  "qa",
55
61
  ],
56
62
  shortcut: { prefix: "CALLS" },
63
+ headless: earningsCallsHeadless,
57
64
  createInstance: () => ({ placement: "floating" }),
58
65
  },
59
- createTickerSurfacePaneTemplate({
60
- id: "earnings-call-transcripts-pane",
61
- paneId: EARNINGS_CALLS_PANE_ID,
62
- label: "Earnings Call Transcripts",
63
- description,
64
- keywords: ["earnings", "call", "transcript", "ect", "qa", "guidance"],
65
- shortcut: "ECT",
66
- publicShare: false,
67
- }),
66
+ {
67
+ ...createTickerSurfacePaneTemplate({
68
+ id: "earnings-call-transcripts-pane",
69
+ paneId: EARNINGS_CALLS_PANE_ID,
70
+ label: "Earnings Call Transcripts",
71
+ description,
72
+ keywords: ["earnings", "call", "transcript", "ect", "qa", "guidance"],
73
+ shortcut: "ECT",
74
+ publicShare: false,
75
+ }),
76
+ headless: earningsTranscriptHeadless,
77
+ },
68
78
  ],
69
79
  };