gloomberb 0.13.1 → 0.13.3

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 (107) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/src/api-client/index.test.ts +34 -0
  4. package/src/api-client/request.ts +4 -1
  5. package/src/cli/desktop-pane-shot.ts +72 -2
  6. package/src/cli/pane-functions/cloud-session.test.ts +77 -0
  7. package/src/cli/pane-functions/cloud-session.ts +47 -0
  8. package/src/cli/pane-functions/index.ts +5 -1
  9. package/src/cli/pane-functions/screenshot.test.ts +27 -0
  10. package/src/cli/pane-functions/screenshot.ts +85 -20
  11. package/src/components/command-bar/assist/runtime.ts +38 -30
  12. package/src/components/command-bar/surface/assist.test.tsx +43 -0
  13. package/src/news/types.ts +1 -0
  14. package/src/plugins/builtin/chat/controller/channels.ts +2 -1
  15. package/src/plugins/builtin/chat/controller/index.ts +13 -5
  16. package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
  17. package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
  18. package/src/plugins/builtin/chat/controller/state.ts +1 -0
  19. package/src/plugins/builtin/chat/controller.test.ts +260 -4
  20. package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
  21. package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
  22. package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
  23. package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
  24. package/src/plugins/builtin/cloud/askg/protocol.test.ts +3 -2
  25. package/src/plugins/builtin/cloud/askg/protocol.ts +16 -4
  26. package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
  27. package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
  28. package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
  29. package/src/plugins/builtin/cloud/plugin.tsx +2 -0
  30. package/src/plugins/builtin/congress-trades/client.ts +11 -0
  31. package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
  32. package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
  33. package/src/plugins/builtin/congress-trades/headless.ts +145 -0
  34. package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
  35. package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
  36. package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
  37. package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
  38. package/src/plugins/builtin/dividend-yield/model.ts +4 -22
  39. package/src/plugins/builtin/dividend-yield/view.ts +17 -0
  40. package/src/plugins/builtin/earnings/headless.test.ts +82 -0
  41. package/src/plugins/builtin/earnings/headless.ts +162 -0
  42. package/src/plugins/builtin/earnings/index.tsx +35 -9
  43. package/src/plugins/builtin/earnings/model.test.ts +29 -1
  44. package/src/plugins/builtin/earnings-calls/data.ts +29 -8
  45. package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
  46. package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
  47. package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
  48. package/src/plugins/builtin/earnings-calls/model.ts +238 -0
  49. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +8 -9
  50. package/src/plugins/builtin/holders/client.ts +29 -0
  51. package/src/plugins/builtin/holders/headless.test.ts +58 -0
  52. package/src/plugins/builtin/holders/headless.ts +162 -0
  53. package/src/plugins/builtin/holders/index.tsx +14 -8
  54. package/src/plugins/builtin/holders/pane.tsx +7 -1
  55. package/src/plugins/builtin/insider/client.ts +50 -0
  56. package/src/plugins/builtin/insider/headless.test.ts +122 -0
  57. package/src/plugins/builtin/insider/headless.ts +117 -0
  58. package/src/plugins/builtin/insider/index.tsx +21 -50
  59. package/src/plugins/builtin/insider/insider-data.ts +1 -1
  60. package/src/plugins/builtin/insider/model.ts +72 -0
  61. package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
  62. package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
  63. package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
  64. package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
  65. package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
  66. package/src/plugins/builtin/news/headless.test.ts +138 -0
  67. package/src/plugins/builtin/news/headless.ts +190 -0
  68. package/src/plugins/builtin/news/index.tsx +13 -9
  69. package/src/plugins/builtin/news/wire/index.tsx +2 -1
  70. package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
  71. package/src/plugins/builtin/research/analyst-headless.ts +172 -0
  72. package/src/plugins/builtin/research/analyst-model.ts +219 -0
  73. package/src/plugins/builtin/research/analyst-pane.test.tsx +145 -0
  74. package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
  75. package/src/plugins/builtin/research/client.ts +12 -0
  76. package/src/plugins/builtin/research/corporate-actions-pane.tsx +27 -1
  77. package/src/plugins/builtin/research/event-model.ts +56 -0
  78. package/src/plugins/builtin/research/headless.test.ts +93 -0
  79. package/src/plugins/builtin/research/headless.ts +161 -0
  80. package/src/plugins/builtin/research/index.test.ts +43 -0
  81. package/src/plugins/builtin/research/index.tsx +28 -18
  82. package/src/plugins/builtin/sec/client.ts +16 -0
  83. package/src/plugins/builtin/sec/headless.test.ts +72 -0
  84. package/src/plugins/builtin/sec/headless.ts +69 -0
  85. package/src/plugins/builtin/sec/index.tsx +20 -75
  86. package/src/plugins/builtin/sec/model.ts +89 -0
  87. package/src/plugins/builtin/short-interest/client.ts +5 -2
  88. package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
  89. package/src/plugins/builtin/short-interest/headless.ts +111 -0
  90. package/src/plugins/builtin/short-interest/index.tsx +15 -9
  91. package/src/plugins/builtin/short-interest/model.ts +15 -4
  92. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  93. package/src/plugins/builtin/thirteenf/data.ts +13 -11
  94. package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
  95. package/src/plugins/builtin/thirteenf/headless.ts +286 -0
  96. package/src/plugins/builtin/thirteenf/index.tsx +4 -0
  97. package/src/plugins/builtin/thirteenf/model.ts +13 -0
  98. package/src/plugins/builtin/thirteenf/table.ts +3 -15
  99. package/src/plugins/prediction-markets/headless.test.ts +119 -0
  100. package/src/plugins/prediction-markets/headless.ts +255 -0
  101. package/src/plugins/prediction-markets/index.tsx +2 -0
  102. package/src/renderers/browser/config-host.test.ts +70 -0
  103. package/src/renderers/browser/config-host.ts +74 -10
  104. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +180 -19
  105. package/src/sources/gloomberb-cloud/news.ts +1 -0
  106. package/src/updater.test.ts +17 -1
  107. package/src/updater.ts +4 -2
@@ -0,0 +1,162 @@
1
+ import type { EarningsEvent } from "../../../types/data-provider";
2
+ import type {
3
+ HeadlessPaneContext,
4
+ HeadlessPaneDefinition,
5
+ HeadlessPaneLoadArgs,
6
+ HeadlessRowsResult,
7
+ } from "../../../types/plugin";
8
+ import {
9
+ loadEarningsCalendar,
10
+ type EarningsCalendarResult,
11
+ } from "./data/cache";
12
+
13
+ const EARNINGS_COLUMNS = [
14
+ { key: "date", header: "Date" },
15
+ { key: "timing", header: "When" },
16
+ { key: "dateStatus", header: "Status" },
17
+ { key: "symbol", header: "Ticker" },
18
+ { key: "name", header: "Name" },
19
+ { key: "epsEstimate", header: "EPS est", align: "right" as const },
20
+ { key: "epsLow", header: "EPS low", align: "right" as const },
21
+ { key: "epsHigh", header: "EPS high", align: "right" as const },
22
+ { key: "epsGrowth", header: "EPS growth", align: "right" as const },
23
+ { key: "epsTrend30dAgo", header: "EPS 30D ago", align: "right" as const },
24
+ { key: "epsRevisionUp30d", header: "Rev up", align: "right" as const },
25
+ { key: "epsRevisionDown30d", header: "Rev down", align: "right" as const },
26
+ { key: "revenueEstimate", header: "Sales est", align: "right" as const },
27
+ { key: "revenueLow", header: "Sales low", align: "right" as const },
28
+ { key: "revenueHigh", header: "Sales high", align: "right" as const },
29
+ { key: "revenueGrowth", header: "Sales growth", align: "right" as const },
30
+ { key: "epsAnalysts", header: "EPS analysts", align: "right" as const },
31
+ { key: "revenueAnalysts", header: "Sales analysts", align: "right" as const },
32
+ ];
33
+
34
+ export interface EarningsCalendarHeadlessDependencies {
35
+ loadCalendar(
36
+ symbols: string[],
37
+ context: HeadlessPaneContext,
38
+ ): Promise<EarningsCalendarResult>;
39
+ }
40
+
41
+ const defaultDependencies: EarningsCalendarHeadlessDependencies = {
42
+ loadCalendar: (symbols, context) => loadEarningsCalendar(context.marketData, symbols),
43
+ };
44
+
45
+ function timingMatches(event: EarningsEvent, timing: string): boolean {
46
+ if (timing === "all") return true;
47
+ if (timing === "unknown") return !event.timing;
48
+ return event.timing?.toUpperCase() === timing;
49
+ }
50
+
51
+ function eventRow(event: EarningsEvent) {
52
+ return {
53
+ date: event.earningsDate.toISOString(),
54
+ earningsCallDate: event.earningsCallDate?.toISOString() ?? null,
55
+ timing: event.timing || null,
56
+ dateStatus: event.isDateEstimate == null
57
+ ? null
58
+ : event.isDateEstimate
59
+ ? "estimated"
60
+ : "confirmed",
61
+ symbol: event.symbol,
62
+ name: event.name,
63
+ epsEstimate: event.epsEstimate ?? null,
64
+ epsLow: event.epsLow ?? null,
65
+ epsHigh: event.epsHigh ?? null,
66
+ epsYearAgo: event.epsYearAgo ?? null,
67
+ epsGrowth: event.epsGrowth ?? null,
68
+ epsAnalysts: event.epsAnalysts ?? null,
69
+ epsTrend7dAgo: event.epsTrend7dAgo ?? null,
70
+ epsTrend30dAgo: event.epsTrend30dAgo ?? null,
71
+ epsRevisionUp7d: event.epsRevisionUp7d ?? null,
72
+ epsRevisionUp30d: event.epsRevisionUp30d ?? null,
73
+ epsRevisionDown7d: event.epsRevisionDown7d ?? null,
74
+ epsRevisionDown30d: event.epsRevisionDown30d ?? null,
75
+ epsActual: event.epsActual ?? null,
76
+ revenueEstimate: event.revenueEstimate ?? null,
77
+ revenueLow: event.revenueLow ?? null,
78
+ revenueHigh: event.revenueHigh ?? null,
79
+ revenueYearAgo: event.revenueYearAgo ?? null,
80
+ revenueGrowth: event.revenueGrowth ?? null,
81
+ revenueAnalysts: event.revenueAnalysts ?? null,
82
+ revenueActual: event.revenueActual ?? null,
83
+ surprise: event.surprise ?? null,
84
+ };
85
+ }
86
+
87
+ export function projectEarningsCalendarHeadless(
88
+ result: EarningsCalendarResult,
89
+ args: HeadlessPaneLoadArgs,
90
+ ): HeadlessRowsResult {
91
+ const timing = String(args.options.timing ?? "all").toUpperCase();
92
+ const normalizedTiming = timing === "ALL" || timing === "UNKNOWN"
93
+ ? timing.toLowerCase()
94
+ : timing;
95
+ const limit = Number(args.options.limit ?? 50);
96
+ const matching = result.events
97
+ .filter((event) => timingMatches(event, normalizedTiming))
98
+ .sort((left, right) => left.earningsDate.getTime() - right.earningsDate.getTime());
99
+ const rows = matching.slice(0, limit).map(eventRow);
100
+
101
+ return {
102
+ columns: EARNINGS_COLUMNS,
103
+ rows,
104
+ errors: result.refreshError ? [result.refreshError] : undefined,
105
+ metadata: {
106
+ fetchedAt: result.fetchedAt,
107
+ stale: result.stale,
108
+ timing: normalizedTiming,
109
+ total: matching.length,
110
+ returned: rows.length,
111
+ truncated: rows.length < matching.length,
112
+ },
113
+ };
114
+ }
115
+
116
+ export function createEarningsCalendarHeadless(
117
+ dependencies: EarningsCalendarHeadlessDependencies = defaultDependencies,
118
+ ): HeadlessPaneDefinition<"rows"> {
119
+ return {
120
+ shape: "rows",
121
+ argument: {
122
+ kind: "symbol-list",
123
+ placeholder: "tickers",
124
+ description: "Comma-separated company symbols.",
125
+ minimum: 1,
126
+ maximum: 100,
127
+ },
128
+ options: [
129
+ {
130
+ key: "timing",
131
+ description: "Release timing to include.",
132
+ type: "enum",
133
+ values: [
134
+ { value: "all" },
135
+ { value: "BMO" },
136
+ { value: "AMC" },
137
+ { value: "TNS" },
138
+ { value: "unknown" },
139
+ ],
140
+ defaultValue: "all",
141
+ },
142
+ {
143
+ key: "limit",
144
+ description: "Maximum earnings rows to return.",
145
+ type: "integer",
146
+ defaultValue: 50,
147
+ minimum: 1,
148
+ maximum: 200,
149
+ },
150
+ ],
151
+ columns: EARNINGS_COLUMNS,
152
+ describe: (args) => `Earnings Calendar | ${args.symbols.join(", ")}`,
153
+ async load(args, context) {
154
+ return projectEarningsCalendarHeadless(
155
+ await dependencies.loadCalendar(args.symbols, context),
156
+ args,
157
+ );
158
+ },
159
+ };
160
+ }
161
+
162
+ export const earningsCalendarHeadless = createEarningsCalendarHeadless();
@@ -7,8 +7,11 @@ import { useAppSelector, usePaneInstance } from "../../../state/app/context";
7
7
  import { parseTickerListInput, formatTickerListInput } from "../../../tickers/list";
8
8
  import { useAssetData, usePluginPaneState, usePluginTickerActions } from "../../runtime";
9
9
  import { useAutoRefresh } from "../shared/auto-refresh";
10
- import type { PaneSettingsContext, PaneSettingsDef } from "../../../types/plugin";
11
- import { formatTickerListInput as formatTickers } from "../../../tickers/list";
10
+ import type {
11
+ PaneSettingsContext,
12
+ PaneSettingsDef,
13
+ PaneTemplateCreateOptions,
14
+ } from "../../../types/plugin";
12
15
  import {
13
16
  attachEarningsCalendarPersistence,
14
17
  loadEarningsCalendar,
@@ -23,6 +26,7 @@ import {
23
26
  type EarningsDisplayRow,
24
27
  type EarningsEventDisplayRow,
25
28
  } from "./model";
29
+ import { earningsCalendarHeadless } from "./headless";
26
30
  import {
27
31
  buildEarningsColumns,
28
32
  renderEarningsCell,
@@ -187,6 +191,18 @@ function EarningsCalendarPane({ focused, width, height }: PaneProps) {
187
191
  );
188
192
  }
189
193
 
194
+ /** Tickers named on the command bar or the CLI win over the active collection. */
195
+ function earningsScopeSymbols(options: PaneTemplateCreateOptions | undefined): string[] {
196
+ if (options?.symbols && options.symbols.length > 0) return options.symbols;
197
+ const raw = options?.arg?.trim() ?? "";
198
+ if (!raw) return [];
199
+ try {
200
+ return parseTickerListInput(raw);
201
+ } catch {
202
+ return [];
203
+ }
204
+ }
205
+
190
206
  /** The monitor's scope lives in pane settings, so it has to be editable there too. */
191
207
  function earningsSettings(context: PaneSettingsContext): PaneSettingsDef {
192
208
  const collections = [
@@ -196,7 +212,7 @@ function earningsSettings(context: PaneSettingsContext): PaneSettingsDef {
196
212
  const symbols = scopedSymbolsFromSettings(context.settings);
197
213
  return {
198
214
  title: "Earnings Scope",
199
- values: { symbolsText: symbols.length > 0 ? formatTickers(symbols) : "" },
215
+ values: { symbolsText: symbols.length > 0 ? formatTickerListInput(symbols) : "" },
200
216
  fields: [
201
217
  {
202
218
  key: "symbolsText",
@@ -265,12 +281,22 @@ export const earningsModule: PluginModule = {
265
281
  label: "Earnings Calendar",
266
282
  description: "Upcoming earnings dates and estimates for your tickers.",
267
283
  keywords: ["earn", "earnings", "calendar", "eps", "revenue", "quarterly"],
268
- shortcut: { prefix: "ERN" },
269
- createInstance: (context) => ({
270
- settings: context.activeCollectionId
271
- ? { collectionId: context.activeCollectionId }
272
- : undefined,
273
- }),
284
+ shortcut: { prefix: "ERN", argPlaceholder: "tickers", argKind: "ticker-list" },
285
+ headless: earningsCalendarHeadless,
286
+ // The shortcut takes tickers, so honor them the way the report does.
287
+ // Ignoring them left `ERN NKE` scoped to the active collection, which
288
+ // rendered "No tickers in scope" while the report listed NKE's earnings.
289
+ createInstance: (context, options) => {
290
+ const symbols = earningsScopeSymbols(options);
291
+ return {
292
+ title: symbols.length > 0 ? `ERN ${formatTickerListInput(symbols)}` : undefined,
293
+ settings: symbols.length > 0
294
+ ? { symbols, symbolsText: formatTickerListInput(symbols) }
295
+ : context.activeCollectionId
296
+ ? { collectionId: context.activeCollectionId }
297
+ : undefined,
298
+ };
299
+ },
274
300
  },
275
301
  {
276
302
  id: "earnings-monitor-pane",
@@ -1,6 +1,13 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { TickerRecord } from "../../../types/ticker";
3
- import { groupEarningsByRelativeDate, resolveEarningsCollectionId, trackedEarningsSymbols } from "./model";
3
+ import type { PaneTemplateContext } from "../../../types/plugin";
4
+ import { earningsModule } from "./index";
5
+ import {
6
+ groupEarningsByRelativeDate,
7
+ resolveEarningsCollectionId,
8
+ scopedSymbolsFromSettings,
9
+ trackedEarningsSymbols,
10
+ } from "./model";
4
11
 
5
12
  function ticker(
6
13
  symbol: string,
@@ -22,6 +29,27 @@ function ticker(
22
29
  };
23
30
  }
24
31
 
32
+ describe("ERN pane scope", () => {
33
+ /**
34
+ * The shortcut takes tickers, but the template used to drop them and follow
35
+ * the active collection instead, so `ERN NKE` opened a pane that reported no
36
+ * tickers in scope while the same argument produced a report.
37
+ */
38
+ test("scopes the pane to the tickers the shortcut was given", async () => {
39
+ const template = earningsModule.paneTemplates?.find(
40
+ (candidate) => candidate.id === "earnings-calendar-pane",
41
+ );
42
+ const context = { activeCollectionId: "main" } as unknown as PaneTemplateContext;
43
+
44
+ const scoped = await template?.createInstance?.(context, { arg: "nke, msft" });
45
+ expect(scopedSymbolsFromSettings(scoped?.settings)).toEqual(["NKE", "MSFT"]);
46
+
47
+ const unscoped = await template?.createInstance?.(context, undefined);
48
+ expect(scopedSymbolsFromSettings(unscoped?.settings)).toEqual([]);
49
+ expect(unscoped?.settings?.collectionId).toBe("main");
50
+ });
51
+ });
52
+
25
53
  describe("trackedEarningsSymbols", () => {
26
54
  test("gives settings-less legacy panes one stable collection scope", () => {
27
55
  expect(resolveEarningsCollectionId(undefined, "main")).toBe("main");
@@ -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
+ });