gloomberb 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -13
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +186 -6
  440. package/src/utils/api-client.ts +433 -33
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,17 +1,24 @@
1
- import { TextAttributes } from "@opentui/core";
2
- import { useKeyboard } from "@opentui/react";
3
- import { useDialog } from "@opentui-ui/dialog/react";
4
- import { useCallback, useEffect } from "react";
1
+ import { Box, ScrollBox, Text } from "../../ui";
2
+ import { TextAttributes } from "../../ui";
3
+ import { usePaneFooter } from "../../components";
4
+ import { useShortcut } from "../../react/input";
5
+ import { useDialog } from "../../ui/dialog";
6
+ import { useCallback, useEffect, useRef } from "react";
5
7
  import { resolveTickerFinancialsForInstrument } from "../../market-data/coordinator";
6
8
  import { instrumentFromTicker } from "../../market-data/request-types";
7
- import { useAppState, usePaneCollection, usePaneInstanceId } from "../../state/app-context";
9
+ import {
10
+ useAppDispatch,
11
+ useAppSelector,
12
+ usePaneCollection,
13
+ usePaneInstanceId,
14
+ } from "../../state/app-context";
8
15
  import { colors, priceColor } from "../../theme/colors";
9
16
  import type { PaneProps } from "../../types/plugin";
10
17
  import type { Quote } from "../../types/financials";
11
18
  import { formatCurrency, padTo } from "../../utils/format";
12
19
  import { formatMarketPrice, formatMarketQuantity } from "../../utils/market-format";
13
20
  import { getBrokerInstance } from "../../utils/broker-instances";
14
- import { getSharedRegistry } from "../registry";
21
+ import { usePluginPaneActions } from "../plugin-runtime";
15
22
  import { isGatewayConfigured } from "./config";
16
23
  import { ChoiceDialog } from "./dialogs";
17
24
  import { useIbkrGatewaySelection } from "./gateway-selection";
@@ -32,9 +39,13 @@ import {
32
39
  } from "./trade-utils";
33
40
 
34
41
  export function TradingPane({ focused, width, height }: PaneProps) {
35
- const { state, dispatch } = useAppState();
42
+ const dispatch = useAppDispatch();
43
+ const config = useAppSelector((state) => state.config);
44
+ const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
45
+ const tickers = useAppSelector((state) => state.tickers);
36
46
  const paneId = usePaneInstanceId();
37
47
  const { collectionId } = usePaneCollection(paneId);
48
+ const { selectTicker, switchTab, switchPanel } = usePluginPaneActions();
38
49
  const dialog = useDialog();
39
50
  const tradeState = useTradingPaneState();
40
51
  const {
@@ -49,8 +60,8 @@ export function TradingPane({ focused, width, height }: PaneProps) {
49
60
  availableAccounts,
50
61
  gatewayRequiredMessage,
51
62
  } = useIbkrGatewaySelection(
52
- state.config,
53
- state.brokerAccounts,
63
+ config,
64
+ brokerAccounts,
54
65
  collectionId,
55
66
  tradeState.brokerInstanceId,
56
67
  );
@@ -96,7 +107,7 @@ export function TradingPane({ focused, width, height }: PaneProps) {
96
107
  useEffect(() => {
97
108
  if (!isGatewayMode || availableAccounts.length === 0 || tradeState.accountId || !selectedInstance) return;
98
109
  const inferred = inferDraftAccountId(
99
- state.config,
110
+ config,
100
111
  collectionId,
101
112
  availableAccounts,
102
113
  selectedInstance.id,
@@ -105,7 +116,7 @@ export function TradingPane({ focused, width, height }: PaneProps) {
105
116
  if (inferred) {
106
117
  updateTradingPaneState({ accountId: inferred });
107
118
  }
108
- }, [isGatewayMode, availableAccounts, tradeState.accountId, state.config, collectionId, selectedInstance]);
119
+ }, [isGatewayMode, availableAccounts, tradeState.accountId, config, collectionId, selectedInstance]);
109
120
 
110
121
  const refresh = useCallback(async () => {
111
122
  if (!selectedInstance || !normalizedConfig || !isGatewayMode || !isGatewayConfigured(selectedInstance.config)) {
@@ -117,7 +128,7 @@ export function TradingPane({ focused, width, height }: PaneProps) {
117
128
  setTradingBusy(true);
118
129
  await refreshGatewayData(selectedInstance);
119
130
  const inferred = inferDraftAccountId(
120
- state.config,
131
+ config,
121
132
  collectionId,
122
133
  availableAccounts,
123
134
  selectedInstance.id,
@@ -136,7 +147,7 @@ export function TradingPane({ focused, width, height }: PaneProps) {
136
147
  selectedInstance,
137
148
  normalizedConfig,
138
149
  isGatewayMode,
139
- state.config,
150
+ config,
140
151
  collectionId,
141
152
  availableAccounts,
142
153
  tradeState.accountId,
@@ -165,13 +176,13 @@ export function TradingPane({ focused, width, height }: PaneProps) {
165
176
  choices={gatewayInstances.map((instance) => ({
166
177
  id: instance.id,
167
178
  label: instance.label,
168
- desc: "Gateway / TWS",
179
+ description: "Gateway / TWS",
169
180
  }))}
170
181
  />
171
182
  ),
172
183
  });
173
184
  if (!selected) return;
174
- const instance = getBrokerInstance(state.config.brokerInstances, selected);
185
+ const instance = getBrokerInstance(config.brokerInstances, selected);
175
186
  if (!instance) return;
176
187
  updateTradingPaneState({
177
188
  brokerInstanceId: instance.id,
@@ -181,7 +192,7 @@ export function TradingPane({ focused, width, height }: PaneProps) {
181
192
  lastError: undefined,
182
193
  lastInfo: undefined,
183
194
  });
184
- }, [dialog, gatewayInstances, lockedBrokerInstanceId, state.config.brokerInstances]);
195
+ }, [dialog, gatewayInstances, lockedBrokerInstanceId, config.brokerInstances]);
185
196
 
186
197
  const chooseAccount = useCallback(async () => {
187
198
  if (!selectedInstance || !normalizedConfig || !gatewayService || !isGatewayMode) return;
@@ -190,7 +201,7 @@ export function TradingPane({ focused, width, height }: PaneProps) {
190
201
  }
191
202
 
192
203
  const nextAccounts = getKnownIbkrAccounts(
193
- state.brokerAccounts,
204
+ brokerAccounts,
194
205
  selectedInstance.id,
195
206
  gatewayService.getSnapshot().accounts,
196
207
  );
@@ -207,14 +218,14 @@ export function TradingPane({ focused, width, height }: PaneProps) {
207
218
  choices={nextAccounts.map((account) => ({
208
219
  id: account.accountId,
209
220
  label: `${selectedInstance.label} → ${account.accountId}`,
210
- desc: `${formatCurrency(account.netLiquidation || 0, account.currency || "USD")} net liq`,
221
+ description: `${formatCurrency(account.netLiquidation || 0, account.currency || "USD")} net liq`,
211
222
  }))}
212
223
  />
213
224
  ),
214
225
  });
215
226
  if (!selected) return;
216
227
  updateTradingPaneState({ accountId: selected });
217
- }, [dialog, selectedInstance, normalizedConfig, gatewayService, isGatewayMode, availableAccounts, refresh]);
228
+ }, [availableAccounts, brokerAccounts, dialog, gatewayService, isGatewayMode, normalizedConfig, refresh, selectedInstance]);
218
229
 
219
230
  const cancelSelectedOrder = useCallback(async () => {
220
231
  if (!selectedOrder || !selectedInstance || !normalizedConfig || !gatewayService || !isGatewayMode) return;
@@ -232,8 +243,7 @@ export function TradingPane({ focused, width, height }: PaneProps) {
232
243
 
233
244
  const openSelectedOrder = useCallback(() => {
234
245
  if (!selectedOrder) return;
235
- const registry = getSharedRegistry();
236
- const ticker = findTickerForOrder(selectedOrder, state.tickers);
246
+ const ticker = findTickerForOrder(selectedOrder, tickers);
237
247
  if (!ticker) {
238
248
  setTradingMessage(undefined, `No local ticker exists for ${selectedOrder.contract.localSymbol || selectedOrder.contract.symbol}.`);
239
249
  return;
@@ -245,12 +255,43 @@ export function TradingPane({ focused, width, height }: PaneProps) {
245
255
  lastInfo: `Loaded order ${selectedOrder.orderId} into ${ticker.metadata.ticker}.`,
246
256
  lastError: undefined,
247
257
  });
248
- registry?.selectTickerFn(ticker.metadata.ticker, paneId);
249
- registry?.switchTabFn("ibkr-trade", paneId);
250
- registry?.switchPanelFn("right");
251
- }, [selectedOrder, state.tickers, paneId]);
258
+ selectTicker(ticker.metadata.ticker, paneId);
259
+ switchTab("ibkr-trade", paneId);
260
+ switchPanel("right");
261
+ }, [selectedOrder, tickers, paneId, selectTicker, switchTab, switchPanel]);
262
+
263
+ const footerActionsRef = useRef<{
264
+ chooseBrokerInstance: () => Promise<void>;
265
+ chooseAccount: () => Promise<void>;
266
+ openSelectedOrder: () => void;
267
+ cancelSelectedOrder: () => Promise<void>;
268
+ refresh: () => Promise<void>;
269
+ }>({
270
+ chooseBrokerInstance: async () => {},
271
+ chooseAccount: async () => {},
272
+ openSelectedOrder: () => {},
273
+ cancelSelectedOrder: async () => {},
274
+ refresh: async () => {},
275
+ });
276
+ footerActionsRef.current = {
277
+ chooseBrokerInstance,
278
+ chooseAccount,
279
+ openSelectedOrder,
280
+ cancelSelectedOrder,
281
+ refresh,
282
+ };
283
+
284
+ usePaneFooter("ibkr-trading-pane", () => ({
285
+ hints: [
286
+ { id: "profile", key: "i", label: "profile", onPress: () => footerActionsRef.current.chooseBrokerInstance().catch(() => {}) },
287
+ { id: "account", key: "a", label: "ccount", onPress: () => footerActionsRef.current.chooseAccount().catch(() => {}) },
288
+ { id: "open", key: "m", label: "open", onPress: () => footerActionsRef.current.openSelectedOrder() },
289
+ { id: "cancel", key: "c", label: "ancel", onPress: () => footerActionsRef.current.cancelSelectedOrder().catch(() => {}) },
290
+ { id: "refresh", key: "r", label: "efresh", onPress: () => footerActionsRef.current.refresh().catch(() => {}) },
291
+ ],
292
+ }), []);
252
293
 
253
- useKeyboard((event) => {
294
+ useShortcut((event) => {
254
295
  if (!focused) return;
255
296
  event.stopPropagation?.();
256
297
 
@@ -290,18 +331,18 @@ export function TradingPane({ focused, width, height }: PaneProps) {
290
331
  const activeAccount = availableAccounts.find((account) => account.accountId === (tradeState.accountId || ""));
291
332
  const orderPanelWidth = Math.max(36, Math.floor(width * 0.6));
292
333
  const listPanelWidth = Math.max(24, width - orderPanelWidth - 1);
293
- const listHeight = Math.max(4, height - 6);
334
+ const listHeight = Math.max(4, height - 4);
294
335
  const getOrderQuote = useCallback((symbol: string): Quote | null => {
295
- const ticker = state.tickers.get(symbol) ?? null;
336
+ const ticker = tickers.get(symbol) ?? null;
296
337
  const instrument = instrumentFromTicker(ticker, symbol);
297
338
  return instrument ? resolveTickerFinancialsForInstrument(instrument)?.quote ?? null : null;
298
- }, [state.tickers]);
339
+ }, [tickers]);
299
340
 
300
341
  return (
301
- <box flexDirection="column" flexGrow={1} paddingX={1}>
302
- <box flexDirection="row" height={1}>
303
- <box flexGrow={1}>
304
- <text fg={
342
+ <Box flexDirection="column" flexGrow={1} paddingX={1}>
343
+ <Box flexDirection="row" height={1}>
344
+ <Box flexGrow={1}>
345
+ <Text fg={
305
346
  displayStatusState === "connected"
306
347
  ? colors.positive
307
348
  : displayStatusState === "error"
@@ -311,13 +352,13 @@ export function TradingPane({ focused, width, height }: PaneProps) {
311
352
  {selectedInstance
312
353
  ? `${selectedInstance.label} · ${isGatewayMode ? "Gateway" : "Flex"} · ${displayStatusState}`
313
354
  : "IBKR · no profile selected"}
314
- </text>
315
- </box>
316
- {tradeState.busy && <text fg={colors.textDim}>Working…</text>}
317
- </box>
355
+ </Text>
356
+ </Box>
357
+ {tradeState.busy && <Text fg={colors.textDim}>Working…</Text>}
358
+ </Box>
318
359
 
319
- <box height={1}>
320
- <text fg={colors.textDim}>
360
+ <Box height={1}>
361
+ <Text fg={colors.textDim}>
321
362
  {activeAccount
322
363
  ? `${selectedInstance?.label || "IBKR"} → ${activeAccount.accountId} · ${formatCurrency(activeAccount.netLiquidation || 0, activeAccount.currency || "USD")} net liq`
323
364
  : isGatewayMode
@@ -327,29 +368,29 @@ export function TradingPane({ focused, width, height }: PaneProps) {
327
368
  : gatewayInstances.length > 0
328
369
  ? "Choose a Gateway / TWS profile"
329
370
  : "Connect an IBKR profile"}
330
- </text>
331
- </box>
371
+ </Text>
372
+ </Box>
332
373
 
333
- <box height={1}>
334
- <text fg={tradeState.lastError ? colors.negative : colors.textDim}>
374
+ <Box height={1}>
375
+ <Text fg={tradeState.lastError ? colors.negative : colors.textDim}>
335
376
  {tradeState.lastError
336
377
  || gatewaySnapshot.status.message
337
378
  || gatewaySnapshot.lastError
338
379
  || tradeState.lastInfo
339
380
  || "Use this console for profile status, accounts, open orders, and executions."}
340
- </text>
341
- </box>
381
+ </Text>
382
+ </Box>
342
383
 
343
- <box height={1}>
344
- <text fg={colors.border}>{"─".repeat(Math.max(1, width - 2))}</text>
345
- </box>
384
+ <Box height={1}>
385
+ <Text fg={colors.border}>{"─".repeat(Math.max(1, width - 2))}</Text>
386
+ </Box>
346
387
 
347
- <box flexDirection="row" height={listHeight}>
348
- <box width={orderPanelWidth} flexDirection="column">
349
- <text attributes={TextAttributes.BOLD} fg={colors.textBright}>Open Orders</text>
350
- <scrollbox flexGrow={1} scrollY>
388
+ <Box flexDirection="row" height={listHeight}>
389
+ <Box width={orderPanelWidth} flexDirection="column">
390
+ <Text attributes={TextAttributes.BOLD} fg={colors.textBright}>Open Orders</Text>
391
+ <ScrollBox flexGrow={1} scrollY>
351
392
  {gatewaySnapshot.openOrders.length === 0 ? (
352
- <text fg={colors.textDim}>No open IBKR orders.</text>
393
+ <Text fg={colors.textDim}>No open IBKR orders.</Text>
353
394
  ) : (
354
395
  gatewaySnapshot.openOrders.map((order, index) => {
355
396
  const selected = index === tradeState.selectedOpenOrderIndex;
@@ -363,7 +404,7 @@ export function TradingPane({ focused, width, height }: PaneProps) {
363
404
  ? formatMarketPrice(order.stopPrice, { contractSecType: order.contract.secType, maxWidth: 9 })
364
405
  : "MKT";
365
406
  return (
366
- <box
407
+ <Box
367
408
  key={order.orderId}
368
409
  backgroundColor={selected ? colors.selected : colors.bg}
369
410
  onMouseDown={() => {
@@ -374,7 +415,7 @@ export function TradingPane({ focused, width, height }: PaneProps) {
374
415
  }
375
416
  }}
376
417
  >
377
- <text fg={selected ? colors.text : colors.textDim}>
418
+ <Text fg={selected ? colors.text : colors.textDim}>
378
419
  {selected ? "▸ " : " "}
379
420
  {padTo(String(order.orderId), 6)}
380
421
  {padTo(order.action, 5)}
@@ -385,55 +426,48 @@ export function TradingPane({ focused, width, height }: PaneProps) {
385
426
  {padTo(orderPrice, 9)}
386
427
  {padTo(`B:${bidStr}`, 10)}
387
428
  {`A:${askStr}`}
388
- </text>
389
- </box>
429
+ </Text>
430
+ </Box>
390
431
  );
391
432
  })
392
433
  )}
393
- </scrollbox>
394
- </box>
434
+ </ScrollBox>
435
+ </Box>
395
436
 
396
- <box width={1}>
397
- <text fg={colors.border}>│</text>
398
- </box>
437
+ <Box width={1}>
438
+ <Text fg={colors.border}>│</Text>
439
+ </Box>
399
440
 
400
- <box width={listPanelWidth} flexDirection="column">
401
- <text attributes={TextAttributes.BOLD} fg={colors.textBright}>Executions</text>
402
- <scrollbox flexGrow={1} scrollY>
441
+ <Box width={listPanelWidth} flexDirection="column">
442
+ <Text attributes={TextAttributes.BOLD} fg={colors.textBright}>Executions</Text>
443
+ <ScrollBox flexGrow={1} scrollY>
403
444
  {gatewaySnapshot.executions.length === 0 ? (
404
- <text fg={colors.textDim}>No recent executions.</text>
445
+ <Text fg={colors.textDim}>No recent executions.</Text>
405
446
  ) : (
406
447
  gatewaySnapshot.executions.slice(0, 20).map((execution) => (
407
- <box
448
+ <Box
408
449
  key={execution.execId}
409
450
  onMouseDown={() => {
410
451
  const symbol = execution.contract.symbol;
411
- if (symbol && state.tickers.has(symbol)) {
412
- getSharedRegistry()?.selectTickerFn(symbol, paneId);
452
+ if (symbol && tickers.has(symbol)) {
453
+ selectTicker(symbol, paneId);
413
454
  }
414
455
  }}
415
456
  >
416
- <text fg={priceColor(execution.side.toUpperCase() === "BOT" ? 1 : -1)}>
457
+ <Text fg={priceColor(execution.side.toUpperCase() === "BOT" ? 1 : -1)}>
417
458
  {padTo(execution.side, 5)}
418
459
  {padTo(execution.contract.localSymbol || execution.contract.symbol, 18)}
419
460
  {padTo(formatMarketQuantity(execution.shares, { contractSecType: execution.contract.secType, maxWidth: 6 }), 6, "right")}
420
461
  {" "}
421
462
  {formatMarketPrice(execution.price, { contractSecType: execution.contract.secType })}
422
- </text>
423
- </box>
463
+ </Text>
464
+ </Box>
424
465
  ))
425
466
  )}
426
- </scrollbox>
427
- </box>
428
- </box>
467
+ </ScrollBox>
468
+ </Box>
469
+ </Box>
429
470
 
430
- <box flexDirection="row" height={1}>
431
- <text fg={colors.textMuted} onMouseDown={() => chooseBrokerInstance().catch(() => {})}>{" [i] Profile "}</text>
432
- <text fg={colors.textMuted} onMouseDown={() => chooseAccount().catch(() => {})}>{" [a] Account "}</text>
433
- <text fg={colors.textMuted} onMouseDown={() => openSelectedOrder()}>{" [Enter] Open "}</text>
434
- <text fg={colors.textMuted} onMouseDown={() => cancelSelectedOrder().catch(() => {})}>{" [c] Cancel "}</text>
435
- <text fg={colors.textMuted} onMouseDown={() => refresh().catch(() => {})}>{" [r] Refresh "}</text>
436
- </box>
437
- </box>
471
+ </Box>
438
472
  );
439
473
  }
@@ -19,6 +19,7 @@ describe("pane-manager split-tree drops", () => {
19
19
  ...cloneLayout(config.layout),
20
20
  dockRoot: null,
21
21
  floating: [],
22
+ detached: [],
22
23
  instances: [],
23
24
  };
24
25
  const floatingOnly = addPaneFloating(layout, createPaneInstance("chat"), 120, 30);
@@ -71,7 +72,7 @@ describe("pane-manager split-tree drops", () => {
71
72
  const next = gridlockAllPanes(layout);
72
73
 
73
74
  expect(next.floating).toHaveLength(0);
74
- expect(getDockedPaneIds(next)).toHaveLength(5);
75
+ expect(getDockedPaneIds(next)).toHaveLength(layout.instances.length);
75
76
  });
76
77
 
77
78
  test("gridlock infers a matching tiled layout from arranged windows", () => {
@@ -80,6 +81,7 @@ describe("pane-manager split-tree drops", () => {
80
81
  ...cloneLayout(config.layout),
81
82
  dockRoot: null,
82
83
  floating: [],
84
+ detached: [],
83
85
  instances: [],
84
86
  };
85
87
  const leftPane = createPaneInstance("chat");
@@ -81,6 +81,11 @@ export interface DockDividerLayout {
81
81
  ratio: number;
82
82
  }
83
83
 
84
+ export interface DockGeometryOptions {
85
+ precise?: boolean;
86
+ dividerSize?: number;
87
+ }
88
+
84
89
  export interface LayoutSimulation {
85
90
  layout: LayoutConfig;
86
91
  previewRect: LayoutBounds | null;
@@ -202,9 +207,24 @@ function removeNodeAtPath(node: DockLayoutNode | null, path: Array<0 | 1>): Dock
202
207
 
203
208
  function normalizeFloatingEntries(layout: LayoutConfig): FloatingPaneEntry[] {
204
209
  const dockedIds = new Set(getDockedInstanceIds(layout.dockRoot));
210
+ const detachedIds = new Set((layout.detached ?? []).map((entry) => entry.instanceId));
205
211
  const seen = new Set<string>();
206
212
  return layout.floating
207
- .filter((entry) => !dockedIds.has(entry.instanceId))
213
+ .filter((entry) => !dockedIds.has(entry.instanceId) && !detachedIds.has(entry.instanceId))
214
+ .filter((entry) => {
215
+ if (seen.has(entry.instanceId)) return false;
216
+ seen.add(entry.instanceId);
217
+ return true;
218
+ })
219
+ .map((entry) => ({ ...entry }));
220
+ }
221
+
222
+ function normalizeDetachedEntries(layout: LayoutConfig) {
223
+ const dockedIds = new Set(getDockedInstanceIds(layout.dockRoot));
224
+ const floatingIds = new Set(layout.floating.map((entry) => entry.instanceId));
225
+ const seen = new Set<string>();
226
+ return (layout.detached ?? [])
227
+ .filter((entry) => !dockedIds.has(entry.instanceId) && !floatingIds.has(entry.instanceId))
208
228
  .filter((entry) => {
209
229
  if (seen.has(entry.instanceId)) return false;
210
230
  seen.add(entry.instanceId);
@@ -249,6 +269,7 @@ function captureDockedMemory(layout: LayoutConfig, dockRoot: DockLayoutNode | nu
249
269
  function capturePlacementMemory(layout: LayoutConfig): LayoutConfig {
250
270
  const dockedMemory = captureDockedMemory(layout, layout.dockRoot);
251
271
  const floatingById = new Map(layout.floating.map((entry) => [entry.instanceId, entry] as const));
272
+ const detachedById = new Map((layout.detached ?? []).map((entry) => [entry.instanceId, entry] as const));
252
273
 
253
274
  return {
254
275
  ...layout,
@@ -262,12 +283,20 @@ function capturePlacementMemory(layout: LayoutConfig): LayoutConfig {
262
283
  width: floating.width,
263
284
  height: floating.height,
264
285
  } : previous.floating;
286
+ const detached = detachedById.get(instance.instanceId);
287
+ const nextDetached = detached ? {
288
+ x: detached.x,
289
+ y: detached.y,
290
+ width: detached.width,
291
+ height: detached.height,
292
+ } : previous.detached;
265
293
 
266
294
  return {
267
295
  ...instance,
268
- placementMemory: nextDocked || nextFloating ? {
296
+ placementMemory: nextDocked || nextFloating || nextDetached ? {
269
297
  docked: nextDocked,
270
298
  floating: nextFloating,
299
+ detached: nextDetached,
271
300
  } : undefined,
272
301
  };
273
302
  }),
@@ -278,6 +307,7 @@ function finalizeLayout(layout: LayoutConfig): LayoutConfig {
278
307
  const normalized = normalizePaneLayout({
279
308
  ...layout,
280
309
  floating: normalizeFloatingEntries(layout),
310
+ detached: normalizeDetachedEntries(layout),
281
311
  });
282
312
  return capturePlacementMemory(normalized);
283
313
  }
@@ -313,6 +343,7 @@ function detachPane(layout: LayoutConfig, instanceId: string): LayoutConfig {
313
343
  return {
314
344
  ...removeDockedLeaf(layout, instanceId),
315
345
  floating: layout.floating.filter((entry) => entry.instanceId !== instanceId),
346
+ detached: (layout.detached ?? []).filter((entry) => entry.instanceId !== instanceId),
316
347
  };
317
348
  }
318
349
 
@@ -373,7 +404,19 @@ function scoreDirectionalCandidate(
373
404
  return primaryDelta * 1000 + secondaryDelta;
374
405
  }
375
406
 
376
- function resolveSplitSizes(total: number, ratio: number, minSize: number): [number, number] {
407
+ function resolveSplitSizes(total: number, ratio: number, minSize: number, precise = false): [number, number] {
408
+ if (precise) {
409
+ if (total <= 0) return [0, 0];
410
+ if (total <= 1) return [total, 0];
411
+ if (total < minSize * 2) {
412
+ const first = total / 2;
413
+ return [first, total - first];
414
+ }
415
+ const preferred = total * clampRatio(ratio);
416
+ const first = Math.max(minSize, Math.min(total - minSize, preferred));
417
+ return [first, total - first];
418
+ }
419
+
377
420
  if (total <= 1) return [1, 0];
378
421
  if (total < minSize * 2) {
379
422
  const first = Math.max(1, Math.floor(total / 2));
@@ -390,6 +433,7 @@ function collectDockGeometry(
390
433
  path: Array<0 | 1> = [],
391
434
  leaves: DockLeafLayout[] = [],
392
435
  dividers: DockDividerLayout[] = [],
436
+ options: DockGeometryOptions = {},
393
437
  ): { leaves: DockLeafLayout[]; dividers: DockDividerLayout[] } {
394
438
  if (!node) return { leaves, dividers };
395
439
  if (node.kind === "pane") {
@@ -397,8 +441,11 @@ function collectDockGeometry(
397
441
  return { leaves, dividers };
398
442
  }
399
443
 
444
+ const precise = options.precise === true;
445
+ const dividerSize = options.dividerSize ?? 1;
446
+
400
447
  if (node.axis === "horizontal") {
401
- const [firstWidth, secondWidth] = resolveSplitSizes(bounds.width, node.ratio, MIN_PANE_WIDTH);
448
+ const [firstWidth, secondWidth] = resolveSplitSizes(bounds.width, node.ratio, MIN_PANE_WIDTH, precise);
402
449
  const firstBounds = { x: bounds.x, y: bounds.y, width: firstWidth, height: bounds.height };
403
450
  const secondBounds = { x: bounds.x + firstWidth, y: bounds.y, width: secondWidth, height: bounds.height };
404
451
  dividers.push({
@@ -407,18 +454,18 @@ function collectDockGeometry(
407
454
  bounds: { ...bounds },
408
455
  ratio: node.ratio,
409
456
  rect: {
410
- x: bounds.x + firstWidth - 1,
457
+ x: precise ? bounds.x + firstWidth - (dividerSize / 2) : bounds.x + firstWidth - 1,
411
458
  y: bounds.y,
412
- width: 1,
459
+ width: dividerSize,
413
460
  height: bounds.height,
414
461
  },
415
462
  });
416
- collectDockGeometry(node.first, firstBounds, [...path, 0], leaves, dividers);
417
- collectDockGeometry(node.second, secondBounds, [...path, 1], leaves, dividers);
463
+ collectDockGeometry(node.first, firstBounds, [...path, 0], leaves, dividers, options);
464
+ collectDockGeometry(node.second, secondBounds, [...path, 1], leaves, dividers, options);
418
465
  return { leaves, dividers };
419
466
  }
420
467
 
421
- const [firstHeight, secondHeight] = resolveSplitSizes(bounds.height, node.ratio, MIN_DOCKED_HEIGHT);
468
+ const [firstHeight, secondHeight] = resolveSplitSizes(bounds.height, node.ratio, MIN_DOCKED_HEIGHT, precise);
422
469
  const firstBounds = { x: bounds.x, y: bounds.y, width: bounds.width, height: firstHeight };
423
470
  const secondBounds = { x: bounds.x, y: bounds.y + firstHeight, width: bounds.width, height: secondHeight };
424
471
  dividers.push({
@@ -428,13 +475,13 @@ function collectDockGeometry(
428
475
  ratio: node.ratio,
429
476
  rect: {
430
477
  x: bounds.x,
431
- y: bounds.y + firstHeight - 1,
478
+ y: precise ? bounds.y + firstHeight - (dividerSize / 2) : bounds.y + firstHeight - 1,
432
479
  width: bounds.width,
433
- height: 1,
480
+ height: dividerSize,
434
481
  },
435
482
  });
436
- collectDockGeometry(node.first, firstBounds, [...path, 0], leaves, dividers);
437
- collectDockGeometry(node.second, secondBounds, [...path, 1], leaves, dividers);
483
+ collectDockGeometry(node.first, firstBounds, [...path, 0], leaves, dividers, options);
484
+ collectDockGeometry(node.second, secondBounds, [...path, 1], leaves, dividers, options);
438
485
  return { leaves, dividers };
439
486
  }
440
487
 
@@ -593,12 +640,12 @@ export function findDockLeaf(layout: LayoutConfig, instanceId: string): DockLeaf
593
640
  return traverseDockLeaves(layout).find((entry) => entry.instanceId === instanceId) ?? null;
594
641
  }
595
642
 
596
- export function getDockLeafLayouts(layout: LayoutConfig, bounds: LayoutBounds): DockLeafLayout[] {
597
- return collectDockGeometry(layout.dockRoot, bounds).leaves;
643
+ export function getDockLeafLayouts(layout: LayoutConfig, bounds: LayoutBounds, options?: DockGeometryOptions): DockLeafLayout[] {
644
+ return collectDockGeometry(layout.dockRoot, bounds, [], [], [], options).leaves;
598
645
  }
599
646
 
600
- export function getDockDividerLayouts(layout: LayoutConfig, bounds: LayoutBounds): DockDividerLayout[] {
601
- return collectDockGeometry(layout.dockRoot, bounds).dividers;
647
+ export function getDockDividerLayouts(layout: LayoutConfig, bounds: LayoutBounds, options?: DockGeometryOptions): DockDividerLayout[] {
648
+ return collectDockGeometry(layout.dockRoot, bounds, [], [], [], options).dividers;
602
649
  }
603
650
 
604
651
  export function getLeafRect(layout: LayoutConfig, instanceId: string, bounds: LayoutBounds): LayoutBounds | null {
@@ -636,6 +683,27 @@ export function resolveFloating(
636
683
  return result;
637
684
  }
638
685
 
686
+ export function detachPaneToFrame(
687
+ layout: LayoutConfig,
688
+ instanceId: string,
689
+ rect: Pick<LayoutConfig["detached"][number], "x" | "y" | "width" | "height">,
690
+ ): LayoutConfig {
691
+ const base = detachPane(layout, instanceId);
692
+ return finalizeLayout({
693
+ ...base,
694
+ detached: [
695
+ ...base.detached.filter((entry) => entry.instanceId !== instanceId),
696
+ {
697
+ instanceId,
698
+ x: Math.max(0, Math.round(rect.x)),
699
+ y: Math.max(0, Math.round(rect.y)),
700
+ width: Math.max(1, Math.round(rect.width)),
701
+ height: Math.max(1, Math.round(rect.height)),
702
+ },
703
+ ],
704
+ });
705
+ }
706
+
639
707
  export function floatAtRect(layout: LayoutConfig, instanceId: string, rect: FloatingRect): LayoutConfig {
640
708
  const base = detachPane(layout, instanceId);
641
709
  return finalizeLayout({
@@ -900,6 +968,7 @@ export function removePane(layout: LayoutConfig, instanceId: string): LayoutConf
900
968
  ...layout,
901
969
  dockRoot: removeDockedLeaf(layout, instanceId).dockRoot,
902
970
  floating: layout.floating.filter((entry) => entry.instanceId !== instanceId),
971
+ detached: (layout.detached ?? []).filter((entry) => entry.instanceId !== instanceId),
903
972
  },
904
973
  [instanceId],
905
974
  ));
@@ -907,7 +976,12 @@ export function removePane(layout: LayoutConfig, instanceId: string): LayoutConf
907
976
 
908
977
  export function isPaneInLayout(layout: LayoutConfig, instanceId: string): boolean {
909
978
  return !!findDockLeaf(layout, instanceId)
910
- || layout.floating.some((entry) => entry.instanceId === instanceId);
979
+ || layout.floating.some((entry) => entry.instanceId === instanceId)
980
+ || (layout.detached ?? []).some((entry) => entry.instanceId === instanceId);
981
+ }
982
+
983
+ export function isPaneDetached(layout: LayoutConfig, instanceId: string): boolean {
984
+ return (layout.detached ?? []).some((entry) => entry.instanceId === instanceId);
911
985
  }
912
986
 
913
987
  export function isPaneDocked(layout: LayoutConfig, instanceId: string): boolean {
@@ -933,7 +1007,7 @@ export function bringToFront(layout: LayoutConfig, instanceId: string): LayoutCo
933
1007
 
934
1008
  export function getLayoutPreview(layout: LayoutConfig): string {
935
1009
  const geometry = getDockLeafLayouts(layout, { x: 0, y: 0, width: 120, height: 40 });
936
- return `${countColumnsFromGeometry(geometry)}c / ${geometry.length}d / ${layout.floating.length}f`;
1010
+ return `${countColumnsFromGeometry(geometry)}c / ${geometry.length}d / ${layout.floating.length}f / ${(layout.detached ?? []).length}x`;
937
1011
  }
938
1012
 
939
1013
  export function gridlockAllPanes(