@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
@@ -123,6 +123,8 @@ export interface ReportFreshness {
123
123
  ageOnly?: true;
124
124
  /** What the newest observation is, said before its time: `newest story Tue 29 Sep 2026 20:17 UTC`. */
125
125
  asOfLabel?: string;
126
+ /** The line says when the report was retrieved after the newest observation's date (`retrieved Wed 28 Oct 2026 19:07 UTC`). */
127
+ retrievedToo?: true;
126
128
  /** What the figures are, said after the status: `funding = current estimate, pays 08:00 UTC`. */
127
129
  note?: string;
128
130
  /** Shorter forms of `note`, in the order a capture's line too long for it tries them. */
@@ -610,6 +612,7 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
610
612
  ...(oldestAgeMinutes != null ? { oldestAgeMinutes } : {}),
611
613
  ...(status === "unreported" && declared.ageOnly ? { ageOnly: true as const } : {}),
612
614
  ...(asOfLabel ? { asOfLabel } : {}),
615
+ ...(asOf && declared.retrievedToo ? { retrievedToo: true as const } : {}),
613
616
  ...(note ? { note } : {}),
614
617
  ...(noteVariants.length > 0 ? { noteVariants } : {}),
615
618
  ...(schedule ? { schedule } : {}),
@@ -1061,8 +1064,11 @@ function statusLineParts(freshness: ReportFreshness, detail: StatusLineDetail =
1061
1064
  const undated = detail.date === "none";
1062
1065
  return [
1063
1066
  ...(freshness.sources?.length ? sourcesAsOfParts(freshness, freshness.sources, undated)
1064
- : freshness.unreached && !freshness.asOf ? []
1067
+ // Undated, a saved copy is not "retrieved" now: its own part says when it was saved.
1068
+ : (freshness.unreached || freshness.savedAt) && !freshness.asOf ? []
1065
1069
  : [asOfText(freshness, detail.asOf === "short", local, undated)]),
1070
+ // A list dated by its newest filing says when it was read as well.
1071
+ ...(freshness.retrievedToo && freshness.asOf && !freshness.savedAt ? [`retrieved ${timeText(freshness.retrievedAt, local)}`] : []),
1066
1072
  ...(freshness.schedule && detail.schedule !== "none"
1067
1073
  ? [typeof detail.schedule === "number" ? freshness.scheduleVariants?.[detail.schedule] ?? freshness.schedule : freshness.schedule] : []),
1068
1074
  ...(freshness.oldestAgeMinutes != null ? [`oldest ${ageText(freshness.oldestAgeMinutes)}`] : []),
@@ -1426,6 +1426,13 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
1426
1426
  data: null,
1427
1427
  bloomberg: [],
1428
1428
  },
1429
+ ROADMAP: {
1430
+ summary: "What is being built, what is planned and what shipped, with each item's votes. Pro members vote on what is building or planned, once per item, and can take the vote back.",
1431
+ usage: ["ROADMAP"],
1432
+ keys: [OPEN, key("v", "ote")],
1433
+ data: null,
1434
+ bloomberg: [],
1435
+ },
1429
1436
  HELP: {
1430
1437
  summary: "Help and the function reference. HELP with a function opens its card, F1 opens the focused pane's, and HELP HELP reaches support.",
1431
1438
  usage: ["HELP", "HELP OMON", "HELP HELP"],
@@ -36,8 +36,8 @@ import {
36
36
  ListingArgError, listingFateLoader, listingReadNotes, loadForListing, loadListingQuoteAnswer, quotedListingChoiceNotes, refuseMovedListing,
37
37
  } from "../listing-arg";
38
38
  import { notATickerStop } from "../company-name-hint";
39
- import { CLI_ERROR_CODE, failWithError, isCliFailure, SERVICE_UNREACHABLE_CODE } from "../errors";
40
- import { DATA_SERVICE_UNREACHABLE_MESSAGE } from "../../utils/network-failure";
39
+ import { CLI_ERROR_CODE, failUnreachable, failWithError, isCliFailure, unreachableError } from "../errors";
40
+ import { DATA_SERVICE_UNREACHABLE_MESSAGE, isNetworkFailure } from "../../utils/network-failure";
41
41
  import { isTickerLinkPeer } from "../../layout/ticker-navigation";
42
42
  import { knownGapMessage, commandPointer, commandPointerLeads, topicPointer } from "../known-gaps";
43
43
  import { selectReportTables, type CliReportTables } from "../report-tables";
@@ -264,7 +264,7 @@ export function reportFailure(
264
264
  const reasons = [...data.errors ?? [], ...typeof data.reason === "string" ? [data.reason] : []];
265
265
  const unreached = reasons.filter((reason) => reason.includes(DATA_SERVICE_UNREACHABLE_MESSAGE));
266
266
  if (unreached.length > 0 && (data.empty || data.unavailableSymbols.length > 0)) {
267
- return { code: SERVICE_UNREACHABLE_CODE, message: `${asked}: ${unreached.join("; ")}.`.replace(/\.\.$/, "."), retryable: true };
267
+ return unreachableError(`${asked}: ${unreached.join("; ")}.`.replace(/\.\.$/, "."));
268
268
  }
269
269
  if (!data.empty && !(noListing && noValues) && !(failsWithoutData && foundNothing(data))) return null;
270
270
  const unavailable = data.unavailableSymbols.length > 0 ? `no data for ${data.unavailableSymbols.join(", ")}` : "no data";
@@ -618,6 +618,9 @@ async function runPaneCliCommand(ctx: CliCommandContext, run: () => Promise<void
618
618
  // A listing it cannot use says what to type instead; a name that opens nothing, what is close to it.
619
619
  if (error instanceof ListingArgError || error instanceof UnknownPaneFunctionError) ctx.fail(error.message, error.details || undefined);
620
620
  const message = error instanceof Error ? error.message : String(error);
621
+ // Nothing answered: said as every command says it, with what to check.
622
+ if (message.includes(DATA_SERVICE_UNREACHABLE_MESSAGE)) failUnreachable(message);
623
+ if (isNetworkFailure(error)) failUnreachable();
621
624
  // A failure the command already worded keeps its advice line.
622
625
  const stated = error instanceof Error ? (error as Error & { details?: unknown }).details : undefined;
623
626
  const details = typeof stated === "string" ? stated : undefined;
@@ -160,7 +160,7 @@ async function answerCompanyName(
160
160
  if (error instanceof ListingArgError) return null;
161
161
  throw error;
162
162
  });
163
- const found = resolved ? { ...resolved, readAs: named.readAs, ...named.meant ? { meant: named.meant } : {} } : null;
163
+ const found = resolved ? { ...resolved, readAs: named.readAs, ...named.meant ? { meant: named.meant } : {}, ...named.readFromName ? { readFromName: named.readFromName } : {} } : null;
164
164
  const [quoted] = found ? await market.dataProvider.getQuotesBatch([found.request], { forceRefresh: refresh, revalidate: true, datesChangeBasis }) : [];
165
165
  if (found && quoted?.quote) return answerQuote(quoted, found, market, request, refresh, null, { datesChangeBasis });
166
166
  if (!unsettled) return null;
@@ -0,0 +1,88 @@
1
+ import type { AppResourceStorePort } from "../core/app-service-ports";
2
+ import type { JsonValue } from "../data/sqlite/json";
3
+ import type { PersistedResourceValue } from "../types/persistence";
4
+ import type { PluginPersistence } from "../types/plugin";
5
+ import { logFailure } from "../utils/log-failure";
6
+
7
+ type SetResourceOptions = Parameters<PluginPersistence["setResource"]>[3];
8
+
9
+ /**
10
+ * A plugin's persistence in a command: what the command fetches through a
11
+ * plugin's cache is saved in the profile, under the namespace the app keeps
12
+ * it in (`plugin:<state id>`), so a later run whose refresh fails answers
13
+ * from that saved copy as the app does. Only a read that may take an expired
14
+ * copy (`allowExpired`, what a cache falls back to when its refresh fails)
15
+ * reaches the saved copies; any other read sees only what this run fetched,
16
+ * so a command still asks for current data rather than answering from the
17
+ * app's cache. Plugin state is not read or kept. With no resource store,
18
+ * nothing outlives the run.
19
+ *
20
+ * The profile's store closes when the command ends, while a plugin may keep
21
+ * the persistence it was set up with: `release()` leaves the store alone from
22
+ * then on, and a store that fails (closed, full) costs the saved copy only,
23
+ * never the command.
24
+ */
25
+ export function createCommandPluginPersistence(
26
+ resources: AppResourceStorePort | null | undefined,
27
+ namespace: string,
28
+ ): PluginPersistence & { release(): void } {
29
+ const fetched = new Map<string, PersistedResourceValue<unknown>>();
30
+ const keyOf = (kind: string, key: string) => `${kind}:${key}`;
31
+ let store = resources ?? null;
32
+ const saved = <T>(operation: string, run: (store: AppResourceStorePort) => T, fallback: T): T => {
33
+ if (!store) return fallback;
34
+ try {
35
+ return run(store);
36
+ } catch (error) {
37
+ return logFailure("cli.saved-copies", `${operation} a saved copy failed`, fallback)(error);
38
+ }
39
+ };
40
+ return {
41
+ release: () => { store = null; },
42
+ getState: () => null,
43
+ setState: () => {},
44
+ deleteState: () => {},
45
+ getResource: <T = unknown>(kind: string, key: string, options?: { sourceKey?: string; schemaVersion?: number; allowExpired?: boolean }) => {
46
+ const own = fetched.get(keyOf(kind, key)) as PersistedResourceValue<T> | undefined;
47
+ if (own || !options?.allowExpired) return own ?? null;
48
+ const record = saved("Reading", (resources) => resources.get<T>(
49
+ { namespace, kind, entityKey: key, sourceKey: options.sourceKey },
50
+ { schemaVersion: options.schemaVersion, allowExpired: true },
51
+ ), null);
52
+ return record && {
53
+ value: record.value,
54
+ fetchedAt: record.fetchedAt,
55
+ staleAt: record.staleAt,
56
+ expiresAt: record.expiresAt,
57
+ sourceKey: record.sourceKey,
58
+ schemaVersion: record.schemaVersion,
59
+ provenance: record.provenance,
60
+ stale: record.stale,
61
+ expired: record.expired,
62
+ };
63
+ },
64
+ setResource: <T = unknown>(kind: string, key: string, value: T, options: SetResourceOptions) => {
65
+ const fetchedAt = Date.now();
66
+ const entry: PersistedResourceValue<T> = {
67
+ value,
68
+ fetchedAt,
69
+ staleAt: fetchedAt + options.cachePolicy.staleMs,
70
+ expiresAt: fetchedAt + options.cachePolicy.expireMs,
71
+ sourceKey: options.sourceKey ?? "",
72
+ schemaVersion: options.schemaVersion ?? 1,
73
+ provenance: options.provenance ?? null,
74
+ };
75
+ fetched.set(keyOf(kind, key), entry);
76
+ saved("Writing", (resources) => resources.set<T>({ namespace, kind, entityKey: key, sourceKey: options.sourceKey }, value, {
77
+ cachePolicy: options.cachePolicy,
78
+ schemaVersion: options.schemaVersion,
79
+ provenance: options.provenance as JsonValue | null | undefined,
80
+ }), undefined);
81
+ return entry;
82
+ },
83
+ deleteResource: (kind: string, key: string, options?: { sourceKey?: string }) => {
84
+ fetched.delete(keyOf(kind, key));
85
+ saved("Deleting", (resources) => resources.delete({ namespace, kind, entityKey: key, sourceKey: options?.sourceKey }), undefined);
86
+ },
87
+ };
88
+ }
@@ -73,6 +73,11 @@ export interface ResultItem {
73
73
  action: () => void | Promise<void>;
74
74
  }
75
75
 
76
+ /** A row plain Enter can run, so the untouched selection may rest on it. */
77
+ export function isDefaultSelectable(item: ResultItem): boolean {
78
+ return item.disabled !== true && item.defaultSelectable !== false;
79
+ }
80
+
76
81
  type ListScreenKind = "root" | "mode" | "picker";
77
82
 
78
83
  export interface ListScreenState {
@@ -7,7 +7,7 @@ import type { TickerRecord } from "../../../../types/ticker";
7
7
  import { listingChoiceQuery, symbolSearchQuery, type TickerSearchCandidate } from "../../../../tickers/search";
8
8
  import type { AssistRowHandlers } from "../../assist/model";
9
9
  import { matchPrefix, type Command } from "../../commands/registry";
10
- import type { ResultItem } from "../../list/model";
10
+ import { isDefaultSelectable, type ResultItem } from "../../list/model";
11
11
  import type { CommandBarCategoryPriorities } from "../../view-model";
12
12
  import type { CommandBarRoute } from "../../workflow/types";
13
13
  import { normalizeCommandTickerSearchText, readRootTickerQuery } from "../ticker-search/results";
@@ -20,11 +20,6 @@ import { buildRootShortcutFeedback } from "./shortcut-feedback";
20
20
  import type { ShortcutIntent } from "./shortcuts";
21
21
  import { clampIndex } from "../../../../utils/math";
22
22
 
23
- /** A row plain Enter can run, so the untouched selection may rest on it. */
24
- function isDefaultSelectable(item: ResultItem): boolean {
25
- return item.disabled !== true && item.defaultSelectable !== false;
26
- }
27
-
28
23
  interface UseCommandBarRootRuntimeOptions {
29
24
  activeCollectionId: string | null;
30
25
  activePortfolio?: AppState["config"]["portfolios"][number];
@@ -343,8 +338,12 @@ export function useCommandBarRootRuntime({
343
338
  if (shiftedIdx >= 0) return shiftedIdx;
344
339
  return clampIndex(current, resultIds.length);
345
340
  }
346
- // Untouched, the selection follows the best row on offer.
341
+ // Untouched, the selection follows the best row on offer. When every row
342
+ // is an offer or a placeholder, none of them is the answer and none is
343
+ // selected: Enter must not take one of them for the ticker still being
344
+ // searched.
347
345
  const defaultIdx = orderedRootResults.findIndex(isDefaultSelectable);
346
+ if (defaultIdx < 0) return -1;
348
347
  return clampIndex(Math.max(rootResultModel.initialIdx, defaultIdx), resultIds.length);
349
348
  });
350
349
  }, [
@@ -1,4 +1,4 @@
1
- import { useCallback, type MutableRefObject } from "react";
1
+ import { useCallback, useEffect, useRef, type MutableRefObject } from "react";
2
2
  import type { AppState } from "../../state/app/context";
3
3
  import type { TickerRecord } from "../../types/ticker";
4
4
  import { t } from "../../i18n";
@@ -12,7 +12,7 @@ import type {
12
12
  PaneTemplateDef,
13
13
  } from "../../types/plugin";
14
14
  import { resolveCommandBarMode, type CommandBarMode } from "./view-model";
15
- import type { ListScreenState, ResultItem } from "./list/model";
15
+ import { isDefaultSelectable, type ListScreenState, type ResultItem } from "./list/model";
16
16
  import type { CommandBarRoute } from "./workflow/types";
17
17
  import type { CollectionCommandId } from "./helpers";
18
18
  import type { CommandBarCollectionWorkflowActions } from "./workflow/collection-actions";
@@ -67,6 +67,7 @@ interface UseCommandBarSelectionRuntimeOptions {
67
67
  rootModeKindRef: MutableRefObject<CommandBarMode>;
68
68
  rootQuery: string;
69
69
  rootQueryRef: MutableRefObject<string>;
70
+ rootSelectedIdx: number;
70
71
  rootSelectionNavigatedRef: MutableRefObject<boolean>;
71
72
  rootShortcutIntent: ShortcutIntent;
72
73
  rootThemeBaseIdRef: MutableRefObject<string | null>;
@@ -78,6 +79,11 @@ interface UseCommandBarSelectionRuntimeOptions {
78
79
  */
79
80
  runRootRow?: RunRootRow;
80
81
  runSecurityDescriptionShortcut: (query?: string, options?: { forceNewPane?: boolean }) => void | Promise<void>;
82
+ /**
83
+ * The search that decides the best row is still out, so the list on screen
84
+ * is a stand-in for the answer.
85
+ */
86
+ searchPending: boolean;
81
87
  setRootQuery: (query: string) => void;
82
88
  stateConfigLayout: AppState["config"]["layout"];
83
89
  stateRef: MutableRefObject<AppState>;
@@ -108,12 +114,14 @@ export function useCommandBarSelectionRuntime({
108
114
  rootModeKindRef,
109
115
  rootQuery,
110
116
  rootQueryRef,
117
+ rootSelectedIdx,
111
118
  rootSelectionNavigatedRef,
112
119
  rootShortcutIntent,
113
120
  rootThemeBaseIdRef,
114
121
  runDirectCommand,
115
122
  runRootRow,
116
123
  runSecurityDescriptionShortcut,
124
+ searchPending,
117
125
  setRootQuery,
118
126
  stateConfigLayout,
119
127
  stateRef,
@@ -276,6 +284,13 @@ export function useCommandBarSelectionRuntime({
276
284
  updateTopRoute,
277
285
  ]);
278
286
 
287
+ /**
288
+ * An Enter pressed with no row to run while the search is still out. It
289
+ * waits for the answer instead of running whatever else the list shows, and
290
+ * is dropped when the user types, moves the selection or leaves the root.
291
+ */
292
+ const pendingEnterRef = useRef<{ query: string; secondary: boolean } | null>(null);
293
+
279
294
  const activateListSelection = useCallback((options?: { secondary?: boolean; item?: ResultItem }) => {
280
295
  const listState = visibleListStateRef.current;
281
296
  if (!listState) return;
@@ -311,7 +326,13 @@ export function useCommandBarSelectionRuntime({
311
326
  secondaryAction: () => runSecurityDescriptionShortcut(symbol, { forceNewPane: true }) };
312
327
  }
313
328
  const selected = options?.item ?? typed ?? listState.results[listState.selectedIdx];
314
- if (!selected || selected.disabled) return;
329
+ if (!selected) {
330
+ if (searchPending && !currentRoute && !options?.item && !rootSelectionNavigatedRef.current) {
331
+ pendingEnterRef.current = { query: rootQueryRef.current, secondary: options?.secondary === true };
332
+ }
333
+ return;
334
+ }
335
+ if (selected.disabled) return;
315
336
 
316
337
  // Opened from an add row: a venue picked from the colon list joins that
317
338
  // collection. Any other text or screen runs as usual.
@@ -396,11 +417,39 @@ export function useCommandBarSelectionRuntime({
396
417
  rootShortcutIntent,
397
418
  runRootRow,
398
419
  runSecurityDescriptionShortcut,
420
+ searchPending,
399
421
  stateConfigLayout,
400
422
  stateRef,
401
423
  visibleListStateRef,
402
424
  ]);
403
425
 
426
+ useEffect(() => {
427
+ const pending = pendingEnterRef.current;
428
+ if (!pending) return;
429
+ if (currentRoute || rootQueryRef.current !== pending.query || rootSelectionNavigatedRef.current) {
430
+ pendingEnterRef.current = null;
431
+ return;
432
+ }
433
+ const listState = visibleListStateRef.current;
434
+ if (!listState?.results.some(isDefaultSelectable)) {
435
+ // Still searching: the answer may bring a row. Answered with none: there is nothing to run.
436
+ if (!searchPending) pendingEnterRef.current = null;
437
+ return;
438
+ }
439
+ // The selection follows the rows in the render after they land.
440
+ if (!listState.results[listState.selectedIdx]) return;
441
+ pendingEnterRef.current = null;
442
+ activateListSelection({ secondary: pending.secondary });
443
+ }, [
444
+ activateListSelection,
445
+ currentRoute,
446
+ rootQueryRef,
447
+ rootSelectedIdx,
448
+ rootSelectionNavigatedRef,
449
+ searchPending,
450
+ visibleListStateRef,
451
+ ]);
452
+
404
453
  return {
405
454
  acceptRootShortcutTab,
406
455
  acceptSelectedShortcutTab,
@@ -532,12 +532,14 @@ export function CommandBar({
532
532
  rootModeKindRef,
533
533
  rootQuery,
534
534
  rootQueryRef,
535
+ rootSelectedIdx,
535
536
  rootSelectionNavigatedRef,
536
537
  rootShortcutIntent,
537
538
  rootThemeBaseIdRef,
538
539
  runDirectCommand,
539
540
  runRootRow,
540
541
  runSecurityDescriptionShortcut,
542
+ searchPending: rootSearching || !rootResultsCurrent,
541
543
  setRootQuery,
542
544
  stateConfigLayout: state.config.layout,
543
545
  stateRef,
@@ -28,10 +28,10 @@ export function usePaneCaptureStamp(paneId: string): string | null {
28
28
 
29
29
  export function captureStampFooter(footer: CombinedPaneFooter, text: string | null): CombinedPaneFooter {
30
30
  if (!text) return footer;
31
- // Set apart from what the pane says about its data, which may itself be a time.
31
+ // The footer sets it apart from what the pane says about its data, which may itself be a time.
32
32
  const segment: PaneFooterSegment = {
33
33
  id: "capture-stamp",
34
- parts: [...(footer.info.length > 0 ? [{ text: "· ", tone: "muted" as const }] : []), { text, tone: "label" }],
34
+ parts: [{ text, tone: "label" }],
35
35
  };
36
36
  return {
37
37
  ...footer,
@@ -11,7 +11,9 @@ import { useOptionalDialog, type PromptContext } from "../../../../ui/dialog";
11
11
  import { nativePaneFooterRows } from "../sizing";
12
12
  import {
13
13
  EMPTY_FOOTER,
14
+ PANE_FOOTER_SEPARATOR,
14
15
  fitPaneFooterInfo,
16
+ separatesPaneFooterSegments,
15
17
  hasPaneFooterContent,
16
18
  layoutPaneFooterHintRow,
17
19
  totalHintsWidth,
@@ -22,6 +24,7 @@ import {
22
24
  } from "./model";
23
25
 
24
26
  export {
27
+ companyDataFooterSegment,
25
28
  datesPaneData,
26
29
  DELAYED_SEGMENT,
27
30
  delayedFooterSegment,
@@ -267,6 +270,7 @@ function FooterContent({
267
270
  // A segment with shorter forms takes the longest that fits beside the hints, gaps and terminal keys included.
268
271
  const keysWidth = nativePaneChrome ? 0 : footer.info.reduce((total, segment) => total + terminalSegmentKey(segment, focused).length, 0);
269
272
  const info = infoWidth != null ? fitPaneFooterInfo(footer.info, infoWidth - keysWidth, { besideHints: hasHints }) : footer.info;
273
+ const lastTextIndex = info.findLastIndex((segment) => !segment.icon);
270
274
 
271
275
  if (!hasInfo && !hasHints) {
272
276
  return <Box flexGrow={1} height={1} />;
@@ -291,12 +295,24 @@ function FooterContent({
291
295
  flexShrink={1}
292
296
  {...(infoWidth != null ? { width: infoWidth } : {})}
293
297
  >
294
- {info.map((segment, index) => (
295
- // An icon is a few cells that mean nothing cut: the text beside it gives way first.
296
- <Box key={segment.id} flexDirection="row" flexShrink={segment.icon ? 0 : undefined} marginRight={index === info.length - 1 ? 0 : 1}>
297
- <SegmentView segment={segment} focused={focused} />
298
- </Box>
299
- ))}
298
+ {info.map((segment, index) => {
299
+ // Every pane's segments are set apart the same way, so two never read as one sentence.
300
+ const separated = index > 0 && separatesPaneFooterSegments(info[index - 1]!, segment);
301
+ return (
302
+ <Box
303
+ key={segment.id}
304
+ flexDirection="row"
305
+ overflow="hidden"
306
+ // A row too narrow cuts from its end: the last text segment gives way, then the row clips the
307
+ // one before it. An icon is a few cells that mean nothing cut, and earlier text says more.
308
+ flexShrink={!segment.icon && index === lastTextIndex ? 1 : 0}
309
+ marginLeft={index > 0 && !separated ? 1 : 0}
310
+ >
311
+ {separated && <Box flexShrink={0}><Text fg={colors.textMuted}>{PANE_FOOTER_SEPARATOR}</Text></Box>}
312
+ <SegmentView segment={segment} focused={focused} />
313
+ </Box>
314
+ );
315
+ })}
300
316
  </Box>
301
317
  )}
302
318
  {hasHints && (
@@ -1,5 +1,6 @@
1
1
  import type { ContextMenuItem } from "../../../../types/context-menu";
2
- import { t } from "../../../../i18n";
2
+ import { t, tf } from "../../../../i18n";
3
+ import { type CompanyFrom, companyFromLabel } from "../../../../types/company-from";
3
4
  import { clipToDisplayWidth, displayWidth } from "../../../../utils/format";
4
5
  import { formatShortDate } from "../../../../utils/datetime-format";
5
6
  import { getShortcutHintWidth } from "../../../ui/shortcut-hint";
@@ -44,8 +45,9 @@ export interface PaneFooterSegment {
44
45
  */
45
46
  quoteStatus?: boolean;
46
47
  /**
47
- * The segment says what the rows leave out (releases a filter hides), which
48
- * a picture of the rows cannot: a `gloom shot` keeps it, as it keeps a
48
+ * The segment says what the rows leave out (releases a filter hides), or
49
+ * whose they are (a receipt's company data from its home line), which a
50
+ * picture of the rows cannot: a `gloom shot` keeps it, as it keeps a
49
51
  * warning.
50
52
  */
51
53
  capture?: boolean;
@@ -117,6 +119,26 @@ export function savedCopyFooterSegment(id: string, savedAt: number, now = Date.n
117
119
  };
118
120
  }
119
121
 
122
+ /**
123
+ * The footer's one form for company data a pane shows from another listing
124
+ * of the company: a receipt or a foreign line (SKHY, HY9H in Frankfurt) reads
125
+ * its home line's news, filings or holders, `company data from 000660 KRX`.
126
+ * Null when the data is the listing's own.
127
+ */
128
+ export function companyDataFooterSegment(from: CompanyFrom | null | undefined): PaneFooterSegment | null {
129
+ if (!from) return null;
130
+ const listing = companyFromLabel(from);
131
+ const text = tf("company data from {listing}", { listing });
132
+ return {
133
+ id: "company-data",
134
+ title: from.name ? `${text} (${from.name})` : text,
135
+ // A capture of the rows keeps it: the picture cannot say whose they are.
136
+ capture: true,
137
+ parts: [{ text, tone: "muted" }],
138
+ variants: [[{ text: tf("from {listing}", { listing }), tone: "muted" }]],
139
+ };
140
+ }
141
+
120
142
  /** A pane whose market data is all delayed, by no one figure: the clock alone, its tooltip saying so. */
121
143
  export const DELAYED_SEGMENT: PaneFooterSegment = delayedFooterSegment({ id: "delayed", detail: "Delayed market data" });
122
144
 
@@ -202,6 +224,25 @@ const INFO_FLOOR_CHARS = 10;
202
224
  const WARNING_FLOOR_CHARS = 24;
203
225
  const HINT_GAP = 1;
204
226
 
227
+ /**
228
+ * What the footer draws between two info segments, muted, as a status draws
229
+ * between its own parts ("Newest story Mon 14:23 · 12h delayed"): two
230
+ * segments side by side never read as one sentence ("company data from PBR ·
231
+ * as of 2026-06-30"). An icon (`⚠[!]`, `◷15m`) is a mark, not a phrase: a
232
+ * column sets it apart.
233
+ */
234
+ export const PANE_FOOTER_SEPARATOR = " · ";
235
+ const SEPARATOR_WIDTH = displayWidth(PANE_FOOTER_SEPARATOR);
236
+
237
+ /** Whether the footer draws {@link PANE_FOOTER_SEPARATOR} between two adjacent segments, else a one-column gap. */
238
+ export function separatesPaneFooterSegments(before: Pick<PaneFooterSegment, "icon">, after: Pick<PaneFooterSegment, "icon">): boolean {
239
+ return !before.icon && !after.icon;
240
+ }
241
+
242
+ function segmentGapWidth(before: PaneFooterSegment, after: PaneFooterSegment): number {
243
+ return separatesPaneFooterSegments(before, after) ? SEPARATOR_WIDTH : 1;
244
+ }
245
+
205
246
  function paneHintWidth(hint: Pick<PaneHint, "key" | "label">, prefix = ""): number {
206
247
  return getShortcutHintWidth(hint.key, hint.label, prefix);
207
248
  }
@@ -216,15 +257,12 @@ function infoTextWidth(segments: readonly PaneFooterSegment[]): number {
216
257
  const text = segment.parts.reduce((sum, part, partIndex) => (
217
258
  sum + (partIndex > 0 ? 1 : 0) + displayWidth(part.text)
218
259
  ), 0);
219
- return total + (index > 0 ? 1 : 0) + text;
260
+ return total + (index > 0 ? segmentGapWidth(segments[index - 1]!, segment) : 0) + text;
220
261
  }, 0);
221
262
  }
222
263
 
223
- /**
224
- * The info segments as they fit `width`: segments with `variants` step down
225
- * together, one variant at a time, until the row fits or none is left.
226
- */
227
- export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number, { besideHints = true }: { besideHints?: boolean } = {}): PaneFooterSegment[] {
264
+ /** Segments with `variants` step down together, one variant at a time, until they fit or none is left. */
265
+ function stepDownTogether(info: PaneFooterSegment[], width: number): PaneFooterSegment[] {
228
266
  const stages = Math.max(0, ...info.map((segment) => segment.variants?.length ?? 0));
229
267
  let fitted = info;
230
268
  for (let stage = 0; stage < stages && infoTextWidth(fitted) > width; stage += 1) {
@@ -237,20 +275,50 @@ export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number, { be
237
275
  } : segment;
238
276
  });
239
277
  }
240
- // A trailing status cut mid-word reads as part of the hints beside it ("quote 12h t[x] put"): one that does not
241
- // fit ends in an ellipsis a column short of them, or is left out when less than half of it would show. A
242
- // warning, an icon or a pressable segment stays as it is, clipped by the row if it must be. With no hints beside it
243
- // (a pane picture's footer), the row clips it, so a frame that fits its own status line sees it overflow.
244
- while (besideHints && fitted.length > 1 && infoTextWidth(fitted) > width) {
278
+ return fitted;
279
+ }
280
+
281
+ /**
282
+ * A trailing status cut mid-word reads as part of the hints beside it ("quote
283
+ * 12h t[x] put"): one that does not fit ends in an ellipsis a column short of
284
+ * them, or is left out when less than half of it would show, until the row
285
+ * fits or `keep` segments are left. A warning, an icon or a pressable segment
286
+ * stays as it is, clipped by the row if it must be.
287
+ */
288
+ function trimTrailing(info: PaneFooterSegment[], width: number, keep: number): PaneFooterSegment[] {
289
+ let fitted = info;
290
+ while (fitted.length > keep && infoTextWidth(fitted) > width) {
245
291
  const last = fitted.at(-1)!;
246
292
  if (last.icon || last.onPress || last.parts.some((part) => part.tone === "warning" || part.tone === "negative")) break;
247
- const room = width - infoTextWidth(fitted.slice(0, -1)) - 2;
293
+ // After the separator, a column short of the hints.
294
+ const room = width - infoTextWidth(fitted.slice(0, -1)) - segmentGapWidth(fitted.at(-2)!, last) - 1;
248
295
  if (room * 2 >= infoTextWidth([last])) return [...fitted.slice(0, -1), clipSegment(last, room)];
249
296
  fitted = fitted.slice(0, -1);
250
297
  }
251
298
  return fitted;
252
299
  }
253
300
 
301
+ /**
302
+ * The info segments as they fit `width`: segments with `variants` step down
303
+ * together, one variant at a time, until the row fits or none is left; beside
304
+ * the hints, the trailing status that still does not fit is then cut or left
305
+ * out (trimTrailing). A status after every segment with shorter forms gives
306
+ * way first, before any of them shortens: a pane puts what goes first last
307
+ * (an update time after a note that explains the row in hand).
308
+ *
309
+ * With no hints beside it (a pane picture's footer), the row clips the rest,
310
+ * so a frame that fits its own status line sees it overflow.
311
+ */
312
+ export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number, { besideHints = true }: { besideHints?: boolean } = {}): PaneFooterSegment[] {
313
+ if (!besideHints) return stepDownTogether(info, width);
314
+ const lastWithVariants = info.findLastIndex((segment) => (segment.variants?.length ?? 0) > 0);
315
+ if (lastWithVariants >= 0 && lastWithVariants < info.length - 1 && infoTextWidth(info) > width) {
316
+ const trimmed = trimTrailing(info, width, lastWithVariants + 1);
317
+ if (infoTextWidth(trimmed) <= width) return trimmed;
318
+ }
319
+ return trimTrailing(stepDownTogether(info, width), width, 1);
320
+ }
321
+
254
322
  /** A segment cut to `width` behind an ellipsis, each part keeping its tone; the full text stays as its title. */
255
323
  function clipSegment(segment: PaneFooterSegment, width: number): PaneFooterSegment {
256
324
  const full = segment.parts.map((part) => part.text).join(" ");
@@ -1,5 +1,7 @@
1
- import { useMemo } from "react";
2
- import { usePaneFooter, type PaneFooterSegment, type PaneHint } from "./footer";
1
+ import { useId, useMemo } from "react";
2
+ import { companyDataFooterSegment, usePaneFooter, type PaneFooterSegment, type PaneHint } from "./footer";
3
+ import type { CompanyFrom } from "../../../types/company-from";
4
+ import { useAppLanguage } from "../../../i18n/react";
3
5
  import { useExternalLinkFooter } from "../../use-external-link-footer";
4
6
  import { loadingErrorFooterInfo } from "../../data-table/table-pane";
5
7
 
@@ -28,6 +30,37 @@ function buildPaneStatusInfo({
28
30
  return [...info, ...(stale ? [STALE_SEGMENT] : []), ...loadingErrorFooterInfo(loading, error)];
29
31
  }
30
32
 
33
+ /**
34
+ * Where the company data note sits: after the warning indicator (-10) and
35
+ * before the pane's own status (0 and the ticker quote's -1), so it leads what
36
+ * the pane says about its data and a narrow footer cuts that status first.
37
+ */
38
+ const COMPANY_DATA_ORDER = -5;
39
+
40
+ /**
41
+ * A company pane on a receipt or a foreign line that shows its company's data
42
+ * from the home line says which: `company data from 000660 KRX · as of
43
+ * 2026-06-30`, before the pane's own status (after a warning). Nothing when
44
+ * the data is the listing's own. Every company pane registers it here, so it
45
+ * reads, looks and sits the same in each.
46
+ */
47
+ export function useCompanyDataFooter(companyFrom: CompanyFrom | null | undefined, enabled = true) {
48
+ const language = useAppLanguage();
49
+ const segment = useMemo(() => companyDataFooterSegment(companyFrom), [
50
+ language,
51
+ companyFrom?.symbol,
52
+ companyFrom?.exchange,
53
+ companyFrom?.name,
54
+ ]);
55
+ // Its own registration, so a view nested in a pane (13D/G under holders) never clears the pane's.
56
+ const id = useId();
57
+ usePaneFooter(
58
+ `company-data:${id}`,
59
+ () => enabled && segment ? { order: COMPANY_DATA_ORDER, info: [segment] } : null,
60
+ [enabled, segment],
61
+ );
62
+ }
63
+
31
64
  export function usePaneStatusFooter({
32
65
  registrationId,
33
66
  loading = false,
@@ -53,6 +53,15 @@ export function usePublishStatusWidgetRoom(columns: number): void {
53
53
  }, []);
54
54
  }
55
55
 
56
+ /**
57
+ * Extra columns the other widgets claim, for a widget that sizes itself to
58
+ * what they leave rather than run into them.
59
+ */
60
+ export function useOtherStatusWidgetClaims(id: string): number {
61
+ const others = () => claimedColumns() - (claims.get(id) ?? 0);
62
+ return useSyncExternalStore(subscribe, others, others);
63
+ }
64
+
56
65
  /** The columns the widgets can use, as the status bar last published them. */
57
66
  export function useStatusWidgetRoom(): number {
58
67
  return useSyncExternalStore(subscribe, () => room, () => room);