gloomberb 0.5.1 → 0.6.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 (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -14
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +9 -10
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +52 -11
  87. package/src/components/layout/shell.test.tsx +315 -107
  88. package/src/components/layout/shell.tsx +1010 -368
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +129 -0
  180. package/src/plugins/builtin/chat-controller.ts +85 -14
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +363 -170
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +99 -9
  440. package/src/utils/api-client.ts +378 -18
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -72,6 +72,7 @@ describe("sanitizeLayout", () => {
72
72
  },
73
73
  ],
74
74
  floating: [],
75
+ detached: [],
75
76
  }, DEFAULT_LAYOUT);
76
77
 
77
78
  expect(findPaneInstance(layout, "ticker-detail:main")?.binding).toEqual({
@@ -92,6 +93,7 @@ describe("sanitizeLayout", () => {
92
93
  },
93
94
  ],
94
95
  floating: [],
96
+ detached: [],
95
97
  }, DEFAULT_LAYOUT);
96
98
 
97
99
  expect(findPaneInstance(layout, "ticker-detail:main")).toBeUndefined();
@@ -141,6 +143,7 @@ describe("sanitizeLayout", () => {
141
143
  },
142
144
  ],
143
145
  floating: [],
146
+ detached: [],
144
147
  }, DEFAULT_LAYOUT);
145
148
 
146
149
  expect(layout.dockRoot?.kind).toBe("split");
@@ -168,6 +171,7 @@ describe("sanitizeLayout", () => {
168
171
  ],
169
172
  docked: [{ instanceId: "portfolio-list:main", columnIndex: 0 }],
170
173
  floating: [],
174
+ detached: [],
171
175
  }, DEFAULT_LAYOUT);
172
176
 
173
177
  expect(layout).toEqual(DEFAULT_LAYOUT);
@@ -175,6 +179,25 @@ describe("sanitizeLayout", () => {
175
179
  });
176
180
 
177
181
  describe("loadConfig", () => {
182
+ test("defaults detached layouts to an empty list for older configs", async () => {
183
+ const dataDir = await createTempConfigDir();
184
+ const layoutWithoutDetached = {
185
+ ...DEFAULT_LAYOUT,
186
+ detached: undefined,
187
+ };
188
+ await writeConfigJson(dataDir, createSavedConfig({
189
+ configVersion: CURRENT_CONFIG_VERSION - 1,
190
+ layout: layoutWithoutDetached,
191
+ layouts: [{ name: "Default", layout: layoutWithoutDetached }],
192
+ }));
193
+
194
+ const config = await loadConfig(dataDir);
195
+
196
+ expect(config.configVersion).toBe(CURRENT_CONFIG_VERSION);
197
+ expect(config.layout.detached).toEqual([]);
198
+ expect(config.layouts[0]?.layout.detached).toEqual([]);
199
+ });
200
+
178
201
  test("fills in missing chart preferences for older configs", async () => {
179
202
  const dataDir = await createTempConfigDir();
180
203
  await writeConfigJson(dataDir, createSavedConfig({
@@ -243,6 +266,54 @@ describe("loadConfig", () => {
243
266
  expect(config.disabledPlugins).toEqual(["chat", "news"]);
244
267
  });
245
268
 
269
+ test("migrates disabled built-in feature plugins to grouped plugin ids", async () => {
270
+ const dataDir = await createTempConfigDir();
271
+ await writeConfigJson(dataDir, createSavedConfig({
272
+ configVersion: CURRENT_CONFIG_VERSION - 1,
273
+ disabledPlugins: ["options", "sec", "world-indices", "earnings-calendar", "options"],
274
+ }));
275
+
276
+ const config = await loadConfig(dataDir);
277
+
278
+ expect(config.disabledPlugins).toEqual(["company-research", "market-overview", "macro"]);
279
+ });
280
+
281
+ test("migrates grouped built-in plugin config keys", async () => {
282
+ const dataDir = await createTempConfigDir();
283
+ await writeConfigJson(dataDir, createSavedConfig({
284
+ configVersion: CURRENT_CONFIG_VERSION - 1,
285
+ pluginConfig: {
286
+ options: {
287
+ selectedExpiration: "2026-06-19",
288
+ },
289
+ "company-research": {
290
+ preferredTab: "analyst-research",
291
+ },
292
+ },
293
+ }));
294
+
295
+ const config = await loadConfig(dataDir);
296
+
297
+ expect(config.pluginConfig).toEqual({
298
+ "company-research": {
299
+ selectedExpiration: "2026-06-19",
300
+ preferredTab: "analyst-research",
301
+ },
302
+ });
303
+ });
304
+
305
+ test("enables Gloomberb Cloud when migrating older default configs", async () => {
306
+ const dataDir = await createTempConfigDir();
307
+ await writeConfigJson(dataDir, createSavedConfig({
308
+ configVersion: 12,
309
+ disabledPlugins: ["gloomberb-cloud", "news"],
310
+ }));
311
+
312
+ const config = await loadConfig(dataDir);
313
+
314
+ expect(config.disabledPlugins).toEqual(["news"]);
315
+ });
316
+
246
317
  test("preserves IBKR gateway configs without migration rewrites", async () => {
247
318
  const dataDir = await createTempConfigDir();
248
319
  await writeConfigJson(dataDir, createSavedConfig({
@@ -315,6 +386,7 @@ describe("loadConfig", () => {
315
386
  ],
316
387
  docked: [{ instanceId: "portfolio-list:main", columnIndex: 0 }],
317
388
  floating: [],
389
+ detached: [],
318
390
  };
319
391
 
320
392
  await writeConfigJson(dataDir, createSavedConfig({
@@ -1,507 +1,64 @@
1
- import { mkdir, readFile, rm, writeFile } from "fs/promises";
2
- import { dirname, join } from "path";
3
- import type {
4
- AppConfig,
5
- BrokerInstanceConfig,
6
- ChartPreferences,
7
- FloatingPlacementMemory,
8
- LayoutConfig,
9
- PanePlacementMemory,
10
- PaneBinding,
11
- PaneInstanceConfig,
12
- SavedLayout,
13
- } from "../types/config";
14
- import {
15
- cloneLayout,
16
- createDefaultConfig,
17
- createPaneInstanceId,
18
- CURRENT_CONFIG_VERSION,
19
- DEFAULT_COLUMNS,
20
- DEFAULT_PORTFOLIO_COLUMN_IDS,
21
- clonePaneSettings,
22
- normalizePaneLayout,
23
- } from "../types/config";
24
- import type { Portfolio, Watchlist } from "../types/ticker";
25
- import { debugLog } from "../utils/debug-log";
1
+ import type { AppConfig } from "../types/config";
26
2
 
27
- const configLog = debugLog.createLogger("config");
28
- const LEGACY_MAIN_PORTFOLIO_COLUMN_IDS = DEFAULT_COLUMNS.map((column) => column.id);
3
+ export { sanitizeLayout } from "./config-layout";
29
4
 
30
- function getGlobalConfigDir(): string {
31
- return join(process.env.HOME || "~", ".gloomberb");
5
+ export interface ConfigStoreHost {
6
+ getDataDir(): Promise<string | null>;
7
+ loadConfig(dataDir: string): Promise<AppConfig>;
8
+ saveConfig(config: AppConfig): Promise<void>;
9
+ initDataDir(dataDir: string): Promise<AppConfig>;
10
+ resetAllData(dataDir: string): Promise<void>;
11
+ exportConfig(config: AppConfig, destPath: string): Promise<void>;
12
+ importConfig(dataDir: string, srcPath: string): Promise<AppConfig>;
32
13
  }
33
14
 
34
- function getGlobalConfigFile(): string {
35
- return join(getGlobalConfigDir(), "config.json");
36
- }
15
+ let configuredHost: ConfigStoreHost | null = null;
16
+ let nodeHostPromise: Promise<ConfigStoreHost> | null = null;
37
17
 
38
- export async function getDataDir(): Promise<string | null> {
39
- try {
40
- const raw = await readFile(getGlobalConfigFile(), "utf-8");
41
- const config = JSON.parse(raw) as { dataDir?: string };
42
- return config.dataDir || null;
43
- } catch {
44
- return null;
45
- }
18
+ export function setConfigStoreHost(host: ConfigStoreHost | null): void {
19
+ configuredHost = host;
46
20
  }
47
21
 
48
-
49
- export async function loadConfig(dataDir: string): Promise<AppConfig> {
50
- const { config } = await loadConfigState(dataDir);
51
- return config;
22
+ async function loadNodeHost(): Promise<ConfigStoreHost> {
23
+ if (!nodeHostPromise) {
24
+ const modulePath = "./config-store-node";
25
+ nodeHostPromise = import(modulePath) as Promise<ConfigStoreHost>;
26
+ }
27
+ return nodeHostPromise;
52
28
  }
53
29
 
54
- async function loadConfigState(dataDir: string): Promise<{ config: AppConfig; needsSave: boolean }> {
55
- const configPath = join(dataDir, "config.json");
56
-
57
- try {
58
- const raw = await readFile(configPath, "utf-8");
59
- const saved = JSON.parse(raw) as Record<string, unknown>;
60
- return normalizeConfig(saved, dataDir);
61
- } catch {
62
- return { config: createDefaultConfig(dataDir), needsSave: true };
30
+ async function getHost(): Promise<ConfigStoreHost> {
31
+ if (configuredHost) return configuredHost;
32
+ if (typeof Bun === "undefined") {
33
+ throw new Error("Config store host is not configured for this renderer.");
63
34
  }
35
+ return loadNodeHost();
64
36
  }
65
37
 
66
- function normalizeConfig(saved: Record<string, unknown>, dataDir: string): { config: AppConfig; needsSave: boolean } {
67
- const defaults = createDefaultConfig(dataDir);
68
- const shouldMigratePortfolioDefaults =
69
- typeof saved.configVersion !== "number" || saved.configVersion < CURRENT_CONFIG_VERSION;
70
- const directLayout = migrateLegacyPortfolioDefaultColumns(
71
- sanitizeLayout(saved.layout, defaults.layout),
72
- shouldMigratePortfolioDefaults,
73
- );
74
- const layouts = sanitizeSavedLayouts(saved.layouts, directLayout).map((entry) => ({
75
- ...entry,
76
- layout: migrateLegacyPortfolioDefaultColumns(entry.layout, shouldMigratePortfolioDefaults),
77
- }));
78
- const activeLayoutIndex = sanitizeActiveLayoutIndex(saved.activeLayoutIndex, layouts.length);
79
- const layout = cloneLayout(layouts[activeLayoutIndex]?.layout ?? directLayout);
80
- const syncedLayouts = layouts.map((entry, index) => (
81
- index === activeLayoutIndex ? { ...entry, layout: cloneLayout(layout) } : entry
82
- ));
83
-
84
- const config: AppConfig = {
85
- dataDir,
86
- configVersion: CURRENT_CONFIG_VERSION,
87
- baseCurrency: typeof saved.baseCurrency === "string" ? saved.baseCurrency : defaults.baseCurrency,
88
- refreshIntervalMinutes: typeof saved.refreshIntervalMinutes === "number" ? saved.refreshIntervalMinutes : defaults.refreshIntervalMinutes,
89
- portfolios: sanitizePortfolios(saved.portfolios, defaults.portfolios),
90
- watchlists: sanitizeWatchlists(saved.watchlists, defaults.watchlists),
91
- layout,
92
- layouts: syncedLayouts,
93
- activeLayoutIndex,
94
- brokerInstances: sanitizeBrokerInstances(saved.brokerInstances),
95
- plugins: sanitizeStringArray(saved.plugins, defaults.plugins),
96
- disabledPlugins: sanitizeDisabledPlugins(saved, defaults.disabledPlugins),
97
- pluginConfig: sanitizePluginConfig(saved.pluginConfig),
98
- theme: typeof saved.theme === "string" ? saved.theme : defaults.theme,
99
- chartPreferences: sanitizeChartPreferences(saved.chartPreferences, defaults.chartPreferences),
100
- recentTickers: sanitizeStringArray(saved.recentTickers, defaults.recentTickers),
101
- onboardingComplete: typeof saved.onboardingComplete === "boolean" ? saved.onboardingComplete : defaults.onboardingComplete,
102
- };
103
-
104
- const needsSave =
105
- saved.configVersion !== CURRENT_CONFIG_VERSION
106
- || !isLayoutConfig(saved.layout)
107
- || !Array.isArray((saved.layout as { instances?: unknown })?.instances)
108
- || !Array.isArray(saved.layouts)
109
- || !Array.isArray(saved.brokerInstances)
110
- || !isPluginConfigMap(saved.pluginConfig)
111
- || !isChartPreferences(saved.chartPreferences)
112
- || typeof saved.activeLayoutIndex !== "number";
38
+ export async function getDataDir(): Promise<string | null> {
39
+ return (await getHost()).getDataDir();
40
+ }
113
41
 
114
- return { config, needsSave };
42
+ export async function loadConfig(dataDir: string): Promise<AppConfig> {
43
+ return (await getHost()).loadConfig(dataDir);
115
44
  }
116
45
 
117
46
  export async function saveConfig(config: AppConfig): Promise<void> {
118
- const configPath = join(config.dataDir, "config.json");
119
- await mkdir(dirname(configPath), { recursive: true });
120
-
121
- const activeLayoutIndex = sanitizeActiveLayoutIndex(config.activeLayoutIndex, config.layouts.length || 1);
122
- const layout = sanitizeLayout(config.layout, createDefaultConfig(config.dataDir).layout);
123
- const layouts = sanitizeSavedLayouts(config.layouts, layout).map((entry, index) => (
124
- index === activeLayoutIndex ? { ...entry, layout: cloneLayout(layout) } : entry
125
- ));
126
-
127
- const persisted: AppConfig = {
128
- ...config,
129
- configVersion: CURRENT_CONFIG_VERSION,
130
- portfolios: sanitizePortfolios(config.portfolios, []),
131
- watchlists: sanitizeWatchlists(config.watchlists, []),
132
- layout,
133
- layouts,
134
- activeLayoutIndex,
135
- brokerInstances: sanitizeBrokerInstances(config.brokerInstances),
136
- plugins: sanitizeStringArray(config.plugins, []),
137
- disabledPlugins: sanitizeDisabledPluginList(config.disabledPlugins),
138
- pluginConfig: sanitizePluginConfig(config.pluginConfig),
139
- chartPreferences: sanitizeChartPreferences(config.chartPreferences, createDefaultConfig(config.dataDir).chartPreferences),
140
- recentTickers: sanitizeStringArray(config.recentTickers, []),
141
- };
142
-
143
- await writeFile(configPath, JSON.stringify(persisted, null, 2), "utf-8");
47
+ return (await getHost()).saveConfig(config);
144
48
  }
145
49
 
146
50
  export async function initDataDir(dataDir: string): Promise<AppConfig> {
147
- configLog.info(`Initializing data directory: ${dataDir}`);
148
- await mkdir(dataDir, { recursive: true });
149
- const { config, needsSave } = await loadConfigState(dataDir);
150
- if (needsSave) {
151
- await saveConfig(config);
152
- }
153
- return config;
51
+ return (await getHost()).initDataDir(dataDir);
154
52
  }
155
53
 
156
54
  export async function resetAllData(dataDir: string): Promise<void> {
157
- await rm(dataDir, { recursive: true, force: true });
55
+ return (await getHost()).resetAllData(dataDir);
158
56
  }
159
57
 
160
58
  export async function exportConfig(config: AppConfig, destPath: string): Promise<void> {
161
- const { dataDir, ...rest } = config;
162
- await writeFile(destPath, JSON.stringify(rest, null, 2), "utf-8");
59
+ return (await getHost()).exportConfig(config, destPath);
163
60
  }
164
61
 
165
62
  export async function importConfig(dataDir: string, srcPath: string): Promise<AppConfig> {
166
- const raw = await readFile(srcPath, "utf-8");
167
- const saved = JSON.parse(raw) as Record<string, unknown>;
168
- const { config } = normalizeConfig(saved, dataDir);
169
- await saveConfig(config);
170
- return config;
171
- }
172
-
173
- function sanitizeStringArray(value: unknown, fallback: string[]): string[] {
174
- return Array.isArray(value)
175
- ? value.filter((entry): entry is string => typeof entry === "string")
176
- : fallback;
177
- }
178
-
179
- function sanitizeDisabledPluginList(value: unknown): string[] {
180
- return [...new Set(sanitizeStringArray(value, []))];
181
- }
182
-
183
- function sanitizeDisabledPlugins(saved: Record<string, unknown>, fallback: string[]): string[] {
184
- return sanitizeDisabledPluginList(saved.disabledPlugins ?? fallback);
185
- }
186
-
187
- function isPlainRecord(value: unknown): value is Record<string, unknown> {
188
- return !!value && typeof value === "object" && !Array.isArray(value);
189
- }
190
-
191
- function isPluginConfigMap(value: unknown): value is Record<string, Record<string, unknown>> {
192
- if (!isPlainRecord(value)) return false;
193
- return Object.values(value).every((entry) => isPlainRecord(entry));
194
- }
195
-
196
- function sanitizePluginConfig(value: unknown): Record<string, Record<string, unknown>> {
197
- if (!isPluginConfigMap(value)) return {};
198
- return Object.fromEntries(
199
- Object.entries(value).map(([pluginId, state]) => [pluginId, { ...state }]),
200
- );
201
- }
202
-
203
- function isChartPreferences(value: unknown): value is ChartPreferences {
204
- if (!value || typeof value !== "object") return false;
205
- const defaultRenderMode = (value as ChartPreferences).defaultRenderMode;
206
- const renderer = (value as ChartPreferences).renderer;
207
- return (defaultRenderMode === "area"
208
- || defaultRenderMode === "line"
209
- || defaultRenderMode === "candles"
210
- || defaultRenderMode === "ohlc")
211
- && (renderer === "auto" || renderer === "kitty" || renderer === "braille");
212
- }
213
-
214
- function sanitizeChartPreferences(value: unknown, fallback: ChartPreferences): ChartPreferences {
215
- if (!value || typeof value !== "object") return { ...fallback };
216
-
217
- const candidate = value as Partial<ChartPreferences>;
218
- const defaultRenderMode = candidate.defaultRenderMode === "area"
219
- || candidate.defaultRenderMode === "line"
220
- || candidate.defaultRenderMode === "candles"
221
- || candidate.defaultRenderMode === "ohlc"
222
- ? candidate.defaultRenderMode
223
- : fallback.defaultRenderMode;
224
- const renderer = candidate.renderer === "auto"
225
- || candidate.renderer === "kitty"
226
- || candidate.renderer === "braille"
227
- ? candidate.renderer
228
- : fallback.renderer;
229
-
230
- return {
231
- defaultRenderMode,
232
- renderer,
233
- };
234
- }
235
-
236
- function sanitizePortfolios(value: unknown, fallback: Portfolio[]): Portfolio[] {
237
- if (!Array.isArray(value)) return fallback.map((portfolio) => ({ ...portfolio }));
238
- return value
239
- .filter((entry): entry is Portfolio =>
240
- !!entry
241
- && typeof entry === "object"
242
- && typeof (entry as Portfolio).id === "string"
243
- && typeof (entry as Portfolio).name === "string"
244
- && typeof (entry as Portfolio).currency === "string",
245
- )
246
- .map((entry) => ({ ...entry }));
247
- }
248
-
249
- function sanitizeWatchlists(value: unknown, fallback: Watchlist[]): Watchlist[] {
250
- if (!Array.isArray(value)) return fallback.map((watchlist) => ({ ...watchlist }));
251
- return value
252
- .filter((entry): entry is Watchlist =>
253
- !!entry
254
- && typeof entry === "object"
255
- && typeof (entry as Watchlist).id === "string"
256
- && typeof (entry as Watchlist).name === "string",
257
- )
258
- .map((entry) => ({ ...entry }));
259
- }
260
-
261
- function sanitizeBrokerInstances(value: unknown): BrokerInstanceConfig[] {
262
- if (!Array.isArray(value)) return [];
263
- return value
264
- .filter((entry): entry is BrokerInstanceConfig =>
265
- !!entry
266
- && typeof entry === "object"
267
- && typeof (entry as BrokerInstanceConfig).id === "string"
268
- && typeof (entry as BrokerInstanceConfig).brokerType === "string"
269
- && typeof (entry as BrokerInstanceConfig).label === "string"
270
- && typeof (entry as BrokerInstanceConfig).config === "object"
271
- && (entry as BrokerInstanceConfig).config !== null,
272
- )
273
- .map((entry) => ({
274
- ...entry,
275
- enabled: entry.enabled ?? true,
276
- config: { ...entry.config },
277
- }));
278
- }
279
-
280
- function isLayoutConfig(value: unknown): value is LayoutConfig {
281
- return !!value
282
- && typeof value === "object"
283
- && Array.isArray((value as LayoutConfig).instances)
284
- && Array.isArray((value as LayoutConfig).floating)
285
- && "dockRoot" in (value as Record<string, unknown>);
286
- }
287
-
288
- function sanitizePaneBinding(value: unknown, fallback: PaneBinding = { kind: "none" }): PaneBinding {
289
- if (!value || typeof value !== "object") return fallback;
290
- if ((value as PaneBinding).kind === "fixed" && typeof (value as Extract<PaneBinding, { kind: "fixed" }>).symbol === "string") {
291
- return { kind: "fixed", symbol: (value as Extract<PaneBinding, { kind: "fixed" }>).symbol };
292
- }
293
- if ((value as PaneBinding).kind === "follow" && typeof (value as Extract<PaneBinding, { kind: "follow" }>).sourceInstanceId === "string") {
294
- return { kind: "follow", sourceInstanceId: (value as Extract<PaneBinding, { kind: "follow" }>).sourceInstanceId };
295
- }
296
- if ((value as PaneBinding).kind === "none") return { kind: "none" };
297
- return fallback;
298
- }
299
-
300
- function sanitizeFloatingPlacementMemory(value: unknown): FloatingPlacementMemory | undefined {
301
- if (!value || typeof value !== "object") return undefined;
302
- const x = typeof (value as FloatingPlacementMemory).x === "number" ? Math.max(0, Math.round((value as FloatingPlacementMemory).x)) : null;
303
- const y = typeof (value as FloatingPlacementMemory).y === "number" ? Math.max(0, Math.round((value as FloatingPlacementMemory).y)) : null;
304
- const width = typeof (value as FloatingPlacementMemory).width === "number" ? Math.max(1, Math.round((value as FloatingPlacementMemory).width)) : null;
305
- const height = typeof (value as FloatingPlacementMemory).height === "number" ? Math.max(1, Math.round((value as FloatingPlacementMemory).height)) : null;
306
- if (x === null || y === null || width === null || height === null) return undefined;
307
- return { x, y, width, height };
308
- }
309
-
310
- function sanitizePlacementMemory(value: unknown): PanePlacementMemory | undefined {
311
- if (!value || typeof value !== "object") return undefined;
312
-
313
- const docked = (() => {
314
- const raw = (value as PanePlacementMemory).docked;
315
- if (!raw || typeof raw !== "object") return undefined;
316
- const path = Array.isArray((raw as { path?: unknown }).path)
317
- ? (raw as { path?: unknown }).path
318
- ?.filter((segment): segment is 0 | 1 => segment === 0 || segment === 1)
319
- : undefined;
320
- const anchorInstanceId = typeof (raw as { anchorInstanceId?: unknown }).anchorInstanceId === "string"
321
- ? (raw as { anchorInstanceId: string }).anchorInstanceId
322
- : undefined;
323
- const position = ["left", "right", "above", "below"].includes(String((raw as { position?: unknown }).position))
324
- ? (raw as { position: "left" | "right" | "above" | "below" }).position
325
- : undefined;
326
- if (!path && !anchorInstanceId && !position) return undefined;
327
- return {
328
- path,
329
- anchorInstanceId,
330
- position,
331
- };
332
- })();
333
-
334
- const floating = sanitizeFloatingPlacementMemory((value as PanePlacementMemory).floating);
335
- if (!docked && !floating) return undefined;
336
- return { docked, floating };
337
- }
338
-
339
- function sanitizePaneInstances(value: unknown, fallback: LayoutConfig): PaneInstanceConfig[] {
340
- if (!Array.isArray(value)) return cloneLayout(fallback).instances;
341
- const seen = new Set<string>();
342
- const instances = value
343
- .filter((entry): entry is PaneInstanceConfig =>
344
- !!entry
345
- && typeof entry === "object"
346
- && typeof (entry as PaneInstanceConfig).instanceId === "string"
347
- && typeof (entry as PaneInstanceConfig).paneId === "string",
348
- )
349
- .map((entry) => {
350
- const instanceId = seen.has(entry.instanceId) ? createPaneInstanceId(entry.paneId) : entry.instanceId;
351
- seen.add(instanceId);
352
- return {
353
- instanceId,
354
- paneId: entry.paneId,
355
- title: typeof entry.title === "string" ? entry.title : undefined,
356
- binding: sanitizePaneBinding(entry.binding),
357
- params: entry.params && typeof entry.params === "object"
358
- ? Object.fromEntries(
359
- Object.entries(entry.params).filter((param): param is [string, string] => typeof param[1] === "string"),
360
- )
361
- : undefined,
362
- settings: sanitizePaneSettings(entry.settings),
363
- placementMemory: sanitizePlacementMemory(entry.placementMemory),
364
- };
365
- });
366
- return instances.length > 0 ? instances : cloneLayout(fallback).instances;
367
- }
368
-
369
- function sanitizePaneSettings(value: unknown): Record<string, unknown> | undefined {
370
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
371
-
372
- const sanitizeValue = (entry: unknown): unknown => {
373
- if (entry == null) return entry;
374
- if (typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean") {
375
- return entry;
376
- }
377
- if (Array.isArray(entry)) {
378
- return entry
379
- .map((child) => sanitizeValue(child))
380
- .filter((child) => child !== undefined);
381
- }
382
- if (typeof entry === "object") {
383
- return Object.fromEntries(
384
- Object.entries(entry as Record<string, unknown>)
385
- .map(([key, child]) => [key, sanitizeValue(child)])
386
- .filter(([, child]) => child !== undefined),
387
- );
388
- }
389
- return undefined;
390
- };
391
-
392
- const settings = Object.fromEntries(
393
- Object.entries(value as Record<string, unknown>)
394
- .map(([key, entry]) => [key, sanitizeValue(entry)])
395
- .filter(([, entry]) => entry !== undefined),
396
- );
397
-
398
- return Object.keys(settings).length > 0 ? clonePaneSettings(settings) : undefined;
399
- }
400
-
401
- function hasExactColumnIds(value: unknown, expected: string[]): boolean {
402
- return Array.isArray(value)
403
- && value.length === expected.length
404
- && value.every((entry, index) => entry === expected[index]);
405
- }
406
-
407
- function migrateLegacyPortfolioDefaultColumns(layout: LayoutConfig, enabled: boolean): LayoutConfig {
408
- if (!enabled) return layout;
409
-
410
- const instanceIndex = layout.instances.findIndex((instance) =>
411
- instance.instanceId === "portfolio-list:main"
412
- && instance.paneId === "portfolio-list"
413
- && hasExactColumnIds(instance.settings?.columnIds, LEGACY_MAIN_PORTFOLIO_COLUMN_IDS)
414
- );
415
- if (instanceIndex < 0) return layout;
416
-
417
- const nextLayout = cloneLayout(layout);
418
- nextLayout.instances[instanceIndex] = {
419
- ...nextLayout.instances[instanceIndex]!,
420
- settings: {
421
- ...(nextLayout.instances[instanceIndex]?.settings ?? {}),
422
- columnIds: [...DEFAULT_PORTFOLIO_COLUMN_IDS],
423
- },
424
- };
425
- return nextLayout;
426
- }
427
-
428
- function getDefaultFollowSourceInstanceId(instances: PaneInstanceConfig[]): string | null {
429
- return instances.find((instance) => instance.paneId === "portfolio-list")?.instanceId ?? null;
430
- }
431
-
432
- function sanitizeFloatingEntries(value: unknown, validInstanceIds: Set<string>): LayoutConfig["floating"] {
433
- if (!Array.isArray(value)) return [];
434
- return value
435
- .filter((entry): entry is LayoutConfig["floating"][number] =>
436
- !!entry
437
- && typeof entry === "object"
438
- && typeof (entry as LayoutConfig["floating"][number]).instanceId === "string"
439
- && typeof (entry as LayoutConfig["floating"][number]).x === "number"
440
- && typeof (entry as LayoutConfig["floating"][number]).y === "number"
441
- && typeof (entry as LayoutConfig["floating"][number]).width === "number"
442
- && typeof (entry as LayoutConfig["floating"][number]).height === "number",
443
- )
444
- .filter((entry) => validInstanceIds.has(entry.instanceId))
445
- .map((entry) => ({
446
- ...entry,
447
- x: Math.max(0, Math.round(entry.x)),
448
- y: Math.max(0, Math.round(entry.y)),
449
- width: Math.max(1, Math.round(entry.width)),
450
- height: Math.max(1, Math.round(entry.height)),
451
- zIndex: typeof entry.zIndex === "number" ? Math.round(entry.zIndex) : entry.zIndex,
452
- }));
453
- }
454
-
455
- export function sanitizeLayout(value: unknown, fallback: LayoutConfig): LayoutConfig {
456
- if (!isLayoutConfig(value)) {
457
- return cloneLayout(fallback);
458
- }
459
-
460
- if (!Array.isArray((value as LayoutConfig & { instances?: unknown }).instances)) {
461
- const layout = cloneLayout(fallback);
462
- return normalizePaneLayout(layout, {
463
- defaultFollowSourceInstanceId: getDefaultFollowSourceInstanceId(layout.instances),
464
- });
465
- }
466
-
467
- const instances = sanitizePaneInstances((value as LayoutConfig & { instances?: unknown }).instances, fallback);
468
- const validInstanceIds = new Set(instances.map((entry) => entry.instanceId));
469
- const dockRoot = (value as { dockRoot?: LayoutConfig["dockRoot"] }).dockRoot ?? null;
470
- const floating = sanitizeFloatingEntries((value as { floating?: unknown }).floating, validInstanceIds);
471
-
472
- const layout: LayoutConfig = {
473
- dockRoot,
474
- instances,
475
- floating,
476
- };
477
-
478
- return normalizePaneLayout(layout, {
479
- defaultFollowSourceInstanceId: getDefaultFollowSourceInstanceId(layout.instances),
480
- });
481
- }
482
-
483
- function sanitizeSavedLayouts(value: unknown, fallbackLayout: LayoutConfig): SavedLayout[] {
484
- if (!Array.isArray(value) || value.length === 0) {
485
- return [{ name: "Default", layout: cloneLayout(fallbackLayout) }];
486
- }
487
-
488
- const layouts = value
489
- .filter((entry): entry is SavedLayout =>
490
- !!entry
491
- && typeof entry === "object"
492
- && typeof (entry as SavedLayout).name === "string",
493
- )
494
- .map((entry) => ({
495
- name: entry.name,
496
- layout: sanitizeLayout(entry.layout, fallbackLayout),
497
- }));
498
-
499
- return layouts.length > 0 ? layouts : [{ name: "Default", layout: cloneLayout(fallbackLayout) }];
500
- }
501
-
502
- function sanitizeActiveLayoutIndex(value: unknown, layoutCount: number): number {
503
- if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value >= layoutCount) {
504
- return 0;
505
- }
506
- return value;
63
+ return (await getHost()).importConfig(dataDir, srcPath);
507
64
  }
@@ -1,3 +1,4 @@
1
+
1
2
  export type JsonPrimitive = string | number | boolean | null;
2
3
  export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
3
4
 
@@ -5,14 +6,10 @@ export function serializeJson(value: unknown): string {
5
6
  return JSON.stringify(value);
6
7
  }
7
8
 
8
- export function parseJson<T>(value: string): T {
9
- return JSON.parse(value) as T;
10
- }
11
-
12
9
  export function safeParseJson<T>(value: string | null): T | null {
13
10
  if (!value) return null;
14
11
  try {
15
- return parseJson<T>(value);
12
+ return JSON.parse(value) as T;
16
13
  } catch {
17
14
  return null;
18
15
  }
@@ -1,3 +1,4 @@
1
+
1
2
  const SQLITE_BUSY_RETRY_ATTEMPTS = 4;
2
3
  const SQLITE_BUSY_RETRY_INITIAL_DELAY_MS = 25;
3
4
  const SQLITE_BUSY_RETRY_MAX_DELAY_MS = 250;