gloomberb 0.5.0 → 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 -13
  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 +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  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 +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  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 +186 -6
  440. package/src/utils/api-client.ts +433 -33
  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
@@ -1,7 +1,8 @@
1
- import { findPaneInstance, type AppConfig } from "../types/config";
2
- import type { BrokerContractRef } from "../types/instrument";
3
- import type { TickerRecord } from "../types/ticker";
4
- import { getDockedPaneIds } from "../plugins/pane-manager";
1
+ import { findPaneInstance, type AppConfig } from "../../types/config";
2
+ import type { BrokerContractRef } from "../../types/instrument";
3
+ import type { TickerRecord } from "../../types/ticker";
4
+ import { getDockedPaneIds } from "../../plugins/pane-manager";
5
+ import { canonicalExchange, normalizeSymbol } from "../../utils/exchanges";
5
6
 
6
7
  export const APP_SESSION_SCHEMA_VERSION = 1;
7
8
  export const APP_SESSION_ID = "app";
@@ -38,8 +39,8 @@ interface SessionStateInput {
38
39
 
39
40
  function normalizeHydrationTarget(target: HydrationTarget): HydrationTarget {
40
41
  return {
41
- symbol: target.symbol.toUpperCase(),
42
- exchange: target.exchange?.toUpperCase(),
42
+ symbol: normalizeSymbol(target.symbol),
43
+ exchange: target.exchange ? canonicalExchange(target.exchange) : undefined,
43
44
  brokerId: target.brokerId,
44
45
  brokerInstanceId: target.brokerInstanceId,
45
46
  instrument: target.instrument ?? null,
@@ -0,0 +1,225 @@
1
+ import type {
2
+ DetachedPlacementMemory,
3
+ FloatingPlacementMemory,
4
+ LayoutConfig,
5
+ PaneBinding,
6
+ PaneInstanceConfig,
7
+ PanePlacementMemory,
8
+ } from "../types/config";
9
+ import {
10
+ cloneLayout,
11
+ clonePaneSettings,
12
+ createPaneInstanceId,
13
+ normalizePaneLayout,
14
+ } from "../types/config";
15
+
16
+ export function isLayoutConfig(value: unknown): value is LayoutConfig {
17
+ return !!value
18
+ && typeof value === "object"
19
+ && Array.isArray((value as LayoutConfig).instances)
20
+ && Array.isArray((value as LayoutConfig).floating)
21
+ && "dockRoot" in (value as Record<string, unknown>);
22
+ }
23
+
24
+ function sanitizePaneBinding(value: unknown, fallback: PaneBinding = { kind: "none" }): PaneBinding {
25
+ if (!value || typeof value !== "object") return fallback;
26
+ if ((value as PaneBinding).kind === "fixed" && typeof (value as Extract<PaneBinding, { kind: "fixed" }>).symbol === "string") {
27
+ return { kind: "fixed", symbol: (value as Extract<PaneBinding, { kind: "fixed" }>).symbol };
28
+ }
29
+ if ((value as PaneBinding).kind === "follow" && typeof (value as Extract<PaneBinding, { kind: "follow" }>).sourceInstanceId === "string") {
30
+ return { kind: "follow", sourceInstanceId: (value as Extract<PaneBinding, { kind: "follow" }>).sourceInstanceId };
31
+ }
32
+ if ((value as PaneBinding).kind === "none") return { kind: "none" };
33
+ return fallback;
34
+ }
35
+
36
+ function sanitizeFloatingPlacementMemory(value: unknown): FloatingPlacementMemory | undefined {
37
+ if (!value || typeof value !== "object") return undefined;
38
+ const x = typeof (value as FloatingPlacementMemory).x === "number" ? Math.max(0, Math.round((value as FloatingPlacementMemory).x)) : null;
39
+ const y = typeof (value as FloatingPlacementMemory).y === "number" ? Math.max(0, Math.round((value as FloatingPlacementMemory).y)) : null;
40
+ const width = typeof (value as FloatingPlacementMemory).width === "number" ? Math.max(1, Math.round((value as FloatingPlacementMemory).width)) : null;
41
+ const height = typeof (value as FloatingPlacementMemory).height === "number" ? Math.max(1, Math.round((value as FloatingPlacementMemory).height)) : null;
42
+ if (x === null || y === null || width === null || height === null) return undefined;
43
+ return { x, y, width, height };
44
+ }
45
+
46
+ function sanitizeDetachedPlacementMemory(value: unknown): DetachedPlacementMemory | undefined {
47
+ if (!value || typeof value !== "object") return undefined;
48
+ const x = typeof (value as DetachedPlacementMemory).x === "number" ? Math.max(0, Math.round((value as DetachedPlacementMemory).x)) : null;
49
+ const y = typeof (value as DetachedPlacementMemory).y === "number" ? Math.max(0, Math.round((value as DetachedPlacementMemory).y)) : null;
50
+ const width = typeof (value as DetachedPlacementMemory).width === "number" ? Math.max(1, Math.round((value as DetachedPlacementMemory).width)) : null;
51
+ const height = typeof (value as DetachedPlacementMemory).height === "number" ? Math.max(1, Math.round((value as DetachedPlacementMemory).height)) : null;
52
+ if (x === null || y === null || width === null || height === null) return undefined;
53
+ return { x, y, width, height };
54
+ }
55
+
56
+ function sanitizePlacementMemory(value: unknown): PanePlacementMemory | undefined {
57
+ if (!value || typeof value !== "object") return undefined;
58
+
59
+ const docked = (() => {
60
+ const raw = (value as PanePlacementMemory).docked;
61
+ if (!raw || typeof raw !== "object") return undefined;
62
+ const path = Array.isArray((raw as { path?: unknown }).path)
63
+ ? (raw as { path?: unknown }).path
64
+ ?.filter((segment): segment is 0 | 1 => segment === 0 || segment === 1)
65
+ : undefined;
66
+ const anchorInstanceId = typeof (raw as { anchorInstanceId?: unknown }).anchorInstanceId === "string"
67
+ ? (raw as { anchorInstanceId: string }).anchorInstanceId
68
+ : undefined;
69
+ const position = ["left", "right", "above", "below"].includes(String((raw as { position?: unknown }).position))
70
+ ? (raw as { position: "left" | "right" | "above" | "below" }).position
71
+ : undefined;
72
+ if (!path && !anchorInstanceId && !position) return undefined;
73
+ return {
74
+ path,
75
+ anchorInstanceId,
76
+ position,
77
+ };
78
+ })();
79
+
80
+ const floating = sanitizeFloatingPlacementMemory((value as PanePlacementMemory).floating);
81
+ const detached = sanitizeDetachedPlacementMemory((value as PanePlacementMemory).detached);
82
+ if (!docked && !floating && !detached) return undefined;
83
+ return { docked, floating, detached };
84
+ }
85
+
86
+ function sanitizePaneSettings(value: unknown): Record<string, unknown> | undefined {
87
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
88
+
89
+ const sanitizeValue = (entry: unknown): unknown => {
90
+ if (entry == null) return entry;
91
+ if (typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean") {
92
+ return entry;
93
+ }
94
+ if (Array.isArray(entry)) {
95
+ return entry
96
+ .map((child) => sanitizeValue(child))
97
+ .filter((child) => child !== undefined);
98
+ }
99
+ if (typeof entry === "object") {
100
+ return Object.fromEntries(
101
+ Object.entries(entry as Record<string, unknown>)
102
+ .map(([key, child]) => [key, sanitizeValue(child)])
103
+ .filter(([, child]) => child !== undefined),
104
+ );
105
+ }
106
+ return undefined;
107
+ };
108
+
109
+ const settings = Object.fromEntries(
110
+ Object.entries(value as Record<string, unknown>)
111
+ .map(([key, entry]) => [key, sanitizeValue(entry)])
112
+ .filter(([, entry]) => entry !== undefined),
113
+ );
114
+
115
+ return Object.keys(settings).length > 0 ? clonePaneSettings(settings) : undefined;
116
+ }
117
+
118
+ function sanitizePaneInstances(value: unknown, fallback: LayoutConfig): PaneInstanceConfig[] {
119
+ if (!Array.isArray(value)) return cloneLayout(fallback).instances;
120
+ const seen = new Set<string>();
121
+ const instances = value
122
+ .filter((entry): entry is PaneInstanceConfig =>
123
+ !!entry
124
+ && typeof entry === "object"
125
+ && typeof (entry as PaneInstanceConfig).instanceId === "string"
126
+ && typeof (entry as PaneInstanceConfig).paneId === "string",
127
+ )
128
+ .map((entry) => {
129
+ const instanceId = seen.has(entry.instanceId) ? createPaneInstanceId(entry.paneId) : entry.instanceId;
130
+ seen.add(instanceId);
131
+ return {
132
+ instanceId,
133
+ paneId: entry.paneId,
134
+ title: typeof entry.title === "string" ? entry.title : undefined,
135
+ binding: sanitizePaneBinding(entry.binding),
136
+ params: entry.params && typeof entry.params === "object"
137
+ ? Object.fromEntries(
138
+ Object.entries(entry.params).filter((param): param is [string, string] => typeof param[1] === "string"),
139
+ )
140
+ : undefined,
141
+ settings: sanitizePaneSettings(entry.settings),
142
+ placementMemory: sanitizePlacementMemory(entry.placementMemory),
143
+ };
144
+ });
145
+ return instances.length > 0 ? instances : cloneLayout(fallback).instances;
146
+ }
147
+
148
+ function getDefaultFollowSourceInstanceId(instances: PaneInstanceConfig[]): string | null {
149
+ return instances.find((instance) => instance.paneId === "portfolio-list")?.instanceId ?? null;
150
+ }
151
+
152
+ function sanitizeFloatingEntries(value: unknown, validInstanceIds: Set<string>): LayoutConfig["floating"] {
153
+ if (!Array.isArray(value)) return [];
154
+ return value
155
+ .filter((entry): entry is LayoutConfig["floating"][number] =>
156
+ !!entry
157
+ && typeof entry === "object"
158
+ && typeof (entry as LayoutConfig["floating"][number]).instanceId === "string"
159
+ && typeof (entry as LayoutConfig["floating"][number]).x === "number"
160
+ && typeof (entry as LayoutConfig["floating"][number]).y === "number"
161
+ && typeof (entry as LayoutConfig["floating"][number]).width === "number"
162
+ && typeof (entry as LayoutConfig["floating"][number]).height === "number",
163
+ )
164
+ .filter((entry) => validInstanceIds.has(entry.instanceId))
165
+ .map((entry) => ({
166
+ ...entry,
167
+ x: Math.max(0, Math.round(entry.x)),
168
+ y: Math.max(0, Math.round(entry.y)),
169
+ width: Math.max(1, Math.round(entry.width)),
170
+ height: Math.max(1, Math.round(entry.height)),
171
+ zIndex: typeof entry.zIndex === "number" ? Math.round(entry.zIndex) : entry.zIndex,
172
+ }));
173
+ }
174
+
175
+ function sanitizeDetachedEntries(value: unknown, validInstanceIds: Set<string>): LayoutConfig["detached"] {
176
+ if (!Array.isArray(value)) return [];
177
+ return value
178
+ .filter((entry): entry is LayoutConfig["detached"][number] =>
179
+ !!entry
180
+ && typeof entry === "object"
181
+ && typeof (entry as LayoutConfig["detached"][number]).instanceId === "string"
182
+ && typeof (entry as LayoutConfig["detached"][number]).x === "number"
183
+ && typeof (entry as LayoutConfig["detached"][number]).y === "number"
184
+ && typeof (entry as LayoutConfig["detached"][number]).width === "number"
185
+ && typeof (entry as LayoutConfig["detached"][number]).height === "number",
186
+ )
187
+ .filter((entry) => validInstanceIds.has(entry.instanceId))
188
+ .map((entry) => ({
189
+ instanceId: entry.instanceId,
190
+ x: Math.max(0, Math.round(entry.x)),
191
+ y: Math.max(0, Math.round(entry.y)),
192
+ width: Math.max(1, Math.round(entry.width)),
193
+ height: Math.max(1, Math.round(entry.height)),
194
+ }));
195
+ }
196
+
197
+ export function sanitizeLayout(value: unknown, fallback: LayoutConfig): LayoutConfig {
198
+ if (!isLayoutConfig(value)) {
199
+ return cloneLayout(fallback);
200
+ }
201
+
202
+ if (!Array.isArray((value as LayoutConfig & { instances?: unknown }).instances)) {
203
+ const layout = cloneLayout(fallback);
204
+ return normalizePaneLayout(layout, {
205
+ defaultFollowSourceInstanceId: getDefaultFollowSourceInstanceId(layout.instances),
206
+ });
207
+ }
208
+
209
+ const instances = sanitizePaneInstances((value as LayoutConfig & { instances?: unknown }).instances, fallback);
210
+ const validInstanceIds = new Set(instances.map((entry) => entry.instanceId));
211
+ const dockRoot = (value as { dockRoot?: LayoutConfig["dockRoot"] }).dockRoot ?? null;
212
+ const floating = sanitizeFloatingEntries((value as { floating?: unknown }).floating, validInstanceIds);
213
+ const detached = sanitizeDetachedEntries((value as { detached?: unknown }).detached, validInstanceIds);
214
+
215
+ const layout: LayoutConfig = {
216
+ dockRoot,
217
+ instances,
218
+ floating,
219
+ detached,
220
+ };
221
+
222
+ return normalizePaneLayout(layout, {
223
+ defaultFollowSourceInstanceId: getDefaultFollowSourceInstanceId(layout.instances),
224
+ });
225
+ }
@@ -0,0 +1,390 @@
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
+ LayoutConfig,
8
+ SavedLayout,
9
+ } from "../types/config";
10
+ import {
11
+ cloneLayout,
12
+ createDefaultConfig,
13
+ CURRENT_CONFIG_VERSION,
14
+ DEFAULT_COLUMNS,
15
+ DEFAULT_PORTFOLIO_COLUMN_IDS,
16
+ } from "../types/config";
17
+ import type { Portfolio, Watchlist } from "../types/ticker";
18
+ import { debugLog } from "../utils/debug-log";
19
+ import { isLayoutConfig, sanitizeLayout } from "./config-layout";
20
+
21
+ const configLog = debugLog.createLogger("config");
22
+ const LEGACY_MAIN_PORTFOLIO_COLUMN_IDS = DEFAULT_COLUMNS.map((column) => column.id);
23
+ const BUILTIN_SOURCE_IDS = new Set(["yahoo", "gloomberb-cloud"]);
24
+ const BUILTIN_PLUGIN_GROUP_ALIASES: Record<string, string> = {
25
+ "comparison-chart": "market-overview",
26
+ correlation: "market-overview",
27
+ "earnings-calendar": "macro",
28
+ "fx-matrix": "market-overview",
29
+ holders: "company-research",
30
+ insider: "company-research",
31
+ "market-movers": "market-overview",
32
+ options: "company-research",
33
+ research: "company-research",
34
+ sectors: "market-overview",
35
+ sec: "company-research",
36
+ "world-indices": "market-overview",
37
+ };
38
+
39
+ function getGlobalConfigDir(): string {
40
+ return join(process.env.HOME || "~", ".gloomberb");
41
+ }
42
+
43
+ function getGlobalConfigFile(): string {
44
+ return join(getGlobalConfigDir(), "config.json");
45
+ }
46
+
47
+ export async function getDataDir(): Promise<string | null> {
48
+ try {
49
+ const raw = await readFile(getGlobalConfigFile(), "utf-8");
50
+ const config = JSON.parse(raw) as { dataDir?: string };
51
+ return config.dataDir || null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+
58
+ export async function loadConfig(dataDir: string): Promise<AppConfig> {
59
+ const { config } = await loadConfigState(dataDir);
60
+ return config;
61
+ }
62
+
63
+ async function loadConfigState(dataDir: string): Promise<{ config: AppConfig; needsSave: boolean }> {
64
+ const configPath = join(dataDir, "config.json");
65
+
66
+ try {
67
+ const raw = await readFile(configPath, "utf-8");
68
+ const saved = JSON.parse(raw) as Record<string, unknown>;
69
+ return normalizeConfig(saved, dataDir);
70
+ } catch {
71
+ return { config: createDefaultConfig(dataDir), needsSave: true };
72
+ }
73
+ }
74
+
75
+ function normalizeConfig(saved: Record<string, unknown>, dataDir: string): { config: AppConfig; needsSave: boolean } {
76
+ const defaults = createDefaultConfig(dataDir);
77
+ const shouldMigratePortfolioDefaults =
78
+ typeof saved.configVersion !== "number" || saved.configVersion < CURRENT_CONFIG_VERSION;
79
+ const shouldEnableCloudDefault =
80
+ typeof saved.configVersion !== "number" || saved.configVersion < 13;
81
+ const directLayout = migrateLegacyPortfolioDefaultColumns(
82
+ sanitizeLayout(saved.layout, defaults.layout),
83
+ shouldMigratePortfolioDefaults,
84
+ );
85
+ const layouts = sanitizeSavedLayouts(saved.layouts, directLayout).map((entry) => ({
86
+ ...entry,
87
+ layout: migrateLegacyPortfolioDefaultColumns(entry.layout, shouldMigratePortfolioDefaults),
88
+ }));
89
+ const activeLayoutIndex = sanitizeActiveLayoutIndex(saved.activeLayoutIndex, layouts.length);
90
+ const layout = cloneLayout(layouts[activeLayoutIndex]?.layout ?? directLayout);
91
+ const syncedLayouts = layouts.map((entry, index) => (
92
+ index === activeLayoutIndex ? { ...entry, layout: cloneLayout(layout) } : entry
93
+ ));
94
+
95
+ const disabledPlugins = sanitizeDisabledPlugins(saved, defaults.disabledPlugins, {
96
+ enableCloudDefault: shouldEnableCloudDefault,
97
+ });
98
+
99
+ const config: AppConfig = {
100
+ dataDir,
101
+ configVersion: CURRENT_CONFIG_VERSION,
102
+ baseCurrency: typeof saved.baseCurrency === "string" ? saved.baseCurrency : defaults.baseCurrency,
103
+ refreshIntervalMinutes: typeof saved.refreshIntervalMinutes === "number" ? saved.refreshIntervalMinutes : defaults.refreshIntervalMinutes,
104
+ portfolios: sanitizePortfolios(saved.portfolios, defaults.portfolios),
105
+ watchlists: sanitizeWatchlists(saved.watchlists, defaults.watchlists),
106
+ layout,
107
+ layouts: syncedLayouts,
108
+ activeLayoutIndex,
109
+ brokerInstances: sanitizeBrokerInstances(saved.brokerInstances),
110
+ plugins: sanitizeStringArray(saved.plugins, defaults.plugins),
111
+ disabledPlugins,
112
+ disabledSources: sanitizeDisabledSources(saved, defaults.disabledSources, disabledPlugins),
113
+ pluginConfig: sanitizePluginConfig(saved.pluginConfig),
114
+ theme: typeof saved.theme === "string" ? saved.theme : defaults.theme,
115
+ chartPreferences: sanitizeChartPreferences(saved.chartPreferences, defaults.chartPreferences),
116
+ recentTickers: sanitizeStringArray(saved.recentTickers, defaults.recentTickers),
117
+ onboardingComplete: typeof saved.onboardingComplete === "boolean" ? saved.onboardingComplete : defaults.onboardingComplete,
118
+ };
119
+
120
+ const needsSave =
121
+ saved.configVersion !== CURRENT_CONFIG_VERSION
122
+ || !isLayoutConfig(saved.layout)
123
+ || !Array.isArray((saved.layout as { instances?: unknown })?.instances)
124
+ || !Array.isArray(saved.layouts)
125
+ || !Array.isArray(saved.brokerInstances)
126
+ || !Array.isArray(saved.disabledSources)
127
+ || !isPluginConfigMap(saved.pluginConfig)
128
+ || !isChartPreferences(saved.chartPreferences)
129
+ || typeof saved.activeLayoutIndex !== "number";
130
+
131
+ return { config, needsSave };
132
+ }
133
+
134
+ export async function saveConfig(config: AppConfig): Promise<void> {
135
+ const configPath = join(config.dataDir, "config.json");
136
+ await mkdir(dirname(configPath), { recursive: true });
137
+
138
+ const activeLayoutIndex = sanitizeActiveLayoutIndex(config.activeLayoutIndex, config.layouts.length || 1);
139
+ const layout = sanitizeLayout(config.layout, createDefaultConfig(config.dataDir).layout);
140
+ const layouts = sanitizeSavedLayouts(config.layouts, layout).map((entry, index) => (
141
+ index === activeLayoutIndex ? { ...entry, layout: cloneLayout(layout) } : entry
142
+ ));
143
+
144
+ const persisted: AppConfig = {
145
+ ...config,
146
+ configVersion: CURRENT_CONFIG_VERSION,
147
+ portfolios: sanitizePortfolios(config.portfolios, []),
148
+ watchlists: sanitizeWatchlists(config.watchlists, []),
149
+ layout,
150
+ layouts,
151
+ activeLayoutIndex,
152
+ brokerInstances: sanitizeBrokerInstances(config.brokerInstances),
153
+ plugins: sanitizeStringArray(config.plugins, []),
154
+ disabledPlugins: sanitizeDisabledPluginList(config.disabledPlugins),
155
+ disabledSources: sanitizeUniqueStringList(config.disabledSources),
156
+ pluginConfig: sanitizePluginConfig(config.pluginConfig),
157
+ chartPreferences: sanitizeChartPreferences(config.chartPreferences, createDefaultConfig(config.dataDir).chartPreferences),
158
+ recentTickers: sanitizeStringArray(config.recentTickers, []),
159
+ };
160
+
161
+ await writeFile(configPath, JSON.stringify(persisted, null, 2), "utf-8");
162
+ }
163
+
164
+ export async function initDataDir(dataDir: string): Promise<AppConfig> {
165
+ configLog.info(`Initializing data directory: ${dataDir}`);
166
+ await mkdir(dataDir, { recursive: true });
167
+ const { config, needsSave } = await loadConfigState(dataDir);
168
+ if (needsSave) {
169
+ await saveConfig(config);
170
+ }
171
+ return config;
172
+ }
173
+
174
+ export async function resetAllData(dataDir: string): Promise<void> {
175
+ await rm(dataDir, { recursive: true, force: true });
176
+ }
177
+
178
+ export async function exportConfig(config: AppConfig, destPath: string): Promise<void> {
179
+ const { dataDir, ...rest } = config;
180
+ await writeFile(destPath, JSON.stringify(rest, null, 2), "utf-8");
181
+ }
182
+
183
+ export async function importConfig(dataDir: string, srcPath: string): Promise<AppConfig> {
184
+ const raw = await readFile(srcPath, "utf-8");
185
+ const saved = JSON.parse(raw) as Record<string, unknown>;
186
+ const { config } = normalizeConfig(saved, dataDir);
187
+ await saveConfig(config);
188
+ return config;
189
+ }
190
+
191
+ function sanitizeStringArray(value: unknown, fallback: string[]): string[] {
192
+ return Array.isArray(value)
193
+ ? value.filter((entry): entry is string => typeof entry === "string")
194
+ : fallback;
195
+ }
196
+
197
+ function normalizeBuiltinPluginId(pluginId: string): string {
198
+ return BUILTIN_PLUGIN_GROUP_ALIASES[pluginId] ?? pluginId;
199
+ }
200
+
201
+ function sanitizeUniqueStringList(value: unknown): string[] {
202
+ return [...new Set(sanitizeStringArray(value, []))];
203
+ }
204
+
205
+ function sanitizeDisabledPluginList(value: unknown, options: { expandLegacyCloudMacro?: boolean } = {}): string[] {
206
+ const raw = sanitizeUniqueStringList(value);
207
+ const disabled = raw.map(normalizeBuiltinPluginId);
208
+ if (options.expandLegacyCloudMacro && raw.includes("gloomberb-cloud")) {
209
+ disabled.push("macro");
210
+ }
211
+ return [...new Set(disabled)];
212
+ }
213
+
214
+ function sanitizeDisabledPlugins(
215
+ saved: Record<string, unknown>,
216
+ fallback: string[],
217
+ options: { enableCloudDefault?: boolean } = {},
218
+ ): string[] {
219
+ const expandLegacyCloudMacro =
220
+ !options.enableCloudDefault
221
+ && (typeof saved.configVersion !== "number" || saved.configVersion < CURRENT_CONFIG_VERSION);
222
+ const disabled = sanitizeDisabledPluginList(saved.disabledPlugins ?? fallback, {
223
+ expandLegacyCloudMacro,
224
+ });
225
+ return options.enableCloudDefault
226
+ ? disabled.filter((pluginId) => pluginId !== "gloomberb-cloud")
227
+ : disabled;
228
+ }
229
+
230
+ function sanitizeDisabledSources(saved: Record<string, unknown>, fallback: string[], disabledPlugins: string[]): string[] {
231
+ const explicit = sanitizeUniqueStringList(saved.disabledSources ?? fallback);
232
+ const legacyPluginIds = sanitizeUniqueStringList(disabledPlugins)
233
+ .filter((pluginId) => BUILTIN_SOURCE_IDS.has(pluginId));
234
+ return [...new Set([...explicit, ...legacyPluginIds])];
235
+ }
236
+
237
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
238
+ return !!value && typeof value === "object" && !Array.isArray(value);
239
+ }
240
+
241
+ function isPluginConfigMap(value: unknown): value is Record<string, Record<string, unknown>> {
242
+ if (!isPlainRecord(value)) return false;
243
+ return Object.values(value).every((entry) => isPlainRecord(entry));
244
+ }
245
+
246
+ function sanitizePluginConfig(value: unknown): Record<string, Record<string, unknown>> {
247
+ if (!isPluginConfigMap(value)) return {};
248
+ return Object.fromEntries(
249
+ Object.entries(value).reduce<Array<[string, Record<string, unknown>]>>((entries, [pluginId, state]) => {
250
+ const normalizedPluginId = normalizeBuiltinPluginId(pluginId);
251
+ const existing = entries.find(([entryPluginId]) => entryPluginId === normalizedPluginId);
252
+ if (existing) {
253
+ existing[1] = { ...state, ...existing[1] };
254
+ } else {
255
+ entries.push([normalizedPluginId, { ...state }]);
256
+ }
257
+ return entries;
258
+ }, []),
259
+ );
260
+ }
261
+
262
+ function isChartPreferences(value: unknown): value is ChartPreferences {
263
+ if (!value || typeof value !== "object") return false;
264
+ const defaultRenderMode = (value as ChartPreferences).defaultRenderMode;
265
+ const renderer = (value as ChartPreferences).renderer;
266
+ return (defaultRenderMode === "area"
267
+ || defaultRenderMode === "line"
268
+ || defaultRenderMode === "candles"
269
+ || defaultRenderMode === "ohlc")
270
+ && (renderer === "auto" || renderer === "kitty" || renderer === "braille");
271
+ }
272
+
273
+ function sanitizeChartPreferences(value: unknown, fallback: ChartPreferences): ChartPreferences {
274
+ if (!value || typeof value !== "object") return { ...fallback };
275
+
276
+ const candidate = value as Partial<ChartPreferences>;
277
+ const defaultRenderMode = candidate.defaultRenderMode === "area"
278
+ || candidate.defaultRenderMode === "line"
279
+ || candidate.defaultRenderMode === "candles"
280
+ || candidate.defaultRenderMode === "ohlc"
281
+ ? candidate.defaultRenderMode
282
+ : fallback.defaultRenderMode;
283
+ const renderer = candidate.renderer === "auto"
284
+ || candidate.renderer === "kitty"
285
+ || candidate.renderer === "braille"
286
+ ? candidate.renderer
287
+ : fallback.renderer;
288
+
289
+ return {
290
+ defaultRenderMode,
291
+ renderer,
292
+ };
293
+ }
294
+
295
+ function sanitizePortfolios(value: unknown, fallback: Portfolio[]): Portfolio[] {
296
+ if (!Array.isArray(value)) return fallback.map((portfolio) => ({ ...portfolio }));
297
+ return value
298
+ .filter((entry): entry is Portfolio =>
299
+ !!entry
300
+ && typeof entry === "object"
301
+ && typeof (entry as Portfolio).id === "string"
302
+ && typeof (entry as Portfolio).name === "string"
303
+ && typeof (entry as Portfolio).currency === "string",
304
+ )
305
+ .map((entry) => ({ ...entry }));
306
+ }
307
+
308
+ function sanitizeWatchlists(value: unknown, fallback: Watchlist[]): Watchlist[] {
309
+ if (!Array.isArray(value)) return fallback.map((watchlist) => ({ ...watchlist }));
310
+ return value
311
+ .filter((entry): entry is Watchlist =>
312
+ !!entry
313
+ && typeof entry === "object"
314
+ && typeof (entry as Watchlist).id === "string"
315
+ && typeof (entry as Watchlist).name === "string",
316
+ )
317
+ .map((entry) => ({ ...entry }));
318
+ }
319
+
320
+ function sanitizeBrokerInstances(value: unknown): BrokerInstanceConfig[] {
321
+ if (!Array.isArray(value)) return [];
322
+ return value
323
+ .filter((entry): entry is BrokerInstanceConfig =>
324
+ !!entry
325
+ && typeof entry === "object"
326
+ && typeof (entry as BrokerInstanceConfig).id === "string"
327
+ && typeof (entry as BrokerInstanceConfig).brokerType === "string"
328
+ && typeof (entry as BrokerInstanceConfig).label === "string"
329
+ && typeof (entry as BrokerInstanceConfig).config === "object"
330
+ && (entry as BrokerInstanceConfig).config !== null,
331
+ )
332
+ .map((entry) => ({
333
+ ...entry,
334
+ enabled: entry.enabled ?? true,
335
+ config: { ...entry.config },
336
+ }));
337
+ }
338
+
339
+ function hasExactColumnIds(value: unknown, expected: string[]): boolean {
340
+ return Array.isArray(value)
341
+ && value.length === expected.length
342
+ && value.every((entry, index) => entry === expected[index]);
343
+ }
344
+
345
+ function migrateLegacyPortfolioDefaultColumns(layout: LayoutConfig, enabled: boolean): LayoutConfig {
346
+ if (!enabled) return layout;
347
+
348
+ const instanceIndex = layout.instances.findIndex((instance) =>
349
+ instance.instanceId === "portfolio-list:main"
350
+ && instance.paneId === "portfolio-list"
351
+ && hasExactColumnIds(instance.settings?.columnIds, LEGACY_MAIN_PORTFOLIO_COLUMN_IDS)
352
+ );
353
+ if (instanceIndex < 0) return layout;
354
+
355
+ const nextLayout = cloneLayout(layout);
356
+ nextLayout.instances[instanceIndex] = {
357
+ ...nextLayout.instances[instanceIndex]!,
358
+ settings: {
359
+ ...(nextLayout.instances[instanceIndex]?.settings ?? {}),
360
+ columnIds: [...DEFAULT_PORTFOLIO_COLUMN_IDS],
361
+ },
362
+ };
363
+ return nextLayout;
364
+ }
365
+
366
+ function sanitizeSavedLayouts(value: unknown, fallbackLayout: LayoutConfig): SavedLayout[] {
367
+ if (!Array.isArray(value) || value.length === 0) {
368
+ return [{ name: "Default", layout: cloneLayout(fallbackLayout) }];
369
+ }
370
+
371
+ const layouts = value
372
+ .filter((entry): entry is SavedLayout =>
373
+ !!entry
374
+ && typeof entry === "object"
375
+ && typeof (entry as SavedLayout).name === "string",
376
+ )
377
+ .map((entry) => ({
378
+ name: entry.name,
379
+ layout: sanitizeLayout(entry.layout, fallbackLayout),
380
+ }));
381
+
382
+ return layouts.length > 0 ? layouts : [{ name: "Default", layout: cloneLayout(fallbackLayout) }];
383
+ }
384
+
385
+ function sanitizeActiveLayoutIndex(value: unknown, layoutCount: number): number {
386
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value >= layoutCount) {
387
+ return 0;
388
+ }
389
+ return value;
390
+ }