gloomberb 0.19.0 → 0.19.2

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 (304) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +2 -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/errors.ts +38 -0
  10. package/src/api-client/hiring.ts +3 -0
  11. package/src/api-client/index.ts +4 -0
  12. package/src/api-client/ipo.ts +24 -1
  13. package/src/api-client/mna.ts +3 -0
  14. package/src/api-client/paths.ts +9 -1
  15. package/src/api-client/roadmap.ts +85 -0
  16. package/src/api-client/types.ts +12 -0
  17. package/src/app/runtime/startup.ts +3 -1
  18. package/src/architecture/core-builtin-allowlist.json +2 -2
  19. package/src/capabilities/chart-series.ts +2 -1
  20. package/src/cli/commands/market.ts +44 -16
  21. package/src/cli/commands/news-search.ts +22 -0
  22. package/src/cli/commands/overview.ts +84 -19
  23. package/src/cli/commands/search.ts +13 -13
  24. package/src/cli/commands/ticker.ts +32 -2
  25. package/src/cli/context.ts +1 -0
  26. package/src/cli/errors.ts +17 -2
  27. package/src/cli/glossary.ts +8 -1
  28. package/src/cli/history-rows.ts +35 -7
  29. package/src/cli/index-roots.ts +0 -7
  30. package/src/cli/known-gaps.ts +11 -0
  31. package/src/cli/listing-arg.ts +74 -11
  32. package/src/cli/pane-functions/capabilities.ts +1 -0
  33. package/src/cli/pane-functions/discovery.ts +21 -1
  34. package/src/cli/pane-functions/freshness.ts +7 -1
  35. package/src/cli/pane-functions/function-help.ts +7 -0
  36. package/src/cli/pane-functions/index.ts +6 -3
  37. package/src/cli/quote-answer.ts +30 -7
  38. package/src/cli/saved-copy-persistence.ts +88 -0
  39. package/src/components/command-bar/list/model.ts +5 -0
  40. package/src/components/command-bar/routes/root/runtime.ts +29 -8
  41. package/src/components/command-bar/selection-runtime.ts +64 -32
  42. package/src/components/command-bar/surface/index.tsx +2 -0
  43. package/src/components/layout/detached-pane-shell.tsx +2 -2
  44. package/src/components/layout/header.tsx +2 -2
  45. package/src/components/layout/pane/capture-stamp.ts +2 -2
  46. package/src/components/layout/pane/footer/index.tsx +23 -6
  47. package/src/components/layout/pane/footer/model.ts +116 -16
  48. package/src/components/layout/pane/header.tsx +2 -2
  49. package/src/components/layout/pane/status-footer.ts +37 -3
  50. package/src/components/layout/status-widget-space.ts +9 -0
  51. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  52. package/src/components/onboarding/onboarding-wizard.tsx +6 -3
  53. package/src/components/ticker/list-table-view.tsx +4 -20
  54. package/src/components/ui/display.tsx +29 -3
  55. package/src/components/ui/icon.tsx +12 -3
  56. package/src/core/app-services.ts +11 -2
  57. package/src/data/app-persistence.ts +9 -7
  58. package/src/data/plugin-cache.ts +20 -3
  59. package/src/data/plugin-state-store.ts +81 -24
  60. package/src/data/resource-store.ts +130 -58
  61. package/src/data/saved-copies.ts +8 -0
  62. package/src/data/session-store.ts +31 -8
  63. package/src/data/sqlite/access.ts +313 -0
  64. package/src/data/sqlite/checkpointer.ts +198 -0
  65. package/src/data/sqlite/database.ts +47 -4
  66. package/src/data/sqlite/retry.ts +50 -9
  67. package/src/data/ticker-store.ts +57 -23
  68. package/src/docs/snapshot.json +32 -5
  69. package/src/i18n/es.ts +2 -4
  70. package/src/i18n/ja.ts +2 -4
  71. package/src/i18n/ko.ts +2 -4
  72. package/src/i18n/zh-cn.ts +2 -4
  73. package/src/i18n/zh-tw.ts +2 -4
  74. package/src/market-data/depositary-pair.ts +25 -1
  75. package/src/market-data/quotes/metadata.ts +7 -0
  76. package/src/market-data/realized-volatility.ts +4 -2
  77. package/src/news/aggregator.ts +7 -1
  78. package/src/news/types.ts +6 -0
  79. package/src/plugins/auto-update-node.ts +10 -6
  80. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  81. package/src/plugins/builtin/alerts/cli.ts +34 -11
  82. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  83. package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
  84. package/src/plugins/builtin/alerts/headless.ts +10 -6
  85. package/src/plugins/builtin/cds/client.ts +142 -30
  86. package/src/plugins/builtin/cds/headless.ts +35 -8
  87. package/src/plugins/builtin/cds/model.ts +145 -18
  88. package/src/plugins/builtin/cds/pane.tsx +16 -5
  89. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  90. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  91. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  92. package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
  93. package/src/plugins/builtin/chart-composer/index.tsx +4 -5
  94. package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
  95. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  96. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  97. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  98. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  99. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  100. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  101. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  102. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  103. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  104. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  105. package/src/plugins/builtin/clinical-trials/model.ts +86 -22
  106. package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
  107. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  108. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
  109. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  110. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  111. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  112. package/src/plugins/builtin/company-attention/pane.tsx +3 -0
  113. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  114. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  115. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  116. package/src/plugins/builtin/composite-plugins.ts +2 -1
  117. package/src/plugins/builtin/cot/client.ts +28 -2
  118. package/src/plugins/builtin/cot/headless.ts +4 -1
  119. package/src/plugins/builtin/cot/index.tsx +4 -2
  120. package/src/plugins/builtin/cot/model.ts +58 -7
  121. package/src/plugins/builtin/cot/pane.tsx +6 -2
  122. package/src/plugins/builtin/cpi/client.ts +6 -5
  123. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  124. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  125. package/src/plugins/builtin/credit-documents/headless.ts +3 -3
  126. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  127. package/src/plugins/builtin/crypto-board/series.ts +4 -4
  128. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  129. package/src/plugins/builtin/doe/client.ts +6 -5
  130. package/src/plugins/builtin/doe/headless.ts +3 -2
  131. package/src/plugins/builtin/doe/pane.tsx +13 -3
  132. package/src/plugins/builtin/doe/prices.ts +2 -3
  133. package/src/plugins/builtin/doe/series.ts +4 -4
  134. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  135. package/src/plugins/builtin/earnings-calls/data.ts +37 -17
  136. package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
  137. package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
  138. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  139. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  140. package/src/plugins/builtin/econ/detail-view.tsx +18 -4
  141. package/src/plugins/builtin/econ/figures.ts +48 -0
  142. package/src/plugins/builtin/econ/index.tsx +14 -5
  143. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  144. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  145. package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
  146. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  147. package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
  148. package/src/plugins/builtin/executives/data.ts +6 -1
  149. package/src/plugins/builtin/executives/pane.tsx +9 -21
  150. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  151. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  152. package/src/plugins/builtin/gpu/client.ts +4 -1
  153. package/src/plugins/builtin/gpu/series.ts +2 -3
  154. package/src/plugins/builtin/help/index.tsx +61 -59
  155. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  156. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  157. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  158. package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
  159. package/src/plugins/builtin/holders/headless.ts +4 -2
  160. package/src/plugins/builtin/holders/pane.tsx +3 -0
  161. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  162. package/src/plugins/builtin/insider/headless.ts +2 -2
  163. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  164. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  165. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  166. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  167. package/src/plugins/builtin/jodi/client.ts +4 -1
  168. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  169. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  170. package/src/plugins/builtin/macro-day/model.ts +39 -5
  171. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  172. package/src/plugins/builtin/market-movers/client.ts +3 -0
  173. package/src/plugins/builtin/market-movers/headless.ts +3 -1
  174. package/src/plugins/builtin/market-movers/index.tsx +10 -5
  175. package/src/plugins/builtin/market-movers/screener.ts +27 -6
  176. package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
  177. package/src/plugins/builtin/mna/headless.ts +2 -0
  178. package/src/plugins/builtin/mna/pane.tsx +3 -1
  179. package/src/plugins/builtin/money-markets/series.ts +3 -5
  180. package/src/plugins/builtin/news/headless.ts +3 -1
  181. package/src/plugins/builtin/news/index.tsx +3 -0
  182. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
  183. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  184. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  185. package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
  186. package/src/plugins/builtin/power/gas-client.ts +2 -1
  187. package/src/plugins/builtin/power/prices-client.ts +3 -2
  188. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  189. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  190. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  191. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  192. package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
  193. package/src/plugins/builtin/research/event-model.ts +30 -4
  194. package/src/plugins/builtin/research/index.tsx +7 -3
  195. package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
  196. package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
  197. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  198. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  199. package/src/plugins/builtin/risk-factors/data.ts +12 -6
  200. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  201. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  202. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  203. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  204. package/src/plugins/builtin/roadmap/model.ts +67 -0
  205. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  206. package/src/plugins/builtin/scanner/live-headless.ts +21 -2
  207. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  208. package/src/plugins/builtin/sec/headless.ts +2 -1
  209. package/src/plugins/builtin/sec/index.tsx +3 -0
  210. package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
  211. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  212. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  213. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  214. package/src/plugins/builtin/shared/report-freshness.ts +8 -0
  215. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  216. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  217. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  218. package/src/plugins/builtin/short-volume/series.ts +2 -3
  219. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  220. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  221. package/src/plugins/builtin/thirteenf/api.ts +15 -9
  222. package/src/plugins/builtin/thirteenf/data.ts +39 -11
  223. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  224. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  225. package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
  226. package/src/plugins/builtin/thirteenf/types.ts +2 -0
  227. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
  228. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  229. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  230. package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
  231. package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
  232. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  233. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
  234. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  235. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  236. package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
  237. package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
  238. package/src/plugins/builtin/volatility/client.ts +43 -16
  239. package/src/plugins/builtin/volatility/headless.ts +5 -2
  240. package/src/plugins/builtin/volatility/model.ts +17 -6
  241. package/src/plugins/builtin/volatility/pane.tsx +11 -4
  242. package/src/plugins/builtin/volatility/series.ts +2 -1
  243. package/src/plugins/builtin/yield-curve/client.ts +59 -1
  244. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  245. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  246. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  247. package/src/plugins/catalog-browser.ts +2 -1
  248. package/src/plugins/host-link.ts +1 -1
  249. package/src/plugins/installer.ts +50 -39
  250. package/src/portfolio/column-values.ts +11 -7
  251. package/src/renderers/dom/desktop/icons.tsx +10 -2
  252. package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
  253. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  254. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  255. package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
  256. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  257. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  258. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  259. package/src/renderers/electrobun/bun/index.ts +26 -2
  260. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  261. package/src/renderers/electrobun/bun/window/create.ts +45 -4
  262. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  263. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  264. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  265. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  266. package/src/renderers/electrobun/launcher/preload.ts +13 -0
  267. package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
  268. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  269. package/src/renderers/electrobun/view/news-source.ts +2 -0
  270. package/src/renderers/electrobun/view/ui-host.tsx +16 -1
  271. package/src/series/resolve.ts +2 -1
  272. package/src/series/types.ts +5 -0
  273. package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
  274. package/src/sources/gloomberb-cloud/index.ts +42 -8
  275. package/src/telemetry/stall-reports-node.ts +13 -2
  276. package/src/telemetry/stall-reports.ts +189 -13
  277. package/src/test-support/outage.ts +8 -0
  278. package/src/tickers/company-name-listing.ts +110 -40
  279. package/src/tickers/company-name.ts +71 -2
  280. package/src/tickers/index-roots.ts +7 -0
  281. package/src/tickers/listing-fate.ts +2 -2
  282. package/src/tickers/search/home-listing.ts +93 -8
  283. package/src/tickers/search/index.ts +7 -3
  284. package/src/tickers/search/ranking.ts +5 -1
  285. package/src/tickers/search/upsert.ts +13 -0
  286. package/src/tickers/ticker-changes.ts +8 -4
  287. package/src/time-series/market.ts +5 -3
  288. package/src/time-series/price-comparison.ts +20 -2
  289. package/src/time-series/resolve.ts +20 -1
  290. package/src/time-series/types.ts +7 -0
  291. package/src/types/company-from.ts +40 -0
  292. package/src/types/data-provider.ts +7 -0
  293. package/src/types/financials.ts +45 -3
  294. package/src/types/headless.ts +6 -0
  295. package/src/ui/host.tsx +6 -1
  296. package/src/utils/crypto-pair.ts +121 -7
  297. package/src/utils/declared-dividends.ts +1 -1
  298. package/src/utils/financial-statements.ts +73 -16
  299. package/src/utils/fundamentals.ts +53 -10
  300. package/src/utils/lease-liabilities.ts +54 -0
  301. package/src/utils/main-thread-stall.ts +47 -2
  302. package/src/utils/perf-marks.ts +23 -11
  303. package/src/utils/price-history-integrity.ts +47 -1
  304. package/src/version.ts +2 -2
@@ -21,6 +21,8 @@ import {
21
21
  notTradedMessage,
22
22
  oneWordCompanyName,
23
23
  quoteBareListing,
24
+ serviceNamedCompany,
25
+ type ListingArg,
24
26
  requireCliListing,
25
27
  resolveCliListing,
26
28
  tradingBoardNote,
@@ -64,7 +66,9 @@ export interface QuoteAnswer {
64
66
  /**
65
67
  * One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
66
68
  * is not listed on says so. A bare symbol the data service read on its home
67
- * listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
69
+ * listing abroad names that venue (SXR8 -> XETRA). One no source quotes that
70
+ * the service marks as another listing's company name is that company
71
+ * (`serviceNamedCompany`: "siemens" is SIE:XETRA); else it is
68
72
  * quoted on its listings (`quoteBareListing`), else names them, or asks for an
69
73
  * exchange when several companies list it (2222); a bare index root (VIX) also
70
74
  * names its index (^VIX), and a futures board code a security keeps (GC) its
@@ -98,6 +102,10 @@ export async function answerQuote(
98
102
  const notTraded = await notTradedMessage(listing, market);
99
103
  if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
100
104
  if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
105
+ // A word the service marks as a company's name is that company, before the symbol is read on its listings abroad.
106
+ const named = !listing.exchange && listing.typed ? await serviceNamedCompany(listing.typed, market) : null;
107
+ const meant = named ? await answerNamedListing(named, market, request, refresh, datesChangeBasis) : null;
108
+ if (meant) return meant;
101
109
  const other = await quoteBareListing(listing, market, request, { refresh, datesChangeBasis });
102
110
  if (other?.kind === "quoted") {
103
111
  return {
@@ -155,17 +163,32 @@ async function answerCompanyName(
155
163
  const named = await oneWordCompanyName(listing.typed, market);
156
164
  // A real ticker a source failed to quote is not a company's name, even when the company carries the word.
157
165
  if (!named || (unsettled && named.symbol.toUpperCase() === listing.symbol.toUpperCase())) return null;
158
- // A listing on a venue the app cannot read is no answer: the word keeps its own "Not a ticker".
166
+ const answer = await answerNamedListing(named, market, request, refresh, datesChangeBasis);
167
+ if (answer) return answer;
168
+ if (!unsettled) return null;
169
+ const sentence = await notATickerWords(listing.typed, market);
170
+ return { result: { ...result, error: new Error(sentence) }, listing, notes: [] };
171
+ }
172
+
173
+ /**
174
+ * The row of the listing a company's name was read as (`named`), saying so;
175
+ * null when the app cannot read its venue or it has no quote.
176
+ */
177
+ async function answerNamedListing(
178
+ named: ListingArg,
179
+ market: MarketContext,
180
+ request: ListingDataRequest,
181
+ refresh: boolean,
182
+ datesChangeBasis: boolean,
183
+ ): Promise<QuoteAnswer | null> {
184
+ // A listing on a venue the app cannot read is no answer: the word keeps its own.
159
185
  const resolved = await resolveCliListing(named.key, null, market).catch((error: unknown) => {
160
186
  if (error instanceof ListingArgError) return null;
161
187
  throw error;
162
188
  });
163
- const found = resolved ? { ...resolved, readAs: named.readAs, ...named.meant ? { meant: named.meant } : {} } : null;
189
+ const found = resolved ? { ...resolved, readAs: named.readAs, ...named.meant ? { meant: named.meant } : {}, ...named.readFromName ? { readFromName: named.readFromName } : {} } : null;
164
190
  const [quoted] = found ? await market.dataProvider.getQuotesBatch([found.request], { forceRefresh: refresh, revalidate: true, datesChangeBasis }) : [];
165
- if (found && quoted?.quote) return answerQuote(quoted, found, market, request, refresh, null, { datesChangeBasis });
166
- if (!unsettled) return null;
167
- const sentence = await notATickerWords(listing.typed, market);
168
- return { result: { ...result, error: new Error(sentence) }, listing, notes: [] };
191
+ return found && quoted?.quote ? answerQuote(quoted, found, market, request, refresh, null, { datesChangeBasis }) : null;
169
192
  }
170
193
 
171
194
  /** A listing a command found a quote for, with what `quote` says beside it. */
@@ -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];
@@ -159,6 +154,13 @@ export function useCommandBarRootRuntime({
159
154
  } {
160
155
  const previousRootSelectionContextRef = useRef<{ query: string; mode: string } | null>(null);
161
156
  const previousRootResultIdsRef = useRef<string[]>([]);
157
+ /**
158
+ * The row the untouched highlight rests on once the query's answer is in,
159
+ * and the answer it was on. A later answer that reorders the rows (a
160
+ * fresher search over a cached one, a broker's) moves the highlight with
161
+ * its row, so Enter opens the row that was highlighted.
162
+ */
163
+ const pinnedRootRowRef = useRef<{ id: string; answer: string | null } | null>(null);
162
164
  const activeMatch = matchPrefix(rootQuery, availableCommands);
163
165
 
164
166
  const tickerSearchRouteQuery = currentRoute?.kind === "mode" && currentRoute.screen === "ticker-search"
@@ -331,6 +333,7 @@ export function useCommandBarRootRuntime({
331
333
  previousRootSelectionContextRef.current = { query: rootQuery, mode: rootModeKind };
332
334
  if (selectionContextChanged) {
333
335
  rootSelectionNavigatedRef.current = false;
336
+ pinnedRootRowRef.current = null;
334
337
  }
335
338
 
336
339
  setRootSelectedIdx((current) => {
@@ -343,11 +346,23 @@ export function useCommandBarRootRuntime({
343
346
  if (shiftedIdx >= 0) return shiftedIdx;
344
347
  return clampIndex(current, resultIds.length);
345
348
  }
346
- // Untouched, the selection follows the best row on offer.
349
+ const pinnedIdx = pinnedRootRowRef.current ? resultIds.indexOf(pinnedRootRowRef.current.id) : -1;
350
+ if (pinnedIdx >= 0) return pinnedIdx;
351
+ // Untouched, the selection follows the best row on offer until the
352
+ // answer is in. When every row is an offer or a placeholder, none of
353
+ // them is the answer and none is selected: Enter must not take one of
354
+ // them for the ticker still being searched.
347
355
  const defaultIdx = orderedRootResults.findIndex(isDefaultSelectable);
348
- return clampIndex(Math.max(rootResultModel.initialIdx, defaultIdx), resultIds.length);
356
+ if (defaultIdx < 0) return -1;
357
+ const selected = clampIndex(Math.max(rootResultModel.initialIdx, defaultIdx), resultIds.length);
358
+ pinnedRootRowRef.current = rootResultsCurrent && resultIds[selected]
359
+ ? { id: resultIds[selected]!, answer: activeRootProviderResultsKey }
360
+ : null;
361
+ return selected;
349
362
  });
350
363
  }, [
364
+ activeRootProviderResultsKey,
365
+ rootResultsCurrent,
351
366
  activeMatch?.command.id,
352
367
  currentRoute,
353
368
  orderedRootResults,
@@ -361,7 +376,13 @@ export function useCommandBarRootRuntime({
361
376
 
362
377
  useEffect(() => {
363
378
  if (!activeRootProviderResultsKey) return;
379
+ // A later answer that only reorders the rows leaves the highlight on its row.
380
+ const pinned = pinnedRootRowRef.current;
381
+ if (pinned && pinned.answer !== activeRootProviderResultsKey
382
+ && orderedRootResults.some((item) => item.id === pinned.id)) return;
364
383
  setRootSelectedIdx(0);
384
+ const top = orderedRootResults[0];
385
+ pinnedRootRowRef.current = top ? { id: top.id, answer: activeRootProviderResultsKey } : null;
365
386
  setRootHoveredIdx(null);
366
387
  nativeListScrollRef.current?.scrollTo(0);
367
388
  }, [activeRootProviderResultsKey, nativeListScrollRef, setRootHoveredIdx, setRootSelectedIdx]);
@@ -1,9 +1,9 @@
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";
5
- import { findExactTickerSearchMatch, listingChoiceQuery } from "../../tickers/search";
6
- import { isKnownExchangeCode, parsePublicTickerKey } from "../../utils/exchanges";
5
+ import { listingChoiceQuery } from "../../tickers/search";
6
+ import { isKnownExchangeCode } from "../../utils/exchanges";
7
7
  import type { PluginRegistry } from "../../plugins/registry";
8
8
  import type { Command } from "./commands/registry";
9
9
  import type {
@@ -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,42 +284,39 @@ 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;
282
- // Enter submits the typed command until a row is deliberately chosen.
283
- // Search ranks suggestions by relevance; its first namesake must not
284
- // replace the listing the shared ticker resolver would open.
297
+ // Enter opens exactly the highlighted row. The typed command runs as typed
298
+ // only while no listing for it is highlighted: before its search answers
299
+ // (`DES HFC` then opens the listing `quote HFC` reads), or while the rows
300
+ // on screen are an earlier query's.
285
301
  const queryChanged = rootQueryRef.current !== rootQuery;
286
302
  const tickerArgument = rootShortcutIntent.kind !== "none" && rootShortcutIntent.argKind === "ticker"
287
303
  ? rootShortcutIntent.argText : null;
288
304
  const listingQuery = listingChoiceQuery(tickerArgument ?? rootQueryRef.current);
289
305
  const choosingVenue = listingQuery && !isKnownExchangeCode(listingQuery.suffix);
290
- const submitTyped = !options?.item && !currentRoute
306
+ const highlighted = listState.results[listState.selectedIdx];
307
+ const listingHighlighted = !queryChanged && (highlighted?.kind === "ticker" || highlighted?.kind === "search");
308
+ const submitTyped = !options?.item && !currentRoute && !listingHighlighted
291
309
  && (queryChanged || (!rootSelectionNavigatedRef.current && !choosingVenue));
292
- let typed = submitTyped && (queryChanged || tickerArgument !== null)
310
+ const typed = submitTyped && (queryChanged || tickerArgument !== null)
293
311
  ? resolveImmediateRootSelection(rootQueryRef.current) : null;
294
- const first = listState.results[listState.selectedIdx];
295
- if (typed && !queryChanged && rootShortcutIntent.kind !== "none" && rootShortcutIntent.argKind === "ticker"
296
- && first && (first.kind === "ticker" || first.kind === "search")) {
297
- const argument = rootShortcutIntent.argText;
298
- // Company names and partial symbols still accept the suggested match.
299
- // An exact symbol or named venue goes through listing resolution.
300
- if (!parsePublicTickerKey(argument).exchange && !findExactTickerSearchMatch(
301
- listState.results.filter((item) => item.kind === "ticker" || item.kind === "search"), argument,
302
- )) typed = null;
303
- }
304
- // A plain symbol follows the same path once it is the default ticker row.
305
- // Commands and company-name searches keep their own default actions.
306
- const plainSymbol = submitTyped && !typed && first && (first.kind === "ticker" || first.kind === "search")
307
- && findExactTickerSearchMatch([first], rootQueryRef.current);
308
- if (plainSymbol && first) {
309
- const symbol = rootQueryRef.current;
310
- typed = { ...first, action: () => runSecurityDescriptionShortcut(symbol),
311
- secondaryAction: () => runSecurityDescriptionShortcut(symbol, { forceNewPane: true }) };
312
- }
313
312
  const selected = options?.item ?? typed ?? listState.results[listState.selectedIdx];
314
- if (!selected || selected.disabled) return;
313
+ if (!selected) {
314
+ if (searchPending && !currentRoute && !options?.item && !rootSelectionNavigatedRef.current) {
315
+ pendingEnterRef.current = { query: rootQueryRef.current, secondary: options?.secondary === true };
316
+ }
317
+ return;
318
+ }
319
+ if (selected.disabled) return;
315
320
 
316
321
  // Opened from an add row: a venue picked from the colon list joins that
317
322
  // collection. Any other text or screen runs as usual.
@@ -378,8 +383,8 @@ export function useCommandBarSelectionRuntime({
378
383
  item: selected,
379
384
  query,
380
385
  rank: selected === typed ? 0 : Math.max(0, listState.results.indexOf(selected)),
381
- // A plain symbol stays a ticker choice; a submitted command is the shortcut itself.
382
- isShortcut: () => !plainSymbol && (selected === typed || resolveImmediateRootSelection(query)?.id === selected.id),
386
+ // A listing row stays a ticker choice; a submitted command is the shortcut itself.
387
+ isShortcut: () => !listingHighlighted && (selected === typed || resolveImmediateRootSelection(query)?.id === selected.id),
383
388
  }, run);
384
389
  }, [
385
390
  closeAll,
@@ -395,12 +400,39 @@ export function useCommandBarSelectionRuntime({
395
400
  rootSelectionNavigatedRef,
396
401
  rootShortcutIntent,
397
402
  runRootRow,
398
- runSecurityDescriptionShortcut,
403
+ searchPending,
399
404
  stateConfigLayout,
400
405
  stateRef,
401
406
  visibleListStateRef,
402
407
  ]);
403
408
 
409
+ useEffect(() => {
410
+ const pending = pendingEnterRef.current;
411
+ if (!pending) return;
412
+ if (currentRoute || rootQueryRef.current !== pending.query || rootSelectionNavigatedRef.current) {
413
+ pendingEnterRef.current = null;
414
+ return;
415
+ }
416
+ const listState = visibleListStateRef.current;
417
+ if (!listState?.results.some(isDefaultSelectable)) {
418
+ // Still searching: the answer may bring a row. Answered with none: there is nothing to run.
419
+ if (!searchPending) pendingEnterRef.current = null;
420
+ return;
421
+ }
422
+ // The selection follows the rows in the render after they land.
423
+ if (!listState.results[listState.selectedIdx]) return;
424
+ pendingEnterRef.current = null;
425
+ activateListSelection({ secondary: pending.secondary });
426
+ }, [
427
+ activateListSelection,
428
+ currentRoute,
429
+ rootQueryRef,
430
+ rootSelectedIdx,
431
+ rootSelectionNavigatedRef,
432
+ searchPending,
433
+ visibleListStateRef,
434
+ ]);
435
+
404
436
  return {
405
437
  acceptRootShortcutTab,
406
438
  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,
@@ -170,9 +170,9 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
170
170
  fireAndForget(desktopWindowBridge.closeDetachedPane?.(desktopWindowBridge.paneId), "desktop.closeDetachedPane");
171
171
  }, [desktopWindowBridge, locked, pluginRegistry]);
172
172
 
173
- const startWindowDrag = useCallback(() => {
173
+ const startWindowDrag = useCallback((press?: { detail?: number }) => {
174
174
  focusPane();
175
- if (nativeWindowChrome) void rendererHost.startWindowDrag?.();
175
+ if (nativeWindowChrome) void rendererHost.startWindowDrag?.(press);
176
176
  }, [focusPane, nativeWindowChrome, rendererHost]);
177
177
 
178
178
  const sharePane = useCallback(() => {
@@ -870,9 +870,9 @@ export function Header({
870
870
  // bar itself wears the prompt's fill and reads as a single strip.
871
871
  const headerBg = nativePaneChrome ? colors.header : headerSurface(colors);
872
872
 
873
- const startWindowDrag = useCallback(() => {
873
+ const startWindowDrag = useCallback((press?: { detail?: number }) => {
874
874
  if (!titleBarOverlay || !nativeWindowChrome) return;
875
- void rendererHost.startWindowDrag?.();
875
+ void rendererHost.startWindowDrag?.(press);
876
876
  }, [nativeWindowChrome, rendererHost, titleBarOverlay]);
877
877
 
878
878
  const openCommandBar = useCallback((event?: HeaderActionEvent) => {
@@ -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,10 +24,12 @@ import {
22
24
  } from "./model";
23
25
 
24
26
  export {
27
+ companyDataFooterSegment,
25
28
  datesPaneData,
26
29
  DELAYED_SEGMENT,
27
30
  delayedFooterSegment,
28
31
  hasPaneFooterContent,
32
+ savedCopyFooterInfo,
29
33
  savedCopyFooterSegment,
30
34
  splitDelayedStatus,
31
35
  type CombinedPaneFooter,
@@ -267,6 +271,7 @@ function FooterContent({
267
271
  // A segment with shorter forms takes the longest that fits beside the hints, gaps and terminal keys included.
268
272
  const keysWidth = nativePaneChrome ? 0 : footer.info.reduce((total, segment) => total + terminalSegmentKey(segment, focused).length, 0);
269
273
  const info = infoWidth != null ? fitPaneFooterInfo(footer.info, infoWidth - keysWidth, { besideHints: hasHints }) : footer.info;
274
+ const lastTextIndex = info.findLastIndex((segment) => !segment.icon);
270
275
 
271
276
  if (!hasInfo && !hasHints) {
272
277
  return <Box flexGrow={1} height={1} />;
@@ -291,12 +296,24 @@ function FooterContent({
291
296
  flexShrink={1}
292
297
  {...(infoWidth != null ? { width: infoWidth } : {})}
293
298
  >
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
- ))}
299
+ {info.map((segment, index) => {
300
+ // Every pane's segments are set apart the same way, so two never read as one sentence.
301
+ const separated = index > 0 && separatesPaneFooterSegments(info[index - 1]!, segment);
302
+ return (
303
+ <Box
304
+ key={segment.id}
305
+ flexDirection="row"
306
+ overflow="hidden"
307
+ // A row too narrow cuts from its end: the last text segment gives way, then the row clips the
308
+ // one before it. An icon is a few cells that mean nothing cut, and earlier text says more.
309
+ flexShrink={!segment.icon && index === lastTextIndex ? 1 : 0}
310
+ marginLeft={index > 0 && !separated ? 1 : 0}
311
+ >
312
+ {separated && <Box flexShrink={0}><Text fg={colors.textMuted}>{PANE_FOOTER_SEPARATOR}</Text></Box>}
313
+ <SegmentView segment={segment} focused={focused} />
314
+ </Box>
315
+ );
316
+ })}
300
317
  </Box>
301
318
  )}
302
319
  {hasHints && (