@gloom-sh/terminal 0.18.5 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (570) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +163 -8
  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/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +19 -7
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/hiring.ts +3 -0
  14. package/src/api-client/index.ts +15 -0
  15. package/src/api-client/ipo.ts +24 -1
  16. package/src/api-client/market-discovery.ts +10 -0
  17. package/src/api-client/market-movers.ts +7 -0
  18. package/src/api-client/members.ts +1 -1
  19. package/src/api-client/mna.ts +3 -0
  20. package/src/api-client/paths.ts +9 -1
  21. package/src/api-client/perps.ts +43 -0
  22. package/src/api-client/research-activity.ts +3 -2
  23. package/src/api-client/roadmap.ts +85 -0
  24. package/src/api-client/types.ts +82 -2
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +0 -2
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +2 -2
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/market.ts +107 -38
  35. package/src/cli/commands/news-search.ts +22 -0
  36. package/src/cli/commands/options-chain.ts +48 -7
  37. package/src/cli/commands/options-leaps.ts +9 -3
  38. package/src/cli/commands/overview.ts +147 -32
  39. package/src/cli/commands/search.ts +21 -1
  40. package/src/cli/commands/themes.ts +11 -2
  41. package/src/cli/commands/ticker.ts +87 -36
  42. package/src/cli/company-name-hint.ts +41 -0
  43. package/src/cli/earnings-calendar.ts +1 -1
  44. package/src/cli/errors.ts +25 -2
  45. package/src/cli/glossary.ts +31 -2
  46. package/src/cli/history-rows.ts +57 -9
  47. package/src/cli/index-roots.ts +0 -7
  48. package/src/cli/index.ts +2 -0
  49. package/src/cli/listing-arg.ts +235 -60
  50. package/src/cli/output-file.ts +68 -0
  51. package/src/cli/pane-functions/capabilities.ts +4 -0
  52. package/src/cli/pane-functions/catalog.ts +80 -7
  53. package/src/cli/pane-functions/discovery.ts +21 -1
  54. package/src/cli/pane-functions/dom.ts +13 -3
  55. package/src/cli/pane-functions/freshness.ts +93 -4
  56. package/src/cli/pane-functions/function-help.ts +20 -10
  57. package/src/cli/pane-functions/headless.ts +17 -7
  58. package/src/cli/pane-functions/index.ts +54 -16
  59. package/src/cli/pane-functions/market-session.ts +66 -4
  60. package/src/cli/pane-functions/options.ts +19 -1
  61. package/src/cli/pane-functions/resolver.ts +19 -8
  62. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  63. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  64. package/src/cli/quote-answer.ts +22 -11
  65. package/src/cli/saved-copy-persistence.ts +88 -0
  66. package/src/cli/ticker-resolution.ts +3 -2
  67. package/src/components/chart/composite/composite-chart.tsx +4 -0
  68. package/src/components/chart/composite/interactions.ts +6 -0
  69. package/src/components/chart/composite/legend.tsx +45 -7
  70. package/src/components/chart/composite/panel-gestures.ts +33 -0
  71. package/src/components/chart/composite/panel-surface.tsx +52 -3
  72. package/src/components/chart/composite/rasterizer.ts +24 -13
  73. package/src/components/chart/composite/scene.ts +39 -4
  74. package/src/components/chart/composite/series-cues.ts +14 -0
  75. package/src/components/chart/composite/text-renderer.ts +9 -2
  76. package/src/components/chart/composite/tool-catalog.ts +36 -0
  77. package/src/components/chart/composite/tools.ts +203 -44
  78. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  79. package/src/components/command-bar/assist/inventory.ts +2 -1
  80. package/src/components/command-bar/commands/direct/index.ts +28 -13
  81. package/src/components/command-bar/list/model.ts +5 -0
  82. package/src/components/command-bar/pane-templates/items.ts +3 -2
  83. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  84. package/src/components/command-bar/selection-runtime.ts +52 -3
  85. package/src/components/command-bar/surface/index.tsx +2 -0
  86. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  87. package/src/components/layout/pane/capture-stamp.ts +2 -2
  88. package/src/components/layout/pane/footer/index.tsx +22 -6
  89. package/src/components/layout/pane/footer/model.ts +83 -15
  90. package/src/components/layout/pane/status-footer.ts +35 -2
  91. package/src/components/layout/status-widget-space.ts +9 -0
  92. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  93. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  94. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  95. package/src/components/onboarding/wizard-positions.ts +3 -3
  96. package/src/components/ticker/list-table-view.tsx +8 -21
  97. package/src/components/ui/choice-dialog.tsx +23 -18
  98. package/src/components/ui/choice-groups.ts +18 -0
  99. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  100. package/src/components/ui/data-table/opentui/model.ts +26 -1
  101. package/src/components/ui/data-table/types.ts +2 -0
  102. package/src/components/ui/display.tsx +18 -3
  103. package/src/components/ui/icon.tsx +4 -1
  104. package/src/components/ui/list-view.tsx +8 -4
  105. package/src/components/ui/query-bar-keys.tsx +2 -0
  106. package/src/components/ui/query-bar.tsx +1 -0
  107. package/src/components/ui/select-button.tsx +5 -1
  108. package/src/components/ui/select-field.tsx +1 -0
  109. package/src/components/ui/table-layout.ts +8 -5
  110. package/src/core/app-runtime.ts +2 -0
  111. package/src/core/app-services.ts +2 -2
  112. package/src/data/config/chart-settings.ts +1 -1
  113. package/src/data/json-ticker-repository.ts +17 -8
  114. package/src/data/plugin-cache.ts +5 -3
  115. package/src/data/saved-copies.ts +8 -0
  116. package/src/data/ticker-repository.ts +16 -9
  117. package/src/data/ticker-writes.ts +81 -0
  118. package/src/data/ttl-cache.ts +165 -0
  119. package/src/docs/snapshot.json +103 -142
  120. package/src/i18n/es.ts +20 -6
  121. package/src/i18n/ja.ts +20 -6
  122. package/src/i18n/ko.ts +20 -6
  123. package/src/i18n/zh-cn.ts +20 -6
  124. package/src/i18n/zh-tw.ts +20 -6
  125. package/src/market-data/depositary-pair.ts +27 -3
  126. package/src/market-data/fund-facts.ts +50 -0
  127. package/src/market-data/market/change-basis.ts +16 -3
  128. package/src/market-data/market/format.ts +25 -0
  129. package/src/market-data/market/freshness.ts +2 -1
  130. package/src/market-data/options-alternatives.ts +81 -4
  131. package/src/market-data/quotes/metadata.ts +7 -0
  132. package/src/news/aggregator.ts +119 -9
  133. package/src/news/cloud-session.ts +21 -0
  134. package/src/news/hooks.ts +1 -0
  135. package/src/news/news-model.ts +1 -0
  136. package/src/news/types.ts +17 -0
  137. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  138. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  139. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  140. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  141. package/src/plugins/builtin/alerts/cli.ts +362 -80
  142. package/src/plugins/builtin/alerts/command.ts +25 -1
  143. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  144. package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
  145. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  146. package/src/plugins/builtin/alerts/format.ts +75 -5
  147. package/src/plugins/builtin/alerts/headless.ts +34 -12
  148. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  149. package/src/plugins/builtin/alerts/history.ts +1 -1
  150. package/src/plugins/builtin/alerts/index.tsx +79 -16
  151. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  152. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  153. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  154. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  155. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  156. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  157. package/src/plugins/builtin/alerts/storage.ts +12 -8
  158. package/src/plugins/builtin/alerts/types.ts +4 -0
  159. package/src/plugins/builtin/analytics/index.tsx +2 -1
  160. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  161. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  162. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  163. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  164. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  165. package/src/plugins/builtin/attention/index.tsx +2 -1
  166. package/src/plugins/builtin/attention/listing.ts +14 -0
  167. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  168. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  169. package/src/plugins/builtin/buildout/format.ts +38 -0
  170. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  171. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  172. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  173. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  174. package/src/plugins/builtin/cds/client.ts +142 -30
  175. package/src/plugins/builtin/cds/headless.ts +35 -8
  176. package/src/plugins/builtin/cds/model.ts +145 -18
  177. package/src/plugins/builtin/cds/pane.tsx +16 -5
  178. package/src/plugins/builtin/changelog/index.tsx +9 -6
  179. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  180. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  181. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  182. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  183. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  184. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  185. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  186. package/src/plugins/builtin/chart-composer/index.tsx +5 -6
  187. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  188. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  189. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  190. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  191. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  192. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  193. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  194. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  195. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  196. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  197. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  198. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  199. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  200. package/src/plugins/builtin/clinical-trials/model.ts +86 -31
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
  202. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  203. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
  204. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  205. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  206. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  207. package/src/plugins/builtin/company-attention/client.ts +8 -9
  208. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  209. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  210. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  211. package/src/plugins/builtin/company-attention/pane.tsx +12 -2
  212. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  213. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  214. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  215. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  216. package/src/plugins/builtin/composite-plugins.ts +2 -1
  217. package/src/plugins/builtin/correlation/headless.ts +13 -5
  218. package/src/plugins/builtin/correlation/history.ts +22 -0
  219. package/src/plugins/builtin/cot/client.ts +22 -18
  220. package/src/plugins/builtin/cot/headless.ts +15 -10
  221. package/src/plugins/builtin/cot/model.ts +14 -1
  222. package/src/plugins/builtin/cot/pane.tsx +22 -14
  223. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  224. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  225. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  226. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  227. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  228. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  229. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  230. package/src/plugins/builtin/credit-documents/headless.ts +10 -10
  231. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  232. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  233. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  234. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  235. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  236. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  237. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  238. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  239. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  240. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  241. package/src/plugins/builtin/docs/targets.ts +5 -1
  242. package/src/plugins/builtin/doe/headless.ts +3 -2
  243. package/src/plugins/builtin/doe/pane.tsx +13 -3
  244. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  245. package/src/plugins/builtin/earnings/board.tsx +62 -71
  246. package/src/plugins/builtin/earnings/headless.ts +40 -4
  247. package/src/plugins/builtin/earnings/scope.ts +31 -43
  248. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  249. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  250. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  251. package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
  252. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  253. package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
  254. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  255. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  256. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  257. package/src/plugins/builtin/econ/figures.ts +48 -0
  258. package/src/plugins/builtin/econ/index.tsx +14 -5
  259. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  260. package/src/plugins/builtin/econ/types.ts +2 -0
  261. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  262. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  263. package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
  264. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  265. package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
  266. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  267. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  268. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  269. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  270. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  271. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  272. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  273. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  274. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  275. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  276. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  277. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  278. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  279. package/src/plugins/builtin/gpu/headless.ts +8 -2
  280. package/src/plugins/builtin/help/function-index.ts +4 -1
  281. package/src/plugins/builtin/help/index.tsx +62 -59
  282. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  283. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  284. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  285. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  286. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  287. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  288. package/src/plugins/builtin/holders/headless.ts +22 -3
  289. package/src/plugins/builtin/holders/pane.tsx +3 -0
  290. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  291. package/src/plugins/builtin/insider/headless.ts +2 -2
  292. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  293. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  294. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  295. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  296. package/src/plugins/builtin/iv-history/client.ts +52 -11
  297. package/src/plugins/builtin/iv-history/format.ts +36 -23
  298. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  299. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  300. package/src/plugins/builtin/iv-history/model.ts +163 -27
  301. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  302. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  303. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  304. package/src/plugins/builtin/jobs/client.ts +25 -29
  305. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  306. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  307. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  308. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  309. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  310. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  311. package/src/plugins/builtin/macro-day/model.ts +39 -5
  312. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  313. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  314. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  315. package/src/plugins/builtin/market-movers/client.ts +27 -7
  316. package/src/plugins/builtin/market-movers/headless.ts +34 -12
  317. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  318. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  319. package/src/plugins/builtin/market-movers/model.ts +4 -1
  320. package/src/plugins/builtin/market-movers/screener.ts +22 -4
  321. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  322. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  323. package/src/plugins/builtin/market-movers/table.ts +17 -5
  324. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  325. package/src/plugins/builtin/mna/headless.ts +2 -0
  326. package/src/plugins/builtin/mna/pane.tsx +3 -1
  327. package/src/plugins/builtin/news/headless.ts +3 -1
  328. package/src/plugins/builtin/news/index.tsx +4 -0
  329. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  330. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  331. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  332. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  333. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  334. package/src/plugins/builtin/openfda/client.ts +14 -2
  335. package/src/plugins/builtin/openfda/index.tsx +3 -2
  336. package/src/plugins/builtin/openfda/model.ts +9 -0
  337. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  338. package/src/plugins/builtin/options/analytics.ts +29 -5
  339. package/src/plugins/builtin/options/enrichment.ts +17 -5
  340. package/src/plugins/builtin/options/table.ts +5 -2
  341. package/src/plugins/builtin/options/types.ts +2 -0
  342. package/src/plugins/builtin/options/view.tsx +17 -4
  343. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  344. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  345. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  346. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  347. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  348. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  349. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  350. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  351. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  352. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  353. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  354. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  355. package/src/plugins/builtin/pe-band/model.ts +12 -49
  356. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  357. package/src/plugins/builtin/perps/cascade.ts +236 -0
  358. package/src/plugins/builtin/perps/client.ts +56 -2
  359. package/src/plugins/builtin/perps/headless.ts +65 -9
  360. package/src/plugins/builtin/perps/index.tsx +7 -5
  361. package/src/plugins/builtin/perps/model.ts +18 -4
  362. package/src/plugins/builtin/perps/pane.tsx +19 -22
  363. package/src/plugins/builtin/perps/status-line.ts +25 -0
  364. package/src/plugins/builtin/plugin-module.ts +5 -1
  365. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  366. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  367. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  368. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  369. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  370. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
  371. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  372. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  373. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  374. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
  375. package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
  376. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  377. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  378. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  379. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  380. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  381. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  382. package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
  383. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  384. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  385. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  386. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  387. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  388. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  389. package/src/plugins/builtin/research/event-model.ts +40 -12
  390. package/src/plugins/builtin/research/headless.ts +1 -1
  391. package/src/plugins/builtin/research/index.tsx +7 -3
  392. package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
  393. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  394. package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
  395. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  396. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  397. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  398. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  399. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  400. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  401. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  402. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  403. package/src/plugins/builtin/roadmap/model.ts +67 -0
  404. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  405. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  406. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  407. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  408. package/src/plugins/builtin/scanner/headless.ts +204 -35
  409. package/src/plugins/builtin/scanner/index.ts +16 -5
  410. package/src/plugins/builtin/scanner/live-headless.ts +48 -5
  411. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  412. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  413. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  414. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  415. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  416. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  417. package/src/plugins/builtin/sec/headless.ts +4 -2
  418. package/src/plugins/builtin/sec/index.tsx +3 -0
  419. package/src/plugins/builtin/series-inventory.ts +2 -0
  420. package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
  421. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  422. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  423. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  424. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  425. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  426. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  427. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  428. package/src/plugins/builtin/shared/report-freshness.ts +11 -2
  429. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  430. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  431. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  432. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  433. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  434. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  435. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  436. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  437. package/src/plugins/builtin/themes/model.ts +16 -7
  438. package/src/plugins/builtin/themes/pane.tsx +5 -5
  439. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  440. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  441. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  442. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  443. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  444. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  445. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  446. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  447. package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
  448. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  449. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
  450. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  451. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  452. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  453. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  454. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  455. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  456. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  457. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  458. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  459. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  460. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  461. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  462. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  463. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  464. package/src/plugins/builtin/volatility/index.tsx +2 -2
  465. package/src/plugins/builtin/volatility/pane.tsx +7 -4
  466. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  467. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  468. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  469. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  470. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  471. package/src/plugins/catalog-browser.ts +2 -1
  472. package/src/plugins/catalog-manifest.ts +2 -1
  473. package/src/plugins/host-link.ts +1 -1
  474. package/src/plugins/marketplace/model.ts +18 -5
  475. package/src/portfolio/column-values.ts +11 -7
  476. package/src/portfolio/futures-positions.ts +6 -3
  477. package/src/portfolio/position-quantity.ts +15 -9
  478. package/src/remote/resources.ts +2 -1
  479. package/src/renderers/dom/data-table/row.tsx +26 -0
  480. package/src/renderers/dom/desktop/icons.tsx +7 -0
  481. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  482. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  483. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  484. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  485. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  486. package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
  487. package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
  488. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  489. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  490. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  491. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  492. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  493. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  494. package/src/renderers/electrobun/bun/index.ts +26 -2
  495. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  496. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  497. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  498. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  499. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  500. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  501. package/src/renderers/electrobun/shared/protocol.ts +12 -0
  502. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  503. package/src/renderers/electrobun/view/main.tsx +24 -1
  504. package/src/renderers/electrobun/view/news-source.ts +5 -1
  505. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  506. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  507. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  508. package/src/renderers/opentui/start.tsx +5 -0
  509. package/src/series/registry.ts +1 -0
  510. package/src/series/types.ts +6 -0
  511. package/src/series/validate.ts +1 -0
  512. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  513. package/src/sources/gloomberb-cloud/index.ts +43 -8
  514. package/src/sources/gloomberb-cloud/news.ts +47 -7
  515. package/src/sources/provider-router/financials.ts +15 -3
  516. package/src/sources/provider-router/index.ts +8 -5
  517. package/src/state/ticker-cleanup.ts +50 -0
  518. package/src/sync/controller.ts +4 -3
  519. package/src/sync/core-contributors.ts +90 -31
  520. package/src/telemetry/stall-reports-dom.ts +38 -0
  521. package/src/telemetry/stall-reports-node.ts +75 -0
  522. package/src/telemetry/stall-reports.ts +214 -0
  523. package/src/test-support/preload.ts +14 -0
  524. package/src/theme/themes.ts +10 -0
  525. package/src/tickers/company-name-listing.ts +53 -13
  526. package/src/tickers/company-name.ts +50 -4
  527. package/src/tickers/index-roots.ts +7 -0
  528. package/src/tickers/listing-fate.ts +2 -2
  529. package/src/tickers/listing-venues.ts +15 -3
  530. package/src/tickers/search/index.ts +6 -2
  531. package/src/tickers/search/ranking.ts +5 -1
  532. package/src/tickers/search/upsert.ts +13 -0
  533. package/src/tickers/terminal-codes.ts +19 -0
  534. package/src/tickers/ticker-changes.ts +8 -4
  535. package/src/time-series/alignment.ts +1 -1
  536. package/src/time-series/economic.ts +18 -4
  537. package/src/time-series/field-catalog.ts +1 -1
  538. package/src/time-series/filed-trailing-eps.ts +197 -0
  539. package/src/time-series/fundamentals.ts +44 -1
  540. package/src/time-series/ice-credit.ts +3 -0
  541. package/src/time-series/price-comparison.ts +30 -2
  542. package/src/time-series/resolve-context.ts +3 -1
  543. package/src/time-series/resolve.ts +56 -4
  544. package/src/time-series/session-bar.ts +11 -0
  545. package/src/time-series/spec.ts +3 -2
  546. package/src/time-series/studies.ts +8 -2
  547. package/src/time-series/total-return.ts +160 -0
  548. package/src/time-series/transforms.ts +2 -3
  549. package/src/time-series/types.ts +22 -1
  550. package/src/types/company-from.ts +40 -0
  551. package/src/types/data-provider.ts +7 -0
  552. package/src/types/financials.ts +59 -1
  553. package/src/types/headless.ts +27 -3
  554. package/src/types/news-source.ts +1 -0
  555. package/src/types/plugin.ts +9 -0
  556. package/src/ui/host.tsx +3 -0
  557. package/src/updater/failure.ts +4 -1
  558. package/src/utils/cli-output.ts +42 -1
  559. package/src/utils/crypto-pair.ts +165 -7
  560. package/src/utils/declared-dividends.ts +1 -1
  561. package/src/utils/forward-dividend.ts +2 -0
  562. package/src/utils/fundamentals.ts +60 -8
  563. package/src/utils/fx-market-hours.ts +26 -3
  564. package/src/utils/main-thread-stall.ts +35 -2
  565. package/src/utils/market-capitalization.ts +24 -1
  566. package/src/utils/price-history.ts +7 -1
  567. package/src/utils/shell-word.ts +8 -0
  568. package/src/utils/trailing-dividend.ts +53 -4
  569. package/src/version.ts +2 -2
  570. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -1,4 +1,7 @@
1
- import { dirname } from "path";
1
+ import { rm } from "fs/promises";
2
+ import { tmpdir } from "os";
3
+ import { dirname, win32 } from "path";
4
+ import { psString, runScriptAfterExit, writePowerShellScript } from "./windows-script";
2
5
 
3
6
  /**
4
7
  * The folders the relaunch script finds `sleep` in, after whatever PATH the
@@ -29,6 +32,8 @@ export interface RelaunchContext {
29
32
  cwd: string;
30
33
  /** PATH as the app sees it. */
31
34
  pathEnv: string;
35
+ /** Where the app keeps its data when GLOOMBERB_HOME moves it. */
36
+ home?: string;
32
37
  }
33
38
 
34
39
  interface RelaunchCommand {
@@ -47,15 +52,16 @@ function currentProcess(): RelaunchContext {
47
52
  args: process.argv.slice(1),
48
53
  cwd: process.cwd(),
49
54
  pathEnv: process.env.PATH ?? "",
55
+ home: process.env.GLOOMBERB_HOME || undefined,
50
56
  };
51
57
  }
52
58
 
53
59
  /**
54
- * What opens the app again once `context.pid` has exited. The script runs in
55
- * `/` with the shell and `open` named by their full paths: the launcher starts
56
- * the app inside its bundle's `Contents/MacOS`, which a rename to the app's
57
- * new name takes away, and a spawn into a folder that is gone fails as if the
58
- * shell were missing (`ENOENT, posix_spawn 'sh'`).
60
+ * What opens the app again once `context.pid` has exited, on a Mac or Linux.
61
+ * The script runs in `/` with the shell and `open` named by their full paths:
62
+ * the launcher starts the app inside its bundle's `Contents/MacOS`, which a
63
+ * rename to the app's new name takes away, and a spawn into a folder that is
64
+ * gone fails as if the shell were missing (`ENOENT, posix_spawn 'sh'`).
59
65
  */
60
66
  export function desktopRelaunchCommand(context: RelaunchContext, bundle?: string): RelaunchCommand {
61
67
  const pathEnv = context.pathEnv ? `${context.pathEnv}:${SYSTEM_PATH}` : SYSTEM_PATH;
@@ -70,8 +76,6 @@ export function desktopRelaunchCommand(context: RelaunchContext, bundle?: string
70
76
  };
71
77
  }
72
78
 
73
- if (context.platform === "win32") return { cmd: [context.execPath, ...context.args] };
74
-
75
79
  const args = context.args.map(shellQuote).join(" ");
76
80
  return {
77
81
  cmd: ["/bin/sh", "-c", `${waitForExit}; cd ${shellQuote(context.cwd)} 2>/dev/null; ${shellQuote(context.execPath)}${args ? ` ${args}` : ""}`],
@@ -80,12 +84,100 @@ export function desktopRelaunchCommand(context: RelaunchContext, bundle?: string
80
84
  };
81
85
  }
82
86
 
87
+ export interface WindowsRelaunch {
88
+ /** The app's process, which the script waits out. */
89
+ pid: number;
90
+ /** `bin\launcher.exe` beside the runtime: what the Start menu opens. */
91
+ launcher: string;
92
+ log: string;
93
+ cancel: string;
94
+ taskName: string;
95
+ /** GLOOMBERB_HOME for the app it opens; a task has the user's environment, not the app's. */
96
+ home?: string;
97
+ }
98
+
99
+ /**
100
+ * The PowerShell 5.1 script that opens the Windows app again. It answers
101
+ * `started` in its log, waits for the app's process to be gone (Chromium's
102
+ * profile stays held until then, and an app opened before hands itself to the
103
+ * closing one and crashes), then opens the launcher. An app that stopped
104
+ * waiting for it writes `cancel` and stays open, so the script opens nothing.
105
+ */
106
+ export function windowsRelaunchScript({ pid, launcher, log, cancel, taskName, home }: WindowsRelaunch): string {
107
+ return [
108
+ "$ErrorActionPreference = 'Stop'",
109
+ `$OldPid = ${pid}`,
110
+ `$Launcher = ${psString(launcher)}`,
111
+ `$Log = ${psString(log)}`,
112
+ `$Cancel = ${psString(cancel)}`,
113
+ `$TaskName = ${psString(taskName)}`,
114
+ `$DataHome = ${psString(home ?? "")}`,
115
+ String.raw`
116
+ $LogEncoding = New-Object System.Text.UTF8Encoding $false
117
+ function Write-Log([string]$Message) {
118
+ for ($Attempt = 0; $Attempt -lt 10; $Attempt += 1) {
119
+ try { [IO.File]::AppendAllText($Log, (Get-Date).ToString('o') + ' ' + $Message + [Environment]::NewLine, $LogEncoding); return } catch { Start-Sleep -Milliseconds 100 }
120
+ }
121
+ }
122
+
123
+ try {
124
+ if (-not (Test-Path -LiteralPath $Launcher)) { Write-Log ('declined: no ' + $Launcher); return }
125
+ Write-Log ('started ' + $PID)
126
+ $Deadline = (Get-Date).AddMinutes(5)
127
+ while ((Get-Process -Id $OldPid -ErrorAction SilentlyContinue) -and -not (Test-Path -LiteralPath $Cancel) -and (Get-Date) -lt $Deadline) {
128
+ Start-Sleep -Milliseconds 250
129
+ }
130
+ if (Test-Path -LiteralPath $Cancel) { Write-Log 'the app stopped waiting; it stays open'; return }
131
+ if (Get-Process -Id $OldPid -ErrorAction SilentlyContinue) { Write-Log 'the app did not quit'; return }
132
+ Start-Sleep -Milliseconds 500
133
+ if ($DataHome) { $env:GLOOMBERB_HOME = $DataHome }
134
+ Start-Process -FilePath $Launcher -WorkingDirectory (Split-Path -Parent $Launcher)
135
+ Write-Log ('opened ' + $Launcher)
136
+ } catch {
137
+ Write-Log ('could not open the app: ' + $_.Exception.Message)
138
+ } finally {
139
+ $ErrorActionPreference = 'Continue'
140
+ schtasks.exe /delete /tn $TaskName /f | Out-Null
141
+ Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
142
+ }
143
+ `,
144
+ ].join("\n");
145
+ }
146
+
147
+ /**
148
+ * Electrobun puts the Windows app in a job that ends every process it starts
149
+ * when it exits, so the script runs from a scheduled task, which outlives it.
150
+ * Resolves once the script has started: the app may quit then.
151
+ */
152
+ async function scheduleWindowsRelaunch(context: RelaunchContext): Promise<void> {
153
+ const name = `gloom-restart-${context.pid}`;
154
+ const script = win32.join(tmpdir(), `${name}.ps1`);
155
+ const log = win32.join(tmpdir(), `${name}.log`);
156
+ const cancel = win32.join(tmpdir(), `${name}.cancel`);
157
+ await rm(log, { force: true });
158
+ await rm(cancel, { force: true });
159
+ await writePowerShellScript(script, windowsRelaunchScript({
160
+ pid: context.pid,
161
+ launcher: win32.join(win32.dirname(context.execPath), "launcher.exe"),
162
+ log,
163
+ cancel,
164
+ taskName: `GloomRestart-${context.pid}`,
165
+ home: context.home,
166
+ }));
167
+ const answer = await runScriptAfterExit(script, log, cancel, `GloomRestart-${context.pid}`);
168
+ if (answer.kind !== "started") throw new Error(`the restart script did not start: ${answer.reason}`);
169
+ }
170
+
83
171
  /**
84
172
  * Opens the app again once this process has exited. On a Mac, `bundle` opens
85
173
  * that bundle instead of the one this process runs from: the app's own, under
86
- * the name it was just given.
174
+ * the name it was just given. Resolves once this process may exit.
87
175
  */
88
- export function scheduleDesktopRelaunch(bundle?: string, context: RelaunchContext = currentProcess()): void {
176
+ export async function scheduleDesktopRelaunch(bundle?: string, context: RelaunchContext = currentProcess()): Promise<void> {
177
+ if (context.platform === "win32") {
178
+ await scheduleWindowsRelaunch(context);
179
+ return;
180
+ }
89
181
  const { cmd, cwd, pathEnv } = desktopRelaunchCommand(context, bundle);
90
182
  const child = Bun.spawn(cmd, {
91
183
  ...(cwd ? { cwd } : {}),
@@ -1,4 +1,4 @@
1
- import { readdir, rm } from "fs/promises";
1
+ import { open, readdir, rm } from "fs/promises";
2
2
  import { basename, dirname, join } from "path";
3
3
  import type Electrobun from "electrobun/bun";
4
4
  import type { UpdateStatusEntry } from "electrobun/bun";
@@ -145,24 +145,101 @@ export function firstTarPath(bytes: Uint8Array): string | null {
145
145
  return null;
146
146
  }
147
147
 
148
+ /**
149
+ * An octal number field of a tar header, read as tar readers do: the digits
150
+ * after any leading spaces, up to the first other character. The checksum is
151
+ * what tells a header from noise.
152
+ */
153
+ function tarOctal(header: Uint8Array, start: number, length: number): number {
154
+ const digits = /^[\0 ]*([0-7]*)/.exec(new TextDecoder().decode(header.subarray(start, start + length)))?.[1];
155
+ return digits ? Number.parseInt(digits, 8) : 0;
156
+ }
157
+
158
+ /** Whether a header's checksum matches it: its bytes summed with the checksum field as spaces. */
159
+ function tarHeaderChecksumMatches(header: Uint8Array): boolean {
160
+ const stored = tarOctal(header, 148, 8);
161
+ let unsigned = 0;
162
+ let signed = 0;
163
+ for (let index = 0; index < TAR_BLOCK; index += 1) {
164
+ const byte = index >= 148 && index < 156 ? 0x20 : header[index]!;
165
+ unsigned += byte;
166
+ signed += byte > 127 ? byte - 256 : byte;
167
+ }
168
+ return stored === unsigned || stored === signed;
169
+ }
170
+
171
+ /**
172
+ * Whether the file at `path` is a whole tar: every header from the first
173
+ * matches its checksum, and the entries reach the end-of-archive block before
174
+ * the file ends. A tar cut off partway (a decompress that failed, an app that
175
+ * quit while writing it) or no tar at all is not. It reads the headers only,
176
+ * so it says nothing about the files' contents.
177
+ */
178
+ export async function isWholeTar(path: string): Promise<boolean> {
179
+ const file = await open(path, "r").catch(() => null);
180
+ if (!file) return false;
181
+ try {
182
+ const { size } = await file.stat();
183
+ const header = new Uint8Array(TAR_BLOCK);
184
+ for (let offset = 0; offset + TAR_BLOCK <= size;) {
185
+ const { bytesRead } = await file.read(header, 0, TAR_BLOCK, offset);
186
+ if (bytesRead < TAR_BLOCK) return false;
187
+ if (header.every((byte) => byte === 0)) return offset > 0;
188
+ if (!tarHeaderChecksumMatches(header)) return false;
189
+ // Octal: the base-256 sizes past 8 GB are not in an app's archive.
190
+ const entrySize = tarOctal(header, 124, 12);
191
+ offset += TAR_BLOCK + Math.ceil(entrySize / TAR_BLOCK) * TAR_BLOCK;
192
+ }
193
+ return false;
194
+ } catch {
195
+ return false;
196
+ } finally {
197
+ await file.close().catch(() => {});
198
+ }
199
+ }
200
+
201
+ /** Whether `path` holds a file that is not a whole tar. A missing one is not damaged. */
202
+ async function isDamagedTar(path: string): Promise<boolean> {
203
+ return await Bun.file(path).exists() && !(await isWholeTar(path));
204
+ }
205
+
206
+ /** Where Electrobun keeps the update with build hash `hash` once it is downloaded. */
207
+ function stagedArchivePath(extractionFolder: string, hash: string | undefined): string | null {
208
+ return hash ? join(extractionFolder, `${hash}.tar`) : null;
209
+ }
210
+
211
+ async function removeStagedArchive(path: string): Promise<void> {
212
+ await rm(path, { force: true }).catch(logFailure("desktop-update", "Could not remove a damaged update archive"));
213
+ }
214
+
148
215
  /**
149
216
  * Removes the unpacked update archives in Electrobun's `self-extraction`
150
- * folder whose app carries its name from before the rename. The one that
151
- * matters is the Windows archive an install from then updated from: it is
152
- * packed again with the folder renamed (scripts/write-legacy-desktop-files.ts),
153
- * so its bytes are not those of the archive the next release's patch is made
154
- * from, and bspatch would turn it into a broken build. Without it the updater
217
+ * folder it must not build on. Electrobun takes `<hash>.tar` there as the
218
+ * update already downloaded without reading it, and a damaged one (part of a
219
+ * tar, from a decompress that failed) then failed to unpack at every install:
220
+ * without it, the download fetches a fresh copy. Also removed are the ones
221
+ * whose app carries its name from before the rename. The one that matters is
222
+ * the Windows archive an install from then updated from: it is packed again
223
+ * with the folder renamed (scripts/write-legacy-desktop-files.ts), so its
224
+ * bytes are not those of the archive the next release's patch is made from,
225
+ * and bspatch would turn it into a broken build. Without it the updater
155
226
  * downloads the full archive instead. Resolves to the paths it removed.
156
227
  */
157
- async function removeLegacyUpdateArchives(folder: string): Promise<string[]> {
228
+ async function removeUnusableUpdateArchives(folder: string): Promise<string[]> {
158
229
  const removed: string[] = [];
159
230
  for (const entry of await readdir(folder).catch(() => [] as string[])) {
160
231
  if (!entry.endsWith(".tar")) continue;
161
232
  const path = join(folder, entry);
162
- const head = await Bun.file(path).slice(0, 64 * 1024).bytes().catch(() => null);
163
- const top = head ? firstTarPath(head)?.replace(/^\.\//, "").split("/")[0] : undefined;
164
- if (top !== LEGACY_DESKTOP_APP_NAME && top !== `${LEGACY_DESKTOP_APP_NAME}.app`) continue;
165
- const done = await rm(path, { force: true }).then(() => true, logFailure("desktop-update", "Could not remove an update archive from before the rename", false));
233
+ let reason: string;
234
+ if (!(await isWholeTar(path))) {
235
+ reason = "Could not remove a damaged update archive";
236
+ } else {
237
+ const head = await Bun.file(path).slice(0, 64 * 1024).bytes().catch(() => null);
238
+ const top = head ? firstTarPath(head)?.replace(/^\.\//, "").split("/")[0] : undefined;
239
+ if (top !== LEGACY_DESKTOP_APP_NAME && top !== `${LEGACY_DESKTOP_APP_NAME}.app`) continue;
240
+ reason = "Could not remove an update archive from before the rename";
241
+ }
242
+ const done = await rm(path, { force: true }).then(() => true, logFailure("desktop-update", reason, false));
166
243
  if (done) removed.push(path);
167
244
  }
168
245
  return removed;
@@ -343,6 +420,12 @@ export function createElectrobunDesktopUpdater(
343
420
  /** An apply is under way, or finished and the app is quitting. */
344
421
  let applyInProgress = false;
345
422
 
423
+ /** Electrobun's `self-extraction` folder, where the download leaves the update as `<hash>.tar`. */
424
+ async function extractionFolder(): Promise<string | null> {
425
+ const appData = await updater.appDataFolder?.().catch(() => null);
426
+ return appData ? join(appData, "self-extraction") : null;
427
+ }
428
+
346
429
  /** The error progress for a failed step, reported to error tracking on the way. */
347
430
  function failed(failure: UpdateFailure, error: string, release?: { version?: string; channel?: UpdateChannel } | null): UpdateProgress {
348
431
  reportUpdateFailure(failure, {
@@ -529,8 +612,8 @@ export function createElectrobunDesktopUpdater(
529
612
  }
530
613
  target = result.release;
531
614
 
532
- const appData = await updater.appDataFolder?.().catch(() => null);
533
- if (appData) await removeLegacyUpdateArchives(join(appData, "self-extraction"));
615
+ const extraction = await extractionFolder();
616
+ if (extraction) await removeUnusableUpdateArchives(extraction);
534
617
  await updater.downloadUpdate();
535
618
  const updateInfo = updater.updateInfo();
536
619
  if (updateInfo?.error) {
@@ -541,6 +624,15 @@ export function createElectrobunDesktopUpdater(
541
624
  onProgress(failed({ phase: "download", code: "interrupted" }, "Desktop update did not finish downloading.", result.release));
542
625
  return;
543
626
  }
627
+ // However Electrobun came by it (a patch, the full download), an archive
628
+ // that is not whole is never offered for a restart. It goes, so the retry
629
+ // downloads it again.
630
+ const staged = extraction ? stagedArchivePath(extraction, updateInfo.hash) : null;
631
+ if (staged && await isDamagedTar(staged)) {
632
+ await removeStagedArchive(staged);
633
+ onProgress(failed({ phase: "verify", code: "corrupt" }, "The downloaded update is damaged.", result.release));
634
+ return;
635
+ }
544
636
 
545
637
  // Downloaded, not installed: applying relaunches the app, and that waits
546
638
  // for the user's explicit request (`apply`).
@@ -570,9 +662,12 @@ export function createElectrobunDesktopUpdater(
570
662
  }
571
663
 
572
664
  const target = readyRelease;
665
+ const stagedHash = updater.updateInfo()?.hash;
573
666
  applyInProgress = true;
574
667
  let relaunching = false;
575
668
  let reportedError = false;
669
+ /** The install failed on the archive itself: it does not unpack. */
670
+ let damaged = false;
576
671
  const report = (progress: UpdateProgress) => {
577
672
  if (progress.phase === "error") reportedError = true;
578
673
  onProgress(progress);
@@ -580,6 +675,7 @@ export function createElectrobunDesktopUpdater(
580
675
  const installFailed = (error: unknown, message: string) => {
581
676
  // Electrobun names the system error in its message ("Failed to replace app: EACCES: ...").
582
677
  const code = installFailureCode(classifyUpdateError(error), installKind);
678
+ if (code === "corrupt") damaged = true;
583
679
  report(failed({ phase: relaunching ? "relaunch" : "apply", code }, message, target));
584
680
  };
585
681
  // Kept before Electrobun quits the app, so the next launch can tell whether it took.
@@ -610,9 +706,14 @@ export function createElectrobunDesktopUpdater(
610
706
  }
611
707
 
612
708
  // Past this point the app is quitting, so the guard stays up. A failed apply
613
- // drops the staged release: the retry starts from the download again.
709
+ // drops the staged release: the retry starts from the download again. An
710
+ // archive that does not unpack goes too, or that download would take it as
711
+ // done once more and the install would fail on it again.
614
712
  if (!relaunching) {
615
713
  readyRelease = null;
714
+ const extraction = damaged ? await extractionFolder() : null;
715
+ const staged = extraction ? stagedArchivePath(extraction, stagedHash) : null;
716
+ if (staged) await removeStagedArchive(staged);
616
717
  applyInProgress = false;
617
718
  }
618
719
  }
@@ -0,0 +1,52 @@
1
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import type { DesktopViewIds } from "../../shared/protocol";
4
+
5
+ /**
6
+ * The copy on disk of the ids the desktop view keeps in its WebView storage
7
+ * (view/view-ids.ts), in the data folder beside the install id. That storage
8
+ * belongs to the app's bundle identifier, and this copy is what a build under
9
+ * another identifier starts from.
10
+ */
11
+ const FILE_NAME = "desktop-view-ids.json";
12
+ const KEY = /^gloomberb\.[a-z0-9.-]{1,64}$/i;
13
+ const MAX_IDS = 16;
14
+ const MAX_VALUE_LENGTH = 4096;
15
+
16
+ function viewIdsPath(dataDir: string): string {
17
+ return join(dataDir, FILE_NAME);
18
+ }
19
+
20
+ /** The entries of `raw` shaped like view ids: a storage key of this app's and a short string. */
21
+ function parseViewIds(raw: unknown): DesktopViewIds {
22
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
23
+ const ids: DesktopViewIds = {};
24
+ for (const [key, value] of Object.entries(raw).slice(0, MAX_IDS)) {
25
+ if (KEY.test(key) && typeof value === "string" && value.length > 0 && value.length <= MAX_VALUE_LENGTH) ids[key] = value;
26
+ }
27
+ return ids;
28
+ }
29
+
30
+ /** What the data folder keeps; nothing when the file is missing or unreadable. */
31
+ export function readDesktopViewIds(dataDir: string): DesktopViewIds {
32
+ try {
33
+ return parseViewIds(JSON.parse(readFileSync(viewIdsPath(dataDir), "utf8")));
34
+ } catch {
35
+ // Intentional ignore: no copy yet (a first launch, or a build from before it was kept), so the view keeps what it has.
36
+ return {};
37
+ }
38
+ }
39
+
40
+ /** Replaces the copy with what the view sent, written whole or not at all. Throws when it cannot, so the view tries again. */
41
+ export function saveDesktopViewIds(dataDir: string, ids: unknown): void {
42
+ const path = viewIdsPath(dataDir);
43
+ const temporary = `${path}.${process.pid}.tmp`;
44
+ try {
45
+ mkdirSync(dataDir, { recursive: true });
46
+ writeFileSync(temporary, JSON.stringify(parseViewIds(ids)));
47
+ renameSync(temporary, path);
48
+ } catch (error) {
49
+ rmSync(temporary, { force: true });
50
+ throw error;
51
+ }
52
+ }
@@ -0,0 +1,92 @@
1
+ import { readFile, writeFile } from "fs/promises";
2
+
3
+ /**
4
+ * PowerShell scripts the Windows app leaves to finish what it cannot do while
5
+ * it runs: its own files are locked until it quits.
6
+ */
7
+
8
+ /** How long a script has to answer before the app stops waiting and keeps running. */
9
+ const SCRIPT_START_TIMEOUT_MS = 20_000;
10
+
11
+ /**
12
+ * `value` as a single-quoted PowerShell string. PowerShell also takes the
13
+ * curly single quotes U+2018 to U+201B for quotes; doubling escapes each.
14
+ */
15
+ export function psString(value: string): string {
16
+ return `'${value.replace(/['\u2018-\u201B]/g, "$&$&")}'`;
17
+ }
18
+
19
+ /** PowerShell 5.1, which every supported Windows has, running `script` hidden and without the user's profile. */
20
+ function powershellCommand(script: string): string[] {
21
+ return ["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File", script];
22
+ }
23
+
24
+ /**
25
+ * Writes `text` as a script for PowerShell 5.1: with Windows line ends, and
26
+ * with a byte order mark, without which it reads the file in the system code
27
+ * page and garbles a path with an accent in it.
28
+ */
29
+ export async function writePowerShellScript(path: string, text: string): Promise<void> {
30
+ await writeFile(path, `\uFEFF${text.replaceAll(/\r?\n/g, "\r\n")}`, "utf8");
31
+ }
32
+
33
+ /** What a script that runs after the app quits answered before the app quit for it. */
34
+ export type ScriptAnswer =
35
+ /** It waits for the app to quit. */
36
+ | { kind: "started" }
37
+ /** It cannot run now, for a while: something it needs is in use. */
38
+ | { kind: "busy"; reason: string }
39
+ /** It cannot run. */
40
+ | { kind: "declined"; reason: string };
41
+
42
+ /**
43
+ * The script's answer in its log, a line `<time> started …`,
44
+ * `<time> busy: …` or `<time> declined: …`; null before it wrote one.
45
+ */
46
+ export function scriptAnswer(log: string): ScriptAnswer | null {
47
+ for (const line of log.replace(/^\uFEFF/, "").split(/\r?\n/)) {
48
+ const match = /^\S+ (started|busy|declined)\b:? ?(.*)$/.exec(line);
49
+ if (!match) continue;
50
+ if (match[1] === "started") return { kind: "started" };
51
+ return { kind: match[1] === "busy" ? "busy" : "declined", reason: match[2]?.trim() ?? "" };
52
+ }
53
+ return null;
54
+ }
55
+
56
+ function schtasks(...args: string[]): void {
57
+ const result = Bun.spawnSync(["schtasks.exe", ...args], { stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
58
+ if (!result.success) throw new Error(`schtasks ${args[0]}: ${result.stderr.toString().trim()}`);
59
+ }
60
+
61
+ /**
62
+ * Runs `script` from the scheduled task `taskName`, which outlives the app (a
63
+ * child of its process dies with it), and resolves to the answer it writes to
64
+ * `log`: only on `started` may the app quit for it. The script removes the
65
+ * task. When it does not answer in time, the app writes `cancel`, which keeps
66
+ * a script that starts late from doing anything, and this throws with the
67
+ * task gone, as it does when the task could not be made.
68
+ */
69
+ export async function runScriptAfterExit(script: string, log: string, cancel: string, taskName: string): Promise<ScriptAnswer> {
70
+ const command = powershellCommand(script).map((part) => (part === script ? `"${part}"` : part)).join(" ");
71
+ try {
72
+ schtasks("/create", "/tn", taskName, "/tr", command, "/sc", "once", "/st", "00:00", "/f");
73
+ schtasks("/run", "/tn", taskName);
74
+ const deadline = Date.now() + SCRIPT_START_TIMEOUT_MS;
75
+ while (Date.now() < deadline) {
76
+ const answer = scriptAnswer(await readFile(log, "utf8").catch(() => ""));
77
+ if (answer) return answer;
78
+ await Bun.sleep(250);
79
+ }
80
+ await writeFile(cancel, "").catch(() => {});
81
+ throw new Error("the script did not start");
82
+ } catch (error) {
83
+ try { schtasks("/delete", "/tn", taskName, "/f"); } catch { /* Intentional ignore: never made, or the script removed it. */ }
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /** Runs `script` in the background, as a child of the app; resolves to its exit code. */
89
+ export async function runScriptInBackground(script: string): Promise<number> {
90
+ const child = Bun.spawn(powershellCommand(script), { stdio: ["ignore", "ignore", "ignore"], windowsHide: true });
91
+ return await child.exited;
92
+ }
@@ -25,9 +25,11 @@ import {
25
25
  type RememberedDesktopWindow,
26
26
  } from "./window/remembered-frame";
27
27
  import { reapStaleTerminalMedia } from "../../opentui/terminal-media";
28
+ import { quitWindowsEventLoopOnItsThread } from "./window/windows-quit";
28
29
  import { installProcessCrashListeners } from "../../../telemetry/crash-reports-node";
29
30
  import { renameLegacyMacApp } from "./desktop/app-name";
30
31
  import { scheduleDesktopRelaunch } from "./desktop/relaunch";
32
+ import { settleInstallerFolder } from "./desktop/installer-folder";
31
33
 
32
34
  console.log = (...args) => console.error(...args);
33
35
  console.info = (...args) => console.error(...args);
@@ -45,6 +47,25 @@ setConfigStoreHost(nodeConfigStoreHost);
45
47
  // before the config, and its off switch, has been read.
46
48
  installProcessCrashListeners();
47
49
 
50
+ // An install the Windows installer made lives in the installer's folder,
51
+ // where its shortcuts, the gloom command and its uninstall entry lead.
52
+ // Electrobun's updater installs an update into a folder of its own and opens
53
+ // it from there: this build moves into the installer's folder and opens again
54
+ // from it before any window shows.
55
+ const installerFolderMove = await settleInstallerFolder({
56
+ platform: process.platform,
57
+ execPath: process.execPath,
58
+ homeDir: homedir(),
59
+ localAppData: process.env.LOCALAPPDATA,
60
+ }).catch((error: unknown) => {
61
+ console.error("[desktop] could not move the app into the installer's folder", error instanceof Error ? error.message : String(error));
62
+ return null;
63
+ });
64
+ if (installerFolderMove?.kind === "relaunching") {
65
+ Utils.quit();
66
+ await new Promise<never>(() => {});
67
+ }
68
+
48
69
  // An install from before the rename runs from Gloomberb.app, where its updater
49
70
  // put this build. It takes the new name and opens again from there before any
50
71
  // window shows. Anywhere else this returns without touching the disk.
@@ -58,7 +79,7 @@ const appRename = await renameLegacyMacApp({
58
79
  return null;
59
80
  });
60
81
  if (appRename?.kind === "renamed") {
61
- scheduleDesktopRelaunch(appRename.bundle);
82
+ await scheduleDesktopRelaunch(appRename.bundle);
62
83
  Utils.quit();
63
84
  // The process is exiting; nothing below may open a window meanwhile.
64
85
  await new Promise<never>(() => {});
@@ -171,12 +192,15 @@ const mainWindow = createAppWindow({
171
192
  preload: desktopBootFramePreload(readDesktopBootFrame(desktopBootFramePath())),
172
193
  });
173
194
  backend.mainWindow = mainWindow;
195
+ quitWindowsEventLoopOnItsThread(mainWindow);
174
196
  // The window is loading its view; the services it will ask for start now
175
197
  // rather than when it asks. Its `init` reports a failure, and retries.
176
198
  startDesktopBackend(backend).catch((error) => {
177
199
  console.error("[desktop] startup failed", error instanceof Error ? error.stack ?? error.message : String(error));
178
200
  });
179
- if (rememberedWindow?.fullscreen) {
201
+ // Windows has no way into fullscreen here, and builds whose frameless window
202
+ // Windows took for fullscreen saved every window as fullscreen.
203
+ if (rememberedWindow?.fullscreen && process.platform !== "win32") {
180
204
  try { mainWindow.setFullScreen(true); } catch { /* the framed window is still the one they sized */ }
181
205
  } else if (rememberedWindow?.maximized) {
182
206
  try { mainWindow.maximize(); } catch { /* the framed window is still the one they sized */ }
@@ -1,5 +1,6 @@
1
1
  import { BrowserWindow } from "electrobun/bun";
2
2
  import {
3
+ applyNativeWindowFrame,
3
4
  applyWindowMoveEvent,
4
5
  applyWindowResizeEvent,
5
6
  updateWindowFrameCache,
@@ -9,6 +10,7 @@ import {
9
10
  import type { WindowFrame, WindowMinimumSize } from "./frame";
10
11
  import { applyWindowsCustomChrome } from "./windows-custom-chrome";
11
12
  import { applyWindowsWindowIcon } from "./windows-icons";
13
+ import { fitPageToClientArea } from "./windows-native";
12
14
 
13
15
  type DesktopTitleBarStyle = "default" | "hidden" | "hiddenInset";
14
16
  type DesktopWindowRenderer = "native" | "cef";
@@ -20,8 +22,13 @@ type DesktopWindowStyleMask = {
20
22
  FullSizeContentView?: boolean;
21
23
  };
22
24
 
25
+ /**
26
+ * No native title bar: the header is the title bar. On Windows "hidden" is a
27
+ * bare popup with no frame to resize by, which Windows also takes for
28
+ * fullscreen; "hiddenInset" keeps the sizing frame and cuts out the caption.
29
+ */
23
30
  function desktopTitleBarStyle(): DesktopTitleBarStyle {
24
- return process.platform === "darwin" ? "hiddenInset" : "hidden";
31
+ return process.platform === "linux" ? "hidden" : "hiddenInset";
25
32
  }
26
33
 
27
34
  function desktopWindowRenderer(): DesktopWindowRenderer {
@@ -43,6 +50,22 @@ function desktopWindowStyleMask(): DesktopWindowStyleMask {
43
50
  };
44
51
  }
45
52
 
53
+ const APP_VIEW_URL = "views://mainview/index.html";
54
+
55
+ /**
56
+ * Electrobun's Windows webview takes a page load that starts while Ctrl is
57
+ * down for a Ctrl+click on a link: it cancels the load and asks for a new
58
+ * window instead. A window opened from a Ctrl chord, as Ctrl+Shift+O pops a
59
+ * pane out, stayed blank. Its page is loaded again, which Electrobun lets
60
+ * through within half a second of the one it cancelled.
61
+ */
62
+ function loadAgainWhenTakenForCtrlClick(window: BrowserWindow): void {
63
+ (window.webview as { on: (name: string, handler: (event: unknown) => void) => void }).on("new-window-open", (event) => {
64
+ const detail = (event as { data?: { detail?: { url?: unknown } } }).data?.detail;
65
+ if (detail?.url === APP_VIEW_URL) window.webview.loadURL(APP_VIEW_URL);
66
+ });
67
+ }
68
+
46
69
  interface AppWindowOptions {
47
70
  title: string;
48
71
  /** Already normalized to at least `minSize`. */
@@ -63,7 +86,7 @@ export function createAppWindow({ title, frame, rpc, minSize, onFrameChange, pre
63
86
  const window = new BrowserWindow({
64
87
  title,
65
88
  frame,
66
- url: "views://mainview/index.html",
89
+ url: APP_VIEW_URL,
67
90
  ...(preload ? { preload } : {}),
68
91
  renderer: desktopWindowRenderer(),
69
92
  rpc,
@@ -74,13 +97,29 @@ export function createAppWindow({ title, frame, rpc, minSize, onFrameChange, pre
74
97
  });
75
98
  applyWindowsWindowIcon(title);
76
99
  applyWindowsCustomChrome(title);
100
+ if (process.platform === "win32") {
101
+ loadAgainWhenTakenForCtrlClick(window);
102
+ // The page exists once it has loaded; until then there is nothing to fit.
103
+ window.webview.on("dom-ready", () => {
104
+ fitPageToClientArea(window.ptr);
105
+ });
106
+ }
77
107
  updateWindowFrameCache(window, frame, minSize);
108
+ const readsNativeFrame = process.platform === "win32";
78
109
  (window as any).on?.("move", (event: WindowMoveEvent) => {
79
- applyWindowMoveEvent(window, event);
110
+ if (readsNativeFrame) {
111
+ if (!applyNativeWindowFrame(window, minSize)) return;
112
+ } else {
113
+ applyWindowMoveEvent(window, event);
114
+ }
80
115
  onFrameChange?.();
81
116
  });
82
117
  (window as any).on?.("resize", (event: WindowResizeEvent) => {
83
- applyWindowResizeEvent(window, event, minSize);
118
+ if (readsNativeFrame) {
119
+ if (!applyNativeWindowFrame(window, minSize)) return;
120
+ } else {
121
+ applyWindowResizeEvent(window, event, minSize);
122
+ }
84
123
  onFrameChange?.();
85
124
  });
86
125
  return window;
@@ -47,3 +47,23 @@ export function applyWindowResizeEvent(
47
47
  }
48
48
  return nextFrame;
49
49
  }
50
+
51
+ type FramedWindow = Pick<BrowserWindow, "frame" | "getFrame" | "isMinimized" | "setFrame">;
52
+
53
+ /**
54
+ * Electrobun's Windows events do not carry the window's frame: a resize
55
+ * reports the client area's size at 0,0, a move the client area's corner as
56
+ * unsigned 16-bit values, and both come while the window is minimized. There
57
+ * the frame is read from the window itself, and a minimized window keeps the
58
+ * frame it had.
59
+ */
60
+ export function applyNativeWindowFrame(window: FramedWindow, minimumSize?: WindowMinimumSize): WindowFrame | null {
61
+ if (window.isMinimized()) return null;
62
+ const read = window.getFrame();
63
+ if (!(read.width > 0 && read.height > 0)) return null;
64
+ const nextFrame = updateWindowFrameCache(window as BrowserWindow, read, minimumSize);
65
+ if (nextFrame && (nextFrame.width !== read.width || nextFrame.height !== read.height)) {
66
+ window.setFrame(nextFrame.x, nextFrame.y, nextFrame.width, nextFrame.height);
67
+ }
68
+ return nextFrame;
69
+ }