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
@@ -0,0 +1,886 @@
1
+ import { isBrokerPortfolioId } from "../../utils/broker-instances";
2
+ import { resolveTickerFinancialsQuoteState } from "../../utils/quote-resolution";
3
+ import {
4
+ cloneLayout,
5
+ DEFAULT_LAYOUT,
6
+ findPaneInstance,
7
+ isFixedTickerPane,
8
+ normalizePaneLayout,
9
+ removePaneInstances,
10
+ type LayoutConfig,
11
+ } from "../../types/config";
12
+ import type { AppConfig, PaneBinding, PaneInstanceConfig, SavedLayout } from "../../types/config";
13
+ import type { Quote, TickerFinancials } from "../../types/financials";
14
+ import type { TickerRecord } from "../../types/ticker";
15
+ import type { BrokerAccount } from "../../types/trading";
16
+ import type { DesktopSharedStateSnapshot } from "../../types/desktop-window";
17
+ import type { ReleaseInfo, UpdateProgress } from "../../updater";
18
+ import { getDockLeafLayouts, getDockedPaneIds } from "../../plugins/pane-manager";
19
+ import type { AppSessionSnapshot } from "./session-persistence";
20
+
21
+ export interface PaneRuntimeState {
22
+ cursorSymbol?: string | null;
23
+ collectionId?: string;
24
+ activeTabId?: string;
25
+ collectionSorts?: Record<string, CollectionSortPreference>;
26
+ pluginState?: Record<string, Record<string, unknown>>;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ export interface LayoutHistoryEntry {
31
+ past: LayoutConfig[];
32
+ future: LayoutConfig[];
33
+ }
34
+
35
+ export type SortDirection = "asc" | "desc";
36
+
37
+ export interface CollectionSortPreference {
38
+ columnId: string | null;
39
+ direction: SortDirection;
40
+ }
41
+
42
+ export interface CommandBarLaunchRequest {
43
+ kind: "plugin-command";
44
+ commandId: string;
45
+ sequence: number;
46
+ }
47
+
48
+ export interface AppState {
49
+ config: AppConfig;
50
+ tickers: Map<string, TickerRecord>;
51
+ financials: Map<string, TickerFinancials>;
52
+ exchangeRates: Map<string, number>;
53
+ brokerAccounts: Record<string, BrokerAccount[]>;
54
+ activePanel: "left" | "right";
55
+ focusedPaneId: string | null;
56
+ paneState: Record<string, PaneRuntimeState>;
57
+ recentTickers: string[];
58
+ commandBarOpen: boolean;
59
+ commandBarQuery: string;
60
+ commandBarLaunchRequest: CommandBarLaunchRequest | null;
61
+ themePreview: string | null;
62
+ refreshing: Set<string>;
63
+ initialized: boolean;
64
+ statusBarVisible: boolean;
65
+ gridlockTipVisible: boolean;
66
+ gridlockTipSequence: number;
67
+ inputCaptured: boolean;
68
+ updateAvailable: ReleaseInfo | null;
69
+ updateProgress: UpdateProgress | null;
70
+ updateCheckInProgress: boolean;
71
+ updateNotice: string | null;
72
+ layoutHistory: Record<number, LayoutHistoryEntry>;
73
+ }
74
+
75
+ export type AppAction =
76
+ | { type: "SET_CONFIG"; config: AppConfig }
77
+ | { type: "SET_TICKERS"; tickers: Map<string, TickerRecord> }
78
+ | { type: "UPDATE_TICKER"; ticker: TickerRecord }
79
+ | { type: "REMOVE_TICKER"; symbol: string }
80
+ | { type: "SET_FINANCIALS"; symbol: string; data: TickerFinancials }
81
+ | { type: "MERGE_QUOTE"; symbol: string; quote: Quote }
82
+ | { type: "HYDRATE_FINANCIALS"; financials: Map<string, TickerFinancials> }
83
+ | { type: "TRACK_TICKER"; symbol: string | null }
84
+ | { type: "SET_ACTIVE_PANEL"; panel: "left" | "right" }
85
+ | { type: "TOGGLE_COMMAND_BAR" }
86
+ | {
87
+ type: "SET_COMMAND_BAR";
88
+ open: boolean;
89
+ query?: string;
90
+ launch?: { kind: "plugin-command"; commandId: string } | null;
91
+ }
92
+ | { type: "SET_COMMAND_BAR_QUERY"; query: string }
93
+ | { type: "SET_REFRESHING"; symbol: string; refreshing: boolean }
94
+ | { type: "SET_BROKER_ACCOUNTS"; instanceId: string; accounts: BrokerAccount[] }
95
+ | { type: "SET_INITIALIZED" }
96
+ | { type: "TOGGLE_STATUS_BAR" }
97
+ | { type: "SHOW_GRIDLOCK_TIP" }
98
+ | { type: "DISMISS_GRIDLOCK_TIP" }
99
+ | { type: "SET_THEME"; theme: string }
100
+ | { type: "PREVIEW_THEME"; theme: string | null }
101
+ | { type: "SET_UPDATE_AVAILABLE"; release: ReleaseInfo | null }
102
+ | { type: "SET_UPDATE_PROGRESS"; progress: UpdateProgress | null }
103
+ | { type: "SET_UPDATE_CHECK_IN_PROGRESS"; checking: boolean }
104
+ | { type: "SET_UPDATE_NOTICE"; notice: string | null }
105
+ | { type: "TOGGLE_PLUGIN"; pluginId: string }
106
+ | { type: "SET_INPUT_CAPTURED"; captured: boolean }
107
+ | { type: "SET_EXCHANGE_RATE"; currency: string; rate: number }
108
+ | { type: "HYDRATE_EXCHANGE_RATES"; exchangeRates: Map<string, number> }
109
+ | { type: "PUSH_LAYOUT_HISTORY" }
110
+ | { type: "UNDO_LAYOUT" }
111
+ | { type: "REDO_LAYOUT" }
112
+ | { type: "UPDATE_LAYOUT"; layout: LayoutConfig }
113
+ | { type: "SWITCH_LAYOUT"; index: number }
114
+ | { type: "NEW_LAYOUT"; name: string }
115
+ | { type: "DELETE_LAYOUT"; index: number }
116
+ | { type: "RENAME_LAYOUT"; index: number; name: string }
117
+ | { type: "DUPLICATE_LAYOUT"; index: number }
118
+ | { type: "FOCUS_PANE"; paneId: string }
119
+ | { type: "FOCUS_NEXT"; paneOrder: string[] }
120
+ | { type: "FOCUS_PREV"; paneOrder: string[] }
121
+ | { type: "HYDRATE_DESKTOP_SNAPSHOT"; snapshot: DesktopSharedStateSnapshot }
122
+ | {
123
+ type: "UPDATE_PLUGIN_PANE_STATE";
124
+ paneId: string;
125
+ pluginId: string;
126
+ key: string;
127
+ value: unknown;
128
+ }
129
+ | { type: "UPDATE_PANE_STATE"; paneId: string; patch: Partial<PaneRuntimeState> };
130
+
131
+ function getDefaultCollectionId(config: AppConfig): string {
132
+ return config.portfolios[0]?.id || config.watchlists[0]?.id || "";
133
+ }
134
+
135
+ function isKnownCollection(config: AppConfig, collectionId: string | undefined): collectionId is string {
136
+ if (!collectionId) return false;
137
+ return config.portfolios.some((portfolio) => portfolio.id === collectionId)
138
+ || config.watchlists.some((watchlist) => watchlist.id === collectionId);
139
+ }
140
+
141
+ function shouldPreserveUnknownCollectionId(collectionId: string | undefined): boolean {
142
+ return isBrokerPortfolioId(collectionId);
143
+ }
144
+
145
+ function getConfiguredCollectionId(config: AppConfig, instance: PaneInstanceConfig): string {
146
+ const candidates = [
147
+ instance.params?.collectionId,
148
+ ];
149
+
150
+ for (const candidate of candidates) {
151
+ if (isKnownCollection(config, candidate) || shouldPreserveUnknownCollectionId(candidate)) {
152
+ return candidate!;
153
+ }
154
+ }
155
+
156
+ return getDefaultCollectionId(config);
157
+ }
158
+
159
+ function defaultPaneStateForInstance(config: AppConfig, instance: PaneInstanceConfig): PaneRuntimeState {
160
+ if (instance.paneId === "portfolio-list") {
161
+ return {
162
+ collectionId: getConfiguredCollectionId(config, instance),
163
+ cursorSymbol: null,
164
+ };
165
+ }
166
+ if (instance.paneId === "ticker-detail") {
167
+ return { activeTabId: "overview" };
168
+ }
169
+ return {};
170
+ }
171
+
172
+ function reconcilePaneState(config: AppConfig, previous: Record<string, PaneRuntimeState>): Record<string, PaneRuntimeState> {
173
+ const next: Record<string, PaneRuntimeState> = {};
174
+ for (const instance of config.layout.instances) {
175
+ const defaults = defaultPaneStateForInstance(config, instance);
176
+ const paneState = { ...defaults, ...(previous[instance.instanceId] ?? {}) };
177
+ if (
178
+ instance.paneId === "portfolio-list"
179
+ && !isKnownCollection(config, paneState.collectionId as string | undefined)
180
+ && !shouldPreserveUnknownCollectionId(paneState.collectionId as string | undefined)
181
+ ) {
182
+ paneState.collectionId = defaults.collectionId;
183
+ }
184
+ next[instance.instanceId] = paneState;
185
+ }
186
+ return next;
187
+ }
188
+
189
+ function reconcileBrokerAccounts(
190
+ config: AppConfig,
191
+ brokerAccounts: Record<string, BrokerAccount[]>,
192
+ ): Record<string, BrokerAccount[]> {
193
+ const validInstanceIds = new Set(config.brokerInstances.map((instance) => instance.id));
194
+ return Object.fromEntries(
195
+ Object.entries(brokerAccounts).filter(([instanceId]) => validInstanceIds.has(instanceId)),
196
+ );
197
+ }
198
+
199
+ function resolveTickerFromBinding(
200
+ state: Pick<AppState, "config" | "paneState">,
201
+ binding: PaneBinding | undefined,
202
+ seen: Set<string>,
203
+ ): string | null {
204
+ if (!binding || binding.kind === "none") return null;
205
+ if (binding.kind === "fixed") return binding.symbol;
206
+ return resolveTickerForPane(state as AppState, binding.sourceInstanceId, seen);
207
+ }
208
+
209
+ export function getPaneState(state: Pick<AppState, "paneState">, paneId: string): PaneRuntimeState {
210
+ return state.paneState[paneId] ?? {};
211
+ }
212
+
213
+ export function resolveTickerForPane(state: AppState, paneId: string, seen = new Set<string>()): string | null {
214
+ if (seen.has(paneId)) return null;
215
+ seen.add(paneId);
216
+ const instance = findPaneInstance(state.config.layout, paneId);
217
+ if (!instance) return null;
218
+ if (instance.paneId === "portfolio-list") {
219
+ const paneState = getPaneState(state, paneId);
220
+ return typeof paneState.cursorSymbol === "string" ? paneState.cursorSymbol : null;
221
+ }
222
+ return resolveTickerFromBinding(state, instance.binding, seen);
223
+ }
224
+
225
+ export function resolveCollectionForPane(state: AppState, paneId: string, seen = new Set<string>()): string | null {
226
+ if (seen.has(paneId)) return null;
227
+ seen.add(paneId);
228
+ const instance = findPaneInstance(state.config.layout, paneId);
229
+ if (!instance) return null;
230
+ if (instance.paneId === "portfolio-list") {
231
+ const paneState = getPaneState(state, paneId);
232
+ const collectionId = typeof paneState.collectionId === "string"
233
+ ? paneState.collectionId
234
+ : getConfiguredCollectionId(state.config, instance);
235
+ if (isKnownCollection(state.config, collectionId) || shouldPreserveUnknownCollectionId(collectionId)) {
236
+ return collectionId ?? null;
237
+ }
238
+ return getConfiguredCollectionId(state.config, instance);
239
+ }
240
+ if (instance.binding?.kind === "follow") {
241
+ return resolveCollectionForPane(state, instance.binding.sourceInstanceId, seen);
242
+ }
243
+ return null;
244
+ }
245
+
246
+ export function resolveTickerFileForPane(state: AppState, paneId: string): TickerRecord | null {
247
+ const symbol = resolveTickerForPane(state, paneId);
248
+ return symbol ? state.tickers.get(symbol) ?? null : null;
249
+ }
250
+
251
+ export function resolveFinancialsForPane(state: AppState, paneId: string): TickerFinancials | null {
252
+ const symbol = resolveTickerForPane(state, paneId);
253
+ return symbol ? state.financials.get(symbol) ?? null : null;
254
+ }
255
+
256
+ export function getFocusedTickerSymbol(state: AppState): string | null {
257
+ return state.focusedPaneId ? resolveTickerForPane(state, state.focusedPaneId) : null;
258
+ }
259
+
260
+ export function getFocusedCollectionId(state: AppState): string | null {
261
+ return state.focusedPaneId ? resolveCollectionForPane(state, state.focusedPaneId) : null;
262
+ }
263
+
264
+ export function getEffectiveThemeId(state: Pick<AppState, "config" | "themePreview">): string {
265
+ return state.themePreview ?? state.config.theme;
266
+ }
267
+
268
+ function clearTickerBindings(layout: LayoutConfig, symbol: string): LayoutConfig {
269
+ return normalizePaneLayout(removePaneInstances(
270
+ layout,
271
+ layout.instances
272
+ .filter((instance) => instance.binding?.kind === "fixed" && isFixedTickerPane(instance) && instance.binding.symbol === symbol)
273
+ .map((instance) => instance.instanceId),
274
+ ));
275
+ }
276
+
277
+ function nextRecentTickers(current: string[], symbol: string | null): string[] {
278
+ if (!symbol) return current;
279
+ if (current[0] === symbol && !current.slice(1).includes(symbol)) {
280
+ return current;
281
+ }
282
+ const next = [symbol, ...current.filter((entry) => entry !== symbol)].slice(0, 50);
283
+ if (next.length === current.length && next.every((entry, index) => entry === current[index])) {
284
+ return current;
285
+ }
286
+ return next;
287
+ }
288
+
289
+ function syncLayouts(layouts: SavedLayout[], activeLayoutIndex: number, layout: LayoutConfig): SavedLayout[] {
290
+ return layouts.map((savedLayout, index) => (
291
+ index === activeLayoutIndex ? { ...savedLayout, layout: cloneLayout(layout) } : savedLayout
292
+ ));
293
+ }
294
+
295
+ const PANEL_RESOLUTION_BOUNDS = { x: 0, y: 0, width: 120, height: 40 };
296
+
297
+ function getPaneOrder(layout: LayoutConfig): string[] {
298
+ return [
299
+ ...getDockedPaneIds(layout),
300
+ ...layout.floating.map((entry) => entry.instanceId),
301
+ ];
302
+ }
303
+
304
+ function getTopFloatingPaneId(layout: LayoutConfig): string | null {
305
+ let topInstanceId: string | null = null;
306
+ let topZIndex = Number.NEGATIVE_INFINITY;
307
+ let topOrder = -1;
308
+
309
+ layout.floating.forEach((entry, index) => {
310
+ const zIndex = entry.zIndex ?? 50;
311
+ if (zIndex > topZIndex || (zIndex === topZIndex && index > topOrder)) {
312
+ topInstanceId = entry.instanceId;
313
+ topZIndex = zIndex;
314
+ topOrder = index;
315
+ }
316
+ });
317
+
318
+ return topInstanceId;
319
+ }
320
+
321
+ function resolveFocusedPaneId(
322
+ previousLayout: LayoutConfig,
323
+ nextLayout: LayoutConfig,
324
+ focusedPaneId: string | null,
325
+ ): string | null {
326
+ const paneOrder = getPaneOrder(nextLayout);
327
+ if (paneOrder.length === 0) return null;
328
+ if (focusedPaneId && paneOrder.includes(focusedPaneId)) {
329
+ return focusedPaneId;
330
+ }
331
+ const previouslyFloating = focusedPaneId
332
+ ? previousLayout.floating.some((entry) => entry.instanceId === focusedPaneId)
333
+ : false;
334
+ if (previouslyFloating) {
335
+ return getTopFloatingPaneId(nextLayout) ?? paneOrder[0] ?? null;
336
+ }
337
+ return paneOrder[0] ?? null;
338
+ }
339
+
340
+ function getPanelFocusTarget(layout: LayoutConfig, panel: "left" | "right"): string | null {
341
+ const leaves = getDockLeafLayouts(layout, PANEL_RESOLUTION_BOUNDS);
342
+ if (leaves.length === 0) return layout.floating[0]?.instanceId ?? null;
343
+ const sorted = [...leaves].sort((a, b) => (
344
+ panel === "left"
345
+ ? a.rect.x - b.rect.x || a.rect.y - b.rect.y
346
+ : (b.rect.x + b.rect.width) - (a.rect.x + a.rect.width) || a.rect.y - b.rect.y
347
+ ));
348
+ return sorted[0]?.instanceId ?? null;
349
+ }
350
+
351
+ const MAX_LAYOUT_HISTORY = 50;
352
+
353
+ function cloneHistoryEntry(entry: LayoutHistoryEntry | undefined): LayoutHistoryEntry {
354
+ return {
355
+ past: entry?.past.map((layout) => cloneLayout(layout)) ?? [],
356
+ future: entry?.future.map((layout) => cloneLayout(layout)) ?? [],
357
+ };
358
+ }
359
+
360
+ function historyForIndex(layoutHistory: Record<number, LayoutHistoryEntry>, index: number): LayoutHistoryEntry {
361
+ return cloneHistoryEntry(layoutHistory[index]);
362
+ }
363
+
364
+ function setHistoryForIndex(
365
+ layoutHistory: Record<number, LayoutHistoryEntry>,
366
+ index: number,
367
+ entry: LayoutHistoryEntry,
368
+ ): Record<number, LayoutHistoryEntry> {
369
+ return {
370
+ ...layoutHistory,
371
+ [index]: {
372
+ past: entry.past.map((layout) => cloneLayout(layout)),
373
+ future: entry.future.map((layout) => cloneLayout(layout)),
374
+ },
375
+ };
376
+ }
377
+
378
+ function removeHistoryIndex(layoutHistory: Record<number, LayoutHistoryEntry>, removedIndex: number): Record<number, LayoutHistoryEntry> {
379
+ const next: Record<number, LayoutHistoryEntry> = {};
380
+ for (const [rawIndex, entry] of Object.entries(layoutHistory)) {
381
+ const index = Number.parseInt(rawIndex, 10);
382
+ if (Number.isNaN(index) || index === removedIndex) continue;
383
+ next[index > removedIndex ? index - 1 : index] = cloneHistoryEntry(entry);
384
+ }
385
+ return next;
386
+ }
387
+
388
+ /** If paneId is a floating pane, bump its zIndex to the top. */
389
+ function bringFloatingToFront(layout: LayoutConfig, paneId: string): LayoutConfig {
390
+ const entry = layout.floating.find((e) => e.instanceId === paneId);
391
+ if (!entry) return layout;
392
+ const maxZ = layout.floating.reduce((max, e) => Math.max(max, e.zIndex ?? 50), 0);
393
+ if ((entry.zIndex ?? 50) >= maxZ) return layout; // already on top
394
+ return {
395
+ ...layout,
396
+ floating: layout.floating.map((e) =>
397
+ e.instanceId === paneId ? { ...e, zIndex: maxZ + 1 } : e,
398
+ ),
399
+ };
400
+ }
401
+
402
+ function focusPaneState(state: AppState, paneId: string): AppState {
403
+ const layout = bringFloatingToFront(state.config.layout, paneId);
404
+ const config = layout !== state.config.layout
405
+ ? { ...state.config, layout, layouts: syncLayouts(state.config.layouts, state.config.activeLayoutIndex, layout) }
406
+ : state.config;
407
+ const recentTickers = nextRecentTickers(
408
+ state.recentTickers,
409
+ resolveTickerForPane(state, paneId),
410
+ );
411
+ if (
412
+ config === state.config &&
413
+ state.focusedPaneId === paneId &&
414
+ recentTickers === state.recentTickers
415
+ ) {
416
+ return state;
417
+ }
418
+ return {
419
+ ...state,
420
+ config,
421
+ focusedPaneId: paneId,
422
+ recentTickers,
423
+ };
424
+ }
425
+
426
+ function bringFocusedFloatingPaneToFront(config: AppConfig, focusedPaneId: string | null): AppConfig {
427
+ if (!focusedPaneId) return config;
428
+ const layout = bringFloatingToFront(config.layout, focusedPaneId);
429
+ if (layout === config.layout) return config;
430
+ return {
431
+ ...config,
432
+ layout,
433
+ layouts: syncLayouts(config.layouts, config.activeLayoutIndex, layout),
434
+ };
435
+ }
436
+
437
+ function withFocusedPane(state: AppState, config: AppConfig): AppState {
438
+ const normalizedLayout = normalizePaneLayout(config.layout);
439
+ const nextConfig = normalizedLayout === config.layout
440
+ ? config
441
+ : {
442
+ ...config,
443
+ layout: normalizedLayout,
444
+ layouts: syncLayouts(config.layouts, config.activeLayoutIndex, normalizedLayout),
445
+ };
446
+ const nextPaneState = reconcilePaneState(nextConfig, state.paneState);
447
+ const focusedPaneId = resolveFocusedPaneId(state.config.layout, nextConfig.layout, state.focusedPaneId);
448
+ const focusedConfig = bringFocusedFloatingPaneToFront(nextConfig, focusedPaneId);
449
+ return {
450
+ ...state,
451
+ config: focusedConfig,
452
+ paneState: nextPaneState,
453
+ brokerAccounts: reconcileBrokerAccounts(focusedConfig, state.brokerAccounts),
454
+ focusedPaneId,
455
+ };
456
+ }
457
+
458
+ function hydrateDesktopSnapshot(state: AppState, snapshot: DesktopSharedStateSnapshot): AppState {
459
+ const baseState = withFocusedPane({
460
+ ...state,
461
+ paneState: snapshot.paneState,
462
+ focusedPaneId: snapshot.focusedPaneId,
463
+ activePanel: snapshot.activePanel,
464
+ statusBarVisible: snapshot.statusBarVisible,
465
+ }, snapshot.config);
466
+ return {
467
+ ...baseState,
468
+ activePanel: snapshot.activePanel,
469
+ statusBarVisible: snapshot.statusBarVisible,
470
+ };
471
+ }
472
+
473
+ export function appReducer(state: AppState, action: AppAction): AppState {
474
+ switch (action.type) {
475
+ case "SET_CONFIG":
476
+ return withFocusedPane({ ...state, themePreview: null, layoutHistory: {} }, action.config);
477
+
478
+ case "SET_TICKERS":
479
+ return { ...state, tickers: action.tickers };
480
+
481
+ case "UPDATE_TICKER": {
482
+ const tickers = new Map(state.tickers);
483
+ tickers.set(action.ticker.metadata.ticker, action.ticker);
484
+ return { ...state, tickers };
485
+ }
486
+
487
+ case "REMOVE_TICKER": {
488
+ const tickers = new Map(state.tickers);
489
+ tickers.delete(action.symbol);
490
+ const financials = new Map(state.financials);
491
+ financials.delete(action.symbol);
492
+ const paneState: Record<string, PaneRuntimeState> = {};
493
+ for (const [paneId, currentState] of Object.entries(state.paneState)) {
494
+ paneState[paneId] = currentState.cursorSymbol === action.symbol
495
+ ? { ...currentState, cursorSymbol: null }
496
+ : currentState;
497
+ }
498
+ const layout = clearTickerBindings(state.config.layout, action.symbol);
499
+ const layouts = syncLayouts(state.config.layouts, state.config.activeLayoutIndex, layout);
500
+ return {
501
+ ...state,
502
+ tickers,
503
+ financials,
504
+ paneState,
505
+ recentTickers: state.recentTickers.filter((symbol) => symbol !== action.symbol),
506
+ config: { ...state.config, layout, layouts },
507
+ };
508
+ }
509
+
510
+ case "SET_FINANCIALS": {
511
+ const financials = new Map(state.financials);
512
+ financials.set(action.symbol, resolveTickerFinancialsQuoteState(action.data)!);
513
+ return { ...state, financials };
514
+ }
515
+
516
+ case "MERGE_QUOTE": {
517
+ const financials = new Map(state.financials);
518
+ const current = state.financials.get(action.symbol);
519
+ financials.set(action.symbol, resolveTickerFinancialsQuoteState(current, action.quote)!);
520
+ return { ...state, financials };
521
+ }
522
+
523
+ case "HYDRATE_FINANCIALS": {
524
+ if (action.financials.size === 0) return state;
525
+ const financials = new Map(state.financials);
526
+ for (const [symbol, data] of action.financials) {
527
+ financials.set(symbol, resolveTickerFinancialsQuoteState(data)!);
528
+ }
529
+ return { ...state, financials };
530
+ }
531
+
532
+ case "TRACK_TICKER":
533
+ return { ...state, recentTickers: nextRecentTickers(state.recentTickers, action.symbol) };
534
+
535
+ case "SET_ACTIVE_PANEL": {
536
+ const firstPane = getPanelFocusTarget(state.config.layout, action.panel);
537
+ return {
538
+ ...state,
539
+ activePanel: action.panel,
540
+ focusedPaneId: firstPane ?? state.focusedPaneId,
541
+ };
542
+ }
543
+
544
+ case "TOGGLE_COMMAND_BAR":
545
+ return state.commandBarOpen
546
+ ? { ...state, commandBarOpen: false, commandBarQuery: "", commandBarLaunchRequest: null, themePreview: null }
547
+ : { ...state, commandBarOpen: true, commandBarQuery: "", commandBarLaunchRequest: null };
548
+
549
+ case "SET_COMMAND_BAR": {
550
+ const launchRequest = action.open && action.launch
551
+ ? {
552
+ ...action.launch,
553
+ sequence: (state.commandBarLaunchRequest?.sequence ?? 0) + 1,
554
+ }
555
+ : null;
556
+ return {
557
+ ...state,
558
+ commandBarOpen: action.open,
559
+ commandBarQuery: action.open ? (action.query ?? "") : "",
560
+ commandBarLaunchRequest: launchRequest,
561
+ themePreview: action.open ? state.themePreview : null,
562
+ };
563
+ }
564
+
565
+ case "SET_COMMAND_BAR_QUERY":
566
+ return { ...state, commandBarQuery: action.query };
567
+
568
+ case "SET_REFRESHING": {
569
+ const refreshing = new Set(state.refreshing);
570
+ if (action.refreshing) refreshing.add(action.symbol);
571
+ else refreshing.delete(action.symbol);
572
+ return { ...state, refreshing };
573
+ }
574
+
575
+ case "SET_BROKER_ACCOUNTS":
576
+ return {
577
+ ...state,
578
+ brokerAccounts: {
579
+ ...state.brokerAccounts,
580
+ [action.instanceId]: action.accounts,
581
+ },
582
+ };
583
+
584
+ case "SET_INITIALIZED":
585
+ return { ...state, initialized: true };
586
+
587
+ case "TOGGLE_STATUS_BAR":
588
+ return { ...state, statusBarVisible: !state.statusBarVisible };
589
+
590
+ case "SHOW_GRIDLOCK_TIP":
591
+ return {
592
+ ...state,
593
+ gridlockTipVisible: true,
594
+ gridlockTipSequence: state.gridlockTipSequence + 1,
595
+ };
596
+
597
+ case "DISMISS_GRIDLOCK_TIP":
598
+ if (!state.gridlockTipVisible) return state;
599
+ return { ...state, gridlockTipVisible: false };
600
+
601
+ case "SET_THEME":
602
+ if (state.config.theme === action.theme && state.themePreview == null) return state;
603
+ return { ...state, themePreview: null, config: { ...state.config, theme: action.theme } };
604
+
605
+ case "PREVIEW_THEME":
606
+ if (state.themePreview === action.theme) return state;
607
+ return { ...state, themePreview: action.theme };
608
+
609
+ case "SET_UPDATE_AVAILABLE":
610
+ return { ...state, updateAvailable: action.release, updateNotice: action.release ? null : state.updateNotice };
611
+
612
+ case "SET_UPDATE_PROGRESS":
613
+ return { ...state, updateProgress: action.progress, updateNotice: action.progress ? null : state.updateNotice };
614
+
615
+ case "SET_UPDATE_CHECK_IN_PROGRESS":
616
+ return { ...state, updateCheckInProgress: action.checking };
617
+
618
+ case "SET_UPDATE_NOTICE":
619
+ return { ...state, updateNotice: action.notice };
620
+
621
+ case "TOGGLE_PLUGIN": {
622
+ const disabledPlugins = state.config.disabledPlugins.includes(action.pluginId)
623
+ ? state.config.disabledPlugins.filter((pluginId) => pluginId !== action.pluginId)
624
+ : [...state.config.disabledPlugins, action.pluginId];
625
+ return { ...state, config: { ...state.config, disabledPlugins } };
626
+ }
627
+
628
+ case "SET_INPUT_CAPTURED":
629
+ return { ...state, inputCaptured: action.captured };
630
+
631
+ case "SET_EXCHANGE_RATE": {
632
+ const exchangeRates = new Map(state.exchangeRates);
633
+ exchangeRates.set(action.currency, action.rate);
634
+ return { ...state, exchangeRates };
635
+ }
636
+
637
+ case "HYDRATE_EXCHANGE_RATES": {
638
+ if (action.exchangeRates.size === 0) return state;
639
+ const exchangeRates = new Map(state.exchangeRates);
640
+ for (const [currency, rate] of action.exchangeRates) {
641
+ exchangeRates.set(currency, rate);
642
+ }
643
+ return { ...state, exchangeRates };
644
+ }
645
+
646
+ case "PUSH_LAYOUT_HISTORY": {
647
+ const currentIndex = state.config.activeLayoutIndex;
648
+ const entry = historyForIndex(state.layoutHistory, currentIndex);
649
+ const snapshot = cloneLayout(state.config.layout);
650
+ const last = entry.past[entry.past.length - 1];
651
+ if (last && JSON.stringify(last) === JSON.stringify(snapshot)) {
652
+ return state;
653
+ }
654
+ entry.past = [...entry.past, snapshot].slice(-MAX_LAYOUT_HISTORY);
655
+ entry.future = [];
656
+ return {
657
+ ...state,
658
+ layoutHistory: setHistoryForIndex(state.layoutHistory, currentIndex, entry),
659
+ };
660
+ }
661
+
662
+ case "UNDO_LAYOUT": {
663
+ const currentIndex = state.config.activeLayoutIndex;
664
+ const entry = historyForIndex(state.layoutHistory, currentIndex);
665
+ if (entry.past.length === 0) return state;
666
+ const target = entry.past[entry.past.length - 1]!;
667
+ entry.past = entry.past.slice(0, -1);
668
+ entry.future = [cloneLayout(state.config.layout), ...entry.future].slice(0, MAX_LAYOUT_HISTORY);
669
+ const layouts = syncLayouts(state.config.layouts, currentIndex, target);
670
+ return withFocusedPane({
671
+ ...state,
672
+ layoutHistory: setHistoryForIndex(state.layoutHistory, currentIndex, entry),
673
+ }, {
674
+ ...state.config,
675
+ layout: cloneLayout(target),
676
+ layouts,
677
+ });
678
+ }
679
+
680
+ case "REDO_LAYOUT": {
681
+ const currentIndex = state.config.activeLayoutIndex;
682
+ const entry = historyForIndex(state.layoutHistory, currentIndex);
683
+ if (entry.future.length === 0) return state;
684
+ const target = entry.future[0]!;
685
+ entry.future = entry.future.slice(1);
686
+ entry.past = [...entry.past, cloneLayout(state.config.layout)].slice(-MAX_LAYOUT_HISTORY);
687
+ const layouts = syncLayouts(state.config.layouts, currentIndex, target);
688
+ return withFocusedPane({
689
+ ...state,
690
+ layoutHistory: setHistoryForIndex(state.layoutHistory, currentIndex, entry),
691
+ }, {
692
+ ...state.config,
693
+ layout: cloneLayout(target),
694
+ layouts,
695
+ });
696
+ }
697
+
698
+ case "UPDATE_LAYOUT": {
699
+ const layouts = syncLayouts(state.config.layouts, state.config.activeLayoutIndex, action.layout);
700
+ return withFocusedPane(state, { ...state.config, layout: action.layout, layouts });
701
+ }
702
+
703
+ case "SWITCH_LAYOUT": {
704
+ if (action.index < 0 || action.index >= state.config.layouts.length) return state;
705
+ const layouts = syncLayouts(state.config.layouts, state.config.activeLayoutIndex, state.config.layout);
706
+ const target = layouts[action.index]!;
707
+ return withFocusedPane(state, {
708
+ ...state.config,
709
+ layout: cloneLayout(target.layout),
710
+ layouts,
711
+ activeLayoutIndex: action.index,
712
+ });
713
+ }
714
+
715
+ case "NEW_LAYOUT": {
716
+ const newLayout: SavedLayout = {
717
+ name: action.name,
718
+ layout: cloneLayout(DEFAULT_LAYOUT),
719
+ };
720
+ const layouts = [...syncLayouts(state.config.layouts, state.config.activeLayoutIndex, state.config.layout), newLayout];
721
+ return withFocusedPane({
722
+ ...state,
723
+ layoutHistory: setHistoryForIndex(state.layoutHistory, layouts.length - 1, { past: [], future: [] }),
724
+ }, {
725
+ ...state.config,
726
+ layout: cloneLayout(newLayout.layout),
727
+ layouts,
728
+ activeLayoutIndex: layouts.length - 1,
729
+ });
730
+ }
731
+
732
+ case "DELETE_LAYOUT": {
733
+ if (state.config.layouts.length <= 1) return state;
734
+ const layouts = state.config.layouts.filter((_, index) => index !== action.index);
735
+ const nextActiveLayoutIndex = action.index <= state.config.activeLayoutIndex
736
+ ? Math.max(0, state.config.activeLayoutIndex - 1)
737
+ : state.config.activeLayoutIndex;
738
+ const nextLayout = layouts[nextActiveLayoutIndex]!;
739
+ return withFocusedPane({
740
+ ...state,
741
+ layoutHistory: removeHistoryIndex(state.layoutHistory, action.index),
742
+ }, {
743
+ ...state.config,
744
+ layout: cloneLayout(nextLayout.layout),
745
+ layouts,
746
+ activeLayoutIndex: nextActiveLayoutIndex,
747
+ });
748
+ }
749
+
750
+ case "RENAME_LAYOUT": {
751
+ if (action.index < 0 || action.index >= state.config.layouts.length) return state;
752
+ return {
753
+ ...state,
754
+ config: {
755
+ ...state.config,
756
+ layouts: state.config.layouts.map((savedLayout, index) => (
757
+ index === action.index ? { ...savedLayout, name: action.name } : savedLayout
758
+ )),
759
+ },
760
+ };
761
+ }
762
+
763
+ case "DUPLICATE_LAYOUT": {
764
+ if (action.index < 0 || action.index >= state.config.layouts.length) return state;
765
+ const source = state.config.layouts[action.index]!;
766
+ const duplicate: SavedLayout = {
767
+ name: `${source.name} Copy`,
768
+ layout: cloneLayout(source.layout),
769
+ };
770
+ const layouts = [...syncLayouts(state.config.layouts, state.config.activeLayoutIndex, state.config.layout), duplicate];
771
+ return withFocusedPane({
772
+ ...state,
773
+ layoutHistory: setHistoryForIndex(state.layoutHistory, layouts.length - 1, { past: [], future: [] }),
774
+ }, {
775
+ ...state.config,
776
+ layout: cloneLayout(duplicate.layout),
777
+ layouts,
778
+ activeLayoutIndex: layouts.length - 1,
779
+ });
780
+ }
781
+
782
+ case "FOCUS_PANE": {
783
+ return focusPaneState(state, action.paneId);
784
+ }
785
+
786
+ case "FOCUS_NEXT": {
787
+ if (action.paneOrder.length === 0) return state;
788
+ const currentIndex = state.focusedPaneId ? action.paneOrder.indexOf(state.focusedPaneId) : -1;
789
+ const nextPaneId = action.paneOrder[(currentIndex + 1) % action.paneOrder.length]!;
790
+ return focusPaneState(state, nextPaneId);
791
+ }
792
+
793
+ case "FOCUS_PREV": {
794
+ if (action.paneOrder.length === 0) return state;
795
+ const currentIndex = state.focusedPaneId ? action.paneOrder.indexOf(state.focusedPaneId) : 0;
796
+ const nextPaneId = action.paneOrder[(currentIndex - 1 + action.paneOrder.length) % action.paneOrder.length]!;
797
+ return focusPaneState(state, nextPaneId);
798
+ }
799
+
800
+ case "HYDRATE_DESKTOP_SNAPSHOT":
801
+ return hydrateDesktopSnapshot(state, action.snapshot);
802
+
803
+ case "UPDATE_PANE_STATE": {
804
+ const current = state.paneState[action.paneId] ?? {};
805
+ if (Object.keys(action.patch).every((key) => Object.is(current[key], action.patch[key]))) {
806
+ return state;
807
+ }
808
+ const nextState = { ...current, ...action.patch };
809
+ const recentTickers = Object.prototype.hasOwnProperty.call(action.patch, "cursorSymbol")
810
+ ? nextRecentTickers(state.recentTickers, typeof nextState.cursorSymbol === "string" ? nextState.cursorSymbol : null)
811
+ : state.recentTickers;
812
+ return {
813
+ ...state,
814
+ paneState: {
815
+ ...state.paneState,
816
+ [action.paneId]: nextState,
817
+ },
818
+ recentTickers,
819
+ };
820
+ }
821
+
822
+ case "UPDATE_PLUGIN_PANE_STATE": {
823
+ const current = state.paneState[action.paneId] ?? {};
824
+ const currentPluginState = current.pluginState ?? {};
825
+ const currentPluginValues = currentPluginState[action.pluginId] ?? {};
826
+ if (Object.is(currentPluginValues[action.key], action.value)) {
827
+ return state;
828
+ }
829
+ return {
830
+ ...state,
831
+ paneState: {
832
+ ...state.paneState,
833
+ [action.paneId]: {
834
+ ...current,
835
+ pluginState: {
836
+ ...currentPluginState,
837
+ [action.pluginId]: {
838
+ ...currentPluginValues,
839
+ [action.key]: action.value,
840
+ },
841
+ },
842
+ },
843
+ },
844
+ };
845
+ }
846
+
847
+ default:
848
+ return state;
849
+ }
850
+ }
851
+
852
+ export function createInitialState(config: AppConfig, sessionSnapshot: AppSessionSnapshot | null = null): AppState {
853
+ const paneState = reconcilePaneState(config, sessionSnapshot?.paneState ?? {});
854
+ const defaultFocusedPaneId = getPaneOrder(config.layout)[0] ?? null;
855
+ const focusedPaneId = sessionSnapshot?.focusedPaneId
856
+ && config.layout.instances.some((instance) => instance.instanceId === sessionSnapshot.focusedPaneId)
857
+ ? sessionSnapshot.focusedPaneId
858
+ : defaultFocusedPaneId;
859
+ const focusedConfig = bringFocusedFloatingPaneToFront(config, focusedPaneId);
860
+ return {
861
+ config: focusedConfig,
862
+ tickers: new Map(),
863
+ financials: new Map(),
864
+ exchangeRates: new Map([["USD", 1]]),
865
+ brokerAccounts: {},
866
+ activePanel: sessionSnapshot?.activePanel === "right" ? "right" : "left",
867
+ focusedPaneId,
868
+ paneState,
869
+ recentTickers: config.recentTickers,
870
+ commandBarOpen: false,
871
+ commandBarQuery: "",
872
+ commandBarLaunchRequest: null,
873
+ themePreview: null,
874
+ refreshing: new Set(),
875
+ initialized: false,
876
+ statusBarVisible: sessionSnapshot?.statusBarVisible !== false,
877
+ gridlockTipVisible: false,
878
+ gridlockTipSequence: 0,
879
+ inputCaptured: false,
880
+ updateAvailable: null,
881
+ updateProgress: null,
882
+ updateCheckInProgress: false,
883
+ updateNotice: null,
884
+ layoutHistory: {},
885
+ };
886
+ }