gloomberb 0.18.2 → 0.18.3

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 (157) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/data.ts +15 -0
  4. package/src/api-client/index.ts +2 -0
  5. package/src/api-client/request.ts +5 -3
  6. package/src/api-client/telemetry.ts +17 -0
  7. package/src/app/global-shortcuts.ts +1 -16
  8. package/src/app/runtime/update.ts +28 -8
  9. package/src/architecture/pane-tabs-allowlist.json +65 -0
  10. package/src/cli/commands/market.ts +73 -33
  11. package/src/cli/commands/ticker.ts +82 -25
  12. package/src/cli/help.ts +1 -1
  13. package/src/cli/index.ts +8 -0
  14. package/src/cli/issuer-membership.ts +103 -0
  15. package/src/cli/listing-arg.ts +15 -2
  16. package/src/cli/name-entries.ts +51 -12
  17. package/src/cli/pane-functions/capabilities.ts +2 -0
  18. package/src/cli/pane-functions/catalog.ts +16 -0
  19. package/src/cli/pane-functions/dom.ts +2 -1
  20. package/src/cli/pane-functions/headless.ts +25 -6
  21. package/src/cli/pane-functions/index.ts +64 -5
  22. package/src/cli/pane-functions/resolver.ts +19 -5
  23. package/src/cli/report-tables.ts +136 -18
  24. package/src/cli/result.ts +12 -4
  25. package/src/cli/symbol-list.ts +47 -11
  26. package/src/components/chart/composite/composite-chart.tsx +14 -5
  27. package/src/components/chart/composite/drawing-store.ts +26 -6
  28. package/src/components/chart/composite/interactions.ts +2 -1
  29. package/src/components/chart/composite/panel-surface.tsx +11 -0
  30. package/src/components/chart/composite/types.ts +6 -0
  31. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  32. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  33. package/src/components/data-table/view.tsx +23 -3
  34. package/src/components/index.ts +1 -0
  35. package/src/components/layout/detached-pane-shell.tsx +1 -0
  36. package/src/components/layout/floating-pane.tsx +1 -0
  37. package/src/components/layout/header.tsx +228 -55
  38. package/src/components/layout/pane/header.tsx +10 -3
  39. package/src/components/layout/pane/index.tsx +1 -0
  40. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  41. package/src/components/ui/stat-grid.tsx +28 -3
  42. package/src/components/ui/status.tsx +3 -1
  43. package/src/core/app-services.ts +4 -0
  44. package/src/i18n/es.ts +22 -0
  45. package/src/i18n/ja.ts +22 -0
  46. package/src/i18n/ko.ts +22 -0
  47. package/src/i18n/zh-cn.ts +22 -0
  48. package/src/i18n/zh-tw.ts +22 -0
  49. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  50. package/src/plugins/builtin/alerts/events.ts +1 -0
  51. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  52. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  53. package/src/plugins/builtin/attention/pane.tsx +16 -14
  54. package/src/plugins/builtin/awards/pane.tsx +6 -6
  55. package/src/plugins/builtin/backtest/export.ts +1 -1
  56. package/src/plugins/builtin/broker-trading/dock.tsx +4 -2
  57. package/src/plugins/builtin/broker-trading/pane.tsx +6 -4
  58. package/src/plugins/builtin/broker-trading/ticket.tsx +10 -5
  59. package/src/plugins/builtin/buildout/pane/index.tsx +9 -6
  60. package/src/plugins/builtin/catalysts/client.ts +2 -2
  61. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  62. package/src/plugins/builtin/chart-composer/pane.tsx +1 -0
  63. package/src/plugins/builtin/commodity-balances/pane.tsx +3 -4
  64. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  65. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  66. package/src/plugins/builtin/company-kpis/pane.tsx +4 -3
  67. package/src/plugins/builtin/correlation/headless.ts +2 -1
  68. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  69. package/src/plugins/builtin/correlation/settings.ts +1 -0
  70. package/src/plugins/builtin/cot/pane.tsx +4 -3
  71. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  72. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  73. package/src/plugins/builtin/doe/pane.tsx +1 -1
  74. package/src/plugins/builtin/equity-screener/headless.ts +10 -0
  75. package/src/plugins/builtin/equity-screener/model.ts +28 -0
  76. package/src/plugins/builtin/equity-screener/pane.tsx +105 -98
  77. package/src/plugins/builtin/exposure/pane.tsx +10 -9
  78. package/src/plugins/builtin/gpu/board.tsx +4 -1
  79. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  80. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  81. package/src/plugins/builtin/gpu/history.tsx +4 -2
  82. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  83. package/src/plugins/builtin/holders/client.ts +2 -1
  84. package/src/plugins/builtin/holders/headless.ts +2 -1
  85. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  86. package/src/plugins/builtin/insider/base-values.ts +11 -7
  87. package/src/plugins/builtin/insider/client.ts +3 -0
  88. package/src/plugins/builtin/insider/headless.ts +19 -6
  89. package/src/plugins/builtin/insider/index.tsx +17 -8
  90. package/src/plugins/builtin/insider/issuer.ts +34 -4
  91. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  92. package/src/plugins/builtin/insider/table-model.ts +35 -13
  93. package/src/plugins/builtin/insider/table.tsx +5 -2
  94. package/src/plugins/builtin/iv-history/events.ts +145 -0
  95. package/src/plugins/builtin/iv-history/format.ts +3 -0
  96. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  97. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  98. package/src/plugins/builtin/market-movers/client.ts +4 -3
  99. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  100. package/src/plugins/builtin/notes/cli.ts +1 -1
  101. package/src/plugins/builtin/notes/export.ts +17 -6
  102. package/src/plugins/builtin/notes/files.ts +16 -5
  103. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  104. package/src/plugins/builtin/options/model-spot.ts +70 -4
  105. package/src/plugins/builtin/options/view.tsx +41 -13
  106. package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
  107. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  108. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  109. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  110. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  111. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  112. package/src/plugins/builtin/options-scenario/pane.tsx +13 -5
  113. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  114. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  115. package/src/plugins/builtin/power/client.ts +2 -2
  116. package/src/plugins/builtin/power/pane.tsx +36 -31
  117. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  118. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  119. package/src/plugins/builtin/seasonality/client.ts +2 -1
  120. package/src/plugins/builtin/sectors/client.ts +4 -3
  121. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  122. package/src/plugins/builtin/shared/research-trading.tsx +2 -0
  123. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  124. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  125. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  126. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  127. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  128. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  129. package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
  130. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  131. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  132. package/src/renderers/dom/desktop/controls.tsx +1 -1
  133. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  134. package/src/renderers/dom/host/tabs.tsx +16 -5
  135. package/src/renderers/dom/host/top-surface.ts +127 -0
  136. package/src/renderers/dom/styles.css +39 -0
  137. package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
  138. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  139. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  140. package/src/renderers/electrobun/view/news-source.ts +4 -2
  141. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  142. package/src/renderers/opentui/start.tsx +7 -0
  143. package/src/sources/gloomberb-cloud/index.ts +7 -0
  144. package/src/sources/provider-router/index.ts +16 -0
  145. package/src/telemetry/crash-reports.ts +4 -0
  146. package/src/telemetry/update-failures-node.ts +59 -0
  147. package/src/telemetry/update-failures.ts +307 -0
  148. package/src/tickers/company-name-listing.ts +9 -7
  149. package/src/tickers/company-name.ts +43 -0
  150. package/src/tickers/listing-fate.ts +30 -0
  151. package/src/types/data-provider.ts +6 -0
  152. package/src/types/headless.ts +7 -0
  153. package/src/types/issuer-listings.ts +28 -0
  154. package/src/updater/failure.ts +163 -0
  155. package/src/updater/index.ts +87 -23
  156. package/src/utils/currency-pair.ts +26 -0
  157. package/src/utils/sec.ts +68 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
4
4
  "packageManager": "bun@1.3.11",
5
5
  "description": "Market research and portfolio tracker for the terminal",
6
6
  "module": "src/public/index.ts",
@@ -74,7 +74,7 @@ index cdaa2bca0fabe577cb667056aa6f8f84211189c2..3839e5232742c2467180c344f51a30f1
74
74
 
75
75
  // Encrypt function
76
76
  diff --git a/dist/api/bun/core/Updater.ts b/dist/api/bun/core/Updater.ts
77
- index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899c9464a5f 100644
77
+ index 39daf6d341780af8e30c2c50327162863f5977ec..0174d2ce1a9a4fa8ae0fd97367293fc5a794286a 100644
78
78
  --- a/dist/api/bun/core/Updater.ts
79
79
  +++ b/dist/api/bun/core/Updater.ts
80
80
  @@ -132,6 +132,19 @@ let updateInfo: {
@@ -97,7 +97,62 @@ index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899
97
97
  function cleanupExtractionFolder(
98
98
  extractionFolder: string,
99
99
  keepTarHash: string,
100
- @@ -176,6 +189,12 @@ const Updater = {
100
+ @@ -158,6 +171,54 @@ function cleanupExtractionFolder(
101
+ }
102
+ }
103
+
104
+ +/**
105
+ + * Where the running app goes while the new one moves in: `<app>.previous`, or,
106
+ + * when an older one is still there and cannot be removed (a bundle another
107
+ + * user owns), a name of its own, so a copy left behind once never blocks
108
+ + * every later update. The app removes the copies it left at its next start.
109
+ + */
110
+ +function updateAsidePath(appPath: string): string {
111
+ + const previous = `${appPath}.previous`;
112
+ + if (!statSync(previous, { throwIfNoEntry: false })) return previous;
113
+ + try {
114
+ + rmSync(previous, { recursive: true, force: true });
115
+ + } catch (e) {
116
+ + // Left for the next start to remove; this update takes another name.
117
+ + }
118
+ + if (!statSync(previous, { throwIfNoEntry: false })) return previous;
119
+ + for (let attempt = 0; ; attempt += 1) {
120
+ + const unique = `${previous}-${Date.now().toString(36)}${attempt ? `-${attempt}` : ""}`;
121
+ + if (!statSync(unique, { throwIfNoEntry: false })) return unique;
122
+ + }
123
+ +}
124
+ +
125
+ +/**
126
+ + * Puts the new app where the running one is. The running app is moved aside
127
+ + * rather than deleted first, so a move that fails (another volume, a
128
+ + * read-only one, no permission) puts it back instead of leaving no app at all.
129
+ + * The copy moved aside is removed when it can be; one that cannot goes at the
130
+ + * app's next start.
131
+ + */
132
+ +function replaceAppBundle(runningAppPath: string, newAppPath: string) {
133
+ + const hadRunningApp = !!statSync(runningAppPath, { throwIfNoEntry: false });
134
+ + const asidePath = hadRunningApp ? updateAsidePath(runningAppPath) : null;
135
+ + if (asidePath) renameSync(runningAppPath, asidePath);
136
+ +
137
+ + emitStatus("replacing-app", "Installing new version...");
138
+ + try {
139
+ + renameSync(newAppPath, runningAppPath);
140
+ + } catch (error) {
141
+ + if (asidePath) renameSync(asidePath, runningAppPath);
142
+ + throw error;
143
+ + }
144
+ + if (!asidePath) return;
145
+ + try {
146
+ + rmSync(asidePath, { recursive: true, force: true });
147
+ + } catch (e) {
148
+ + // The new app is in place; the app removes the old copy at its next start.
149
+ + }
150
+ +}
151
+ +
152
+ const Updater = {
153
+ updateInfo: () => {
154
+ return updateInfo;
155
+ @@ -176,6 +237,12 @@ const Updater = {
101
156
  onStatusChangeCallback = callback;
102
157
  },
103
158
 
@@ -110,7 +165,7 @@ index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899
110
165
  // todo: allow switching channels, by default will check the current channel
111
166
  checkForUpdate: async () => {
112
167
  emitStatus("checking", "Checking for updates...");
113
- @@ -196,11 +215,11 @@ const Updater = {
168
+ @@ -196,11 +263,11 @@ const Updater = {
114
169
 
115
170
  const cacheBuster = Math.random().toString(36).substring(7);
116
171
  const platformPrefix = getPlatformPrefix(
@@ -124,7 +179,7 @@ index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899
124
179
 
125
180
  try {
126
181
  const updateInfoResponse = await fetch(updateInfoUrl);
127
- @@ -329,11 +348,11 @@ const Updater = {
182
+ @@ -329,11 +396,11 @@ const Updater = {
128
183
 
129
184
  // check if there's a patch file for it
130
185
  const platformPrefix = getPlatformPrefix(
@@ -138,7 +193,7 @@ index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899
138
193
  emitStatus(
139
194
  "fetching-patch",
140
195
  `Checking for patch: ${currentHash.slice(0, 8)}`,
141
- @@ -612,12 +631,12 @@ const Updater = {
196
+ @@ -612,12 +679,12 @@ const Updater = {
142
197
 
143
198
  const cacheBuster = Math.random().toString(36).substring(7);
144
199
  const platformPrefix = getPlatformPrefix(
@@ -153,6 +208,52 @@ index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899
153
208
  const prevVersionCompressedTarballPath = join(
154
209
  appDataFolder,
155
210
  "self-extraction",
211
+ @@ -760,7 +827,12 @@ const Updater = {
212
+ mkdirSync(extractionFolder, { recursive: true });
213
+ }
214
+
215
+ - let latestHash = (await Updater.checkForUpdate()).hash;
216
+ + // Install the build that was downloaded. Checking again here fetched
217
+ + // the newest manifest instead: once a newer release was out (or the
218
+ + // check failed offline) its tar was not there and the install
219
+ + // returned without a word, so a download waiting for a restart could
220
+ + // never be installed.
221
+ + let latestHash = updateInfo.hash;
222
+ const latestTarPath = join(extractionFolder, `${latestHash}.tar`);
223
+
224
+ let appBundleSubpath: string = "";
225
+ @@ -894,14 +966,7 @@ const Updater = {
226
+ emitStatus("replacing-app", "Removing old version...");
227
+
228
+ if (currentOS === "macos") {
229
+ - // Remove existing app before installing the new one
230
+ - if (statSync(runningAppBundlePath, { throwIfNoEntry: false })) {
231
+ - rmSync(runningAppBundlePath, { recursive: true });
232
+ - }
233
+ -
234
+ - emitStatus("replacing-app", "Installing new version...");
235
+ - // Move new app to running location
236
+ - renameSync(newAppBundlePath, runningAppBundlePath);
237
+ + replaceAppBundle(runningAppBundlePath, newAppBundlePath);
238
+
239
+ // Remove quarantine extended attributes to prevent "damaged" error
240
+ // The inner bundle is already signed/notarized, but macOS applies
241
+ @@ -918,14 +983,7 @@ const Updater = {
242
+ // On Linux, we now have directory bundles instead of AppImage files
243
+ // The app is stored in {appDataFolder}/app/
244
+ const appBundleDir = join(appDataFolder, "app");
245
+ -
246
+ - // Remove existing app directory if it exists
247
+ - if (statSync(appBundleDir, { throwIfNoEntry: false })) {
248
+ - rmSync(appBundleDir, { recursive: true });
249
+ - }
250
+ -
251
+ - // Move new app bundle directory to app location
252
+ - renameSync(newAppBundlePath, appBundleDir);
253
+ + replaceAppBundle(appBundleDir, newAppBundlePath);
254
+
255
+ // Ensure launcher binary is executable
256
+ const launcherPath = join(appBundleDir, "bin", "launcher");
156
257
  diff --git a/dist/api/bun/preload/.generated/compiled.ts b/dist/api/bun/preload/.generated/compiled.ts
157
258
  index 3ab1d7d8de8938ae60823650fd3a29b952a76cb6..e0e841e6a87ce41533c6bdda8d29400aaf478e31 100644
158
259
  --- a/dist/api/bun/preload/.generated/compiled.ts
@@ -27,6 +27,7 @@ import {
27
27
  import type { LettersParams } from "./letters";
28
28
  import type { IpoCalendarParams, IpoCalendarPayload } from "./ipo";
29
29
  import type { TickerChanges } from "../types/ticker-changes";
30
+ import type { IssuerListings } from "../types/issuer-listings";
30
31
  import type { OrganizationTickersResponse } from "./organization-tickers";
31
32
  import type { CryptoMarketsPayload, StablecoinSupplyHistoryPayload, StablecoinSupplyPayload } from "./crypto-markets";
32
33
  import type { CentralBankRatesPayload } from "./central-bank-rates";
@@ -470,6 +471,20 @@ export class CloudDataApi {
470
471
  return answers;
471
472
  }
472
473
 
474
+ /** Every listing of the company a listing belongs to (RCI and RCI.B:TSX are Rogers); empty for one the service does not hold. */
475
+ async getCloudIssuerListings(
476
+ symbol: string,
477
+ exchange = "",
478
+ options: { signal?: AbortSignal } = {},
479
+ ): Promise<CloudMarketResponse<IssuerListings>> {
480
+ const params = new URLSearchParams({ symbol: symbol.trim().toUpperCase() });
481
+ if (exchange.trim()) params.set("exchange", exchange.trim().toUpperCase());
482
+ return this.request<CloudMarketResponse<IssuerListings>>(
483
+ `/market/issuer-listings?${params}`,
484
+ options.signal ? { signal: options.signal } : undefined,
485
+ );
486
+ }
487
+
473
488
  /** The listed company each organization name resolves to, if any: a trial's lead sponsors, a page at a time. */
474
489
  async getCloudOrganizationTickers(names: readonly string[], signal?: AbortSignal): Promise<OrganizationTickersResponse> {
475
490
  return this.request<OrganizationTickersResponse>("/market/organization-tickers", {
@@ -54,6 +54,7 @@ export type {
54
54
  CrashReportSurface,
55
55
  CrashReportsPayload,
56
56
  FunctionUsageCount,
57
+ UpdateFailureReport,
57
58
  UsageCountsPayload,
58
59
  UsageCountsSurface,
59
60
  } from "./telemetry";
@@ -774,6 +775,7 @@ class GloomApiClient {
774
775
  getCloudTickerChanges = this.data.getCloudTickerChanges.bind(this.data);
775
776
  getCloudOrganizationTickers = this.data.getCloudOrganizationTickers.bind(this.data);
776
777
  getCloudTickerChangesBatch = this.data.getCloudTickerChangesBatch.bind(this.data);
778
+ getCloudIssuerListings = this.data.getCloudIssuerListings.bind(this.data);
777
779
  searchInstrumentsBatch = this.data.searchInstrumentsBatch.bind(this.data);
778
780
  getCloudHistory = this.data.getCloudHistory.bind(this.data);
779
781
  getCloudExchangeRate = this.data.getCloudExchangeRate.bind(this.data);
@@ -413,9 +413,10 @@ export class CloudApiRequestTransport {
413
413
  // change lookup: a note beside a quote, never one of the refused market
414
414
  // requests a lookup of many symbols waits out and retries. Nor the
415
415
  // tickers beside a list of organization names, which say so in place,
416
- // or a 13F filer's EDGAR filing list, whose report says it went unchecked.
417
- // Nor the batched searches and ticker changes a list of symbols starts
418
- // with: a service without them answers 404, and whatever they leave
416
+ // or a 13F filer's EDGAR filing list, whose report says it went unchecked,
417
+ // or a company's other listings, beside a list check that still answers
418
+ // for the listing itself. Nor the batched searches and ticker changes a
419
+ // list of symbols starts with: a service without them answers 404, and whatever they leave
419
420
  // unanswered is looked up one symbol at a time, which is tracked.
420
421
  if (
421
422
  path.startsWith("/telemetry/")
@@ -423,6 +424,7 @@ export class CloudApiRequestTransport {
423
424
  || path === "/cloud/geo/layers"
424
425
  || path.startsWith("/cloud/sec/13f/filer?")
425
426
  || path.startsWith("/market/ticker-changes?")
427
+ || path.startsWith("/market/issuer-listings?")
426
428
  || path === "/market/ticker-changes/batch"
427
429
  || path === "/market/search/batch"
428
430
  || path === "/market/organization-tickers"
@@ -16,6 +16,23 @@ export interface CrashReportError {
16
16
  stack?: string;
17
17
  plugin?: string;
18
18
  pane?: string;
19
+ /** A failed update step, field by field. Servers that do not know it drop it. */
20
+ update?: UpdateFailureReport;
21
+ }
22
+
23
+ /**
24
+ * A failed update step as error tracking gets it: fixed values and version
25
+ * numbers only, never a path, URL or the error's own text.
26
+ */
27
+ export interface UpdateFailureReport {
28
+ phase: "check" | "download" | "verify" | "apply" | "relaunch";
29
+ code: string;
30
+ appVersion: string;
31
+ targetVersion?: string;
32
+ channel: "stable" | "nightly";
33
+ platform: "macos" | "windows" | "linux" | "other";
34
+ arch: "arm64" | "x64" | "other";
35
+ installKind: string;
19
36
  }
20
37
 
21
38
  /** Body of `POST /telemetry/errors`. The server forwards each error to error tracking. */
@@ -8,7 +8,7 @@ import type { AppAction, AppState } from "../state/app/context";
8
8
  import type { TickerRecord } from "../types/ticker";
9
9
  import { findPaneInstance } from "../types/config";
10
10
  import type { ReleaseInfo } from "../updater";
11
- import { canRestartToApply, canSelfUpdate } from "../updater";
11
+ import { canRestartToApply, canRetryUpdate } from "../updater";
12
12
  import { getVisiblePaneCycleOrder } from "../components/layout/pane/cycle-order";
13
13
  import { requestFunctionHelp, useFunctionHelpHost } from "../plugins/builtin/help/function-card";
14
14
  import { buildRegistryHelpIndex, helpFunctionForPane } from "../plugins/builtin/help/function-index";
@@ -37,21 +37,6 @@ import {
37
37
  /** Long enough to read a few conflicts and choose Review. */
38
38
  const KEYBINDING_NOTICE_DURATION_MS = 15_000;
39
39
 
40
- /**
41
- * Whether the install-update key has something to retry: a release this build
42
- * can install itself whose last attempt failed. A fresh one downloads on its
43
- * own, and a manual or managed one names its own command. (A downloaded
44
- * desktop update has its own use of the key: restarting into it.)
45
- */
46
- export function canRetryUpdate(
47
- state: Pick<AppState, "updateAvailable" | "updateProgress" | "updateCheckInProgress">,
48
- ): boolean {
49
- return !!state.updateAvailable
50
- && state.updateProgress?.phase === "error"
51
- && !state.updateCheckInProgress
52
- && canSelfUpdate(state.updateAvailable);
53
- }
54
-
55
40
  /**
56
41
  * Tells the user once per launch when `config.json` holds a binding that does
57
42
  * not parse, names no action, or lands on a key another action already has.
@@ -6,6 +6,7 @@ import type { AppAction, AppState } from "../../state/app/context";
6
6
  import {
7
7
  applyUpdate,
8
8
  canRestartToApply,
9
+ canRetryUpdate,
9
10
  canSelfUpdate,
10
11
  checkForUpdateDetailed,
11
12
  discardStagedUpdate,
@@ -15,6 +16,8 @@ import {
15
16
  type UpdateProgress,
16
17
  } from "../../updater";
17
18
  import { resolveUpdateChannel, stagedUpdateFitsChannel } from "../../updater/channel";
19
+ import { updateFailureReason } from "../../updater/failure";
20
+ import { t, tf } from "../../i18n";
18
21
  import type { UpdateChannel } from "../../types/config";
19
22
  import { VERSION } from "../../version";
20
23
  import { debugLog } from "../../utils/debug-log";
@@ -27,11 +30,14 @@ const UPDATE_CHECK_INTERVAL_MS = 60 * 60_000; // hourly
27
30
  const updateLog = debugLog.createLogger("update");
28
31
 
29
32
  /**
30
- * The header and the command bar show a failed update's message, and the header
31
- * cuts it to its room: the log keeps what the updater actually said.
33
+ * The header says why an update failed in a few words and the command bar
34
+ * shows the whole error: the log keeps what the updater actually said, with
35
+ * the step and the reason.
32
36
  */
33
37
  function logUpdateFailure(progress: UpdateProgress): void {
34
- if (progress.phase === "error") updateLog.error("update failed", { error: progress.error });
38
+ if (progress.phase === "error") {
39
+ updateLog.error("update failed", { error: progress.error, phase: progress.failure?.phase, code: progress.failure?.code });
40
+ }
35
41
  }
36
42
 
37
43
  /** An update is being downloaded, applied, or is downloaded and waiting for a restart. */
@@ -127,7 +133,9 @@ export function useAppUpdateRuntime({
127
133
  return { kind: "error", error: message || "Update check failed" };
128
134
  });
129
135
 
130
- if (result.kind === "error") updateLog.warn("update check failed", { error: result.error });
136
+ if (result.kind === "error") {
137
+ updateLog.warn("update check failed", { error: result.error, code: result.failure?.code });
138
+ }
131
139
 
132
140
  if (!manual) {
133
141
  if (result.kind === "available" && result.release.tagName !== declinedTagRef.current) {
@@ -154,13 +162,25 @@ export function useAppUpdateRuntime({
154
162
  return;
155
163
  }
156
164
 
157
- dispatch({ type: "SET_UPDATE_NOTICE", notice: `Update check failed: ${result.error}` });
165
+ // The reason in a few words, as the failed banner gives it; the log has the error.
166
+ const reason = result.failure ? t(updateFailureReason(result.failure)) : result.error;
167
+ dispatch({ type: "SET_UPDATE_NOTICE", notice: tf("Update check failed: {reason}", { reason }) });
158
168
  }, [dispatch, stateRef]);
159
169
 
160
170
  const runUpdateCommand = useCallback(() => {
161
- if (canRestartToApply(stateRef.current.updateProgress)) restartToApplyUpdate();
162
- else void runUpdateCheck(true);
163
- }, [restartToApplyUpdate, runUpdateCheck, stateRef]);
171
+ const state = stateRef.current;
172
+ if (canRestartToApply(state.updateProgress)) {
173
+ restartToApplyUpdate();
174
+ return;
175
+ }
176
+ // After a failed download or install, the command tries again, as the key does.
177
+ if (canRetryUpdate(state)) {
178
+ startUpdate(state.updateAvailable!);
179
+ return;
180
+ }
181
+ if (state.updateProgress?.phase === "error") dispatch({ type: "SET_UPDATE_PROGRESS", progress: null });
182
+ void runUpdateCheck(true);
183
+ }, [dispatch, restartToApplyUpdate, runUpdateCheck, startUpdate, stateRef]);
164
184
 
165
185
  useEffect(() => {
166
186
  if (!enabled || isDetachedWindow) return;
@@ -0,0 +1,65 @@
1
+ {
2
+ "src/plugins/builtin/account-management/pane.tsx": {
3
+ "hasSession || apiClient.isSignedIn()": "Signed out, every tab is the same sign-in wall."
4
+ },
5
+ "src/plugins/builtin/analytics/index.tsx": {
6
+ "portfolioTabs.length > 0": "The tabs are the user's portfolios; with none there is nothing to choose between."
7
+ },
8
+ "src/plugins/builtin/analytics/risk-pane.tsx": {
9
+ "portfolio": "No local portfolio yet: the pane says to add holdings instead of offering views of nothing."
10
+ },
11
+ "src/plugins/builtin/attention/pane.tsx": {
12
+ "!signInWall": "Signed out, every tab is the same sign-in wall."
13
+ },
14
+ "src/plugins/builtin/awards/pane.tsx": {
15
+ "!denied": "Access denied draws the sign-in or plan wall for every tab."
16
+ },
17
+ "src/plugins/builtin/backtest/pane.tsx": {
18
+ "symbol": "No ticker yet: the pane asks for one before any view applies."
19
+ },
20
+ "src/plugins/builtin/catalysts/pane.tsx": {
21
+ "!openId": "An open event replaces the list by design and registers its own Evidence and History strip."
22
+ },
23
+ "src/plugins/builtin/cloud/team/pane.tsx": {
24
+ "signedIn": "The tabs are the user's teams; signed out there are none and the pane is a sign-in wall."
25
+ },
26
+ "src/plugins/builtin/company-attention/pane.tsx": {
27
+ "!signInWall && !openCompany && !app": "Signed out it is a sign-in wall; an open company or app replaces the board by design and registers its own strip."
28
+ },
29
+ "src/plugins/builtin/company-kpis/pane.tsx": {
30
+ "symbol && !signInWall": "No ticker yet, or signed out: every tab would be the same prompt or sign-in wall."
31
+ },
32
+ "src/plugins/builtin/congress-trades/pane.tsx": {
33
+ "tickerFilter": "A ticker's own congressional trades are one list; the Trades, Members and Tickers tabs belong to the whole-market view."
34
+ },
35
+ "src/plugins/builtin/debt-maturities/pane.tsx": {
36
+ "symbol && !signInWall": "No ticker yet, or signed out: every tab would be the same prompt or sign-in wall."
37
+ },
38
+ "src/plugins/builtin/earnings-ripple/pane.tsx": {
39
+ "holdings.length": "No holdings to ripple from yet: the pane says how to add them instead of offering views of nothing."
40
+ },
41
+ "src/plugins/builtin/estimate-revisions/pane.tsx": {
42
+ "!symbol || signInWall": "No ticker yet, or signed out: every tab would be the same prompt or sign-in wall."
43
+ },
44
+ "src/plugins/builtin/gpu/pane.tsx": {
45
+ "!signIn": "Signed out or unverified, every tab is the same sign-in wall."
46
+ },
47
+ "src/plugins/builtin/portfolio-list/pane/index.tsx": {
48
+ "showCollectionTabs": "The tabs are the pane's collections; a single collection has nothing to switch to."
49
+ },
50
+ "src/plugins/builtin/power/pane.tsx": {
51
+ "!signInWall": "Signed out, every tab is the same sign-in wall."
52
+ },
53
+ "src/plugins/builtin/research-search/pane.tsx": {
54
+ "signInRequired || verificationRequired": "Signed out or unverified, every mode is the same sign-in wall."
55
+ },
56
+ "src/plugins/builtin/shared/filing-year-reader.tsx": {
57
+ "years.length > 1": "The tabs are the filings on record, known once their list answers; a single filing has no other year to pick."
58
+ },
59
+ "src/plugins/builtin/supply-chain/pane.tsx": {
60
+ "symbol": "No ticker yet: the pane asks for one before any view applies."
61
+ },
62
+ "src/plugins/builtin/ticker-detail/pane.tsx": {
63
+ "!paneSettings.hideTabs && ticker": "The hideTabs setting locks the pane to one view, and with no ticker selected there is nothing to research."
64
+ }
65
+ }
@@ -218,8 +218,9 @@ import { centralBankEventsMessage } from "../central-bank-events";
218
218
  import { classSharesToday } from "../../plugins/builtin/holders/client";
219
219
  import { filingFormMatches, SEC_FILING_FETCH_LIMIT } from "../../plugins/builtin/sec/forms";
220
220
  import { currentReportItemsLabel } from "../../utils/sec-filings";
221
- import { loadParsedInsiderFilings } from "../../plugins/builtin/insider/client";
222
- import { insiderEmptyNotice } from "../../plugins/builtin/insider/issuer";
221
+ import { INSIDER_REPORT_FILINGS, loadParsedInsiderFilings } from "../../plugins/builtin/insider/client";
222
+ import { insiderEmptyNotice, loadHomeInsiderReportsNote } from "../../plugins/builtin/insider/issuer";
223
+ import { buildInsiderSummary } from "../../plugins/builtin/insider/model";
223
224
  import {
224
225
  BENEFICIAL_REPORT_COLUMNS,
225
226
  beneficialCoverageNotices,
@@ -290,8 +291,8 @@ function uniqueEntries(entries: readonly string[]): string[] {
290
291
 
291
292
  /**
292
293
  * The symbols to resolve: each entry with a space read as a company's name
293
- * when search finds one, else as its words, each a symbol (`readNameEntries`),
294
- * with the repeats that leaves dropped. A name found stays whole.
294
+ * when search finds one, else as its words when they are symbols
295
+ * (`readNameEntries`), with the repeats that leaves dropped. A name stays whole.
295
296
  */
296
297
  async function readSymbols(symbols: readonly string[], dataProvider: Parameters<typeof readNameEntries>[1]): Promise<string[]> {
297
298
  return uniqueEntries(await readNameEntries(symbols, dataProvider));
@@ -1549,50 +1550,81 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1549
1550
 
1550
1551
  type Form4Insiders = "available" | "none" | "foreign-private-issuer" | "no-sec-reports";
1551
1552
 
1553
+ /** The company's own Form 4 filings as `fn INS` reads them: whether it has any, and its summary of them. */
1554
+ interface Form4Report {
1555
+ status: Form4Insiders;
1556
+ /** `fn INS`'s line of the last 90 days' open-market buys and sales in the filings read; null when it has none. */
1557
+ summary: string | null;
1558
+ /** The Form 4 lines in the filings read, and how many filings that was. */
1559
+ lines: number;
1560
+ filings: number;
1561
+ /** The newest filing's date (YYYY-MM-DD), which dates the report. */
1562
+ newestFiled: string | null;
1563
+ /** "Canadian insider reports (SEDI) not covered." beside the reason a company from abroad has none. */
1564
+ home: string | null;
1565
+ }
1566
+
1552
1567
  /**
1553
- * Whether the company has Form 4 filings of its own, read as `fn INS` reads
1554
- * them; null when they could not be read. A listing abroad the SEC lists
1555
- * nothing for files no SEC reports at all.
1568
+ * The company's Form 4 filings, read as `fn INS` reads its first page (its
1569
+ * latest 20 of its own) and summed up as it sums them; null when they could
1570
+ * not be read. A listing abroad the SEC lists nothing for files no SEC
1571
+ * reports at all, and a company from abroad with none names its home
1572
+ * market's insider reports as not covered either.
1556
1573
  */
1557
- async function loadForm4Insiders(market: MarketContext, listing: CliListing): Promise<Form4Insiders | null> {
1574
+ async function loadForm4Report(market: MarketContext, listing: CliListing): Promise<Form4Report | null> {
1575
+ const { symbol, exchange } = listing.request;
1558
1576
  try {
1559
- const { parsed, foreignPrivateIssuer, noFilings } = await loadParsedInsiderFilings(market.dataProvider, listing.request.symbol, {
1560
- limit: 1,
1561
- exchange: listing.request.exchange,
1577
+ const { parsed, foreignPrivateIssuer, noFilings } = await loadParsedInsiderFilings(market.dataProvider, symbol, {
1578
+ limit: INSIDER_REPORT_FILINGS,
1579
+ exchange,
1562
1580
  });
1563
- if (parsed.length > 0) return "available";
1564
- if (noFilings && nonUsSecListingVenue(listing.request.symbol, listing.request.exchange)) return "no-sec-reports";
1565
- return foreignPrivateIssuer ? "foreign-private-issuer" : "none";
1581
+ if (parsed.length > 0) {
1582
+ const filed = parsed.map(({ filing }) => new Date(filing.filingDate).getTime()).filter(Number.isFinite);
1583
+ return {
1584
+ status: "available",
1585
+ summary: buildInsiderSummary(parsed, Date.now(), parsed),
1586
+ lines: parsed.filter(({ transaction }) => transaction != null).length,
1587
+ filings: new Set(parsed.map(({ filing }) => filing.accessionNumber)).size,
1588
+ newestFiled: filed.length > 0 ? new Date(Math.max(...filed)).toISOString().slice(0, 10) : null,
1589
+ home: null,
1590
+ };
1591
+ }
1592
+ const status: Form4Insiders = noFilings && nonUsSecListingVenue(symbol, exchange) ? "no-sec-reports"
1593
+ : foreignPrivateIssuer ? "foreign-private-issuer" : "none";
1594
+ const home = await loadHomeInsiderReportsNote(market.dataProvider, listing.key, exchange, foreignPrivateIssuer);
1595
+ return { status, summary: null, lines: 0, filings: 0, newestFiled: null, home };
1566
1596
  } catch (error) {
1567
- logFailure("cli.commands-market", "Read insider filing availability failed")(error);
1597
+ logFailure("cli.commands-market", "Read insider filings failed")(error);
1568
1598
  return null;
1569
1599
  }
1570
1600
  }
1571
1601
 
1572
1602
  /**
1573
- * Holder lists name institutions and funds; individual insiders rarely appear.
1574
- * The insider share of the company is reported either way, so say it. The
1575
- * Form 4 trades are pointed to only when the company has some; otherwise the
1576
- * line says why there are none, in the words `fn INS` uses.
1603
+ * The insider lines under `insider`'s holders. A company's insiders file
1604
+ * Form 4, which 13F holder lists rarely name, so the report leads with
1605
+ * `fn INS`'s summary of the Form 4 filings and points to the transactions;
1606
+ * a company with none says why, in the words `fn INS` uses. The insider share
1607
+ * of the company follows when the holder data reports it.
1577
1608
  */
1578
1609
  function insiderSummary(
1579
1610
  data: HolderData,
1580
- ownerTypes: Set<string>,
1581
1611
  listing: Pick<CliListing, "key">,
1582
1612
  identity: ListingIdentity,
1583
- form4: Form4Insiders | null,
1613
+ form4: Form4Report | null,
1584
1614
  ): string {
1585
1615
  const held = data.summary?.insidersPercentHeld;
1586
1616
  const share = held == null || !Number.isFinite(held)
1587
1617
  ? ""
1588
1618
  : `Insiders hold ${formatFractionPercentCell(held)} of shares outstanding.`;
1589
- const listed = data.holders.some((holder) => ownerTypes.has(holder.ownerType));
1590
1619
  // The listing as the command read it (CAR:ASX), not the source's spelling of it (CAR.AX), which fn does not take.
1591
- const transactions = form4 === "available"
1592
- ? listed ? "" : `Form 4 transactions: gloomberb fn INS ${listing.key}`
1593
- : form4 === "no-sec-reports" ? noSecReportsNotice(identity)
1594
- : form4 ? insiderEmptyNotice(form4 === "foreign-private-issuer") : "";
1595
- return [share, transactions].filter(Boolean).join("\n");
1620
+ const transactions = form4?.status === "available"
1621
+ ? [
1622
+ ...form4.summary ? [form4.summary] : [],
1623
+ `${form4.lines} Form 4 ${form4.lines === 1 ? "line" : "lines"} in the latest ${form4.filings} ${form4.filings === 1 ? "filing" : "filings"}${form4.newestFiled ? `, newest filed ${form4.newestFiled}` : ""}: gloomberb fn INS ${listing.key}`,
1624
+ ].join("\n")
1625
+ : form4?.status === "no-sec-reports" ? [noSecReportsNotice(identity), form4.home].filter(Boolean).join(" ")
1626
+ : form4 ? insiderEmptyNotice(form4.status === "foreign-private-issuer", form4.home) : "";
1627
+ return [transactions, share].filter(Boolean).join("\n");
1596
1628
  }
1597
1629
 
1598
1630
  /** `holders --form 13d|13g|all`: the 13D/13G beneficial owners, joined by name to the 13F holders. */
@@ -1682,7 +1714,7 @@ async function runHolders(
1682
1714
  { command: commandName, noun: "holder data" },
1683
1715
  ),
1684
1716
  loadListingQuote(market.dataProvider, listing),
1685
- insiderCommand ? loadForm4Insiders(market, listing) : null,
1717
+ insiderCommand ? loadForm4Report(market, listing) : null,
1686
1718
  ]);
1687
1719
  const identity = listingIdentity(listing, quote);
1688
1720
  const choice = await listingChoiceNotes(listing, market, quote, venues);
@@ -1704,7 +1736,9 @@ async function runHolders(
1704
1736
  const markedRows = rows.some((row) => holderShareBasisMarker(row));
1705
1737
  // The heading names the listing; its unit, date and how much of the list follows on the same line.
1706
1738
  // One "as of" only when every row shown is of that date; each row carries its own.
1707
- const facts = holderListFacts({
1739
+ // An insider list the 13F filings name nobody in is not "none reported": the Form 4 summary under it says what insiders did.
1740
+ const form4Only = insiderCommand && rows.length === 0;
1741
+ const facts = form4Only ? [] : holderListFacts({
1708
1742
  currency: valueCurrency,
1709
1743
  listingCurrency: data.currency,
1710
1744
  period: holderListPeriod(shownRows, data.asOf),
@@ -1715,8 +1749,10 @@ async function runHolders(
1715
1749
  });
1716
1750
  const unvaluedNote = unvaluedHoldersNote(shownRows);
1717
1751
  const columns = holderReportColumns(shownRows, valueCurrency);
1718
- // Dated as HDS dates it: by the list's as-of.
1719
- const freshness = rowsFreshness(shownRows, { ...SEC_FILINGS, basis: "13F filings" }, { asOf: data.asOf ?? null });
1752
+ // Dated as HDS dates it: by the list's as-of; an insider report of Form 4 filings alone as INS dates it, by the newest.
1753
+ const freshness = form4Only
1754
+ ? rowsFreshness(form4?.newestFiled ? [{ filingDate: form4.newestFiled }] : [], { ...SEC_FILINGS, basis: "Form 4 filings", observedKey: "filingDate" })
1755
+ : rowsFreshness(shownRows, { ...SEC_FILINGS, basis: "13F filings" }, { asOf: data.asOf ?? null });
1720
1756
  // Named as fn HDS takes it, so the closing notes point to the same listing.
1721
1757
  const requested = exchangeOption && !raw.includes(":") ? `${raw}:${exchangeOption.toUpperCase()}` : raw;
1722
1758
  const { notices } = holderReportNotices({ data, requested, shown: shownRows, reported: rows.length, total });
@@ -1738,7 +1774,11 @@ async function runHolders(
1738
1774
  truncated: shown < (total ?? rows.length),
1739
1775
  valueBasis,
1740
1776
  positionsBasis,
1741
- ...(insiderCommand ? { form4Insiders: form4 } : {}),
1777
+ ...(insiderCommand ? {
1778
+ form4Insiders: form4?.status ?? null,
1779
+ // INS's summary of the latest Form 4 filings, as `fn INS` gives it in its metadata.
1780
+ form4: form4 ? { summary: form4.summary, lines: form4.lines, filings: form4.filings, newestFiled: form4.newestFiled, notCovered: form4.home } : null,
1781
+ } : {}),
1742
1782
  },
1743
1783
  ...(choice.length > 0 ? { warnings: choice } : {}),
1744
1784
  }, {
@@ -1772,7 +1812,7 @@ async function runHolders(
1772
1812
  exportIdentity: { symbolOrSeries: listing.key },
1773
1813
  summary: () => [
1774
1814
  [valueBasis ? `Value = ${valueBasis}.` : "", positionsBasis ?? "", shareBasisNote ?? "", unvaluedNote ?? ""].filter(Boolean).join(" "),
1775
- insiderCommand ? insiderSummary(data, ownerTypes, listing, identity, form4) : "",
1815
+ insiderCommand ? insiderSummary(data, listing, identity, form4) : "",
1776
1816
  ].filter(Boolean).join("\n"),
1777
1817
  empty: `No holders reported for ${listingTitle(identity)}.`,
1778
1818
  });