gloomberb 0.16.6 → 0.17.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 (348) hide show
  1. package/package.json +2 -1
  2. package/src/api-client/chat-pane-link.ts +47 -0
  3. package/src/api-client/chat.ts +9 -1
  4. package/src/api-client/data.ts +37 -1
  5. package/src/api-client/equity-screener.ts +3 -0
  6. package/src/api-client/europe-energy.ts +91 -0
  7. package/src/api-client/index.ts +6 -0
  8. package/src/api-client/letters.ts +41 -0
  9. package/src/api-client/normalizers.ts +36 -6
  10. package/src/api-client/organization-tickers.ts +44 -0
  11. package/src/api-client/request.ts +3 -1
  12. package/src/api-client/types.ts +27 -1
  13. package/src/app/runtime/desktop-deeplink.ts +48 -15
  14. package/src/architecture/core-builtin-allowlist.json +1 -1
  15. package/src/architecture/dom-cell-metrics-allowlist.json +28 -0
  16. package/src/ask-gloom/pane-context.ts +23 -19
  17. package/src/ask-gloom/pane-request.ts +2 -2
  18. package/src/cli/commands/fred-series.ts +77 -11
  19. package/src/cli/commands/market.ts +77 -20
  20. package/src/cli/commands/overview.ts +29 -20
  21. package/src/cli/commands/ticker.ts +64 -11
  22. package/src/cli/desktop-pane-shot-view.ts +114 -0
  23. package/src/cli/desktop-pane-shot.ts +30 -31
  24. package/src/cli/entry.ts +7 -0
  25. package/src/cli/export-source.ts +43 -0
  26. package/src/cli/freshness.ts +18 -3
  27. package/src/cli/glossary.ts +17 -0
  28. package/src/cli/history-rows.ts +174 -13
  29. package/src/cli/known-gaps.ts +14 -3
  30. package/src/cli/listing-arg.ts +20 -2
  31. package/src/cli/listing-suggestion.ts +159 -0
  32. package/src/cli/native-smoke.ts +13 -0
  33. package/src/cli/not-a-ticker.ts +13 -1
  34. package/src/cli/pane-functions/capabilities.ts +16 -8
  35. package/src/cli/pane-functions/dom.ts +28 -13
  36. package/src/cli/pane-functions/function-help.ts +51 -6
  37. package/src/cli/pane-functions/headless.ts +26 -7
  38. package/src/cli/pane-functions/index.ts +46 -3
  39. package/src/cli/pane-functions/report-notices.ts +6 -3
  40. package/src/cli/pane-functions/report.ts +16 -2
  41. package/src/cli/pane-functions/resolver.ts +7 -4
  42. package/src/cli/report-tables.ts +31 -7
  43. package/src/cli/result.ts +54 -11
  44. package/src/cli/symbol-rows.ts +18 -1
  45. package/src/components/chart/composite/axis-overlays.tsx +2 -0
  46. package/src/components/chart/composite/composite-chart.tsx +4 -21
  47. package/src/components/chart/composite/legend.tsx +10 -6
  48. package/src/components/chart/composite/price-axis-labels.tsx +1 -0
  49. package/src/components/command-bar/list/model.ts +2 -0
  50. package/src/components/command-bar/pane-templates/items.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +1 -0
  52. package/src/components/command-bar/routes/ticker-search/results.ts +3 -1
  53. package/src/components/command-bar/workflow/ops.ts +8 -97
  54. package/src/components/command-bar/workflow/pane-reuse.ts +177 -0
  55. package/src/components/data-table/view.tsx +1 -0
  56. package/src/components/feed-data-table/stack-view.tsx +18 -42
  57. package/src/components/layout/missing-pane.tsx +2 -0
  58. package/src/components/layout/shell/index.tsx +1 -0
  59. package/src/components/layout/shell/pane/ask.ts +12 -7
  60. package/src/components/layout/shell/pane/capture.ts +8 -34
  61. package/src/components/markdown-text.tsx +37 -6
  62. package/src/components/reader/article-reader.tsx +91 -266
  63. package/src/components/reader/detail-reader.tsx +210 -0
  64. package/src/components/reader/reader-text.tsx +239 -0
  65. package/src/components/table-view-shared.tsx +8 -4
  66. package/src/components/ticker/badge/list.tsx +15 -4
  67. package/src/components/ui/choice-dialog.tsx +3 -0
  68. package/src/components/ui/data-table/types.ts +7 -0
  69. package/src/components/ui/detail-scroll-body.tsx +31 -4
  70. package/src/components/ui/display.tsx +46 -1
  71. package/src/components/ui/icon.tsx +11 -1
  72. package/src/components/ui/list-view.tsx +3 -1
  73. package/src/components/ui/query-bar-keys.tsx +2 -0
  74. package/src/components/ui/query-bar.tsx +9 -1
  75. package/src/components/ui/select-button.tsx +3 -0
  76. package/src/data/config/store/fold-panes.ts +202 -11
  77. package/src/data/config/store/migrations.ts +13 -1
  78. package/src/docs/snapshot.json +29 -0
  79. package/src/i18n/es.ts +14 -0
  80. package/src/i18n/ja.ts +14 -0
  81. package/src/i18n/ko.ts +14 -0
  82. package/src/i18n/zh-cn.ts +14 -0
  83. package/src/i18n/zh-tw.ts +14 -0
  84. package/src/layout/desk-keys.ts +7 -0
  85. package/src/layout/desks.ts +2 -2
  86. package/src/market-data/market/status.ts +67 -6
  87. package/src/market-data/quotes/contributions.ts +15 -0
  88. package/src/market-data/quotes/regular-session-valuation.ts +131 -0
  89. package/src/market-data/quotes/resolution.ts +4 -0
  90. package/src/market-data/snapshot-provider.ts +6 -3
  91. package/src/notifications/app-badge.ts +30 -0
  92. package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
  93. package/src/plugins/builtin/account-management/form-components.tsx +53 -1
  94. package/src/plugins/builtin/account-management/keyboard.ts +5 -0
  95. package/src/plugins/builtin/account-management/model.ts +27 -0
  96. package/src/plugins/builtin/account-management/pane.tsx +62 -2
  97. package/src/plugins/builtin/alerts/cli.ts +6 -4
  98. package/src/plugins/builtin/analytics/index.tsx +1 -1
  99. package/src/plugins/builtin/awards/pane.tsx +29 -18
  100. package/src/plugins/builtin/backtest/pane.tsx +2 -1
  101. package/src/plugins/builtin/bond-calculator/index.tsx +3 -5
  102. package/src/plugins/builtin/buildout/detail/company.tsx +7 -4
  103. package/src/plugins/builtin/builtin-plugin-meta.ts +5 -4
  104. package/src/plugins/builtin/catalysts/model.ts +92 -10
  105. package/src/plugins/builtin/catalysts/pane.tsx +6 -4
  106. package/src/plugins/builtin/cds/index.tsx +21 -30
  107. package/src/plugins/builtin/changelog/index.tsx +11 -12
  108. package/src/plugins/builtin/chart-composer/headless-schema.ts +2 -0
  109. package/src/plugins/builtin/chart-composer/headless.ts +87 -5
  110. package/src/plugins/builtin/chart-composer/pane.tsx +3 -2
  111. package/src/plugins/builtin/chart-composer/session-history.ts +20 -5
  112. package/src/plugins/builtin/chat/command-bar.ts +6 -8
  113. package/src/plugins/builtin/chat/content/composer-runtime.ts +2 -2
  114. package/src/plugins/builtin/chat/content/composer.tsx +13 -4
  115. package/src/plugins/builtin/chat/content/footer.tsx +15 -5
  116. package/src/plugins/builtin/chat/content/handle-prompt.tsx +326 -0
  117. package/src/plugins/builtin/chat/content/index.tsx +113 -9
  118. package/src/plugins/builtin/chat/content/layout-metrics.ts +4 -0
  119. package/src/plugins/builtin/chat/content/open-targets.ts +20 -4
  120. package/src/plugins/builtin/chat/content/scroll.ts +6 -1
  121. package/src/plugins/builtin/chat/content/transcript.tsx +11 -2
  122. package/src/plugins/builtin/chat/ghost-user.ts +1 -1
  123. package/src/plugins/builtin/chat/handle-prompt.ts +128 -0
  124. package/src/plugins/builtin/chat/index.tsx +44 -21
  125. package/src/plugins/builtin/chat/layout.ts +7 -14
  126. package/src/plugins/builtin/chat/message/desktop.tsx +59 -11
  127. package/src/plugins/builtin/chat/message/header.tsx +69 -21
  128. package/src/plugins/builtin/chat/message/pane-chip.tsx +223 -0
  129. package/src/plugins/builtin/chat/message/person-icons.tsx +53 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +73 -9
  131. package/src/plugins/builtin/chat/message/render-state.ts +15 -2
  132. package/src/plugins/builtin/chat/message/terminal.tsx +33 -8
  133. package/src/plugins/builtin/chat/message/types.ts +3 -0
  134. package/src/plugins/builtin/chat/pane-links/model.ts +149 -0
  135. package/src/plugins/builtin/chat/pane-links/open.ts +29 -0
  136. package/src/plugins/builtin/chat/pane-links/previews.ts +119 -0
  137. package/src/plugins/builtin/chat/pane-state.ts +5 -8
  138. package/src/plugins/builtin/chat/people.ts +202 -0
  139. package/src/plugins/builtin/chat/profile-popover.ts +18 -2
  140. package/src/plugins/builtin/chat/status-widget.tsx +3 -8
  141. package/src/plugins/builtin/chat/unread-badge.ts +65 -0
  142. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +9 -6
  143. package/src/plugins/builtin/clinical-trials/client.ts +26 -1
  144. package/src/plugins/builtin/clinical-trials/index.tsx +10 -2
  145. package/src/plugins/builtin/clinical-trials/model.ts +269 -0
  146. package/src/plugins/builtin/clinical-trials/pane.tsx +247 -62
  147. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +156 -0
  148. package/src/plugins/builtin/cloud/askg/attached-pane.tsx +3 -3
  149. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +3 -3
  150. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -0
  151. package/src/plugins/builtin/comment-letters/pane.tsx +30 -11
  152. package/src/plugins/builtin/commodities-energy/index.tsx +179 -0
  153. package/src/plugins/builtin/commodity-balances/client.ts +208 -0
  154. package/src/plugins/builtin/commodity-balances/headless.ts +66 -0
  155. package/src/plugins/builtin/commodity-balances/index.tsx +32 -0
  156. package/src/plugins/builtin/commodity-balances/model.ts +261 -0
  157. package/src/plugins/builtin/commodity-balances/pane.tsx +227 -0
  158. package/src/plugins/builtin/composite-plugins.ts +26 -1
  159. package/src/plugins/builtin/credit-boards/index.tsx +21 -42
  160. package/src/plugins/builtin/credit-boards/model.ts +0 -2
  161. package/src/plugins/builtin/credit-conditions/index.tsx +10 -19
  162. package/src/plugins/builtin/credit-documents/index.tsx +27 -10
  163. package/src/plugins/builtin/debt-maturities/index.tsx +3 -17
  164. package/src/plugins/builtin/debug/index.tsx +2 -2
  165. package/src/plugins/builtin/dividend-yield/client.ts +3 -1
  166. package/src/plugins/builtin/dividend-yield/headless.ts +3 -1
  167. package/src/plugins/builtin/dividend-yield/pane.tsx +3 -1
  168. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  169. package/src/plugins/builtin/docs/blocks.tsx +4 -3
  170. package/src/plugins/builtin/docs/pane.tsx +16 -8
  171. package/src/plugins/builtin/earnings/index.tsx +2 -0
  172. package/src/plugins/builtin/earnings-calls/pane.tsx +2 -12
  173. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  174. package/src/plugins/builtin/equity-screener/headless.ts +36 -2
  175. package/src/plugins/builtin/equity-screener/model.ts +121 -68
  176. package/src/plugins/builtin/executives/pane.tsx +3 -1
  177. package/src/plugins/builtin/filing-events/index.tsx +20 -0
  178. package/src/plugins/builtin/fixed-income/index.tsx +46 -0
  179. package/src/plugins/builtin/fixed-income/model.ts +27 -0
  180. package/src/plugins/builtin/fixed-income/pane.tsx +162 -0
  181. package/src/plugins/builtin/fixed-income/shortcut.ts +52 -0
  182. package/src/plugins/builtin/futures/index.tsx +24 -7
  183. package/src/plugins/builtin/futures/series.ts +23 -2
  184. package/src/plugins/builtin/futures/use-front-returns.ts +3 -2
  185. package/src/plugins/builtin/futures-curve/pane.tsx +6 -4
  186. package/src/plugins/builtin/help/shortcut-table.tsx +4 -2
  187. package/src/plugins/builtin/insider/detail.tsx +20 -12
  188. package/src/plugins/builtin/insider/index.tsx +2 -2
  189. package/src/plugins/builtin/iv-history/index.tsx +3 -0
  190. package/src/plugins/builtin/iv-history/screen-pane.tsx +13 -5
  191. package/src/plugins/builtin/jodi/client.ts +220 -0
  192. package/src/plugins/builtin/jodi/headless.ts +63 -0
  193. package/src/plugins/builtin/jodi/index.tsx +32 -0
  194. package/src/plugins/builtin/jodi/model.ts +75 -0
  195. package/src/plugins/builtin/jodi/pane.tsx +141 -0
  196. package/src/plugins/builtin/market-heatmap/evidence.ts +13 -4
  197. package/src/plugins/builtin/market-heatmap/headless.ts +3 -3
  198. package/src/plugins/builtin/market-heatmap/index.tsx +41 -23
  199. package/src/plugins/builtin/market-heatmap/live.ts +2 -1
  200. package/src/plugins/builtin/market-heatmap/portfolio.ts +67 -21
  201. package/src/plugins/builtin/market-movers/table.ts +2 -2
  202. package/src/plugins/builtin/market-valuation/align.ts +10 -9
  203. package/src/plugins/builtin/mna/distress/model.ts +52 -1
  204. package/src/plugins/builtin/mna/distress/pane.tsx +17 -12
  205. package/src/plugins/builtin/mna/index.tsx +2 -0
  206. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  207. package/src/plugins/builtin/news/wire/industry-pane.tsx +28 -14
  208. package/src/plugins/builtin/news/wire/letters/client.ts +108 -0
  209. package/src/plugins/builtin/news/wire/letters/model.ts +69 -0
  210. package/src/plugins/builtin/news/wire/letters/pane.tsx +155 -0
  211. package/src/plugins/builtin/news/wire/mutes.ts +2 -1
  212. package/src/plugins/builtin/news/wire/news/query-presets.ts +4 -1
  213. package/src/plugins/builtin/openfda/client.ts +123 -15
  214. package/src/plugins/builtin/openfda/model.ts +163 -0
  215. package/src/plugins/builtin/openfda/pane.tsx +109 -65
  216. package/src/plugins/builtin/openfda/types.ts +25 -0
  217. package/src/plugins/builtin/options/view.tsx +1 -1
  218. package/src/plugins/builtin/pe-band/model.ts +3 -2
  219. package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
  220. package/src/plugins/builtin/plugin-module.ts +3 -1
  221. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +1 -1
  222. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +19 -4
  223. package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -2
  224. package/src/plugins/builtin/portfolio-list/cli/shared.ts +39 -0
  225. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +18 -5
  226. package/src/plugins/builtin/portfolio-list/live-valuation.ts +25 -13
  227. package/src/plugins/builtin/power/evidence.ts +32 -3
  228. package/src/plugins/builtin/power/gas-client.ts +110 -0
  229. package/src/plugins/builtin/power/gas-model.ts +194 -0
  230. package/src/plugins/builtin/power/gas.tsx +145 -0
  231. package/src/plugins/builtin/power/headless.ts +4 -1
  232. package/src/plugins/builtin/power/index.tsx +19 -9
  233. package/src/plugins/builtin/power/pane.tsx +2 -3
  234. package/src/plugins/builtin/power/prices-headless.ts +75 -7
  235. package/src/plugins/builtin/power/prices.tsx +36 -13
  236. package/src/plugins/builtin/primary-dealers/client.ts +108 -0
  237. package/src/plugins/builtin/primary-dealers/headless.ts +53 -0
  238. package/src/plugins/builtin/primary-dealers/index.tsx +17 -0
  239. package/src/plugins/builtin/primary-dealers/model.ts +206 -0
  240. package/src/plugins/builtin/primary-dealers/pane.tsx +149 -0
  241. package/src/plugins/builtin/relative-rotation/index.tsx +3 -0
  242. package/src/plugins/builtin/research/analyst-model.ts +9 -5
  243. package/src/plugins/builtin/research/headless-schema.ts +2 -1
  244. package/src/plugins/builtin/research/relative-valuation-headless.ts +68 -32
  245. package/src/plugins/builtin/research/relative-valuation-model.ts +45 -24
  246. package/src/plugins/builtin/research/relative-valuation-pane.tsx +39 -4
  247. package/src/plugins/builtin/scanner/flow-pane.tsx +8 -1
  248. package/src/plugins/builtin/sec/filing-display.tsx +3 -36
  249. package/src/plugins/builtin/sec/index.tsx +131 -43
  250. package/src/plugins/builtin/sec/model.ts +148 -2
  251. package/src/plugins/builtin/sec/row-summaries.ts +105 -0
  252. package/src/plugins/builtin/sectors/index.tsx +2 -0
  253. package/src/plugins/builtin/sectors/sector-model.ts +2 -2
  254. package/src/plugins/builtin/series-inventory.ts +1 -0
  255. package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
  256. package/src/plugins/builtin/shared/use-quote-board.ts +179 -43
  257. package/src/plugins/builtin/shared/wall-teaser.tsx +2 -2
  258. package/src/plugins/builtin/shared/zip-text.ts +46 -0
  259. package/src/plugins/builtin/short-interest/watch-model.ts +11 -2
  260. package/src/plugins/builtin/themes/model.ts +2 -2
  261. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +11 -0
  262. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +81 -19
  263. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +1 -1
  264. package/src/plugins/builtin/ticker-detail/headless.ts +2 -2
  265. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -1
  266. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +7 -5
  267. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -6
  268. package/src/plugins/builtin/time-sales/headless.ts +22 -0
  269. package/src/plugins/builtin/trace-bonds/client.ts +196 -0
  270. package/src/plugins/builtin/trace-bonds/headless.ts +69 -0
  271. package/src/plugins/builtin/trace-bonds/index.tsx +33 -0
  272. package/src/plugins/builtin/trace-bonds/model.ts +115 -0
  273. package/src/plugins/builtin/trace-bonds/pane.tsx +191 -0
  274. package/src/plugins/builtin/treasury-auctions/index.tsx +33 -47
  275. package/src/plugins/builtin/treasury-daily/client.ts +96 -0
  276. package/src/plugins/builtin/treasury-daily/headless.ts +69 -0
  277. package/src/plugins/builtin/treasury-daily/index.tsx +17 -0
  278. package/src/plugins/builtin/treasury-daily/model.ts +69 -0
  279. package/src/plugins/builtin/treasury-daily/pane.tsx +140 -0
  280. package/src/plugins/builtin/world-venue-map/entity-detail.tsx +15 -2
  281. package/src/plugins/builtin/world-venue-map/map.tsx +1 -0
  282. package/src/plugins/builtin-editorial.ts +1 -1
  283. package/src/plugins/catalog-browser.ts +25 -2
  284. package/src/plugins/marketplace/model.ts +57 -21
  285. package/src/renderers/dom/build-assets.ts +14 -5
  286. package/src/renderers/dom/data-table/index.tsx +5 -3
  287. package/src/renderers/dom/desktop/controls.tsx +2 -1
  288. package/src/renderers/dom/desktop/icons.tsx +25 -0
  289. package/src/renderers/dom/desktop/list-view.tsx +1 -1
  290. package/src/renderers/dom/desktop/menu.tsx +1 -1
  291. package/src/renderers/dom/desktop/query-bar.tsx +3 -1
  292. package/src/renderers/dom/host/chart-surface.tsx +7 -2
  293. package/src/renderers/dom/host/resize-rows.ts +111 -0
  294. package/src/renderers/dom/host/sticky-scroll.ts +22 -3
  295. package/src/renderers/dom/host/style.ts +14 -0
  296. package/src/renderers/dom/host/surface-3d.tsx +2 -1
  297. package/src/renderers/dom/host/surface-fit.ts +10 -6
  298. package/src/renderers/dom/host/text.tsx +2 -2
  299. package/src/renderers/dom/styles.css +63 -6
  300. package/src/renderers/electrobun/bun/desktop/app-badge.ts +37 -0
  301. package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
  302. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  303. package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +37 -0
  304. package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -40
  305. package/src/renderers/electrobun/bun/desktop/macos-objc.ts +41 -0
  306. package/src/renderers/electrobun/bun/tui-entry.ts +18 -1
  307. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  308. package/src/renderers/electrobun/view/app-badge-host.ts +17 -0
  309. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +8 -3
  310. package/src/renderers/electrobun/view/main.tsx +2 -0
  311. package/src/renderers/electrobun/view/remote/asset-data-client.ts +5 -1
  312. package/src/series/validate.ts +1 -1
  313. package/src/shares/api.ts +5 -1
  314. package/src/shares/location.ts +31 -0
  315. package/src/shares/open.ts +26 -0
  316. package/src/shares/pane-image.ts +18 -6
  317. package/src/shares/personal-pane.ts +42 -54
  318. package/src/sources/gloomberb-cloud/index.ts +7 -2
  319. package/src/sources/gloomberb-cloud/normalizers.ts +5 -0
  320. package/src/sources/history-coverage.ts +22 -4
  321. package/src/sources/history-result.ts +3 -1
  322. package/src/sources/provider-errors.ts +2 -1
  323. package/src/sources/provider-router/batches.ts +18 -2
  324. package/src/sources/provider-router/index.ts +2 -1
  325. package/src/state/app/context/index.tsx +3 -2
  326. package/src/test-support/builtin-panes.ts +31 -0
  327. package/src/tickers/listing-venues.ts +22 -0
  328. package/src/tickers/search/index.ts +7 -2
  329. package/src/tickers/search/ranking.ts +19 -0
  330. package/src/time-series/chart-data.ts +2 -0
  331. package/src/time-series/parsed-history-cache.ts +41 -11
  332. package/src/time-series/resolve.ts +47 -15
  333. package/src/types/config.ts +1 -1
  334. package/src/types/data-provider.ts +13 -1
  335. package/src/types/financials.ts +13 -0
  336. package/src/types/headless.ts +22 -0
  337. package/src/types/plugin.ts +33 -6
  338. package/src/types/price-history.ts +2 -0
  339. package/src/ui/host.tsx +6 -0
  340. package/src/utils/crypto-pair.ts +1 -1
  341. package/src/utils/currency-pair.ts +66 -0
  342. package/src/utils/decimal-comma.ts +57 -0
  343. package/src/utils/guards.ts +25 -0
  344. package/src/utils/local-utc-offset.ts +37 -0
  345. package/src/utils/plugin-proxy-hosts.json +8 -1
  346. package/src/components/chart/composite/crosshair-readout.tsx +0 -150
  347. package/src/plugins/builtin/chat/message/ai-badge.tsx +0 -18
  348. package/src/plugins/builtin/credit-boards/pane.tsx +0 -30
@@ -1,15 +1,29 @@
1
1
  import { comparablePriceEarnings, notMeaningfulMultiples } from "../../../utils/price-earnings";
2
2
  import { selectMarketCapitalization } from "../../../utils/market-capitalization";
3
3
  import { reportedEnterpriseValue } from "../../../utils/fundamentals";
4
+ import { getExtendedSessionDisplay, getQuoteSessionFields } from "../../../market-data/market/status";
5
+ import { atRegularSessionPrice, movePriceDerivedFigures } from "../../../market-data/quotes/regular-session-valuation";
4
6
  import type { Quote, TickerFinancials } from "../../../types/financials";
5
7
 
6
8
  export const RELATIVE_VALUATION_STALE_QUOTE_NOTICE = "Quote stale: quote-based values unavailable";
7
9
  export const RELATIVE_VALUATION_STALE_FUNDAMENTALS_NOTICE = "Fundamentals stale: retained values may be out of date";
8
10
 
9
- export function relativeValuationValues(financials: TickerFinancials | null) {
11
+ /**
12
+ * A peer's row. Last, its change and the extended print are the quote's
13
+ * sessions as `quote` reads them (getQuoteSessionFields): after the close,
14
+ * the close and its move, and the pre-market or after-hours print measured
15
+ * from it. Every figure measured at the price (market cap, P/E, forward P/E
16
+ * and through the cap the FCF yield) is at that regular session price too.
17
+ */
18
+ export function relativeValuationValues(snapshot: TickerFinancials | null) {
19
+ const reported = snapshot?.quote;
20
+ const financials = snapshot ? atRegularSessionPrice(snapshot) : null;
10
21
  const quote = financials?.quote;
11
22
  const fundamentals = financials?.fundamentals;
12
23
  const quoteStale = quote?.stale === true;
24
+ const session = getQuoteSessionFields(quoteStale ? null : quote);
25
+ // An overnight price that is the session's bid and ask midpoint, nothing having traded in it.
26
+ const extendedMark = !quoteStale && getExtendedSessionDisplay(quote)?.mark === true;
13
27
  const capitalization = selectMarketCapitalization(quoteStale ? undefined : quote, fundamentals);
14
28
  const enterpriseValue = reportedEnterpriseValue(fundamentals);
15
29
  // A zero or non-finite enterprise value is the source's gap, and the ratio it serves beside it comes from the same observation.
@@ -27,21 +41,21 @@ export function relativeValuationValues(financials: TickerFinancials | null) {
27
41
  ...(forwardNotMeaningful ? { forwardPE: forwardNotMeaningful } : {}),
28
42
  };
29
43
  return {
30
- price: quoteStale ? null : quote?.price ?? null,
44
+ ...session,
45
+ extendedMark,
31
46
  quoteStale: quote?.stale ?? null,
32
47
  quoteAsOf: quote && Number.isFinite(quote.lastUpdated) && quote.lastUpdated > 0 ? quote.lastUpdated : null,
33
- // Retain the rejected observation separately from values used for comparison.
34
- reportedQuote: quote ? {
35
- price: quote.price, changePercent: quote.changePercent, marketCap: quote.marketCap ?? null,
36
- currency: quote.currency, lastUpdated: quote.lastUpdated, stale: quote.stale ?? null,
37
- providerId: quote.providerId ?? null, dataSource: quote.dataSource ?? null,
48
+ // The observation as sent, apart from the values used for comparison: a rejected stale quote, or the print the source priced.
49
+ reportedQuote: reported ? {
50
+ price: reported.price, changePercent: reported.changePercent, marketCap: reported.marketCap ?? null,
51
+ currency: reported.currency, lastUpdated: reported.lastUpdated, stale: reported.stale ?? null,
52
+ providerId: reported.providerId ?? null, dataSource: reported.dataSource ?? null,
38
53
  } : null,
39
54
  fundamentalsProvenance: fundamentals ? {
40
55
  source: fundamentals.source ?? null, retrievedAt: fundamentals.fetchedAt ?? null,
41
56
  stale: fundamentals.stale ?? null,
42
57
  } : null,
43
58
  currency: quote?.currency ?? null,
44
- changePercent: quoteStale ? null : quote?.changePercent ?? null,
45
59
  marketCap: capitalization?.value ?? null,
46
60
  marketCapCurrency: capitalization?.currency ?? null,
47
61
  marketCapProvenance: capitalization?.provenance ?? null,
@@ -68,17 +82,27 @@ function scaled(value: number | undefined, ratio: number): number | undefined {
68
82
  return value != null && Number.isFinite(value) ? value * ratio : value;
69
83
  }
70
84
 
85
+ /** What a quote says about its sessions; a newer quote's replaces the snapshot's whole, so a close never pairs with another day's print. */
86
+ const SESSION_FIELDS = [
87
+ "marketState", "changeSessionDate", "regularClose", "regularCloseSessionDate", "regularChange", "regularChangePercent",
88
+ "preMarketPrice", "preMarketChange", "preMarketChangePercent", "postMarketPrice", "postMarketChange", "postMarketChangePercent",
89
+ "extendedSessionPrint", "overnightPrice", "overnightChange", "overnightChangePercent", "overnightPriceKind",
90
+ ] as const satisfies readonly (keyof Quote)[];
91
+
71
92
  /**
72
- * The snapshot with a newer quote laid over it. Fundamentals stay from the
73
- * snapshot. Values measured against the price move by live price / snapshot
74
- * price, but only from a price they were measured at: the quote's market cap
75
- * (and through it the FCF yield) when the snapshot quote is not stale, and the
76
- * P/E multiples and the fundamentals' cap when the fundamentals are not stale
77
- * either. A stale snapshot quote drops its cap, so the row falls back to the
78
- * fundamentals' cap as it would without the live quote. EV/Sales stays at the
79
- * snapshot: only its equity part moves with the price, and the snapshot does
80
- * not say how large that part is. A quote in another currency, or a stale one,
81
- * leaves the snapshot alone.
93
+ * The snapshot with a newer quote laid over it, its sessions included, so
94
+ * relativeValuationValues reads the newer quote's regular session and
95
+ * extended print. Fundamentals stay from the snapshot. Values measured
96
+ * against the price move by live price / snapshot price (and from there to
97
+ * the regular session price in relativeValuationValues), but only from a
98
+ * price they were measured at: the quote's market cap (and through it the
99
+ * FCF yield) when the snapshot quote is not stale, and the fundamentals'
100
+ * price-derived figures (movePriceDerivedFigures: the P/E multiples, the cap,
101
+ * and the equity part of the enterprise value and EV/Sales) when the
102
+ * fundamentals are not stale either. A stale snapshot quote drops its cap, so
103
+ * the row falls back to the fundamentals' cap as it would without the live
104
+ * quote. A quote in another currency, or a stale one, leaves the snapshot
105
+ * alone.
82
106
  */
83
107
  export function withLiveQuote(financials: TickerFinancials | null, live: Quote | null | undefined): TickerFinancials | null {
84
108
  const base = financials?.quote;
@@ -91,6 +115,8 @@ export function withLiveQuote(financials: TickerFinancials | null, live: Quote |
91
115
  price: live.price,
92
116
  change: live.change,
93
117
  changePercent: live.changePercent,
118
+ previousClose: live.previousClose ?? base.previousClose,
119
+ ...Object.fromEntries(SESSION_FIELDS.map((key) => [key, live[key]])),
94
120
  lastUpdated: live.lastUpdated,
95
121
  stale: live.stale,
96
122
  ...(live.dataSource ? { dataSource: live.dataSource } : {}),
@@ -103,11 +129,6 @@ export function withLiveQuote(financials: TickerFinancials | null, live: Quote |
103
129
  return {
104
130
  ...financials,
105
131
  quote: { ...quote, marketCap: scaled(base.marketCap, ratio) },
106
- fundamentals: fundamentals && fundamentals.stale !== true ? {
107
- ...fundamentals,
108
- trailingPE: scaled(fundamentals.trailingPE, ratio),
109
- forwardPE: scaled(fundamentals.forwardPE, ratio),
110
- marketCap: fundamentals.marketCapCurrency === live.currency ? scaled(fundamentals.marketCap, ratio) : fundamentals.marketCap,
111
- } : fundamentals,
132
+ fundamentals: fundamentals && fundamentals.stale !== true ? movePriceDerivedFigures(fundamentals, live.currency, ratio) : fundamentals,
112
133
  };
113
134
  }
@@ -19,6 +19,7 @@ import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
19
19
  import type { TickerFinancials } from "../../../types/financials";
20
20
  import { normalizeSymbol } from "../../../utils/exchanges";
21
21
  import { colors, priceColor } from "../../../theme/colors";
22
+ import { extendedSessionStateLabel, type ExtendedSession } from "../../../market-data/market/status";
22
23
  import { compareSortValues, nextHeaderSort, type SortDirection } from "../../../utils/sort-values";
23
24
  import { formatCompact, formatCurrency, formatLevelPercent, formatNumber, formatPercent, formatPercentRaw } from "../../../utils/format";
24
25
  import { parseDisplayDate } from "../../../utils/datetime-format";
@@ -30,7 +31,8 @@ import { useBoundTicker as useSymbolBinding } from "../shared/ticker-request";
30
31
  import { useFxRatesMap } from "../../../market-data/hooks";
31
32
  import { RELATIVE_VALUATION_STALE_FUNDAMENTALS_NOTICE, relativeValuationValues, withLiveQuote } from "./relative-valuation-model";
32
33
 
33
- type RelativeColumnId = "symbol" | "price" | "changePercent" | "marketCap" | "trailingPE" | "forwardPE" | "evSales" | "fcfYield" | "revenueGrowth" | "operatingMargin";
34
+ type ExtendedColumnId = "preMarket" | "afterHours" | "overnight";
35
+ type RelativeColumnId = "symbol" | "price" | "changePercent" | ExtendedColumnId | "marketCap" | "trailingPE" | "forwardPE" | "evSales" | "fcfYield" | "revenueGrowth" | "operatingMargin";
34
36
  type RelativeColumn = DataTableColumn & { id: RelativeColumnId };
35
37
  type RelativeRow = ReturnType<typeof relativeValuationValues> & {
36
38
  symbol: string;
@@ -45,7 +47,24 @@ function relativeSymbolsFromPane(symbol: string | null, paneSettings: Record<str
45
47
  return symbol ? [symbol] : [];
46
48
  }
47
49
 
48
- function buildRelativeColumns(baseCurrency: string): RelativeColumn[] {
50
+ const EXTENDED_COLUMN_IDS: Record<ExtendedSession, ExtendedColumnId> = { PRE: "preMarket", POST: "afterHours", OVERNIGHT: "overnight" };
51
+
52
+ /** The extended sessions some peer has a print for, in the order their columns go. */
53
+ function extendedSessionsOf(rows: readonly RelativeRow[]): ExtendedSession[] {
54
+ return (["PRE", "POST", "OVERNIGHT"] as const).filter((session) => rows.some((row) => row.extendedSession === session));
55
+ }
56
+
57
+ /** A peer's move in the extended-session column `columnId`, from the close in LAST. */
58
+ function extendedMove(row: RelativeRow, columnId: ExtendedColumnId): number | null {
59
+ return row.extendedSession != null && EXTENDED_COLUMN_IDS[row.extendedSession] === columnId ? row.extendedChangePercent : null;
60
+ }
61
+
62
+ /**
63
+ * LAST and CHG% are the regular session, as `quote` reads them; a PRE-MKT,
64
+ * AFTER-HRS or OVERNIGHT column, named as BI and MOST name it, follows CHG%
65
+ * for each session in `extendedSessions`.
66
+ */
67
+ function buildRelativeColumns(baseCurrency: string, extendedSessions: readonly ExtendedSession[]): RelativeColumn[] {
49
68
  const symbolWidth = 8;
50
69
  const priceWidth = 10;
51
70
  const pctWidth = 8;
@@ -55,6 +74,9 @@ function buildRelativeColumns(baseCurrency: string): RelativeColumn[] {
55
74
  { id: "symbol", label: "TICKER", width: symbolWidth, align: "left" },
56
75
  { id: "price", label: "LAST", width: priceWidth, align: "right" },
57
76
  { id: "changePercent", label: "CHG%", width: pctWidth, align: "right" },
77
+ ...extendedSessions.map((session): RelativeColumn => ({
78
+ id: EXTENDED_COLUMN_IDS[session], label: extendedSessionStateLabel(session), width: 9, align: "right",
79
+ })),
58
80
  { id: "marketCap", label: `MCAP ${baseCurrency}`, width: capWidth, align: "right" },
59
81
  { id: "trailingPE", label: "P/E", width: metricWidth, align: "right" },
60
82
  { id: "forwardPE", label: "FWD", width: metricWidth, align: "right" },
@@ -74,7 +96,9 @@ interface RelativeSortPreference {
74
96
  const DEFAULT_RELATIVE_SORT: RelativeSortPreference = { columnId: "marketCap", direction: "desc" };
75
97
 
76
98
  function relativeSortValue(row: RelativeRow, columnId: RelativeColumnId): string | number | null {
77
- return columnId === "symbol" ? row.symbol.toLocaleLowerCase() : row[columnId];
99
+ if (columnId === "symbol") return row.symbol.toLocaleLowerCase();
100
+ if (columnId === "preMarket" || columnId === "afterHours" || columnId === "overnight") return extendedMove(row, columnId);
101
+ return row[columnId];
78
102
  }
79
103
 
80
104
  function sortRelativeRowIndices(
@@ -153,7 +177,12 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
153
177
  };
154
178
  }), [liveQuotes, peers]);
155
179
  const fxRates = useFxRatesMap([baseCurrency, ...rows.map((row) => row.marketCapCurrency)]);
156
- const columns = useMemo(() => buildRelativeColumns(baseCurrency), [baseCurrency]);
180
+ // A pre-market or after-hours print gets its own column when a peer has one.
181
+ const extendedSessionsKey = extendedSessionsOf(rows).join(",");
182
+ const columns = useMemo(
183
+ () => buildRelativeColumns(baseCurrency, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : []),
184
+ [baseCurrency, extendedSessionsKey],
185
+ );
157
186
 
158
187
  const comparableRows = useMemo(() => rows.map((row) => ({
159
188
  ...row, marketCap: convertMarketCapitalization(row.marketCap, row.marketCapCurrency, baseCurrency, fxRates),
@@ -195,6 +224,12 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
195
224
  return { text: row.price != null ? formatCurrency(row.price, row.currency ?? "USD") : "-", color: colors.text };
196
225
  case "changePercent":
197
226
  return { text: row.changePercent != null ? formatPercentRaw(row.changePercent) : "-", color: priceColor(row.changePercent ?? 0) };
227
+ case "preMarket":
228
+ case "afterHours":
229
+ case "overnight": {
230
+ const move = extendedMove(row, column.id);
231
+ return { text: move != null ? formatPercentRaw(move) : "-", value: move, color: move == null ? colors.textDim : priceColor(move) };
232
+ }
198
233
  case "marketCap":
199
234
  return { text: formatCompact(row.marketCap ?? undefined, { fixedDecimals: true }), color: colors.textDim };
200
235
  case "trailingPE":
@@ -66,6 +66,12 @@ function buildColumns(width: number, dated: boolean): DataTableColumn[] {
66
66
  ];
67
67
  }
68
68
 
69
+ function sideColor(side: ScannerFlowEvent["side"]): string {
70
+ if (side === "ask") return colors.positive;
71
+ if (side === "bid") return colors.negative;
72
+ return side === "mid" ? colors.text : colors.textDim;
73
+ }
74
+
69
75
  // Stable table adapters so memoized rows survive feed ticks.
70
76
  const eventKey = (event: ScannerFlowEvent) => event.id;
71
77
 
@@ -90,7 +96,8 @@ function renderCell(
90
96
  case "expiry":
91
97
  return { text: formatFlowExpiry(event.expiry), color: colors.textDim };
92
98
  case "side":
93
- return { text: formatFlowSide(event.side), color: colors.textDim };
99
+ // Bought at the ask reads green, sold at the bid red, as on every tape; the word stays.
100
+ return { text: formatFlowSide(event.side), color: sideColor(event.side) };
94
101
  case "size":
95
102
  return { text: formatCompact(event.size) };
96
103
  case "premium":
@@ -1,5 +1,4 @@
1
- import { Box, Text } from "../../../ui";
2
- import { colors } from "../../../theme/colors";
1
+ import { EmptyState } from "../../../components";
3
2
  import { formatShortDate } from "../../../utils/datetime-format";
4
3
  import { formatCompact, formatCurrency, formatNumber } from "../../../utils/format";
5
4
  import { transactionTypeLabel, type InsiderTransaction } from "../insider/insider-data";
@@ -21,17 +20,8 @@ export function formatFilingFormLabel(form: string, fallback = "FORM 4"): string
21
20
  return value ? `FORM ${value}` : fallback;
22
21
  }
23
22
 
24
- export function renderFilingNotice(message: string, width: number) {
25
- const lines = wrapNoticeLines(message, width - 4);
26
- return (
27
- <Box flexDirection="column" paddingX={1} paddingY={1}>
28
- {lines.map((line, index) => (
29
- <Box key={index} height={1}>
30
- <Text fg={colors.textDim}>{line}</Text>
31
- </Box>
32
- ))}
33
- </Box>
34
- );
23
+ export function renderFilingNotice(message: string) {
24
+ return <EmptyState title={message} />;
35
25
  }
36
26
 
37
27
  export function buildInsiderTransactionTitle(transaction: InsiderTransaction): string {
@@ -61,26 +51,3 @@ export function buildInsiderTransactionDetailBody(transaction: InsiderTransactio
61
51
  ];
62
52
  return lines.join("\n");
63
53
  }
64
-
65
- function wrapNoticeLines(text: string, width: number): string[] {
66
- const maxWidth = Math.max(width, 12);
67
- const words = text.trim().split(/\s+/);
68
- const lines: string[] = [];
69
- let current = "";
70
-
71
- for (const word of words) {
72
- if (!current) {
73
- current = word;
74
- continue;
75
- }
76
- if (current.length + 1 + word.length <= maxWidth) {
77
- current = `${current} ${word}`;
78
- } else {
79
- lines.push(current);
80
- current = word;
81
- }
82
- }
83
-
84
- if (current) lines.push(current);
85
- return lines;
86
- }
@@ -6,16 +6,21 @@ import { getSharedMarketDataCoordinator } from "../../../market-data/coordinator
6
6
  import { useShortcut } from "../../../react/input";
7
7
  import { isPlainKey } from "../../../utils/keyboard";
8
8
  import { instrumentFromTicker } from "../../../market-data/request-types";
9
- import { useDebouncedPluginPaneState } from "../../runtime";
9
+ import { useDebouncedPluginPaneState, usePluginPaneState } from "../../runtime";
10
10
  import { usePaneSettingValue } from "../../../state/app/context";
11
11
  import type { ScrollBoxRenderable } from "../../../ui";
12
- import { EmptyState, FeedDataTableStackView, Spinner, StatGrid, usePaneNoticeFooter, useTableLoadMore, type FeedDataTableItem, type StatItem } from "../../../components";
12
+ import { EmptyState, FeedDataTableStackView, QueryBar, Spinner, TickerBadgeList, usePaneNoticeFooter, useTableLoadMore, type FeedDataTableItem, type PaneHint, type QueryBarFilter } from "../../../components";
13
+ import { displayWidth } from "../../../utils/format";
14
+ import type { DetailFact } from "../../../components/reader/detail-reader";
15
+ import type { ThemeColors } from "../../../theme/colors";
16
+ import { useThemeColors } from "../../../theme/theme-context";
17
+ import { useOpenDescription } from "../shared/open-description";
18
+ import { toneColor } from "../shared/research-cells";
13
19
  import { mayBeUsEquityOrFundTicker, mayBeUsEquityTicker, secFilingItemCodes } from "../../../utils/sec";
14
- import { parseForm4Xml, transactionTypeLabel } from "../insider/insider-data";
20
+ import { parseForm4Xml } from "../insider/insider-data";
15
21
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
16
22
  import {
17
23
  formatFilingMetaDate,
18
- buildInsiderTransactionTitle,
19
24
  buildInsiderTransactionDetailBody,
20
25
  renderFilingNotice,
21
26
  } from "./filing-display";
@@ -37,6 +42,11 @@ import { SignInWall } from "../cloud/auth-actions";
37
42
  import { ETF_FILING_FORMS, ETF_FORMS_SETTING, filterFilingsByForms, parseFormsSetting, SEC_FILING_FETCH_LIMIT } from "./forms";
38
43
  import { createSecHeadless, fundFilingsEmpty, secHeadless } from "./headless";
39
44
  import {
45
+ ALL_FORMS,
46
+ COMPANY_FORM_GROUPS,
47
+ filingTone,
48
+ filterFilingsByGroup,
49
+ FUND_FORM_GROUPS,
40
50
  getFilingDisplayTitle,
41
51
  getFilingColumnText,
42
52
  getFormDescription,
@@ -45,6 +55,7 @@ import {
45
55
  secIssuerLabel,
46
56
  secReportedAcceptance,
47
57
  } from "./model";
58
+ import { rowSummary, rowSummaryTarget } from "./row-summaries";
48
59
  import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
49
60
  import { createSecFilingSearchProvider, resetSecFilingFocusRequests, useSecFilingFocusRequest } from "./command-bar-search";
50
61
 
@@ -53,6 +64,8 @@ export { secHeadless } from "./headless";
53
64
  const SEC_FILING_PAGE_SIZE = 50;
54
65
  const NO_FILINGS: SecFilingItem[] = [];
55
66
  const OWNERSHIP_FORMS = new Set(["3", "4", "5"]);
67
+ /** At most this many rows on screen have their insider forms and fund flow notices read for their row text. */
68
+ const OWNERSHIP_ROWS_READ = 40;
56
69
 
57
70
  function formatFiledAt(filing: SecFilingItem): string {
58
71
  return formatFilingMetaDate(filing.filingDate);
@@ -122,16 +135,6 @@ function buildDetailBodyWithDocuments({
122
135
  return lines.join("\n");
123
136
  }
124
137
 
125
- function buildForm4Preview(content: string | null): string | null {
126
- if (!content) return null;
127
- const transactions = parseForm4Xml(content);
128
- const tx = transactions[0];
129
- if (!tx) return null;
130
- if (transactions.length === 1) return `${tx.reportedName} — ${buildInsiderTransactionTitle(tx)}`;
131
- const types = [...new Set(transactions.map((transaction) => transactionTypeLabel(transaction.transactionType)))];
132
- return `${tx.reportedName} — ${transactions.length} transactions: ${types.join(", ")}`;
133
- }
134
-
135
138
  function buildForm4Detail(content: string | null, filing: SecFilingItem): string {
136
139
  if (!content) return buildDetailBody(filing);
137
140
  const transactions = parseForm4Xml(content);
@@ -149,6 +152,9 @@ function buildForm4Detail(content: string | null, filing: SecFilingItem): string
149
152
  }
150
153
 
151
154
  function toFeedItems(
155
+ colors: ThemeColors,
156
+ issuerTicker: { symbol: string; open: () => void } | null,
157
+ showIssuer: boolean,
152
158
  filings: SecFilingItem[],
153
159
  selectedAccessionNumber: string | undefined,
154
160
  contentCache: Map<string, string | null>,
@@ -169,10 +175,10 @@ function toFeedItems(
169
175
  ? `${buildDetailBody(filing)}\n\nReadable filing content was not available for this document.`
170
176
  : buildDetailBody(filing);
171
177
 
172
- // For Form 4s, build structured preview and detail from parsed XML
173
- const form4Preview = isOwnership && hasFetchedContent
174
- ? buildForm4Preview(fetchedContent ?? null)
175
- : null;
178
+ // An insider form read once says who traded and how, a fund's 24F-2NT its
179
+ // year's flows; the row says it too.
180
+ const summaryTarget = rowSummaryTarget(filing);
181
+ const readSummary = summaryTarget ? rowSummary(filing.form, contentCache.get(summaryTarget.accessionNumber)) : null;
176
182
  const form4Detail = isOwnership && hasFetchedContent
177
183
  ? buildForm4Detail(fetchedContent ?? null, filing)
178
184
  : null;
@@ -197,13 +203,28 @@ function toFeedItems(
197
203
  const enrichedTitle = formDesc
198
204
  ? `${displayTitle} — ${formDesc}`
199
205
  : displayTitle;
206
+ const summary = readSummary?.title ?? getFilingColumnText(filing);
207
+ const items = secFilingItemCodes(filing.items);
208
+ const facts: DetailFact[] = [
209
+ { label: "Issuer", value: filing.companyName || "Name unavailable" },
210
+ ...(issuerTicker ? [{ label: "Ticker", value: issuerTicker.symbol, link: { label: `Open ${issuerTicker.symbol}`, onOpen: issuerTicker.open } }] : []),
211
+ { label: "CIK", value: filing.cik },
212
+ { label: "Form", value: formDesc ? `${filing.form} · ${formDesc}` : filing.form, color: toneColor(filingTone(filing, readSummary?.direction), colors) },
213
+ { label: "Filed", value: formatFiledAt(filing) },
214
+ ...(acceptedAt ? [{ label: "SEC-reported acceptance", value: acceptedAt }] : []),
215
+ ...(items ? [{ label: "Items", value: items }] : []),
216
+ { label: "Accession", value: filing.accessionNumber },
217
+ ];
200
218
 
201
219
  return {
202
220
  id: filing.accessionNumber,
203
221
  eyebrow: filing.form,
204
- title: form4Preview || getFilingColumnText(filing),
222
+ eyebrowColor: toneColor(filingTone(filing, readSummary?.direction), colors),
223
+ // A ticker shared by several filers names the filer on each row.
224
+ title: showIssuer && filing.companyName ? `${filing.companyName}: ${summary}` : summary,
205
225
  timestamp: filing.filingDate,
206
226
  detailTitle: enrichedTitle,
227
+ detailFacts: facts,
207
228
  detailMeta: [
208
229
  secIssuerLabel(filing),
209
230
  `Filed ${formatFiledAt(filing)}`,
@@ -218,8 +239,11 @@ function toFeedItems(
218
239
  });
219
240
  }
220
241
 
221
- function SecView({ width, height, focused }: { width: number; height: number; focused: boolean }) {
242
+ /** `paneType` is set for the standalone pane and absent in the DES tab, whose header already names the security. */
243
+ function SecView({ width, height, focused, paneType }: { width: number; height: number; focused: boolean; paneType?: string }) {
222
244
  const { ticker } = usePaneTickerIdentity();
245
+ const colors = useThemeColors();
246
+ const openDescription = useOpenDescription();
223
247
  const selectionKey = `selectedIdx:${ticker?.metadata.ticker ?? "none"}`;
224
248
  const [selectedIdx, setSelectedIdx] = useDebouncedPluginPaneState<number>(selectionKey, 0);
225
249
  // The open filing is what the pane shows, so it is pane state: it restores
@@ -251,10 +275,22 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
251
275
  const issuerFilings = useResolvedEntryValue(filingsEntry) ?? NO_FILINGS;
252
276
  // The filter runs over every filing the service returned for the issuer,
253
277
  // the same list the unfiltered pane shows, not over a first page of it.
254
- const filings = useMemo(
278
+ const formFilings = useMemo(
255
279
  () => (forms ? filterFilingsByForms(issuerFilings, forms) : issuerFilings),
256
280
  [forms, issuerFilings],
257
281
  );
282
+ // The fund view (ETF) has its own groups; each view keeps its own choice.
283
+ const groups = forms ? FUND_FORM_GROUPS : COMPANY_FORM_GROUPS;
284
+ const [storedGroup, setFormGroup] = usePluginPaneState<string>(forms ? "fundFormGroup" : "formGroup", ALL_FORMS);
285
+ const formGroup = groups.some((group) => group.value === storedGroup) ? storedGroup : ALL_FORMS;
286
+ // A ticker can map to more than one filer (CIK); each can be listed alone.
287
+ const issuers = useMemo(() => secFilingIssuers(formFilings), [formFilings]);
288
+ const [storedIssuer, setIssuer] = usePluginPaneState<string>("issuerCik", ALL_FORMS);
289
+ const issuerCik = issuers.some((issuer) => issuer.cik === storedIssuer) ? storedIssuer : ALL_FORMS;
290
+ const filings = useMemo(() => {
291
+ const grouped = filterFilingsByGroup(formFilings, groups, formGroup);
292
+ return issuerCik === ALL_FORMS ? grouped : grouped.filter((filing) => filing.cik === issuerCik);
293
+ }, [formFilings, formGroup, groups, issuerCik]);
258
294
  const filterHistoryCapped = !!forms && issuerFilings.length >= SEC_FILING_FETCH_LIMIT;
259
295
  const [visibleCount, setVisibleCount] = useState(SEC_FILING_PAGE_SIZE);
260
296
  const visibleFilings = useMemo(() => filings.slice(0, visibleCount), [filings, visibleCount]);
@@ -266,7 +302,7 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
266
302
  );
267
303
  useEffect(() => {
268
304
  setVisibleCount(SEC_FILING_PAGE_SIZE);
269
- }, [ticker?.metadata.ticker, ticker?.metadata.exchange, formsSetting]);
305
+ }, [ticker?.metadata.ticker, ticker?.metadata.exchange, formsSetting, formGroup, issuerCik]);
270
306
  const loading = filingsEntry?.phase === "loading" || filingsEntry?.phase === "refreshing";
271
307
  // A restored or shared open filing can sit past the first page, or be gone
272
308
  // from the feed entirely.
@@ -284,7 +320,7 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
284
320
  const error = filingsEntry?.error && filingsEntry.error.reasonCode !== "NO_DATA" ? filingsEntry.error.message : null;
285
321
  // Hosted, the only filings source is Gloom Cloud, which needs an account.
286
322
  const cloudSession = useResearchCloudSession();
287
- const authWall = filings.length === 0 && isCloudSessionRequired(error);
323
+ const authWall = formFilings.length === 0 && isCloudSessionRequired(error);
288
324
 
289
325
  const openFiling = openItemId
290
326
  ? filings.find((filing) => filing.accessionNumber === openItemId) ?? null
@@ -298,14 +334,30 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
298
334
  || documentsEntry?.phase === "refreshing"
299
335
  );
300
336
 
301
- // Only the filing in view needs its content; queueing every ownership form in
302
- // the list fires dozens of SEC requests the user never looks at.
337
+ // The open filing's content comes first. Insider forms say who traded only
338
+ // in their text, so the ones around the cursor are read after it, one at a
339
+ // time, for their rows; the rest of the list is never queued, which would
340
+ // fire dozens of requests for rows nobody looks at. Read text is cached.
303
341
  const selectedFiling = visibleFilings[selectedIdx];
304
- const contentTargets = useMemo(() => [
342
+ // The rows on screen, as a share of the list so it holds for terminal rows and desktop pixels alike.
343
+ const [onScreen, setOnScreen] = useState({ first: 0, count: height });
344
+ const trackScroll = useCallback(() => {
345
+ loadMore();
346
+ const box = scrollRef.current;
347
+ if (!box?.viewport || !(box.scrollHeight > 0) || visibleFilings.length === 0) return;
348
+ const first = Math.floor((box.scrollTop / box.scrollHeight) * visibleFilings.length);
349
+ const count = Math.ceil((box.viewport.height / box.scrollHeight) * visibleFilings.length) + 1;
350
+ setOnScreen((current) => (current.first === first && current.count === count ? current : { first, count }));
351
+ }, [loadMore, visibleFilings.length]);
352
+ const nearbyOwnership = useMemo(() => [
353
+ ...(selectedFiling ? [selectedFiling] : []),
354
+ ...visibleFilings.slice(onScreen.first, onScreen.first + Math.min(OWNERSHIP_ROWS_READ, onScreen.count)),
355
+ ].map(rowSummaryTarget).filter((target): target is SecFilingItem => !!target), [onScreen, selectedFiling, visibleFilings]);
356
+ const viewedTargets = useMemo(() => [
305
357
  ...(openFiling ? [openFiling] : []),
306
358
  ...buildInlineFilingContentTargets(openFiling, openDocuments),
307
- ...(selectedFiling && OWNERSHIP_FORMS.has(selectedFiling.form.trim()) ? [selectedFiling] : []),
308
- ], [openDocuments, openFiling, selectedFiling]);
359
+ ], [openDocuments, openFiling]);
360
+ const contentTargets = useMemo(() => [...viewedTargets, ...nearbyOwnership], [nearbyOwnership, viewedTargets]);
309
361
  const { contentCache, contentErrors, retry: retryContent, loadingKey } = useSecFilingContentCache({
310
362
  scopeKey: `${ticker?.metadata.ticker ?? "none"}:${ticker?.metadata.exchange ?? ""}:${eligibleTicker}`,
311
363
  targets: contentTargets,
@@ -352,14 +404,24 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
352
404
  }
353
405
  }, [selectedIdx, setSelectedIdx, visibleFilings.length]);
354
406
 
407
+ // The pane's own ticker opens its description; a fund or equity both have one.
408
+ const issuerTicker = useMemo(() => (ticker && paneType ? {
409
+ symbol: ticker.metadata.ticker,
410
+ open: () => openDescription(ticker.metadata.ticker, ticker.metadata.exchange || undefined),
411
+ } : null), [openDescription, paneType, ticker]);
412
+ const hints = useMemo<PaneHint[]>(() => (issuerTicker
413
+ ? [{ id: "sec-description", key: "d", label: "es", title: `Open ${issuerTicker.symbol}`, onPress: issuerTicker.open }]
414
+ : []), [issuerTicker]);
355
415
  usePaneStatusLinkFooter({
356
416
  registrationId: "sec",
357
417
  focused,
358
418
  url: openFiling?.filingUrl,
359
419
  source: openFiling?.form,
360
420
  label: "filing",
361
- loading: loading || loadingDocuments || !!loadingKey,
421
+ // Rows read in the background for their insiders do not hold up the pane.
422
+ loading: loading || loadingDocuments || (!!loadingKey && viewedTargets.some((target) => target.accessionNumber === loadingKey)),
362
423
  error: authWall ? null : error ?? detailError,
424
+ hints,
363
425
  showOpenHint: !!openFiling?.filingUrl,
364
426
  });
365
427
 
@@ -373,25 +435,48 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
373
435
  title: "Fund filings",
374
436
  });
375
437
 
376
- // One cell per filer: a ticker can map to more than one CIK.
377
- const issuerItems = useMemo<StatItem[]>(() => secFilingIssuers(filings).map((issuer) => ({
378
- id: issuer.cik,
379
- label: "Issuer",
380
- value: issuer.companyName || "Name unavailable",
381
- detail: `CIK ${issuer.cik}`,
382
- })), [filings]);
438
+ const queryFilters = useMemo<QueryBarFilter[]>(() => [
439
+ {
440
+ id: "form",
441
+ label: "Form",
442
+ value: formGroup,
443
+ defaultValue: ALL_FORMS,
444
+ options: [{ value: ALL_FORMS, label: forms ? "All fund forms" : "All forms" }, ...groups.map((group) => ({ value: group.value, label: group.label }))],
445
+ onChange: (value: string) => { setFormGroup(value); setSelectedIdx(0, { immediate: true }); setOpenItemId(null); },
446
+ },
447
+ ...(issuers.length > 1 ? [{
448
+ id: "issuer",
449
+ label: "Filer",
450
+ value: issuerCik,
451
+ defaultValue: ALL_FORMS,
452
+ options: [{ value: ALL_FORMS, label: "All filers" }, ...issuers.map((issuer) => ({ value: issuer.cik, label: issuer.companyName || `CIK ${issuer.cik}` }))],
453
+ onChange: (value: string) => { setIssuer(value); setSelectedIdx(0, { immediate: true }); setOpenItemId(null); },
454
+ }] : []),
455
+ ], [formGroup, forms, groups, issuerCik, issuers, setFormGroup, setIssuer, setOpenItemId, setSelectedIdx]);
456
+ // The ticker and the filer once, beside the filters: every row is their filing.
457
+ const issuerMeta = issuers.length === 1 ? secIssuerLabel(issuers[0]!).replace(/CIK 0+/, "CIK ") : undefined;
458
+ const queryBar = (
459
+ <QueryBar
460
+ width={width}
461
+ filters={queryFilters}
462
+ meta={issuerMeta}
463
+ leading={issuerTicker
464
+ ? <TickerBadgeList symbols={[issuerTicker.symbol]} width={displayWidth(issuerTicker.symbol) + 2} listed onOpen={issuerTicker.open} />
465
+ : undefined}
466
+ />
467
+ );
383
468
 
384
469
  if (!ticker) {
385
470
  return <EmptyState title="No ticker selected." message="Select a ticker to view SEC filings." />;
386
471
  }
387
472
  if (!eligibleTicker) {
388
- return renderFilingNotice(forms ? "Fund filings are only shown for US-listed funds and equities." : "SEC filings are only shown for US equities.", width);
473
+ return renderFilingNotice(forms ? "Fund filings are only shown for US-listed funds and equities." : "SEC filings are only shown for US equities.");
389
474
  }
390
475
  if (authWall) return <SignInWall placement="sec-signin" action="view SEC filings" needsVerification={cloudSession.needsVerification} />;
391
476
  if (loading && filings.length === 0) return <Spinner label="Loading SEC filings..." />;
392
- if (error && filings.length === 0) return <EmptyState title="SEC filings unavailable." message={error} />;
393
- if (filings.length === 0) {
394
- return renderFilingNotice(forms ? `No fund filings for ${ticker.metadata.ticker}.` : `No recent SEC filings for ${ticker.metadata.ticker}.`, width);
477
+ if (error && formFilings.length === 0) return <EmptyState title="SEC filings unavailable." message={error} />;
478
+ if (formFilings.length === 0) {
479
+ return renderFilingNotice(forms ? `No fund filings for ${ticker.metadata.ticker}.` : `No recent SEC filings for ${ticker.metadata.ticker}.`);
395
480
  }
396
481
 
397
482
  return (
@@ -400,6 +485,9 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
400
485
  height={height}
401
486
  focused={focused}
402
487
  items={toFeedItems(
488
+ colors,
489
+ issuerTicker,
490
+ issuers.length > 1 && issuerCik === ALL_FORMS,
403
491
  visibleFilings,
404
492
  openFiling?.accessionNumber,
405
493
  contentCache,
@@ -414,12 +502,12 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
414
502
  openItemId={openItemId}
415
503
  onOpenItemIdChange={setOpenItemId}
416
504
  prefetchDetail={prefetchDocuments}
417
- rootBefore={<StatGrid items={issuerItems} width={width} />}
505
+ rootBefore={queryBar}
418
506
  sourceLabel="Form"
419
507
  titleLabel="Filing"
420
- emptyStateTitle="No SEC filings."
508
+ emptyStateTitle={formGroup === ALL_FORMS ? "No SEC filings." : "No filings of these forms."}
421
509
  scrollRef={scrollRef}
422
- onBodyScrollActivity={loadMore}
510
+ onBodyScrollActivity={trackScroll}
423
511
  />
424
512
  );
425
513
  }