gloomberb 0.9.7 → 0.9.9

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 (276) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.test.ts +3 -3
  4. package/src/api-client/index.ts +48 -6
  5. package/src/api-client/request.test.ts +63 -0
  6. package/src/api-client/request.ts +53 -5
  7. package/src/api-client/types.ts +45 -2
  8. package/src/app/pane-runtime/layout-placement.ts +0 -1
  9. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  10. package/src/app/runtime/desktop-deeplink.ts +496 -0
  11. package/src/app.tsx +34 -0
  12. package/src/brokers/account-cache.ts +2 -10
  13. package/src/brokers/broker-portfolio-sync.ts +0 -1
  14. package/src/brokers/broker-ticker-sync.ts +20 -16
  15. package/src/brokers/remote-broker-adapter.ts +3 -0
  16. package/src/capabilities/registry.test.ts +25 -0
  17. package/src/capabilities/registry.ts +22 -5
  18. package/src/cli/commands/automation.ts +1 -3
  19. package/src/cli/commands/command-utils.ts +0 -7
  20. package/src/cli/commands/overview.ts +2 -2
  21. package/src/cli/commands/plugins.ts +34 -17
  22. package/src/cli/index.ts +2 -2
  23. package/src/cli.test.ts +0 -157
  24. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  25. package/src/components/chart/bar-chart-renderer.ts +111 -55
  26. package/src/components/chart/comparison/renderer.ts +0 -1
  27. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  28. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  29. package/src/components/chart/core/index.test.ts +0 -24
  30. package/src/components/chart/core/renderer.ts +0 -4
  31. package/src/components/chart/core/resolution.test.ts +0 -17
  32. package/src/components/chart/core/resolution.ts +0 -26
  33. package/src/components/chart/core/scroll.test.ts +0 -6
  34. package/src/components/chart/core/scroll.ts +0 -22
  35. package/src/components/chart/core/viewport.test.ts +2 -23
  36. package/src/components/chart/core/viewport.ts +0 -27
  37. package/src/components/chart/market-session.ts +1 -1
  38. package/src/components/chart/native/surface/hooks.ts +10 -3
  39. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  40. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  41. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  42. package/src/components/chart/stock-chart/footer.ts +68 -13
  43. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  44. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  45. package/src/components/command-bar/helpers.test.ts +1 -80
  46. package/src/components/command-bar/helpers.ts +1 -36
  47. package/src/components/command-bar/pane-settings.ts +1 -1
  48. package/src/components/command-bar/pane-templates/items.ts +11 -3
  49. package/src/components/command-bar/panel/runtime.ts +0 -2
  50. package/src/components/command-bar/routes/root/results.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  52. package/src/components/command-bar/surface/index.test.tsx +52 -16
  53. package/src/components/command-bar/surface/index.tsx +0 -1
  54. package/src/components/command-bar/workflow/ops.ts +81 -20
  55. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  56. package/src/components/feed-data-table/stack-view.tsx +0 -1
  57. package/src/components/index.ts +1 -0
  58. package/src/components/layout/shell/index.test.tsx +2 -113
  59. package/src/components/layout/shell/index.tsx +0 -1
  60. package/src/components/layout/shell/menu.ts +0 -1
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  62. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  63. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  64. package/src/components/price-selector-dialog.tsx +3 -3
  65. package/src/components/ticker/badge/text.tsx +0 -1
  66. package/src/components/toggle-list.tsx +29 -73
  67. package/src/components/ui/button.tsx +4 -1
  68. package/src/components/ui/checkbox.tsx +76 -0
  69. package/src/components/ui/choice-dialog.tsx +15 -3
  70. package/src/components/ui/frame.tsx +1 -1
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/message-composer.tsx +4 -0
  73. package/src/components/ui/native-select.tsx +4 -2
  74. package/src/components/ui/page-stack-view.tsx +1 -1
  75. package/src/components/ui/ui.test.tsx +0 -78
  76. package/src/core/state/app/layout-reducer.ts +0 -1
  77. package/src/data/config/layout.ts +3 -3
  78. package/src/data/resource-store.ts +6 -0
  79. package/src/data/sqlite/cache.test.ts +5 -0
  80. package/src/data/ticker-store.ts +0 -7
  81. package/src/market-data/coordinator/auxiliary.ts +1 -44
  82. package/src/market-data/coordinator/entries.ts +0 -1
  83. package/src/market-data/coordinator/events.test.ts +49 -0
  84. package/src/market-data/coordinator/events.ts +25 -4
  85. package/src/market-data/coordinator/index.test.ts +223 -10
  86. package/src/market-data/coordinator/index.ts +41 -21
  87. package/src/market-data/coordinator/quotes.ts +13 -11
  88. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  89. package/src/market-data/hooks.tsx +17 -13
  90. package/src/market-data/market/freshness.ts +58 -1
  91. package/src/market-data/market/status.ts +19 -1
  92. package/src/market-data/query-store.ts +0 -4
  93. package/src/market-data/quotes/freshness.test.ts +49 -1
  94. package/src/market-data/quotes/freshness.ts +8 -0
  95. package/src/market-data/quotes/resolution.test.ts +185 -0
  96. package/src/market-data/quotes/resolution.ts +96 -10
  97. package/src/market-data/request-types.test.ts +61 -1
  98. package/src/market-data/request-types.ts +42 -8
  99. package/src/market-data/result-types.ts +0 -11
  100. package/src/market-data/selectors.ts +1 -5
  101. package/src/news/aggregator.ts +5 -1
  102. package/src/pane-settings.test.ts +0 -18
  103. package/src/plugins/builtin/account-management/footer.ts +2 -5
  104. package/src/plugins/builtin/account-management/form-components.tsx +175 -59
  105. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  106. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  107. package/src/plugins/builtin/account-management/model.ts +259 -23
  108. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  109. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  110. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  111. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  112. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  113. package/src/plugins/builtin/buildout/format.ts +2 -11
  114. package/src/plugins/builtin/chat/chat.test.tsx +127 -1
  115. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  116. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  117. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  118. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  119. package/src/plugins/builtin/chat/content/index.tsx +153 -16
  120. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  121. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  122. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  123. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  124. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  125. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  126. package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
  127. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  128. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
  132. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  133. package/src/plugins/builtin/chat/test-harness.tsx +22 -12
  134. package/src/plugins/builtin/cloud/plugin.tsx +8 -1
  135. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  136. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  137. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  138. package/src/plugins/builtin/correlation/index.tsx +4 -1
  139. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  140. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  141. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  142. package/src/plugins/builtin/earnings/index.tsx +0 -9
  143. package/src/plugins/builtin/econ/index.tsx +1 -5
  144. package/src/plugins/builtin/help/index.tsx +2 -2
  145. package/src/plugins/builtin/holders/pane.tsx +0 -5
  146. package/src/plugins/builtin/insider/index.tsx +1 -8
  147. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  148. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  149. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  150. package/src/plugins/builtin/news/index.tsx +0 -1
  151. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  152. package/src/plugins/builtin/options/view.test.tsx +1 -38
  153. package/src/plugins/builtin/plugin-groups.ts +17 -3
  154. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  155. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
  156. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  157. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  158. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  159. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  160. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  161. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  162. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  163. package/src/plugins/builtin/sec/index.tsx +0 -2
  164. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  165. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  166. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  167. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
  168. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  169. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  170. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  171. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  172. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  173. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  174. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  175. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  176. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  177. package/src/plugins/builtin/world-indices/model.ts +0 -7
  178. package/src/plugins/ibkr/account-cache.ts +1 -1
  179. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  180. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  181. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  182. package/src/plugins/ibkr/trade/utils.ts +1 -7
  183. package/src/plugins/loader.ts +2 -1
  184. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  185. package/src/plugins/registry/context.ts +15 -0
  186. package/src/plugins/registry/contributions.ts +22 -11
  187. package/src/plugins/registry/index.test.ts +44 -0
  188. package/src/plugins/registry/index.ts +75 -26
  189. package/src/remote/controller.ts +1 -7
  190. package/src/remote/json-patch.ts +15 -1
  191. package/src/remote/server.ts +16 -6
  192. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  193. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  194. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  195. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  196. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  197. package/src/renderers/electrobun/bun/index.ts +42 -0
  198. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  199. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  201. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  202. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  203. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  204. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  205. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  206. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  207. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  208. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  209. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  210. package/src/renderers/electrobun/view/main.tsx +3 -0
  211. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  212. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  213. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  214. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  215. package/src/renderers/opentui/host.tsx +11 -3
  216. package/src/renderers/opentui/input-host.tsx +5 -1
  217. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  218. package/src/sources/gloomberb-cloud/index.ts +1 -30
  219. package/src/sources/gloomberb-cloud/news.ts +0 -12
  220. package/src/sources/provider-router/batches.ts +11 -3
  221. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  222. package/src/sources/provider-router/financial-routes.ts +77 -5
  223. package/src/sources/provider-router/financials.test.ts +49 -0
  224. package/src/sources/provider-router/financials.ts +40 -3
  225. package/src/sources/provider-router/index.test.ts +314 -0
  226. package/src/sources/provider-router/primary.ts +17 -3
  227. package/src/sources/provider-router/search.ts +5 -0
  228. package/src/sources/provider-router/streaming.ts +6 -1
  229. package/src/sources/provider-router/supplemental.ts +4 -3
  230. package/src/sources/sec-edgar.test.ts +42 -0
  231. package/src/sources/sec-edgar.ts +1 -2
  232. package/src/sources/yahoo-finance/history.ts +5 -1
  233. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  234. package/src/sources/yahoo-finance/mappers.ts +2 -1
  235. package/src/sources/yahoo-finance/options.ts +59 -14
  236. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  237. package/src/sources/yahoo-finance/requests.ts +2 -1
  238. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  239. package/src/sources/yahoo-finance.test.ts +40 -21
  240. package/src/sources/yahoo-finance.ts +19 -10
  241. package/src/state/app/context/index.test.tsx +0 -81
  242. package/src/state/app/context/selector.test.tsx +6 -2
  243. package/src/state/hooks/quote-streaming.test.ts +14 -1
  244. package/src/state/hooks/quote-streaming.ts +5 -4
  245. package/src/sync/controller.test.ts +31 -0
  246. package/src/sync/controller.ts +300 -0
  247. package/src/sync/core-contributors.test.ts +251 -0
  248. package/src/sync/core-contributors.ts +400 -0
  249. package/src/sync/profile-analytics.ts +39 -0
  250. package/src/sync/react.ts +53 -0
  251. package/src/sync/types.ts +77 -0
  252. package/src/test-support/data-provider.ts +0 -1
  253. package/src/types/desktop-deeplink.ts +7 -0
  254. package/src/types/plugin.ts +3 -0
  255. package/src/ui/host.tsx +15 -1
  256. package/src/ui/index.tsx +2 -0
  257. package/src/updater.test.ts +0 -21
  258. package/src/updater.ts +20 -16
  259. package/src/utils/async-deadline.ts +26 -0
  260. package/src/utils/exchanges.test.ts +11 -1
  261. package/src/utils/exchanges.ts +5 -1
  262. package/src/utils/selection-clipboard.ts +2 -2
  263. package/src/version.ts +1 -1
  264. package/src/components/layout/pane/header.test.tsx +0 -39
  265. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  266. package/src/plugins/builtin/chat/index.tsx +0 -4
  267. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  268. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  269. package/src/plugins/builtin/help/index.test.tsx +0 -242
  270. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  271. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  272. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  273. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  274. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  275. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  276. /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "Market research and portfolio tracker for the terminal",
5
5
  "module": "src/index.tsx",
6
6
  "type": "module",
@@ -38,6 +38,11 @@ export class CloudAuthApi {
38
38
  username: typeof user.username === "string" ? user.username : null,
39
39
  emailVerified: user.emailVerified === true,
40
40
  image: typeof user.image === "string" ? user.image : null,
41
+ syncEnabled: user.syncEnabled === false ? false : true,
42
+ weeklyRoundupEnabled: user.weeklyRoundupEnabled === false ? false : true,
43
+ positionAlertsEnabled: user.positionAlertsEnabled === false ? false : true,
44
+ lastSyncAt: typeof user.lastSyncAt === "string" ? user.lastSyncAt : null,
45
+ lastRoundupEmailAt: typeof user.lastRoundupEmailAt === "string" ? user.lastRoundupEmailAt : null,
41
46
  createdAt: typeof user.createdAt === "string" ? user.createdAt : "",
42
47
  updatedAt: typeof user.updatedAt === "string" ? user.updatedAt : "",
43
48
  });
@@ -148,6 +153,12 @@ export class CloudAuthApi {
148
153
  xAccount: profile.xAccount,
149
154
  sharedPortfolioId: profile.sharedPortfolioId,
150
155
  acceptUnknownDms: profile.acceptUnknownDms,
156
+ portfolioAnalytics: profile.portfolioAnalytics,
157
+ syncEnabled: profile.syncEnabled,
158
+ weeklyRoundupEnabled: profile.weeklyRoundupEnabled,
159
+ positionAlertsEnabled: profile.positionAlertsEnabled,
160
+ lastSyncAt: profile.lastSyncAt,
161
+ lastRoundupEmailAt: profile.lastRoundupEmailAt,
151
162
  updatedAt: profile.updatedAt ?? currentUser.updatedAt,
152
163
  }));
153
164
  }
@@ -164,4 +175,13 @@ export class CloudAuthApi {
164
175
  }),
165
176
  });
166
177
  }
178
+
179
+ async deleteAccount(): Promise<void> {
180
+ await this.options.request("/account", {
181
+ method: "DELETE",
182
+ body: JSON.stringify({}),
183
+ });
184
+ this.options.setSessionToken(null);
185
+ this.options.setCurrentUser(null);
186
+ }
167
187
  }
@@ -415,7 +415,7 @@ describe("apiClient chat timestamps", () => {
415
415
  seenCreatedAts.push(message.createdAt);
416
416
  });
417
417
 
418
- await (apiClient as any).handleSocketMessage(JSON.stringify({
418
+ await (apiClient as any).socket.handleSocketMessage(JSON.stringify({
419
419
  type: "chat.message",
420
420
  channelId: "everyone",
421
421
  data: {
@@ -531,11 +531,11 @@ describe("apiClient chat timestamps", () => {
531
531
  seenNotifications.push(`${notification.id}:${notification.message.createdAt}`);
532
532
  });
533
533
 
534
- await (apiClient as any).handleSocketMessage(JSON.stringify({
534
+ await (apiClient as any).socket.handleSocketMessage(JSON.stringify({
535
535
  type: "chat.presence",
536
536
  onlineCount: 5,
537
537
  }));
538
- await (apiClient as any).handleSocketMessage(JSON.stringify({
538
+ await (apiClient as any).socket.handleSocketMessage(JSON.stringify({
539
539
  type: "chat.notification",
540
540
  data: {
541
541
  id: "n1",
@@ -39,15 +39,17 @@ import type {
39
39
  CloudCongressHousePayload,
40
40
  CloudNewsPayload,
41
41
  CloudNewsListResponse,
42
- CloudTweetPayload,
43
- CloudTweetQueryType,
44
42
  CloudTweetSearchResponse,
45
43
  CloudMarketResponse,
46
44
  CloudMarketBatchTarget,
47
45
  CloudMarketBatchPayload,
48
46
  CloudVerificationResponse,
47
+ CloudRoundupPreviewResponse,
48
+ CloudSyncPushResponse,
49
+ CloudSyncSnapshotResponse,
49
50
  QuoteStreamTarget,
50
51
  } from "./types";
52
+ import type { SyncSettings, SyncSnapshot } from "../sync/types";
51
53
 
52
54
  export type * from "./types";
53
55
  export { setCloudApiFetchTransport } from "./request";
@@ -154,10 +156,6 @@ class GloomApiClient {
154
156
  return this.transport.getSocketAuthToken();
155
157
  }
156
158
 
157
- private async handleSocketMessage(raw: string): Promise<void> {
158
- await this.socket.handleSocketMessage(raw);
159
- }
160
-
161
159
  async ensureVerifiedSession(): Promise<AuthUser | null> {
162
160
  return this.auth.ensureVerifiedSession();
163
161
  }
@@ -198,10 +196,54 @@ class GloomApiClient {
198
196
  return this.auth.updateAccountProfile(update);
199
197
  }
200
198
 
199
+ async getSyncSnapshot(): Promise<CloudSyncSnapshotResponse> {
200
+ return this.request<CloudSyncSnapshotResponse>("/sync/snapshot", { method: "GET" });
201
+ }
202
+
203
+ async putSyncSnapshot(snapshot: SyncSnapshot, options?: { baseRevision?: number | null }): Promise<CloudSyncPushResponse> {
204
+ return this.request<CloudSyncPushResponse>("/sync/snapshot", {
205
+ method: "PUT",
206
+ body: JSON.stringify({
207
+ snapshot,
208
+ baseRevision: options?.baseRevision ?? null,
209
+ }),
210
+ });
211
+ }
212
+
213
+ async updateSyncSettings(update: Partial<SyncSettings>): Promise<SyncSettings> {
214
+ const result = await this.request<{ settings: SyncSettings }>("/sync/settings", {
215
+ method: "PATCH",
216
+ body: JSON.stringify(update),
217
+ });
218
+ if (this.currentUser) {
219
+ this.currentUser = {
220
+ ...this.currentUser,
221
+ syncEnabled: result.settings.syncEnabled,
222
+ weeklyRoundupEnabled: result.settings.weeklyRoundupEnabled,
223
+ positionAlertsEnabled: result.settings.positionAlertsEnabled,
224
+ lastSyncAt: result.settings.lastSyncAt ?? this.currentUser.lastSyncAt,
225
+ lastRoundupEmailAt: result.settings.lastRoundupEmailAt ?? this.currentUser.lastRoundupEmailAt,
226
+ };
227
+ }
228
+ return result.settings;
229
+ }
230
+
231
+ async getRoundupPreview(): Promise<CloudRoundupPreviewResponse> {
232
+ return this.request<CloudRoundupPreviewResponse>("/sync/roundup/preview", { method: "POST", body: JSON.stringify({}) });
233
+ }
234
+
235
+ async sendRoundupTestEmail(): Promise<CloudRoundupPreviewResponse> {
236
+ return this.request<CloudRoundupPreviewResponse>("/sync/roundup/test-email", { method: "POST", body: JSON.stringify({}) });
237
+ }
238
+
201
239
  async changePassword(currentPassword: string, newPassword: string): Promise<void> {
202
240
  return this.auth.changePassword(currentPassword, newPassword);
203
241
  }
204
242
 
243
+ async deleteAccount(): Promise<void> {
244
+ return this.auth.deleteAccount();
245
+ }
246
+
205
247
  async getChannels(): Promise<ChatChannel[]> {
206
248
  return this.chat.getChannels();
207
249
  }
@@ -0,0 +1,63 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CloudApiRequestTransport } from "./request";
3
+
4
+ function responseWithBody(body: () => Promise<string>): Response {
5
+ return {
6
+ ok: true,
7
+ status: 200,
8
+ headers: new Headers(),
9
+ text: body,
10
+ } as Response;
11
+ }
12
+
13
+ describe("CloudApiRequestTransport market deadlines", () => {
14
+ test("aborts when response headers never arrive", async () => {
15
+ let signal: AbortSignal | null | undefined;
16
+ const transport = new CloudApiRequestTransport({
17
+ marketRequestTimeoutMs: 10,
18
+ fetchTransport: async (_url, init) => {
19
+ signal = init?.signal;
20
+ return new Promise<Response>(() => {});
21
+ },
22
+ });
23
+
24
+ await expect(transport.request("/market/quote?symbol=AAPL")).rejects.toThrow(
25
+ "Cloud market request timed out after 10ms",
26
+ );
27
+ expect(signal?.aborted).toBe(true);
28
+ });
29
+
30
+ test("keeps the deadline active while reading the response body", async () => {
31
+ let signal: AbortSignal | null | undefined;
32
+ const transport = new CloudApiRequestTransport({
33
+ marketRequestTimeoutMs: 10,
34
+ fetchTransport: async (_url, init) => {
35
+ signal = init?.signal;
36
+ return responseWithBody(async () => new Promise<string>(() => {}));
37
+ },
38
+ });
39
+
40
+ await expect(transport.request("/market/history?symbol=AAPL")).rejects.toThrow(
41
+ "Cloud market request timed out after 10ms",
42
+ );
43
+ expect(signal?.aborted).toBe(true);
44
+ });
45
+
46
+ test("preserves a caller abort instead of replacing it with the market deadline", async () => {
47
+ let fetchCalls = 0;
48
+ const controller = new AbortController();
49
+ controller.abort(new Error("cancelled by caller"));
50
+ const transport = new CloudApiRequestTransport({
51
+ marketRequestTimeoutMs: 100,
52
+ fetchTransport: async () => {
53
+ fetchCalls += 1;
54
+ return responseWithBody(async () => "{}");
55
+ },
56
+ });
57
+
58
+ await expect(transport.request("/market/quote?symbol=AAPL", {
59
+ signal: controller.signal,
60
+ })).rejects.toThrow("cancelled by caller");
61
+ expect(fetchCalls).toBe(0);
62
+ });
63
+ });
@@ -1,7 +1,9 @@
1
1
  import { httpFetch } from "../utils/http-transport";
2
+ import { withDeadline } from "../utils/async-deadline";
2
3
  import { ApiRequestError, parseApiErrorMessage } from "./errors";
3
4
 
4
5
  const DEFAULT_API_URL = "https://api.gloom.sh";
6
+ const DEFAULT_MARKET_REQUEST_TIMEOUT_MS = 10_000;
5
7
  const SESSION_COOKIE_NAMES = ["__Secure-gloomberb.session_token", "gloomberb.session_token"] as const;
6
8
 
7
9
  type CloudApiResponse = Pick<Response, "ok" | "status" | "headers" | "text">;
@@ -21,13 +23,28 @@ function getCloudApiBaseUrl(): string {
21
23
  return process.env.GLOOMBERB_API_URL ?? DEFAULT_API_URL;
22
24
  }
23
25
 
26
+ function throwIfRequestAborted(signal: AbortSignal | null | undefined): void {
27
+ if (!signal?.aborted) return;
28
+ throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
29
+ }
30
+
24
31
  export class CloudApiRequestTransport {
25
32
  private sessionToken: string | null = null;
26
33
  private sessionCookieName: SessionCookieName | null = null;
27
34
  private websocketToken: string | null = null;
35
+ private readonly fetchTransport: CloudApiFetchTransport | null;
36
+ private readonly marketRequestTimeoutMs: number;
28
37
 
29
38
  readonly baseUrl = getCloudApiBaseUrl();
30
39
 
40
+ constructor(options: {
41
+ fetchTransport?: CloudApiFetchTransport;
42
+ marketRequestTimeoutMs?: number;
43
+ } = {}) {
44
+ this.fetchTransport = options.fetchTransport ?? null;
45
+ this.marketRequestTimeoutMs = options.marketRequestTimeoutMs ?? DEFAULT_MARKET_REQUEST_TIMEOUT_MS;
46
+ }
47
+
31
48
  getSessionToken(): string | null {
32
49
  return this.sessionToken;
33
50
  }
@@ -61,6 +78,37 @@ export class CloudApiRequestTransport {
61
78
  }
62
79
 
63
80
  async request<T>(path: string, options?: RequestInit): Promise<T> {
81
+ if (!path.startsWith("/market/")) {
82
+ return this.performRequest<T>(path, options);
83
+ }
84
+
85
+ const controller = new AbortController();
86
+ const callerSignal = options?.signal;
87
+ const abortFromCaller = () => controller.abort(callerSignal?.reason);
88
+ if (callerSignal?.aborted) {
89
+ abortFromCaller();
90
+ } else {
91
+ callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
92
+ }
93
+
94
+ try {
95
+ const request = this.performRequest<T>(path, {
96
+ ...options,
97
+ signal: controller.signal,
98
+ });
99
+ return await withDeadline(
100
+ request,
101
+ this.marketRequestTimeoutMs,
102
+ `Cloud market request timed out after ${this.marketRequestTimeoutMs}ms: ${path}`,
103
+ (error) => controller.abort(error),
104
+ );
105
+ } finally {
106
+ callerSignal?.removeEventListener("abort", abortFromCaller);
107
+ }
108
+ }
109
+
110
+ private async performRequest<T>(path: string, options?: RequestInit): Promise<T> {
111
+ throwIfRequestAborted(options?.signal);
64
112
  const headers = new Headers(options?.headers);
65
113
  if (!headers.has("Content-Type") && options?.method && options.method !== "GET") {
66
114
  headers.set("Content-Type", "application/json");
@@ -68,21 +116,21 @@ export class CloudApiRequestTransport {
68
116
  this.setSessionCookieHeader(headers);
69
117
  headers.set("Origin", this.baseUrl);
70
118
 
71
- const res = await cloudApiFetchTransport(`${this.baseUrl}${path}`, {
119
+ const res = await (this.fetchTransport ?? cloudApiFetchTransport)(`${this.baseUrl}${path}`, {
72
120
  ...options,
73
121
  headers,
74
122
  credentials: "include",
75
123
  });
76
-
124
+ throwIfRequestAborted(options?.signal);
77
125
  this.extractSessionCookie(res);
126
+ const text = await res.text();
127
+ throwIfRequestAborted(options?.signal);
78
128
 
79
129
  if (!res.ok) {
80
- const body = await res.text();
81
- const msg = parseApiErrorMessage(body);
130
+ const msg = parseApiErrorMessage(text);
82
131
  throw new ApiRequestError(msg, res.status);
83
132
  }
84
133
 
85
- const text = await res.text();
86
134
  if (!text) return undefined as T;
87
135
  const parsed = JSON.parse(text) as T & { token?: string };
88
136
  if (typeof parsed?.token === "string" && parsed.token.length > 0) {
@@ -6,10 +6,9 @@ import type {
6
6
  HolderData,
7
7
  HolderRecord,
8
8
  OptionsChain,
9
- PricePoint,
10
9
  Quote,
11
- TickerFinancials,
12
10
  } from "../types/financials";
11
+ import type { SyncSettings, SyncSnapshot } from "../sync/types";
13
12
 
14
13
  export interface ChatUserSummary {
15
14
  id: string;
@@ -20,6 +19,7 @@ export interface ChatUserSummary {
20
19
  title?: string | null;
21
20
  profilePublic?: boolean;
22
21
  acceptUnknownDms?: boolean;
22
+ portfolioAnalytics?: PublicPortfolioAnalytics | null;
23
23
  }
24
24
 
25
25
  export interface ChatMessage {
@@ -83,6 +83,12 @@ export interface AuthUser {
83
83
  xAccount?: string | null;
84
84
  sharedPortfolioId?: string | null;
85
85
  acceptUnknownDms?: boolean;
86
+ portfolioAnalytics?: PublicPortfolioAnalytics | null;
87
+ syncEnabled?: boolean;
88
+ weeklyRoundupEnabled?: boolean;
89
+ positionAlertsEnabled?: boolean;
90
+ lastSyncAt?: string | null;
91
+ lastRoundupEmailAt?: string | null;
86
92
  createdAt: string;
87
93
  updatedAt: string;
88
94
  }
@@ -104,9 +110,20 @@ export interface AccountProfile {
104
110
  xAccount: string | null;
105
111
  sharedPortfolioId: string | null;
106
112
  acceptUnknownDms: boolean;
113
+ portfolioAnalytics?: PublicPortfolioAnalytics | null;
114
+ syncEnabled: boolean;
115
+ weeklyRoundupEnabled: boolean;
116
+ positionAlertsEnabled: boolean;
117
+ lastSyncAt: string | null;
118
+ lastRoundupEmailAt: string | null;
107
119
  updatedAt: string | null;
108
120
  }
109
121
 
122
+ export interface PublicPortfolioAnalytics {
123
+ oneYearReturn?: number | null;
124
+ spyBeta?: number | null;
125
+ }
126
+
110
127
  export interface BuildoutAccountResponse {
111
128
  user: {
112
129
  id: string;
@@ -151,8 +168,34 @@ export type AccountProfileUpdate = Partial<{
151
168
  xAccount: string | null;
152
169
  sharedPortfolioId: string | null;
153
170
  acceptUnknownDms: boolean;
171
+ portfolioAnalytics: PublicPortfolioAnalytics | null;
172
+ syncEnabled: boolean;
173
+ weeklyRoundupEnabled: boolean;
174
+ positionAlertsEnabled: boolean;
154
175
  }>;
155
176
 
177
+ export interface CloudSyncSnapshotResponse {
178
+ snapshot: SyncSnapshot | null;
179
+ revision: number | null;
180
+ updatedAt: string | null;
181
+ settings: SyncSettings;
182
+ }
183
+
184
+ export interface CloudSyncPushResponse {
185
+ revision: number;
186
+ updatedAt: string;
187
+ settings: SyncSettings;
188
+ }
189
+
190
+ export interface CloudRoundupPreviewResponse {
191
+ subject: string;
192
+ text: string;
193
+ html: string;
194
+ sender: string;
195
+ replyTo: string;
196
+ recipient: string;
197
+ }
198
+
156
199
  export interface CloudQuotePayload extends Quote {
157
200
  providerId: "gloomberb-cloud";
158
201
  dataSource: "live" | "delayed";
@@ -1,6 +1,5 @@
1
1
  import type { PluginRegistry } from "../../plugins/registry";
2
2
  import {
3
- findDockLeaf,
4
3
  getDockLeafLayouts,
5
4
  getLeafRect,
6
5
  } from "../../plugins/pane-manager";
@@ -0,0 +1,87 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { resolveDesktopDeepLinkAction } from "./desktop-deeplink";
3
+
4
+ describe("desktop deeplinks", () => {
5
+ test("routes cloud roundup links to account management", () => {
6
+ expect(resolveDesktopDeepLinkAction("gloomberb://cloud/roundup?week=2026-07-03")).toEqual({
7
+ type: "open-account-management",
8
+ route: { kind: "cloud-roundup", week: "2026-07-03" },
9
+ message: "Opened weekly roundup settings for 2026-07-03.",
10
+ });
11
+ });
12
+
13
+ test("routes cloud alert links to account management", () => {
14
+ expect(resolveDesktopDeepLinkAction("gloomberb://cloud/alerts")).toEqual({
15
+ type: "open-account-management",
16
+ route: { kind: "cloud-alerts", week: null },
17
+ message: "Opened portfolio alert settings.",
18
+ });
19
+ });
20
+
21
+ test("routes ticker links with arbitrary registered tab ids", () => {
22
+ expect(resolveDesktopDeepLinkAction("gloomberb://ticker/NVDA?tab=analyst-research")).toEqual({
23
+ type: "open-ticker",
24
+ symbol: "NVDA",
25
+ tabId: "analyst-research",
26
+ message: "Opened NVDA analyst-research tab.",
27
+ });
28
+ });
29
+
30
+ test("routes portfolio and watchlist links", () => {
31
+ expect(resolveDesktopDeepLinkAction("gloomberb://portfolio/main")).toEqual({
32
+ type: "open-collection",
33
+ kind: "portfolio",
34
+ collectionId: "main",
35
+ message: "Opened portfolio main.",
36
+ });
37
+ expect(resolveDesktopDeepLinkAction("gloomberb://watchlist/favorites")).toEqual({
38
+ type: "open-collection",
39
+ kind: "watchlist",
40
+ collectionId: "favorites",
41
+ message: "Opened watchlist favorites.",
42
+ });
43
+ });
44
+
45
+ test("routes alert links with structured values", () => {
46
+ expect(resolveDesktopDeepLinkAction("gloomberb://alert/new?symbol=nvda&side=above&price=$200")).toEqual({
47
+ type: "create-alert",
48
+ values: { symbol: "NVDA", condition: "above", price: "200" },
49
+ message: "Created NVDA above 200 alert.",
50
+ });
51
+ });
52
+
53
+ test("routes chat links", () => {
54
+ expect(resolveDesktopDeepLinkAction("gloomberb://chat/channel/everyone")).toEqual({
55
+ type: "open-chat-channel",
56
+ channelId: "everyone",
57
+ message: "Opened chat everyone.",
58
+ });
59
+ expect(resolveDesktopDeepLinkAction("gloomberb://chat/dm?users=@vince,@alex")).toEqual({
60
+ type: "open-chat-dm",
61
+ participants: "@vince,@alex",
62
+ message: "Opened DM.",
63
+ });
64
+ });
65
+
66
+ test("routes news links", () => {
67
+ expect(resolveDesktopDeepLinkAction("gloomberb://news?ticker=7203.T")).toEqual({
68
+ type: "open-news",
69
+ kind: "ticker",
70
+ symbol: "7203.T",
71
+ message: "Opened 7203.T news.",
72
+ });
73
+ expect(resolveDesktopDeepLinkAction("gloomberb://news/breaking")).toEqual({
74
+ type: "open-news",
75
+ kind: "breaking",
76
+ symbol: null,
77
+ message: "Opened breaking news.",
78
+ });
79
+ });
80
+
81
+ test("rejects command-bar, non-gloomberb, and malformed links", () => {
82
+ expect(resolveDesktopDeepLinkAction("gloomberb://command?query=profile").type).toBe("unsupported");
83
+ expect(resolveDesktopDeepLinkAction("gloomberb://search/NVDA").type).toBe("unsupported");
84
+ expect(resolveDesktopDeepLinkAction("https://gloom.sh/cloud").type).toBe("unsupported");
85
+ expect(resolveDesktopDeepLinkAction("not a url").type).toBe("unsupported");
86
+ });
87
+ });