gloomberb 0.17.5 → 0.17.6

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 (302) hide show
  1. package/package.json +3 -1
  2. package/src/api-client/chat.ts +9 -0
  3. package/src/api-client/commodity-balances.ts +77 -0
  4. package/src/api-client/data.ts +31 -2
  5. package/src/api-client/earnings.ts +10 -0
  6. package/src/api-client/index.ts +5 -0
  7. package/src/api-client/insider-signals.ts +75 -0
  8. package/src/api-client/market-discovery.ts +1 -1
  9. package/src/api-client/normalizers.ts +1 -0
  10. package/src/api-client/paths.ts +9 -0
  11. package/src/api-client/socket.ts +10 -0
  12. package/src/api-client/types.ts +25 -1
  13. package/src/app/runtime/broker-import.ts +23 -21
  14. package/src/app/runtime/plugin-bindings.ts +13 -7
  15. package/src/app/runtime/update.ts +14 -1
  16. package/src/architecture/core-builtin-allowlist.json +2 -10
  17. package/src/brokers/broker-portfolio-sync.ts +66 -0
  18. package/src/brokers/sync-broker-instance.ts +75 -41
  19. package/src/cli/commands/market.ts +101 -34
  20. package/src/cli/commands/options-chain.ts +20 -6
  21. package/src/cli/commands/overview.ts +52 -11
  22. package/src/cli/commands/search.ts +1 -1
  23. package/src/cli/commands/ticker.ts +24 -4
  24. package/src/cli/context.ts +13 -0
  25. package/src/cli/errors.ts +15 -2
  26. package/src/cli/fx-pair.ts +77 -0
  27. package/src/cli/history-rows.ts +25 -11
  28. package/src/cli/index.ts +24 -12
  29. package/src/cli/listing-arg.ts +61 -138
  30. package/src/cli/listing-suggestion.ts +3 -21
  31. package/src/cli/pane-functions/catalog.ts +50 -0
  32. package/src/cli/pane-functions/freshness.ts +44 -9
  33. package/src/cli/pane-functions/function-help.ts +8 -8
  34. package/src/cli/pane-functions/headless.ts +19 -7
  35. package/src/cli/pane-functions/index.ts +63 -5
  36. package/src/cli/pane-functions/resolver.ts +29 -8
  37. package/src/cli/pane-functions/screenshot.ts +2 -0
  38. package/src/cli/report-tables.ts +11 -4
  39. package/src/cli/result.ts +2 -0
  40. package/src/cli/series-search.ts +6 -1
  41. package/src/cli/ticker-resolution.ts +1 -2
  42. package/src/cli/unknown-command.ts +18 -0
  43. package/src/components/chart/composite/composite-chart.tsx +80 -19
  44. package/src/components/chart/composite/format.ts +10 -0
  45. package/src/components/chart/composite/legend.tsx +18 -4
  46. package/src/components/chart/composite/panel-labels.ts +38 -0
  47. package/src/components/chart/composite/panel-series.ts +1 -0
  48. package/src/components/chart/composite/panel-surface.tsx +25 -7
  49. package/src/components/chart/composite/price-axis-labels.tsx +16 -3
  50. package/src/components/chart/composite/scene.ts +60 -2
  51. package/src/components/chart/composite/series-axis-tags.tsx +67 -0
  52. package/src/components/chart/composite/tools.ts +2 -2
  53. package/src/components/chart/composite/types.ts +28 -0
  54. package/src/components/chart/core/view-reset.ts +46 -0
  55. package/src/components/command-bar/commands/collection.ts +1 -1
  56. package/src/components/command-bar/commands/direct/index.ts +1 -1
  57. package/src/components/command-bar/list/model.ts +7 -0
  58. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  59. package/src/components/command-bar/routes/root/command-items.ts +1 -1
  60. package/src/components/command-bar/routes/root/pick-prefetch.ts +38 -0
  61. package/src/components/command-bar/routes/root/provider-search.ts +6 -0
  62. package/src/components/command-bar/routes/root/runtime.ts +3 -0
  63. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -1
  64. package/src/components/command-bar/routes/root/shortcut-items.ts +1 -1
  65. package/src/components/command-bar/routes/ticker-search/actions.ts +34 -0
  66. package/src/components/command-bar/surface/index.tsx +5 -0
  67. package/src/components/command-bar/workflow/collection-actions.ts +6 -5
  68. package/src/components/command-bar/workflow/ops.ts +4 -2
  69. package/src/components/command-bar/workflow/pane-reuse.ts +3 -2
  70. package/src/components/command-bar/workflow/tickers.ts +1 -1
  71. package/src/components/layout/pane/content.tsx +11 -8
  72. package/src/components/layout/pane/ticker-fate-gate.tsx +56 -0
  73. package/src/components/onboarding/wizard-model.ts +1 -1
  74. package/src/components/onboarding/wizard-positions.ts +18 -10
  75. package/src/components/ticker/badge/format.ts +64 -15
  76. package/src/components/ticker/badge/index.tsx +32 -11
  77. package/src/components/ticker/badge/list.tsx +4 -3
  78. package/src/components/ticker/badge/text.tsx +2 -2
  79. package/src/components/ticker/list-table-view.tsx +8 -1
  80. package/src/components/ui/data-table/opentui/index.tsx +61 -20
  81. package/src/components/ui/data-table/opentui/model.ts +104 -0
  82. package/src/components/ui/inline-quick-add.tsx +4 -1
  83. package/src/components/ui/query-bar-keys.tsx +29 -5
  84. package/src/components/ui/query-bar.tsx +29 -4
  85. package/src/components/ui/windowed-list.tsx +27 -0
  86. package/src/docs/snapshot.json +248 -5
  87. package/src/i18n/es.ts +1 -1
  88. package/src/i18n/ja.ts +1 -1
  89. package/src/i18n/ko.ts +1 -1
  90. package/src/i18n/zh-cn.ts +1 -1
  91. package/src/i18n/zh-tw.ts +1 -1
  92. package/src/layout-marketplace/cloud.ts +16 -5
  93. package/src/layout-marketplace/gallery.tsx +4 -4
  94. package/src/layout-marketplace/linked-sync.ts +9 -3
  95. package/src/layout-marketplace/linked.ts +24 -3
  96. package/src/layout-marketplace/model.ts +39 -4
  97. package/src/market-data/coordinator/index.ts +34 -1
  98. package/src/market-data/futures-contract.ts +53 -0
  99. package/src/market-data/hooks.tsx +57 -1
  100. package/src/market-data/market/bar-period.ts +71 -0
  101. package/src/market-data/market/format.ts +14 -2
  102. package/src/market-data/market/freshness.ts +28 -1
  103. package/src/market-data/market/status.ts +152 -13
  104. package/src/market-data/model-dividend-yield.ts +76 -0
  105. package/src/market-data/quotes/freshness.ts +30 -1
  106. package/src/{plugins/builtin/portfolio-list → market-data/quotes}/live-valuation.ts +6 -6
  107. package/src/market-data/quotes/regular-session-valuation.ts +32 -1
  108. package/src/plugins/builtin/analytics/metrics.ts +8 -4
  109. package/src/plugins/builtin/analytics/risk-headless.ts +12 -5
  110. package/src/plugins/builtin/analytics/risk-model.ts +74 -15
  111. package/src/plugins/builtin/analytics/risk-pane.tsx +6 -6
  112. package/src/plugins/builtin/builtin-plugin-meta.ts +0 -1
  113. package/src/plugins/builtin/chart-composer/editor/controller.ts +14 -10
  114. package/src/plugins/builtin/chart-composer/formula.ts +233 -0
  115. package/src/plugins/builtin/chart-composer/headless.ts +25 -1
  116. package/src/plugins/builtin/chart-composer/index.tsx +3 -2
  117. package/src/plugins/builtin/chart-composer/pane.tsx +21 -8
  118. package/src/plugins/builtin/chart-composer/presets.ts +91 -2
  119. package/src/plugins/builtin/chart-composer/quick-add.tsx +9 -2
  120. package/src/plugins/builtin/chart-composer/series-catalog.ts +44 -11
  121. package/src/plugins/builtin/chart-composer/series-expression.ts +1 -1
  122. package/src/plugins/builtin/chart-composer/settings.ts +16 -4
  123. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  124. package/src/plugins/builtin/chat/content/channel-navigation.ts +17 -6
  125. package/src/plugins/builtin/chat/content/index.tsx +12 -5
  126. package/src/plugins/builtin/chat/content/mentions.ts +50 -21
  127. package/src/plugins/builtin/chat/content/scroll.ts +12 -5
  128. package/src/plugins/builtin/chat/content/snapshot.ts +11 -0
  129. package/src/plugins/builtin/chat/content/transcript.tsx +48 -37
  130. package/src/plugins/builtin/chat/content/whole-message-copy.ts +44 -2
  131. package/src/plugins/builtin/chat/controller/index.ts +68 -17
  132. package/src/plugins/builtin/chat/controller/mention-candidates.ts +104 -0
  133. package/src/plugins/builtin/chat/controller/realtime.ts +9 -0
  134. package/src/plugins/builtin/chat/controller/state.ts +10 -0
  135. package/src/plugins/builtin/chat/controller/view.ts +2 -0
  136. package/src/plugins/builtin/chat/conversation-recency.ts +73 -0
  137. package/src/plugins/builtin/chat/conversations.ts +12 -5
  138. package/src/plugins/builtin/chat/layout.ts +23 -5
  139. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  140. package/src/plugins/builtin/chat/sidebar-rows.ts +18 -5
  141. package/src/plugins/builtin/chat/sidebar.tsx +6 -1
  142. package/src/plugins/builtin/cloud/askg/manifest.ts +4 -3
  143. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +16 -5
  144. package/src/plugins/builtin/commodities-energy/index.tsx +23 -4
  145. package/src/plugins/builtin/commodity-balances/client.ts +51 -193
  146. package/src/plugins/builtin/commodity-balances/headless.ts +10 -3
  147. package/src/plugins/builtin/commodity-balances/model.ts +5 -180
  148. package/src/plugins/builtin/commodity-balances/pane.tsx +3 -0
  149. package/src/plugins/builtin/custom-view/index.tsx +1 -1
  150. package/src/plugins/builtin/custom-view/loader.ts +57 -7
  151. package/src/plugins/builtin/custom-view/sources.ts +146 -0
  152. package/src/plugins/builtin/custom-view/view-spec.ts +37 -1
  153. package/src/plugins/builtin/earnings/board-model.ts +33 -3
  154. package/src/plugins/builtin/earnings/board-table.ts +59 -14
  155. package/src/plugins/builtin/earnings/board.tsx +36 -3
  156. package/src/plugins/builtin/earnings/headless.ts +24 -5
  157. package/src/plugins/builtin/earnings/history-model.ts +9 -1
  158. package/src/plugins/builtin/earnings/history-table.ts +16 -5
  159. package/src/plugins/builtin/earnings/history.tsx +40 -10
  160. package/src/plugins/builtin/earnings/index.tsx +3 -1
  161. package/src/plugins/builtin/earnings/no-earnings.ts +52 -0
  162. package/src/plugins/builtin/econ-statistics/detail.tsx +4 -1
  163. package/src/plugins/builtin/econ-statistics/headless.ts +5 -0
  164. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  165. package/src/plugins/builtin/econ-statistics/view.ts +4 -0
  166. package/src/plugins/builtin/futures/model.ts +5 -4
  167. package/src/plugins/builtin/futures/series.ts +38 -1
  168. package/src/plugins/builtin/futures-curve/client.ts +4 -4
  169. package/src/plugins/builtin/futures-curve/headless.ts +20 -2
  170. package/src/plugins/builtin/futures-curve/index.tsx +2 -2
  171. package/src/plugins/builtin/futures-curve/model.ts +45 -2
  172. package/src/plugins/builtin/futures-curve/pane.tsx +14 -10
  173. package/src/plugins/builtin/insider/headless.ts +74 -4
  174. package/src/plugins/builtin/insider/index.tsx +99 -5
  175. package/src/plugins/builtin/insider/signals-client.ts +69 -0
  176. package/src/plugins/builtin/insider/signals-model.ts +146 -0
  177. package/src/plugins/builtin/insider/signals.tsx +176 -0
  178. package/src/plugins/builtin/iv-history/client.ts +41 -2
  179. package/src/plugins/builtin/iv-history/headless.ts +24 -16
  180. package/src/plugins/builtin/iv-history/screen-pane.tsx +25 -7
  181. package/src/plugins/builtin/jodi/client.ts +42 -205
  182. package/src/plugins/builtin/jodi/headless.ts +13 -4
  183. package/src/plugins/builtin/jodi/model.ts +18 -43
  184. package/src/plugins/builtin/jodi/pane.tsx +5 -0
  185. package/src/plugins/builtin/market-movers/client.ts +32 -6
  186. package/src/plugins/builtin/market-movers/headless.ts +7 -2
  187. package/src/plugins/builtin/market-movers/model.ts +64 -3
  188. package/src/plugins/builtin/market-movers/screener.ts +20 -9
  189. package/src/plugins/builtin/market-valuation/detail.tsx +7 -2
  190. package/src/plugins/builtin/market-valuation/headless.ts +17 -4
  191. package/src/plugins/builtin/market-valuation/pane.tsx +7 -3
  192. package/src/plugins/builtin/market-valuation/view.ts +10 -0
  193. package/src/plugins/builtin/options-positioning/client.ts +10 -0
  194. package/src/plugins/builtin/options-positioning/headless.ts +88 -13
  195. package/src/plugins/builtin/options-positioning/index.tsx +2 -2
  196. package/src/plugins/builtin/options-positioning/model.ts +33 -0
  197. package/src/plugins/builtin/options-positioning/pane.tsx +38 -16
  198. package/src/plugins/builtin/options-scenario/charts.tsx +13 -4
  199. package/src/plugins/builtin/options-scenario/client.ts +111 -17
  200. package/src/plugins/builtin/options-scenario/collar.ts +141 -0
  201. package/src/plugins/builtin/options-scenario/csp.ts +1 -1
  202. package/src/plugins/builtin/options-scenario/freshness.ts +27 -0
  203. package/src/plugins/builtin/options-scenario/headless.ts +28 -12
  204. package/src/plugins/builtin/options-scenario/index.tsx +1 -1
  205. package/src/plugins/builtin/options-scenario/model.ts +55 -5
  206. package/src/plugins/builtin/options-scenario/pane.tsx +24 -5
  207. package/src/plugins/builtin/portfolio-list/allocation.ts +9 -4
  208. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +39 -20
  209. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  210. package/src/plugins/builtin/portfolio-list/cli/shared.ts +15 -7
  211. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +1 -1
  212. package/src/plugins/builtin/portfolio-list/column-values.ts +4 -3
  213. package/src/plugins/builtin/portfolio-list/command-bar.ts +1 -1
  214. package/src/plugins/builtin/portfolio-list/headless.ts +10 -3
  215. package/src/plugins/builtin/portfolio-list/pane/index.tsx +36 -4
  216. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  217. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +35 -6
  218. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  219. package/src/plugins/builtin/portfolio-list/settings.ts +22 -0
  220. package/src/plugins/builtin/portfolio-list/since-last-seen.ts +197 -0
  221. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  222. package/src/plugins/builtin/portfolio-list/table.tsx +14 -2
  223. package/src/plugins/builtin/research/corporate-actions-pane.tsx +14 -5
  224. package/src/plugins/builtin/research/event-model.ts +41 -49
  225. package/src/plugins/builtin/research/headless.ts +1 -1
  226. package/src/plugins/builtin/reverse-dcf/headless.ts +25 -9
  227. package/src/plugins/builtin/reverse-dcf/index.tsx +4 -1
  228. package/src/plugins/builtin/reverse-dcf/model.ts +39 -5
  229. package/src/plugins/builtin/reverse-dcf/pane.tsx +16 -6
  230. package/src/plugins/builtin/shared/earnings-report.ts +48 -1
  231. package/src/plugins/builtin/shared/history-span.ts +55 -0
  232. package/src/plugins/builtin/shared/report-freshness.ts +20 -11
  233. package/src/plugins/builtin/shared/series-detail-chart.tsx +5 -2
  234. package/src/plugins/builtin/shared/series-list-detail.tsx +7 -3
  235. package/src/plugins/builtin/shared/ticker-surface.ts +16 -2
  236. package/src/plugins/builtin/shared/use-quote-board.ts +12 -5
  237. package/src/plugins/builtin/themes/headless.ts +15 -2
  238. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +85 -17
  239. package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +92 -0
  240. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +26 -8
  241. package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -2
  242. package/src/plugins/builtin/ticker-detail/headless.ts +95 -22
  243. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  244. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  245. package/src/plugins/builtin/world-venue-map/headless.ts +17 -3
  246. package/src/{plugins/builtin/portfolio-list → portfolio}/mutations.ts +6 -6
  247. package/src/portfolio/valuation-basis.ts +4 -21
  248. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  249. package/src/remote/controller.ts +27 -7
  250. package/src/remote/schema.ts +25 -9
  251. package/src/renderers/cloudflare/http-proxy.ts +21 -13
  252. package/src/renderers/dom/desktop/query-bar.tsx +1 -1
  253. package/src/renderers/dom/dom-ui-host.tsx +2 -0
  254. package/src/renderers/dom/windowed-list.tsx +201 -0
  255. package/src/renderers/dom/windowed-rows.ts +65 -0
  256. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -9
  257. package/src/renderers/electrobun/bun/desktop/http-stream.ts +21 -6
  258. package/src/renderers/electrobun/bun/desktop/update.ts +21 -6
  259. package/src/renderers/electrobun/shared/protocol.ts +4 -2
  260. package/src/renderers/electrobun/view/http-fetch.ts +12 -1
  261. package/src/sources/listing-symbols.ts +8 -4
  262. package/src/state/app/context/index.tsx +13 -0
  263. package/src/state/app/tab-cursor.ts +85 -0
  264. package/src/sync/core-contributors.ts +206 -59
  265. package/src/test-support/config-store.ts +31 -0
  266. package/src/test-support/moved-symbols.ts +64 -0
  267. package/src/theme/colors.ts +30 -0
  268. package/src/tickers/listing-fate.ts +242 -0
  269. package/src/tickers/search/index.ts +45 -3
  270. package/src/tickers/search/ranking.ts +19 -2
  271. package/src/tickers/search/types.ts +3 -0
  272. package/src/tickers/search/upsert.ts +22 -7
  273. package/src/tickers/ticker-changes.ts +26 -9
  274. package/src/time-series/bar-spacing.ts +19 -0
  275. package/src/time-series/chart-data.ts +37 -13
  276. package/src/time-series/field-catalog.ts +5 -2
  277. package/src/time-series/forward-valuation.ts +10 -7
  278. package/src/time-series/fundamentals.ts +49 -11
  279. package/src/time-series/hooks.ts +2 -0
  280. package/src/time-series/live-bars.ts +3 -1
  281. package/src/time-series/parsed-history-cache.ts +2 -0
  282. package/src/time-series/price-units.ts +69 -0
  283. package/src/time-series/resolve.ts +50 -5
  284. package/src/time-series/session-bar.ts +138 -0
  285. package/src/time-series/spec.ts +12 -3
  286. package/src/time-series/studies.ts +211 -12
  287. package/src/time-series/transforms.ts +2 -0
  288. package/src/time-series/types.ts +11 -1
  289. package/src/time-series/valuation-price.ts +13 -0
  290. package/src/types/config.ts +10 -1
  291. package/src/types/financials.ts +2 -0
  292. package/src/types/headless.ts +27 -0
  293. package/src/types/plugin.ts +7 -0
  294. package/src/ui/host.tsx +5 -0
  295. package/src/updater/index.ts +3 -1
  296. package/src/utils/asset-category.ts +20 -0
  297. package/src/utils/decimal-comma.ts +83 -3
  298. package/src/utils/fundamentals.ts +7 -2
  299. package/src/utils/http-proxy-response.ts +66 -9
  300. package/src/utils/plugin-proxy-hosts.json +1 -5
  301. package/src/utils/typo-distance.ts +22 -0
  302. package/src/plugins/builtin/shared/zip-text.ts +0 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.17.5",
3
+ "version": "0.17.6",
4
4
  "packageManager": "bun@1.3.11",
5
5
  "description": "Market research and portfolio tracker for the terminal",
6
6
  "module": "src/public/index.ts",
@@ -83,6 +83,7 @@
83
83
  "benchmark:tui:compare": "bun run scripts/benchmark-tui-compare.ts",
84
84
  "benchmark:tui:memory": "bun run scripts/benchmark-tui-memory.ts",
85
85
  "check:pane-chrome": "bun run scripts/pane-chrome/checks.ts",
86
+ "check:chat-mentions": "bun scripts/pane-chrome/chat-mentions.ts",
86
87
  "check:pane-screenshot": "bun run scripts/pane-chrome/screenshot.ts",
87
88
  "check:pane-rest": "bun run scripts/pane-chrome/rest-diff.ts",
88
89
  "check:pane-layout": "bun run scripts/pane-chrome/layout-sweep.ts",
@@ -111,6 +112,7 @@
111
112
  "gloom-polls": "github:gloom-sh/gloom-polls",
112
113
  "gloom-prediction-markets": "github:gloom-sh/gloom-prediction-markets",
113
114
  "happy-dom": "^20.11.1",
115
+ "playwright-core": "1.60.0",
114
116
  "png-to-ico": "^2.1.8",
115
117
  "rcedit": "^4.0.1",
116
118
  "typescript": "^5.9.2",
@@ -12,6 +12,7 @@ import type {
12
12
  ChatAttachment,
13
13
  ChatChannel,
14
14
  ChatChannelState,
15
+ ChatMentionCandidate,
15
16
  ChatDiscordLink,
16
17
  ChatMessage,
17
18
  ChatNotification,
@@ -118,6 +119,14 @@ export class CloudChatApi {
118
119
  this.emitAttachmentSupport();
119
120
  }
120
121
 
122
+ getMentionCandidates(channelId: string): Promise<ChatMentionCandidate[]> {
123
+ return this.request(`/chat/channels/${encodeURIComponent(channelId)}/mention-candidates`);
124
+ }
125
+
126
+ subscribeMentionCandidatesChanged(listener: () => void): () => void {
127
+ return this.options.socket.subscribeChatMentionCandidatesChanged(listener);
128
+ }
129
+
121
130
  async getChannels(): Promise<ChatChannel[]> {
122
131
  const channels =
123
132
  await this.request<ChatChannel[]>("/chat/channels");
@@ -0,0 +1,77 @@
1
+ /** Whether a board is whole, missing a part, or not loaded on the server yet. */
2
+ type BalanceBoardStatus = "available" | "partial" | "unavailable";
3
+
4
+ /** One country's crude or natural gas balance, as GET /cloud/jodi/board serves it. */
5
+ export interface JodiBalanceRow {
6
+ country: string;
7
+ /** The month the figures are for, YYYY-MM: each country's own latest report. Null when it never reported one. */
8
+ month: string | null;
9
+ production: number | null;
10
+ demand: number | null;
11
+ imports: number | null;
12
+ exports: number | null;
13
+ stocks: number | null;
14
+ unit: string;
15
+ }
16
+
17
+ export interface JodiBalanceBoard {
18
+ /** The newest month on the board, YYYY-MM. */
19
+ month: string;
20
+ commodity: "oil" | "gas";
21
+ rows: JodiBalanceRow[];
22
+ }
23
+
24
+ /** The Oil & Gas board: JODI's monthly crude and natural gas balances. */
25
+ export interface JodiBoardPayload {
26
+ source: "JODI";
27
+ status: BalanceBoardStatus;
28
+ /** When JODI last confirmed the oldest copy on the board, ISO. */
29
+ checkedAt: string | null;
30
+ /** The server has not been able to read JODI for days: these are the last copies it read. */
31
+ stale: boolean;
32
+ gaps: string[];
33
+ oil: JodiBalanceBoard | null;
34
+ gas: JodiBalanceBoard | null;
35
+ }
36
+
37
+ /** One crop, country and attribute of the latest market year, as GET /cloud/psd/board serves it. */
38
+ export interface PsdBalanceRow {
39
+ id: string;
40
+ crop: string;
41
+ country: string;
42
+ attribute: string;
43
+ value: number;
44
+ unit: string;
45
+ /** Market year, 2026/27. */
46
+ year: string;
47
+ cropOrder: number;
48
+ countryOrder: number;
49
+ attributeOrder: number;
50
+ }
51
+
52
+ /** A season-average farm price forecast. */
53
+ export interface PsdPriceForecast {
54
+ id: string;
55
+ crop: string;
56
+ forecast: number;
57
+ unit: string;
58
+ season: string;
59
+ cropOrder: number;
60
+ seasonYear: number;
61
+ }
62
+
63
+ /** The Crops board: USDA's world crop balances and season-average price forecasts. */
64
+ export interface PsdBoardPayload {
65
+ source: "USDA";
66
+ status: BalanceBoardStatus;
67
+ checkedAt: string | null;
68
+ stale: boolean;
69
+ gaps: string[];
70
+ /** YYYY-MM of the newest estimate on the board: the monthly supply and demand report that set it. */
71
+ month: string | null;
72
+ marketYear: string | null;
73
+ rows: PsdBalanceRow[];
74
+ prices: PsdPriceForecast[];
75
+ /** The model forecast date of the prices, YYYY-MM-DD. */
76
+ pricesDate: string | null;
77
+ }
@@ -35,10 +35,12 @@ import type { CloudCurveHistory, CloudCurveId, CloudCurveView, CloudWorldCurves
35
35
  import type { CdxBoardPayload, CloudCreditBoardParams, SovrBoardPayload } from "./credit-boards";
36
36
  import type { ShortVolumePayload, ShortVolumeScope } from "./short-volume";
37
37
  import type { BeneficialOwnersPayload, CloudBeneficialOwnersParams } from "./beneficial-owners";
38
+ import type { CloudInsiderSignalsParams, InsiderSignalsPayload } from "./insider-signals";
38
39
  import type { SocialMentionDayPosts, SocialMentionsPayload, SocialMentionsRange } from "./social-mentions";
39
40
  import type { FuturesContractTermsPayload, FuturesCurveAsOfPayload, FuturesCurvePayload } from "./futures-curve";
40
41
  import type { CotBoardPayload, CotContractPayload, CotFamily, CotClass } from "./cot";
41
42
  import type { DoeBoardPayload, DoePricesPayload } from "./doe";
43
+ import type { JodiBoardPayload, PsdBoardPayload } from "./commodity-balances";
42
44
  import type { CarbonBoard, CarbonHistory, EuropePowerBoard, EuropePowerHistory, GasStorageBoard, GasStorageHistory, GasStorageSeasonal } from "./europe-energy";
43
45
  import type { GpuBoardPayload, GpuEventsPayload, GpuHistoryPayload, GpuHistoryQuery } from "./gpu";
44
46
  import type { CpiBoardPayload } from "./cpi";
@@ -81,6 +83,7 @@ import {
81
83
  cloudExchangeRatePath,
82
84
  cloudSec13FPath,
83
85
  cloudSecBeneficialOwnersPath,
86
+ cloudSecInsiderSignalsPath,
84
87
  cloudSecFilingContentPath,
85
88
  cloudSecFilingDocumentsPath,
86
89
  cloudSecFilingsPath,
@@ -143,6 +146,7 @@ import type {
143
146
  CloudMarketResponse,
144
147
  CloudMarketScreenerCategory,
145
148
  CloudMarketScreenerPayload,
149
+ CloudMoverSession,
146
150
  CloudNewsListResponse,
147
151
  CloudNewsPayload,
148
152
  CloudSavedSearch,
@@ -283,8 +287,10 @@ export class CloudDataApi {
283
287
  return this.request(`/news/article-summary?${new URLSearchParams({ url })}`);
284
288
  }
285
289
 
286
- getMarketMovers(category: "day_gainers" | "day_losers" | "most_actives", count = 25, refresh = false): Promise<CloudMarketResponse<MarketMoversPayload>> {
290
+ /** `session` is what Gainers and Losers are ranked by (see CloudMoverSession). */
291
+ getMarketMovers(category: "day_gainers" | "day_losers" | "most_actives", count = 25, refresh = false, session?: CloudMoverSession): Promise<CloudMarketResponse<MarketMoversPayload>> {
287
292
  const params = new URLSearchParams({ category, count: String(count), mode: refresh ? "refresh" : "cache-first" });
293
+ if (session && category !== "most_actives") params.set("session", session);
288
294
  return this.request(`/market/movers?${params}`);
289
295
  }
290
296
 
@@ -300,12 +306,17 @@ export class CloudDataApi {
300
306
  return this.requestMarketSymbol("/market/dividends", symbol, exchange);
301
307
  }
302
308
 
303
- /** Gainers, losers and most active, or the pre-market, after-hours and gap lists with a `side`. */
309
+ /**
310
+ * Gainers, losers and most active, or the pre-market, after-hours and gap
311
+ * lists with a `side`. `session` is what Gainers and Losers are ranked by
312
+ * (see CloudMoverSession).
313
+ */
304
314
  async getCloudMarketScreener<Category extends CloudMarketScreenerCategory | CloudSessionMoversCategory>(
305
315
  category: Category,
306
316
  count = 25,
307
317
  mode: "cache-first" | "refresh" = "cache-first",
308
318
  side?: CloudSessionMoversSide,
319
+ session?: CloudMoverSession,
309
320
  ): Promise<CloudMarketResponse<Category extends CloudSessionMoversCategory ? CloudSessionMoversPayload : CloudMarketScreenerPayload>> {
310
321
  const requestedCount = Number.isFinite(count) ? Math.round(count) : 25;
311
322
  const params = new URLSearchParams({
@@ -314,6 +325,7 @@ export class CloudDataApi {
314
325
  mode,
315
326
  });
316
327
  if (side && isSessionMoversCategory(category)) params.set("side", side);
328
+ if (session && (category === "gainers" || category === "losers")) params.set("session", session);
317
329
  return this.request(`/market/screener?${params.toString()}`);
318
330
  }
319
331
 
@@ -590,6 +602,14 @@ export class CloudDataApi {
590
602
  return this.request<DoePricesPayload>("/cloud/doe/prices");
591
603
  }
592
604
 
605
+ getCloudJodiBoard(signal?: AbortSignal): Promise<JodiBoardPayload> {
606
+ return this.request<JodiBoardPayload>("/cloud/jodi/board", { signal });
607
+ }
608
+
609
+ getCloudPsdBoard(signal?: AbortSignal): Promise<PsdBoardPayload> {
610
+ return this.request<PsdBoardPayload>("/cloud/psd/board", { signal });
611
+ }
612
+
593
613
  async getCloudAttention(window: AttentionWindow = "now", symbol?: string): Promise<AttentionPayload> {
594
614
  const path = symbol ? `/cloud/attention/${encodeURIComponent(symbol)}` : "/cloud/attention";
595
615
  return this.request<AttentionPayload>(`${path}?window=${window}`);
@@ -934,6 +954,15 @@ export class CloudDataApi {
934
954
  });
935
955
  }
936
956
 
957
+ async getCloudSecInsiderSignals(
958
+ params: CloudInsiderSignalsParams = {},
959
+ options?: { signal?: AbortSignal },
960
+ ): Promise<InsiderSignalsPayload> {
961
+ return this.request<InsiderSignalsPayload>(cloudSecInsiderSignalsPath(params), {
962
+ signal: options?.signal,
963
+ });
964
+ }
965
+
937
966
  async getCloudSecFilingDocuments(
938
967
  params: CloudSecFilingParams,
939
968
  ): Promise<CloudSecDocumentsResponse> {
@@ -27,6 +27,8 @@ interface EarningsRealizedMove {
27
27
  afterClose: number;
28
28
  }
29
29
 
30
+ type EarningsActualSource = "calendar" | "press-release";
31
+
30
32
  export interface EarningsReport {
31
33
  symbol: string;
32
34
  name: string | null;
@@ -43,6 +45,12 @@ export interface EarningsReport {
43
45
  /** For an upcoming report, the consensus every earnings view shows, with `epsAnalysts` behind it. */
44
46
  epsEstimate: number | null;
45
47
  epsActual: number | null;
48
+ /**
49
+ * Where `epsActual` comes from: `press-release` is the company's own
50
+ * release, read the day it reports and provisional until the calendar's
51
+ * figure replaces it. Absent from older servers.
52
+ */
53
+ epsActualSource?: EarningsActualSource | null;
46
54
  epsAnalysts: number | null;
47
55
  revenueEstimate: number | null;
48
56
  revenueAnalysts: number | null;
@@ -69,6 +77,8 @@ export interface EarningsCalendarPayload {
69
77
 
70
78
  interface EarningsHistoryReport extends EarningsReport {
71
79
  revenueActual: number | null;
80
+ /** `press-release` until the statements have the quarter; absent from older servers. */
81
+ revenueActualSource?: "statements" | "press-release" | null;
72
82
  }
73
83
 
74
84
  export interface EarningsHistoryPayload {
@@ -645,6 +645,8 @@ class GloomApiClient {
645
645
  getChannels = this.chat.getChannels.bind(this.chat);
646
646
  getChatPresence = this.chat.getPresence.bind(this.chat);
647
647
  getChatState = this.chat.getState.bind(this.chat);
648
+ getChatMentionCandidates = this.chat.getMentionCandidates.bind(this.chat);
649
+ subscribeChatMentionCandidatesChanged = this.chat.subscribeMentionCandidatesChanged.bind(this.chat);
648
650
  updateChatChannelState = this.chat.updateChannelState.bind(this.chat);
649
651
  markChatNotificationsDelivered = this.chat.markNotificationsDelivered.bind(this.chat);
650
652
  getUnseenChatNotificationIds = this.chat.getUnseenNotificationIds.bind(this.chat);
@@ -826,6 +828,8 @@ class GloomApiClient {
826
828
  getCloudAward = this.data.getCloudAward.bind(this.data);
827
829
  getCloudDoeBoard = this.data.getCloudDoeBoard.bind(this.data);
828
830
  getCloudDoePrices = this.data.getCloudDoePrices.bind(this.data);
831
+ getCloudJodiBoard = this.data.getCloudJodiBoard.bind(this.data);
832
+ getCloudPsdBoard = this.data.getCloudPsdBoard.bind(this.data);
829
833
  getCloudAttention = this.data.getCloudAttention.bind(this.data);
830
834
  getCloudPowerBoard = this.data.getCloudPowerBoard.bind(this.data);
831
835
  getCloudPowerHistory = this.data.getCloudPowerHistory.bind(this.data);
@@ -864,6 +868,7 @@ class GloomApiClient {
864
868
  getRiskReport = this.data.getRiskReport.bind(this.data);
865
869
  getCloudSecFilings = this.data.getCloudSecFilings.bind(this.data);
866
870
  getCloudSecBeneficialOwners = this.data.getCloudSecBeneficialOwners.bind(this.data);
871
+ getCloudSecInsiderSignals = this.data.getCloudSecInsiderSignals.bind(this.data);
867
872
  getCloudSecFilingDocuments = this.data.getCloudSecFilingDocuments.bind(this.data);
868
873
  getCloudSecFilingContent = this.data.getCloudSecFilingContent.bind(this.data);
869
874
  getCloudSec13F = this.data.getCloudSec13F.bind(this.data);
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Companies ranked by recent open-market insider buying, as
3
+ * `GET /cloud/sec/insider-signals` returns them. Only Form 4 purchases (code
4
+ * P) by officers, directors and 10% owners count. Dates are `YYYY-MM-DD`,
5
+ * values USD.
6
+ */
7
+
8
+ type InsiderSignalRole =
9
+ | "CEO"
10
+ | "CFO"
11
+ | "COO"
12
+ | "President"
13
+ | "Chair"
14
+ | "Officer"
15
+ | "Director"
16
+ | "10% owner";
17
+
18
+ interface InsiderSignalBuyer {
19
+ name: string;
20
+ role: InsiderSignalRole;
21
+ value: number;
22
+ /** Days the insider bought in the window. */
23
+ buys: number;
24
+ lastBuy: string;
25
+ }
26
+
27
+ export interface InsiderSignalRow {
28
+ ticker: string;
29
+ issuerCik: string;
30
+ company: string;
31
+ /** 0 to 100. */
32
+ score: number;
33
+ /** Why the company ranks, strongest first, in plain words. */
34
+ reasons: string[];
35
+ /** Distinct insiders who bought in the window. */
36
+ insiders: number;
37
+ /** Insider buy days in the window. */
38
+ buys: number;
39
+ value: number;
40
+ shares: number;
41
+ firstBuy: string;
42
+ lastBuy: string;
43
+ lastFiled: string;
44
+ signals: {
45
+ clusterInsiders: number;
46
+ repeatBuys: number;
47
+ usualRatio: number | null;
48
+ stakeIncreasePct: number | null;
49
+ newPosition: boolean;
50
+ };
51
+ /** The biggest buyers, up to five. */
52
+ buyers: InsiderSignalBuyer[];
53
+ }
54
+
55
+ export interface InsiderSignalsPayload {
56
+ /** Days of buying each row covers, ending `through`. */
57
+ window: number;
58
+ through: string;
59
+ /** The newest filing day read. */
60
+ asOf: string | null;
61
+ /**
62
+ * Days whose filings have all been read. `missingDays`: weekdays since
63
+ * `from` the server no longer reads on its own and has not read whole, a gap
64
+ * an outage left (left out by servers that predate it).
65
+ */
66
+ coverage: { from: string | null; through: string | null; missingDays?: string[] };
67
+ rows: InsiderSignalRow[];
68
+ }
69
+
70
+ export interface CloudInsiderSignalsParams {
71
+ /** Days, 1 to 90; the server reads 30 when left out. */
72
+ window?: number;
73
+ /** Rows, 1 to 200; the server reads 50 when left out. */
74
+ limit?: number;
75
+ }
@@ -19,7 +19,7 @@ export interface ScreenerQuote {
19
19
  lastUpdated?: number;
20
20
  /** Session-fixed, so the price column reads its decimals from it rather than from each tick. */
21
21
  previousClose?: number;
22
- /** The quote's sessions beside the live print the list ranks by, when the server sends them. */
22
+ /** The quote's sessions beside the live print the server ranked the row by, when the server sends them. */
23
23
  sessions?: CloudMoverSessionFields;
24
24
  }
25
25
 
@@ -220,6 +220,7 @@ export function normalizeChatChannel(
220
220
  ...channel,
221
221
  kind: channel.kind ?? fallbackKind,
222
222
  created_at: normalizeTimestamp(channel.created_at),
223
+ ...(typeof channel.lastMessageAt === "string" ? { lastMessageAt: normalizeTimestamp(channel.lastMessageAt) } : {}),
223
224
  ...(channel.dmUser ? { dmUser: normalizeChatUser(channel.dmUser) } : {}),
224
225
  ...(Array.isArray(channel.members) ? { members: channel.members.map(normalizeChatUser) } : {}),
225
226
  };
@@ -7,6 +7,7 @@ import { isUsListingExchange, normalizeSymbol, parsePublicTickerKey, publicTicke
7
7
  import { nonUsSecListingVenue } from "../utils/sec";
8
8
  import type { HistoryRetention } from "../sources/history-retention";
9
9
  import type { CloudBeneficialOwnersParams } from "./beneficial-owners";
10
+ import type { CloudInsiderSignalsParams } from "./insider-signals";
10
11
  import type { RevenueBreakdownView } from "./revenue-breakdown";
11
12
 
12
13
  export type CloudHistoryParams = {
@@ -444,6 +445,14 @@ export function cloudSecBeneficialOwnersPath(params: CloudBeneficialOwnersParams
444
445
  return appendQuery("/cloud/sec/beneficial-owners", search);
445
446
  }
446
447
 
448
+ /** Companies ranked by recent open-market insider buying. */
449
+ export function cloudSecInsiderSignalsPath(params: CloudInsiderSignalsParams = {}): string {
450
+ const search = new URLSearchParams();
451
+ if (params.window != null) search.set("window", String(params.window));
452
+ if (params.limit != null) search.set("limit", String(params.limit));
453
+ return appendQuery("/cloud/sec/insider-signals", search);
454
+ }
455
+
447
456
  export function cloudSecFilingDocumentsPath(
448
457
  params: CloudSecFilingParams,
449
458
  ): string {
@@ -109,6 +109,7 @@ export class CloudApiSocket {
109
109
  private reconnectDelayMs = 1000;
110
110
 
111
111
  private readonly channelListeners = new Map<string, Set<ChannelListener>>();
112
+ private readonly chatMentionCandidatesListeners = new Set<() => void>();
112
113
  private readonly chatNotificationListeners =
113
114
  new Set<ChatNotificationListener>();
114
115
  private readonly chatChannelStateListeners =
@@ -254,6 +255,11 @@ export class CloudApiSocket {
254
255
  };
255
256
  }
256
257
 
258
+ subscribeChatMentionCandidatesChanged(listener: () => void): () => void {
259
+ this.chatMentionCandidatesListeners.add(listener);
260
+ return () => { this.chatMentionCandidatesListeners.delete(listener); };
261
+ }
262
+
257
263
  /**
258
264
  * The reader's read state or mute changed on another device or window. Like
259
265
  * notifications, it never opens the socket by itself.
@@ -631,6 +637,10 @@ export class CloudApiSocket {
631
637
  return;
632
638
  }
633
639
 
640
+ if (parsed?.type === "chat.mentions_changed") {
641
+ for (const listener of this.chatMentionCandidatesListeners) listener();
642
+ return;
643
+ }
634
644
  if (parsed?.type === "chat.channel_state") {
635
645
  const state = normalizeChatChannelStateFrame(parsed.data);
636
646
  if (state) {
@@ -143,6 +143,21 @@ export interface ChatChannel {
143
143
  teamId?: string;
144
144
  dmUser?: ChatUserSummary | null;
145
145
  members?: ChatUserSummary[];
146
+ /**
147
+ * When the newest message you can see in a direct or group conversation was
148
+ * sent; null when it has none. Absent from channels of other kinds and from
149
+ * servers that do not report it.
150
+ */
151
+ lastMessageAt?: string | null;
152
+ }
153
+
154
+ /** Minimal identities scoped by the server to this channel and existing DMs. */
155
+ export interface ChatMentionCandidate {
156
+ id: string;
157
+ username: string;
158
+ displayName: string | null;
159
+ channelActivityAt: string | null;
160
+ conversationActivityAt: string | null;
146
161
  }
147
162
 
148
163
  export interface ChatChannelState {
@@ -1517,6 +1532,15 @@ export interface CloudMarketBatchPayload<T> {
1517
1532
 
1518
1533
  export type CloudMarketScreenerCategory = "gainers" | "losers" | "most-active";
1519
1534
 
1535
+ /**
1536
+ * What Gloom Cloud selects and ranks Gainers and Losers by: `regular` the last
1537
+ * completed regular session's move, the Chg% a row shows; `extended` the
1538
+ * latest print, after-hours or pre-market included. While the regular session
1539
+ * trades the two are one list. A server that predates the choice ignores it
1540
+ * and ranks by the latest print.
1541
+ */
1542
+ export type CloudMoverSession = "regular" | "extended";
1543
+
1520
1544
  /**
1521
1545
  * A movers row's quote sessions, under the quote's field names and meaning:
1522
1546
  * the session it was read in, the day its change is dated to, the last
@@ -1529,7 +1553,7 @@ export type CloudMoverSessionFields = Partial<Pick<Quote,
1529
1553
  | "preMarketPrice" | "preMarketChange" | "preMarketChangePercent"
1530
1554
  | "postMarketPrice" | "postMarketChange" | "postMarketChangePercent">>;
1531
1555
 
1532
- /** price, change and changePercent are the live print the list is ranked by; the session fields say how it reads. */
1556
+ /** price, change and changePercent are the live print the server ranks the list by; the session fields say how it reads. */
1533
1557
  export interface CloudMarketScreenerItem extends CloudMoverSessionFields {
1534
1558
  rank: number;
1535
1559
  symbol: string;
@@ -8,7 +8,7 @@ import {
8
8
  import type { SyncBrokerInstanceResult } from "../../brokers/sync-broker-instance";
9
9
  import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
10
10
  import type { PluginRegistry } from "../../plugins/registry";
11
- import { adoptHeldPortfolioCurrencies } from "../../plugins/builtin/portfolio-list/mutations";
11
+ import { adoptHeldPortfolioCurrencies } from "../../portfolio/mutations";
12
12
  import { saveConfigImmediately } from "../../state/config-save-scheduler";
13
13
  import type { AppAction, AppState } from "../../state/app/context";
14
14
  import type { AppConfig } from "../../types/config";
@@ -18,6 +18,7 @@ import { getBrokerInstance } from "../../utils/broker-instances";
18
18
  export interface AppBrokerImportRuntime {
19
19
  importBrokerPositions: (
20
20
  instanceId: string,
21
+ /** What the sync reads. It lands on the app's tickers as they are when it commits, whatever was passed. */
21
22
  tickerMap?: Map<string, TickerRecord>,
22
23
  options?: BrokerImportOptions,
23
24
  ) => Promise<SyncBrokerInstanceResult>;
@@ -52,6 +53,13 @@ export function useBrokerImportRuntime({
52
53
  stateRef: { current: AppState };
53
54
  tickerRepository: AppTickerRepositoryPort;
54
55
  }): AppBrokerImportRuntime {
56
+ /**
57
+ * Commits a sync: carried onto the app as it is now (a pull or an edit while
58
+ * the broker answered stays) and handed to the app before anything is
59
+ * written. With a wait in between, a pull landing in it would be overwritten
60
+ * by this older copy and pushed from here as an edit. The writes start in
61
+ * the same tick, so the copy of anyone writing after lands on disk after it.
62
+ */
55
63
  const applyBrokerImportResult = useCallback(async (
56
64
  instanceId: string,
57
65
  result: SyncBrokerInstanceResult,
@@ -62,31 +70,27 @@ export function useBrokerImportRuntime({
62
70
  },
63
71
  ) => {
64
72
  throwIfBrokerImportCancelled(options?.signal);
65
- dispatch({ type: "SET_BROKER_ACCOUNTS", instanceId, accounts: result.brokerAccounts });
66
-
67
- const liveConfig = stateRef.current.config;
73
+ const live = stateRef.current;
74
+ result.rebase(live);
68
75
  const nextConfig = options?.callerOwnsConfig
69
- ? (result.config !== baseConfig ? result.config : liveConfig)
70
- : rebaseBrokerSyncConfig(liveConfig, baseConfig, result.config, instanceId);
71
- if (nextConfig !== liveConfig) {
72
- throwIfBrokerImportCancelled(options?.signal);
73
- dispatch({ type: "SET_CONFIG", config: nextConfig });
74
- await saveConfigImmediately(nextConfig);
75
- throwIfBrokerImportCancelled(options?.signal);
76
- pluginRegistry.events.emit("config:changed", { config: nextConfig });
76
+ ? (result.config !== baseConfig ? result.config : live.config)
77
+ : rebaseBrokerSyncConfig(live.config, baseConfig, result.config, instanceId);
78
+ const configChanged = nextConfig !== live.config;
79
+
80
+ dispatch({ type: "SET_BROKER_ACCOUNTS", instanceId, accounts: result.brokerAccounts });
81
+ if (configChanged) dispatch({ type: "SET_CONFIG", config: nextConfig });
82
+ for (const ticker of [...result.addedTickers, ...result.updatedTickers]) {
83
+ dispatch({ type: "UPDATE_TICKER", ticker });
77
84
  }
85
+ await Promise.all([result.commit(), configChanged ? saveConfigImmediately(nextConfig) : undefined]);
78
86
 
87
+ if (configChanged) pluginRegistry.events.emit("config:changed", { config: nextConfig });
79
88
  for (const ticker of result.addedTickers) {
80
- throwIfBrokerImportCancelled(options?.signal);
81
89
  pluginRegistry.events.emit("ticker:added", { symbol: ticker.metadata.ticker, ticker });
82
90
  }
83
- for (const ticker of [...result.addedTickers, ...result.updatedTickers]) {
84
- throwIfBrokerImportCancelled(options?.signal);
85
- dispatch({ type: "UPDATE_TICKER", ticker: { ...ticker } });
86
- }
87
91
 
88
92
  for (const position of result.positions) {
89
- throwIfBrokerImportCancelled(options?.signal);
93
+ if (options?.signal?.aborted) break;
90
94
  // Skip market data for broker option symbols; position marks are already available.
91
95
  // Position data (markPrice, marketValue, unrealizedPnl) is used directly.
92
96
  if (options?.refreshImportedTickers !== false && position.assetCategory !== "OPT") {
@@ -120,8 +124,6 @@ export function useBrokerImportRuntime({
120
124
  throwIfBrokerImportCancelled(options?.signal);
121
125
  options?.onCommitStart?.();
122
126
  commitStarted = true;
123
- await result.commit();
124
- throwIfBrokerImportCancelled(options?.signal);
125
127
  await applyBrokerImportResult(instanceId, result, baseConfig, {
126
128
  ...options,
127
129
  callerOwnsConfig: options?.config !== undefined,
@@ -155,7 +157,7 @@ export function useBrokerImportRuntime({
155
157
  deferPersistence: true,
156
158
  onResult: async (result, instance, previousConfig) => {
157
159
  if (!getBrokerInstance(stateRef.current.config.brokerInstances, instance.id)) return;
158
- await result.commit();
160
+ // Startup syncs run beside the first pull: each lands on what it left.
159
161
  await applyBrokerImportResult(instance.id, result, previousConfig, {
160
162
  refreshImportedTickers: false,
161
163
  });
@@ -3,7 +3,7 @@ import {
3
3
  clearPersistedBrokerAccounts,
4
4
  getBrokerAccountCacheSourceKey,
5
5
  } from "../../brokers/account-cache";
6
- import { findPortfolioHeir } from "../../brokers/broker-portfolio-sync";
6
+ import { clearLeftoverBrokerHoldings, findPortfolioHeir, knownBrokerHoldings } from "../../brokers/broker-portfolio-sync";
7
7
  import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
8
8
  import type { MarketDataCoordinator } from "../../market-data/coordinator";
9
9
  import { instrumentFromTicker } from "../../market-data/request-types";
@@ -216,15 +216,20 @@ export function bindPluginRegistryRuntimeAccess({
216
216
  return [heirId ? { ...portfolio, brokerInstanceId: heirId } : portfolio];
217
217
  });
218
218
  const nextTickers = new Map(stateRef.current.tickers);
219
+ const nextBrokerInstances = stateRef.current.config.brokerInstances.filter((entry) => entry.id !== instanceId);
220
+ // What older removals and pulls from other devices left behind goes as well.
221
+ const known = knownBrokerHoldings({ portfolios: nextPortfolios, brokerInstances: nextBrokerInstances });
219
222
 
220
223
  for (const ticker of stateRef.current.tickers.values()) {
221
- const nextPositions = ticker.metadata.positions.filter((position) => position.brokerInstanceId !== instanceId);
224
+ // A position here without a profile came from another device's sync of this one.
225
+ const nextPositions = ticker.metadata.positions.filter((position) => position.brokerInstanceId !== instanceId
226
+ && !(removedPortfolioIds.has(position.portfolio) && !position.brokerInstanceId));
222
227
  const heldPortfolioIds = new Set(nextPositions.map((position) => position.portfolio));
223
228
  const nextPortfolioRefs = ticker.metadata.portfolios.filter((portfolioId) =>
224
229
  !removedPortfolioIds.has(portfolioId) && (!heirs.has(portfolioId) || heldPortfolioIds.has(portfolioId)));
225
230
  const nextBrokerContracts = (ticker.metadata.broker_contracts ?? []).filter((contract) => contract.brokerInstanceId !== instanceId);
226
231
 
227
- const nextTicker: TickerRecord = {
232
+ const removedTicker: TickerRecord = {
228
233
  ...ticker,
229
234
  metadata: {
230
235
  ...ticker.metadata,
@@ -233,12 +238,13 @@ export function bindPluginRegistryRuntimeAccess({
233
238
  broker_contracts: nextBrokerContracts,
234
239
  },
235
240
  };
241
+ const nextTicker = clearLeftoverBrokerHoldings(removedTicker, known) ?? removedTicker;
236
242
 
237
243
  const shouldDeleteTicker =
238
- nextPositions.length === 0
239
- && nextPortfolioRefs.length === 0
244
+ nextTicker.metadata.positions.length === 0
245
+ && nextTicker.metadata.portfolios.length === 0
240
246
  && nextTicker.metadata.watchlists.length === 0
241
- && nextBrokerContracts.length === 0
247
+ && (nextTicker.metadata.broker_contracts ?? []).length === 0
242
248
  && nextTicker.metadata.tags.length === 0
243
249
  && Object.keys(nextTicker.metadata.custom).length === 0;
244
250
 
@@ -256,7 +262,7 @@ export function bindPluginRegistryRuntimeAccess({
256
262
 
257
263
  const nextConfig = {
258
264
  ...stateRef.current.config,
259
- brokerInstances: stateRef.current.config.brokerInstances.filter((entry) => entry.id !== instanceId),
265
+ brokerInstances: nextBrokerInstances,
260
266
  portfolios: nextPortfolios,
261
267
  };
262
268
 
@@ -68,11 +68,24 @@ export function useAppUpdateRuntime({
68
68
  runUpdateCommand: () => void;
69
69
  } {
70
70
  const restartingRef = useRef(false);
71
+ /** The tag of a release whose download found nothing to take; the hourly check does not offer it again. */
72
+ const declinedTagRef = useRef<string | null>(null);
71
73
 
72
74
  const startUpdate = useCallback((release: ReleaseInfo) => {
73
75
  dispatch({ type: "SET_UPDATE_PROGRESS", progress: { phase: "downloading", percent: 0 } });
74
76
  void performUpdate(release, (progress) => {
75
77
  logUpdateFailure(progress);
78
+ if (progress.phase === "done") {
79
+ // The download had nothing to take (that build is the installed one):
80
+ // an answer like a check's, so it passes as a notice like one. The
81
+ // release goes before the progress, so the effect below never starts
82
+ // it again, and the hourly check does not offer it again.
83
+ declinedTagRef.current = release.tagName;
84
+ dispatch({ type: "SET_UPDATE_AVAILABLE", release: null });
85
+ dispatch({ type: "SET_UPDATE_PROGRESS", progress: null });
86
+ dispatch({ type: "SET_UPDATE_NOTICE", notice: progress.message ?? null });
87
+ return;
88
+ }
76
89
  dispatch({ type: "SET_UPDATE_PROGRESS", progress });
77
90
  });
78
91
  }, [dispatch]);
@@ -116,7 +129,7 @@ export function useAppUpdateRuntime({
116
129
  if (result.kind === "error") updateLog.warn("update check failed", { error: result.error });
117
130
 
118
131
  if (!manual) {
119
- if (result.kind === "available") {
132
+ if (result.kind === "available" && result.release.tagName !== declinedTagRef.current) {
120
133
  dispatch({ type: "SET_UPDATE_AVAILABLE", release: result.release });
121
134
  }
122
135
  return;