gloomberb 0.10.4 → 0.11.0

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 (555) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +30 -3
  3. package/README.zh-CN.md +1 -1
  4. package/package.json +5 -5
  5. package/src/api-client/auth.ts +34 -0
  6. package/src/api-client/data.ts +25 -0
  7. package/src/api-client/index.test.ts +81 -0
  8. package/src/api-client/index.ts +37 -0
  9. package/src/api-client/request.test.ts +30 -0
  10. package/src/api-client/request.ts +44 -20
  11. package/src/api-client/socket-health.test.ts +79 -0
  12. package/src/api-client/socket.ts +108 -5
  13. package/src/api-client/types.ts +139 -0
  14. package/src/app/global-shortcuts.test.tsx +144 -9
  15. package/src/app/global-shortcuts.ts +33 -11
  16. package/src/app/runtime/broker-import.ts +39 -5
  17. package/src/app/runtime/startup.ts +7 -1
  18. package/src/app/runtime/update.ts +26 -1
  19. package/src/app.tsx +36 -19
  20. package/src/brokers/broker-ticker-sync.ts +3 -8
  21. package/src/brokers/sync-broker-instance.test.ts +59 -0
  22. package/src/brokers/sync-broker-instance.ts +37 -13
  23. package/src/capabilities/chart-series.ts +318 -0
  24. package/src/capabilities/factories.ts +46 -0
  25. package/src/capabilities/index.ts +1 -0
  26. package/src/capabilities/registry.test.ts +135 -0
  27. package/src/capabilities/registry.ts +31 -4
  28. package/src/capabilities/types.ts +52 -0
  29. package/src/cli/commands/overview.ts +2 -2
  30. package/src/cli/desktop-pane-shot.ts +2 -0
  31. package/src/cli/entry.ts +11 -1
  32. package/src/cli/native-smoke.ts +5 -0
  33. package/src/cli/pane-functions/catalog.ts +4 -0
  34. package/src/cli/pane-functions/data.ts +6 -1
  35. package/src/cli/pane-functions/discovery.ts +76 -10
  36. package/src/cli/pane-functions/report.test.ts +76 -0
  37. package/src/cli/pane-functions/report.ts +7 -1
  38. package/src/cli/pane-functions/resolver.ts +4 -1
  39. package/src/cli/pane-functions/screenshot.test.ts +56 -3
  40. package/src/cli/pane-functions/screenshot.ts +68 -5
  41. package/src/components/chart/composite/composite-chart.test.tsx +54 -0
  42. package/src/components/chart/composite/composite-chart.tsx +92 -15
  43. package/src/components/chart/composite/downsample.test.ts +136 -0
  44. package/src/components/chart/composite/downsample.ts +208 -0
  45. package/src/components/chart/composite/panel-series.test.ts +66 -0
  46. package/src/components/chart/composite/panel-series.ts +101 -0
  47. package/src/components/chart/multi-line/renderer.ts +1 -0
  48. package/src/components/chart/native/kitty/index.test.ts +25 -3
  49. package/src/components/chart/native/kitty/support.ts +37 -3
  50. package/src/components/chart/native/use-chart-text-fallback.ts +19 -0
  51. package/src/components/chart/static/chart/surface.tsx +43 -12
  52. package/src/components/chart/static/multi-line-chart-surface.tsx +41 -5
  53. package/src/components/command-bar/commands/direct/index.ts +18 -0
  54. package/src/components/command-bar/commands/registry.ts +14 -0
  55. package/src/components/command-bar/routes/root/article-results.test.ts +83 -0
  56. package/src/components/command-bar/routes/root/article-results.ts +55 -0
  57. package/src/components/command-bar/routes/root/command-items.ts +3 -0
  58. package/src/components/command-bar/routes/root/provider-search.ts +7 -3
  59. package/src/components/command-bar/routes/root/results.ts +38 -11
  60. package/src/components/command-bar/routes/root/runtime.ts +6 -2
  61. package/src/components/command-bar/routes/ticker-search/actions.ts +1 -1
  62. package/src/components/command-bar/routes/ticker-search/results.test.ts +34 -0
  63. package/src/components/command-bar/routes/ticker-search/results.ts +4 -6
  64. package/src/components/command-bar/routes/ticker-search/route.ts +2 -2
  65. package/src/components/command-bar/routing/list-state.ts +2 -1
  66. package/src/components/command-bar/surface/assist.test.tsx +13 -6
  67. package/src/components/command-bar/surface/index.test.tsx +65 -0
  68. package/src/components/command-bar/surface/index.tsx +35 -0
  69. package/src/components/command-bar/view-model.test.ts +43 -0
  70. package/src/components/command-bar/view-model.ts +41 -4
  71. package/src/components/command-bar/workflow/collection-actions.test.ts +94 -0
  72. package/src/components/command-bar/workflow/collection-actions.ts +14 -3
  73. package/src/components/command-bar/workflow/ops.test.ts +136 -0
  74. package/src/components/command-bar/workflow/ops.ts +71 -1
  75. package/src/components/index.ts +1 -1
  76. package/src/components/layout/header.test.tsx +35 -0
  77. package/src/components/layout/header.tsx +101 -9
  78. package/src/components/layout/pane/cycle-order.test.ts +0 -1
  79. package/src/components/layout/pane/footer/index.test.tsx +19 -19
  80. package/src/components/layout/pane/header.tsx +2 -13
  81. package/src/components/layout/shell/index.tsx +3 -0
  82. package/src/components/layout/status-bar.test.tsx +36 -1
  83. package/src/components/layout/status-bar.tsx +48 -2
  84. package/src/components/onboarding/account-step/chooser-panel.tsx +27 -50
  85. package/src/components/onboarding/account-step/form-panel.tsx +85 -29
  86. package/src/components/onboarding/account-step/index.tsx +10 -1
  87. package/src/components/onboarding/account-step/model.ts +3 -3
  88. package/src/components/onboarding/account-step/qr-panel.tsx +68 -0
  89. package/src/components/onboarding/account-step/signed-in-panel.tsx +4 -7
  90. package/src/components/onboarding/onboarding-frame.tsx +517 -0
  91. package/src/components/onboarding/onboarding-steps.tsx +0 -264
  92. package/src/components/onboarding/onboarding-wizard.test.tsx +626 -200
  93. package/src/components/onboarding/onboarding-wizard.tsx +848 -266
  94. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +81 -51
  95. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +14 -20
  96. package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +8 -11
  97. package/src/components/onboarding/portfolio-step/choose-panel.tsx +18 -22
  98. package/src/components/onboarding/portfolio-step/index.tsx +3 -0
  99. package/src/components/onboarding/portfolio-step/types.ts +3 -0
  100. package/src/components/onboarding/wizard-account.ts +38 -3
  101. package/src/components/onboarding/wizard-broker-sync.ts +45 -67
  102. package/src/components/onboarding/wizard-model.ts +18 -2
  103. package/src/components/ui/data-table/opentui/index.tsx +29 -9
  104. package/src/components/ui/data-table/types.ts +2 -0
  105. package/src/components/ui/external-link.tsx +4 -2
  106. package/src/components/ui/loading.tsx +1 -1
  107. package/src/components/ui/status.tsx +76 -4
  108. package/src/components/ui/table-layout.test.ts +24 -0
  109. package/src/components/ui/table-layout.ts +90 -23
  110. package/src/components/ui/tabs.tsx +86 -4
  111. package/src/core/app-services.ts +14 -3
  112. package/src/core/connection-health.test.ts +143 -0
  113. package/src/core/connection-health.ts +295 -0
  114. package/src/core/state/app/layout-reducer.ts +34 -0
  115. package/src/core/state/app/layout.ts +21 -0
  116. package/src/core/state/app/state.ts +10 -0
  117. package/src/core/state/app/types.ts +7 -1
  118. package/src/data/config/chart-settings.ts +11 -2
  119. package/src/data/config/store/index.test.ts +83 -9
  120. package/src/data/config/store/migrations.ts +16 -0
  121. package/src/data/config/store/normalize.ts +59 -1
  122. package/src/data/fred-series.ts +8 -2
  123. package/src/i18n/es.ts +126 -0
  124. package/src/i18n/ja.ts +126 -0
  125. package/src/i18n/ko.ts +126 -0
  126. package/src/i18n/zh-cn.ts +126 -0
  127. package/src/i18n/zh-tw.ts +126 -0
  128. package/src/market-data/coordinator/chart.ts +3 -2
  129. package/src/market-data/hooks.tsx +3 -2
  130. package/src/market-data/market/status.test.ts +12 -0
  131. package/src/market-data/market/status.ts +34 -17
  132. package/src/news/aggregator.test.ts +39 -0
  133. package/src/news/aggregator.ts +78 -22
  134. package/src/news/hooks.ts +4 -0
  135. package/src/notifications/app-notifier.test.ts +28 -6
  136. package/src/notifications/app-notifier.ts +24 -5
  137. package/src/plugins/broker-sync/broker-sync.test.ts +130 -0
  138. package/src/plugins/broker-sync/native-loader.ts +10 -0
  139. package/src/plugins/broker-sync/normalize.ts +271 -0
  140. package/src/plugins/broker-sync/public.ts +122 -0
  141. package/src/plugins/broker-sync/robinhood-native.ts +367 -0
  142. package/src/plugins/broker-sync/robinhood.ts +114 -0
  143. package/src/plugins/broker-sync/simplefin-native.ts +201 -0
  144. package/src/plugins/broker-sync/simplefin.ts +70 -0
  145. package/src/plugins/builtin/account-management/footer.ts +8 -19
  146. package/src/plugins/builtin/account-management/pane.tsx +13 -8
  147. package/src/plugins/builtin/ai/pi/host.ts +6 -5
  148. package/src/plugins/builtin/ai/providers.ts +1 -1
  149. package/src/plugins/builtin/ai/screener/footer.ts +0 -7
  150. package/src/plugins/builtin/ai/screener/pane.test.tsx +2 -1
  151. package/src/plugins/builtin/ai/screener/pane.tsx +3 -1
  152. package/src/plugins/builtin/ai/screener/results-view.tsx +8 -0
  153. package/src/plugins/builtin/ai/screener/runner.ts +7 -3
  154. package/src/plugins/builtin/ai/workspace/pane.tsx +13 -2
  155. package/src/plugins/builtin/alerts/alert-engine.ts +13 -0
  156. package/src/plugins/builtin/alerts/constants.ts +2 -1
  157. package/src/plugins/builtin/alerts/index.tsx +67 -36
  158. package/src/plugins/builtin/alerts/pane.tsx +17 -65
  159. package/src/plugins/builtin/analytics/index.tsx +24 -11
  160. package/src/plugins/builtin/analytics/pane-model.ts +48 -14
  161. package/src/plugins/builtin/analytics/portfolio-selection.ts +19 -0
  162. package/src/plugins/builtin/analytics/view.tsx +5 -3
  163. package/src/plugins/builtin/broker-manager/detail.tsx +7 -2
  164. package/src/plugins/builtin/broker-manager/footer.ts +4 -9
  165. package/src/plugins/builtin/broker-manager/index.tsx +1 -6
  166. package/src/plugins/builtin/buildout/detail/ui.tsx +15 -4
  167. package/src/plugins/builtin/buildout/pane/header.tsx +1 -0
  168. package/src/plugins/builtin/changelog/index.tsx +60 -45
  169. package/src/plugins/builtin/chart-composer/catalog-inventory.test.ts +97 -0
  170. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +366 -0
  171. package/src/plugins/builtin/chart-composer/chart-spec.ts +21 -1
  172. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +307 -0
  173. package/src/plugins/builtin/chart-composer/editor/controller.ts +1 -1
  174. package/src/plugins/builtin/chart-composer/index.tsx +45 -5
  175. package/src/plugins/builtin/chart-composer/pane.tsx +7 -88
  176. package/src/plugins/builtin/chart-composer/presets.test.ts +48 -0
  177. package/src/plugins/builtin/chart-composer/presets.ts +69 -3
  178. package/src/plugins/builtin/chart-composer/quick-add.tsx +8 -5
  179. package/src/plugins/builtin/chart-composer/semantic.ts +18 -14
  180. package/src/plugins/builtin/chart-composer/series-catalog.test.ts +39 -0
  181. package/src/plugins/builtin/chart-composer/series-catalog.ts +104 -3
  182. package/src/plugins/builtin/chart-composer/shortcuts.test.ts +10 -10
  183. package/src/plugins/builtin/chart-composer/shortcuts.ts +4 -12
  184. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +230 -23
  185. package/src/plugins/builtin/chat/chat.test.tsx +22 -0
  186. package/src/plugins/builtin/chat/content/composer.tsx +6 -4
  187. package/src/plugins/builtin/chat/content/index.tsx +7 -0
  188. package/src/plugins/builtin/chat/content/snapshot.ts +9 -2
  189. package/src/plugins/builtin/chat/content/transcript.tsx +10 -0
  190. package/src/plugins/builtin/chat/controller/channel-actions.ts +11 -1
  191. package/src/plugins/builtin/chat/controller/channels.ts +13 -3
  192. package/src/plugins/builtin/chat/controller/index.ts +61 -7
  193. package/src/plugins/builtin/chat/controller/lifecycle.ts +3 -0
  194. package/src/plugins/builtin/chat/controller/message-loading.ts +5 -1
  195. package/src/plugins/builtin/chat/controller/messages.ts +3 -1
  196. package/src/plugins/builtin/chat/controller/notifications.ts +32 -11
  197. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  198. package/src/plugins/builtin/chat/controller/view.ts +1 -0
  199. package/src/plugins/builtin/chat/controller.test.ts +117 -28
  200. package/src/plugins/builtin/cloud/auth-commands.ts +18 -0
  201. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +243 -0
  202. package/src/plugins/builtin/cloud/device-signin.test.ts +217 -0
  203. package/src/plugins/builtin/cloud/device-signin.ts +240 -0
  204. package/src/plugins/builtin/cloud/plugin.tsx +7 -1
  205. package/src/plugins/builtin/cloud-tweets/table.tsx +57 -10
  206. package/src/plugins/builtin/composite-plugins.ts +39 -5
  207. package/src/plugins/builtin/congress-trades/detail.tsx +2 -4
  208. package/src/plugins/builtin/congress-trades/footer.ts +7 -16
  209. package/src/plugins/builtin/congress-trades/pane.tsx +12 -20
  210. package/src/plugins/builtin/connections/index.ts +53 -0
  211. package/src/plugins/builtin/connections/pane.test.tsx +79 -0
  212. package/src/plugins/builtin/connections/pane.tsx +258 -0
  213. package/src/plugins/builtin/correlation/index.test.tsx +2 -1
  214. package/src/plugins/builtin/correlation/index.tsx +58 -19
  215. package/src/plugins/builtin/correlation/matrix/model.ts +18 -7
  216. package/src/plugins/builtin/correlation/relationship/pane.tsx +43 -19
  217. package/src/plugins/builtin/correlation/settings.ts +1 -1
  218. package/src/plugins/builtin/credit-conditions/client.test.ts +80 -0
  219. package/src/plugins/builtin/credit-conditions/client.ts +105 -0
  220. package/src/plugins/builtin/credit-conditions/index.test.tsx +111 -0
  221. package/src/plugins/builtin/credit-conditions/index.tsx +227 -0
  222. package/src/plugins/builtin/credit-conditions/model.test.ts +43 -0
  223. package/src/plugins/builtin/credit-conditions/model.ts +64 -0
  224. package/src/plugins/builtin/debug/index.tsx +11 -6
  225. package/src/plugins/builtin/dividend-yield/client.test.ts +58 -0
  226. package/src/plugins/builtin/dividend-yield/client.ts +252 -0
  227. package/src/plugins/builtin/dividend-yield/index.tsx +61 -0
  228. package/src/plugins/builtin/dividend-yield/model.ts +83 -0
  229. package/src/plugins/builtin/dividend-yield/pane.tsx +305 -0
  230. package/src/plugins/builtin/dividend-yield/types.ts +22 -0
  231. package/src/plugins/builtin/earnings/data/cache.test.ts +10 -7
  232. package/src/plugins/builtin/earnings/data/cache.ts +23 -9
  233. package/src/plugins/builtin/earnings/index.tsx +57 -6
  234. package/src/plugins/builtin/econ/calendar-model.test.ts +37 -3
  235. package/src/plugins/builtin/econ/calendar-model.ts +32 -26
  236. package/src/plugins/builtin/econ/fred-cache.test.ts +8 -1
  237. package/src/plugins/builtin/econ/fred-series-map.ts +18 -0
  238. package/src/plugins/builtin/econ/index.test.tsx +105 -0
  239. package/src/plugins/builtin/econ/index.tsx +95 -52
  240. package/src/plugins/builtin/fear-greed/cache.test.ts +18 -3
  241. package/src/plugins/builtin/fear-greed/cache.ts +18 -6
  242. package/src/plugins/builtin/fear-greed/charts.tsx +9 -2
  243. package/src/plugins/builtin/fear-greed/format.ts +0 -9
  244. package/src/plugins/builtin/fear-greed/pane.tsx +21 -21
  245. package/src/plugins/builtin/futures/contracts.ts +100 -0
  246. package/src/plugins/builtin/futures/index.tsx +298 -0
  247. package/src/plugins/builtin/futures/model.test.ts +129 -0
  248. package/src/plugins/builtin/futures/model.ts +136 -0
  249. package/src/plugins/builtin/futures/pane.test.tsx +239 -0
  250. package/src/plugins/builtin/futures/table.test.ts +155 -0
  251. package/src/plugins/builtin/futures/table.tsx +241 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +160 -134
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +12 -1
  254. package/src/plugins/builtin/fx-matrix/pane.test.tsx +109 -0
  255. package/src/plugins/builtin/help/components.tsx +1 -30
  256. package/src/plugins/builtin/help/index.tsx +5 -25
  257. package/src/plugins/builtin/holders/pane.tsx +17 -12
  258. package/src/plugins/builtin/holders/treemap.tsx +4 -2
  259. package/src/plugins/builtin/insider/index.tsx +20 -10
  260. package/src/plugins/builtin/ipo-calendar/cache.ts +101 -0
  261. package/src/plugins/builtin/ipo-calendar/client.test.ts +31 -0
  262. package/src/plugins/builtin/ipo-calendar/client.ts +296 -0
  263. package/src/plugins/builtin/ipo-calendar/index.tsx +65 -0
  264. package/src/plugins/builtin/ipo-calendar/model.ts +145 -0
  265. package/src/plugins/builtin/ipo-calendar/pane.tsx +328 -0
  266. package/src/plugins/builtin/ipo-calendar/types.ts +19 -0
  267. package/src/plugins/builtin/kelly-sizer/index.test.tsx +3 -3
  268. package/src/plugins/builtin/kelly-sizer/model.ts +8 -1
  269. package/src/plugins/builtin/kelly-sizer/pane.tsx +33 -15
  270. package/src/plugins/builtin/kelly-sizer/view.tsx +12 -5
  271. package/src/plugins/builtin/market-halts/client.ts +114 -0
  272. package/src/plugins/builtin/market-halts/index.tsx +29 -0
  273. package/src/plugins/builtin/market-halts/model.test.ts +187 -0
  274. package/src/plugins/builtin/market-halts/model.ts +291 -0
  275. package/src/plugins/builtin/market-halts/pane.tsx +256 -0
  276. package/src/plugins/builtin/market-heatmap/data.test.ts +1 -0
  277. package/src/plugins/builtin/market-heatmap/data.ts +12 -3
  278. package/src/plugins/builtin/market-heatmap/index.tsx +131 -46
  279. package/src/plugins/builtin/market-movers/index.tsx +145 -63
  280. package/src/plugins/builtin/market-movers/model.ts +19 -3
  281. package/src/plugins/builtin/market-movers/screener.test.ts +108 -0
  282. package/src/plugins/builtin/market-movers/screener.ts +168 -10
  283. package/src/plugins/builtin/news/index.tsx +78 -99
  284. package/src/plugins/builtin/news/wire/article-search.test.ts +72 -0
  285. package/src/plugins/builtin/news/wire/article-search.ts +140 -0
  286. package/src/plugins/builtin/news/wire/breaking/pane.tsx +18 -8
  287. package/src/plugins/builtin/news/wire/categories.ts +35 -0
  288. package/src/plugins/builtin/news/wire/default-feeds.ts +47 -1
  289. package/src/plugins/builtin/news/wire/feed-config.test.ts +10 -5
  290. package/src/plugins/builtin/news/wire/index.tsx +41 -4
  291. package/src/plugins/builtin/news/wire/industry-pane.tsx +38 -19
  292. package/src/plugins/builtin/news/wire/news/detail-view.tsx +10 -5
  293. package/src/plugins/builtin/news/wire/news/footer.ts +1 -0
  294. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +15 -6
  295. package/src/plugins/builtin/news/wire/news/query-presets.ts +16 -2
  296. package/src/plugins/builtin/news/wire/news/table.test.tsx +54 -0
  297. package/src/plugins/builtin/news/wire/news/table.tsx +111 -17
  298. package/src/plugins/builtin/notes/files.test.ts +18 -0
  299. package/src/plugins/builtin/notes/files.ts +10 -4
  300. package/src/plugins/builtin/notes/index.tsx +4 -1
  301. package/src/plugins/builtin/notes/quick-notes-pane.test.tsx +182 -0
  302. package/src/plugins/builtin/notes/quick-notes-pane.tsx +74 -31
  303. package/src/plugins/builtin/notes/ticker-notes-tab.test.tsx +380 -0
  304. package/src/plugins/builtin/notes/ticker-notes-tab.tsx +97 -40
  305. package/src/plugins/builtin/options/calc-seed.test.ts +118 -0
  306. package/src/plugins/builtin/options/calc-seed.ts +52 -0
  307. package/src/plugins/builtin/options/footer.ts +4 -1
  308. package/src/plugins/builtin/options/index.tsx +22 -1
  309. package/src/plugins/builtin/options/live-quotes.ts +8 -0
  310. package/src/plugins/builtin/options/table.ts +3 -1
  311. package/src/plugins/builtin/options/view.test.tsx +5 -1
  312. package/src/plugins/builtin/options/view.tsx +112 -17
  313. package/src/plugins/builtin/options-calculator/index.tsx +35 -0
  314. package/src/plugins/builtin/options-calculator/model.test.ts +172 -0
  315. package/src/plugins/builtin/options-calculator/model.ts +273 -0
  316. package/src/plugins/builtin/options-calculator/pane.test.tsx +138 -0
  317. package/src/plugins/builtin/options-calculator/pane.tsx +218 -0
  318. package/src/plugins/builtin/plugin-module.test.ts +16 -2
  319. package/src/plugins/builtin/plugin-module.ts +5 -1
  320. package/src/plugins/builtin/polls/client.ts +57 -0
  321. package/src/plugins/builtin/polls/hooks.ts +20 -0
  322. package/src/plugins/builtin/polls/index.tsx +35 -0
  323. package/src/plugins/builtin/polls/normalize.test.ts +230 -0
  324. package/src/plugins/builtin/polls/normalize.ts +319 -0
  325. package/src/plugins/builtin/polls/pane.tsx +722 -0
  326. package/src/plugins/builtin/polls/types.ts +77 -0
  327. package/src/plugins/builtin/portfolio-list/header.tsx +15 -4
  328. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -3
  329. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +1 -1
  330. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -0
  331. package/src/plugins/builtin/portfolio-list/summary/index.tsx +2 -1
  332. package/src/plugins/builtin/research/analyst-pane.tsx +40 -67
  333. package/src/plugins/builtin/research/corporate-actions-pane.tsx +25 -19
  334. package/src/plugins/builtin/research/index.test.ts +6 -25
  335. package/src/plugins/builtin/research/index.tsx +11 -5
  336. package/src/plugins/builtin/research/relative-valuation-pane.tsx +86 -26
  337. package/src/plugins/builtin/scanner/denied.tsx +31 -0
  338. package/src/plugins/builtin/scanner/feed.ts +94 -0
  339. package/src/plugins/builtin/scanner/flow-model.test.ts +85 -0
  340. package/src/plugins/builtin/scanner/flow-model.ts +133 -0
  341. package/src/plugins/builtin/scanner/flow-pane.tsx +197 -0
  342. package/src/plugins/builtin/scanner/hilo-bars.tsx +121 -0
  343. package/src/plugins/builtin/scanner/hilo-model.test.ts +44 -0
  344. package/src/plugins/builtin/scanner/hilo-model.ts +75 -0
  345. package/src/plugins/builtin/scanner/hilo-pane.tsx +165 -0
  346. package/src/plugins/builtin/scanner/index.ts +109 -0
  347. package/src/plugins/builtin/scanner/waiting.tsx +11 -0
  348. package/src/plugins/builtin/sec/index.tsx +11 -7
  349. package/src/plugins/builtin/sectors/index.tsx +105 -81
  350. package/src/plugins/builtin/sectors/move-bar.tsx +43 -0
  351. package/src/plugins/builtin/sectors/sector-data.ts +18 -0
  352. package/src/plugins/builtin/sectors/sector-model.ts +13 -15
  353. package/src/plugins/builtin/shared/auto-refresh.ts +51 -0
  354. package/src/plugins/builtin/shared/cloud-upgrade.ts +29 -5
  355. package/src/plugins/builtin/shared/pane-footer.ts +16 -3
  356. package/src/plugins/builtin/shared/screener-live-quotes.test.ts +91 -0
  357. package/src/plugins/builtin/shared/screener-live-quotes.ts +134 -0
  358. package/src/plugins/builtin/shared/table-pane.ts +1 -5
  359. package/src/plugins/builtin/shared/use-quote-board.test.tsx +211 -0
  360. package/src/plugins/builtin/shared/use-quote-board.ts +195 -0
  361. package/src/plugins/builtin/short-interest/client.ts +124 -0
  362. package/src/plugins/builtin/short-interest/index.tsx +62 -0
  363. package/src/plugins/builtin/short-interest/model.ts +113 -0
  364. package/src/plugins/builtin/short-interest/pane.tsx +252 -0
  365. package/src/plugins/builtin/short-interest/types.ts +9 -0
  366. package/src/plugins/builtin/substack/article-detail.tsx +15 -1
  367. package/src/plugins/builtin/substack/pane-footer.ts +3 -22
  368. package/src/plugins/builtin/substack/pane.tsx +1 -2
  369. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  370. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  371. package/src/plugins/builtin/thirteenf/pane.tsx +6 -2
  372. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +10 -4
  373. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +67 -11
  374. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +15 -8
  375. package/src/plugins/builtin/ticker-detail/index.tsx +5 -21
  376. package/src/plugins/builtin/ticker-detail/overview/components.tsx +72 -32
  377. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -10
  378. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -1
  379. package/src/plugins/builtin/ticker-detail/quote-monitor/card.test.tsx +70 -0
  380. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +34 -26
  381. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +96 -49
  382. package/src/plugins/builtin/ticker-detail/research-tabs.ts +32 -0
  383. package/src/plugins/builtin/ticker-research-backend-plugin.ts +2 -9
  384. package/src/plugins/builtin/ticker-research-plugin.ts +6 -0
  385. package/src/plugins/builtin/treasury-auctions/cache.test.ts +173 -0
  386. package/src/plugins/builtin/treasury-auctions/cache.ts +104 -0
  387. package/src/plugins/builtin/treasury-auctions/client.test.ts +181 -0
  388. package/src/plugins/builtin/treasury-auctions/client.ts +151 -0
  389. package/src/plugins/builtin/treasury-auctions/index.tsx +90 -0
  390. package/src/plugins/builtin/treasury-auctions/model.test.ts +147 -0
  391. package/src/plugins/builtin/treasury-auctions/model.ts +188 -0
  392. package/src/plugins/builtin/treasury-auctions/pane.test.tsx +170 -0
  393. package/src/plugins/builtin/treasury-auctions/pane.tsx +417 -0
  394. package/src/plugins/builtin/treasury-auctions/types.ts +59 -0
  395. package/src/plugins/builtin/tv/pane.tsx +67 -7
  396. package/src/plugins/builtin/tv/youtube-embed.test.ts +84 -0
  397. package/src/plugins/builtin/tv/youtube-embed.ts +173 -0
  398. package/src/plugins/builtin/tv/youtube-stream.ts +27 -2
  399. package/src/plugins/builtin/volatility/client.ts +87 -0
  400. package/src/plugins/builtin/volatility/index.test.tsx +107 -0
  401. package/src/plugins/builtin/volatility/index.tsx +193 -0
  402. package/src/plugins/builtin/volatility/model.test.ts +66 -0
  403. package/src/plugins/builtin/volatility/model.ts +113 -0
  404. package/src/plugins/builtin/world-indices/footer.ts +43 -23
  405. package/src/plugins/builtin/world-indices/index.tsx +44 -93
  406. package/src/plugins/builtin/world-indices/indices.ts +11 -2
  407. package/src/plugins/builtin/world-indices/model.ts +17 -24
  408. package/src/plugins/builtin/world-indices/pane.test.tsx +116 -0
  409. package/src/plugins/builtin/world-indices/table.tsx +82 -37
  410. package/src/plugins/builtin/yield-curve/index.tsx +16 -7
  411. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +25 -0
  412. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -0
  413. package/src/plugins/catalog-backend.ts +8 -0
  414. package/src/plugins/catalog-ui.ts +8 -0
  415. package/src/plugins/event-bus.ts +1 -0
  416. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  417. package/src/plugins/ibkr/trade/tab/footer.ts +9 -23
  418. package/src/plugins/ibkr/trade/tab/index.tsx +1 -5
  419. package/src/plugins/ibkr/trading/pane-view.tsx +149 -87
  420. package/src/plugins/ibkr/trading/pane.tsx +2 -2
  421. package/src/plugins/ownership.ts +3 -0
  422. package/src/plugins/pane-manager/floating.ts +8 -2
  423. package/src/plugins/pane-manager.test.ts +13 -0
  424. package/src/plugins/prediction-markets/backend-plugin.ts +2 -0
  425. package/src/plugins/prediction-markets/capability.test.ts +281 -0
  426. package/src/plugins/prediction-markets/capability.ts +171 -0
  427. package/src/plugins/prediction-markets/chart.tsx +18 -26
  428. package/src/plugins/prediction-markets/columns.ts +3 -2
  429. package/src/plugins/prediction-markets/controller/catalog.ts +15 -12
  430. package/src/plugins/prediction-markets/controller/detail.ts +4 -0
  431. package/src/plugins/prediction-markets/controller/index.ts +0 -15
  432. package/src/plugins/prediction-markets/detail/book.tsx +3 -23
  433. package/src/plugins/prediction-markets/detail/overview.tsx +0 -18
  434. package/src/plugins/prediction-markets/detail/pane.tsx +36 -12
  435. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  436. package/src/plugins/prediction-markets/index.tsx +2 -0
  437. package/src/plugins/prediction-markets/metrics.ts +28 -18
  438. package/src/plugins/prediction-markets/navigation.ts +1 -0
  439. package/src/plugins/prediction-markets/pane.test.tsx +1 -1
  440. package/src/plugins/prediction-markets/pane.tsx +0 -1
  441. package/src/plugins/prediction-markets/services/fetch.ts +3 -2
  442. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +82 -23
  443. package/src/plugins/prediction-markets/services/kalshi/normalize.ts +3 -1
  444. package/src/plugins/prediction-markets/services/kalshi/types.ts +14 -0
  445. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +26 -14
  446. package/src/plugins/prediction-markets/services/polymarket/detail.ts +67 -18
  447. package/src/plugins/prediction-markets/test-helpers.tsx +0 -1
  448. package/src/plugins/prediction-markets/types.ts +3 -9
  449. package/src/plugins/registry/context.ts +6 -1
  450. package/src/plugins/registry/index.test.ts +2 -0
  451. package/src/plugins/registry/index.ts +53 -3
  452. package/src/plugins/runtime/context.tsx +4 -2
  453. package/src/plugins/runtime/index.tsx +9 -0
  454. package/src/remote/controller.test.ts +17 -4
  455. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  456. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -2
  457. package/src/renderers/electrobun/bun/desktop/capability-bridge.test.ts +64 -0
  458. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +41 -7
  459. package/src/renderers/electrobun/bun/desktop/host-requests.ts +5 -2
  460. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +33 -0
  461. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +53 -6
  462. package/src/renderers/electrobun/bun/desktop/initialization.test.ts +27 -0
  463. package/src/renderers/electrobun/bun/desktop/initialization.ts +6 -1
  464. package/src/renderers/electrobun/bun/index.ts +1 -0
  465. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  466. package/src/renderers/electrobun/view/app-services.ts +25 -2
  467. package/src/renderers/electrobun/view/build-assets.ts +5 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +186 -65
  469. package/src/renderers/electrobun/view/cli-pane-shot-health.test.ts +25 -0
  470. package/src/renderers/electrobun/view/cli-pane-shot-health.ts +31 -0
  471. package/src/renderers/electrobun/view/data-table/index.tsx +10 -11
  472. package/src/renderers/electrobun/view/data-table/row.tsx +2 -0
  473. package/src/renderers/electrobun/view/desktop/controls.test.tsx +68 -0
  474. package/src/renderers/electrobun/view/desktop/controls.tsx +25 -1
  475. package/src/renderers/electrobun/view/host/box.test.tsx +80 -0
  476. package/src/renderers/electrobun/view/host/media-surface.tsx +70 -3
  477. package/src/renderers/electrobun/view/host/tabs.tsx +97 -5
  478. package/src/renderers/electrobun/view/host/text.tsx +5 -2
  479. package/src/renderers/electrobun/view/input-host.test.tsx +140 -0
  480. package/src/renderers/electrobun/view/input-host.tsx +4 -2
  481. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +19 -1
  482. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +7 -0
  483. package/src/renderers/electrobun/view/native-stubs/chart/kitty-support.ts +10 -0
  484. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +46 -0
  485. package/src/renderers/electrobun/view/remote/capability-invoker.ts +34 -13
  486. package/src/renderers/electrobun/view/remote/connection-health-backend.ts +19 -0
  487. package/src/renderers/electrobun/view/remote/connection-health.test.ts +44 -0
  488. package/src/renderers/electrobun/view/remote/connection-health.ts +49 -0
  489. package/src/renderers/electrobun/view/styles.css +5 -0
  490. package/src/renderers/opentui/dialog-host.tsx +279 -80
  491. package/src/renderers/opentui/host.tsx +15 -1
  492. package/src/renderers/opentui/terminal-media.test.ts +102 -0
  493. package/src/renderers/opentui/terminal-media.ts +142 -0
  494. package/src/renderers/opentui/test-utils.tsx +9 -6
  495. package/src/renderers/opentui/toast-host.tsx +136 -7
  496. package/src/renderers/opentui/ui-host.tsx +19 -3
  497. package/src/sources/provider-router/connection-health.test.ts +28 -0
  498. package/src/sources/provider-router/connection-health.ts +47 -0
  499. package/src/sources/provider-router/history.test.ts +53 -4
  500. package/src/sources/provider-router/history.ts +119 -14
  501. package/src/sources/provider-router/index.ts +12 -1
  502. package/src/sources/yahoo-finance/history.ts +2 -9
  503. package/src/sources/yahoo-finance/http.ts +1 -1
  504. package/src/sources/yahoo-finance/symbols.ts +1 -1
  505. package/src/sources/yahoo-finance/types.ts +19 -0
  506. package/src/state/app/context/index.test.tsx +101 -0
  507. package/src/state/app/context/index.tsx +10 -0
  508. package/src/state/config-save-scheduler.ts +1 -6
  509. package/src/state/persist-scheduler.test.ts +31 -0
  510. package/src/state/persist-scheduler.ts +21 -8
  511. package/src/sync/auth-transition.test.ts +36 -0
  512. package/src/sync/auth-transition.ts +22 -0
  513. package/src/sync/core-contributors.test.ts +58 -1
  514. package/src/sync/core-contributors.ts +39 -2
  515. package/src/sync/react.ts +9 -0
  516. package/src/test-support/plugin-runtime.ts +3 -0
  517. package/src/theme/font-scale.test.ts +14 -0
  518. package/src/theme/font-scale.ts +69 -0
  519. package/src/tickers/search/index.test.ts +284 -2
  520. package/src/tickers/search/index.ts +55 -18
  521. package/src/tickers/search/ranking.ts +143 -18
  522. package/src/tickers/search/types.ts +1 -0
  523. package/src/time-series/alignment.ts +15 -11
  524. package/src/time-series/field-catalog.ts +10 -0
  525. package/src/time-series/fundamentals.ts +19 -11
  526. package/src/time-series/hooks.ts +8 -2
  527. package/src/time-series/parsed-history-cache.ts +35 -0
  528. package/src/time-series/resolution.ts +10 -0
  529. package/src/time-series/resolve.test.ts +332 -3
  530. package/src/time-series/resolve.ts +225 -25
  531. package/src/time-series/spec-studies.test.ts +23 -0
  532. package/src/time-series/spec.ts +21 -2
  533. package/src/time-series/transforms.test.ts +76 -1
  534. package/src/time-series/transforms.ts +39 -8
  535. package/src/time-series/types.ts +9 -2
  536. package/src/time-series/use-chart-resolution.test.tsx +105 -1
  537. package/src/time-series/use-chart-resolution.ts +67 -13
  538. package/src/types/config.ts +30 -27
  539. package/src/types/media.ts +2 -0
  540. package/src/types/plugin.ts +8 -1
  541. package/src/ui/host.tsx +4 -0
  542. package/src/ui/pointer-drag.ts +12 -0
  543. package/src/ui/qr.test.ts +79 -0
  544. package/src/ui/qr.ts +82 -0
  545. package/src/updater/github-releases.test.ts +3 -3
  546. package/src/updater/github-releases.ts +1 -1
  547. package/src/updater.test.ts +117 -16
  548. package/src/updater.ts +32 -5
  549. package/src/utils/exchanges.ts +1 -0
  550. package/src/utils/external-url.test.ts +32 -0
  551. package/src/utils/external-url.ts +23 -0
  552. package/src/utils/sort-values.ts +31 -0
  553. package/src/utils/zoned-date-time.ts +67 -0
  554. package/src/components/onboarding/wizard-keyboard.ts +0 -306
  555. package/src/plugins/builtin/chart-composer/date-window-dialog.tsx +0 -120
@@ -48,6 +48,26 @@ export function useAppGlobalShortcuts({
48
48
  return;
49
49
  }
50
50
 
51
+ // Terminals send Ctrl; the browser and the desktop webview send Cmd on
52
+ // macOS, which the OpenTUI host also reports as `super` under the kitty
53
+ // protocol. Alt stays out so Alt-digit keeps its terminal meaning. While a
54
+ // dialog, the command bar or an editable field owns the keyboard the digit
55
+ // must not move layouts, but it still has to be swallowed there or the
56
+ // webview hands Cmd-digit to the browser's own tab switcher.
57
+ if (!isDetachedWindow
58
+ && /^[1-9]$/.test(event.name ?? "")
59
+ && (event.ctrl || event.meta || event.super)) {
60
+ const layouts = state.config.layouts ?? [];
61
+ const idx = parseInt(event.name!, 10) - 1;
62
+ const uiOwnsKeyboard = dialogOpen || state.commandBarOpen || event.targetEditable === true;
63
+ if (!uiOwnsKeyboard && idx < layouts.length && idx !== state.config.activeLayoutIndex) {
64
+ dispatch({ type: "SWITCH_LAYOUT", index: idx });
65
+ }
66
+ event.preventDefault();
67
+ event.stopPropagation();
68
+ return;
69
+ }
70
+
51
71
  if (dialogOpen) return;
52
72
 
53
73
  if (!isDetachedWindow && (
@@ -70,16 +90,8 @@ export function useAppGlobalShortcuts({
70
90
  });
71
91
  return;
72
92
  }
73
- if (!isDetachedWindow && /^[1-9]$/.test(event.name ?? "") && event.ctrl && (state.config.layouts ?? []).length > 1) {
74
- const idx = parseInt(event.name!, 10) - 1;
75
- const layouts = state.config.layouts ?? [];
76
- if (idx < layouts.length && idx !== state.config.activeLayoutIndex) {
77
- dispatch({ type: "SWITCH_LAYOUT", index: idx });
78
- }
79
- event.preventDefault();
80
- event.stopPropagation();
81
- return;
82
- }
93
+
94
+ const hasShortcutModifier = event.ctrl || event.meta || event.super || event.alt;
83
95
 
84
96
  if (state.commandBarOpen) return;
85
97
 
@@ -103,7 +115,17 @@ export function useAppGlobalShortcuts({
103
115
 
104
116
  if (state.inputCaptured) return;
105
117
 
106
- const hasShortcutModifier = event.ctrl || event.meta || event.super || event.alt;
118
+ const isQuestionMark = event.name === "?"
119
+ || event.key === "?"
120
+ || event.sequence === "?"
121
+ || (event.name === "/" && event.shift);
122
+ if (!isDetachedWindow && !hasShortcutModifier && isQuestionMark) {
123
+ event.preventDefault();
124
+ event.stopPropagation();
125
+ pluginRegistry.showPane("help");
126
+ return;
127
+ }
128
+
107
129
  if (!hasShortcutModifier && !isDetachedWindow && event.name === "q") {
108
130
  rendererHost.requestExit();
109
131
  } else if (!hasShortcutModifier && event.name === "r") {
@@ -12,11 +12,26 @@ export interface AppBrokerImportRuntime {
12
12
  importBrokerPositions: (
13
13
  instanceId: string,
14
14
  tickerMap?: Map<string, TickerRecord>,
15
- options?: { refreshImportedTickers?: boolean; config?: AppConfig },
15
+ options?: BrokerImportOptions,
16
16
  ) => Promise<SyncBrokerInstanceResult>;
17
17
  autoImportBrokerPositions: (tickerMap: Map<string, TickerRecord>) => Promise<void>;
18
18
  }
19
19
 
20
+ export interface BrokerImportOptions {
21
+ refreshImportedTickers?: boolean;
22
+ config?: AppConfig;
23
+ persistResolvedBrokerConfig?: boolean;
24
+ signal?: AbortSignal;
25
+ onCommitStart?: () => void;
26
+ onCommitEnd?: () => void;
27
+ }
28
+
29
+ function throwIfBrokerImportCancelled(signal?: AbortSignal): void {
30
+ if (signal?.aborted) {
31
+ throw new Error("Broker import was cancelled.");
32
+ }
33
+ }
34
+
20
35
  export function useBrokerImportRuntime({
21
36
  dispatch,
22
37
  pluginRegistry,
@@ -34,24 +49,30 @@ export function useBrokerImportRuntime({
34
49
  instanceId: string,
35
50
  result: SyncBrokerInstanceResult,
36
51
  baseConfig: AppConfig,
37
- options?: { refreshImportedTickers?: boolean },
52
+ options?: Pick<BrokerImportOptions, "refreshImportedTickers" | "signal">,
38
53
  ) => {
54
+ throwIfBrokerImportCancelled(options?.signal);
39
55
  dispatch({ type: "SET_BROKER_ACCOUNTS", instanceId, accounts: result.brokerAccounts });
40
56
 
41
57
  if (result.config !== baseConfig) {
58
+ throwIfBrokerImportCancelled(options?.signal);
42
59
  dispatch({ type: "SET_CONFIG", config: result.config });
43
60
  await saveConfigImmediately(result.config);
61
+ throwIfBrokerImportCancelled(options?.signal);
44
62
  pluginRegistry.events.emit("config:changed", { config: result.config });
45
63
  }
46
64
 
47
65
  for (const ticker of result.addedTickers) {
66
+ throwIfBrokerImportCancelled(options?.signal);
48
67
  pluginRegistry.events.emit("ticker:added", { symbol: ticker.metadata.ticker, ticker });
49
68
  }
50
69
  for (const ticker of [...result.addedTickers, ...result.updatedTickers]) {
70
+ throwIfBrokerImportCancelled(options?.signal);
51
71
  dispatch({ type: "UPDATE_TICKER", ticker: { ...ticker } });
52
72
  }
53
73
 
54
74
  for (const position of result.positions) {
75
+ throwIfBrokerImportCancelled(options?.signal);
55
76
  // Skip Yahoo Finance for broker option symbols; position marks are already available.
56
77
  // Position data (markPrice, marketValue, unrealizedPnl) is used directly.
57
78
  if (options?.refreshImportedTickers !== false && position.assetCategory !== "OPT") {
@@ -63,7 +84,7 @@ export function useBrokerImportRuntime({
63
84
  const importBrokerPositions = useCallback(async (
64
85
  instanceId: string,
65
86
  tickerMap?: Map<string, TickerRecord>,
66
- options?: { refreshImportedTickers?: boolean; config?: AppConfig },
87
+ options?: BrokerImportOptions,
67
88
  ) => {
68
89
  const baseConfig = options?.config ?? stateRef.current.config;
69
90
  const result = await syncBrokerInstance({
@@ -73,11 +94,24 @@ export function useBrokerImportRuntime({
73
94
  tickerRepository,
74
95
  existingTickers: tickerMap ?? new Map(stateRef.current.tickers),
75
96
  resources: pluginRegistry.persistence.resources,
97
+ persistResolvedBrokerConfig: options?.persistResolvedBrokerConfig,
98
+ signal: options?.signal,
99
+ deferPersistence: true,
76
100
  });
77
101
 
78
- await applyBrokerImportResult(instanceId, result, baseConfig, options);
102
+ let commitStarted = false;
103
+ try {
104
+ throwIfBrokerImportCancelled(options?.signal);
105
+ options?.onCommitStart?.();
106
+ commitStarted = true;
107
+ await result.commit();
108
+ throwIfBrokerImportCancelled(options?.signal);
109
+ await applyBrokerImportResult(instanceId, result, baseConfig, options);
79
110
 
80
- return result;
111
+ return result;
112
+ } finally {
113
+ if (commitStarted) options?.onCommitEnd?.();
114
+ }
81
115
  }, [applyBrokerImportResult, pluginRegistry.brokers, pluginRegistry.persistence.resources, stateRef, tickerRepository]);
82
116
 
83
117
  const autoImportBrokerPositions = useCallback(async (tickerMap: Map<string, TickerRecord>) => {
@@ -73,7 +73,13 @@ export function useAppStartupRuntime({
73
73
  state.config.brokerInstances,
74
74
  pluginRegistry.brokers,
75
75
  );
76
- } catch {}
76
+ } catch (error) {
77
+ console.error("[startup] Failed to load persisted broker accounts:", error);
78
+ pluginRegistry.notify({
79
+ body: "Failed to load saved broker account data. Check local storage permissions.",
80
+ type: "error",
81
+ });
82
+ }
77
83
  await measurePerfAsync("startup.app.initialize-state", () => initializeAppState({
78
84
  config: state.config,
79
85
  tickerRepository,
@@ -1,5 +1,7 @@
1
1
  import { useCallback, useEffect, type Dispatch } from "react";
2
- import type { AppAction } from "../../state/app/context";
2
+ import type { PluginRegistry } from "../../plugins/registry";
3
+ import { saveConfigImmediately } from "../../state/config-save-scheduler";
4
+ import type { AppAction, AppState } from "../../state/app/context";
3
5
  import {
4
6
  canSelfUpdate,
5
7
  checkForUpdateDetailed,
@@ -8,15 +10,21 @@ import {
8
10
  } from "../../updater";
9
11
  import { VERSION } from "../../version";
10
12
 
13
+ const UPDATE_CHECK_INTERVAL_MS = 60 * 60_000; // hourly
14
+
11
15
  export function useAppUpdateRuntime({
12
16
  dispatch,
13
17
  isDetachedWindow,
18
+ pluginRegistry,
19
+ stateRef,
14
20
  updateAvailable,
15
21
  updateCheckInProgress,
16
22
  updateProgress,
17
23
  }: {
18
24
  dispatch: Dispatch<AppAction>;
19
25
  isDetachedWindow: boolean;
26
+ pluginRegistry: PluginRegistry;
27
+ stateRef: { current: AppState };
20
28
  updateAvailable: ReleaseInfo | null;
21
29
  updateCheckInProgress: boolean;
22
30
  updateProgress: unknown;
@@ -70,8 +78,25 @@ export function useAppUpdateRuntime({
70
78
  useEffect(() => {
71
79
  if (isDetachedWindow) return;
72
80
  void runUpdateCheck(false);
81
+ const interval = setInterval(() => { void runUpdateCheck(false); }, UPDATE_CHECK_INTERVAL_MS);
82
+ return () => { clearInterval(interval); };
73
83
  }, [isDetachedWindow, runUpdateCheck]);
74
84
 
85
+ // First launch on a new version: show that release's notes, then remember the version.
86
+ useEffect(() => {
87
+ if (isDetachedWindow) return;
88
+ const config = stateRef.current.config;
89
+ if (config.lastLaunchedVersion === VERSION) return;
90
+ const isUpgrade = !!config.lastLaunchedVersion;
91
+ const nextConfig = { ...config, lastLaunchedVersion: VERSION };
92
+ dispatch({ type: "SET_CONFIG", config: nextConfig });
93
+ void saveConfigImmediately(nextConfig).catch(() => {});
94
+ if (!isUpgrade) return;
95
+ void pluginRegistry.createPaneFromTemplateAsyncFn("changelog-pane", {
96
+ values: { version: VERSION },
97
+ }).catch(() => {});
98
+ }, [dispatch, isDetachedWindow, pluginRegistry, stateRef]);
99
+
75
100
  useEffect(() => {
76
101
  if (isDetachedWindow) return;
77
102
  if (!updateAvailable || updateProgress || updateCheckInProgress) return;
package/src/app.tsx CHANGED
@@ -21,7 +21,7 @@ import { useDialog } from "./ui/dialog";
21
21
  import { PluginRegistry } from "./plugins/registry";
22
22
  import type { LoadedExternalPlugin } from "./plugins/loader";
23
23
  import type { AppServicesFactory, AppTickerRepositoryPort } from "./core/app-service-ports";
24
- import { ThemeProvider, useThemeColors } from "./theme/theme-context";
24
+ import { useThemeColors } from "./theme/theme-context";
25
25
  import type { AppConfig } from "./types/config";
26
26
  import type { DesktopDeepLinkBridge } from "./types/desktop-deeplink";
27
27
  import type { CliLaunchRequest } from "./types/plugin";
@@ -68,6 +68,8 @@ interface AppInnerProps {
68
68
  desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
69
69
  desktopDeepLinkBridge?: DesktopDeepLinkBridge;
70
70
  remoteControlAdapter?: RemoteControlAdapter;
71
+ onboardingActive?: boolean;
72
+ onOnboardingComplete?: (config: AppConfig) => void | Promise<void>;
71
73
  }
72
74
 
73
75
  function ThemedAppRoot({ children }: { children: ReactNode }) {
@@ -98,6 +100,8 @@ function AppInner({
98
100
  desktopApplicationMenuBridge,
99
101
  desktopDeepLinkBridge,
100
102
  remoteControlAdapter,
103
+ onboardingActive = false,
104
+ onOnboardingComplete,
101
105
  }: AppInnerProps) {
102
106
  const dispatch = useAppDispatch();
103
107
  const stateRef = useAppStateRef();
@@ -219,6 +223,8 @@ function AppInner({
219
223
  const { runUpdateCheck, startUpdate } = useAppUpdateRuntime({
220
224
  dispatch,
221
225
  isDetachedWindow,
226
+ pluginRegistry,
227
+ stateRef,
222
228
  updateAvailable: state.updateAvailable,
223
229
  updateCheckInProgress: state.updateCheckInProgress,
224
230
  updateProgress: state.updateProgress,
@@ -277,7 +283,11 @@ function AppInner({
277
283
  dispatch,
278
284
  tickerRepository,
279
285
  pluginRegistry,
280
- initialized: state.initialized && desktopWindowBridge?.kind !== "detached",
286
+ // Keep a first-run workspace stable while the local guide is active. Once
287
+ // onboarding finishes, the normal pull-before-push sync starts immediately.
288
+ initialized: state.initialized
289
+ && desktopWindowBridge?.kind !== "detached"
290
+ && !onboardingActive,
281
291
  });
282
292
 
283
293
  useAppPaneRuntime({
@@ -359,7 +369,14 @@ function AppInner({
359
369
  desktopWindowBridge={desktopWindowBridge}
360
370
  >
361
371
  <ThemedAppRoot>
362
- <Header />
372
+ <Header
373
+ onOpenHelp={() => pluginRegistry.showPane("help")}
374
+ onOpenChangelog={(version) => {
375
+ void pluginRegistry.createPaneFromTemplateAsyncFn("changelog-pane", {
376
+ values: { version },
377
+ }).catch(() => {});
378
+ }}
379
+ />
363
380
  <TransientLayoutProvider>
364
381
  <Shell
365
382
  pluginRegistry={pluginRegistry}
@@ -369,6 +386,13 @@ function AppInner({
369
386
  />
370
387
  <StatusBar />
371
388
  </TransientLayoutProvider>
389
+ {onboardingActive && onOnboardingComplete ? (
390
+ <OnboardingWizard
391
+ pluginRegistry={pluginRegistry}
392
+ importBrokerPositions={importBrokerPositions}
393
+ onComplete={onOnboardingComplete}
394
+ />
395
+ ) : null}
372
396
  {state.commandBarOpen && (
373
397
  <CommandBar
374
398
  dataProvider={dataProvider}
@@ -434,7 +458,10 @@ export function App({
434
458
  const [config, setConfig] = useState(() => {
435
459
  return initialCliLaunch.config;
436
460
  });
437
- const [showOnboarding, setShowOnboarding] = useState(!effectiveInitialConfig.onboardingComplete);
461
+ const [showOnboarding, setShowOnboarding] = useState(() => (
462
+ desktopWindowBridge?.kind !== "detached"
463
+ && (!effectiveInitialConfig.onboardingComplete || !!effectiveInitialConfig.onboardingProgress)
464
+ ));
438
465
 
439
466
  useEffect(() => bindAppActivity(renderer), [renderer]);
440
467
 
@@ -466,21 +493,6 @@ export function App({
466
493
  });
467
494
  }, [cliLaunchRequest, config, desktopSnapshot, desktopWindowBridge?.kind, services.persistence.sessions]);
468
495
 
469
- if (showOnboarding) {
470
- return (
471
- <ThemeProvider themeId={config.theme}>
472
- <OnboardingWizard
473
- config={config}
474
- pluginRegistry={services.pluginRegistry}
475
- onComplete={(updatedConfig) => {
476
- setConfig(updatedConfig);
477
- setShowOnboarding(false);
478
- }}
479
- />
480
- </ThemeProvider>
481
- );
482
- }
483
-
484
496
  return (
485
497
  <RemoteUiRegistryProvider>
486
498
  <AppProvider
@@ -502,6 +514,11 @@ export function App({
502
514
  desktopApplicationMenuBridge={desktopApplicationMenuBridge}
503
515
  desktopDeepLinkBridge={desktopDeepLinkBridge}
504
516
  remoteControlAdapter={remoteControlAdapter}
517
+ onboardingActive={showOnboarding}
518
+ onOnboardingComplete={(updatedConfig) => {
519
+ setConfig(updatedConfig);
520
+ setShowOnboarding(false);
521
+ }}
505
522
  />
506
523
  </AppProvider>
507
524
  </RemoteUiRegistryProvider>
@@ -107,19 +107,17 @@ function updateExistingTicker(
107
107
  };
108
108
  }
109
109
 
110
- export async function upsertBrokerPositionTicker({
111
- tickerRepository,
110
+ export function upsertBrokerPositionTicker({
112
111
  tickers,
113
112
  instance,
114
113
  portfolioId,
115
114
  position,
116
115
  }: {
117
- tickerRepository: AppTickerRepositoryPort;
118
116
  tickers: Map<string, TickerRecord>;
119
117
  instance: BrokerInstanceConfig;
120
118
  portfolioId: string;
121
119
  position: BrokerPosition;
122
- }): Promise<{ ticker: TickerRecord; created: boolean }> {
120
+ }): { ticker: TickerRecord; created: boolean } {
123
121
  const brokerContract = position.brokerContract
124
122
  ? {
125
123
  ...position.brokerContract,
@@ -132,14 +130,11 @@ export async function upsertBrokerPositionTicker({
132
130
 
133
131
  if (!existingTicker) {
134
132
  return {
135
- ticker: await tickerRepository.createTicker(
136
- createTickerMetadata(position, portfolioId, positionEntry, brokerContract),
137
- ),
133
+ ticker: { metadata: createTickerMetadata(position, portfolioId, positionEntry, brokerContract) },
138
134
  created: true,
139
135
  };
140
136
  }
141
137
 
142
138
  const ticker = updateExistingTicker(existingTicker, instance, portfolioId, position, positionEntry, brokerContract);
143
- await tickerRepository.saveTicker(ticker);
144
139
  return { ticker, created: false };
145
140
  }
@@ -166,6 +166,30 @@ describe("syncBrokerInstance", () => {
166
166
  ]);
167
167
  });
168
168
 
169
+ test("fails cleanly when the broker account cache cannot be persisted", async () => {
170
+ const config = {
171
+ ...createDefaultConfig("/tmp/gloomberb-sync-broker-persistence-error"),
172
+ portfolios: [],
173
+ brokerInstances: [createBrokerInstance()],
174
+ };
175
+ const tickerRepository = createTickerRepository();
176
+
177
+ await expect(syncBrokerInstance({
178
+ config,
179
+ instanceId: "demo-broker",
180
+ brokers: new Map([["demo", createDemoBroker()]]),
181
+ tickerRepository: tickerRepository as any,
182
+ resources: {
183
+ get: () => null,
184
+ list: () => [],
185
+ set: () => { throw new Error("disk full"); },
186
+ delete: () => {},
187
+ } as any,
188
+ })).rejects.toThrow("disk full");
189
+
190
+ expect(await tickerRepository.loadTicker("AAPL")).toBeNull();
191
+ });
192
+
169
193
  test("imports account and position data from one broker portfolio snapshot", async () => {
170
194
  const instance = createBrokerInstance();
171
195
  const config = {
@@ -228,6 +252,41 @@ describe("syncBrokerInstance", () => {
228
252
  }
229
253
  });
230
254
 
255
+ test("stages broker cache and ticker writes until a cancellation-safe commit", async () => {
256
+ const instance = createBrokerInstance();
257
+ const config = {
258
+ ...createDefaultConfig("/tmp/gloomberb-sync-broker-cancel"),
259
+ portfolios: [],
260
+ brokerInstances: [instance],
261
+ };
262
+ const tickerRepository = createTickerRepository();
263
+ const persistence = new AppPersistence(":memory:");
264
+ const abortController = new AbortController();
265
+
266
+ try {
267
+ const result = await syncBrokerInstance({
268
+ config,
269
+ instanceId: instance.id,
270
+ brokers: new Map([["demo", createDemoBroker()]]),
271
+ tickerRepository: tickerRepository as any,
272
+ resources: persistence.resources,
273
+ signal: abortController.signal,
274
+ deferPersistence: true,
275
+ });
276
+
277
+ expect(await tickerRepository.loadAllTickers()).toEqual([]);
278
+ expect(loadPersistedBrokerAccounts(persistence.resources, instance, createDemoBroker())).toBeNull();
279
+
280
+ abortController.abort();
281
+ await expect(result.commit()).rejects.toThrow("Broker import was cancelled.");
282
+
283
+ expect(await tickerRepository.loadAllTickers()).toEqual([]);
284
+ expect(loadPersistedBrokerAccounts(persistence.resources, instance, createDemoBroker())).toBeNull();
285
+ } finally {
286
+ persistence.close();
287
+ }
288
+ });
289
+
231
290
  test("preserves the last account snapshot when a broker portfolio snapshot fails", async () => {
232
291
  const instance = createBrokerInstance();
233
292
  const config = {
@@ -26,6 +26,8 @@ export interface SyncBrokerInstanceArgs {
26
26
  existingTickers?: Map<string, TickerRecord>;
27
27
  resources?: AppResourceStorePort;
28
28
  persistResolvedBrokerConfig?: boolean;
29
+ signal?: AbortSignal;
30
+ deferPersistence?: boolean;
29
31
  }
30
32
 
31
33
  export interface SyncBrokerInstanceResult {
@@ -36,6 +38,7 @@ export interface SyncBrokerInstanceResult {
36
38
  portfolioIds: string[];
37
39
  addedTickers: TickerRecord[];
38
40
  updatedTickers: TickerRecord[];
41
+ commit(): Promise<void>;
39
42
  }
40
43
 
41
44
  export interface SyncBrokerInstancesArgs {
@@ -68,6 +71,12 @@ function markBrokerInstanceSynced(
68
71
  };
69
72
  }
70
73
 
74
+ function throwIfBrokerImportCancelled(signal?: AbortSignal): void {
75
+ if (signal?.aborted) {
76
+ throw new Error("Broker import was cancelled.");
77
+ }
78
+ }
79
+
71
80
  async function maybePersistResolvedBrokerConfig(
72
81
  config: AppConfig,
73
82
  instance: BrokerInstanceConfig,
@@ -108,6 +117,8 @@ export async function syncBrokerInstance({
108
117
  existingTickers,
109
118
  resources,
110
119
  persistResolvedBrokerConfig = false,
120
+ signal,
121
+ deferPersistence = false,
111
122
  }: SyncBrokerInstanceArgs): Promise<SyncBrokerInstanceResult> {
112
123
  const instance = getBrokerInstance(config.brokerInstances, instanceId);
113
124
  if (!instance) {
@@ -123,37 +134,32 @@ export async function syncBrokerInstance({
123
134
  }
124
135
 
125
136
  const valid = await broker.validate(instance).catch(() => false);
137
+ throwIfBrokerImportCancelled(signal);
126
138
  if (!valid) {
127
139
  throw new Error(`${broker.name} setup is incomplete.`);
128
140
  }
129
141
 
130
142
  const tickers = await loadTickerMap(tickerRepository, existingTickers);
143
+ throwIfBrokerImportCancelled(signal);
131
144
 
132
145
  let brokerAccounts: BrokerAccount[] = [];
133
146
  let positions: BrokerPosition[];
134
147
  if (broker.importPortfolioSnapshot) {
135
148
  const snapshot = await broker.importPortfolioSnapshot(instance);
149
+ throwIfBrokerImportCancelled(signal);
136
150
  brokerAccounts = snapshot.accounts;
137
151
  positions = snapshot.positions;
138
- if (resources) {
139
- try {
140
- persistBrokerAccounts(resources, instance, broker, brokerAccounts);
141
- } catch {}
142
- }
143
152
  } else {
144
153
  if (broker.listAccounts) {
145
154
  try {
146
155
  brokerAccounts = await broker.listAccounts(instance);
147
- if (resources) {
148
- try {
149
- persistBrokerAccounts(resources, instance, broker, brokerAccounts);
150
- } catch {}
151
- }
156
+ throwIfBrokerImportCancelled(signal);
152
157
  } catch (error) {
153
158
  throw error;
154
159
  }
155
160
  }
156
161
  positions = await broker.importPositions(instance);
162
+ throwIfBrokerImportCancelled(signal);
157
163
  }
158
164
 
159
165
  const accountMetadata = new Map(
@@ -231,15 +237,16 @@ export async function syncBrokerInstance({
231
237
  }
232
238
 
233
239
  nextConfig = await maybePersistResolvedBrokerConfig(nextConfig, instance, broker, persistResolvedBrokerConfig);
240
+ throwIfBrokerImportCancelled(signal);
234
241
  nextConfig = markBrokerInstanceSynced(nextConfig, instance.id, syncedAt);
235
242
 
236
243
  const addedTickers = new Map<string, TickerRecord>();
237
244
  const updatedTickers = new Map<string, TickerRecord>();
238
245
 
239
246
  for (const position of positions) {
247
+ throwIfBrokerImportCancelled(signal);
240
248
  const portfolioId = findReusableBrokerPortfolioId(nextConfig, instance, position.accountId);
241
- const { ticker, created } = await upsertBrokerPositionTicker({
242
- tickerRepository,
249
+ const { ticker, created } = upsertBrokerPositionTicker({
243
250
  tickers,
244
251
  instance,
245
252
  portfolioId,
@@ -263,12 +270,28 @@ export async function syncBrokerInstance({
263
270
  }
264
271
 
265
272
  for (const ticker of cleanedTickers.values()) {
266
- await tickerRepository.saveTicker(ticker);
267
273
  if (!addedTickers.has(ticker.metadata.ticker) && !updatedTickers.has(ticker.metadata.ticker)) {
268
274
  updatedTickers.set(ticker.metadata.ticker, ticker);
269
275
  }
270
276
  }
271
277
 
278
+ let committed = false;
279
+ const commit = async () => {
280
+ if (committed) return;
281
+ throwIfBrokerImportCancelled(signal);
282
+ if (resources) {
283
+ persistBrokerAccounts(resources, instance, broker, brokerAccounts);
284
+ }
285
+ await Promise.all([...addedTickers.values(), ...updatedTickers.values()].map((ticker) => (
286
+ tickerRepository.saveTicker(ticker)
287
+ )));
288
+ committed = true;
289
+ };
290
+
291
+ if (!deferPersistence) {
292
+ await commit();
293
+ }
294
+
272
295
  return {
273
296
  config: nextConfig,
274
297
  tickers,
@@ -277,6 +300,7 @@ export async function syncBrokerInstance({
277
300
  portfolioIds,
278
301
  addedTickers: [...addedTickers.values()],
279
302
  updatedTickers: [...updatedTickers.values()],
303
+ commit,
280
304
  };
281
305
  }
282
306