gloomberb 0.15.6 → 0.15.8

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 (311) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/src/api-client/data.ts +42 -10
  5. package/src/api-client/doe.ts +75 -0
  6. package/src/api-client/estimate-revisions.ts +1 -1
  7. package/src/api-client/futures-curve.ts +1 -1
  8. package/src/api-client/index.ts +7 -0
  9. package/src/api-client/market-discovery.ts +84 -0
  10. package/src/api-client/paths.ts +17 -11
  11. package/src/api-client/request.ts +12 -2
  12. package/src/api-client/tape.ts +2 -2
  13. package/src/api-client/types.ts +4 -0
  14. package/src/app/dialog-bridge.tsx +33 -0
  15. package/src/app/pane-runtime/index.tsx +7 -2
  16. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -1
  17. package/src/app/pane-runtime/pane-template-runtime.ts +8 -28
  18. package/src/app/pane-runtime/plugin-bindings.ts +34 -12
  19. package/src/app/pane-runtime/ticker-open-runtime.ts +5 -3
  20. package/src/app/runtime/broker-import.ts +1 -1
  21. package/src/app/runtime/desktop-menu.ts +9 -6
  22. package/src/app/runtime/plugin-bindings.ts +7 -1
  23. package/src/app/runtime/ticker-refresh.ts +1 -1
  24. package/src/app.tsx +12 -0
  25. package/src/brokers/directory.ts +11 -2
  26. package/src/brokers/profile-form.ts +2 -1
  27. package/src/brokers/remove-profile.ts +61 -0
  28. package/src/brokers/signed-in/connect.ts +20 -7
  29. package/src/brokers/signed-in/sign-in-dialog.tsx +68 -30
  30. package/src/cli/desktop-pane-shot.ts +2 -2
  31. package/src/cli/history-rows.ts +1 -1
  32. package/src/cli/pane-functions/function-help.ts +18 -3
  33. package/src/cli/ticker-resolution.ts +2 -2
  34. package/src/components/chart/composite/composite-chart.tsx +15 -2
  35. package/src/components/chart/composite/format.ts +24 -11
  36. package/src/components/chart/composite/rasterizer.ts +67 -16
  37. package/src/components/chart/composite/scene.ts +53 -2
  38. package/src/components/chart/composite/session-shading.ts +3 -2
  39. package/src/components/chart/composite/text-renderer.ts +52 -1
  40. package/src/components/chart/composite/time-axis.ts +100 -30
  41. package/src/components/chart/composite/tools.ts +2 -1
  42. package/src/components/chart/composite/types.ts +4 -0
  43. package/src/components/command-bar/action-runtime.ts +7 -67
  44. package/src/components/command-bar/commands/direct/index.ts +9 -5
  45. package/src/components/command-bar/commands/direct/runtime.ts +12 -11
  46. package/src/components/command-bar/commands/plugin/actions.ts +6 -35
  47. package/src/components/command-bar/helpers.ts +3 -31
  48. package/src/components/command-bar/keyboard-handlers.ts +7 -219
  49. package/src/components/command-bar/keyboard-shortcuts.ts +25 -99
  50. package/src/components/command-bar/layout-items/current-layout.ts +2 -1
  51. package/src/components/command-bar/layout-items/focused-pane.ts +2 -1
  52. package/src/components/command-bar/layout-items/types.ts +2 -0
  53. package/src/components/command-bar/layout-items.ts +3 -0
  54. package/src/components/command-bar/list/model.ts +1 -1
  55. package/src/components/command-bar/list/navigation.ts +5 -12
  56. package/src/components/command-bar/pane-actions.ts +17 -14
  57. package/src/components/command-bar/pane-settings.ts +7 -250
  58. package/src/components/command-bar/pane-templates/workflow.ts +6 -13
  59. package/src/components/command-bar/panel/index.tsx +4 -110
  60. package/src/components/command-bar/panel/layout.ts +4 -16
  61. package/src/components/command-bar/panel/runtime.ts +1 -87
  62. package/src/components/command-bar/panel/state.ts +12 -29
  63. package/src/components/command-bar/panel/types.ts +2 -26
  64. package/src/components/command-bar/picker-activation.ts +7 -56
  65. package/src/components/command-bar/routes/root/plugin-install.ts +3 -3
  66. package/src/components/command-bar/routes/root/search-report.ts +2 -1
  67. package/src/components/command-bar/routing/actions.ts +21 -59
  68. package/src/components/command-bar/routing/confirm.ts +15 -88
  69. package/src/components/command-bar/routing/effects.ts +1 -20
  70. package/src/components/command-bar/routing/launch-request.ts +1 -36
  71. package/src/components/command-bar/routing/list-state.ts +5 -36
  72. package/src/components/command-bar/selection-runtime.ts +6 -35
  73. package/src/components/command-bar/surface/environment.ts +10 -6
  74. package/src/components/command-bar/surface/index.tsx +4 -37
  75. package/src/components/command-bar/workflow/alert.ts +23 -17
  76. package/src/components/command-bar/workflow/broker.ts +30 -40
  77. package/src/components/command-bar/workflow/builtin.ts +33 -38
  78. package/src/components/command-bar/workflow/collection-actions.ts +27 -28
  79. package/src/components/command-bar/workflow/collection-ticker.ts +4 -4
  80. package/src/components/command-bar/workflow/coordinator.ts +27 -156
  81. package/src/components/command-bar/workflow/fields.ts +6 -22
  82. package/src/components/command-bar/workflow/submit.ts +28 -43
  83. package/src/components/command-bar/workflow/ticker-listing-picker.ts +12 -9
  84. package/src/components/command-bar/workflow/tickers.ts +2 -2
  85. package/src/components/command-bar/workflow/types.ts +10 -35
  86. package/src/components/feedback-dialog.tsx +1 -13
  87. package/src/components/form-modal/broker-step.tsx +119 -0
  88. package/src/components/form-modal/confirm.tsx +145 -0
  89. package/src/components/form-modal/content.tsx +698 -0
  90. package/src/components/form-modal/deps.ts +77 -0
  91. package/src/components/form-modal/field-row.tsx +318 -0
  92. package/src/components/form-modal/host.tsx +169 -0
  93. package/src/components/form-modal/index.ts +2 -0
  94. package/src/components/form-modal/model.ts +131 -0
  95. package/src/components/form-modal/request.ts +67 -0
  96. package/src/components/form-modal/routes.ts +139 -0
  97. package/src/components/layout/shell/index.tsx +49 -28
  98. package/src/components/layout/status-bar.tsx +2 -1
  99. package/src/components/onboarding/first-run-workspace.ts +1 -1
  100. package/src/components/onboarding/onboarding-frame.tsx +4 -1
  101. package/src/components/onboarding/onboarding-wizard.tsx +1 -1
  102. package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -5
  103. package/src/components/onboarding/wizard-model.ts +13 -5
  104. package/src/components/onboarding/wizard-positions.ts +56 -7
  105. package/src/components/pane-settings-dialog.tsx +7 -1
  106. package/src/components/pane-template-wizard.tsx +4 -149
  107. package/src/components/sign-in-code-panel.tsx +22 -9
  108. package/src/components/textarea-keys.ts +13 -0
  109. package/src/components/ui/button.tsx +3 -2
  110. package/src/components/ui/confirm-dialog.tsx +62 -10
  111. package/src/components/ui/display.tsx +6 -3
  112. package/src/components/ui/fields.tsx +5 -0
  113. package/src/components/ui/frame.tsx +7 -2
  114. package/src/components/ui/header-case.ts +25 -3
  115. package/src/components/ui/list-view.tsx +16 -9
  116. package/src/components/ui/query-bar.tsx +8 -1
  117. package/src/core/app-runtime.ts +1 -1
  118. package/src/core/state/app/types.ts +1 -15
  119. package/src/data/config/home.ts +82 -11
  120. package/src/data/config/store/node.ts +29 -16
  121. package/src/i18n/es.ts +54 -7
  122. package/src/i18n/ja.ts +54 -7
  123. package/src/i18n/ko.ts +54 -7
  124. package/src/i18n/zh-cn.ts +54 -7
  125. package/src/i18n/zh-tw.ts +54 -7
  126. package/src/layout-marketplace/gallery.tsx +11 -3
  127. package/src/market-data/coordinator/fx-legs.ts +1 -1
  128. package/src/market-data/history-session.ts +3 -5
  129. package/src/market-data/market/freshness.ts +1 -1
  130. package/src/market-data/market/status.ts +1 -1
  131. package/src/market-data/market/trading-sessions.ts +65 -1
  132. package/src/market-data/quotes/metadata.ts +3 -3
  133. package/src/market-data/quotes/resolution.ts +3 -3
  134. package/src/news/types.ts +1 -1
  135. package/src/plugins/builtin/alerts/alert-engine.ts +2 -0
  136. package/src/plugins/builtin/alerts/cli.ts +2 -2
  137. package/src/plugins/builtin/alerts/types.ts +2 -0
  138. package/src/plugins/builtin/broker-manager/add-flow.tsx +625 -0
  139. package/src/plugins/builtin/broker-manager/add-request.ts +34 -0
  140. package/src/plugins/builtin/broker-manager/detail.tsx +128 -50
  141. package/src/plugins/builtin/broker-manager/footer.ts +6 -5
  142. package/src/plugins/builtin/broker-manager/index.tsx +62 -21
  143. package/src/plugins/builtin/broker-manager/model.ts +7 -0
  144. package/src/plugins/builtin/broker-manager/pane-actions.tsx +47 -44
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/headless-schema.ts +6 -0
  147. package/src/plugins/builtin/chart-composer/pane.tsx +28 -1
  148. package/src/plugins/builtin/cloud/company-picker.tsx +658 -0
  149. package/src/plugins/builtin/composite-plugins.ts +2 -0
  150. package/src/plugins/builtin/credit-boards/model.ts +1 -1
  151. package/src/plugins/builtin/dividend-yield/browser.tsx +2 -25
  152. package/src/plugins/builtin/dividend-yield/client.ts +34 -122
  153. package/src/plugins/builtin/dividend-yield/index.tsx +9 -9
  154. package/src/plugins/builtin/dividend-yield/provider-client.ts +1 -1
  155. package/src/plugins/builtin/doe/client.ts +73 -0
  156. package/src/plugins/builtin/doe/headless.ts +93 -0
  157. package/src/plugins/builtin/doe/index.tsx +41 -0
  158. package/src/plugins/builtin/doe/model.ts +215 -0
  159. package/src/plugins/builtin/doe/pane.tsx +148 -0
  160. package/src/plugins/builtin/doe/range-bar.tsx +45 -0
  161. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +7 -7
  162. package/src/plugins/builtin/estimate-revisions/client.ts +3 -3
  163. package/src/plugins/builtin/estimate-revisions/pane.tsx +2 -2
  164. package/src/plugins/builtin/executives/pane.tsx +17 -8
  165. package/src/plugins/builtin/fear-greed/charts.tsx +1 -1
  166. package/src/plugins/builtin/filing-events/pane.tsx +23 -6
  167. package/src/plugins/builtin/futures/contracts.ts +3 -3
  168. package/src/plugins/builtin/futures/returns.ts +1 -1
  169. package/src/plugins/builtin/futures-curve/client.ts +1 -1
  170. package/src/plugins/builtin/futures-curve/model.ts +1 -1
  171. package/src/plugins/builtin/help/index.tsx +8 -3
  172. package/src/plugins/builtin/holders/pane.tsx +1 -1
  173. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  174. package/src/plugins/builtin/market-heatmap/data.ts +13 -256
  175. package/src/plugins/builtin/market-heatmap/index.tsx +5 -15
  176. package/src/plugins/builtin/market-movers/client.ts +1 -1
  177. package/src/plugins/builtin/market-movers/screener.ts +60 -142
  178. package/src/plugins/builtin/market-valuation/defs.ts +1 -2
  179. package/src/plugins/builtin/market-valuation/sources.ts +1 -3
  180. package/src/plugins/builtin/news/wire/default-feeds.ts +2 -8
  181. package/src/plugins/builtin/news/wire/feed-config.ts +5 -1
  182. package/src/plugins/builtin/news/wire/persisted-articles.ts +9 -5
  183. package/src/plugins/builtin/news/wire/rss/cli.ts +1 -1
  184. package/src/plugins/builtin/news/wire/rss/managed-feed.ts +19 -0
  185. package/src/plugins/builtin/news/wire/rss/parser.ts +18 -9
  186. package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
  187. package/src/plugins/builtin/options/view.tsx +1 -1
  188. package/src/plugins/builtin/plugin-marketplace/model.ts +5 -0
  189. package/src/plugins/builtin/plugin-marketplace/pane.tsx +4 -2
  190. package/src/plugins/builtin/portfolio-list/cli/render.ts +1 -1
  191. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -20
  192. package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -2
  193. package/src/plugins/builtin/portfolio-list/mutations.ts +53 -1
  194. package/src/plugins/builtin/relative-rotation/client.ts +1 -1
  195. package/src/plugins/builtin/research/analyst-model.ts +1 -1
  196. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +6 -6
  197. package/src/plugins/builtin/research/event-model.ts +1 -1
  198. package/src/plugins/builtin/risk-factors/pane.tsx +29 -14
  199. package/src/plugins/builtin/shared/pro-feature-wall.tsx +47 -0
  200. package/src/plugins/builtin/shared/ticker-request.ts +1 -1
  201. package/src/plugins/builtin/short-interest/client.ts +14 -106
  202. package/src/plugins/builtin/short-interest/headless.ts +2 -2
  203. package/src/plugins/builtin/short-interest/index.tsx +5 -5
  204. package/src/plugins/builtin/short-interest/pane.tsx +4 -4
  205. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +42 -10
  206. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  207. package/src/plugins/builtin/ticker-detail/pane.tsx +2 -2
  208. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +1 -1
  209. package/src/plugins/builtin/time-sales/client.ts +3 -3
  210. package/src/plugins/builtin/vol-surface/model.ts +1 -1
  211. package/src/plugins/builtin/volatility/model.ts +1 -1
  212. package/src/plugins/bundle.ts +2 -2
  213. package/src/plugins/catalog-browser.ts +2 -0
  214. package/src/plugins/catalog-manifest.ts +0 -1
  215. package/src/plugins/catalog-ui.ts +1 -1
  216. package/src/plugins/catalog.ts +0 -2
  217. package/src/plugins/host-link.ts +1 -1
  218. package/src/plugins/host-resolver.ts +1 -1
  219. package/src/plugins/loader.ts +6 -5
  220. package/src/plugins/registry/host-actions.ts +6 -1
  221. package/src/plugins/registry/index.ts +1 -0
  222. package/src/plugins/runtime/context.tsx +2 -0
  223. package/src/plugins/runtime/index.tsx +2 -0
  224. package/src/public/__snapshots__/public-api.test.ts.snap +0 -1
  225. package/src/public/market-data.ts +0 -7
  226. package/src/remote/controller-utils.ts +1 -1
  227. package/src/remote/controller.ts +27 -3
  228. package/src/remote/form.ts +20 -0
  229. package/src/remote/resources.ts +7 -0
  230. package/src/remote/schema.ts +25 -3
  231. package/src/remote/semantic-tree.tsx +15 -0
  232. package/src/remote/types.ts +2 -0
  233. package/src/renderers/browser/research-entry.ts +2 -2
  234. package/src/renderers/cloudflare/http-proxy.ts +157 -33
  235. package/src/renderers/cloudflare/proxy-session.ts +270 -0
  236. package/src/renderers/cloudflare/worker.ts +4 -1
  237. package/src/renderers/dom/desktop/controls.tsx +2 -1
  238. package/src/renderers/dom/dialog-host.tsx +17 -8
  239. package/src/renderers/dom/styles.css +4 -0
  240. package/src/renderers/electrobun/bun/application-menu/index.ts +4 -4
  241. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  242. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -2
  243. package/src/renderers/electrobun/bun/external-plugins.ts +1 -1
  244. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +3 -6
  245. package/src/renderers/electrobun/view/cli-pane-shot-health.ts +3 -3
  246. package/src/renderers/opentui/dialog-host.tsx +14 -2
  247. package/src/renderers/opentui/start.tsx +2 -2
  248. package/src/renderers/opentui/terminal-media.ts +2 -2
  249. package/src/renderers/opentui/test-utils.tsx +9 -2
  250. package/src/renderers/opentui/toast-host.tsx +1 -1
  251. package/src/sources/gloomberb-cloud/index.ts +70 -37
  252. package/src/sources/gloomberb-cloud/news.ts +3 -3
  253. package/src/sources/history-result.ts +1 -0
  254. package/src/sources/history-retention.ts +22 -6
  255. package/src/sources/{yahoo-finance/symbols.ts → listing-symbols.ts} +16 -41
  256. package/src/sources/listing-target.ts +4 -4
  257. package/src/sources/provider-router/cache.ts +16 -6
  258. package/src/sources/provider-router/financials.ts +1 -1
  259. package/src/sources/provider-router/history.ts +7 -4
  260. package/src/state/app/bootstrap.ts +1 -1
  261. package/src/state/app/context/index.tsx +13 -0
  262. package/src/sync/core-contributors.ts +181 -18
  263. package/src/test-support/plugin-runtime.ts +1 -0
  264. package/src/tickers/instrument-kind.ts +1 -1
  265. package/src/tickers/open-target.ts +5 -5
  266. package/src/tickers/search/index.ts +46 -13
  267. package/src/tickers/search/ranking.ts +4 -4
  268. package/src/tickers/search/upsert.ts +4 -1
  269. package/src/time-series/live-bars.ts +8 -3
  270. package/src/time-series/resolution.ts +1 -1
  271. package/src/time-series/resolve.ts +225 -18
  272. package/src/time-series/spec.ts +1 -0
  273. package/src/time-series/types.ts +9 -1
  274. package/src/types/data-provider.ts +2 -0
  275. package/src/types/financials.ts +5 -5
  276. package/src/types/instrument.ts +1 -1
  277. package/src/types/plugin.ts +3 -3
  278. package/src/types/price-history.ts +3 -1
  279. package/src/ui/dialog-bridge.tsx +50 -0
  280. package/src/ui/dialog-context.ts +24 -0
  281. package/src/ui/dialog-stack.ts +47 -1
  282. package/src/ui/dialog.tsx +2 -10
  283. package/src/ui/host.tsx +6 -1
  284. package/src/ui/index.tsx +7 -2
  285. package/src/utils/exchanges.ts +1 -0
  286. package/src/utils/latest-financial-period.ts +1 -1
  287. package/src/utils/plugin-proxy-hosts.json +1 -5
  288. package/src/utils/price-history.ts +35 -1
  289. package/src/utils/sec.ts +3 -2
  290. package/src/utils/zoned-date-time.ts +67 -0
  291. package/src/app/pane-template-dialog-wizard.tsx +0 -67
  292. package/src/components/command-bar/multi-select-picker.tsx +0 -225
  293. package/src/components/command-bar/multi-select-runtime.ts +0 -91
  294. package/src/components/command-bar/workflow/body.tsx +0 -164
  295. package/src/components/command-bar/workflow/field-row.tsx +0 -231
  296. package/src/components/command-bar/workflow/route-actions.ts +0 -235
  297. package/src/components/command-bar/workflow/runtime.ts +0 -247
  298. package/src/plugins/builtin/yahoo/index.ts +0 -38
  299. package/src/sources/yahoo-finance/chart-period.ts +0 -138
  300. package/src/sources/yahoo-finance/financials.ts +0 -370
  301. package/src/sources/yahoo-finance/history.ts +0 -150
  302. package/src/sources/yahoo-finance/http.ts +0 -119
  303. package/src/sources/yahoo-finance/mappers.ts +0 -549
  304. package/src/sources/yahoo-finance/names.ts +0 -28
  305. package/src/sources/yahoo-finance/options.ts +0 -190
  306. package/src/sources/yahoo-finance/quote-summary.ts +0 -176
  307. package/src/sources/yahoo-finance/requests.ts +0 -245
  308. package/src/sources/yahoo-finance/snapshots.ts +0 -336
  309. package/src/sources/yahoo-finance/types.ts +0 -198
  310. package/src/sources/yahoo-finance/yahoo-chart-interval.ts +0 -18
  311. package/src/sources/yahoo-finance.ts +0 -339
@@ -1,6 +1,7 @@
1
1
  import { useCallback, type Dispatch } from "react";
2
2
  import { getPaneTemplateDisplayLabel } from "../../components/command-bar/pane-templates/items";
3
3
  import { createPaneTemplateOrThrow } from "../../components/command-bar/workflow/ops";
4
+ import { openFormModal } from "../../components/form-modal";
4
5
  import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
5
6
  import type { PluginRegistry } from "../../plugins/registry";
6
7
  import type { AppAction, AppState } from "../../state/app/context";
@@ -10,10 +11,7 @@ import type {
10
11
  PaneDef,
11
12
  PaneTemplateCreateOptions,
12
13
  PaneTemplateInstanceConfig,
13
- WizardStep,
14
14
  } from "../../types/plugin";
15
- import type { DialogApi } from "../../ui/dialog";
16
- import { runPaneTemplateDialogWizard } from "../pane-template-dialog-wizard";
17
15
 
18
16
  interface UseAppPaneTemplateRuntimeOptions {
19
17
  buildPaneInstance: (paneType: string, options?: {
@@ -24,7 +22,6 @@ interface UseAppPaneTemplateRuntimeOptions {
24
22
  instanceId?: string;
25
23
  }) => PaneInstanceConfig | null;
26
24
  dataProvider: DataProvider;
27
- dialog: DialogApi;
28
25
  dispatch: Dispatch<AppAction>;
29
26
  notify: (body: string, options?: { type?: "info" | "success" | "error" }) => void;
30
27
  placePaneInstance: (
@@ -40,7 +37,6 @@ interface UseAppPaneTemplateRuntimeOptions {
40
37
  export function useAppPaneTemplateRuntime({
41
38
  buildPaneInstance,
42
39
  dataProvider,
43
- dialog,
44
40
  dispatch,
45
41
  notify,
46
42
  placePaneInstance,
@@ -48,38 +44,23 @@ export function useAppPaneTemplateRuntime({
48
44
  stateRef,
49
45
  tickerRepository,
50
46
  }: UseAppPaneTemplateRuntimeOptions) {
51
- const runPaneTemplateWizard = useCallback((steps: WizardStep[]) => (
52
- runPaneTemplateDialogWizard(dialog, steps)
53
- ), [dialog]);
54
-
55
47
  const createPaneFromTemplate = useCallback(async (templateId: string, options?: PaneTemplateCreateOptions) => {
56
48
  const template = pluginRegistry.paneTemplates.get(templateId);
57
49
  if (!template) return;
58
50
 
59
- let resolvedOptions = options;
60
- const shouldRunDialogWizard = !!template.wizard
51
+ // A template that asks for settings asks in the same form the command bar
52
+ // opens, which creates the pane when it is sent.
53
+ const asksForSettings = !!template.wizard
61
54
  && template.wizard.length > 0
62
55
  && !options?.values
63
56
  && (!options?.arg || template.wizard.some((step) => step.type === "textarea"));
64
- if (shouldRunDialogWizard && template.wizard) {
65
- const wizardSteps = options?.arg && template.shortcut?.argPlaceholder
66
- ? template.wizard.map((step) => (
67
- step.key === template.shortcut?.argPlaceholder
68
- ? { ...step, defaultValue: options.arg }
69
- : step
70
- ))
71
- : template.wizard;
72
- const values = await runPaneTemplateWizard(wizardSteps);
73
- if (!values) return;
74
- resolvedOptions = {
75
- ...options,
76
- values,
77
- arg: template.shortcut?.argPlaceholder ? values[template.shortcut.argPlaceholder] : options?.arg,
78
- };
57
+ if (asksForSettings) {
58
+ openFormModal({ kind: "pane-template", templateId, arg: options?.arg, options });
59
+ return;
79
60
  }
80
61
 
81
62
  try {
82
- await createPaneTemplateOrThrow(templateId, resolvedOptions, {
63
+ await createPaneTemplateOrThrow(templateId, options, {
83
64
  dataProvider,
84
65
  tickerRepository,
85
66
  pluginRegistry,
@@ -101,7 +82,6 @@ export function useAppPaneTemplateRuntime({
101
82
  notify,
102
83
  placePaneInstance,
103
84
  pluginRegistry,
104
- runPaneTemplateWizard,
105
85
  stateRef,
106
86
  tickerRepository,
107
87
  ]);
@@ -4,6 +4,9 @@ import {
4
4
  createPaneTemplateOrThrow,
5
5
  } from "../../components/command-bar/workflow/ops";
6
6
  import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
7
+ import { openFormModal } from "../../components/form-modal";
8
+ import { t } from "../../i18n";
9
+ import { openBrokerAddFlow } from "../../plugins/builtin/broker-manager/add-request";
7
10
  import { getPanelFocusTarget } from "../../core/state/app/layout";
8
11
  import { setLayoutManagerDispatch } from "../../plugins/builtin/layout-manager";
9
12
  import { setMarketplaceHost } from "../../plugins/builtin/plugin-marketplace/store";
@@ -48,6 +51,7 @@ import type {
48
51
  import type { TickerOpenTarget } from "../../tickers/open-target";
49
52
  import { instrumentFromTicker } from "../../market-data/request-types";
50
53
  import { tickerInstrumentLabel } from "../../tickers/instrument-label";
54
+ import { isDialogOpen } from "../../ui/dialog-stack";
51
55
  import { stableStringify } from "../../utils/hash";
52
56
 
53
57
  // Registry callbacks are rebound on renders. Request ownership must survive
@@ -70,7 +74,7 @@ interface BindAppPanePluginRegistryOptions {
70
74
  externalPlugins: readonly LoadedExternalPlugin[];
71
75
  focusVisiblePane: (paneId: string, layout?: LayoutConfig) => void;
72
76
  isDetachedWindow: boolean;
73
- openPaneSettings: (paneId?: string) => Promise<void>;
77
+ openPaneSettings: (paneId?: string, options?: { fieldKey?: string }) => Promise<void>;
74
78
  openPinnedTicker: (rawSymbol: string, options?: PinTickerOptions) => Promise<void>;
75
79
  persistConfig: (nextConfig: AppState["config"]) => void;
76
80
  persistLayout: (layout: LayoutConfig, options?: { pushHistory?: boolean }) => void;
@@ -131,25 +135,43 @@ export function bindAppPanePluginRegistry({
131
135
  if (paneId) dispatch({ type: "FOCUS_PANE", paneId });
132
136
  },
133
137
  switchTab: (tabId, paneId) => switchTickerResearchTab(tabId, paneId),
138
+ // The bar would open over a dialog without its keys, so it waits for none to be open.
134
139
  openCommandBar: (query) => {
135
- if (isDetachedWindow) return;
140
+ if (isDetachedWindow || isDialogOpen()) return;
136
141
  dispatch({ type: "SET_COMMAND_BAR", open: true, query });
137
142
  },
143
+ // Forms submit through the main window's state, so a detached window says where to go.
138
144
  openPluginCommandWorkflow: (commandId) => {
139
- if (isDetachedWindow) return;
140
- dispatch({
141
- type: "SET_COMMAND_BAR",
142
- open: true,
143
- query: "",
144
- launch: { kind: "plugin-command", commandId },
145
- });
145
+ if (isDetachedWindow) {
146
+ pluginRegistry.notify({ body: t("Open this from the main window."), type: "info" });
147
+ return;
148
+ }
149
+ const command = pluginRegistry.commands.get(commandId);
150
+ if (!command?.wizard || command.wizard.length === 0) {
151
+ // Nothing to fill in: the bar opens, as it always has for such a command.
152
+ if (!isDialogOpen()) dispatch({ type: "SET_COMMAND_BAR", open: true, query: "" });
153
+ return;
154
+ }
155
+ openFormModal({ kind: "plugin-command", commandId });
156
+ },
157
+ openBuiltInWorkflow: (actionId) => {
158
+ if (isDetachedWindow) {
159
+ pluginRegistry.notify({ body: t("Open this from the main window."), type: "info" });
160
+ return;
161
+ }
162
+ // Profiles are added in the Brokers pane, not in a form.
163
+ if (actionId === "add-broker-account") {
164
+ openBrokerAddFlow(pluginRegistry);
165
+ return;
166
+ }
167
+ openFormModal({ kind: "builtin", actionId });
146
168
  },
147
169
  getLayout: () => stateRef.current.config.layout,
148
170
  updateLayout: (layout) => {
149
171
  if (isDetachedWindow) return;
150
172
  persistLayout(layout);
151
173
  },
152
- openPaneSettings: (paneId) => { void openPaneSettings(paneId); },
174
+ openPaneSettings: (paneId, options) => { void openPaneSettings(paneId, options); },
153
175
  showPane: (paneId) => {
154
176
  if (isDetachedWindow) return;
155
177
  showPane(paneId);
@@ -335,7 +357,7 @@ export function bindAppPanePluginRegistry({
335
357
  toggleable: plugin.toggleable === true,
336
358
  enabled: !disabled.has(plugin.id),
337
359
  source: external ? "external" : "builtin",
338
- ...(external?.directory ? { directory: external.directory } : {}),
360
+ ...(external?.directory ? { directory: external.directory, path: external.path } : {}),
339
361
  ...(external?.commit ? { commit: external.commit } : {}),
340
362
  ...(external?.linked ? { linked: true } : {}),
341
363
  hasSetup,
@@ -358,7 +380,7 @@ export function bindAppPanePluginRegistry({
358
380
  toggleable: true,
359
381
  enabled: !disabled.has(entry.plugin.id),
360
382
  source: "external",
361
- ...(entry.directory ? { directory: entry.directory } : {}),
383
+ ...(entry.directory ? { directory: entry.directory, path: entry.path } : {}),
362
384
  ...(entry.commit ? { commit: entry.commit } : {}),
363
385
  ...(entry.linked ? { linked: true } : {}),
364
386
  ...(entry.unsupportedTarget ? { unsupportedTarget: entry.unsupportedTarget } : {}),
@@ -23,8 +23,9 @@ import {
23
23
  type TickerOpenTarget,
24
24
  } from "../../tickers/open-target";
25
25
  import { AmbiguousTickerError, findExactTickerSearchMatch } from "../../tickers/search";
26
+ import { isDialogOpen } from "../../ui/dialog-stack";
26
27
  import { parsePublicTickerKey } from "../../utils/exchanges";
27
- import { tickerHasYahooSuffix } from "../../sources/yahoo-finance/symbols";
28
+ import { tickerHasListingSuffix } from "../../sources/listing-symbols";
28
29
  import { instrumentFromTicker } from "../../market-data/request-types";
29
30
  import { tickerInstrumentLabel } from "../../tickers/instrument-label";
30
31
 
@@ -75,7 +76,8 @@ export function useAppTickerOpenRuntime({
75
76
  return target;
76
77
  } catch (err) {
77
78
  if (!canPresentFeedback()) return null;
78
- if (err instanceof AmbiguousTickerError) {
79
+ // Ticker search offers the listings, unless a dialog would sit over the bar.
80
+ if (err instanceof AmbiguousTickerError && !isDialogOpen()) {
79
81
  dispatch({ type: "SET_COMMAND_BAR", open: true, query: rawSymbol,
80
82
  launch: { kind: "ticker-search", query: rawSymbol } });
81
83
  }
@@ -114,7 +116,7 @@ export function useAppTickerOpenRuntime({
114
116
  if (!options?.forceNewPane && !existing && instrument === null && !target.ticker.metadata.broker_contracts?.length) {
115
117
  existing = findFixedTickerPaneForSymbol(currentLayout, paneType, symbol);
116
118
  }
117
- if (!options?.forceNewPane && !existing && !instrument && (parsePublicTickerKey(symbol).exchange || tickerHasYahooSuffix(symbol))) {
119
+ if (!options?.forceNewPane && !existing && !instrument && (parsePublicTickerKey(symbol).exchange || tickerHasListingSuffix(symbol))) {
118
120
  existing = currentLayout.instances.find((instance) => {
119
121
  if (instance.paneId !== paneType || instance.binding?.kind !== "fixed" || instance.binding.instrument || !isPaneInLayout(currentLayout, instance.instanceId)) return false;
120
122
  const ticker = currentState.tickers.get(instance.binding.symbol);
@@ -87,7 +87,7 @@ export function useBrokerImportRuntime({
87
87
 
88
88
  for (const position of result.positions) {
89
89
  throwIfBrokerImportCancelled(options?.signal);
90
- // Skip Yahoo Finance for broker option symbols; position marks are already available.
90
+ // Skip market data for broker option symbols; position marks are already available.
91
91
  // Position data (markPrice, marketValue, unrealizedPnl) is used directly.
92
92
  if (options?.refreshImportedTickers !== false && position.assetCategory !== "OPT") {
93
93
  refreshQuote(position.ticker, position.exchange, undefined, 1);
@@ -4,9 +4,12 @@ import type { PluginRegistry } from "../../plugins/registry";
4
4
  import type { AppAction, AppState } from "../../state/app/context";
5
5
  import type { DesktopApplicationMenuBridge } from "../../types/desktop-menu";
6
6
  import type { DesktopWindowBridge } from "../../types/desktop-window";
7
+ import { isDialogOpen } from "../../ui/dialog-stack";
7
8
  import type { RendererHost } from "../../ui/host";
8
9
  import { recordFunctionOpen } from "../../telemetry/usage-counts";
9
10
 
11
+ const OPENS_OVER_DIALOGS = new Set<string>(["open-command-bar", "open-plugin-workflow", "open-builtin-workflow"]);
12
+
10
13
  export function useDesktopApplicationMenuRuntime({
11
14
  desktopApplicationMenuBridge,
12
15
  desktopWindowKind,
@@ -27,6 +30,11 @@ export function useDesktopApplicationMenuRuntime({
27
30
  useEffect(() => {
28
31
  if (desktopWindowKind !== "main" || !desktopApplicationMenuBridge) return;
29
32
  return desktopApplicationMenuBridge.subscribe((command) => {
33
+ // The native menu stays reachable over a dialog, accelerators included.
34
+ // A bar opened there would sit over the dialog without its keys, and Esc
35
+ // would close the dialog under it; a form would stack over one the user
36
+ // has not finished. Both wait for the dialog to close.
37
+ if (OPENS_OVER_DIALOGS.has(command.type) && isDialogOpen()) return;
30
38
  switch (command.type) {
31
39
  case "open-command-bar":
32
40
  dispatch({ type: "SET_COMMAND_BAR", open: true, query: command.query });
@@ -35,12 +43,7 @@ export function useDesktopApplicationMenuRuntime({
35
43
  pluginRegistry.openPluginCommandWorkflow(command.commandId);
36
44
  break;
37
45
  case "open-builtin-workflow":
38
- dispatch({
39
- type: "SET_COMMAND_BAR",
40
- open: true,
41
- query: "",
42
- launch: { kind: "builtin-workflow", actionId: command.actionId },
43
- });
46
+ pluginRegistry.openBuiltInWorkflow(command.actionId);
44
47
  break;
45
48
  case "open-url":
46
49
  void rendererHost.openExternal(command.url).catch((error) => {
@@ -9,6 +9,7 @@ import type { MarketDataCoordinator } from "../../market-data/coordinator";
9
9
  import { instrumentFromTicker } from "../../market-data/request-types";
10
10
  import type { PluginRegistry } from "../../plugins/registry";
11
11
  import { saveConfigImmediately } from "../../state/config-save-scheduler";
12
+ import { settleWithin } from "../../utils/async-deadline";
12
13
  import type { AppAction, AppState } from "../../state/app/context";
13
14
  import type { AppConfig, BrokerInstanceConfig } from "../../types/config";
14
15
  import type { DataProvider } from "../../types/data-provider";
@@ -18,6 +19,9 @@ import {
18
19
  getBrokerInstance,
19
20
  } from "../../utils/broker-instances";
20
21
 
22
+ /** How long removing a profile waits for its adapter to disconnect before going on without it. */
23
+ const ADAPTER_DISCONNECT_WAIT_MS = 5_000;
24
+
21
25
  export function bindPluginRegistryRuntimeAccess({
22
26
  dataProvider,
23
27
  dispatch,
@@ -186,8 +190,10 @@ export function bindPluginRegistryRuntimeAccess({
186
190
 
187
191
  clearPersistedBrokerAccounts(pluginRegistry.persistence.resources, instance);
188
192
 
193
+ // Best effort: an adapter that throws, rejects or never answers (a desktop
194
+ // call to a busy Bun process) must not keep the profile.
189
195
  const broker = pluginRegistry.brokers.get(instance.brokerType);
190
- await broker?.disconnect?.(instance).catch(() => {});
196
+ await settleWithin(Promise.resolve().then(() => broker?.disconnect?.(instance)), ADAPTER_DISCONNECT_WAIT_MS);
191
197
 
192
198
  // A portfolio another profile of the account can keep (the account switched
193
199
  // to or from sign-in) goes to that profile; only this profile's positions go.
@@ -96,7 +96,7 @@ export function useTickerRefreshRuntime({
96
96
  if (quote.currency) void marketData.loadFxRate(quote.currency).catch(() => {});
97
97
  void marketData.loadFxRate(baseCurrency).catch(() => {});
98
98
  } catch {
99
- // Silently fail - the list can fall back to stale cache or Yahoo.
99
+ // Silently fail - the list can fall back to stale cache or the backend.
100
100
  } finally {
101
101
  quoteRefreshInFlight.delete(key);
102
102
  }
package/src/app.tsx CHANGED
@@ -19,6 +19,7 @@ import { DetachedPaneShell } from "./components/layout/detached-pane-shell";
19
19
  import { TransientLayoutProvider } from "./components/layout/transient-layout";
20
20
  import { CommandBar } from "./components/command-bar/surface";
21
21
  import { OnboardingWizard } from "./components/onboarding/onboarding-wizard";
22
+ import { CompanyPickerHost } from "./plugins/builtin/cloud/company-picker";
22
23
  import { SignInGate } from "./components/sign-in-gate";
23
24
  import { useDialog } from "./ui/dialog";
24
25
  import { PluginRegistry } from "./plugins/registry";
@@ -39,6 +40,7 @@ import { useBrokerImportRuntime } from "./app/runtime/broker-import";
39
40
  import { useDesktopDeepLinkRuntime } from "./app/runtime/desktop-deeplink";
40
41
  import { useDesktopApplicationMenuRuntime } from "./app/runtime/desktop-menu";
41
42
  import { useAppGlobalShortcuts } from "./app/global-shortcuts";
43
+ import { AppDialogBridge } from "./app/dialog-bridge";
42
44
  import { KeybindingsProvider, useResolvedKeybindings } from "./app/keybindings";
43
45
  import { useAppPaneRuntime } from "./app/pane-runtime";
44
46
  import { bindPluginRegistryRuntimeAccess } from "./app/runtime/plugin-bindings";
@@ -181,6 +183,10 @@ function AppInner({
181
183
  const toast = useToastHost();
182
184
  const isDetachedWindow = desktopWindowBridge?.kind === "detached";
183
185
  const detachedPaneId = isDetachedWindow ? desktopWindowBridge.paneId ?? null : null;
186
+ const focusDetachedPane = useMemo(() => {
187
+ const focus = desktopWindowBridge?.kind === "main" ? desktopWindowBridge.focusDetachedPane : undefined;
188
+ return focus ? (paneId: string) => { void focus(paneId).catch(() => {}); } : undefined;
189
+ }, [desktopWindowBridge]);
184
190
  const [desktopDockPreview, setDesktopDockPreview] = useState<DesktopDockPreviewState | null>(null);
185
191
  const [commandBarNativeOccluder, setCommandBarNativeOccluder] = useState<LayoutBounds | null>(null);
186
192
  appActiveRef.current = appActive;
@@ -354,6 +360,7 @@ function AppInner({
354
360
  dialog,
355
361
  dispatch,
356
362
  externalPlugins,
363
+ focusDetachedPane,
357
364
  isDetachedWindow,
358
365
  notify,
359
366
  persistConfig,
@@ -409,6 +416,7 @@ function AppInner({
409
416
  if (desktopWindowBridge?.kind === "detached" && desktopWindowBridge.paneId) {
410
417
  return (
411
418
  <KeybindingsProvider value={keybindings}>
419
+ <AppDialogBridge />
412
420
  <ContextMenuProvider pluginRegistry={pluginRegistry}>
413
421
  <RemoteControlHost
414
422
  adapter={remoteControlAdapter}
@@ -432,6 +440,7 @@ function AppInner({
432
440
 
433
441
  return (
434
442
  <KeybindingsProvider value={keybindings}>
443
+ <AppDialogBridge />
435
444
  <ContextMenuProvider pluginRegistry={pluginRegistry}>
436
445
  <RemoteControlHost
437
446
  adapter={remoteControlAdapter}
@@ -448,6 +457,8 @@ function AppInner({
448
457
  <TransientLayoutProvider>
449
458
  <Shell
450
459
  pluginRegistry={pluginRegistry}
460
+ dataProvider={dataProvider}
461
+ tickerRepository={tickerRepository}
451
462
  desktopWindowBridge={desktopWindowBridge}
452
463
  desktopDockPreview={desktopDockPreview}
453
464
  commandBarNativeOccluder={commandBarNativeOccluder}
@@ -460,6 +471,7 @@ function AppInner({
460
471
  }}
461
472
  />
462
473
  </TransientLayoutProvider>
474
+ {!onboardingActive ? <CompanyPickerHost pluginRegistry={pluginRegistry} /> : null}
463
475
  {onboardingActive && onOnboardingComplete ? (
464
476
  <OnboardingWizard
465
477
  pluginRegistry={pluginRegistry}
@@ -4,6 +4,7 @@
4
4
  * adapter. One broker offered both ways is one entry with two methods, so Add
5
5
  * Broker and onboarding list "Interactive Brokers" once.
6
6
  */
7
+ import { t, tf } from "../i18n";
7
8
  import type { BrokerAdapter } from "../types/broker";
8
9
  import type { SignedInBroker } from "./signed-in/client";
9
10
  import { SIGNED_IN_BROKER_TYPE } from "./signed-in/profile";
@@ -48,9 +49,17 @@ export function buildBrokerDirectory({
48
49
  return [...entries.values()].sort((left, right) => left.name.localeCompare(right.name));
49
50
  }
50
51
 
52
+ /** How a broker connects, beside its name in a list of brokers. */
53
+ export function brokerMethodSummary(entry: BrokerDirectoryEntry): string {
54
+ const signsIn = entry.methods.some((method) => method.kind === "signed-in");
55
+ const onDevice = entry.methods.some((method) => method.kind === "device");
56
+ if (signsIn && onDevice) return t("Sign in, or on this device");
57
+ return signsIn ? t("Sign in") : t("On this device");
58
+ }
59
+
51
60
  export function brokerMethodLabel(entry: BrokerDirectoryEntry, method: BrokerMethod): string {
52
61
  if (method.kind === "signed-in") {
53
- return entry.methods.some((candidate) => candidate.kind === "device") ? "Sign in (recommended)" : "Sign in";
62
+ return entry.methods.some((candidate) => candidate.kind === "device") ? t("Sign in (recommended)") : t("Sign in");
54
63
  }
55
- return entry.methods.length > 1 ? `On this device (${method.adapter.name})` : method.adapter.name;
64
+ return entry.methods.length > 1 ? tf("On this device ({broker})", { broker: method.adapter.name }) : method.adapter.name;
56
65
  }
@@ -1,6 +1,7 @@
1
1
  import type { BrokerAdapter, BrokerConfigField } from "../types/broker";
2
2
  import type { BrokerInstanceConfig } from "../types/config";
3
3
  import { resolveBrokerConfigFields } from "../types/broker";
4
+ import { t, tf } from "../i18n";
4
5
 
5
6
  export interface BrokerProfileDraft {
6
7
  label: string;
@@ -93,7 +94,7 @@ export function validateBrokerProfileValues(
93
94
  for (const field of getVisibleBrokerConfigFields(adapter, resolvedValues)) {
94
95
  if (!field.required) continue;
95
96
  if (!normalizeFormValue(resolvedValues[field.key]).trim()) {
96
- return `${field.label} is required.`;
97
+ return tf("{label} is required.", { label: t(field.label) });
97
98
  }
98
99
  }
99
100
  return null;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Removing a broker profile, from the Brokers pane or the command bar. A
3
+ * signed-in profile's connection belongs to the Gloom account, so it is
4
+ * disconnected there too, as far as Gloom answers: the profile leaves this
5
+ * device either way, and the message says what the account kept.
6
+ */
7
+ import type { ConfirmModalOptions } from "../components/form-modal";
8
+ import { t, tf } from "../i18n";
9
+ import type { BrokerInstanceConfig } from "../types/config";
10
+ import { disconnectSignedInProfile, signedInBrokerForProfile } from "./signed-in/connect";
11
+ import { isSignedInBrokerProfile } from "./signed-in/profile";
12
+
13
+ /**
14
+ * The confirm before `onConfirm` removes `instance`. `brokerName` names the
15
+ * broker when the connector list does not.
16
+ */
17
+ export function brokerProfileRemovalConfirm(
18
+ instance: BrokerInstanceConfig,
19
+ brokerName: string,
20
+ onConfirm: () => Promise<void>,
21
+ ): ConfirmModalOptions {
22
+ const signedIn = isSignedInBrokerProfile(instance) ? signedInBrokerForProfile(instance, brokerName) : null;
23
+ return {
24
+ confirmId: "remove-broker-profile",
25
+ title: "Disconnect broker?",
26
+ body: [
27
+ tf("Remove \"{label}\" and imported broker data?", { label: instance.label }),
28
+ t("Broker-managed portfolios, positions, and contracts will be removed."),
29
+ ...(signedIn
30
+ ? [tf("This also disconnects {broker} from your other devices and agents.", { broker: signedIn.name })]
31
+ : []),
32
+ ],
33
+ confirmLabel: "Disconnect",
34
+ tone: "danger",
35
+ onConfirm,
36
+ };
37
+ }
38
+
39
+ export interface BrokerProfileRemoval {
40
+ message: string;
41
+ /** The Gloom account still holds the broker, so its other devices and agents keep it. */
42
+ accountKept: boolean;
43
+ }
44
+
45
+ export async function removeBrokerProfile(
46
+ instance: BrokerInstanceConfig,
47
+ brokerName: string,
48
+ removeBrokerInstance: (instanceId: string) => Promise<void>,
49
+ ): Promise<BrokerProfileRemoval> {
50
+ const account = await disconnectSignedInProfile(instance);
51
+ await removeBrokerInstance(instance.id);
52
+ const label = instance.label;
53
+ if (!account.stillConnected) return { message: tf("Removed {label}.", { label }), accountKept: false };
54
+ const broker = signedInBrokerForProfile(instance, brokerName).name;
55
+ return {
56
+ accountKept: true,
57
+ message: account.signedOut
58
+ ? tf("Removed {label}. {broker} is still connected to your Gloom account. Sign in to Gloom to disconnect {broker} from your account.", { label, broker })
59
+ : tf("Removed {label}. {broker} is still connected to your Gloom account.", { label, broker }),
60
+ };
61
+ }
@@ -4,6 +4,7 @@
4
4
  * connecting again syncs the profile it already has.
5
5
  */
6
6
  import { ApiRequestError } from "../../api-client/errors";
7
+ import { withDeadline } from "../../utils/async-deadline";
7
8
  import type { AppConfig, BrokerInstanceConfig } from "../../types/config";
8
9
  import { findSignedInBroker } from "./catalog";
9
10
  import { disconnectSignedInBroker, type SignedInBroker } from "./client";
@@ -31,7 +32,7 @@ export interface ConnectSignedInBrokerDeps<T> {
31
32
  getConfig(): AppConfig;
32
33
  createBrokerInstance(brokerType: string, label: string, values: Record<string, unknown>): Promise<BrokerInstanceConfig>;
33
34
  syncBrokerInstance(instanceId: string): Promise<T>;
34
- /** Opens the connect dialog; replaced in tests. */
35
+ /** Asks the user to connect: a form's own connect step, or the connect dialog when missing. */
35
36
  requestSignIn?: (broker: SignedInBroker) => Promise<boolean>;
36
37
  }
37
38
 
@@ -50,20 +51,32 @@ export async function connectSignedInBrokerProfile<T>(
50
51
  return { instance, synced: await deps.syncBrokerInstance(instance.id) };
51
52
  }
52
53
 
54
+ /** Long enough for a slow answer, short enough that removing a profile never hangs on one. */
55
+ const DISCONNECT_TIMEOUT_MS = 10_000;
56
+
57
+ /** What became of the Gloom account's connection behind a profile being removed. */
58
+ export type SignedInProfileDisconnect =
59
+ | { stillConnected: false }
60
+ /** The account keeps the broker: signed out of Gloom, or Gloom did not do it. */
61
+ | { stillConnected: true; signedOut: boolean };
62
+
53
63
  /**
54
64
  * Disconnects the Gloom account's connection behind a signed-in profile before
55
- * the profile goes. Already gone is fine; signed out of Gloom, the account
56
- * keeps the broker, which the caller reports as `stillConnected`.
65
+ * the profile goes. Already gone is fine. It never throws: the profile leaves
66
+ * this device whatever Gloom answers, and the caller says what the account kept.
57
67
  */
58
- export async function disconnectSignedInProfile(instance: BrokerInstanceConfig): Promise<{ stillConnected: boolean }> {
68
+ export async function disconnectSignedInProfile(instance: BrokerInstanceConfig): Promise<SignedInProfileDisconnect> {
59
69
  if (!isSignedInBrokerProfile(instance)) return { stillConnected: false };
60
70
  try {
61
- await disconnectSignedInBroker(signedInBrokerId(instance));
71
+ await withDeadline(
72
+ disconnectSignedInBroker(signedInBrokerId(instance)),
73
+ DISCONNECT_TIMEOUT_MS,
74
+ "Gloom did not answer the disconnect.",
75
+ );
62
76
  return { stillConnected: false };
63
77
  } catch (error) {
64
78
  if (error instanceof ApiRequestError && error.status === 404) return { stillConnected: false };
65
- if (error instanceof ApiRequestError && error.status === 401) return { stillConnected: true };
66
- throw error;
79
+ return { stillConnected: true, signedOut: error instanceof ApiRequestError && error.status === 401 };
67
80
  }
68
81
  }
69
82