gloomberb 0.5.1 → 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 -14
  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 +9 -10
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +52 -11
  87. package/src/components/layout/shell.test.tsx +315 -107
  88. package/src/components/layout/shell.tsx +1010 -368
  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 +129 -0
  180. package/src/plugins/builtin/chat-controller.ts +85 -14
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +363 -170
  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 +99 -9
  440. package/src/utils/api-client.ts +378 -18
  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,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,7 +41,7 @@ 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
 
@@ -63,7 +74,7 @@ function createShellPluginRegistry(options?: {
63
74
  getPluginPaneTemplateIds: () => [],
64
75
  hasPaneSettings: (paneId: string) => paneId === "portfolio-list:main",
65
76
  openPaneSettingsFn: () => {},
66
- openCommandBarFn: () => {},
77
+ openCommandBar: () => {},
67
78
  updateLayoutFn: () => {},
68
79
  hideWidget: () => {},
69
80
  } as unknown as PluginRegistry;
@@ -72,8 +83,26 @@ function createShellPluginRegistry(options?: {
72
83
  function createBrokerPortfolioRegistry(): PluginRegistry {
73
84
  const pane = portfolioListPlugin.panes?.[0];
74
85
  if (!pane) throw new Error("missing portfolio pane");
75
- return {
76
- 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]]),
77
106
  paneTemplates: new Map(),
78
107
  commands: new Map(),
79
108
  tickerActions: new Map(),
@@ -83,16 +112,19 @@ function createBrokerPortfolioRegistry(): PluginRegistry {
83
112
  getPluginPaneTemplateIds: () => [],
84
113
  hasPaneSettings: () => true,
85
114
  openPaneSettingsFn: () => {},
86
- openCommandBarFn: () => {},
115
+ openCommandBar: () => {},
87
116
  updateLayoutFn: () => {},
88
117
  hideWidget: () => {},
89
118
  focusPaneFn: () => {},
90
- pinTickerFn: () => {},
119
+ pinTicker: () => {},
120
+ navigateTicker: () => {},
91
121
  showPaneFn: () => {},
92
122
  getLayoutFn: () => ({ dockRoot: null, instances: [], floating: [] }),
93
123
  getTermSizeFn: () => ({ width: 120, height: 40 }),
94
124
  Slot: () => null,
95
125
  } as unknown as PluginRegistry;
126
+
127
+ return registry;
96
128
  }
97
129
 
98
130
  function makeQuote(overrides: Partial<Quote> = {}): Quote {
@@ -282,20 +314,6 @@ function BrokerShellHarness({ pluginRegistry }: { pluginRegistry: PluginRegistry
282
314
  );
283
315
  }
284
316
 
285
- function FocusBorderProbe({ width, height }: Pick<PaneProps, "width" | "height">) {
286
- return (
287
- <box flexDirection="column" width={width} height={height}>
288
- <box height={1} width={width}>
289
- <text>Read Probe</text>
290
- </box>
291
- <box height={1} width={width} flexDirection="row">
292
- <box flexGrow={1} />
293
- <text>Right</text>
294
- </box>
295
- </box>
296
- );
297
- }
298
-
299
317
  function ChartShellHarness({
300
318
  pluginRegistry,
301
319
  config,
@@ -408,6 +426,11 @@ describe("Header", () => {
408
426
  });
409
427
 
410
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
+
411
434
  test("uses the live floating preview rect for native occluders", () => {
412
435
  const state = buildNativeWindowState(
413
436
  ["portfolio-list:main"],
@@ -439,6 +462,26 @@ describe("Shell", () => {
439
462
  ]);
440
463
  });
441
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
+
442
485
  test("ignores docked drag previews when building native occluders", () => {
443
486
  const state = buildNativeWindowState(
444
487
  ["portfolio-list:main"],
@@ -578,12 +621,105 @@ describe("Shell", () => {
578
621
  );
579
622
 
580
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);
581
717
  await act(async () => {
582
- await testSetup!.mockMouse.click(37, 1);
718
+ await testSetup!.mockMouse.click(highZActionCol! + 1, 1);
583
719
  });
584
720
  await testSetup.renderOnce();
585
721
 
586
- expect(testSetup.captureCharFrame()).toContain("Settings");
722
+ expect(testSetup.captureCharFrame()).toContain("Dock Pane");
587
723
  });
588
724
 
589
725
  test("applies a chart resolution chip on the first click even when the chart pane is unfocused", async () => {
@@ -614,7 +750,7 @@ describe("Shell", () => {
614
750
  };
615
751
 
616
752
  const provider = createChartShellDataProvider({ [symbol]: history });
617
- setSharedDataProviderForTests(provider);
753
+ setSharedMarketDataForTests(provider);
618
754
  setSharedMarketDataCoordinator(new MarketDataCoordinator(provider));
619
755
 
620
756
  const pluginRegistry = createShellPluginRegistry({
@@ -653,7 +789,11 @@ describe("Shell", () => {
653
789
  spanColumn += span.width;
654
790
  }
655
791
  expect(resolutionCol).toBeGreaterThanOrEqual(0);
656
- 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");
657
797
 
658
798
  await act(async () => {
659
799
  await testSetup!.mockMouse.click(resolutionCol, resolutionRow);
@@ -717,84 +857,6 @@ describe("Shell", () => {
717
857
  expect(toasts).toEqual(["Retiled all panes"]);
718
858
  });
719
859
 
720
- test("keeps focused docked pane body text inside the focus border", async () => {
721
- const config = createDefaultConfig("/tmp/gloomberb-shell-focus-border-test");
722
- const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
723
- if (!mainPane) throw new Error("missing default portfolio pane");
724
-
725
- const singlePaneLayout = {
726
- dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
727
- instances: [{ ...mainPane }],
728
- floating: [],
729
- };
730
- const state = {
731
- ...createInitialState({
732
- ...config,
733
- layout: cloneLayout(singlePaneLayout),
734
- layouts: [{ name: "Default", layout: cloneLayout(singlePaneLayout) }],
735
- }),
736
- focusedPaneId: "portfolio-list:main",
737
- };
738
-
739
- testSetup = await testRender(
740
- <AppContext value={{ state, dispatch: () => {} }}>
741
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
742
- <Shell pluginRegistry={createShellPluginRegistry({
743
- portfolioListComponent: FocusBorderProbe,
744
- })} />
745
- </DialogProvider>
746
- </AppContext>,
747
- { width: 40, height: 10 },
748
- );
749
-
750
- await testSetup.renderOnce();
751
-
752
- const frame = testSetup.captureCharFrame();
753
- expect(frame).toContain("│Read Probe");
754
- expect(frame).toContain("Right│");
755
- expect(frame).not.toContain("│ead Probe");
756
- expect(frame).not.toContain("Righ│");
757
- });
758
-
759
- test("keeps focused floating pane body text inside the focus border", async () => {
760
- const config = createDefaultConfig("/tmp/gloomberb-shell-floating-focus-border-test");
761
- const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
762
- if (!detailPane) throw new Error("missing detail pane");
763
-
764
- const floatingOnlyLayout = {
765
- dockRoot: null,
766
- instances: [{ ...detailPane }],
767
- floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8, zIndex: 75 }],
768
- };
769
- const state = {
770
- ...createInitialState({
771
- ...config,
772
- layout: cloneLayout(floatingOnlyLayout),
773
- layouts: [{ name: "Default", layout: cloneLayout(floatingOnlyLayout) }],
774
- }),
775
- focusedPaneId: "ticker-detail:main",
776
- };
777
-
778
- testSetup = await testRender(
779
- <AppContext value={{ state, dispatch: () => {} }}>
780
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
781
- <Shell pluginRegistry={createShellPluginRegistry({
782
- tickerDetailComponent: FocusBorderProbe,
783
- })} />
784
- </DialogProvider>
785
- </AppContext>,
786
- { width: 40, height: 12 },
787
- );
788
-
789
- await testSetup.renderOnce();
790
-
791
- const frame = testSetup.captureCharFrame();
792
- expect(frame).toContain("│Read Probe");
793
- expect(frame).toContain("Right│");
794
- expect(frame).not.toContain("│ead Probe");
795
- expect(frame).not.toContain("Righ│");
796
- });
797
-
798
860
  test("shows the gridlock tip after snapping a pane to a half screen", () => {
799
861
  const config = createDefaultConfig("/tmp/gloomberb-shell-snap-test");
800
862
  const snapLayout = cloneLayout(config.layout);
@@ -818,6 +880,20 @@ describe("Shell", () => {
818
880
  }));
819
881
  });
820
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
+
821
897
  test("closes the focused docked pane with Ctrl+W", async () => {
822
898
  const config = createDefaultConfig("/tmp/gloomberb-shell-test");
823
899
  const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
@@ -903,6 +979,47 @@ describe("Shell", () => {
903
979
  expect(updateLayout?.layout.dockRoot).toEqual({ kind: "pane", instanceId: "portfolio-list:main" });
904
980
  });
905
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
+
906
1023
  test("keeps a floating pane at the preview rect after a free drag release", () => {
907
1024
  const config = createDefaultConfig("/tmp/gloomberb-shell-drag-test");
908
1025
  const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
@@ -930,6 +1047,91 @@ describe("Shell", () => {
930
1047
  }));
931
1048
  });
932
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
+
933
1135
  test("keeps the last floating pane body row visible above the border", async () => {
934
1136
  const config = createDefaultConfig("/tmp/gloomberb-shell-footer-test");
935
1137
  const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
@@ -972,6 +1174,12 @@ describe("Shell", () => {
972
1174
  await testSetup!.renderOnce();
973
1175
  });
974
1176
 
975
- 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("└───────────────────────────");
976
1184
  });
977
1185
  });