gloomberb 0.13.1 → 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 (92) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/index.test.ts +34 -0
  3. package/src/api-client/request.ts +4 -1
  4. package/src/cli/pane-functions/cloud-session.test.ts +54 -0
  5. package/src/cli/pane-functions/cloud-session.ts +38 -0
  6. package/src/cli/pane-functions/index.ts +5 -1
  7. package/src/cli/pane-functions/screenshot.ts +2 -19
  8. package/src/components/command-bar/assist/runtime.ts +38 -30
  9. package/src/components/command-bar/surface/assist.test.tsx +43 -0
  10. package/src/news/types.ts +1 -0
  11. package/src/plugins/builtin/chat/controller/channels.ts +2 -1
  12. package/src/plugins/builtin/chat/controller/index.ts +13 -5
  13. package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
  14. package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
  15. package/src/plugins/builtin/chat/controller/state.ts +1 -0
  16. package/src/plugins/builtin/chat/controller.test.ts +260 -4
  17. package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
  18. package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
  19. package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
  20. package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
  21. package/src/plugins/builtin/cloud/askg/protocol.test.ts +3 -2
  22. package/src/plugins/builtin/cloud/askg/protocol.ts +16 -4
  23. package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
  24. package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
  25. package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
  26. package/src/plugins/builtin/cloud/plugin.tsx +2 -0
  27. package/src/plugins/builtin/congress-trades/client.ts +11 -0
  28. package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
  29. package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
  30. package/src/plugins/builtin/congress-trades/headless.ts +145 -0
  31. package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
  32. package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
  33. package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
  34. package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
  35. package/src/plugins/builtin/dividend-yield/model.ts +4 -22
  36. package/src/plugins/builtin/dividend-yield/view.ts +17 -0
  37. package/src/plugins/builtin/earnings/headless.test.ts +82 -0
  38. package/src/plugins/builtin/earnings/headless.ts +162 -0
  39. package/src/plugins/builtin/earnings/index.tsx +2 -0
  40. package/src/plugins/builtin/earnings-calls/data.ts +29 -8
  41. package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
  42. package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
  43. package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
  44. package/src/plugins/builtin/earnings-calls/model.ts +238 -0
  45. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +8 -9
  46. package/src/plugins/builtin/holders/client.ts +29 -0
  47. package/src/plugins/builtin/holders/headless.test.ts +58 -0
  48. package/src/plugins/builtin/holders/headless.ts +162 -0
  49. package/src/plugins/builtin/holders/index.tsx +14 -8
  50. package/src/plugins/builtin/holders/pane.tsx +7 -1
  51. package/src/plugins/builtin/insider/client.ts +50 -0
  52. package/src/plugins/builtin/insider/headless.test.ts +122 -0
  53. package/src/plugins/builtin/insider/headless.ts +117 -0
  54. package/src/plugins/builtin/insider/index.tsx +21 -50
  55. package/src/plugins/builtin/insider/insider-data.ts +1 -1
  56. package/src/plugins/builtin/insider/model.ts +72 -0
  57. package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
  58. package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
  59. package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
  60. package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
  61. package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
  62. package/src/plugins/builtin/news/headless.test.ts +138 -0
  63. package/src/plugins/builtin/news/headless.ts +190 -0
  64. package/src/plugins/builtin/news/index.tsx +13 -9
  65. package/src/plugins/builtin/news/wire/index.tsx +2 -1
  66. package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
  67. package/src/plugins/builtin/research/analyst-headless.ts +172 -0
  68. package/src/plugins/builtin/research/analyst-model.ts +219 -0
  69. package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
  70. package/src/plugins/builtin/research/client.ts +12 -0
  71. package/src/plugins/builtin/research/headless.test.ts +93 -0
  72. package/src/plugins/builtin/research/headless.ts +161 -0
  73. package/src/plugins/builtin/research/index.tsx +28 -18
  74. package/src/plugins/builtin/sec/client.ts +16 -0
  75. package/src/plugins/builtin/sec/headless.test.ts +72 -0
  76. package/src/plugins/builtin/sec/headless.ts +69 -0
  77. package/src/plugins/builtin/sec/index.tsx +20 -75
  78. package/src/plugins/builtin/sec/model.ts +89 -0
  79. package/src/plugins/builtin/short-interest/client.ts +5 -2
  80. package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
  81. package/src/plugins/builtin/short-interest/headless.ts +111 -0
  82. package/src/plugins/builtin/short-interest/index.tsx +15 -9
  83. package/src/plugins/builtin/thirteenf/data.ts +13 -11
  84. package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
  85. package/src/plugins/builtin/thirteenf/headless.ts +286 -0
  86. package/src/plugins/builtin/thirteenf/index.tsx +4 -0
  87. package/src/plugins/builtin/thirteenf/model.ts +13 -0
  88. package/src/plugins/builtin/thirteenf/table.ts +3 -15
  89. package/src/plugins/prediction-markets/headless.test.ts +119 -0
  90. package/src/plugins/prediction-markets/headless.ts +255 -0
  91. package/src/plugins/prediction-markets/index.tsx +2 -0
  92. package/src/sources/gloomberb-cloud/news.ts +1 -0
@@ -45,35 +45,37 @@ export async function loadBrowserRows(
45
45
  tab: ThirteenFBrowserTab,
46
46
  query: string,
47
47
  signal?: AbortSignal,
48
- options: { forceRefresh?: boolean; offset?: number } = {},
48
+ options: { forceRefresh?: boolean; offset?: number; limit?: number } = {},
49
49
  ): Promise<BrowserLoadResult> {
50
50
  const now = new Date();
51
51
  const from = dateYearsAgo(2, now);
52
52
  const to = todayIso(now);
53
53
  const quarter = latestLikely13FQuarter(now);
54
54
  const offset = Math.max(0, options.offset ?? 0);
55
+ const browserLimit = Math.max(1, options.limit ?? BROWSER_PAGE_LIMIT);
56
+ const latestLimit = Math.max(1, options.limit ?? LATEST_FILINGS_PAGE_LIMIT);
55
57
  const apiOptions = { forceRefresh: options.forceRefresh };
56
58
  const pageApiOptions = { forceRefresh: options.forceRefresh, offset };
57
59
 
58
60
  if (tab === "performance") {
59
- const topFunds = await listTopThirteenFFunds(quarter, BROWSER_PAGE_LIMIT, signal, pageApiOptions);
61
+ const topFunds = await listTopThirteenFFunds(quarter, browserLimit, signal, pageApiOptions);
60
62
  const funds: ThirteenFFund[] = topFunds.map((fund) => ({ cik: fund.cik, name: fund.name }));
61
- const forms = await loadLatestFormsForFunds(funds.slice(0, FORM_ENRICHMENT_LIMIT), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh });
63
+ const forms = await loadLatestFormsForFunds(funds.slice(0, Math.min(browserLimit, FORM_ENRICHMENT_LIMIT)), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh });
62
64
  return {
63
65
  rows: buildBrowserRows({ topFunds, forms, source: "performance" }),
64
66
  quarter,
65
67
  period: topFunds[0]?.periodOfReport,
66
- hasMore: topFunds.length >= BROWSER_PAGE_LIMIT,
68
+ hasMore: topFunds.length >= browserLimit,
67
69
  nextOffset: offset + topFunds.length,
68
70
  };
69
71
  }
70
72
 
71
73
  if (tab === "latest") {
72
- const filings = await listThirteenFFilings(recentIso(21, now), to, LATEST_FILINGS_PAGE_LIMIT, signal, pageApiOptions);
74
+ const filings = await listThirteenFFilings(recentIso(21, now), to, latestLimit, signal, pageApiOptions);
73
75
  return {
74
76
  rows: buildBrowserRows({ latestFilings: filings, source: "latest" }),
75
77
  period: filings[0]?.periodOfReport,
76
- hasMore: filings.length >= LATEST_FILINGS_PAGE_LIMIT,
78
+ hasMore: filings.length >= latestLimit,
77
79
  nextOffset: offset + filings.length,
78
80
  };
79
81
  }
@@ -97,7 +99,7 @@ export async function loadBrowserRows(
97
99
  try {
98
100
  const holders = await lookupThirteenFHoldersByCusip(cusip, periodOfReport, signal, apiOptions);
99
101
  const forms = new Map<string, ThirteenFFormSummary>();
100
- const pageCiks = holders.ciks.slice(offset, offset + BROWSER_PAGE_LIMIT);
102
+ const pageCiks = holders.ciks.slice(offset, offset + browserLimit);
101
103
  const funds = await Promise.all(pageCiks.map(async (cik): Promise<ThirteenFFund> => {
102
104
  const fundForms = await listThirteenFForms(cik, from, to, 1, signal, apiOptions);
103
105
  if (fundForms[0]) forms.set(cik, fundForms[0]);
@@ -136,10 +138,10 @@ export async function loadBrowserRows(
136
138
  nextOffset: offset + 1,
137
139
  };
138
140
  }
139
- const funds = await searchThirteenFFunds(trimmed, BROWSER_PAGE_LIMIT, signal, pageApiOptions);
141
+ const funds = await searchThirteenFFunds(trimmed, browserLimit, signal, pageApiOptions);
140
142
  const [forms, topFunds] = await Promise.all([
141
- loadLatestFormsForFunds(funds.slice(0, FORM_ENRICHMENT_LIMIT), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh }),
142
- listTopThirteenFFunds(quarter, BROWSER_PAGE_LIMIT, signal, { forceRefresh: options.forceRefresh }).catch(() => []),
143
+ loadLatestFormsForFunds(funds.slice(0, Math.min(browserLimit, FORM_ENRICHMENT_LIMIT)), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh }),
144
+ listTopThirteenFFunds(quarter, browserLimit, signal, { forceRefresh: options.forceRefresh }).catch(() => []),
143
145
  ]);
144
146
  const topByCik = new Map(topFunds.map((fund) => [fund.cik, fund]));
145
147
  const matchedTopFunds = funds
@@ -153,7 +155,7 @@ export async function loadBrowserRows(
153
155
  source: "funds",
154
156
  }),
155
157
  quarter,
156
- hasMore: funds.length >= BROWSER_PAGE_LIMIT,
158
+ hasMore: funds.length >= browserLimit,
157
159
  nextOffset: offset + funds.length,
158
160
  };
159
161
  }
@@ -0,0 +1,112 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createTestDataProvider } from "../../../test-support/data-provider";
3
+ import { createDefaultConfig } from "../../../types/config";
4
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
5
+ import { createThirteenFHeadless } from "./headless";
6
+ import type { FundDetailData } from "./types";
7
+
8
+ const detail: FundDetailData = {
9
+ cik: "0001067983",
10
+ name: "Berkshire Hathaway",
11
+ forms: [
12
+ {
13
+ url: "https://www.sec.gov/latest",
14
+ accessionNumber: "latest",
15
+ submissionType: "13F-HR",
16
+ periodOfReport: "2026-06-30",
17
+ filedAsOfDate: "2026-08-14",
18
+ cik: "0001067983",
19
+ companyName: "Berkshire Hathaway",
20
+ tableValueTotal: 300,
21
+ tableEntryTotal: 2,
22
+ isAmendment: false,
23
+ },
24
+ {
25
+ url: "https://www.sec.gov/previous",
26
+ accessionNumber: "previous",
27
+ submissionType: "13F-HR",
28
+ periodOfReport: "2026-03-31",
29
+ filedAsOfDate: "2026-05-15",
30
+ cik: "0001067983",
31
+ companyName: "Berkshire Hathaway",
32
+ tableValueTotal: 200,
33
+ tableEntryTotal: 1,
34
+ isAmendment: false,
35
+ },
36
+ ],
37
+ latestForm: null,
38
+ previousForm: null,
39
+ latestHoldings: [],
40
+ previousHoldings: [],
41
+ };
42
+ detail.latestForm = detail.forms[0]!;
43
+ detail.previousForm = detail.forms[1]!;
44
+ detail.latestHoldings = [
45
+ {
46
+ accessionNumber: "latest",
47
+ cik: detail.cik,
48
+ issuer: "Apple Inc",
49
+ titleOfClass: "COM",
50
+ cusip: "037833100",
51
+ ticker: "AAPL",
52
+ value: 300,
53
+ shares: 3,
54
+ shareType: "SH",
55
+ investmentDiscretion: "DFND",
56
+ votingAuthoritySole: 3,
57
+ votingAuthorityShared: 0,
58
+ votingAuthorityNone: 0,
59
+ putCall: "",
60
+ },
61
+ ];
62
+ detail.previousHoldings = [{ ...detail.latestHoldings[0]!, accessionNumber: "previous", value: 200, shares: 2 }];
63
+
64
+ function context(): HeadlessPaneContext {
65
+ return {
66
+ marketData: createTestDataProvider(),
67
+ apiClient: {} as HeadlessPaneContext["apiClient"],
68
+ config: createDefaultConfig("/tmp/gloomberb-headless-thirteenf"),
69
+ signal: new AbortController().signal,
70
+ };
71
+ }
72
+
73
+ function args(view: string, limit = 50): HeadlessPaneLoadArgs {
74
+ return {
75
+ rawArgument: "1067983",
76
+ argument: "1067983",
77
+ symbols: [],
78
+ options: { view, limit },
79
+ };
80
+ }
81
+
82
+ describe("13F headless model", () => {
83
+ test("projects browser rows and switches to fund holdings", async () => {
84
+ const headless = createThirteenFHeadless({
85
+ loadBrowser: async () => ({
86
+ rows: [
87
+ { id: "one", cik: detail.cik, name: detail.name, estQuarterReturn: 8.2, source: "performance" },
88
+ { id: "two", cik: "0000000002", name: "Second Fund", estQuarterReturn: 4.1, source: "performance" },
89
+ ],
90
+ quarter: "2026Q2",
91
+ }),
92
+ loadDetail: async () => detail,
93
+ });
94
+
95
+ const browser = await headless.load(args("performance", 1), context());
96
+ expect(browser.rows).toEqual([expect.objectContaining({
97
+ cik: detail.cik,
98
+ name: detail.name,
99
+ estQuarterReturn: 8.2,
100
+ })]);
101
+
102
+ const holdings = await headless.load(args("holdings", 5), context());
103
+ expect(holdings.rows).toEqual([expect.objectContaining({
104
+ ticker: "AAPL",
105
+ value: 300,
106
+ sharesChange: 1,
107
+ action: "add",
108
+ actionLabel: "Add",
109
+ })]);
110
+ expect(holdings.metadata).toMatchObject({ view: "holdings", fund: detail.name });
111
+ });
112
+ });
@@ -0,0 +1,286 @@
1
+ import type {
2
+ HeadlessPaneColumn,
3
+ HeadlessPaneContext,
4
+ HeadlessPaneDefinition,
5
+ HeadlessPaneLoadArgs,
6
+ } from "../../../types/plugin";
7
+ import { normalizeCik } from "./api";
8
+ import {
9
+ loadBrowserRows,
10
+ loadFundDetail,
11
+ type BrowserLoadResult,
12
+ } from "./data";
13
+ import {
14
+ DEFAULT_BROWSER_SORT,
15
+ DEFAULT_HOLDING_SORT,
16
+ DEFAULT_TIMELINE_SORT,
17
+ buildFundHoldingRows,
18
+ buildTimelineRows,
19
+ inferBrowserTabFromQuery,
20
+ isCikQuery,
21
+ positionType,
22
+ sortBrowserRows,
23
+ sortHoldingRows,
24
+ sortTimelineRows,
25
+ } from "./model";
26
+ import {
27
+ actionLabel,
28
+ formatChangeShares,
29
+ formatMoneyCompact,
30
+ formatPercentMaybe,
31
+ formatRawPercentMaybe,
32
+ formatShares,
33
+ formatShortDate,
34
+ } from "./format";
35
+ import type {
36
+ FundDetailData,
37
+ ThirteenFBrowserTab,
38
+ } from "./types";
39
+
40
+ const BROWSER_COLUMNS: HeadlessPaneColumn[] = [
41
+ { key: "name", header: "Fund" },
42
+ { key: "cik", header: "CIK" },
43
+ { key: "periodOfReport", header: "Period" },
44
+ {
45
+ key: "estQuarterReturn",
46
+ header: "Est 13F",
47
+ align: "right",
48
+ format: (value) => formatRawPercentMaybe(value == null ? null : Number(value)),
49
+ },
50
+ {
51
+ key: "tableValueTotal",
52
+ header: "Value",
53
+ align: "right",
54
+ format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
55
+ },
56
+ { key: "tableEntryTotal", header: "Rows", align: "right" },
57
+ {
58
+ key: "filedAsOfDate",
59
+ header: "Filed",
60
+ format: (value) => formatShortDate(typeof value === "string" ? value : null),
61
+ },
62
+ ];
63
+
64
+ const HOLDING_COLUMNS: HeadlessPaneColumn[] = [
65
+ { key: "ticker", header: "Ticker" },
66
+ { key: "type", header: "Type" },
67
+ { key: "issuer", header: "Issuer" },
68
+ {
69
+ key: "value",
70
+ header: "Value",
71
+ align: "right",
72
+ format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
73
+ },
74
+ {
75
+ key: "estimatedPnl",
76
+ header: "Est P&L",
77
+ align: "right",
78
+ format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
79
+ },
80
+ {
81
+ key: "weight",
82
+ header: "Weight",
83
+ align: "right",
84
+ format: (value) => formatPercentMaybe(value == null ? null : Number(value)),
85
+ },
86
+ {
87
+ key: "shares",
88
+ header: "Shares",
89
+ align: "right",
90
+ format: (value) => formatShares(value == null ? null : Number(value)),
91
+ },
92
+ {
93
+ key: "sharesChange",
94
+ header: "QoQ",
95
+ align: "right",
96
+ format: (value) => formatChangeShares(value == null ? null : Number(value)),
97
+ },
98
+ { key: "actionLabel", header: "Action" },
99
+ ];
100
+
101
+ const FILING_COLUMNS: HeadlessPaneColumn[] = [
102
+ { key: "periodOfReport", header: "Period" },
103
+ {
104
+ key: "filedAsOfDate",
105
+ header: "Filed",
106
+ format: (value) => formatShortDate(typeof value === "string" ? value : null),
107
+ },
108
+ {
109
+ key: "tableValueTotal",
110
+ header: "Value",
111
+ align: "right",
112
+ format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
113
+ },
114
+ { key: "tableEntryTotal", header: "Rows", align: "right" },
115
+ {
116
+ key: "valueChangePercent",
117
+ header: "Value%",
118
+ align: "right",
119
+ format: (value) => formatPercentMaybe(value == null ? null : Number(value)),
120
+ },
121
+ { key: "submissionType", header: "Form" },
122
+ ];
123
+
124
+ type HeadlessThirteenFView =
125
+ | "auto"
126
+ | "performance"
127
+ | "funds"
128
+ | "by-ticker"
129
+ | "latest"
130
+ | "holdings"
131
+ | "filings";
132
+
133
+ export interface ThirteenFHeadlessDependencies {
134
+ loadBrowser(
135
+ tab: ThirteenFBrowserTab,
136
+ query: string,
137
+ limit: number,
138
+ args: HeadlessPaneLoadArgs,
139
+ ctx: HeadlessPaneContext,
140
+ ): Promise<BrowserLoadResult>;
141
+ loadDetail(
142
+ cik: string,
143
+ name: string,
144
+ args: HeadlessPaneLoadArgs,
145
+ ctx: HeadlessPaneContext,
146
+ ): Promise<FundDetailData>;
147
+ }
148
+
149
+ const defaultDependencies: ThirteenFHeadlessDependencies = {
150
+ loadBrowser: (tab, query, limit, _args, ctx) => loadBrowserRows(
151
+ tab,
152
+ query,
153
+ ctx.signal,
154
+ { limit },
155
+ ),
156
+ loadDetail: (cik, name, _args, ctx) => loadFundDetail(cik, name, ctx.signal),
157
+ };
158
+
159
+ function browserTab(view: HeadlessThirteenFView, query: string): ThirteenFBrowserTab {
160
+ if (view === "performance" || view === "funds" || view === "latest") return view;
161
+ if (view === "by-ticker") return "byTicker";
162
+ return inferBrowserTabFromQuery(query);
163
+ }
164
+
165
+ async function resolveFund(
166
+ query: string,
167
+ limit: number,
168
+ args: HeadlessPaneLoadArgs,
169
+ ctx: HeadlessPaneContext,
170
+ dependencies: ThirteenFHeadlessDependencies,
171
+ ): Promise<{ cik: string; name: string }> {
172
+ if (!query) throw new Error("13F holdings and filings require a fund name or CIK.");
173
+ if (isCikQuery(query)) return { cik: normalizeCik(query), name: query };
174
+ const result = await dependencies.loadBrowser("funds", query, Math.min(limit, 25), args, ctx);
175
+ const exact = result.rows.find((row) => row.name.toLocaleLowerCase() === query.toLocaleLowerCase());
176
+ const fund = exact ?? result.rows[0];
177
+ if (!fund) throw new Error(`No 13F fund found for "${query}".`);
178
+ return { cik: fund.cik, name: fund.name };
179
+ }
180
+
181
+ export function createThirteenFHeadless(
182
+ dependencies: ThirteenFHeadlessDependencies = defaultDependencies,
183
+ ): HeadlessPaneDefinition<"rows"> {
184
+ return {
185
+ shape: "rows",
186
+ argument: {
187
+ kind: "free-text",
188
+ placeholder: "fund, ticker, or CIK",
189
+ description: "Optional fund name, ticker, CIK, or latest keyword.",
190
+ optional: true,
191
+ },
192
+ options: [
193
+ {
194
+ key: "view",
195
+ description: "Fund browser mode or detail tab.",
196
+ type: "enum",
197
+ values: [
198
+ { value: "auto" },
199
+ { value: "performance" },
200
+ { value: "funds" },
201
+ { value: "by-ticker", aliases: ["ticker", "byTicker"] },
202
+ { value: "latest" },
203
+ { value: "holdings" },
204
+ { value: "filings" },
205
+ ],
206
+ defaultValue: "auto",
207
+ },
208
+ {
209
+ key: "limit",
210
+ aliases: ["count", "rows"],
211
+ description: "Maximum rows.",
212
+ type: "integer",
213
+ defaultValue: 50,
214
+ minimum: 1,
215
+ maximum: 200,
216
+ },
217
+ ],
218
+ describe: (args) => `13F Funds | ${String(args.options.view)}`,
219
+ async load(args, ctx) {
220
+ const query = typeof args.argument === "string" ? args.argument.trim() : "";
221
+ const requestedView = String(args.options.view) as HeadlessThirteenFView;
222
+ const view = requestedView === "auto" && isCikQuery(query) ? "holdings" : requestedView;
223
+ const limit = Number(args.options.limit);
224
+
225
+ if (view === "holdings" || view === "filings") {
226
+ const fund = await resolveFund(query, limit, args, ctx, dependencies);
227
+ const detail = await dependencies.loadDetail(fund.cik, fund.name, args, ctx);
228
+ if (view === "filings") {
229
+ const rows = sortTimelineRows(buildTimelineRows(detail.forms), DEFAULT_TIMELINE_SORT)
230
+ .slice(0, limit)
231
+ .map((row) => ({ ...row }));
232
+ return {
233
+ columns: FILING_COLUMNS,
234
+ rows,
235
+ metadata: {
236
+ view,
237
+ cik: detail.cik,
238
+ fund: detail.name,
239
+ latestPeriod: detail.latestForm?.periodOfReport ?? null,
240
+ },
241
+ };
242
+ }
243
+ const rows = sortHoldingRows(
244
+ buildFundHoldingRows(detail).filter((row) => row.value != null),
245
+ DEFAULT_HOLDING_SORT,
246
+ )
247
+ .slice(0, limit)
248
+ .map((row) => ({
249
+ ...row,
250
+ type: positionType(row),
251
+ actionLabel: actionLabel(row.action),
252
+ }));
253
+ return {
254
+ columns: HOLDING_COLUMNS,
255
+ rows,
256
+ metadata: {
257
+ view,
258
+ cik: detail.cik,
259
+ fund: detail.name,
260
+ latestPeriod: detail.latestForm?.periodOfReport ?? null,
261
+ previousPeriod: detail.previousForm?.periodOfReport ?? null,
262
+ },
263
+ };
264
+ }
265
+
266
+ const tab = browserTab(view, query);
267
+ const result = await dependencies.loadBrowser(tab, query, limit, args, ctx);
268
+ return {
269
+ columns: BROWSER_COLUMNS,
270
+ rows: sortBrowserRows(result.rows, DEFAULT_BROWSER_SORT)
271
+ .slice(0, limit)
272
+ .map((row) => ({ ...row })),
273
+ ...(result.warning ? { errors: [result.warning] } : {}),
274
+ metadata: {
275
+ view: tab,
276
+ query: query || null,
277
+ period: result.period ?? null,
278
+ quarter: result.quarter ?? null,
279
+ hasMore: result.hasMore ?? false,
280
+ },
281
+ };
282
+ },
283
+ };
284
+ }
285
+
286
+ export const thirteenFHeadless = createThirteenFHeadless();
@@ -11,6 +11,9 @@ import {
11
11
  attachThirteenFApiPersistence,
12
12
  resetThirteenFApiPersistence,
13
13
  } from "./api";
14
+ import { thirteenFHeadless } from "./headless";
15
+
16
+ export { thirteenFHeadless } from "./headless";
14
17
 
15
18
  function queryFromOptions(options?: PaneTemplateCreateOptions): string {
16
19
  return (options?.arg ?? options?.values?.query ?? "").trim();
@@ -55,6 +58,7 @@ export const thirteenFModule: PluginModule = {
55
58
  argKind: "text",
56
59
  argOptional: true,
57
60
  },
61
+ headless: thirteenFHeadless,
58
62
  createInstance(_context: PaneTemplateContext, options?: PaneTemplateCreateOptions) {
59
63
  const query = queryFromOptions(options);
60
64
  const browserMode = inferBrowserTabFromQuery(query);
@@ -615,6 +615,19 @@ export function selectedIndexById<T extends { id: string }>(rows: T[], selectedI
615
615
  return index >= 0 ? index : rows.length > 0 ? 0 : -1;
616
616
  }
617
617
 
618
+ export function positionType(row: {
619
+ putCall: string;
620
+ titleOfClass: string;
621
+ shareType: string;
622
+ }): string {
623
+ const putCall = row.putCall.trim().toUpperCase();
624
+ if (putCall === "PUT" || putCall === "CALL") return putCall;
625
+ const title = row.titleOfClass.trim().toUpperCase();
626
+ if (title) return title;
627
+ const shareType = row.shareType.trim().toUpperCase();
628
+ return shareType || "--";
629
+ }
630
+
618
631
  export function isCikQuery(query: string): boolean {
619
632
  return CIK_RE.test(query.trim());
620
633
  }
@@ -12,6 +12,7 @@ import {
12
12
  formatShares,
13
13
  formatShortDate,
14
14
  } from "./format";
15
+ import { positionType } from "./model";
15
16
  import type {
16
17
  FilingPositionColumn,
17
18
  FilingPositionRow,
@@ -82,7 +83,7 @@ export function renderHoldingCell(
82
83
  attributes: TextAttributes.BOLD,
83
84
  };
84
85
  case "type":
85
- return { text: formatPositionType(row), color: selectedColor ?? colors.textDim };
86
+ return { text: positionType(row), color: selectedColor ?? colors.textDim };
86
87
  case "issuer":
87
88
  return { text: row.issuer, color: selectedColor ?? colors.text };
88
89
  case "value":
@@ -136,7 +137,7 @@ export function renderFilingPositionCell(
136
137
  attributes: TextAttributes.BOLD,
137
138
  };
138
139
  case "type":
139
- return { text: formatPositionType(row), color: selectedColor ?? colors.textDim };
140
+ return { text: positionType(row), color: selectedColor ?? colors.textDim };
140
141
  case "issuer":
141
142
  return { text: row.issuer, color: selectedColor ?? colors.text };
142
143
  case "value":
@@ -185,19 +186,6 @@ export function renderTimelineCell(
185
186
  }
186
187
  }
187
188
 
188
- function formatPositionType(row: {
189
- putCall: string;
190
- titleOfClass: string;
191
- shareType: string;
192
- }): string {
193
- const putCall = row.putCall.trim().toUpperCase();
194
- if (putCall === "PUT" || putCall === "CALL") return putCall;
195
- const title = row.titleOfClass.trim().toUpperCase();
196
- if (title) return title;
197
- const shareType = row.shareType.trim().toUpperCase();
198
- return shareType || "--";
199
- }
200
-
201
189
  function actionColor(action: FundHoldingRow["action"]): string {
202
190
  switch (action) {
203
191
  case "new":
@@ -0,0 +1,119 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../types/plugin";
3
+ import {
4
+ createPredictionMarketsHeadless,
5
+ type PredictionMarketsHeadlessDependencies,
6
+ } from "./headless";
7
+ import type { PredictionMarketSummary, PredictionVenue } from "./types";
8
+
9
+ function market(
10
+ key: string,
11
+ venue: PredictionVenue,
12
+ title: string,
13
+ volume24h: number,
14
+ endsAt: string,
15
+ ): PredictionMarketSummary {
16
+ return {
17
+ key,
18
+ venue,
19
+ marketId: `${key}-id`,
20
+ title,
21
+ marketLabel: "Yes",
22
+ eventLabel: title,
23
+ category: title.includes("Fed") ? "Economics" : "Politics",
24
+ tags: [],
25
+ status: "open",
26
+ url: `https://example.com/${key}`,
27
+ description: `${title} market`,
28
+ endsAt,
29
+ updatedAt: "2026-09-04T20:00:00.000Z",
30
+ yesPrice: 0.62,
31
+ noPrice: 0.38,
32
+ yesBid: 0.61,
33
+ yesAsk: 0.63,
34
+ noBid: 0.37,
35
+ noAsk: 0.39,
36
+ spread: 0.02,
37
+ lastTradePrice: 0.62,
38
+ volume24h,
39
+ volume24hUnit: "usd",
40
+ totalVolume: volume24h * 10,
41
+ totalVolumeUnit: "usd",
42
+ openInterest: volume24h / 2,
43
+ openInterestUnit: venue === "kalshi" ? "contracts" : "usd",
44
+ liquidity: volume24h / 4,
45
+ liquidityUnit: "usd",
46
+ };
47
+ }
48
+
49
+ const markets = [
50
+ market("poly-fed", "polymarket", "Will the Fed cut rates?", 2_000_000, "2026-10-01T00:00:00.000Z"),
51
+ market("kalshi-election", "kalshi", "Will the incumbent win?", 500_000, "2026-11-01T00:00:00.000Z"),
52
+ ];
53
+
54
+ function args(
55
+ argument: string | null,
56
+ options: Record<string, string | number | boolean>,
57
+ ): HeadlessPaneLoadArgs {
58
+ return {
59
+ rawArgument: argument ?? "",
60
+ argument,
61
+ symbols: [],
62
+ options,
63
+ };
64
+ }
65
+
66
+ const context = {} as HeadlessPaneContext;
67
+
68
+ function dependencies(seen: string[]): PredictionMarketsHeadlessDependencies {
69
+ return {
70
+ loadPolymarket: async (query) => {
71
+ seen.push(`polymarket:${query}`);
72
+ return markets.filter((entry) => entry.venue === "polymarket");
73
+ },
74
+ loadKalshi: async (query) => {
75
+ seen.push(`kalshi:${query}`);
76
+ return markets.filter((entry) => entry.venue === "kalshi");
77
+ },
78
+ now: () => new Date("2026-09-04T21:00:00.000Z"),
79
+ };
80
+ }
81
+
82
+ describe("prediction markets headless", () => {
83
+ test("projects venue data through the pane's shared row ordering", async () => {
84
+ const definition = createPredictionMarketsHeadless(dependencies([]));
85
+
86
+ const result = await definition.load(args(null, {
87
+ venue: "all",
88
+ category: "all",
89
+ tab: "top",
90
+ limit: 1,
91
+ }), context);
92
+
93
+ expect(result.rows).toEqual([
94
+ expect.objectContaining({
95
+ key: "poly-fed",
96
+ venue: "polymarket",
97
+ yesProbability: 0.62,
98
+ volume24h: 2_000_000,
99
+ }),
100
+ ]);
101
+ expect(result.metadata).toMatchObject({ total: 2, returned: 1, truncated: true });
102
+ });
103
+
104
+ test("uses the shortcut venue prefix without querying the other venue", async () => {
105
+ const seen: string[] = [];
106
+ const definition = createPredictionMarketsHeadless(dependencies(seen));
107
+
108
+ const result = await definition.load(args("polymarket:fed", {
109
+ venue: "all",
110
+ category: "macro",
111
+ tab: "ending",
112
+ limit: 20,
113
+ }), context);
114
+
115
+ expect(seen).toEqual(["polymarket:fed"]);
116
+ expect(result.rows.map((row) => row.key)).toEqual(["poly-fed"]);
117
+ expect(result.metadata).toMatchObject({ venue: "polymarket", query: "fed" });
118
+ });
119
+ });