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
@@ -45,6 +45,7 @@ import { commentLettersPlugin } from "./builtin/comment-letters";
45
45
  import { openFdaPlugin } from "./builtin/openfda";
46
46
  import { browserGloomberbCloudPlugin } from "./builtin/cloud/browser";
47
47
  import { changelogModule } from "./builtin/changelog";
48
+ import { roadmapModule } from "./builtin/roadmap";
48
49
  import { chartComposerModule } from "./builtin/chart-composer";
49
50
  import { connectionsModule } from "./builtin/connections";
50
51
  import { relativeRotationModule } from "./builtin/relative-rotation";
@@ -126,7 +127,7 @@ import { FIXED_INCOME_HOSTS } from "./builtin/fixed-income/model";
126
127
 
127
128
  const browserApplicationPlugin = composeBuiltinPlugin({
128
129
  ...applicationPluginMeta,
129
- modules: [layoutManagerModule, helpModule, changelogModule, connectionsModule, docsModule],
130
+ modules: [layoutManagerModule, helpModule, changelogModule, connectionsModule, docsModule, roadmapModule],
130
131
  });
131
132
 
132
133
  const runtimeOnly = (module: PluginModule): PluginModule => {
@@ -32,7 +32,7 @@ const LINKED_PACKAGES = ["gloomberb", "react"] as const;
32
32
 
33
33
  /**
34
34
  * The names the host is published under: `gloomberb`, which plugins import
35
- * as, and `@gloom-sh/terminal` once releases are also published under it.
35
+ * as, and `@gloom-sh/terminal`.
36
36
  */
37
37
  const HOST_PACKAGE_NAMES: ReadonlySet<string> = new Set(["gloomberb", "@gloom-sh/terminal"]);
38
38
 
@@ -141,7 +141,13 @@ function run(
141
141
  /** Whether a `git` command is on the PATH; plugins from GitHub need one to install or update. */
142
142
  export function gitInstalled(): boolean {
143
143
  if (typeof Bun !== "undefined") return Bun.which("git") !== null;
144
- return gitSucceeds(["--version"], process.cwd());
144
+ try {
145
+ execFileSync("git", ["--version"], { stdio: "ignore" });
146
+ return true;
147
+ } catch {
148
+ // Intentional ignore: a git that does not start is the answer this asks for.
149
+ return false;
150
+ }
145
151
  }
146
152
 
147
153
  /** What a plugin install says when there is no git to run: what to install, then to try again. */
@@ -171,14 +177,9 @@ async function git(args: string[], cwd: string, quiet: boolean, failure: string)
171
177
  return result.stdout.trim();
172
178
  }
173
179
 
174
- function isOnBranch(dir: string): boolean {
175
- try {
176
- execFileSync("git", ["symbolic-ref", "-q", "HEAD"], { cwd: dir, stdio: "ignore" });
177
- return true;
178
- } catch {
179
- // Intentional ignore: Detached HEAD is expected after a pinned install.
180
- return false;
181
- }
180
+ /** False on a detached HEAD, which a pinned install leaves. */
181
+ function isOnBranch(dir: string): Promise<boolean> {
182
+ return gitSucceedsAsync(["symbolic-ref", "-q", "HEAD"], dir);
182
183
  }
183
184
 
184
185
  function pinMatches(commit: string | null, expected: string | undefined): boolean {
@@ -208,27 +209,27 @@ async function checkoutCommit(targetDir: string, commit: string, quiet: boolean)
208
209
  await git(["checkout", "--detach", "--force", commit], targetDir, quiet, `Could not check out ${commit.slice(0, 7)}`);
209
210
  }
210
211
 
211
- function gitSucceeds(args: string[], cwd: string): boolean {
212
- try {
213
- execFileSync("git", args, { cwd, stdio: "ignore" });
214
- return true;
215
- } catch {
216
- // Intentional ignore: Non-ancestor and shallow histories use package versions and commit dates instead.
217
- return false;
218
- }
212
+
213
+ /*
214
+ * The update path runs in the app (the terminal, the desktop's Bun process),
215
+ * where a sync git call holds the thread for the whole process start: tens of
216
+ * milliseconds each on Windows, several per plugin. These never block it.
217
+ */
218
+ async function gitSucceedsAsync(args: string[], cwd: string): Promise<boolean> {
219
+ const result = await run("git", args, cwd, { stdout: "pipe", stderr: "pipe" })
220
+ .catch(logFailure("plugin-installer", "Could not run git", null));
221
+ return result?.code === 0;
219
222
  }
220
223
 
221
- function gitOutput(args: string[], cwd: string): string | null {
222
- try {
223
- return execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString("utf-8").trim();
224
- } catch {
225
- // Intentional ignore: Shallow or manifest-free checkouts may lack historical metadata.
226
- return null;
227
- }
224
+ /** Null when git fails: shallow or manifest-free checkouts may lack historical metadata. */
225
+ async function gitOutputAsync(args: string[], cwd: string): Promise<string | null> {
226
+ const result = await run("git", args, cwd, { stdout: "pipe", stderr: "pipe" })
227
+ .catch(logFailure("plugin-installer", "Could not run git", null));
228
+ return result?.code === 0 ? result.stdout.trim() : null;
228
229
  }
229
230
 
230
- function versionAt(dir: string, commit: string): string | null {
231
- const text = gitOutput(["show", `${commit}:package.json`], dir);
231
+ async function versionAt(dir: string, commit: string): Promise<string | null> {
232
+ const text = await gitOutputAsync(["show", `${commit}:package.json`], dir);
232
233
  if (!text) return null;
233
234
  try {
234
235
  const version = JSON.parse(text).version;
@@ -247,15 +248,15 @@ function versionAt(dir: string, commit: string): string | null {
247
248
  * it; otherwise the versions the two commits declare decide, then, when
248
249
  * those match, their commit dates.
249
250
  */
250
- export function describeOlderCommit(dir: string, installed: string, candidate: string, label = candidate.slice(0, 7)): string | null {
251
- if (installed === candidate || gitSucceeds(["merge-base", "--is-ancestor", installed, candidate], dir)) return null;
252
- const installedVersion = versionAt(dir, installed);
253
- const byVersion = compareSemver(versionAt(dir, candidate), installedVersion);
251
+ export async function describeOlderCommit(dir: string, installed: string, candidate: string, label = candidate.slice(0, 7)): Promise<string | null> {
252
+ if (installed === candidate || await gitSucceedsAsync(["merge-base", "--is-ancestor", installed, candidate], dir)) return null;
253
+ const installedVersion = await versionAt(dir, installed);
254
+ const byVersion = compareSemver(await versionAt(dir, candidate), installedVersion);
254
255
  const older = () => `the registry's ${label} is older than the installed ${byVersion ? installedVersion : installed.slice(0, 7)}`;
255
- if (gitSucceeds(["merge-base", "--is-ancestor", candidate, installed], dir)) return older();
256
+ if (await gitSucceedsAsync(["merge-base", "--is-ancestor", candidate, installed], dir)) return older();
256
257
  if (byVersion !== null && byVersion !== 0) return byVersion < 0 ? older() : null;
257
- const candidateTime = Number(gitOutput(["show", "-s", "--format=%ct", candidate], dir));
258
- const installedTime = Number(gitOutput(["show", "-s", "--format=%ct", installed], dir));
258
+ const candidateTime = Number(await gitOutputAsync(["show", "-s", "--format=%ct", candidate], dir));
259
+ const installedTime = Number(await gitOutputAsync(["show", "-s", "--format=%ct", installed], dir));
259
260
  return candidateTime > 0 && installedTime > 0 && candidateTime < installedTime ? older() : null;
260
261
  }
261
262
 
@@ -403,19 +404,29 @@ export interface PluginUpdateResult extends PluginDirectoryInfo {
403
404
  kept?: string;
404
405
  }
405
406
 
407
+ function githubRepoFromUrl(url: string): string | null {
408
+ const match = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(url.trim());
409
+ return match ? `${match[1]}/${match[2]}` : null;
410
+ }
411
+
406
412
  /** `owner/repo` from the checkout's origin remote, or null for anything that is not a GitHub clone. */
407
413
  export function readPluginRemote(pluginDir: string): string | null {
408
414
  try {
409
- const url = execFileSync("git", ["config", "--get", "remote.origin.url"], { cwd: pluginDir, stdio: ["ignore", "pipe", "ignore"] })
410
- .toString("utf-8").trim();
411
- const match = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(url);
412
- return match ? `${match[1]}/${match[2]}` : null;
415
+ return githubRepoFromUrl(execFileSync("git", ["config", "--get", "remote.origin.url"], { cwd: pluginDir, stdio: ["ignore", "pipe", "ignore"] })
416
+ .toString("utf-8"));
413
417
  } catch {
414
418
  // Intentional ignore: Hand-copied plugins and local repositories may have no origin remote.
415
419
  return null;
416
420
  }
417
421
  }
418
422
 
423
+ /** `readPluginRemote` without holding the thread, for the app's own update check. */
424
+ export async function readPluginRemoteAsync(pluginDir: string): Promise<string | null> {
425
+ // No origin remote (a hand-copied plugin, a local repository) is a failed git, and no repo.
426
+ const url = await gitOutputAsync(["config", "--get", "remote.origin.url"], pluginDir);
427
+ return url ? githubRepoFromUrl(url) : null;
428
+ }
429
+
419
430
  /** The sha from `git ls-remote origin HEAD`; null when the answer is not one. */
420
431
  export function parseRemoteHead(output: string): string | null {
421
432
  const sha = output.trim().split(/\s+/)[0] ?? "";
@@ -500,7 +511,7 @@ export async function updatePlugin(name: string, options: PluginInstallOptions =
500
511
  const commit = await fetchPin(targetDir, pin, quiet);
501
512
  // Updates only move forward: a checkout installed from the default
502
513
  // branch can be ahead of the tag the registry reviewed.
503
- const kept = before ? describeOlderCommit(targetDir, before, commit, pin.ref ?? commit.slice(0, 7)) : null;
514
+ const kept = before ? await describeOlderCommit(targetDir, before, commit, pin.ref ?? commit.slice(0, 7)) : null;
504
515
  if (kept) {
505
516
  if (!quiet) console.log(cliStyles.muted(`Kept ${name}: ${kept}.`));
506
517
  return { ...describe(targetDir, name, null), before, changed: false, kept };
@@ -510,7 +521,7 @@ export async function updatePlugin(name: string, options: PluginInstallOptions =
510
521
  // Not in the registry, so there is nothing reviewed to land on: follow the
511
522
  // remote's default branch, whether the checkout is on a branch or detached
512
523
  // from an earlier pin.
513
- if (isOnBranch(targetDir)) {
524
+ if (await isOnBranch(targetDir)) {
514
525
  await git(["pull", "--ff-only"], targetDir, quiet, `Failed to update ${name}`);
515
526
  } else {
516
527
  await git(["fetch", "--depth", "1", "origin", "HEAD"], targetDir, quiet, `Failed to fetch ${name}`);
@@ -185,15 +185,19 @@ function positionSideLabel(ticker: TickerRecord, activeTab: string | undefined):
185
185
  return "MIX";
186
186
  }
187
187
 
188
- /** The currency a row is listed in: its position's, else its quote's; an index's level is in points and has none. */
188
+ /**
189
+ * The currency a row is listed in: its position's, else its quote's or the one saved with it. An index's level is in
190
+ * points and has none, and a row whose currency no source gave (a coin pair before its quote) shows none, never the
191
+ * dollars it is valued in meanwhile: a row not held has no position currency, only that fallback.
192
+ */
189
193
  function listedCurrency(
190
- positionCurrency: string | undefined,
191
- quoteCurrency: string,
194
+ metrics: Pick<PortfolioPositionMetrics, "positionCount" | "positionCurrency">,
192
195
  ticker: TickerRecord,
193
196
  quote: TickerFinancials["quote"],
194
197
  ): string {
195
- if (!positionCurrency && isIndexLevel(quote?.instrumentType || ticker.metadata.assetCategory)) return "";
196
- return (positionCurrency || quoteCurrency || ticker.metadata.currency || "").toUpperCase();
198
+ const held = metrics.positionCount > 0 ? metrics.positionCurrency : "";
199
+ if (!held && isIndexLevel(quote?.instrumentType || ticker.metadata.assetCategory)) return "";
200
+ return (held || quote?.currency || ticker.metadata.currency || "").toUpperCase();
197
201
  }
198
202
 
199
203
  function compactText(value: string | null | undefined): string {
@@ -458,7 +462,7 @@ export function getColumnValue(
458
462
  ),
459
463
  };
460
464
  case "currency":
461
- return { text: compactText(currencyCodeLabel(listedCurrency(positionCurrency, quoteCurrency, ticker, quote))) };
465
+ return { text: compactText(currencyCodeLabel(listedCurrency(positionMetrics, ticker, quote))) };
462
466
  case "sector":
463
467
  return { text: compactText(ticker.metadata.sector || financials?.profile?.sector) };
464
468
  case "industry":
@@ -713,7 +717,7 @@ export function getSortValue(
713
717
  ?? ticker.metadata.exchange
714
718
  ?? null;
715
719
  case "currency":
716
- return listedCurrency(positionCurrency, quoteCurrency, ticker, quote) || null;
720
+ return listedCurrency(positionMetrics, ticker, quote) || null;
717
721
  case "sector":
718
722
  return ticker.metadata.sector || financials?.profile?.sector || null;
719
723
  case "industry":
@@ -165,6 +165,13 @@ const ICONS: Record<IconName, ReactNode> = {
165
165
  <path d="M7.3 2.9l1.8 1.8" stroke="currentColor" strokeWidth="1.1" />
166
166
  </>
167
167
  ),
168
+ // An envelope: a message written to you.
169
+ mail: (
170
+ <>
171
+ <rect x="1.5" y="2.75" width="9" height="6.5" rx="1.1" stroke="currentColor" strokeWidth="1.1" />
172
+ <path d="M2 3.4 6 6.5l4-3.1" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" strokeLinejoin="round" />
173
+ </>
174
+ ),
168
175
  restore: (
169
176
  <>
170
177
  <rect x="2" y="4" width="6" height="6" rx="0.8" stroke="currentColor" strokeWidth="1.1" />
@@ -173,7 +180,7 @@ const ICONS: Record<IconName, ReactNode> = {
173
180
  ),
174
181
  };
175
182
 
176
- export function WebIcon({ name, size = 12, color }: IconProps) {
183
+ export function WebIcon({ name, size = 12, color, fullHeight }: IconProps) {
177
184
  return (
178
185
  <svg
179
186
  viewBox="0 0 12 12"
@@ -182,7 +189,8 @@ export function WebIcon({ name, size = 12, color }: IconProps) {
182
189
  fill="none"
183
190
  aria-hidden="true"
184
191
  data-gloom-icon={name}
185
- style={{ flex: "none", display: "block", color }}
192
+ // The viewBox keeps its size and sits in the middle of the taller box (preserveAspectRatio's default).
193
+ style={{ flex: "none", display: "block", color, ...(fullHeight ? { height: "100%" } : {}) }}
186
194
  >
187
195
  {ICONS[name]}
188
196
  </svg>
@@ -352,16 +352,14 @@ export class DesktopBackend {
352
352
  execPath: process.execPath,
353
353
  argv: process.argv,
354
354
  });
355
- try {
356
- scheduleDesktopRelaunch();
357
- } catch (error) {
358
- this.restartInProgress = false;
359
- console.error("[desktop-recovery] failed to schedule restart", summarizeError(error));
360
- throw error;
361
- }
362
- this.detachedWindows.closeAll();
363
- this.teardownServices();
364
- Utils.quit();
355
+ scheduleDesktopRelaunch().then(
356
+ // As a quit does: an app whose windows are gone ends at once on Windows.
357
+ () => this.quit(),
358
+ (error: unknown) => {
359
+ this.restartInProgress = false;
360
+ console.error("[desktop-recovery] failed to schedule restart", summarizeError(error));
361
+ },
362
+ );
365
363
  }
366
364
 
367
365
  private syncConfigAccessors(): void {
@@ -566,6 +564,7 @@ export class DesktopBackend {
566
564
  case "host.exit":
567
565
  case "host.windowControl":
568
566
  case "host.windowFullscreen":
567
+ case "host.startWindowDrag":
569
568
  case "host.openExternal":
570
569
  case "host.copyText":
571
570
  case "host.focusWindow":
@@ -18,6 +18,8 @@ import type { DesktopBackend } from "./backend";
18
18
 
19
19
  const INITIAL_DOCK_SUPPRESSION_MS = 800;
20
20
  const WINDOW_CONTROL_DOCK_SUPPRESSION_MS = 5_000;
21
+ /** How long after the main window has its data the popped-out windows of the last session open. */
22
+ const LAUNCH_WINDOWS_DELAY_MS = 300;
21
23
 
22
24
  export class DesktopDetachedWindowManager {
23
25
  private readonly windows = new Map<string, BrowserWindow>();
@@ -27,9 +29,29 @@ export class DesktopDetachedWindowManager {
27
29
  private readonly pendingMoveFlush = new Set<string>();
28
30
  private readonly suppressDockUntil = new Map<string, number>();
29
31
  private readonly windowControlDockSuppressed = new Set<string>();
32
+ /**
33
+ * At launch, popped-out windows wait for the main window. Opening a window
34
+ * holds this process until the system has made it (on Windows, its
35
+ * browser too), and the main window's `init` used to wait behind each one.
36
+ */
37
+ private windowsAllowed = false;
38
+ private windowsTimer: ReturnType<typeof setTimeout> | null = null;
30
39
 
31
40
  constructor(private readonly backend: DesktopBackend) {}
32
41
 
42
+ /**
43
+ * The main window has its data: the popped-out windows open shortly
44
+ * after, once it has had a moment to draw and ask for what it shows.
45
+ */
46
+ openAfterMainWindow(): void {
47
+ if (this.windowsAllowed || this.windowsTimer) return;
48
+ this.windowsTimer = setTimeout(() => {
49
+ this.windowsTimer = null;
50
+ this.windowsAllowed = true;
51
+ this.reconcile();
52
+ }, LAUNCH_WINDOWS_DELAY_MS);
53
+ }
54
+
33
55
  focusDetachedPane(instanceId: string): boolean {
34
56
  return this.focusWindowForRpcKey(detachedRpcKey(instanceId));
35
57
  }
@@ -88,7 +110,7 @@ export class DesktopDetachedWindowManager {
88
110
  for (const [instanceId, entry] of desiredEntries) {
89
111
  const existingWindow = this.windows.get(instanceId);
90
112
  if (!existingWindow) {
91
- this.createWindow(instanceId, entry);
113
+ if (this.windowsAllowed) this.createWindow(instanceId, entry);
92
114
  continue;
93
115
  }
94
116
 
@@ -10,6 +10,7 @@ import { safeExternalUrl } from "../../../../utils/external-url";
10
10
  import { getContextMenuRequestId, normalizeContextMenuItems } from "../context-menu/normalize";
11
11
  import { MAIN_WINDOW_RPC_KEY } from "../window/focus";
12
12
  import { applyDesktopWindowControl } from "../window/controls";
13
+ import { startSystemWindowMove } from "../window/windows-native";
13
14
  import { saveTextFileToDownloads } from "../../../../utils/save-text-file";
14
15
  import { setAppBadge } from "./app-badge";
15
16
  import { desktopBootFramePath, saveDesktopBootFrame } from "../window/boot-frame";
@@ -111,6 +112,13 @@ export async function handleDesktopHostRequest(
111
112
  }
112
113
  case "host.windowFullscreen":
113
114
  return isWindowFullscreen(backend, backend.rpcs.getRpcWindowKey(rpc));
115
+ case "host.startWindowDrag": {
116
+ const targetWindow = windowForRpcKey(backend, backend.rpcs.getRpcWindowKey(rpc));
117
+ if (!targetWindow?.ptr || !startSystemWindowMove(targetWindow.ptr)) {
118
+ throw new Error("This window cannot be moved by the system.");
119
+ }
120
+ return null;
121
+ }
114
122
  case "host.openExternal": {
115
123
  if (typeof request.payload.url !== "string") {
116
124
  throw new Error("host.openExternal requires a URL.");
@@ -25,6 +25,7 @@ import { telemetryOptedOut } from "../../../../telemetry/crash-reports";
25
25
  import { describeNodeOs, readOrCreateInstallId } from "../../../../telemetry/crash-reports-node";
26
26
  import type { DesktopBackend, DesktopRpc } from "./backend";
27
27
  import { readDesktopViewIds } from "./view-ids";
28
+ import { markStartupComplete } from "../../../../utils/main-thread-stall";
28
29
 
29
30
  interface DesktopWindowTarget {
30
31
  kind: "main" | "detached";
@@ -194,5 +195,12 @@ export async function initializeDesktopBackend(
194
195
  backend.workspace = createDesktopWorkspace(config, backend.getSessionSnapshot());
195
196
  backend.detachedWindows.reconcile();
196
197
  }
197
- return buildInitializationPayload(backend, config, services, windowTarget);
198
+ const init = buildInitializationPayload(backend, config, services, windowTarget);
199
+ // The main window has what it needs to draw: this process is up, and the
200
+ // popped-out windows open next.
201
+ if (windowTarget.kind === "main") {
202
+ markStartupComplete();
203
+ backend.detachedWindows.openAfterMainWindow();
204
+ }
205
+ return init;
198
206
  }