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,6 +1,15 @@
1
- const RESERVED_PANE_CHROME_ROWS = 2;
2
1
 
3
- export function getPaneBodyHeight(height: number): number {
4
- // Reserve the header row plus the bottom border/resize row.
5
- return Math.max(1, height - RESERVED_PANE_CHROME_ROWS);
2
+ const PANE_HEADER_ROWS = 1;
3
+ const PANE_FOOTER_ROWS = 1;
4
+
5
+ export function shouldReservePaneFooter(nativePaneChrome: boolean | undefined, showFooter: boolean): boolean {
6
+ return !nativePaneChrome || showFooter;
7
+ }
8
+
9
+ export function getPaneBodyHeight(height: number, reserveFooter = true): number {
10
+ return Math.max(1, height - PANE_HEADER_ROWS - (reserveFooter ? PANE_FOOTER_ROWS : 0));
11
+ }
12
+
13
+ export function getPaneBodyWidth(width: number): number {
14
+ return Math.max(1, width);
6
15
  }
@@ -0,0 +1,35 @@
1
+ import { resolveCollectionForPane, resolveTickerForPane, type AppState } from "../../state/app-context";
2
+ import type { PaneInstanceConfig } from "../../types/config";
3
+ import type { PaneDef } from "../../types/plugin";
4
+
5
+ export function getPaneDisplayTitle(
6
+ state: Pick<AppState, "config" | "paneState">,
7
+ instance: PaneInstanceConfig,
8
+ paneDef: PaneDef,
9
+ ): string {
10
+ if (instance.paneId === "chat") {
11
+ return instance.title ?? "#everyone";
12
+ }
13
+
14
+ if (instance.paneId === "ticker-detail") {
15
+ const ticker = resolveTickerForPane(state as AppState, instance.instanceId);
16
+ if (ticker) return ticker;
17
+ const collectionId = resolveCollectionForPane(state as AppState, instance.instanceId);
18
+ return state.config.portfolios.find((portfolio) => portfolio.id === collectionId)?.name
19
+ ?? state.config.watchlists.find((watchlist) => watchlist.id === collectionId)?.name
20
+ ?? instance.title
21
+ ?? paneDef.name;
22
+ }
23
+
24
+ if (instance.title) return instance.title;
25
+
26
+ if (instance.paneId === "portfolio-list") {
27
+ const collectionId = resolveCollectionForPane(state as AppState, instance.instanceId);
28
+ return state.config.portfolios.find((portfolio) => portfolio.id === collectionId)?.name
29
+ ?? state.config.watchlists.find((watchlist) => watchlist.id === collectionId)?.name
30
+ ?? paneDef.name;
31
+ }
32
+
33
+ const ticker = resolveTickerForPane(state as AppState, instance.instanceId);
34
+ return ticker ? `${paneDef.name}: ${ticker}` : paneDef.name;
35
+ }
@@ -1,9 +1,12 @@
1
+ import { Box, useUiCapabilities } from "../../ui";
1
2
  import type { ReactNode } from "react";
2
- import { paneBg } from "../../theme/colors";
3
+ import { colors, paneBg } from "../../theme/colors";
3
4
  import { PaneHeader } from "./pane-header";
4
- import { getPaneBodyHeight } from "./pane-sizing";
5
+ import { hasPaneFooterContent, PaneFooterBar, type CombinedPaneFooter } from "./pane-footer";
6
+ import { getPaneBodyHeight, shouldReservePaneFooter } from "./pane-sizing";
5
7
 
6
8
  interface PaneWrapperProps {
9
+ paneId?: string;
7
10
  title?: string;
8
11
  focused: boolean;
9
12
  width?: number;
@@ -12,10 +15,17 @@ interface PaneWrapperProps {
12
15
  showActions?: boolean;
13
16
  onMouseDown?: (event: any) => void;
14
17
  onMouseMove?: (event: any) => void;
18
+ onHeaderMouseDown?: (event: any) => void;
19
+ onHeaderMouseDrag?: (event: any) => void;
20
+ onHeaderMouseDragEnd?: (event: any) => void;
21
+ onHeaderContextMenu?: (event: any) => void;
22
+ onActionMouseDown?: (event: any) => void;
23
+ footer?: CombinedPaneFooter | null;
15
24
  children: ReactNode;
16
25
  }
17
26
 
18
27
  export function PaneWrapper({
28
+ paneId,
19
29
  title,
20
30
  focused,
21
31
  width = 0,
@@ -24,30 +34,68 @@ export function PaneWrapper({
24
34
  showActions = false,
25
35
  onMouseDown,
26
36
  onMouseMove,
37
+ onHeaderMouseDown,
38
+ onHeaderMouseDrag,
39
+ onHeaderMouseDragEnd,
40
+ onHeaderContextMenu,
41
+ onActionMouseDown,
42
+ footer,
27
43
  children,
28
44
  }: PaneWrapperProps) {
45
+ const { nativePaneChrome } = useUiCapabilities();
29
46
  const bg = paneBg(focused);
47
+ const showFooter = hasPaneFooterContent(footer);
48
+ const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
30
49
  const bodyHeight = typeof height === "number"
31
- ? title ? getPaneBodyHeight(height) : height
50
+ ? title ? getPaneBodyHeight(height, reserveFooter) : height
32
51
  : undefined;
33
52
 
34
53
  return (
35
- <box
54
+ <Box
36
55
  flexDirection="column"
37
56
  width={width}
38
57
  height={height}
39
58
  flexGrow={flexGrow}
40
59
  backgroundColor={bg}
41
60
  overflow="hidden"
61
+ {...(nativePaneChrome ? {
62
+ "data-gloom-role": "pane-window",
63
+ "data-gloom-pane-id": paneId,
64
+ "data-floating": "false",
65
+ "data-focused": focused ? "true" : "false",
66
+ style: { "--pane-border-color": focused ? colors.borderFocused : colors.border },
67
+ } : {})}
42
68
  onMouseDown={onMouseDown}
43
69
  onMouseMove={onMouseMove}
44
70
  >
45
71
  {title && (
46
- <PaneHeader title={title} width={width} focused={focused} showActions={showActions} />
72
+ <PaneHeader
73
+ title={title}
74
+ width={width}
75
+ focused={focused}
76
+ showActions={showActions}
77
+ onHeaderMouseDown={onHeaderMouseDown}
78
+ onHeaderMouseDrag={onHeaderMouseDrag}
79
+ onHeaderMouseDragEnd={onHeaderMouseDragEnd}
80
+ onHeaderContextMenu={onHeaderContextMenu}
81
+ onActionMouseDown={onActionMouseDown}
82
+ />
47
83
  )}
48
- <box height={bodyHeight} flexGrow={bodyHeight == null ? 1 : 0} overflow="hidden">
84
+ <Box
85
+ height={bodyHeight}
86
+ flexGrow={bodyHeight == null ? 1 : 0}
87
+ flexBasis={bodyHeight == null ? 0 : undefined}
88
+ overflow="hidden"
89
+ >
49
90
  {children}
50
- </box>
51
- </box>
91
+ </Box>
92
+ {title && reserveFooter && (
93
+ <PaneFooterBar
94
+ footer={footer}
95
+ focused={focused}
96
+ width={typeof width === "number" ? width : undefined}
97
+ />
98
+ )}
99
+ </Box>
52
100
  );
53
101
  }
@@ -1,21 +1,32 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { TextAttributes } from "@opentui/core";
3
- import { testRender } from "@opentui/react/test-utils";
3
+ import { testRender } from "../../renderers/opentui/test-utils";
4
4
  import { DialogProvider } from "@opentui-ui/dialog/react";
5
5
  import type { ReactNode } from "react";
6
- import { act, useReducer } from "react";
6
+ import { act, createElement, useReducer } from "react";
7
7
  import { AppContext, appReducer, createInitialState } from "../../state/app-context";
8
8
  import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator";
9
- import { setSharedDataProviderForTests } from "../../plugins/registry";
9
+ import { setSharedMarketDataForTests } from "../../plugins/registry";
10
10
  import { cloneLayout, createDefaultConfig } from "../../types/config";
11
11
  import type { PluginRegistry } from "../../plugins/registry";
12
12
  import { setSharedRegistryForTests } from "../../plugins/registry";
13
13
  import { portfolioListPlugin } from "../../plugins/builtin/portfolio-list";
14
+ import { PluginRenderProvider } from "../../plugins/plugin-runtime";
15
+ import { createTestPluginRuntime } from "../../test-support/plugin-runtime";
14
16
  import type { PaneProps } from "../../types/plugin";
15
17
  import { StockChart } from "../chart/stock-chart";
16
18
  import { StatusBar } from "./status-bar";
17
19
  import { Header } from "./header";
18
- import { buildNativeWindowState, finalizePaneDragRelease, resolveNativeDockDividers, Shell } from "./shell";
20
+ import {
21
+ buildNativeWindowState,
22
+ constrainFloatingRectToBounds,
23
+ finalizePaneDragRelease,
24
+ resolvePaneManagementShortcut,
25
+ resolveAppHeaderHeightCells,
26
+ resolveNativeDockDividers,
27
+ resolvePaneDragFloatingRect,
28
+ Shell,
29
+ } from "./shell";
19
30
  import type { DataProvider } from "../../types/data-provider";
20
31
  import type { PricePoint, Quote, TickerFinancials } from "../../types/financials";
21
32
  import type { TickerRecord } from "../../types/ticker";
@@ -30,11 +41,12 @@ afterEach(() => {
30
41
  }
31
42
  harnessState = null;
32
43
  setSharedMarketDataCoordinator(null);
33
- setSharedDataProviderForTests(undefined);
44
+ setSharedMarketDataForTests(undefined);
34
45
  setSharedRegistryForTests(undefined);
35
46
  });
36
47
 
37
48
  function createShellPluginRegistry(options?: {
49
+ portfolioListComponent?: (props: PaneProps) => ReactNode;
38
50
  tickerDetailComponent?: (props: PaneProps) => ReactNode;
39
51
  }): PluginRegistry {
40
52
  return {
@@ -42,7 +54,7 @@ function createShellPluginRegistry(options?: {
42
54
  ["portfolio-list", {
43
55
  id: "portfolio-list",
44
56
  name: "Portfolio List",
45
- component: () => <text>Portfolio Body</text>,
57
+ component: options?.portfolioListComponent ?? (() => <text>Portfolio Body</text>),
46
58
  defaultPosition: "left",
47
59
  }],
48
60
  ["ticker-detail", {
@@ -62,7 +74,7 @@ function createShellPluginRegistry(options?: {
62
74
  getPluginPaneTemplateIds: () => [],
63
75
  hasPaneSettings: (paneId: string) => paneId === "portfolio-list:main",
64
76
  openPaneSettingsFn: () => {},
65
- openCommandBarFn: () => {},
77
+ openCommandBar: () => {},
66
78
  updateLayoutFn: () => {},
67
79
  hideWidget: () => {},
68
80
  } as unknown as PluginRegistry;
@@ -71,8 +83,26 @@ function createShellPluginRegistry(options?: {
71
83
  function createBrokerPortfolioRegistry(): PluginRegistry {
72
84
  const pane = portfolioListPlugin.panes?.[0];
73
85
  if (!pane) throw new Error("missing portfolio pane");
74
- return {
75
- panes: new Map([["portfolio-list", pane]]),
86
+
87
+ let registry: PluginRegistry;
88
+ const runtime = createTestPluginRuntime({
89
+ pinTicker: (symbol, options) => registry.pinTicker(symbol, options),
90
+ navigateTicker: (symbol) => registry.navigateTicker(symbol),
91
+ selectTicker: (symbol, paneId) => registry.selectTicker(symbol, paneId),
92
+ switchTab: (tabId, paneId) => registry.switchTab(tabId, paneId),
93
+ switchPanel: (panel) => registry.switchPanel(panel),
94
+ });
95
+ const wrappedPane = {
96
+ ...pane,
97
+ component: (props: PaneProps) => (
98
+ <PluginRenderProvider pluginId={portfolioListPlugin.id} runtime={runtime}>
99
+ {createElement(pane.component as any, props)}
100
+ </PluginRenderProvider>
101
+ ),
102
+ };
103
+
104
+ registry = {
105
+ panes: new Map([["portfolio-list", wrappedPane]]),
76
106
  paneTemplates: new Map(),
77
107
  commands: new Map(),
78
108
  tickerActions: new Map(),
@@ -82,16 +112,19 @@ function createBrokerPortfolioRegistry(): PluginRegistry {
82
112
  getPluginPaneTemplateIds: () => [],
83
113
  hasPaneSettings: () => true,
84
114
  openPaneSettingsFn: () => {},
85
- openCommandBarFn: () => {},
115
+ openCommandBar: () => {},
86
116
  updateLayoutFn: () => {},
87
117
  hideWidget: () => {},
88
118
  focusPaneFn: () => {},
89
- pinTickerFn: () => {},
119
+ pinTicker: () => {},
120
+ navigateTicker: () => {},
90
121
  showPaneFn: () => {},
91
122
  getLayoutFn: () => ({ dockRoot: null, instances: [], floating: [] }),
92
123
  getTermSizeFn: () => ({ width: 120, height: 40 }),
93
124
  Slot: () => null,
94
125
  } as unknown as PluginRegistry;
126
+
127
+ return registry;
95
128
  }
96
129
 
97
130
  function makeQuote(overrides: Partial<Quote> = {}): Quote {
@@ -393,6 +426,11 @@ describe("Header", () => {
393
426
  });
394
427
 
395
428
  describe("Shell", () => {
429
+ test("uses the desktop titlebar overlay height for shell chrome math", () => {
430
+ expect(resolveAppHeaderHeightCells({ titleBarOverlay: true, cellHeightPx: 18 })).toBe(28 / 18);
431
+ expect(resolveAppHeaderHeightCells({ titleBarOverlay: false, cellHeightPx: 18 })).toBe(1);
432
+ });
433
+
396
434
  test("uses the live floating preview rect for native occluders", () => {
397
435
  const state = buildNativeWindowState(
398
436
  ["portfolio-list:main"],
@@ -424,6 +462,26 @@ describe("Shell", () => {
424
462
  ]);
425
463
  });
426
464
 
465
+ test("offsets native occluders by the actual app header height", () => {
466
+ const state = buildNativeWindowState(
467
+ [],
468
+ [
469
+ {
470
+ paneId: "ticker-detail:main",
471
+ rect: { x: 8, y: 2, width: 36, height: 12 },
472
+ zIndex: 75,
473
+ },
474
+ ],
475
+ null,
476
+ { open: false, width: 120, contentHeight: 40 },
477
+ [],
478
+ [],
479
+ 28 / 18,
480
+ );
481
+
482
+ expect(state.occluders[0]?.rect.y).toBeCloseTo(2 + (28 / 18));
483
+ });
484
+
427
485
  test("ignores docked drag previews when building native occluders", () => {
428
486
  const state = buildNativeWindowState(
429
487
  ["portfolio-list:main"],
@@ -563,12 +621,105 @@ describe("Shell", () => {
563
621
  );
564
622
 
565
623
  await testSetup.renderOnce();
624
+ const actionCol = testSetup.captureCharFrame().split("\n")[0]?.indexOf("...");
625
+ expect(actionCol).toBeGreaterThanOrEqual(0);
626
+ await act(async () => {
627
+ await testSetup!.mockMouse.click(actionCol! + 1, 1);
628
+ });
629
+ await testSetup.renderOnce();
630
+
631
+ const frame = testSetup.captureCharFrame();
632
+ expect(frame).toContain("Settings");
633
+ expect(frame).toContain("Ctrl+,");
634
+ expect(frame).not.toContain("CmdOrCtrl");
635
+ });
636
+
637
+ test("shows a Pop Out action in the pane menu when a desktop bridge is available", async () => {
638
+ const config = createDefaultConfig("/tmp/gloomberb-shell-test");
639
+ const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
640
+ if (!mainPane) throw new Error("missing default portfolio pane");
641
+
642
+ const singlePaneLayout = {
643
+ dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
644
+ instances: [{ ...mainPane }],
645
+ floating: [],
646
+ detached: [],
647
+ };
648
+ const nextConfig = {
649
+ ...config,
650
+ layout: cloneLayout(singlePaneLayout),
651
+ layouts: [{ name: "Default", layout: cloneLayout(singlePaneLayout) }],
652
+ };
653
+ const state = createInitialState(nextConfig);
654
+ const pluginRegistry = createShellPluginRegistry();
655
+
656
+ testSetup = await testRender(
657
+ <AppContext value={{ state, dispatch: () => {} }}>
658
+ <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
659
+ <Shell
660
+ pluginRegistry={pluginRegistry}
661
+ desktopWindowBridge={{
662
+ kind: "main",
663
+ popOutPane: async () => {},
664
+ subscribeState: () => () => {},
665
+ subscribeDockPreview: () => () => {},
666
+ }}
667
+ />
668
+ </DialogProvider>
669
+ </AppContext>,
670
+ { width: 40, height: 10 },
671
+ );
672
+
673
+ await testSetup.renderOnce();
674
+ const floatingActionCol = testSetup.captureCharFrame().split("\n")[0]?.indexOf("...");
675
+ expect(floatingActionCol).toBeGreaterThanOrEqual(0);
676
+ await act(async () => {
677
+ await testSetup!.mockMouse.click(floatingActionCol! + 1, 1);
678
+ });
679
+ await testSetup.renderOnce();
680
+
681
+ expect(testSetup.captureCharFrame()).toContain("Pop Out");
682
+ });
683
+
684
+ test("shows the pane menu above a high z-index floating pane", async () => {
685
+ const config = createDefaultConfig("/tmp/gloomberb-shell-test");
686
+ const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
687
+ if (!detailPane) throw new Error("missing default detail pane");
688
+
689
+ const floatingOnlyLayout = {
690
+ dockRoot: null,
691
+ instances: [{ ...detailPane }],
692
+ floating: [{ instanceId: "ticker-detail:main", x: 0, y: 0, width: 40, height: 8, zIndex: 195 }],
693
+ };
694
+ const nextConfig = {
695
+ ...config,
696
+ layout: cloneLayout(floatingOnlyLayout),
697
+ layouts: [{ name: "Default", layout: cloneLayout(floatingOnlyLayout) }],
698
+ };
699
+ const state = {
700
+ ...createInitialState(nextConfig),
701
+ focusedPaneId: "ticker-detail:main",
702
+ };
703
+ const pluginRegistry = createShellPluginRegistry();
704
+
705
+ testSetup = await testRender(
706
+ <AppContext value={{ state, dispatch: () => {} }}>
707
+ <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
708
+ <Shell pluginRegistry={pluginRegistry} />
709
+ </DialogProvider>
710
+ </AppContext>,
711
+ { width: 40, height: 10 },
712
+ );
713
+
714
+ await testSetup.renderOnce();
715
+ const highZActionCol = testSetup.captureCharFrame().split("\n")[0]?.indexOf("...");
716
+ expect(highZActionCol).toBeGreaterThanOrEqual(0);
566
717
  await act(async () => {
567
- await testSetup!.mockMouse.click(37, 1);
718
+ await testSetup!.mockMouse.click(highZActionCol! + 1, 1);
568
719
  });
569
720
  await testSetup.renderOnce();
570
721
 
571
- expect(testSetup.captureCharFrame()).toContain("Settings");
722
+ expect(testSetup.captureCharFrame()).toContain("Dock Pane");
572
723
  });
573
724
 
574
725
  test("applies a chart resolution chip on the first click even when the chart pane is unfocused", async () => {
@@ -599,7 +750,7 @@ describe("Shell", () => {
599
750
  };
600
751
 
601
752
  const provider = createChartShellDataProvider({ [symbol]: history });
602
- setSharedDataProviderForTests(provider);
753
+ setSharedMarketDataForTests(provider);
603
754
  setSharedMarketDataCoordinator(new MarketDataCoordinator(provider));
604
755
 
605
756
  const pluginRegistry = createShellPluginRegistry({
@@ -638,7 +789,11 @@ describe("Shell", () => {
638
789
  spanColumn += span.width;
639
790
  }
640
791
  expect(resolutionCol).toBeGreaterThanOrEqual(0);
641
- expect(testSetup.captureCharFrame()).toContain("AAPL - AUTO");
792
+ const initialBoldResolutionText = resolutionSpansBefore
793
+ .filter((span) => (span.attributes & TextAttributes.BOLD) !== 0)
794
+ .map((span) => span.text)
795
+ .join("");
796
+ expect(initialBoldResolutionText).toContain("AUTO");
642
797
 
643
798
  await act(async () => {
644
799
  await testSetup!.mockMouse.click(resolutionCol, resolutionRow);
@@ -651,9 +806,11 @@ describe("Shell", () => {
651
806
  harnessState?.config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main")?.settings?.chartResolution,
652
807
  ).toBe("1wk");
653
808
  const resolutionSpans = testSetup.captureSpans().lines[resolutionRow]?.spans ?? [];
654
- expect(
655
- resolutionSpans.some((span) => span.text === targetResolution && (span.attributes & TextAttributes.BOLD) !== 0),
656
- ).toBe(true);
809
+ const boldResolutionText = resolutionSpans
810
+ .filter((span) => (span.attributes & TextAttributes.BOLD) !== 0)
811
+ .map((span) => span.text)
812
+ .join("");
813
+ expect(boldResolutionText).toContain(targetResolution);
657
814
  });
658
815
 
659
816
  test("keeps the cash drawer and gridlock tip on distinct click rows in the full app layout", async () => {
@@ -723,6 +880,20 @@ describe("Shell", () => {
723
880
  }));
724
881
  });
725
882
 
883
+ test("resolves pane management shortcuts", () => {
884
+ const base = { ctrl: false, meta: true, super: true, shift: true, alt: false };
885
+ expect(resolvePaneManagementShortcut({ ...base, name: ",", key: ",", shift: false })).toBe("settings");
886
+ expect(resolvePaneManagementShortcut({ ...base, name: "w", key: "w", ctrl: true, meta: false, super: false, shift: false })).toBe("close");
887
+ expect(resolvePaneManagementShortcut({ ...base, name: "D", key: "D" })).toBe("toggle-floating");
888
+ expect(resolvePaneManagementShortcut({ ...base, name: "o", key: "o" })).toBe("pop-out");
889
+ expect(resolvePaneManagementShortcut({ ...base, name: "c", key: "c" })).toBe("copy-screenshot");
890
+ expect(resolvePaneManagementShortcut({ ...base, name: "l", key: "l" })).toBe("layout-actions");
891
+ expect(resolvePaneManagementShortcut({ ...base, name: "g", key: "g" })).toBe("gridlock-all");
892
+ expect(resolvePaneManagementShortcut({ ...base, name: "n", key: "n" })).toBeNull();
893
+ expect(resolvePaneManagementShortcut({ ...base, name: "d", key: "d", alt: true })).toBeNull();
894
+ expect(resolvePaneManagementShortcut({ ...base, name: "d", key: "d", meta: false, super: false })).toBeNull();
895
+ });
896
+
726
897
  test("closes the focused docked pane with Ctrl+W", async () => {
727
898
  const config = createDefaultConfig("/tmp/gloomberb-shell-test");
728
899
  const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
@@ -808,6 +979,47 @@ describe("Shell", () => {
808
979
  expect(updateLayout?.layout.dockRoot).toEqual({ kind: "pane", instanceId: "portfolio-list:main" });
809
980
  });
810
981
 
982
+ test("closes the focused floating pane with Cmd+W", async () => {
983
+ const config = createDefaultConfig("/tmp/gloomberb-shell-test");
984
+ const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
985
+ const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
986
+ if (!mainPane || !detailPane) throw new Error("missing default panes");
987
+
988
+ const mixedLayout = {
989
+ dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
990
+ instances: [{ ...mainPane }, { ...detailPane }],
991
+ floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8 }],
992
+ };
993
+ const state = {
994
+ ...createInitialState({
995
+ ...config,
996
+ layout: cloneLayout(mixedLayout),
997
+ layouts: [{ name: "Default", layout: cloneLayout(mixedLayout) }],
998
+ }),
999
+ focusedPaneId: "ticker-detail:main",
1000
+ };
1001
+ const actions: Array<any> = [];
1002
+
1003
+ testSetup = await testRender(
1004
+ <AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
1005
+ <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
1006
+ <Shell pluginRegistry={createShellPluginRegistry()} />
1007
+ </DialogProvider>
1008
+ </AppContext>,
1009
+ { width: 40, height: 12 },
1010
+ );
1011
+
1012
+ await testSetup.renderOnce();
1013
+ await act(async () => {
1014
+ testSetup!.mockInput.pressKey("w", { meta: true });
1015
+ await testSetup!.renderOnce();
1016
+ });
1017
+
1018
+ const updateLayout = actions.find((action) => action.type === "UPDATE_LAYOUT");
1019
+ expect(updateLayout?.layout.instances.map((instance: { instanceId: string }) => instance.instanceId)).toEqual(["portfolio-list:main"]);
1020
+ expect(updateLayout?.layout.floating).toEqual([]);
1021
+ });
1022
+
811
1023
  test("keeps a floating pane at the preview rect after a free drag release", () => {
812
1024
  const config = createDefaultConfig("/tmp/gloomberb-shell-drag-test");
813
1025
  const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
@@ -835,6 +1047,91 @@ describe("Shell", () => {
835
1047
  }));
836
1048
  });
837
1049
 
1050
+ test("derives a floating pane drag release rect from the release pointer", () => {
1051
+ const rect = resolvePaneDragFloatingRect(
1052
+ {
1053
+ mode: "floating",
1054
+ startX: 7,
1055
+ startY: 3,
1056
+ origRect: { x: 4, y: 2, width: 30, height: 8 },
1057
+ },
1058
+ { x: 4, y: 2, width: 30, height: 8 },
1059
+ 18,
1060
+ 6,
1061
+ 80,
1062
+ 24,
1063
+ );
1064
+
1065
+ expect(rect).toEqual({
1066
+ x: 15,
1067
+ y: 5,
1068
+ width: 30,
1069
+ height: 8,
1070
+ });
1071
+ });
1072
+
1073
+ test("keeps a floating pane drag inside fractional desktop bounds", () => {
1074
+ const rect = resolvePaneDragFloatingRect(
1075
+ {
1076
+ mode: "floating",
1077
+ startX: 38,
1078
+ startY: 9,
1079
+ origRect: { x: 30, y: 7, width: 24, height: 8 },
1080
+ },
1081
+ { x: 30, y: 7, width: 24, height: 8 },
1082
+ 90,
1083
+ 40,
1084
+ 52.5,
1085
+ 18.5,
1086
+ );
1087
+
1088
+ expect(rect).toEqual({
1089
+ x: 28.5,
1090
+ y: 10.5,
1091
+ width: 24,
1092
+ height: 8,
1093
+ });
1094
+ });
1095
+
1096
+ test("fits oversized floating panes into the current canvas", () => {
1097
+ const rect = constrainFloatingRectToBounds(
1098
+ { x: 80, y: 30, width: 100, height: 40, zIndex: 75 },
1099
+ 50.5,
1100
+ 12.5,
1101
+ );
1102
+
1103
+ expect(rect).toEqual({
1104
+ x: 0,
1105
+ y: 0,
1106
+ width: 50.5,
1107
+ height: 12.5,
1108
+ zIndex: 75,
1109
+ });
1110
+ });
1111
+
1112
+ test("keeps the pointer anchored when releasing a docked pane drag without an intermediate preview", () => {
1113
+ const rect = resolvePaneDragFloatingRect(
1114
+ {
1115
+ mode: "docked",
1116
+ startX: 22,
1117
+ startY: 4,
1118
+ origRect: { x: 10, y: 2, width: 40, height: 12 },
1119
+ },
1120
+ { x: 8, y: 1, width: 32, height: 10 },
1121
+ 30,
1122
+ 8,
1123
+ 80,
1124
+ 24,
1125
+ );
1126
+
1127
+ expect(rect).toEqual({
1128
+ x: 18,
1129
+ y: 6,
1130
+ width: 32,
1131
+ height: 10,
1132
+ });
1133
+ });
1134
+
838
1135
  test("keeps the last floating pane body row visible above the border", async () => {
839
1136
  const config = createDefaultConfig("/tmp/gloomberb-shell-footer-test");
840
1137
  const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
@@ -877,6 +1174,12 @@ describe("Shell", () => {
877
1174
  await testSetup!.renderOnce();
878
1175
  });
879
1176
 
880
- expect(testSetup.captureCharFrame()).toContain("Footer Probe");
1177
+ const frame = testSetup.captureCharFrame();
1178
+ const lines = frame.split("\n");
1179
+
1180
+ expect(frame).toContain("Footer Probe");
1181
+ expect(lines[8]).toContain("Footer Probe");
1182
+ expect(lines[9]).not.toContain("Footer Probe");
1183
+ expect(lines[9]).toContain("└───────────────────────────");
881
1184
  });
882
1185
  });