gloomberb 0.17.4 → 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 (384) hide show
  1. package/package.json +3 -1
  2. package/patches/electrobun@1.18.1.patch +80 -0
  3. package/src/api-client/chat.ts +9 -0
  4. package/src/api-client/commodity-balances.ts +77 -0
  5. package/src/api-client/data.ts +35 -3
  6. package/src/api-client/earnings.ts +10 -0
  7. package/src/api-client/index.ts +5 -0
  8. package/src/api-client/insider-signals.ts +75 -0
  9. package/src/api-client/market-discovery.ts +1 -1
  10. package/src/api-client/normalizers.ts +1 -0
  11. package/src/api-client/paths.ts +9 -0
  12. package/src/api-client/research-activity.ts +10 -3
  13. package/src/api-client/socket.ts +10 -0
  14. package/src/api-client/types.ts +25 -1
  15. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
  16. package/src/app/runtime/broker-import.ts +23 -21
  17. package/src/app/runtime/plugin-bindings.ts +13 -7
  18. package/src/app/runtime/update.ts +64 -4
  19. package/src/app.tsx +2 -0
  20. package/src/architecture/core-builtin-allowlist.json +2 -10
  21. package/src/ask-gloom/pane-context.ts +11 -1
  22. package/src/brokers/broker-portfolio-sync.ts +66 -0
  23. package/src/brokers/sync-broker-instance.ts +75 -41
  24. package/src/cli/commands/market.ts +101 -34
  25. package/src/cli/commands/options-chain.ts +20 -6
  26. package/src/cli/commands/overview.ts +54 -13
  27. package/src/cli/commands/search.ts +1 -1
  28. package/src/cli/commands/system.ts +19 -1
  29. package/src/cli/commands/ticker.ts +24 -4
  30. package/src/cli/context.ts +13 -0
  31. package/src/cli/errors.ts +15 -2
  32. package/src/cli/fx-pair.ts +77 -0
  33. package/src/cli/glossary.ts +34 -4
  34. package/src/cli/history-rows.ts +25 -11
  35. package/src/cli/index.ts +30 -12
  36. package/src/cli/listing-arg.ts +61 -138
  37. package/src/cli/listing-suggestion.ts +3 -21
  38. package/src/cli/pane-functions/catalog.ts +50 -0
  39. package/src/cli/pane-functions/freshness.ts +44 -9
  40. package/src/cli/pane-functions/function-help.ts +9 -9
  41. package/src/cli/pane-functions/headless.ts +19 -7
  42. package/src/cli/pane-functions/index.ts +63 -5
  43. package/src/cli/pane-functions/market-session.ts +18 -3
  44. package/src/cli/pane-functions/resolver.ts +29 -8
  45. package/src/cli/pane-functions/screenshot.ts +2 -0
  46. package/src/cli/report-tables.ts +11 -4
  47. package/src/cli/result.ts +2 -0
  48. package/src/cli/series-search.ts +6 -1
  49. package/src/cli/ticker-resolution.ts +1 -2
  50. package/src/cli/unknown-command.ts +18 -0
  51. package/src/components/chart/composite/composite-chart.tsx +80 -19
  52. package/src/components/chart/composite/format.ts +17 -0
  53. package/src/components/chart/composite/legend.tsx +26 -15
  54. package/src/components/chart/composite/panel-labels.ts +38 -0
  55. package/src/components/chart/composite/panel-series.ts +1 -0
  56. package/src/components/chart/composite/panel-surface.tsx +25 -7
  57. package/src/components/chart/composite/price-axis-labels.tsx +16 -3
  58. package/src/components/chart/composite/scene.ts +60 -2
  59. package/src/components/chart/composite/series-axis-tags.tsx +67 -0
  60. package/src/components/chart/composite/tools.ts +2 -2
  61. package/src/components/chart/composite/types.ts +28 -0
  62. package/src/components/chart/core/view-reset.ts +46 -0
  63. package/src/components/command-bar/commands/collection.ts +1 -1
  64. package/src/components/command-bar/commands/direct/index.ts +48 -1
  65. package/src/components/command-bar/commands/registry.ts +8 -0
  66. package/src/components/command-bar/list/model.ts +7 -0
  67. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  68. package/src/components/command-bar/panel/prompt-binding.ts +8 -0
  69. package/src/components/command-bar/routes/root/command-items.ts +17 -2
  70. package/src/components/command-bar/routes/root/pick-prefetch.ts +38 -0
  71. package/src/components/command-bar/routes/root/provider-search.ts +6 -0
  72. package/src/components/command-bar/routes/root/runtime.ts +3 -0
  73. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -1
  74. package/src/components/command-bar/routes/root/shortcut-items.ts +1 -1
  75. package/src/components/command-bar/routes/ticker-search/actions.ts +34 -0
  76. package/src/components/command-bar/surface/index.tsx +7 -1
  77. package/src/components/command-bar/workflow/collection-actions.ts +6 -5
  78. package/src/components/command-bar/workflow/ops.ts +8 -5
  79. package/src/components/command-bar/workflow/pane-reuse.ts +3 -2
  80. package/src/components/command-bar/workflow/tickers.ts +1 -1
  81. package/src/components/form-modal/confirm.tsx +1 -0
  82. package/src/components/form-modal/request.ts +2 -0
  83. package/src/components/layout/detached-pane-shell.tsx +6 -1
  84. package/src/components/layout/pane/content.tsx +11 -8
  85. package/src/components/layout/pane/sidebar.tsx +24 -11
  86. package/src/components/layout/pane/ticker-fate-gate.tsx +56 -0
  87. package/src/components/layout/shell/index.tsx +5 -1
  88. package/src/components/layout/shell/menu.ts +8 -0
  89. package/src/components/layout/shell/pane/capture.ts +29 -12
  90. package/src/components/onboarding/wizard-model.ts +1 -1
  91. package/src/components/onboarding/wizard-positions.ts +18 -10
  92. package/src/components/ticker/badge/format.ts +64 -15
  93. package/src/components/ticker/badge/index.tsx +32 -11
  94. package/src/components/ticker/badge/list.tsx +4 -3
  95. package/src/components/ticker/badge/text.tsx +2 -2
  96. package/src/components/ticker/list-table-view.tsx +8 -1
  97. package/src/components/ui/confirm-dialog.tsx +30 -4
  98. package/src/components/ui/data-table/opentui/index.tsx +61 -20
  99. package/src/components/ui/data-table/opentui/model.ts +104 -0
  100. package/src/components/ui/fields.tsx +5 -0
  101. package/src/components/ui/inline-quick-add.tsx +4 -1
  102. package/src/components/ui/query-bar-keys.tsx +29 -5
  103. package/src/components/ui/query-bar.tsx +29 -4
  104. package/src/components/ui/windowed-list.tsx +27 -0
  105. package/src/data/config/store/normalize.ts +8 -0
  106. package/src/docs/snapshot.json +248 -5
  107. package/src/i18n/es.ts +3 -1
  108. package/src/i18n/ja.ts +3 -1
  109. package/src/i18n/ko.ts +3 -1
  110. package/src/i18n/zh-cn.ts +3 -1
  111. package/src/i18n/zh-tw.ts +3 -1
  112. package/src/layout-marketplace/cloud.ts +16 -5
  113. package/src/layout-marketplace/gallery.tsx +4 -4
  114. package/src/layout-marketplace/linked-sync.ts +9 -3
  115. package/src/layout-marketplace/linked.ts +24 -3
  116. package/src/layout-marketplace/model.ts +39 -4
  117. package/src/market-data/coordinator/events.ts +5 -1
  118. package/src/market-data/coordinator/index.ts +38 -1
  119. package/src/market-data/coordinator/quotes.ts +4 -0
  120. package/src/market-data/futures-contract.ts +53 -0
  121. package/src/market-data/hooks.tsx +225 -30
  122. package/src/market-data/market/bar-period.ts +71 -0
  123. package/src/market-data/market/format.ts +14 -2
  124. package/src/market-data/market/freshness.ts +28 -1
  125. package/src/market-data/market/status.ts +152 -13
  126. package/src/market-data/model-dividend-yield.ts +76 -0
  127. package/src/market-data/quotes/freshness.ts +30 -1
  128. package/src/{plugins/builtin/portfolio-list → market-data/quotes}/live-valuation.ts +6 -6
  129. package/src/market-data/quotes/regular-session-valuation.ts +32 -1
  130. package/src/plugins/builtin/analytics/metrics.ts +8 -4
  131. package/src/plugins/builtin/analytics/risk-headless.ts +12 -5
  132. package/src/plugins/builtin/analytics/risk-model.ts +74 -15
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +6 -6
  134. package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
  135. package/src/plugins/builtin/broker-manager/model.ts +2 -2
  136. package/src/plugins/builtin/builtin-plugin-meta.ts +0 -1
  137. package/src/plugins/builtin/chart-composer/editor/controller.ts +14 -10
  138. package/src/plugins/builtin/chart-composer/formula.ts +233 -0
  139. package/src/plugins/builtin/chart-composer/headless.ts +27 -2
  140. package/src/plugins/builtin/chart-composer/index.tsx +3 -2
  141. package/src/plugins/builtin/chart-composer/pane.tsx +41 -10
  142. package/src/plugins/builtin/chart-composer/presets.ts +91 -2
  143. package/src/plugins/builtin/chart-composer/quick-add.tsx +9 -2
  144. package/src/plugins/builtin/chart-composer/series-catalog.ts +44 -11
  145. package/src/plugins/builtin/chart-composer/series-expression.ts +1 -1
  146. package/src/plugins/builtin/chart-composer/settings.ts +16 -4
  147. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  148. package/src/plugins/builtin/chat/content/channel-navigation.ts +17 -6
  149. package/src/plugins/builtin/chat/content/footer.tsx +5 -2
  150. package/src/plugins/builtin/chat/content/index.tsx +12 -5
  151. package/src/plugins/builtin/chat/content/mentions.ts +50 -21
  152. package/src/plugins/builtin/chat/content/scroll.ts +12 -5
  153. package/src/plugins/builtin/chat/content/snapshot.ts +11 -0
  154. package/src/plugins/builtin/chat/content/transcript.tsx +48 -37
  155. package/src/plugins/builtin/chat/content/whole-message-copy.ts +44 -2
  156. package/src/plugins/builtin/chat/controller/index.ts +88 -17
  157. package/src/plugins/builtin/chat/controller/mention-candidates.ts +104 -0
  158. package/src/plugins/builtin/chat/controller/messages.ts +14 -2
  159. package/src/plugins/builtin/chat/controller/realtime.ts +9 -0
  160. package/src/plugins/builtin/chat/controller/state.ts +25 -0
  161. package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
  162. package/src/plugins/builtin/chat/controller/view.ts +4 -0
  163. package/src/plugins/builtin/chat/conversation-recency.ts +73 -0
  164. package/src/plugins/builtin/chat/conversations.ts +12 -5
  165. package/src/plugins/builtin/chat/index.tsx +1 -1
  166. package/src/plugins/builtin/chat/layout.ts +23 -5
  167. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  168. package/src/plugins/builtin/chat/share/flow.tsx +18 -9
  169. package/src/plugins/builtin/chat/sidebar-rows.ts +18 -5
  170. package/src/plugins/builtin/chat/sidebar.tsx +6 -1
  171. package/src/plugins/builtin/chat/unread-badge.ts +38 -7
  172. package/src/plugins/builtin/cloud/askg/manifest.ts +4 -3
  173. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
  174. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +16 -5
  175. package/src/plugins/builtin/commodities-energy/index.tsx +23 -4
  176. package/src/plugins/builtin/commodity-balances/client.ts +51 -193
  177. package/src/plugins/builtin/commodity-balances/headless.ts +10 -3
  178. package/src/plugins/builtin/commodity-balances/model.ts +5 -180
  179. package/src/plugins/builtin/commodity-balances/pane.tsx +3 -0
  180. package/src/plugins/builtin/custom-view/index.tsx +1 -1
  181. package/src/plugins/builtin/custom-view/loader.ts +57 -7
  182. package/src/plugins/builtin/custom-view/sources.ts +146 -0
  183. package/src/plugins/builtin/custom-view/view-spec.ts +37 -1
  184. package/src/plugins/builtin/earnings/board-model.ts +33 -3
  185. package/src/plugins/builtin/earnings/board-table.ts +59 -14
  186. package/src/plugins/builtin/earnings/board.tsx +36 -3
  187. package/src/plugins/builtin/earnings/headless.ts +24 -5
  188. package/src/plugins/builtin/earnings/history-model.ts +9 -1
  189. package/src/plugins/builtin/earnings/history-table.ts +16 -5
  190. package/src/plugins/builtin/earnings/history.tsx +40 -10
  191. package/src/plugins/builtin/earnings/index.tsx +3 -1
  192. package/src/plugins/builtin/earnings/no-earnings.ts +52 -0
  193. package/src/plugins/builtin/econ-statistics/detail.tsx +4 -1
  194. package/src/plugins/builtin/econ-statistics/headless.ts +5 -0
  195. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  196. package/src/plugins/builtin/econ-statistics/view.ts +4 -0
  197. package/src/plugins/builtin/futures/model.ts +5 -4
  198. package/src/plugins/builtin/futures/series.ts +38 -1
  199. package/src/plugins/builtin/futures-curve/client.ts +4 -4
  200. package/src/plugins/builtin/futures-curve/headless.ts +33 -8
  201. package/src/plugins/builtin/futures-curve/index.tsx +4 -4
  202. package/src/plugins/builtin/futures-curve/model.ts +109 -6
  203. package/src/plugins/builtin/futures-curve/pane.tsx +24 -13
  204. package/src/plugins/builtin/insider/headless.ts +74 -4
  205. package/src/plugins/builtin/insider/index.tsx +99 -5
  206. package/src/plugins/builtin/insider/signals-client.ts +69 -0
  207. package/src/plugins/builtin/insider/signals-model.ts +146 -0
  208. package/src/plugins/builtin/insider/signals.tsx +176 -0
  209. package/src/plugins/builtin/iv-history/client.ts +41 -2
  210. package/src/plugins/builtin/iv-history/headless.ts +24 -16
  211. package/src/plugins/builtin/iv-history/screen-pane.tsx +25 -7
  212. package/src/plugins/builtin/jodi/client.ts +42 -205
  213. package/src/plugins/builtin/jodi/headless.ts +13 -4
  214. package/src/plugins/builtin/jodi/model.ts +18 -43
  215. package/src/plugins/builtin/jodi/pane.tsx +5 -0
  216. package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
  217. package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
  218. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  219. package/src/plugins/builtin/macro-day/release-table.ts +3 -3
  220. package/src/plugins/builtin/market-movers/client.ts +32 -6
  221. package/src/plugins/builtin/market-movers/headless.ts +7 -2
  222. package/src/plugins/builtin/market-movers/model.ts +64 -3
  223. package/src/plugins/builtin/market-movers/screener.ts +20 -9
  224. package/src/plugins/builtin/market-valuation/detail.tsx +7 -2
  225. package/src/plugins/builtin/market-valuation/headless.ts +17 -4
  226. package/src/plugins/builtin/market-valuation/pane.tsx +7 -3
  227. package/src/plugins/builtin/market-valuation/view.ts +10 -0
  228. package/src/plugins/builtin/mna/model.ts +2 -1
  229. package/src/plugins/builtin/options-positioning/client.ts +10 -0
  230. package/src/plugins/builtin/options-positioning/headless.ts +88 -13
  231. package/src/plugins/builtin/options-positioning/index.tsx +2 -2
  232. package/src/plugins/builtin/options-positioning/model.ts +47 -0
  233. package/src/plugins/builtin/options-positioning/pane.tsx +55 -26
  234. package/src/plugins/builtin/options-scenario/charts.tsx +13 -4
  235. package/src/plugins/builtin/options-scenario/client.ts +111 -17
  236. package/src/plugins/builtin/options-scenario/collar.ts +141 -0
  237. package/src/plugins/builtin/options-scenario/csp.ts +1 -1
  238. package/src/plugins/builtin/options-scenario/freshness.ts +27 -0
  239. package/src/plugins/builtin/options-scenario/headless.ts +28 -12
  240. package/src/plugins/builtin/options-scenario/index.tsx +1 -1
  241. package/src/plugins/builtin/options-scenario/model.ts +55 -5
  242. package/src/plugins/builtin/options-scenario/pane.tsx +24 -5
  243. package/src/plugins/builtin/perps/client.ts +77 -6
  244. package/src/plugins/builtin/perps/headless.ts +25 -14
  245. package/src/plugins/builtin/perps/index.tsx +1 -1
  246. package/src/plugins/builtin/perps/model.ts +32 -0
  247. package/src/plugins/builtin/perps/pane.tsx +5 -4
  248. package/src/plugins/builtin/portfolio-list/allocation.ts +9 -4
  249. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +39 -20
  250. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  251. package/src/plugins/builtin/portfolio-list/cli/shared.ts +15 -7
  252. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +1 -1
  253. package/src/plugins/builtin/portfolio-list/column-values.ts +4 -3
  254. package/src/plugins/builtin/portfolio-list/command-bar.ts +1 -1
  255. package/src/plugins/builtin/portfolio-list/headless.ts +10 -3
  256. package/src/plugins/builtin/portfolio-list/pane/index.tsx +36 -4
  257. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  258. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +35 -6
  259. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  260. package/src/plugins/builtin/portfolio-list/settings.ts +22 -0
  261. package/src/plugins/builtin/portfolio-list/since-last-seen.ts +197 -0
  262. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  263. package/src/plugins/builtin/portfolio-list/table.tsx +14 -2
  264. package/src/plugins/builtin/rate-path/model.ts +1 -1
  265. package/src/plugins/builtin/rate-path/pane.tsx +2 -1
  266. package/src/plugins/builtin/research/corporate-actions-pane.tsx +14 -5
  267. package/src/plugins/builtin/research/event-model.ts +41 -49
  268. package/src/plugins/builtin/research/event-sources.ts +1 -1
  269. package/src/plugins/builtin/research/headless.ts +1 -1
  270. package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
  271. package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
  272. package/src/plugins/builtin/reverse-dcf/headless.ts +25 -9
  273. package/src/plugins/builtin/reverse-dcf/index.tsx +4 -1
  274. package/src/plugins/builtin/reverse-dcf/model.ts +39 -5
  275. package/src/plugins/builtin/reverse-dcf/pane.tsx +16 -6
  276. package/src/plugins/builtin/sectors/client.ts +46 -19
  277. package/src/plugins/builtin/sectors/sector-model.ts +22 -10
  278. package/src/plugins/builtin/shared/earnings-report.ts +48 -1
  279. package/src/plugins/builtin/shared/history-span.ts +55 -0
  280. package/src/plugins/builtin/shared/report-freshness.ts +20 -11
  281. package/src/plugins/builtin/shared/series-detail-chart.tsx +5 -2
  282. package/src/plugins/builtin/shared/series-list-detail.tsx +7 -3
  283. package/src/plugins/builtin/shared/ticker-surface.ts +16 -2
  284. package/src/plugins/builtin/shared/use-quote-board.ts +12 -5
  285. package/src/plugins/builtin/themes/headless.ts +15 -2
  286. package/src/plugins/builtin/thirteenf/api.ts +60 -43
  287. package/src/plugins/builtin/thirteenf/data.ts +76 -35
  288. package/src/plugins/builtin/thirteenf/headless.ts +2 -0
  289. package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
  290. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +85 -17
  291. package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +92 -0
  292. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +26 -8
  293. package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -2
  294. package/src/plugins/builtin/ticker-detail/headless.ts +95 -22
  295. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  296. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  297. package/src/plugins/builtin/world-venue-map/headless.ts +17 -3
  298. package/src/plugins/builtin/yield-curve/curves.ts +15 -1
  299. package/src/plugins/builtin/yield-curve/headless.ts +17 -7
  300. package/src/plugins/builtin/yield-curve/history.ts +2 -0
  301. package/src/plugins/builtin/yield-curve/index.tsx +23 -18
  302. package/src/plugins/builtin/yield-curve/series.ts +19 -9
  303. package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
  304. package/src/plugins/builtin/yield-curve/world.ts +4 -0
  305. package/src/plugins/registry/host-actions.ts +3 -0
  306. package/src/{plugins/builtin/portfolio-list → portfolio}/mutations.ts +6 -6
  307. package/src/portfolio/valuation-basis.ts +4 -21
  308. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  309. package/src/remote/controller.ts +27 -7
  310. package/src/remote/schema.ts +25 -9
  311. package/src/renderers/browser/app-services.ts +7 -1
  312. package/src/renderers/browser/config-host.ts +55 -2
  313. package/src/renderers/browser/first-screen.ts +193 -0
  314. package/src/renderers/browser/main.tsx +59 -7
  315. package/src/renderers/cloudflare/http-proxy.ts +21 -13
  316. package/src/renderers/dom/desktop/controls.tsx +2 -0
  317. package/src/renderers/dom/desktop/query-bar.tsx +1 -1
  318. package/src/renderers/dom/dom-ui-host.tsx +2 -0
  319. package/src/renderers/dom/windowed-list.tsx +201 -0
  320. package/src/renderers/dom/windowed-rows.ts +65 -0
  321. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
  322. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -7
  323. package/src/renderers/electrobun/bun/desktop/http-stream.ts +21 -6
  324. package/src/renderers/electrobun/bun/desktop/update.ts +132 -14
  325. package/src/renderers/electrobun/shared/protocol.ts +7 -4
  326. package/src/renderers/electrobun/view/http-fetch.ts +12 -1
  327. package/src/renderers/electrobun/view/update-host.ts +2 -2
  328. package/src/shares/bug-report-dialog.tsx +162 -0
  329. package/src/shares/bug-report-flow.tsx +84 -0
  330. package/src/shares/bug-report.ts +42 -0
  331. package/src/shares/capture-flow.ts +19 -0
  332. package/src/shares/capture-preview.tsx +129 -0
  333. package/src/shares/chat.ts +10 -1
  334. package/src/shares/dialog.tsx +16 -91
  335. package/src/shares/flow.tsx +12 -16
  336. package/src/shares/personal-pane.ts +35 -1
  337. package/src/sources/listing-symbols.ts +8 -4
  338. package/src/state/app/context/index.tsx +13 -0
  339. package/src/state/app/tab-cursor.ts +85 -0
  340. package/src/sync/core-contributors.ts +206 -59
  341. package/src/telemetry/usage-counts.ts +16 -0
  342. package/src/test-support/config-store.ts +31 -0
  343. package/src/test-support/moved-symbols.ts +64 -0
  344. package/src/theme/colors.ts +30 -0
  345. package/src/tickers/listing-fate.ts +242 -0
  346. package/src/tickers/open-target.ts +61 -11
  347. package/src/tickers/search/index.ts +45 -3
  348. package/src/tickers/search/ranking.ts +19 -2
  349. package/src/tickers/search/types.ts +3 -0
  350. package/src/tickers/search/upsert.ts +22 -7
  351. package/src/tickers/ticker-changes.ts +26 -9
  352. package/src/time-series/bar-spacing.ts +19 -0
  353. package/src/time-series/chart-data.ts +37 -13
  354. package/src/time-series/field-catalog.ts +5 -2
  355. package/src/time-series/forward-valuation.ts +10 -7
  356. package/src/time-series/fundamentals.ts +49 -11
  357. package/src/time-series/hooks.ts +2 -0
  358. package/src/time-series/live-bars.ts +3 -1
  359. package/src/time-series/parsed-history-cache.ts +2 -0
  360. package/src/time-series/price-units.ts +69 -0
  361. package/src/time-series/resolve.ts +71 -11
  362. package/src/time-series/session-bar.ts +138 -0
  363. package/src/time-series/spec.ts +20 -3
  364. package/src/time-series/studies.ts +211 -12
  365. package/src/time-series/transforms.ts +2 -0
  366. package/src/time-series/types.ts +11 -1
  367. package/src/time-series/valuation-price.ts +13 -0
  368. package/src/types/config.ts +24 -1
  369. package/src/types/financials.ts +2 -0
  370. package/src/types/headless.ts +27 -0
  371. package/src/types/plugin.ts +7 -0
  372. package/src/ui/host.tsx +5 -0
  373. package/src/updater/channel.ts +51 -0
  374. package/src/updater/github-releases.ts +85 -16
  375. package/src/updater/index.ts +128 -17
  376. package/src/utils/asset-category.ts +20 -0
  377. package/src/utils/datetime-format.ts +7 -1
  378. package/src/utils/decimal-comma.ts +83 -3
  379. package/src/utils/fundamentals.ts +7 -2
  380. package/src/utils/http-proxy-response.ts +66 -9
  381. package/src/utils/plugin-proxy-hosts.json +1 -5
  382. package/src/utils/semver.ts +47 -2
  383. package/src/utils/typo-distance.ts +22 -0
  384. package/src/plugins/builtin/shared/zip-text.ts +0 -46
@@ -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
 
@@ -7,13 +7,17 @@ import {
7
7
  canRestartToApply,
8
8
  canSelfUpdate,
9
9
  checkForUpdateDetailed,
10
+ discardStagedUpdate,
10
11
  performUpdate,
11
12
  type ReleaseInfo,
12
13
  type UpdateCheckResult,
13
14
  type UpdateProgress,
14
15
  } from "../../updater";
16
+ import { resolveUpdateChannel, stagedUpdateFitsChannel } from "../../updater/channel";
17
+ import type { UpdateChannel } from "../../types/config";
15
18
  import { VERSION } from "../../version";
16
19
  import { debugLog } from "../../utils/debug-log";
20
+ import { isPrereleaseVersion } from "../../utils/semver";
17
21
  import { reportCrash } from "../../telemetry/crash-reports";
18
22
  import { runAutomated } from "../../telemetry/usage-counts";
19
23
 
@@ -41,6 +45,7 @@ export function useAppUpdateRuntime({
41
45
  pluginRegistry,
42
46
  stateRef,
43
47
  updateAvailable,
48
+ updateChannel = "stable",
44
49
  updateCheckInProgress,
45
50
  updateProgress,
46
51
  }: {
@@ -50,6 +55,8 @@ export function useAppUpdateRuntime({
50
55
  pluginRegistry: PluginRegistry;
51
56
  stateRef: { current: AppState };
52
57
  updateAvailable: ReleaseInfo | null;
58
+ /** The channel in effect (`resolveUpdateChannel`); a change checks again at once. */
59
+ updateChannel?: UpdateChannel;
53
60
  updateCheckInProgress: boolean;
54
61
  updateProgress: unknown;
55
62
  }): {
@@ -61,11 +68,24 @@ export function useAppUpdateRuntime({
61
68
  runUpdateCommand: () => void;
62
69
  } {
63
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);
64
73
 
65
74
  const startUpdate = useCallback((release: ReleaseInfo) => {
66
75
  dispatch({ type: "SET_UPDATE_PROGRESS", progress: { phase: "downloading", percent: 0 } });
67
76
  void performUpdate(release, (progress) => {
68
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
+ }
69
89
  dispatch({ type: "SET_UPDATE_PROGRESS", progress });
70
90
  });
71
91
  }, [dispatch]);
@@ -97,7 +117,8 @@ export function useAppUpdateRuntime({
97
117
  // failed check like any other: the hourly one must not end as an
98
118
  // unhandled rejection, and a manual one must clear its progress state.
99
119
  // The timeout is still reported, as it was when it went unhandled.
100
- const result = await checkForUpdateDetailed(VERSION).catch((error: unknown): UpdateCheckResult => {
120
+ const channel = resolveUpdateChannel(stateRef.current.config);
121
+ const result = await checkForUpdateDetailed(VERSION, channel).catch((error: unknown): UpdateCheckResult => {
101
122
  const message = error instanceof Error ? error.message : "";
102
123
  if (message.startsWith("RPC request timed out")) {
103
124
  reportCrash(error, { kind: "unhandled-rejection" });
@@ -108,7 +129,7 @@ export function useAppUpdateRuntime({
108
129
  if (result.kind === "error") updateLog.warn("update check failed", { error: result.error });
109
130
 
110
131
  if (!manual) {
111
- if (result.kind === "available") {
132
+ if (result.kind === "available" && result.release.tagName !== declinedTagRef.current) {
112
133
  dispatch({ type: "SET_UPDATE_AVAILABLE", release: result.release });
113
134
  }
114
135
  return;
@@ -123,7 +144,7 @@ export function useAppUpdateRuntime({
123
144
 
124
145
  if (result.kind === "current") {
125
146
  dispatch({ type: "SET_UPDATE_AVAILABLE", release: null });
126
- dispatch({ type: "SET_UPDATE_NOTICE", notice: `Already on v${VERSION}` });
147
+ dispatch({ type: "SET_UPDATE_NOTICE", notice: result.notice ?? `Already on v${VERSION}` });
127
148
  return;
128
149
  }
129
150
 
@@ -147,6 +168,44 @@ export function useAppUpdateRuntime({
147
168
  return () => { clearInterval(interval); };
148
169
  }, [enabled, isDetachedWindow, runUpdateCheck]);
149
170
 
171
+ // A channel switch checks again at once and says what it found: nightly
172
+ // starts on today's build, stable says when this nightly build stays. An
173
+ // update already downloading or waiting for a restart is left to the effect
174
+ // below, which takes it back once it is ready if the channel rules it out.
175
+ const checkedChannelRef = useRef(updateChannel);
176
+ useEffect(() => {
177
+ if (!enabled || isDetachedWindow || checkedChannelRef.current === updateChannel) return;
178
+ checkedChannelRef.current = updateChannel;
179
+ if (updateInFlight(stateRef.current.updateProgress)) return;
180
+ // What the other channel offered, or how its download failed, is not this one's answer.
181
+ if (stateRef.current.updateProgress) dispatch({ type: "SET_UPDATE_PROGRESS", progress: null });
182
+ dispatch({ type: "SET_UPDATE_AVAILABLE", release: null });
183
+ void runUpdateCheck(true);
184
+ }, [dispatch, enabled, isDetachedWindow, runUpdateCheck, stateRef, updateChannel]);
185
+
186
+ // A downloaded update the channel does not allow (a nightly build waiting
187
+ // for a restart after the switch back to stable, or one that finished
188
+ // downloading after it) is taken back rather than installed by the restart,
189
+ // and stable checks again.
190
+ const discardingRef = useRef(false);
191
+ useEffect(() => {
192
+ if (!enabled || isDetachedWindow || discardingRef.current) return;
193
+ const { updateProgress: progress, updateAvailable: staged } = stateRef.current;
194
+ if (progress?.phase !== "ready" || stagedUpdateFitsChannel(staged, updateChannel)) return;
195
+ discardingRef.current = true;
196
+ void discardStagedUpdate().then((error) => {
197
+ discardingRef.current = false;
198
+ if (error) {
199
+ updateLog.warn("could not take back the staged update", { error, version: staged?.version });
200
+ dispatch({ type: "SET_UPDATE_NOTICE", notice: `Could not take back v${staged?.version ?? "?"}: ${error}` });
201
+ return;
202
+ }
203
+ dispatch({ type: "SET_UPDATE_PROGRESS", progress: null });
204
+ dispatch({ type: "SET_UPDATE_AVAILABLE", release: null });
205
+ void runUpdateCheck(true);
206
+ });
207
+ }, [dispatch, enabled, isDetachedWindow, runUpdateCheck, stateRef, updateAvailable, updateChannel, updateProgress]);
208
+
150
209
  // First launch on a new version: show that release's notes, then remember the version.
151
210
  useEffect(() => {
152
211
  if (!enabled || isDetachedWindow) return;
@@ -156,7 +215,8 @@ export function useAppUpdateRuntime({
156
215
  const nextConfig = { ...config, lastLaunchedVersion: VERSION };
157
216
  dispatch({ type: "SET_CONFIG", config: nextConfig });
158
217
  void saveConfigImmediately(nextConfig).catch(() => {});
159
- if (!isUpgrade) return;
218
+ // Nightly builds come daily and the changelog lists stable releases only.
219
+ if (!isUpgrade || isPrereleaseVersion(VERSION)) return;
160
220
  // Opened by the app, not the user: usage counts leave it out.
161
221
  void runAutomated(() => pluginRegistry.createPaneFromTemplateAsync("changelog-pane", {
162
222
  values: { version: VERSION },
package/src/app.tsx CHANGED
@@ -50,6 +50,7 @@ import { bindPluginRegistryRuntimeAccess } from "./app/runtime/plugin-bindings";
50
50
  import { useAppStartupRuntime } from "./app/runtime/startup";
51
51
  import { useTickerRefreshRuntime } from "./app/runtime/ticker-refresh";
52
52
  import { useAppUpdateRuntime } from "./app/runtime/update";
53
+ import { resolveUpdateChannel } from "./updater/channel";
53
54
  import { createCoreSyncContributors } from "./sync/core-contributors";
54
55
  import { useCloudSyncRuntime } from "./sync/react";
55
56
  import { RemoteControlHost, type RemoteControlAdapter } from "./remote/app-host";
@@ -278,6 +279,7 @@ function AppInner({
278
279
  pluginRegistry,
279
280
  stateRef,
280
281
  updateAvailable: state.updateAvailable,
282
+ updateChannel: resolveUpdateChannel(state.config),
281
283
  updateCheckInProgress: state.updateCheckInProgress,
282
284
  updateProgress: state.updateProgress,
283
285
  });
@@ -3,7 +3,6 @@
3
3
  "src/app.tsx": ["cloud"],
4
4
  "src/app/global-shortcuts.ts": ["help"],
5
5
  "src/app/pane-runtime/plugin-bindings.ts": ["broker-manager","layout-manager"],
6
- "src/app/runtime/broker-import.ts": ["portfolio-list"],
7
6
  "src/app/runtime/desktop-deeplink.ts": ["account-management","chat"],
8
7
  "src/app/runtime/startup.ts": ["chat"],
9
8
  "src/cli/commands/market.ts": ["holders","insider","research","sec"],
@@ -19,21 +18,14 @@
19
18
  "src/cli/pane-functions/resolver.ts": ["chart-composer"],
20
19
  "src/cli/pane-functions/screenshot-evidence.ts": ["attention","awards","catalysts","company-attention","company-kpis","credit-documents","exposure","market-heatmap","options-calculator","options-scenario","power","realized-vol","supply-chain","vol-surface","volatility","world-venue-map"],
21
20
  "src/cli/pane-functions/screenshot.ts": ["chart-composer","econ-statistics","market-valuation","portfolio-list","ticker-detail","time-sales"],
22
- "src/components/command-bar/commands/collection.ts": ["portfolio-list"],
23
- "src/components/command-bar/commands/direct/index.ts": ["broker-manager","portfolio-list"],
24
- "src/components/command-bar/routes/root/command-items.ts": ["portfolio-list"],
21
+ "src/components/command-bar/commands/direct/index.ts": ["broker-manager"],
25
22
  "src/components/command-bar/routes/root/help-items.ts": ["help"],
26
- "src/components/command-bar/routes/root/shortcut-items.ts": ["portfolio-list"],
27
23
  "src/components/command-bar/workflow/builtin.ts": ["portfolio-list"],
28
- "src/components/command-bar/workflow/collection-actions.ts": ["portfolio-list"],
29
24
  "src/components/command-bar/workflow/ops.ts": ["correlation","portfolio-list","ticker-detail"],
30
- "src/components/command-bar/workflow/tickers.ts": ["portfolio-list"],
31
25
  "src/components/form-modal/routes.ts": ["portfolio-list"],
32
26
  "src/components/index.ts": ["portfolio-list"],
33
27
  "src/components/layout/shell/index.tsx": ["cloud"],
34
28
  "src/components/onboarding/use-onboarding-upgrade.ts": ["chat"],
35
- "src/components/onboarding/wizard-model.ts": ["portfolio-list"],
36
- "src/components/onboarding/wizard-positions.ts": ["portfolio-list"],
37
29
  "src/layout/desks.ts": ["chart-composer"],
38
30
  "src/plugins/catalog-browser.ts": ["alerts","analytics","attention","awards","backtest","bond-calculator","catalysts","cds","central-bank-rates","changelog","chart-composer","clinical-trials","cloud","comment-letters","commodities-energy","commodity-balances","company-attention","company-kpis","connections","correlation","cot","cpi","credit-boards","credit-conditions","credit-documents","crypto-board","debt-maturities","dividend-yield","docs","doe","earnings-calls","earnings-ripple","econ","econ-statistics","equity-screener","estimate-revisions","executives","exposure","fear-greed","filing-events","fixed-income","futures","futures-curve","fx-matrix","gpu","help","insider","ipo-calendar","iv-history","jobs","jodi","kelly-sizer","layout-manager","macro-day","market-halts","market-heatmap","market-valuation","members","mna","money-markets","news","openfda","options","options-calculator","options-positioning","options-scenario","pe-band","perps","portfolio-list","power","primary-dealers","rate-path","realized-vol","relative-rotation","research","research-search","revenue-breakdown","reverse-dcf","risk-factors","scanner","seasonality","sec","sectors","short-interest","short-volume","social-mentions","supply-chain","ticker-detail","time-sales","trace-bonds","treasury-auctions","treasury-daily","vol-surface","volatility","world-indices","world-venue-map","yield-curve"],
39
31
  "src/plugins/catalog-ui.ts": ["alerts","clinical-trials","cloud","comment-letters","custom-view","fear-greed","ipo-calendar","market-halts","market-heatmap","members","news","notes","openfda","research-search"],
@@ -43,5 +35,5 @@
43
35
  "src/renderers/electrobun/view/cli-pane-shot-entry.tsx": ["econ-statistics","market-valuation","time-sales"],
44
36
  "src/renderers/electrobun/view/main.tsx": ["notes"],
45
37
  "src/renderers/electrobun/view/remote/connection-health.ts": ["connections"],
46
- "src/sync/core-contributors.ts": ["analytics","portfolio-list"]
38
+ "src/sync/core-contributors.ts": ["analytics"]
47
39
  }
@@ -41,7 +41,7 @@ function scalarArgs(source: ArgSource, privateFields: PaneSharePrivateFields | u
41
41
  * that opens this pane (a chart opened with `GP` stays `GP`), else the first
42
42
  * template that opens it.
43
43
  */
44
- function functionCode(registry: Pick<PluginRegistry, "paneTemplates">, paneType: string, title: string): string | null {
44
+ export function functionCode(registry: Pick<PluginRegistry, "paneTemplates">, paneType: string, title: string): string | null {
45
45
  const prefixes = [...registry.paneTemplates.values()]
46
46
  .filter((template) => template.paneId === paneType && template.shortcut?.prefix)
47
47
  .map((template) => template.shortcut!.prefix.toUpperCase());
@@ -50,6 +50,16 @@ function functionCode(registry: Pick<PluginRegistry, "paneTemplates">, paneType:
50
50
  return prefixes[0] ?? null;
51
51
  }
52
52
 
53
+ /**
54
+ * A pane named by its function and what it shows, such as `DES AAPL`: its
55
+ * title, led by its function code unless the title already starts with it.
56
+ */
57
+ export function paneFunctionLabel(pane: { title: string; code: string | null; paneType: string }): string {
58
+ const title = pane.title.trim() || pane.paneType;
59
+ if (!pane.code) return title;
60
+ return title.split(/\s+/)[0]?.toUpperCase() === pane.code ? title : `${pane.code} ${title}`;
61
+ }
62
+
53
63
  export interface PaneAttachmentSource {
54
64
  registry: Pick<PluginRegistry, "paneTemplates" | "brokers" | "getPanePluginId" | "getBrokerPluginId">;
55
65
  pane: Pick<ResolvedPane, "instance" | "def">;
@@ -181,10 +181,17 @@ export function clearBrokerInstanceTickerData(
181
181
  brokerPortfolioIds: Set<string>,
182
182
  /** Portfolios this profile just took over: whatever another profile imported there goes too. */
183
183
  takenOverPortfolioIds: ReadonlySet<string> = new Set(),
184
+ /**
185
+ * Portfolios this sync writes. A position there without a profile came from
186
+ * another device's sync of the same account, so this one replaces it too;
187
+ * one entered by hand stays.
188
+ */
189
+ syncedPortfolioIds: ReadonlySet<string> = new Set(),
184
190
  ): TickerRecord | null {
185
191
  const positions = ticker.metadata.positions.filter((position) =>
186
192
  position.brokerInstanceId !== instanceId
187
193
  && !(takenOverPortfolioIds.has(position.portfolio) && position.brokerInstanceId)
194
+ && !(syncedPortfolioIds.has(position.portfolio) && !position.brokerInstanceId && position.broker !== "manual")
188
195
  );
189
196
  const remainingPositionPortfolios = new Set(positions.map((position) => position.portfolio));
190
197
  const portfolios = ticker.metadata.portfolios.filter((portfolioId) =>
@@ -211,6 +218,65 @@ export function clearBrokerInstanceTickerData(
211
218
  };
212
219
  }
213
220
 
221
+ /** The profile a broker portfolio id was made for: `broker:<profile>:<account>`. */
222
+ function brokerInstanceIdOfPortfolio(portfolioId: string): string | undefined {
223
+ return isBrokerPortfolioId(portfolioId) ? portfolioId.split(":")[1] || undefined : undefined;
224
+ }
225
+
226
+ /** The broker portfolios and profiles that still exist somewhere this device knows of. */
227
+ export interface KnownBrokerHoldings {
228
+ portfolioIds: ReadonlySet<string>;
229
+ instanceIds: ReadonlySet<string>;
230
+ }
231
+
232
+ export function knownBrokerHoldings(
233
+ ...sources: Array<{ portfolios?: ReadonlyArray<{ id: string }>; brokerInstances?: ReadonlyArray<{ id: string }> }>
234
+ ): KnownBrokerHoldings {
235
+ return {
236
+ portfolioIds: new Set(sources.flatMap((source) => source.portfolios?.map((portfolio) => portfolio.id) ?? [])),
237
+ instanceIds: new Set(sources.flatMap((source) => source.brokerInstances?.map((instance) => instance.id) ?? [])),
238
+ };
239
+ }
240
+
241
+ /**
242
+ * A broker portfolio that is gone, along with the profile it was made for. One
243
+ * that still has its profile stays: that profile restores it on its next sync.
244
+ */
245
+ function isGoneBrokerPortfolio(portfolioId: string, known: KnownBrokerHoldings): boolean {
246
+ if (!isBrokerPortfolioId(portfolioId) || known.portfolioIds.has(portfolioId)) return false;
247
+ const instanceId = brokerInstanceIdOfPortfolio(portfolioId);
248
+ return !instanceId || !known.instanceIds.has(instanceId);
249
+ }
250
+
251
+ function isLeftoverBrokerPosition(position: TickerPosition, known: KnownBrokerHoldings): boolean {
252
+ return isGoneBrokerPortfolio(position.portfolio, known)
253
+ && !(position.brokerInstanceId && known.instanceIds.has(position.brokerInstanceId));
254
+ }
255
+
256
+ /**
257
+ * The ticker without what removed broker profiles left on it: positions and
258
+ * links in portfolios that are gone with their profile, and contracts of
259
+ * profiles that are gone. A pull from another device brings such positions
260
+ * without the profile that imported them, as a profile never leaves its
261
+ * device, so nothing else ever clears them. Null when there is none.
262
+ */
263
+ export function clearLeftoverBrokerHoldings(ticker: TickerRecord, known: KnownBrokerHoldings): TickerRecord | null {
264
+ const { metadata } = ticker;
265
+ const positions = metadata.positions.filter((position) => !isLeftoverBrokerPosition(position, known));
266
+ const held = new Set(positions.map((position) => position.portfolio));
267
+ const portfolios = metadata.portfolios.filter((portfolioId) => held.has(portfolioId) || !isGoneBrokerPortfolio(portfolioId, known));
268
+ const contracts = metadata.broker_contracts ?? [];
269
+ const brokerContracts = contracts.filter((contract) => !contract.brokerInstanceId || known.instanceIds.has(contract.brokerInstanceId));
270
+ if (
271
+ positions.length === metadata.positions.length
272
+ && portfolios.length === metadata.portfolios.length
273
+ && brokerContracts.length === contracts.length
274
+ ) {
275
+ return null;
276
+ }
277
+ return { ...ticker, metadata: { ...metadata, positions, portfolios, broker_contracts: brokerContracts } };
278
+ }
279
+
214
280
  function inferBrokerAccountId(position: TickerPosition, portfolioId: string, instanceId: string): string | undefined {
215
281
  if (position.brokerAccountId) return position.brokerAccountId;
216
282
  const prefix = `broker:${instanceId}:`;
@@ -7,9 +7,11 @@ import type { TickerRecord } from "../types/ticker";
7
7
  import { getBrokerInstance } from "../utils/broker-instances";
8
8
  import {
9
9
  clearBrokerInstanceTickerData,
10
+ clearLeftoverBrokerHoldings,
10
11
  ensureBrokerPortfolio,
11
12
  findReusableBrokerPortfolioId,
12
13
  isHeldByOtherSignedInProfile,
14
+ knownBrokerHoldings,
13
15
  removeStaleBrokerPortfolios,
14
16
  resolvePortfolioBrokerId,
15
17
  } from "./broker-portfolio-sync";
@@ -33,6 +35,12 @@ export interface SyncBrokerInstanceArgs {
33
35
  deferPersistence?: boolean;
34
36
  }
35
37
 
38
+ /** The app as it is when a sync commits, which may be seconds after the sync read it. */
39
+ interface BrokerSyncCommitTarget {
40
+ tickers: ReadonlyMap<string, TickerRecord>;
41
+ config: Pick<AppConfig, "portfolios" | "brokerInstances">;
42
+ }
43
+
36
44
  export interface SyncBrokerInstanceResult {
37
45
  config: AppConfig;
38
46
  tickers: Map<string, TickerRecord>;
@@ -41,6 +49,15 @@ export interface SyncBrokerInstanceResult {
41
49
  portfolioIds: string[];
42
50
  addedTickers: TickerRecord[];
43
51
  updatedTickers: TickerRecord[];
52
+ /**
53
+ * Carries the sync onto the app as it is now, which may be seconds after the
54
+ * sync read it, so what changed while the broker answered (a pull from
55
+ * another device, an edit) stays; `tickers`, `addedTickers` and
56
+ * `updatedTickers` then hold the sync over it. Hand them to the app before
57
+ * anything waits: a pull landing in a wait would be overwritten in turn.
58
+ */
59
+ rebase(onto: BrokerSyncCommitTarget): void;
60
+ /** Writes the sync's tickers and broker accounts. */
44
61
  commit(): Promise<void>;
45
62
  }
46
63
 
@@ -310,62 +327,78 @@ export async function syncBrokerInstance({
310
327
  : []);
311
328
 
312
329
  nextConfig = removeStaleBrokerPortfolios(nextConfig, instance.id, currentPortfolioIds);
313
- const cleanedTickers = new Map<string, TickerRecord>();
314
- for (const ticker of tickers.values()) {
315
- const cleanedTicker = clearBrokerInstanceTickerData(ticker, instance.id, brokerPortfolioIds, takenOverPortfolioIds);
316
- if (!cleanedTicker) continue;
317
- tickers.set(cleanedTicker.metadata.ticker, cleanedTicker);
318
- cleanedTickers.set(cleanedTicker.metadata.ticker, cleanedTicker);
319
- }
320
-
321
330
  nextConfig = await maybePersistResolvedBrokerConfig(nextConfig, instance, broker, persistResolvedBrokerConfig);
322
331
  throwIfBrokerImportCancelled(signal);
323
332
  nextConfig = markBrokerInstanceSynced(nextConfig, instance.id, syncedAt);
333
+ const syncedConfig = nextConfig;
334
+
335
+ // What the sync does to the tickers, given them: it runs on the tickers it
336
+ // read, and again on the live ones when it commits after the broker answered.
337
+ const writeTickers = (source: ReadonlyMap<string, TickerRecord>, liveConfig?: BrokerSyncCommitTarget["config"]) => {
338
+ const tickers = new Map(source);
339
+ // Only a portfolio and a profile gone everywhere this device knows of make a leftover.
340
+ const known = knownBrokerHoldings(config, syncedConfig, ...(liveConfig ? [liveConfig] : []));
341
+ const cleanedTickers = new Map<string, TickerRecord>();
342
+ for (const ticker of tickers.values()) {
343
+ const cleared = clearBrokerInstanceTickerData(
344
+ ticker, instance.id, brokerPortfolioIds, takenOverPortfolioIds, currentPortfolioIds,
345
+ );
346
+ const cleanedTicker = clearLeftoverBrokerHoldings(cleared ?? ticker, known) ?? cleared;
347
+ if (!cleanedTicker) continue;
348
+ tickers.set(cleanedTicker.metadata.ticker, cleanedTicker);
349
+ cleanedTickers.set(cleanedTicker.metadata.ticker, cleanedTicker);
350
+ }
324
351
 
325
- const addedTickers = new Map<string, TickerRecord>();
326
- const updatedTickers = new Map<string, TickerRecord>();
352
+ const addedTickers = new Map<string, TickerRecord>();
353
+ const updatedTickers = new Map<string, TickerRecord>();
354
+
355
+ for (const position of positions) {
356
+ throwIfBrokerImportCancelled(signal);
357
+ const portfolioId = findReusableBrokerPortfolioId(syncedConfig, instance, position.accountId, portfolioBrokerId);
358
+ if (heldElsewhere.has(portfolioId)) continue;
359
+ const { ticker, created } = upsertBrokerPositionTicker({
360
+ tickers,
361
+ instance,
362
+ portfolioId,
363
+ position,
364
+ });
365
+ if (created) {
366
+ addedTickers.set(position.ticker, ticker);
367
+ } else {
368
+ cleanedTickers.delete(ticker.metadata.ticker);
369
+ if (!addedTickers.has(position.ticker)) {
370
+ updatedTickers.set(position.ticker, ticker);
371
+ }
372
+ }
327
373
 
328
- for (const position of positions) {
329
- throwIfBrokerImportCancelled(signal);
330
- const portfolioId = findReusableBrokerPortfolioId(nextConfig, instance, position.accountId, portfolioBrokerId);
331
- if (heldElsewhere.has(portfolioId)) continue;
332
- const { ticker, created } = upsertBrokerPositionTicker({
333
- tickers,
334
- instance,
335
- portfolioId,
336
- position,
337
- });
338
- if (created) {
339
- addedTickers.set(position.ticker, ticker);
340
- } else {
341
- cleanedTickers.delete(ticker.metadata.ticker);
342
- if (!addedTickers.has(position.ticker)) {
374
+ tickers.set(position.ticker, ticker);
375
+ if (addedTickers.has(position.ticker)) {
376
+ addedTickers.set(position.ticker, ticker);
377
+ } else {
343
378
  updatedTickers.set(position.ticker, ticker);
344
379
  }
345
380
  }
346
381
 
347
- tickers.set(position.ticker, ticker);
348
- if (addedTickers.has(position.ticker)) {
349
- addedTickers.set(position.ticker, ticker);
350
- } else {
351
- updatedTickers.set(position.ticker, ticker);
352
- }
353
- }
354
-
355
- for (const ticker of cleanedTickers.values()) {
356
- if (!addedTickers.has(ticker.metadata.ticker) && !updatedTickers.has(ticker.metadata.ticker)) {
357
- updatedTickers.set(ticker.metadata.ticker, ticker);
382
+ for (const ticker of cleanedTickers.values()) {
383
+ if (!addedTickers.has(ticker.metadata.ticker) && !updatedTickers.has(ticker.metadata.ticker)) {
384
+ updatedTickers.set(ticker.metadata.ticker, ticker);
385
+ }
358
386
  }
359
- }
387
+ return { tickers, addedTickers: [...addedTickers.values()], updatedTickers: [...updatedTickers.values()] };
388
+ };
360
389
 
390
+ let written = writeTickers(tickers);
361
391
  let committed = false;
392
+ const rebase = (onto: BrokerSyncCommitTarget) => {
393
+ if (!committed) written = writeTickers(onto.tickers, onto.config);
394
+ };
362
395
  const commit = async () => {
363
396
  if (committed) return;
364
397
  throwIfBrokerImportCancelled(signal);
365
398
  if (resources) {
366
399
  persistBrokerAccounts(resources, instance, broker, brokerAccounts);
367
400
  }
368
- await Promise.all([...addedTickers.values(), ...updatedTickers.values()].map((ticker) => (
401
+ await Promise.all([...written.addedTickers, ...written.updatedTickers].map((ticker) => (
369
402
  tickerRepository.saveTicker(ticker)
370
403
  )));
371
404
  committed = true;
@@ -377,12 +410,13 @@ export async function syncBrokerInstance({
377
410
 
378
411
  return {
379
412
  config: nextConfig,
380
- tickers,
413
+ get tickers() { return written.tickers; },
381
414
  brokerAccounts,
382
415
  positions,
383
416
  portfolioIds,
384
- addedTickers: [...addedTickers.values()],
385
- updatedTickers: [...updatedTickers.values()],
417
+ get addedTickers() { return written.addedTickers; },
418
+ get updatedTickers() { return written.updatedTickers; },
419
+ rebase,
386
420
  commit,
387
421
  };
388
422
  }