gloomberb 0.8.1 → 0.9.0

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 (196) hide show
  1. package/README.md +106 -54
  2. package/package.json +14 -13
  3. package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
  4. package/src/api-client/chat.ts +8 -0
  5. package/src/api-client/index.test.ts +30 -0
  6. package/src/api-client/index.ts +4 -0
  7. package/src/api-client/normalizers.ts +1 -0
  8. package/src/api-client/types.ts +1 -0
  9. package/src/app/global-shortcuts.test.tsx +54 -4
  10. package/src/app/global-shortcuts.ts +11 -5
  11. package/src/capabilities/factories.ts +4 -0
  12. package/src/components/chart/comparison/data.test.ts +54 -0
  13. package/src/components/chart/comparison/data.ts +47 -24
  14. package/src/components/chart/core/projection.ts +51 -27
  15. package/src/components/command-bar/action-runtime.ts +1 -0
  16. package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
  17. package/src/components/command-bar/layout-items/current-layout.ts +33 -0
  18. package/src/components/command-bar/list/view.tsx +3 -1
  19. package/src/components/command-bar/pane-templates/items.ts +4 -1
  20. package/src/components/command-bar/panel/body.tsx +1 -0
  21. package/src/components/command-bar/routes/root/results.ts +0 -30
  22. package/src/components/command-bar/routes/root/runtime.ts +2 -20
  23. package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
  24. package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
  25. package/src/components/command-bar/routing/launch-request.ts +14 -7
  26. package/src/components/command-bar/routing/list-state.ts +1 -3
  27. package/src/components/command-bar/surface/index.test.tsx +76 -4
  28. package/src/components/command-bar/surface/index.tsx +0 -2
  29. package/src/components/command-bar/surface/pane.test.tsx +1 -0
  30. package/src/components/command-bar/surface/test-harness.tsx +1 -0
  31. package/src/components/command-bar/view-model.ts +3 -3
  32. package/src/components/data-table/stack-view.test.tsx +14 -11
  33. package/src/components/data-table/stack-view.tsx +1 -2
  34. package/src/components/data-table/view.test.tsx +24 -18
  35. package/src/components/data-table/view.tsx +313 -83
  36. package/src/components/feed-data-table/stack-view.tsx +6 -8
  37. package/src/components/layout/pane/footer/index.test.tsx +6 -0
  38. package/src/components/layout/pane/footer/index.tsx +14 -2
  39. package/src/components/layout/pane/title.ts +5 -1
  40. package/src/components/layout/shell/index.test.tsx +120 -2
  41. package/src/components/layout/shell/index.tsx +2 -0
  42. package/src/components/layout/shell/menu.ts +1 -7
  43. package/src/components/layout/shell/pane/actions.ts +8 -0
  44. package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
  45. package/src/components/layout/shell/shortcuts.ts +14 -6
  46. package/src/components/layout/shell/window-mode/index.ts +8 -3
  47. package/src/components/layout/window-edit/presentation.ts +4 -4
  48. package/src/components/onboarding/onboarding-steps.tsx +3 -2
  49. package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
  50. package/src/components/pane-settings-dialog/tui.tsx +1 -1
  51. package/src/components/ticker/list-table-view.tsx +17 -25
  52. package/src/components/ui/choice-dialog.tsx +12 -2
  53. package/src/components/ui/multi-select/dialog.tsx +17 -5
  54. package/src/components/ui/remote-image.tsx +1 -14
  55. package/src/components/ui/shortcut-hint.tsx +14 -2
  56. package/src/components/ui/table-layout.test.ts +2 -2
  57. package/src/components/ui/table-layout.ts +8 -2
  58. package/src/components/ui/tabs.tsx +115 -86
  59. package/src/components/ui/ui.test.tsx +83 -4
  60. package/src/core/state/app/types.ts +12 -4
  61. package/src/market-data/coordinator/auxiliary.ts +29 -1
  62. package/src/market-data/coordinator/index.ts +17 -1
  63. package/src/market-data/hooks.tsx +17 -1
  64. package/src/market-data/selectors.ts +4 -0
  65. package/src/plugins/builtin/account-management/form-components.tsx +0 -4
  66. package/src/plugins/builtin/account-management/model.test.ts +54 -0
  67. package/src/plugins/builtin/account-management/model.ts +13 -3
  68. package/src/plugins/builtin/account-management/pane.tsx +5 -11
  69. package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
  70. package/src/plugins/builtin/alerts/pane.tsx +5 -3
  71. package/src/plugins/builtin/analytics/index.tsx +0 -3
  72. package/src/plugins/builtin/analytics/view.tsx +6 -5
  73. package/src/plugins/builtin/broker-manager/index.tsx +6 -6
  74. package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
  75. package/src/plugins/builtin/changelog/index.tsx +40 -27
  76. package/src/plugins/builtin/changelog/model.test.ts +77 -0
  77. package/src/plugins/builtin/changelog/model.ts +98 -0
  78. package/src/plugins/builtin/chat/channel-labels.ts +53 -0
  79. package/src/plugins/builtin/chat/channels.test.ts +28 -0
  80. package/src/plugins/builtin/chat/channels.ts +36 -19
  81. package/src/plugins/builtin/chat/chat.test.tsx +264 -5
  82. package/src/plugins/builtin/chat/content/channel-navigation.ts +50 -11
  83. package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
  84. package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
  85. package/src/plugins/builtin/chat/content/composer.tsx +23 -2
  86. package/src/plugins/builtin/chat/content/index.tsx +66 -4
  87. package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
  88. package/src/plugins/builtin/chat/content/shortcuts.ts +27 -1
  89. package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
  90. package/src/plugins/builtin/chat/content/types.ts +1 -0
  91. package/src/plugins/builtin/chat/controller/index.ts +41 -0
  92. package/src/plugins/builtin/chat/controller/messages.ts +18 -0
  93. package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
  94. package/src/plugins/builtin/chat/controller/utils.ts +6 -3
  95. package/src/plugins/builtin/chat/controller/view.ts +1 -1
  96. package/src/plugins/builtin/chat/controller.test.ts +189 -0
  97. package/src/plugins/builtin/chat/edit-window.ts +24 -0
  98. package/src/plugins/builtin/chat/layout.ts +94 -31
  99. package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
  100. package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
  101. package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
  102. package/src/plugins/builtin/chat/message/render-state.ts +5 -1
  103. package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
  104. package/src/plugins/builtin/chat/message/types.ts +2 -0
  105. package/src/plugins/builtin/chat/pane.tsx +35 -7
  106. package/src/plugins/builtin/chat/sidebar.test.tsx +72 -0
  107. package/src/plugins/builtin/chat/status-widget.tsx +29 -11
  108. package/src/plugins/builtin/chat/test-harness.tsx +2 -0
  109. package/src/plugins/builtin/cloud/plugin.tsx +3 -0
  110. package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
  111. package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
  112. package/src/plugins/builtin/earnings/index.tsx +8 -7
  113. package/src/plugins/builtin/econ/index.tsx +6 -8
  114. package/src/plugins/builtin/help/components.tsx +1 -1
  115. package/src/plugins/builtin/help/index.test.tsx +105 -30
  116. package/src/plugins/builtin/help/index.tsx +326 -164
  117. package/src/plugins/builtin/help/shortcut-model.ts +5 -6
  118. package/src/plugins/builtin/holders/pane.tsx +7 -6
  119. package/src/plugins/builtin/market-movers/index.tsx +6 -3
  120. package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
  121. package/src/plugins/builtin/options/view.tsx +16 -8
  122. package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
  123. package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
  124. package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
  125. package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
  126. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
  127. package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
  128. package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
  129. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
  130. package/src/plugins/builtin/research/analyst-pane.tsx +1 -2
  131. package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
  132. package/src/plugins/builtin/research/index.test.ts +30 -1
  133. package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
  134. package/src/plugins/builtin/sec/filing-documents.ts +66 -0
  135. package/src/plugins/builtin/sec/index.tsx +132 -20
  136. package/src/plugins/builtin/sectors/index.tsx +13 -23
  137. package/src/plugins/builtin/shared/ticker-request.ts +1 -0
  138. package/src/plugins/builtin/substack/api/auth.ts +185 -0
  139. package/src/plugins/builtin/substack/api/cache.ts +78 -0
  140. package/src/plugins/builtin/substack/api/loaders.ts +146 -0
  141. package/src/plugins/builtin/substack/api/store.ts +186 -0
  142. package/src/plugins/builtin/substack/api/types.ts +40 -0
  143. package/src/plugins/builtin/substack/api.test.ts +298 -0
  144. package/src/plugins/builtin/substack/article-detail.tsx +359 -0
  145. package/src/plugins/builtin/substack/article-stack.tsx +129 -0
  146. package/src/plugins/builtin/substack/content.ts +478 -0
  147. package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
  148. package/src/plugins/builtin/substack/index.tsx +49 -0
  149. package/src/plugins/builtin/substack/login-view.tsx +126 -0
  150. package/src/plugins/builtin/substack/model.test.ts +155 -0
  151. package/src/plugins/builtin/substack/normalize.ts +343 -0
  152. package/src/plugins/builtin/substack/pane-footer.ts +71 -0
  153. package/src/plugins/builtin/substack/pane-state.ts +170 -0
  154. package/src/plugins/builtin/substack/pane.tsx +500 -0
  155. package/src/plugins/builtin/substack/table.ts +111 -0
  156. package/src/plugins/builtin/substack/types.ts +71 -0
  157. package/src/plugins/builtin/substack/utils.ts +131 -0
  158. package/src/plugins/builtin/thirteenf/pane.tsx +26 -25
  159. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
  160. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
  161. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
  162. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
  163. package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
  164. package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
  165. package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
  166. package/src/plugins/builtin/world-indices/index.tsx +9 -6
  167. package/src/plugins/catalog-ui.ts +2 -0
  168. package/src/plugins/pane-manager/layout-state.ts +11 -0
  169. package/src/plugins/pane-manager.ts +1 -1
  170. package/src/plugins/prediction-markets/pane.tsx +10 -9
  171. package/src/plugins/registry/index.ts +3 -0
  172. package/src/plugins/runtime/context.tsx +1 -0
  173. package/src/plugins/runtime/index.tsx +4 -0
  174. package/src/react/use-throttled-commit-value.ts +116 -0
  175. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
  176. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
  177. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  178. package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
  179. package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
  180. package/src/renderers/electrobun/view/host/input.tsx +23 -2
  181. package/src/renderers/electrobun/view/http-fetch.ts +10 -3
  182. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  183. package/src/sources/gloomberb-cloud/index.ts +4 -0
  184. package/src/sources/provider-router/cache.ts +1 -0
  185. package/src/sources/provider-router/document-routes.ts +45 -0
  186. package/src/sources/provider-router/index.ts +4 -0
  187. package/src/sources/sec-edgar/content.ts +9 -3
  188. package/src/sources/sec-edgar.test.ts +108 -1
  189. package/src/sources/sec-edgar.ts +105 -1
  190. package/src/sources/yahoo-finance.ts +4 -0
  191. package/src/state/app/context/index.test.tsx +16 -0
  192. package/src/test-support/plugin-runtime.ts +1 -0
  193. package/src/tickers/search/index.ts +1 -0
  194. package/src/types/data-provider.ts +11 -0
  195. package/src/ui/host.tsx +1 -0
  196. package/src/version.ts +1 -1
@@ -1,12 +1,13 @@
1
- import { useCallback, useMemo } from "react";
2
- import { TextAttributes } from "../../../ui";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Box, ScrollBox, Text, TextAttributes, type ScrollBoxRenderable } from "../../../ui";
3
3
  import {
4
- DataTableView,
4
+ DataTableStackView,
5
5
  usePaneFooter,
6
6
  type DataTableCell,
7
7
  type DataTableColumn,
8
8
  type DataTableKeyEvent,
9
9
  } from "../../../components";
10
+ import type { SecFilingDocument, SecFilingItem } from "../../../types/data-provider";
10
11
  import type {
11
12
  AnalystEstimateRecord,
12
13
  AnalystResearchData,
@@ -16,9 +17,23 @@ import type {
16
17
  } from "../../../types/financials";
17
18
  import { blendHex, colors } from "../../../theme/colors";
18
19
  import { formatCompact, formatCurrency, formatNumber, formatPercent, formatPercentRaw } from "../../../utils/format";
20
+ import { isPlainKey } from "../../../utils/keyboard";
21
+ import { wrapTextLines } from "../../../utils/text-wrap";
22
+ import { useResolvedEntryValue, useSecFilingContent, useSecFilingDocuments, useSecFilingsQuery } from "../../../market-data/hooks";
23
+ import { getSharedMarketDataCoordinator } from "../../../market-data/coordinator";
24
+ import { instrumentFromTicker } from "../../../market-data/request-types";
25
+ import { isUsEquityTicker } from "../../../utils/sec";
19
26
  import { useAssetData } from "../../runtime";
20
27
  import { handleRefreshKey, loadingErrorFooterInfo, refreshFooterHint } from "../shared/table-pane";
21
28
  import { useBoundTicker as useSymbolBinding, useTickerRequest } from "../shared/ticker-request";
29
+ import {
30
+ documentContentKey,
31
+ documentContentTarget,
32
+ documentHeading,
33
+ formatCompactDocumentLabel,
34
+ isDefaultVisibleFilingDocument,
35
+ isInlineExhibitDocument,
36
+ } from "../sec/filing-documents";
22
37
 
23
38
  type EventStatus = "Earnings" | "Q Est" | "FY Est" | "TTM" | "Dividend" | "Split";
24
39
 
@@ -46,6 +61,9 @@ type EstimatePair = {
46
61
  revenue?: AnalystEstimateRecord;
47
62
  };
48
63
 
64
+ const SEC_EVENT_FILING_LIMIT = 50;
65
+ const SEC_EVENT_MATCH_WINDOW_DAYS = 7;
66
+
49
67
  function todayDateKey(): string {
50
68
  const now = new Date();
51
69
  const year = now.getFullYear();
@@ -84,6 +102,75 @@ function daysBetween(leftDate: string, rightDate: string): number {
84
102
  return Math.abs(left - right) / 86_400_000;
85
103
  }
86
104
 
105
+ function dateKeyToEpochDay(dateKey: string): number | null {
106
+ const timestamp = new Date(`${dateKey}T00:00:00Z`).getTime();
107
+ if (!Number.isFinite(timestamp)) return null;
108
+ return Math.floor(timestamp / 86_400_000);
109
+ }
110
+
111
+ function signedDaysBetween(leftDate: string, rightDate: string): number {
112
+ const left = dateKeyToEpochDay(leftDate);
113
+ const right = dateKeyToEpochDay(rightDate);
114
+ if (left == null || right == null) return Number.POSITIVE_INFINITY;
115
+ return right - left;
116
+ }
117
+
118
+ function filingDateKey(filing: SecFilingItem): string {
119
+ const value = filing.filingDate as Date | string | number;
120
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
121
+ const date = new Date(value);
122
+ if (!Number.isNaN(date.getTime())) return date.toISOString().slice(0, 10);
123
+ return String(value).slice(0, 10);
124
+ }
125
+
126
+ function isSecEarningsFilingCandidate(filing: SecFilingItem): boolean {
127
+ const form = filing.form.trim().toUpperCase();
128
+ return form === "8-K"
129
+ || form === "8-K/A"
130
+ || form === "10-Q"
131
+ || form === "10-Q/A"
132
+ || form === "10-K"
133
+ || form === "10-K/A";
134
+ }
135
+
136
+ function filingSearchText(filing: SecFilingItem): string {
137
+ return [
138
+ filing.form,
139
+ filing.items,
140
+ filing.primaryDocument,
141
+ filing.primaryDocDescription,
142
+ ].filter(Boolean).join(" ").toUpperCase();
143
+ }
144
+
145
+ function scoreFilingForEarnings(filing: SecFilingItem, earningsDate: string): number | null {
146
+ if (!isSecEarningsFilingCandidate(filing)) return null;
147
+ const delta = signedDaysBetween(earningsDate, filingDateKey(filing));
148
+ if (!Number.isFinite(delta) || delta < -1 || delta > SEC_EVENT_MATCH_WINDOW_DAYS) return null;
149
+
150
+ const form = filing.form.trim().toUpperCase();
151
+ const text = filingSearchText(filing);
152
+ let score = Math.abs(delta) * 10;
153
+ if (delta < 0) score += 12;
154
+ if (form.startsWith("10-")) score += 30;
155
+ if (text.includes("2.02")) score -= 18;
156
+ if (text.includes("9.01")) score -= 5;
157
+ if (/RESULTS OF OPERATIONS|FINANCIAL CONDITION|EARNINGS/i.test(text)) score -= 6;
158
+ return score;
159
+ }
160
+
161
+ export function matchEarningsSecFiling(row: { status: string; date: string } | null | undefined, filings: readonly SecFilingItem[]): SecFilingItem | null {
162
+ if (!row || row.status !== "Earnings") return null;
163
+ let best: { filing: SecFilingItem; score: number } | null = null;
164
+ for (const filing of filings) {
165
+ const score = scoreFilingForEarnings(filing, row.date);
166
+ if (score == null) continue;
167
+ if (!best || score < best.score) {
168
+ best = { filing, score };
169
+ }
170
+ }
171
+ return best?.filing ?? null;
172
+ }
173
+
87
174
  function statementForEarningsDate(
88
175
  quarterlyStatements: readonly FinancialStatement[],
89
176
  earningsDate: string,
@@ -293,6 +380,98 @@ function toneColor(tone: EventRow["tone"]): string {
293
380
  return colors.text;
294
381
  }
295
382
 
383
+ function eventDetailTitle(row: EventRow): string {
384
+ return `${row.status} | ${row.date}`;
385
+ }
386
+
387
+ function eventSummaryLine(row: EventRow): string {
388
+ return [
389
+ row.date,
390
+ row.period,
391
+ row.qEps != null ? `EPS ${formatNumber(row.qEps, 2)}` : null,
392
+ row.qRevenue != null ? `Rev ${formatCompact(row.qRevenue)}` : null,
393
+ row.annualEps != null ? `Ann EPS ${formatNumber(row.annualEps, 2)}` : null,
394
+ row.annualRevenue != null ? `Ann Rev ${formatCompact(row.annualRevenue)}` : null,
395
+ row.value !== "-" ? row.value : null,
396
+ row.detail || null,
397
+ ].filter((line): line is string => !!line).join(" | ");
398
+ }
399
+
400
+ function buildEventDetailBody({
401
+ row,
402
+ secFilingsLoading,
403
+ filing,
404
+ documents,
405
+ documentsLoading,
406
+ inlineContent,
407
+ primaryContent,
408
+ primaryContentLoading,
409
+ }: {
410
+ row: EventRow;
411
+ secFilingsLoading: boolean;
412
+ filing: SecFilingItem | null;
413
+ documents: SecFilingDocument[];
414
+ documentsLoading: boolean;
415
+ inlineContent: Map<string, string | null>;
416
+ primaryContent: string | null | undefined;
417
+ primaryContentLoading: boolean;
418
+ }): string {
419
+ const lines: string[] = ["Summary", eventSummaryLine(row)];
420
+ if (row.status !== "Earnings") {
421
+ return lines.join("\n");
422
+ }
423
+
424
+ lines.push("", "SEC Filing");
425
+ if (secFilingsLoading && !filing) {
426
+ lines.push("Loading recent SEC filings...");
427
+ return lines.join("\n");
428
+ }
429
+ if (!filing) {
430
+ lines.push("No related SEC filing found in recent filings.");
431
+ return lines.join("\n");
432
+ }
433
+
434
+ lines.push([
435
+ `${filing.form} filed ${filingDateKey(filing)}`,
436
+ filing.items ? `Items ${filing.items}` : null,
437
+ `Accession ${filing.accessionNumber}`,
438
+ ].filter(Boolean).join(" | "));
439
+
440
+ lines.push("", "Documents");
441
+ if (documentsLoading && documents.length === 0) {
442
+ lines.push("Loading filing documents...");
443
+ } else if (documents.length === 0) {
444
+ lines.push("No filing documents were listed for this filing.");
445
+ } else {
446
+ const visibleDocuments = documents.filter(isDefaultVisibleFilingDocument);
447
+ lines.push(...visibleDocuments.map(formatCompactDocumentLabel));
448
+ const hiddenCount = documents.length - visibleDocuments.length;
449
+ if (hiddenCount > 0) lines.push(`+ ${hiddenCount} support documents hidden`);
450
+ }
451
+
452
+ const exhibits = documents.filter(isInlineExhibitDocument);
453
+ if (exhibits.length > 0) {
454
+ lines.push("", "Inline Exhibits");
455
+ for (const document of exhibits) {
456
+ const key = documentContentKey(filing, document);
457
+ const hasContent = inlineContent.has(key);
458
+ const content = inlineContent.get(key);
459
+ lines.push("", documentHeading(document));
460
+ lines.push(hasContent
461
+ ? content || "Readable document content was not available for this exhibit."
462
+ : "Loading exhibit content...");
463
+ }
464
+ }
465
+
466
+ if (!documentsLoading && exhibits.length === 0) {
467
+ lines.push("", "Primary Filing Content");
468
+ lines.push(primaryContentLoading
469
+ ? "Loading filing content..."
470
+ : primaryContent || "Readable filing content was not available.");
471
+ }
472
+ return lines.join("\n");
473
+ }
474
+
296
475
  export function CorporateActionsView({
297
476
  focused,
298
477
  width,
@@ -305,7 +484,7 @@ export function CorporateActionsView({
305
484
  footerPaneId?: string;
306
485
  }) {
307
486
  const dataProvider = useAssetData();
308
- const { symbol, exchange, currency } = useSymbolBinding();
487
+ const { symbol, ticker, exchange, currency } = useSymbolBinding();
309
488
  const actionsLoader = useCallback((nextSymbol: string, nextExchange: string, forceRefresh: boolean) => {
310
489
  if (!dataProvider?.getCorporateActions) throw new Error("Corporate actions source unavailable");
311
490
  return dataProvider.getCorporateActions(nextSymbol, nextExchange, forceRefresh ? { cacheMode: "refresh" } : undefined);
@@ -341,6 +520,12 @@ export function CorporateActionsView({
341
520
  buildEventRows(actionsData, analystData, financialsData, displayCurrency)
342
521
  ), [actionsData, analystData, displayCurrency, financialsData]);
343
522
  const columns = useMemo(() => buildEventColumns(), []);
523
+ const [selectedIdx, setSelectedIdx] = useState(0);
524
+ const [openRowId, setOpenRowId] = useState<string | null>(null);
525
+ const detailScrollRef = useRef<ScrollBoxRenderable>(null);
526
+ const [inlineContent, setInlineContent] = useState<Map<string, string | null>>(new Map());
527
+ const documentFetchStartedRef = useRef(new Set<string>());
528
+ const documentFetchGenRef = useRef(0);
344
529
  const todayKey = todayDateKey();
345
530
  const futureRowBackground = blendHex(colors.bg, colors.positive, 0.16);
346
531
  const loading = actionsLoading || analystLoading || financialsLoading;
@@ -350,6 +535,163 @@ export function CorporateActionsView({
350
535
  reloadAnalyst();
351
536
  reloadFinancials();
352
537
  }, [reloadActions, reloadAnalyst, reloadFinancials]);
538
+ const openRow = openRowId
539
+ ? rows.find((row) => row.id === openRowId) ?? null
540
+ : null;
541
+ const instrument = useMemo(() => instrumentFromTicker(ticker, symbol), [symbol, ticker]);
542
+ const secFilingsEntry = useSecFilingsQuery(
543
+ openRow?.status === "Earnings" && instrument && isUsEquityTicker(ticker)
544
+ ? { instrument, count: SEC_EVENT_FILING_LIMIT }
545
+ : null,
546
+ );
547
+ const secFilings = useResolvedEntryValue(secFilingsEntry) ?? [];
548
+ const secFilingsLoading = openRow?.status === "Earnings" && (
549
+ secFilingsEntry?.phase === "idle"
550
+ || secFilingsEntry?.phase === "loading"
551
+ || secFilingsEntry?.phase === "refreshing"
552
+ );
553
+ const matchedFiling = useMemo(
554
+ () => matchEarningsSecFiling(openRow, secFilings),
555
+ [openRow, secFilings],
556
+ );
557
+ const documentsEntry = useSecFilingDocuments(matchedFiling);
558
+ const documents = useResolvedEntryValue(documentsEntry) ?? [];
559
+ const documentsLoading = !!matchedFiling && (
560
+ documentsEntry?.phase === "idle"
561
+ || documentsEntry?.phase === "loading"
562
+ || documentsEntry?.phase === "refreshing"
563
+ );
564
+ const hasInlineExhibits = documents.some(isInlineExhibitDocument);
565
+ const filingContentEntry = useSecFilingContent(
566
+ matchedFiling && !documentsLoading && !hasInlineExhibits ? matchedFiling : null,
567
+ );
568
+ const primaryContent = useResolvedEntryValue(filingContentEntry);
569
+ const primaryContentLoading = !!matchedFiling && (
570
+ filingContentEntry?.phase === "idle"
571
+ || filingContentEntry?.phase === "loading"
572
+ || filingContentEntry?.phase === "refreshing"
573
+ );
574
+
575
+ useEffect(() => {
576
+ if (rows.length > 0 && selectedIdx >= rows.length) {
577
+ setSelectedIdx(Math.max(0, rows.length - 1));
578
+ }
579
+ }, [rows.length, selectedIdx]);
580
+
581
+ useEffect(() => {
582
+ if (openRowId && !rows.some((row) => row.id === openRowId)) {
583
+ setOpenRowId(null);
584
+ }
585
+ }, [openRowId, rows]);
586
+
587
+ useEffect(() => {
588
+ if (!openRowId) return;
589
+ const scrollBox = detailScrollRef.current;
590
+ if (scrollBox) scrollBox.scrollTop = 0;
591
+ }, [openRowId]);
592
+
593
+ useEffect(() => {
594
+ setInlineContent(new Map());
595
+ documentFetchStartedRef.current.clear();
596
+ documentFetchGenRef.current += 1;
597
+ }, [exchange, symbol]);
598
+
599
+ useEffect(() => {
600
+ if (!matchedFiling || documents.length === 0) return;
601
+ const coordinator = getSharedMarketDataCoordinator();
602
+ if (!coordinator) return;
603
+ const gen = documentFetchGenRef.current;
604
+ const documentsToFetch = documents.filter((document) => {
605
+ if (!isInlineExhibitDocument(document)) return false;
606
+ const key = documentContentKey(matchedFiling, document);
607
+ return !inlineContent.has(key) && !documentFetchStartedRef.current.has(key);
608
+ });
609
+ if (documentsToFetch.length === 0) return;
610
+
611
+ for (const document of documentsToFetch) {
612
+ documentFetchStartedRef.current.add(documentContentKey(matchedFiling, document));
613
+ }
614
+
615
+ (async () => {
616
+ for (const document of documentsToFetch) {
617
+ if (documentFetchGenRef.current !== gen) return;
618
+ const key = documentContentKey(matchedFiling, document);
619
+ try {
620
+ const entry = await coordinator.loadSecFilingContent(documentContentTarget(matchedFiling, document));
621
+ if (documentFetchGenRef.current !== gen) return;
622
+ setInlineContent((prev) => new Map(prev).set(key, entry.data ?? entry.lastGoodData ?? null));
623
+ } catch {
624
+ if (documentFetchGenRef.current !== gen) return;
625
+ setInlineContent((prev) => new Map(prev).set(key, null));
626
+ }
627
+ }
628
+ })();
629
+ }, [documents, inlineContent, matchedFiling]);
630
+ const detailBody = openRow
631
+ ? buildEventDetailBody({
632
+ row: openRow,
633
+ secFilingsLoading,
634
+ filing: matchedFiling,
635
+ documents,
636
+ documentsLoading,
637
+ inlineContent,
638
+ primaryContent,
639
+ primaryContentLoading,
640
+ })
641
+ : "";
642
+ const detailTextWidth = Math.max(width - 2, 12);
643
+ const scrollDetailBy = useCallback((delta: number) => {
644
+ const scrollBox = detailScrollRef.current;
645
+ if (!scrollBox?.viewport) return;
646
+ const maxScrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height);
647
+ scrollBox.scrollTop = Math.max(0, Math.min(maxScrollTop, scrollBox.scrollTop + delta));
648
+ }, []);
649
+
650
+ const handleDetailKeyDown = useCallback((event: DataTableKeyEvent) => {
651
+ if (isPlainKey(event, "j", "down")) {
652
+ event.stopPropagation?.();
653
+ event.preventDefault?.();
654
+ scrollDetailBy(1);
655
+ return true;
656
+ }
657
+ if (isPlainKey(event, "k", "up")) {
658
+ event.stopPropagation?.();
659
+ event.preventDefault?.();
660
+ scrollDetailBy(-1);
661
+ return true;
662
+ }
663
+ return false;
664
+ }, [scrollDetailBy]);
665
+ const detailContent = openRow ? (
666
+ <Box
667
+ flexDirection="column"
668
+ flexGrow={1}
669
+ flexBasis={0}
670
+ minHeight={0}
671
+ overflow="hidden"
672
+ paddingX={1}
673
+ paddingY={1}
674
+ >
675
+ <ScrollBox
676
+ ref={detailScrollRef}
677
+ flexGrow={1}
678
+ flexBasis={0}
679
+ minHeight={0}
680
+ scrollY
681
+ focusable={false}
682
+ >
683
+ <Box flexDirection="column">
684
+ {wrapTextLines(detailBody, detailTextWidth).map((line, index) => (
685
+ <Box key={`event-detail-${index}`} height={1}>
686
+ <Text fg={colors.text}>{line}</Text>
687
+ </Box>
688
+ ))}
689
+ </Box>
690
+ </ScrollBox>
691
+ </Box>
692
+ ) : (
693
+ <Box flexGrow={1} />
694
+ );
353
695
 
354
696
  const renderCell = useCallback((
355
697
  row: EventRow,
@@ -390,8 +732,19 @@ export function CorporateActionsView({
390
732
  }), [error, footerPaneId, loading, reload]);
391
733
 
392
734
  return (
393
- <DataTableView<EventRow, EventColumn>
735
+ <DataTableStackView<EventRow, EventColumn>
394
736
  focused={focused}
737
+ detailOpen={!!openRow}
738
+ onBack={() => setOpenRowId(null)}
739
+ detailContent={detailContent}
740
+ detailTitle={openRow ? eventDetailTitle(openRow) : undefined}
741
+ selection={{
742
+ kind: "index",
743
+ selectedIndex: selectedIdx,
744
+ onChange: (index) => setSelectedIdx(index),
745
+ }}
746
+ onActivate={(row) => setOpenRowId(row.id)}
747
+ onDetailKeyDown={handleDetailKeyDown}
395
748
  rootWidth={width}
396
749
  rootHeight={height}
397
750
  onRootKeyDown={handleKeyDown}
@@ -401,8 +754,6 @@ export function CorporateActionsView({
401
754
  sortDirection="desc"
402
755
  onHeaderClick={() => {}}
403
756
  getItemKey={(row) => row.id}
404
- isSelected={() => false}
405
- onSelect={() => {}}
406
757
  renderCell={renderCell}
407
758
  getRowBackgroundColor={(row) => (
408
759
  row.date > todayKey ? futureRowBackground : undefined
@@ -8,7 +8,7 @@ import {
8
8
  sortRatingRows,
9
9
  type RatingSortPreference,
10
10
  } from "./analyst-pane";
11
- import { buildEventRows } from "./corporate-actions-pane";
11
+ import { buildEventRows, matchEarningsSecFiling } from "./corporate-actions-pane";
12
12
 
13
13
  const ratings: AnalystRatingRecord[] = [
14
14
  {
@@ -337,4 +337,33 @@ describe("event rows", () => {
337
337
  expect(rows[2]?.qEps).toBeUndefined();
338
338
  expect(rows[2]?.annualEps).toBeUndefined();
339
339
  });
340
+
341
+ test("matches reported earnings to nearby SEC earnings-release filings", () => {
342
+ const row = buildEventRows({
343
+ symbol: "AAPL",
344
+ dividends: [],
345
+ splits: [],
346
+ earnings: [{ date: "2026-01-30", epsActual: 2.4 }],
347
+ }, null, null, "USD").find((candidate) => candidate.status === "Earnings");
348
+ const filings = [
349
+ {
350
+ accessionNumber: "0000320193-26-000010",
351
+ form: "10-Q",
352
+ filingDate: new Date("2026-02-04T00:00:00Z"),
353
+ cik: "0000320193",
354
+ filingUrl: "https://www.sec.gov/Archives/edgar/data/320193/0000320193-26-000010-index.htm",
355
+ },
356
+ {
357
+ accessionNumber: "0000320193-26-000009",
358
+ form: "8-K",
359
+ filingDate: "2026-01-31T00:00:00.000Z" as unknown as Date,
360
+ items: "2.02,9.01",
361
+ primaryDocDescription: "Results of Operations and Financial Condition",
362
+ cik: "0000320193",
363
+ filingUrl: "https://www.sec.gov/Archives/edgar/data/320193/0000320193-26-000009-index.htm",
364
+ },
365
+ ];
366
+
367
+ expect(matchEarningsSecFiling(row, filings)?.accessionNumber).toBe("0000320193-26-000009");
368
+ });
340
369
  });
@@ -165,9 +165,12 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
165
165
  return (
166
166
  <DataTableView<RelativeRow, RelativeColumn>
167
167
  focused={focused}
168
- selectedIndex={rows.length > 0 ? selectedIdx : -1}
169
- onSelectIndex={(index) => setSelectedIdx(index)}
170
- onActivateIndex={(_index, row) => navigateTicker(row.symbol)}
168
+ selection={{
169
+ kind: "index",
170
+ selectedIndex: rows.length > 0 ? selectedIdx : -1,
171
+ onChange: (index) => setSelectedIdx(index),
172
+ }}
173
+ onActivate={(row) => navigateTicker(row.symbol)}
171
174
  onRootKeyDown={handleKeyDown}
172
175
  rootWidth={width}
173
176
  rootHeight={height}
@@ -177,11 +180,6 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
177
180
  sortDirection="asc"
178
181
  onHeaderClick={() => {}}
179
182
  getItemKey={(row) => row.symbol}
180
- isSelected={(row) => rows[selectedIdx]?.symbol === row.symbol}
181
- onSelect={(row, index) => {
182
- setSelectedIdx(index);
183
- navigateTicker(row.symbol);
184
- }}
185
183
  renderCell={renderCell}
186
184
  emptyStateTitle={loading ? "Loading peers..." : error ?? "No peers"}
187
185
  />
@@ -0,0 +1,66 @@
1
+ import type { SecFilingDocument, SecFilingItem } from "../../../types/data-provider";
2
+
3
+ const SEC_EXHIBIT_TYPE_RE = /^EX[-\s]?\d/i;
4
+ const SEC_XBRL_EXHIBIT_TYPE_RE = /^EX[-\s]?101(?:\.|$)/i;
5
+ const SEC_NON_READABLE_DOCUMENT_RE = /\.(?:cal|def|gif|jpg|jpeg|json|lab|png|pre|sch|xsd|zip)(?:$|[?#])/i;
6
+ const SEC_SUPPORT_DOCUMENT_TYPE_RE = /^(?:GRAPHIC|XML|ZIP|EX[-\s]?101(?:\.|$)|EX-FILING FEES)/i;
7
+
8
+ export function documentContentKey(filing: SecFilingItem, document: SecFilingDocument): string {
9
+ return `${filing.accessionNumber}:${document.document || document.url}`;
10
+ }
11
+
12
+ export function documentContentTarget(filing: SecFilingItem, document: SecFilingDocument): SecFilingItem {
13
+ return {
14
+ ...filing,
15
+ accessionNumber: documentContentKey(filing, document),
16
+ form: document.type || filing.form,
17
+ primaryDocument: document.document,
18
+ primaryDocDescription: document.description,
19
+ primaryDocumentUrl: document.url,
20
+ filingUrl: document.url,
21
+ };
22
+ }
23
+
24
+ export function formatDocumentLabel(document: SecFilingDocument): string {
25
+ const label = document.isPrimary ? "PRIMARY" : document.type || "DOCUMENT";
26
+ const description = document.description
27
+ && document.description !== document.document
28
+ && document.description !== document.type
29
+ ? ` | ${document.description}`
30
+ : "";
31
+ const size = document.size ? ` | ${document.size}` : "";
32
+ return `${label.padEnd(8)} ${document.document}${description}${size}`;
33
+ }
34
+
35
+ export function formatCompactDocumentLabel(document: SecFilingDocument): string {
36
+ const label = document.isPrimary ? "PRIMARY" : document.type || "DOCUMENT";
37
+ const description = document.description
38
+ && document.description !== document.document
39
+ && document.description !== document.type
40
+ ? ` | ${document.description}`
41
+ : "";
42
+ return `${label} ${document.document}${description}`;
43
+ }
44
+
45
+ export function documentHeading(document: SecFilingDocument): string {
46
+ const label = document.type || "DOCUMENT";
47
+ const description = document.description
48
+ && document.description !== document.document
49
+ && document.description !== document.type
50
+ ? ` | ${document.description}`
51
+ : "";
52
+ return `${label} | ${document.document}${description}`;
53
+ }
54
+
55
+ export function isInlineExhibitDocument(document: SecFilingDocument): boolean {
56
+ const type = document.type.trim();
57
+ if (!SEC_EXHIBIT_TYPE_RE.test(type)) return false;
58
+ if (SEC_XBRL_EXHIBIT_TYPE_RE.test(type)) return false;
59
+ return !SEC_NON_READABLE_DOCUMENT_RE.test(document.document) && !SEC_NON_READABLE_DOCUMENT_RE.test(document.url);
60
+ }
61
+
62
+ export function isDefaultVisibleFilingDocument(document: SecFilingDocument): boolean {
63
+ if (document.isPrimary) return true;
64
+ if (isInlineExhibitDocument(document)) return true;
65
+ return !SEC_SUPPORT_DOCUMENT_TYPE_RE.test(document.type.trim());
66
+ }