@gloom-sh/terminal 0.19.0 → 0.19.1

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 (203) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +23 -1
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/data.ts +2 -1
  9. package/src/api-client/hiring.ts +3 -0
  10. package/src/api-client/index.ts +4 -0
  11. package/src/api-client/ipo.ts +24 -1
  12. package/src/api-client/mna.ts +3 -0
  13. package/src/api-client/paths.ts +9 -1
  14. package/src/api-client/roadmap.ts +85 -0
  15. package/src/api-client/types.ts +12 -0
  16. package/src/architecture/core-builtin-allowlist.json +2 -2
  17. package/src/cli/commands/market.ts +35 -12
  18. package/src/cli/commands/news-search.ts +22 -0
  19. package/src/cli/commands/overview.ts +86 -20
  20. package/src/cli/commands/ticker.ts +32 -2
  21. package/src/cli/errors.ts +17 -2
  22. package/src/cli/glossary.ts +6 -0
  23. package/src/cli/index-roots.ts +0 -7
  24. package/src/cli/listing-arg.ts +57 -10
  25. package/src/cli/pane-functions/capabilities.ts +1 -0
  26. package/src/cli/pane-functions/discovery.ts +21 -1
  27. package/src/cli/pane-functions/freshness.ts +7 -1
  28. package/src/cli/pane-functions/function-help.ts +7 -0
  29. package/src/cli/pane-functions/index.ts +6 -3
  30. package/src/cli/quote-answer.ts +1 -1
  31. package/src/cli/saved-copy-persistence.ts +88 -0
  32. package/src/components/command-bar/list/model.ts +5 -0
  33. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  34. package/src/components/command-bar/selection-runtime.ts +52 -3
  35. package/src/components/command-bar/surface/index.tsx +2 -0
  36. package/src/components/layout/pane/capture-stamp.ts +2 -2
  37. package/src/components/layout/pane/footer/index.tsx +22 -6
  38. package/src/components/layout/pane/footer/model.ts +83 -15
  39. package/src/components/layout/pane/status-footer.ts +35 -2
  40. package/src/components/layout/status-widget-space.ts +9 -0
  41. package/src/components/ticker/list-table-view.tsx +4 -20
  42. package/src/components/ui/display.tsx +18 -3
  43. package/src/components/ui/icon.tsx +4 -1
  44. package/src/data/plugin-cache.ts +5 -3
  45. package/src/data/saved-copies.ts +8 -0
  46. package/src/docs/snapshot.json +32 -5
  47. package/src/i18n/es.ts +2 -4
  48. package/src/i18n/ja.ts +2 -4
  49. package/src/i18n/ko.ts +2 -4
  50. package/src/i18n/zh-cn.ts +2 -4
  51. package/src/i18n/zh-tw.ts +2 -4
  52. package/src/market-data/depositary-pair.ts +25 -1
  53. package/src/market-data/quotes/metadata.ts +7 -0
  54. package/src/news/aggregator.ts +7 -1
  55. package/src/news/types.ts +6 -0
  56. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  57. package/src/plugins/builtin/alerts/cli.ts +34 -11
  58. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  59. package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
  60. package/src/plugins/builtin/alerts/headless.ts +10 -6
  61. package/src/plugins/builtin/cds/client.ts +142 -30
  62. package/src/plugins/builtin/cds/headless.ts +35 -8
  63. package/src/plugins/builtin/cds/model.ts +145 -18
  64. package/src/plugins/builtin/cds/pane.tsx +16 -5
  65. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  66. package/src/plugins/builtin/chart-composer/index.tsx +4 -5
  67. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  68. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  69. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  70. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  71. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  72. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  73. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  74. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  75. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  76. package/src/plugins/builtin/clinical-trials/model.ts +86 -22
  77. package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
  78. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  79. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
  80. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  81. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  82. package/src/plugins/builtin/company-attention/pane.tsx +3 -0
  83. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  84. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  85. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  86. package/src/plugins/builtin/composite-plugins.ts +2 -1
  87. package/src/plugins/builtin/credit-documents/headless.ts +3 -3
  88. package/src/plugins/builtin/doe/headless.ts +3 -2
  89. package/src/plugins/builtin/doe/pane.tsx +13 -3
  90. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  91. package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
  92. package/src/plugins/builtin/earnings-calls/pane.tsx +7 -0
  93. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  94. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  95. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  96. package/src/plugins/builtin/econ/figures.ts +48 -0
  97. package/src/plugins/builtin/econ/index.tsx +14 -5
  98. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  99. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  100. package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
  101. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  102. package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
  103. package/src/plugins/builtin/help/index.tsx +61 -59
  104. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  105. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  106. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  107. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  108. package/src/plugins/builtin/holders/headless.ts +4 -2
  109. package/src/plugins/builtin/holders/pane.tsx +3 -0
  110. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  111. package/src/plugins/builtin/insider/headless.ts +2 -2
  112. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  113. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  114. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  115. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  116. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  117. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  118. package/src/plugins/builtin/macro-day/model.ts +39 -5
  119. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  120. package/src/plugins/builtin/market-movers/client.ts +2 -0
  121. package/src/plugins/builtin/market-movers/headless.ts +3 -1
  122. package/src/plugins/builtin/market-movers/screener.ts +8 -4
  123. package/src/plugins/builtin/mna/headless.ts +2 -0
  124. package/src/plugins/builtin/mna/pane.tsx +3 -1
  125. package/src/plugins/builtin/news/headless.ts +3 -1
  126. package/src/plugins/builtin/news/index.tsx +3 -0
  127. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
  128. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  129. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  130. package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
  131. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  132. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  133. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  134. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  135. package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
  136. package/src/plugins/builtin/research/event-model.ts +30 -4
  137. package/src/plugins/builtin/research/index.tsx +7 -3
  138. package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
  139. package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
  140. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  141. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  142. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  143. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  144. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  145. package/src/plugins/builtin/roadmap/model.ts +67 -0
  146. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  147. package/src/plugins/builtin/scanner/live-headless.ts +21 -2
  148. package/src/plugins/builtin/sec/headless.ts +2 -1
  149. package/src/plugins/builtin/sec/index.tsx +3 -0
  150. package/src/plugins/builtin/shared/cloud-resource.ts +13 -5
  151. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  152. package/src/plugins/builtin/shared/report-freshness.ts +8 -0
  153. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  154. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  155. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  156. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  157. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  158. package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
  159. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  160. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
  161. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  162. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  163. package/src/plugins/builtin/volatility/pane.tsx +5 -2
  164. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  165. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  166. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  167. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  168. package/src/plugins/catalog-browser.ts +2 -1
  169. package/src/plugins/host-link.ts +1 -1
  170. package/src/portfolio/column-values.ts +11 -7
  171. package/src/renderers/dom/desktop/icons.tsx +7 -0
  172. package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
  173. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  174. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  175. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  176. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  177. package/src/renderers/electrobun/bun/index.ts +26 -2
  178. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  179. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  180. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  181. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  182. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  183. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  184. package/src/renderers/electrobun/view/news-source.ts +2 -0
  185. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  186. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  187. package/src/sources/gloomberb-cloud/index.ts +42 -8
  188. package/src/tickers/company-name-listing.ts +53 -13
  189. package/src/tickers/company-name.ts +6 -1
  190. package/src/tickers/index-roots.ts +7 -0
  191. package/src/tickers/listing-fate.ts +2 -2
  192. package/src/tickers/search/index.ts +6 -2
  193. package/src/tickers/search/ranking.ts +5 -1
  194. package/src/tickers/search/upsert.ts +13 -0
  195. package/src/tickers/ticker-changes.ts +8 -4
  196. package/src/types/company-from.ts +40 -0
  197. package/src/types/data-provider.ts +7 -0
  198. package/src/types/financials.ts +27 -0
  199. package/src/types/headless.ts +6 -0
  200. package/src/utils/crypto-pair.ts +121 -7
  201. package/src/utils/declared-dividends.ts +1 -1
  202. package/src/utils/fundamentals.ts +36 -5
  203. package/src/version.ts +2 -2
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { formatActionChords, hasKeybindingCaptureRequest, subscribeKeybindingCapture, useKeybindings } from "../../../app/keybindings";
3
- import { Notice, Section, SectionHeading, usePaneFooter, usePaneMenuItems, usePaneTabs, type PaneHint, type TableSection } from "../../../components";
3
+ import { Notice, usePaneFooter, usePaneMenuItems, usePaneTabs, type PaneHint, type TableSection } from "../../../components";
4
4
  import { t, tf } from "../../../i18n";
5
5
  import { colors } from "../../../theme/colors";
6
6
  import type { PaneProps } from "../../../types/plugin";
@@ -18,6 +18,7 @@ import { FunctionsTable } from "./functions-table";
18
18
  import { KeybindingsEditor } from "./keybindings-editor";
19
19
  import { ShortcutTable, type ShortcutTableEntry } from "./shortcut-table";
20
20
  import { resolveCommandShortcuts, resolveWindowTemplates } from "./shortcut-model";
21
+ import { badgeColumnWidth } from "./table-cells";
21
22
 
22
23
  const HELP_TABS = [
23
24
  { label: "Basics", value: "basics" },
@@ -47,6 +48,9 @@ const TRY_THESE = [
47
48
  { query: "EE TSLA", description: "What analysts expect from Tesla" },
48
49
  ] as const;
49
50
 
51
+ /** The most of a table its key column takes, however many chips a row carries. */
52
+ const KEY_COLUMN_MAX_SHARE = 0.4;
53
+
50
54
  function HelpPane({ focused, width, height }: PaneProps) {
51
55
  const registry = getSharedRegistry();
52
56
  const { openCommandBar, showPane, createPaneFromTemplate } = usePluginAppActions();
@@ -83,7 +87,7 @@ function HelpPane({ focused, width, height }: PaneProps) {
83
87
  // The UPGRADE line says how long the trial is; the length comes from /pricing.
84
88
  const [pricing, setPricing] = useState<CloudPricing | null>(null);
85
89
  useEffect(() => {
86
- if (activeTabId !== "basics" || pricing) return;
90
+ if (activeTabId !== "reference" || pricing) return;
87
91
  let live = true;
88
92
  void apiClient.getCloudPricing()
89
93
  .then((next) => {
@@ -131,10 +135,11 @@ function HelpPane({ focused, width, height }: PaneProps) {
131
135
  // Each tab's actions. `l` moves between tabs, so layout actions take `a`.
132
136
  const tabHints = useMemo<PaneHint[]>(() => {
133
137
  if (activeTabId === "basics") {
134
- return [
135
- { id: "docs", key: "d", label: "ocs", title: "Open Docs", onPress: openDocs },
136
- { id: "layout-actions", key: "a", label: " layout actions", title: "Layout Actions", onPress: openLayoutActions },
137
- ];
138
+ return [{ id: "docs", key: "d", label: "ocs", title: "Open Docs", onPress: openDocs }];
139
+ }
140
+ // The layout rows are here, so their action is.
141
+ if (activeTabId === "reference") {
142
+ return [{ id: "layout-actions", key: "a", label: " layout actions", title: "Layout Actions", onPress: openLayoutActions }];
138
143
  }
139
144
  if (activeTabId === "functions") {
140
145
  return [{ id: "plugins", key: "p", label: "lugins", title: "Manage Plugins", onPress: openPluginManager }];
@@ -192,32 +197,31 @@ function HelpPane({ focused, width, height }: PaneProps) {
192
197
  ].filter((item) => hasCode(item.badges[0]!)),
193
198
  }], [hasCode]);
194
199
 
195
- const commandBarSections = useMemo<Array<TableSection<ShortcutTableEntry>>>(() => [{
196
- label: "Command Bar",
197
- items: [
198
- ...(commandBarBadges.length > 0
199
- ? [entry("open", commandBarBadges, "Open command mode for actions, pane commands, and typed prefixes.")]
200
- : []),
201
- ...(tickerSearchBadges.length > 0
202
- ? [entry("ticker-search", tickerSearchBadges, "Open ticker search directly.")]
203
- : []),
204
- entry("des", ["DES", "<ticker>"], "Open security details for a specific ticker."),
205
- entry("help-card", ["HELP", "<function>"], "Open a function's help card: what it shows, its keys, how fresh its data is. HELP HELP reaches support."),
206
- entry("docs", ["DOCS", "<function>"], "Read the docs and Academy lessons in the app. The ? on a pane opens its entry."),
207
- entry("upgrade", ["UPGRADE"], tf("Go Pro for real-time data at gloom.sh/cloud, free for {days} days.", { days: trialDays })),
208
- entry("mcp", ["MCP"], "Connect Claude Code, Codex, Cursor or any MCP client to Gloom's research tools."),
209
- entry("move", ["Up/Down", "Ctrl+P/N"], "Move through command bar results."),
210
- entry("page", ["PageUp/PageDown", "Ctrl+Home/End"], "Jump a page, or to the first or last result."),
211
- entry("run", ["Enter", "Shift+Enter"], "Run the selected result or its secondary action."),
212
- entry("accept-arg", ["Tab"], "Accept a suggested command argument when one is available."),
213
- entry("close", ["Esc", ...tickerSearchBadges.filter((badge) => badge === "`" || badge.includes("+"))], "Close the command bar."),
214
- entry("clear", ["Ctrl+U"], "Clear command text."),
215
- entry("delete-word", ["Ctrl+W"], "Delete the previous word in command text."),
216
- entry("back", ["Backspace"], "Go back from a nested command screen when the query is empty."),
217
- ],
218
- }], [commandBarBadges, tickerSearchBadges, trialDays]);
219
-
220
200
  const referenceSections = useMemo<Array<TableSection<ShortcutTableEntry>>>(() => [
201
+ {
202
+ label: "Command Bar",
203
+ items: [
204
+ ...(commandBarBadges.length > 0
205
+ ? [entry("cmd-open", commandBarBadges, "Open command mode for actions, pane commands, and typed prefixes.")]
206
+ : []),
207
+ ...(tickerSearchBadges.length > 0
208
+ ? [entry("cmd-ticker-search", tickerSearchBadges, "Open ticker search directly.")]
209
+ : []),
210
+ entry("cmd-des", ["DES", "<ticker>"], "Open security details for a specific ticker."),
211
+ entry("cmd-help-card", ["HELP", "<function>"], "Open a function's help card: what it shows, its keys, how fresh its data is. HELP HELP reaches support."),
212
+ entry("cmd-docs", ["DOCS", "<function>"], "Read the docs and Academy lessons in the app. The ? on a pane opens its entry."),
213
+ entry("cmd-upgrade", ["UPGRADE"], tf("Go Pro for real-time data at gloom.sh/cloud, free for {days} days.", { days: trialDays })),
214
+ entry("cmd-mcp", ["MCP"], "Connect Claude Code, Codex, Cursor or any MCP client to Gloom's research tools."),
215
+ entry("cmd-move", ["Up/Down", "Ctrl+P/N"], "Move through command bar results."),
216
+ entry("cmd-page", ["PageUp/PageDown", "Ctrl+Home/End"], "Jump a page, or to the first or last result."),
217
+ entry("cmd-run", ["Enter", "Shift+Enter"], "Run the selected result or its secondary action."),
218
+ entry("cmd-accept-arg", ["Tab"], "Accept a suggested command argument when one is available."),
219
+ entry("cmd-close", ["Esc", ...tickerSearchBadges.filter((badge) => badge === "`" || badge.includes("+"))], "Close the command bar."),
220
+ entry("cmd-clear", ["Ctrl+U"], "Clear command text."),
221
+ entry("cmd-delete-word", ["Ctrl+W"], "Delete the previous word in command text."),
222
+ entry("cmd-back", ["Backspace"], "Go back from a nested command screen when the query is empty."),
223
+ ],
224
+ },
221
225
  {
222
226
  label: "Navigation",
223
227
  items: [
@@ -235,7 +239,8 @@ function HelpPane({ focused, width, height }: PaneProps) {
235
239
  entry("search", ["/"], "Search in the focused pane."),
236
240
  entry("sidebar", ["<", ">"], "Narrow or widen the list beside the content, as in MAP and ASKG."),
237
241
  entry("warnings", ["!"], "Open the focused pane's data warnings."),
238
- entry("notification", [...actionBadges("notification-action"), ...actionBadges("notification-dismiss")], "Run or dismiss the newest notification."),
242
+ entry("notification", actionBadges("notification-action"), "Run the newest notification."),
243
+ entry("notification-dismiss", actionBadges("notification-dismiss"), "Dismiss the newest notification."),
239
244
  ],
240
245
  },
241
246
  {
@@ -303,6 +308,14 @@ function HelpPane({ focused, width, height }: PaneProps) {
303
308
  entry("close-pane", ["Esc", "Esc"], "Close the focused pane when nothing is being dragged."),
304
309
  ],
305
310
  },
311
+ {
312
+ // Docked panes keep the saved layout and floating ones follow the pointer; the keyboard's way is Window Mode below.
313
+ label: "Layout",
314
+ items: [
315
+ entry("layout-drag", ["Drag"], "Floating panes can be dragged by the title bar and resized from the lower-right corner."),
316
+ { ...entry("layout-actions", ["LMA"], "Use Layout Actions for split, move, duplicate, close all floating panes, undo, redo, and layout presets."), run: openLayoutActions },
317
+ ],
318
+ },
306
319
  {
307
320
  label: "Window Mode",
308
321
  items: [
@@ -320,7 +333,14 @@ function HelpPane({ focused, width, height }: PaneProps) {
320
333
  entry("commit", ["Enter", "Esc"], "Commit pending changes or exit window mode."),
321
334
  ],
322
335
  },
323
- ], [askBadges, copyBadges, functionHelpBadges, pasteBadges, windowMoveBadges, windowResizeBadges]);
336
+ ], [askBadges, commandBarBadges, copyBadges, functionHelpBadges, openLayoutActions, pasteBadges, tickerSearchBadges, trialDays, windowMoveBadges, windowResizeBadges]);
337
+
338
+ // One key column for every key table in Help, wide enough for the longest group of chips, so the descriptions
339
+ // line up from table to table. A narrow pane caps it, and a row past the cap shows the chips that fit whole.
340
+ const keyColumnWidth = useMemo(() => badgeColumnWidth(
341
+ [...trySections, ...startSections, ...referenceSections].flatMap((section) => section.items.map((item) => item.badges)),
342
+ { min: 8, max: Math.max(8, Math.floor(width * KEY_COLUMN_MAX_SHARE)) },
343
+ ), [referenceSections, startSections, trySections, width]);
324
344
 
325
345
  const renderContent = () => {
326
346
  switch (activeTabId) {
@@ -339,38 +359,19 @@ function HelpPane({ focused, width, height }: PaneProps) {
339
359
  <>
340
360
  {trySections.length > 0 && (
341
361
  <Box marginBottom={1}>
342
- <ShortcutTable sections={trySections} width={bodyWidth} keyLabel="TYPE" descriptionLabel="OPENS" />
362
+ <ShortcutTable sections={trySections} width={bodyWidth} keyWidth={keyColumnWidth} keyLabel="TYPE" descriptionLabel="OPENS" />
343
363
  </Box>
344
364
  )}
345
365
 
346
- <Box flexDirection="column" gap={1}>
347
- <SectionHeading title="How To Use Gloom" />
348
- <Box flexDirection="column">
349
- <Text fg={colors.textDim} wrapText>{t("Click anything to use it; the keys shown beside actions are shortcuts.")}</Text>
350
- <Text fg={colors.textDim} wrapText>
351
- {commandBarBadges[0]
352
- ? tf("Not sure of a code? Click the search bar at the top, or press {key}, and type what you want in plain words, like \"top news\". The Functions tab lists everything.", { key: commandBarBadges[0] })
353
- : t("Not sure of a code? Click the search bar at the top and type what you want in plain words, like \"top news\". The Functions tab lists everything.")}
354
- </Text>
355
- </Box>
356
- </Box>
366
+ <Text fg={colors.textDim} wrapText>
367
+ {commandBarBadges[0]
368
+ ? tf("Not sure of a code? Click the search bar at the top, or press {key}, and type what you want in plain words, like \"top news\". The Functions tab lists everything.", { key: commandBarBadges[0] })
369
+ : t("Not sure of a code? Click the search bar at the top and type what you want in plain words, like \"top news\". The Functions tab lists everything.")}
370
+ </Text>
357
371
 
358
372
  <Box marginTop={1}>
359
- <ShortcutTable sections={startSections} width={bodyWidth} />
373
+ <ShortcutTable sections={startSections} width={bodyWidth} keyWidth={keyColumnWidth} />
360
374
  </Box>
361
-
362
- <Box marginTop={1}>
363
- <ShortcutTable sections={commandBarSections} width={bodyWidth} />
364
- </Box>
365
-
366
- <Section title="Layout Basics">
367
- <Text fg={colors.text}>{t("Docked panes stay in the saved layout.")}</Text>
368
- <Text fg={colors.text} wrapText>{t("Floating panes can be dragged by the title bar and resized from the lower-right corner.")}</Text>
369
- {windowMoveBadges[0] && windowResizeBadges[0] && (
370
- <Text fg={colors.text} wrapText>{tf("From the keyboard, {move} moves the focused window and {resize} resizes it.", { move: windowMoveBadges[0], resize: windowResizeBadges[0] })}</Text>
371
- )}
372
- <Text fg={colors.text} wrapText>{t("Use Layout Actions for split, move, duplicate, close all floating panes, undo, redo, and layout presets.")}</Text>
373
- </Section>
374
375
  </>
375
376
  );
376
377
  }
@@ -396,6 +397,7 @@ function HelpPane({ focused, width, height }: PaneProps) {
396
397
  <ShortcutTable
397
398
  sections={referenceSections}
398
399
  width={width}
400
+ keyWidth={keyColumnWidth}
399
401
  height={contentHeight}
400
402
  after={(
401
403
  <Box paddingX={1} flexShrink={0}>
@@ -22,7 +22,7 @@ import {
22
22
  import { t } from "../../../i18n";
23
23
  import { useThemeColors } from "../../../theme/theme-context";
24
24
  import { Box, useUiCapabilities } from "../../../ui";
25
- import { badgeCell, badgeColumnWidth } from "./table-cells";
25
+ import { badgeCell } from "./table-cells";
26
26
 
27
27
  export interface ShortcutTableEntry {
28
28
  id: string;
@@ -38,6 +38,7 @@ type ShortcutRow = SectionedRow<ShortcutTableEntry>;
38
38
  export function ShortcutTable({
39
39
  sections,
40
40
  width,
41
+ keyWidth,
41
42
  height: fillHeight,
42
43
  after,
43
44
  keyLabel = "KEY",
@@ -46,6 +47,8 @@ export function ShortcutTable({
46
47
  sections: ReadonlyArray<TableSection<ShortcutTableEntry>>;
47
48
  /** Content width available inside the tab body. */
48
49
  width: number;
50
+ /** The key column's width, one for every table in Help so their descriptions line up. */
51
+ keyWidth: number;
49
52
  /** Fill this many rows and scroll, instead of growing to fit every row. */
50
53
  height?: number;
51
54
  /** Sits under a full-height table inside the same frame. */
@@ -59,10 +62,6 @@ export function ShortcutTable({
59
62
  () => buildSectionedRows(sections, (entry) => entry.id),
60
63
  [sections],
61
64
  );
62
- const keyWidth = useMemo(
63
- () => badgeColumnWidth(rows.filter(isSectionedItemRow).map((row) => row.item.badges), { min: 8, max: 24 }),
64
- [rows],
65
- );
66
65
  const columns = useMemo<ShortcutColumn[]>(() => [
67
66
  { id: "key", label: keyLabel, width: keyWidth, align: "left" },
68
67
  { id: "description", label: descriptionLabel, width: 20, align: "left", flexGrow: 1 },
@@ -72,7 +71,7 @@ export function ShortcutTable({
72
71
  if (!isSectionedItemRow(row)) return EMPTY_TABLE_CELL;
73
72
  const run = row.item.run;
74
73
  const cell: DataTableCell = column.id === "key"
75
- ? badgeCell(row.item.badges, column.width)
74
+ ? badgeCell(row.item.badges, column.width, { wholeOnly: true })
76
75
  : { text: t(row.item.description), color: colors.text };
77
76
  if (!run) return cell;
78
77
  return {
@@ -31,10 +31,21 @@ export function badgeColumnWidth(
31
31
  return Math.max(min, Math.min(max, widest));
32
32
  }
33
33
 
34
+ /**
35
+ * The chips of a group that fit a column whole, in order. A chip that would be
36
+ * cut ("Ctrl+Hor") is left out instead, so what shows is always a real key; the
37
+ * first chip stays whatever the width.
38
+ */
39
+ export function wholeBadges(labels: readonly string[], width: number): readonly string[] {
40
+ let count = Math.min(1, labels.length);
41
+ while (count < labels.length && badgeGroupWidth(labels.slice(0, count + 1)) <= width) count += 1;
42
+ return labels.slice(0, count);
43
+ }
44
+
34
45
  export function badgeCell(
35
46
  labels: readonly string[],
36
47
  width: number,
37
- options: { tone?: "neutral" | "accent"; emptyLabel?: string; emptyColor?: string } = {},
48
+ options: { tone?: "neutral" | "accent"; emptyLabel?: string; emptyColor?: string; wholeOnly?: boolean } = {},
38
49
  ): DataTableCell {
39
50
  if (labels.length === 0) {
40
51
  return options.emptyLabel
@@ -45,7 +56,7 @@ export function badgeCell(
45
56
  text: labels.join(" "),
46
57
  content: (
47
58
  <Box flexDirection="row" gap={BADGE_GAP} width={width} height={1} overflow="hidden">
48
- {labels.map((label, index) => (
59
+ {(options.wholeOnly ? wholeBadges(labels, width) : labels).map((label, index) => (
49
60
  <Badge key={`${label}:${index}`} label={label} variant="solid" tone={options.tone ?? "neutral"} />
50
61
  ))}
51
62
  </Box>
@@ -11,6 +11,7 @@ import { listingAbroad, type IssuerListingParams } from "../../../api-client/pat
11
11
  import { createPluginCache } from "../../../data/plugin-cache";
12
12
  import { secListingKey } from "../../../utils/sec";
13
13
  import { finiteOrNull, recordOrNull } from "../../../utils/guards";
14
+ import { readCompanyFrom } from "../../../types/company-from";
14
15
  import { cachedCloudResource, loadCloudResource, type CloudResource } from "../shared/cloud-resource";
15
16
  import { CLOUD_SESSION_REQUIRED } from "../shared/research-cloud-session";
16
17
 
@@ -110,8 +111,11 @@ function normalizeBeneficialOwnersPayload(value: unknown, ticker: string): Benef
110
111
  if (!raw || !Array.isArray(raw.owners)) throw new Error("The server returned an invalid 13D/13G list");
111
112
  const coverage = recordOrNull(raw.coverage);
112
113
  const nextOffset = finiteOrNull(raw.nextOffset);
114
+ const companyFrom = readCompanyFrom(raw.companyFrom);
115
+ const { companyFrom: _unread, ...rest } = raw;
113
116
  return {
114
- ...raw,
117
+ ...rest,
118
+ ...(companyFrom ? { companyFrom } : {}),
115
119
  ticker: text(raw.ticker) ?? ticker,
116
120
  cik: cikOrNull(raw.cik) ?? "",
117
121
  companyName: text(raw.companyName) ?? "",
@@ -9,6 +9,7 @@ import {
9
9
  type DataTableCell,
10
10
  type DataTableKeyEvent,
11
11
  } from "../../../components";
12
+ import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
12
13
  import { handleRefreshKey, loadingErrorFooterInfo } from "../../../components/data-table/table-pane";
13
14
  import type { IssuerListingParams } from "../../../api-client/paths";
14
15
  import { useAsyncResource } from "../../../react/async-resource";
@@ -233,6 +234,8 @@ export function BeneficialOwnersView({
233
234
  focused,
234
235
  title: "13D/13G coverage",
235
236
  });
237
+ // A foreign line's owners are its company's, read from the home line.
238
+ useCompanyDataFooter(payload?.companyFrom);
236
239
  usePaneFooter("holders-13dg", () => ({
237
240
  info: [
238
241
  ...(payload?.asOf ? [{ id: "as-of", parts: [{ text: `as of ${payload.asOf.slice(0, 10)}`, tone: "muted" as const }] }] : []),
@@ -7,7 +7,7 @@ import { loadHolderSnapshot } from "./client";
7
7
  import { holderValueCurrency } from "./format";
8
8
  import { logFailure } from "../../../utils/log-failure";
9
9
  import type { HolderData } from "../../../types/financials";
10
- import { SEC_FILINGS } from "../shared/report-freshness";
10
+ import { SEC_FILING_ROWS, SEC_FILINGS } from "../shared/report-freshness";
11
11
  import type { BeneficialOwnersPayload } from "../../../api-client/beneficial-owners";
12
12
  import { fetchBeneficialOwners, type BeneficialOwnersRequest } from "./beneficial-client";
13
13
  import { resolveHeadlessInstrument, resolveHeadlessIssuerListing } from "../shared/headless-market-data";
@@ -34,6 +34,7 @@ import {
34
34
  } from "./report-rows";
35
35
  import { REPORT_ROWS } from "../shared/headless-options";
36
36
  import { fundHoldersAnswer } from "./fund-holders";
37
+ import { companyDataNotes } from "../../../types/company-from";
37
38
  import {
38
39
  HOLDER_ROWS_MAX,
39
40
  indexedHoldersPartialNote,
@@ -197,7 +198,7 @@ export function createHoldersHeadless(
197
198
  rows: limited,
198
199
  // Reports that could not be read this time are missing from the rows.
199
200
  complete: beneficialListComplete(payload),
200
- freshness: { ...SEC_FILINGS, basis: "13D/13G filings", observedKey: "filingDate" },
201
+ freshness: { ...SEC_FILING_ROWS, basis: "13D/13G filings" },
201
202
  metadata: {
202
203
  symbol: payload.ticker || symbol,
203
204
  name: payload.companyName || null,
@@ -212,6 +213,7 @@ export function createHoldersHeadless(
212
213
  [payload.companyName || symbol, ...beneficialListFacts(payload, form, rows.length, history)].join(" · "),
213
214
  ...(legend ? [legend] : []),
214
215
  ...beneficialCoverageNotices(payload.coverage),
216
+ ...companyDataNotes(payload.companyFrom),
215
217
  ],
216
218
  returned: limited.length,
217
219
  total: rows.length,
@@ -12,6 +12,7 @@ import {
12
12
  type DataTableKeyEvent,
13
13
  type PageRequest,
14
14
  } from "../../../components";
15
+ import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
15
16
  import { handleRefreshKey, loadingErrorFooterInfo } from "../../../components/data-table/table-pane";
16
17
  import { useAsyncResource } from "../../../react/async-resource";
17
18
  import { useShortcut } from "../../../react/input";
@@ -321,6 +322,8 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
321
322
  }
322
323
  }, [currency, fundMatches, marketCap]);
323
324
 
325
+ // Holders read from the company's home line say whose; the SEC index's rows are the listing's own.
326
+ useCompanyDataFooter(indexed ? null : listData?.companyFrom, viewMode !== "13dg");
324
327
  // The 13D/G tab registers its own footer.
325
328
  usePaneFooter("holders", () => {
326
329
  if (viewMode === "13dg") return null;
@@ -25,6 +25,7 @@ import {
25
25
  import { HOLDER_ROWS_MAX } from "./index-holders";
26
26
  import { buildRows, DEFAULT_SORT, sortRows } from "./table-model";
27
27
  import type { HolderColumnId, SortPreference } from "./types";
28
+ import { companyDataNotes } from "../../../types/company-from";
28
29
 
29
30
  /**
30
31
  * A holder as `holders` and `fn HDS` report it; the keys are the JSON
@@ -231,7 +232,9 @@ export function holderReportNotices({
231
232
  ? `The source lists its ${reported} largest holders; fn 13F ${requested} lists all ${total.toLocaleString("en-US")}.`
232
233
  : null;
233
234
  return {
234
- notices: [header, basis, cut, positionsBasis, holderShareBasisNote(data, shown), unvaluedHoldersNote(shown, sort)]
235
+ notices: [header, basis, cut, positionsBasis, holderShareBasisNote(data, shown), unvaluedHoldersNote(shown, sort),
236
+ // The SEC index's rows are the listing's own; the source's, its company's home line's when it says so.
237
+ ...(indexed ? [] : companyDataNotes(data.companyFrom))]
235
238
  .filter((line): line is string => !!line),
236
239
  valueBasis,
237
240
  positionsBasis,
@@ -20,7 +20,7 @@ import {
20
20
  } from "./model";
21
21
  import { isAmendedInsiderFiling, relevantInsiderAmendments } from "./amendments";
22
22
  import { formatInsiderName } from "./display";
23
- import { SEC_FILINGS } from "../shared/report-freshness";
23
+ import { SEC_FILING_ROWS } from "../shared/report-freshness";
24
24
  import { resolveHeadlessInstrument, resolveHeadlessIssuerListing } from "../shared/headless-market-data";
25
25
  import { noSecReportsNotice } from "../../../utils/sec";
26
26
  import { parsePublicTickerKey } from "../../../utils/exchanges";
@@ -329,7 +329,7 @@ export function createInsiderHeadless(
329
329
  ): HeadlessPaneDefinition<"rows"> {
330
330
  return {
331
331
  shape: "rows",
332
- freshness: { ...SEC_FILINGS, basis: "Form 4 filings", observedKey: "filingDate" },
332
+ freshness: { ...SEC_FILING_ROWS, basis: "Form 4 filings" },
333
333
  argument: {
334
334
  kind: "ticker",
335
335
  optional: true,
@@ -1,5 +1,5 @@
1
1
  import { apiClient } from "../../../api-client";
2
- import type { IpoCalendarParams, IpoCalendarPayload, IpoDeal, IpoSourceHealth } from "../../../api-client/ipo";
2
+ import type { IpoCalendarParams, IpoCalendarPayload, IpoDeal, IpoLockup, IpoSourceHealth } from "../../../api-client/ipo";
3
3
  import type { HeadlessPaneApiClient } from "../../../types/plugin";
4
4
  import { createPluginCache } from "../../../data/plugin-cache";
5
5
  import { cachedCloudResource, loadCloudResource, unavailableOnServer, type CloudResource } from "../shared/cloud-resource";
@@ -45,6 +45,21 @@ function validDeal(deal: IpoDeal): boolean {
45
45
  && validFirstDay(deal.firstDay);
46
46
  }
47
47
 
48
+ function validLockup(value: unknown): value is IpoLockup {
49
+ const lockup = value as IpoLockup | null;
50
+ return !!lockup && typeof lockup === "object" && typeof lockup.endsOn === "string" && dayOrNull(lockup.endsOn)
51
+ && typeof lockup.days === "number" && Number.isFinite(lockup.days) && lockup.days > 0
52
+ && (lockup.basis === "prospectus" || lockup.basis === "typical")
53
+ && dayOrNull(lockup.prospectusDate ?? null) && textOrNull(lockup.prospectusUrl ?? null);
54
+ }
55
+
56
+ /** A lock-up estimate the client cannot read is left off its deal; the deal stays. */
57
+ function withReadableLockup(deal: IpoDeal): IpoDeal {
58
+ if (deal.lockup == null || validLockup(deal.lockup)) return deal;
59
+ const { lockup: _unreadable, ...rest } = deal;
60
+ return rest;
61
+ }
62
+
48
63
  function validSource(source: IpoSourceHealth): boolean {
49
64
  return !!source && text(source.id) && Array.isArray(source.mics) && source.mics.every(text)
50
65
  && typeof source.ok === "boolean" && textOrNull(source.asOf);
@@ -61,7 +76,8 @@ export function validateIpoCalendar(data: IpoCalendarPayload): IpoCalendarPayloa
61
76
  }
62
77
  return {
63
78
  asOf: data.asOf,
64
- deals: data.deals.filter(validDeal).map((deal) => (REGIONS.has(deal.region) ? deal : { ...deal, region: "other" })),
79
+ deals: data.deals.filter(validDeal).map(withReadableLockup)
80
+ .map((deal) => (REGIONS.has(deal.region) ? deal : { ...deal, region: "other" })),
65
81
  sources: data.sources.filter(validSource),
66
82
  };
67
83
  }
@@ -9,11 +9,13 @@ import { fetchIpoCalendar } from "./client";
9
9
  import {
10
10
  calendarDate,
11
11
  filterIpoDeals,
12
+ formatIpoLockupEnd,
12
13
  formatIpoPrice,
13
14
  formatIpoReturn,
14
15
  formatIpoSize,
15
16
  IPO_STATUSES,
16
17
  ipoBoardStatus,
18
+ ipoKeyword,
17
19
  isIpoTab,
18
20
  marketsBehind,
19
21
  marketsBehindText,
@@ -21,7 +23,10 @@ import {
21
23
  DEFAULT_IPO_SORT,
22
24
  type IpoTab,
23
25
  } from "./model";
26
+ import { ipoDetailSentences, loadIpoLockupNews, type IpoLockupStory } from "./listing-ipo";
24
27
  import { REPORT_FILTER, REPORT_ROWS } from "../shared/headless-options";
28
+ import { DATE_OPTION_EXAMPLES, isoDateOption } from "../../../utils/calendar-date";
29
+ import { logFailure } from "../../../utils/log-failure";
25
30
 
26
31
  const COLUMNS: HeadlessPaneColumn[] = [
27
32
  { key: "ticker", header: "Ticker" },
@@ -33,6 +38,8 @@ const COLUMNS: HeadlessPaneColumn[] = [
33
38
  { key: "price", header: "Price", align: "right" },
34
39
  { key: "offerSizeUsd", header: "Size", align: "right", format: (value) => formatIpoSize(value as number | null) },
35
40
  { key: "firstDayReturn", header: "Return", align: "right", format: (value) => formatIpoReturn(value as number | null) },
41
+ // A US IPO's estimated lock-up end: "~2026-11-02".
42
+ { key: "lockupEnds", header: "Lockup ends", format: (value) => formatIpoLockupEnd(typeof value === "string" ? value : null) },
36
43
  ];
37
44
 
38
45
  type StatusOption = IpoStatus | "all";
@@ -68,7 +75,7 @@ function toRow(deal: IpoDeal, now: Date) {
68
75
  status: ipoBoardStatus(deal, now),
69
76
  listingType: deal.listingType,
70
77
  currency: deal.currency,
71
- price: formatIpoPrice(deal),
78
+ price: formatIpoPrice(deal, { full: true }),
72
79
  priceLow: deal.priceLow,
73
80
  priceHigh: deal.priceHigh,
74
81
  offerPrice: deal.offerPrice,
@@ -78,26 +85,60 @@ function toRow(deal: IpoDeal, now: Date) {
78
85
  firstDayClose: deal.firstDay?.close ?? null,
79
86
  firstDayReturn: deal.firstDay?.returnPct ?? null,
80
87
  filedDate: deal.filedDate,
88
+ lockupEnds: deal.lockup?.endsOn ?? null,
89
+ lockupBasis: deal.lockup?.basis ?? null,
90
+ prospectusDate: deal.lockup?.prospectusDate ?? null,
81
91
  };
82
92
  }
83
93
 
84
- function projectIpoCalendarHeadless(payload: IpoCalendarPayload, args: HeadlessPaneLoadArgs): HeadlessRowsResult {
94
+ /** One deal found: its IPO in words, as `ticker` gives it, with its price now and its lock-up news. */
95
+ interface IpoDealDetail {
96
+ price: number | null;
97
+ news: IpoLockupStory[];
98
+ }
99
+
100
+ /** Lock-up estimates soonest first, then the ones that have ended, latest first. */
101
+ function byLockupEnd(deals: readonly IpoDeal[], now: Date): IpoDeal[] {
102
+ const today = now.toISOString().slice(0, 10);
103
+ const rank = (deal: IpoDeal) => (deal.lockup!.endsOn >= today ? 0 : 1);
104
+ return [...deals].sort((a, b) => rank(a) - rank(b)
105
+ || (rank(a) === 0 ? a.lockup!.endsOn.localeCompare(b.lockup!.endsOn) : b.lockup!.endsOn.localeCompare(a.lockup!.endsOn))
106
+ || a.id.localeCompare(b.id));
107
+ }
108
+
109
+ /** The most rows `--limit` lists. */
110
+ const MAX_ROWS = 500;
111
+
112
+ const SPAC_NOTE = "A SPAC's merger with its target (de-SPAC) is no IPO, so the calendar lists the SPACs' own IPOs.";
113
+
114
+ export function projectIpoCalendarHeadless(
115
+ payload: IpoCalendarPayload,
116
+ args: HeadlessPaneLoadArgs,
117
+ detail: IpoDealDetail | null = null,
118
+ now = new Date(),
119
+ ): HeadlessRowsResult {
85
120
  const query = typeof args.argument === "string" ? args.argument.trim() : "";
86
121
  const region = regionOption(args);
87
122
  const status = statusOption(args);
88
123
  const limit = Number(args.options.limit ?? 50);
89
- const now = new Date();
90
- const matching = sortIpoDeals(
91
- filterIpoDeals(payload.deals, region, query, now).filter((deal) => status === "all" || deal.status === status),
92
- DEFAULT_IPO_SORT,
93
- now,
94
- );
124
+ const keyword = ipoKeyword(query);
125
+ const found = filterIpoDeals(payload.deals, region, query, now).filter((deal) => status === "all" || deal.status === status);
126
+ const matching = keyword === "lockup" ? byLockupEnd(found, now) : sortIpoDeals(found, DEFAULT_IPO_SORT, now);
95
127
  const rows = matching.slice(0, limit).map((deal) => toRow(deal, now));
96
128
  const behind = marketsBehindText(marketsBehind(payload.sources, region, payload.deals));
129
+ const single = matching.length === 1 && query && !keyword ? matching[0]! : null;
130
+ const notes = [
131
+ ...rows.length < matching.length
132
+ ? [`Showing ${rows.length} of ${matching.length} deals; --limit ${Math.min(matching.length, MAX_ROWS)} lists ${matching.length <= MAX_ROWS ? "them all" : "more"}.`]
133
+ : [],
134
+ ...single ? ipoDetailSentences(single, detail?.price ?? null, detail?.news ?? [], now) : [],
135
+ ...keyword === "spac" || keyword === "de-spac" ? [SPAC_NOTE] : [],
136
+ ];
97
137
  return {
98
138
  columns: COLUMNS,
99
139
  rows,
100
140
  ...(behind ? { errors: [behind] } : {}),
141
+ ...(notes.length ? { notes } : {}),
101
142
  metadata: {
102
143
  asOf: payload.asOf,
103
144
  total: matching.length,
@@ -149,7 +190,15 @@ export const ipoCalendarHeadless: HeadlessPaneDefinition<"rows"> = {
149
190
  type: "integer",
150
191
  defaultValue: 50,
151
192
  minimum: 1,
152
- maximum: 500,
193
+ maximum: MAX_ROWS,
194
+ },
195
+ {
196
+ key: "from",
197
+ type: "string",
198
+ placeholder: "date",
199
+ description: `First listing date, as ${DATE_OPTION_EXAMPLES}; the calendar keeps a year of listings.`,
200
+ example: "--from 2025-11-01",
201
+ normalize: (value) => isoDateOption(value, "from"),
153
202
  },
154
203
  ],
155
204
  columns: COLUMNS,
@@ -164,7 +213,22 @@ export const ipoCalendarHeadless: HeadlessPaneDefinition<"rows"> = {
164
213
  async load(args, ctx) {
165
214
  // The default window has no filed or withdrawn deals, so a stage is asked for by name.
166
215
  const status = statusOption(args);
167
- const params = status === "all" ? {} : { status };
168
- return projectIpoCalendarHeadless(await fetchIpoCalendar(ctx.apiClient, ctx.signal, params), args);
216
+ const from = typeof args.options.from === "string" && args.options.from ? args.options.from : undefined;
217
+ const params = { ...(status === "all" ? {} : { status }), ...(from ? { from } : {}) };
218
+ const payload = await fetchIpoCalendar(ctx.apiClient, ctx.signal, params);
219
+ const projected = projectIpoCalendarHeadless(payload, args);
220
+ // One deal found: its price now and its lock-up news, which never fail the report.
221
+ const query = typeof args.argument === "string" ? args.argument.trim() : "";
222
+ const matched = projected.rows.length === 1 && query && !ipoKeyword(query)
223
+ ? payload.deals.find((deal) => deal.id === projected.rows[0]!.id) : undefined;
224
+ if (!matched?.symbol) return projected;
225
+ const [price, news] = await Promise.all([
226
+ matched.status === "listed"
227
+ ? ctx.marketData.getQuote(matched.symbol, matched.exchange ?? undefined).then((quote) => quote.price ?? null,
228
+ logFailure("plugins.ipo-calendar", "Load the price of an IPO failed", null))
229
+ : Promise.resolve(null),
230
+ loadIpoLockupNews(ctx.apiClient, matched).catch(logFailure("plugins.ipo-calendar", "Load an IPO's lock-up news failed", [] as IpoLockupStory[])),
231
+ ]);
232
+ return projectIpoCalendarHeadless(payload, args, { price, news });
169
233
  },
170
234
  };