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,69 @@
1
+ import type {
2
+ HeadlessPaneColumn,
3
+ HeadlessPaneContext,
4
+ HeadlessPaneDefinition,
5
+ HeadlessPaneLoadArgs,
6
+ } from "../../../types/plugin";
7
+ import type { SecFilingItem } from "../../../types/data-provider";
8
+ import { loadSecFilings } from "./client";
9
+ import { buildSecFilingRows } from "./model";
10
+
11
+ const SEC_COLUMNS: HeadlessPaneColumn[] = [
12
+ {
13
+ key: "filedAt",
14
+ header: "Filed",
15
+ format: (value) => typeof value === "string" ? value.slice(0, 10) : "-",
16
+ },
17
+ { key: "form", header: "Form" },
18
+ { key: "filing", header: "Filing" },
19
+ { key: "items", header: "Items" },
20
+ { key: "accessionNumber", header: "Accession" },
21
+ ];
22
+
23
+ export interface SecHeadlessDependencies {
24
+ loadFilings(
25
+ symbol: string,
26
+ limit: number,
27
+ args: HeadlessPaneLoadArgs,
28
+ ctx: HeadlessPaneContext,
29
+ ): Promise<SecFilingItem[]>;
30
+ }
31
+
32
+ const defaultDependencies: SecHeadlessDependencies = {
33
+ loadFilings: (symbol, limit, _args, ctx) => loadSecFilings(ctx.marketData, symbol, limit),
34
+ };
35
+
36
+ export function createSecHeadless(
37
+ dependencies: SecHeadlessDependencies = defaultDependencies,
38
+ ): HeadlessPaneDefinition<"rows"> {
39
+ return {
40
+ shape: "rows",
41
+ argument: {
42
+ kind: "ticker",
43
+ placeholder: "ticker",
44
+ description: "US equity ticker.",
45
+ },
46
+ options: [{
47
+ key: "limit",
48
+ aliases: ["count", "rows"],
49
+ description: "Maximum recent filings.",
50
+ type: "integer",
51
+ defaultValue: 50,
52
+ minimum: 1,
53
+ maximum: 200,
54
+ }],
55
+ columns: SEC_COLUMNS,
56
+ describe: (args) => `SEC Filings | ${args.symbols[0]}`,
57
+ async load(args, ctx) {
58
+ const symbol = args.symbols[0]!;
59
+ const limit = Number(args.options.limit);
60
+ const filings = await dependencies.loadFilings(symbol, limit, args, ctx);
61
+ return {
62
+ rows: buildSecFilingRows(filings).slice(0, limit),
63
+ metadata: { symbol, returned: Math.min(filings.length, limit) },
64
+ };
65
+ },
66
+ };
67
+ }
68
+
69
+ export const secHeadless = createSecHeadless();
@@ -27,54 +27,19 @@ import {
27
27
  useSecFilingContentCache,
28
28
  } from "./filing-content";
29
29
  import { usePaneStatusLinkFooter } from "../shared/pane-footer";
30
+ import { secHeadless } from "./headless";
31
+ import {
32
+ getFilingDisplayTitle,
33
+ getFormDescription,
34
+ getMeaningfulPrimaryDescription,
35
+ } from "./model";
36
+
37
+ export { secHeadless } from "./headless";
30
38
 
31
39
  const SEC_FILING_FETCH_LIMIT = 20_000;
32
40
  const SEC_FILING_PAGE_SIZE = 50;
33
41
  const OWNERSHIP_FORMS = new Set(["3", "4", "5"]);
34
42
 
35
- function getDisplayFormLabel(form: string): string {
36
- const trimmed = form.trim();
37
- return /^\d+(?:\/[A-Z])?$/i.test(trimmed)
38
- ? `FORM ${trimmed}`
39
- : trimmed;
40
- }
41
-
42
- function normalizeComparableText(value: string): string {
43
- return value
44
- .toUpperCase()
45
- .replace(/\bFORM\b/g, "")
46
- .replace(/[^A-Z0-9]+/g, "");
47
- }
48
-
49
- function escapeRegExp(value: string): string {
50
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
51
- }
52
-
53
- function stripRedundantFormPrefix(form: string, description: string): string {
54
- const pattern = escapeRegExp(form.trim()).replace(/\s+/g, "\\s+");
55
- return description
56
- .trim()
57
- .replace(new RegExp(`^(?:FORM\\s+)?${pattern}(?:\\s*[:|-]\\s*|\\s+)`, "i"), "")
58
- .trim();
59
- }
60
-
61
- function getMeaningfulPrimaryDescription(filing: SecFilingItem): string | undefined {
62
- const description = filing.primaryDocDescription?.trim();
63
- if (!description) return undefined;
64
- if (normalizeComparableText(description) === normalizeComparableText(filing.form)) return undefined;
65
-
66
- const stripped = stripRedundantFormPrefix(filing.form, description);
67
- if (!stripped) return undefined;
68
- if (normalizeComparableText(stripped) === normalizeComparableText(filing.form)) return undefined;
69
- return stripped;
70
- }
71
-
72
- function getFilingDisplayTitle(filing: SecFilingItem): string {
73
- const description = getMeaningfulPrimaryDescription(filing);
74
- const formLabel = getDisplayFormLabel(filing.form);
75
- return description ? `${formLabel} | ${description}` : formLabel;
76
- }
77
-
78
43
  function formatFiledAt(filing: SecFilingItem): string {
79
44
  return formatFilingMetaDate(filing.filingDate);
80
45
  }
@@ -161,29 +126,6 @@ function buildForm4Detail(content: string | null, filing: SecFilingItem): string
161
126
  return lines.join("\n");
162
127
  }
163
128
 
164
- function getFormDescription(form: string): string {
165
- const f = form.trim().toUpperCase();
166
- switch (f) {
167
- case "10-K": return "Annual Report";
168
- case "10-K/A": return "Annual Report (Amended)";
169
- case "10-Q": return "Quarterly Report";
170
- case "10-Q/A": return "Quarterly Report (Amended)";
171
- case "8-K": return "Current Report";
172
- case "8-K/A": return "Current Report (Amended)";
173
- case "4": return "Insider Transaction";
174
- case "3": return "Initial Insider Ownership";
175
- case "5": return "Annual Insider Ownership";
176
- case "SC 13G": return "Beneficial Ownership (Passive)";
177
- case "SC 13G/A": return "Beneficial Ownership (Amended)";
178
- case "SC 13D": return "Beneficial Ownership (Active)";
179
- case "SC 13D/A": return "Beneficial Ownership (Amended)";
180
- case "DEF 14A": return "Proxy Statement";
181
- case "S-1": return "Registration Statement";
182
- case "20-F": return "Annual Report (Foreign)";
183
- default: return "";
184
- }
185
- }
186
-
187
129
  function toFeedItems(
188
130
  filings: SecFilingItem[],
189
131
  selectedAccessionNumber: string | undefined,
@@ -360,15 +302,18 @@ export const secModule: PluginModule = {
360
302
  ],
361
303
 
362
304
  paneTemplates: [
363
- createTickerSurfacePaneTemplate({
364
- id: "sec-pane",
365
- paneId: "sec",
366
- label: "SEC",
367
- description: "Recent SEC filings for the selected ticker.",
368
- keywords: ["sec", "filings", "10-k", "10-q", "8-k"],
369
- shortcut: "SEC",
370
- canCreate: (_context, options) => !options?.ticker || isUsEquityTicker(options.ticker),
371
- }),
305
+ {
306
+ ...createTickerSurfacePaneTemplate({
307
+ id: "sec-pane",
308
+ paneId: "sec",
309
+ label: "SEC",
310
+ description: "Recent SEC filings for the selected ticker.",
311
+ keywords: ["sec", "filings", "10-k", "10-q", "8-k"],
312
+ shortcut: "SEC",
313
+ canCreate: (_context, options) => !options?.ticker || isUsEquityTicker(options.ticker),
314
+ }),
315
+ headless: secHeadless,
316
+ },
372
317
  ],
373
318
 
374
319
  setup(ctx) {
@@ -0,0 +1,89 @@
1
+ import type { SecFilingItem } from "../../../types/data-provider";
2
+
3
+ export function getDisplayFormLabel(form: string): string {
4
+ const trimmed = form.trim();
5
+ return /^\d+(?:\/[A-Z])?$/i.test(trimmed)
6
+ ? `FORM ${trimmed}`
7
+ : trimmed;
8
+ }
9
+
10
+ function normalizeComparableText(value: string): string {
11
+ return value
12
+ .toUpperCase()
13
+ .replace(/\bFORM\b/g, "")
14
+ .replace(/[^A-Z0-9]+/g, "");
15
+ }
16
+
17
+ function escapeRegExp(value: string): string {
18
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ }
20
+
21
+ function stripRedundantFormPrefix(form: string, description: string): string {
22
+ const pattern = escapeRegExp(form.trim()).replace(/\s+/g, "\\s+");
23
+ return description
24
+ .trim()
25
+ .replace(new RegExp(`^(?:FORM\\s+)?${pattern}(?:\\s*[:|-]\\s*|\\s+)`, "i"), "")
26
+ .trim();
27
+ }
28
+
29
+ export function getMeaningfulPrimaryDescription(filing: SecFilingItem): string | undefined {
30
+ const description = filing.primaryDocDescription?.trim();
31
+ if (!description) return undefined;
32
+ if (normalizeComparableText(description) === normalizeComparableText(filing.form)) return undefined;
33
+
34
+ const stripped = stripRedundantFormPrefix(filing.form, description);
35
+ if (!stripped) return undefined;
36
+ if (normalizeComparableText(stripped) === normalizeComparableText(filing.form)) return undefined;
37
+ return stripped;
38
+ }
39
+
40
+ export function getFilingDisplayTitle(filing: SecFilingItem): string {
41
+ const description = getMeaningfulPrimaryDescription(filing);
42
+ const formLabel = getDisplayFormLabel(filing.form);
43
+ return description ? `${formLabel} | ${description}` : formLabel;
44
+ }
45
+
46
+ export function getFormDescription(form: string): string {
47
+ const normalized = form.trim().toUpperCase();
48
+ switch (normalized) {
49
+ case "10-K": return "Annual Report";
50
+ case "10-K/A": return "Annual Report (Amended)";
51
+ case "10-Q": return "Quarterly Report";
52
+ case "10-Q/A": return "Quarterly Report (Amended)";
53
+ case "8-K": return "Current Report";
54
+ case "8-K/A": return "Current Report (Amended)";
55
+ case "4": return "Insider Transaction";
56
+ case "3": return "Initial Insider Ownership";
57
+ case "5": return "Annual Insider Ownership";
58
+ case "SC 13G": return "Beneficial Ownership (Passive)";
59
+ case "SC 13G/A": return "Beneficial Ownership (Amended)";
60
+ case "SC 13D": return "Beneficial Ownership (Active)";
61
+ case "SC 13D/A": return "Beneficial Ownership (Amended)";
62
+ case "DEF 14A": return "Proxy Statement";
63
+ case "S-1": return "Registration Statement";
64
+ case "20-F": return "Annual Report (Foreign)";
65
+ default: return "";
66
+ }
67
+ }
68
+
69
+ export function buildSecFilingRows(filings: readonly SecFilingItem[]) {
70
+ return filings.map((filing) => {
71
+ const displayTitle = getFilingDisplayTitle(filing);
72
+ const formDescription = getFormDescription(filing.form);
73
+ return {
74
+ filedAt: filing.filingDate instanceof Date
75
+ ? filing.filingDate.toISOString()
76
+ : String(filing.filingDate),
77
+ acceptedAt: filing.acceptedAt instanceof Date
78
+ ? filing.acceptedAt.toISOString()
79
+ : filing.acceptedAt ? String(filing.acceptedAt) : null,
80
+ form: filing.form,
81
+ filing: formDescription ? `${displayTitle} | ${formDescription}` : displayTitle,
82
+ items: filing.items ?? null,
83
+ accessionNumber: filing.accessionNumber,
84
+ primaryDocument: filing.primaryDocument ?? null,
85
+ cik: filing.cik,
86
+ url: filing.filingUrl,
87
+ };
88
+ });
89
+ }
@@ -0,0 +1,54 @@
1
+ import type { DataProvider } from "../../../types/data-provider";
2
+ import type { PricePoint, Quote } from "../../../types/financials";
3
+ import type { SectorDef } from "./sector-data";
4
+ import {
5
+ computeTrailingReturn,
6
+ latestHistoryClose,
7
+ ONE_MONTH_DAYS,
8
+ ONE_YEAR_DAYS,
9
+ type SectorRow,
10
+ } from "./sector-model";
11
+
12
+ export interface SectorRowOutcome {
13
+ etf: string;
14
+ row: Partial<SectorRow> | null;
15
+ }
16
+
17
+ export async function loadSectorRows(
18
+ sectors: readonly SectorDef[],
19
+ provider: DataProvider,
20
+ ): Promise<SectorRowOutcome[]> {
21
+ const quotes = new Map<string, Quote | null>();
22
+ if (provider.getQuotesBatch) {
23
+ const results = await provider.getQuotesBatch(
24
+ sectors.map((sector) => ({ symbol: sector.etf, exchange: "" })),
25
+ ).catch(() => []);
26
+ for (const result of results) quotes.set(result.target.symbol, result.quote ?? null);
27
+ } else {
28
+ await Promise.all(sectors.map(async (sector) => {
29
+ try {
30
+ quotes.set(sector.etf, await provider.getQuote(sector.etf, ""));
31
+ } catch {
32
+ quotes.set(sector.etf, null);
33
+ }
34
+ }));
35
+ }
36
+
37
+ return Promise.all(sectors.map(async (sector) => {
38
+ const history: PricePoint[] = await provider.getPriceHistory(sector.etf, "", "1Y")
39
+ .catch(() => []);
40
+ const quote = quotes.get(sector.etf) ?? null;
41
+ if (!quote && history.length === 0) return { etf: sector.etf, row: null };
42
+ const price = quote?.price ?? latestHistoryClose(history);
43
+ return {
44
+ etf: sector.etf,
45
+ row: {
46
+ price,
47
+ changePercent: quote?.changePercent ?? null,
48
+ return1M: computeTrailingReturn(history, ONE_MONTH_DAYS, price),
49
+ return1Y: computeTrailingReturn(history, ONE_YEAR_DAYS, price),
50
+ currency: quote?.currency ?? "USD",
51
+ },
52
+ };
53
+ }));
54
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
3
+ import { createSectorsHeadless } from "./headless";
4
+
5
+ function args(collection: string): HeadlessPaneLoadArgs {
6
+ return { rawArgument: "", argument: null, symbols: [], options: { collection } };
7
+ }
8
+
9
+ describe("sectors headless model", () => {
10
+ test("changes the loaded ETF universe with the collection option", async () => {
11
+ const requested: string[][] = [];
12
+ const headless = createSectorsHeadless({
13
+ load: async (_args, definitions) => {
14
+ requested.push(definitions.map(({ etf }) => etf));
15
+ return definitions.map((definition, index) => ({
16
+ etf: definition.etf,
17
+ row: { price: 100, changePercent: index, return1M: index + 1, return1Y: index + 2, currency: "USD" },
18
+ }));
19
+ },
20
+ });
21
+
22
+ const sectors = await headless.load(args("sectors"), {} as HeadlessPaneContext);
23
+ const industries = await headless.load(args("industries"), {} as HeadlessPaneContext);
24
+ expect(sectors.rows.some((row) => row.etf === "XLK")).toBe(true);
25
+ expect(industries.rows.some((row) => row.etf === "SMH")).toBe(true);
26
+ expect(requested[0]).not.toEqual(requested[1]);
27
+ });
28
+ });
@@ -0,0 +1,80 @@
1
+ import type { DataProvider } from "../../../types/data-provider";
2
+ import type {
3
+ HeadlessPaneDefinition,
4
+ HeadlessPaneLoadArgs,
5
+ } from "../../../types/plugin";
6
+ import { formatCurrency, formatPercentRaw } from "../../../utils/format";
7
+ import { loadSectorRows, type SectorRowOutcome } from "./client";
8
+ import { getSectorCollection, type SectorCollectionId, type SectorDef } from "./sector-data";
9
+ import { DEFAULT_SORT_PREFERENCE, sortRows, type SectorRow } from "./sector-model";
10
+
11
+ const COLUMNS = [
12
+ { key: "name", header: "Sector" },
13
+ { key: "etf", header: "ETF" },
14
+ { key: "price", header: "Last", align: "right" as const, format: (value: unknown, row: Record<string, unknown>) => value == null ? "-" : formatCurrency(Number(value), String(row.currency)) },
15
+ { key: "changePercent", header: "1D", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
16
+ { key: "return1M", header: "1M", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
17
+ { key: "return1Y", header: "1Y", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
18
+ ];
19
+
20
+ export function projectSectorRows(
21
+ definitions: readonly SectorDef[],
22
+ outcomes: readonly SectorRowOutcome[],
23
+ ): SectorRow[] {
24
+ const byEtf = new Map(outcomes.map((outcome) => [outcome.etf, outcome.row]));
25
+ return sortRows(definitions.map((definition) => ({
26
+ ...definition,
27
+ price: byEtf.get(definition.etf)?.price ?? null,
28
+ changePercent: byEtf.get(definition.etf)?.changePercent ?? null,
29
+ return1M: byEtf.get(definition.etf)?.return1M ?? null,
30
+ return1Y: byEtf.get(definition.etf)?.return1Y ?? null,
31
+ currency: byEtf.get(definition.etf)?.currency ?? "USD",
32
+ loading: false,
33
+ })), DEFAULT_SORT_PREFERENCE);
34
+ }
35
+
36
+ export interface SectorsHeadlessDependencies {
37
+ load(
38
+ args: HeadlessPaneLoadArgs,
39
+ definitions: readonly SectorDef[],
40
+ provider: DataProvider,
41
+ ): Promise<SectorRowOutcome[]>;
42
+ }
43
+
44
+ const defaultDependencies: SectorsHeadlessDependencies = {
45
+ load: (_args, definitions, provider) => loadSectorRows(definitions, provider),
46
+ };
47
+
48
+ export function createSectorsHeadless(
49
+ dependencies: SectorsHeadlessDependencies = defaultDependencies,
50
+ ): HeadlessPaneDefinition<"rows"> {
51
+ return {
52
+ shape: "rows",
53
+ argument: { kind: "none" },
54
+ options: [{
55
+ key: "collection",
56
+ description: "Sector or industry ETF collection.",
57
+ type: "enum",
58
+ values: [{ value: "sectors" }, { value: "industries" }],
59
+ defaultValue: "sectors",
60
+ }],
61
+ columns: COLUMNS,
62
+ describe: (args) => `Sector Performance | ${String(args.options.collection)}`,
63
+ async load(args, ctx) {
64
+ const collectionId = args.options.collection as SectorCollectionId;
65
+ const definitions = getSectorCollection(collectionId).items;
66
+ const outcomes = await dependencies.load(args, definitions, ctx.marketData);
67
+ const rows = projectSectorRows(definitions, outcomes);
68
+ return {
69
+ rows: rows.map((row) => ({ ...row })),
70
+ metadata: {
71
+ collection: collectionId,
72
+ available: outcomes.filter((outcome) => outcome.row).length,
73
+ requested: definitions.length,
74
+ },
75
+ };
76
+ },
77
+ };
78
+ }
79
+
80
+ export const sectorsHeadless = createSectorsHeadless();
@@ -3,7 +3,6 @@ import { Box } from "../../../ui";
3
3
  import { DataTableView, Tabs, usePaneFooter, type DataTableCell, type DataTableKeyEvent, type PaneFooterSegment } from "../../../components";
4
4
  import type { PaneProps } from "../../../types/plugin";
5
5
  import type { PluginModule } from "../plugin-module";
6
- import type { PricePoint, Quote } from "../../../types/financials";
7
6
  import { usePaneSettingValue } from "../../../state/app/context";
8
7
  import { colors, priceColor } from "../../../theme/colors";
9
8
  import { formatCurrency, formatPercentRaw } from "../../../utils/format";
@@ -22,11 +21,7 @@ import {
22
21
  DEFAULT_SORT_PREFERENCE,
23
22
  INITIAL_REFRESH_BY_COLLECTION,
24
23
  INITIAL_ROWS_BY_COLLECTION,
25
- ONE_MONTH_DAYS,
26
- ONE_YEAR_DAYS,
27
24
  buildSectorColumns,
28
- computeTrailingReturn,
29
- latestHistoryClose,
30
25
  nextSortPreference,
31
26
  normalizeRowsForCollection,
32
27
  sortRows,
@@ -37,6 +32,10 @@ import {
37
32
  type SectorRowsByCollection,
38
33
  type SectorSortPreference,
39
34
  } from "./sector-model";
35
+ import { loadSectorRows } from "./client";
36
+ import { sectorsHeadless } from "./headless";
37
+
38
+ export { sectorsHeadless } from "./headless";
40
39
 
41
40
  /** Stable identity: a fresh literal here would refetch the board every render. */
42
41
  const NO_SAVED_ETFS: string[] = [];
@@ -105,52 +104,7 @@ function SectorPerformancePane({ focused, width, height }: PaneProps) {
105
104
  rows.map((row) => ({ ...row, loading: true }))
106
105
  )));
107
106
 
108
- const loadQuotes = async (): Promise<Map<string, Quote | null>> => {
109
- const quotes = new Map<string, Quote | null>();
110
- if (dataProvider.getQuotesBatch) {
111
- // A failed batch leaves every quote missing, which the row outcome below
112
- // reports as unavailable instead of stamping the pane as fresh.
113
- const results = await dataProvider.getQuotesBatch(
114
- sectorDefs.map((sector) => ({ symbol: sector.etf, exchange: "" })),
115
- ).catch(() => []);
116
- for (const result of results) {
117
- quotes.set(result.target.symbol, result.quote ?? null);
118
- }
119
- return quotes;
120
- }
121
-
122
- await Promise.all(sectorDefs.map(async (sector) => {
123
- try {
124
- quotes.set(sector.etf, await dataProvider.getQuote(sector.etf, ""));
125
- } catch {
126
- quotes.set(sector.etf, null);
127
- }
128
- }));
129
- return quotes;
130
- };
131
-
132
- const loadRows = async (): Promise<Array<{ etf: string; row: Partial<SectorRow> | null }>> => {
133
- const quotesByEtf = await loadQuotes();
134
- return Promise.all(sectorDefs.map(async (sector) => {
135
- const history: PricePoint[] = await dataProvider.getPriceHistory(sector.etf, "", "1Y")
136
- .catch(() => []);
137
- const quote = quotesByEtf.get(sector.etf) ?? null;
138
- if (!quote && history.length === 0) return { etf: sector.etf, row: null };
139
- const price = quote?.price ?? latestHistoryClose(history);
140
- return {
141
- etf: sector.etf,
142
- row: {
143
- price,
144
- changePercent: quote?.changePercent ?? null,
145
- return1M: computeTrailingReturn(history, ONE_MONTH_DAYS, price),
146
- return1Y: computeTrailingReturn(history, ONE_YEAR_DAYS, price),
147
- currency: quote?.currency ?? "USD",
148
- },
149
- };
150
- }));
151
- };
152
-
153
- loadRows().then((outcomes) => {
107
+ loadSectorRows(sectorDefs, dataProvider).then((outcomes) => {
154
108
  if (fetchGenRef.current !== gen) return;
155
109
  const loadedByEtf = new Map(outcomes.map((outcome) => [outcome.etf, outcome.row]));
156
110
  setRowsByCollection((prev) => updateRowsForCollection(prev, collectionId, sectorDefs, (rows) => (
@@ -365,6 +319,7 @@ export const sectorsModule: PluginModule = {
365
319
  description: "S&P 500 sector and industry performance sorted by daily change.",
366
320
  keywords: ["sector", "sectors", "industry", "semis", "defense", "food", "leisure", "etf", "xlk", "xlv", "xlf", "performance", "spdr"],
367
321
  shortcut: { prefix: "BI" },
322
+ headless: sectorsHeadless,
368
323
  },
369
324
  ],
370
325
  };
@@ -1,5 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo } from "react";
2
2
  import { apiClient } from "../../../api-client";
3
+ import { recordResearchActivity, researchUpgradeUrl } from "../../../api-client/research-activity";
4
+ import { getCurrentPluginTarget } from "../../current-target";
3
5
  import type { PaneFooterSegment } from "../../../components";
4
6
  import { tf } from "../../../i18n";
5
7
  import { useAppLanguage } from "../../../i18n/react";
@@ -25,10 +27,12 @@ let cloudUpgradeOpener: (() => void) | null = null;
25
27
  * already hold. Both URLs are account-bound, so no session handoff is needed.
26
28
  */
27
29
  export async function resolveCloudUpgradeUrl(): Promise<string> {
28
- if (!apiClient.isSignedIn()) return CLOUD_UPGRADE_URL;
30
+ recordResearchActivity("upgrade_intent");
31
+ const returnTo = getCurrentPluginTarget() === "web" ? window.location.href : undefined;
32
+ if (!apiClient.isSignedIn() || !apiClient.getCurrentUser()?.emailVerified) return researchUpgradeUrl(returnTo);
29
33
  const { url } = resolvePlanAccess(apiClient.getCurrentUser()).hasProAccess
30
34
  ? await apiClient.createBillingPortal()
31
- : await apiClient.createCloudCheckout();
35
+ : await apiClient.createCloudCheckout(returnTo);
32
36
  return url;
33
37
  }
34
38
 
@@ -142,8 +146,8 @@ export function useCloudAccessFooter({
142
146
  onPress: openUpgrade,
143
147
  parts: [{
144
148
  text: shortcutScope
145
- ? tf("{delay} delayed · u upgrade", { delay: delayLabel })
146
- : tf("{delay} delayed · upgrade", { delay: delayLabel }),
149
+ ? tf("{delay} delayed · u try Pro live", { delay: delayLabel })
150
+ : tf("{delay} delayed · try Pro live", { delay: delayLabel }),
147
151
  tone: "warning",
148
152
  }],
149
153
  };
@@ -110,9 +110,12 @@ function normalizeCloudRecords(payload: CloudShortInterestPayload): ShortInteres
110
110
  * Yahoo carries the current and prior settlement plus percent of float, so it
111
111
  * stays as the fallback when the cloud route is unavailable.
112
112
  */
113
- export async function fetchShortInterest(symbol: string): Promise<ShortInterestRecord[]> {
113
+ export async function fetchShortInterest(
114
+ symbol: string,
115
+ cloudClient: Pick<typeof apiClient, "getCloudShortInterest"> = apiClient,
116
+ ): Promise<ShortInterestRecord[]> {
114
117
  try {
115
- const response = await apiClient.getCloudShortInterest(symbol);
118
+ const response = await cloudClient.getCloudShortInterest(symbol);
116
119
  const records = response.status === "success" && response.data
117
120
  ? normalizeCloudRecords(response.data)
118
121
  : [];
@@ -0,0 +1,96 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CloudDataApi } from "../../../api-client/data";
3
+ import type { CloudMarketResponse, CloudShortInterestPayload } from "../../../api-client/types";
4
+ import { createTestDataProvider } from "../../../test-support/data-provider";
5
+ import { createDefaultConfig } from "../../../types/config";
6
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
7
+ import { fetchShortInterest } from "./client";
8
+ import { createShortInterestHeadless } from "./headless";
9
+
10
+ const records = [
11
+ {
12
+ settlementDate: new Date("2026-08-15T00:00:00.000Z"),
13
+ sharesShort: 12_000_000,
14
+ shortRatio: 2.5,
15
+ averageDailyVolume: 4_800_000,
16
+ shortPercentFloat: 3.2,
17
+ },
18
+ {
19
+ settlementDate: new Date("2026-08-31T00:00:00.000Z"),
20
+ sharesShort: 15_000_000,
21
+ shortRatio: 3,
22
+ averageDailyVolume: 5_000_000,
23
+ shortPercentFloat: 4.1,
24
+ },
25
+ ];
26
+
27
+ function context(): HeadlessPaneContext {
28
+ return {
29
+ marketData: createTestDataProvider(),
30
+ apiClient: {} as HeadlessPaneContext["apiClient"],
31
+ config: createDefaultConfig("/tmp/gloomberb-headless-short-interest"),
32
+ signal: new AbortController().signal,
33
+ };
34
+ }
35
+
36
+ function args(overrides: Partial<HeadlessPaneLoadArgs["options"]> = {}): HeadlessPaneLoadArgs {
37
+ return {
38
+ rawArgument: "AMD",
39
+ argument: "AMD",
40
+ symbols: ["AMD"],
41
+ options: { order: "newest", limit: 24, ...overrides },
42
+ };
43
+ }
44
+
45
+ describe("short interest client", () => {
46
+ test("uses the served market route for FINRA history", async () => {
47
+ const paths: string[] = [];
48
+ const dataApi = new CloudDataApi(async <T>(path: string): Promise<T> => {
49
+ paths.push(path);
50
+ return {
51
+ status: "success",
52
+ data: {
53
+ symbol: "AAPL",
54
+ issueName: "Apple Inc.",
55
+ points: [{
56
+ settlementDate: "2026-08-31",
57
+ sharesShort: 15_000_000,
58
+ previousSharesShort: 12_000_000,
59
+ averageDailyVolume: 5_000_000,
60
+ daysToCover: 3,
61
+ changePercent: 25,
62
+ revised: false,
63
+ }],
64
+ },
65
+ } as CloudMarketResponse<CloudShortInterestPayload> as T;
66
+ });
67
+
68
+ const result = await fetchShortInterest("aapl", dataApi);
69
+
70
+ expect(paths).toEqual(["/market/short-interest?symbol=AAPL"]);
71
+ expect(result[0]).toMatchObject({ sharesShort: 15_000_000, shortRatio: 3 });
72
+ });
73
+ });
74
+
75
+ describe("short interest headless model", () => {
76
+ test("projects raw settlement values and applies order and limit", async () => {
77
+ const headless = createShortInterestHeadless({
78
+ loadRecords: async () => records,
79
+ });
80
+
81
+ const newest = await headless.load(args({ limit: 1 }), context());
82
+ expect(newest.rows).toEqual([{
83
+ settlementDate: "2026-08-31",
84
+ sharesShort: 15_000_000,
85
+ daysToCover: 3,
86
+ averageDailyVolume: 5_000_000,
87
+ shortPercentFloat: 4.1,
88
+ }]);
89
+
90
+ const oldest = await headless.load(args({ order: "oldest", limit: 1 }), context());
91
+ expect(oldest.rows[0]).toMatchObject({
92
+ settlementDate: "2026-08-15",
93
+ sharesShort: 12_000_000,
94
+ });
95
+ });
96
+ });