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
@@ -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,11 +45,19 @@ 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;
54
+ /**
55
+ * When the copy the pane shows was saved, on the saved-copy segment
56
+ * ({@link savedCopyFooterSegment}). The footer draws that segment first,
57
+ * wherever a pane registers it, and once: the oldest when several parts of
58
+ * the pane show a saved copy.
59
+ */
60
+ savedCopyAt?: number;
52
61
  /**
53
62
  * Shorter forms of `parts`, longest first. When the row is too narrow for
54
63
  * `parts` beside the hints, it shows the first that fits, or the last, and
@@ -111,12 +120,43 @@ export function savedCopyFooterSegment(id: string, savedAt: number, now = Date.n
111
120
  return {
112
121
  id,
113
122
  quoteStatus: true,
123
+ savedCopyAt: savedAt,
114
124
  title: `${t("Couldn't refresh")}: ${text}`,
115
125
  parts: [{ text, tone: "warning" }],
116
126
  variants: [[{ text: t("saved copy"), tone: "warning" }]],
117
127
  };
118
128
  }
119
129
 
130
+ /**
131
+ * The saved-copy segment of a pane showing several answers (a list and the
132
+ * report it opens): the oldest of the copies among `savedAts` (ms), or no
133
+ * segment when none of them is one.
134
+ */
135
+ export function savedCopyFooterInfo(savedAts: readonly (number | null | undefined)[], id = "saved-copy"): PaneFooterSegment[] {
136
+ const times = savedAts.filter((at): at is number => typeof at === "number" && Number.isFinite(at));
137
+ return times.length > 0 ? [savedCopyFooterSegment(id, Math.min(...times))] : [];
138
+ }
139
+
140
+ /**
141
+ * The footer's one form for company data a pane shows from another listing
142
+ * of the company: a receipt or a foreign line (SKHY, HY9H in Frankfurt) reads
143
+ * its home line's news, filings or holders, `company data from 000660 KRX`.
144
+ * Null when the data is the listing's own.
145
+ */
146
+ export function companyDataFooterSegment(from: CompanyFrom | null | undefined): PaneFooterSegment | null {
147
+ if (!from) return null;
148
+ const listing = companyFromLabel(from);
149
+ const text = tf("company data from {listing}", { listing });
150
+ return {
151
+ id: "company-data",
152
+ title: from.name ? `${text} (${from.name})` : text,
153
+ // A capture of the rows keeps it: the picture cannot say whose they are.
154
+ capture: true,
155
+ parts: [{ text, tone: "muted" }],
156
+ variants: [[{ text: tf("from {listing}", { listing }), tone: "muted" }]],
157
+ };
158
+ }
159
+
120
160
  /** A pane whose market data is all delayed, by no one figure: the clock alone, its tooltip saying so. */
121
161
  export const DELAYED_SEGMENT: PaneFooterSegment = delayedFooterSegment({ id: "delayed", detail: "Delayed market data" });
122
162
 
@@ -202,6 +242,25 @@ const INFO_FLOOR_CHARS = 10;
202
242
  const WARNING_FLOOR_CHARS = 24;
203
243
  const HINT_GAP = 1;
204
244
 
245
+ /**
246
+ * What the footer draws between two info segments, muted, as a status draws
247
+ * between its own parts ("Newest story Mon 14:23 · 12h delayed"): two
248
+ * segments side by side never read as one sentence ("company data from PBR ·
249
+ * as of 2026-06-30"). An icon (`⚠[!]`, `◷15m`) is a mark, not a phrase: a
250
+ * column sets it apart.
251
+ */
252
+ export const PANE_FOOTER_SEPARATOR = " · ";
253
+ const SEPARATOR_WIDTH = displayWidth(PANE_FOOTER_SEPARATOR);
254
+
255
+ /** Whether the footer draws {@link PANE_FOOTER_SEPARATOR} between two adjacent segments, else a one-column gap. */
256
+ export function separatesPaneFooterSegments(before: Pick<PaneFooterSegment, "icon">, after: Pick<PaneFooterSegment, "icon">): boolean {
257
+ return !before.icon && !after.icon;
258
+ }
259
+
260
+ function segmentGapWidth(before: PaneFooterSegment, after: PaneFooterSegment): number {
261
+ return separatesPaneFooterSegments(before, after) ? SEPARATOR_WIDTH : 1;
262
+ }
263
+
205
264
  function paneHintWidth(hint: Pick<PaneHint, "key" | "label">, prefix = ""): number {
206
265
  return getShortcutHintWidth(hint.key, hint.label, prefix);
207
266
  }
@@ -216,15 +275,12 @@ function infoTextWidth(segments: readonly PaneFooterSegment[]): number {
216
275
  const text = segment.parts.reduce((sum, part, partIndex) => (
217
276
  sum + (partIndex > 0 ? 1 : 0) + displayWidth(part.text)
218
277
  ), 0);
219
- return total + (index > 0 ? 1 : 0) + text;
278
+ return total + (index > 0 ? segmentGapWidth(segments[index - 1]!, segment) : 0) + text;
220
279
  }, 0);
221
280
  }
222
281
 
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[] {
282
+ /** Segments with `variants` step down together, one variant at a time, until they fit or none is left. */
283
+ function stepDownTogether(info: PaneFooterSegment[], width: number): PaneFooterSegment[] {
228
284
  const stages = Math.max(0, ...info.map((segment) => segment.variants?.length ?? 0));
229
285
  let fitted = info;
230
286
  for (let stage = 0; stage < stages && infoTextWidth(fitted) > width; stage += 1) {
@@ -237,20 +293,50 @@ export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number, { be
237
293
  } : segment;
238
294
  });
239
295
  }
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) {
296
+ return fitted;
297
+ }
298
+
299
+ /**
300
+ * A trailing status cut mid-word reads as part of the hints beside it ("quote
301
+ * 12h t[x] put"): one that does not fit ends in an ellipsis a column short of
302
+ * them, or is left out when less than half of it would show, until the row
303
+ * fits or `keep` segments are left. A warning, an icon or a pressable segment
304
+ * stays as it is, clipped by the row if it must be.
305
+ */
306
+ function trimTrailing(info: PaneFooterSegment[], width: number, keep: number): PaneFooterSegment[] {
307
+ let fitted = info;
308
+ while (fitted.length > keep && infoTextWidth(fitted) > width) {
245
309
  const last = fitted.at(-1)!;
246
310
  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;
311
+ // After the separator, a column short of the hints.
312
+ const room = width - infoTextWidth(fitted.slice(0, -1)) - segmentGapWidth(fitted.at(-2)!, last) - 1;
248
313
  if (room * 2 >= infoTextWidth([last])) return [...fitted.slice(0, -1), clipSegment(last, room)];
249
314
  fitted = fitted.slice(0, -1);
250
315
  }
251
316
  return fitted;
252
317
  }
253
318
 
319
+ /**
320
+ * The info segments as they fit `width`: segments with `variants` step down
321
+ * together, one variant at a time, until the row fits or none is left; beside
322
+ * the hints, the trailing status that still does not fit is then cut or left
323
+ * out (trimTrailing). A status after every segment with shorter forms gives
324
+ * way first, before any of them shortens: a pane puts what goes first last
325
+ * (an update time after a note that explains the row in hand).
326
+ *
327
+ * With no hints beside it (a pane picture's footer), the row clips the rest,
328
+ * so a frame that fits its own status line sees it overflow.
329
+ */
330
+ export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number, { besideHints = true }: { besideHints?: boolean } = {}): PaneFooterSegment[] {
331
+ if (!besideHints) return stepDownTogether(info, width);
332
+ const lastWithVariants = info.findLastIndex((segment) => (segment.variants?.length ?? 0) > 0);
333
+ if (lastWithVariants >= 0 && lastWithVariants < info.length - 1 && infoTextWidth(info) > width) {
334
+ const trimmed = trimTrailing(info, width, lastWithVariants + 1);
335
+ if (infoTextWidth(trimmed) <= width) return trimmed;
336
+ }
337
+ return trimTrailing(stepDownTogether(info, width), width, 1);
338
+ }
339
+
254
340
  /** A segment cut to `width` behind an ellipsis, each part keeping its tone; the full text stays as its title. */
255
341
  function clipSegment(segment: PaneFooterSegment, width: number): PaneFooterSegment {
256
342
  const full = segment.parts.map((part) => part.text).join(" ");
@@ -382,7 +468,20 @@ export function combinePaneFooterRegistrations(registrations: Map<string, PaneFo
382
468
  const shown = yielded.length > 0 ? hints.filter((hint) => !yielded.includes(hint)) : hints;
383
469
 
384
470
  if (info.length === 0 && shown.length === 0 && menu.length === 0 && keys.length === 0) return EMPTY_FOOTER;
385
- return { info, hints: shown, menu, keys };
471
+ return { info: savedCopyFirst(info), hints: shown, menu, keys };
472
+ }
473
+
474
+ /**
475
+ * The saved-copy segment in its one place: first, so it reads the same in
476
+ * every pane and is the last a narrow row cuts, and once, the oldest copy,
477
+ * when a list and the report it opens each register one.
478
+ */
479
+ function savedCopyFirst(info: PaneFooterSegment[]): PaneFooterSegment[] {
480
+ let oldest: PaneFooterSegment | undefined;
481
+ for (const segment of info) {
482
+ if (segment.savedCopyAt != null && (oldest?.savedCopyAt == null || segment.savedCopyAt < oldest.savedCopyAt)) oldest = segment;
483
+ }
484
+ return oldest ? [oldest, ...info.filter((segment) => segment.savedCopyAt == null)] : info;
386
485
  }
387
486
 
388
487
  function sameFooterParts(left: PaneFooterPart[], right: PaneFooterPart[]): boolean {
@@ -439,6 +538,7 @@ export function samePaneFooterRegistration(
439
538
  && segment.shortcut === other.shortcut
440
539
  && !!segment.onPress === !!other.onPress
441
540
  && segment.disabled === other.disabled
541
+ && segment.savedCopyAt === other.savedCopyAt
442
542
  && sameFooterParts(segment.parts, other.parts)
443
543
  && (segment.variants?.length ?? 0) === (other.variants?.length ?? 0)
444
544
  && (segment.variants ?? []).every((variant, variantIndex) => sameFooterParts(variant, other.variants![variantIndex]!));
@@ -178,13 +178,13 @@ export function PaneHeader({
178
178
  event.stopPropagation?.();
179
179
  onHeaderMouseDown?.(event);
180
180
  };
181
- const beginWindowDrag = (event?: { stopPropagation?: () => void }) => {
181
+ const beginWindowDrag = (event?: { stopPropagation?: () => void; detail?: number }) => {
182
182
  event?.stopPropagation?.();
183
183
  if (titleBar) {
184
184
  onHeaderMouseDown?.(event);
185
185
  return;
186
186
  }
187
- void rendererHost.startWindowDrag?.();
187
+ void rendererHost.startWindowDrag?.(event);
188
188
  };
189
189
  const barHandlers = windowDrag
190
190
  ? { onMouseDown: beginWindowDrag }
@@ -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
 
@@ -9,7 +11,8 @@ const EMPTY_STATUS_INFO: PaneFooterSegment[] = [];
9
11
  // has no per-pane footer hint. Do not reintroduce one. See PR #589. Panes bind
10
12
  // it with `usePaneRefreshKey`.
11
13
 
12
- const STALE_SEGMENT: PaneFooterSegment = { id: "stale", parts: [{ text: "stale", tone: "warning" }] };
14
+ /** The shared warning for data on screen older than it should be, for a pane composing its own footer. */
15
+ export const STALE_SEGMENT: PaneFooterSegment = { id: "stale", parts: [{ text: "stale", tone: "warning" }] };
13
16
 
14
17
  interface PaneStatusInfoOptions {
15
18
  loading?: boolean;
@@ -28,6 +31,37 @@ function buildPaneStatusInfo({
28
31
  return [...info, ...(stale ? [STALE_SEGMENT] : []), ...loadingErrorFooterInfo(loading, error)];
29
32
  }
30
33
 
34
+ /**
35
+ * Where the company data note sits: after the warning indicator (-10) and
36
+ * before the pane's own status (0 and the ticker quote's -1), so it leads what
37
+ * the pane says about its data and a narrow footer cuts that status first.
38
+ */
39
+ const COMPANY_DATA_ORDER = -5;
40
+
41
+ /**
42
+ * A company pane on a receipt or a foreign line that shows its company's data
43
+ * from the home line says which: `company data from 000660 KRX · as of
44
+ * 2026-06-30`, before the pane's own status (after a warning). Nothing when
45
+ * the data is the listing's own. Every company pane registers it here, so it
46
+ * reads, looks and sits the same in each.
47
+ */
48
+ export function useCompanyDataFooter(companyFrom: CompanyFrom | null | undefined, enabled = true) {
49
+ const language = useAppLanguage();
50
+ const segment = useMemo(() => companyDataFooterSegment(companyFrom), [
51
+ language,
52
+ companyFrom?.symbol,
53
+ companyFrom?.exchange,
54
+ companyFrom?.name,
55
+ ]);
56
+ // Its own registration, so a view nested in a pane (13D/G under holders) never clears the pane's.
57
+ const id = useId();
58
+ usePaneFooter(
59
+ `company-data:${id}`,
60
+ () => enabled && segment ? { order: COMPANY_DATA_ORDER, info: [segment] } : null,
61
+ [enabled, segment],
62
+ );
63
+ }
64
+
31
65
  export function usePaneStatusFooter({
32
66
  registrationId,
33
67
  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);
@@ -219,7 +219,8 @@ export function OnboardingCoach({
219
219
  </Box>
220
220
  <Box height={1} />
221
221
  <Box flexGrow={1} minHeight={0}>{children}</Box>
222
- <Box height={1} flexDirection="row" gap={1}>{actions}</Box>
222
+ {/* As OnboardingActions: a button that does not fit beside the one before it wraps under it. */}
223
+ <Box flexDirection="row" flexWrap="wrap" alignItems="center" columnGap={1} flexShrink={0}>{actions}</Box>
223
224
  </Box>
224
225
  );
225
226
  }
@@ -322,12 +322,15 @@ export function OnboardingWizard(props: OnboardingWizardProps) {
322
322
  return <OnboardingCoach step={t("YOUR WORKSPACE")}
323
323
  title={tf("Built around {ticker}", { ticker })}
324
324
  actions={<>
325
- {/* The card has no room for both keys: like a toast, the dismiss key
326
- is in the desktop tooltip. F10 also dismisses it. */}
325
+ {/* Each button shows its key, as Connect shows Alt+Enter; in the
326
+ terminal the second wraps under the first. The desktop card has no
327
+ room for both: like a toast, its dismiss key is in the tooltip.
328
+ F10 also dismisses it. */}
327
329
  <OnboardingButton
328
330
  label="Keep exploring"
329
331
  variant="ghost"
330
- title={notificationDismissShortcut ? `${t("Keep exploring")} (${notificationDismissShortcut})` : undefined}
332
+ shortcut={!desktop && notificationDismissShortcut ? notificationDismissShortcut : undefined}
333
+ title={desktop && notificationDismissShortcut ? `${t("Keep exploring")} (${notificationDismissShortcut})` : undefined}
331
334
  onPress={() => { void finish(); }}
332
335
  />
333
336
  <OnboardingButton
@@ -29,8 +29,6 @@ import type { QuoteFlashDirection } from "../quote-flash";
29
29
  import { objectVersion } from "../../utils/object-version";
30
30
  import { getCompletedRegularSessionDisplay } from "../../market-data/market/status";
31
31
  import { usePaneFooter } from "../layout/pane/footer";
32
- import { Button } from "../ui/button";
33
- import { PaneStatusBody } from "../ui/status";
34
32
 
35
33
  export interface TickerTableCell {
36
34
  text: string;
@@ -99,11 +97,10 @@ export interface TickerListTableViewProps {
99
97
  emptyTitle?: string;
100
98
  emptyHint?: string;
101
99
  /**
102
- * Starts the pane's add-ticker flow. Given, the empty table offers a
103
- * "+ ticker" button for it and says so before the key.
100
+ * The pane's own key for adding a ticker, which the empty table names. The
101
+ * command bar's key when absent. The pane shows its add control once, in its
102
+ * own row, so the empty table points at the key and offers no second one.
104
103
  */
105
- onAddTicker?: () => void;
106
- /** The pane's key for the same flow, named after the click. The command bar's key when absent. */
107
104
  addTickerShortcut?: string;
108
105
  virtualize?: boolean;
109
106
  overscan?: number;
@@ -237,24 +234,12 @@ export function TickerListTableView({
237
234
  rowMenuAccelerators,
238
235
  emptyTitle = t("No tickers."),
239
236
  emptyHint,
240
- onAddTicker,
241
237
  addTickerShortcut,
242
238
  virtualize = true,
243
239
  overscan = 4,
244
240
  }: TickerListTableViewProps) {
245
241
  const commandBarShortcut = useCommandBarShortcut();
246
- const addLabel = `+ ${t("ticker")}`;
247
- const resolvedEmptyHint = emptyHint ?? (onAddTicker
248
- ? tf("Click {button} to add one, or press {shortcut}.", { button: addLabel, shortcut: addTickerShortcut ?? commandBarShortcut })
249
- : tf("Press {shortcut} to add one.", { shortcut: commandBarShortcut }));
250
- const emptyContent = onAddTicker ? (
251
- <PaneStatusBody
252
- empty
253
- emptyTitle={emptyTitle}
254
- emptyMessage={resolvedEmptyHint}
255
- actions={<Button label="Add ticker" displayLabel={addLabel} compact onPress={onAddTicker} />}
256
- />
257
- ) : undefined;
242
+ const resolvedEmptyHint = emptyHint ?? tf("Press {shortcut} to add one.", { shortcut: addTickerShortcut ?? commandBarShortcut });
258
243
  const renderer = useRendererHost();
259
244
  const { showContextMenu } = useContextMenu();
260
245
  const { nativeContextMenu } = useUiCapabilities();
@@ -398,7 +383,6 @@ export function TickerListTableView({
398
383
  getRowVersion={getRowVersion}
399
384
  emptyStateTitle={emptyTitle}
400
385
  emptyStateHint={resolvedEmptyHint}
401
- emptyContent={emptyContent}
402
386
  virtualize={virtualize}
403
387
  overscan={overscan}
404
388
  rootBefore={rootBefore}
@@ -7,6 +7,7 @@ import { Box, Text, TextAttributes, useUiCapabilities } from "../../ui";
7
7
  import { truncateToDisplayWidth } from "../../utils/format";
8
8
  import { CountryFlagLead } from "./country-flag";
9
9
  import { usePaneLinkMenuEntry } from "./external-link";
10
+ import { ICON_GLYPHS, Icon, type IconName } from "./icon";
10
11
  import { titleCase } from "./header-case";
11
12
  import { statToneColor, type StatTone } from "./stat-grid";
12
13
 
@@ -199,6 +200,10 @@ export interface BadgeProps {
199
200
  /** For a domain scale, such as a sentiment score or chart series. */
200
201
  color?: string;
201
202
  variant?: "subtle" | "solid";
203
+ /** A mark before the label, in its colour: an SVG on the desktop, its glyph in the terminal. */
204
+ icon?: IconName;
205
+ /** False for a label that is not English to translate, such as a username. */
206
+ translate?: boolean;
202
207
  }
203
208
 
204
209
  /**
@@ -209,9 +214,20 @@ export interface BadgeProps {
209
214
  export const NATIVE_BADGE_STYLE = { height: "calc(100% - 2px)", borderRadius: "var(--gloom-radius-sm)" };
210
215
  /** How far a subtle chip's fill is tinted toward its tone. */
211
216
  export const BADGE_TINT = 0.28;
217
+ const NATIVE_ICON_BADGE_STYLE = { ...NATIVE_BADGE_STYLE, gap: "var(--gloom-space-xs)" };
212
218
  const NATIVE_BADGE_TEXT_STYLE = { fontWeight: "var(--gloom-weight-regular)" };
219
+ /**
220
+ * A label with lowercase letters has them in the middle of the chip,
221
+ * wherever the font puts them in its line: the line is cut to x-height and
222
+ * baseline, and that is what the chip centres. Centred whole, SF Mono's
223
+ * lowercase sit a pixel and a half low. An icon centres in the chip, so it
224
+ * reads on the letters' line. A label in capitals and digits keeps the
225
+ * whole line, as ticker chips do; a browser without `text-box` keeps it too.
226
+ */
227
+ const NATIVE_LOWERCASE_BADGE_TEXT_STYLE = { ...NATIVE_BADGE_TEXT_STYLE, textBox: "trim-both ex alphabetic" };
228
+ const LOWERCASE = /\p{Ll}/u;
213
229
 
214
- export function Badge({ label, tone = "neutral", color, variant = "subtle" }: BadgeProps) {
230
+ export function Badge({ label, tone = "neutral", color, variant = "subtle", icon, translate = true }: BadgeProps) {
215
231
  const colors = useThemeColors();
216
232
  const { nativePaneChrome } = useUiCapabilities();
217
233
  const accent = color ?? (tone === "neutral" ? colors.textDim : tone === "accent" ? colors.borderFocused : colors[tone]);
@@ -220,10 +236,20 @@ export function Badge({ label, tone = "neutral", color, variant = "subtle" }: Ba
220
236
  const background = neutral ? colors.selected : solid ? accent : blendHex(colors.bg, accent, BADGE_TINT);
221
237
  const baseForeground = neutral ? colors.selectedText : solid ? colors.bg : accent;
222
238
  const foreground = blendForContrast(baseForeground, background, higherContrast("#000000", "#ffffff", background), 4.5);
239
+ const text = translate ? t(label) : label;
240
+ const nativeIcon = nativePaneChrome && icon;
241
+ // The terminal draws the icon's glyph as text in front of the label.
242
+ const shown = icon && !nativePaneChrome ? [ICON_GLYPHS[icon], text].filter(Boolean).join(" ") : text;
223
243
  const chip = (
224
244
  <Box height={nativePaneChrome ? undefined : 1} paddingX={1} backgroundColor={background}
225
- data-gloom-ui="badge" justifyContent={nativePaneChrome ? "center" : undefined} style={nativePaneChrome ? NATIVE_BADGE_STYLE : undefined}>
226
- <Text fg={foreground} attributes={TextAttributes.BOLD} style={nativePaneChrome ? NATIVE_BADGE_TEXT_STYLE : undefined}>{t(label)}</Text>
245
+ data-gloom-ui="badge" {...(nativeIcon ? { flexDirection: "row", alignItems: "center" } as const : {})}
246
+ justifyContent={nativePaneChrome && !nativeIcon ? "center" : undefined}
247
+ style={nativePaneChrome ? nativeIcon ? NATIVE_ICON_BADGE_STYLE : NATIVE_BADGE_STYLE : undefined}>
248
+ {nativeIcon ? <Icon name={nativeIcon} color={foreground} fullHeight /> : null}
249
+ {shown || !nativeIcon ? (
250
+ <Text fg={foreground} attributes={TextAttributes.BOLD}
251
+ style={nativePaneChrome ? LOWERCASE.test(text) ? NATIVE_LOWERCASE_BADGE_TEXT_STYLE : NATIVE_BADGE_TEXT_STYLE : undefined}>{shown}</Text>
252
+ ) : null}
227
253
  </Box>
228
254
  );
229
255
  return nativePaneChrome ? <Box height={1} flexShrink={0} justifyContent="center">{chip}</Box> : chip;
@@ -38,7 +38,8 @@ export type IconName =
38
38
  | "team"
39
39
  | "pro"
40
40
  | "clock"
41
- | "edit";
41
+ | "edit"
42
+ | "mail";
42
43
 
43
44
  /** Terminal glyph for each icon; the desktop host draws SVG. */
44
45
  export const ICON_GLYPHS: Record<IconName, string> = {
@@ -84,6 +85,8 @@ export const ICON_GLYPHS: Record<IconName, string> = {
84
85
  // Time, for data that arrives late (a delayed quote).
85
86
  clock: "◷",
86
87
  edit: "✎",
88
+ // A message written to you (a DM): text presentation, one cell.
89
+ mail: "✉",
87
90
  };
88
91
 
89
92
  export interface IconProps {
@@ -91,13 +94,19 @@ export interface IconProps {
91
94
  /** Pixel size on the desktop; the terminal draws one glyph. */
92
95
  size?: number;
93
96
  color?: string;
97
+ /**
98
+ * Desktop: takes its row's full height and draws in the middle of it. A
99
+ * box centred in a row an odd number of pixels taller lands on a whole
100
+ * pixel, half a pixel off; the drawing inside the box does not.
101
+ */
102
+ fullHeight?: boolean;
94
103
  }
95
104
 
96
105
  /** One glyph from the app's icon set. */
97
- export function Icon({ name, size = 12, color }: IconProps) {
106
+ export function Icon({ name, size = 12, color, fullHeight }: IconProps) {
98
107
  const colors = useThemeColors();
99
108
  const HostIcon = useUiHost().Icon as ComponentType<IconProps> | undefined;
100
- if (HostIcon) return <HostIcon name={name} size={size} color={color} />;
109
+ if (HostIcon) return <HostIcon name={name} size={size} color={color} fullHeight={fullHeight} />;
101
110
  return <Text fg={color ?? colors.textDim} selectable={false}>{ICON_GLYPHS[name]}</Text>;
102
111
  }
103
112
 
@@ -10,8 +10,17 @@ import { createAppRuntime } from "./app-runtime";
10
10
 
11
11
  export type AppServices = ReturnType<typeof createAppServices>;
12
12
 
13
- export function createAppServices({ config, plugins, externalPlugins }: AppServicesFactoryOptions) {
14
- const persistence = new AppPersistence(join(config.dataDir, ".gloomberb-cache.db"));
13
+ interface AppServicesOptions extends AppServicesFactoryOptions {
14
+ /**
15
+ * Runs where the user waits on it (the terminal, the desktop app), so no
16
+ * database statement waits on another program's lock (data/sqlite/access.ts).
17
+ * A command sets it to false and waits instead.
18
+ */
19
+ interactive?: boolean;
20
+ }
21
+
22
+ export function createAppServices({ config, plugins, externalPlugins, interactive = true }: AppServicesOptions) {
23
+ const persistence = new AppPersistence(join(config.dataDir, ".gloomberb-cache.db"), { interactive });
15
24
  const tickerRepository = new TickerRepository(persistence.tickers);
16
25
  const providerRouter = new AssetDataRouter(null, [], persistence.resources, connectionHealth);
17
26
  const runtime = createAppRuntime({
@@ -1,7 +1,7 @@
1
1
  import { PluginStateStore } from "./plugin-state-store";
2
2
  import { ResourceStore } from "./resource-store";
3
3
  import { SessionStore } from "./session-store";
4
- import { SqliteDatabase } from "./sqlite/database";
4
+ import { SqliteDatabase, type SqliteDatabaseOptions } from "./sqlite/database";
5
5
  import { TickerStore } from "./ticker-store";
6
6
 
7
7
  export class AppPersistence {
@@ -11,15 +11,17 @@ export class AppPersistence {
11
11
  readonly pluginState: PluginStateStore;
12
12
  readonly sessions: SessionStore;
13
13
 
14
- constructor(dbPath: string) {
15
- this.database = new SqliteDatabase(dbPath);
16
- this.tickers = new TickerStore(this.database.connection);
17
- this.resources = new ResourceStore(this.database.connection);
18
- this.pluginState = new PluginStateStore(this.database.connection);
19
- this.sessions = new SessionStore(this.database.connection);
14
+ constructor(dbPath: string, options: SqliteDatabaseOptions = {}) {
15
+ this.database = new SqliteDatabase(dbPath, options);
16
+ const { connection, access } = this.database;
17
+ this.tickers = new TickerStore(connection, access);
18
+ this.resources = new ResourceStore(connection, access);
19
+ this.pluginState = new PluginStateStore(connection, access);
20
+ this.sessions = new SessionStore(connection, access);
20
21
  }
21
22
 
22
23
  close(): void {
24
+ this.resources.close();
23
25
  this.database.close();
24
26
  }
25
27
  }
@@ -2,6 +2,7 @@ import { logFailure } from "../utils/log-failure";
2
2
  import type { CachePolicy } from "../types/persistence";
3
3
  import type { PluginPersistence } from "../types/plugin";
4
4
  import { CachedQuery, type CachedValue } from "./cached-query";
5
+ import { isTransientFailure } from "../api-client/errors";
5
6
 
6
7
  export interface PluginCacheResult<T> {
7
8
  data: T;
@@ -12,6 +13,20 @@ export interface PluginCacheResult<T> {
12
13
  source: "cache" | "network" | "stale-fallback";
13
14
  }
14
15
 
16
+ /**
17
+ * `result`, or the failure of the refresh it stands in for when a saved copy
18
+ * may not answer for it. A saved copy covers only a failure asking again may
19
+ * get past (`isTransientFailure`: no connection, a timeout, a rate-limited or
20
+ * overloaded server, a reply whose source did not answer this time); a
21
+ * refusal (another 4xx, such as a series no longer served or a signed-out
22
+ * session) and an answer that failed validation are thrown, so they show as
23
+ * the errors they are instead of as old data.
24
+ */
25
+ export function savedCopyForTransientFailure<R extends { source?: PluginCacheResult<unknown>["source"]; error?: unknown }>(result: R): R {
26
+ if (result.source === "stale-fallback" && !isTransientFailure(result.error)) throw result.error;
27
+ return result;
28
+ }
29
+
15
30
  /** Shared persistence/request lifecycle; keys, codecs, validation and policies stay with the plugin. */
16
31
  export function createPluginCache<T, Stored = T>(options: {
17
32
  kind: string;
@@ -108,11 +123,13 @@ export function createPluginCache<T, Stored = T>(options: {
108
123
  }
109
124
  return { value, fetchedAt, staleAt: fetchedAt + options.policy.staleMs, expiresAt: fetchedAt + options.policy.expireMs, source: options.source };
110
125
  } });
126
+ // A retained copy carries its refresh failure, whatever was thrown (an Error without a message included).
127
+ const failed = "refreshError" in result;
111
128
  const error = result.refreshError;
112
129
  return {
113
- data: result.value, fetchedAt: result.fetchedAt, stale: !!error || result.staleAt <= Date.now(),
114
- source: error ? "stale-fallback" : result === before ? "cache" : "network",
115
- ...(error ? { error, refreshError: error instanceof Error ? error.message : String(error) } : {}),
130
+ data: result.value, fetchedAt: result.fetchedAt, stale: failed || result.staleAt <= Date.now(),
131
+ source: failed ? "stale-fallback" : result === before ? "cache" : "network",
132
+ ...(failed ? { error, refreshError: error instanceof Error ? error.message : String(error) } : {}),
116
133
  };
117
134
  },
118
135
  };