gloomberb 0.5.1 → 0.6.1

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 (506) hide show
  1. package/README.md +91 -11
  2. package/package.json +13 -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 +318 -158
  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 +53 -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/native/surface-visibility.ts +48 -0
  53. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  54. package/src/components/chart/static-chart-surface.tsx +191 -0
  55. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  56. package/src/components/chart/stock-chart.tsx +452 -191
  57. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  58. package/src/components/command-bar/command-bar.test.tsx +269 -20
  59. package/src/components/command-bar/command-bar.tsx +830 -353
  60. package/src/components/command-bar/command-registry.ts +10 -20
  61. package/src/components/command-bar/helpers.test.ts +1 -1
  62. package/src/components/command-bar/helpers.ts +9 -11
  63. package/src/components/command-bar/pane-template-display.ts +4 -8
  64. package/src/components/command-bar/root-shortcuts.ts +12 -11
  65. package/src/components/command-bar/view-model.test.ts +8 -42
  66. package/src/components/command-bar/view-model.ts +8 -36
  67. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  68. package/src/components/command-bar/workflow-ops.ts +40 -9
  69. package/src/components/command-bar/workflow-types.ts +2 -1
  70. package/src/components/data-table-stack-view.test.tsx +206 -0
  71. package/src/components/data-table-stack-view.tsx +152 -0
  72. package/src/components/data-table-view.test.tsx +188 -0
  73. package/src/components/data-table-view.tsx +279 -0
  74. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  75. package/src/components/feed-data-table-stack-view.tsx +365 -0
  76. package/src/components/index.ts +26 -31
  77. package/src/components/layout/detached-pane-shell.tsx +185 -0
  78. package/src/components/layout/floating-pane.tsx +87 -14
  79. package/src/components/layout/header.tsx +139 -49
  80. package/src/components/layout/pane-content.tsx +42 -0
  81. package/src/components/layout/pane-footer.test.tsx +195 -0
  82. package/src/components/layout/pane-footer.tsx +425 -0
  83. package/src/components/layout/pane-header.test.tsx +39 -0
  84. package/src/components/layout/pane-header.tsx +192 -14
  85. package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
  86. package/src/components/layout/pane-sizing.ts +9 -10
  87. package/src/components/layout/pane-title.ts +35 -0
  88. package/src/components/layout/pane.tsx +52 -11
  89. package/src/components/layout/shell.test.tsx +315 -107
  90. package/src/components/layout/shell.tsx +1010 -368
  91. package/src/components/layout/status-bar.test.tsx +17 -10
  92. package/src/components/layout/status-bar.tsx +367 -100
  93. package/src/components/layout/titlebar-overlay.ts +2 -0
  94. package/src/components/markdown-editor.tsx +29 -14
  95. package/src/components/markdown-text.tsx +25 -24
  96. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  97. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  98. package/src/components/pane-settings-dialog.tsx +521 -89
  99. package/src/components/pane-template-wizard.test.tsx +1 -1
  100. package/src/components/pane-template-wizard.tsx +38 -37
  101. package/src/components/price-selector-dialog.tsx +22 -21
  102. package/src/components/ticker-badge-list.tsx +53 -0
  103. package/src/components/ticker-badge-text.test.tsx +44 -1
  104. package/src/components/ticker-badge-text.tsx +78 -38
  105. package/src/components/ticker-badge.tsx +28 -7
  106. package/src/components/ticker-list-table-view.tsx +248 -0
  107. package/src/components/ticker-list-table.test.tsx +47 -1
  108. package/src/components/ticker-list-table.tsx +212 -41
  109. package/src/components/toggle-list.tsx +114 -8
  110. package/src/components/ui/button.tsx +24 -16
  111. package/src/components/ui/choice-dialog.tsx +89 -0
  112. package/src/components/ui/data-table.tsx +313 -114
  113. package/src/components/ui/external-link.tsx +52 -13
  114. package/src/components/ui/fields.tsx +44 -30
  115. package/src/components/ui/frame.tsx +24 -57
  116. package/src/components/ui/index.ts +15 -50
  117. package/src/components/ui/list-view.tsx +65 -25
  118. package/src/components/ui/loading.tsx +5 -60
  119. package/src/components/ui/message-composer.test.tsx +81 -0
  120. package/src/components/ui/message-composer.tsx +139 -0
  121. package/src/components/ui/multi-select-dialog.tsx +100 -32
  122. package/src/components/ui/multi-select.ts +112 -0
  123. package/src/components/ui/native-select.tsx +120 -0
  124. package/src/components/ui/page-stack-view.tsx +49 -19
  125. package/src/components/ui/remote-image.tsx +50 -0
  126. package/src/components/ui/row-value-cache.test.ts +17 -0
  127. package/src/components/ui/row-value-cache.ts +41 -0
  128. package/src/components/ui/shortcut-hint.tsx +55 -0
  129. package/src/components/ui/status.tsx +12 -67
  130. package/src/components/ui/table-layout.ts +59 -0
  131. package/src/components/ui/tabs.tsx +249 -27
  132. package/src/components/ui/toggle.tsx +19 -127
  133. package/src/components/ui/ui.test.tsx +266 -53
  134. package/src/components/use-double-click-activation.ts +7 -2
  135. package/src/components/use-external-link-footer.ts +79 -0
  136. package/src/core/app-services.ts +95 -0
  137. package/src/core/state/app-state.ts +886 -0
  138. package/src/{state → core/state}/session-persistence.ts +7 -6
  139. package/src/data/config-layout.ts +225 -0
  140. package/src/data/config-store-node.ts +390 -0
  141. package/src/data/config-store.test.ts +72 -0
  142. package/src/data/config-store.ts +35 -478
  143. package/src/data/sqlite-json.ts +2 -5
  144. package/src/data/sqlite-retry.ts +1 -0
  145. package/src/index.tsx +2 -68
  146. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  147. package/src/market-data/coordinator.test.ts +73 -4
  148. package/src/market-data/coordinator.ts +190 -24
  149. package/src/market-data/hooks.test.tsx +1 -1
  150. package/src/market-data/hooks.tsx +109 -87
  151. package/src/market-data/query-store.ts +3 -3
  152. package/src/market-data/request-types.test.ts +0 -14
  153. package/src/market-data/result-types.ts +1 -0
  154. package/src/news/aggregator.test.ts +267 -0
  155. package/src/news/aggregator.ts +452 -0
  156. package/src/news/hooks.ts +39 -0
  157. package/src/news/ticker-symbols.ts +21 -0
  158. package/src/news/types.ts +77 -0
  159. package/src/notifications/app-notifier.ts +21 -10
  160. package/src/pane-settings.test.ts +0 -2
  161. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  162. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  163. package/src/plugins/builtin/ai/providers.ts +1 -9
  164. package/src/plugins/builtin/ai/runner.ts +41 -5
  165. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  166. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  167. package/src/plugins/builtin/ai/settings.ts +0 -1
  168. package/src/plugins/builtin/ai/utils.ts +1 -71
  169. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  170. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  171. package/src/plugins/builtin/alerts/index.tsx +263 -155
  172. package/src/plugins/builtin/alerts/types.ts +7 -0
  173. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  174. package/src/plugins/builtin/analytics/index.tsx +78 -120
  175. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  176. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  177. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  178. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  179. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  180. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  181. package/src/plugins/builtin/chat-controller.test.ts +129 -0
  182. package/src/plugins/builtin/chat-controller.ts +85 -14
  183. package/src/plugins/builtin/chat.test.tsx +283 -36
  184. package/src/plugins/builtin/chat.tsx +363 -170
  185. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  186. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  187. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  188. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  189. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  190. package/src/plugins/builtin/correlation/index.tsx +51 -46
  191. package/src/plugins/builtin/correlation/settings.ts +1 -2
  192. package/src/plugins/builtin/debug.tsx +99 -70
  193. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  194. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  195. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  196. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  197. package/src/plugins/builtin/earnings/index.tsx +187 -94
  198. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  199. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  200. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  201. package/src/plugins/builtin/econ/index.tsx +236 -421
  202. package/src/plugins/builtin/econ/types.ts +1 -0
  203. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  204. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  205. package/src/plugins/builtin/help.test.tsx +120 -60
  206. package/src/plugins/builtin/help.tsx +266 -82
  207. package/src/plugins/builtin/holders/index.test.ts +45 -0
  208. package/src/plugins/builtin/holders/index.tsx +949 -0
  209. package/src/plugins/builtin/insider/index.tsx +207 -253
  210. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  211. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  212. package/src/plugins/builtin/layout-manager.tsx +12 -40
  213. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  214. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  215. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  216. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  217. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  218. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  219. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  220. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  221. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  222. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  223. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  224. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  225. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  226. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  227. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  228. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  229. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  230. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  231. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  232. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  233. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  234. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  235. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  236. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  237. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  238. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  239. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  240. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  241. package/src/plugins/builtin/news.tsx +98 -25
  242. package/src/plugins/builtin/notes-files.ts +51 -10
  243. package/src/plugins/builtin/notes.tsx +332 -131
  244. package/src/plugins/builtin/options.test.tsx +185 -0
  245. package/src/plugins/builtin/options.tsx +346 -206
  246. package/src/plugins/builtin/plugin-groups.ts +92 -0
  247. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  248. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  249. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  250. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  251. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  252. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  253. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  254. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  255. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  256. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  257. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  258. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  259. package/src/plugins/builtin/research/index.tsx +724 -0
  260. package/src/plugins/builtin/sec.tsx +67 -19
  261. package/src/plugins/builtin/sectors/index.tsx +42 -80
  262. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  263. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  264. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  265. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  266. package/src/plugins/builtin/ticker-detail/pane.tsx +143 -49
  267. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  268. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  269. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +82 -10
  270. package/src/plugins/builtin/ticker-detail.test.tsx +258 -33
  271. package/src/plugins/builtin/ticker-surface.ts +68 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  273. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  274. package/src/plugins/builtin/yahoo.ts +55 -1
  275. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  276. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  277. package/src/plugins/catalog-ui.test.ts +24 -0
  278. package/src/plugins/catalog-ui.ts +44 -0
  279. package/src/plugins/catalog.ts +2 -50
  280. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  281. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  282. package/src/plugins/ibkr/account-cache.ts +15 -120
  283. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  284. package/src/plugins/ibkr/config.test.ts +0 -18
  285. package/src/plugins/ibkr/dialogs.tsx +14 -56
  286. package/src/plugins/ibkr/flex.test.ts +57 -2
  287. package/src/plugins/ibkr/flex.ts +116 -12
  288. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  289. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  290. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  291. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  292. package/src/plugins/ibkr/gateway-types.ts +27 -0
  293. package/src/plugins/ibkr/index.test.tsx +3 -11
  294. package/src/plugins/ibkr/index.tsx +1 -0
  295. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  296. package/src/plugins/ibkr/price-normalization.ts +1 -0
  297. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  298. package/src/plugins/ibkr/trade-tab.test.tsx +55 -3
  299. package/src/plugins/ibkr/trade-tab.tsx +166 -117
  300. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  301. package/src/plugins/pane-manager.test.ts +3 -1
  302. package/src/plugins/pane-manager.ts +93 -19
  303. package/src/plugins/plugin-runtime.test.tsx +176 -74
  304. package/src/plugins/plugin-runtime.tsx +228 -10
  305. package/src/plugins/prediction-markets/cache.ts +49 -0
  306. package/src/plugins/prediction-markets/categories.ts +0 -10
  307. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  308. package/src/plugins/prediction-markets/chart.tsx +21 -20
  309. package/src/plugins/prediction-markets/cli.ts +1 -0
  310. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  311. package/src/plugins/prediction-markets/controller.ts +37 -108
  312. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  313. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  314. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  315. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  316. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  317. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  318. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  319. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  320. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  321. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  322. package/src/plugins/prediction-markets/launch.ts +1 -1
  323. package/src/plugins/prediction-markets/metrics.ts +25 -12
  324. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  325. package/src/plugins/prediction-markets/pane.tsx +180 -82
  326. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  327. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  328. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  329. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  330. package/src/plugins/prediction-markets/settings.ts +0 -5
  331. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  332. package/src/plugins/registry.test.ts +307 -0
  333. package/src/plugins/registry.ts +293 -82
  334. package/src/react/input.ts +61 -0
  335. package/src/react/plugins/plugin-slot.tsx +21 -0
  336. package/src/react/use-raf-callback.ts +44 -0
  337. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  338. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  339. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  340. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  341. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  342. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  343. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  344. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  345. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  346. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  347. package/src/renderers/electrobun/bun/index.ts +1149 -0
  348. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  349. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  350. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  351. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  352. package/src/renderers/electrobun/view/app-services.ts +348 -0
  353. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  354. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  355. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  356. package/src/renderers/electrobun/view/config-host.ts +35 -0
  357. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  358. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  359. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  360. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  361. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  362. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  363. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  364. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  365. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  366. package/src/renderers/electrobun/view/input-host.tsx +112 -0
  367. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  368. package/src/renderers/electrobun/view/key-event.ts +134 -0
  369. package/src/renderers/electrobun/view/main.tsx +104 -0
  370. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  372. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  373. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  374. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  375. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  376. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  377. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  378. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  379. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  380. package/src/renderers/electrobun/view/ui-host.tsx +1509 -0
  381. package/src/renderers/electrobun/view/update-host.ts +38 -0
  382. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  383. package/src/renderers/opentui/dialog-host.tsx +56 -0
  384. package/src/renderers/opentui/host.tsx +140 -0
  385. package/src/renderers/opentui/image-loader.test.ts +25 -0
  386. package/src/renderers/opentui/image-loader.ts +105 -0
  387. package/src/renderers/opentui/image-surface.tsx +266 -0
  388. package/src/renderers/opentui/input-host.tsx +63 -0
  389. package/src/renderers/opentui/start.tsx +71 -0
  390. package/src/renderers/opentui/test-utils.tsx +76 -0
  391. package/src/renderers/opentui/toast-host.tsx +10 -0
  392. package/src/renderers/opentui/ui-host.tsx +98 -0
  393. package/src/sources/gloomberb-cloud.test.ts +288 -17
  394. package/src/sources/gloomberb-cloud.ts +176 -9
  395. package/src/sources/provider-errors.ts +1 -0
  396. package/src/sources/provider-router.test.ts +220 -81
  397. package/src/sources/provider-router.ts +294 -111
  398. package/src/sources/sec-edgar.ts +19 -6
  399. package/src/sources/yahoo-finance.test.ts +0 -5
  400. package/src/sources/yahoo-finance.ts +420 -11
  401. package/src/state/app-activity.ts +7 -7
  402. package/src/state/app-bootstrap.ts +114 -40
  403. package/src/state/app-context-selector.test.tsx +70 -1
  404. package/src/state/app-context.test.ts +71 -3
  405. package/src/state/app-context.test.tsx +14 -7
  406. package/src/state/app-context.tsx +209 -869
  407. package/src/state/config-save-scheduler.ts +35 -0
  408. package/src/state/pane-scroll-registry.tsx +254 -0
  409. package/src/state/persist-scheduler.test.ts +67 -0
  410. package/src/state/persist-scheduler.ts +69 -0
  411. package/src/state/selectors-ui.ts +55 -0
  412. package/src/state/selectors.ts +15 -71
  413. package/src/state/session-persistence.test.ts +1 -1
  414. package/src/state/ticker-refresh-queue.ts +52 -3
  415. package/src/state/use-inline-tickers.ts +18 -16
  416. package/src/state/use-quote-streaming.test.ts +0 -6
  417. package/src/state/use-quote-streaming.test.tsx +1 -1
  418. package/src/state/use-quote-streaming.ts +3 -2
  419. package/src/state/use-session-snapshot.ts +32 -17
  420. package/src/test-support/plugin-runtime.ts +149 -0
  421. package/src/theme/color-utils.ts +1 -0
  422. package/src/theme/colors.ts +16 -0
  423. package/src/types/broker.ts +16 -1
  424. package/src/types/capability-route-source.ts +23 -0
  425. package/src/types/config.test.ts +23 -0
  426. package/src/types/config.ts +145 -10
  427. package/src/types/context-menu.ts +95 -0
  428. package/src/types/data-provider.ts +34 -3
  429. package/src/types/desktop-menu.ts +13 -0
  430. package/src/types/desktop-window.ts +29 -0
  431. package/src/types/financials.ts +123 -2
  432. package/src/types/instrument.ts +1 -0
  433. package/src/types/news-source.ts +9 -0
  434. package/src/types/persistence.ts +1 -0
  435. package/src/types/plugin.ts +32 -8
  436. package/src/ui/ascii-font.ts +85 -0
  437. package/src/ui/context-menu.test.ts +94 -0
  438. package/src/ui/context-menu.tsx +433 -0
  439. package/src/ui/dialog.tsx +55 -0
  440. package/src/ui/host.tsx +364 -0
  441. package/src/ui/index.tsx +134 -0
  442. package/src/ui/toast.tsx +40 -0
  443. package/src/updater.test.ts +69 -0
  444. package/src/updater.ts +61 -18
  445. package/src/utils/api-client.test.ts +99 -9
  446. package/src/utils/api-client.ts +378 -18
  447. package/src/utils/back-navigation.ts +6 -0
  448. package/src/utils/cli-output.ts +1 -0
  449. package/src/utils/currency-units.test.ts +1 -6
  450. package/src/utils/currency-units.ts +1 -0
  451. package/src/utils/debug-log.ts +68 -0
  452. package/src/utils/dom-screenshot.ts +326 -0
  453. package/src/utils/exchange-rate-map.ts +1 -0
  454. package/src/utils/exchanges.ts +214 -0
  455. package/src/utils/format.test.ts +7 -0
  456. package/src/utils/format.ts +1 -0
  457. package/src/utils/fuzzy-search.ts +1 -0
  458. package/src/utils/html-entities.test.ts +8 -0
  459. package/src/utils/html-entities.ts +32 -0
  460. package/src/utils/http-transport.ts +11 -0
  461. package/src/utils/main-thread-monitor.ts +127 -0
  462. package/src/utils/perf-marks.ts +54 -0
  463. package/src/utils/quote-freshness.ts +27 -50
  464. package/src/utils/quote-resolution.test.ts +5 -5
  465. package/src/utils/quote-resolution.ts +0 -1
  466. package/src/utils/quote-time.ts +1 -5
  467. package/src/utils/sec.test.ts +0 -12
  468. package/src/utils/selection-clipboard.ts +14 -6
  469. package/src/utils/shortcut-labels.test.ts +20 -0
  470. package/src/utils/shortcut-labels.ts +67 -0
  471. package/src/utils/slugify.ts +1 -0
  472. package/src/utils/terminal-input-reset.ts +1 -0
  473. package/src/utils/text-wrap.ts +82 -0
  474. package/src/utils/throttled-fetch.test.ts +19 -0
  475. package/src/utils/throttled-fetch.ts +11 -9
  476. package/src/utils/ticker-list.test.ts +1 -7
  477. package/src/utils/ticker-list.ts +1 -0
  478. package/src/utils/timestamp.ts +29 -0
  479. package/src/utils/tweet-text.test.ts +13 -0
  480. package/src/utils/tweet-text.ts +18 -0
  481. package/src/utils/url.ts +11 -0
  482. package/src/version.ts +1 -1
  483. package/src/components/chart/chart-control-hint.tsx +0 -39
  484. package/src/components/detail-feed-view.test.tsx +0 -180
  485. package/src/components/detail-feed-view.tsx +0 -531
  486. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  487. package/src/components/layout/pixel-mouse.test.ts +0 -40
  488. package/src/components/spinner.tsx +0 -2
  489. package/src/components/tab-bar.tsx +0 -17
  490. package/src/components/ui/multi-select-chips.tsx +0 -69
  491. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  492. package/src/plugins/builtin/ask-ai.tsx +0 -9
  493. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  494. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  495. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  496. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  497. package/src/plugins/builtin/fred-settings.ts +0 -10
  498. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  499. package/src/plugins/builtin/options-availability.ts +0 -158
  500. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  501. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  502. package/src/plugins/prediction-markets/status.ts +0 -24
  503. package/src/plugins/prediction-markets/table.tsx +0 -87
  504. package/src/utils/quote-time.test.ts +0 -22
  505. package/src/utils/selection-clipboard.test.ts +0 -43
  506. 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
+ }